package ru.hflabs.rcd.service; import com.google.common.collect.*; import org.springframework.context.ApplicationEventPublisher; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import ru.hflabs.rcd.accessor.FieldAccessor; import ru.hflabs.rcd.event.modify.ChangeEvent; import ru.hflabs.rcd.exception.constraint.IllegalPrimaryKeyException; import ru.hflabs.rcd.model.*; import ru.hflabs.rcd.model.change.ChangeSet; import ru.hflabs.rcd.model.change.ChangeType; import ru.hflabs.rcd.model.change.HistoryBuilder; import ru.hflabs.rcd.model.change.Predicates; import ru.hflabs.rcd.model.criteria.FilterActivity; import ru.hflabs.rcd.model.criteria.FilterCriteria; import ru.hflabs.rcd.model.criteria.FilterCriteriaValue; import ru.hflabs.rcd.model.path.MetaFieldNamedPath; import ru.hflabs.rcd.model.rule.Rule; import ru.hflabs.util.core.ExceptionUtil; import ru.hflabs.util.core.Pair; import java.util.*; import static ru.hflabs.rcd.accessor.Accessors.linkDescendants; import static ru.hflabs.rcd.model.CriteriaUtils.createCriteriaByIDs; import static ru.hflabs.rcd.model.ModelUtils.*; /** * Класс <class>ServiceUtils</class> реализует всмомогательные методы для работы с сервисами * * @see ModelUtils */ public abstract class ServiceUtils { protected ServiceUtils() { // embedded constructor } /** * Выполняет проверку коллекции найденных документов * * @param checkedCollectionClass класс проверяемых документов * @param expectedIDs коллекция ожидаемых идентификаторов * @param foundDocuments коллекция найденных документов * @param quietly флаг безопасной проверки * @return Возвращает коллецию найденных документов * @throws IllegalPrimaryKeyException Исключение, возникающее, если коллекция найденных документов не соответствует коллекции ожидаемых идентификаторов */ public static <T extends Identifying> Collection<T> checkFoundDocuments(Class<T> checkedCollectionClass, Set<String> expectedIDs, Collection<T> foundDocuments, boolean quietly) { if (!quietly && expectedIDs.size() != foundDocuments.size()) { Set<String> notFoundIDs = Sets.difference(expectedIDs, Sets.newHashSet(Collections2.transform(foundDocuments, ID_FUNCTION))); throw new IllegalPrimaryKeyException( String.format("Can't find '%s' with IDs '%s'", checkedCollectionClass.getSimpleName(), StringUtils.collectionToCommaDelimitedString(notFoundIDs)) ); } return foundDocuments; } /** * Возвращает документ из коллекции * * @param documents коллекция документов * @param exception исключительная ситуация или <code>NULL</code>, если необходимо выполнить безопасную проверку * @return Возвращает документ * @throws E Исключительная ситуация, если размер коллекции не равен <code>1</code> */ public static <E extends RuntimeException, T> T extractSingleDocument(Collection<T> documents, Pair<String, Class<E>> exception) throws E { if (documents != null && documents.size() == 1) { return documents.iterator().next(); } else if (exception != null) { ExceptionUtil.throwException(exception.first, exception.second); } return null; } /** * Возвращает документ из коллекции * * @param documents коллекция документов * @return Возвращает документ * @throws IllegalArgumentException Исключительная ситуация, если размер коллекции не равен <code>1</code> */ public static <T> T extractSingleDocument(Collection<T> documents) throws IllegalArgumentException { return extractSingleDocument( documents, Pair.valueOf(String.format("Unexpected documents count (expected %d, got %d)", 1, documents.size()), IllegalArgumentException.class) ); } /** * Позвращает первый документ по сформированному критерию * * @param service сервис поиска документов * @param criteria критерий поиска * @param fillTransitive флаг необходимости заполнения транзитивных зависимостей * @return Возвращает найденный документ или <code>NULL</code>, если поиск вернул пустой результат */ public static <T extends Identifying> T findOneDocumentBy(IFilterService<T> service, FilterCriteria criteria, boolean fillTransitive) { return FluentIterable.<T>from( service.findByCriteria(criteria.injectCount(1), fillTransitive).getResult() ).first().orNull(); } /** * Возвращает документ, который однозначно можно определить по заданному критерию * * @param service сервис работы с документом * @param criteria критерий поиска * @param fillTransitive флаг необходимости заполнения транзитивных зависимостей * @return Возвращает найденный документ или <code>NULL</code>, если документ однозначно определить не удалось */ public static <T extends Identifying> T findUniqueDocumentBy(IFilterService<T> service, FilterCriteria criteria, boolean fillTransitive) { criteria = criteria.injectOffset(0).injectCount(2); Collection<T> result = service.findByCriteria(criteria, fillTransitive).getResult(); return (result.size() == 1) ? result.iterator().next() : null; } /** * Возвращает документ, который однозначно можно определить по коллекции фильтров * * @param service сервис работы с документом * @param filters коллекция фильтров * @param fillTransitive флаг необходимости заполнения транзитивных зависимостей * @return Возвращает найденный документ или <code>NULL</code>, если документ однозначно определить не удалось */ public static <T extends Identifying> T findUniqueDocumentBy(IFilterService<T> service, Map<String, FilterCriteriaValue<?>> filters, boolean fillTransitive) { return findUniqueDocumentBy(service, new FilterCriteria().injectFilters(filters), fillTransitive); } /** * Выполняет оповещение слушателей об изменениях * * @param eventPublisher сервис публикации событий * @param source источник события * @param changeSet набор изменений */ public static <T> void publishChangeEvent(ApplicationEventPublisher eventPublisher, Object source, ChangeSet<T> changeSet) { eventPublisher.publishEvent(new ChangeEvent(source, changeSet)); } /** * Выполняет оповещение слушателей об изменениях * * @param eventPublisher сервис публикации событий * @param source источник события * @param descriptor дескриптор изменений * @param types целевые типы событий или <code>NULL</code>, если необходимо опубликовать все типы событий */ public static <T extends Historical> void publishChangeEvent(ApplicationEventPublisher eventPublisher, Object source, HistoryBuilder<T> descriptor, ChangeType... types) { Collection<ChangeType> targetTypes = (types == null || types.length == 0) ? EnumSet.allOf(ChangeType.class) : Sets.newHashSet(types); for (ChangeType changeType : targetTypes) { if (!descriptor.getChangeSet(changeType).isEmpty()) { publishChangeEvent(eventPublisher, source, descriptor.getChangeSet(changeType)); } } } /** * Выполняет заполнение родителей для переданных потомков * * @param objects коллекция потомков * @param service сервис получения связанных родителей * @return Возвращает модифицированную коллекцию объектов */ public static <R extends ManyToOne<T>, T extends Identifying> Collection<R> injectRelations(Collection<R> objects, IFilterService<T> service) { Collection<R> notNullRelations = Collections2.filter(objects, Predicates.notNull(RELATIVE_ID_FUNCTION)); if (!CollectionUtils.isEmpty(notNullRelations)) { Map<String, Collection<R>> relativeId2objects = Multimaps.index(notNullRelations, RELATIVE_ID_FUNCTION).asMap(); Collection<T> relations = service.findAllByCriteria( createCriteriaByIDs(T.PRIMARY_KEY, relativeId2objects.keySet()).injectActivity(FilterActivity.ALL), true ); for (T relation : relations) { for (R object : relativeId2objects.get(relation.getId())) { object.setRelative(relation); } } } return objects; } /** * Выполняет наполнение потомков для переданных родителей * * @param objects коллекция родительских объектов * @param service сервис получения связанных потомков * @return Возращает заполненные связанные сущности */ public static <R extends Identifying & OneToMany<T>, T extends Identifying & ManyToOne<R>> Collection<R> injectRelative(Collection<R> objects, IManyToOneService<T> service) { List<R> result = Lists.newArrayListWithExpectedSize(objects.size()); for (R object : objects) { result.add(linkDescendants(object, service.findAllByRelativeId(object.getId(), null, false))); } return result; } public static <NP extends MetaFieldNamedPath, T extends Essence, R extends Rule<NP, T, R>> Collection<R> injectRuleRelations(Collection<R> objects, IFilterService<T> service, FieldAccessor<T, R> fromAccessor, FieldAccessor<T, R> toAccessor) { if (!CollectionUtils.isEmpty(objects)) { Map<String, Collection<R>> relativeId2FromObjects = Multimaps.index(objects, FROM_RULE_FIELD_ID).asMap(); Map<String, Collection<R>> relativeId2ToObjects = Multimaps.index(objects, TO_RULE_FIELD_ID).asMap(); Set<String> relativeIDs = ImmutableSet.<String>builder() .addAll(relativeId2FromObjects.keySet()) .addAll(relativeId2ToObjects.keySet()) .build(); Collection<T> relations = service.findAllByCriteria( createCriteriaByIDs(Identifying.PRIMARY_KEY, relativeIDs).injectActivity(FilterActivity.ALL), true ); for (T relation : relations) { Collection<R> fromRules = relativeId2FromObjects.get(relation.getId()); if (fromRules != null) { for (R rule : fromRules) { fromAccessor.inject(rule, relation); } } Collection<R> toRules = relativeId2ToObjects.get(relation.getId()); if (toRules != null) { for (R rule : toRules) { toAccessor.inject(rule, relation); } } } } return objects; } /** * Выполняет актуализацию правил перекодирования * * @param mergeService сервис актуализации * @param dependencies коллекцию обновившихся зависимостей * @param existed коллекция правил для обновления * @return Возвращает коллекцию обновленных правил */ public static <D extends Essence, NP extends MetaFieldNamedPath, T extends Essence, R extends Rule<NP, T, R>> Collection<R> updateRulesByDependencies( IMergeService<Collection<D>, Collection<R>, Collection<R>> mergeService, Collection<D> dependencies, Collection<R> existed) { return !CollectionUtils.isEmpty(existed) ? mergeService.merge(dependencies, existed) : existed; } }