blob: 877e49b16e1c0d1c208e6f4046a084fa8b3f97ac [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2009, 2012 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;
}
}