Skip to main content
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/org.eclipse.emf.compare/src/org/eclipse/emf/compare/internal/utils/DiffUtil.java')
-rw-r--r--plugins/org.eclipse.emf.compare/src/org/eclipse/emf/compare/internal/utils/DiffUtil.java1057
1 files changed, 1057 insertions, 0 deletions
diff --git a/plugins/org.eclipse.emf.compare/src/org/eclipse/emf/compare/internal/utils/DiffUtil.java b/plugins/org.eclipse.emf.compare/src/org/eclipse/emf/compare/internal/utils/DiffUtil.java
new file mode 100644
index 000000000..dfa567f9a
--- /dev/null
+++ b/plugins/org.eclipse.emf.compare/src/org/eclipse/emf/compare/internal/utils/DiffUtil.java
@@ -0,0 +1,1057 @@
+/*******************************************************************************
+ * Copyright (c) 2012, 2013 Obeo.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Obeo - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.emf.compare.internal.utils;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.HashMultiset;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Multiset;
+import com.google.common.collect.Sets;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Set;
+
+import org.eclipse.emf.compare.AttributeChange;
+import org.eclipse.emf.compare.Comparison;
+import org.eclipse.emf.compare.Diff;
+import org.eclipse.emf.compare.DifferenceKind;
+import org.eclipse.emf.compare.DifferenceSource;
+import org.eclipse.emf.compare.DifferenceState;
+import org.eclipse.emf.compare.EMFCompareMessages;
+import org.eclipse.emf.compare.Match;
+import org.eclipse.emf.compare.ReferenceChange;
+import org.eclipse.emf.compare.utils.IEqualityHelper;
+import org.eclipse.emf.compare.utils.ReferenceUtil;
+import org.eclipse.emf.ecore.EObject;
+import org.eclipse.emf.ecore.EReference;
+import org.eclipse.emf.ecore.EStructuralFeature;
+import org.eclipse.emf.ecore.EcorePackage;
+import org.eclipse.emf.ecore.util.InternalEList;
+
+/**
+ * This utility class will be used to provide similarity implementations.
+ *
+ * @author <a href="mailto:laurent.goubet@obeo.fr">Laurent Goubet</a>
+ */
+public final class DiffUtil {
+ /** This utility class does not need to be instantiated. */
+ private DiffUtil() {
+ // Hides default constructor
+ }
+
+ /**
+ * Computes the dice coefficient between the two given String's bigrams.
+ * <p>
+ * This implementation is case sensitive.
+ * </p>
+ *
+ * @param first
+ * First of the two Strings to compare.
+ * @param second
+ * Second of the two Strings to compare.
+ * @return The dice coefficient of the two given String's bigrams, ranging from 0 to 1.
+ */
+ public static double diceCoefficient(String first, String second) {
+ final char[] str1 = first.toCharArray();
+ final char[] str2 = second.toCharArray();
+
+ final double coefficient;
+
+ if (Arrays.equals(str1, str2)) {
+ coefficient = 1d;
+ } else if (str1.length <= 2 || str2.length <= 2) {
+ int equalChars = 0;
+
+ for (int i = 0; i < Math.min(str1.length, str2.length); i++) {
+ if (str1[i] == str2[i]) {
+ equalChars++;
+ }
+ }
+
+ int union = str1.length + str2.length;
+ if (str1.length != str2.length) {
+ coefficient = (double)equalChars / union;
+ } else {
+ coefficient = ((double)equalChars * 2) / union;
+ }
+ } else {
+ Set<String> s1Bigrams = Sets.newHashSet();
+ Set<String> s2Bigrams = Sets.newHashSet();
+
+ for (int i = 0; i < str1.length - 1; i++) {
+ char[] chars = new char[] {str1[i], str1[i + 1], };
+ s1Bigrams.add(String.valueOf(chars));
+ }
+ for (int i = 0; i < str2.length - 1; i++) {
+ char[] chars = new char[] {str2[i], str2[i + 1], };
+ s2Bigrams.add(String.valueOf(chars));
+ }
+
+ Set<String> intersection = Sets.intersection(s1Bigrams, s2Bigrams);
+ coefficient = (2d * intersection.size()) / (s1Bigrams.size() + s2Bigrams.size());
+ }
+
+ return coefficient;
+ }
+
+ /**
+ * This will compute the longest common subsequence between the two given Lists, ignoring any object that
+ * is included in {@code ignoredElements}. We will use
+ * {@link IEqualityHelper#matchingValues(Object, Object)} in order to try and match the values from both
+ * lists two-by-two. This can thus be used both for reference values or attribute values. If there are two
+ * subsequences of the same "longest" length, the first (according to the second argument) will be
+ * returned.
+ * <p>
+ * Take note that this might be slower than {@link #longestCommonSubsequence(Comparison, List, List)} and
+ * should only be used when elements should be removed from the potential LCS. This is mainly aimed at
+ * merge operations during three-way comparisons as some objects might be in conflict and thus shifting
+ * the computed insertion indices.
+ * </p>
+ * <p>
+ * Please see {@link #longestCommonSubsequence(Comparison, List, List)} for a more complete description.
+ * </p>
+ *
+ * @param comparison
+ * This will be used in order to retrieve the Match for EObjects when comparing them.
+ * @param ignoredElements
+ * Specifies elements that should be excluded from the subsequences.
+ * @param sequence1
+ * First of the two sequences to consider.
+ * @param sequence2
+ * Second of the two sequences to consider.
+ * @param <E>
+ * Type of the sequences content.
+ * @return The LCS of the two given sequences. Will never be the same instance as one of the input
+ * sequences.
+ * @see #longestCommonSubsequence(Comparison, List, List).
+ */
+ public static <E> List<E> longestCommonSubsequence(Comparison comparison, Iterable<E> ignoredElements,
+ List<E> sequence1, List<E> sequence2) {
+ final List<E> copy1 = Lists.newArrayList(sequence1);
+ final List<E> copy2 = Lists.newArrayList(sequence2);
+
+ // Reduce sets
+ final List<E> prefix = trimPrefix(comparison, ignoredElements, copy1, copy2);
+ final List<E> suffix = trimSuffix(comparison, ignoredElements, copy1, copy2);
+
+ final List<E> subLCS;
+ // FIXME extract an interface for the LCS and properly separate these two differently typed
+ // implementations.
+ if (copy1.size() > Short.MAX_VALUE || copy2.size() > Short.MAX_VALUE) {
+ subLCS = intLongestCommonSubsequence(comparison, ignoredElements, copy1, copy2);
+ } else {
+ subLCS = shortLongestCommonSubsequence(comparison, ignoredElements, copy1, copy2);
+ }
+
+ return ImmutableList.copyOf(Iterables.concat(prefix, subLCS, suffix));
+ }
+
+ /**
+ * This will compute the longest common subsequence between the two given Lists. We will use
+ * {@link IEqualityHelper#matchingValues(Object, Object)} in order to try and match the values from both
+ * lists two-by-two. This can thus be used both for reference values or attribute values. If there are two
+ * subsequences of the same "longest" length, the first (according to the second argument) will be
+ * returned.
+ * <p>
+ * For example, it the two given sequence are, in this order, <code>{"a", "b", "c", "d", "e"}</code> and
+ * <code>{"c", "z", "d", "a", "b"}</code>, there are two "longest" subsequences : <code>{"a", "b"}</code>
+ * and <code>{"c", "d"}</code>. The first of those two subsequences in the second list is
+ * <code>{"c", "d"}</code>. On the other hand, the LCS of <code>{"a", "b", "c", "d", "e"}</code> and
+ * <code>{"y", "c", "d", "e", "b"}</code> is <code>{"c", "d", "e"}</code>.
+ * </p>
+ * <p>
+ * The following algorithm has been inferred from the wikipedia article on the Longest Common Subsequence,
+ * http://en.wikipedia.org/wiki/Longest_common_subsequence_problem at the time of writing. It is
+ * decomposed in two : we first compute the LCS matrix, then we backtrack through the input to determine
+ * the LCS. Evaluation will be shortcut after the first part if the LCS is one of the two input sequences.
+ * </p>
+ * <p>
+ * Note : we are not using Iterables as input in order to make use of the random access cost of
+ * ArrayLists. This might also be converted to directly use arrays. This implementation will not play well
+ * with LinkedLists or any List which needs to iterate over the values for each call to
+ * {@link List#get(int)}, i.e any list which is not instanceof RandomAccess or does not satisfy its
+ * contract.
+ * </p>
+ *
+ * @param comparison
+ * This will be used in order to retrieve the Match for EObjects when comparing them.
+ * @param sequence1
+ * First of the two sequences to consider.
+ * @param sequence2
+ * Second of the two sequences to consider.
+ * @param <E>
+ * Type of the sequences content.
+ * @return The LCS of the two given sequences. Will never be the same instance as one of the input
+ * sequences.
+ */
+ public static <E> List<E> longestCommonSubsequence(Comparison comparison, List<E> sequence1,
+ List<E> sequence2) {
+ return longestCommonSubsequence(comparison, Collections.<E> emptyList(), sequence1, sequence2);
+ }
+
+ /**
+ * Trims and returns the common prefix of the two given sequences. All ignored elements within or after
+ * this common prefix will also be trimmed.
+ * <p>
+ * Note that the two given sequences will be modified in-place.
+ * </p>
+ *
+ * @param comparison
+ * This will be used in order to retrieve the Match for EObjects when comparing them.
+ * @param ignoredElements
+ * Specifies elements that should be excluded from the subsequences.
+ * @param sequence1
+ * First of the two sequences to consider.
+ * @param sequence2
+ * Second of the two sequences to consider.
+ * @param <E>
+ * Type of the sequences content.
+ * @return The common prefix of the two given sequences, less their ignored elements. As a side note, both
+ * {@code sequence1} and {@code sequence2} will have been trimmed of their prefix when this
+ * returns.
+ */
+ private static <E> List<E> trimPrefix(Comparison comparison, Iterable<E> ignoredElements,
+ List<E> sequence1, List<E> sequence2) {
+ final IEqualityHelper equalityHelper = comparison.getEqualityHelper();
+ final int size1 = sequence1.size();
+ final int size2 = sequence2.size();
+
+ final List<E> prefix = Lists.newArrayList();
+ int start1 = 1;
+ int start2 = 1;
+ boolean matching = true;
+ while (start1 <= size1 && start2 <= size2 && matching) {
+ final E first = sequence1.get(start1 - 1);
+ final E second = sequence2.get(start2 - 1);
+ if (equalityHelper.matchingValues(first, second)) {
+ prefix.add(first);
+ start1++;
+ start2++;
+ } else {
+ boolean ignore1 = contains(comparison, equalityHelper, ignoredElements, first);
+ boolean ignore2 = contains(comparison, equalityHelper, ignoredElements, second);
+ if (ignore1) {
+ start1++;
+ }
+ if (ignore2) {
+ start2++;
+ }
+ if (!ignore1 && !ignore2) {
+ matching = false;
+ }
+ }
+ }
+ sequence1.subList(0, start1 - 1).clear();
+ sequence2.subList(0, start2 - 1).clear();
+
+ return prefix;
+ }
+
+ /**
+ * Trims and returns the common suffix of the two given sequences. All ignored elements within or before
+ * this common suffix will also be trimmed.
+ * <p>
+ * Note that the two given sequences will be modified in-place.
+ * </p>
+ *
+ * @param comparison
+ * This will be used in order to retrieve the Match for EObjects when comparing them.
+ * @param ignoredElements
+ * Specifies elements that should be excluded from the subsequences.
+ * @param sequence1
+ * First of the two sequences to consider.
+ * @param sequence2
+ * Second of the two sequences to consider.
+ * @param <E>
+ * Type of the sequences content.
+ * @return The common suffix of the two given sequences, less their ignored elements. As a side note, both
+ * {@code sequence1} and {@code sequence2} will have been trimmed of their suffix when this
+ * returns.
+ */
+ private static <E> List<E> trimSuffix(Comparison comparison, Iterable<E> ignoredElements,
+ List<E> sequence1, List<E> sequence2) {
+ final IEqualityHelper equalityHelper = comparison.getEqualityHelper();
+ final int size1 = sequence1.size();
+ final int size2 = sequence2.size();
+
+ final List<E> suffix = Lists.newArrayList();
+ int end1 = size1;
+ int end2 = size2;
+ boolean matching = true;
+ while (end1 > 0 && end2 > 0 && matching) {
+ final E first = sequence1.get(end1 - 1);
+ final E second = sequence2.get(end2 - 1);
+ if (equalityHelper.matchingValues(first, second)) {
+ suffix.add(first);
+ end1--;
+ end2--;
+ } else {
+ boolean ignore1 = contains(comparison, equalityHelper, ignoredElements, first);
+ boolean ignore2 = contains(comparison, equalityHelper, ignoredElements, second);
+ if (ignore1) {
+ end1--;
+ }
+ if (ignore2) {
+ end2--;
+ }
+ if (!ignore1 && !ignore2) {
+ matching = false;
+ }
+ }
+ }
+ sequence1.subList(end1, size1).clear();
+ sequence2.subList(end2, size2).clear();
+
+ return Lists.reverse(suffix);
+ }
+
+ /**
+ * Checks whether the given {@code sequence} contains the given {@code element} according to the semantics
+ * of the given {@code equalityHelper}.
+ *
+ * @param comparison
+ * This will be used in order to retrieve the Match for EObjects when comparing them.
+ * @param equalityHelper
+ * The {@link IEqualityHelper} gives us the necessary semantics for Object matching.
+ * @param sequence
+ * The sequence which elements we need to compare with {@code element}.
+ * @param element
+ * The element we are seeking in {@code sequence}.
+ * @param <E>
+ * Type of the sequence content.
+ * @return {@code true} if the given {@code sequence} contains an element matching {@code element},
+ * {@code false} otherwise.
+ * @see IEqualityHelper#matchingValues(Comparison, Object, Object)
+ */
+ private static <E> boolean contains(Comparison comparison, IEqualityHelper equalityHelper,
+ Iterable<E> sequence, E element) {
+ final Iterator<E> iterator = sequence.iterator();
+ while (iterator.hasNext()) {
+ E candidate = iterator.next();
+ if (equalityHelper.matchingValues(candidate, element)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * This is a classic, single-threaded implementation. We use shorts for the score matrix so as to limit
+ * the memory cost (we know the max LCS length is not greater than Short#MAX_VALUE).
+ *
+ * @param comparison
+ * This will be used in order to retrieve the Match for EObjects when comparing them.
+ * @param ignoredElements
+ * Specifies elements that should be excluded from the subsequences.
+ * @param sequence1
+ * First of the two sequences to consider.
+ * @param sequence2
+ * Second of the two sequences to consider.
+ * @param <E>
+ * Type of the sequences content.
+ * @return The LCS of the two given sequences. Will never be the same instance as one of the input
+ * sequences.
+ */
+ private static <E> List<E> shortLongestCommonSubsequence(Comparison comparison,
+ Iterable<E> ignoredElements, List<E> sequence1, List<E> sequence2) {
+ final IEqualityHelper equalityHelper = comparison.getEqualityHelper();
+ final int size1 = sequence1.size();
+ final int size2 = sequence2.size();
+
+ final short[][] matrix = new short[size1 + 1][size2 + 1];
+
+ // Compute the LCS matrix
+ for (int i = 1; i <= size1; i++) {
+ final E first = sequence1.get(i - 1);
+ for (int j = 1; j <= size2; j++) {
+ // assume array dereferencing and arithmetics faster than equals
+ final short current = matrix[i - 1][j - 1];
+ final short nextIfNoMatch = (short)Math.max(matrix[i - 1][j], matrix[i][j - 1]);
+
+ if (nextIfNoMatch > current) {
+ matrix[i][j] = nextIfNoMatch;
+ } else {
+ final E second = sequence2.get(j - 1);
+ if (equalityHelper.matchingValues(first, second)
+ && !contains(comparison, equalityHelper, ignoredElements, second)) {
+ matrix[i][j] = (short)(1 + current);
+ } else {
+ matrix[i][j] = nextIfNoMatch;
+ }
+ }
+ }
+ }
+
+ // Traceback the matrix to create the final LCS
+ int current1 = size1;
+ int current2 = size2;
+ final List<E> result = Lists.newArrayList();
+
+ while (current1 > 0 && current2 > 0) {
+ final short currentLength = matrix[current1][current2];
+ final short nextLeft = matrix[current1][current2 - 1];
+ final short nextUp = matrix[current1 - 1][current2];
+ if (currentLength > nextLeft && currentLength > nextUp) {
+ result.add(sequence1.get(current1 - 1));
+ current1--;
+ current2--;
+ } else if (nextLeft >= nextUp) {
+ current2--;
+ } else {
+ current1--;
+ }
+ }
+
+ return Lists.reverse(result);
+ }
+
+ /**
+ * This is a classic, single-threaded implementation. We know the max LCS length is greater than
+ * Short#MAX_VALUE... the score matrix will thus be int-typed, resulting in a huge memory cost.
+ *
+ * @param comparison
+ * This will be used in order to retrieve the Match for EObjects when comparing them.
+ * @param ignoredElements
+ * Specifies elements that should be excluded from the subsequences.
+ * @param sequence1
+ * First of the two sequences to consider.
+ * @param sequence2
+ * Second of the two sequences to consider.
+ * @param <E>
+ * Type of the sequences content.
+ * @return The LCS of the two given sequences. Will never be the same instance as one of the input
+ * sequences.
+ */
+ private static <E> List<E> intLongestCommonSubsequence(Comparison comparison,
+ Iterable<E> ignoredElements, List<E> sequence1, List<E> sequence2) {
+ final IEqualityHelper equalityHelper = comparison.getEqualityHelper();
+ final int size1 = sequence1.size();
+ final int size2 = sequence2.size();
+
+ final int[][] matrix = new int[size1 + 1][size2 + 1];
+
+ // Compute the LCS matrix
+ for (int i = 1; i <= size1; i++) {
+ final E first = sequence1.get(i - 1);
+ for (int j = 1; j <= size2; j++) {
+ // assume array dereferencing and arithmetics faster than equals
+ final int current = matrix[i - 1][j - 1];
+ final int nextIfNoMatch = Math.max(matrix[i - 1][j], matrix[i][j - 1]);
+
+ if (nextIfNoMatch > current) {
+ matrix[i][j] = nextIfNoMatch;
+ } else {
+ final E second = sequence2.get(j - 1);
+ if (equalityHelper.matchingValues(first, second)
+ && !contains(comparison, equalityHelper, ignoredElements, second)) {
+ matrix[i][j] = 1 + current;
+ } else {
+ matrix[i][j] = nextIfNoMatch;
+ }
+ }
+ }
+ }
+
+ // Traceback the matrix to create the final LCS
+ int current1 = size1;
+ int current2 = size2;
+ final List<E> result = Lists.newArrayList();
+
+ while (current1 > 0 && current2 > 0) {
+ final int currentLength = matrix[current1][current2];
+ final int nextLeft = matrix[current1][current2 - 1];
+ final int nextUp = matrix[current1 - 1][current2];
+ if (currentLength > nextLeft && currentLength > nextUp) {
+ result.add(sequence1.get(current1 - 1));
+ current1--;
+ current2--;
+ } else if (nextLeft >= nextUp) {
+ current2--;
+ } else {
+ current1--;
+ }
+ }
+
+ return Lists.reverse(result);
+ }
+
+ /*
+ * TODO perf : all "lookups" in source and target could be rewritten by using the lcs elements' matches.
+ * This may or may not help, should be profiled.
+ */
+ /**
+ * This will try and determine the index at which a given element from the {@code source} list should be
+ * inserted in the {@code target} list. We expect {@code newElement} to be an element from the
+ * {@code source} or to have a Match that allows us to map it to one of the {@code source} list's
+ * elements.
+ * <p>
+ * The expected insertion index will always be relative to the Longest Common Subsequence (LCS) between
+ * the two given lists, ignoring all elements from that LCS that have changed between the target list and
+ * the common origin of the two. If there are more than one "longest" subsequence between the two lists,
+ * the insertion index will be relative to the first that comes in the {@code target} list.
+ * </p>
+ * <p>
+ * Note : we are not using Iterables as input in order to make use of the random access cost of
+ * ArrayLists. This might also be converted to directly use arrays. This implementation will not play well
+ * with LinkedLists or any List which needs to iterate over the values for each call to
+ * {@link List#get(int)}, i.e any list which is not instanceof RandomAccess or does not satisfy its
+ * contract.
+ * </p>
+ *
+ * @param comparison
+ * This will be used in order to retrieve the Match for EObjects when comparing them.
+ * @param ignoredElements
+ * If there are elements from {@code target} that should be ignored when searching for an
+ * insertion index, set them here. Can be {@code null} or an empty list.
+ * @param source
+ * The List from which one element has to be added to the {@code target} list.
+ * @param target
+ * The List into which one element from {@code source} has to be added.
+ * @param newElement
+ * The element from {@code source} that needs to be added into {@code target}.
+ * @param <E>
+ * Type of the sequences content.
+ * @return The index at which {@code newElement} should be inserted in {@code target}.
+ * @see #longestCommonSubsequence(Comparison, List, List)
+ * @noreference This method is not intended to be referenced by clients.
+ */
+ public static <E> int findInsertionIndex(Comparison comparison, Iterable<E> ignoredElements,
+ List<E> source, List<E> target, E newElement) {
+ final IEqualityHelper equalityHelper = comparison.getEqualityHelper();
+
+ // We assume that "newElement" is in source but not in the target yet
+ final List<E> lcs;
+ if (ignoredElements != null) {
+ lcs = longestCommonSubsequence(comparison, ignoredElements, source, target);
+ } else {
+ lcs = longestCommonSubsequence(comparison, source, target);
+ }
+
+ E firstLCS = null;
+ E lastLCS = null;
+ if (lcs.size() > 0) {
+ firstLCS = lcs.get(0);
+ lastLCS = lcs.listIterator(lcs.size()).previous();
+ }
+
+ final int noLCS = -2;
+ int currentIndex = -1;
+ int firstLCSIndex = -1;
+ int lastLCSIndex = -1;
+ if (firstLCS == null) {
+ // We have no LCS
+ firstLCSIndex = noLCS;
+ lastLCSIndex = noLCS;
+ }
+
+ ListIterator<E> sourceIterator = source.listIterator();
+ for (int i = 0; sourceIterator.hasNext() && (currentIndex == -1 || firstLCSIndex == -1); i++) {
+ final E sourceElement = sourceIterator.next();
+ if (currentIndex == -1 && equalityHelper.matchingValues(sourceElement, newElement)) {
+ currentIndex = i;
+ }
+ if (firstLCSIndex == -1 && equalityHelper.matchingValues(sourceElement, firstLCS)) {
+ firstLCSIndex = i;
+ }
+ }
+ // The list may contain duplicates, use a reverse iteration to find the last from LCS.
+ final int sourceSize = source.size();
+ sourceIterator = source.listIterator(sourceSize);
+ for (int i = sourceSize - 1; sourceIterator.hasPrevious() && lastLCSIndex == -1; i--) {
+ final E sourceElement = sourceIterator.previous();
+ if (lastLCSIndex == -1 && equalityHelper.matchingValues(sourceElement, lastLCS)) {
+ lastLCSIndex = i;
+ }
+ }
+
+ int insertionIndex = -1;
+ if (firstLCSIndex == noLCS) {
+ // We have no LCS. The two lists have no element in common. Insert at the very end of the target.
+ insertionIndex = target.size();
+ } else if (currentIndex < firstLCSIndex) {
+ // The object we are to insert is before the LCS in source.
+ insertionIndex = insertBeforeLCS(target, equalityHelper, firstLCS);
+ } else if (currentIndex > lastLCSIndex) {
+ // The object we are to insert is after the LCS in source.
+ insertionIndex = findInsertionIndexAfterLCS(target, equalityHelper, lastLCS);
+ } else {
+ // Our object is in-between two elements A and B of the LCS in source
+ insertionIndex = findInsertionIndexWithinLCS(source, target, equalityHelper, lcs, currentIndex);
+ }
+
+ // We somehow failed to determine the insertion index. Insert at the very end.
+ if (insertionIndex == -1) {
+ insertionIndex = target.size();
+ }
+
+ return insertionIndex;
+ }
+
+ /**
+ * This will be called to try and find the insertion index for an element that is located in-between two
+ * elements of the LCS between {@code source} and {@code target}.
+ *
+ * @param source
+ * The List from which one element has to be added to the {@code target} list.
+ * @param target
+ * The List into which one element from {@code source} has to be added.
+ * @param equalityHelper
+ * The equality helper to use for this computation.
+ * @param lcs
+ * The lcs between {@code source} and {@code target}.
+ * @param currentIndex
+ * Current index (in {@code source} of the element we are to insert into {@code target}.
+ * @param <E>
+ * Type of the sequences content.
+ * @return The index in the target list in which should be inserted that element.
+ */
+ private static <E> int findInsertionIndexWithinLCS(List<E> source, List<E> target,
+ final IEqualityHelper equalityHelper, final List<E> lcs, int currentIndex) {
+ int insertionIndex = -1;
+ /*
+ * If any element of the subsequence {<index of A>, <index of B>} from source had been in the same
+ * subsequence in target, it would have been part of the LCS. We thus know none is.
+ */
+ // The insertion index will be just after A in target
+
+ // First, find which element of the LCS is "A"
+ int lcsIndexOfSubsequenceStart = -1;
+ for (int i = 0; i < currentIndex; i++) {
+ final E sourceElement = source.get(i);
+
+ boolean isInLCS = false;
+ for (int j = lcsIndexOfSubsequenceStart + 1; j < lcs.size() && !isInLCS; j++) {
+ final E lcsElement = lcs.get(j);
+
+ if (equalityHelper.matchingValues(sourceElement, lcsElement)) {
+ isInLCS = true;
+ lcsIndexOfSubsequenceStart++;
+ }
+ }
+ }
+
+ // Do we have duplicates before A in the lcs?
+ final Multiset<E> dupesLCS = HashMultiset.create(lcs.subList(0, lcsIndexOfSubsequenceStart + 1));
+ final E subsequenceStart = lcs.get(lcsIndexOfSubsequenceStart);
+ int duplicatesToGo = dupesLCS.count(subsequenceStart) - 1;
+
+ // Then, find the index of "A" in target
+ for (int i = 0; i < target.size() && insertionIndex == -1; i++) {
+ final E targetElement = target.get(i);
+
+ if (equalityHelper.matchingValues(targetElement, subsequenceStart)) {
+ if (duplicatesToGo > 0) {
+ duplicatesToGo--;
+ } else {
+ insertionIndex = i + 1;
+ }
+ }
+ }
+
+ return insertionIndex;
+ }
+
+ /**
+ * This will be called when we are to insert an element after the LCS in the {@code target} list.
+ *
+ * @param target
+ * The List into which one element has to be added.
+ * @param equalityHelper
+ * The equality helper to use for this computation.
+ * @param lastLCS
+ * The last element of the LCS.
+ * @param <E>
+ * Type of the sequences content.
+ * @return The index to use for insertion into {@code target} in order to add an element just after the
+ * LCS.
+ */
+ private static <E> int findInsertionIndexAfterLCS(List<E> target, IEqualityHelper equalityHelper,
+ E lastLCS) {
+ int insertionIndex = -1;
+ // The insertion index will be inside the subsequence {<LCS end>, <list.size()>} in target.
+ /*
+ * We'll insert it just after the LCS end : there cannot be any common element between the two lists
+ * "after" the LCS since it would be part of the LCS itself.
+ */
+ for (int i = target.size() - 1; i >= 0 && insertionIndex == -1; i--) {
+ final E targetElement = target.get(i);
+ if (equalityHelper.matchingValues(targetElement, lastLCS)) {
+ // We've reached the last element of the LCS in target. insert after it.
+ insertionIndex = i + 1;
+ }
+ }
+ return insertionIndex;
+ }
+
+ /**
+ * This will be called when we are to insert an element before the LCS in the {@code target} list.
+ *
+ * @param target
+ * The List into which one element has to be added.
+ * @param equalityHelper
+ * The equality helper to use for this computation.
+ * @param firstLCS
+ * The first element of the LCS.
+ * @param <E>
+ * Type of the sequences content.
+ * @return The index to use for insertion into {@code target} in order to add an element just before the
+ * LCS.
+ */
+ private static <E> int insertBeforeLCS(List<E> target, IEqualityHelper equalityHelper, E firstLCS) {
+ int insertionIndex = -1;
+ // The insertion index will be inside the subsequence {0, <LCS start>} in target
+ /*
+ * We'll insert it just before the LCS start : there cannot be any common element between the two
+ * lists "before" the LCS since it would be part of the LCS itself.
+ */
+ for (int i = 0; i < target.size() && insertionIndex == -1; i++) {
+ final E targetElement = target.get(i);
+
+ if (equalityHelper.matchingValues(targetElement, firstLCS)) {
+ // We've reached the first element from the LCS in target. Insert here
+ insertionIndex = i;
+ }
+ }
+ return insertionIndex;
+ }
+
+ /**
+ * This will try and determine the index at which a given element from the {@code source} list should be
+ * inserted in the {@code target} list. We expect {@code newElement} to be an element from the
+ * {@code source} or to have a Match that allows us to map it to one of the {@code source} list's
+ * elements.
+ * <p>
+ * The expected insertion index will always be relative to the Longest Common Subsequence (LCS) between
+ * the two given lists. If there are more than one "longest" subsequence between the two lists, the
+ * insertion index will be relative to the first that comes in the {@code target} list.
+ * </p>
+ * <p>
+ * For example, assume {@code source} is <code>{"1", "2", "4", "6", "8", "3", "0", "7", "5"}</code> and
+ * {@code target} is <code>{"8", "1", "2", "9", "3", "4", "7"}</code>; I try to merge the addition of
+ * {@code "0"} in the right list. The returned "insertion index" will be {@code 5} : just after
+ * {@code "3"}. There are two subsequence of the same "longest" length 4 :
+ * <code>{"1", "2", "3", "7"}</code> and <code>{"1", "2", "4", "7"}</code>. However, the first of those
+ * two in {@code target} is <code>{"1", "2", "3", "7"}</code>. The closest element before {@code "0"} in
+ * this LCS in {@code source} is {@code "3"}.
+ * </p>
+ * <p>
+ * Note : we are not using Iterables as input in order to make use of the random access cost of
+ * ArrayLists. This might also be converted to directly use arrays. This implementation will not play well
+ * with LinkedLists or any List which needs to iterate over the values for each call to
+ * {@link List#get(int)}, i.e any list which is not instanceof RandomAccess or does not satisfy its
+ * contract.
+ * </p>
+ *
+ * @param comparison
+ * This will be used in order to retrieve the Match for EObjects when comparing them.
+ * @param source
+ * The List from which one element has to be added to the {@code target} list.
+ * @param target
+ * The List into which one element from {@code source} has to be added.
+ * @param newElement
+ * The element from {@code source} that needs to be added into {@code target}.
+ * @param <E>
+ * Type of the sequences content.
+ * @return The index at which {@code newElement} should be inserted in {@code target}.
+ * @see #longestCommonSubsequence(Comparison, List, List)
+ */
+ public static <E> int findInsertionIndex(Comparison comparison, List<E> source, List<E> target,
+ E newElement) {
+ return findInsertionIndex(comparison, null, source, target, newElement);
+ }
+
+ /**
+ * This is the main entry point for {@link #findInsertionIndex(Comparison, Iterable, List, List, Object)}.
+ * It will use default algorithms to determine the source and target lists as well as the list of elements
+ * that should be ignored when computing the insertion index.
+ *
+ * @param comparison
+ * This will be used in order to retrieve the Match for EObjects when comparing them.
+ * @param diff
+ * The diff which merging will trigger the need for an insertion index in its target list.
+ * @param rightToLeft
+ * {@code true} if the merging will be done into the left list, so that we should consider the
+ * right model as the source and the left as the target.
+ * @return The index at which this {@code diff}'s value should be inserted into the 'target' list, as
+ * inferred from {@code rightToLeft}.
+ * @see #findInsertionIndex(Comparison, Iterable, List, List, Object)
+ */
+ @SuppressWarnings("unchecked")
+ public static int findInsertionIndex(Comparison comparison, Diff diff, boolean rightToLeft) {
+ final EStructuralFeature feature;
+ final Object value;
+ if (diff instanceof AttributeChange) {
+ feature = ((AttributeChange)diff).getAttribute();
+ value = ((AttributeChange)diff).getValue();
+ } else if (diff instanceof ReferenceChange) {
+ feature = ((ReferenceChange)diff).getReference();
+ value = ((ReferenceChange)diff).getValue();
+ } else {
+ throw new IllegalArgumentException(EMFCompareMessages.getString(
+ "DiffUtil.IllegalDiff", diff.eClass().getName())); //$NON-NLS-1$
+ }
+ if (!feature.isMany()) {
+ throw new IllegalArgumentException(EMFCompareMessages.getString(
+ "DiffUtil.IllegalFeature", feature.getName())); //$NON-NLS-1$
+ }
+ final Match match = diff.getMatch();
+
+ final EObject expectedContainer;
+ if (feature instanceof EReference && ((EReference)feature).isContainment()
+ && diff.getKind() == DifferenceKind.MOVE) {
+ // The value can only be an EObject, and its match cannot be null.
+ // If any of these two assumptions is wrong, something went horribly awry beforehand.
+ final Match valueMatch = comparison.getMatch((EObject)value);
+
+ final Match targetContainerMatch;
+ // If it exists, use the source side's container as reference
+ if (rightToLeft && valueMatch.getRight() != null) {
+ targetContainerMatch = comparison.getMatch(valueMatch.getRight().eContainer());
+ } else if (!rightToLeft && valueMatch.getLeft() != null) {
+ targetContainerMatch = comparison.getMatch(valueMatch.getLeft().eContainer());
+ } else {
+ // Otherwise, the value we're moving on one side has been removed from its source side.
+ targetContainerMatch = comparison.getMatch(valueMatch.getOrigin().eContainer());
+ }
+ if (rightToLeft) {
+ expectedContainer = targetContainerMatch.getLeft();
+ } else {
+ expectedContainer = targetContainerMatch.getRight();
+ }
+ } else if (rightToLeft) {
+ expectedContainer = match.getLeft();
+ } else {
+ expectedContainer = match.getRight();
+ }
+
+ final List<Object> sourceList = getSourceList(diff, feature, rightToLeft);
+ final List<Object> targetList;
+
+ if (expectedContainer != null) {
+ final List<Object> temp = (List<Object>)ReferenceUtil.safeEGet(expectedContainer, feature);
+ if (feature == EcorePackage.Literals.ECLASS__ESUPER_TYPES
+ || feature == EcorePackage.Literals.EOPERATION__EEXCEPTIONS) {
+ // workaround 394286
+ targetList = temp;
+ } else if (temp instanceof InternalEList<?>) {
+ // EMF ignores the "resolve" flag for containment lists...
+ targetList = ((InternalEList<Object>)temp).basicList();
+ } else {
+ targetList = temp;
+ }
+ } else {
+ targetList = ImmutableList.of();
+ }
+
+ Iterable<Object> ignoredElements = Iterables.concat(computeIgnoredElements(targetList, diff),
+ Collections.singleton(value));
+ // We know we'll have to iterate quite a number of times on this one.
+ ignoredElements = Lists.newArrayList(ignoredElements);
+
+ return DiffUtil.findInsertionIndex(comparison, ignoredElements, sourceList, targetList, value);
+ }
+
+ /**
+ * Retrieves the "source" list of the given {@code diff}. This will be different according to the kind of
+ * change and the direction of the merging.
+ *
+ * @param diff
+ * The diff for which merging we need a 'source'.
+ * @param feature
+ * The feature on which the merging is actually taking place.
+ * @param rightToLeft
+ * Direction of the merging. {@code true} if the merge is to be done on the left side, making
+ * 'source' the right side, {@code false} otherwise.
+ * @return The list that should be used as a source for this merge. May be empty, but never
+ * <code>null</code>.
+ */
+ @SuppressWarnings("unchecked")
+ private static List<Object> getSourceList(Diff diff, EStructuralFeature feature, boolean rightToLeft) {
+ final Match match = diff.getMatch();
+ final List<Object> sourceList;
+ final EObject expectedContainer;
+
+ if (diff.getKind() == DifferenceKind.MOVE) {
+ final boolean undoingLeft = rightToLeft && diff.getSource() == DifferenceSource.LEFT;
+ final boolean undoingRight = !rightToLeft && diff.getSource() == DifferenceSource.RIGHT;
+
+ if ((undoingLeft || undoingRight) && match.getOrigin() != null) {
+ expectedContainer = match.getOrigin();
+ } else if (rightToLeft) {
+ expectedContainer = match.getRight();
+ } else {
+ expectedContainer = match.getLeft();
+ }
+
+ } else {
+ if (match.getOrigin() != null && diff.getKind() == DifferenceKind.DELETE) {
+ expectedContainer = match.getOrigin();
+ } else if (rightToLeft) {
+ expectedContainer = match.getRight();
+ } else {
+ expectedContainer = match.getLeft();
+ }
+ }
+
+ if (expectedContainer != null) {
+ final List<Object> temp = (List<Object>)ReferenceUtil.safeEGet(expectedContainer, feature);
+ if (feature == EcorePackage.Literals.ECLASS__ESUPER_TYPES
+ || feature == EcorePackage.Literals.EOPERATION__EEXCEPTIONS) {
+ // workaround 394286
+ sourceList = temp;
+ } else if (temp instanceof InternalEList<?>) {
+ // EMF ignores the "resolve" flag for containment lists...
+ sourceList = ((InternalEList<Object>)temp).basicList();
+ } else {
+ sourceList = temp;
+ }
+ } else {
+ sourceList = ImmutableList.of();
+ }
+
+ return sourceList;
+ }
+
+ /**
+ * When computing the insertion index of an element in a list, we need to ignore all elements present in
+ * that list that feature unresolved Diffs on the same feature.
+ *
+ * @param candidates
+ * The sequence in which we need to compute an insertion index.
+ * @param diff
+ * The diff we are computing an insertion index for.
+ * @param <E>
+ * Type of the list's content.
+ * @return The list of elements that should be ignored when computing the insertion index for a new
+ * element in {@code candidates}.
+ */
+ private static <E> Iterable<E> computeIgnoredElements(Iterable<E> candidates, final Diff diff) {
+ final Match match = diff.getMatch();
+ final Iterable<? extends Diff> filteredCandidates = Lists.newArrayList(match.getDifferences());
+ final EStructuralFeature feature;
+ if (diff instanceof AttributeChange) {
+ feature = ((AttributeChange)diff).getAttribute();
+ } else if (diff instanceof ReferenceChange) {
+ feature = ((ReferenceChange)diff).getReference();
+ } else {
+ return Collections.emptyList();
+ }
+
+ final Set<E> ignored = Sets.newLinkedHashSet();
+ for (E candidate : candidates) {
+ if (candidate instanceof EObject) {
+ final Iterable<? extends Diff> differences = match.getComparison().getDifferences(
+ (EObject)candidate);
+ if (Iterables.any(differences, new UnresolvedDiffMatching(feature, candidate))) {
+ ignored.add(candidate);
+ }
+ } else {
+ if (Iterables.any(filteredCandidates, new UnresolvedDiffMatching(feature, candidate))) {
+ ignored.add(candidate);
+ }
+ }
+ }
+ return ignored;
+ }
+
+ /**
+ * This can be used to check whether a given Diff affects a value for which we can find another,
+ * unresolved Diff on a given Feature.
+ *
+ * @author <a href="mailto:laurent.goubet@obeo.fr">Laurent Goubet</a>
+ */
+ private static class UnresolvedDiffMatching implements Predicate<Diff> {
+ /** Feature on which we expect an unresolved diff. */
+ private final EStructuralFeature feature;
+
+ /** Element for which we expect an unresolved diff. */
+ private final Object element;
+
+ /**
+ * Constructs a predicate that can be used to retrieve all unresolved diffs that apply to the given
+ * {@code feature} and {@code element}.
+ *
+ * @param feature
+ * The feature which our diffs must concern.
+ * @param element
+ * The element which must be our diffs' target.
+ */
+ public UnresolvedDiffMatching(EStructuralFeature feature, Object element) {
+ this.feature = feature;
+ this.element = element;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @see com.google.common.base.Predicate#apply(java.lang.Object)
+ */
+ public boolean apply(Diff input) {
+ boolean apply = false;
+ if (input instanceof AttributeChange) {
+ apply = input.getState() == DifferenceState.UNRESOLVED
+ && ((AttributeChange)input).getAttribute() == feature
+ && matchingValues((AttributeChange)input, element);
+ } else if (input instanceof ReferenceChange) {
+ apply = input.getState() == DifferenceState.UNRESOLVED
+ && ((ReferenceChange)input).getReference() == feature
+ && matchingValues((ReferenceChange)input, element);
+ } else {
+ apply = false;
+ }
+ return apply;
+ }
+
+ /**
+ * Checks that the value of the given diff matches <code>value</code>, resorting to the equality
+ * helper if needed.
+ *
+ * @param diff
+ * The diff which value we need to check.
+ * @param value
+ * The expected value of <code>diff</code>
+ * @return <code>true</code> if the value matches.
+ */
+ private boolean matchingValues(AttributeChange diff, Object value) {
+ if (diff.getValue() == value) {
+ return true;
+ }
+ // Only resort to the equality helper as "last resort"
+ final IEqualityHelper helper = diff.getMatch().getComparison().getEqualityHelper();
+ return helper.matchingAttributeValues(diff.getValue(), value);
+ }
+
+ /**
+ * Checks that the value of the given diff matches <code>value</code>, resorting to the equality
+ * helper if needed.
+ *
+ * @param diff
+ * The diff which value we need to check.
+ * @param value
+ * The expected value of <code>diff</code>
+ * @return <code>true</code> if the value matches.
+ */
+ private boolean matchingValues(ReferenceChange diff, Object value) {
+ if (diff.getValue() == value) {
+ return true;
+ }
+ // Only resort to the equality helper as "last resort"
+ final IEqualityHelper helper = diff.getMatch().getComparison().getEqualityHelper();
+ return helper.matchingValues(diff.getValue(), value);
+ }
+ }
+}

Back to the top