diff --git a/org.eventb.texttools/src/org/eventb/texttools/PersistenceHelper.java b/org.eventb.texttools/src/org/eventb/texttools/PersistenceHelper.java index 8823c16356207380f2a30b59c4efc3fd2d2f583a..a5d3b3520b12cbd194c41f10aff414e60ce1d78e 100644 --- a/org.eventb.texttools/src/org/eventb/texttools/PersistenceHelper.java +++ b/org.eventb.texttools/src/org/eventb/texttools/PersistenceHelper.java @@ -168,7 +168,7 @@ public class PersistenceHelper { if (d.getState() != DifferenceState.MERGED) evbMerger.copyRightToLeft(d,null); } catch(Exception e) { - System.out.println("SKIPPED:"+d); + System.out.println("SKIPED:"+d); } } diff --git a/org.eventb.texttools/src/org/eventb/texttools/diffmerge/EventBEObjectMatcher.java b/org.eventb.texttools/src/org/eventb/texttools/diffmerge/EventBEObjectMatcher.java index 204f50c96b0627ca37574323f7ac66be620998dd..ea517c08d116dd5ac1d5fe7e7d3003ac5215ffdd 100644 --- a/org.eventb.texttools/src/org/eventb/texttools/diffmerge/EventBEObjectMatcher.java +++ b/org.eventb.texttools/src/org/eventb/texttools/diffmerge/EventBEObjectMatcher.java @@ -15,6 +15,7 @@ import org.eclipse.emf.ecore.resource.Resource; import org.eclipse.emf.ecore.util.EcoreUtil; import org.eclipse.emf.ecore.xmi.XMIResource; import org.eventb.emf.core.EventBNamed; +import org.eventb.emf.core.context.Context; import org.eventb.emf.core.machine.Variant; import com.google.common.collect.Iterables; @@ -68,14 +69,13 @@ public class EventBEObjectMatcher implements IEObjectMatcher { } // this logic was previously found in isSimiliar of EventBMatchEngine - private boolean matching(EObject left, EObject candidate) { + public static boolean matching(EObject left, EObject candidate) { /* * If the type differs it can not be a match */ if (!areSameType(left, candidate)) { return false; } - /* * Only one variant may exist in a model, so two variants are a match */ @@ -87,10 +87,10 @@ public class EventBEObjectMatcher implements IEObjectMatcher { * Improve matching for event b named objects with same name */ if (left instanceof EventBNamed && candidate instanceof EventBNamed) { - EventBNamed r = (EventBNamed) left; - EventBNamed c = (EventBNamed) left; + EventBNamed l = (EventBNamed) left; + EventBNamed c = (EventBNamed) candidate; - if (r.getName().equals(c.getName())) { + if (l.getName().equals(c.getName())) { return true; } } @@ -107,7 +107,7 @@ public class EventBEObjectMatcher implements IEObjectMatcher { return false; } - private boolean areSameType(final EObject obj1, final EObject obj2) { + private static boolean areSameType(final EObject obj1, final EObject obj2) { return obj1 != null && obj2 != null && obj1.eClass().equals(obj2.eClass()); } @@ -120,11 +120,11 @@ public class EventBEObjectMatcher implements IEObjectMatcher { * @param obj2 * @return */ - private boolean areVariants(final EObject obj1, final EObject obj2) { + private static boolean areVariants(final EObject obj1, final EObject obj2) { return areSameType(obj1, obj2) && obj2 instanceof Variant; } - private String getEMFId(EObject eObject) { + private static String getEMFId(EObject eObject) { final String identifier; if (eObject == null) { identifier = null; diff --git a/org.eventb.texttools/src/org/eventb/texttools/diffmerge/EventBMerger.java b/org.eventb.texttools/src/org/eventb/texttools/diffmerge/EventBMerger.java index 397a0d16a0e4b9e0a5ee9ee2ee785705f47f93ea..48b372d2ef07616ce9859741b408ded9810736cd 100644 --- a/org.eventb.texttools/src/org/eventb/texttools/diffmerge/EventBMerger.java +++ b/org.eventb.texttools/src/org/eventb/texttools/diffmerge/EventBMerger.java @@ -1,5 +1,6 @@ package org.eventb.texttools.diffmerge; +import org.eclipse.emf.common.util.EList; import org.eclipse.emf.compare.AttributeChange; import org.eclipse.emf.compare.Diff; import org.eclipse.emf.compare.ReferenceChange; @@ -13,7 +14,8 @@ import org.eventb.texttools.TextPositionUtil; public class EventBMerger extends AbstractMerger { private AttributeChangeMerger am = new AttributeChangeMerger(); - private ReferenceChangeMerger rm = new ReferenceChangeMerger(); + // replacement of emfcompare version (solves a bug with multivalued references + private MyReferenceChangeMerger rm = new MyReferenceChangeMerger(); @Override public boolean isMergerFor(Diff target) { @@ -65,7 +67,6 @@ public class EventBMerger extends AbstractMerger { ReferenceChange d = (ReferenceChange) diff; //System.out.println("REF CHANGE:"+d.getReference()); //System.out.println("REF VALUE:"+ d.getValue()); - rm.copyRightToLeft(diff,null); Object l = left.eGet(d.getReference()); Object r = right.eGet(d.getReference()); @@ -78,7 +79,6 @@ public class EventBMerger extends AbstractMerger { AttributeChange d = (AttributeChange) diff; //System.out.println("ATTR CHANGE:"+d.getAttribute()); //System.out.println("ATTR VALUE:"+ d.getValue()); - am.copyRightToLeft(diff,null); Object l = left.eGet(d.getAttribute()); Object r = right.eGet(d.getAttribute()); diff --git a/org.eventb.texttools/src/org/eventb/texttools/diffmerge/MyReferenceChangeMerger.java b/org.eventb.texttools/src/org/eventb/texttools/diffmerge/MyReferenceChangeMerger.java new file mode 100644 index 0000000000000000000000000000000000000000..3db58f273af7efa58dd3530ead09179e6ee32cc3 --- /dev/null +++ b/org.eventb.texttools/src/org/eventb/texttools/diffmerge/MyReferenceChangeMerger.java @@ -0,0 +1,756 @@ +/******************************************************************************* + * Copyright (c) 2012, 2017 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 + * Philip Langer - fixes for bug 413520 + *******************************************************************************/ +package org.eventb.texttools.diffmerge; + +import static com.google.common.collect.Iterators.filter; +import static org.eclipse.emf.compare.merge.IMergeCriterion.NONE; +import static org.eclipse.emf.compare.utils.ReferenceUtil.safeEGet; +import static org.eclipse.emf.compare.utils.ReferenceUtil.safeEIsSet; +import static org.eclipse.emf.compare.utils.ReferenceUtil.safeESet; + +import com.google.common.collect.Iterators; + +import java.util.Iterator; +import java.util.List; + +import org.eclipse.emf.common.util.EList; +import org.eclipse.emf.compare.Comparison; +import org.eclipse.emf.compare.Diff; +import org.eclipse.emf.compare.DifferenceSource; +import org.eclipse.emf.compare.Match; +import org.eclipse.emf.compare.ReferenceChange; +import org.eclipse.emf.compare.internal.utils.DiffUtil; +import org.eclipse.emf.compare.merge.AbstractMerger; +import org.eclipse.emf.compare.merge.IMergeCriterion; +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.resource.Resource; +import org.eclipse.emf.ecore.util.EcoreUtil; +import org.eclipse.emf.ecore.xmi.XMIResource; + +/** + * This specific implementation of {@link AbstractMerger} will be used to merge reference changes. + * + * @author <a href="mailto:laurent.goubet@obeo.fr">Laurent Goubet</a> + */ +public class MyReferenceChangeMerger extends AbstractMerger { + /** + * {@inheritDoc} + * + * @see org.eclipse.emf.compare.merge.IMerger#isMergerFor(org.eclipse.emf.compare.Diff) + */ + public boolean isMergerFor(Diff target) { + return target instanceof ReferenceChange; + } + + @Override + public boolean apply(IMergeCriterion criterion) { + return criterion == null || criterion == NONE; + } + + /** + * Merge the given difference rejecting it. + * + * @param diff + * The difference to merge. + * @param rightToLeft + * The direction of the merge. + */ + @Override + protected void reject(final Diff diff, boolean rightToLeft) { + ReferenceChange referenceChange = (ReferenceChange)diff; + DifferenceSource source = referenceChange.getSource(); + switch (referenceChange.getKind()) { + case ADD: + // We have a ADD on left, thus nothing in right. We need to revert the addition + removeFromTarget(referenceChange, rightToLeft); + break; + case DELETE: + // DELETE in the left, thus an element in right. We need to re-create that element + addInTarget(referenceChange, rightToLeft); + break; + case MOVE: + moveElement(referenceChange, rightToLeft); + break; + case CHANGE: + EObject container = null; + if (source == DifferenceSource.LEFT) { + container = referenceChange.getMatch().getLeft(); + + } else { + container = referenceChange.getMatch().getRight(); + } + // Is it an unset? + if (container != null) { + final EObject leftValue = (EObject)safeEGet(container, referenceChange.getReference()); + if (leftValue == null) { + // Value has been unset in the right, and we are merging towards right. + // We need to re-add this element + addInTarget(referenceChange, rightToLeft); + } else { + // We'll actually need to "reset" this reference to its original value + resetInTarget(referenceChange, rightToLeft); + } + } else { + // we have no left, and the source is on the left. Can only be an unset + addInTarget(referenceChange, rightToLeft); + } + break; + default: + break; + } + } + + /** + * Merge the given difference accepting it. + * + * @param diff + * The difference to merge. + * @param rightToLeft + * The direction of the merge. + */ + @Override + protected void accept(final Diff diff, boolean rightToLeft) { + ReferenceChange referenceChange = (ReferenceChange)diff; + DifferenceSource source = diff.getSource(); + switch (diff.getKind()) { + case ADD: + // Create the same element in right + addInTarget(referenceChange, rightToLeft); + break; + case DELETE: + // Delete that same element from right + removeFromTarget(referenceChange, rightToLeft); + break; + case MOVE: + moveElement(referenceChange, rightToLeft); + break; + case CHANGE: + EObject container = null; + if (source == DifferenceSource.LEFT) { + container = referenceChange.getMatch().getLeft(); + } else { + container = referenceChange.getMatch().getRight(); + } + // Is it an unset? + if (container != null) { + final EObject leftValue = (EObject)safeEGet(container, referenceChange.getReference()); + if (leftValue == null) { + removeFromTarget(referenceChange, rightToLeft); + } else { + addInTarget(referenceChange, rightToLeft); + } + } else { + // we have no left, and the source is on the left. Can only be an unset + removeFromTarget(referenceChange, rightToLeft); + } + break; + default: + break; + } + } + + /** + * This will be called when trying to copy a "MOVE" diff. + * + * @param diff + * The diff we are currently merging. + * @param rightToLeft + * Whether we should move the value in the left or right side. + */ + protected void moveElement(ReferenceChange diff, boolean rightToLeft) { + final Comparison comparison = diff.getMatch().getComparison(); + final Match valueMatch = comparison.getMatch(diff.getValue()); + final EReference reference = diff.getReference(); + + final EObject expectedContainer; + if (reference.isContainment()) { + /* + * We cannot "trust" the holding match (getMatch) in this case. However, "valueMatch" cannot be + * null : we cannot have detected a move if the moved element is not matched on both sides. Use + * that information to retrieve the proper "target" container. + */ + 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 = diff.getMatch().getLeft(); + } else { + expectedContainer = diff.getMatch().getRight(); + } + if (expectedContainer == null) { + throw new IllegalStateException( + "Couldn't move element because its parent hasn't been merged yet: " + diff); //$NON-NLS-1$ + } + + final EObject expectedValue; + if (valueMatch == null) { + // The value being moved is out of the scope + /* + * Note : there should not be a way to end up with a "move" for an out of scope value : a move can + * only be detected if the object is matched on both sides, otherwise all we can see is "add" and + * "delete"... Is this "fallback" code even reachable? If so, how? + */ + // We need to look it up + if (reference.isMany()) { + @SuppressWarnings("unchecked") + final List<EObject> targetList = (List<EObject>)safeEGet(expectedContainer, reference); + expectedValue = findMatchIn(comparison, targetList, diff.getValue()); + } else { + expectedValue = (EObject)safeEGet(expectedContainer, reference); + } + } else { + if (rightToLeft) { + expectedValue = valueMatch.getLeft(); + } else { + expectedValue = valueMatch.getRight(); + } + } + // If expectedValue is null at this point, we have to copy the value from the other side. + // It can happens with a move between the ancestor and one side, while the other side doesn't has the + // value. + if (expectedValue == null) { + addInTarget(diff, rightToLeft); + } else { + // We now know the target container, target reference and target value. + doMove(diff, comparison, expectedContainer, expectedValue, rightToLeft); + } + } + + /** + * This will do the actual work of moving the element into its reference. All sanity checks were made in + * {@link #moveElement(boolean)} and no more verification will be made here. + * + * @param diff + * The diff we are currently merging. + * @param comparison + * Comparison holding this Diff. + * @param expectedContainer + * The container in which we are reorganizing a reference. + * @param expectedValue + * The value that is to be moved within its reference. + * @param rightToLeft + * Whether we should move the value in the left or right side. + */ + @SuppressWarnings("unchecked") + protected void doMove(ReferenceChange diff, Comparison comparison, EObject expectedContainer, + EObject expectedValue, boolean rightToLeft) { + final EReference reference = getMoveTargetReference(comparison, diff, rightToLeft); + + if (reference.isMany()) { + // Element to move cannot be part of the LCS... or there would not be a MOVE diff + int insertionIndex = findInsertionIndex(comparison, diff, rightToLeft); + + /* + * However, it could still have been located "before" its new index, in which case we need to take + * it into account. + */ + final List<EObject> targetList = (List<EObject>)safeEGet(expectedContainer, reference); + final int currentIndex = targetList.indexOf(expectedValue); + if (insertionIndex > currentIndex && currentIndex >= 0) { + insertionIndex--; + } + + if (currentIndex == -1) { + // happens for container changes for example. + if (!reference.isContainment()) { + targetList.remove(expectedValue); + } + if (insertionIndex < 0 || insertionIndex > targetList.size()) { + targetList.add(expectedValue); + } else { + targetList.add(insertionIndex, expectedValue); + } + } else if (targetList instanceof EList<?>) { + if (insertionIndex < 0 || insertionIndex > targetList.size()) { + ((EList<EObject>)targetList).move(targetList.size() - 1, expectedValue); + } else { + ((EList<EObject>)targetList).move(insertionIndex, expectedValue); + } + } else { + targetList.remove(expectedValue); + if (insertionIndex < 0 || insertionIndex > targetList.size()) { + targetList.add(expectedValue); + } else { + targetList.add(insertionIndex, expectedValue); + } + } + } else { + safeESet(expectedContainer, reference, expectedValue); + } + } + + /** + * Returns the reference of the target container in case of a MOVE Diff. + * + * @param comparison + * the comparison object holding the given Diff. + * @param diff + * the given Diff. + * @param rightToLeft + * whether we should move the value in the left or right side. + * @return the reference of the target container in case of a MOVE Diff. + */ + private EReference getMoveTargetReference(Comparison comparison, ReferenceChange diff, + boolean rightToLeft) { + final EReference reference; + final DifferenceSource source = diff.getSource(); + final Match valueMatch = comparison.getMatch(diff.getValue()); + if (!diff.getReference().isContainment() || valueMatch == null) { + reference = diff.getReference(); + } else if (rightToLeft && source == DifferenceSource.LEFT) { + EObject sourceValue = valueMatch.getRight(); + if (sourceValue == null) { + sourceValue = valueMatch.getOrigin(); + } + EStructuralFeature feature = sourceValue.eContainingFeature(); + if (feature instanceof EReference) { + reference = (EReference)feature; + } else { + // FIXME Manage this case. See javadoc of eContainingFeature. This is possible and will happen + // with feature maps. http: + // //download.eclipse.org/modeling/emf/emf/javadoc/2.8.0/org/eclipse/emf/ecore/EObject.html#eContainingFeature%28%29 + reference = diff.getReference(); + } + } else if (!rightToLeft && source == DifferenceSource.RIGHT) { + EObject sourceValue = valueMatch.getLeft(); + if (sourceValue == null) { + sourceValue = valueMatch.getOrigin(); + } + EStructuralFeature feature = sourceValue.eContainingFeature(); + if (feature instanceof EReference) { + reference = (EReference)feature; + } else { + // FIXME Manage this case. See javadoc of eContainingFeature. This is possible and will happen + // with feature maps. http: + // //download.eclipse.org/modeling/emf/emf/javadoc/2.8.0/org/eclipse/emf/ecore/EObject.html#eContainingFeature%28%29 + reference = diff.getReference(); + } + } else { + reference = diff.getReference(); + } + return reference; + } + + /** + * This will be called when we need to create an element in the target side. + * <p> + * All necessary sanity checks have been made to ensure that the current operation is one that should + * create an object in its side or add an objet to a reference. In other words, either : + * <ul> + * <li>We are copying from right to left and + * <ul> + * <li>we are copying an addition to the right side (we need to create the same object in the left), or + * </li> + * <li>we are copying a deletion from the left side (we need to revert the deletion).</li> + * </ul> + * </li> + * <li>We are copying from left to right and + * <ul> + * <li>we are copying a deletion from the right side (we need to revert the deletion), or</li> + * <li>we are copying an addition to the left side (we need to create the same object in the right).</li> + * </ul> + * </li> + * </ul> + * </p> + * + * @param diff + * The diff we are currently merging. + * @param rightToLeft + * Tells us whether we are to add an object on the left or right side. + */ + @SuppressWarnings("unchecked") + protected void addInTarget(ReferenceChange diff, boolean rightToLeft) { + final Match match = diff.getMatch(); + final EObject expectedContainer; + if (rightToLeft) { + expectedContainer = match.getLeft(); + } else { + expectedContainer = match.getRight(); + } + + if (expectedContainer == null) { + throw new IllegalStateException( + "Couldn't add in target because its parent hasn't been merged yet: " + diff); //$NON-NLS-1$ + } + + final Comparison comparison = match.getComparison(); + final EReference reference = diff.getReference(); + final EObject expectedValue; + final Match valueMatch = comparison.getMatch(diff.getValue()); + boolean needXmiId = false; + if (valueMatch == null) { + // This is an out of scope value. + if (diff.getValue().eIsProxy()) { + // Copy the proxy + expectedValue = EcoreUtil.copy(diff.getValue()); + } else { + // Use the same value. + expectedValue = diff.getValue(); + } + } else if (rightToLeft) { + if (reference.isContainment() || valueMatch.getLeft() == null) { + expectedValue = createCopy(diff.getValue()); + valueMatch.setLeft(expectedValue); + needXmiId = true; + } else { + expectedValue = valueMatch.getLeft(); + } + } else { + if (reference.isContainment() || valueMatch.getRight() == null) { + expectedValue = createCopy(diff.getValue()); + valueMatch.setRight(expectedValue); + needXmiId = true; + } else { + expectedValue = valueMatch.getRight(); + } + } + + // We have the container, reference and value. We need to know the insertion index. + if (reference.isMany()) { + final int insertionIndex = findInsertionIndex(comparison, diff, rightToLeft); + + final List<EObject> targetList = (List<EObject>)safeEGet(expectedContainer, reference); + addAt(targetList, expectedValue, insertionIndex); + } else { + safeESet(expectedContainer, reference, expectedValue); + } + + if (needXmiId) { + // Copy XMI ID when applicable. + final Resource initialResource = diff.getValue().eResource(); + final Resource targetResource = expectedValue.eResource(); + if (initialResource instanceof XMIResource && targetResource instanceof XMIResource) { + ((XMIResource)targetResource).setID(expectedValue, + ((XMIResource)initialResource).getID(diff.getValue())); + } + } + + checkImpliedDiffsOrdering(diff, rightToLeft); + } + + /** + * This will be called when we need to remove an element from the target side. + * <p> + * All necessary sanity checks have been made to ensure that the current operation is one that should + * delete an object. In other words, we are : + * <ul> + * <li>Copying from right to left and either + * <ul> + * <li>we are copying a deletion from the right side (we need to remove the same object in the left) or, + * </li> + * <li>we are copying an addition to the left side (we need to revert the addition).</li> + * </ul> + * </li> + * <li>Copying from left to right and either + * <ul> + * <li>we are copying an addition to the right side (we need to revert the addition), or.</li> + * <li>we are copying a deletion from the left side (we need to remove the same object in the right).</li> + * </ul> + * </li> + * </ul> + * </p> + * + * @param diff + * The diff we are currently merging. + * @param rightToLeft + * Tells us whether we are to add an object on the left or right side. + */ + @SuppressWarnings("unchecked") + protected void removeFromTarget(ReferenceChange diff, boolean rightToLeft) { + final Match match = diff.getMatch(); + final EReference reference = diff.getReference(); + final EObject currentContainer; + if (rightToLeft) { + currentContainer = match.getLeft(); + } else { + currentContainer = match.getRight(); + } + final Comparison comparison = match.getComparison(); + final Match valueMatch = comparison.getMatch(diff.getValue()); + + if (currentContainer == null) { + // Nothing to do, parent already removed + return; + } + + final EObject expectedValue; + + if (valueMatch == null) { + // value is out of the scope... we need to look it up + if (reference.isMany()) { + final List<EObject> targetList = (List<EObject>)safeEGet(currentContainer, reference); + expectedValue = findMatchIn(comparison, targetList, diff.getValue()); + } else { + // the value will not be needed anyway + expectedValue = null; + } + } else if (rightToLeft) { + expectedValue = valueMatch.getLeft(); + } else { + expectedValue = valueMatch.getRight(); + } + + //System.out.println("VALUE:"+diff.getValue()); + //System.out.println("LEFT:"+safeEGet(currentContainer, reference)); + //System.out.println("MATCH:"+expectedValue); + + // We have the container, reference and value to remove. Expected value can be null when the + // deletion was made on both side (i.e. a pseudo delete) + if (reference.isMany()) { + final List<EObject> targetList = (List<EObject>)safeEGet(currentContainer, reference); + if (expectedValue != null) { + /* + * TODO if the same value appears twice, should we try and find the one that has actually been + * deleted? Can it happen? For now, remove the first occurence we find. + */ + targetList.remove(expectedValue); + //System.out.println("REMOVE:"+expectedValue); + //System.out.println("NEW LIST:"+safeEGet(currentContainer, reference)); + } + } else if (reference.isContainment() && expectedValue != null) { + EcoreUtil.remove(expectedValue); + if (rightToLeft && valueMatch != null) { + valueMatch.setLeft(null); + } else if (valueMatch != null) { + valueMatch.setRight(null); + } + // TODO remove dangling? remove empty Match? + } else { + currentContainer.eUnset(reference); + } + } + + /** + * This will be called by the merge operations in order to reset a reference to its original value, be + * that the left or right side. + * <p> + * Should never be called on multi-valued references. + * </p> + * + * @param diff + * The diff we are currently merging. + * @param rightToLeft + * Tells us the direction of this merge operation. + */ + protected void resetInTarget(ReferenceChange diff, boolean rightToLeft) { + final Match match = diff.getMatch(); + final EReference reference = diff.getReference(); + final EObject targetContainer; + if (rightToLeft) { + targetContainer = match.getLeft(); + } else { + targetContainer = match.getRight(); + } + + final EObject originContainer; + if (match.getComparison().isThreeWay()) { + originContainer = match.getOrigin(); + } else if (rightToLeft) { + originContainer = match.getRight(); + } else { + originContainer = match.getLeft(); + } + + if (originContainer == null || !safeEIsSet(originContainer, reference)) { + targetContainer.eUnset(reference); + } else { + final EObject originalValue = (EObject)safeEGet(originContainer, reference); + final Match valueMatch = match.getComparison().getMatch(originalValue); + final EObject expectedValue; + if (valueMatch == null) { + // Value is out of the scope, use it as-is + expectedValue = originalValue; + } else if (rightToLeft) { + expectedValue = valueMatch.getLeft(); + } else { + expectedValue = valueMatch.getRight(); + } + safeESet(targetContainer, reference, expectedValue); + } + } + + /** + * In the case of many-to-many eOpposite references, EMF will simply report the difference made on one + * side of the equivalence to the other, without considering ordering in any way. In such cases, we'll + * iterate over our equivalences after the merge, and double-check the ordering ourselves, fixing it as + * needed. + * <p> + * Note that both implied and equivalent diffs will be double-checked from here. + * </p> + * + * @param diff + * The diff we are currently merging. + * @param rightToLeft + * Direction of the merge. + * @since 3.1 + */ + protected void checkImpliedDiffsOrdering(ReferenceChange diff, boolean rightToLeft) { + final EReference reference = diff.getReference(); + final List<Diff> mergedImplications; + if (isAccepting(diff, rightToLeft)) { + mergedImplications = diff.getImplies(); + } else { + mergedImplications = diff.getImpliedBy(); + } + + Iterator<Diff> impliedDiffs = mergedImplications.iterator(); + if (reference.isMany() && diff.getEquivalence() != null) { + impliedDiffs = Iterators.concat(impliedDiffs, diff.getEquivalence().getDifferences().iterator()); + } + final Iterator<ReferenceChange> impliedReferenceChanges = filter(impliedDiffs, ReferenceChange.class); + + while (impliedReferenceChanges.hasNext()) { + final ReferenceChange implied = impliedReferenceChanges.next(); + if (implied != diff && isInTerminalState(implied)) { + if (implied.getReference().isMany() && isAdd(implied, rightToLeft)) { + internalCheckOrdering(implied, rightToLeft); + checkImpliedDiffsOrdering(implied, rightToLeft); + } + } + } + } + + /** + * Checks a particular difference for the ordering of its target values. This will be used to double-check + * that equivalent differences haven't been "broken" by EMF by not preserving their value order. + * <p> + * Should only be used on <u>merged</u> differences which target <u>many-valued</u> references. + * </p> + * + * @param diff + * The diff that is to be checked. + * @param rightToLeft + * Direction of the merge that took place. + */ + private void internalCheckOrdering(ReferenceChange diff, boolean rightToLeft) { + final EStructuralFeature feature = diff.getReference(); + final EObject value = diff.getValue(); + final Match match = diff.getMatch(); + final Comparison comparison = match.getComparison(); + final Match valueMatch = comparison.getMatch(value); + + final EObject sourceContainer; + final EObject targetContainer; + final EObject newValue; + if (rightToLeft) { + sourceContainer = match.getRight(); + targetContainer = match.getLeft(); + newValue = valueMatch.getLeft(); + } else { + sourceContainer = match.getLeft(); + targetContainer = match.getRight(); + newValue = valueMatch.getRight(); + } + + final List<Object> sourceList = ReferenceUtil.getAsList(sourceContainer, feature); + final List<Object> targetList = ReferenceUtil.getAsList(targetContainer, feature); + + final List<Object> lcs = DiffUtil.longestCommonSubsequence(comparison, sourceList, targetList); + if (lcs.contains(valueMatch.getLeft()) || lcs.contains(valueMatch.getRight())) { + // Ordering is correct on this one + return; + } + + int insertionIndex = DiffUtil.findInsertionIndex(comparison, sourceList, targetList, value); + if (insertionIndex >= 0) { + /* + * We've used unresolving views of the eobject lists since we didn't know whether there was + * actually any work to do. Use the real list now. + */ + @SuppressWarnings("unchecked") + final List<EObject> changedList = (List<EObject>)safeEGet(targetContainer, feature); + if (changedList.size() > 1) { + if (changedList instanceof EList<?>) { + if (insertionIndex > changedList.size()) { + ((EList<EObject>)changedList).move(changedList.size() - 1, newValue); + } else { + ((EList<EObject>)changedList).move(insertionIndex, newValue); + } + } else { + changedList.remove(newValue); + if (insertionIndex > changedList.size()) { + changedList.add(newValue); + } else { + changedList.add(insertionIndex, newValue); + } + } + } + } + } + + /** + * Seeks a match of the given {@code element} in the given list, using the equality helper to find it. + * This is only used when moving or deleting proxies for now. + * + * @param comparison + * The comparison which Diff we are currently merging. + * @param list + * The list from which we seek a value. + * @param element + * The value for which we need a match in {@code list}. + * @return The match of {@code element} in {@code list}, {@code null} if none. + */ + protected EObject findMatchIn(Comparison comparison, List<EObject> list, EObject element) { + //final IEqualityHelper helper = comparison.getEqualityHelper(); + final Iterator<EObject> it = list.iterator(); + while (it.hasNext()) { + final EObject next = it.next(); + if (EventBEObjectMatcher.matching(next, element)) + return next; + //if (helper.matchingValues(next, element)) { + // return next; + //} + } + return null; + } + + /** + * This will be used by the distinct merge actions in order to find the index at which a value should be + * inserted in its target list. See {@link DiffUtil#findInsertionIndex(Comparison, Diff, boolean)} for + * more on this. + * <p> + * Sub-classes can override this if the insertion order is irrelevant. A return value of {@code -1} will + * be considered as "no index" and the value will be inserted at the end of its target list. + * </p> + * + * @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}. {@code -1} if the value should be inserted at the end of its + * target list. + * @see DiffUtil#findInsertionIndex(Comparison, Diff, boolean) + */ + protected int findInsertionIndex(Comparison comparison, Diff diff, boolean rightToLeft) { + return DiffUtil.findInsertionIndex(comparison, diff, rightToLeft); + } +}