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 super.createFast(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;
- }
- }