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 v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * 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; } }