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.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
	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)
				.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));
	}
}