blob: 200e362d91f6b73d4262f0a1bdf5769d019deccc [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2012, 2015 Obeo and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Obeo - initial API and implementation
* Philip Langer - [446947, 458147] Support for three-way text merging
*******************************************************************************/
package org.eclipse.emf.compare.merge;
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 java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.compare.AttributeChange;
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.internal.ThreeWayTextDiff;
import org.eclipse.emf.compare.internal.utils.DiffUtil;
import org.eclipse.emf.compare.utils.IEqualityHelper;
import org.eclipse.emf.ecore.EAttribute;
import org.eclipse.emf.ecore.EEnum;
import org.eclipse.emf.ecore.EEnumLiteral;
import org.eclipse.emf.ecore.ENamedElement;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.emf.ecore.impl.DynamicEObjectImpl;
/**
* This specific implementation of {@link AbstractMerger} will be used to merge attribute changes.
*
* @author <a href="mailto:laurent.goubet@obeo.fr">Laurent Goubet</a>
*/
public class AttributeChangeMerger extends AbstractMerger {
/**
* {@inheritDoc}
*
* @see org.eclipse.emf.compare.merge.IMerger#isMergerFor(org.eclipse.emf.compare.Diff)
*/
public boolean isMergerFor(Diff target) {
return target instanceof AttributeChange;
}
/**
* {@inheritDoc}
*
* @see org.eclipse.emf.compare.merge.AbstractMerger#accept(org.eclipse.emf.compare.Diff, boolean)
*/
@Override
protected void accept(final Diff diff, boolean rightToLeft) {
AttributeChange attributeChange = (AttributeChange)diff;
switch (diff.getKind()) {
case ADD:
// Create the same element in right
addInTarget(attributeChange, rightToLeft);
break;
case DELETE:
// Delete that same element from right
removeFromTarget(attributeChange, rightToLeft);
break;
case MOVE:
moveElement(attributeChange, rightToLeft);
break;
case CHANGE:
changeValue(attributeChange, 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) {
AttributeChange attributeChange = (AttributeChange)diff;
switch (diff.getKind()) {
case ADD:
// We have a ADD on right. we need to revert this addition
removeFromTarget(attributeChange, rightToLeft);
break;
case DELETE:
// DELETE in the right. We need to re-create this element
addInTarget(attributeChange, rightToLeft);
break;
case MOVE:
moveElement(attributeChange, rightToLeft);
break;
case CHANGE:
changeValue(attributeChange, 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 objet 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(AttributeChange diff, boolean rightToLeft) {
final Match match = diff.getMatch();
final EObject targetContainer = getTargetContainer(diff, rightToLeft);
if (targetContainer == 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 Object expectedValue = diff.getValue();
// We have the container, attribute and value. We need to know the insertion index.
if (attribute.isMany()) {
final int insertionIndex = findInsertionIndex(comparison, diff, rightToLeft);
final List<Object> targetList = (List<Object>)safeEGet(targetContainer, attribute);
addAt(targetList, expectedValue, insertionIndex);
} else {
safeESet(targetContainer, attribute, expectedValue);
}
}
/**
* 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(AttributeChange diff, boolean rightToLeft) {
final EObject currentContainer = getTargetContainer(diff, rightToLeft);
if (currentContainer != null) {
final Object expectedValue = diff.getValue();
final EStructuralFeature attribute = diff.getAttribute();
// We have the container, attribute and value to remove.
if (attribute.isMany()) {
if (attribute.isUnique()) {
final List<Object> targetList = (List<Object>)safeEGet(currentContainer, attribute);
targetList.remove(expectedValue);
} else {
removeNonUniqueFromTarget(diff, rightToLeft);
}
} else {
currentContainer.eUnset(attribute);
}
}
}
@SuppressWarnings("unchecked")
private void removeNonUniqueFromTarget(AttributeChange diff, boolean rightToLeft) {
Comparison comparison = diff.getMatch().getComparison();
IEqualityHelper equalityHelper = comparison.getEqualityHelper();
EAttribute attribute = diff.getAttribute();
EObject sourceContainer;
EObject targetContainer;
if (rightToLeft) {
sourceContainer = diff.getMatch().getRight();
targetContainer = diff.getMatch().getLeft();
} else {
sourceContainer = diff.getMatch().getLeft();
targetContainer = diff.getMatch().getRight();
}
List<Object> sourceList = (List<Object>)safeEGet(sourceContainer, attribute);
List<Object> targetList = (List<Object>)safeEGet(targetContainer, attribute);
Object valueToRemove = diff.getValue();
List<Object> lcs = DiffUtil.longestCommonSubsequence(comparison, sourceList, targetList);
// The current index, in the target list, of that value
int currentIndexInTarget = -1;
Iterator<Object> lcsIteratorForTargetLookup = lcs.iterator();
Object currentLCS = null;
if (lcsIteratorForTargetLookup.hasNext()) {
currentLCS = lcsIteratorForTargetLookup.next();
}
for (int j = 0; j < targetList.size() && currentIndexInTarget == -1; j++) {
if (currentLCS == null) {
// we've iterated on our whole LCS.
// first instance of our target is the one to move.
if (equalityHelper.matchingAttributeValues(targetList.get(j), valueToRemove)) {
currentIndexInTarget = j;
}
} else if (equalityHelper.matchingAttributeValues(targetList.get(j), currentLCS)) {
// this matches our current lcs item. continue to next one if any
if (lcsIteratorForTargetLookup.hasNext()) {
currentLCS = lcsIteratorForTargetLookup.next();
} else {
currentLCS = null;
}
} else {
// This item is not in our LCS. Is it the one we're looking for?
if (equalityHelper.matchingAttributeValues(targetList.get(j), valueToRemove)) {
currentIndexInTarget = j;
}
}
}
// value might not exist in target list if it has already been removed or if this was a pseudo
// conflict deletion
if (currentIndexInTarget >= 0) {
targetList.remove(currentIndexInTarget);
}
}
/**
* 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(AttributeChange diff, boolean rightToLeft) {
final EObject expectedContainer = getTargetContainer(diff, rightToLeft);
if (expectedContainer == null) {
throw new IllegalStateException(
"Couldn't move element because its target parent hasn't been merged yet: " + diff); //$NON-NLS-1$
}
final Comparison comparison = diff.getMatch().getComparison();
final Object expectedValue = diff.getValue();
// We now know the target container, target attribute and target value.
doMove(diff, comparison, expectedContainer, expectedValue, 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.
*/
protected void doMove(AttributeChange diff, Comparison comparison, EObject expectedContainer,
Object expectedValue, boolean rightToLeft) {
final EStructuralFeature attribute = diff.getAttribute();
if (attribute.isMany()) {
if (!attribute.isUnique()) {
doMoveNonUniqueAttribute(diff, comparison, rightToLeft);
} else {
doMoveUniqueAttribute(diff, comparison, expectedContainer, expectedValue, rightToLeft);
}
} else {
// This will never happen with the default diff engine, but may still be done from extenders
safeESet(expectedContainer, attribute, expectedValue);
}
}
@SuppressWarnings("unchecked")
private void doMoveUniqueAttribute(AttributeChange diff, Comparison comparison, EObject expectedContainer,
Object expectedValue, boolean rightToLeft) {
final EStructuralFeature attribute = diff.getAttribute();
// 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) {
insertionIndex--;
}
if (targetList instanceof EList<?>) {
if (insertionIndex < 0 || insertionIndex >= targetList.size()) {
((EList<Object>)targetList).move(targetList.size() - 1, expectedValue);
} else {
((EList<Object>)targetList).move(insertionIndex, expectedValue);
}
} else {
targetList.remove(expectedValue);
if (insertionIndex < 0 || insertionIndex > targetList.size()) {
targetList.add(expectedValue);
} else {
targetList.add(insertionIndex, expectedValue);
}
}
}
@SuppressWarnings("unchecked")
private void doMoveNonUniqueAttribute(AttributeChange diff, Comparison comparison, boolean rightToLeft) {
IEqualityHelper equalityHelper = comparison.getEqualityHelper();
EAttribute attribute = diff.getAttribute();
EObject sourceContainer;
EObject targetContainer;
if (rightToLeft) {
sourceContainer = diff.getMatch().getRight();
targetContainer = diff.getMatch().getLeft();
} else {
sourceContainer = diff.getMatch().getLeft();
targetContainer = diff.getMatch().getRight();
}
List<Object> sourceList = (List<Object>)safeEGet(sourceContainer, attribute);
List<Object> targetList = (List<Object>)safeEGet(targetContainer, attribute);
// copy this one : we'll have to modify target while iterating over this view
List<Object> copyTarget = new ArrayList<>(targetList);
Object valueToMove = diff.getValue();
Set<Object> ignoredElements = DiffUtil.computeIgnoredElements(comparison, equalityHelper, targetList,
diff, rightToLeft);
if (ignoredElements.isEmpty()) {
ignoredElements = Collections.singleton(valueToMove);
} else {
ignoredElements.add(valueToMove);
}
List<Object> lcs = DiffUtil.longestCommonSubsequence(comparison, ignoredElements, sourceList,
copyTarget);
// We need to find the current index, in the source list, of the value to move
int currentIndexInSource = -1;
Iterator<Object> lcsIteratorForSourceLookup = lcs.iterator();
Object currentLCS = null;
if (lcsIteratorForSourceLookup.hasNext()) {
currentLCS = lcsIteratorForSourceLookup.next();
}
for (int j = 0; j < sourceList.size() && currentIndexInSource == -1; j++) {
if (currentLCS == null) {
// we've iterated on our whole LCS.
// first instance of our target is the one to move.
if (equalityHelper.matchingAttributeValues(sourceList.get(j), valueToMove)) {
currentIndexInSource = j;
}
} else if (equalityHelper.matchingAttributeValues(sourceList.get(j), currentLCS)) {
// this matches our current lcs item. continue to next one if any
if (lcsIteratorForSourceLookup.hasNext()) {
currentLCS = lcsIteratorForSourceLookup.next();
} else {
currentLCS = null;
}
} else {
// This item is not in our LCS. Is it the one we're looking for?
if (equalityHelper.matchingAttributeValues(sourceList.get(j), valueToMove)) {
currentIndexInSource = j;
}
}
}
// The current index, in the target list, of that value
int currentIndexInTarget = -1;
Iterator<Object> lcsIteratorForTargetLookup = lcs.iterator();
currentLCS = null;
if (lcsIteratorForTargetLookup.hasNext()) {
currentLCS = lcsIteratorForTargetLookup.next();
}
for (int j = 0; j < targetList.size() && currentIndexInTarget == -1; j++) {
if (currentLCS == null) {
// we've iterated on our whole LCS.
// first instance of our target is the one to move.
if (equalityHelper.matchingAttributeValues(targetList.get(j), valueToMove)) {
currentIndexInTarget = j;
}
} else if (equalityHelper.matchingAttributeValues(targetList.get(j), currentLCS)) {
// this matches our current lcs item. continue to next one if any
if (lcsIteratorForTargetLookup.hasNext()) {
currentLCS = lcsIteratorForTargetLookup.next();
} else {
currentLCS = null;
}
} else {
// This item is not in our LCS. Is it the one we're looking for?
if (equalityHelper.matchingAttributeValues(targetList.get(j), valueToMove)) {
currentIndexInTarget = j;
}
}
}
// Then the index, in the target, at which this value needs to be moved
int insertionIndex = DiffUtil.findInsertionIndexForElementAt(comparison, sourceList, copyTarget, lcs,
currentIndexInSource);
if (targetList instanceof EList<?>) {
if (insertionIndex < 0 || insertionIndex >= targetList.size()) {
((EList<Object>)targetList).move(targetList.size() - 1, currentIndexInTarget);
} else {
((EList<Object>)targetList).move(insertionIndex, currentIndexInTarget);
}
} else {
targetList.remove(currentIndexInTarget);
if (insertionIndex < 0 || insertionIndex > targetList.size()) {
targetList.add(valueToMove);
} else {
targetList.add(insertionIndex, valueToMove);
}
}
}
/**
* This will be called by the merge operations in order to reset an attribute to its original value, be
* that the left or right side.
* <p>
* Should never be called on multi-valued attributes.
* </p>
*
* @param diff
* The diff we are currently merging.
* @param rightToLeft
* Tells us the direction of this merge operation.
* @deprecated this has been refactored into {@link #changeValue(AttributeChange, boolean)}.
*/
@Deprecated
protected void resetInTarget(AttributeChange diff, boolean rightToLeft) {
final EStructuralFeature attribute = diff.getAttribute();
final EObject targetContainer = getTargetContainer(diff, rightToLeft);
final EObject sourceContainer = getSourceContainer(diff, rightToLeft);
if (sourceContainer == null || !safeEIsSet(targetContainer, attribute)
|| !safeEIsSet(sourceContainer, attribute)) {
targetContainer.eUnset(attribute);
} else {
final Object expectedValue = safeEGet(sourceContainer, attribute);
safeESet(targetContainer, attribute, expectedValue);
}
}
/**
* This will be called by the merge operations in order to change single-valued attributes.
*
* @param diff
* The diff we are currently merging.
* @param rightToLeft
* Direction of the merge.
*/
protected void changeValue(AttributeChange diff, boolean rightToLeft) {
final EObject targetContainer = getTargetContainer(diff, rightToLeft);
if (targetContainer == null) {
// We're merging the unset of an attribute value under a containment deletion.
// There's actually no action to take in such a case.
return;
}
final EStructuralFeature attribute = diff.getAttribute();
final Object targetValue = getTargetValue(diff, rightToLeft);
if (isUnset(diff, targetValue)) {
targetContainer.eUnset(attribute);
} else {
safeESet(targetContainer, attribute, targetValue);
}
}
/**
* Returns the target value, that is, the value to be set when merging the given {@code diff} in the
* direction indicated by {@code rightToLeft}.
*
* @param diff
* The diff we are currently merging.
* @param rightToLeft
* Direction of the merge.
* @return The target value to be set when merging.
*/
private Object getTargetValue(AttributeChange diff, boolean rightToLeft) {
final EStructuralFeature attribute = diff.getAttribute();
final EObject targetContainer = getTargetContainer(diff, rightToLeft);
final EObject sourceContainer = getSourceContainer(diff, rightToLeft);
final Object sourceValue = safeEGet(sourceContainer, attribute);
final Object targetValue;
if (isEnumChangeOfDynamicEObject(diff, targetContainer)) {
final EEnum eEnum = getAttributeTypeEnumFromDynamicObject(targetContainer, attribute);
targetValue = eEnum.getEEnumLiteral(((ENamedElement)sourceValue).getName());
} else if (requireThreeWayTextMerge(diff)) {
targetValue = performThreeWayTextMerge(diff, rightToLeft);
} else {
targetValue = sourceValue;
}
return targetValue;
}
/**
* Specifies whether the given {@code diff} concerns a change of a dynamic EObject at an attribute with an
* EEnum type.
*
* @param diff
* The diff to check.
* @param targetContainer
* The target container.
* @return <code>true</code> if is a change of a dynamic EObject with an EEnum-typed attribute,
* <code>false</code> otherwise.
*/
private boolean isEnumChangeOfDynamicEObject(AttributeChange diff, EObject targetContainer) {
return diff.getAttribute().getEType() instanceof EEnum
&& targetContainer instanceof DynamicEObjectImpl;
}
/**
* Returns the EEnum that is the attribute type of the given {@code attribute} used in the given container
* object {@code dynamicObject}.
*
* @param dynamicObject
* The container object.
* @param attribute
* The attribute.
* @return The EEnum acting as type of {@code attribute}.
*/
private EEnum getAttributeTypeEnumFromDynamicObject(EObject dynamicObject, EStructuralFeature attribute) {
return (EEnum)((EEnumLiteral)safeEGet(dynamicObject, attribute)).eContainer();
}
/**
* Returns the source container, that is the original container holding the original value before the
* given {@code diff} is applied. This is different depending on direction indicated by
* {@code rightToLeft}.
*
* @param diff
* The diff we are currently merging.
* @param rightToLeft
* Direction of the merge.
* @return The source container.
*/
private EObject getSourceContainer(AttributeChange diff, boolean rightToLeft) {
final EObject sourceContainer;
final Match match = diff.getMatch();
if (match.getComparison().isThreeWay() && isRejectingChange(diff, rightToLeft)) {
sourceContainer = match.getOrigin();
} else if (rightToLeft) {
sourceContainer = match.getRight();
} else {
sourceContainer = match.getLeft();
}
return sourceContainer;
}
/**
* Returns the target container, that is the container holding the value to be updated when merging the
* given {@code diff} in the direction indicated by {@code rightToLeft}.
*
* @param diff
* The diff we are currently merging.
* @param rightToLeft
* Direction of the merge.
* @return The target container to be updated when merging.
*/
private EObject getTargetContainer(AttributeChange diff, boolean rightToLeft) {
final Match match = diff.getMatch();
if (rightToLeft) {
return match.getLeft();
} else {
return match.getRight();
}
}
/**
* Specifies whether the given {@code diff} unsets the attribute value when updating the attribute with
* the given {@code targetValue}.
*
* @param diff
* The difference to check.
* @param targetValue
* The value to be set.
* @return <code>true</code> if setting {@code targetValue} is an unset, <code>false</code> otherwise.
*/
private boolean isUnset(AttributeChange diff, Object targetValue) {
final Object defaultValue = diff.getAttribute().getDefaultValue();
return targetValue == null || targetValue.equals(defaultValue)
|| (defaultValue == null && "".equals(targetValue)); //$NON-NLS-1$
}
/**
* Specifies whether a three-way text merge is required for applying the given {@code diff} in the
* direction indicated in {@code rightToLeft}.
* <p>
* Three-way text merging is required when accepting or rejecting the changes of a
* {@link #isStringAttribute(EAttribute) String attributes} in a three-way merge scenario.
* </p>
*
* @param diff
* The diff to be applied.
* @return <code>true</code> if three-way text merging is required, <code>false</code> otherwise.
*/
private boolean requireThreeWayTextMerge(AttributeChange diff) {
return diff.getMatch().getComparison().isThreeWay() && isStringAttribute(diff.getAttribute());
}
/**
* Specifies whether the given {@code attribute} is an attribute of type String.
*
* @param attribute
* The attribute to be checked.
* @return <code>true</code> if it is a String attribute, <code>false</code> otherwise.
*/
private boolean isStringAttribute(EAttribute attribute) {
return attribute.getEAttributeType().getInstanceClass() == String.class;
}
/**
* Specifies whether applying the given {@code diff} in the direction indicated in {@code rightToLeft}
* means accepting the change as opposed to rejecting the change.
*
* @param diff
* The diff to be checked.
* @param rightToLeft
* The direction of applying {@code diff}.
* @return <code>true</code> if it means accepting the change, <code>false</code> otherwise.
*/
private boolean isAcceptingChange(AttributeChange diff, boolean rightToLeft) {
return (DifferenceSource.LEFT.equals(diff.getSource()) && !rightToLeft)
|| (DifferenceSource.RIGHT.equals(diff.getSource()) && rightToLeft);
}
/**
* Specifies whether applying the given {@code diff} in the direction indicated in {@code rightToLeft}
* means rejecting the change as opposed to accepting the change.
*
* @param diff
* The diff to be checked.
* @param rightToLeft
* The direction of applying {@code diff}.
* @return <code>true</code> if it means rejecting the change, <code>false</code> otherwise.
*/
private boolean isRejectingChange(AttributeChange diff, boolean rightToLeft) {
return !isAcceptingChange(diff, rightToLeft);
}
/**
* Performs a three-way text merge for the given {@code diff} and returns the merged text.
* <p>
* Depending on whether the given {@code diff} is an accept or reject in the context of the merge
* direction indicated by {@code rightToLeft}, this method will perform different strategies of merging.
* </p>
*
* @param diff
* The diff for which a three-way text diff is to be performed.
* @param rightToLeft
* The direction of applying the {@code diff}.
* @return The merged text.
*/
private String performThreeWayTextMerge(AttributeChange diff, boolean rightToLeft) {
if (isAcceptingChange(diff, rightToLeft)) {
return performAcceptingThreeWayTextMerge(diff);
} else {
return performRejectingThreeWayTextMerge(diff, rightToLeft);
}
}
/**
* Performs a three-way text merge accepting the given {@code diff} and returns the merged text.
* <p>
* This method must only be called for {@link #isStringAttribute(EAttribute) String attributes}. As the
* three-way text merging is symmetric, the result is equal irrespectively of the direction of merging as
* long as applying the change {@link #isAcceptingChange(AttributeChange, boolean) means accepting it} as
* opposed to rejecting it.
* </p>
*
* @param diff
* The diff for which a three-way text diff is to be performed.
* @return The merged text.
*/
private String performAcceptingThreeWayTextMerge(AttributeChange diff) {
final Match match = diff.getMatch();
final EAttribute attribute = diff.getAttribute();
final String originValue = (String)safeEGet(match.getOrigin(), attribute);
final String leftValue = (String)safeEGet(match.getLeft(), attribute);
final String rightValue = (String)safeEGet(match.getRight(), attribute);
return performThreeWayTextMerge(leftValue, rightValue, originValue);
}
/**
* Performs a three-way text merge rejecting the given {@code diff} and returns the merged text.
* <p>
* When rejecting an attribute change, we need to undo its effects on the attribute value, which is in
* most of the cases done by setting the value to the original value. However, if there is a concurrent
* attribute change of the same attribute value at the opposite side, it might have been merged to the
* current side already. Therefore, we need to undo only those differences in the attribute value that
* come from the current to-be-rejected diff.
* </p>
* <p>
* This is done by applying a normal three-way merge, but instead of the origin value, left value, and
* right value, we compute the three-way merge as follows: as origin value, we use the value of the
* {@code diff}, which is the value as it was set on the respective side. As a left value, the value of
* the current side as it is currently stored in the model; thus, a potential merging of opposite diffs
* may have changed this value already. And as a right value, we use the actual origin value from the
* origin model.
* </p>
* <p>
* Since we consider the current value as it is in the model right now (including potential previous
* merges) and the origin value as the two changed sides, a three-way merge will apply the changes we did
* through merging and reset all other to the origin value.
* </p>
* <p>
* Note that, if {@code diff} is an unset (that is, the current value is empty or null or default), we
* just use the original value.
* </p>
*
* @param diff
* The diff for which a three-way text diff is to be performed.
* @param rightToLeft
* The direction of applying the {@code diff}.
* @return The merged text.
*/
private String performRejectingThreeWayTextMerge(AttributeChange diff, boolean rightToLeft) {
final EAttribute attribute = diff.getAttribute();
final EObject originContainer = diff.getMatch().getOrigin();
final String originValue = (String)safeEGet(originContainer, attribute);
final String changedValueFromModel = getChangedValueFromModel(diff);
final String changedValue = (String)diff.getValue();
if (isUnset(diff, changedValueFromModel)) {
return originValue;
} else {
return performThreeWayTextMerge(changedValueFromModel, originValue, changedValue);
}
}
/**
* Returns the changed value, as it is right now stored in the model, of the attribute that is affected by
* the given {@code diff}.
*
* @param diff
* The diff to get the changed value for.
* @return The changed value.
*/
private String getChangedValueFromModel(AttributeChange diff) {
final EAttribute attribute = diff.getAttribute();
final EObject changedContainer;
switch (diff.getSource()) {
case LEFT:
changedContainer = diff.getMatch().getLeft();
break;
case RIGHT:
changedContainer = diff.getMatch().getRight();
break;
default:
throw new IllegalArgumentException();
}
return (String)safeEGet(changedContainer, attribute);
}
/**
* Performs a three-way text merge for the given {@code origin}, {@code left}, and {@code right} text
* versions.
*
* @param left
* The left version of the String.
* @param right
* The right version of the String.
* @param origin
* The original version of the String.
* @return The merged version.
* @since 3.2
*/
protected String performThreeWayTextMerge(final String left, final String right, final String origin) {
ThreeWayTextDiff textDiff = new ThreeWayTextDiff(origin, left, right);
return textDiff.getMerged();
}
/**
* 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);
}
}