/* * Copyright (c) 2012 Data Harmonisation Panel * * All rights reserved. This program and the accompanying materials are made * available under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * You should have received a copy of the GNU Lesser General Public License * along with this distribution. If not, see <http://www.gnu.org/licenses/>. * * Contributors: * HUMBOLDT EU Integrated Project #030962 * Data Harmonisation Panel <http://www.dhpanel.eu> */ package eu.esdihumboldt.cst.internal; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map.Entry; import org.springframework.core.convert.ConversionException; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ListMultimap; import eu.esdihumboldt.cst.MultiValue; import eu.esdihumboldt.hale.common.align.extension.transformation.PropertyTransformationFactory; import eu.esdihumboldt.hale.common.align.model.Cell; import eu.esdihumboldt.hale.common.align.model.ChildContext; import eu.esdihumboldt.hale.common.align.model.Entity; import eu.esdihumboldt.hale.common.align.model.EntityDefinition; import eu.esdihumboldt.hale.common.align.model.Priority; import eu.esdihumboldt.hale.common.align.model.impl.PropertyEntityDefinition; import eu.esdihumboldt.hale.common.align.model.transformation.tree.SourceNode; import eu.esdihumboldt.hale.common.align.model.transformation.tree.TargetNode; import eu.esdihumboldt.hale.common.align.model.transformation.tree.TransformationTreeUtil; import eu.esdihumboldt.hale.common.align.model.transformation.tree.visitor.CellNodeValidator; import eu.esdihumboldt.hale.common.align.service.TransformationFunctionService; import eu.esdihumboldt.hale.common.align.transformation.engine.TransformationEngine; import eu.esdihumboldt.hale.common.align.transformation.function.PropertyTransformation; import eu.esdihumboldt.hale.common.align.transformation.function.PropertyValue; import eu.esdihumboldt.hale.common.align.transformation.function.impl.PropertyValueImpl; import eu.esdihumboldt.hale.common.align.transformation.report.TransformationLog; import eu.esdihumboldt.hale.common.align.transformation.report.TransformationReporter; import eu.esdihumboldt.hale.common.align.transformation.report.impl.CellLog; import eu.esdihumboldt.hale.common.align.transformation.report.impl.TransformationMessageImpl; import eu.esdihumboldt.hale.common.convert.ConversionUtil; import eu.esdihumboldt.hale.common.core.io.Value; import eu.esdihumboldt.hale.common.instance.model.Group; import eu.esdihumboldt.hale.common.instance.model.MutableInstance; import eu.esdihumboldt.hale.common.instance.model.impl.DefaultInstance; import eu.esdihumboldt.hale.common.schema.model.PropertyDefinition; import eu.esdihumboldt.hale.common.schema.model.TypeDefinition; import eu.esdihumboldt.hale.common.schema.model.constraint.type.Binding; import eu.esdihumboldt.hale.common.schema.model.constraint.type.ElementType; import eu.esdihumboldt.util.Pair; /** * Function executor on a transformation tree. * * @author Simon Templer */ public class FunctionExecutor extends CellNodeValidator { private final EngineManager engines; private final TransformationFunctionService transformations; private final TransformationContext context; private final Priority functionPriority; private final ThreadLocal<Cell> typeCell = new ThreadLocal<>(); /** * Create a function executor. * * @param reporter the transformation reporter * @param engines the transformation engine manager * @param context the transformation execution context * @param functionPriority the prioritylevel of the function */ public FunctionExecutor(TransformationReporter reporter, EngineManager engines, TransformationContext context, Priority functionPriority) { super(reporter, context.getServiceProvider()); this.engines = engines; this.context = context; this.functionPriority = functionPriority; this.transformations = context.getServiceProvider().getService( TransformationFunctionService.class); } /** * @see CellNodeValidator#processValid(Cell, ListMultimap, ListMultimap) */ @Override protected void processValid(Cell cell, ListMultimap<String, Pair<SourceNode, Entity>> sources, ListMultimap<String, Pair<TargetNode, Entity>> targets) { if (cell.getPriority() != functionPriority) { // ignore the priorities that do not match return; } /* * if the result node is only one and its value had already been set, it * is not necessary to execute this function of a lower priority. (if * there are more than one target node we will need to execute them all * and check at the end of the transformation. */ if (targets.size() == 1) { TargetNode targetNode = targets.values().iterator().next().getFirst(); if (targetNode.isDefined()) { // a result has been set already, being in lower priority we now // pass return; } } String functionId = cell.getTransformationIdentifier(); List<PropertyTransformationFactory> transformations = this.transformations .getPropertyTransformations(functionId); if (transformations == null || transformations.isEmpty()) { reporter.error(new TransformationMessageImpl(cell, MessageFormat.format( "No transformation for function {0} found. Skipping property transformation.", functionId), null)); } else { // TODO select based on e.g. preferred transformation engine? PropertyTransformationFactory transformation = transformations.iterator().next(); executeTransformation(transformation, cell, sources, targets); } } /** * Execute a property transformation. * * @param transformation the transformation factory * @param cell the alignment cell * @param sources the named source entities and nodes * @param targets the named target entities and nodes */ @SuppressWarnings({ "rawtypes", "unchecked" }) protected void executeTransformation(PropertyTransformationFactory transformation, Cell cell, ListMultimap<String, Pair<SourceNode, Entity>> sources, ListMultimap<String, Pair<TargetNode, Entity>> targets) { TransformationLog cellLog = new CellLog(reporter, cell); PropertyTransformation<?> function; try { // TODO cache function objects? function = transformation.createExtensionObject(); } catch (Exception e) { cellLog.error(cellLog.createMessage("Error creating transformation function.", e)); return; } TransformationEngine engine = engines.get(transformation.getEngineId(), cellLog); if (engine == null) { // TODO instead try another transformation cellLog.error(cellLog.createMessage( "Skipping property transformation: No matching transformation engine found", null)); return; } // configure function // set expected result ListMultimap<String, PropertyEntityDefinition> expectedResult = ArrayListMultimap.create( targets.keySet().size(), 1); for (Entry<String, Pair<TargetNode, Entity>> targetEntry : targets.entries()) { EntityDefinition def = targetEntry.getValue().getSecond().getDefinition(); expectedResult.put(targetEntry.getKey(), toPropertyEntityDefinition(def)); } function.setExpectedResult(expectedResult); // set source variables ListMultimap<String, PropertyValue> variables = ArrayListMultimap.create(); for (Entry<String, Pair<SourceNode, Entity>> sourceEntry : sources.entries()) { EntityDefinition def = sourceEntry.getValue().getSecond().getDefinition(); SourceNode sourceNode = sourceEntry.getValue().getFirst(); if (TransformationTreeUtil.isEager(cell, sourceNode, cellLog, context.getServiceProvider())) { // eager source - all values Object[] values = sourceNode.getAllValues(); if (values != null) { for (int i = 0; i < values.length; i++) { PropertyValue propertyValue = new PropertyValueImpl(values[i], toPropertyEntityDefinition(def)); variables.put(sourceEntry.getKey(), propertyValue); } } } else { // non-eager source - one value Object value = sourceNode.getValue(); PropertyValue propertyValue = new PropertyValueImpl(value, toPropertyEntityDefinition(def)); variables.put(sourceEntry.getKey(), propertyValue); } } function.setVariables(variables); // set parameters function.setParameters(cell.getTransformationParameters()); // set context function.setExecutionContext(context.getCellContext(cell)); // set target type TypeDefinition targetType = null; if (!targets.isEmpty()) { TargetNode target = targets.values().iterator().next().getFirst(); targetType = target.getEntityDefinition().getType(); } function.setTargetType(targetType); function.setTypeCell(typeCell.get()); // execute function try { ((PropertyTransformation) function).execute(transformation.getIdentifier(), engine, transformation.getExecutionParameters(), cellLog, cell); } catch (Throwable e) { // TODO instead try another transformation? cellLog.error(cellLog.createMessage( "Skipping property transformation: Executing property transformation failed.", e)); return; } // apply function results ListMultimap<String, Object> results = function.getResults(); if (results != null) { for (String name : results.keySet()) { List<Object> values = results.get(name); List<Pair<TargetNode, Entity>> nodes = targets.get(name); if (nodes.size() > values.size()) { cellLog.warn(cellLog.createMessage(MessageFormat.format( "Transformation result misses values for result with name {0}", name), null)); } if (values.size() > nodes.size()) { cellLog.warn(cellLog.createMessage( MessageFormat .format("More transformation results than target nodes for result with name {0}", name), null)); } int count = Math.min(values.size(), nodes.size()); // FIXME if multiple target nodes should be implemented ever // ith value is ignored if ith node already has a value // instead it should probably look to put ith value into i+1th // node... for (int i = 0; i < count; i++) { Object value = values.get(i); TargetNode node = nodes.get(i).getFirst(); if (value instanceof MultiValue) { MultiValue originalValue = (MultiValue) value; MultiValue processedValue = new MultiValue(originalValue.size()); for (Object o : originalValue) { processedValue.add(processValue(cellLog, function, o, node)); } value = processedValue; } else { value = processValue(cellLog, function, value, node); } /* * TODO * * set node value only if no result has already been set. If * a value is already there and we are in a lower priority * executor, we do not overwrite. */ if (!node.isDefined()) { node.setResult(value); } } } } } /** * Processes the given value. Does not handle {@link MultiValue}! * * @param cellLog the transformation log * @param function the property function * @param value the value to process * @param node the target node * @return the processed value */ private Object processValue(TransformationLog cellLog, PropertyTransformation<?> function, Object value, TargetNode node) { if (function.allowAutomatedResultConversion()) { if (!(value instanceof Group)) { // convert value for target try { value = convert(value, toPropertyEntityDefinition(node.getEntityDefinition())); } catch (Throwable e) { // ignore, but create error cellLog.error(cellLog .createMessage( "Conversion according to target property failed, using value as is.", e)); } } else { // TODO any conversion necessary/possible } } else { // unwrap value if (value instanceof Value) { value = ((Value) value).getValue(); } } /* * If the value is no group, but it should be one, create an instance * wrapping the value */ TypeDefinition propertyType = toPropertyEntityDefinition(node.getEntityDefinition()) .getDefinition().getPropertyType(); if (!(value instanceof Group) && !propertyType.getChildren().isEmpty()) { MutableInstance instance = new DefaultInstance(propertyType, null); instance.setValue(value); value = instance; } return value; } /** * Convert a value according to a target property entity definition. * * @param value the value to convert * @param propertyEntityDefinition the target property entity definition * @return the converted object * @throws ConversionException if an error occurs during conversion */ private Object convert(Object value, PropertyEntityDefinition propertyEntityDefinition) throws ConversionException { if (value == null) { return null; } PropertyDefinition def = propertyEntityDefinition.getDefinition(); Binding binding = def.getPropertyType().getConstraint(Binding.class); Class<?> target = binding.getBinding(); // special handling for Value if (value instanceof Value) { // try value's internal conversion Object result = ((Value) value).as(target); if (result != null) { return result; } else { // unwrap value value = ((Value) value).getValue(); if (value == null) { return null; } } } if (target.isAssignableFrom(value.getClass())) { return value; } if (Collection.class.isAssignableFrom(target) && target.isAssignableFrom(List.class)) { // collection / list ElementType elementType = def.getPropertyType().getConstraint(ElementType.class); return ConversionUtil.getAsList(value, elementType.getBinding(), true); } // XXX what about a value that is a collection but the target is no // collection? return ConversionUtil.getAs(value, target); } /** * Returns a {@link PropertyEntityDefinition} for a given entity definition. * * @param def the entity definition * @return the property entity definition */ private PropertyEntityDefinition toPropertyEntityDefinition(EntityDefinition def) { if (def instanceof PropertyEntityDefinition) { return (PropertyEntityDefinition) def; } return new PropertyEntityDefinition(def.getType(), new ArrayList<ChildContext>( def.getPropertyPath()), def.getSchemaSpace(), def.getFilter()); } /** * Set the current type cell. The value is stored in a {@link ThreadLocal}. * * @param typeCell the current type cell */ public void setTypeCell(Cell typeCell) { this.typeCell.set(typeCell); } }