blob: 42b13537c4502fcaf7ed9983874a5e7cfe46df48 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2014, 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
* Alexandra Buzila - Fixes for bug 446252
* Stefan Dirix - Fixes for Bugs 453218 and 453749
*******************************************************************************/
package org.eclipse.emf.compare.merge;
import static org.eclipse.emf.compare.internal.utils.ComparisonUtil.isFeatureMapContainment;
import static org.eclipse.emf.compare.utils.ReferenceUtil.safeEGet;
import static org.eclipse.emf.compare.utils.ReferenceUtil.safeESet;
import com.google.common.collect.Iterables;
import java.util.List;
import org.eclipse.emf.compare.Comparison;
import org.eclipse.emf.compare.Diff;
import org.eclipse.emf.compare.DifferenceSource;
import org.eclipse.emf.compare.Equivalence;
import org.eclipse.emf.compare.FeatureMapChange;
import org.eclipse.emf.compare.Match;
import org.eclipse.emf.compare.ReferenceChange;
import org.eclipse.emf.compare.internal.utils.ComparisonUtil;
import org.eclipse.emf.compare.internal.utils.DiffUtil;
import org.eclipse.emf.compare.utils.IEqualityHelper;
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.BasicFeatureMap;
import org.eclipse.emf.ecore.util.FeatureMap;
import org.eclipse.emf.ecore.util.FeatureMapUtil;
import org.eclipse.emf.ecore.xmi.XMIResource;
/**
* This specific implementation of {@link AbstractMerger} will be used to merge attribute changes.
*
* @author <a href="mailto:laurent.goubet@obeo.fr">Laurent Goubet</a>
* @since 3.2
*/
public class FeatureMapChangeMerger extends AbstractMerger {
/**
* {@inheritDoc}
*
* @see org.eclipse.emf.compare.merge.IMerger#isMergerFor(org.eclipse.emf.compare.Diff)
*/
public boolean isMergerFor(Diff target) {
return target instanceof FeatureMapChange;
}
/**
* {@inheritDoc}
*
* @see org.eclipse.emf.compare.merge.AbstractMerger#accept(org.eclipse.emf.compare.Diff, boolean)
*/
@Override
protected void accept(final Diff diff, boolean rightToLeft) {
FeatureMapChange featureMapChange = (FeatureMapChange)diff;
switch (diff.getKind()) {
case ADD:
// Create the same element in right
addInTarget(featureMapChange, rightToLeft);
break;
case DELETE:
// Delete that same element from right
removeFromTarget(featureMapChange, rightToLeft);
break;
case MOVE:
moveElement(featureMapChange, rightToLeft);
break;
case CHANGE:
changeValue(featureMapChange, rightToLeft);
break;
default:
break;
}
}
/**
* {@inheritDoc}
*
* @see org.eclipse.emf.compare.merge.AbstractMerger#reject(org.eclipse.emf.compare.Diff, boolean)
*/
@Override
protected void reject(Diff diff, boolean rightToLeft) {
FeatureMapChange featureMapChange = (FeatureMapChange)diff;
switch (diff.getKind()) {
case ADD:
// We have a ADD on right. we need to revert this addition
removeFromTarget(featureMapChange, rightToLeft);
break;
case DELETE:
// DELETE in the right. We need to re-create this element
addInTarget(featureMapChange, rightToLeft);
break;
case MOVE:
moveElement(featureMapChange, rightToLeft);
break;
case CHANGE:
changeValue(featureMapChange, rightToLeft);
break;
default:
break;
}
}
/**
* 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 object to an attribute. 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(FeatureMapChange 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 EStructuralFeature attribute = diff.getAttribute();
final FeatureMap.Entry expectedValue = (FeatureMap.Entry)diff.getValue();
// We have the container, attribute and value. We need to know the insertion index.
final int insertionIndex = findInsertionIndex(comparison, diff, rightToLeft);
final List<Object> targetList = (List<Object>)safeEGet(expectedContainer, attribute);
addFeatureMapValueInTarget(comparison, rightToLeft, targetList, insertionIndex, expectedValue);
}
/**
* Add the FeatueMapEntry value at the insertionIndex position in the list.
*
* @param comparison
* The comparison object.
* @param rightToLeft
* The way of merge.
* @param list
* The list into which {@code value} should be added.
* @param insertionIndex
* The index at which {@code value} should be inserted into {@code list}. {@code -1} if it
* should be appended at the end of the list.
* @param entry
* The value we need to add to {@code list}.
*/
private void addFeatureMapValueInTarget(final Comparison comparison, final boolean rightToLeft,
final List<Object> list, final int insertionIndex, final FeatureMap.Entry entry) {
final Object value = entry.getValue();
final EStructuralFeature key = entry.getEStructuralFeature();
if (value instanceof EObject) {
final EObject copy;
// The value has its equivalent on the opposite side, or not
final Match match = comparison.getMatch((EObject)value);
final EObject left = match.getLeft();
final EObject right = match.getRight();
if (rightToLeft && left != null) {
copy = left;
} else if (!rightToLeft && right != null) {
copy = right;
} else {
copy = createCopy((EObject)value);
}
((BasicFeatureMap)(Object)list).addUnique(insertionIndex, FeatureMapUtil.createEntry(key, copy));
if (DiffUtil.isContainmentReference(key)) {
if (rightToLeft) {
match.setLeft(copy);
} else {
match.setRight(copy);
}
// Copy XMI ID when applicable.
final Resource initialResource = ((EObject)value).eResource();
final Resource targetResource = copy.eResource();
if (initialResource instanceof XMIResource && targetResource instanceof XMIResource) {
((XMIResource)targetResource).setID(copy,
((XMIResource)initialResource).getID((EObject)value));
}
}
} else {
((BasicFeatureMap)(Object)list).add(insertionIndex, FeatureMapUtil.createEntry(key, value));
}
}
/**
* 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(FeatureMapChange diff, boolean rightToLeft) {
final EObject currentContainer;
if (rightToLeft) {
currentContainer = diff.getMatch().getLeft();
} else {
currentContainer = diff.getMatch().getRight();
}
if (currentContainer != null) {
FeatureMap.Entry expectedValue = (FeatureMap.Entry)diff.getValue();
if (!isDiffSourceIsMergeTarget(diff, rightToLeft)) {
final List<Object> targetList = (List<Object>)safeEGet(currentContainer, diff.getAttribute());
for (Object object : targetList) {
if (diff.getMatch().getComparison().getEqualityHelper().matchingValues(expectedValue,
object)) {
expectedValue = (FeatureMap.Entry)object;
break;
}
}
}
final EStructuralFeature attribute = diff.getAttribute();
// We have the container, attribute and value to remove.
/*
* TODO if the same value appears twice, should we try and find the one that has actually been
* deleted? Will it happen that often? For now, remove the first occurence we find.
*/
final List<Object> targetList = (List<Object>)safeEGet(currentContainer, attribute);
final Comparison comparison = diff.getMatch().getComparison();
removeFeatureMapValueFromTarget(comparison, rightToLeft, targetList, expectedValue);
}
}
/**
* Remove the FeatueMapEntry value from the list.
*
* @param comparison
* The comparison object.
* @param rightToLeft
* The way of merge.
* @param list
* The list from which {@code value} should be removed.
* @param entry
* The value we need to remove from {@code list}.
*/
private void removeFeatureMapValueFromTarget(final Comparison comparison, final boolean rightToLeft,
final List<Object> list, final FeatureMap.Entry entry) {
final Object value = entry.getValue();
final EStructuralFeature key = entry.getEStructuralFeature();
if (((EReference)key).isContainment()) {
final Match expectedContainerMatch = comparison.getMatch((EObject)value);
if (rightToLeft) {
expectedContainerMatch.setLeft(null);
} else {
expectedContainerMatch.setRight(null);
}
}
((BasicFeatureMap)(Object)list).remove(key, value);
}
/**
* 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(final FeatureMapChange diff, final boolean rightToLeft) {
final Match match = diff.getMatch();
final Comparison comparison = match.getComparison();
final EObject expectedContainer = ComparisonUtil.moveElementGetExpectedContainer(comparison, diff,
rightToLeft);
if (expectedContainer == null) {
throw new IllegalStateException(
"Couldn't move element because its parent hasn't been merged yet: " + diff); //$NON-NLS-1$
}
final FeatureMap.Entry expectedEntry = moveElementGetExpectedEntry(comparison, diff,
expectedContainer, rightToLeft);
// We now know the target container, target attribute and target entry.
doMove(diff, comparison, expectedContainer, expectedEntry, rightToLeft);
}
/**
* Get the expected FeatureMap.Entry to move.
*
* @param comparison
* The comparison object.
* @param diff
* The diff we are currently merging.
* @param expectedContainer
* The expected container that will contain the expected entry to move.
* @param rightToLeft
* Whether we should move the value in the left or right side.
* @return The expected entry if found, <code>null</code> otherwise.
*/
private FeatureMap.Entry moveElementGetExpectedEntry(final Comparison comparison,
final FeatureMapChange diff, final EObject expectedContainer, final boolean rightToLeft) {
final FeatureMap.Entry expectedEntry;
if (isDiffSourceIsMergeTarget(diff, rightToLeft)) {
expectedEntry = getExpectedEntryWhenDiffSourceIsMergeTarget(comparison, diff);
} else {
expectedEntry = getExpectedEntryWhenDiffSourceIsNotMergeTarget(comparison, diff,
expectedContainer, rightToLeft);
}
return expectedEntry;
}
/**
* Get the expected FeatureMap.Entry to move.
*
* @param comparison
* The comparison object.
* @param diff
* The diff we are currently merging.
* @param expectedContainer
* The expected container that will contain the expected entry to move.
* @param rightToLeft
* Whether we should move the value in the left or right side.
* @return The expected entry if found, <code>null</code> otherwise.
*/
@SuppressWarnings("unchecked")
private FeatureMap.Entry getExpectedEntryWhenDiffSourceIsNotMergeTarget(final Comparison comparison,
final FeatureMapChange diff, final EObject expectedContainer, final boolean rightToLeft) {
FeatureMap.Entry expectedEntry = null;
final IEqualityHelper equalityHelper = comparison.getEqualityHelper();
final FeatureMap.Entry diffEntry = (FeatureMap.Entry)diff.getValue();
// It is a Move on containment entry and the diff has an equivalence.
Equivalence equ = diff.getEquivalence();
if (isFeatureMapContainment(diff) && equ != null) {
// There is a ReferenceChange associated with the FeatureMapChange. This ReferenceChange
// contains the expected value to move.
for (ReferenceChange equivalence : Iterables.filter(equ.getDifferences(),
ReferenceChange.class)) {
final Match equivalenceMatchValue = comparison.getMatch(equivalence.getValue());
final Object expectedEntryValue;
if (rightToLeft) {
expectedEntryValue = equivalenceMatchValue.getLeft();
} else {
expectedEntryValue = equivalenceMatchValue.getRight();
}
expectedEntry = FeatureMapUtil.createEntry(diffEntry.getEStructuralFeature(),
expectedEntryValue);
break;
}
} else {
final List<Object> targetList = (List<Object>)safeEGet(expectedContainer, diff.getAttribute());
for (Object object : targetList) {
if (equalityHelper.matchingValues(diffEntry, object)) {
expectedEntry = (FeatureMap.Entry)object;
break;
}
}
}
return expectedEntry;
}
/**
* Get the expected FeatureMap.Entry to move.
*
* @param comparison
* The comparison object.
* @param diff
* The diff we are currently merging.
* @return The expected entry if found, <code>null</code> otherwise.
*/
@SuppressWarnings("unchecked")
private FeatureMap.Entry getExpectedEntryWhenDiffSourceIsMergeTarget(final Comparison comparison,
final FeatureMapChange diff) {
final FeatureMap.Entry expectedEntry;
final IEqualityHelper equalityHelper = comparison.getEqualityHelper();
final FeatureMap.Entry diffEntry = (FeatureMap.Entry)diff.getValue();
final Match matchValue = comparison.getMatch((EObject)diffEntry.getValue());
final EObject value;
if (diff.getSource() == DifferenceSource.RIGHT) {
value = matchValue.getRight();
} else {
value = matchValue.getLeft();
}
if (comparison.isThreeWay() && isFeatureMapContainment(diff)) {
// search the origin key associated to the value
EStructuralFeature originKey = null;
final List<Object> originList = (List<Object>)safeEGet(matchValue.getOrigin().eContainer(),
diff.getAttribute());
for (Object object : originList) {
if (object instanceof FeatureMap.Entry
&& equalityHelper.matchingValues(value, ((FeatureMap.Entry)object).getValue())) {
// same value, get the key
originKey = ((FeatureMap.Entry)object).getEStructuralFeature();
break;
}
}
expectedEntry = FeatureMapUtil.createEntry(originKey, value);
} else if (((EReference)diffEntry.getEStructuralFeature()).isContainment()) {
EStructuralFeature targetReference = getTargetReference(comparison, diff);
expectedEntry = FeatureMapUtil.createEntry(targetReference, value);
} else {
expectedEntry = FeatureMapUtil.createEntry(diffEntry.getEStructuralFeature(), value);
}
return expectedEntry;
}
/**
* Get the target EStructuralFeature when moving a FeatureMap.Entry.
*
* @param comparison
* The comparison object.
* @param diff
* The diff we are currently merging.
* @return The target reference, i.e. the reference into which the value will be moved.
*/
private EStructuralFeature getTargetReference(final Comparison comparison, final FeatureMapChange diff) {
final FeatureMap.Entry diffEntry = (FeatureMap.Entry)diff.getValue();
final Match equivalenceMatchValue = comparison.getMatch((EObject)diffEntry.getValue());
final EObject targetValue;
if (diff.getSource() == DifferenceSource.LEFT) {
targetValue = equivalenceMatchValue.getRight();
} else {
targetValue = equivalenceMatchValue.getLeft();
}
return targetValue.eContainingFeature();
}
/**
* Checks if the source of the given diff is the same as the target of the merge.
*
* @param diff
* The diff we are currently merging.
* @param rightToLeft
* Whether we should merge the diff in the left or right side.
* @return true if the source of the given diff is the same as the target of the merge, false otherwise.
*/
private boolean isDiffSourceIsMergeTarget(final Diff diff, final boolean rightToLeft) {
DifferenceSource source = diff.getSource();
return source == DifferenceSource.LEFT && rightToLeft
|| source == DifferenceSource.RIGHT && !rightToLeft;
}
/**
* This will do the actual work of moving the element into its attribute. 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 an attribute.
* @param expectedValue
* The value that is to be moved within its attribute.
* @param rightToLeft
* Whether we should move the value in the left or right side.
*/
@SuppressWarnings("unchecked")
protected void doMove(FeatureMapChange diff, Comparison comparison, EObject expectedContainer,
FeatureMap.Entry expectedValue, boolean rightToLeft) {
final EStructuralFeature attribute = diff.getAttribute();
if (attribute.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<Object> targetList = (List<Object>)safeEGet(expectedContainer, attribute);
final int currentIndex = targetList.indexOf(expectedValue);
if (insertionIndex > currentIndex && currentIndex >= 0) {
insertionIndex--;
}
if (currentIndex == -1) {
if (insertionIndex < 0 || insertionIndex >= targetList.size()) {
((BasicFeatureMap)(Object)targetList).addUnique(expectedValue);
} else {
((BasicFeatureMap)(Object)targetList).addUnique(insertionIndex, expectedValue);
}
} else {
if (insertionIndex < 0 || insertionIndex >= targetList.size()) {
((BasicFeatureMap)(Object)targetList).move(targetList.size() - 1, expectedValue);
} else {
((BasicFeatureMap)(Object)targetList).move(insertionIndex, expectedValue);
}
}
} else {
// This will never happen with the default diff engine, but may still be done from extenders
safeESet(expectedContainer, attribute, expectedValue);
}
}
/**
* This will be called by the merge operations in order to change a key.
*
* @param diff
* The diff we are currently merging.
* @param rightToLeft
* Direction of the merge.
*/
@SuppressWarnings("unchecked")
protected void changeValue(FeatureMapChange diff, boolean rightToLeft) {
final Match match = diff.getMatch();
final IEqualityHelper equalityHelper = match.getComparison().getEqualityHelper();
final EStructuralFeature attribute = diff.getAttribute();
final FeatureMap.Entry entry = (FeatureMap.Entry)diff.getValue();
// the value we're looking for in expected and origin container.
final Object entryValue = entry.getValue();
// Get the XMI ID
final String originValueId;
if (entryValue instanceof EObject) {
final Resource initialResource = ((EObject)entryValue).eResource();
if (initialResource instanceof XMIResource) {
originValueId = ((XMIResource)initialResource).getID((EObject)entryValue);
} else {
originValueId = null;
}
} else {
originValueId = null;
}
final EObject expectedContainer;
if (rightToLeft) {
expectedContainer = match.getLeft();
} else {
expectedContainer = match.getRight();
}
final EObject originContainer;
final boolean resetToOrigin = diff.getSource() == DifferenceSource.LEFT && rightToLeft
|| diff.getSource() == DifferenceSource.RIGHT && !rightToLeft;
if (resetToOrigin && match.getComparison().isThreeWay()) {
originContainer = match.getOrigin();
} else if (rightToLeft) {
originContainer = match.getRight();
} else {
originContainer = match.getLeft();
}
// search the origin key associated to the value
EStructuralFeature originKey = null;
final List<Object> originList = (List<Object>)safeEGet(originContainer, attribute);
for (Object object : originList) {
if (object instanceof FeatureMap.Entry) {
// same value, get the key
if (equalityHelper.matchingValues(entryValue, ((FeatureMap.Entry)object).getValue())) {
originKey = ((FeatureMap.Entry)object).getEStructuralFeature();
break;
}
}
}
if (originKey == null) {
throw new RuntimeException("FeatureMapChangeMerger: Cannot find the key to change."); //$NON-NLS-1$
}
// search the value in expected container to change his key.
final List<Object> targetList = (List<Object>)safeEGet(expectedContainer, attribute);
int index = 0;
for (Object object : targetList) {
if (object instanceof FeatureMap.Entry) {
// same value, now change the key
Object targetValue = ((FeatureMap.Entry)object).getValue();
if (equalityHelper.matchingValues(entryValue, targetValue)) {
// forced to use setUnique(int, Entry) because if the originKey is not present in the
// target map, the setUnique(EStructuralFeature, int, Object) will not validate the key
((BasicFeatureMap)(Object)targetList).setUnique(index,
FeatureMapUtil.createEntry(originKey, targetValue));
// setUnique(int, Entry) doesn't keep ID, so copy XMI ID when applicable.
final Resource targetResource;
if (DiffUtil.isContainmentReference(originKey) && targetValue instanceof EObject
&& originValueId != null) {
targetResource = ((EObject)targetValue).eResource();
} else {
targetResource = null;
}
if (targetResource instanceof XMIResource) {
((XMIResource)targetResource).setID((EObject)targetValue, originValueId);
}
break;
}
}
index++;
}
}
/**
* 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);
}
}