blob: 1758fcf7d4eb7a3aab2f402200455bb4ba655a6f [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 java.util.Collections;
import java.util.List;
import org.eclipse.emf.common.util.Monitor;
import org.eclipse.emf.common.util.URI;
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.EMFCompareMessages;
import org.eclipse.emf.compare.Match;
import org.eclipse.emf.compare.MatchResource;
import org.eclipse.emf.compare.ResourceAttachmentChange;
import org.eclipse.emf.compare.utils.DiffUtil;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.InternalEObject;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.resource.ResourceSet;
import org.eclipse.emf.ecore.xmi.XMIResource;
/**
* This specific implementation of {@link AbstractMerger} will be used to merge resource attachment changes.
*
* @author <a href="mailto:laurent.goubet@obeo.fr">Laurent Goubet</a>
*/
public class ResourceAttachmentChangeMerger extends AbstractMerger {
/**
* {@inheritDoc}
*
* @see org.eclipse.emf.compare.merge.IMerger#isMergerFor(org.eclipse.emf.compare.Diff)
*/
public boolean isMergerFor(Diff target) {
return target instanceof ResourceAttachmentChange;
}
/**
* {@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 ResourceAttachmentChange diff = (ResourceAttachmentChange)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 root in right
addInTarget(diff, false);
break;
case DELETE:
// Delete that same root from right
removeFromTarget(diff, false);
break;
default:
// other cases are unknown at the time of writing
break;
}
} else {
// merge all "required by" diffs
mergeRequiredBy(diff, false, monitor);
switch (diff.getKind()) {
case ADD:
// Revert the addition of this root
removeFromTarget(diff, false);
break;
case DELETE:
// re-create this element
addInTarget(diff, false);
break;
default:
// other cases are unknown at the time of writing
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 ResourceAttachmentChange diff = (ResourceAttachmentChange)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:
// Revert the addition of this root
removeFromTarget(diff, true);
break;
case DELETE:
// re-create this element
addInTarget(diff, true);
break;
default:
// other cases are unknown at the time of writing
break;
}
} else {
// merge all "requires" diffs
mergeRequires(diff, true, monitor);
switch (diff.getKind()) {
case ADD:
// Create the same root in left
addInTarget(diff, true);
break;
case DELETE:
// Delete that same root from left
removeFromTarget(diff, true);
break;
default:
// other cases are unknown at the time of writing
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. 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 root 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 root in the right).</li>
* </ul>
* </li>
* </ul>
* </p>
*
* @param diff
* The difference we are to merge.
* @param rightToLeft
* Tells us whether we are to add an object on the left or right side.
*/
protected void addInTarget(ResourceAttachmentChange diff, boolean rightToLeft) {
final Match match = diff.getMatch();
final Comparison comparison = match.getComparison();
final Resource expectedContainer = findOrCreateTargetResource(match, rightToLeft);
if (expectedContainer == null) {
// TODO log
diff.setState(DifferenceState.UNRESOLVED);
return;
}
final EObject sourceValue;
if (rightToLeft) {
sourceValue = match.getRight();
} else {
sourceValue = match.getLeft();
}
final EObject expectedValue;
if (rightToLeft) {
if (match.getLeft() != null) {
expectedValue = match.getLeft();
} else {
expectedValue = createCopy(sourceValue);
match.setLeft(expectedValue);
}
} else if (match.getRight() != null) {
expectedValue = match.getRight();
} else {
expectedValue = createCopy(sourceValue);
match.setRight(expectedValue);
}
// We have the container, reference and value. We need to know the insertion index.
final Resource initialResource = sourceValue.eResource();
final List<EObject> sourceList = initialResource.getContents();
final List<EObject> targetList = expectedContainer.getContents();
final int insertionIndex = findInsertionIndex(comparison, sourceList, targetList, expectedValue);
addAt(targetList, expectedValue, insertionIndex);
// Copy XMI ID when applicable.
if (initialResource instanceof XMIResource && expectedContainer instanceof XMIResource) {
((XMIResource)expectedContainer).setID(expectedValue, ((XMIResource)initialResource)
.getID(sourceValue));
}
}
/**
* This will try and locate the "target" resource of this merge in the current comparison. If we can't
* locate it, we assume that it needs to be created as we are in the process of adding a new element to
* it.
*
* @param match
* Match of the root which resource we need to find or create.
* @param rightToLeft
* Direction of the merge. This will tell us which side we are to look up for the target
* resource.
* @return The resource we could find in the current comparison if any. Otherwise, we'll return either a
* newly created resource that can serve as a target of this merge, or <code>null</code> if no
* valid target resource can be created.
*/
protected Resource findOrCreateTargetResource(Match match, boolean rightToLeft) {
final Comparison comparison = match.getComparison();
final Resource sourceRes;
if (rightToLeft) {
sourceRes = match.getRight().eResource();
} else {
sourceRes = match.getLeft().eResource();
}
final MatchResource soughtMatch = getMatchResource(comparison, sourceRes);
// Is the resource already existing or do we need to create it?
final Resource target;
if (rightToLeft && soughtMatch.getLeft() != null) {
target = soughtMatch.getLeft();
} else if (!rightToLeft && soughtMatch.getRight() != null) {
target = soughtMatch.getRight();
} else {
// we need to create it.
final URI targetURI = computeTargetURI(match, rightToLeft);
// FIXME this will most likely fail with remote URIs : we'll need to make it local afterwards
if (targetURI == null) {
// We treat null as "no valid target". We'll cancel the merge operation.
// FIXME we need to rollback the current merge operation.
throw new RuntimeException("Couldn't create a valid target resource for "
+ sourceRes.getURI());
}
final List<MatchResource> matchedResources = comparison.getMatchedResources();
final int size = matchedResources.size();
ResourceSet targetSet = null;
for (int i = 0; i < size && targetSet == null; i++) {
final MatchResource matchRes = matchedResources.get(i);
if (rightToLeft && matchRes.getLeft() != null) {
targetSet = matchRes.getLeft().getResourceSet();
} else if (!rightToLeft && matchRes.getRight() != null) {
targetSet = matchRes.getRight().getResourceSet();
}
}
if (targetSet == null) {
// Cannot create the target
throw new RuntimeException(EMFCompareMessages.getString(
"ResourceAttachmentChangeSpec.MissingRS", targetURI.lastSegment())); //$NON-NLS-1$
}
// This resource might already exists... in which case we cannot use it
if (targetSet.getURIConverter().exists(targetURI, Collections.emptyMap())) {
// FIXME we need to rollback the current merge operation.
throw new RuntimeException("The resource '" + sourceRes.getURI()
+ "' already exists at that location.");
} else {
target = targetSet.createResource(targetURI);
if (rightToLeft) {
soughtMatch.setLeft(target);
soughtMatch.setLeftURI(target.getURI().toString());
} else {
soughtMatch.setRight(target);
soughtMatch.setRightURI(target.getURI().toString());
}
}
}
return target;
}
/**
* Computes the URI of the "target" resource. Will be used if we need to create or "find" it.
*
* @param match
* Match of the root for which we need a resource URI.
* @param rightToLeft
* Direction of the merge.
* @return The URI that is to be used for our target resource. <code>null</code> if we cannot compute a
* valid target URI.
*/
protected URI computeTargetURI(Match match, boolean rightToLeft) {
final EObject sourceObject;
final EObject targetObject;
if (rightToLeft) {
sourceObject = match.getRight();
targetObject = match.getLeft();
} else {
sourceObject = match.getLeft();
targetObject = match.getRight();
}
final Resource currentResource;
if (targetObject == null) {
// A new root in a resource we don't have yet.
// Is this object container somewhere else (controlled)?
final EObject sourceContainer = sourceObject.eContainer();
if (sourceContainer != null) {
final Match containerMatch = match.getComparison().getMatch(sourceContainer);
if (rightToLeft) {
currentResource = containerMatch.getLeft().eResource();
} else {
currentResource = containerMatch.getRight().eResource();
}
} else {
return null;
}
} else {
currentResource = targetObject.eResource();
}
final Resource sourceResource = sourceObject.eResource();
final MatchResource matchCurrent = getMatchResource(match.getComparison(), currentResource);
final Resource currentFromSourceSide;
if (rightToLeft) {
currentFromSourceSide = matchCurrent.getRight();
} else {
currentFromSourceSide = matchCurrent.getLeft();
}
// Case of control/uncontrol
final URI relativeTargetURI = sourceResource.getURI().deresolve(currentFromSourceSide.getURI());
return relativeTargetURI.resolve(currentResource.getURI());
}
/**
* Returns the MatchResource corresponding to the given <code>resource</code>.
*
* @param comparison
* The current comparison.
* @param resource
* Resource for which we need a MatchResource.
* @return The MatchResource corresponding to the given <code>resource</code>.
*/
protected MatchResource getMatchResource(Comparison comparison, Resource resource) {
final List<MatchResource> matchedResources = comparison.getMatchedResources();
final int size = matchedResources.size();
MatchResource soughtMatch = null;
for (int i = 0; i < size && soughtMatch == null; i++) {
final MatchResource matchRes = matchedResources.get(i);
if (matchRes.getRight() == resource || matchRes.getLeft() == resource
|| matchRes.getOrigin() == resource) {
soughtMatch = matchRes;
}
}
if (soughtMatch == null) {
// This should never happen
throw new RuntimeException(EMFCompareMessages.getString(
"ResourceAttachmentChangeSpec.MissingMatch", resource.getURI().lastSegment())); //$NON-NLS-1$
}
return soughtMatch;
}
/**
* 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 root from 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 root from the right).</li>
* </ul>
* </li>
* </ul>
* </p>
*
* @param diff
* The difference we are to merge.
* @param rightToLeft
* Tells us whether we are to add an object on the left or right side.
*/
protected void removeFromTarget(ResourceAttachmentChange diff, boolean rightToLeft) {
final Match valueMatch = diff.getMatch();
final EObject expectedValue;
if (rightToLeft) {
expectedValue = valueMatch.getLeft();
} else {
expectedValue = valueMatch.getRight();
}
// if this is a pseudo conflict, we have no value to remove
if (expectedValue != null) {
// We only wish to remove the element from its containing resource, not from its container.
// This will not affect the match.
final Resource resource = ((InternalEObject)expectedValue).eDirectResource();
resource.getContents().remove(expectedValue);
}
}
/**
* 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 source
* The List from which one element has to be added to the {@code target} list.
* @param target
* The List into which one element from {@code source} has to be added.
* @param newElement
* The element from {@code source} that needs to be added into {@code target}.
* @param <E>
* Type of the sequences content.
* @return The index at which the new value should be inserted into the 'target' list, as inferred from
* its position in {@code source}. {@code -1} if the value should be inserted at the end of its
* target list.
* @see DiffUtil#findInsertionIndex(Comparison, Diff, boolean)
*/
protected <E> int findInsertionIndex(Comparison comparison, List<E> source, List<E> target, E newElement) {
return DiffUtil.findInsertionIndex(comparison, source, target, newElement);
}
}