| /******************************************************************************* |
| * 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(); |
| } |
| } |
| } |
| } |