/******************************************************************************* * Copyright (c) 2009, 2014 SAP AG and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * SAP AG - initial API and implementation ******************************************************************************/ package org.eclipse.ocl.examples.impactanalyzer.deltaPropagation; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import org.eclipse.emf.common.notify.Notification; import org.eclipse.emf.ecore.EOperation; import org.eclipse.ocl.ecore.CallExp; import org.eclipse.ocl.ecore.IfExp; import org.eclipse.ocl.ecore.IteratorExp; import org.eclipse.ocl.ecore.LoopExp; import org.eclipse.ocl.ecore.OCL; import org.eclipse.ocl.ecore.OCL.Helper; import org.eclipse.ocl.ecore.OCLExpression; import org.eclipse.ocl.ecore.OperationCallExp; import org.eclipse.ocl.ecore.opposites.DefaultOppositeEndFinder; import org.eclipse.ocl.ecore.opposites.OppositeEndFinder; import org.eclipse.ocl.examples.impactanalyzer.PartialEvaluator; import org.eclipse.ocl.examples.impactanalyzer.ValueNotFoundException; import org.eclipse.ocl.examples.impactanalyzer.impl.OperationBodyToCallMapper; import org.eclipse.ocl.examples.impactanalyzer.util.OCLFactory; import org.eclipse.ocl.examples.impactanalyzer.util.Tuple.Pair; import org.eclipse.ocl.util.OCLStandardLibraryUtil; import org.eclipse.ocl.utilities.PredefinedType; /** * Can evaluate an OCL expression when the model is in some state which just got modified by a change indicated by an event * {@link Notification} such that the evaluation result is based on the state that the model was in <em>before</em> the * modification occurred. This is similar to the <code>@pre</code> operator in OCL. * <p> * * Additionally, if the expression to evaluate is a {@link CallExp} expression, the evaluation result of its * {@link CallExp#getSource() source} expression can be provided, cutting short the evaluation of this source * expression. For this, it uses an adapted OCL evaluation environment. * * TODO need to be able to accept a set of variable definitions for the starting scope that are added to the initial evaluation environment on the OCL instance being used * * @author Axel Uhl * */ public class PartialEvaluatorImpl implements PartialEvaluator { private final OCL ocl; private final Helper helper; private PartialEcoreEnvironmentFactory factory; /** * Uses a {@link DefaultOppositeEndFinder} to navigate hidden opposite properties and evaluates * the model based on its current state. */ public PartialEvaluatorImpl(OCLFactory oclFactory) { this(new PartialEcoreEnvironmentFactory(), oclFactory); } /** * Constructs the OCL instance using {@link OCLFactory#createOCL(OppositeEndFinder)}, passing the * <code>oppositeEndFinder</code> provided. A default {@link PartialEcoreEnvironmentFactory} is * used, configured as well with the <code>oppositeEndFinder</code> provided here. */ public PartialEvaluatorImpl(OCLFactory oclFactory, OppositeEndFinder oppositeEndFinder) { this.factory = new PartialEcoreEnvironmentFactory(oppositeEndFinder); this.ocl = oclFactory.createOCL(this.factory); helper = ocl.createOCLHelper(); } protected PartialEvaluatorImpl(PartialEcoreEnvironmentFactory factory, OCLFactory oclFactory) { this.factory = factory; this.ocl = oclFactory.createOCL(factory); helper = ocl.createOCLHelper(); } /** * Taking a {@link Notification} object such that an evaluation will be * based on the state *before* the notification. For example, if the * notification indicates the removal of a reference from an element * <tt>e1</tt> to an element <tt>e2</tt> across reference <tt>r</tt> then * when during partial evaluation <tt>r</tt> is traversed starting from * <tt>e1</tt> then <tt>e2</tt> will show in the results although in the * current version of the model it would not. * <p> * * A {@link DefaultOppositeEndFinder} is used for hidden opposite * navigation. * * @param atPre * if <code>null</code>, the constructor behaves the same as * {@link #PartialEvaluatorImpl(OCLFactory)} */ public PartialEvaluatorImpl(Notification atPre, OCLFactory oclFactory) { this(new PartialEcoreEnvironmentFactory(atPre), oclFactory); } /** * Taking a {@link Notification} object such that an evaluation will be * based on the state *before* the notification. For example, if the * notification indicates the removal of a reference from an element * <tt>e1</tt> to an element <tt>e2</tt> across reference <tt>r</tt> then * when during partial evaluation <tt>r</tt> is traversed starting from * <tt>e1</tt> then <tt>e2</tt> will show in the results although in the * current version of the model it would not. * <p> * * @param atPre * if <code>null</code>, the constructor behaves the same as * {@link #PartialEvaluatorImpl(OCLFactory, OppositeEndFinder)} */ public PartialEvaluatorImpl(Notification atPre, OppositeEndFinder oppositeEndFinder, OCLFactory oclFactory) { this(new PartialEcoreEnvironmentFactory(atPre, oppositeEndFinder), oclFactory); } public OCL getOcl() { return ocl; } public Helper getHelper() { return helper; } public Object evaluate(Object context, CallExp e, Object valueOfSourceExpression) { factory.setExpressionValue((OCLExpression) e.getSource(), valueOfSourceExpression); return ocl.evaluate(context, e); } public Object evaluate(Object context, OCLExpression e) { return ocl.evaluate(context, e); } /** * Determines the operation of which <tt>bodyExpression</tt> is the body. If <tt>bodyOperation</tt> * is not the body of an operation in the scope of <tt>mapper</tt>, then <tt>null</tt> is returned. */ private EOperation getOperationFromBody(OCLExpression bodyExpression, OperationBodyToCallMapper mapper) { EOperation result = null; Set<OperationCallExp> calls = mapper.getCallsOf(bodyExpression); if (!calls.isEmpty()) { result = calls.iterator().next().getReferredOperation(); } return result; } /** * Determines if a change of <tt>e</tt>'s value from <tt>oldValue</tt> to * <tt>newValue</tt> may or may not have an effect on the overall expression by which <tt>e</tt> is used. If this * methods returns <tt>true</tt> then this guarantees that the change has no effect. If it returns <tt>false</tt> this only * means that the absence of an effect could not be proven and that it is still possible that the change had no effect. * <p> * * The method first applies partial evaluation along chains of {@link CallExp} and operation calls. If the computation of the * full value based on old and new source value reaches the end of such a chain of {@link CallExp} and operation * body/operation call constructs, {@link #transitivelyPropagateDelta(OCLExpression, Collection, OperationBodyToCallMapper)} tries to * propagate the delta between old and new value further. If the result of this delta propagation is empty for all * expressions to which it propagates then this proves that the original change indicated by <tt>oldSourceValue</tt> * and <tt>newSourceValue</tt> has no effect on the overall expression. * * @param mapper * needs to be able to map an operation body in which <tt>callExp</tt> is used to the calls of that operation, as * well as all operation calls in which those calls appear (transitively) to the calls of those operations, and so * on, for the overall expression for which the effect of the change is to be analyzed. */ public boolean hasNoEffectOnOverallExpression(OCLExpression e, Object oldValue, Object newValue, OperationBodyToCallMapper mapper) { boolean result; boolean oldEqualsNew = (oldValue == null && newValue == null) || (oldValue != null && oldValue.equals(newValue)); if (oldEqualsNew) { result = true; } else { try { if (e.eContainer() != null && e.eContainer() instanceof CallExp && ((CallExp) e.eContainer()).getSource() == e) { // e is source of a CallExp CallExp callExp = (CallExp) e.eContainer(); Object oldCallExpValue = evaluate(/* self context */null, callExp, oldValue); Object newCallExpValue = evaluate(/* self context */null, callExp, newValue); result = hasNoEffectOnOverallExpression(callExp, oldCallExpValue, newCallExpValue, mapper); } else { // check if it's an operation body EOperation op = getOperationFromBody(e, mapper); if (op != null) { result = true; for (OperationCallExp call : mapper.getCallsOf(e)) { result = hasNoEffectOnOverallExpression(call, oldValue, newValue, mapper); if (!result) { // if one operation call may have an effect then so may the original change break; } } } else { // neither was e the source of a CallExp nor was it the body of an operation that gets called // try delta propagation: Collection<Object> delta = symmetricDifference(oldValue, newValue); result = transitivelyPropagateDelta(e, delta, mapper).isEmpty(); } } } catch (ValueNotFoundException ex) { // can't perform the partial evaluation due to access to undefined variable result = false; // we don't know, so we have to answer conservatively } } return result; } @SuppressWarnings("unchecked") private Collection<Object> symmetricDifference(Object oldSourceValue, Object newSourceValue) { Collection<Object> result; Collection<Object> oldSourceValueAsCollection = ((oldSourceValue instanceof Collection<?> ? (Collection<Object>) oldSourceValue : Collections.singleton(oldSourceValue))); Collection<Object> newSourceValueAsCollection = ((newSourceValue instanceof Collection<?> ? (Collection<Object>) newSourceValue : Collections.singleton(newSourceValue))); if (oldSourceValueAsCollection instanceof List<?> || newSourceValueAsCollection instanceof List<?>) { // at least one value is ordered; perform position-by-position comparison result = computeOrderedSymmetricDifference(oldSourceValueAsCollection, newSourceValueAsCollection); } else { // both values are unordered; compare using set semantics result = computeUnorderedSymmetricDifference(oldSourceValueAsCollection, newSourceValueAsCollection); } return result; } private Set<Object> computeUnorderedSymmetricDifference( Collection<Object> oldSourceValueAsCollection, Collection<Object> newSourceValueAsCollection) { Set<Object> result = new HashSet<Object>(); result.addAll(oldSourceValueAsCollection); result.removeAll(newSourceValueAsCollection); Collection<Object> newSourceValueMinusOldSourceValue = new HashSet<Object>(); newSourceValueMinusOldSourceValue .addAll(newSourceValueAsCollection); newSourceValueMinusOldSourceValue .removeAll(oldSourceValueAsCollection); result.addAll(newSourceValueMinusOldSourceValue); return result; } private List<Object> computeOrderedSymmetricDifference( Collection<Object> oldSourceValueAsCollection, Collection<Object> newSourceValueAsCollection) { List<Object> result = new ArrayList<Object>(); Iterator<Object> oldIt = oldSourceValueAsCollection.iterator(); Iterator<Object> newIt = newSourceValueAsCollection.iterator(); while (oldIt.hasNext() || newIt.hasNext()) { Object old = null; boolean oldItHadNext = oldIt.hasNext(); if (oldIt.hasNext()) { old = oldIt.next(); } Object nw = null; boolean newItHadNext = newIt.hasNext(); if (newIt.hasNext()) { nw = newIt.next(); } if (oldItHadNext && newItHadNext) { if (old != nw) { result.add(old); result.add(nw); } } else if (oldItHadNext) { result.add(old); } else { result.add(nw); } } return result; } /** * Determines a strategy that can propagate a delta for <tt>e</tt>'s value to a superset of the * delta for another expression using <tt>e</tt>. For example, the delta of the source expression * of an <tt>asSet()</tt> call propagates to the <tt>asSet()</tt> call expression unchanged. * * @return a propagation strategy if the expression using <tt>e</tt> is monotonic in <tt>e</tt> such * that mapping <tt>e</tt>'s delta is possible at all, or <tt>null</tt> if no propagation strategy is * defined or possible to define for the use of <tt>e</tt>. */ private DeltaPropagationStrategy getDeltaPropagationStrategy(OCLExpression e, OperationBodyToCallMapper mapper) { DeltaPropagationStrategy result = null; if (e.eContainer() != null && e.eContainer() instanceof CallExp && ((CallExp) e.eContainer()).getSource() == e) { CallExp callExp = (CallExp) e.eContainer(); if (callExp instanceof IteratorExp) { IteratorExp loopExp = (IteratorExp) callExp; switch (OCLStandardLibraryUtil.getOperationCode(loopExp.getName())) { case PredefinedType.COLLECT: case PredefinedType.SELECT: case PredefinedType.REJECT: // the iterator's delta can be computed from the source delta by applying the iterator to the source delta collection result = new IteratorSourcePropagationStrategy(loopExp, this); break; } } else if (callExp instanceof OperationCallExp) { switch (OCLStandardLibraryUtil.getOperationCode(((OperationCallExp) callExp).getReferredOperation().getName())) { case PredefinedType.UNION: case PredefinedType.INTERSECTION: case PredefinedType.MINUS: case PredefinedType.INCLUDING: case PredefinedType.EXCLUDING: case PredefinedType.APPEND: case PredefinedType.PREPEND: case PredefinedType.INSERT_AT: case PredefinedType.AS_BAG: case PredefinedType.AS_ORDERED_SET: case PredefinedType.AS_SEQUENCE: case PredefinedType.AS_SET: case PredefinedType.FLATTEN: // case PredefinedType.REVERSE // not supported as of now // the iterator's delta (superset) equals the source's delta result = new IdentityPropagationStrategy(callExp); break; } } } else { // Not the source of a CallExp. // Check if e is the last parameter expression of a call to any of the stdlib operations // that are monotonic in their last parameter: if (e.eContainer() != null && e.eContainer() instanceof OperationCallExp && ((OperationCallExp) e.eContainer()).getArgument().contains(e)) { OperationCallExp operationCall = (OperationCallExp) e.eContainer(); if (e == operationCall.getArgument().get(operationCall.getArgument().size() - 1)) { // last parameter? switch (OCLStandardLibraryUtil.getOperationCode(operationCall.getReferredOperation().getName())) { case PredefinedType.UNION: case PredefinedType.INTERSECTION: case PredefinedType.INCLUDING: case PredefinedType.APPEND: case PredefinedType.PREPEND: case PredefinedType.INSERT_AT: result = new IdentityPropagationStrategy(operationCall); break; } } } else { // not a parameter of an operation; check if e is the thenExpression or elseExpression of an IfExp if (e.eContainer() != null && e.eContainer() instanceof IfExp && (((IfExp) e.eContainer()).getThenExpression() == e || ((IfExp) e.eContainer()).getElseExpression() == e)) { result = new IdentityPropagationStrategy((IfExp) e.eContainer()); } else { // no then or else expression of an IfExp // Check if e is the body of an operation: Set<OperationCallExp> callsOfOperationBody = mapper.getCallsOf(e); if (!callsOfOperationBody.isEmpty()) { result = new OperationBodyPropagationStrategy(mapper); } else { // Not an operation body whose operation is called anywhere in the scope of the overall expression. // Check is e is the body of a ->collect expression if (e.eContainer() != null && e.eContainer() instanceof IteratorExp && ((IteratorExp) e.eContainer()).getBody() == e) { LoopExp loopExp = (LoopExp) e.eContainer(); if (OCLStandardLibraryUtil.getOperationCode(loopExp.getName()) == PredefinedType.COLLECT) { result = new IdentityPropagationStrategy(loopExp); } } } } } } return result; } /** * Tries to find a {@link DeltaPropagationStrategy propagation strategy} for the combination of the OCL expression <tt>e</tt> * and a given <tt>delta</tt> for its evaluation result. If no such strategy is found, the pair of <tt>e</tt> and * <tt>delta</tt> is returned. Otherwise, the strategy is applied, and the <tt>delta</tt> is mapped to a delta for another * expression using <tt>e</tt>. The result of this step is recursively passed to this method so that propagation terminates * when no propagation strategy can be found for an expression. * <p> * * When the object collection returned in the {@link Pair#getB b} component of the result is empty then this means that the * overall expression in which <tt>e</tt> occurs will not be affected by the <tt>delta</tt> of <tt>e</tt>'s value. * <p> * * <b>Postcondition</b>: * <tt>this.{@link #getDeltaPropagationStrategy(OCLExpression, OperationBodyToCallMapper) getDeltaPropagationStrategy}(result.getA()) == null</tt> * * @param deltaForEValue * may be null, empty or a valid non-empty collection specifying a delta in <tt>e</tt>'s evaluation result * @param mapper * needs to be able to map an operation body in which <tt>callExp</tt> is used to the calls of that operation, as * well as all operation calls in which those calls appear (transitively) to the calls of those operations, and so * on, for the overall expression for which the effect of the change is to be analyzed. * @return zero or more pairs with a non-<tt>null</tt>, non-empty {@link Pair#getB b} component which is a collection * specifying the delta to which <tt>delta</tt> maps for the expression returned as the {@link Pair#getA a} component * of the pair contained in the collection returned. If the collection returned is empty this means that * the <tt>delta</tt> of <tt>e</tt>'s value has no effect on the overall expression in which <tt>e</tt> appears. */ public Collection<Pair<OCLExpression, Collection<Object>>> transitivelyPropagateDelta(OCLExpression e, Collection<Object> deltaForEValue, OperationBodyToCallMapper mapper) { return transitivelyPropagateDelta(e, deltaForEValue, mapper, new HashMap<OCLExpression, Set<Collection<Object>>>()); } private Collection<Pair<OCLExpression, Collection<Object>>> transitivelyPropagateDelta(OCLExpression e, Collection<Object> deltaForEValue, OperationBodyToCallMapper mapper, Map<OCLExpression, Set<Collection<Object>>> cache) { Collection<Pair<OCLExpression, Collection<Object>>> result; Set<Collection<Object>> cacheEntry = cache.get(e); if (cacheEntry != null && cacheEntry.contains(deltaForEValue)) { // we visited the same expression for an equal delta already; the only thing we can do to // avoid an endless recursion is stop here, give up and return the current delta for e result = getResultCollectionFromSingleDelta(e, deltaForEValue); } else { if (cacheEntry == null) { cacheEntry = new HashSet<Collection<Object>>(); cache.put(e, cacheEntry); } cacheEntry.add(deltaForEValue); DeltaPropagationStrategy propagationStrategy = getDeltaPropagationStrategy(e, mapper); if (propagationStrategy == null) { result = getResultCollectionFromSingleDelta(e, deltaForEValue); } else { Collection<Pair<OCLExpression, Collection<Object>>> propagated = null; try { propagated = propagationStrategy.mapDelta(e, deltaForEValue); } catch (ValueNotFoundException vnfe) { // that's ok; probably an access to "self" or another variable that isn't known at this time } if (propagated == null) { result = getResultCollectionFromSingleDelta(e, deltaForEValue); } else { result = new HashSet<Pair<OCLExpression, Collection<Object>>>(); for (Pair<OCLExpression, Collection<Object>> singlePropagationResult : propagated) { Collection<Pair<OCLExpression, Collection<Object>>> singleResult = transitivelyPropagateDelta( singlePropagationResult.getA(), singlePropagationResult.getB(), mapper, cache); result.addAll(singleResult); } } } } return result; } static Collection<Pair<OCLExpression, Collection<Object>>> getResultCollectionFromSingleDelta(OCLExpression e, Collection<Object> delta) { Collection<Pair<OCLExpression, Collection<Object>>> result; // no (further) propagation possible if (delta == null || delta.isEmpty()) { // no change anyhow result = Collections.emptySet(); } else { result = Collections.singleton(new Pair<OCLExpression, Collection<Object>>(e, delta)); } return result; } }