/* * Copyright (c) 2010-2015 Evolveum * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.evolveum.midpoint.model.common.expression.evaluator; import com.evolveum.midpoint.model.common.expression.*; import com.evolveum.midpoint.prism.Item; import com.evolveum.midpoint.prism.ItemDefinition; import com.evolveum.midpoint.prism.PrismPropertyValue; import com.evolveum.midpoint.prism.PrismValue; import com.evolveum.midpoint.prism.delta.DeltaSetTriple; import com.evolveum.midpoint.prism.delta.ItemDelta; import com.evolveum.midpoint.prism.delta.PlusMinusZero; import com.evolveum.midpoint.prism.delta.PrismValueDeltaSetTriple; import com.evolveum.midpoint.prism.path.ItemPath; import com.evolveum.midpoint.prism.polystring.PolyString; import com.evolveum.midpoint.schema.constants.ExpressionConstants; import com.evolveum.midpoint.schema.result.OperationResult; import com.evolveum.midpoint.security.api.SecurityEnforcer; import com.evolveum.midpoint.task.api.Task; import com.evolveum.midpoint.util.MiscUtil; import com.evolveum.midpoint.util.PrettyPrinter; import com.evolveum.midpoint.util.Processor; import com.evolveum.midpoint.util.exception.ExpressionEvaluationException; import com.evolveum.midpoint.util.exception.ObjectNotFoundException; import com.evolveum.midpoint.util.exception.SchemaException; import com.evolveum.midpoint.util.exception.TunnelException; import com.evolveum.midpoint.util.logging.Trace; import com.evolveum.midpoint.util.logging.TraceManager; import com.evolveum.midpoint.xml.ns._public.common.common_3.TransformExpressionEvaluatorType; import com.evolveum.midpoint.xml.ns._public.common.common_3.TransformExpressionRelativityModeType; import javax.xml.namespace.QName; import java.util.*; import java.util.Map.Entry; /** * @author Radovan Semancik */ public abstract class AbstractValueTransformationExpressionEvaluator<V extends PrismValue, D extends ItemDefinition, E extends TransformExpressionEvaluatorType> implements ExpressionEvaluator<V,D> { private SecurityEnforcer securityEnforcer; private E expressionEvaluatorType; private static final Trace LOGGER = TraceManager.getTrace(AbstractValueTransformationExpressionEvaluator.class); protected AbstractValueTransformationExpressionEvaluator(E expressionEvaluatorType, SecurityEnforcer securityEnforcer) { this.expressionEvaluatorType = expressionEvaluatorType; this.securityEnforcer = securityEnforcer; } public E getExpressionEvaluatorType() { return expressionEvaluatorType; } /* (non-Javadoc) * @see com.evolveum.midpoint.common.expression.ExpressionEvaluator#evaluate(java.util.Collection, java.util.Map, boolean, java.lang.String, com.evolveum.midpoint.schema.result.OperationResult) */ @Override public PrismValueDeltaSetTriple<V> evaluate(ExpressionEvaluationContext context) throws SchemaException, ExpressionEvaluationException, ObjectNotFoundException { PrismValueDeltaSetTriple<V> outputTriple; if (expressionEvaluatorType.getRelativityMode() == TransformExpressionRelativityModeType.ABSOLUTE) { outputTriple = evaluateAbsoluteExpression(context.getSources(), context.getVariables(), context, context.getContextDescription(), context.getTask(), context.getResult()); } else if (expressionEvaluatorType.getRelativityMode() == null || expressionEvaluatorType.getRelativityMode() == TransformExpressionRelativityModeType.RELATIVE) { if (context.getSources() == null || context.getSources().isEmpty()) { // Special case. No sources, so there will be no input variables and no combinations. Everything goes to zero set. outputTriple = evaluateAbsoluteExpression(null, context.getVariables(), context, context.getContextDescription(), context.getTask(), context.getResult()); } else { List<SourceTriple<?,?>> sourceTriples = processSources(context.getSources(), isIncludeNullInputs(), context); outputTriple = evaluateRelativeExpression(sourceTriples, context.getVariables(), context.isSkipEvaluationMinus(), context.isSkipEvaluationPlus(), isIncludeNullInputs(), context, context.getContextDescription(), context.getTask(), context.getResult()); } } else { throw new IllegalArgumentException("Unknown relativity mode "+expressionEvaluatorType.getRelativityMode()); } return outputTriple; } protected Boolean isIncludeNullInputs() { return expressionEvaluatorType.isIncludeNullInputs(); } protected boolean isRelative() { return expressionEvaluatorType.getRelativityMode() != TransformExpressionRelativityModeType.ABSOLUTE; } private List<SourceTriple<?,?>> processSources(Collection<Source<?,?>> sources, Boolean includeNulls, ExpressionEvaluationContext params) { List<SourceTriple<?,?>> sourceTriples = new ArrayList<SourceTriple<?,?>>(sources == null ? 0 : sources.size()); if (sources == null) { return sourceTriples; } for (Source<?,?> source: sources) { SourceTriple<?,?> sourceTriple = new SourceTriple<>(source); ItemDelta<?,?> delta = source.getDelta(); if (delta != null) { sourceTriple.merge((DeltaSetTriple) delta.toDeltaSetTriple((Item) source.getItemOld())); } else { if (source.getItemOld() != null) { sourceTriple.addAllToZeroSet((Collection)source.getItemOld().getValues()); } } if (includeNulls == null || includeNulls) { // Make sure that we properly handle the "null" states, i.e. the states when we enter // "empty" value and exit "empty" value for a property // We need this to properly handle "negative" expressions, i.e. expressions that return non-null // value for null input. We need to make sure such expressions receive the null input when needed Item<?,?> itemOld = source.getItemOld(); Item<?,?> itemNew = source.getItemNew(); if (itemOld == null || itemOld.isEmpty()) { if (!(itemNew == null || itemNew.isEmpty())) { // change empty -> non-empty: we are removing "null" value sourceTriple.addToMinusSet(null); } else if (sourceTriple.hasMinusSet()) { // special case: change empty -> empty, but there is still a delete delta // so it seems something was deleted. This is strange case, but we prefer the delta over // the absolute states (which may be out of date). // Similar case than that of non-empty -> empty (see below) sourceTriple.addToPlusSet(null); } } else { if (itemNew == null || itemNew.isEmpty()) { // change non-empty -> empty: we are adding "null" value sourceTriple.addToPlusSet(null); } } } sourceTriples.add(sourceTriple); if (LOGGER.isTraceEnabled()) { LOGGER.trace("Processes source triple\n{}",sourceTriple.debugDump()); } } return sourceTriples; } private PrismValueDeltaSetTriple<V> evaluateAbsoluteExpression(Collection<Source<?,?>> sources, ExpressionVariables variables, ExpressionEvaluationContext params, String contextDescription, Task task, OperationResult result) throws ExpressionEvaluationException, ObjectNotFoundException, SchemaException { PrismValueDeltaSetTriple<V> outputTriple; if (hasDeltas(sources) || hasDeltas(variables)) { Collection<V> outputSetOld = null; if (!params.isSkipEvaluationMinus()) { outputSetOld = evaluateScriptExpression(sources, variables, contextDescription, false, params, task, result); } Collection<V> outputSetNew = null; if (!params.isSkipEvaluationPlus()) { outputSetNew = evaluateScriptExpression(sources, variables, contextDescription, true, params, task, result); } outputTriple = PrismValueDeltaSetTriple.diffPrismValueDeltaSetTriple(outputSetOld, outputSetNew); } else { // No need to execute twice. There is no change. Collection<V> outputSetNew = evaluateScriptExpression(sources, variables, contextDescription, true, params, task, result); outputTriple = new PrismValueDeltaSetTriple<V>(); outputTriple.addAllToZeroSet(outputSetNew); } return outputTriple; } private boolean hasDeltas(Collection<Source<?,?>> sources) { if (sources == null) { return false; } for (Source<?,?> source: sources) { if (source.getDelta() != null && !source.getDelta().isEmpty()) { return true; } } return false; } private boolean hasDeltas(ExpressionVariables variables) { for (Entry<QName,Object> entry: variables.entrySet()) { Object value = entry.getValue(); if (value instanceof ObjectDeltaObject<?>) { if (((ObjectDeltaObject<?>)value).getObjectDelta() != null && !((ObjectDeltaObject<?>)value).getObjectDelta().isEmpty()) { return true; } } else if (value instanceof ItemDeltaItem<?,?>) { if (((ItemDeltaItem<?,?>)value).getDelta() != null && !((ItemDeltaItem<?,?>)value).getDelta().isEmpty()) { return true; } } } return false; } private Collection<V> evaluateScriptExpression(Collection<Source<?,?>> sources, ExpressionVariables variables, String contextDescription, boolean useNew, ExpressionEvaluationContext params, Task task, OperationResult result) throws ExpressionEvaluationException, ObjectNotFoundException, SchemaException { ExpressionVariables scriptVariables = new ExpressionVariables(); if (useNew) { scriptVariables.addVariableDefinitionsNew(variables); } else { scriptVariables.addVariableDefinitionsOld(variables); } if (sources != null) { // Add sources to variables for (Source<?,?> source: sources) { LOGGER.trace("source: {}", source); QName name = source.getName(); if (name == null) { if (sources.size() == 1) { name = ExpressionConstants.VAR_INPUT; } else { throw new ExpressionSyntaxException("No name definition for source in "+contextDescription); } } Object value = null; if (useNew) { value = getRealContent(source.getItemNew(), source.getResidualPath()); } else { value = getRealContent(source.getItemOld(), source.getResidualPath()); } scriptVariables.addVariableDefinition(name, value); } } List<V> scriptResults = transformSingleValue(scriptVariables, null, useNew, params, (useNew ? "(new) " : "(old) " ) + contextDescription, task, result); if (scriptResults == null || scriptResults.isEmpty()) { return null; } Collection<V> outputSet = new ArrayList<V>(scriptResults.size()); for (V pval: scriptResults) { if (pval instanceof PrismPropertyValue<?>) { if (((PrismPropertyValue<?>) pval).getValue() == null) { continue; } Object realValue = ((PrismPropertyValue<?>)pval).getValue(); if (realValue instanceof String) { if (((String)realValue).isEmpty()) { continue; } } if (realValue instanceof PolyString) { if (((PolyString)realValue).isEmpty()) { continue; } } } outputSet.add(pval); } return outputSet; } protected abstract List<V> transformSingleValue(ExpressionVariables variables, PlusMinusZero valueDestination, boolean useNew, ExpressionEvaluationContext context, String contextDescription, Task task, OperationResult result) throws ExpressionEvaluationException, ObjectNotFoundException, SchemaException; private Object getRealContent(Item<?,?> item, ItemPath residualPath) { if (residualPath == null || residualPath.isEmpty()) { return item; } if (item == null) { return null; } return item.find(residualPath); } private Object getRealContent(PrismValue pval, ItemPath residualPath) { if (residualPath == null || residualPath.isEmpty()) { return pval; } if (pval == null) { return null; } return pval.find(residualPath); } private PrismValueDeltaSetTriple<V> evaluateRelativeExpression(final List<SourceTriple<?,?>> sourceTriples, final ExpressionVariables variables, final boolean skipEvaluationMinus, final boolean skipEvaluationPlus, final Boolean includeNulls, final ExpressionEvaluationContext evaluationContext, final String contextDescription, final Task task, final OperationResult result) throws ExpressionEvaluationException, ObjectNotFoundException, SchemaException { List<Collection<? extends PrismValue>> valueCollections = new ArrayList<>(sourceTriples.size()); for (SourceTriple<?,?> sourceTriple: sourceTriples) { Collection<? extends PrismValue> values = sourceTriple.union(); if (values.isEmpty()) { // No values for this source. Add null instead. It will make sure that the expression will // be evaluate at least once. values.add(null); } valueCollections.add(values); } final PrismValueDeltaSetTriple<V> outputTriple = new PrismValueDeltaSetTriple<>(); Processor<Collection<? extends PrismValue>> processor = pvalues -> { if (includeNulls != null && !includeNulls && MiscUtil.isAllNull(pvalues)) { // The case that all the sources are null. There is no point executing the expression. return; } Map<QName, Object> sourceVariables = new HashMap<>(); Iterator<SourceTriple<PrismValue,?>> sourceTriplesIterator = (Iterator)sourceTriples.iterator(); boolean hasMinus = false; boolean hasZero = false; boolean hasPlus = false; for (PrismValue pval: pvalues) { SourceTriple<PrismValue,?> sourceTriple = sourceTriplesIterator.next(); QName name = sourceTriple.getName(); sourceVariables.put(name, getRealContent(pval, sourceTriple.getResidualPath())); // Note: a value may be both in plus and minus sets, e.g. in case that the value is replaced // with the same value. We pretend that this is the same as ADD case. // TODO: maybe we will need better handling in the future. Maybe we would need // to execute the script twice? if (sourceTriple.presentInPlusSet(pval)) { hasPlus = true; } else if (sourceTriple.presentInZeroSet(pval)) { hasZero = true; } else if (sourceTriple.presentInMinusSet(pval)) { hasMinus = true; } } if (!hasPlus && !hasMinus && !hasZero && !MiscUtil.isAllNull(pvalues)) { throw new IllegalStateException("Internal error! The impossible has happened! pvalues="+pvalues+"; source triples: "+sourceTriples+"; in "+contextDescription); } if (hasPlus && hasMinus) { // The combination of values that are both in plus and minus. Evaluating this combination // does not make sense. Just skip it. // Note: There will NOT be a single value that is in both plus and minus (e.g. "replace with itself" case). // That case is handled by the elseif branches above. This case strictly applies to // combination of different values from the plus and minus sets. return; } if (hasPlus && skipEvaluationPlus) { // The results will end up in the plus set, therefore we can skip it return; } else if (hasMinus && skipEvaluationMinus) { // The results will end up in the minus set, therefore we can skip it return; } ExpressionVariables scriptVariables = new ExpressionVariables(); scriptVariables.addVariableDefinitions(sourceVariables); PlusMinusZero valueDestination = null; boolean useNew = false; if (hasPlus) { // Pluses and zeroes: Result goes to plus set, use NEW values for variables scriptVariables.addVariableDefinitionsNew(variables); valueDestination = PlusMinusZero.PLUS; useNew = true; } else if (hasMinus) { // Minuses and zeroes: Result goes to minus set, use OLD values for variables scriptVariables.addVariableDefinitionsOld(variables); valueDestination = PlusMinusZero.MINUS; } else { // All zeros: Result goes to zero set, use NEW values for variables scriptVariables.addVariableDefinitionsNew(variables); valueDestination = PlusMinusZero.ZERO; useNew = true; } List<V> scriptResults; try { scriptResults = transformSingleValue(scriptVariables, valueDestination, useNew, evaluationContext, contextDescription, task, result); } catch (ExpressionEvaluationException e) { throw new TunnelException(new ExpressionEvaluationException(e.getMessage()+ "("+dumpSourceValues(sourceVariables)+") in "+contextDescription,e)); } catch (ObjectNotFoundException e) { throw new TunnelException(new ObjectNotFoundException(e.getMessage()+ "("+dumpSourceValues(sourceVariables)+") in "+contextDescription,e)); } catch (SchemaException e) { throw new TunnelException(new SchemaException(e.getMessage()+ "("+dumpSourceValues(sourceVariables)+") in "+contextDescription,e)); } catch (RuntimeException e) { throw new TunnelException(new RuntimeException(e.getMessage()+ "("+dumpSourceValues(sourceVariables)+") in "+contextDescription,e)); } outputTriple.addAllToSet(valueDestination, scriptResults); }; try { MiscUtil.carthesian((Collection)valueCollections, (Processor)processor); } catch (TunnelException e) { Throwable originalException = e.getCause(); if (originalException instanceof ExpressionEvaluationException) { throw (ExpressionEvaluationException)originalException; } else if (originalException instanceof ObjectNotFoundException) { throw (ObjectNotFoundException)originalException; } else if (originalException instanceof SchemaException) { throw (SchemaException)originalException; } else if (originalException instanceof RuntimeException) { throw (RuntimeException)originalException; } else { throw new IllegalStateException("Unexpected exception: "+e+": "+e.getMessage(),e); } } cleanupTriple(outputTriple); return outputTriple; } private void cleanupTriple(PrismValueDeltaSetTriple<V> triple) { if (triple == null) { return; } Collection<V> minusSet = triple.getMinusSet(); if (minusSet == null) { return; } Collection<V> plusSet = triple.getPlusSet(); if (plusSet == null) { return; } Iterator<V> plusIter = plusSet.iterator(); while (plusIter.hasNext()) { V plusVal = plusIter.next(); if (minusSet.contains(plusVal)) { plusIter.remove(); minusSet.remove(plusVal); triple.addToZeroSet(plusVal); } } } private String dumpSourceValues(Map<QName, Object> variables) { StringBuilder sb = new StringBuilder(); for (Entry<QName, Object> entry: variables.entrySet()) { sb.append(PrettyPrinter.prettyPrint(entry.getKey())); sb.append("="); sb.append(PrettyPrinter.prettyPrint(entry.getValue())); sb.append("; "); } return sb.toString(); } /* (non-Javadoc) * @see com.evolveum.midpoint.common.expression.ExpressionEvaluator#shortDebugDump() */ @Override public abstract String shortDebugDump(); }