/******************************************************************************* * 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.instanceScope; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.eclipse.emf.common.notify.Notification; import org.eclipse.emf.ecore.EAnnotation; import org.eclipse.emf.ecore.EClass; import org.eclipse.emf.ecore.EClassifier; import org.eclipse.emf.ecore.EObject; import org.eclipse.emf.ecore.EOperation; import org.eclipse.ocl.common.OCLCommon; import org.eclipse.ocl.ecore.OCLExpression; import org.eclipse.ocl.ecore.Variable; import org.eclipse.ocl.examples.impactanalyzer.util.AnnotatedEObject; import org.eclipse.ocl.examples.impactanalyzer.util.HighlightingToStringVisitor; import org.eclipse.ocl.examples.impactanalyzer.util.OclHelper; import org.eclipse.ocl.examples.impactanalyzer.util.SemanticIdentity; /** * Abstract implementation of the {@link NavigationStep} interface. Provides fields for source and target type and performs the * necessary type checks in {@link #navigate(Set, TracebackCache, Notification)}. Furthermore, does the unfolding of the <code>from</code> * argument to {@link #navigate(Set, TracebackCache, Notification)} and dispatches for individual objects to the * {@link #navigate(AnnotatedEObject, TracebackCache, Notification)} operation to be implemented by all subclasses. Furthermore, generaly * bookkeeping facilities are implemented here, such as counting cache misses, managing an {@link #id ID} and maintaining for * which {@link OCLExpression}s this is the corresponding navigation step (see {@link #debugInfo}). * <p> * * This class implements the observer pattern specified by the {@link NavigationStep} interface for setting the source and target * type and for defining this step as always empty. * <p> * * A default {@link #hashCode} and {@link #equals} implementation is provided based on the <em>behavior</em> of this step. * Navigation steps are only allowed to call themselves equal to another step if for all <code>from</code> objects their * {@link #navigate(AnnotatedEObject, TracebackCache, Notification)} operation returns equal results for an equal model state and the same * original change {@link Notification notification}.<p> * * Subclasses have to make sure that any modification to equals/hashCode-related parts of the object's state * are announced by firing {@link HashCodeChangeListener#beforeHashCodeChange(NavigationStep, int)} before * and {@link HashCodeChangeListener#afterHashCodeChange(NavigationStep, int)} after the change. This class * manages this for those state changes affecting this class's implementation of equals/hashCode. * * @author Axel Uhl */ public abstract class AbstractNavigationStep implements NavigationStep { private static int idCounter; private final int id; private EClass sourceType; private EClass targetType; private final Set<OCLExpression> debugInfo; private int cacheMisses; private int resultObjectsCounter; private List<AlwaysEmptyChangeListener> alwaysEmptyChangeListeners = null; private List<SourceTypeChangeListener> sourceTypeChangeListeners = null; private List<TargetTypeChangeListener> targetTypeChangeListeners = null; private List<HashCodeChangeListener> hashCodeChangeListeners = null; private boolean alwaysEmpty; private String annotation; private final SemanticIdentity semanticIdentity; /** * The navigateCounter counts how many times the navigate method of this NavigationStep is called */ private int navigateCounter; /** * Used for calls to {@link AbstractNavigationStep#fireAfterHashCodeChange(int)} and * {@link AbstractNavigationStep#fireBeforeHashCodeChange(int)} to generate a new token. */ private static int maxToken = 0; private Set<Variable> leavingScopes = new HashSet<Variable>(); private Set<Variable> enteringScopes = new HashSet<Variable>(); public AbstractNavigationStep(EClass sourceType, EClass targetType, OCLExpression debugInfo) { this.sourceType = sourceType; this.targetType = targetType; this.debugInfo = new HashSet<OCLExpression>(); this.debugInfo.add(debugInfo); this.id = idCounter++; this.semanticIdentity = new AbstractNavigationStepIdentity(); } protected static int newTokenForFiringHashCodeChangeEvent() { return maxToken++; } private class AbstractNavigationStepIdentity extends SemanticIdentity { /** * For source and target type, special rules apply. Normally, if either * of them is <code>null</code>, this means that it hasn't been * initialized yet, e.g., because it depends on some * {@link IndirectingStep} getting its source or target type set later. * This will then propagate through the observer pattern (see * {@link #addSourceTypeChangeListener(SourceTypeChangeListener)} and * {@link #addTargetTypeChangeListener(TargetTypeChangeListener)} to the * using step(s). However, until this propagation has taken place, we * don't know yet what the source or target type, respectively, will be. * Therefore, we have to be conservative and assume that they eventually * will be set to different values, so <code>false</code> will be * returned for <code>null</code> values of either source or target * type. * <p> * * There is one exception, though: if a step {@link #isAbsolute() is * absolute}, then its source type may be ignored and will be ignored * for this equality comparison. */ @Override public boolean equals(Object o) { if (this == o) { return true; } boolean result = false; if (o instanceof AbstractNavigationStepIdentity) { AbstractNavigationStep otherStep = ((AbstractNavigationStepIdentity) o).getNavigationStep(); result = getNavigationStep().getClass() == otherStep.getClass() && getNavigationStep().alwaysEmpty == otherStep.alwaysEmpty && ((isAbsolute() && otherStep.isAbsolute()) || (getNavigationStep().sourceType != null && otherStep.sourceType != null && getNavigationStep().sourceType.equals(otherStep.sourceType))) && getNavigationStep().targetType != null && otherStep.targetType != null && getNavigationStep().targetType.equals(otherStep.targetType); } return result; } @Override public int calculateHashCode() { return 4711 ^ getClass().hashCode() ^ (alwaysEmpty ? 31 : 0) ^ (sourceType == null ? 0 : sourceType.hashCode()) ^ (targetType == null ? 0 : targetType.hashCode()); } public AbstractNavigationStep getNavigationStep() { return AbstractNavigationStep.this; } @Override public NavigationStep getStep() { return getNavigationStep(); } } public EClass getTargetType() { return this.targetType; } public EClass getSourceType() { return this.sourceType; } public void addAlwaysEmptyChangeListener(AlwaysEmptyChangeListener listener) { if(this.alwaysEmptyChangeListeners == null){ this.alwaysEmptyChangeListeners = new ArrayList<AlwaysEmptyChangeListener>(1); } alwaysEmptyChangeListeners.add(listener); } public void addSourceTypeChangeListener(SourceTypeChangeListener listener) { if(this.sourceTypeChangeListeners == null){ this.sourceTypeChangeListeners = new ArrayList<SourceTypeChangeListener>(1); } sourceTypeChangeListeners.add(listener); } public void addTargetTypeChangeListener(TargetTypeChangeListener listener) { if(this.targetTypeChangeListeners == null){ this.targetTypeChangeListeners = new ArrayList<TargetTypeChangeListener>(1); } targetTypeChangeListeners.add(listener); } public void addHashCodeChangeListener(HashCodeChangeListener listener) { if (this.hashCodeChangeListeners == null) { this.hashCodeChangeListeners = new ArrayList<HashCodeChangeListener>(1); } hashCodeChangeListeners.add(listener); } void setSourceType(EClass sourceType) { boolean changed = (this.sourceType == null && sourceType != null) || (this.sourceType != null && !this.sourceType.equals(sourceType)); if (changed) { fireBeforeHashCodeChange(newTokenForFiringHashCodeChangeEvent()); this.sourceType = sourceType; fireAfterHashCodeChange(newTokenForFiringHashCodeChangeEvent()); if(sourceTypeChangeListeners != null){ for (SourceTypeChangeListener listener : sourceTypeChangeListeners) { listener.sourceTypeChanged(this); } } } } protected void fireAfterHashCodeChange(int token) { if (hashCodeChangeListeners != null) { for (HashCodeChangeListener listener : hashCodeChangeListeners) { listener.afterHashCodeChange(this, token); } } } protected void fireBeforeHashCodeChange(int token) { if (hashCodeChangeListeners != null) { for (HashCodeChangeListener listener : hashCodeChangeListeners) { listener.beforeHashCodeChange(this, token); } } } void setTargetType(EClass targetType) { boolean changed = (this.targetType == null && targetType != null) || (this.targetType != null && !this.targetType.equals(targetType)); if (changed) { fireBeforeHashCodeChange(newTokenForFiringHashCodeChangeEvent()); this.targetType = targetType; fireAfterHashCodeChange(newTokenForFiringHashCodeChangeEvent()); if(targetTypeChangeListeners != null){ for (TargetTypeChangeListener listener : targetTypeChangeListeners) { listener.targetTypeChanged(this); } } } } public void addExpressionForWhichThisIsNavigationStep(OCLExpression oclExpression) { debugInfo.add(oclExpression); } public int getNavigateCounter() { return navigateCounter; } public int getResultObjectsCounter() { return resultObjectsCounter; } public Set<OCLExpression> getDebugInfo() { return debugInfo; } protected AnnotatedEObject annotateEObject(AnnotatedEObject fromObject, EObject next) { if (AnnotatedEObject.IS_IN_DEBUG_MODE) { return new AnnotatedEObject(next, fromObject, getAnnotation()); } else { return new AnnotatedEObject(next, AnnotatedEObject.NOT_IN_DEBUG_MODE_MESSAGE); } } private String getAnnotation() { if (annotation == null) { annotation = getVerboseDebugInfo(); } return annotation; } /** * Constructs a human-readable description of the OCL expression used as debug info for this * navigation step. This includes traveling up to the root expression in which the debug info * expression is embedded. */ private String getVerboseDebugInfo() { try { if (AnnotatedEObject.IS_IN_DEBUG_MODE) { StringBuilder result = new StringBuilder(); result.append("Step's expressions: "); for (OCLExpression debugInfo : getDebugInfo()) { result.append(debugInfo); OCLExpression root = OclHelper.getRootExpression(debugInfo); if (root != debugInfo) { result.append("\n ==== in expression =====\n"); result.append(root.accept(HighlightingToStringVisitor.getInstance(root, debugInfo))); } result.append(((getDefines(OclHelper.getRootExpression(debugInfo)) != null) ? "\n ===== which is the body of operation " + getDefines(OclHelper.getRootExpression(debugInfo)).getName() + " =====" : "")); } return result.toString(); } else { return AnnotatedEObject.NOT_IN_DEBUG_MODE_MESSAGE; } } catch (Exception e) { throw new RuntimeException(e); } } private EOperation getDefines(OCLExpression rootExpression) { EOperation result = null; if (rootExpression.eContainer() instanceof EAnnotation) { EAnnotation annotation = (EAnnotation) rootExpression.eContainer(); if (OCLCommon.isDelegateURI(annotation.getSource()) && annotation.eContainer() instanceof EOperation) { result = (EOperation) annotation.eContainer(); } } return result; } /** * The incrementing of the navigate counter gets its own protected method because subclasses * must be able to suppress incrementing under special circumstances * (e.g. suppress count of additional recursive round trip in IndirectingStep) */ protected void incrementNavigateCounter(Set<AnnotatedEObject> from){ navigateCounter++; } /** * Breaks down the navigation from the <tt>from</tt> set to the individual elements in <tt>from</tt> and manages the type * checks. * * @param cache * keys are lists of which the first element (index 0) is the {@link NavigationStep}, the second element (index 1) * the from-object (of type {@link AnnotatedEObject}) for which to look up any previously computed results. */ public Set<AnnotatedEObject> navigate(Set<AnnotatedEObject> from, TracebackCache cache, Notification changeEvent) { incrementNavigateCounter(from); Set<AnnotatedEObject> result = new HashSet<AnnotatedEObject>(from.size()); if (isAbsolute()) { from = Collections.singleton(null); } if (!isAlwaysEmpty()) { // don't do anything for empty steps for (AnnotatedEObject fromObject : from) { // for absolute steps, don't do the source type check and invoke just once, passing null for "from" if (isAbsolute() || doesSourceTypeMatch(fromObject)) { // use a copy of the TracebackCache if there are several objects in "from" // because for each fromObject, different variable scopes and values may result for (AnnotatedEObject singleResult : getFromCacheOrNavigate(fromObject, cache, changeEvent)) { if (AbstractTracer.doesTypeMatch(getTargetType(), singleResult)) { result.add(singleResult); } } } } } resultObjectsCounter += result.size(); return result; } protected boolean doesSourceTypeMatch(AnnotatedEObject fromObject) { return AbstractTracer.doesTypeMatch(getSourceType(), fromObject); } private Collection<AnnotatedEObject> getFromCacheOrNavigate(AnnotatedEObject fromObject, TracebackCache cache, Notification changeEvent) { Set<AnnotatedEObject> result; result = cache.get(this, fromObject); if (result == null) { cacheMisses++; result = navigate(fromObject, cache, changeEvent); cache.put(this, fromObject, result); } return result; } /** * By default, navigation steps depend on the object set to which they are applied. * * @return always <tt>false</tt> */ public boolean isAbsolute() { return false; } /** * By default it is expected that steps return non-empty sets in some cases. * @return always <tt>false</tt> */ public boolean isAlwaysEmpty() { return alwaysEmpty; } protected void setAlwaysEmpty() { if (!this.alwaysEmpty) { fireBeforeHashCodeChange(newTokenForFiringHashCodeChangeEvent()); this.alwaysEmpty = true; fireAfterHashCodeChange(newTokenForFiringHashCodeChangeEvent()); if(alwaysEmptyChangeListeners != null){ for (AlwaysEmptyChangeListener listener : alwaysEmptyChangeListeners) { listener.alwaysEmptyChanged(this); } } } } protected abstract Set<AnnotatedEObject> navigate(AnnotatedEObject fromObject, TracebackCache cache, Notification changeEvent); @Override public String toString() { Map<NavigationStep, Integer> visited = new HashMap<NavigationStep, Integer>(); return toString(visited, /* indent */ 0); } private int getCacheMisses() { return cacheMisses; } protected String toString(Map<NavigationStep, Integer> visited, int indent) { if (visited.containsKey(this)) { return "(#"+getCacheMisses()+"/"+getNavigateCounter()+") GOTO "+visited.get(this); } else { visited.put(this, id); return ""+id+"(#"+getCacheMisses()+"/"+getNavigateCounter()+"):" + "(" + (getSourceType() == null ? "null" : getSourceType().getName()) + ")" + contentToString(visited, indent) + "(" + (getTargetType() == null ? "null" : getTargetType().getName()) + ")"; } } public String contentToString(Map<NavigationStep, Integer> visited, int indent) { return ""; } /** * For <tt>a</tt> or <tt>b</tt> being <tt>null</tt> (a yet unresolved {@link IndirectingStep}, probablu), * we unfortunately don't know yet if there will be a non-empty subtype tree intersection. Therefore, * this method returns <tt>true</tt> if either of <tt>a</tt> or <tt>b</tt> is <tt>null</tt><p> */ protected static boolean haveIntersectingSubclassTree(EClass a, EClass b) { boolean result = a==null || b==null || a.equals(b); if (!result) { Collection<EClass> targetSubtypesIncludingTargetType = new HashSet<EClass>(AllSubclassesFinder.getInstance().getAllSubclasses(a)); targetSubtypesIncludingTargetType.add(a); if (targetSubtypesIncludingTargetType.contains(b)) { result = true; } else { Collection<EClass> sourceSubtypesIncludingSourceType = new HashSet<EClass>(AllSubclassesFinder.getInstance().getAllSubclasses(b)); sourceSubtypesIncludingSourceType.add(b); Collection<EClass> smaller; Collection<EClass> larger; if (targetSubtypesIncludingTargetType.size() < sourceSubtypesIncludingSourceType.size()) { smaller = targetSubtypesIncludingTargetType; larger = sourceSubtypesIncludingSourceType; } else { smaller = sourceSubtypesIncludingSourceType; larger = targetSubtypesIncludingTargetType; } for (Object fromSmaller : smaller) { if (larger.contains((EClassifier) fromSmaller)) { result = true; break; } } } } return result; } /** * The default size in particular for atomic navigation steps is <tt>1</tt>. */ public int size() { Set<NavigationStep> visited = new HashSet<NavigationStep>(); return size(visited); } /** * The default size in particular for atomic navigation steps is <tt>1</tt>. */ protected int size(Set<NavigationStep> visited) { if(visited.contains(this)){ return 0; }else{ visited.add(this); return 1; } } public int distinctSize(){ Set<SemanticIdentity> visited = new HashSet<SemanticIdentity>(); return distinctSize(visited); } protected int distinctSize(Set<SemanticIdentity> visited){ if(visited.contains(this.getSemanticIdentity())){ return 0; }else{ visited.add(this.getSemanticIdentity()); return 1; } } public int getId(){ return id; } public SemanticIdentity getSemanticIdentity() { return semanticIdentity; } /** * @return always non-<code>null</code>, but possibly empty set of expressions that form scopes that are left when this * step is navigated. */ public Set<Variable> getLeavingScopes(){ return Collections.unmodifiableSet(leavingScopes); } public void addLeavingScopes(Set<Variable> leavingScopes){ this.leavingScopes.addAll(leavingScopes); } /** * @return always non-<code>null</code>, but possibly empty set of expressions that form scopes that are entered when this * step is navigated. */ public Set<Variable> getEnteringScopes(){ return Collections.unmodifiableSet(enteringScopes); } public void addEnteringScopes(Set<Variable> enteringScope){ this.enteringScopes.addAll(enteringScope); } }