blob: c99adc317528943cdfd3b7705e6bba6b34992b6e [file] [log] [blame]
/*******************************************************************************
* 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.merge;
import static org.eclipse.emf.compare.utils.ReferenceUtil.safeEGet;
import static org.eclipse.emf.compare.utils.ReferenceUtil.safeEIsSet;
import java.util.List;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.common.util.Monitor;
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.DifferenceState;
import org.eclipse.emf.compare.Match;
import org.eclipse.emf.compare.utils.DiffUtil;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EStructuralFeature;
/**
* 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.IMerger#copyLeftToRight(org.eclipse.emf.compare.Diff,
* org.eclipse.emf.common.util.Monitor)
*/
public void copyLeftToRight(Diff target, Monitor monitor) {
// Don't merge an already merged (or discarded) diff
if (target.getState() != DifferenceState.UNRESOLVED) {
return;
}
final AttributeChange diff = (AttributeChange)target;
// Change the diff's state before we actually merge it : this allows us to avoid requirement cycles.
diff.setState(DifferenceState.MERGED);
if (diff.getEquivalence() != null) {
for (Diff equivalent : diff.getEquivalence().getDifferences()) {
equivalent.setState(DifferenceState.MERGED);
}
}
if (diff.getSource() == DifferenceSource.LEFT) {
// merge all "requires" diffs
mergeRequires(diff, false, monitor);
switch (diff.getKind()) {
case ADD:
// Create the same element in right
addInTarget(diff, false);
break;
case DELETE:
// Delete that same element from right
removeFromTarget(diff, false);
break;
case MOVE:
moveElement(diff, false);
break;
case CHANGE:
changeValue(diff, false);
break;
default:
break;
}
} else {
// merge all "required by" diffs
mergeRequiredBy(diff, false, monitor);
switch (diff.getKind()) {
case ADD:
// We have a ADD on right. we need to revert this addition
removeFromTarget(diff, false);
break;
case DELETE:
// DELETE in the right. We need to re-create this element
addInTarget(diff, false);
break;
case MOVE:
moveElement(diff, false);
break;
case CHANGE:
changeValue(diff, false);
break;
default:
break;
}
}
}
/**
* {@inheritDoc}
*
* @see org.eclipse.emf.compare.merge.IMerger#copyRightToLeft(org.eclipse.emf.compare.Diff,
* org.eclipse.emf.common.util.Monitor)
*/
public void copyRightToLeft(Diff target, Monitor monitor) {
// Don't merge an already merged (or discarded) diff
if (target.getState() != DifferenceState.UNRESOLVED) {
return;
}
final AttributeChange diff = (AttributeChange)target;
// Change the diff's state before we actually merge it : this allows us to avoid requirement cycles.
diff.setState(DifferenceState.MERGED);
if (diff.getEquivalence() != null) {
for (Diff equivalent : diff.getEquivalence().getDifferences()) {
equivalent.setState(DifferenceState.MERGED);
}
}
if (diff.getSource() == DifferenceSource.LEFT) {
// merge all "required by" diffs
mergeRequiredBy(diff, true, monitor);
switch (diff.getKind()) {
case ADD:
// We have a ADD on left, thus nothing in right. We need to revert the addition
removeFromTarget(diff, true);
break;
case DELETE:
// DELETE in the left, thus an element in right. We need to re-create that element
addInTarget(diff, true);
break;
case MOVE:
moveElement(diff, true);
break;
case CHANGE:
changeValue(diff, true);
break;
default:
break;
}
} else {
// merge all "requires" diffs
mergeRequires(diff, true, monitor);
switch (diff.getKind()) {
case ADD:
addInTarget(diff, true);
break;
case DELETE:
removeFromTarget(diff, true);
break;
case MOVE:
moveElement(diff, true);
break;
case CHANGE:
changeValue(diff, true);
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 expectedContainer;
if (rightToLeft) {
expectedContainer = match.getLeft();
} else {
expectedContainer = match.getRight();
}
if (expectedContainer == null) {
// FIXME throw exception? log? re-try to merge our requirements?
// one of the "required" diffs should have created our container.
} else {
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>)expectedContainer.eGet(attribute);
addAt(targetList, expectedValue, insertionIndex);
} else {
expectedContainer.eSet(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;
if (rightToLeft) {
currentContainer = diff.getMatch().getLeft();
} else {
currentContainer = diff.getMatch().getRight();
}
if (currentContainer == null) {
// FIXME throw exception? log? re-try to merge our requirements?
} else {
final Object expectedValue = diff.getValue();
final EStructuralFeature attribute = diff.getAttribute();
// We have the container, attribute and value to remove.
if (attribute.isMany()) {
/*
* 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>)currentContainer.eGet(attribute);
targetList.remove(expectedValue);
} else {
currentContainer.eUnset(attribute);
}
}
}
/**
* 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;
if (rightToLeft) {
expectedContainer = diff.getMatch().getLeft();
} else {
expectedContainer = diff.getMatch().getRight();
}
if (expectedContainer == null) {
// TODO throws exception?
} else {
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.
*/
@SuppressWarnings("unchecked")
protected void doMove(AttributeChange diff, Comparison comparison, EObject expectedContainer,
Object 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>)expectedContainer.eGet(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);
}
}
} else {
// This will never happen with the default diff engine, but may still be done from extenders
expectedContainer.eSet(attribute, expectedValue);
}
}
/**
* 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 Match match = diff.getMatch();
final EStructuralFeature attribute = diff.getAttribute();
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(targetContainer, attribute)
|| !safeEIsSet(originContainer, attribute)) {
targetContainer.eUnset(attribute);
} else {
final Object expectedValue = originContainer.eGet(attribute);
targetContainer.eSet(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 Match match = diff.getMatch();
final EStructuralFeature attribute = diff.getAttribute();
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();
}
final Object targetValue = safeEGet(originContainer, attribute);
// Though not the "default value", we consider that an empty string is an unset attribute.
final Object defaultValue = attribute.getDefaultValue();
boolean isUnset = targetValue == null || targetValue.equals(defaultValue)
|| (defaultValue == null && "".equals(targetValue)); //$NON-NLS-1$
if (isUnset) {
expectedContainer.eUnset(attribute);
} else {
expectedContainer.eSet(attribute, targetValue);
}
}
/**
* 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);
}
}