blob: 32f87ee576b1595b4553d46364b3b9a1ad3a3488 [file] [log] [blame]
/*******************************************************************************
* 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.ProximityEObjectMatcher.DistanceFunction;
import org.eclipse.emf.compare.match.eobject.URIDistance;
import org.eclipse.emf.compare.match.eobject.WeightProvider;
import org.eclipse.emf.compare.match.eobject.internal.ReflectiveWeightProvider;
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 ReflectiveWeightProvider();
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];
}
}