blob: 9f7ffc37c62bda5862b97e9cbacf56bde5a214fb [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2012, 2016 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
* Stefan Dirix - bugs 441172, 452147 and 454579
* Alexandra Buzila - Fixes for Bug 446252
* Martin Fleck - bug 507177
*******************************************************************************/
package org.eclipse.emf.compare.merge;
import static com.google.common.base.Predicates.not;
import static com.google.common.base.Predicates.or;
import static org.eclipse.emf.compare.ConflictKind.PSEUDO;
import static org.eclipse.emf.compare.DifferenceSource.RIGHT;
import static org.eclipse.emf.compare.DifferenceState.DISCARDED;
import static org.eclipse.emf.compare.DifferenceState.MERGED;
import static org.eclipse.emf.compare.DifferenceState.MERGING;
import static org.eclipse.emf.compare.DifferenceState.UNRESOLVED;
import static org.eclipse.emf.compare.merge.IMergeCriterion.NONE;
import static org.eclipse.emf.compare.utils.EMFComparePredicates.hasConflict;
import static org.eclipse.emf.compare.utils.EMFComparePredicates.hasSameReferenceAs;
import static org.eclipse.emf.compare.utils.EMFComparePredicates.isDiffOnEOppositeOf;
import static org.eclipse.emf.compare.utils.EMFComparePredicates.sameSideAs;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Collections2;
import com.google.common.collect.Iterables;
import com.google.common.collect.Iterators;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import java.lang.ref.WeakReference;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.common.util.Monitor;
import org.eclipse.emf.compare.Comparison;
import org.eclipse.emf.compare.Conflict;
import org.eclipse.emf.compare.ConflictKind;
import org.eclipse.emf.compare.Diff;
import org.eclipse.emf.compare.DifferenceKind;
import org.eclipse.emf.compare.DifferenceSource;
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.utils.EMFCompareCopier;
import org.eclipse.emf.compare.utils.EMFComparePredicates;
import org.eclipse.emf.compare.utils.ReferenceUtil;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EReference;
import org.eclipse.emf.ecore.util.EObjectEList;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.emf.ecore.util.FeatureMap;
import org.eclipse.emf.ecore.util.InternalEList;
/**
* Abstract implementation of an {@link IMerger}. This can be used as a base implementation to avoid
* re-implementing the whole contract.
*
* @author <a href="mailto:laurent.goubet@obeo.fr">Laurent Goubet</a>
* @since 3.0
*/
public abstract class AbstractMerger implements IMerger2, IMergeOptionAware, IMergeCriterionAware {
/** The key of the merge option that allows to the mergers to consider sub-diffs of a diff as a whole. */
public static final String SUB_DIFF_AWARE_OPTION = "subDiffAwareOption"; //$NON-NLS-1$
/** A predicate for diffs with an unresolved state. */
private static final Predicate<? super Diff> HAS_UNRESOLVED_STATE = EMFComparePredicates
.hasState(UNRESOLVED);
/**
* The cached result for
* {@link #getMergerDelegate(Diff, org.eclipse.emf.compare.merge.IMerger.Registry2, IMergeCriterion)}.
*/
private static WeakReference<CachedMergerDelegate> cachedMergerDelegateReference;
/**
* The map of all merge options that this merger should be aware of.
*
* @since 3.4
*/
protected Map<Object, Object> mergeOptions;
/** Ranking of this merger. */
private int ranking;
/** Registry from which this merger has been created. */
private Registry2 registry;
/**
* Default constructor.
*/
public AbstractMerger() {
this.mergeOptions = Maps.newHashMap();
}
/**
* {@inheritDoc}
*
* @see org.eclipse.emf.compare.merge.IMerger#getRanking()
*/
public int getRanking() {
return ranking;
}
/**
* {@inheritDoc}
*
* @see org.eclipse.emf.compare.merge.IMerger#setRanking(int)
*/
public void setRanking(int r) {
ranking = r;
}
/**
* {@inheritDoc}
*
* @see org.eclipse.emf.compare.merge.IMerger#getRegistry()
*/
public Registry getRegistry() {
return registry;
}
/**
* {@inheritDoc}
*
* @see org.eclipse.emf.compare.merge.IMerger#setRegistry(org.eclipse.emf.compare.merge.IMerger.Registry)
*/
public void setRegistry(Registry registry) {
if (this.registry != null && registry != null) {
throw new IllegalStateException("The registry has to be set only once."); //$NON-NLS-1$
}
if (registry != null && !(registry instanceof Registry2)) {
throw new IllegalArgumentException("The registry must implement Registry2"); //$NON-NLS-1$
}
this.registry = (Registry2)registry;
}
/**
* Default implementation of apply for mergers that extends this class. Will accept <code>null</code> or
* AdditiveMergeCriterion.INSTANCE.
*
* @param criterion
* The merge criterion
* @return <code>true</code> if the given criterion is null or is AdditiveMergeCriterion.INSTANCE.
* @since 3.4
*/
public boolean apply(IMergeCriterion criterion) {
return criterion == null || criterion == NONE || criterion == AdditiveMergeCriterion.INSTANCE;
}
/**
* {@inheritDoc}
*
* @see org.eclipse.emf.compare.merge.IMergeOptionAware#getMergeOptions()
*/
public Map<Object, Object> getMergeOptions() {
return this.mergeOptions;
}
/**
* {@inheritDoc}
*
* @see org.eclipse.emf.compare.merge.IMergeOptionAware#setMergeOptions(java.util.Map)
* @since 3.3
*/
public void setMergeOptions(Map<Object, Object> options) {
this.mergeOptions = options;
}
/**
* Check the SUB_DIFF_AWARE_OPTION state.
*
* @return true if the SUB_DIFF_AWARE_OPTION of the merge options is set to true, false otherwise.
*/
private boolean isHandleSubDiffs() {
if (this.mergeOptions != null) {
Object subDiffs = this.mergeOptions.get(SUB_DIFF_AWARE_OPTION);
return subDiffs == Boolean.TRUE;
}
return false;
}
/**
* {@inheritDoc}
*
* @since 3.2
*/
public Set<Diff> getDirectMergeDependencies(Diff diff, boolean mergeRightToLeft) {
final Set<Diff> dependencies = new LazyLinkedHashSet<Diff>();
if (isAccepting(diff, mergeRightToLeft)) {
dependencies.addAll(diff.getRequires());
dependencies.addAll(diff.getImpliedBy());
} else {
dependencies.addAll(diff.getImplies());
dependencies.addAll(diff.getRequiredBy());
}
dependencies.addAll(diff.getRefinedBy());
if (diff.getEquivalence() != null) {
final Diff masterEquivalence = findMasterEquivalence(diff, mergeRightToLeft);
if (masterEquivalence != null && masterEquivalence != diff) {
dependencies.add(masterEquivalence);
}
}
return dependencies;
}
/**
* {@inheritDoc}
*
* @since 3.2
*/
public Set<Diff> getDirectResultingMerges(Diff target, boolean mergeRightToLeft) {
final Set<Diff> resulting = new LazyLinkedHashSet<Diff>();
resulting.addAll(getImpliedMerges(target, mergeRightToLeft));
resulting.addAll(getLogicallyResultingMerges(target, mergeRightToLeft));
return resulting;
}
/**
* Interlocked differences only occur in special cases: When both ends of a one-to-one feature have the
* same type and are actually set to the container object in an instance model.
* <p>
* For each end of the feature usually two differences are determined: Setting the feature in object A and
* in object B. Each pair of differences is equivalent. But when the value of the feature is set to its
* containing object, those differences may ALL act as equivalent depending on the merge direction.
* <p>
* These interlocked differences are therefore indirectly equivalent and need special treatment to avoid
* merging the same effects twice. These differences are determined by this method.
*
* @see <a href="https://bugs.eclipse.org/bugs/show_bug.cgi?id=452147">Bugzilla #452147</a> for more
* information.
* @param referenceChange
* The diff for which interlocked differences are determined.
* @param mergeRightToLeft
* The direction in which we're considering a merge.
* @return All interlocked differences in regards to the given {@code referenceChange} and
* {@code mergeDirection}.
*/
private Collection<? extends Diff> findInterlockedOneToOneDiffs(ReferenceChange referenceChange,
boolean mergeRightToLeft) {
final boolean sanityChecks = referenceChange.getKind() != DifferenceKind.CHANGE
|| referenceChange.getReference().isMany()
|| referenceChange.getReference().getEOpposite().isMany();
// check if value to be set is the container itself
final EObject sourceContainer = ComparisonUtil.getExpectedSide(referenceChange.getMatch(),
referenceChange.getSource(), mergeRightToLeft);
if (!sanityChecks && sourceContainer != null) {
final Object sourceValue = ReferenceUtil.safeEGet(sourceContainer,
referenceChange.getReference());
if (sourceValue == sourceContainer) {
// collect all diffs which might be "equal"
final Match match = referenceChange.getMatch();
final Set<Diff> candidates = new LinkedHashSet<Diff>();
for (Diff diff : match.getDifferences()) {
candidates.add(diff);
if (diff.getEquivalence() != null) {
candidates.addAll(diff.getEquivalence().getDifferences());
}
}
// special case - check for interlocked diffs and return them as result
return filterInterlockedOneToOneDiffs(candidates, referenceChange, mergeRightToLeft);
}
}
return Collections.emptyList();
}
/**
* Checks for interlocked differences from a list of candidates. See
* {@link #findInterlockedOneToOneDiffs(ReferenceChange, boolean)} for more information.
*
* @param diffsToCheck
* The differences to be checked for indirect equivalence.
* @param referenceChange
* The diff to which the determined differences are indirectly equivalent.
* @param mergeRightToLeft
* The direction in which we're considering a merge.
* @return All differences (and their equivalents) from {@code diffsToCheck} which are indirectly
* equivalent to {@code referenceChange}. Does not modify the given collection.
*/
private Collection<? extends Diff> filterInterlockedOneToOneDiffs(Collection<? extends Diff> diffsToCheck,
ReferenceChange referenceChange, boolean mergeRightToLeft) {
final Object sourceContainer = ComparisonUtil.getExpectedSide(referenceChange.getMatch(),
referenceChange.getSource(), mergeRightToLeft);
final EReference sourceReference = referenceChange.getReference();
final Set<Diff> result = new LinkedHashSet<Diff>();
for (Diff candidate : diffsToCheck) {
if (candidate instanceof ReferenceChange) {
// check if container & reference(-opposite) are the same as from the given referenceChange
final Object candidateContainer = ComparisonUtil.getExpectedSide(candidate.getMatch(),
candidate.getSource(), mergeRightToLeft);
final EReference candidateReference = ((ReferenceChange)candidate).getReference();
if (sourceContainer == candidateContainer
&& sourceReference.getEOpposite() == candidateReference) {
result.add(candidate);
if (candidate.getEquivalence() != null) {
result.addAll(candidate.getEquivalence().getDifferences());
}
}
}
}
return result;
}
/**
* {@inheritDoc}
*
* @since 3.2
*/
public Set<Diff> getDirectResultingRejections(Diff target, boolean rightToLeft) {
final Set<Diff> directlyImpliedRejections = new LazyLinkedHashSet<Diff>();
final Conflict conflict = target.getConflict();
if (conflict != null && conflict.getKind() == ConflictKind.REAL) {
if (isAccepting(target, rightToLeft)) {
directlyImpliedRejections.addAll(conflict.getDifferences());
return Sets.filter(directlyImpliedRejections, not(sameSideAs(target)));
}
}
return directlyImpliedRejections;
}
/**
* Even within 'equivalent' differences, there might be one that we need to consider as the "master", one
* part of the equivalence that should take precedence over the others when merging.
* <p>
* There are four main cases in which this happens :
* <ol>
* <li>Equivalent differences regarding two "eOpposite" sides, with one side being a single-valued
* reference while the other side is a multi-valued reference (one-to-many). In such a case, we need the
* 'many' side of that equivalence to be merged over the 'single' side, so as to avoid potential ordering
* issues. Additionally, to avoid losing information, equivalent differences with
* {@link DifferenceKind.ADD} instead of {@link DifferenceKind.REMOVE} must be merged first.</li>
* <li>Equivalent differences regarding two "eOpposite" sides, with both sides being a single-valued
* reference (one-to-one). In such a case, we need to merge the difference that results in setting a
* feature value over the difference unsetting a feature. This is needed to prevent information loss.</li>
* <li>Equivalent differences with conflicts: basically, if one of the diffs of an equivalence relation is
* in conflict while the others are not, then none of the equivalent differences can be automatically
* merged. We need to consider the conflict to be taking precedence over the others to make sure that the
* conflict is resolved before even trying to merge anything.</li>
* <li>Equivalent {@link ReferenceChange} and {@link FeatureMapChange} differences: in this case the
* {@link FeatureMapChange} difference will take precedence over the {@link ReferenceChange} when the the
* resulting operation actively modifies a FeatureMap. The {@link ReferenceChange} will take precedence
* when a FeatureMap is only modified implicitly. This happens in order to prevent special cases in which
* the {@link ReferenceChangeMerger} cannot ensure the correct order of the feature map attribute.</li>
* </ol>
* </p>
*
* @param diff
* The diff we need to check the equivalence for a 'master' difference.
* @param mergeRightToLeft
* Direction of the merge operation.
* @return The master difference of this equivalence relation. May be <code>null</code> if there are none.
*/
private Diff findMasterEquivalence(Diff diff, boolean mergeRightToLeft) {
final List<Diff> equivalentDiffs = diff.getEquivalence().getDifferences();
final Optional<Diff> firstConflicting = Iterables.tryFind(equivalentDiffs,
hasConflict(ConflictKind.REAL));
final Diff idealMasterDiff;
if (diff instanceof ReferenceChange) {
final ReferenceChange referenceChange = (ReferenceChange)diff;
idealMasterDiff = getMasterEquivalenceForReferenceChange(referenceChange, mergeRightToLeft);
} else if (diff instanceof FeatureMapChange) {
final FeatureMapChange featureMapChange = (FeatureMapChange)diff;
idealMasterDiff = getMasterEquivalenceForFeatureMapChange(featureMapChange, mergeRightToLeft);
} else {
idealMasterDiff = null;
}
final Diff masterDiff;
// conflicting equivalents take precedence over the ideal master equivalence
if (firstConflicting.isPresent() && !hasRealConflict(idealMasterDiff)) {
if (hasRealConflict(diff)) {
masterDiff = null;
} else {
masterDiff = firstConflicting.get();
}
} else {
masterDiff = idealMasterDiff;
}
return masterDiff;
}
/**
* Determines if the given {@link Diff} has a conflict of kind {@link ConflictKind#REAL}.
*
* @param diff
* The {@link Diff} to check.
* @return {@code true} if the diff exists and has a conflict of kind {@link ConflictKind#REAL},
* {@code false} otherwise.
* @since 3.4
*/
private boolean hasRealConflict(Diff diff) {
return diff != null && diff.getConflict() != null
&& diff.getConflict().getKind() == ConflictKind.REAL;
}
/**
* Returns the master equivalence for a {@link FeatureMapChange}.
*
* @see AbstractMerger#findMasterEquivalence(Diff, boolean)
* @param diff
* The {@link Diff} we need to check the equivalence for a 'master' difference.
* @param mergeRightToLeft
* Direction of the current merging.
* @return The master difference of {@code diff} and its equivalent diffs. This method may return
* <code>null</code> if there is no master diff.
*/
private Diff getMasterEquivalenceForFeatureMapChange(FeatureMapChange diff, boolean mergeRightToLeft) {
if (diff.getKind() == DifferenceKind.MOVE) {
final Comparison comparison = diff.getMatch().getComparison();
final FeatureMap.Entry entry = (FeatureMap.Entry)diff.getValue();
if (entry.getValue() instanceof EObject) {
final Match valueMatch = comparison.getMatch((EObject)entry.getValue());
final EObject expectedValue = ComparisonUtil.getExpectedSide(valueMatch, diff.getSource(),
mergeRightToLeft);
// Try to find the ReferenceChange-MasterEquivalence when the expected value will not be
// contained in a FeatureMap
if (!ComparisonUtil.isContainedInFeatureMap(expectedValue)) {
return Iterators.tryFind(diff.getEquivalence().getDifferences().iterator(),
Predicates.instanceOf(ReferenceChange.class)).orNull();
}
}
}
return null;
}
/**
* Returns the master equivalence for a {@link ReferenceChange}.
*
* @see AbstractMerger#findMasterEquivalence(Diff, boolean)
* @param diff
* The {@link Diff} we need to check the equivalence for a 'master' difference.
* @param mergeRightToLeft
* Direction of the current merging.
* @return The master difference of {@code diff} and its equivalent diffs. This method may return
* <code>null</code> if there is no master diff.
*/
private Diff getMasterEquivalenceForReferenceChange(ReferenceChange diff, boolean mergeRightToLeft) {
Diff masterDiff = getMasterEquivalenceOnReference(diff, mergeRightToLeft);
if (masterDiff == null) {
masterDiff = getMasterEquivalenceOnFeatureMap(diff, mergeRightToLeft);
}
return masterDiff;
}
/**
* Returns the master equivalence for a {@link ReferenceChange} from among its equivalents with the same
* or {@code eOpposite} reference.
*
* @see AbstractMerger#findMasterEquivalence(Diff, boolean)
* @param diff
* The {@link Diff} we need to check the equivalence for a 'master' difference.
* @param mergeRightToLeft
* Direction of the current merging.
* @return The master difference of {@code diff} and its equivalent diffs. This method may return
* <code>null</code> if there is no master diff.
*/
private Diff getMasterEquivalenceOnReference(ReferenceChange diff, final boolean mergeRightToLeft) {
Diff masterDiff = null;
/*
* For the following, we'll only consider diffs that are either on the same reference as "diff", or on
* its eopposite.
*/
final Predicate<Diff> candidateFilter = or(isDiffOnEOppositeOf(diff), hasSameReferenceAs(diff));
final List<Diff> equivalentDiffs = diff.getEquivalence().getDifferences();
// We need to lookup the first multi-valued addition
final Optional<Diff> multiValuedAddition = Iterators.tryFind(
Iterators.filter(equivalentDiffs.iterator(), candidateFilter), new Predicate<Diff>() {
public boolean apply(Diff input) {
return input instanceof ReferenceChange
&& ((ReferenceChange)input).getReference().isMany()
&& isAdd((ReferenceChange)input, mergeRightToLeft);
}
});
final Iterator<Diff> candidateDiffs = Iterators.filter(equivalentDiffs.iterator(), candidateFilter);
if (multiValuedAddition.isPresent()) {
// We have at least one multi-valued addition. It will take precedence if there is any
// single-valued reference change or multi-valued deletion
while (masterDiff == null && candidateDiffs.hasNext()) {
final ReferenceChange next = (ReferenceChange)candidateDiffs.next();
if (!next.getReference().isMany() || !isAdd(next, mergeRightToLeft)) {
masterDiff = multiValuedAddition.get();
}
}
} else {
// The only diff that could take precedence is a single-valued set, _if_ there is any multi-valued
// deletion or single-valued unset in the list.
ReferenceChange candidate = null;
if (candidateDiffs.hasNext()) {
candidate = (ReferenceChange)candidateDiffs.next();
}
while (masterDiff == null && candidateDiffs.hasNext()) {
assert candidate != null;
final ReferenceChange next = (ReferenceChange)candidateDiffs.next();
if (candidate.getReference().isMany() || isUnset(candidate, mergeRightToLeft)) {
// candidate is a multi-valued deletion or an unset. Do we have a single-valued set in the
// list?
if (!next.getReference().isMany() && isSet(next, mergeRightToLeft)) {
masterDiff = next;
}
} else if (isSet(candidate, mergeRightToLeft)) {
// candidate is a set. Is it our master diff?
if (next.getReference().isMany() || isUnset(next, mergeRightToLeft)) {
masterDiff = candidate;
}
} else {
// candidate is a change on a single-valued reference. This has no influence over the
// 'master' lookup. Go on to the next.
candidate = next;
}
}
}
return masterDiff;
}
/**
* Returns the master equivalence of type {@link FeatureMapChange}, for a {@link ReferenceChange}.
*
* @see AbstractMerger#findMasterEquivalence(Diff, boolean)
* @param diff
* The {@link Diff} we need to check the equivalence for a 'master' difference.
* @param mergeRightToLeft
* Direction of the current merging.
* @return The master difference of {@code diff} and its equivalent diffs. This method may return
* <code>null</code> if there is no master diff.
*/
private Diff getMasterEquivalenceOnFeatureMap(ReferenceChange diff, boolean mergeRightToLeft) {
if (diff.getKind() == DifferenceKind.MOVE) {
Comparison comparison = diff.getMatch().getComparison();
Match valueMatch = comparison.getMatch(diff.getValue());
EObject sourceValue = ComparisonUtil.getExpectedSide(valueMatch, diff.getSource(),
mergeRightToLeft);
// No FeatureMap-MasterEquivalence when the resulting destination is not a FeatureMap
if (!ComparisonUtil.isContainedInFeatureMap(sourceValue)) {
return null;
}
}
return Iterators.tryFind(diff.getEquivalence().getDifferences().iterator(),
Predicates.instanceOf(FeatureMapChange.class)).orNull();
}
/**
* Checks whether the given {@code diff} is of kind {@link DifferenceKind#CHANGE} and its reference is
* one-to-one.
*
* @param diff
* The ReferenceChange to check.
* @return {@code true} if the given {@code diff} is of kind {@link DifferenceKind#CHANGE} and describes a
* one-to-one reference, {@code false} otherwise.
*/
private boolean isOneToOneAndChange(ReferenceChange diff) {
if (diff.getKind() == DifferenceKind.CHANGE) {
EReference reference = diff.getReference();
if (reference != null && !reference.isMany()) {
EReference eOpposite = reference.getEOpposite();
return eOpposite != null && !eOpposite.isMany();
}
}
return false;
}
/**
* Returns a set of differences that should be logically merged with the given diff. As opposed to
* {@link #getDirectMergeDependencies(Diff, boolean) merge dependencies}, it is not structurally necessary
* to merge these diffs before the given diff. This may include the diff's {@link Diff#getImpliedBy()
* implications}, the diff's {@link Diff#getRefines() refinement} or any other diff that should be
* logically merged with the given diff.
*
* @param diff
* The difference we're considering merging.
* @param mergeRightToLeft
* The direction in which we're considering a merge.
* @return The Set of all differences that will be merged because we've merged <code>diff</code>.
* @since 3.5
*/
protected Set<Diff> getLogicallyResultingMerges(Diff diff, boolean mergeRightToLeft) {
final Set<Diff> resulting = new LazyLinkedHashSet<Diff>();
// we need to add the implication side that actually needs merging
if (isAccepting(diff, mergeRightToLeft)) {
resulting.addAll(diff.getImpliedBy());
} else {
resulting.addAll(diff.getImplies());
}
// refinements should only be merged together
resulting.addAll(diff.getRefines());
// any logically linked sub diffs should be merged as well
if (isHandleSubDiffs()) {
final Iterable<Diff> directSubDiffs = ComparisonUtil.getDirectSubDiffs(!mergeRightToLeft)
.apply(diff);
Iterables.addAll(resulting, directSubDiffs);
}
return resulting;
}
/**
* Returns all differences that are automatically set to the targets {@link Diff#getState() state} if the
* given target diff is merged. This may include the diff's {@link Diff#getImplies() implications}, the
* diff's {@link Diff#getEquivalence() equivalences} or any other diff that requires no merging by itself.
*
* @param target
* The difference we're considering merging.
* @param mergeRightToLeft
* The direction in which we're considering a merge.
* @return The Set of all differences that will be set to <code>MERGED</code> because we've merged
* <code>target</code>.
* @since 3.5
*/
protected Set<Diff> getImpliedMerges(Diff target, boolean mergeRightToLeft) {
final Set<Diff> impliedMerges = new LazyLinkedHashSet<Diff>();
if (isAccepting(target, mergeRightToLeft)) {
impliedMerges.addAll(target.getImplies());
} else {
impliedMerges.addAll(target.getImpliedBy());
}
if (target.getEquivalence() != null) {
impliedMerges.addAll(target.getEquivalence().getDifferences());
impliedMerges.remove(target);
}
if (target.getConflict() != null && target.getConflict().getKind() == PSEUDO) {
// Only diffs _from the same side_ are implied!
target.getConflict().getDifferences().stream()
.filter(d -> d != target && d.getSource() == target.getSource())
.forEach(impliedMerges::add);
}
// If a diff refines another, we have to check if the "macro" diff has to be merged with it. It is the
// case when the unresolved diffs that refine the "macro" diff are all contained by the set
// (target + resulting) (see https://bugs.eclipse.org/bugs/show_bug.cgi?id=458961)
EList<Diff> refines = target.getRefines();
if (!refines.isEmpty()) {
Set<Diff> impliedMergesWithTarget = Sets.newHashSet(impliedMerges);
impliedMergesWithTarget.add(target);
for (Diff refine : refines) {
Collection<Diff> unresolvedRefinedDiffs = Collections2.filter(refine.getRefinedBy(),
HAS_UNRESOLVED_STATE);
if (impliedMergesWithTarget.containsAll(unresolvedRefinedDiffs)) {
if (impliedMerges.add(refine)) {
impliedMergesWithTarget.add(refine);
}
}
}
}
// Bug 452147:
// Add interlocked differences to the resulting merges to avoid merging redundant differences with
// undefined consequences.
if (target instanceof ReferenceChange) {
final ReferenceChange refTarget = (ReferenceChange)target;
if (isOneToOneAndChange(refTarget)) {
impliedMerges.addAll(findInterlockedOneToOneDiffs(refTarget, mergeRightToLeft));
}
}
return impliedMerges;
}
/**
* Executes a copy in the given merge direction. This method is a generalization of
* {@link #copyLeftToRight(Diff, Monitor)} and {@link #copyRightToLeft(Diff, Monitor)}.
*
* @param target
* The difference to handle.
* @param monitor
* Monitor.
* @param rightToLeft
* Merge direction.
* @since 3.5
*/
protected void copyDiff(Diff target, Monitor monitor, boolean rightToLeft) {
if (isInTerminalState(target)) {
return;
}
// Change the diff's state before we actually merge it : this allows us to avoid requirement cycles.
target.setState(MERGING);
// Mark all implied diffs as merged before actually merging this one
Set<Diff> impliedMerges = getImpliedMerges(target, rightToLeft);
while (!impliedMerges.isEmpty()) {
Diff impliedMerge = impliedMerges.iterator().next();
// avoid implication circles
if (impliedMerge != target && !isInTerminalState(impliedMerge)) {
if (isAccepting(impliedMerge, rightToLeft)) {
impliedMerge.setState(MERGED);
} else {
impliedMerge.setState(DISCARDED);
}
impliedMerges.addAll(getImpliedMerges(impliedMerge, rightToLeft));
}
impliedMerges.remove(impliedMerge);
impliedMerges.remove(target);
}
if (isAccepting(target, rightToLeft)) {
accept(target, rightToLeft);
target.setState(MERGED);
} else {
reject(target, rightToLeft);
target.setState(DISCARDED);
}
}
/**
* {@inheritDoc}
*
* @see org.eclipse.emf.compare.merge.IMerger#copyLeftToRight(org.eclipse.emf.compare.Diff,
* org.eclipse.emf.common.util.Monitor)
* @since 3.1
*/
public void copyLeftToRight(Diff target, Monitor monitor) {
copyDiff(target, monitor, false);
}
/**
* {@inheritDoc}
*
* @see org.eclipse.emf.compare.merge.IMerger#copyRightToLeft(org.eclipse.emf.compare.Diff,
* org.eclipse.emf.common.util.Monitor)
* @since 3.1
*/
public void copyRightToLeft(Diff target, Monitor monitor) {
copyDiff(target, monitor, true);
}
/**
* Accept the given difference. This may be overridden by clients.
*
* @param diff
* the difference to merge
* @param rightToLeft
* the direction of the merge
* @since 3.1
*/
protected void accept(final Diff diff, boolean rightToLeft) {
// Empty default implementation
}
/**
* Reject the given difference. This may be overridden by clients.
*
* @param diff
* the difference to merge
* @param rightToLeft
* the direction of the merge
* @since 3.1
*/
protected void reject(final Diff diff, boolean rightToLeft) {
// Empty default implementation
}
/**
* This can be used by mergers to merge another (required, equivalent...) difference using the right
* merger for that diff.
*
* @param diff
* The diff we need to merge.
* @param rightToLeft
* Direction of that merge.
* @param monitor
* The monitor we should use to report progress.
*/
protected void mergeDiff(Diff diff, boolean rightToLeft, Monitor monitor) {
final DelegatingMerger delegate = getMergerDelegate(diff);
if (rightToLeft) {
delegate.copyRightToLeft(diff, monitor);
} else {
delegate.copyLeftToRight(diff, monitor);
}
}
/**
* Find the best merger for diff and wrap it in a delegate that will take the current merge criterion into
* account. The current merge criterion should be stored in the merger's mergeOptions map using
* IMergeCriterion.OPTION_MERGE_CRITERION as a key.
*
* @param diff
* The diff
* @return the best merger to use for merging the diff
* @since 3.4
*/
protected DelegatingMerger getMergerDelegate(Diff diff) {
IMergeCriterion criterion = (IMergeCriterion)getMergeOptions()
.get(IMergeCriterion.OPTION_MERGE_CRITERION);
return getMergerDelegate(diff, (Registry2)getRegistry(), criterion);
}
/**
* Find the best merger for diff and wrap it in a delegate that will take the given merge criterion into
* account. This is NOT Thread-safe!
*
* @param diff
* The diff
* @param registry
* The registry of mergers where to look for mergers
* @param criterion
* The merge criterion to use
* @return The best merger for diff and criterion, wrapped in a delegate to deal with setting/restoring
* the criterion in the merger used.
* @since 3.4
*/
public static DelegatingMerger getMergerDelegate(Diff diff, Registry2 registry,
IMergeCriterion criterion) {
// First check if we have a cached result...
if (cachedMergerDelegateReference != null) {
CachedMergerDelegate cachedMergerDelegate = cachedMergerDelegateReference.get();
if (cachedMergerDelegate != null && cachedMergerDelegate.matches(diff, registry, criterion)) {
return cachedMergerDelegate.delegatingMerger;
}
}
Iterator<IMerger> it = registry.getMergersByRankDescending(diff, criterion);
if (!it.hasNext()) {
throw new IllegalStateException("No merger found for diff " + diff.getClass().getSimpleName()); //$NON-NLS-1$
}
IMerger merger = it.next();
final DelegatingMerger delegatingMerger = new DelegatingMerger(merger, criterion);
// Cache the result.
cachedMergerDelegateReference = new WeakReference<AbstractMerger.CachedMergerDelegate>(
new CachedMergerDelegate(diff, registry, criterion, delegatingMerger));
return delegatingMerger;
}
/**
* Returns whether the given difference is in a terminal state or not. Differences that are in a terminal
* state, i.e., either MERGED or DISCARDED, do not need to be handled by the merger.
*
* @param target
* difference
* @return true if the target should be merged, false otherwise.
* @since 3.5
*/
public static boolean isInTerminalState(Diff target) {
switch (target.getState()) {
case MERGED:
case DISCARDED:
return true;
default:
return false;
}
}
/**
* Specifies whether the given {@code diff} will add a value in the target model for the current merging.
* <p>
* To check whether the {@code diff} is an addition, we have to check the direction of the merge,
* specified in {@code rightToLeft} and the {@link Diff#getSource() source of the diff}. Therefore, this
* method delegates to {@link #isLeftAddOrRightDelete(ReferenceChange)} and
* {@link #isLeftDeleteOrRightAdd(ReferenceChange)}.
* </p>
*
* @param diff
* The difference to check.
* @param rightToLeft
* Direction of the merge.
* @return <code>true</code> if {@code diff} will add a value with this merge, <code>false</code>
* otherwise.
* @since 3.2
*/
protected boolean isAdd(ReferenceChange diff, boolean rightToLeft) {
if (rightToLeft) {
return isLeftDeleteOrRightAdd(diff);
} else {
return isLeftAddOrRightDelete(diff);
}
}
/**
* Specifies whether the given {@code diff} is either an addition on the left-hand side or a deletion on
* the right-hand side.
*
* @param diff
* The difference to check.
* @return <code>true</code> if it is a left addition or a right deletion.
*/
private boolean isLeftAddOrRightDelete(ReferenceChange diff) {
if (diff.getSource() == DifferenceSource.LEFT) {
return diff.getKind() == DifferenceKind.ADD;
} else {
return diff.getKind() == DifferenceKind.DELETE;
}
}
/**
* Specifies whether the given {@code diff} is either a deletion on the left-hand side or an addition on
* the right-hand side.
*
* @param diff
* The difference to check.
* @return <code>true</code> if it is a left deletion or a right addition.
*/
private boolean isLeftDeleteOrRightAdd(ReferenceChange diff) {
if (diff.getSource() == DifferenceSource.LEFT) {
return diff.getKind() == DifferenceKind.DELETE;
} else {
return diff.getKind() == DifferenceKind.ADD;
}
}
/**
* Checks whether the given diff will result in the unsetting of a reference in the given merge direction.
*
* @param diff
* The difference to check.
* @param mergeRightToLeft
* Direction of the merge.
* @return <code>true</code> if {@code diff} will unset a value with this merge, <code>false</code> if
* this will either "set" or "change" values... or if the given diff is affecting a multi-valued
* reference.
*/
protected boolean isUnset(ReferenceChange diff, boolean mergeRightToLeft) {
if (diff.getKind() != DifferenceKind.CHANGE) {
return false;
}
boolean isUnset = false;
final Match match = diff.getMatch();
final EObject container;
if (diff.getSource() == DifferenceSource.LEFT) {
container = match.getLeft();
} else {
container = match.getRight();
}
if (container == null) {
// This is an unset diff. However, if we're merging towards the source, we're actually "rejecting"
// the unset, and the merge operation will be a "set"
isUnset = isAccepting(diff, mergeRightToLeft);
} else {
if (!ReferenceUtil.safeEIsSet(container, diff.getReference())) {
// No value on the source side, this is an unset
// Same case as above, if we are rejecting the diff, it is a "set" operation
isUnset = isAccepting(diff, mergeRightToLeft);
} else {
// The feature is set on the source side. If we're merging towards the other side, this cannot
// be an unset.
// Otherwise we're going to reset this reference to its previous value. That will end as an
// "unset" if the "previous value" is unset itself.
if (isRejecting(diff, mergeRightToLeft)) {
final EObject originContainer;
if (match.getComparison().isThreeWay()) {
originContainer = match.getOrigin();
} else if (mergeRightToLeft) {
originContainer = match.getRight();
} else {
originContainer = match.getLeft();
}
isUnset = originContainer == null
|| !ReferenceUtil.safeEIsSet(originContainer, diff.getReference());
}
}
}
return isUnset;
}
/**
* Checks whether the given diff will result in the setting of a reference in the given merge direction.
*
* @param diff
* The difference to check.
* @param mergeRightToLeft
* Direction of the merge.
* @return <code>true</code> if {@code diff} will set a value with this merge, <code>false</code> if this
* will either "unset" or "change" values... or if the given diff is affecting a multi-valued
* reference.
* @since 3.5
*/
protected boolean isSet(ReferenceChange diff, boolean mergeRightToLeft) {
if (diff.getKind() != DifferenceKind.CHANGE) {
return false;
}
boolean isSet = false;
final Match match = diff.getMatch();
final EObject container;
if (diff.getSource() == DifferenceSource.LEFT) {
container = match.getLeft();
} else {
container = match.getRight();
}
if (container == null) {
// This is an unset diff. However, if we're merging towards the source, we're actually "rejecting"
// the unset, and the merge operation will be a "set"
isSet = isRejecting(diff, mergeRightToLeft);
} else {
if (!ReferenceUtil.safeEIsSet(container, diff.getReference())) {
// No value on the source side, this is an unset
// Same case as above, if we are rejecting the diff, it is a "set" operation
isSet = isRejecting(diff, mergeRightToLeft);
} else {
// The feature is set on the source side. If we're merging towards the other side, this is a
// "set" operation if the feature is not set on the target side.
// Otherwise we're going to reset this reference to its previous value. That will end as an
// "unset" if the "previous value" is unset itself.
if (isRejecting(diff, mergeRightToLeft)) {
final EObject originContainer;
if (match.getComparison().isThreeWay()) {
originContainer = match.getOrigin();
} else if (mergeRightToLeft) {
originContainer = match.getRight();
} else {
originContainer = match.getLeft();
}
isSet = originContainer != null
&& ReferenceUtil.safeEIsSet(originContainer, diff.getReference());
} else {
final EObject targetContainer;
if (mergeRightToLeft) {
targetContainer = match.getLeft();
} else {
targetContainer = match.getRight();
}
isSet = targetContainer == null
|| !ReferenceUtil.safeEIsSet(targetContainer, diff.getReference());
}
}
}
return isSet;
}
/**
* Checks whether the given merge direction will result in accepting this difference based on the
* difference's {@link Diff#getSource() source}.
*
* <pre>
* | LEFT | RIGHT
* --------------------+-------+-------
* Merge Left to Right | true | false
* Merge Right to Left | false | true
* </pre>
*
* @param diff
* difference
* @param mergeRightToLeft
* merge direction
* @return true if the merge source direction matches the difference source, false otherwise.
* @see #isRejecting(Diff, boolean)
* @since 3.5
*/
public static boolean isAccepting(Diff diff, boolean mergeRightToLeft) {
if (diff.getSource() == RIGHT) {
return mergeRightToLeft;
} else {
return !mergeRightToLeft;
}
}
/**
* Checks whether the given merge direction will result in rejecting this difference.
*
* @param diff
* The difference we're merging.
* @param mergeRightToLeft
* Direction of the merge operation.
* @return <code>true</code> if we're rejecting this diff.
* @see #isAccepting(Diff, boolean)
*/
private boolean isRejecting(Diff diff, boolean mergeRightToLeft) {
return !isAccepting(diff, mergeRightToLeft);
}
/**
* This will create a copy of the given EObject that can be used as the target of an addition (or the
* reverting of a deletion).
* <p>
* The target will be self-contained and will have no reference towards any other EObject set (neither
* containment nor "classic" references). All of its attributes' values will match the given
* {@code referenceObject}'s.
* </p>
*
* @param referenceObject
* The EObject for which we'll create a copy.
* @return A self-contained copy of {@code referenceObject}.
* @see EMFCompareCopier#copy(EObject)
*/
protected EObject createCopy(EObject referenceObject) {
/*
* We can't simply use EcoreUtil.copy. References will have their own diffs and will thus be merged
* later on.
*/
final EcoreUtil.Copier copier = new EMFCompareCopier();
return copier.copy(referenceObject);
}
/**
* Adds the given {@code value} into the given {@code list} at the given {@code index}. An {@code index}
* lower than zero or greater than the list's size will mean that the value should be appended at the end
* of the list.
*
* @param list
* The list into which {@code value} should be added.
* @param value
* The value we need to add to {@code list}.
* @param <E>
* Type of objects contained in the list.
* @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.
*/
@SuppressWarnings("unchecked")
protected <E> void addAt(List<E> list, E value, int insertionIndex) {
if (list instanceof InternalEList<?>) {
if (insertionIndex < 0 || insertionIndex > list.size()) {
((InternalEList<Object>)list).addUnique(value);
} else {
((InternalEList<Object>)list).addUnique(insertionIndex, value);
}
} else {
if (insertionIndex < 0 || insertionIndex > list.size()) {
list.add(value);
} else {
list.add(insertionIndex, value);
}
}
}
/**
* Cache the result for {@link AbstractMerger#getMergerDelegate(Diff)}.
*/
private static final class CachedMergerDelegate {
/**
* The previously used diff.
*/
private final Diff diff;
/**
* The previously used registry.
*/
private final Registry2 registry;
/**
* The previously used criterion.
*/
private final IMergeCriterion criterion;
/**
* The previous result.
*/
private final DelegatingMerger delegatingMerger;
/**
* Creates an instance for the result.
*
* @param diff
* the diff.
* @param registry
* the registry.
* @param criterion
* the criterion.
* @param delegatingMerger
* the resulting merger.
*/
private CachedMergerDelegate(Diff diff, Registry2 registry, IMergeCriterion criterion,
DelegatingMerger delegatingMerger) {
this.diff = diff;
this.registry = registry;
this.criterion = criterion;
this.delegatingMerger = delegatingMerger;
}
/**
* Returns whether this cached merger delegate matches the given diff, registry, and criteria.
*
* @param otherDiff
* the diff.
* @param otherRegistry
* the registry.
* @param otherCritereon
* the criterion.
* @return whether this cached merger delegate matches the given diff, registry, and criteria.
*/
public boolean matches(Diff otherDiff, Registry2 otherRegistry, IMergeCriterion otherCritereon) {
return this.diff == otherDiff && this.registry == otherRegistry
&& this.criterion == otherCritereon;
}
}
/**
* A lazy linked hash set.
* <p>
* This set implementation specializes {@link #addAll(Collection)} to recognize special types of
* collections which it caches in {@link #delegate}, setting {@link #set} to {@code true}, if the delegate
* is set. Other methods can the delegate to the {@link #delegate}, until it is necessary to
* {@link #collapse()} the delegate creating an actual initialized {@link LinkedHashSet}.
* </p>
*
* @param <E>
* the type the elements of the set.
*/
private static class LazyLinkedHashSet<E extends EObject> extends LinkedHashSet<E> {
/**
* It's unlikely this value will ever be serialized.
*/
private static final long serialVersionUID = 1L;
/**
* The delegate collection that may be lazily {@link #collapse() collapsed} into the base
* {@link LinkedHashSet}.
*/
private transient Collection<? extends E> delegate;
/**
* Whether or not the {@link #delegate} is itself a {@link Set}.
*/
private transient boolean set;
/**
* Creates an empty new instance.
*/
protected LazyLinkedHashSet() {
}
/**
* Adds the {@link #delegate} to the base {@link LinkedHashSet} if it's not {@code null}, and clears
* the {@link #delegate}.
*/
private void collapse() {
if (delegate != null) {
for (E e : delegate) {
super.add(e);
}
delegate = null;
set = false;
}
}
/**
* {@inheritDoc}
* <p>
* This implementation returns an iterator over the {@link #delegate} instead of the
* {@link LinkedHashSet}, if the delegate is not {@code null}. If {@link Iterator#remove()} is called,
* it {@link #collapse() collapses} the delegate, and removes the element from the collapsed
* {@link LinkedHashSet}.
* </p>
*/
@Override
public Iterator<E> iterator() {
if (delegate != null) {
final Iterator<? extends E> iterator;
if (set) {
iterator = delegate.iterator();
} else {
iterator = ((InternalEList<? extends E>)delegate).basicIterator();
}
return new Iterator<E>() {
private E e;
public boolean hasNext() {
return iterator.hasNext();
}
public E next() {
// Remember the element in case remove is called.
e = iterator.next();
return e;
}
public void remove() {
collapse();
LazyLinkedHashSet.super.remove(e);
}
};
} else {
return super.iterator();
}
}
@Override
public int size() {
if (delegate != null) {
return delegate.size();
} else {
return super.size();
}
}
@Override
public boolean isEmpty() {
if (delegate != null) {
return false;
} else {
return super.isEmpty();
}
}
@Override
public boolean contains(Object o) {
if (set) {
return delegate.contains(o);
} else if (delegate != null && delegate.size() < 4) {
return ((InternalEList<?>)delegate).basicContains(o);
} else {
collapse();
return super.contains(o);
}
}
@Override
public boolean containsAll(Collection<?> c) {
if (set) {
return delegate.containsAll(c);
} else if (delegate != null && delegate.size() < 4) {
return ((InternalEList<?>)delegate).basicContainsAll(c);
} else {
collapse();
return super.containsAll(c);
}
}
@Override
public boolean add(E e) {
collapse();
return super.add(e);
}
@Override
public boolean remove(Object o) {
collapse();
return super.remove(o);
}
@Override
public void clear() {
delegate = null;
set = false;
super.clear();
}
@Override
public boolean removeAll(Collection<?> c) {
collapse();
return super.removeAll(c);
}
@Override
public Object[] toArray() {
if (delegate != null) {
return delegate.toArray();
} else {
return super.toArray();
}
}
@Override
public <T> T[] toArray(T[] a) {
if (delegate != null) {
return delegate.toArray(a);
} else {
return super.toArray(a);
}
}
@Override
public boolean addAll(Collection<? extends E> c) {
// If the collection is empty...
if (c.isEmpty()) {
// There is nothing to add.
return false;
} else if (isEmpty()) {
// If this is still empty, we can still lazily consume special types of collections.
if (c instanceof EObjectEList) {
// If the collection is an EObjectEList, then the elements are necessarily unique so the
// collection can be directly used as a set.
delegate = c;
} else if (c instanceof LazyLinkedHashSet<?>) {
// If the collection is a lazy linked hash set that has a delegate...
LazyLinkedHashSet<? extends E> other = (LazyLinkedHashSet<? extends E>)c;
if (other.delegate != null) {
// We can unwrap it and directly use the delegate.
delegate = other.delegate;
set = other.set;
} else {
// Otherwise we can use that set as the delegate.
delegate = c;
set = true;
}
} else if (c instanceof Set) {
// If the collection is a set, we can use that set as the delegate.
delegate = c;
set = true;
} else {
super.addAll(c);
}
return true;
} else {
collapse();
return super.addAll(c);
}
}
@Override
public boolean retainAll(Collection<?> c) {
collapse();
return super.retainAll(c);
}
@Override
public boolean equals(Object o) {
collapse();
return super.equals(o);
}
@Override
public int hashCode() {
if (delegate != null) {
return delegate.hashCode();
} else {
return super.hashCode();
}
}
@Override
public String toString() {
if (delegate != null) {
return delegate.toString();
} else {
return super.toString();
}
}
}
}