package org.molgenis.data.mapper.service.impl; import com.google.common.collect.Iterables; import com.google.common.collect.Multimap; import org.apache.commons.lang3.StringUtils; import org.molgenis.data.Entity; import org.molgenis.data.EntityManager; import org.molgenis.data.mapper.algorithmgenerator.bean.GeneratedAlgorithm; import org.molgenis.data.mapper.algorithmgenerator.service.AlgorithmGeneratorService; import org.molgenis.data.mapper.mapping.model.AttributeMapping; import org.molgenis.data.mapper.mapping.model.EntityMapping; import org.molgenis.data.mapper.service.AlgorithmService; import org.molgenis.data.meta.AttributeType; import org.molgenis.data.meta.model.Attribute; import org.molgenis.data.meta.model.EntityType; import org.molgenis.data.semantic.Relation; import org.molgenis.data.semanticsearch.explain.bean.ExplainedAttribute; import org.molgenis.data.semanticsearch.service.OntologyTagService; import org.molgenis.data.semanticsearch.service.SemanticSearchService; import org.molgenis.js.magma.JsMagmaScriptEvaluator; import org.molgenis.ontology.core.model.OntologyTerm; import org.molgenis.security.core.runas.RunAsSystem; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import java.sql.Timestamp; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; import static com.google.common.collect.Sets.newLinkedHashSet; import static java.lang.Double.parseDouble; import static java.lang.Long.parseLong; import static java.lang.Math.round; import static java.lang.Math.toIntExact; import static java.lang.String.format; import static java.util.Collections.emptyList; import static java.util.Objects.requireNonNull; import static org.apache.commons.lang3.StringUtils.isEmpty; import static org.molgenis.data.DataConverter.toBoolean; public class AlgorithmServiceImpl implements AlgorithmService { private static final Logger LOG = LoggerFactory.getLogger(AlgorithmServiceImpl.class); private final OntologyTagService ontologyTagService; private final SemanticSearchService semanticSearchService; private final AlgorithmGeneratorService algorithmGeneratorService; private final JsMagmaScriptEvaluator jsMagmaScriptEvaluator; private final EntityManager entityManager; @Autowired public AlgorithmServiceImpl(OntologyTagService ontologyTagService, SemanticSearchService semanticSearchService, AlgorithmGeneratorService algorithmGeneratorService, EntityManager entityManager, JsMagmaScriptEvaluator jsMagmaScriptEvaluator) { this.ontologyTagService = requireNonNull(ontologyTagService); this.semanticSearchService = requireNonNull(semanticSearchService); this.algorithmGeneratorService = requireNonNull(algorithmGeneratorService); this.entityManager = requireNonNull(entityManager); this.jsMagmaScriptEvaluator = requireNonNull(jsMagmaScriptEvaluator); } @Override public String generateAlgorithm(Attribute targetAttribute, EntityType targetEntityType, List<Attribute> sourceAttributes, EntityType sourceEntityType) { return algorithmGeneratorService .generate(targetAttribute, sourceAttributes, targetEntityType, sourceEntityType); } @Override @RunAsSystem public void autoGenerateAlgorithm(EntityType sourceEntityType, EntityType targetEntityType, EntityMapping mapping, Attribute targetAttribute) { LOG.debug("createAttributeMappingIfOnlyOneMatch: target= " + targetAttribute.getName()); Multimap<Relation, OntologyTerm> tagsForAttribute = ontologyTagService .getTagsForAttribute(targetEntityType, targetAttribute); Map<Attribute, ExplainedAttribute> relevantAttributes = semanticSearchService .decisionTreeToFindRelevantAttributes(sourceEntityType, targetAttribute, tagsForAttribute.values(), null); GeneratedAlgorithm generatedAlgorithm = algorithmGeneratorService .generate(targetAttribute, relevantAttributes, targetEntityType, sourceEntityType); if (StringUtils.isNotBlank(generatedAlgorithm.getAlgorithm())) { AttributeMapping attributeMapping = mapping.addAttributeMapping(targetAttribute.getName()); attributeMapping.setAlgorithm(generatedAlgorithm.getAlgorithm()); attributeMapping.getSourceAttributes().addAll(generatedAlgorithm.getSourceAttributes()); attributeMapping.setAlgorithmState(generatedAlgorithm.getAlgorithmState()); LOG.debug("Creating attribute mapping: " + targetAttribute.getName() + " = " + generatedAlgorithm .getAlgorithm()); } } @Override public Iterable<AlgorithmEvaluation> applyAlgorithm(Attribute targetAttribute, String algorithm, Iterable<Entity> sourceEntities) { return Iterables.transform(sourceEntities, entity -> { AlgorithmEvaluation algorithmResult = new AlgorithmEvaluation(entity); Object derivedValue; try { Object result = derivedValue = jsMagmaScriptEvaluator.eval(algorithm, entity); derivedValue = convert(result, targetAttribute); } catch (RuntimeException e) { return algorithmResult.errorMessage(e.getMessage()); } return algorithmResult.value(derivedValue); }); } @Override public Object apply(AttributeMapping attributeMapping, Entity sourceEntity, EntityType sourceEntityType) { String algorithm = attributeMapping.getAlgorithm(); if (isEmpty(algorithm)) { return null; } Object value = jsMagmaScriptEvaluator.eval(algorithm, sourceEntity); return convert(value, attributeMapping.getTargetAttribute()); } @Override public Collection<String> getSourceAttributeNames(String algorithmScript) { Collection<String> result = emptyList(); if (!isEmpty(algorithmScript)) { result = findMatchesForPattern(algorithmScript, "\\$\\('([^\\$\\(\\)]+)'\\)"); if (result.isEmpty()) { result = findMatchesForPattern(algorithmScript, "\\$\\(([^\\$\\(\\)]+)\\)"); } } return result; } private static Collection<String> findMatchesForPattern(String algorithmScript, String patternString) { LinkedHashSet<String> result = newLinkedHashSet(); Matcher matcher = Pattern.compile(patternString).matcher(algorithmScript); while (matcher.find()) { result.add(matcher.group(1)); } return result; } @SuppressWarnings("unchecked") private Object convert(Object value, Attribute attr) { Object convertedValue; AttributeType attrType = attr.getDataType(); switch (attrType) { case BOOL: convertedValue = value != null ? toBoolean(value) : null; break; case CATEGORICAL: case XREF: case FILE: convertedValue = value != null ? entityManager .getReference(attr.getRefEntity(), convert(value, attr.getRefEntity().getIdAttribute())) : null; break; case CATEGORICAL_MREF: case MREF: case ONE_TO_MANY: Collection<Object> valueIds = (Collection<Object>) value; convertedValue = valueIds.stream().map(valueId -> entityManager .getReference(attr.getRefEntity(), convert(valueId, attr.getRefEntity().getIdAttribute()))) .collect(Collectors.toList()); break; case DATE: convertedValue = value != null ? new Date(parseLong(value.toString())) : null; break; case DATE_TIME: convertedValue = value != null ? new Timestamp(parseLong(value.toString())) : null; break; case DECIMAL: convertedValue = value != null ? parseDouble(value.toString()) : null; break; case EMAIL: case ENUM: case HTML: case HYPERLINK: case SCRIPT: case STRING: case TEXT: convertedValue = value != null ? value.toString() : null; break; case INT: convertedValue = value != null ? toIntExact(round(parseDouble(value.toString()))) : null; break; case LONG: convertedValue = value != null ? round(parseDouble(value.toString())) : null; break; case COMPOUND: throw new RuntimeException(format("Illegal attribute type [%s]", attrType.toString())); default: throw new RuntimeException(format("Unknown attribute type [%s]", attrType.toString())); } return convertedValue; } }