package ru.hflabs.rcd.soap;
import com.google.common.base.Function;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import lombok.AccessLevel;
import lombok.Setter;
import org.dozer.Mapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.spi.LocationAwareLogger;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import ru.hflabs.rcd.Version;
import ru.hflabs.rcd.event.recode.RecodeFailedEvent;
import ru.hflabs.rcd.event.recode.RecodeSuccessEvent;
import ru.hflabs.rcd.exception.ApplicationException;
import ru.hflabs.rcd.exception.constraint.IllegalPrimaryKeyException;
import ru.hflabs.rcd.exception.search.document.UnknownDictionaryException;
import ru.hflabs.rcd.exception.search.document.UnknownFieldException;
import ru.hflabs.rcd.exception.search.document.UnknownGroupException;
import ru.hflabs.rcd.exception.search.rule.UnknownRecodeRuleException;
import ru.hflabs.rcd.exception.search.rule.UnknownRecodeRuleSetException;
import ru.hflabs.rcd.exception.search.rule.UnknownRuleSetNameException;
import ru.hflabs.rcd.model.criteria.FilterCriteria;
import ru.hflabs.rcd.model.criteria.FilterCriteriaValue;
import ru.hflabs.rcd.model.criteria.FilterResult;
import ru.hflabs.rcd.model.document.*;
import ru.hflabs.rcd.model.notification.NotifyType;
import ru.hflabs.rcd.model.path.DictionaryNamedPath;
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.rcd.soap.model.*;
import ru.hflabs.rcd.term.Condition;
import ru.hflabs.util.core.FormatUtil;
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import java.util.Collection;
import java.util.Map;
import static ru.hflabs.rcd.model.CriteriaUtils.createCriteriaByIDs;
import static ru.hflabs.rcd.model.CriteriaUtils.createCriteriaByRelative;
import static ru.hflabs.rcd.model.ModelUtils.*;
import static ru.hflabs.rcd.service.ServiceUtils.extractSingleDocument;
import static ru.hflabs.rcd.service.ServiceUtils.findOneDocumentBy;
import static ru.hflabs.rcd.soap.mapper.ThrowableMapper.createError;
/**
* Класс <class>RecodeWebService</class> реализует WEB сервис перекодировки справочников
*
* @author Nazin Alexander
* @see WService
*/
@Setter
@Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
public class RecodeWebService implements ApplicationEventPublisherAware, WService {
private static final Logger LOG = LoggerFactory.getLogger(RecodeWebService.class);
/** Фабрика создания SOAP классов */
private static final ObjectFactory OBJECT_FACTORY = new ObjectFactory();
/** Сервис публикации событий */
@Setter(AccessLevel.NONE)
private ApplicationEventPublisher eventPublisher;
/** Сервис преобразования сущности SOAP в API модель */
private Mapper mapper;
/** Сервис работы с группами справочников */
private IGroupService groupService;
/** Сервис работы со справочниками */
private IDictionaryService dictionaryService;
/** Сервис работы с МЕТА-полями справочника */
private IMetaFieldService metaFieldService;
/** Сервис работы со значениями полей */
private IFieldService fieldService;
/** Сервис работы с записями справочника */
private IRecordService recordService;
/** Сервис работы с наборами правил перекодирования */
private IRecodeRuleSetService recodeRuleSetService;
/** Сервис работы с правилами перекодирования */
private IRecodeRuleService recodeRuleService;
/*
* Сервисы конвертации
*/
private Function<Group, WGroup> toWGroupTransformer;
private Function<Dictionary, WDictionary> toWDictionaryTransformer;
private Function<MetaField, WMetaField> toWMetaFieldTransformer;
private Function<Record, WRecord> toWRecordTransformer;
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.eventPublisher = applicationEventPublisher;
}
@GET
@Path("/version")
@Override
public VersionResponse version() {
final VersionResponse response = OBJECT_FACTORY.createVersionResponse();
response.setVersion(Version.getVersion());
response.setRevision(Version.getRevision());
return response;
}
/**
* Определяет {@link NotifyType тип оповещения} по исключительной ситуации
*
* @param exception исключительная ситуация
* @return Возвращает тип оповещения
*/
private static NotifyType determineNotifyType(Throwable exception) {
if (exception instanceof UnknownRecodeRuleException) { // правило перекодирования не найдено
return NotifyType.NO_RULE;
} else if (exception instanceof UnknownRecodeRuleSetException) { // набор правил перекодирования не найден
return NotifyType.NO_RULE_SET;
} else if (exception instanceof UnknownRuleSetNameException) {
return NotifyType.NO_RULE_ALIAS;
} else if (exception instanceof UnknownFieldException) { // значение поля не найдено
return NotifyType.NO_VALUE;
} else if (exception instanceof UnknownDictionaryException) { // справочник не найден
return NotifyType.NO_DICTIONARY;
} else if (exception instanceof UnknownGroupException) { // группа справочников не найдена
return NotifyType.NO_GROUP;
} else { // необработанная ошибка
return NotifyType.ERROR;
}
}
/**
* Выполняет поиск целевого поля перекодирования
*
* @param ruleSet целевой набор правил
* @param fromPath исходный путь записи
* @param toPath целевой путь записи
* @return Возвращает найденное целевое поле перекодировани
*/
private Field doRecode(RecodeRuleSet ruleSet, FieldNamedPath fromPath, MetaFieldNamedPath toPath) throws Exception {
// Пытаемся определить существующее правило
RecodeRule rule = findOneDocumentBy(
recodeRuleService,
createCriteriaByRelative(RecodeRule.RECODE_RULE_SET_ID, ruleSet.getId(), RecodeRule.VALUE, fromPath.getFieldValue()),
false
);
// Определяем целевую запись
if (rule != null) { // если найдено конкретное правило перекодирования
return fieldService.findByID(rule.getToFieldId(), false, false);
} else {
// Получаем значение поля по умолчанию
Field defaultField = (ruleSet.getDefaultFieldId() != null) ?
fieldService.findByID(ruleSet.getDefaultFieldId(), false, false) :
null;
boolean isFromFieldExist = fieldService.isFieldExist(ruleSet.getFromFieldId(), fromPath.getFieldValue());
// Возвращаем результат в зависимости от состояния исходного поля и значения по умолчанию
if (isFromFieldExist && defaultField != null) {
return defaultField;
} else if (!isFromFieldExist && defaultField != null) {
eventPublisher.publishEvent(
new RecodeFailedEvent(
this,
ruleSet.getName(),
fromPath,
toPath,
NotifyType.NO_VALUE,
new UnknownFieldException(fromPath.toString())
)
);
return defaultField;
} else if (isFromFieldExist) {
throw new UnknownRecodeRuleException(fromPath, toPath);
} else {
throw new UnknownFieldException(fromPath.toString());
}
}
}
/**
* Выполняет поиск целевого значения поля
*
* @param ruleSet целевой набор правил
* @param fromValue исходное значение поля
* @return Возвращает найденное целевое значение поля
* @throws ErrorResponse Исключительная ситуация при выполнении перекодировки
*/
private RecodeResponse doRecode(RecodeRuleSet ruleSet, String fromValue) throws ErrorResponse {
RecodeResponse recodeResponse = OBJECT_FACTORY.createRecodeResponse();
final FieldNamedPath fromPath = new FieldNamedPath(ruleSet.getFromNamedPath(), fromValue);
final MetaFieldNamedPath toPath = ruleSet.getToNamedPath();
try {
Field targetField = doRecode(ruleSet, fromPath, toPath);
String result = targetField.getValue();
eventPublisher.publishEvent(new RecodeSuccessEvent(this, ruleSet.getName(), fromPath, new FieldNamedPath(toPath, result)));
recodeResponse.setValue(FormatUtil.format(result));
return recodeResponse;
} catch (Throwable th) {
NotifyType notifyType = determineNotifyType(th);
eventPublisher.publishEvent(new RecodeFailedEvent(this, ruleSet.getName(), fromPath, toPath, notifyType, th));
throw createErrorResponse(notifyType, th);
}
}
@POST
@Path("/recode")
@Override
public RecodeResponse recode(RecodeRequest parameters) throws ErrorResponse {
Assert.notNull(parameters, "Request parameters must not be NULL");
WRecodeCriteria criteria = parameters.getCriteria();
final FieldNamedPath fromPath = new FieldNamedPath(
criteria.getFromGroup(),
criteria.getFromDictionary(),
null,
criteria.getFromValue()
);
final MetaFieldNamedPath toPath = new MetaFieldNamedPath(
criteria.getToGroup(),
StringUtils.hasText(criteria.getToDictionary()) ? criteria.getToDictionary() : criteria.getFromDictionary(),
null
);
validateDictionaryNamedPath(fromPath);
validateDictionaryNamedPath(toPath);
try {
RecodeRuleSet ruleSet = recodeRuleSetService.findRecodeRuleSetByNamedPath(fromPath, toPath, false, false);
return doRecode(ruleSet, criteria.getFromValue());
} catch (ErrorResponse ex) {
throw ex;
} catch (Throwable th) {
NotifyType notifyType = determineNotifyType(th);
eventPublisher.publishEvent(new RecodeFailedEvent(this, null, fromPath, toPath, determineNotifyType(th), th));
throw createErrorResponse(notifyType, th);
}
}
@POST
@Path("/recodeByAlias")
@Override
public RecodeResponse recodeByAlias(RecodeByAliasRequest parameters) throws ErrorResponse {
Assert.notNull(parameters, "Request parameters must not be NULL");
String alias = parameters.getAlias();
Assert.isTrue(StringUtils.hasText(alias), "Rule set alias must be not empty");
try {
RecodeRuleSet ruleSet = recodeRuleSetService.findUniqueByNamedPath(alias, false);
return doRecode(ruleSet, parameters.getFromValue());
} catch (ErrorResponse ex) {
throw ex;
} catch (Throwable th) {
NotifyType notifyType = determineNotifyType(th);
eventPublisher.publishEvent(new RecodeFailedEvent(this, alias, null, null, notifyType, th));
throw createErrorResponse(notifyType, th);
}
}
@POST
@Path("/getGroups")
@Override
public SearchGroupsResponse getGroups(SearchGroupsRequest parameters) throws ErrorResponse {
Assert.notNull(parameters, "Request parameters must not be NULL");
FilterCriteria filterCriteria = createFilterCriteria(parameters.getCriteria());
try {
FilterResult<Group> filterResult = groupService.findByCriteria(filterCriteria, false);
SearchGroupsResponse response = createSearchResponse(filterResult, OBJECT_FACTORY.createSearchGroupsResponse());
response.getGroup().addAll(Collections2.transform(filterResult.getResult(), toWGroupTransformer));
return response;
} catch (ApplicationException ex) {
throw createErrorResponse(NotifyType.ERROR, ex, LocationAwareLogger.DEBUG_INT);
} catch (Throwable th) {
throw createErrorResponse(NotifyType.ERROR, th);
}
}
@GET
@Path("getDictionary/{id}")
@Override
public WDictionary getDictionary(@PathParam("id") String id) throws ErrorResponse {
Assert.isTrue(StringUtils.hasText(id), "Dictionary ID must not be NULL or EMPTY");
try {
Dictionary dictionary = dictionaryService.findByID(id, true, false);
dictionary.setDescendants(metaFieldService.findAllByRelativeId(dictionary.getId(), null, false));
return toWDictionaryTransformer.apply(dictionary);
} catch (IllegalPrimaryKeyException ex) {
throw createErrorResponse(NotifyType.NO_DICTIONARY, ex, LocationAwareLogger.DEBUG_INT);
} catch (Throwable th) {
throw createErrorResponse(NotifyType.ERROR, th);
}
}
@POST
@Path("/getDictionaries")
@Override
public SearchDictionariesResponse getDictionaries(SearchDictionariesRequest parameters) throws ErrorResponse {
Assert.notNull(parameters, "Request parameters must not be NULL");
FilterCriteria filterCriteria = createFilterCriteria(parameters.getCriteria());
try {
String search = filterCriteria.getSearch();
if (StringUtils.hasText(search)) {
Collection<String> targetGroupIDs = Collections2.transform(
groupService.findAllByCriteria(new FilterCriteria().injectSearch(search), false),
ID_FUNCTION
);
if (!CollectionUtils.isEmpty(targetGroupIDs)) {
filterCriteria = filterCriteria
.injectSearch(search, Condition.OR)
.injectFilters(
ImmutableMap.<String, FilterCriteriaValue<?>>of(
Dictionary.GROUP_ID, new FilterCriteriaValue.StringsValue(targetGroupIDs).injectCondition(Condition.OR)
)
);
}
}
FilterResult<Dictionary> filterResult = dictionaryService.findByCriteria(filterCriteria, true);
SearchDictionariesResponse response = createSearchResponse(filterResult, OBJECT_FACTORY.createSearchDictionariesResponse());
response.getDictionary().addAll(Collections2.transform(filterResult.getResult(), new Function<Dictionary, WDictionary>() {
@Override
public WDictionary apply(Dictionary input) {
input.setDescendants(metaFieldService.findAllByRelativeId(input.getId(), null, false));
return toWDictionaryTransformer.apply(input);
}
}));
return response;
} catch (ApplicationException ex) {
throw createErrorResponse(NotifyType.ERROR, ex, LocationAwareLogger.DEBUG_INT);
} catch (Throwable th) {
throw createErrorResponse(NotifyType.ERROR, th);
}
}
/**
* Выполняет поиск справочнка по его описанию
*
* @param dictionaryDefinition описание справочника
* @return Возвращает найденный справочник
*/
private Dictionary retrieveDictionary(WDictionaryDefinition dictionaryDefinition) {
if (StringUtils.hasText(dictionaryDefinition.getId())) {
return dictionaryService.findByID(dictionaryDefinition.getId(), true, false);
} else if (dictionaryDefinition.getPath() != null) {
return dictionaryService.findUniqueByNamedPath(
new DictionaryNamedPath(dictionaryDefinition.getPath().getGroupName(), dictionaryDefinition.getPath().getDictionaryName()),
false
);
} else {
throw new ApplicationException("Dictionary ID or unique path must not be NULL");
}
}
@POST
@Path("/getRecords")
@Override
public SearchRecordsResponse getRecords(SearchRecordsRequest parameters) throws ErrorResponse {
Assert.notNull(parameters, "Request parameters must not be NULL");
WDictionaryDefinition dictionaryDefinition = parameters.getDictionary();
Assert.notNull(dictionaryDefinition, "Dictionary definition must not be NULL");
FilterCriteria filterCriteria = createFilterCriteria(parameters.getCriteria());
try {
Dictionary dictionary = retrieveDictionary(dictionaryDefinition);
FilterResult<Record> filterResult = recordService.findRecordsByCriteria(dictionary.getId(), filterCriteria, false);
SearchRecordsResponse response = createSearchResponse(filterResult, OBJECT_FACTORY.createSearchRecordsResponse());
response.getRecord().addAll(Collections2.transform(filterResult.getResult(), toWRecordTransformer));
return response;
} catch (ApplicationException ex) {
throw createErrorResponse(NotifyType.ERROR, ex, LocationAwareLogger.DEBUG_INT);
} catch (Throwable th) {
throw createErrorResponse(NotifyType.ERROR, th);
}
}
/**
* Формирует и возвращает коллекцию соответствий идентификатора поля перекодирования к записи справочника
*
* @param dictionaryId идентификатор справочника
* @param metaFieldName название МЕТА-поля перекодирования
* @param rules коллекция целевых правил
* @param fieldFunction функция доступа к идентификатору поля перекодирования
* @return Возвращает коллекцию составленных соответствий
*/
private Map<String, Record> createRule2Records(String dictionaryId, final String metaFieldName, Collection<RecodeRule> rules, Function<Rule<?, ?, ?>, String> fieldFunction) {
Collection<Record> records = recordService.findRecordsByCriteria(
dictionaryId,
createCriteriaByIDs(Field.PRIMARY_KEY, Collections2.transform(rules, fieldFunction)),
false
).getResult();
return Maps.uniqueIndex(records, new Function<Record, String>() {
@Override
public String apply(Record input) {
Field field = input.retrieveFieldByName(metaFieldName);
Assert.notNull(field, String.format("Field with name '%s' not exist in record with ID '%s'", metaFieldName, input.getId()));
return field.getId();
}
});
}
/**
* Выполняет поиск набора правил перекодирования по его описанию
*
* @param ruleDefinition описание набора
* @return Возвращает найденный набор правил перекодирования
*/
private RecodeRuleSet retrieveRecodeRuleSet(WRuleDefinition ruleDefinition) {
if (StringUtils.hasText(ruleDefinition.getAlias())) {
return recodeRuleSetService.findUniqueByNamedPath(ruleDefinition.getAlias(), false);
} else if (ruleDefinition.getPath() != null) {
WRulePath rulePath = ruleDefinition.getPath();
final Dictionary fromDictionary = retrieveDictionary(rulePath.getFromDictionary());
final Dictionary toDictionary = retrieveDictionary(rulePath.getToDictionary());
return recodeRuleSetService.findRecodeRuleSetByNamedPath(
new MetaFieldNamedPath(createDictionaryNamedPath(fromDictionary), null),
new MetaFieldNamedPath(createDictionaryNamedPath(toDictionary), null),
true,
false
);
} else {
throw new ApplicationException("Rule set alias or unique path must not be NULL");
}
}
@POST
@Path("/getRules")
@Override
public SearchRulesResponse getRules(SearchRulesRequest parameters) throws ErrorResponse {
Assert.notNull(parameters, "Request parameters must not be NULL");
WRuleDefinition ruleDefinition = parameters.getRuleSet();
Assert.notNull(ruleDefinition, "Rule definition must not be NULL");
FilterCriteria filterCriteria = createFilterCriteria(parameters.getCriteria());
try {
final RecodeRuleSet ruleSet = retrieveRecodeRuleSet(ruleDefinition);
// Устанавливаем название полей перекодировки
SearchRulesResponse response = OBJECT_FACTORY.createSearchRulesResponse();
{
response.setAlias(ruleSet.getName());
response.setFromFieldName(ruleSet.getFromFieldName());
response.setToFieldName(ruleSet.getToFieldName());
}
// Выполняем поиск записи по умолчанию
if (StringUtils.hasText(ruleSet.getDefaultFieldId())) {
Record defaultRecord = extractSingleDocument(
recordService.findRecordsByCriteria(
ruleSet.getToDictionaryId(),
createCriteriaByIDs(Field.PRIMARY_KEY, ruleSet.getDefaultFieldId()),
false
).getResult()
);
response.setDefaultRecord(toWRecordTransformer.apply(defaultRecord));
}
// Выполняем поиск правил перекодирования по найденному набору
FilterCriteria rulesFilterCriteria = createCriteriaByIDs(RecodeRule.RECODE_RULE_SET_ID, ruleSet.getId())
.injectOffset(filterCriteria.getOffset())
.injectCount(filterCriteria.getCount());
FilterResult<RecodeRule> filterResult = recodeRuleService.findByCriteria(rulesFilterCriteria, false);
response.setFilterCount(filterResult.getCountByFilter());
response.setTotalCount(filterResult.getCountByFilter());
// Получаем записи источника и назначения
final Map<String, Record> fromRecords = createRule2Records(
ruleSet.getFromDictionaryId(), ruleSet.getFromFieldName(), filterResult.getResult(), FROM_RULE_FIELD_ID
);
final Map<String, Record> toRecords = createRule2Records(
ruleSet.getToDictionaryId(), ruleSet.getToFieldName(), filterResult.getResult(), TO_RULE_FIELD_ID
);
// Выполняем заполнение правил
response.getRule().addAll(Collections2.transform(filterResult.getResult(), new Function<RecodeRule, WRule>() {
@Override
public WRule apply(RecodeRule input) {
WRule result = new WRule();
result.setId(input.getId());
result.setFromRecord(toWRecordTransformer.apply(fromRecords.get(input.getFromFieldId())));
result.setToRecord(toWRecordTransformer.apply(toRecords.get(input.getToFieldId())));
return result;
}
}));
return response;
} catch (ApplicationException ex) {
throw createErrorResponse(NotifyType.ERROR, ex, LocationAwareLogger.DEBUG_INT);
} catch (Throwable th) {
throw createErrorResponse(NotifyType.ERROR, th);
}
}
/**
* Выполняет конвертацию результатов поиска
*
* @param filterResult найденная коллекцию объектов
* @param response целевой класс ответа
* @return Возвращает сформированный ответ
*/
private static <R extends WSearchResponse, T> R createSearchResponse(FilterResult<T> filterResult, R response) {
response.setFilterCount(filterResult.getCountByFilter());
response.setTotalCount(filterResult.getTotalCount());
return response;
}
/**
* Выполняет конвертацию критерия поиска
*
* @param wSearchCriteria оригинальный критерий
* @return Возвращает сформированный критерий поиска
*/
private FilterCriteria createFilterCriteria(WSearchCriteria wSearchCriteria) {
return (wSearchCriteria != null) ?
mapper.map(wSearchCriteria, FilterCriteria.class) :
new FilterCriteria().injectCount(FilterCriteria.COUNT_DEFAULT);
}
/**
* Формирует ответное сообщение об ошибке выполнения запроса
*
* @param notifyType тип ошибки
* @param cause исключительная ситуация
* @return Возвращает сообщение об ошибке выполнения запроса
*/
private static ErrorResponse createErrorResponse(NotifyType notifyType, Throwable cause) {
switch (notifyType) {
case ERROR: {
return createErrorResponse(notifyType, cause, LocationAwareLogger.ERROR_INT);
}
default: {
return createErrorResponse(notifyType, cause, LocationAwareLogger.DEBUG_INT);
}
}
}
/**
* Формирует ответное сообщение об ошибке выполнения запроса
*
* @param notifyType тип ошибки
* @param cause исключительная ситуация
* @param logLevel уровень логирования ошибки
* @return Возвращает сообщение об ошибке выполнения запроса
*/
private static ErrorResponse createErrorResponse(NotifyType notifyType, Throwable cause, int logLevel) {
String message = cause.getMessage();
if (LocationAwareLogger.class.isAssignableFrom(LOG.getClass())) {
((LocationAwareLogger) LOG).log(null, LOG.getClass().getName(), logLevel, message, null, cause);
} else {
LOG.error(message, cause);
}
return new ErrorResponse(message, createError(notifyType, cause), cause);
}
}