/*******************************************************************************
* Copyright (c) 2012, 2015 Obeo and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Obeo - initial API and implementation
* Alexandra Buzila - Bug 450360
* Philip Langer - Bug 460778
* Stefan Dirix - Bugs 457652, 461011 and 461291
*******************************************************************************/
package org.eclipse.emf.compare.match.eobject;
import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.emf.common.util.BasicDiagnostic;
import org.eclipse.emf.common.util.Diagnostic;
import org.eclipse.emf.common.util.Monitor;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.compare.CompareFactory;
import org.eclipse.emf.compare.Comparison;
import org.eclipse.emf.compare.ComparisonCanceledException;
import org.eclipse.emf.compare.EMFCompare;
import org.eclipse.emf.compare.EMFCompareMessages;
import org.eclipse.emf.compare.Match;
import org.eclipse.emf.compare.match.eobject.EObjectIndex.Side;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.InternalEObject;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.emf.ecore.util.InternalEList;
import org.eclipse.emf.ecore.xmi.XMIResource;
/**
* This implementation of an {@link IEObjectMatcher} will create {@link Match}es based on the input EObjects
* identifiers (either XMI:ID or attribute ID) alone.
*
* @author <a href="mailto:laurent.goubet@obeo.fr">Laurent Goubet</a>
*/
public class IdentifierEObjectMatcher implements IEObjectMatcher {
/**
* This instance might have a delegate matcher. The delegate matcher will be called when no ID is found
* and its results are aggregated with the current matcher.
*/
private Optional<IEObjectMatcher> delegate;
/**
* This will be used to determine what represents the "identifier" of an EObject. By default, we will use
* the following logic, in order (i.e. if condition 1 is fulfilled, we will not try conditions 2 and 3) :
* <ol>
* <li>If the given eObject is a proxy, it is uniquely identified by its URI fragment.</li>
* <li>If the eObject is located in an XMI resource and has an XMI ID, use this as its unique identifier.
* </li>
* <li>If the eObject's EClass has an eIdAttribute set, use this attribute's value.</li>
* </ol>
*/
private Function<EObject, String> idComputation = new DefaultIDFunction();
/** A diagnostic to be used for reporting on the matches. */
private BasicDiagnostic diagnostic;
/**
* Creates an ID based matcher without any delegate.
*/
public IdentifierEObjectMatcher() {
this(null, new DefaultIDFunction());
}
/**
* Creates an ID based matcher with the given delegate when no ID can be found.
*
* @param delegateWhenNoID
* the matcher to delegate to when no ID is found.
*/
public IdentifierEObjectMatcher(IEObjectMatcher delegateWhenNoID) {
this(delegateWhenNoID, new DefaultIDFunction());
}
/**
* Creates an ID based matcher computing the ID with the given function.
*
* @param idComputation
* the function used to compute the ID.
*/
public IdentifierEObjectMatcher(Function<EObject, String> idComputation) {
this(null, idComputation);
}
/**
* Create an ID based matcher with a delegate which is going to be called when no ID is found for a given
* EObject. It is computing the ID with the given function
*
* @param delegateWhenNoID
* the delegate matcher to use when no ID is found.
* @param idComputation
* the function used to compute the ID.
*/
public IdentifierEObjectMatcher(IEObjectMatcher delegateWhenNoID,
Function<EObject, String> idComputation) {
this.delegate = Optional.fromNullable(delegateWhenNoID);
this.idComputation = idComputation;
}
/**
* {@inheritDoc}
*/
public void createMatches(Comparison comparison, Iterator<? extends EObject> leftEObjects,
Iterator<? extends EObject> rightEObjects, Iterator<? extends EObject> originEObjects,
Monitor monitor) {
if (monitor.isCanceled()) {
throw new ComparisonCanceledException();
}
final List<EObject> leftEObjectsNoID = Lists.newArrayList();
final List<EObject> rightEObjectsNoID = Lists.newArrayList();
final List<EObject> originEObjectsNoID = Lists.newArrayList();
diagnostic = new BasicDiagnostic(Diagnostic.OK, "org.eclipse.emf.common", 0, //$NON-NLS-1$
org.eclipse.emf.common.CommonPlugin.INSTANCE.getString("_UI_OK_diagnostic_0"), null); //$NON-NLS-1$
// TODO Change API to pass the monitor to matchPerId()
final Set<Match> matches = matchPerId(leftEObjects, rightEObjects, originEObjects, leftEObjectsNoID,
rightEObjectsNoID, originEObjectsNoID);
addDiagnostic(comparison);
Iterables.addAll(comparison.getMatches(), matches);
if (!leftEObjectsNoID.isEmpty() || !rightEObjectsNoID.isEmpty() || !originEObjectsNoID.isEmpty()) {
if (delegate.isPresent()) {
doDelegation(comparison, leftEObjectsNoID, rightEObjectsNoID, originEObjectsNoID, monitor);
} else {
for (EObject eObject : leftEObjectsNoID) {
if (monitor.isCanceled()) {
throw new ComparisonCanceledException();
}
Match match = CompareFactory.eINSTANCE.createMatch();
match.setLeft(eObject);
matches.add(match);
}
for (EObject eObject : rightEObjectsNoID) {
if (monitor.isCanceled()) {
throw new ComparisonCanceledException();
}
Match match = CompareFactory.eINSTANCE.createMatch();
match.setRight(eObject);
matches.add(match);
}
for (EObject eObject : originEObjectsNoID) {
if (monitor.isCanceled()) {
throw new ComparisonCanceledException();
}
Match match = CompareFactory.eINSTANCE.createMatch();
match.setOrigin(eObject);
matches.add(match);
}
}
}
}
/**
* Execute matching process for the delegated IEObjectMatcher.
*
* @param comparison
* the comparison object that contains the matches
* @param monitor
* the monitor instance to track the matching progress
* @param leftEObjectsNoID
* remaining left objects after matching
* @param rightEObjectsNoID
* remaining right objects after matching
* @param originEObjectsNoID
* remaining origin objects after matching
*/
protected void doDelegation(Comparison comparison, final List<EObject> leftEObjectsNoID,
final List<EObject> rightEObjectsNoID, final List<EObject> originEObjectsNoID, Monitor monitor) {
delegate.get().createMatches(comparison, leftEObjectsNoID.iterator(), rightEObjectsNoID.iterator(),
originEObjectsNoID.iterator(), monitor);
}
/**
* Matches the EObject per ID.
*
* @param leftEObjects
* the objects to match (left side).
* @param rightEObjects
* the objects to match (right side).
* @param originEObjects
* the objects to match (origin side).
* @param leftEObjectsNoID
* remaining left objects after matching
* @param rightEObjectsNoID
* remaining right objects after matching
* @param originEObjectsNoID
* remaining origin objects after matching
* @return the match built in the process.
*/
protected Set<Match> matchPerId(Iterator<? extends EObject> leftEObjects,
Iterator<? extends EObject> rightEObjects, Iterator<? extends EObject> originEObjects,
final List<EObject> leftEObjectsNoID, final List<EObject> rightEObjectsNoID,
final List<EObject> originEObjectsNoID) {
MatchComputation computation = new MatchComputation(leftEObjects, rightEObjects, originEObjects,
leftEObjectsNoID, rightEObjectsNoID, originEObjectsNoID);
computation.compute();
return computation.getMatches();
}
/**
* This method is used to determine the parent objects during matching. The default implementation of this
* method returns the eContainer of the given {@code eObject}. Can be overwritten by clients to still
* allow proper matching when using a more complex architecture.
*
* @param eObject
* The {@link EObject} for which the parent object is to determine.
* @return The parent of the given {@code eObject}.
* @since 3.2
*/
protected EObject getParentEObject(EObject eObject) {
final EObject parent;
if (eObject != null) {
parent = eObject.eContainer();
} else {
parent = null;
}
return parent;
}
/**
* Adds a warning diagnostic to the comparison for the duplicate ID.
*
* @param side
* the side where the duplicate ID was found
* @param eObject
* the element with the duplicate ID
*/
private void reportDuplicateID(Side side, EObject eObject) {
final String duplicateID = idComputation.apply(eObject);
final String sideName = side.name().toLowerCase();
final String uriString = getUriString(eObject);
final String message;
if (uriString != null) {
message = EMFCompareMessages.getString("IdentifierEObjectMatcher.duplicateIdWithResource", //$NON-NLS-1$
duplicateID, sideName, uriString);
} else {
message = EMFCompareMessages.getString("IdentifierEObjectMatcher.duplicateId", //$NON-NLS-1$
duplicateID, sideName);
}
diagnostic
.add(new BasicDiagnostic(Diagnostic.WARNING, EMFCompare.DIAGNOSTIC_SOURCE, 0, message, null));
}
/**
* Returns a String representation of the URI of the given {@code eObject}'s resource.
* <p>
* If the {@code eObject}'s resource or its URI is <code>null</code>, this method returns
* <code>null</code>.
* </p>
*
* @param eObject
* The {@link EObject} for which's resource the string representation of its URI is determined.
* @return A String representation of the given {@code eObject}'s resource URI.
*/
private String getUriString(EObject eObject) {
String uriString = null;
final Resource resource = eObject.eResource();
if (resource != null && resource.getURI() != null) {
final URI uri = resource.getURI();
if (uri.isPlatform()) {
uriString = uri.toPlatformString(true);
} else {
uriString = uri.toString();
}
}
return uriString;
}
/**
* Adds the diagnostic to the comparison.
*
* @param comparison
* the comparison
*/
private void addDiagnostic(Comparison comparison) {
if (comparison.getDiagnostic() == null) {
comparison.setDiagnostic(diagnostic);
} else {
((BasicDiagnostic)comparison.getDiagnostic()).merge(diagnostic);
}
}
/**
* The default function used to retrieve IDs from EObject. You might want to extend or compose with it if
* you want to reuse its behavior.
*/
public static class DefaultIDFunction implements Function<EObject, String> {
/**
* Return an ID for an EObject, null if not found.
*
* @param eObject
* The EObject for which we need an identifier.
* @return The identifier for that EObject if we could determine one. <code>null</code> if no
* condition (see description above) is fulfilled for the given eObject.
*/
public String apply(EObject eObject) {
final String identifier;
if (eObject == null) {
identifier = null;
} else if (eObject.eIsProxy()) {
identifier = ((InternalEObject)eObject).eProxyURI().fragment();
} else {
final Resource eObjectResource = eObject.eResource();
final String xmiID;
if (eObjectResource instanceof XMIResource) {
xmiID = ((XMIResource)eObjectResource).getID(eObject);
} else {
xmiID = null;
}
if (xmiID != null) {
identifier = xmiID;
} else {
identifier = EcoreUtil.getID(eObject);
}
}
return identifier;
}
}
/**
* Helper class to manage two different maps within one class based on a switch boolean.
*
* @param <K>
* The class used as key in the internal maps.
* @param <V>
* The class used as value in the internal maps.
*/
private class SwitchMap<K, V> {
/**
* Map used when the switch boolean is true.
*/
final Map<K, V> trueMap = Maps.newHashMap();
/**
* Map used when the switch boolean is false.
*/
final Map<K, V> falseMap = Maps.newHashMap();
/**
* Puts the key-value pair in the map corresponding to the switch.
*
* @param switcher
* The boolean variable defining which map is to be used.
* @param key
* The key which is to be put into a map.
* @param value
* The value which is to be put into a map.
* @return {@code true} if the key was already contained in the chosen map, {@code false} otherwise.
*/
public boolean put(boolean switcher, K key, V value) {
final Map<K, V> selectedMap = getMap(switcher);
final boolean isContained = selectedMap.containsKey(key);
selectedMap.put(key, value);
return isContained;
}
/**
* Returns the value mapped to key.
*
* @param switcher
* The boolean variable defining which map is to be used.
* @param key
* The key for which the value is looked up.
* @return The value {@link V} if it exists, {@code null} otherwise.
*/
public V get(boolean switcher, K key) {
final Map<K, V> selectedMap = getMap(switcher);
return selectedMap.get(key);
}
/**
* Selects the map based on the given boolean.
*
* @param switcher
* Defined which map is to be used.
* @return {@link #trueMap} if {@code switcher} is true, {@link #falseMap} otherwise.
*/
private Map<K, V> getMap(boolean switcher) {
if (switcher) {
return falseMap;
} else {
return trueMap;
}
}
}
/**
* Computes matches from eObjects. We'll only iterate once on each of the three sides, building the
* matches as we go.
*
* @author <a href="mailto:axel.richard@obeo.fr">Axel Richard</a>
*/
private class MatchComputation {
/** Matches created by the {@link #compute()} process. */
private final Set<Match> matches;
/**
* We will try and mimic the structure of the input model. These maps do not need to be ordered, we
* only need fast lookup. Map each match to its left eObject.
*/
private final Map<EObject, Match> leftEObjectsToMatch;
/** Map each match to its right eObject. */
private final Map<EObject, Match> rightEObjectsToMatch;
/** Map each match to its origin eObject. */
private final Map<EObject, Match> originEObjectsToMatch;
/** Left eObjects to match. */
private Iterator<? extends EObject> leftEObjects;
/** Right eObjects to match. */
private Iterator<? extends EObject> rightEObjects;
/** Origin eObjects to match. */
private Iterator<? extends EObject> originEObjects;
/** Remaining left objects after matching. */
private List<EObject> leftEObjectsNoID;
/** Remaining right objects after matching. */
private List<EObject> rightEObjectsNoID;
/** Remaining origin objects after matching. */
private List<EObject> originEObjectsNoID;
/**
* This lookup map will be used by iterations on right and origin to find the match in which they
* should add themselves.
*/
private SwitchMap<String, Match> idProxyMap;
/**
* Constructor.
*
* @param leftEObjects
* Left eObjects to match.
* @param rightEObjects
* Right eObjects to match.
* @param originEObjects
* Origin eObjects to match.
* @param leftEObjectsNoID
* Remaining left objects after matching.
* @param rightEObjectsNoID
* Remaining left objects after matching.
* @param originEObjectsNoID
* Remaining left objects after matching.
*/
MatchComputation(Iterator<? extends EObject> leftEObjects, Iterator<? extends EObject> rightEObjects,
Iterator<? extends EObject> originEObjects, final List<EObject> leftEObjectsNoID,
final List<EObject> rightEObjectsNoID, final List<EObject> originEObjectsNoID) {
this.matches = Sets.newLinkedHashSet();
this.leftEObjectsToMatch = Maps.newHashMap();
this.rightEObjectsToMatch = Maps.newHashMap();
this.originEObjectsToMatch = Maps.newHashMap();
this.idProxyMap = new SwitchMap<String, Match>();
this.leftEObjects = leftEObjects;
this.rightEObjects = rightEObjects;
this.originEObjects = originEObjects;
this.leftEObjectsNoID = leftEObjectsNoID;
this.rightEObjectsNoID = rightEObjectsNoID;
this.originEObjectsNoID = originEObjectsNoID;
}
/**
* Returns the matches created by this computation.
*
* @return the matches created by this computation.
*/
public Set<Match> getMatches() {
return matches;
}
/**
* Computes matches.
*/
public void compute() {
computeLeftSide();
computeRightSide();
computeOriginSide();
reorganizeMatches();
}
/**
* Computes matches for left side.
*/
private void computeLeftSide() {
while (leftEObjects.hasNext()) {
final EObject left = leftEObjects.next();
final String identifier = idComputation.apply(left);
if (identifier != null) {
final Match match = CompareFactory.eINSTANCE.createMatch();
match.setLeft(left);
// Can we find a parent? Assume we're iterating in containment order
final EObject parentEObject = getParentEObject(left);
final Match parent = leftEObjectsToMatch.get(parentEObject);
if (parent != null) {
((InternalEList<Match>)parent.getSubmatches()).addUnique(match);
} else {
matches.add(match);
}
final boolean isAlreadyContained = idProxyMap.put(left.eIsProxy(), identifier, match);
if (isAlreadyContained) {
reportDuplicateID(Side.LEFT, left);
}
leftEObjectsToMatch.put(left, match);
} else {
leftEObjectsNoID.add(left);
}
}
}
/**
* Computes matches for right side.
*/
private void computeRightSide() {
while (rightEObjects.hasNext()) {
final EObject right = rightEObjects.next();
// Do we have an existing match?
final String identifier = idComputation.apply(right);
if (identifier != null) {
Match match = idProxyMap.get(right.eIsProxy(), identifier);
if (match != null) {
if (match.getRight() != null) {
reportDuplicateID(Side.RIGHT, right);
}
match.setRight(right);
rightEObjectsToMatch.put(right, match);
} else {
// Otherwise, create and place it.
match = CompareFactory.eINSTANCE.createMatch();
match.setRight(right);
// Can we find a parent?
final EObject parentEObject = getParentEObject(right);
final Match parent = rightEObjectsToMatch.get(parentEObject);
if (parent != null) {
((InternalEList<Match>)parent.getSubmatches()).addUnique(match);
} else {
matches.add(match);
}
rightEObjectsToMatch.put(right, match);
idProxyMap.put(right.eIsProxy(), identifier, match);
}
} else {
rightEObjectsNoID.add(right);
}
}
}
/**
* Computes matches for origin side.
*/
private void computeOriginSide() {
while (originEObjects.hasNext()) {
final EObject origin = originEObjects.next();
// Do we have an existing match?
final String identifier = idComputation.apply(origin);
if (identifier != null) {
Match match = idProxyMap.get(origin.eIsProxy(), identifier);
if (match != null) {
if (match.getOrigin() != null) {
reportDuplicateID(Side.ORIGIN, origin);
}
match.setOrigin(origin);
originEObjectsToMatch.put(origin, match);
} else {
// Otherwise, create and place it.
match = CompareFactory.eINSTANCE.createMatch();
match.setOrigin(origin);
// Can we find a parent?
final EObject parentEObject = getParentEObject(origin);
final Match parent = originEObjectsToMatch.get(parentEObject);
if (parent != null) {
((InternalEList<Match>)parent.getSubmatches()).addUnique(match);
} else {
matches.add(match);
}
idProxyMap.put(origin.eIsProxy(), identifier, match);
originEObjectsToMatch.put(origin, match);
}
} else {
originEObjectsNoID.add(origin);
}
}
}
/**
* Reorganize matches.
*/
private void reorganizeMatches() {
// For all root matches, check if they can be put under another match.
for (Match match : ImmutableSet.copyOf(matches)) {
EObject parentEObject = getParentEObject(match.getLeft());
Match parent = leftEObjectsToMatch.get(parentEObject);
if (parent != null) {
matches.remove(match);
((InternalEList<Match>)parent.getSubmatches()).addUnique(match);
} else {
parentEObject = getParentEObject(match.getRight());
parent = rightEObjectsToMatch.get(parentEObject);
if (parent != null) {
matches.remove(match);
((InternalEList<Match>)parent.getSubmatches()).addUnique(match);
} else {
parentEObject = getParentEObject(match.getOrigin());
parent = originEObjectsToMatch.get(parentEObject);
if (parent != null) {
matches.remove(match);
((InternalEList<Match>)parent.getSubmatches()).addUnique(match);
}
}
}
}
}
}
}