CodeValueServiceImpl.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 static org.gringlobal.service.LanguageService.MCPD_IETF_TAG;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import javax.annotation.Resource;
import javax.persistence.Entity;
import lombok.extern.slf4j.Slf4j;
import org.gringlobal.api.exception.InvalidApiUsageException;
import org.gringlobal.custom.validation.javax.CodeValueField;
import org.gringlobal.model.CodeValue;
import org.gringlobal.model.CodeValueLang;
import org.gringlobal.model.QCodeValue;
import org.gringlobal.model.SysLang;
import org.gringlobal.persistence.CodeValueLangRepository;
import org.gringlobal.persistence.CodeValueRepository;
import org.gringlobal.service.CodeValueService;
import org.gringlobal.service.CodeValueTranslationService;
import org.gringlobal.service.CodeValueTranslationService.TranslatedCodeValue;
import org.gringlobal.service.filter.CodeValueFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.domain.PageRequest;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.ReflectionUtils;
import org.springframework.validation.annotation.Validated;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.core.types.dsl.PathBuilder;
@Service
@Transactional(readOnly = true)
@Validated
@CacheConfig(cacheNames = CodeValueServiceImpl.CACHE_NAME)
@Slf4j
public class CodeValueServiceImpl extends FilteredTranslatedCRUDServiceImpl<CodeValue, CodeValueLang, TranslatedCodeValue, CodeValueFilter, CodeValueRepository> implements CodeValueService {
public static final String CACHE_NAME="codeValues";
@Autowired
private CodeValueLangRepository cvLangRepository;
@Resource
private Set<Class<?>> entityClassSet;
@Component
protected static class CodeValueTranslationSupport extends BaseTranslationSupport<CodeValue, CodeValueLang, TranslatedCodeValue, CodeValueFilter, CodeValueLangRepository> implements CodeValueTranslationService {
@Autowired
private CodeValueRepository codeValueRepository;
public CodeValueTranslationSupport() {
super();
}
@Override
protected TranslatedCodeValue toTranslated(CodeValue entity, String title, String description) {
return TranslatedCodeValue.from(entity, title, description);
}
@Override
@Cacheable(value = CACHE_NAME, key = "'translatedcodevalue-' + #groupName + '-' + #code + '-' + #locale.displayName") // may return old values, but will eventually refresh
public Optional<TranslatedCodeValue> findTranslatedCodeValue(String groupName, String code, Locale locale) {
SysLang targetLanguage = languageService.getLanguage(locale);
if (targetLanguage == null) {
targetLanguage = languageService.getLanguage(Locale.ENGLISH);
}
Optional<TranslatedCodeValue> translatedCodeValue = Optional.empty();
CodeValue codeValue = codeValueRepository.getByGroupNameAndValue(groupName, code);
if (codeValue != null) {
PathBuilder<CodeValue> entity = new PathBuilder<>(CodeValue.class, QCodeValue.codeValue.getMetadata());
var translations = fetchTranslations(entity.eq(codeValue), PageRequest.of(0, 1), targetLanguage).getContent();
if (!translations.isEmpty()) {
translatedCodeValue = Optional.ofNullable(translations.get(0));
}
}
return translatedCodeValue;
}
}
@Override
@PreAuthorize("hasAuthority('GROUP_ADMINS')")
@Cacheable(key = "'tcv-' + #id", unless = "#result == null") // must use special cache key!
public TranslatedCodeValue loadTranslated(long id) {
return super.loadTranslated(id);
}
@Override
@PreAuthorize("hasAuthority('GROUP_ADMINS')")
@CacheEvict(allEntries = true)
@Transactional
public CodeValue create(CodeValue source) {
var target = new CodeValue();
target.apply(source);
return repository.save(target);
}
@Override
@PreAuthorize("hasAuthority('GROUP_ADMINS')")
@CacheEvict(allEntries = true)
@Transactional
public CodeValue createFast(CodeValue source) {
return repository.save(source);
}
@Override
@PreAuthorize("hasAuthority('GROUP_ADMINS')")
@CacheEvict(allEntries = true)
public TranslatedCodeValue create(TranslatedCodeValue source) {
return super.create(source);
}
@Override
@Transactional
@PreAuthorize("hasAuthority('GROUP_ADMINS')")
@CacheEvict(allEntries = true)
public CodeValue update(CodeValue updated, CodeValue target) {
target.apply(updated);
return repository.save(target);
}
@Override
@Transactional
@PreAuthorize("hasAuthority('GROUP_ADMINS')")
@CacheEvict(allEntries = true)
public CodeValue updateFast(CodeValue updated, CodeValue target) {
target.apply(updated);
return repository.save(target);
}
@Override
@PreAuthorize("hasAuthority('GROUP_ADMINS')")
@CacheEvict(allEntries = true)
public CodeValue update(CodeValue updated) {
return super.update(updated);
}
@Override
@PreAuthorize("hasAuthority('GROUP_ADMINS')")
@CacheEvict(allEntries = true)
public CodeValueLang upsert(CodeValue entity, SysLang sysLang, CodeValueLang input) {
return super.upsert(entity, sysLang, input);
}
@Override
@PreAuthorize("hasAuthority('GROUP_ADMINS')")
@CacheEvict(allEntries = true)
@Transactional
public CodeValue remove(CodeValue entity) {
entity = get(entity);
Map<Class<?>, Set<Field>> codeValueFields = findCodeValueReferences(entity.getGroupName());
if (!codeValueFields.isEmpty()) {
checkCodeValueUsage(codeValueFields, entity.getValue());
}
return super.remove(entity);
}
@Override
@PreAuthorize("hasAuthority('GROUP_ADMINS')")
@CacheEvict(allEntries = true)
@Transactional
public CodeValue replaceAndRemove(CodeValue entity, CodeValue replacement) {
entity = get(entity);
replacement = get(replacement);
Map<Class<?>, Set<Field>> codeValueFields = findCodeValueReferences(entity.getGroupName());
if (!codeValueFields.isEmpty()) {
replaceCodeValue(codeValueFields, entity, replacement);
}
return remove(entity);
}
@Override
@PreAuthorize("hasAuthority('GROUP_ADMINS')")
@CacheEvict(allEntries = true)
public CodeValueLang remove(CodeValue entity, SysLang sysLang) {
return super.remove(entity, sysLang);
}
@Override
@PreAuthorize("hasAuthority('GROUP_ADMINS')")
public List<CodeValueLang> getLangs(long entityId) {
return super.getLangs(entityId);
}
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
@PreAuthorize("hasAuthority('GROUP_ADMINS')")
@CacheEvict(allEntries = true)
public CodeValue ensureCodeValue(String groupName, String value, String title, String description) {
CodeValue codeValue = repository.getByGroupNameAndValue(groupName, value);
if (codeValue == null) {
log.info("Adding CodeValue group={} value={} {} {}", groupName, value, title, description);
var translatedCodeValue = create(TranslatedCodeValue.from(new CodeValue(groupName, value), title, description));
codeValue = translatedCodeValue.entity;
}
return codeValue;
}
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW, readOnly = true)
@Cacheable(key = "'codevalue-' + #groupName + '-' + #value", sync = true)
public boolean validate(String groupName, String value) {
return repository.existsCodeValueByGroupNameAndValue(groupName, value);
}
@Override
@Cacheable(key = "'mcpdOfCode-' + #groupName + '-' + #value", sync = true)
public String findMcpdOfCodeValue(String groupName, String value) {
SysLang sl = languageService.getLanguage(MCPD_IETF_TAG);
CodeValue cv = repository.getByGroupNameAndValue(groupName, value);
if (cv == null)
return null;
CodeValueLang cvl = cvLangRepository.getByEntityAndSysLang(cv, sl);
return cvl != null ? cvl.getTitle() : null;
}
@Override
@Cacheable(key = "'codeOfMcpd-' + #groupName + '-' + #mcpd", sync = true)
public String findCodeValueOfMCPD(String groupName, Integer mcpd) {
if (mcpd == null)
return null;
CodeValue cv = cvLangRepository.findCodeValueOfMCPD(groupName, String.valueOf(mcpd));
return cv != null ? cv.getValue() : null;
}
@Override
@Cacheable(key = "'codeOfMcpd-' + #groupName + '-' + #mcpd", sync = true)
public String findCodeValueOfMCPD(String groupName, String mcpd) {
if (mcpd == null)
return null;
CodeValue cv = cvLangRepository.findCodeValueOfMCPD(groupName, mcpd);
return cv != null ? cv.getValue() : null;
}
@Override
public Map<Class<?>, Set<Field>> findCodeValueReferences(String groupName) {
// map of classes and fields that use CodeValues with given groupName
Map<Class<?>, Set<Field>> usageInFieldsMap = new HashMap<>();
// searching for CodeValues usages with given groupName in entity classes
entityClassSet.forEach(aClass -> ReflectionUtils.doWithFields(aClass, field -> {
ReflectionUtils.makeAccessible(field);
var codeValueField = field.getAnnotation(CodeValueField.class);
if (Objects.nonNull(codeValueField) && Objects.equals(codeValueField.value(), groupName)) {
usageInFieldsMap.compute(aClass, (key, list) -> {
if (list == null) {
list = new HashSet<>();
}
list.add(field);
return list;
});
}
}));
return usageInFieldsMap;
}
@Override
public List<CodeValueStats> getStatistics(String groupName) {
List<CodeValueStats> codeValueStatsList = new ArrayList<>();
Map<Class<?>, Set<Field>> codeValueFields = findCodeValueReferences(groupName);
if (codeValueFields.isEmpty()) {
return codeValueStatsList;
}
for (Map.Entry<Class<?>, Set<Field>> entry: codeValueFields.entrySet()) {
Class<?> targetClass = entry.getKey();
if (! targetClass.isAnnotationPresent(Entity.class)) {
continue;
}
var root = new PathBuilder<>(targetClass, "t");
for (Field field: entry.getValue()) {
PathBuilder<Object> cvField = root.get(field.getName());
var q = jpaQueryFactory.select(cvField, root.count()).from(root).groupBy(cvField).where(cvField.isNotNull());
q.fetch().forEach(result -> {
CodeValueStats codeValueStats = new CodeValueStats();
codeValueStats.value = result.get(0, String.class);
codeValueStats.number = Optional.ofNullable(result.get(1, Long.class)).orElse(0L).intValue();
codeValueStats.entity = targetClass.getSimpleName();
codeValueStats.property = field.getName();
codeValueStatsList.add(codeValueStats);
});
}
}
return codeValueStatsList;
}
private void checkCodeValueUsage(Map<Class<?>, Set<Field>> codeValueFields, String targetValue) {
for (Map.Entry<Class<?>, Set<Field>> entry: codeValueFields.entrySet()) {
Class<?> targetClass = entry.getKey();
if (! targetClass.isAnnotationPresent(Entity.class)) {
continue;
}
var root = new PathBuilder<>(targetClass, "t");
var q = jpaQueryFactory.selectFrom(root);
BooleanExpression predicate = null;
for (Field field: entry.getValue()) {
PathBuilder<Object> cvField = root.get(field.getName());
if (predicate != null) {
predicate = predicate.or(cvField.eq(targetValue));
} else {
predicate = cvField.eq(targetValue);
}
}
q.where(predicate);
List<?> result = q.limit(1).fetch();
if (Objects.nonNull(result) && !result.isEmpty()) {
throw new InvalidApiUsageException("Can't delete a code value that is in use");
}
}
}
private void replaceCodeValue(Map<Class<?>, Set<Field>> codeValueFields, CodeValue existingCode, CodeValue replacementCode) {
for (Map.Entry<Class<?>, Set<Field>> entry: codeValueFields.entrySet()) {
Class<?> targetClass = entry.getKey();
if (! targetClass.isAnnotationPresent(Entity.class)) {
continue;
}
var root = new PathBuilder<>(targetClass, "t");
for (Field field: entry.getValue()) {
var q = jpaQueryFactory.update(root);
PathBuilder<Object> cvField = root.get(field.getName());
q.set(cvField, replacementCode.getValue());
q.where(cvField.eq(existingCode.getValue()));
var updateCount = q.execute();
if (updateCount > 0) {
log.info("Replaced {} code values in {}.{} from {} to {}", updateCount, targetClass.getSimpleName(), field.getName(), existingCode.getValue(), replacementCode.getValue());
}
}
}
}
public static class CodeValueStats {
public String value;
public String entity;
public String property;
public Integer number;
}
}