package com.sap.furcas.runtime.referenceresolving;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import org.eclipse.emf.common.notify.Notification;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.ecore.EClass;
import org.eclipse.emf.ecore.EClassifier;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EPackage;
import org.eclipse.emf.ecore.EParameter;
import org.eclipse.ocl.Environment;
import org.eclipse.ocl.ParserException;
import org.eclipse.ocl.ecore.CollectionType;
import org.eclipse.ocl.ecore.EcoreEnvironment;
import org.eclipse.ocl.ecore.EcoreFactory;
import org.eclipse.ocl.ecore.IteratorExp;
import org.eclipse.ocl.ecore.OCL;
import org.eclipse.ocl.ecore.OCL.Helper;
import org.eclipse.ocl.ecore.OCLExpression;
import org.eclipse.ocl.ecore.Variable;
import org.eclipse.ocl.ecore.VariableExp;
import org.eclipse.ocl.ecore.opposites.OppositeEndFinder;
import org.eclipse.ocl.ecore.utilities.AbstractVisitor;
import org.eclipse.ocl.types.OrderedSetType;
import org.eclipse.ocl.types.SequenceType;
import org.eclipse.ocl.util.CollectionUtil;
import org.eclipse.ocl.util.TypeUtil;
import org.eclipse.ocl.utilities.PredefinedType;
import com.sap.emf.ocl.trigger.ExpressionWithContext;
import com.sap.furcas.metamodel.FURCAS.TCS.LookupScopePArg;
import com.sap.furcas.metamodel.FURCAS.TCS.PostfixPArg;
import com.sap.furcas.metamodel.FURCAS.TCS.PrefixPArg;
import com.sap.furcas.metamodel.FURCAS.TCS.Property;
import com.sap.furcas.metamodel.FURCAS.TCS.ReferenceByPArg;
import com.sap.furcas.metamodel.FURCAS.TCS.SequenceElement;
import com.sap.furcas.metamodel.FURCAS.textblocks.LexedToken;
import com.sap.furcas.metamodel.FURCAS.textblocks.TextblocksPackage;
import com.sap.furcas.metamodel.FURCAS.textblocks.Version;
import com.sap.furcas.runtime.common.util.ContextAndForeachHelper;
import com.sap.furcas.runtime.tcs.PropertyArgumentUtil;
import com.sap.furcas.runtime.textblocks.TbUtil;
/**
* Represents an OCL "query" used to find an existing element in the model to assign to a property. The query is based on a
* parameter whose value is taken from a token in the text blocks model.
* <p>
*
* The tricky part about this type of updater, from the perspective of the OCL Impact Analysis, is that its parameterized OCL
* expression must contain some subexpression for the parameter usage. This must not be a literal expression because then the
* impact analysis may be able to evaluate it and cut short the impact analysis process. For example, were the parameter
* expression represented as a string literal <code>'???'</code> in an expression <code>...->select(i.x = '???')</code> then
* the impact analysis may try a partial evaluation of <code>i.x = '???'</code> for the pre-change and post-change models. If both
* values of <code>i.x</code> are not equal to <code>'???'</code> (which is of course very likely) then the impact analysis would
* infer that no change to the overall expression's value has happened. However, this may not be true depending on the actual
* value used for the parameter at the time of evaluation.
*
* @author Axel Uhl (D043530)
*
*/
public class OCLQueryPropertyUpdater extends AbstractFurcasOCLBasedModelUpdater {
private final Property property;
/**
* The OCL expression compiled from the {@link LookupScopePArg} attached to the {@link #property}
*/
private final OCLExpression lookupScopeExp;
/**
* The OCLK expression extracted from the {@link ReferenceByPArg} attached to the {@link #property}
*/
private final OCLExpression referenceByExp;
/**
* The OCL expression synthesized by constructing a "collect" iterator expression whose source is a copy
* of the {@link #lookupScopeExpression} and whose body expression is a copy of the
* {@link #referenceByExpression}
*/
private final IteratorExp collectExp;
/**
* the prefix to be prepended to the identifier provided by a token before the concatenated string
* is compared to the result of the {@link #referenceByExp} evaluated on the elements returned by
* the {@link #lookupScopeExp}.
*/
private final PrefixPArg prefix;
/**
* the postfix to be appended to the identifier provided by a token before the concatenated string
* is compared to the result of the {@link #referenceByExp} evaluated on the elements returned by
* the {@link #lookupScopeExp}.
*/
private final PostfixPArg postfix;
/**
* The {@link Activator} to notify when a token is changed. The syntax registry will, in turn,
* notify the {@link TokenChanger}s registered with it.
*/
private final TokenChanger tokenChanger;
/**
* The visitor returns the first "self" variable found referenced by the expression. If no such variable
* is found, <code>null</code> is returned by the visitor. All {@link VariableExp} expressions
* referencing any variable named "self" will have their {@link VariableExp#getReferredVariable() variable}
* set to the first "self" variable found.
*
* @author Axel Uhl (D043530)
*
*/
private class SelfRenamingVisitor extends AbstractVisitor<Variable> {
private final String newNameForSelf;
public SelfRenamingVisitor(String newNameForSelf) {
this.newNameForSelf = newNameForSelf;
}
@Override
protected Variable handleVariable(org.eclipse.ocl.expressions.Variable<EClassifier, EParameter> variable,
Variable initResult) {
if (variable.getName().equals(Environment.SELF_VARIABLE_NAME)) {
variable.setName(newNameForSelf);
if (result == null) {
result = (Variable) variable;
}
}
return result;
}
@Override
public Variable visitVariableExp(org.eclipse.ocl.expressions.VariableExp<EClassifier, EParameter> v) {
if (result != null && v.getReferredVariable().getName().equals(Environment.SELF_VARIABLE_NAME)) {
v.setReferredVariable(result);
}
return visitVariable(v.getReferredVariable());
}
public Variable visit(OCLExpression exp) {
return safeVisit(exp);
}
}
protected OCLQueryPropertyUpdater(Property property, EPackage.Registry metamodelPackageRegistry,
OppositeEndFinder oppositeEndFinder, TokenChanger tokenChanger) throws ParserException {
super(property.getPropertyReference().getStrucfeature(), metamodelPackageRegistry, oppositeEndFinder,
null, // triggerExpressionsWithContext are computed in getTriggerExpressionsWithContext
/* notifyNewContextElements */true,
determineSelfKind(getExpressionString(property)), ContextAndForeachHelper
.getContextTag(getExpressionString(property)));
this.property = property;
this.prefix = PropertyArgumentUtil.getPrefixPArg(property);
this.postfix = PropertyArgumentUtil.getPostfixPArg(property);
String unpreparedLookupScopeQuery = getExpressionString(property);
String lookupScopeAsString = ContextAndForeachHelper.prepareOclQuery(unpreparedLookupScopeQuery);
Helper oclHelper = createOCLHelper(unpreparedLookupScopeQuery, property.getParentTemplate(), getOppositeEndFinder());
lookupScopeExp = oclHelper.createQuery(lookupScopeAsString);
OCLExpression collectSource = oclHelper.createQuery(lookupScopeAsString);
// The following is an OCL expression where "self" refers to an element produced by
// the lookupScope expression. Therefore, its type is goverend by the lookupScope expression's
// result type.
String referenceByAsString = PropertyArgumentUtil.getReferenceByAsOCL(PropertyArgumentUtil.getReferenceByPArg(property));
oclHelper.setContext(((CollectionType) lookupScopeExp.getType()).getElementType());
referenceByExp = oclHelper.createQuery(referenceByAsString);
// Now modify a copy of referenceByExp such that all "self" expressions have their variable
// renamed to a unique iterator variable name which will then be used in a "collect"
// LoopExp expression of which the referenceByExp is the body and the
// lookupScopeExp is the source
OCLExpression collectBody = oclHelper.createQuery(referenceByAsString);
Variable firstSelf = renameAllSelf(collectBody, "i___84923___");
collectExp = EcoreFactory.eINSTANCE.createIteratorExp();
collectExp.setName(PredefinedType.COLLECT_NAME);
collectExp.setSource(collectSource);
collectExp.setBody(collectBody);
collectExp.getIterator().add(firstSelf);
collectExp.setType(getCollectType(collectExp, (EcoreEnvironment) oclHelper.getEnvironment()));
this.tokenChanger = tokenChanger;
}
private static EClassifier getCollectType(IteratorExp collectExp, EcoreEnvironment env) {
EClassifier elementType = collectExp.getBody().getType();
if (elementType instanceof CollectionType) {
CollectionType ct = (CollectionType) elementType;
elementType = CollectionUtil.getFlattenedElementType(ct);
}
if (collectExp.getSource().getType() instanceof SequenceType
|| collectExp.getSource().getType() instanceof OrderedSetType<?, ?>) {
return TypeUtil.resolveSequenceType(env, elementType);
} else {
return TypeUtil.resolveBagType(env, elementType);
}
}
private static String getExpressionString(Property property) {
LookupScopePArg qarg = PropertyArgumentUtil.getLookupScopePArg(property);
if (qarg == null) {
throw new RuntimeException("Didn't find a query argument in rule for property "+property.getPropertyReference().getStrucfeature());
}
return qarg.getQuery();
}
@Override
public Collection<ExpressionWithContext> getTriggerExpressionsWithContext() {
Collection<ExpressionWithContext> result = new ArrayList<ExpressionWithContext>();
try {
String unpreparedLookupScopeQuery = getExpressionString(property);
EClass parsingContextForLookupScope = (EClass) ContextAndForeachHelper.getParsingContext(
unpreparedLookupScopeQuery, property.getParentTemplate());
ExpressionWithContext lookupScopeExpWithContext = new ExpressionWithContext(lookupScopeExp,
parsingContextForLookupScope);
ExpressionWithContext collectExpWithContext = new ExpressionWithContext(collectExp,
parsingContextForLookupScope);
result.add(lookupScopeExpWithContext);
result.add(collectExpWithContext);
} catch (ParserException e) {
throw new RuntimeException(e);
}
return result;
}
/**
* This model updater is registered for changes coming from one of the two expressions {@link #collectExp} and
* {@link #lookupScopeExp}. If the {@link #collectExp} has changed its value for a context element, this means that
* the list of string literals available for performing the query for setting the property on that context element
* may have changed. If the property to be updated by the query {@link #isResolved is yet unresolved}, resolving is
* attempted now.
* <p>
*
* If the {@link #lookupScopeExp} may have changed its value then this may also mean that the query now resolves to
* a different element. If the property is not currently resolved, resolving is attempted now. If the property is
* currently {@link #isResolved(EObject) resolved} and the element to which it is resolved is still in the candidate
* list, only the token in the text blocks model may need to be updated, based on user preferences, dirty state,
* etc.
* <p>
*
* If the property is currently resolved and the {@link #lookupScopeExp} expression's value may have changed but the
* element to which the property is currently bound does no longer occur in the scope, a new lookup is performed. If
* successful, the property is bound to the new lookup result.
*/
@Override
public void notify(OCLExpression expression, Collection<EObject> affectedContextObjects, OppositeEndFinder oppositeEndFinder, Notification change) {
Collection<EObject> tokens = findTokensDocumentQueryExecution();
for (EObject eo : affectedContextObjects) {
for (EObject elementToUpdate : getElementsToUpdate(eo)) {
for (LexedToken token : filterTokens(elementToUpdate, tokens)) {
if (!isResolved(elementToUpdate, token)) {
resolve(elementToUpdate, token);
} else if (expression == lookupScopeExp) {
OCL ocl = org.eclipse.ocl.examples.impactanalyzer.util.OCL.newInstance(oppositeEndFinder);
Object elementsInScope = ocl.evaluate(eo, lookupScopeExp);
if (ocl.getEnvironment().getOCLStandardLibrary().getInvalid() == elementsInScope) {
continue; // only assign if result was not "invalid"
}
Object oldValue = getResolvedElement(token);
if (((Collection<?>) elementsInScope).contains(oldValue)) {
// resolved element still in scope; update token to reflect name change
tokenChanger.requestTokenValueChange(token, token.getValue(),
getNewTokenValue(ocl, oldValue));
} else {
// element to which identifier resolved so far is no
// longer in scope; resolve again, now based on modified scope
resolve(elementToUpdate, token);
}
}
}
}
}
}
/**
* From the <code>elementToUpdate</code> and the {@link #property} find out the tokens that were parsed by the
* {@link #property} rule and that were produced within the execution of the template that led to
* <code>element</code>'s creation.
*/
private Collection<LexedToken> filterTokens(EObject elementToUpdate, Collection<EObject> tokens) {
Collection<LexedToken> result = new ArrayList<LexedToken>();
for (EObject eo : tokens) {
if (eo instanceof LexedToken && eo.eResource() != null) {
LexedToken lt = (LexedToken) eo;
if (lt.getVersion() == Version.PREVIOUS) {
continue;
}
if (lt.getVersion() == Version.REFERENCE && TbUtil.getNewestVersion(lt) != lt) {
continue;
}
if (lt.getParent().getCorrespondingModelElements().contains(elementToUpdate)) {
result.add(lt);
}
}
}
return result;
}
private Collection<EObject> findTokensDocumentQueryExecution() {
Collection<EObject> result = getOppositeEndFinder().navigateOppositePropertyWithBackwardScope(
TextblocksPackage.eINSTANCE.getDocumentNode_SequenceElement(), property);
if (result == null) {
return Collections.emptyList();
}
return result;
}
/**
* Computes the identifier by which the element needs to be referenced by the {@link #property}.
*
* @param element expected to conform to the element type of the {@link #lookupScopeExp} expression's
* result type which is assumed to be a collection type
*/
private String getNewTokenValue(OCL ocl, Object element) {
String newToken = (String) ocl.evaluate(element, referenceByExp);
return PropertyArgumentUtil.stripPrefixPostfix(newToken, prefix, postfix);
}
/**
* Based on the <code>token</code> from which the identifier is obtained, prefixed with {@link #prefix} and
* postfixed with {@link #postfix}, a lookup is performed by computing {@link #lookupScopeExp} for <code>eo</code>,
* then computing the {@link #referenceByExp} strings for each of the resulting scope elements and comparing them to
* the prefixed/postfixed token value. The first match is assigned to <code>eo</code>'s
* {@link #getPropertyToUpdate() property}.<p>
*
* If the {@link #lookupScopeExp} evaluates to <code>invalid</code>, resolution fails; the property
* is not updated.
*/
private void resolve(EObject elementToUpdate, LexedToken token) {
OCL ocl = org.eclipse.ocl.examples.impactanalyzer.util.OCL.newInstance(getOppositeEndFinder());
Object elementsInScope = ocl.evaluate(elementToUpdate, lookupScopeExp);
if (ocl.getEnvironment().getOCLStandardLibrary().getInvalid() == elementsInScope) {
return;
}
EObject currentlyResolved = getResolvedElement(token);
token.getReferencedElements().clear(); // Break the reference
for (Object o : (Collection<?>) elementsInScope) {
EObject candidate = (EObject) o;
if (token.getValue().equals(getNewTokenValue(ocl, candidate))) {
if (getPropertyToUpdate().isMany()) {
@SuppressWarnings("unchecked")
EList<EObject> list = (EList<EObject>) elementToUpdate.eGet(getPropertyToUpdate());
int i = list.indexOf(currentlyResolved);
if (i < 0) {
list.add(candidate);
} else {
list.remove(i);
list.add(i, candidate);
}
} else {
elementToUpdate.eSet(getPropertyToUpdate(), candidate);
}
token.getReferencedElements().add(candidate);
break;
}
}
if (token.getReferencedElements().isEmpty()) {
// Reference could not be resolved. Completely break it in the model
elementToUpdate.eUnset(getPropertyToUpdate());
}
}
/**
* Tells if the property to be updated by this updater is currently resolved.
*/
private boolean isResolved(EObject element, LexedToken token) {
EObject resolved = getResolvedElement(token);
if (resolved == null) {
return false;
} else if (getPropertyToUpdate().isMany()) {
List<?> list = (List<?>) element.eGet(getPropertyToUpdate());
return list.contains(resolved);
}
return true;
}
private EObject getResolvedElement(LexedToken token) {
if (token.getReferencedElements().isEmpty()) {
return null;
}
return token.getReferencedElements().get(0);
}
private Variable renameAllSelf(OCLExpression collectBody, String newNameForSelf) {
return new SelfRenamingVisitor(newNameForSelf).visit(collectBody);
}
@Override
protected SequenceElement getSequenceElement() {
return property;
}
}