/*******************************************************************************
* 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
* Stefan Dirix - bug 473730 (ignore URI type descriptions)
*******************************************************************************/
package org.eclipse.emf.compare.utils;
import com.google.common.base.Function;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import java.lang.ref.WeakReference;
import java.lang.reflect.Array;
import java.util.concurrent.ExecutionException;
import org.eclipse.emf.common.notify.impl.AdapterImpl;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.compare.Comparison;
import org.eclipse.emf.compare.Match;
import org.eclipse.emf.compare.match.DefaultMatchEngine;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.emf.ecore.util.FeatureMap;
/**
* EMF Compare needs its own rules for "equality", which are based on similarity instead of strict equality.
* These will be used throughout the process.
*
* @author <a href="mailto:laurent.goubet@obeo.fr">Laurent Goubet</a>
*/
public class EqualityHelper extends AdapterImpl implements IEqualityHelper {
/** A cache keeping track of the URIs for EObjects. */
private final LoadingCache<EObject, URI> uriCache;
/**
* {@link #matchingEObjects(EObject, EObject)} is called _a lot_ of successive times with the same first
* parameter... we use this as a poor man's cache.
*/
private WeakReference<EObject> lastMatchedEObject;
/** See #lastMatchedEObject. */
private WeakReference<Match> lastMatch;
/**
* Creates a new EqualityHelper.
*
* @deprecated use the EqualityHelper(Cache) constructor instead.
*/
@Deprecated
public EqualityHelper() {
this(createDefaultCache(CacheBuilder.newBuilder()
.maximumSize(DefaultMatchEngine.DEFAULT_EOBJECT_URI_CACHE_MAX_SIZE)));
}
/**
* Creates a new EqualityHelper with the given cache.
*
* @param uriCache
* the cache to be used for {@link EcoreUtil#getURI(EObject)} calls.
*/
public EqualityHelper(LoadingCache<EObject, URI> uriCache) {
this.uriCache = uriCache;
}
/**
* {@inheritDoc}
*
* @see org.eclipse.emf.common.notify.impl.AdapterImpl#getTarget()
*/
@Override
public Comparison getTarget() {
return (Comparison)super.getTarget();
}
/**
* {@inheritDoc}
*
* @see org.eclipse.emf.common.notify.impl.AdapterImpl#isAdapterForType(java.lang.Object)
*/
@Override
public boolean isAdapterForType(Object type) {
return type == IEqualityHelper.class;
}
/**
* Check that the two given values are "equal", considering the specifics of EMF.
*
* @param comparison
* Provides us with the Match necessary for EObject comparison.
* @param object1
* First of the two objects to compare here.
* @param object2
* Second of the two objects to compare here.
* @return <code>true</code> if both objects are to be considered equal, <code>false</code> otherwise.
* @see #matchingValues(Object, Object)
*/
@Deprecated
public boolean matchingValues(Comparison comparison, Object object1, Object object2) {
return matchingValues(object1, object2);
}
/**
* {@inheritDoc}
*
* @see org.eclipse.emf.compare.utils.IEqualityHelper#matchingValues(java.lang.Object, java.lang.Object)
*/
public boolean matchingValues(Object object1, Object object2) {
final boolean equal;
if (object1 == object2) {
equal = true;
} else if (object1 instanceof EObject && object2 instanceof EObject) {
// [248442] This will handle FeatureMapEntries detection
equal = matchingEObjects((EObject)object1, (EObject)object2);
} else if (isNullOrEmptyString(object1) && isNullOrEmptyString(object2)) {
// Special case, consider that the empty String is equal to null (unset attributes)
equal = true;
} else if (object1 instanceof String || object1 instanceof Integer || object1 instanceof Boolean) {
// primitives and String are much more common than arrays... and isArray() is expensive.
equal = object1.equals(object2);
} else if (object1 != null && object1.getClass().isArray() && object2 != null
&& object2.getClass().isArray()) {
// [299641] compare arrays by their content instead of instance equality
equal = matchingArrays(object1, object2);
} else if (object1 instanceof FeatureMap.Entry && object2 instanceof FeatureMap.Entry) {
EStructuralFeature key1 = ((FeatureMap.Entry)object1).getEStructuralFeature();
EStructuralFeature key2 = ((FeatureMap.Entry)object2).getEStructuralFeature();
Object value1 = ((FeatureMap.Entry)object1).getValue();
Object value2 = ((FeatureMap.Entry)object2).getValue();
equal = key1.equals(key2) && matchingValues(value1, value2);
} else {
equal = object1 != null && object1.equals(object2);
}
return equal;
}
/**
* Returns {@code true} if the given {@code object} is {@code null} or the empty String.
*
* @param object
* The object we need to test.
* @return {@code true} if the given {@code object} is {@code null} or the empty String.
*/
private boolean isNullOrEmptyString(Object object) {
return object == null || object instanceof String && ((String)object).length() == 0;
}
/**
* Compares two values as EObjects, using their Match if it can be found, comparing through their URIs
* otherwise.
*
* @param object1
* First of the two objects to compare here.
* @param object2
* Second of the two objects to compare here.
* @return <code>true</code> if these two EObjects are to be considered equal, <code>false</code>
* otherwise.
*/
protected boolean matchingEObjects(EObject object1, EObject object2) {
final Match match = getMatch(object1);
final boolean equal;
// Match could be null if the value is out of the scope
if (match != null) {
equal = match.getLeft() == object2 || match.getRight() == object2 || match.getOrigin() == object2;
// use getTarget().getMatch() to avoid invalidating the cache here
} else if (getTarget().getMatch(object2) != null) {
equal = false;
} else if (object1.eClass() != object2.eClass()) {
equal = false;
} else {
equal = matchingURIs(object1, object2);
}
return equal;
}
/**
* Retrieves the match of the given EObject. This will cache the latest access so as to avoid a hashmap
* lookup in the comparison's inverse cross referencer. This is most useful when computing the LCS of two
* sequences, where we call {@link #matchingEObjects(EObject, EObject)} over and over with the same first
* object.
*
* @param o
* The object for which we need the associated Match.
* @return Match of this EObject if any, <code>null</code> otherwise.
*/
protected Match getMatch(EObject o) {
final Match match;
if (lastMatchedEObject != null && lastMatchedEObject.get() == o) {
Match temp = lastMatch.get();
if (temp != null) {
match = temp;
} else {
match = getTarget().getMatch(o);
lastMatch = new WeakReference<Match>(match);
}
} else {
match = getTarget().getMatch(o);
lastMatchedEObject = new WeakReference<EObject>(o);
lastMatch = new WeakReference<Match>(match);
}
return match;
}
/**
* Compare the URIs (of similar concept) of EObjects.
*
* @param object1
* First of the two objects to compare here.
* @param object2
* Second of the two objects to compare here.
* @return <code>true</code> if these two EObjects have the same URIs, <code>false</code> otherwise.
*/
protected boolean matchingURIs(EObject object1, EObject object2) {
// An object that is uncontained and is not a proxy has no URI. bypass them.
if (!object1.eIsProxy() && isUncontained(object1) || !object2.eIsProxy() && isUncontained(object2)) {
return false;
}
final boolean equal;
final URI uri1 = uriCache.getUnchecked(object1);
final URI uri2 = uriCache.getUnchecked(object2);
if (uri1.hasFragment() && uri2.hasFragment()) {
final String uri1Fragment = removeURIAttachment(uri1.fragment());
final String uri2Fragment = removeURIAttachment(uri2.fragment());
equal = uri1Fragment.equals(uri2Fragment);
} else {
equal = uri1.equals(uri2);
}
return equal;
}
/**
* To some {@link URI}s a human friendly description is attached describing the type the {@link URI} is
* pointing to. The description is marked by a "?" at the beginning and end. This method returns the
* fragment without the attached type description.
*
* @param fragment
* The {@link URI} fragment to check for a type description attachment
* @return The fragment of the {@link URI} stripped from type description if it has one, otherwise the
* original fragment is returned.
*/
private String removeURIAttachment(String fragment) {
// check if fragment contains at least two question marks
final int questionMark1 = fragment.indexOf('?');
final boolean hasTwoQuestionMarks = questionMark1 != -1
&& fragment.indexOf('?', questionMark1 + 1) != -1;
if (hasTwoQuestionMarks) {
return fragment.substring(0, questionMark1);
}
return fragment;
}
/**
* Checks whether the given object is contained anywhere.
*
* @param object
* The object whose container we are to check.
* @return <code>true</code> if the object has no reachable container, <code>false</code> otherwise.
*/
private boolean isUncontained(EObject object) {
return object.eContainer() == null && object.eResource() == null;
}
/**
* {@inheritDoc}
*
* @see org.eclipse.emf.compare.utils.IEqualityHelper#matchingAttributeValues(java.lang.Object,
* java.lang.Object)
*/
public boolean matchingAttributeValues(Object object1, Object object2) {
// The default equality helper handles attributes and references the same.
return matchingValues(object1, object2);
}
/**
* Compares two values as arrays, checking that their length and content match each other.
*
* @param object1
* First of the two objects to compare here.
* @param object2
* Second of the two objects to compare here.
* @return <code>true</code> if these two arrays are to be considered equal, <code>false</code> otherwise.
*/
private boolean matchingArrays(Object object1, Object object2) {
boolean equal = true;
final int length1 = Array.getLength(object1);
if (length1 != Array.getLength(object2)) {
equal = false;
} else {
for (int i = 0; i < length1 && equal; i++) {
final Object element1 = Array.get(object1, i);
final Object element2 = Array.get(object2, i);
equal = matchingValues(element1, element2);
}
}
return equal;
}
/**
* The EqualityHelper often needs to get an EObject uri. As such it has an internal cache that clients
* might leverage through this method.
*
* @param object
* any EObject.
* @return the URI of the given EObject, or {@code null} if we somehow could not compute it.
*/
@Deprecated
public URI getURI(EObject object) {
try {
return uriCache.get(object);
} catch (ExecutionException e) {
return null;
}
}
/**
* Returns the cache used by this object.
*
* @return the cache used by this object.
*/
@Deprecated
public Cache<EObject, URI> getCache() {
return uriCache;
}
/**
* Create a cache as required by EqualityHelper.
*
* @param cacheBuilder
* The builder to use to instantiate the cache.
* @return the new cache.
*/
public static LoadingCache<EObject, URI> createDefaultCache(CacheBuilder<Object, Object> cacheBuilder) {
return cacheBuilder.build(CacheLoader.from(new URICacheFunction()));
}
/**
* This is the function that will be used by our {@link #uriCache} to compute its values.
*
* @author <a href="mailto:laurent.goubet@obeo.fr">Laurent Goubet</a>
*/
private static class URICacheFunction implements Function<EObject, URI> {
/**
* {@inheritDoc}
*
* @see com.google.common.base.Function#apply(java.lang.Object)
*/
public URI apply(EObject input) {
if (input == null) {
return null;
}
return EcoreUtil.getURI(input);
}
}
}