package ru.hflabs.rcd.service; import com.google.common.base.Function; import com.google.common.base.Predicate; import com.google.common.collect.*; import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; 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.Accessors; import ru.hflabs.rcd.event.ContextEvent; import ru.hflabs.rcd.exception.constraint.ConstraintException; import ru.hflabs.rcd.exception.constraint.DuplicateNameException; import ru.hflabs.rcd.exception.constraint.IllegalPermissionsException; import ru.hflabs.rcd.exception.constraint.document.IllegalMetaFieldException; import ru.hflabs.rcd.exception.constraint.rule.IllegalRecodeRuleException; import ru.hflabs.rcd.exception.search.document.UnknownDictionaryException; import ru.hflabs.rcd.model.Essence; import ru.hflabs.rcd.model.Historical; import ru.hflabs.rcd.model.Named; import ru.hflabs.rcd.model.change.ChangeType; import ru.hflabs.rcd.model.change.HistoryBuilder; import ru.hflabs.rcd.model.criteria.FilterCriteria; import ru.hflabs.rcd.model.criteria.FilterCriteriaValue; import ru.hflabs.rcd.model.document.*; import ru.hflabs.rcd.model.document.Dictionary; import ru.hflabs.rcd.model.path.DictionaryNamedPath; import ru.hflabs.rcd.model.path.DirectionNamedPath; 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.model.rule.Rule; import ru.hflabs.rcd.service.document.*; import ru.hflabs.rcd.service.rule.IRecodeRuleService; import ru.hflabs.rcd.service.rule.IRecodeRuleSetService; import ru.hflabs.util.core.EqualsUtil; import ru.hflabs.util.core.MD5; import ru.hflabs.util.spring.Assert; import java.util.*; import static ru.hflabs.rcd.accessor.Accessors.injectName; import static ru.hflabs.rcd.accessor.Accessors.linkRelative; import static ru.hflabs.rcd.model.CriteriaUtils.createCriteriaByIDs; import static ru.hflabs.rcd.model.ModelUtils.*; import static ru.hflabs.rcd.service.ServiceUtils.injectRelative; import static ru.hflabs.rcd.service.ServiceUtils.publishChangeEvent; /** * Класс <class>DocumentManagerService</class> реализует сервис управления справочниками * * @author Nazin Alexander */ public class DocumentManagerService implements IManagerService, ApplicationEventPublisherAware { /** Функция расчета уникального идентификатора для правила */ private static final Function<Rule<?, ?, ?>, String> RULE_NAME_FUNCTION = new Function<Rule<?, ?, ?>, String>() { @Override public String apply(Rule<?, ?, ?> input) { return MD5.asHex(input.getFromFieldId(), input.getToFieldId()); } }; /** Сервис копирования прав доступа для групп */ private static final IMergeService.Single<Group> MERGE_SERVICE_GROUP = MergeServices.chain( MergeServices.<Group>copyId(), MergeServices.<Group>copyPermission() ); /** Сервис копирования флагов МЕТА-поля */ private static final IMergeService.Single<MetaField> MERGE_SERVICE_META_FIELD = MergeServices.chain( MergeServices.<MetaField>copyId(), MergeServices.<MetaField>copyName(), new MergeServices.MetaFieldFlagsMergeService() ); /** Сервис публикации событий */ private ApplicationEventPublisher eventPublisher; /** Сервис работы с историей документов */ private IHistoryService historyService; /** Сервис управления группами справочников */ private IGroupService groupService; /** Сервис управления справочниками */ private IDictionaryService dictionaryService; /** Сервис управления МЕТА-полями справочников */ private IMetaFieldService metaFieldService; /** Сервис управления значениями полей справочников */ private IFieldService fieldService; /** Сервис работы с записями справочника */ private IRecordService recordService; /** Сервис управления наборами правил перекодирования */ private IRecodeRuleSetService recodeRuleSetService; /** Сервис управления правилами перекодирования */ private IRecodeRuleService rulesService; @Override public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { this.eventPublisher = applicationEventPublisher; } public void setHistoryService(IHistoryService historyService) { this.historyService = historyService; } public void setGroupService(IGroupService groupService) { this.groupService = groupService; } public void setDictionaryService(IDictionaryService dictionaryService) { this.dictionaryService = dictionaryService; } public void setMetaFieldService(IMetaFieldService metaFieldService) { this.metaFieldService = metaFieldService; } public void setFieldService(IFieldService fieldService) { this.fieldService = fieldService; } public void setRecordService(IRecordService recordService) { this.recordService = recordService; } public void setRecodeRuleSetService(IRecodeRuleSetService recodeRuleSetService) { this.recodeRuleSetService = recodeRuleSetService; } public void setRulesService(IRecodeRuleService rulesService) { this.rulesService = rulesService; } /** * Выполняет оповещение слушателей об изменениях * * @param descriptor дескриптор изменений * @param types целевые типы событий */ private <T extends Essence & Historical> void doPublishEvent(HistoryBuilder<T> descriptor, ChangeType... types) { publishChangeEvent(eventPublisher, this, descriptor, types); } /** * Выполняет построение дескриптора изменений именованной сущности * * @param service сервис поиска сущностей * @param filterCriteriaValues критерии поиска существующих сущностей * @param values коллекция модификации * @param mergeService сервис слияния новой и старой сущности * @return Возвращает построенный дескриптор */ private <T extends Essence & Named & Historical> HistoryBuilder<T> buildNamedChangeSet(IFilterService<T> service, Map<String, FilterCriteriaValue<?>> filterCriteriaValues, Collection<T> values, IMergeService.Single<T> mergeService) { // Выполняем формирование критерия поиска final FilterCriteria filterCriteria = new FilterCriteria().injectFilters(filterCriteriaValues); // Получаем существующие сущности final Map<String, T> existedEssences = Maps.newHashMap(Maps.uniqueIndex(service.findAllByCriteria(filterCriteria, false), LOWER_CASE_NAME_FUNCTION)); // Выполняем построение дескриптора изменений return historyService.createChangeSet( service.retrieveTargetClass(), existedEssences, Maps.uniqueIndex(values, LOWER_CASE_NAME_FUNCTION), mergeService ); } @Override @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Throwable.class) public Collection<Group> storeGroups(Collection<Group> groups) { Assert.isTrue(!CollectionUtils.isEmpty(groups), "Groups must not be NULL or EMPTY"); // Выполняем построения дескриптора изменений групп HistoryBuilder<Group> groupsDescriptor = buildNamedChangeSet( groupService, ImmutableMap.<String, FilterCriteriaValue<?>>of( Group.NAME, new FilterCriteriaValue.StringsValue(Collections2.transform(groups, NAME_FUNCTION)) ), groups, MERGE_SERVICE_GROUP ); // Для модифицированных групп проверяем права на запись for (Group group : groupsDescriptor.getEssences(ChangeType.ACTUAL_SET)) { if (!hasPermission(group, Group.PERMISSION_WRITE)) { throw new IllegalPermissionsException.IllegalWritePermissionsException( String.format("%s with name '%s' does not have write permission", Group.class.getSimpleName(), group.getName()) ); } } // Выполняем создание и обновление групп doPublishEvent(groupsDescriptor, ChangeType.CREATE, ChangeType.UPDATE); // Для каждой актуальной группы выполняем модификацию ее справочников Collection<Group> storedGroups = groupsDescriptor.getEssences(ChangeType.ACTUAL_SET); for (Group group : storedGroups) { if (!CollectionUtils.isEmpty(group.getDescendants())) { HistoryBuilder<Dictionary> dictionariesDescriptor = storeDictionaries(group, group.getDescendants()); group.setDescendants(dictionariesDescriptor.getEssences(ChangeType.ACTUAL_SET)); } } // Возвращаем модифицированные группы return storedGroups; } /** * Выполняет создание и изменение коллекции справочников * * @param group группа, к которой относятся справочники * @param dictionaries модифицируемая коллекция справочников * @return Возвращает модифицированные справочники */ private HistoryBuilder<Dictionary> storeDictionaries(final Group group, Collection<Dictionary> dictionaries) { // Выполняем построение дескриптора изменения справочников HistoryBuilder<Dictionary> dictionariesDescriptor = buildNamedChangeSet( dictionaryService, ImmutableMap.<String, FilterCriteriaValue<?>>of( Dictionary.GROUP_ID, new FilterCriteriaValue.StringValue(group.getId()), Dictionary.NAME, new FilterCriteriaValue.StringsValue(Collections2.transform(dictionaries, NAME_FUNCTION)) ), Collections2.transform(dictionaries, new Function<Dictionary, Dictionary>() { @Override public Dictionary apply(Dictionary input) { return linkRelative(group, input); } }), MergeServices.chain( MergeServices.<Dictionary>copyId(), MergeServices.<Dictionary>copyName() ) ); // Выполняем создание и обновление справочников doPublishEvent(dictionariesDescriptor, ChangeType.CREATE, ChangeType.UPDATE); // Для каждого актуального справочника выполняем модификацию структуры и контента for (Dictionary dictionary : dictionariesDescriptor.getEssences(ChangeType.ACTUAL_SET)) { if (!CollectionUtils.isEmpty(dictionary.getDescendants())) { HistoryBuilder<MetaField> metaFieldsDescriptor = buildMetaFieldChangeSet(dictionary, dictionary.getDescendants()); doPublishEvent(metaFieldsDescriptor); Map<String, MetaField> existedMetaFields = Maps.uniqueIndex(metaFieldsDescriptor.getEssences(ChangeType.ACTUAL_SET), LOWER_CASE_NAME_FUNCTION); String primaryMetaFieldName = LOWER_CASE_NAME_FUNCTION.apply(retrievePrimaryMetaField(existedMetaFields.values())); dictionary.setDescendants(existedMetaFields.values()); if (!CollectionUtils.isEmpty(dictionary.getRecords())) { // Выполняем преобразования название МЕТА-полей записи в нижний регистр ImmutableList.Builder<Record> records = ImmutableList.builder(); for (Record record : dictionary.getRecords()) { Map<String, Field> fields = Maps.newLinkedHashMap(); for (Map.Entry<String, Field> entry : record.getFields().entrySet()) { Assert.isNull( fields.put(LOWER_CASE_FUNCTION.apply(entry.getKey()), entry.getValue()), String.format("Duplicate meta field name '%s'", entry.getKey()), DuplicateNameException.class ); } records.add(record.injectFields(fields)); } HistoryBuilder<Field> fieldsDescriptor = buildFieldChangeSets(records.build(), existedMetaFields, primaryMetaFieldName); doPublishEvent(fieldsDescriptor); dictionary.setRecords(createRecords(dictionary.getId(), existedMetaFields.values(), fieldsDescriptor.getEssences(ChangeType.ACTUAL_SET))); } } } // Возвращаем сформированный дескриптор return dictionariesDescriptor; } /** * Выполняет создание и изменение МЕТА-полей справочника * * @param dictionary справочник, к которому относятся поля * @param metaFields модифицируемые значения МЕТА-полей * @return Возвращает модифицированные значения полей */ private HistoryBuilder<MetaField> buildMetaFieldChangeSet(final Dictionary dictionary, Collection<MetaField> metaFields) { // Проверяем, что только одно МЕТА-поле является основным Assert.notNull( retrievePrimaryMetaField(metaFields), String.format("Dictionary '%s' must have one primary field", dictionary.getName()), IllegalMetaFieldException.class ); // Выполняем построение дескриптора изменений МЕТА-полей return buildNamedChangeSet( metaFieldService, ImmutableMap.<String, FilterCriteriaValue<?>>of(MetaField.DICTIONARY_ID, new FilterCriteriaValue.StringValue(dictionary.getId())), Collections2.transform(dictionary.getDescendants(), new Function<MetaField, MetaField>() { @Override public MetaField apply(MetaField input) { return linkRelative(dictionary, input); } }), MERGE_SERVICE_META_FIELD ); } /** * Выполняет создание и изменение значений полей справочника * * @param records модифицируемые значения полей * @param metaFields коллекция МЕТА-полей * @param primaryMetaFieldName название первичного МЕТА-поля * @return Возвращает дескриптор изменений полей */ private HistoryBuilder<Field> buildFieldChangeSets(Collection<Record> records, Map<String, MetaField> metaFields, String primaryMetaFieldName) { // Коллекция соответствий МЕТА-поля к значениям полей final Map<MetaField, Collection<Field>> currentMetaField2Fields = Maps.newLinkedHashMap(); // Выполняем валидацию каждого записей справочника for (Map.Entry<String, MetaField> entry : metaFields.entrySet()) { MetaField metaField = entry.getValue(); final Collection<String> uniqueFieldValidator = metaField.isFlagEstablished(MetaField.FLAG_UNIQUE) ? new HashSet<String>() : new ArrayList<String>(); final Collection<Field> fields = Lists.newArrayList(); for (Record record : records) { // Получаем первичный ключ записи final Field primaryKey = record.retrieveFieldByName(primaryMetaFieldName); Assert.notNull(primaryKey, String.format("Record missed primary value for field with name '%s'", primaryMetaFieldName), IllegalMetaFieldException.class); // Получаем поле записи Field field = record.retrieveFieldByName(entry.getKey()); Assert.notNull(field, String.format("Record missed value for field with name '%s'", metaField.getName()), IllegalMetaFieldException.class); // Устанавливам системные поля, которые идентифицируют запись { field = linkRelative(metaField, field); field = injectName(field, createRecordId(primaryKey)); } // Проверяем, что поле не дублируется Assert.isTrue( uniqueFieldValidator.add(LOWER_CASE_FUNCTION.apply(field.getValue())), String.format("Duplicate value '%s' for field with name '%s'", field.getValue(), metaField.getName()), ConstraintException.class ); fields.add(field); } currentMetaField2Fields.put(metaField, fields); } // Выполняем формирование дескрипторов изменений полей final HistoryBuilder<Field> fieldsDescriptor = new HistoryBuilder<Field>(Field.class); for (Map.Entry<MetaField, Collection<Field>> entry : currentMetaField2Fields.entrySet()) { HistoryBuilder<Field> descriptor = buildNamedChangeSet( fieldService, ImmutableMap.<String, FilterCriteriaValue<?>>of(Field.META_FIELD_ID, new FilterCriteriaValue.StringValue(entry.getKey().getId())), entry.getValue(), MergeServices.<Field>copyId() ); fieldsDescriptor.addChangeSets(descriptor.getChangeSets()); } // Возвращаем сформированный дескриптор return fieldsDescriptor; } /** * Выполняет поиск целевых справочников для экспорта * * @param path именованный путь справочника или <code>NULL</code> * @return Возвращает целевые справочники */ private Collection<Dictionary> dumpDictionaries(DictionaryNamedPath path) { // Выполняем поиск целевого справочника if (path != null && StringUtils.hasText(path.getGroupName()) && StringUtils.hasText(path.getDictionaryName())) { return Arrays.asList(dictionaryService.findUniqueByNamedPath(path, false)); } // Выполняем поиск всех справочников для указанной группы if (path != null && StringUtils.hasText(path.getGroupName())) { Group group = groupService.findUniqueByNamedPath(path.getGroupName(), false); return dictionaryService.findAllByRelativeId(group.getId(), null, true); } // Выполняем поиск всех справочников по указанному имени if (path != null && StringUtils.hasText(path.getDictionaryName())) { Collection<Dictionary> dictionaries = dictionaryService.findAllByCriteria(createCriteriaByIDs(Dictionary.NAME, path.getDictionaryName()), true); if (!CollectionUtils.isEmpty(dictionaries)) { return dictionaries; } throw new UnknownDictionaryException(path.getDictionaryName()); } // Возвращаем все справочники return dictionaryService.findAllByCriteria(new FilterCriteria(), true); } @Override public Collection<Group> dumpGroups(DictionaryNamedPath path) { // Выполняем заполнение контента справочников Collection<Dictionary> dictionaries = dumpDictionaries(path); for (Dictionary dictionary : dictionaries) { Collection<MetaField> metaFields = injectRelative( metaFieldService.findAllByRelativeId(dictionary.getId(), null, false), fieldService ); dictionary.setDescendants(metaFields); } // Формируем карту соответствий группы к справочникам Map<Group, Collection<Dictionary>> group2dictionaries = Multimaps.index(dictionaries, Accessors.GROUP_TO_DICTIONARY_INJECTOR).asMap(); for (Map.Entry<Group, Collection<Dictionary>> entry : group2dictionaries.entrySet()) { entry.getKey().setDescendants(entry.getValue()); } return Sets.newLinkedHashSet(group2dictionaries.keySet()); } /** * Выполняет поиск подходящего контекста для именнованного пути значения * * @param contexts коллекция существующих контекстов * @return Возвращает найденный контекст */ private <NP, I> Context<NP, I> findExistedContext(String prefix, final NP namedPath, Collection<Context<NP, I>> contexts) { // Получаем кандидатов Collection<Context<NP, I>> candidates = Sets.newHashSet(Collections2.filter(contexts, new Predicate<Context<NP, I>>() { @Override public boolean apply(Context<NP, I> input) { return EqualsUtil.equals(input.getNamedPath(), namedPath); } })); if (candidates.size() == 1) { // подходящий контекст найден final Context<NP, I> existedContext = candidates.iterator().next(); // Проверяем, что МЕТА-поле является уникальным Assert.isTrue( existedContext.getDocumentContext().metaField.isFlagEstablished(MetaField.FLAG_UNIQUE), String.format("%s meta field '%s' is not unique", prefix, existedContext.getDocumentContext().metaField.getName()), IllegalRecodeRuleException.class ); return existedContext; } else if (candidates.isEmpty()) { // кандидаты не найдены throw new IllegalRecodeRuleException( String.format("%s value not found (%s)", prefix, namedPath) ); } else { // найдено несколько кандидатов throw new IllegalRecodeRuleException( String.format("Too many %s values found (%s). Expected %d, but got %d", prefix, namedPath, 1, candidates.size()) ); } } /** * Выполняет валидацию правила * * @param rule целевое правило * @param contexts коллекция контектов * @return Возвращает модифицированное правило */ private <NP extends MetaFieldNamedPath, T extends Essence, R extends Rule<NP, T, R>> R validateRuleByContext(R rule, Collection<Context<NP, T>> contexts) { Context<NP, T> fromContext = findExistedContext("source", rule.getFromNamedPath(), contexts); rule.injectFrom(fromContext.getEssence()); Context<NP, T> toContext = findExistedContext("destination", rule.getToNamedPath(), contexts); rule.injectTo(toContext.getEssence()); Assert.isTrue( !EqualsUtil.equals(rule.getFromFieldId(), rule.getToFieldId()), String.format("Mapping '%s' to itself is not allowed", rule.getFromNamedPath()), IllegalRecodeRuleException.class ); return rule; } /** * Выполняет построение дескриптора изменений для правил перекодирования * * @param recodeRules правила перекодирования * @param contexts контексты значений полей * @return Возвращает дескриптор изменений */ public HistoryBuilder<RecodeRule> buildRecodeRuleChangeSet(final RecodeRuleSet ruleSet, Collection<RecodeRule> recodeRules, Collection<DocumentContext> contexts) { Collection<Context<FieldNamedPath, Field>> fieldContexts = Lists.newArrayList(Collections2.transform(contexts, Contexts.FIELD_CONTEXT)); // Выполняем валидацию правил Map<FieldNamedPath, RecodeRule> validated = Maps.newLinkedHashMap(); for (RecodeRule rule : recodeRules) { rule = validateRuleByContext(rule, fieldContexts); if (validated.put(rule.getFromNamedPath(), rule) != null) { throw new IllegalRecodeRuleException(String.format("Duplicate source value for rule '%s'", rule)); } } // Выполняем построение дескриптора изменений Collection<RecodeRule> existedRules = rulesService.findAllByRelativeId(ruleSet.getId(), null, false); Map<String, RecodeRule> existedNamedRules = Maps.newHashMap(Maps.uniqueIndex(existedRules, RULE_NAME_FUNCTION)); return historyService.createChangeSet( RecodeRule.class, existedNamedRules, Maps.uniqueIndex(validated.values(), RULE_NAME_FUNCTION), MergeServices.<RecodeRule>copyId() ); } /** * Выполняет построение дескриптора изменений для наборов правил перекодирования * * @param recodeRuleSets наборы правил перекодирования * @param contexts контексты значений полей * @return Возвращает дескриптор изменений */ public HistoryBuilder<RecodeRuleSet> buildRecodeRuleSetChangeSet(Collection<RecodeRuleSet> recodeRuleSets, Collection<DocumentContext> contexts) { Collection<Context<MetaFieldNamedPath, MetaField>> metaFieldContexts = Lists.newArrayList(Collections2.transform(contexts, Contexts.META_FIELD_CONTEXT)); // Выполняем валидацию наборов правил Collection<RecodeRuleSet> validated = Lists.newLinkedList(); for (RecodeRuleSet ruleSet : recodeRuleSets) { validated.add(validateRuleByContext(ruleSet, metaFieldContexts)); if (ruleSet.getDefaultPath() != null) { Context<FieldNamedPath, Field> defaultFieldContext = findExistedContext("default", ruleSet.getDefaultPath(), Collections2.transform(contexts, Contexts.FIELD_CONTEXT)); ruleSet.setDefaultFieldId(defaultFieldContext.getEssence().getId()); } } // Выполняем построение дескриптора изменений Collection<DirectionNamedPath<MetaFieldNamedPath>> ruleSetPaths = Collections2.transform(validated, new Function<RecodeRuleSet, DirectionNamedPath<MetaFieldNamedPath>>() { @Override public DirectionNamedPath<MetaFieldNamedPath> apply(RecodeRuleSet input) { return createRulePath(input); } }); Map<String, RecodeRuleSet> existedRuleSets = Maps.newHashMap(Maps.uniqueIndex(recodeRuleSetService.findRecodeRuleSetByNamedPath(ruleSetPaths, false), RULE_NAME_FUNCTION)); return historyService.createChangeSet( RecodeRuleSet.class, existedRuleSets, Maps.uniqueIndex(validated, RULE_NAME_FUNCTION), MergeServices.chain( MergeServices.<RecodeRuleSet>copyId(), MergeServices.<RecodeRuleSet>copyName() ) ); } @Override @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Throwable.class) public Collection<RecodeRuleSet> storeRecodeRuleSets(Collection<RecodeRuleSet> recodeRuleSets) { Assert.isTrue(!CollectionUtils.isEmpty(recodeRuleSets), "Recode rule sets must not be NULL or EMPTY"); // Получаем коллекцию существующих контекстов Collection<DocumentContext> documentContexts = fieldService.findDocumentContexts(createNamedPath(recodeRuleSets)); // Выполняем построение дескрипторов изменений HistoryBuilder<RecodeRuleSet> ruleSetDescriptor = buildRecodeRuleSetChangeSet(recodeRuleSets, documentContexts); HistoryBuilder<RecodeRule> ruleDescriptor = new HistoryBuilder<RecodeRule>(RecodeRule.class); for (final RecodeRuleSet ruleSet : ruleSetDescriptor.getEssences(ChangeType.ACTUAL_SET)) { if (!CollectionUtils.isEmpty(ruleSet.getRecodeRules())) { HistoryBuilder<RecodeRule> changeDescriptor = buildRecodeRuleChangeSet( ruleSet, Collections2.transform(ruleSet.getRecodeRules(), new Function<RecodeRule, RecodeRule>() { @Override public RecodeRule apply(RecodeRule input) { return input .injectRecodeRuleSet(ruleSet) .injectFromNamedPath(new FieldNamedPath(ruleSet.getFromNamedPath(), input.getFromNamedPath().getFieldValue())) .injectToNamedPath(new FieldNamedPath(ruleSet.getToNamedPath(), input.getToNamedPath().getFieldValue())); } }), documentContexts ); ruleDescriptor.addChangeSets(changeDescriptor.getChangeSets()); ruleSet.setRecodeRules(changeDescriptor.getEssences(ChangeType.ACTUAL_SET)); } } // Модифицируем правила doPublishEvent(ruleSetDescriptor, ChangeType.CREATE, ChangeType.UPDATE); doPublishEvent(ruleDescriptor); // Возвращаем модифицированные наборы правил return recodeRuleSets; } @Override public <T extends ApplicationEvent> void propagateEvent(T event) { if (event instanceof ContextEvent) { ((ContextEvent) event).overrideSource(this); eventPublisher.publishEvent(event); } } }