BaseTranslationSupport.java
/*
* Copyright 2023 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.service.impl;
import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.Tuple;
import com.querydsl.core.types.ExpressionUtils;
import com.querydsl.core.types.Order;
import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.core.types.Predicate;
import com.querydsl.core.types.dsl.Expressions;
import com.querydsl.core.types.dsl.ListPath;
import com.querydsl.core.types.dsl.PathBuilder;
import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;
import org.apache.commons.lang3.StringUtils;
import org.genesys.blocks.model.BasicModel;
import org.genesys.blocks.model.EmptyModel;
import org.genesys.blocks.model.EntityId;
import org.genesys.blocks.model.filters.EmptyModelFilter;
import org.genesys.server.model.impl.QTranslatedUuidModel;
import org.genesys.server.model.impl.QLangModel;
import org.genesys.server.model.impl.ITranslatedModel;
import org.genesys.server.model.impl.LangModel;
import org.genesys.server.persistence.LangModelRepository;
import org.genesys.server.service.TranslationService;
import org.jetbrains.annotations.Nullable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.lang.reflect.ParameterizedType;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
@Transactional(readOnly = true)
public abstract class BaseTranslationSupport<E extends BasicModel & ITranslatedModel, L extends LangModel<L, E>,
T extends TranslationService.Translation<E, L>,
F extends EmptyModelFilter<?, E>, LR extends LangModelRepository<L, E>> extends CRUDServiceImpl<L, LR> implements TranslationService<E, L, T, F> {
@Autowired
protected JPAQueryFactory jpaQueryFactory;
@Resource
private Set<String> supportedLocales;
@PersistenceContext
protected EntityManager em;
@Autowired
protected JpaRepository<E, Long> owningEntityRepository;
@Autowired
protected LR langsRepository;
private final Class<E> targetType;
private final Class<L> langType;
private final Class<T> translatedType;
private final String variableName; // used in JPA query construction
@SuppressWarnings("unchecked")
protected BaseTranslationSupport() {
this.targetType = ((Class<E>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0]);
this.langType = ((Class<L>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[1]);
this.translatedType = ((Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[2]);
String simpleName = targetType.getSimpleName();
this.variableName = Character.toLowerCase(simpleName.charAt(0)) + simpleName.substring(1);
}
@Override
public Page<T> list(F filter, Pageable page) {
BooleanBuilder predicate = new BooleanBuilder();
if (filter != null) {
predicate.and(filter.buildPredicate());
}
return fetchTranslations(predicate, page, LocaleContextHolder.getLocale().toLanguageTag(), null);
}
@Override
public final T getTranslated(E input) {
return getTranslated(input, LocaleContextHolder.getLocale());
}
@Override
public T getTranslated(E input, Locale locale) {
// E savedEntity = owningEntityRepository.getReferenceById(input.getId());
assert(input != null);
assert(! input.isNew());
return fetchTranslation(input, locale.toLanguageTag());
}
@Override
public final List<T> getTranslated(List<E> input) {
return getTranslated(input, LocaleContextHolder.getLocale());
}
@Override
public List<T> getTranslated(List<E> input, Locale locale) {
// E savedEntity = owningEntityRepository.getReferenceById(input.getId());
if (input == null) return null;
if (input.isEmpty()) return List.of();
assert(input != null && !input.isEmpty());
assert(input.stream().noneMatch(EmptyModel::isNew));
PathBuilder<E> entity = new PathBuilder<E>(targetType, variableName);
var idList = input.stream().map(EntityId::getId).collect(Collectors.toList());
Comparator<T> sorter = (a, b) -> Integer.compare(idList.indexOf(a.getEntity().getId()), idList.indexOf(b.getEntity().getId())); // Keep order of items
var translated = fetchTranslations(entity.get("id").in(idList), Pageable.unpaged(), locale.toLanguageTag(), sorter).getContent();
return translated;
}
protected T fetchTranslation(final E entity, final String languageTag) {
PathBuilder<L> langPath = new PathBuilder<L>(langType, QLangModel.langModel.getMetadata().getName());
var lang = jpaQueryFactory.selectFrom(langPath).where(QLangModel.langModel.entity().id.eq(entity.getId()).and(QLangModel.langModel.languageTag.eq(languageTag))).fetchFirst();
return toTranslated(entity, languageTag, lang);
}
protected Page<T> fetchTranslations(final Predicate predicate, final Pageable page, final String languageTag, Comparator<? super T> sorter) {
final var TITLE_PROP = QLangModel.langModel.title.getMetadata().getName();
PathBuilder<E> entity = new PathBuilder<E>(targetType, variableName);
ListPath<L, PathBuilder<L>> langs = entity.getList(QTranslatedUuidModel.translatedUuidModel.langs.getMetadata().getName(), langType);
PathBuilder<L> lan = new PathBuilder<L>(langType, "lan"); // join alias
PathBuilder<L> def = new PathBuilder<L>(langType, "def"); // join alias
PathBuilder<String> lanTitle = lan.get(TITLE_PROP, String.class);
PathBuilder<String> defTitle = def.get(TITLE_PROP, String.class);
var title = Expressions.cases().when(lanTitle.isNotNull()).then(lanTitle).otherwise(defTitle);
// query aliases for use in orderby clause
var titlePath = ExpressionUtils.path(String.class, TITLE_PROP);
// Note: This join in allows for sorting by translated title and description
JPAQuery<Tuple> query = jpaQueryFactory.from(entity)
.select(entity, Expressions.as(title, titlePath), def, lan)
// Default language
.leftJoin(langs, def).on(def.get(QLangModel.langModel.languageTag).eq(Locale.ENGLISH.getLanguage()))
// Target language
.leftJoin(langs, lan).on(lan.get(QLangModel.langModel.languageTag).eq(languageTag))
// Filters
.where(predicate);
if (page.isPaged()) {
query.offset(page.getOffset()).limit(page.getPageSize());
}
var totalElements = query.fetchCount();
for (Sort.Order o : page.getSort()) {
if (o.getProperty().equalsIgnoreCase(TITLE_PROP)) {
query.orderBy(new OrderSpecifier<String>(o.isAscending() ? Order.ASC : Order.DESC, titlePath));
} else {
query.orderBy(new OrderSpecifier(o.isAscending() ? Order.ASC : Order.DESC, entity.get(o.getProperty())));
}
}
var contentStream = query.fetch().stream().map(tuple -> {
var defaultLang = tuple.get(2, langType);
var requestedLang = tuple.get(3, langType);
return toTranslated(tuple.get(0, targetType), languageTag, requestedLang == null ? defaultLang : requestedLang);
});
if (sorter != null) {
contentStream = contentStream.sorted(sorter);
}
var content = contentStream.collect(Collectors.toList());
return new PageImpl<>(content, page, totalElements);
}
private final T toTranslated(@Nullable E entity, String requestedLang, @Nullable L translation) {
try {
var translated = translatedType.getConstructor().newInstance();
translated.setEntity(entity);
if (Objects.equals(requestedLang, entity.getOriginalLanguageTag())) {
// Don't include translation if entity is already in the requested language
} else {
translated.setTranslation(translation);
}
return translated;
} catch (Throwable e) {
LOG.error("Cannot create translated class: {}", e.getMessage(), e);
throw new RuntimeException("Cannot create translated class", e);
}
}
@Transactional
public L deleteTranslation(E entity, String languageTag) {
L existing = langsRepository.getByEntityAndLanguageTag(entity, languageTag);
if (existing == null) {
return null;
}
return remove(existing);
}
public List<L> listExistingTranslations(E entity) {
var list = langsRepository.findAllByEntity(entity);
return list;
}
public List<L> listTranslations(E entity) {
var list = langsRepository.findAllByEntity(entity);
List<L> res = list;
res.sort((lang1, lang2) -> {
if (Locale.ENGLISH.getLanguage().equals(lang1.getLanguageTag())) {
return -1;
} else if(Locale.ENGLISH.getLanguage().equals(lang2.getLanguageTag())) {
return 1;
} else {
return lang1.getLanguageTag().compareToIgnoreCase(lang2.getLanguageTag());
}
});
return res;
}
@Transactional
@Override
public L addLang(E entity, L input) {
assert(entity != null);
assert(input.getId() == null);
assert(input.getEntity() == null);
assert(StringUtils.isNotBlank(input.getLanguageTag()));
assert(!Objects.equals(entity.getOriginalLanguageTag(), input.getLanguageTag())); // Ensure we don't recreate translations of the original language
input.setEntity(entity);
return create(input);
}
@Override
public L getLang(E entity, String languageTag) {
return langsRepository.getByEntityAndLanguageTag(entity, languageTag);
}
@Override
@Transactional
public L upsertLang(E entity, L input) {
assert(input.getEntity() == null || entity.getId().equals(input.getEntity().getId()));
assert(StringUtils.isNotBlank(input.getLanguageTag()));
L existing = getLang(entity, input.getLanguageTag());
if (existing == null) {
assert(input.isNew());
input.setEntity(entity);
return create(input);
} else {
return update(input, existing);
}
}
/**
* The default implementation reloads the entity and sysLang, saves incoming data
*/
@Override
public L create(L source) {
assert(source.isNew());
source.setEntity(owningEntityRepository.getReferenceById(source.getEntity().getId()));
return langsRepository.save(source);
}
/**
* The default implementation uses Copyable
*/
@Override
public L update(L updated, L target) {
target.apply(updated);
return repository.save(target);
}
}