PublishableEntityAspect.java
/*
* Copyright 2025 Global Crop Diversity Trust
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.genesys.server.component.aspect;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import lombok.Getter;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.Strings;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.genesys.blocks.model.BasicModel;
import org.genesys.blocks.model.Publishable;
import org.genesys.blocks.util.TransactionHelper;
import org.genesys.server.api.v2.facade.APIFilteredTranslatedServiceFacade;
import org.genesys.server.api.v2.model.LangModelDTO;
import org.genesys.server.api.v2.model.Translated;
import org.genesys.server.model.impl.ITranslatedModel;
import org.genesys.server.model.impl.LangModel;
import org.genesys.server.service.TranslatorService;
import org.hibernate.Hibernate;
import org.hibernate.proxy.HibernateProxy;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.DependsOn;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;
@Aspect
@DependsOn("currentApplicationContext")
@Slf4j
public class PublishableEntityAspect implements InitializingBean, ApplicationContextAware {
/** Default language */
private String defaultLanguageTag = "en";
/** List of languages to auto-translate */
private List<String> autoTranslate = List.of("en");
@Autowired
private ApplicationEventPublisher applicationEventPublisher;
@Autowired
private TransactionHelper transactionHelper;
@SuppressWarnings("rawtypes")
private Map<Class<?>, APIFilteredTranslatedServiceFacade<Object, Translated, LangModelDTO, BasicModel, ?, ?>> entityTranslationServicesMap;
private ApplicationContext applicationContext;
@Pointcut("saveActivityPost() || saveArticle() || saveSubset() || saveDataset() || saveDescriptor() || saveDescriptorList()")
public void saveAutoTranslatedEntity() {}
@Pointcut("saveAnyLang()")
public void saveAutoTranslatedLang() {}
@Pointcut("execution(* org.genesys.server.persistence.ArticleRepository.save(..)) || execution(* org.genesys.server.persistence.ArticleRepository.saveAndFlush(..)) || execution(* org.genesys.server.persistence.ArticleRepository.saveAll(..)) || execution(* org.genesys.server.persistence.ArticleRepository.saveAllAndFlush(..))")
public void saveArticle() {}
@Pointcut("execution(* org.genesys.server.persistence.ActivityPostRepository.save(..)) || execution(* org.genesys.server.persistence.ActivityPostRepository.saveAndFlush(..)) || execution(* org.genesys.server.persistence.ActivityPostRepository.saveAll(..)) || execution(* org.genesys.server.persistence.ActivityPostRepository.saveAllAndFlush(..))")
public void saveActivityPost() {}
@Pointcut("execution(* org.genesys.server.persistence.SubsetRepository.save(..)) || execution(* org.genesys.server.persistence.SubsetRepository.saveAndFlush(..)) || execution(* org.genesys.server.persistence.SubsetRepository.saveAll(..)) || execution(* org.genesys.server.persistence.SubsetRepository.saveAllAndFlush(..))")
public void saveSubset() {}
@Pointcut("execution(* org.genesys.server.persistence.dataset.DatasetRepository.save(..)) || execution(* org.genesys.server.persistence.dataset.DatasetRepository.saveAndFlush(..)) || execution(* org.genesys.server.persistence.dataset.DatasetRepository.saveAll(..)) || execution(* org.genesys.server.persistence.dataset.DatasetRepository.saveAllAndFlush(..))")
public void saveDataset() {}
@Pointcut("execution(* org.genesys.server.persistence.traits.DescriptorRepository.save(..)) || execution(* org.genesys.server.persistence.traits.DescriptorRepository.saveAndFlush(..)) || execution(* org.genesys.server.persistence.traits.DescriptorRepository.saveAll(..)) || execution(* org.genesys.server.persistence.traits.DescriptorRepository.saveAllAndFlush(..))")
public void saveDescriptor() {}
@Pointcut("execution(* org.genesys.server.persistence.traits.DescriptorListRepository.save(..)) || execution(* org.genesys.server.persistence.traits.DescriptorListRepository.saveAndFlush(..)) || execution(* org.genesys.server.persistence.traits.DescriptorListRepository.saveAll(..)) || execution(* org.genesys.server.persistence.traits.DescriptorListRepository.saveAllAndFlush(..))")
public void saveDescriptorList() {}
@Pointcut("execution(* org.genesys.server.persistence.LangModelRepository.save(..)) || execution(* org.genesys.server.persistence.LangModelRepository.saveAndFlush(..)) || execution(* org.genesys.server.persistence.LangModelRepository.saveAll(..)) || execution(* org.genesys.server.persistence.LangModelRepository.saveAllAndFlush(..))")
public void saveAnyLang() {}
// @Pointcut("execution(* org.genesys.server.persistence.ArticleLangRepository.save(..)) || execution(* org.genesys.server.persistence.ArticleLangRepository.saveAll(..))")
// public void saveArticleLang() {}
// @Pointcut("execution(* org.genesys.server.persistence.ActivityLangPostRepository.save(..)) || execution(* org.genesys.server.persistence.ActivityLangPostRepository.saveAll(..))")
// public void saveActivityPostLang() {}
// @Pointcut("execution(* org.genesys.server.persistence.SubsetLangRepository.save(..)) || execution(* org.genesys.server.persistence.SubsetLangRepository.saveAll(..))")
// public void saveSubsetLang() {}
// @Pointcut("execution(* org.genesys.server.persistence.DatasetLangRepository.save(..)) || execution(* org.genesys.server.persistence.DatasetLangRepository.saveAll(..))")
// public void saveDatasetLang() {}
// @Pointcut("execution(* org.genesys.server.persistence.DescriptorLangRepository.save(..)) || execution(* org.genesys.server.persistence.DescriptorLangRepository.saveAll(..))")
// public void saveDescriptorLang() {}
// @Pointcut("execution(* org.genesys.server.persistence.DescriptorListLangRepository.save(..)) || execution(* org.genesys.server.persistence.DescriptorListLangRepository.saveAll(..))")
// public void saveDescriptorListLang() {}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@Override
public void afterPropertiesSet() throws Exception {
@SuppressWarnings("rawtypes")
Map<Class<?>, APIFilteredTranslatedServiceFacade<Object, Translated, LangModelDTO, BasicModel, ?, ?>> entityTranslationServicesMap = new HashMap<>();
for (var translatedServiceName : applicationContext.getBeanNamesForType(APIFilteredTranslatedServiceFacade.class)) {
@SuppressWarnings({ "unchecked", "rawtypes" })
var serviceImpl = (APIFilteredTranslatedServiceFacade<Object, Translated, LangModelDTO, BasicModel, ?, ?>) applicationContext.getBean(translatedServiceName);
var modelType = serviceImpl.getModelType();
var modelLangType = serviceImpl.getLangModelType();
if (isPublishableTranslatedType(modelType)) {
log.warn("Registering automatic translation of {} with {} using {}", modelType.getName(), modelLangType.getName(), translatedServiceName);
entityTranslationServicesMap.put(modelType, serviceImpl);
} else {
log.info("Ignoring service {} for {} using {}, not Publishable", translatedServiceName, modelType.getName(), modelLangType.getName());
}
}
log.warn("{} is ready to auto-translate {} to {}", this.getClass().getName(), entityTranslationServicesMap.keySet(), this.autoTranslate);
this.entityTranslationServicesMap = Collections.unmodifiableMap(entityTranslationServicesMap);
}
@AfterReturning(value = "saveAutoTranslatedEntity() || saveAutoTranslatedLang()", returning = "result")
public void afterPersistingEntity(Object result) {
log.debug("AfterReturning saveAutoTranslatedEntity(): {}", result);
Set<ITranslatedModel> publishedEntities = new LinkedHashSet<>();
if (result instanceof Collection) {
Collection<?> entities = (Collection<?>) result;
for (Object entity : entities) {
processSavedEntity(entity, publishedEntities);
}
} else {
processSavedEntity(result, publishedEntities);
}
if (!publishedEntities.isEmpty()) {
log.info("Found {} published entities for translation", publishedEntities.size());
applicationEventPublisher.publishEvent(new PublishedTranslatedEntitiesEvent(publishedEntities));
}
}
private void processSavedEntity(Object entity, Set<ITranslatedModel> publishedEntities) {
if (isPublishableTranslatedEntity(entity)) {
var publishableEntity = (Publishable) entity;
if (publishableEntity.isPublished()) {
// Is published
log.info("Published entity updated, should translate: {}", publishableEntity.getClass().getName());
publishedEntities.add((ITranslatedModel) publishableEntity);
} else {
// Not published
log.debug("Entity updated, will not auto-translate: published={} {}", publishableEntity.isPublished(), publishableEntity.getClass().getName());
}
} else if (entity instanceof LangModel) {
// Update translations if the en lang model of a published entity is changed and is not machine translated
var langModel = ((LangModel<?, ?>) entity);
if (Strings.CS.equals(this.defaultLanguageTag, langModel.getLanguageTag()) && !langModel.isMachineTranslated()) {
// Is curated translation in default language
var targetEntity = langModel.getEntity();
if (targetEntity instanceof HibernateProxy) {
targetEntity = (BasicModel) Hibernate.unproxy(targetEntity);
}
if (isPublishableTranslatedEntity(targetEntity) && ((Publishable) targetEntity).isPublished()) {
log.info("LangModel updated, should translate lang={} {} of {}", langModel.getLanguageTag(), langModel.getClass().getName(), targetEntity.getClass().getName());
publishedEntities.add((ITranslatedModel) targetEntity);
}
} else {
// Not a candidate for auto-translation
log.debug("LangModel updated, will not auto-translate MT={} lang={} {}", langModel.isMachineTranslated(), langModel.getLanguageTag(), langModel.getClass().getName());
return;
}
}
}
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handlePublishedEntitiesEvent(PublishedTranslatedEntitiesEvent event) {
if (event.getEntities() == null || event.getEntities().isEmpty()) {
return;
}
log.info("Processing translations for {} published entities", event.getEntities().size());
for (ITranslatedModel entity : event.getEntities()) {
transactionHelper.executeInTransaction(false, () -> {
processEntityTranslations(entity);
return true;
});
}
}
private void processEntityTranslations(ITranslatedModel entity) throws TranslatorService.TranslatorException {
var translationService = entityTranslationServicesMap.get(entity.getClass());
if (translationService == null) {
log.warn("No translation service found for entity {}", entity.getClass().getSimpleName());
return;
}
// Note: The first language tag must be "en".
for (String langTag : this.autoTranslate) {
if (Strings.CS.equals(langTag, entity.getOriginalLanguageTag())) {
// Don't translate original language
continue;
}
var translation = translationService.loadTranslated((BasicModel) entity, langTag);
var lang = translation.getTranslation();
if (lang == null || lang.isMachineTranslated()) {
log.warn("Auto-translating to {}: {} {}", langTag, entity.getClass().getSimpleName(), entity);
// Update machine-translated text
var updatedLang = translationService.machineTranslate((BasicModel) entity, langTag);
translationService.upsertTranslation(translationService.get((BasicModel) entity), updatedLang);
}
}
}
@Getter
public static class PublishedTranslatedEntitiesEvent {
private final Set<ITranslatedModel> entities;
public PublishedTranslatedEntitiesEvent(Set<ITranslatedModel> entities) {
this.entities = entities;
}
}
private boolean isPublishableTranslatedType(Class<?> type) {
return Publishable.class.isAssignableFrom(type) && ITranslatedModel.class.isAssignableFrom(type);
}
private boolean isPublishableTranslatedEntity(Object o) {
return o instanceof Publishable && o instanceof ITranslatedModel;
}
/** Set the default language on which other translations are based. Usually "en" */
public void setDefaultLanguage(@NonNull String defaultLanguageTag) {
this.defaultLanguageTag = defaultLanguageTag;
setAutoTranslate(this.autoTranslate); // Update list of auto-translated languages to move default language to be the first
}
/** Set the languages to automatically translate */
public void setAutoTranslate(@NonNull List<String> autoTranslated) {
var autoTranslate = new LinkedList<String>();
autoTranslated.stream().distinct().forEach(autoTranslate::add); // Unique
// Ensure default language is first in the list
if (autoTranslate.size() == 0 || ! Strings.CS.equals(this.defaultLanguageTag, autoTranslate.get(0))) {
autoTranslate.remove(this.defaultLanguageTag);
autoTranslate.add(0, this.defaultLanguageTag); // Default language is first
}
this.autoTranslate = Collections.unmodifiableList(autoTranslate);
}
}