CodeValueServiceImpl.java

  1. /*
  2.  * Copyright 2020 Global Crop Diversity Trust
  3.  *
  4.  * Licensed under the Apache License, Version 2.0 (the "License");
  5.  * you may not use this file except in compliance with the License.
  6.  * You may obtain a copy of the License at
  7.  *
  8.  *   http://www.apache.org/licenses/LICENSE-2.0
  9.  *
  10.  * Unless required by applicable law or agreed to in writing, software
  11.  * distributed under the License is distributed on an "AS IS" BASIS,
  12.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13.  * See the License for the specific language governing permissions and
  14.  * limitations under the License.
  15.  */
  16. package org.gringlobal.service.impl;

  17. import static org.gringlobal.service.LanguageService.MCPD_IETF_TAG;

  18. import java.lang.reflect.Field;
  19. import java.util.ArrayList;
  20. import java.util.HashMap;
  21. import java.util.HashSet;
  22. import java.util.List;
  23. import java.util.Locale;
  24. import java.util.Map;
  25. import java.util.Objects;
  26. import java.util.Optional;
  27. import java.util.Set;

  28. import javax.annotation.Resource;
  29. import javax.persistence.Entity;

  30. import lombok.extern.slf4j.Slf4j;
  31. import org.gringlobal.api.exception.InvalidApiUsageException;
  32. import org.gringlobal.custom.validation.javax.CodeValueField;
  33. import org.gringlobal.model.CodeValue;
  34. import org.gringlobal.model.CodeValueLang;
  35. import org.gringlobal.model.QCodeValue;
  36. import org.gringlobal.model.SysLang;
  37. import org.gringlobal.persistence.CodeValueLangRepository;
  38. import org.gringlobal.persistence.CodeValueRepository;
  39. import org.gringlobal.service.CodeValueService;
  40. import org.gringlobal.service.CodeValueTranslationService;
  41. import org.gringlobal.service.CodeValueTranslationService.TranslatedCodeValue;
  42. import org.gringlobal.service.filter.CodeValueFilter;
  43. import org.springframework.beans.factory.annotation.Autowired;
  44. import org.springframework.cache.annotation.CacheConfig;
  45. import org.springframework.cache.annotation.CacheEvict;
  46. import org.springframework.cache.annotation.Cacheable;
  47. import org.springframework.data.domain.PageRequest;
  48. import org.springframework.security.access.prepost.PreAuthorize;
  49. import org.springframework.stereotype.Component;
  50. import org.springframework.stereotype.Service;
  51. import org.springframework.transaction.annotation.Propagation;
  52. import org.springframework.transaction.annotation.Transactional;
  53. import org.springframework.util.ReflectionUtils;
  54. import org.springframework.validation.annotation.Validated;

  55. import com.querydsl.core.types.dsl.BooleanExpression;
  56. import com.querydsl.core.types.dsl.PathBuilder;

  57. @Service
  58. @Transactional(readOnly = true)
  59. @Validated
  60. @CacheConfig(cacheNames = CodeValueServiceImpl.CACHE_NAME)
  61. @Slf4j
  62. public class CodeValueServiceImpl extends FilteredTranslatedCRUDServiceImpl<CodeValue, CodeValueLang, TranslatedCodeValue, CodeValueFilter, CodeValueRepository> implements CodeValueService {
  63.     public static final String CACHE_NAME="codeValues";

  64.     @Autowired
  65.     private CodeValueLangRepository cvLangRepository;

  66.     @Resource
  67.     private Set<Class<?>> entityClassSet;

  68.     @Component
  69.     protected static class CodeValueTranslationSupport extends BaseTranslationSupport<CodeValue, CodeValueLang, TranslatedCodeValue, CodeValueFilter, CodeValueLangRepository> implements CodeValueTranslationService {
  70.        
  71.         @Autowired
  72.         private CodeValueRepository codeValueRepository;
  73.        
  74.         public CodeValueTranslationSupport() {
  75.             super();
  76.         }

  77.         @Override
  78.         protected TranslatedCodeValue toTranslated(CodeValue entity, String title, String description) {
  79.             return TranslatedCodeValue.from(entity, title, description);
  80.         }

  81.         @Override
  82.         @Cacheable(value = CACHE_NAME, key = "'translatedcodevalue-' + #groupName + '-' + #code + '-' + #locale.displayName") // may return old values, but will eventually refresh
  83.         public Optional<TranslatedCodeValue> findTranslatedCodeValue(String groupName, String code, Locale locale) {
  84.             SysLang targetLanguage = languageService.getLanguage(locale);
  85.             if (targetLanguage == null) {
  86.                 targetLanguage = languageService.getLanguage(Locale.ENGLISH);
  87.             }

  88.             Optional<TranslatedCodeValue> translatedCodeValue = Optional.empty();

  89.             CodeValue codeValue = codeValueRepository.getByGroupNameAndValue(groupName, code);
  90.             if (codeValue != null) {
  91.                 PathBuilder<CodeValue> entity = new PathBuilder<>(CodeValue.class, QCodeValue.codeValue.getMetadata());
  92.                 var translations = fetchTranslations(entity.eq(codeValue), PageRequest.of(0, 1), targetLanguage).getContent();
  93.                 if (!translations.isEmpty()) {
  94.                     translatedCodeValue = Optional.ofNullable(translations.get(0));
  95.                 }
  96.             }
  97.             return translatedCodeValue;
  98.         }
  99.     }

  100.     @Override
  101.     @PreAuthorize("hasAuthority('GROUP_ADMINS')")
  102.     @Cacheable(key = "'tcv-' + #id", unless = "#result == null") // must use special cache key!
  103.     public TranslatedCodeValue loadTranslated(long id) {
  104.         return super.loadTranslated(id);
  105.     }

  106.     @Override
  107.     @PreAuthorize("hasAuthority('GROUP_ADMINS')")
  108.     @CacheEvict(allEntries = true)
  109.     @Transactional
  110.     public CodeValue create(CodeValue source) {
  111.         var target = new CodeValue();
  112.         target.apply(source);
  113.         return repository.save(target);
  114.     }

  115.     @Override
  116.     @PreAuthorize("hasAuthority('GROUP_ADMINS')")
  117.     @CacheEvict(allEntries = true)
  118.     @Transactional
  119.     public CodeValue createFast(CodeValue source) {
  120.         return super.createFast(source);
  121.     }

  122.     @Override
  123.     @PreAuthorize("hasAuthority('GROUP_ADMINS')")
  124.     @CacheEvict(allEntries = true)
  125.     public TranslatedCodeValue create(TranslatedCodeValue source) {
  126.         return super.create(source);
  127.     }

  128.     @Override
  129.     @Transactional
  130.     @PreAuthorize("hasAuthority('GROUP_ADMINS')")
  131.     @CacheEvict(allEntries = true)
  132.     public CodeValue update(CodeValue updated, CodeValue target) {
  133.         target.apply(updated);
  134.         return repository.save(target);
  135.     }

  136.     @Override
  137.     @Transactional
  138.     @PreAuthorize("hasAuthority('GROUP_ADMINS')")
  139.     @CacheEvict(allEntries = true)
  140.     public CodeValue updateFast(CodeValue updated, CodeValue target) {
  141.         target.apply(updated);
  142.         return repository.save(target);
  143.     }

  144.     @Override
  145.     @PreAuthorize("hasAuthority('GROUP_ADMINS')")
  146.     @CacheEvict(allEntries = true)
  147.     public CodeValue update(CodeValue updated) {
  148.         return super.update(updated);
  149.     }

  150.     @Override
  151.     @PreAuthorize("hasAuthority('GROUP_ADMINS')")
  152.     @CacheEvict(allEntries = true)
  153.     public CodeValueLang upsert(CodeValue entity, SysLang sysLang, CodeValueLang input) {
  154.         return super.upsert(entity, sysLang, input);
  155.     }

  156.     @Override
  157.     @PreAuthorize("hasAuthority('GROUP_ADMINS')")
  158.     @CacheEvict(allEntries = true)
  159.     @Transactional
  160.     public CodeValue remove(CodeValue entity) {
  161.         entity = get(entity);
  162.         Map<Class<?>, Set<Field>> codeValueFields = findCodeValueReferences(entity.getGroupName());

  163.         if (!codeValueFields.isEmpty()) {
  164.             checkCodeValueUsage(codeValueFields, entity.getValue());
  165.         }
  166.         return super.remove(entity);
  167.     }

  168.     @Override
  169.     @PreAuthorize("hasAuthority('GROUP_ADMINS')")
  170.     @CacheEvict(allEntries = true)
  171.     @Transactional
  172.     public CodeValue replaceAndRemove(CodeValue entity, CodeValue replacement) {
  173.         entity = get(entity);
  174.         replacement = get(replacement);
  175.         Map<Class<?>, Set<Field>> codeValueFields = findCodeValueReferences(entity.getGroupName());

  176.         if (!codeValueFields.isEmpty()) {
  177.             replaceCodeValue(codeValueFields, entity, replacement);
  178.         }
  179.         return remove(entity);
  180.     }

  181.     @Override
  182.     @PreAuthorize("hasAuthority('GROUP_ADMINS')")
  183.     @CacheEvict(allEntries = true)
  184.     public CodeValueLang remove(CodeValue entity, SysLang sysLang) {
  185.         return super.remove(entity, sysLang);
  186.     }

  187.     @Override
  188.     @PreAuthorize("hasAuthority('GROUP_ADMINS')")
  189.     public List<CodeValueLang> getLangs(long entityId) {
  190.         return super.getLangs(entityId);
  191.     }

  192.     @Override
  193.     @Transactional(propagation = Propagation.REQUIRES_NEW)
  194.     @PreAuthorize("hasAuthority('GROUP_ADMINS')")
  195.     @CacheEvict(allEntries = true)
  196.     public CodeValue ensureCodeValue(String groupName, String value, String title, String description) {
  197.         CodeValue codeValue = repository.getByGroupNameAndValue(groupName, value);

  198.         if (codeValue == null) {
  199.             log.info("Adding CodeValue group={} value={} {} {}", groupName, value, title, description);
  200.             var translatedCodeValue = create(TranslatedCodeValue.from(new CodeValue(groupName, value), title, description));
  201.             codeValue = translatedCodeValue.entity;
  202.         }

  203.         return codeValue;
  204.     }

  205.     @Override
  206.     @Transactional(propagation = Propagation.REQUIRES_NEW, readOnly = true)
  207.     @Cacheable(key = "'codevalue-' + #groupName + '-' + #value", sync = true)
  208.     public boolean validate(String groupName, String value) {
  209.         return repository.existsCodeValueByGroupNameAndValue(groupName, value);
  210.     }

  211.     @Override
  212.     @Cacheable(key = "'mcpdOfCode-' + #groupName + '-' + #value", sync = true)
  213.     public String findMcpdOfCodeValue(String groupName, String value) {
  214.         SysLang sl = languageService.getLanguage(MCPD_IETF_TAG);
  215.         CodeValue cv = repository.getByGroupNameAndValue(groupName, value);
  216.         if (cv == null)
  217.             return null;

  218.         CodeValueLang cvl = cvLangRepository.getByEntityAndSysLang(cv, sl);
  219.         return cvl != null ? cvl.getTitle() : null;
  220.     }

  221.     @Override
  222.     @Cacheable(key = "'codeOfMcpd-' + #groupName + '-' + #mcpd", sync = true)
  223.     public String findCodeValueOfMCPD(String groupName, Integer mcpd) {
  224.         if (mcpd == null)
  225.             return null;

  226.         CodeValue cv = cvLangRepository.findCodeValueOfMCPD(groupName, String.valueOf(mcpd));
  227.         return cv != null ? cv.getValue() : null;
  228.     }


  229.     @Override
  230.     @Cacheable(key = "'codeOfMcpd-' + #groupName + '-' + #mcpd", sync = true)
  231.     public String findCodeValueOfMCPD(String groupName, String mcpd) {
  232.         if (mcpd == null)
  233.             return null;

  234.         CodeValue cv = cvLangRepository.findCodeValueOfMCPD(groupName, mcpd);
  235.         return cv != null ? cv.getValue() : null;
  236.     }


  237.     @Override
  238.     public Map<Class<?>, Set<Field>> findCodeValueReferences(String groupName) {
  239.         // map of classes and fields that use CodeValues with given groupName
  240.         Map<Class<?>, Set<Field>> usageInFieldsMap = new HashMap<>();

  241.         // searching for CodeValues usages with given groupName in entity classes
  242.         entityClassSet.forEach(aClass -> ReflectionUtils.doWithFields(aClass, field -> {
  243.             ReflectionUtils.makeAccessible(field);

  244.             var codeValueField = field.getAnnotation(CodeValueField.class);
  245.             if (Objects.nonNull(codeValueField) && Objects.equals(codeValueField.value(), groupName)) {
  246.                 usageInFieldsMap.compute(aClass, (key, list) -> {
  247.                     if (list == null) {
  248.                         list = new HashSet<>();
  249.                     }
  250.                     list.add(field);
  251.                     return list;
  252.                 });
  253.             }
  254.         }));
  255.         return usageInFieldsMap;
  256.     }

  257.     @Override
  258.     public List<CodeValueStats> getStatistics(String groupName) {
  259.         List<CodeValueStats> codeValueStatsList = new ArrayList<>();
  260.         Map<Class<?>, Set<Field>> codeValueFields = findCodeValueReferences(groupName);
  261.         if (codeValueFields.isEmpty()) {
  262.             return codeValueStatsList;
  263.         }
  264.         for (Map.Entry<Class<?>, Set<Field>> entry: codeValueFields.entrySet()) {
  265.             Class<?> targetClass = entry.getKey();
  266.             if (! targetClass.isAnnotationPresent(Entity.class)) {
  267.                 continue;
  268.             }

  269.             var root = new PathBuilder<>(targetClass, "t");

  270.             for (Field field: entry.getValue()) {
  271.                 PathBuilder<Object> cvField = root.get(field.getName());
  272.                 var q = jpaQueryFactory.select(cvField, root.count()).from(root).groupBy(cvField).where(cvField.isNotNull());
  273.                 q.fetch().forEach(result -> {
  274.                     CodeValueStats codeValueStats = new CodeValueStats();
  275.                     codeValueStats.value = result.get(0, String.class);
  276.                     codeValueStats.number = Optional.ofNullable(result.get(1, Long.class)).orElse(0L).intValue();
  277.                     codeValueStats.entity = targetClass.getSimpleName();
  278.                     codeValueStats.property = field.getName();
  279.                     codeValueStatsList.add(codeValueStats);
  280.                 });
  281.             }
  282.         }

  283.         return codeValueStatsList;
  284.     }

  285.     private void checkCodeValueUsage(Map<Class<?>, Set<Field>> codeValueFields, String targetValue) {

  286.         for (Map.Entry<Class<?>, Set<Field>> entry: codeValueFields.entrySet()) {
  287.             Class<?> targetClass = entry.getKey();
  288.             if (! targetClass.isAnnotationPresent(Entity.class)) {
  289.                 continue;
  290.             }

  291.             var root = new PathBuilder<>(targetClass, "t");
  292.             var q = jpaQueryFactory.selectFrom(root);
  293.            
  294.             BooleanExpression predicate = null;
  295.             for (Field field: entry.getValue()) {
  296.                 PathBuilder<Object> cvField = root.get(field.getName());
  297.                 if (predicate != null) {
  298.                     predicate = predicate.or(cvField.eq(targetValue));
  299.                 } else {
  300.                     predicate = cvField.eq(targetValue);
  301.                 }
  302.             }

  303.             q.where(predicate);
  304.             List<?> result = q.limit(1).fetch();
  305.             if (Objects.nonNull(result) && !result.isEmpty()) {
  306.                 throw new InvalidApiUsageException("Can't delete a code value that is in use");
  307.             }
  308.         }
  309.     }

  310.     private void replaceCodeValue(Map<Class<?>, Set<Field>> codeValueFields, CodeValue existingCode, CodeValue replacementCode) {
  311.         for (Map.Entry<Class<?>, Set<Field>> entry: codeValueFields.entrySet()) {
  312.             Class<?> targetClass = entry.getKey();
  313.             if (! targetClass.isAnnotationPresent(Entity.class)) {
  314.                 continue;
  315.             }

  316.             var root = new PathBuilder<>(targetClass, "t");

  317.             for (Field field: entry.getValue()) {
  318.                 var q = jpaQueryFactory.update(root);
  319.                 PathBuilder<Object> cvField = root.get(field.getName());
  320.                 q.set(cvField, replacementCode.getValue());
  321.                 q.where(cvField.eq(existingCode.getValue()));
  322.                 var updateCount = q.execute();
  323.                 if (updateCount > 0) {
  324.                     log.info("Replaced {} code values in {}.{} from {} to {}", updateCount, targetClass.getSimpleName(), field.getName(), existingCode.getValue(), replacementCode.getValue());
  325.                 }
  326.             }
  327.         }
  328.     }

  329.     public static class CodeValueStats {
  330.         public String value;
  331.         public String entity;
  332.         public String property;
  333.         public Integer number;
  334.     }


  335. }