BaseTranslationSupport.java
/*
* Copyright 2020 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.gringlobal.service.impl;
import java.lang.reflect.ParameterizedType;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.stream.Collectors;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import org.genesys.blocks.model.EmptyModel;
import org.genesys.blocks.model.EntityId;
import org.gringlobal.model.CooperatorOwnedLang;
import org.gringlobal.model.QCooperatorOwnedLang;
import org.gringlobal.model.QTranslatedCooperatorOwnedModel;
import org.gringlobal.model.SysLang;
import org.gringlobal.model.TranslatedCooperatorOwnedModel;
import org.gringlobal.persistence.CooperatorOwnedLangRepository;
import org.gringlobal.persistence.SysLangRepository;
import org.gringlobal.service.LanguageService;
import org.gringlobal.service.TranslationService;
import org.gringlobal.service.filter.TranslatedEntityFilter;
import org.jetbrains.annotations.Nullable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
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 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;
/**
* @author Maxym Borodenko
*/
@Transactional(readOnly = true)
public abstract class BaseTranslationSupport<E extends TranslatedCooperatorOwnedModel<L, E>, L extends CooperatorOwnedLang<L, E>,
T extends TranslationService.Translation<E, L>,
F extends TranslatedEntityFilter<?, E>, LR extends CooperatorOwnedLangRepository<L, E>> extends CRUDServiceImpl<L, LR> implements TranslationService<E, L, T, F> {
protected static final int TARGET_TYPE_GENERIC_INDEX = 0;
protected static final int LANG_TYPE_GENERIC_INDEX = 1;
@PersistenceContext
protected EntityManager em;
@Autowired
protected JpaRepository<E, Long> owningEntityRepository;
@Autowired
protected LR langsRepository;
@Autowired
@Lazy
protected LanguageService languageService;
@Autowired
private SysLangRepository sysLangRepository;
private final Class<E> targetType;
private final Class<L> langType;
@SuppressWarnings("unchecked")
protected BaseTranslationSupport() {
this.targetType = ((Class<E>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[TARGET_TYPE_GENERIC_INDEX]);
this.langType = ((Class<L>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[LANG_TYPE_GENERIC_INDEX]);
}
@Override
public Page<T> list(F filter, Pageable page) {
BooleanBuilder predicate = new BooleanBuilder();
if (filter != null) {
predicate.and(filter.buildPredicate());
}
SysLang targetLanguage = languageService.getLanguage(LocaleContextHolder.getLocale());
if (targetLanguage == null) {
targetLanguage = languageService.getLanguage(Locale.ENGLISH);
}
return fetchTranslations(predicate, page, targetLanguage);
}
@Override
public T getTranslated(E input) {
// E savedEntity = owningEntityRepository.getReferenceById(input.getId());
assert(input != null);
assert(! input.isNew());
SysLang targetLanguage = languageService.getLanguage(LocaleContextHolder.getLocale());
if (targetLanguage == null) {
targetLanguage = languageService.getLanguage(Locale.ENGLISH);
}
PathBuilder<E> entity = new PathBuilder<E>(targetType, toVariable(targetType));
return fetchTranslations(entity.eq(input), PageRequest.of(0, 1), targetLanguage).getContent().get(0);
}
@Override
public List<T> getTranslated(List<E> input) {
// 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));
SysLang targetLanguage = languageService.getLanguage(LocaleContextHolder.getLocale());
if (targetLanguage == null) {
targetLanguage = languageService.getLanguage(Locale.ENGLISH);
}
PathBuilder<E> entity = new PathBuilder<E>(targetType, toVariable(targetType));
return fetchTranslations(entity.get("id").in(input.stream().map(EntityId::getId).collect(Collectors.toList())), Pageable.ofSize(Integer.MAX_VALUE), targetLanguage).getContent();
}
protected Page<T> fetchTranslations(final Predicate predicate, final Pageable page, final SysLang targetLanguage) {
final var ID_PROP = QCooperatorOwnedLang.cooperatorOwnedLang.sysLang().id.getMetadata().getName();
final var SYS_LANG_PROP = QCooperatorOwnedLang.cooperatorOwnedLang.sysLang().getMetadata().getName();
final var TITLE_PROP = QCooperatorOwnedLang.cooperatorOwnedLang.title.getMetadata().getName();
final var DESCRIPTION_PROP = QCooperatorOwnedLang.cooperatorOwnedLang.description.getMetadata().getName();
PathBuilder<E> entity = new PathBuilder<E>(targetType, toVariable(targetType));
ListPath<L, PathBuilder<L>> langs = entity.getList(QTranslatedCooperatorOwnedModel.translatedCooperatorOwnedModel.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<SysLang> lanSysLang = lan.get(SYS_LANG_PROP, SysLang.class);
PathBuilder<Long> lanSysLangId = lanSysLang.get(ID_PROP, Long.class);
PathBuilder<String> lanTitle = lan.get(TITLE_PROP, String.class);
PathBuilder<String> lanDescription = lan.get(DESCRIPTION_PROP, String.class);
PathBuilder<SysLang> defSysLang = def.get(SYS_LANG_PROP, SysLang.class);
PathBuilder<Long> defSysLangId = defSysLang.get(ID_PROP, Long.class);
PathBuilder<String> defTitle = def.get(TITLE_PROP, String.class);
PathBuilder<String> defDescription = def.get(DESCRIPTION_PROP, String.class);
var title = Expressions.cases().when(lanTitle.isNotNull()).then(lanTitle).otherwise(defTitle);
var description = Expressions.cases().when(lanDescription.isNotNull()).then(lanDescription).otherwise(defDescription);
// query aliases for use in orderby clause
var titlePath = ExpressionUtils.path(String.class, TITLE_PROP);
var descriptionPath = ExpressionUtils.path(String.class, DESCRIPTION_PROP);
JPAQuery<Tuple> query = jpaQueryFactory.from(entity)
.select(entity, Expressions.as(title, titlePath), Expressions.as(description, descriptionPath))
// Default language
.leftJoin(langs, def).on(defSysLangId.eq(LanguageService.DEFAULT_LANGUAGE.getId()))
// Target language
.leftJoin(langs, lan).on(lanSysLangId.eq(targetLanguage.getId()))
// 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 if (o.getProperty().equalsIgnoreCase(DESCRIPTION_PROP)) {
query.orderBy(new OrderSpecifier<String>(o.isAscending() ? Order.ASC : Order.DESC, descriptionPath));
} else {
query.orderBy(new OrderSpecifier(o.isAscending() ? Order.ASC : Order.DESC, entity.get(o.getProperty())));
}
}
var content = query.fetch().stream().map(tuple -> {
return toTranslated(tuple.get(0, targetType), tuple.get(1, String.class), tuple.get(2, String.class));
}).collect(Collectors.toList());
return new PageImpl<>(content, page, totalElements);
}
protected abstract T toTranslated(@Nullable E entity, @Nullable String title, @Nullable String description);
private String toVariable(Class<E> clazz) {
String simpleName = clazz.getSimpleName();
return Character.toLowerCase(simpleName.charAt(0)) + simpleName.substring(1);
}
@Transactional
public L deleteTranslation(E entity, SysLang sysLang) {
L existing = langsRepository.getByEntityAndSysLang(entity, sysLang);
langsRepository.delete(existing);
return existing;
}
public List<L> listExistingTranslations(E entity) {
var list = langsRepository.findAllByEntity(entity);
list.forEach(this::_lazyLoad);
return list;
}
public List<L> listTranslations(E entity) {
var list = langsRepository.findAllByEntity(entity);
Optional<L> defaultTranslation = list.stream().filter(ctl -> ctl.getSysLang().getId().equals(LanguageService.DEFAULT_LANGUAGE.getId())).findFirst();
List<L> res = new ArrayList<>();
languageService.listEnabledLanguages().forEach(sysLang -> {
res.add(
list.stream()
// find existing
.filter(ctl -> ctl.getSysLang().getId().equals(sysLang.getId())).findFirst()
// or create with default
.orElse(newLang(defaultTranslation, sysLang))
);
});
return res;
}
@Transactional
public L addLang(E entity, SysLang sysLang, L input) {
assert(entity != null);
assert(sysLang != null);
assert(input.getId() == null);
input.setEntity(entity);
input.setSysLang(sysLang);
return _lazyLoad(langsRepository.save(input));
}
public L getLang(E entity, SysLang sysLang) {
return _lazyLoad(langsRepository.getByEntityAndSysLang(entity, sysLang));
}
@Transactional
public L upsertLang(E entity, SysLang sysLang, L input) {
assert(input.getEntity() == null || entity.getId().equals(input.getEntity().getId()));
assert(input.getSysLang() == null || sysLang.getId().equals(input.getSysLang().getId()));
L existing = getLang(entity, sysLang);
if (existing == null) {
assert(input.isNew());
input.setEntity(entity);
input.setSysLang(sysLang);
return create(input);
} else {
return update(input, existing);
}
}
public L newLang() {
try {
return langType.getConstructor().newInstance();
} catch (Throwable e) {
throw new RuntimeException("Could not create an instance", e);
}
}
private L newLang(Optional<L> defaultTranslation, SysLang sysLang) {
try {
return langType.getConstructor(Optional.class, SysLang.class).newInstance(defaultTranslation, sysLang);
} catch (Throwable e) {
throw new RuntimeException("Could not create an instance", e);
}
}
/**
* 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()));
source.setSysLang(sysLangRepository.getReferenceById(source.getSysLang().getId()));
return _lazyLoad(langsRepository.save(source));
}
/**
* The default implementation updates only the title and description
*/
@Override
public L update(L updated, L target) {
target.setTitle(updated.getTitle());
target.setDescription(updated.getDescription());
return _lazyLoad(repository.save(target));
}
}