/*
* 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.functions.groovy;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.google.common.base.Joiner;
import com.google.common.collect.ListMultimap;
import eu.esdihumboldt.cst.functions.groovy.internal.GroovyUtil;
import eu.esdihumboldt.cst.functions.groovy.internal.TargetCollector;
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.impl.PropertyEntityDefinition;
import eu.esdihumboldt.hale.common.align.transformation.engine.TransformationEngine;
import eu.esdihumboldt.hale.common.align.transformation.function.ExecutionContext;
import eu.esdihumboldt.hale.common.align.transformation.function.PropertyValue;
import eu.esdihumboldt.hale.common.align.transformation.function.TransformationException;
import eu.esdihumboldt.hale.common.align.transformation.function.impl.AbstractSingleTargetPropertyTransformation;
import eu.esdihumboldt.hale.common.align.transformation.function.impl.NoResultException;
import eu.esdihumboldt.hale.common.align.transformation.function.impl.PropertyValueImpl;
import eu.esdihumboldt.hale.common.align.transformation.report.TransformationLog;
import eu.esdihumboldt.hale.common.core.io.Value;
import eu.esdihumboldt.hale.common.instance.groovy.InstanceBuilder;
import eu.esdihumboldt.hale.common.instance.model.Instance;
import eu.esdihumboldt.hale.common.instance.model.MutableInstance;
import eu.esdihumboldt.hale.common.schema.model.TypeDefinition;
import eu.esdihumboldt.util.groovy.sandbox.GroovyService;
import eu.esdihumboldt.util.groovy.sandbox.GroovyService.ResultProcessor;
import groovy.lang.Binding;
import groovy.lang.Closure;
import groovy.lang.GroovyShell;
import groovy.lang.Script;
/**
* Property transformation based on a Groovy script.
*
* @author Simon Templer
*/
public class GroovyTransformation extends
AbstractSingleTargetPropertyTransformation<TransformationEngine>implements GroovyConstants {
/**
* Name of the parameter specifying if instances should be used as variables
* in the binding.
*/
public static final String PARAM_INSTANCE_VARIABLES = "variablesAsInstances";
@Override
protected Object evaluate(String transformationIdentifier, TransformationEngine engine,
ListMultimap<String, PropertyValue> variables, String resultName,
PropertyEntityDefinition resultProperty, Map<String, String> executionParameters,
TransformationLog log) throws TransformationException, NoResultException {
// determine if instances should be used in variables or their values
boolean useInstanceVariables = getOptionalParameter(PARAM_INSTANCE_VARIABLES,
Value.of(false)).as(Boolean.class);
// instance builder
InstanceBuilder builder = createBuilder(resultProperty);
// create the script binding
List<? extends Entity> varDefs = null;
if (getCell().getSource() != null) {
varDefs = getCell().getSource().get(ENTITY_VARIABLE);
}
Binding binding = createGroovyBinding(variables.get(ENTITY_VARIABLE), varDefs, getCell(),
getTypeCell(), builder, useInstanceVariables, log, getExecutionContext(),
resultProperty.getDefinition().getPropertyType());
Object result;
try {
GroovyService service = getExecutionContext().getService(GroovyService.class);
Script groovyScript = GroovyUtil.getScript(this, binding, service);
// evaluate the script
result = evaluate(groovyScript, builder,
resultProperty.getDefinition().getPropertyType(), service);
} catch (TransformationException | NoResultException e) {
throw e;
} catch (Throwable e) {
throw new TransformationException("Error evaluating the cell script", e);
}
if (result == null) {
throw new NoResultException();
}
return result;
}
/**
* Evaluate a Groovy script.
*
* @param groovyScript the script
* @param builder the instance builder, may be <code>null</code>
* @param targetType the type definition of the target property
* @param service the Groovy service
* @return the result property value or instance
* @throws TransformationException if the evaluation fails
* @throws NoResultException if no result returned from the evaluation
*/
public static Object evaluate(Script groovyScript, final InstanceBuilder builder,
final TypeDefinition targetType, GroovyService service)
throws TransformationException, NoResultException {
try {
return service.evaluate(groovyScript, new ResultProcessor<Object>() {
@Override
public Object process(Script groovyScript, Object scriptResult) throws Exception {
Object result = scriptResult;
Object target = groovyScript.getBinding().getVariable(BINDING_TARGET);
if (target instanceof TargetCollector) {
TargetCollector collector = (TargetCollector) target;
if (collector.size() == 0) {
// use script result as result
result = scriptResult;
}
else if (collector.size() == 1) {
// use single collector value as result
// -> instance value is set to return value if
// applicable
result = collector.toMultiValue(builder, targetType).get(0);
}
else {
// use collector MultiValue as result
result = collector.toMultiValue(builder, targetType);
}
}
else if (target instanceof Closure<?>) {
// legacy way to set target binding
if (builder != null) {
result = builder.createInstance(targetType, (Closure<?>) target);
}
else {
throw new TransformationException(
"An instance is not applicable for the target.");
}
}
else if (target != null) {
// use target as result
result = target;
}
// use script result as instance value (if possible)
if (result instanceof MutableInstance && scriptResult != target
&& scriptResult != result) {
MutableInstance resInstance = ((MutableInstance) result);
if (resInstance.getValue() == null) {
// only override value with script result if current
// value is null
// XXX there may still be cases there this is not
// desired and users instead have to make sure they
// return null from the function
resInstance.setValue(scriptResult);
}
}
return result;
}
});
} catch (RuntimeException | TransformationException | NoResultException e) {
throw e;
} catch (Exception e) {
throw new TransformationException(e.getMessage(), e);
}
}
/**
* Creates an instance builder for the given result property if applicable.
*
* @param resultProperty the result property the instance builder should be
* created for
* @return an instance builder or <code>null</code> if for the property no
* instance builder should be used
*/
public static InstanceBuilder createBuilder(PropertyEntityDefinition resultProperty) {
if (!resultProperty.getDefinition().getPropertyType().getChildren().isEmpty()) {
// property has children and is thus represented as instance
return new InstanceBuilder(false);
}
return null;
}
/**
* Create a Groovy binding from the list of variables.
*
* @param vars the variable values
* @param varDefs definition of the assigned variables, in case some
* variable values are not set, may be <code>null</code>
* @param cell the cell the binding is created for
* @param typeCell the type cell the binding is created for, may be
* <code>null</code>
* @param builder the instance builder for creating target instances, or
* <code>null</code> if not applicable
* @param useInstanceVariables if instances should be used as variables for
* the binding instead of extracting the instance values
* @param log the transformation log
* @param context the execution context
* @param targetInstanceType the type of the target instance
* @return the binding for use with {@link GroovyShell}
*/
public static Binding createGroovyBinding(List<PropertyValue> vars,
List<? extends Entity> varDefs, Cell cell, Cell typeCell, InstanceBuilder builder,
boolean useInstanceVariables, TransformationLog log, ExecutionContext context,
TypeDefinition targetInstanceType) {
Binding binding = GroovyUtil.createBinding(builder, cell, typeCell, log, context,
targetInstanceType);
// collect definitions to check if all were provided
Set<EntityDefinition> notDefined = new HashSet<>();
if (varDefs != null) {
for (Entity entity : varDefs) {
notDefined.add(entity.getDefinition());
}
}
// keep only defs where no value is provided
if (!notDefined.isEmpty()) {
for (PropertyValue var : vars) {
notDefined.remove(var.getProperty());
}
}
// add null value for missing variables
if (!notDefined.isEmpty()) {
vars = new ArrayList<>(vars);
for (EntityDefinition entityDef : notDefined) {
if (entityDef instanceof PropertyEntityDefinition) {
vars.add(new PropertyValueImpl(null, (PropertyEntityDefinition) entityDef));
}
}
}
for (PropertyValue var : vars) {
// add the variable to the environment
addToBinding(binding, var.getProperty(),
getUseValue(var.getValue(), useInstanceVariables));
}
return binding;
}
/**
* Returns the full variable name used for the given entity definition.
*
* @param entityDefinition the entity definition
* @return the full variable name used
*/
public static String getVariableName(PropertyEntityDefinition entityDefinition) {
List<String> names = new ArrayList<String>();
for (ChildContext context : entityDefinition.getPropertyPath()) {
names.add(context.getChild().getName().getLocalPart());
}
return Joiner.on('_').join(names);
}
/**
* Adds the variable to the binding.
*
* @param binding the binding to add to
* @param prop the property of the variable
* @param value the value of the variable
*/
public static void addToBinding(Binding binding, PropertyEntityDefinition prop, Object value) {
// determine the variable name
String name = prop.getDefinition().getName().getLocalPart();
// add with short name, but ensure no variable with only a short
// name is overridden
if (binding.getVariables().get(name) == null || prop.getPropertyPath().size() == 1) {
binding.setVariable(name, value);
}
// add with long name if applicable
if (prop.getPropertyPath().size() > 1) {
binding.setVariable(getVariableName(prop), value);
}
}
/**
* Extracts the value to be used in the binding from the present value.
*
* @param value the original unmodified value
* @param useInstanceVariables if instances should be used as variables for
* the binding instead of extracting the instance values
* @return the value to be used by the script
*/
public static Object getUseValue(Object value, boolean useInstanceVariables) {
// determine the variable value
if (value instanceof Instance) {
if (!useInstanceVariables) {
// extract value from instance
value = ((Instance) value).getValue();
}
}
return value;
}
}