/******************************************************************************* * Copyright (c) 2012 Obeo. * 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: * Obeo - initial API and implementation *******************************************************************************/ package org.eclipse.mylyn.docs.intent.compare.match; import com.google.common.base.Predicate; import com.google.common.cache.LoadingCache; import com.google.common.collect.Iterables; import com.google.common.collect.Iterators; import com.google.common.collect.Sets; import java.util.Iterator; import java.util.Set; import org.eclipse.emf.common.notify.Notifier; import org.eclipse.emf.common.util.BasicMonitor; import org.eclipse.emf.common.util.EList; import org.eclipse.emf.common.util.Monitor; import org.eclipse.emf.common.util.URI; import org.eclipse.emf.compare.CompareFactory; import org.eclipse.emf.compare.Comparison; import org.eclipse.emf.compare.DifferenceKind; import org.eclipse.emf.compare.DifferenceSource; import org.eclipse.emf.compare.Match; import org.eclipse.emf.compare.diff.DefaultDiffEngine; import org.eclipse.emf.compare.diff.FeatureFilter; import org.eclipse.emf.compare.diff.IDiffProcessor; import org.eclipse.emf.compare.internal.spec.ComparisonSpec; import org.eclipse.emf.compare.match.DefaultEqualityHelperFactory; import org.eclipse.emf.compare.match.IEqualityHelperFactory; import org.eclipse.emf.compare.match.eobject.DefaultWeightProvider; import org.eclipse.emf.compare.match.eobject.ProximityEObjectMatcher.DistanceFunction; import org.eclipse.emf.compare.match.eobject.URIDistance; import org.eclipse.emf.compare.match.eobject.WeightProvider; import org.eclipse.emf.compare.utils.DiffUtil; import org.eclipse.emf.compare.utils.EqualityHelper; import org.eclipse.emf.compare.utils.IEqualityHelper; import org.eclipse.emf.compare.utils.ReferenceUtil; import org.eclipse.emf.ecore.EAttribute; import org.eclipse.emf.ecore.EObject; import org.eclipse.emf.ecore.EReference; import org.eclipse.emf.ecore.EStructuralFeature; import org.eclipse.emf.ecore.util.FeatureMap; import org.eclipse.emf.ecore.util.InternalEList; /** * This distance function implementation will actually compare the given EObject. * * @author <a href="mailto:cedric.brun@obeo.fr">Cedric Brun</a> */ // CHECKSTYLE:OFF public class EditionDistance implements DistanceFunction { /** * Weight coefficient of a change of location (uri). */ private int locationChangeCoef = 4; /** * Weight coefficient of a change of order within a reference. */ private int orderChangeCoef = 5; /** * The instance used to compare location of EObjects. */ private URIDistance uriDistance = new URIDistance(); /** * a thresholds ratio discrete function per the number of features. */ // CHECKSTYLE:OFF we know these are magic numbers, so be it, they happens to have the same value but there // is no semantic. private double[] thresholds = {0, 0.6, 0.6, 0.55, 0.465 }; /** * The fake comparison is used to make the diff engine super class happy. We are reusing the same instance * which we are updating because of the cost of adding even a single Match in it (and subsequent growing * of list) which gets very significant considering how much we are calling this during a single * comparison. */ public Comparison fakeComparison; /** * instance providing the weight for each feature. */ private WeightProvider weights = new DefaultWeightProvider(); private Notifier leftRoot; private Notifier rightRoot; private IntentCountingDiffEngine countingDiffEngine; /** * Instantiate a new Edition Distance. */ public EditionDistance(Notifier leftRoot, Notifier rightRoot) { this.leftRoot = leftRoot; this.rightRoot = rightRoot; this.countingDiffEngine = new IntentCountingDiffEngine(this, 0); IEqualityHelperFactory fakeEqualityHelperFactory = new DefaultEqualityHelperFactory() { @Override public IEqualityHelper createEqualityHelper() { final LoadingCache<EObject, URI> cache = EqualityHelper.createDefaultCache(getCacheBuilder()); return new EqualityHelper(cache) { @Override protected boolean matchingURIs(EObject object1, EObject object2) { /* * we might trying to compare children of the objects under scrutinity right now, it * might happen if a containment reference is seen as "relevant" for the matching * process. In those cases, we don't want to compare the whole uris and instead want * to compare just the current fragment. This has very important performance * implications. */ if (object1.eContainer() != null && object2.eContainer() != null && fakeComparison.getMatch(object1.eContainer()) != null) { return uriDistance.retrieveFragment(object1).equals( uriDistance.retrieveFragment(object2)); } return uriDistance.proximity(object1, object2) == 0; } }; } }; this.fakeComparison = new ComparisonSpec() { /* * We did override this method to avoid the extra cost of maintaining a cross referencer for such * a fake comparison. */ @Override public Match getMatch(EObject element) { for (Match m : getMatches()) { if (m.getLeft() == element || m.getRight() == element || m.getOrigin() == element) { return m; } /* * use a temporary variable as buffer for the "equal" boolean. We know that the following * try/catch block can, and will, only initialize it once ... but the compiler does not. */ else if (uriDistance.proximity(m.getLeft(), m.getRight()) == 0) { return m; } } return null; } }; Match fakeMatch = CompareFactory.eINSTANCE.createMatch(); ((InternalEList<Match>)fakeComparison.getMatches()).addUnique(fakeMatch); IEqualityHelper equalityHelper = fakeEqualityHelperFactory.createEqualityHelper(); fakeComparison.eAdapters().add(equalityHelper); equalityHelper.setTarget(fakeComparison); } /** * {@inheritDoc} */ public double distance(Comparison inProgress, EObject a, EObject b) { this.uriDistance.setComparison(inProgress); double maxDist = Math.max(getThresholdAmount(a), getThresholdAmount(b)); double measuredDist = new CountingDiffEngine(maxDist, this.fakeComparison).measureDifferences( inProgress, a, b); if (measuredDist > maxDist) { return Double.MAX_VALUE; } return measuredDist; } /** * {@inheritDoc} */ public boolean areIdentic(Comparison inProgress, EObject a, EObject b) { boolean areRoots = a.equals(leftRoot) && b.equals(rightRoot) || b.equals(leftRoot) && a.equals(rightRoot); return areRoots || countingDiffEngine.measureDifferences(inProgress, a, b) == 0; } /** * Create a new builder to instantiate and configure an EditionDistance. * * @param leftRoot * the left root of the comparison * @param rightRoot * the right root of the comparison * @return a configuration builder. */ public static Builder builder(Notifier leftRoot, Notifier rightRoot) { return new Builder(leftRoot, rightRoot); } /** * Builder class to configure an EditionDistance instance. */ public static class Builder { /** * The EditionDistance built by the builder. */ private EditionDistance toBeBuilt; /** * Create the builder. */ protected Builder(Notifier leftRoot, Notifier rightRoot) { this.toBeBuilt = new EditionDistance(leftRoot, rightRoot); } /** * Specify the weight of any change of uri between two instances. * * @param weight * the new weight. * @return the current builder instance. */ public Builder uri(int weight) { this.toBeBuilt.locationChangeCoef = weight; return this; } /** * Specify the weight of any change of reference order between two instances. * * @param weight * the new weight. * @return the current builder instance. */ public Builder order(int weight) { this.toBeBuilt.orderChangeCoef = weight; return this; } /** * Configure custom weight provider. * * @param provider * the weight provider to use. * @return the current builder instance. */ public Builder weightProvider(WeightProvider provider) { this.toBeBuilt.weights = provider; return this; } /** * return the configured instance. * * @return the configured instance. */ public EditionDistance build() { return toBeBuilt; } } /** * This class is an implementation of a {@link IDiffProcessor} which counts the number of differences to * given an overall distance between two objects. */ class CountingDiffProcessor implements IDiffProcessor { /** * Keeps track of features which have already been detected as changed so that we can apply different * weight in those cases. */ private Set<EStructuralFeature> alreadyChanged = Sets.newLinkedHashSet(); /** * The current distance. */ private double distance; /** * {@inheritDoc} */ public void referenceChange(Match match, EReference reference, EObject value, DifferenceKind kind, DifferenceSource source) { if (!alreadyChanged.contains(reference)) { switch (kind) { case MOVE: distance += weights.getWeight(reference) * orderChangeCoef; break; case ADD: case DELETE: case CHANGE: distance += weights.getWeight(reference); break; default: break; } alreadyChanged.add(reference); } else { distance += 1; } } /** * {@inheritDoc} */ public void attributeChange(Match match, EAttribute attribute, Object value, DifferenceKind kind, DifferenceSource source) { if (!alreadyChanged.contains(attribute)) { Object aValue = ReferenceUtil.safeEGet(match.getLeft(), attribute); Object bValue = ReferenceUtil.safeEGet(match.getRight(), attribute); switch (kind) { case MOVE: distance += weights.getWeight(attribute) * orderChangeCoef; break; case ADD: case DELETE: case CHANGE: if (aValue instanceof String && bValue instanceof String) { distance += weights.getWeight(attribute) * (1 - DiffUtil.diceCoefficient((String)aValue, (String)bValue)); } else { distance += weights.getWeight(attribute); } break; default: break; } alreadyChanged.add(attribute); } else { distance += 1; } } /** * {@inheritDoc} * * @see org.eclipse.emf.compare.diff.IDiffProcessor#resourceAttachmentChange(org.eclipse.emf.compare.Match, * java.lang.String, org.eclipse.emf.compare.DifferenceKind, * org.eclipse.emf.compare.DifferenceSource) */ public void resourceAttachmentChange(Match match, String uri, DifferenceKind kind, DifferenceSource source) { // Not important for the distance computation } /** * return the computed distance. * * @return the computed distance. */ public double getComputedDistance() { return distance; } /** * Clear the diff processor state so that it's ready for the next computation. */ public void reset() { this.alreadyChanged.clear(); } } /** * An implementation of a diff engine which count and measure the detected changes. */ class CountingDiffEngine extends DefaultDiffEngine { /** * The maximum distance until which we just have to stop. */ private double maxDistance; /** The comparison for which this engine will detect differences. */ private final Comparison comparison; /** * Create the diff engine. * * @param maxDistance * the maximum distance we might reach. * @param fakeComparison * the comparison instance to use while measuring the differences between the two objects. */ public CountingDiffEngine(double maxDistance, Comparison fakeComparison) { super(new CountingDiffProcessor()); this.maxDistance = maxDistance; // will always return the same instance. this.comparison = fakeComparison; } @Override protected void checkResourceAttachment(Match match, Monitor monitor) { /* * we really don't want to check that... */ } @Override protected void computeDifferences(Match match, EAttribute attribute, boolean checkOrdering) { if (getCounter().getComputedDistance() <= maxDistance) { super.computeDifferences(match, attribute, checkOrdering); } } @Override protected void computeDifferences(Match match, EReference reference, boolean checkOrdering) { if (getCounter().getComputedDistance() <= maxDistance) { super.computeDifferences(match, reference, checkOrdering); } } /** * Measure the difference between two objects and return a distance value. * * @param comparisonInProgress * the comparison which is currently being matched. * @param a * first object. * @param b * second object. * @return the distance between them computed using the number of changes required to change a to b. */ public double measureDifferences(Comparison comparisonInProgress, EObject a, EObject b) { Match fakeMatch = createOrUpdateFakeMatch(a, b); getCounter().reset(); double changes = 0; if (!haveSameContainer(comparisonInProgress, a, b)) { changes += locationChangeCoef * weights.getParentWeight(a); } else { int aIndex = getContainmentIndex(a); int bIndex = getContainmentIndex(b); if (aIndex != bIndex) { /* * we just want to pick the same positioned object if two exactly similar objects are * candidates in the same container. */ changes += 1; } } if (a.eContainingFeature() != b.eContainingFeature()) { changes += Math.max(weights.getContainingFeatureWeight(a), weights.getContainingFeatureWeight(b)); } if (changes <= maxDistance) { checkForDifferences(fakeMatch, new BasicMonitor()); changes += getCounter().getComputedDistance(); } return changes; } /** * return the position in which an Object is contained in its parent list. * * @param a * any EObject * @return the position in which an Object is contained in its parent list, 0 if there is no container * or if the reference is single valued. */ private int getContainmentIndex(EObject a) { EStructuralFeature feat = a.eContainingFeature(); EObject container = a.eContainer(); int position = 0; if (container != null) { if (feat instanceof EAttribute) { position = indexFromFeatureMap(a, feat, container); } else if (feat != null) { if (feat.isMany()) { EList<?> eList = (EList<?>)container.eGet(feat, false); position = eList.indexOf(a); } } } return position; } /** * the position of the {@link EObject} a in its container featureMap. * * @param a * the {@link EObject}. * @param feat * the containing feature. * @param container * the containing EObject. * @return the position of the {@link EObject} a in its container featureMap. */ private int indexFromFeatureMap(EObject a, EStructuralFeature feat, EObject container) { FeatureMap featureMap = (FeatureMap)container.eGet(feat, false); for (int i = 0, size = featureMap.size(); i < size; ++i) { if (featureMap.getValue(i) == a) { EStructuralFeature entryFeature = featureMap.getEStructuralFeature(i); if (entryFeature instanceof EReference && ((EReference)entryFeature).isContainment()) { return i; } } } return 0; } /** * Check whether two {@link EObject} have the same containers or not. * * @param inProgress * the comparison currently being matched. * @param a * any {@link EObject} * @param b * any other {@link EObject} * @return true if they have the same container. If the containers have been matched their match will * be used, on the contrary the URI will be indirectly use through the EqualityHelper. */ private boolean haveSameContainer(Comparison inProgress, EObject a, EObject b) { EObject aContainer = a.eContainer(); EObject bContainer = b.eContainer(); if ((aContainer == null && bContainer != null) || (aContainer != null && bContainer == null)) { return false; } /* * we consider two null containers as being the "same". */ boolean matching = aContainer == null && bContainer == null; Match mA = inProgress.getMatch(aContainer); Match mB = inProgress.getMatch(bContainer); if (!matching) { if (mA == null && mB == null) { /* * The Objects have to be out of scope then. */ matching = fakeComparison.getEqualityHelper().matchingValues(aContainer, bContainer); } else { matching = isReferencedByTheMatch(bContainer, mA) || isReferencedByTheMatch(aContainer, mB); } } return matching; } /** * Return true if the given {@link EObject} is referenced by the left/right or origin match reference. * * @param eObj * any {@link EObject}. * @param match * any Match. * @return true if the given {@link EObject} is referenced by the left/right or origin match * reference. */ private boolean isReferencedByTheMatch(EObject eObj, Match match) { return match != null && (match.getRight() == eObj || match.getLeft() == eObj || match.getOrigin() == eObj); } /** * Create a mock {@link Match} between the two given EObjects so that we can use the exposed * {@link #checkForDifferences(Match, org.eclipse.emf.common.util.Monitor)} method to check for * differences. * * @param a * First of the two EObjects for which we want to force a comparison. * @param b * Second of the two EObjects for which we want to force a comparison. * @return The created Match. */ private Match createOrUpdateFakeMatch(EObject a, EObject b) { Match fakeMatch = comparison.getMatches().get(0); fakeMatch.setLeft(a); fakeMatch.setRight(b); return fakeMatch; } protected CountingDiffProcessor getCounter() { return (CountingDiffProcessor)getDiffProcessor(); } @Override protected FeatureFilter createFeatureFilter() { return new FeatureFilter() { @Override public Iterator<EReference> getReferencesToCheck(Match match) { return Iterators.filter(super.getReferencesToCheck(match), new Predicate<EReference>() { public boolean apply(EReference input) { return weights.getWeight(input) != 0; } }); } @Override public Iterator<EAttribute> getAttributesToCheck(Match match) { return Iterators.filter(super.getAttributesToCheck(match), new Predicate<EAttribute>() { public boolean apply(EAttribute input) { return weights.getWeight(input) != 0; } }); } }; } } /** * {@inheritDoc} */ public double getThresholdAmount(EObject eObj) { Predicate<EStructuralFeature> featureFilter = new Predicate<EStructuralFeature>() { public boolean apply(EStructuralFeature feat) { return weights.getWeight(feat) != 0; } }; // When can you safely says these are not the same EObjects *at all* ? // lets consider every feature which is set, and add this in the max distance. // and then tweak the max value adding half a location change // thats very empirical... and might be wrong in the end, but it gives pretty good results with // Ecore so I'll try to gather as much as test data I can and add the corresponding test to be able to // assess the quality of further changes. int max = 0; int nbFeatures = 0; for (EReference feat : Iterables.filter(eObj.eClass().getEAllReferences(), featureFilter)) { if (eObj.eIsSet(feat)) { max += weights.getWeight(feat); nbFeatures++; } } for (EAttribute feat : Iterables.filter(eObj.eClass().getEAllAttributes(), featureFilter)) { if (eObj.eIsSet(feat)) { max += weights.getWeight(feat); nbFeatures++; } } // max = max + (locationChangeCoef * weights.getParentWeight(eObj)); max = max + weights.getContainingFeatureWeight(eObj); return max * getThresholdRatio(nbFeatures); } /** * return a ratio to appli on the amount of maximum un-similarity amount depending on the number of * features which are considered. * * @param nbFeatures * the nb of features which should be considerd to compute the amount. * @return a ratio to appli on the amount of maximum un-similarity amount depending on the number of * features which are considered. */ private double getThresholdRatio(int nbFeatures) { if (nbFeatures >= thresholds.length) { return 0.465d; } return thresholds[nbFeatures]; } }