package org.eclipse.bpel.compare.ui.gef.bpel; | |
/******************************************************************************* | |
* Copyright (c) 2006, 2012, 2008 Obeo. | |
* All rights reserved. This program and the accompanying materials | |
* are made available under the terms of the Eclipse Public License 2.0 | |
* which accompanies this distribution, and is available at | |
* https://www.eclipse.org/legal/epl-2.0/ | |
* | |
* SPDX-License-Identifier: EPL-2.0 | |
* | |
* Contributors: | |
* Obeo - initial API and implementation | |
*******************************************************************************/ | |
import java.util.ArrayList; | |
import java.util.Collection; | |
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.EMFPlugin; | |
import org.eclipse.emf.common.util.BasicMonitor; | |
import org.eclipse.emf.common.util.Monitor; | |
import org.eclipse.emf.common.util.TreeIterator; | |
import org.eclipse.emf.compare.EMFComparePlugin; | |
import org.eclipse.emf.compare.FactoryException; | |
import org.eclipse.emf.compare.match.MatchOptions; | |
import org.eclipse.emf.compare.match.engine.IMatchEngine; | |
import org.eclipse.emf.compare.match.internal.statistic.NameSimilarity; | |
import org.eclipse.emf.compare.match.internal.statistic.StructureSimilarity; | |
import org.eclipse.emf.compare.match.metamodel.Match2Elements; | |
import org.eclipse.emf.compare.match.metamodel.Match3Elements; | |
import org.eclipse.emf.compare.match.metamodel.MatchElement; | |
import org.eclipse.emf.compare.match.metamodel.MatchFactory; | |
import org.eclipse.emf.compare.match.metamodel.MatchModel; | |
import org.eclipse.emf.compare.match.metamodel.Side; | |
import org.eclipse.emf.compare.match.metamodel.UnmatchElement; | |
import org.eclipse.emf.compare.match.statistic.MetamodelFilter; | |
import org.eclipse.emf.compare.util.EFactory; | |
import org.eclipse.emf.compare.util.EMFCompareMap; | |
import org.eclipse.emf.compare.util.EMFComparePreferenceConstants; | |
import org.eclipse.emf.compare.util.EclipseModelUtils; | |
import org.eclipse.emf.ecore.EObject; | |
import org.eclipse.emf.ecore.EReference; | |
import org.eclipse.emf.ecore.EStructuralFeature; | |
import org.eclipse.emf.ecore.resource.Resource; | |
import org.eclipse.emf.ecore.resource.ResourceSet; | |
import org.eclipse.emf.ecore.util.EcoreUtil; | |
import org.eclipse.emf.ecore.xmi.XMIResource; | |
/** | |
* These services are useful when one wants to compare models more precisely using the method modelDiff. | |
* | |
* @author <a href="mailto:cedric.brun@obeo.fr">Cedric Brun</a> | |
*/ | |
public class BPELMatchEngine implements IMatchEngine { | |
//public class BPELMatchEngine extends GenericMatchEngine { | |
/** Used while computing similarity, this defines the general threshold. */ | |
private static final double GENERAL_THRESHOLD = 0.96d; | |
/** Containmnent reference for the matched elements root. */ | |
private static final String MATCH_ELEMENT_NAME = "matchedElements"; //$NON-NLS-1$ | |
/** | |
* Minimal number of attributes an element must have for content comparison. | |
*/ | |
private static final int MIN_ATTRIBUTES_COUNT = 5; | |
/** This constant is used as key for the buffering of name similarity. */ | |
private static final char NAME_SIMILARITY = 'n'; | |
/** This constant is used as key for the buffering of relations similarity. */ | |
private static final char RELATION_SIMILARITY = 'r'; | |
/** Containmnent reference for {@link MatchElement}s' submatches. */ | |
private static final String SUBMATCH_ELEMENT_NAME = "subMatchElements"; //$NON-NLS-1$ | |
/** This constant is used as key for the buffering of type similarity. */ | |
private static final char TYPE_SIMILARITY = 't'; | |
/** Containmnent reference for the {@link MatchModel}'s unmatched elements. */ | |
private static final String UNMATCH_ELEMENT_NAME = "unmatchedElements"; //$NON-NLS-1$ | |
/** This constant is used as key for the buffering of value similarity. */ | |
private static final char VALUE_SIMILARITY = 'v'; | |
/** | |
* {@link MetamodelFilter} used for filtering unused features of the objects we're computing the | |
* similarity for. | |
*/ | |
protected final MetamodelFilter filter = new MetamodelFilter(); | |
/** Contains the options given to the match procedure. */ | |
protected final Map<String, Object> options = new EMFCompareMap<String, Object>(); | |
/** | |
* This map allows us memorize the {@link EObject} we've been able to match thanks to their functional ID. | |
*/ | |
private final Map<String, EObject> matchedByID = new EMFCompareMap<String, EObject>(); | |
/** | |
* This map allows us memorize the {@link EObject} we've been able to match thanks to their XMI ID. | |
*/ | |
private final Map<String, EObject> matchedByXMIID = new EMFCompareMap<String, EObject>(); | |
/** | |
* This map is used to cache the comparison results Pair(Element1, Element2) => [nameSimilarity, | |
* valueSimilarity, relationSimilarity, TypeSimilarity]. | |
*/ | |
private final Map<String, Double> metricsCache = new EMFCompareMap<String, Double>(); | |
/** | |
* This list allows us to memorize the unMatched elements for a three-way comparison.<br/> | |
* <p> | |
* More specifically, we will populate this list with the {@link UnmatchElement}s created by the | |
* comparison between the left and the ancestor model, followed by the {@link UnmatchElement} created by | |
* the comparison between the right and the ancestor model.<br/> Those {@link UnmatchElement} will then | |
* be filtered to retain only those that actually cannot be matched. | |
* </p> | |
*/ | |
private final Set<EObject> remainingUnMatchedElements = new HashSet<EObject>(); | |
/** | |
* This list will be intensively used while matching elements to keep track of the unmatched ones from the | |
* left model. | |
*/ | |
private final List<EObject> stillToFindFromModel1 = new ArrayList<EObject>(); | |
/** | |
* This list will be intensively used while matching elements to keep track of the unmatched ones from the | |
* right model. | |
*/ | |
private final List<EObject> stillToFindFromModel2 = new ArrayList<EObject>(); | |
/** | |
* The options map must be initialized to avoid potential NPEs. This initializer will take care of this | |
* issue. | |
*/ | |
{ | |
options.putAll(loadPreferenceOptionMap()); | |
} | |
/** | |
* {@inheritDoc} | |
* | |
* @see org.eclipse.emf.compare.match.api.IMatchEngine#contentMatch(org.eclipse.emf.ecore.EObject, | |
* org.eclipse.emf.ecore.EObject, org.eclipse.emf.ecore.EObject, java.util.Map) | |
*/ | |
public MatchModel contentMatch(EObject leftObject, EObject rightObject, EObject ancestor, | |
Map<String, Object> optionMap) { | |
final MatchModel root = MatchFactory.eINSTANCE.createMatchModel(); | |
setModelRoots(root, leftObject.eResource(), rightObject.eResource(), ancestor.eResource()); | |
final Monitor monitor = createProgressMonitor(); | |
final MatchModel leftObjectAncestorMatch = contentMatch(leftObject, ancestor, optionMap); | |
final MatchModel rightObjectAncestorMatch = contentMatch(rightObject, ancestor, optionMap); | |
final List<MatchElement> leftObjectMatchedElements = new ArrayList<MatchElement>( | |
leftObjectAncestorMatch.getMatchedElements()); | |
final List<MatchElement> rightObjectMatchedElements = new ArrayList<MatchElement>( | |
rightObjectAncestorMatch.getMatchedElements()); | |
// populates the unmatched elements list for later use | |
for (Object unMatch : leftObjectAncestorMatch.getUnmatchedElements()) | |
remainingUnMatchedElements.add(((UnmatchElement)unMatch).getElement()); | |
for (Object unMatch : rightObjectAncestorMatch.getUnmatchedElements()) | |
remainingUnMatchedElements.add(((UnmatchElement)unMatch).getElement()); | |
try { | |
Match3Elements subMatchRoot = null; | |
if (leftObjectMatchedElements.size() > 0 && rightObjectMatchedElements.size() > 0) { | |
final Match2Elements leftObjectMatchRoot = (Match2Elements)leftObjectMatchedElements.get(0); | |
final Match2Elements rightObjectMatchRoot = (Match2Elements)rightObjectMatchedElements.get(0); | |
subMatchRoot = MatchFactory.eINSTANCE.createMatch3Elements(); | |
subMatchRoot.setSimilarity(absoluteMetric(leftObjectMatchRoot.getLeftElement(), | |
rightObjectMatchRoot.getLeftElement(), rightObjectMatchRoot.getRightElement())); | |
subMatchRoot.setLeftElement(leftObjectMatchRoot.getLeftElement()); | |
subMatchRoot.setRightElement(rightObjectMatchRoot.getLeftElement()); | |
subMatchRoot.setOriginElement(rightObjectMatchRoot.getRightElement()); | |
redirectedAdd(root, MATCH_ELEMENT_NAME, subMatchRoot); | |
createSub3Match(root, subMatchRoot, leftObjectMatchRoot, rightObjectMatchRoot); | |
} else { | |
for (EObject left : leftObjectMatchedElements) | |
stillToFindFromModel1.add(left); | |
for (EObject right : rightObjectMatchedElements) | |
stillToFindFromModel2.add(right); | |
} | |
// We will now check through the unmatched object for matches. | |
processUnmatchedElements(root, subMatchRoot); | |
// #createSub3Match(MatchModel, Match3Element, Match2Elements, | |
// Match2Elements) will have updated "remainingUnMatchedElements" | |
final Set<EObject> remainingLeft = new HashSet<EObject>(); | |
final Set<EObject> remainingRight = new HashSet<EObject>(); | |
for (EObject unMatched : remainingUnMatchedElements) { | |
if (unMatched.eResource() == leftObject.eResource()) { | |
remainingLeft.add(unMatched); | |
final TreeIterator<EObject> iterator = unMatched.eAllContents(); | |
while (iterator.hasNext()) | |
remainingLeft.add(iterator.next()); | |
} else if (unMatched.eResource() == rightObject.eResource()) { | |
remainingRight.add(unMatched); | |
final TreeIterator<EObject> iterator = unMatched.eAllContents(); | |
while (iterator.hasNext()) | |
remainingRight.add(iterator.next()); | |
} | |
} | |
stillToFindFromModel1.clear(); | |
stillToFindFromModel2.clear(); | |
final List<Match2Elements> mappings = mapLists(new ArrayList<EObject>(remainingLeft), | |
new ArrayList<EObject>(remainingRight), this | |
.<Integer> getOption(MatchOptions.OPTION_SEARCH_WINDOW), monitor); | |
for (Match2Elements map : mappings) { | |
final Match3Elements subMatch = MatchFactory.eINSTANCE.createMatch3Elements(); | |
subMatch.setLeftElement(map.getLeftElement()); | |
subMatch.setRightElement(map.getRightElement()); | |
if (subMatchRoot == null) | |
redirectedAdd(root, MATCH_ELEMENT_NAME, subMatch); | |
else | |
redirectedAdd(subMatchRoot, SUBMATCH_ELEMENT_NAME, subMatch); | |
} | |
final Map<EObject, Boolean> unMatchedElements = new EMFCompareMap<EObject, Boolean>(); | |
for (EObject remoteUnMatch : stillToFindFromModel1) { | |
unMatchedElements.put(remoteUnMatch, true); | |
} | |
for (EObject unMatch : stillToFindFromModel2) { | |
unMatchedElements.put(unMatch, false); | |
} | |
createThreeWayUnmatchElements(root, unMatchedElements); | |
} catch (FactoryException e) { | |
EMFComparePlugin.log(e, false); | |
} catch (InterruptedException e) { | |
// Cannot be thrown since we have no monitor | |
} | |
return root; | |
} | |
/** | |
* {@inheritDoc} | |
* | |
* @see org.eclipse.emf.compare.match.api.IMatchEngine#contentMatch(org.eclipse.emf.ecore.EObject, | |
* org.eclipse.emf.ecore.EObject, java.util.Map) | |
*/ | |
public MatchModel contentMatch(EObject leftObject, EObject rightObject, Map<String, Object> optionMap) { | |
if (optionMap != null && optionMap.size() > 0) | |
loadOptionMap(optionMap); | |
final Monitor monitor = createProgressMonitor(); | |
final MatchModel root = MatchFactory.eINSTANCE.createMatchModel(); | |
setModelRoots(root, leftObject.eResource(), rightObject.eResource()); | |
/* | |
* As we could very well be passed two EClasses (as opposed to modelMatch which compares all roots of | |
* a resource), we cannot filter the model. | |
*/ | |
final Set<EObject> still1 = new HashSet<EObject>(); | |
final Set<EObject> still2 = new HashSet<EObject>(); | |
// navigate through both objects at the same time and realize mappings.. | |
try { | |
if (!this.<Boolean> getOption(MatchOptions.OPTION_IGNORE_XMI_ID)) | |
matchByXMIID(leftObject, rightObject); | |
if (!this.<Boolean> getOption(MatchOptions.OPTION_IGNORE_ID)) | |
matchByID(leftObject, rightObject); | |
if (isSimilar(leftObject, rightObject)) { | |
stillToFindFromModel1.clear(); | |
stillToFindFromModel2.clear(); | |
final Match2Elements matchModelRoot = recursiveMappings(leftObject, rightObject, monitor); | |
redirectedAdd(root, MATCH_ELEMENT_NAME, matchModelRoot); | |
createSubMatchElements(matchModelRoot, new ArrayList<EObject>(stillToFindFromModel1), | |
new ArrayList<EObject>(stillToFindFromModel2), monitor); | |
still1.addAll(stillToFindFromModel1); | |
still2.addAll(stillToFindFromModel2); | |
createUnmatchElements(root, still1, Side.LEFT); | |
createUnmatchElements(root, still2, Side.RIGHT); | |
} else { | |
// The two objects passed as this method's parameters are not | |
// similar. Creates unmatch root. | |
still1.add(leftObject); | |
still1.add(rightObject); | |
createUnmatchElements(root, still1, null); | |
} | |
} catch (FactoryException e) { | |
EMFComparePlugin.log(e, false); | |
} catch (InterruptedException e) { | |
// Cannot be thrown since we have no monitor | |
} | |
return root; | |
} | |
/** | |
* {@inheritDoc} | |
* | |
* @see org.eclipse.emf.compare.match.api.IMatchEngine#modelMatch(org.eclipse.emf.ecore.EObject, | |
* org.eclipse.emf.ecore.EObject, org.eclipse.emf.ecore.EObject, java.util.Map) | |
*/ | |
public MatchModel modelMatch(EObject leftRoot, EObject rightRoot, EObject ancestor, | |
Map<String, Object> optionMap) throws InterruptedException { | |
if (optionMap != null && optionMap.size() > 0) | |
loadOptionMap(optionMap); | |
MatchModel result = null; | |
// Creates and sizes progress monitor | |
final Monitor monitor = createProgressMonitor(); | |
int size = 1; | |
for (EObject root : leftRoot.eResource().getContents()) { | |
final Iterator<EObject> rootContent = root.eAllContents(); | |
while (rootContent.hasNext()) { | |
rootContent.next(); | |
size++; | |
} | |
} | |
startMonitor(monitor, size * 2); | |
result = doMatch(leftRoot.eResource(), rightRoot.eResource(), ancestor.eResource(), monitor); | |
return result; | |
} | |
/** | |
* {@inheritDoc} | |
* | |
* @see org.eclipse.emf.compare.match.api.IMatchEngine#modelMatch(org.eclipse.emf.ecore.EObject, | |
* org.eclipse.emf.ecore.EObject, java.util.Map) | |
*/ | |
public MatchModel modelMatch(EObject leftRoot, EObject rightRoot, Map<String, Object> optionMap) | |
throws InterruptedException { | |
if (optionMap != null && optionMap.size() > 0) | |
loadOptionMap(optionMap); | |
MatchModel result = null; | |
// Creates and sizes progress monitor | |
final Monitor monitor = createProgressMonitor(); | |
int size = 1; | |
for (EObject root : leftRoot.eResource().getContents()) { | |
final Iterator<EObject> rootContent = root.eAllContents(); | |
while (rootContent.hasNext()) { | |
rootContent.next(); | |
size++; | |
} | |
} | |
startMonitor(monitor, size); | |
result = doMatch(leftRoot.eResource(), rightRoot.eResource(), monitor); | |
return result; | |
} | |
/** | |
* {@inheritDoc} | |
* | |
* @see org.eclipse.emf.compare.match.api.IMatchEngine#reset() | |
*/ | |
public void reset() { | |
filter.clear(); | |
options.clear(); | |
matchedByID.clear(); | |
matchedByXMIID.clear(); | |
metricsCache.clear(); | |
remainingUnMatchedElements.clear(); | |
stillToFindFromModel1.clear(); | |
stillToFindFromModel2.clear(); | |
options.putAll(loadPreferenceOptionMap()); | |
} | |
/** | |
* {@inheritDoc} | |
* | |
* @see org.eclipse.emf.compare.match.api.IMatchEngine#resourceMatch(org.eclipse.emf.ecore.resource.Resource, | |
* org.eclipse.emf.ecore.resource.Resource, java.util.Map) | |
*/ | |
public MatchModel resourceMatch(Resource leftResource, Resource rightResource, | |
Map<String, Object> optionMap) throws InterruptedException { | |
if (optionMap != null && optionMap.size() > 0) | |
loadOptionMap(optionMap); | |
MatchModel result = null; | |
// Creates and sizes progress monitor | |
final Monitor monitor = createProgressMonitor(); | |
int size = 1; | |
for (EObject root : leftResource.getContents()) { | |
final Iterator<EObject> rootContent = root.eAllContents(); | |
while (rootContent.hasNext()) { | |
rootContent.next(); | |
size++; | |
} | |
} | |
startMonitor(monitor, size); | |
result = doMatch(leftResource, rightResource, monitor); | |
return result; | |
} | |
/** | |
* {@inheritDoc} | |
* | |
* @see org.eclipse.emf.compare.match.api.IMatchEngine#resourceMatch(org.eclipse.emf.ecore.resource.Resource, | |
* org.eclipse.emf.ecore.resource.Resource, org.eclipse.emf.ecore.resource.Resource, java.util.Map) | |
*/ | |
public MatchModel resourceMatch(Resource leftResource, Resource rightResource, Resource ancestorResource, | |
Map<String, Object> optionMap) throws InterruptedException { | |
if (optionMap != null && optionMap.size() > 0) | |
loadOptionMap(optionMap); | |
MatchModel result = null; | |
// Creates and sizes progress monitor | |
final Monitor monitor = createProgressMonitor(); | |
int size = 1; | |
for (EObject root : leftResource.getContents()) { | |
final Iterator<EObject> rootContent = root.eAllContents(); | |
while (rootContent.hasNext()) { | |
rootContent.next(); | |
size++; | |
} | |
} | |
startMonitor(monitor, size * 2); | |
result = doMatch(leftResource, rightResource, ancestorResource, monitor); | |
return result; | |
} | |
/** | |
* {@inheritDoc} | |
* | |
* @see org.eclipse.emf.compare.match.api.IMatchEngine#resourceSetMatch(org.eclipse.emf.ecore.resource.ResourceSet, | |
* org.eclipse.emf.ecore.resource.ResourceSet, java.util.Map) | |
*/ | |
public MatchModel resourceSetMatch(ResourceSet leftResourceSet, ResourceSet rightResourceSet, | |
Map<String, Object> optionMap) { | |
// TODO this should be implemented. It will break both match and diff | |
// MMs so wait till 0.9/1.0. | |
throw new UnsupportedOperationException("Not implemented yet."); //$NON-NLS-1$ | |
} | |
/** | |
* {@inheritDoc} | |
* | |
* @see org.eclipse.emf.compare.match.api.IMatchEngine#resourceSetMatch(org.eclipse.emf.ecore.resource.ResourceSet, | |
* org.eclipse.emf.ecore.resource.ResourceSet, org.eclipse.emf.ecore.resource.ResourceSet, | |
* java.util.Map) | |
*/ | |
public MatchModel resourceSetMatch(ResourceSet leftResourceSet, ResourceSet rightResourceSet, | |
ResourceSet ancestorResourceSet, Map<String, Object> optionMap) { | |
// TODO this should be implemented. It will break both match and diff | |
// MMs so wait till 0.9/1.0. | |
throw new UnsupportedOperationException("Not implemented yet."); //$NON-NLS-1$ | |
} | |
/** | |
* This will compute the similarity between two {@link EObject}s' contents. | |
* | |
* @param obj1 | |
* First of the two {@link EObject}s. | |
* @param obj2 | |
* Second of the two {@link EObject}s. | |
* @return <code>double</code> representing the similarity between the two {@link EObject}s' contents. | |
* 0 < value < 1. | |
* @throws FactoryException | |
* Thrown if we cannot compute the {@link EObject}s' contents similarity metrics. | |
* @see NameSimilarity#contentValue(EObject, MetamodelFilter) | |
*/ | |
protected double contentSimilarity(EObject obj1, EObject obj2) throws FactoryException { | |
double similarity = 0d; | |
final Double value = getSimilarityFromCache(obj1, obj2, VALUE_SIMILARITY); | |
if (value != null) { | |
similarity = value; | |
} else { | |
similarity = NameSimilarity.nameSimilarityMetric(NameSimilarity.contentValue(obj1, filter), | |
NameSimilarity.contentValue(obj2, filter)); | |
setSimilarityInCache(obj1, obj2, VALUE_SIMILARITY, similarity); | |
} | |
return similarity; | |
} | |
/** | |
* This will iterate through the given {@link List} and return its element which is most similar (as given | |
* by {@link #absoluteMetric(EObject, EObject)}) to the given {@link EObject}. | |
* | |
* @param eObj | |
* {@link EObject} we're searching a similar item for in the list. | |
* @param list | |
* {@link List} in which we are to find an object similar to <code>eObj</code>. | |
* @return The element from <code>list</code> which is the most similar to <code>eObj</code>. | |
* @throws FactoryException | |
* Thrown if we cannot compute the {@link #absoluteMetric(EObject, EObject) absolute metric} | |
* between <code>eObj</code> and one of the list's objects. | |
*/ | |
protected EObject findMostSimilar(EObject eObj, List<EObject> list) throws FactoryException { | |
double max = 0d; | |
EObject resultObject = null; | |
final Iterator<EObject> it = list.iterator(); | |
while (it.hasNext() && max != 1.0d) { | |
final EObject next = it.next(); | |
if (this.<Boolean> getOption(MatchOptions.OPTION_DISTINCT_METAMODELS) | |
|| eObj.eClass() == next.eClass()) { | |
final double similarity = absoluteMetric(eObj, next); | |
if (similarity > max) { | |
max = similarity; | |
resultObject = next; | |
} | |
} | |
} | |
return resultObject; | |
} | |
/** | |
* This will return the value associated to the given key in the options map. | |
* <p> | |
* NOTE : Misuses of this method will easily throw {@link ClassCastException}s. | |
* </p> | |
* | |
* @param <T> | |
* Expected type of the value associated to <code>key</code>. | |
* @param key | |
* Key of the value to retrieve. | |
* @return Value associated to the given key in the options map. | |
* @throws ClassCastException | |
* If the value isn't assignment compatible with the expected type. | |
*/ | |
@SuppressWarnings("unchecked") | |
protected <T> T getOption(String key) throws ClassCastException { | |
return (T)options.get(key); | |
} | |
/** | |
* This will lookup in the {@link #matchedByID} map and check if the two given objects have indeed been | |
* matched by their ID. | |
* | |
* @param left | |
* Left of the two objects to check. | |
* @param right | |
* right of the two objects to check. | |
* @return <code>True</code> these objects haven't been matched by their ID, <code>False</code> | |
* otherwise. | |
* @throws FactoryException | |
* Thrown if we cannot compute the key for the object to match. | |
*/ | |
protected boolean haveDistinctID(EObject left, EObject right) throws FactoryException { | |
boolean result = false; | |
final StringBuilder leftKey = new StringBuilder(); | |
leftKey.append(NameSimilarity.findName(left)); | |
leftKey.append(left.hashCode()); | |
final EObject matched = matchedByID.get(leftKey.toString()); | |
// must be the same instance | |
if (matched != null) | |
result = matched != right; | |
else | |
// we didn't match a single element with this ID. | |
// This could be because no IDs are defined. | |
result = EcoreUtil.getID(left) != null; | |
return result; | |
} | |
/** | |
* This will lookup in the {@link #matchedByXMIID} map and check if the two given objects have indeed been | |
* matched by their XMI ID. | |
* | |
* @param left | |
* Left of the two objects to check. | |
* @param right | |
* right of the two objects to check. | |
* @return <code>True</code> these objects haven't been matched by their XMI ID, <code>False</code> | |
* otherwise. | |
* @throws FactoryException | |
* Thrown if we cannot compute the key for the object to match. | |
*/ | |
protected boolean haveDistinctXMIID(EObject left, EObject right) throws FactoryException { | |
boolean result = false; | |
final StringBuilder leftKey = new StringBuilder(); | |
leftKey.append(NameSimilarity.findName(left)); | |
leftKey.append(left.hashCode()); | |
final EObject matched = matchedByXMIID.get(leftKey.toString()); | |
// must be the same instance | |
if (matched != null) | |
result = matched != right; | |
else | |
// we didn't match a single element with this ID. | |
// This could be because no IDs are defined. | |
result = left.eResource() instanceof XMIResource | |
&& ((XMIResource)left.eResource()).getID(left) != null; | |
return result; | |
} | |
/** | |
* Returns <code>True</code> if the 2 given {@link EObject}s are considered similar. | |
* | |
* @param obj1 | |
* The first {@link EObject} to compare. | |
* @param obj2 | |
* Second of the {@link EObject}s to compare. | |
* @return <code>True</code> if both elements have the same serialization ID, <code>False</code> | |
* otherwise. | |
* @throws FactoryException | |
* Thrown if we cannot compute one of the needed similarity. | |
*/ | |
protected boolean isSimilar(EObject obj1, EObject obj2) throws FactoryException { | |
boolean similar = false; | |
// Defines threshold constants to assume objects' similarity | |
final double nameOnlyMetricThreshold = 0.7d; | |
final double fewerAttributesNameThreshold = 0.8d; | |
final double relationsThreshold = 0.3d; | |
final double nameThreshold = 0.2d; | |
final double contentThreshold = 0.9d; | |
final double triWayThreshold = 0.9d; | |
final double generalThreshold = GENERAL_THRESHOLD; | |
// Computes some of the required metrics | |
final double nameSimilarity = nameSimilarity(obj1, obj2); | |
final boolean hasSameUri = hasSameUri(obj1, obj2); | |
final int obj1NonNullFeatures = nonNullFeaturesCount(obj1); | |
final int obj2NonNullFeatures = nonNullFeaturesCount(obj2); | |
if (!this.<Boolean> getOption(MatchOptions.OPTION_DISTINCT_METAMODELS) | |
&& obj1.eClass() != obj2.eClass()) { | |
similar = false; | |
} else if (!this.<Boolean> getOption(MatchOptions.OPTION_IGNORE_ID) && haveDistinctID(obj1, obj2)) { | |
similar = false; | |
} else if (!this.<Boolean> getOption(MatchOptions.OPTION_IGNORE_XMI_ID) | |
&& haveDistinctXMIID(obj1, obj2)) { | |
similar = false; | |
} else if (nameSimilarity == 1) { | |
similar = true; | |
// softer tests if we don't have enough attributes to compare the | |
// objects | |
} else if (obj1NonNullFeatures == 1 && obj2NonNullFeatures == 1) { | |
similar = nameSimilarity > nameOnlyMetricThreshold; | |
} else if (nameSimilarity > fewerAttributesNameThreshold | |
&& obj1NonNullFeatures <= MIN_ATTRIBUTES_COUNT && obj2NonNullFeatures <= MIN_ATTRIBUTES_COUNT | |
&& typeSimilarity(obj1, obj2) > generalThreshold) { | |
similar = true; | |
} else { | |
final double contentSimilarity = contentSimilarity(obj1, obj2); | |
final double relationsSimilarity = relationsSimilarity(obj1, obj2); | |
if (relationsSimilarity == 1 && hasSameUri && nameSimilarity > nameThreshold) { | |
similar = true; | |
} else if (contentSimilarity == 1 && relationsSimilarity == 1) { | |
similar = true; | |
} else if (contentSimilarity > generalThreshold && relationsSimilarity > relationsThreshold | |
&& nameSimilarity > nameThreshold) { | |
similar = true; | |
} else if (relationsSimilarity > generalThreshold && contentSimilarity > contentThreshold) { | |
similar = true; | |
} else if (contentSimilarity > triWayThreshold && nameSimilarity > triWayThreshold | |
&& relationsSimilarity > triWayThreshold) { | |
similar = true; | |
} else if (contentSimilarity > generalThreshold && nameSimilarity > generalThreshold | |
&& typeSimilarity(obj1, obj2) > generalThreshold) { | |
similar = true; | |
} | |
} | |
return similar; | |
} | |
/** | |
* This will compute the similarity between two {@link EObject}s' names. | |
* | |
* @param obj1 | |
* First of the two {@link EObject}s. | |
* @param obj2 | |
* Second of the two {@link EObject}s. | |
* @return <code>double</code> representing the similarity between the two {@link EObject}s' names. 0 | |
* < value < 1. | |
* @see NameSimilarity#nameSimilarityMetric(String, String) | |
*/ | |
protected double nameSimilarity(EObject obj1, EObject obj2) { | |
double similarity = 0d; | |
try { | |
final Double value = getSimilarityFromCache(obj1, obj2, NAME_SIMILARITY); | |
if (value != null) { | |
similarity = value; | |
} else { | |
similarity = NameSimilarity.nameSimilarityMetric(NameSimilarity.findName(obj1), | |
NameSimilarity.findName(obj2)); | |
setSimilarityInCache(obj1, obj2, NAME_SIMILARITY, similarity); | |
} | |
} catch (FactoryException e) { | |
// fails silently, will return a similarity of 0d | |
} | |
return similarity; | |
} | |
/* | |
* created as package visibility method to allow access from initializer's listener. Shouldn't be further | |
* opened. | |
*/ | |
/** | |
* This will load all the needed options with their default values. | |
* | |
* @return Map containing all the needed options with their default values. | |
*/ | |
/* package */Map<String, Object> loadPreferenceOptionMap() { | |
final Map<String, Object> optionMap = new EMFCompareMap<String, Object>(17); | |
optionMap.put(MatchOptions.OPTION_SEARCH_WINDOW, getPreferenceSearchWindow()); | |
optionMap.put(MatchOptions.OPTION_IGNORE_ID, getPreferenceIgnoreID()); | |
optionMap.put(MatchOptions.OPTION_IGNORE_XMI_ID, getPreferenceIgnoreXMIID()); | |
optionMap.put(MatchOptions.OPTION_DISTINCT_METAMODELS, getPreferenceDistinctMetaModel()); | |
optionMap.put(MatchOptions.OPTION_PROGRESS_MONITOR, null); | |
return optionMap; | |
} | |
/** | |
* Returns an absolute comparison metric between the two given {@link EObject}s. | |
* | |
* @param obj1 | |
* The first {@link EObject} to compare. | |
* @param obj2 | |
* Second of the {@link EObject}s to compare. | |
* @return An absolute comparison metric. 0 < value < 1. | |
* @throws FactoryException | |
* Thrown if we cannot compute the content similarity. | |
*/ | |
private double absoluteMetric(EObject obj1, EObject obj2) throws FactoryException { | |
final double nameSimilarity = nameSimilarity(obj1, obj2); | |
if (nameSimilarity == 1.0d) { | |
return 1.0; | |
} | |
final double relationsSimilarity = relationsSimilarity(obj1, obj2); | |
double sameUri = 0d; | |
if (hasSameUri(obj1, obj2)) | |
sameUri = 1; | |
final double positionSimilarity = relationsSimilarity / 2d + sameUri / 2d; | |
final double contentWeight = 0.5d; | |
if (nonNullFeaturesCount(obj1) > MIN_ATTRIBUTES_COUNT | |
&& nonNullFeaturesCount(obj2) > MIN_ATTRIBUTES_COUNT) { | |
final double nameWeight = 0.8d; | |
final double positionWeight = 0.4d; | |
final double contentSimilarity = contentSimilarity(obj1, obj2); | |
// Computing type similarity really is time expensive | |
// double typeSimilarity = typeSimilarity(obj1, obj2); | |
return (contentSimilarity * contentWeight + nameSimilarity * nameWeight + positionSimilarity | |
* positionWeight) | |
/ (contentWeight + nameWeight + positionWeight); | |
} | |
// we didn't have enough features to compute an accurate metric | |
final double nameWeight = 0.4d; | |
final double positionWeight = 0.2d; | |
return (nameSimilarity * nameWeight + positionSimilarity * positionWeight) | |
/ (nameWeight + positionWeight); | |
} | |
/** | |
* Returns an absolute comparison metric between the three given {@link EObject}s. | |
* | |
* @param obj1 | |
* The first {@link EObject} to compare. | |
* @param obj2 | |
* Second of the {@link EObject}s to compare. | |
* @param obj3 | |
* Second of the {@link EObject}s to compare. | |
* @return An absolute comparison metric | |
* @throws FactoryException | |
* Thrown if we cannot compute the content similarity. | |
*/ | |
private double absoluteMetric(EObject obj1, EObject obj2, EObject obj3) throws FactoryException { | |
final double metric1 = absoluteMetric(obj1, obj2); | |
final double metric2 = absoluteMetric(obj1, obj3); | |
final double metric3 = absoluteMetric(obj2, obj3); | |
return (metric1 + metric2 + metric3) / 3; | |
} | |
/** | |
* This will recursively create three-way submatches and add them under the given {@link MatchModel}. The | |
* two {@link Match2Elements} we consider as parameters are the result of the two-way comparisons between : | |
* <ul> | |
* <li>The left and origin model.</li> | |
* <li>The right and origin model.</li> | |
* </ul> | |
* <br/><br/>We can then consider that a {@link Match3Element} would be : | |
* | |
* <pre> | |
* match.leftElement = left.getLeftElement(); | |
* match.originElement = left.getRightElement() = right.getRightElement(); | |
* match.rightElement = right.getLeftElement(); | |
* </pre> | |
* | |
* @param root | |
* {@link MatchModel} under which to add our {@link Match3Element}s. | |
* @param matchElementRoot | |
* Root of the {@link Match3Element}s' hierarchy for the current element to be created. | |
* @param left | |
* Left {@link Match2Elements} to consider. | |
* @param right | |
* Right {@link Match2Elements} to consider. | |
* @throws FactoryException | |
* Thrown if we cannot compute the {@link #absoluteMetric(EObject, EObject, EObject) absolute | |
* metric} between the three elements or if we cannot add a {@link Match3Element} under the | |
* given <code>matchElementRoot</code>. | |
*/ | |
private void createSub3Match(MatchModel root, Match3Elements matchElementRoot, Match2Elements left, | |
Match2Elements right) throws FactoryException { | |
final List<MatchElement> leftSubMatches = left.getSubMatchElements(); | |
final List<MatchElement> rightSubMatches = right.getSubMatchElements(); | |
final List<MatchElement> leftNotFound = new ArrayList<MatchElement>(leftSubMatches); | |
final List<MatchElement> rightNotFound = new ArrayList<MatchElement>(rightSubMatches); | |
for (MatchElement nextLeft : leftSubMatches) { | |
final Match2Elements nextLeftMatch = (Match2Elements)nextLeft; | |
Match2Elements correspondingMatch = null; | |
for (MatchElement nextRight : rightNotFound) { | |
final Match2Elements nextRightMatch = (Match2Elements)nextRight; | |
if (nextRightMatch.getRightElement().equals(nextLeftMatch.getRightElement())) { | |
correspondingMatch = nextRightMatch; | |
break; | |
} | |
} | |
if (correspondingMatch != null) { | |
final Match3Elements match = MatchFactory.eINSTANCE.createMatch3Elements(); | |
match.setSimilarity(absoluteMetric(nextLeftMatch.getLeftElement(), correspondingMatch | |
.getLeftElement(), correspondingMatch.getRightElement())); | |
match.setLeftElement(nextLeftMatch.getLeftElement()); | |
match.setRightElement(correspondingMatch.getLeftElement()); | |
match.setOriginElement(correspondingMatch.getRightElement()); | |
redirectedAdd(matchElementRoot, SUBMATCH_ELEMENT_NAME, match); | |
createSub3Match(root, matchElementRoot, nextLeftMatch, correspondingMatch); | |
leftNotFound.remove(nextLeftMatch); | |
rightNotFound.remove(correspondingMatch); | |
} | |
} | |
for (MatchElement nextLeftNotFound : leftNotFound) { | |
stillToFindFromModel1.add(nextLeftNotFound); | |
} | |
for (MatchElement nextRightNotFound : rightNotFound) { | |
stillToFindFromModel2.add(nextRightNotFound); | |
} | |
} | |
/** | |
* Creates the {@link Match2Elements submatch elements} corresponding to the mapping of objects from the | |
* two given {@link List}s. | |
* | |
* @param root | |
* Root of the {@link MatchModel} where to insert all these mappings. | |
* @param list1 | |
* First of the lists used to compute mapping. | |
* @param list2 | |
* Second of the lists used to compute mapping. | |
* @param monitor | |
* {@link CompareProgressMonitor progress monitor} to display while the comparison lasts. Might | |
* be <code>null</code>, in which case we won't monitor progress. | |
* @throws FactoryException | |
* Thrown if we cannot match the elements of the two lists or add submatch elements to | |
* <code>root</code>. | |
* @throws InterruptedException | |
* Thrown if the operation is cancelled or fails somehow. | |
*/ | |
private void createSubMatchElements(EObject root, List<EObject> list1, List<EObject> list2, | |
Monitor monitor) throws FactoryException, InterruptedException { | |
stillToFindFromModel1.clear(); | |
stillToFindFromModel2.clear(); | |
final List<Match2Elements> mappings = mapLists(list1, list2, this | |
.<Integer> getOption(MatchOptions.OPTION_SEARCH_WINDOW), monitor); | |
final Iterator<Match2Elements> it = mappings.iterator(); | |
while (it.hasNext()) { | |
final Match2Elements map = it.next(); | |
final Match2Elements match = recursiveMappings(map.getLeftElement(), map.getRightElement(), | |
monitor); | |
redirectedAdd(root, SUBMATCH_ELEMENT_NAME, match); | |
} | |
} | |
/** | |
* Creates {@link UnmatchElement}s and {@link RemoteUnmatchElement}s wrapped around all the elements of | |
* the given {@link List}. | |
* | |
* @param root | |
* Root of the {@link MatchModel} under which to insert all these elements. | |
* @param unMatchedElements | |
* {@link List} containing all the elements we haven't been able to match. | |
* @throws FactoryException | |
* Thrown if we cannot add elements under the given {@link MatchModel root}. | |
*/ | |
private void createThreeWayUnmatchElements(MatchModel root, Map<EObject, Boolean> unMatchedElements) | |
throws FactoryException { | |
for( Map.Entry<EObject, Boolean> entry : unMatchedElements.entrySet()) { | |
// We will only consider the highest level of an unmatched element | |
// hierarchy | |
if (!unMatchedElements.containsKey(entry.getKey().eContainer())) { | |
final UnmatchElement unMap; | |
if( entry.getValue()) { | |
unMap = MatchFactory.eINSTANCE.createUnmatchElement(); | |
unMap.setRemote(true); | |
} | |
else | |
unMap = MatchFactory.eINSTANCE.createUnmatchElement(); | |
unMap.setElement( entry.getKey()); | |
redirectedAdd(root, UNMATCH_ELEMENT_NAME, unMap); | |
} | |
} | |
unMatchedElements.clear(); | |
} | |
/** | |
* Creates {@link UnmatchElement}s wrapped around all the elements of the given {@link List}. | |
* | |
* @param root | |
* Root of the {@link MatchModel} under which to insert all these {@link UnmatchElement}s. | |
* @param unMatchedElements | |
* {@link Set} containing all the elements we haven't been able to match. | |
* @throws FactoryException | |
* Thrown if we cannot add elements under the given {@link MatchModel root}. | |
*/ | |
private void createUnmatchElements(MatchModel root, Set<EObject> unMatchedElements, Side side) | |
throws FactoryException { | |
for (EObject element : unMatchedElements) { | |
final UnmatchElement unMap = MatchFactory.eINSTANCE.createUnmatchElement(); | |
unMap.setElement(element); | |
if (side != null) | |
unMap.setSide(side); | |
redirectedAdd(root, UNMATCH_ELEMENT_NAME, unMap); | |
} | |
unMatchedElements.clear(); | |
} | |
/** | |
* This method handles the creation and returning of a two way model match. | |
* | |
* @param leftResource | |
* Left model for the comparison. | |
* @param rightResource | |
* Right model for the comparison. | |
* @param monitor | |
* Progress monitor to display while the comparison lasts. | |
* @return The corresponding {@link MatchModel}. | |
* @throws InterruptedException | |
* Thrown if the comparison is interrupted somehow. | |
*/ | |
private MatchModel doMatch(Resource leftResource, Resource rightResource, Monitor monitor) | |
throws InterruptedException { | |
final MatchModel root = MatchFactory.eINSTANCE.createMatchModel(); | |
setModelRoots(root, leftResource, rightResource); | |
// filters unused features | |
filterUnused(leftResource); | |
filterUnused(rightResource); | |
// navigate through both models at the same time and realize mappings.. | |
try { | |
if (!this.<Boolean> getOption(MatchOptions.OPTION_IGNORE_XMI_ID)) | |
if (leftResource instanceof XMIResource && rightResource instanceof XMIResource) | |
matchByXMIID((XMIResource)leftResource, (XMIResource)rightResource); | |
if (!this.<Boolean> getOption(MatchOptions.OPTION_IGNORE_ID)) | |
matchByID(leftResource, rightResource); | |
monitor.subTask("Matching roots"); //$NON-NLS-1$ | |
final List<Match2Elements> matchedRoots = mapLists(leftResource.getContents(), rightResource | |
.getContents(), this.<Integer> getOption(MatchOptions.OPTION_SEARCH_WINDOW), monitor); | |
stillToFindFromModel1.clear(); | |
stillToFindFromModel2.clear(); | |
final List<EObject> unMatchedLeftRoots = new ArrayList<EObject>(leftResource.getContents()); | |
final List<EObject> unMatchedRightRoots = new ArrayList<EObject>(rightResource.getContents()); | |
// These sets will help us in keeping track of the yet to be found | |
// elements | |
final Set<EObject> still1 = new HashSet<EObject>(); | |
final Set<EObject> still2 = new HashSet<EObject>(); | |
// If one of the resources has no roots, considers it as deleted | |
if (leftResource.getContents().size() > 0 && rightResource.getContents().size() > 0) { | |
Match2Elements matchModelRoot = MatchFactory.eINSTANCE.createMatch2Elements(); | |
// We haven't found any similar roots, we then consider the | |
// firsts to be similar. | |
if (matchedRoots.size() == 0) { | |
final Match2Elements rootMapping = MatchFactory.eINSTANCE.createMatch2Elements(); | |
rootMapping.setLeftElement(leftResource.getContents().get(0)); | |
EObject rightElement = findMostSimilar(leftResource.getContents().get(0), | |
unMatchedRightRoots); | |
if (rightElement == null) | |
rightElement = unMatchedRightRoots.get(0); | |
rootMapping.setRightElement(rightElement); | |
matchedRoots.add(rootMapping); | |
} | |
monitor.subTask("Processing matched roots' contents"); //$NON-NLS-1$ | |
for (Match2Elements matchedRoot : matchedRoots) { | |
List<EObject> rightContentsWithChildren = getContentsWithChildren(matchedRoot.getRightElement()); | |
final Match2Elements rootMapping = myRecursiveMappings(matchedRoot.getLeftElement(), | |
matchedRoot.getRightElement(), rightContentsWithChildren, monitor); | |
stillToFindFromModel2.addAll(rightContentsWithChildren); | |
// final Match2Elements rootMapping = recursiveMappings(matchedRoot.getLeftElement(), | |
// matchedRoot.getRightElement(), monitor); | |
// this is the first passage | |
if (matchModelRoot.getLeftElement() == null) { | |
matchModelRoot = rootMapping; | |
redirectedAdd(root, MATCH_ELEMENT_NAME, matchModelRoot); | |
} else { | |
redirectedAdd(matchModelRoot, SUBMATCH_ELEMENT_NAME, rootMapping); | |
} | |
// Synchronizes the two lists to avoid multiple elements | |
still1.removeAll(stillToFindFromModel1); | |
still2.removeAll(stillToFindFromModel2); | |
// checks for matches within the yet to found elements lists | |
createSubMatchElements(rootMapping, new ArrayList<EObject>(stillToFindFromModel1), | |
new ArrayList<EObject>(stillToFindFromModel2), monitor); | |
// Adds all unfound elements to the sets | |
still1.addAll(stillToFindFromModel1); | |
still2.addAll(stillToFindFromModel2); | |
unMatchedLeftRoots.remove(matchedRoot.getLeftElement()); | |
unMatchedRightRoots.remove(matchedRoot.getRightElement()); | |
} | |
// We'll iterate through the unMatchedRoots all contents | |
monitor.subTask("Processing unmatched roots"); //$NON-NLS-1$ | |
createSubMatchElements(matchModelRoot, unMatchedLeftRoots, unMatchedRightRoots, monitor); | |
} else { | |
// Roots are unmatched, this is either a file addition or | |
// deletion | |
still1.addAll(unMatchedLeftRoots); | |
still2.addAll(unMatchedRightRoots); | |
} | |
// Now takes care of remaining unfound elements | |
still1.addAll(stillToFindFromModel1); | |
still2.addAll(stillToFindFromModel2); | |
createUnmatchElements(root, still1, Side.LEFT); | |
createUnmatchElements(root, still2, Side.RIGHT); | |
} catch (FactoryException e) { | |
EMFComparePlugin.log(e, false); | |
} | |
return root; | |
} | |
/** | |
* This method handles the creation and returning of a three way model match. | |
* | |
* @param leftResource | |
* Left model for the comparison. | |
* @param rightResource | |
* Right model for the comparison. | |
* @param ancestorResource | |
* Common ancestor of the right and left models. | |
* @param monitor | |
* Progress monitor to display while the comparison lasts. | |
* @return The corresponding {@link MatchModel}. | |
* @throws InterruptedException | |
* Thrown if the comparison is interrupted somehow. | |
*/ | |
private MatchModel doMatch(Resource leftResource, Resource rightResource, Resource ancestorResource, | |
Monitor monitor) throws InterruptedException { | |
final MatchModel root = MatchFactory.eINSTANCE.createMatchModel(); | |
setModelRoots(root, leftResource, rightResource, ancestorResource); | |
final MatchModel root1AncestorMatch = doMatch(leftResource, ancestorResource, monitor); | |
final MatchModel root2AncestorMatch = doMatch(rightResource, ancestorResource, monitor); | |
final List<MatchElement> root1MatchedElements = new ArrayList<MatchElement>(root1AncestorMatch | |
.getMatchedElements()); | |
final List<MatchElement> root2MatchedElements = new ArrayList<MatchElement>(root2AncestorMatch | |
.getMatchedElements()); | |
// populates the unmatched elements list for later use | |
for (Object unMatch : root1AncestorMatch.getUnmatchedElements()) | |
remainingUnMatchedElements.add(((UnmatchElement)unMatch).getElement()); | |
for (Object unMatch : root2AncestorMatch.getUnmatchedElements()) | |
remainingUnMatchedElements.add(((UnmatchElement)unMatch).getElement()); | |
try { | |
final Match3Elements subMatchRoot = MatchFactory.eINSTANCE.createMatch3Elements(); | |
if (root2MatchedElements.size() > 0) { | |
final Match2Elements root1Match = (Match2Elements)root1MatchedElements.get(0); | |
final Match2Elements root2Match = (Match2Elements)root2MatchedElements.get(0); | |
subMatchRoot.setSimilarity(absoluteMetric(root1Match.getLeftElement(), root2Match | |
.getLeftElement(), root2Match.getRightElement())); | |
subMatchRoot.setLeftElement(root1Match.getLeftElement()); | |
subMatchRoot.setRightElement(root2Match.getLeftElement()); | |
subMatchRoot.setOriginElement(root2Match.getRightElement()); | |
redirectedAdd(root, MATCH_ELEMENT_NAME, subMatchRoot); | |
createSub3Match(root, subMatchRoot, root1Match, root2Match); | |
} else if (root1MatchedElements.size() > 0) { | |
stillToFindFromModel1.add(root1MatchedElements.get(0)); | |
} | |
// We will now check through the unmatched object for matches. This | |
// will allow for a more accurate detection | |
// for models with multiple roots. | |
processUnmatchedElements(root, subMatchRoot); | |
// #processUnmatchedElements(MatchModel, Match3Element) | |
// will have updated "remainingUnMatchedElements" | |
final Set<EObject> remainingLeft = new HashSet<EObject>(); | |
final Set<EObject> remainingRight = new HashSet<EObject>(); | |
for (EObject unMatched : remainingUnMatchedElements) { | |
if (unMatched.eResource() == leftResource) { | |
remainingLeft.add(unMatched); | |
final TreeIterator<EObject> iterator = unMatched.eAllContents(); | |
while (iterator.hasNext()) | |
remainingLeft.add(iterator.next()); | |
} else if (unMatched.eResource() == rightResource) { | |
remainingRight.add(unMatched); | |
final TreeIterator<EObject> iterator = unMatched.eAllContents(); | |
while (iterator.hasNext()) | |
remainingRight.add(iterator.next()); | |
} | |
} | |
stillToFindFromModel1.clear(); | |
stillToFindFromModel2.clear(); | |
final List<Match2Elements> mappings = mapLists(new ArrayList<EObject>(remainingLeft), | |
new ArrayList<EObject>(remainingRight), this | |
.<Integer> getOption(MatchOptions.OPTION_SEARCH_WINDOW), monitor); | |
for (Match2Elements map : mappings) { | |
final Match3Elements subMatch = MatchFactory.eINSTANCE.createMatch3Elements(); | |
subMatch.setLeftElement(map.getLeftElement()); | |
subMatch.setRightElement(map.getRightElement()); | |
redirectedAdd(subMatchRoot, SUBMATCH_ELEMENT_NAME, subMatch); | |
} | |
final Map<EObject, Boolean> unMatchedElements = new EMFCompareMap<EObject, Boolean>(); | |
for (EObject remoteUnMatch : stillToFindFromModel1) { | |
unMatchedElements.put(remoteUnMatch, true); | |
} | |
for (EObject unMatch : stillToFindFromModel2) { | |
unMatchedElements.put(unMatch, false); | |
} | |
createThreeWayUnmatchElements(root, unMatchedElements); | |
} catch (FactoryException e) { | |
EMFComparePlugin.log(e, false); | |
} | |
return root; | |
} | |
/** | |
* Filters unused features of the resource. | |
* | |
* @param resource | |
* Resource to be apply filter on. | |
*/ | |
private void filterUnused(Resource resource) { | |
for (EObject root : resource.getContents()) | |
filter.analyseModel(root); | |
} | |
/** | |
* Workaround for bug #235606 : elements held by a reference with containment=true and derived=true are | |
* not matched since not returned by {@link EObject#eContents()}. This allows us to return the list of all | |
* contents from an EObject <u>including</u> those references. | |
* | |
* @param eObject | |
* The EObject we seek the content of. | |
* @return The list of all the content of a given EObject, derived containmnent references included. | |
*/ | |
@SuppressWarnings("unchecked") | |
private List<EObject> getContents(EObject eObject) { | |
// TODO can this be cached (Map<EClass, List<EReference>>)? | |
final List<EObject> result = new ArrayList(eObject.eContents()); | |
for (EReference reference : eObject.eClass().getEAllReferences()) { | |
if (reference.isContainment() && reference.isDerived()) { | |
final Object value = eObject.eGet(reference); | |
if (value instanceof Collection) | |
result.addAll((Collection)value); | |
else if (value instanceof EObject) | |
result.add((EObject)value); | |
} | |
} | |
return result; | |
} | |
/** | |
* Returns whether we should assume the metamodels of the compared models are distinct. | |
* | |
* @return <code>true</code> if the metamodels are to be assumed distinct, <code>false</code> | |
* otherwise. | |
*/ | |
private boolean getPreferenceDistinctMetaModel() { | |
if (EMFPlugin.IS_ECLIPSE_RUNNING && EMFComparePlugin.getDefault() != null) | |
return EMFComparePlugin.getDefault().getBoolean( | |
EMFComparePreferenceConstants.PREFERENCES_KEY_DISTINCT_METAMODEL); | |
return MatchOptions.DEFAULT_DISTINCT_METAMODEL; | |
} | |
/** | |
* Returns whether we should ignore the IDs or compare using them. | |
* | |
* @return <code>True</code> if we should ignore ID, <code>False</code> otherwise. | |
*/ | |
private boolean getPreferenceIgnoreID() { | |
if (EMFPlugin.IS_ECLIPSE_RUNNING && EMFComparePlugin.getDefault() != null) | |
return EMFComparePlugin.getDefault().getBoolean( | |
EMFComparePreferenceConstants.PREFERENCES_KEY_IGNORE_ID); | |
return MatchOptions.DEFAULT_IGNORE_ID; | |
} | |
/** | |
* Returns whether we should ignore the XMI IDs or compare with them. | |
* | |
* @return <code>True</code> if we should ignore XMI ID, <code>False</code> otherwise. | |
*/ | |
private boolean getPreferenceIgnoreXMIID() { | |
if (EMFPlugin.IS_ECLIPSE_RUNNING && EMFComparePlugin.getDefault() != null) | |
return EMFComparePlugin.getDefault().getBoolean( | |
EMFComparePreferenceConstants.PREFERENCES_KEY_IGNORE_XMIID); | |
return MatchOptions.DEFAULT_IGNORE_XMI_ID; | |
} | |
/** | |
* Returns the search window corresponding to the number of siblings to consider while matching. Reducing | |
* this number (on the preferences page) considerably improve performances while reducing precision. | |
* | |
* @return An <code>int</code> representing the number of siblings to consider for matching. | |
*/ | |
private int getPreferenceSearchWindow() { | |
int searchWindow = MatchOptions.DEFAULT_SEARCH_WINDOW; | |
if (EMFPlugin.IS_ECLIPSE_RUNNING | |
&& EMFComparePlugin.getDefault() != null | |
&& EMFComparePlugin.getDefault().getInt( | |
EMFComparePreferenceConstants.PREFERENCES_KEY_SEARCH_WINDOW) > 0) | |
searchWindow = EMFComparePlugin.getDefault().getInt( | |
EMFComparePreferenceConstants.PREFERENCES_KEY_SEARCH_WINDOW); | |
if (searchWindow < 0) | |
searchWindow = 0; | |
return searchWindow; | |
} | |
/** | |
* Returns the given similarity between the two given {@link EObject}s as it is stored in cache.<br/> | |
* <p> | |
* <code>similarityKind</code> must be one of | |
* <ul> | |
* <li>{@link #NAME_SIMILARITY}</li> | |
* <li>{@link #TYPE_SIMILARITY}</li> | |
* <li>{@link #VALUE_SIMILARITY}</li> | |
* <li>{@link #RELATION_SIMILARITY}</li> | |
* </ul> | |
* </p> | |
* | |
* @param obj1 | |
* First of the two {@link EObject}s we seek the similarity for. | |
* @param obj2 | |
* Second of the two {@link EObject}s we seek the similarity for. | |
* @param similarityKind | |
* Kind of similarity to get. | |
* @return The similarity as described by <code>similarityKind</code> as it is stored in cache for the | |
* two given {@link EObject}s. | |
*/ | |
private Double getSimilarityFromCache(EObject obj1, EObject obj2, char similarityKind) { | |
return metricsCache.get(pairHashCode(obj1, obj2, similarityKind)); | |
} | |
/** | |
* Checks wether the two given {@link EObject} have the same URI. | |
* | |
* @param obj1 | |
* First of the two {@link EObject} we're comparing. | |
* @param obj2 | |
* Second {@link EObject} we're comparing. | |
* @return <code>True</code> if the {@link EObject}s have the same URI, <code>False</code> otherwise. | |
*/ | |
private boolean hasSameUri(EObject obj1, EObject obj2) { | |
if (obj1.eResource() != null && obj2.eResource() != null) | |
return obj1.eResource().getURIFragment(obj1).equals(obj2.eResource().getURIFragment(obj2)); | |
return false; | |
} | |
/** | |
* This replaces the contents of the defaults options map with the options overridden by the given map. | |
* | |
* @param map | |
* Map containing the option given to the match procedure. cannot be <code>null</code>. | |
*/ | |
private void loadOptionMap(Map<String, Object> map) { | |
options.putAll(map); | |
if (this.<Integer> getOption(MatchOptions.OPTION_SEARCH_WINDOW) < 0) | |
options.put(MatchOptions.OPTION_SEARCH_WINDOW, getPreferenceSearchWindow()); | |
} | |
/** | |
* Returns a list containing mappings of the nodes of both given {@link List}s. | |
* | |
* @param list1 | |
* First of the lists from which we need to map the elements | |
* @param list2 | |
* Second list to map the elements from. | |
* @param window | |
* Number of siblings to consider for the matching. | |
* @param monitor | |
* {@link CompareProgressMonitor Progress monitor} to display while the comparison lasts. Might | |
* be <code>null</code>, in which case we won't monitor progress. | |
* @return A {@link List} containing mappings of the nodes of both given {@link List}s. | |
* @throws FactoryException | |
* Thrown if the metrics cannot be computed. | |
* @throws InterruptedException | |
* Thrown if the matching process is interrupted somehow. | |
*/ | |
private List<Match2Elements> mapLists(List<EObject> list1, List<EObject> list2, int window, | |
Monitor monitor) throws FactoryException, InterruptedException { | |
final List<Match2Elements> result = new ArrayList<Match2Elements>(); | |
int curIndex = 0 - window / 2; | |
final List<EObject> notFoundList1 = new ArrayList<EObject>(list1); | |
final List<EObject> notFoundList2 = new ArrayList<EObject>(list2); | |
final Iterator<EObject> it1 = list1.iterator(); | |
// then iterate over the 2 lists and compare the elements | |
while (it1.hasNext() && notFoundList2.size() > 0) { | |
final EObject obj1 = it1.next(); | |
final StringBuilder obj1Key = new StringBuilder(); | |
obj1Key.append(NameSimilarity.findName(obj1)); | |
obj1Key.append(obj1.hashCode()); | |
EObject obj2 = matchedByID.get(obj1Key.toString()); | |
if (obj2 == null) { | |
// subtracts the difference between the notfound and the | |
// original list to avoid ArrayOutOfBounds | |
final int end = Math.min(curIndex + window - (list2.size() - notFoundList2.size()), | |
notFoundList2.size()); | |
final int index = Math | |
.min(Math.max(curIndex - (list2.size() - notFoundList2.size()), 0), end); | |
obj2 = findMostSimilar(obj1, notFoundList2.subList(index, end)); | |
if (obj2 != null) { | |
// checks if the most similar to obj2 is obj1 | |
final EObject obj1Check = findMostSimilar(obj2, notFoundList1); | |
if (obj1Check != obj1 && obj1Check != null && isSimilar(obj1Check, obj2)) { | |
continue; | |
} | |
} | |
} | |
if (notFoundList1.contains(obj1) && notFoundList2.contains(obj2) && isSimilar(obj1, obj2)) { | |
final Match2Elements mapping = MatchFactory.eINSTANCE.createMatch2Elements(); | |
final double metric = absoluteMetric(obj1, obj2); | |
mapping.setLeftElement(obj1); | |
mapping.setRightElement(obj2); | |
mapping.setSimilarity(metric); | |
result.add(mapping); | |
notFoundList2.remove(obj2); | |
notFoundList1.remove(obj1); | |
} | |
curIndex += 1; | |
monitor.worked(1); | |
if (monitor.isCanceled()) | |
throw new InterruptedException(); | |
} | |
// now putting the not found elements aside for later | |
stillToFindFromModel2.addAll(notFoundList2); | |
stillToFindFromModel1.addAll(notFoundList1); | |
return result; | |
} | |
/** | |
* Returns a list containing mappings of the nodes of both given {@link List}s. | |
* | |
* @param list1 | |
* First of the lists from which we need to map the elements | |
* @param list2 | |
* Second list to map the elements from. | |
* @param window | |
* Number of siblings to consider for the matching. | |
* @param monitor | |
* {@link CompareProgressMonitor Progress monitor} to display while the comparison lasts. Might | |
* be <code>null</code>, in which case we won't monitor progress. | |
* @return A {@link List} containing mappings of the nodes of both given {@link List}s. | |
* @throws FactoryException | |
* Thrown if the metrics cannot be computed. | |
* @throws InterruptedException | |
* Thrown if the matching process is interrupted somehow. | |
*/ | |
private List<Match2Elements> myMapLists(List<EObject> list1, List<EObject> list2, int window, | |
Monitor monitor) throws FactoryException, InterruptedException { | |
final List<Match2Elements> result = new ArrayList<Match2Elements>(); | |
int curIndex = 0 - window / 2; | |
final List<EObject> notFoundList1 = new ArrayList<EObject>(list1); | |
final List<EObject> notFoundList2 = new ArrayList<EObject>(list2); | |
final Iterator<EObject> it1 = list1.iterator(); | |
// then iterate over the 2 lists and compare the elements | |
while (it1.hasNext() && notFoundList2.size() > 0) { | |
final EObject obj1 = it1.next(); | |
final StringBuilder obj1Key = new StringBuilder(); | |
obj1Key.append(NameSimilarity.findName(obj1)); | |
obj1Key.append(obj1.hashCode()); | |
EObject obj2 = matchedByID.get(obj1Key.toString()); | |
if (obj2 == null) { | |
// subtracts the difference between the notfound and the | |
// original list to avoid ArrayOutOfBounds | |
final int end = Math.min(curIndex + window - (list2.size() - notFoundList2.size()), | |
notFoundList2.size()); | |
final int index = Math | |
.min(Math.max(curIndex - (list2.size() - notFoundList2.size()), 0), end); | |
obj2 = findMostSimilar(obj1, notFoundList2.subList(index, end)); | |
if (obj2 != null) { | |
// checks if the most similar to obj2 is obj1 | |
final EObject obj1Check = findMostSimilar(obj2, notFoundList1); | |
if (obj1Check != obj1 && obj1Check != null && isSimilar(obj1Check, obj2)) { | |
continue; | |
} | |
} | |
} | |
if (notFoundList1.contains(obj1) && notFoundList2.contains(obj2) && isSimilar(obj1, obj2)) { | |
final Match2Elements mapping = MatchFactory.eINSTANCE.createMatch2Elements(); | |
final double metric = absoluteMetric(obj1, obj2); | |
mapping.setLeftElement(obj1); | |
mapping.setRightElement(obj2); | |
mapping.setSimilarity(metric); | |
result.add(mapping); | |
notFoundList2.remove(obj2); | |
notFoundList1.remove(obj1); | |
list2.remove(obj2); | |
} | |
curIndex += 1; | |
monitor.worked(1); | |
if (monitor.isCanceled()) | |
throw new InterruptedException(); | |
} | |
// now putting the not found elements aside for later | |
// stillToFindFromModel2.addAll(notFoundList2); | |
stillToFindFromModel1.addAll(notFoundList1); | |
return result; | |
} | |
/** | |
* Iterates through both of the given EObjects to find all of their children that can be matched by their | |
* functional ID, then populates {@link #matchedByID} with those mappings. | |
* <p> | |
* Note that this method will perform a check to ensure the two objects' resources are indeed | |
* XMIResources. | |
* </p> | |
* | |
* @param obj1 | |
* First of the two EObjects to visit. | |
* @param obj2 | |
* Second of the EObjects to visit. | |
* @throws FactoryException | |
* Thrown if we couldn't compute a key to store the items in cache. | |
*/ | |
private void matchByID(EObject obj1, EObject obj2) throws FactoryException { | |
matchedByID.clear(); | |
final Iterator<EObject> iterator1 = obj1.eAllContents(); | |
while (iterator1.hasNext()) { | |
final EObject item1 = iterator1.next(); | |
final String item1ID = EcoreUtil.getID(item1); | |
if (item1ID != null) { | |
final Iterator<EObject> iterator2 = obj2.eAllContents(); | |
while (iterator2.hasNext()) { | |
final EObject item2 = iterator2.next(); | |
final String item2ID = EcoreUtil.getID(item2); | |
if (item2 != null && item1ID.equals(item2ID)) { | |
final StringBuilder item1Key = new StringBuilder(); | |
item1Key.append(NameSimilarity.findName(item1)); | |
item1Key.append(item1.hashCode()); | |
matchedByID.put(item1Key.toString(), item2); | |
break; | |
} | |
} | |
} | |
} | |
} | |
/** | |
* Iterates through both of the given {@link XMIResource resources} to find all the elements that can be | |
* matched by their XMI ID, then populates {@link #matchedByID} with those mappings. | |
* | |
* @param left | |
* First of the two {@link XMIResource resources} to visit. | |
* @param right | |
* Second of the {@link XMIResource resources} to visit. | |
* @throws FactoryException | |
* Thrown if we couldn't compute a key to store the items in cache. | |
*/ | |
private void matchByID(Resource left, Resource right) throws FactoryException { | |
matchedByID.clear(); | |
final Iterator<EObject> leftIterator = left.getAllContents(); | |
while (leftIterator.hasNext()) { | |
final EObject item1 = leftIterator.next(); | |
final String item1ID = EcoreUtil.getID(item1); | |
if (item1ID != null) { | |
final Iterator<EObject> rightIterator = right.getAllContents(); | |
while (rightIterator.hasNext()) { | |
final EObject item2 = rightIterator.next(); | |
final String item2ID = EcoreUtil.getID(item2); | |
if (item2 != null && item1ID.equals(item2ID)) { | |
final StringBuilder item1Key = new StringBuilder(); | |
item1Key.append(NameSimilarity.findName(item1)); | |
item1Key.append(item1.hashCode()); | |
matchedByID.put(item1Key.toString(), item2); | |
break; | |
} | |
} | |
} | |
} | |
} | |
/** | |
* Iterates through both of the given EObjects to find all of their children that can be matched by their | |
* XMI ID, then populates {@link #matchedByXMIID} with those mappings. | |
* <p> | |
* Note that this method will perform a check to ensure the two objects' resources are indeed | |
* XMIResources. | |
* </p> | |
* | |
* @param obj1 | |
* First of the two EObjects to visit. | |
* @param obj2 | |
* Second of the EObjects to visit. | |
* @throws FactoryException | |
* Thrown if we couldn't compute a key to store the items in cache. | |
*/ | |
private void matchByXMIID(EObject obj1, EObject obj2) throws FactoryException { | |
matchedByXMIID.clear(); | |
if (obj1 != null && obj2 != null && obj1.eResource() instanceof XMIResource | |
&& obj2.eResource() instanceof XMIResource) { | |
final XMIResource left = (XMIResource)obj1.eResource(); | |
final XMIResource right = (XMIResource)obj2.eResource(); | |
final Iterator<EObject> iterator = obj1.eAllContents(); | |
while (iterator.hasNext()) { | |
final EObject item1 = iterator.next(); | |
final String item1ID = left.getID(item1); | |
if (item1ID != null) { | |
final EObject item2 = right.getEObject(item1ID); | |
if (item2 != null) { | |
final StringBuilder item1Key = new StringBuilder(); | |
item1Key.append(NameSimilarity.findName(item1)); | |
item1Key.append(item1.hashCode()); | |
matchedByXMIID.put(item1Key.toString(), item2); | |
} | |
} | |
} | |
} | |
} | |
/** | |
* Iterates through both of the given {@link XMIResource resources} to find all the elements that can be | |
* matched by their XMI ID, then populates {@link #matchedByXMIID} with those mappings. | |
* | |
* @param left | |
* First of the two {@link XMIResource resources} to visit. | |
* @param right | |
* Second of the {@link XMIResource resources} to visit. | |
* @throws FactoryException | |
* Thrown if we couldn't compute a key to store the items in cache. | |
*/ | |
private void matchByXMIID(XMIResource left, XMIResource right) throws FactoryException { | |
matchedByXMIID.clear(); | |
final Iterator<EObject> leftIterator = left.getAllContents(); | |
while (leftIterator.hasNext()) { | |
final EObject item1 = leftIterator.next(); | |
final String item1ID = left.getID(item1); | |
if (item1ID != null) { | |
final EObject item2 = right.getEObject(item1ID); | |
if (item2 != null) { | |
final StringBuilder item1Key = new StringBuilder(); | |
item1Key.append(NameSimilarity.findName(item1)); | |
item1Key.append(item1.hashCode()); | |
matchedByXMIID.put(item1Key.toString(), item2); | |
} | |
} | |
} | |
} | |
/** | |
* Counts all the {@link EStructuralFeature features} of the given {@link EObject} that are | |
* <code>null</code> or initialized to the empty {@link String} "". | |
* | |
* @param eobj | |
* {@link EObject} we need to count the empty features of. | |
* @return The number of features initialized to <code>null</code> or the empty String. | |
*/ | |
private int nonNullFeaturesCount(EObject eobj) { | |
// TODO should probably cache result here | |
int nonNullFeatures = 0; | |
final Iterator<EStructuralFeature> features = eobj.eClass().getEAllStructuralFeatures().iterator(); | |
while (features.hasNext()) { | |
final EStructuralFeature feature = features.next(); | |
if (eobj.eGet(feature) != null && !"".equals(eobj.eGet(feature).toString())) //$NON-NLS-1$ | |
nonNullFeatures++; | |
} | |
return nonNullFeatures; | |
} | |
/** | |
* Computes an unique key between to {@link EObject}s to store their similarity in cache. | |
* <p> | |
* <code>similarityKind</code> must be one of | |
* <ul> | |
* <li>{@link #NAME_SIMILARITY}</li> | |
* <li>{@link #TYPE_SIMILARITY}</li> | |
* <li>{@link #VALUE_SIMILARITY}</li> | |
* <li>{@link #RELATION_SIMILARITY}</li> | |
* </ul> | |
* </p> | |
* | |
* @param obj1 | |
* First of the two {@link EObject}s. | |
* @param obj2 | |
* Second of the two {@link EObject}s. | |
* @param similarityKind | |
* Kind of similarity this key will represent in cache. | |
* @return Unique key for the similarity cache. | |
*/ | |
private String pairHashCode(EObject obj1, EObject obj2, char similarityKind) { | |
if (similarityKind == NAME_SIMILARITY || similarityKind == TYPE_SIMILARITY | |
|| similarityKind == VALUE_SIMILARITY || similarityKind == RELATION_SIMILARITY) { | |
final StringBuilder hash = new StringBuilder(); | |
hash.append(similarityKind).append(obj1.hashCode()).append(obj2.hashCode()); | |
return hash.toString(); | |
} | |
throw new IllegalArgumentException("DifferencesServices.illegalSimilarityKind"); //$NON-NLS-1$ | |
} | |
/** | |
* Allows for a more accurate modifications detection for three way comparison with multiple roots models. | |
* | |
* @param root | |
* Root of the {@link MatchModel}. | |
* @param subMatchRoot | |
* Root of the {@link Match3Element}s' hierarchy for the current element to be created. | |
* @throws FactoryException | |
* Thrown if we cannot compute {@link EObject}s similarity or if adding elements to either | |
* <code>root</code> or <code>subMatchRoot</code> fails somehow. | |
*/ | |
private void processUnmatchedElements(MatchModel root, Match3Elements subMatchRoot) | |
throws FactoryException { | |
for (EObject obj1 : new ArrayList<EObject>(stillToFindFromModel1)) { | |
boolean matchFound = false; | |
if (obj1 instanceof Match2Elements) { | |
final Match2Elements match1 = (Match2Elements)obj1; | |
for (EObject obj2 : new ArrayList<EObject>(stillToFindFromModel2)) { | |
if (obj2 instanceof Match2Elements) { | |
final Match2Elements match2 = (Match2Elements)obj2; | |
if (match1.getRightElement() == match2.getRightElement()) { | |
matchFound = true; | |
final Match3Elements match = MatchFactory.eINSTANCE.createMatch3Elements(); | |
match.setSimilarity(absoluteMetric(match1.getLeftElement(), match2 | |
.getLeftElement(), match2.getRightElement())); | |
match.setLeftElement(match1.getLeftElement()); | |
match.setRightElement(match2.getLeftElement()); | |
match.setOriginElement(match2.getRightElement()); | |
// This will happen if we couldn't match previously | |
if (subMatchRoot == null) { | |
redirectedAdd(root, MATCH_ELEMENT_NAME, match); | |
createSub3Match(root, match, match1, match2); | |
} else { | |
redirectedAdd(subMatchRoot, SUBMATCH_ELEMENT_NAME, match); | |
createSub3Match(root, subMatchRoot, match1, match2); | |
} | |
stillToFindFromModel1.remove(match1); | |
stillToFindFromModel2.remove(match2); | |
} | |
} | |
} | |
if (!matchFound) { | |
remainingUnMatchedElements.add(match1.getLeftElement()); | |
} | |
} | |
} | |
for (EObject eObj : new ArrayList<EObject>(stillToFindFromModel1)) { | |
if (eObj instanceof Match2Elements) { | |
final Match2Elements nextLeftNotFound = (Match2Elements)eObj; | |
final UnmatchElement unMatch = MatchFactory.eINSTANCE.createUnmatchElement(); | |
unMatch.setElement(nextLeftNotFound.getLeftElement()); | |
remainingUnMatchedElements.remove(nextLeftNotFound.getLeftElement()); | |
remainingUnMatchedElements.remove(nextLeftNotFound.getRightElement()); | |
redirectedAdd(root, UNMATCH_ELEMENT_NAME, unMatch); | |
} | |
} | |
for (EObject eObj : new ArrayList<EObject>(stillToFindFromModel2)) { | |
if (eObj instanceof Match2Elements) { | |
final Match2Elements nextRightNotFound = (Match2Elements)eObj; | |
final UnmatchElement unMatch = MatchFactory.eINSTANCE.createUnmatchElement(); | |
unMatch.setRemote(true); | |
unMatch.setElement(nextRightNotFound.getLeftElement()); | |
remainingUnMatchedElements.remove(nextRightNotFound.getLeftElement()); | |
remainingUnMatchedElements.remove(nextRightNotFound.getRightElement()); | |
redirectedAdd(root, UNMATCH_ELEMENT_NAME, unMatch); | |
} | |
} | |
} | |
/** | |
* We consider here <code>current1</code> and <code>current2</code> are similar. This method creates | |
* the mapping for the objects <code>current1</code> and <code>current2</code>, Then submappings for | |
* these two elements' contents. | |
* | |
* @param current1 | |
* First element of the two elements mapping. | |
* @param current2 | |
* Second of the two elements mapping. | |
* @param monitor | |
* {@link CompareProgressMonitor Progress monitor} to display while the comparison lasts. Might | |
* be <code>null</code>, in which case we won't monitor progress. | |
* @return The mapping for <code>current1</code> and <code>current2</code> and their content. | |
* @throws FactoryException | |
* Thrown when the metrics cannot be computed for <code>current1</code> and | |
* <code>current2</code>. | |
* @throws InterruptedException | |
* Thrown if the matching process is interrupted somehow. | |
*/ | |
private Match2Elements recursiveMappings(EObject current1, EObject current2, | |
Monitor monitor) throws FactoryException, InterruptedException { | |
Match2Elements mapping = null; | |
mapping = MatchFactory.eINSTANCE.createMatch2Elements(); | |
mapping.setLeftElement(current1); | |
mapping.setRightElement(current2); | |
mapping.setSimilarity(absoluteMetric(current1, current2)); | |
final List<Match2Elements> mapList = mapLists(getContents(current1), getContents(current2), this | |
.<Integer> getOption(MatchOptions.OPTION_SEARCH_WINDOW), monitor); | |
// We can map other elements with mapLists; we iterate through them. | |
final Iterator<Match2Elements> it = mapList.iterator(); | |
while (it.hasNext()) { | |
final Match2Elements subMapping = it.next(); | |
// As we know source and target are similars, we call recursive | |
// mappings onto these objects | |
EFactory.eAdd(mapping, SUBMATCH_ELEMENT_NAME, recursiveMappings(subMapping.getLeftElement(), | |
subMapping.getRightElement(), monitor)); | |
} | |
return mapping; | |
} | |
/** | |
* We consider here <code>current1</code> and <code>current2</code> are similar. This method creates | |
* the mapping for the objects <code>current1</code> and <code>current2</code>, Then submappings for | |
* these two elements' contents. | |
* | |
* @param current1 | |
* First element of the two elements mapping. | |
* @param current2 | |
* Second of the two elements mapping. | |
* @param monitor | |
* {@link CompareProgressMonitor Progress monitor} to display while the comparison lasts. Might | |
* be <code>null</code>, in which case we won't monitor progress. | |
* @return The mapping for <code>current1</code> and <code>current2</code> and their content. | |
* @throws FactoryException | |
* Thrown when the metrics cannot be computed for <code>current1</code> and | |
* <code>current2</code>. | |
* @throws InterruptedException | |
* Thrown if the matching process is interrupted somehow. | |
*/ | |
private Match2Elements myRecursiveMappings(EObject current1, EObject current2, List<EObject> rightChildren, | |
Monitor monitor) throws FactoryException, InterruptedException { | |
Match2Elements mapping = null; | |
mapping = MatchFactory.eINSTANCE.createMatch2Elements(); | |
mapping.setLeftElement(current1); | |
mapping.setRightElement(current2); | |
mapping.setSimilarity(absoluteMetric(current1, current2)); | |
final List<Match2Elements> mapList = myMapLists(getContents(current1), rightChildren, this | |
.<Integer> getOption(MatchOptions.OPTION_SEARCH_WINDOW), monitor); | |
// We can map other elements with mapLists; we iterate through them. | |
final Iterator<Match2Elements> it = mapList.iterator(); | |
while (it.hasNext()) { | |
final Match2Elements subMapping = it.next(); | |
// As we know source and target are similars, we call recursive | |
// mappings onto these objects | |
EFactory.eAdd(mapping, SUBMATCH_ELEMENT_NAME, myRecursiveMappings(subMapping.getLeftElement(), | |
subMapping.getRightElement(), rightChildren, monitor)); | |
} | |
return mapping; | |
} | |
private List<EObject> getContentsWithChildren(EObject eObject) { | |
List<EObject> result = new ArrayList<EObject>(); | |
List<EObject> children = getContents(eObject); | |
result.addAll(children); | |
for (EObject child : children) { | |
result.addAll(getContentsWithChildren(child)); | |
} | |
return result; | |
} | |
/** | |
* This method is an indirection for adding Mappings in the current MappingGroup. | |
* | |
* @param object | |
* {@link EObject} to add a feature value to. | |
* @param name | |
* Name of the feature to consider. | |
* @param value | |
* Value to add to the feature <code>name</code> of <code>object</code>. | |
* @throws FactoryException | |
* Thrown if the value's affectation fails. | |
*/ | |
private void redirectedAdd(EObject object, String name, Object value) throws FactoryException { | |
EFactory.eAdd(object, name, value); | |
} | |
/** | |
* This will compute the similarity between two {@link EObject}s' relations. | |
* | |
* @param obj1 | |
* First of the two {@link EObject}s. | |
* @param obj2 | |
* Second of the two {@link EObject}s. | |
* @return <code>double</code> representing the similarity between the two {@link EObject}s' relations. | |
* 0 < value < 1. | |
* @throws FactoryException | |
* Thrown if we cannot compute the relations' similarity metrics. | |
* @see StructureSimilarity#relationsSimilarityMetric(EObject, EObject, MetamodelFilter) | |
*/ | |
private double relationsSimilarity(EObject obj1, EObject obj2) throws FactoryException { | |
double similarity = 0d; | |
final Double value = getSimilarityFromCache(obj1, obj2, RELATION_SIMILARITY); | |
if (value != null) { | |
similarity = value; | |
} else { | |
similarity = StructureSimilarity.relationsSimilarityMetric(obj1, obj2, filter); | |
setSimilarityInCache(obj1, obj2, RELATION_SIMILARITY, similarity); | |
} | |
return similarity; | |
} | |
/** | |
* Sets the values of the {@link MatchModel}'s left and right models. | |
* | |
* @param modelRoot | |
* Root of the {@link MatchModel}. | |
* @param left | |
* Element from which to resolve the left model URI. | |
* @param right | |
* Element from which to resolve the right model URI. | |
*/ | |
private void setModelRoots(MatchModel modelRoot, Resource left, Resource right) { | |
setModelRoots(modelRoot, left, right, null); | |
} | |
/** | |
* Sets the values of the {@link MatchModel}'s left, right and ancestor models. | |
* | |
* @param modelRoot | |
* Root of the {@link MatchModel}. | |
* @param left | |
* Element from which to resolve the left model URI. | |
* @param right | |
* Element from which to resolve the right model URI. | |
* @param ancestor | |
* Element from which to resolve the ancestor model URI. Can be <code>null</code>. | |
*/ | |
private void setModelRoots(MatchModel modelRoot, Resource left, Resource right, Resource ancestor) { | |
// Sets values of left, right and ancestor model URIs | |
if (left != null) { | |
modelRoot.getLeftRoots().addAll(left.getContents()); | |
} | |
if (right != null) { | |
modelRoot.getRightRoots().addAll(right.getContents()); | |
} | |
if (ancestor != null) { | |
modelRoot.getAncestorRoots().addAll(ancestor.getContents()); | |
} | |
} | |
/** | |
* Stores in cache the given similarity between the two given {@link EObject}s.<br/> | |
* <p> | |
* <code>similarityKind</code> must be one of | |
* <ul> | |
* <li>{@link #NAME_SIMILARITY}</li> | |
* <li>{@link #TYPE_SIMILARITY}</li> | |
* <li>{@link #VALUE_SIMILARITY}</li> | |
* <li>{@link #RELATION_SIMILARITY}</li> | |
* </ul> | |
* </p> | |
* | |
* @param obj1 | |
* First of the two {@link EObject}s we're setting the similarity for. | |
* @param obj2 | |
* Second of the two {@link EObject}s we're setting the similarity for. | |
* @param similarityKind | |
* Kind of similarity to set. | |
* @param similarity | |
* Value of the similarity between the two {@link EObject}s. | |
*/ | |
private void setSimilarityInCache(EObject obj1, EObject obj2, char similarityKind, double similarity) { | |
metricsCache.put(pairHashCode(obj1, obj2, similarityKind), new Double(similarity)); | |
} | |
/** | |
* Starts the monitor for comparison progress. Externalized here to avoid multiple usage of the Strings. | |
* | |
* @param monitor | |
* The monitor that need be started | |
* @param size | |
* Size of the monitor | |
*/ | |
private void startMonitor(Monitor monitor, int size) { | |
monitor.beginTask("Comparing model", size); //$NON-NLS-1$ | |
monitor.subTask("Browsing model"); //$NON-NLS-1$ | |
} | |
/** | |
* This will compute the similarity between two {@link EObject}s' types. | |
* | |
* @param obj1 | |
* First of the two {@link EObject}s. | |
* @param obj2 | |
* Second of the two {@link EObject}s. | |
* @return <code>double</code> representing the similarity between the two {@link EObject}s' types. 0 | |
* < value < 1. | |
* @throws FactoryException | |
* Thrown if we cannot compute the type similarity metrics. | |
* @see StructureSimilarity#typeSimilarityMetric(EObject, EObject) | |
*/ | |
private double typeSimilarity(EObject obj1, EObject obj2) throws FactoryException { | |
double similarity = 0d; | |
final Double value = getSimilarityFromCache(obj1, obj2, TYPE_SIMILARITY); | |
if (value != null) { | |
similarity = value; | |
} else { | |
similarity = StructureSimilarity.typeSimilarityMetric(obj1, obj2); | |
setSimilarityInCache(obj1, obj2, TYPE_SIMILARITY, similarity); | |
} | |
return similarity; | |
} | |
/** | |
* Creates the progress monitor that will be displayed to the user while the comparison lasts. | |
* | |
* @return The progress monitor that will be displayed to the user while the comparison lasts. | |
*/ | |
private Monitor createProgressMonitor() { | |
Monitor monitor = new BasicMonitor(); | |
final Object delegateMonitor = getOption(MatchOptions.OPTION_PROGRESS_MONITOR); | |
if (delegateMonitor != null && EMFPlugin.IS_ECLIPSE_RUNNING) { | |
monitor = EclipseModelUtils.createProgressMonitor(delegateMonitor); | |
} | |
return monitor; | |
} | |
} |