package ru.hflabs.rcd.service.document.recodeRule; import com.google.common.base.Predicate; import com.google.common.collect.*; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import ru.hflabs.rcd.accessor.RuleFieldAccessor; import ru.hflabs.rcd.event.modify.ChangeEvent; import ru.hflabs.rcd.exception.constraint.rule.AttachedRecodeRuleException; import ru.hflabs.rcd.exception.search.rule.UnknownRecodeRuleException; import ru.hflabs.rcd.model.change.Diff; import ru.hflabs.rcd.model.change.Predicates; import ru.hflabs.rcd.model.criteria.FilterCriteriaValue; import ru.hflabs.rcd.model.document.Field; import ru.hflabs.rcd.model.document.MetaField; import ru.hflabs.rcd.model.path.FieldNamedPath; import ru.hflabs.rcd.model.path.MetaFieldNamedPath; import ru.hflabs.rcd.model.rule.RecodeRule; import ru.hflabs.rcd.model.rule.RecodeRuleSet; import ru.hflabs.rcd.service.IMergeService; import ru.hflabs.rcd.service.ServiceUtils; import ru.hflabs.rcd.service.document.DocumentServiceTemplate; import ru.hflabs.rcd.service.document.IFieldService; import ru.hflabs.rcd.service.rule.IRecodeRuleService; import ru.hflabs.rcd.service.rule.IRecodeRuleSetService; import ru.hflabs.util.core.EqualsUtil; import ru.hflabs.util.spring.Assert; import java.util.Collection; import java.util.Collections; import java.util.Map; import java.util.Set; import static ru.hflabs.rcd.accessor.Accessors.*; import static ru.hflabs.rcd.model.CriteriaUtils.*; import static ru.hflabs.rcd.model.ModelUtils.*; import static ru.hflabs.rcd.service.ServiceUtils.*; /** * Класс <class>RecodeRuleService</class> реализует сервис работы с правилами перекодирования * * @author Nazin Alexander */ public class RecodeRuleService extends DocumentServiceTemplate<RecodeRule> implements IRecodeRuleService { /** Сервис работы с наборами правил перекодировани */ private IRecodeRuleSetService recodeRuleSetService; /** Сервис работы со значениями полей */ private IFieldService fieldService; /** Сервис обновления значений полей правил перекодирования */ private IMergeService<RecodeRuleSet, Collection<RecodeRule>, Collection<RecodeRule>> metaFieldActualizeService; public RecodeRuleService() { super(RecodeRule.class); this.metaFieldActualizeService = new MetaFieldActualizeService(); } public void setRecodeRuleSetService(IRecodeRuleSetService recodeRuleSetService) { this.recodeRuleSetService = recodeRuleSetService; } public void setFieldService(IFieldService fieldService) { this.fieldService = fieldService; } @Override protected Collection<RecodeRule> injectTransitiveDependencies(Collection<RecodeRule> objects) { return super.injectTransitiveDependencies(injectRelations(injectRuleRelations(objects, fieldService, FROM_RULE_INJECTOR, TO_RULE_INJECTOR), recodeRuleSetService)); } @Override public RecodeRule findUniqueByRelativeId(String relativeId, String value, boolean fillTransitive, boolean quietly) { RecodeRule result = findUniqueDocumentBy(this, createCriteriaByRelative(RecodeRule.RECODE_RULE_SET_ID, relativeId, RecodeRule.FROM_FIELD_ID, value), fillTransitive); if (result == null && !quietly) { throw new UnknownRecodeRuleException(String.format("Rule from set '%s' and source field ID '%s' not found", relativeId, value)); } return result; } @Override public Collection<RecodeRule> findAllByRelativeId(String relativeId, String searchQuery, boolean fillTransitive) { Assert.isTrue(StringUtils.hasText(relativeId), "ID must not be NULL or EMPTY"); return findAllByCriteria(createCriteriaByIDs(RecodeRule.RECODE_RULE_SET_ID, relativeId).injectSearch(searchQuery), fillTransitive); } /** * Выполняет поиск правил привязанных к идентификаторам полей * * @param recodeRuleSetId идентификатор набора правил * @param fieldName направление поля поиска * @param fieldValues значение поля поиска * @param fillTransitive флаг заполнения транзитивных зависимостей * @return Возвращает коллекцию найденных правил */ private Collection<RecodeRule> doFindAllByFields(String recodeRuleSetId, String fieldName, Collection<String> fieldValues, boolean fillTransitive) { return findAllByCriteria( createCriteriaByRelative(RecodeRule.RECODE_RULE_SET_ID, recodeRuleSetId, fieldName, fieldValues), fillTransitive ); } @Override public Collection<RecodeRule> findAllByFieldIDs(String recodeRuleSetId, Collection<String> fromFieldIDs, boolean fillTransitive) { return doFindAllByFields(recodeRuleSetId, RecodeRule.FROM_FIELD_ID, fromFieldIDs, fillTransitive); } @Override @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Throwable.class) public Collection<RecodeRule> modify(Collection<RecodeRule> toCreate, Collection<RecodeRule> toUpdate, Collection<RecodeRule> toClose, boolean needValidation) { return ImmutableList.<RecodeRule>builder() .addAll(create(toCreate, needValidation)) .addAll(update(toUpdate, needValidation)) .addAll(close(toClose, needValidation)) .build(); } /** * Выполняет создание правил перекодирования по созданным значениям полей * * @param fields коллекцию изменившихся значений полей */ private Collection<RecodeRule> doCreateFromRulesByDependencies(Collection<Field> fields) { Map<String, Field> id2fields = Maps.uniqueIndex(fields, ID_FUNCTION); Map<String, Collection<Field>> metaFieldId2fields = Multimaps.index( fields, RELATIVE_ID_FUNCTION ).asMap(); // Получаем все наборы перекодировок, в которых участвуют МЕТА-поля в качестве источника Collection<RecodeRuleSet> ruleSets = recodeRuleSetService.findAllByCriteria( createCriteriaByIDs(RecodeRuleSet.FROM_FIELD_ID, metaFieldId2fields.keySet()), false ); // Для каждого набора выполняем поиск существующих правил с таким же исходным значением ImmutableSet.Builder<RecodeRule> toCreate = ImmutableSet.builder(); for (RecodeRuleSet ruleSet : ruleSets) { Collection<Field> fromFields = metaFieldId2fields.get(FROM_RULE_INJECTOR.applyRelativeId(ruleSet)); // Получаем коллекцию идентификаторов полей, для которых правила перекодирования настроены final Collection<String> existedFieldIDs = Collections2.transform( findAllByFieldIDs(ruleSet.getId(), Collections2.transform(fromFields, ID_FUNCTION), false), FROM_RULE_FIELD_ID ); // Для каждого поля, для которого НЕ настроено правило перекодирования, определяем существующее правило перекодирование по исходному значению Collection<Field> fieldsToCheck = Collections2.filter(fromFields, new Predicate<Field>() { @Override public boolean apply(Field input) { return !existedFieldIDs.contains(input.getId()); } }); for (Field fromField : fieldsToCheck) { RecodeRule existedRule = findOneDocumentBy( this, createCriteriaByRelative(RecodeRule.RECODE_RULE_SET_ID, ruleSet.getId(), RecodeRule.VALUE, fromField.getValue()), true ); // Если правило с таким же исходным значением существует, то выполняем создание нового правила для поля if (existedRule != null) { String toFieldId = existedRule.getToFieldId(); // Получаем актуальное целевое значение Field toField = id2fields.containsKey(toFieldId) ? id2fields.get(toFieldId) : existedRule.getTo(); // Формируем правило RecodeRule newRule = new RecodeRule().injectRecodeRuleSet(ruleSet); newRule = FROM_RULE_INJECTOR.inject(newRule, fromField); newRule = TO_RULE_INJECTOR.inject(newRule, toField); // Добавляем правило на создание toCreate.add(newRule); } } } // Выполняем создание правил return create(toCreate.build(), false); } /** * Выполняем актуализацию правил, в которых изменился источник * * @param existedRules коллекция сущеуствующих правил * @param changedRules коллекция актуализированных правил * @return Возвращает обновленную коллекцию правил */ private Collection<RecodeRule> doUpdateFromRulesByDependencies(Collection<Field> changedFields, Collection<RecodeRule> existedRules, Collection<RecodeRule> changedRules) { if (CollectionUtils.isEmpty(changedRules)) { return changedRules; } Map<String, Field> id2fields = Maps.uniqueIndex(changedFields, ID_FUNCTION); ImmutableList.Builder<RecodeRule> toUpdate = ImmutableList.builder(); // Для каждого из актуализированных правил проверяем существование правила с таким же исходным значением for (Map.Entry<String, Collection<RecodeRule>> ruleSetId2rules : Multimaps.index(changedRules, RELATIVE_ID_FUNCTION).asMap().entrySet()) { final String ruleSetId = ruleSetId2rules.getKey(); // Формируем карту принадлежиности правил к именованному пути Map<FieldNamedPath, Collection<RecodeRule>> namedPath2rules = Multimaps.index( ruleSetId2rules.getValue(), FROM_RULE_INJECTOR.getNamedPathFunction() ).asMap(); // Выполняем проверку для каждого именованного пути for (Map.Entry<FieldNamedPath, Collection<RecodeRule>> toCheck : namedPath2rules.entrySet()) { // Пытаемся получить существующее правило по значению исходного пути RecodeRule existedRule = findOneDocumentBy( this, createCriteriaByRelative(RecodeRule.RECODE_RULE_SET_ID, ruleSetId, RecodeRule.VALUE, toCheck.getKey().getFieldValue()), true ); if (existedRule != null) { // Правило найдено - переопределяем целевое поле String toFieldId = existedRule.getToFieldId(); // Получаем актуальное целевое значение Field toField = id2fields.containsKey(toFieldId) ? id2fields.get(toFieldId) : existedRule.getTo(); // Выполняем актуализацию назначения для проверяемых правил for (RecodeRule rule : toCheck.getValue()) { if (EqualsUtil.equals(rule.getToFieldId(), toFieldId)) { toUpdate.add(TO_RULE_INJECTOR.inject(rule, toField)); } else { toUpdate.add(TO_RULE_INJECTOR.inject(shallowClone(rule), toField)); } } } else { // Правило не найдено toUpdate.addAll(toCheck.getValue()); } } } // Выполняем обновление return update(toUpdate.build(), existedRules, false); } /** * Выполняем актуализацию правил, в которых изменилось назначение * * @param existedRules коллекция сущеуствующих правил * @param changedRules коллекция актуализированных правил * @return Возвращает обновленную коллекцию правил */ private Collection<RecodeRule> doUpdateToRulesByDependencies(Collection<RecodeRule> existedRules, Collection<RecodeRule> changedRules) { return update(changedRules, existedRules, false); } /** * Выполняет обновление правил перекодирования по изменившимся значениям полей * * @param fields коллекцию изменившихся значений полей */ private Collection<RecodeRule> doUpdateByDependencies(Collection<Field> fields) { final Set<String> fieldIDs = Sets.newHashSet(Collections2.transform(fields, ID_FUNCTION)); // Получаем коллекцию существующих правил Collection<RecodeRule> existedRules = findAllByCriteria(createCriteriaByIDs(RecodeRule.FIELD_ID, fieldIDs), true); // Выполняем актуализацию правил Collection<RecodeRule> updatedRules = updateRulesByDependencies(RecodeRuleActualizeService.BY_FIELD, fields, existedRules); // Проверяем, что после актуализации есть изменения if (!CollectionUtils.isEmpty(updatedRules)) { ImmutableList.Builder<RecodeRule> result = ImmutableList.builder(); // Отбираем и обновляем те правила, в которых изменился источник result.addAll(doUpdateFromRulesByDependencies( fields, existedRules, Lists.newArrayList(Collections2.filter(updatedRules, new Predicate<RecodeRule>() { @Override public boolean apply(RecodeRule input) { return fieldIDs.contains(input.getFromFieldId()); } }))) ); // Отбираем и обновляем те правила, в которых изменилось назначение result.addAll(doUpdateToRulesByDependencies( existedRules, Lists.newArrayList(Collections2.filter(updatedRules, new Predicate<RecodeRule>() { @Override public boolean apply(RecodeRule input) { return fieldIDs.contains(input.getToFieldId()); } }))) ); // Возвращаем результат обновления return result.build(); } return Collections.emptyList(); } @SuppressWarnings("unchecked") @Override @Transactional(propagation = Propagation.MANDATORY, rollbackFor = Throwable.class) public <T> Collection<RecodeRule> modifyByDependencies(Class<T> dependencyClass, Collection<T> dependencies) { if (Field.class.equals(dependencyClass) && !CollectionUtils.isEmpty(dependencies)) { return ImmutableList.<RecodeRule>builder() .addAll(doCreateFromRulesByDependencies((Collection<Field>) dependencies)) .addAll(doUpdateByDependencies((Collection<Field>) dependencies)) .build(); } return Collections.emptyList(); } /** * Возвращает количество правил перекодирования по коллекции полей * * @param fields коллекция полей * @return Возвращает количество найденных правил */ private int countByFields(Collection<Field> fields) { return !CollectionUtils.isEmpty(fields) ? countByCriteria(createCriteriaByDocumentIDs(RecodeRule.FIELD_ID, fields)) : 0; } /** * Возвращает количество правил перекодирования по коллекции наборов правил * * @param ruleSets набор правил * @return Возвращает количество найденных правил */ private int countByRuleSets(Collection<RecodeRuleSet> ruleSets) { return !CollectionUtils.isEmpty(ruleSets) ? countByCriteria(createCriteriaByDocumentIDs(RecodeRule.RECODE_RULE_SET_ID, ruleSets)) : 0; } /** * Выполняет обновление правил перекодирования по изменившимся наборам * * @param ruleSets коллекция изменившихся наборов */ private void updateByRuleSets(Collection<RecodeRuleSet> ruleSets) { // Получаем количество существующих правил int count = countByRuleSets(ruleSets); // Проверяем, что правила найдены if (count > 0) { Map<String, RecodeRuleSet> id2ruleSet = Maps.uniqueIndex(ruleSets, ID_FUNCTION); // Получаем существующие правила Map<String, Collection<RecodeRule>> existed = Multimaps.index( findByCriteria(createCriteriaByDocumentIDs(RecodeRule.RECODE_RULE_SET_ID, ruleSets).injectCount(count), true).getResult(), RELATIVE_ID_FUNCTION ).asMap(); // Для каждого изменившегося набора формируем коллекции на создание и закрытие, если правила конфликтуют for (Map.Entry<String, Collection<RecodeRule>> existedEntry : existed.entrySet()) { ImmutableSet.Builder<RecodeRule> toUpdate = ImmutableSet.builder(); ImmutableSet.Builder<RecodeRule> toClose = ImmutableSet.builder(); RecodeRuleSet ruleSet = id2ruleSet.get(existedEntry.getKey()); Collection<RecodeRule> existedRules = existedEntry.getValue(); // Выполняем актуализацию правил Collection<RecodeRule> updatedRules = metaFieldActualizeService.merge(ruleSet, existedRules); // Выполняем закрытие тех правил, для которых различаются Map<FieldNamedPath, Collection<RecodeRule>> fromNamedPath2rules = Multimaps.index( updatedRules, FROM_RULE_INJECTOR.getNamedPathFunction() ).asMap(); for (Collection<RecodeRule> toCheck : fromNamedPath2rules.values()) { if (toCheck.size() > 1 && Sets.newHashSet(Collections2.transform(toCheck, TO_RULE_INJECTOR.getNamedPathFunction())).size() != 1) { toClose.addAll(toCheck); } else { toUpdate.addAll(toCheck); } } // Выполняем обновление update(toUpdate.build(), existedRules, false); // Выполняем закрытие doClose(toClose.build(), null); } } } @Override protected void handleOtherCreateEvent(ChangeEvent event) { if (Field.class.equals(event.getChangedClass())) { doCreateFromRulesByDependencies(event.getChanged(Field.class)); } } @Override protected void handleOtherUpdateEvent(ChangeEvent event) { if (Field.class.equals(event.getChangedClass())) { modifyByDependencies(Field.class, event.getChangedByPredicate(Field.class, Predicates.CHANGE_VALUE_PREDICATE)); } else if (RecodeRuleSet.class.equals(event.getChangedClass())) { // Формируем правло отбора изменившихся наборов Predicate<Collection<Diff>> targetDiffs = new Predicate<Collection<Diff>>() { @Override public boolean apply(Collection<Diff> input) { for (Diff diff : input) { if (RecodeRuleSet.FROM_FIELD_ID.equals(diff.getField()) || RecodeRuleSet.TO_FIELD_ID.equals(diff.getField())) { return true; } } return false; } }; // Выполняем обновление правил перекодирования по изменившимся наборам updateByRuleSets(event.getChangedByPredicate(RecodeRuleSet.class, targetDiffs)); } } @Override protected void handleOtherCloseEvent(ChangeEvent event) { if (Field.class.equals(event.getChangedClass())) { // Определяем количество призязанных правил int existedRulesCount = countByFields(event.getChanged(Field.class)); // Проверяем, что к закрываемым значениям полей не привязано правил Assert.isTrue( existedRulesCount == 0, String.format("Can't %s %s. Cause by: found %d attached recode rules", event.getChangeType().name().toLowerCase(), event.getChangedClass().getSimpleName(), existedRulesCount ), AttachedRecodeRuleException.class ); } else if (RecodeRuleSet.class.equals(event.getChangedClass())) { closeByCriteria(createCriteriaByDocumentIDs(RecodeRule.RECODE_RULE_SET_ID, event.getChanged(RecodeRuleSet.class))); } } /** * Класс <class>FieldInstanceToRecodeRuleMergeService</class> реализует сервис обновления правил перекодирования по изменившимся наборам перекодирования * * @author Nazin Alexander */ private class MetaFieldActualizeService implements IMergeService<RecodeRuleSet, Collection<RecodeRule>, Collection<RecodeRule>> { /** * Проверяет необходимость и выполняет обновление значения поля перекодирования * * @param ruleSet набор правил перекодирования * @param ruleSetAccessor сервис доступа к МЕТА-полю * @param rule правило перекодирования * @param ruleAccessor сервис доступа к значению поля * @return Возвращает обновленное правило перекодирования */ private RecodeRule doMerge( RecodeRuleSet ruleSet, RuleFieldAccessor<MetaFieldNamedPath, MetaField, RecodeRuleSet> ruleSetAccessor, RecodeRule rule, RuleFieldAccessor<FieldNamedPath, Field, RecodeRule> ruleAccessor) { // Получаем идентификатор МЕТА-поля String rrsMetaFieldId = ruleSetAccessor.applyRelativeId(ruleSet); // Получаем текущее значение поля перекодирования Field oldField = ruleAccessor.apply(rule); // Проверяем, что значение поля перекодирования изменилось if (!EqualsUtil.equals(rrsMetaFieldId, oldField.getMetaFieldId())) { // Получаем уникальное значение поля по идентификаторам МЕТА-поля и записи Field newField = ServiceUtils.findUniqueDocumentBy( fieldService, ImmutableMap.<String, FilterCriteriaValue<?>>of( Field.META_FIELD_ID, new FilterCriteriaValue.StringValue(rrsMetaFieldId), Field.NAME, new FilterCriteriaValue.StringValue(oldField.getName()) ), true ); // Обновляем значение поля в правиле ruleAccessor.inject(rule, newField); } // Возвращаем обновленное правило return rule; } @Override public Collection<RecodeRule> merge(RecodeRuleSet ruleSet, Collection<RecodeRule> existed) { ImmutableList.Builder<RecodeRule> result = ImmutableList.builder(); for (RecodeRule rule : existed) { RecodeRule updated = shallowClone(rule); // Актуализируем источник updated = doMerge(ruleSet, FROM_SET_INJECTOR, updated, FROM_RULE_INJECTOR); // Актуализируем назначение updated = doMerge(ruleSet, TO_SET_INJECTOR, updated, TO_RULE_INJECTOR); // Сохраняем обновленное значение result.add(updated); } // Возвращаем обновленное правило return result.build(); } } }