/******************************************************************************* * Copyright (c) 2012, 2017 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 - added #getExpectedSide * Philip Langer - added #delete(Diff) *******************************************************************************/ package org.eclipse.emf.compare.internal.utils; import static com.google.common.base.Predicates.instanceOf; import static com.google.common.base.Predicates.not; import static com.google.common.base.Predicates.or; import static com.google.common.collect.Iterables.addAll; import static com.google.common.collect.Iterables.concat; import static com.google.common.collect.Iterables.filter; import static com.google.common.collect.Iterables.getFirst; import static org.eclipse.emf.compare.ConflictKind.REAL; import static org.eclipse.emf.compare.DifferenceKind.ADD; import static org.eclipse.emf.compare.DifferenceKind.CHANGE; import static org.eclipse.emf.compare.DifferenceKind.DELETE; import static org.eclipse.emf.compare.DifferenceKind.MOVE; import static org.eclipse.emf.compare.utils.EMFComparePredicates.hasConflict; import static org.eclipse.emf.compare.utils.EMFComparePredicates.ofKind; import com.google.common.base.Function; import com.google.common.base.Predicate; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashSet; import org.eclipse.emf.common.util.EList; import org.eclipse.emf.common.util.URI; import org.eclipse.emf.compare.AttributeChange; import org.eclipse.emf.compare.Comparison; import org.eclipse.emf.compare.Conflict; import org.eclipse.emf.compare.Diff; import org.eclipse.emf.compare.DifferenceSource; import org.eclipse.emf.compare.Equivalence; import org.eclipse.emf.compare.FeatureMapChange; import org.eclipse.emf.compare.Match; import org.eclipse.emf.compare.MatchResource; import org.eclipse.emf.compare.ReferenceChange; import org.eclipse.emf.compare.ResourceAttachmentChange; import org.eclipse.emf.compare.utils.ReferenceUtil; import org.eclipse.emf.ecore.EAnnotation; import org.eclipse.emf.ecore.EObject; import org.eclipse.emf.ecore.EStructuralFeature; import org.eclipse.emf.ecore.resource.Resource; import org.eclipse.emf.ecore.resource.ResourceSet; import org.eclipse.emf.ecore.util.EcoreUtil; import org.eclipse.emf.ecore.util.ExtendedMetaData; import org.eclipse.emf.ecore.util.FeatureMap; /** * This utility class provides common methods for navigation over and querying of the Comparison model. * * @author <a href="mailto:laurent.goubet@obeo.fr">Laurent Goubet</a> */ public final class ComparisonUtil { /** * Predicate to know if the given diff respects the requirements of a cascading diff. */ @SuppressWarnings("unchecked") private static final Predicate<Diff> CASCADING_DIFF = not( or(hasConflict(REAL), instanceOf(ResourceAttachmentChange.class), ofKind(MOVE))); /** Hides default constructor. */ private ComparisonUtil() { // prevents instantiation } /** * When merging a {@link Diff}, returns the sub diffs of this given diff, and all associated diffs (see * {@link DiffUtil#getAssociatedDiffs(Iterable, boolean, Diff)}) of these sub diffs. * <p> * If the diff is an {@link org.eclipse.emf.compare.AttributeChange}, a * {@link org.eclipse.emf.compare.FeatureMapChange} or a * {@link org.eclipse.emf.compare.ResourceAttachmentChange} , this method will return an empty iterable. * </p> * <p> * If the diff is a {@link ReferenceChange} this method will return all differences contained in the match * that contains the value of the reference change, and all associated diffs of these differences. * </p> * * @param leftToRight * the direction of merge. * @return an iterable containing the sub diffs of this given diff, and all associated diffs of these sub * diffs. * @since 3.0 */ public static Function<Diff, Iterable<Diff>> getSubDiffs(final boolean leftToRight) { return getSubDiffs(leftToRight, false, new LinkedHashSet<Diff>()); } /** * When merging a {@link Diff}, returns the first level of sub diffs of this given diff, and all * associated diffs (see {@link DiffUtil#getAssociatedDiffs(Iterable, boolean, Diff)}) of these sub diffs. * <p> * If the diff is an {@link org.eclipse.emf.compare.AttributeChange}, a * {@link org.eclipse.emf.compare.FeatureMapChange} or a * {@link org.eclipse.emf.compare.ResourceAttachmentChange} , this method will return an empty iterable. * </p> * <p> * If the diff is a {@link ReferenceChange} this method will return the first level differences contained * in the match that contains the value of the reference change, and all associated diffs of these * differences. * </p> * * @param leftToRight * the direction of merge. * @return an iterable containing the first level of sub diffs of this given diff, and all associated * diffs of these sub diffs. * @since 3.3 */ public static Function<Diff, Iterable<Diff>> getDirectSubDiffs(final boolean leftToRight) { return getSubDiffs(leftToRight, true, new LinkedHashSet<Diff>()); } /** * Checks if the given difference is either an addition or a "set" from the default value to a new * reference. * * @param difference * The given difference. * @return <code>true</code> if this is an addition or "set" diff. */ public static boolean isAddOrSetDiff(Diff difference) { boolean result = false; if (difference.getKind() == ADD) { result = true; } else if (difference.getKind() == CHANGE) { final EStructuralFeature feature; if (difference instanceof ReferenceChange) { feature = ((ReferenceChange)difference).getReference(); } else if (difference instanceof AttributeChange) { feature = ((AttributeChange)difference).getAttribute(); } else if (difference instanceof FeatureMapChange) { feature = ((FeatureMapChange)difference).getAttribute(); } else { feature = null; } if (feature != null && !feature.isMany()) { final Match match = difference.getMatch(); final EObject source; if (match.getComparison().isThreeWay()) { source = match.getOrigin(); } else { source = match.getRight(); } if (source == null) { result = true; } else { result = isEqualToDefault(source, feature); } } } return result; } /** * Checks whether the given feature of the given EObject is set to its default value or the empty String. * * @param object * The object which feature value we need to check. * @param feature * The feature which value we need to check. * @return <code>true</code> is this object's feature is set to a value equal to its default. */ private static boolean isEqualToDefault(EObject object, EStructuralFeature feature) { final Object value = ReferenceUtil.safeEGet(object, feature); final Object defaultValue = feature.getDefaultValue(); if (value == null) { return defaultValue == null; } return value.equals(feature.getDefaultValue()) || "".equals(value); //$NON-NLS-1$ } /** * Checks if the given difference is either a deletion or a "unset" to the default value. * * @param difference * The given difference. * @return <code>true</code> if this is a deletion or "unset" diff. */ public static boolean isDeleteOrUnsetDiff(Diff difference) { boolean result = false; if (difference.getKind() == DELETE) { result = true; } else if (difference.getKind() == CHANGE) { final EStructuralFeature feature; if (difference instanceof ReferenceChange) { feature = ((ReferenceChange)difference).getReference(); } else if (difference instanceof AttributeChange) { feature = ((AttributeChange)difference).getAttribute(); } else if (difference instanceof FeatureMapChange) { feature = ((FeatureMapChange)difference).getAttribute(); } else { feature = null; } if (feature != null && !feature.isMany()) { final Match match = difference.getMatch(); final EObject expectedContainer; if (difference.getSource() == DifferenceSource.LEFT) { expectedContainer = match.getLeft(); } else { expectedContainer = match.getRight(); } if (expectedContainer == null) { result = true; } else { result = isEqualToDefault(expectedContainer, feature); } } } return result; } /** * Checks whether the given diff corresponds to a feature map containment change. This holds true for * differences on feature map containment references' values. * * @param diff * The diff to consider. * @return <code>true</code> if the given {@code diff} is to be considered a containment change, * <code>false</code> otherwise. */ public static boolean isFeatureMapContainment(final Diff diff) { // If the value of the FeatureMap.Entry is contained in the same container than the FeatureMap, it is // a containment change. if (diff instanceof FeatureMapChange) { FeatureMap.Entry entry = (FeatureMap.Entry)((FeatureMapChange)diff).getValue(); Object entryValue = entry.getValue(); if (entryValue instanceof EObject) { EObject container = ((EObject)entryValue).eContainer(); Match match = diff.getMatch(); return container == match.getLeft() || container == match.getRight() || container == match.getOrigin(); } } return false; } /** * Get the expected target container in case of a move. * * @param comparison * The comparison object. * @param diff * The diff we are currently merging. * @param rightToLeft * Whether we should move the value in the left or right side. * @return The expected target container if found, <code>null</code> otherwise. */ public static EObject moveElementGetExpectedContainer(final Comparison comparison, final FeatureMapChange diff, final boolean rightToLeft) { final EObject expectedContainer; if (!isFeatureMapContainment(diff)) { if (rightToLeft) { expectedContainer = diff.getMatch().getLeft(); } else { expectedContainer = diff.getMatch().getRight(); } } else if (diff.getSource() == DifferenceSource.LEFT) { if (rightToLeft) { expectedContainer = getContainerInEquivalence(comparison, diff, rightToLeft); } else { expectedContainer = diff.getMatch().getRight(); } } else { if (rightToLeft) { expectedContainer = diff.getMatch().getLeft(); } else { expectedContainer = getContainerInEquivalence(comparison, diff, rightToLeft); } } return expectedContainer; } /** * Get the expected target container in the equivalent diffs of the given diff in case of a move. * * @param comparison * The comparison object. * @param diff * The diff we are currently merging. * @param rightToLeft * Whether we should move the value in the left or right side. * @return The expected target container if found, <code>null</code> otherwise. */ private static EObject getContainerInEquivalence(final Comparison comparison, final FeatureMapChange diff, final boolean rightToLeft) { EObject expectedContainer = null; Equivalence equ = diff.getEquivalence(); if (equ != null) { for (Diff equivalence : equ.getDifferences()) { if (equivalence instanceof ReferenceChange) { final Match valueMatch = comparison.getMatch(((ReferenceChange)equivalence).getValue()); /* * We cannot "trust" the holding match (getMatch) in this case. However, "valueMatch" * cannot be null : we cannot have detected a move if the moved element is not matched on * both sides. Use that information to retrieve the proper "target" container. */ final Match targetContainerMatch; // If it exists, use the source side's container as reference if (rightToLeft && valueMatch.getRight() != null) { targetContainerMatch = comparison.getMatch(valueMatch.getRight().eContainer()); } else if (!rightToLeft && valueMatch.getLeft() != null) { targetContainerMatch = comparison.getMatch(valueMatch.getLeft().eContainer()); } else { // Otherwise, the value we're moving on one side has been removed from its source // side. targetContainerMatch = comparison.getMatch(valueMatch.getOrigin().eContainer()); } if (rightToLeft) { expectedContainer = targetContainerMatch.getLeft(); } else { expectedContainer = targetContainerMatch.getRight(); } break; } } } else if (rightToLeft) { expectedContainer = diff.getMatch().getLeft(); } else { expectedContainer = diff.getMatch().getRight(); } return expectedContainer; } /** * When merging a {@link Diff}, returns the sub diffs of this given diff, and all associated diffs (see * {@link DiffUtil#getAssociatedDiffs(Iterable, boolean, Diff)}) of these sub diffs. * <p> * If the diff is an {@link org.eclipse.emf.compare.AttributeChange}, a * {@link org.eclipse.emf.compare.FeatureMapChange} or a * {@link org.eclipse.emf.compare.ResourceAttachmentChange}, this method will return an empty iterable. * </p> * <p> * If the diff is a {@link ReferenceChange} this method will return all differences contained in the match * that contains the value of the reference change, and all associated diffs of these differences. * </p> * * @param leftToRight * the direction of merge. * @param firstLevelOnly * to get only the first level of subDiffs. * @param processedDiffs * a set of diffs which have been already processed. * @return an iterable containing the sub diffs of this given diff, and all associated diffs of these sub * diffs. * @since 3.0 */ private static Function<Diff, Iterable<Diff>> getSubDiffs(final boolean leftToRight, final boolean firstLevelOnly, final LinkedHashSet<Diff> processedDiffs) { return new Function<Diff, Iterable<Diff>>() { public Iterable<Diff> apply(Diff diff) { if (diff instanceof ReferenceChange) { Match matchOfValue = diff.getMatch().getComparison() .getMatch(((ReferenceChange)diff).getValue()); if (((ReferenceChange)diff).getReference().isContainment()) { final Iterable<Diff> subDiffs; // if the diff is a Move diff, we don't want its children. if (ofKind(MOVE).apply(diff)) { subDiffs = ImmutableList.of(); } else if (matchOfValue != null && !firstLevelOnly) { subDiffs = filter(matchOfValue.getAllDifferences(), CASCADING_DIFF); } else if (matchOfValue != null && firstLevelOnly) { subDiffs = filter(matchOfValue.getDifferences(), CASCADING_DIFF); } else { subDiffs = ImmutableList.of(); } addAll(processedDiffs, subDiffs); final Iterable<Diff> associatedDiffs = getAssociatedDiffs(diff, subDiffs, processedDiffs, leftToRight, firstLevelOnly); return ImmutableSet.copyOf(concat(subDiffs, associatedDiffs)); } } return ImmutableSet.of(); } }; } /** * When merging a {@link Diff}, returns the associated diffs of the sub diffs of the diff, and all sub * diffs (see {@link DiffUtil#getSubDiffs(boolean)}) of these associated diffs. * <p> * The associated diffs of a diff are : * </p> * <li>{@link Diff#getRequiredBy()} if the source of the diff is the left side and the direction of the * merge is right to left.</li> * <li>{@link Diff#getRequiredBy()} if the source of the diff is the right side and the direction of the * merge is left to right.</li> * <li>{@link Diff#getRequires()} if the source of the diff is the left side and the direction of the * merge is left to right.</li> * <li>{@link Diff#getRequires()} if the source of the diff is the right side and the direction of the * merge is right to left.</li> * <li>{@link Diff#getRefines()} in any case.</li> * </ul> * * @param diffRoot * the given diff. * @param subDiffs * the iterable of sub diffs for which we want the associated diffs. * @param processedDiffs * a set of diffs which have been already processed. * @param leftToRight * the direction of merge. * @param firstLevelOnly * to get only the first level of subDiffs. * @return an iterable containing the associated diffs of these given sub diffs, and all sub diffs of * these associated diffs. * @since 3.0 */ private static Iterable<Diff> getAssociatedDiffs(final Diff diffRoot, Iterable<Diff> subDiffs, LinkedHashSet<Diff> processedDiffs, boolean leftToRight, boolean firstLevelOnly) { Collection<Diff> associatedDiffs = new LinkedHashSet<Diff>(); for (Diff diff : subDiffs) { final Collection<Diff> reqs = new LinkedHashSet<Diff>(); if (leftToRight) { if (diff.getSource() == DifferenceSource.LEFT) { reqs.addAll(diff.getRequires()); } else { reqs.addAll(diff.getRequiredBy()); } } else { if (diff.getSource() == DifferenceSource.LEFT) { reqs.addAll(diff.getRequiredBy()); } else { reqs.addAll(diff.getRequires()); } } reqs.remove(diffRoot); associatedDiffs.addAll(reqs); associatedDiffs.addAll(diff.getRefines()); for (Diff req : reqs) { if (!Iterables.contains(subDiffs, req) && !processedDiffs.contains(req)) { processedDiffs.add(req); addAll(associatedDiffs, getSubDiffs(leftToRight, firstLevelOnly, processedDiffs).apply(req)); } } } return associatedDiffs; } /** * Returns the comparison associated with the given object. The given object is expected to be an instance * of one of the Class from the ComparePackage model. * * @param object * the object from which the comparison should be retrieved. * @return the comparison. */ public static Comparison getComparison(EObject object) { final Comparison comparison; if (object instanceof Match) { comparison = ((Match)object).getComparison(); } else if (object instanceof Diff) { comparison = getComparison((Diff)object); } else if (object instanceof MatchResource) { comparison = ((MatchResource)object).getComparison(); } else if (object instanceof Equivalence) { EObject eContainer = object.eContainer(); if (eContainer instanceof Comparison) { comparison = (Comparison)eContainer; } else { EList<Diff> differences = ((Equivalence)object).getDifferences(); Diff first = getFirst(differences, null); if (first != null) { comparison = first.getMatch().getComparison(); } else { comparison = null; } } } else if (object instanceof Conflict) { EObject eContainer = object.eContainer(); if (eContainer instanceof Comparison) { comparison = (Comparison)eContainer; } else { EList<Diff> differences = ((Conflict)object).getDifferences(); Diff first = getFirst(differences, null); if (first != null) { comparison = first.getMatch().getComparison(); } else { comparison = null; } } } else { comparison = null; } return comparison; } /** * Returns the comparison associated with the given diff. * * @param diff * The diff, which must either have a match or a MatchResource container, otherwise a NPE will * be thrown. * @return The comparison that contains the given diff, or {@code null} if there's none. */ public static Comparison getComparison(Diff diff) { final Comparison comparison; if (diff.eContainer() instanceof MatchResource) { comparison = ((MatchResource)diff.eContainer()).getComparison(); } else { comparison = diff.getMatch().getComparison(); } return comparison; } /** * Determines the side of the given {@link Match} which represents the model state the other side will be * changed to. * * @param match * The match whose side is returned. * @param source * The source from which side the differences are determined. * @param mergeRightToLeft * The direction of the merge. * @return The side of the given {@code match} which represents the desired model state in regards to the * given {@link DifferenceSource} and {@code MergeDirection}. */ public static EObject getExpectedSide(Match match, DifferenceSource source, boolean mergeRightToLeft) { final EObject result; // Bug 458818: prevent NPE if match is null if (match != null) { final boolean undoingLeft = mergeRightToLeft && source == DifferenceSource.LEFT; final boolean undoingRight = !mergeRightToLeft && source == DifferenceSource.RIGHT; final Comparison comparison = match.getComparison(); if (comparison.isThreeWay() && (undoingLeft || undoingRight) && match.getOrigin() != null) { result = match.getOrigin(); } else if (mergeRightToLeft) { result = match.getRight(); } else { result = match.getLeft(); } } else { result = null; } return result; } /** * Determines if the given {@link EObject} is contained directly within a FeatureMap by checking the * {@link EAnnotation}s. * * @param object * The object to check. * @return {@true} if the {@code object} is directly contained within a FeatureMap. */ public static boolean isContainedInFeatureMap(EObject object) { final EAnnotation annotation = object.eContainingFeature() .getEAnnotation(ExtendedMetaData.ANNOTATION_URI); if (annotation != null) { final String groupKind = ExtendedMetaData.FEATURE_KINDS[ExtendedMetaData.GROUP_FEATURE]; return annotation.getDetails().containsKey(groupKind); } return false; } /** * Checks if both resources are platform resources and only one exists. * * @param leftResource * the first resource to check. * @param rightResource * the second resource to check. * @return true if both resources are platform resources and only one exists, false otherwise. */ public static boolean bothArePlatformResourcesAndOnlyOneExists(Resource leftResource, Resource rightResource) { boolean existingPlatformResources = false; if (leftResource != null && rightResource != null) { final ResourceSet leftResourceSet = leftResource.getResourceSet(); final ResourceSet rightResourceSet = rightResource.getResourceSet(); if (leftResourceSet != null && rightResourceSet != null) { final URI leftURI = leftResource.getURI(); final URI rightURI = rightResource.getURI(); if (leftURI.isPlatformResource() && rightURI.isPlatformResource()) { boolean baseExists = leftResourceSet.getURIConverter().exists(leftURI, Collections.emptyMap()); boolean changedExists = rightResourceSet.getURIConverter().exists(rightURI, Collections.emptyMap()); existingPlatformResources = (baseExists && !changedExists) || (!baseExists && changedExists); } } } return existingPlatformResources; } /** * Checks if both resources have resource set. * * @param leftResource * the first resource to check. * @param rightResource * the second resource to check. * @return true if both resources have resource set, false otherwise. */ public static boolean bothResourceHaveResourceSet(Resource leftResource, Resource rightResource) { if (leftResource != null && rightResource != null) { final ResourceSet leftResourceSet = leftResource.getResourceSet(); final ResourceSet rightResourceSet = rightResource.getResourceSet(); if (leftResourceSet != null && rightResourceSet != null) { return true; } } return false; } /** * {@link EcoreUtil#delete(EObject) Deletes} the given <code>diff</code>. * <p> * Conflicts and equivalences of the <code>diff</code> will also be removed if they get meaningless after * the <code>diff</code> has been deleted. A conflict is meaningless, if it has diffs only on one side * after the deletion. An equivalence is meaningless, if it has only one diff left. * </p> * * @param diff * The diff to delete. */ public static void delete(Diff diff) { final Comparison comparison = diff.getMatch().getComparison(); final Conflict conflict = diff.getConflict(); final Equivalence equivalence = diff.getEquivalence(); EcoreUtil.delete(diff); if (conflict != null && (conflict.getLeftDifferences().isEmpty() || conflict.getRightDifferences().isEmpty())) { conflict.getDifferences().clear(); comparison.getConflicts().remove(conflict); } if (equivalence != null && equivalence.getDifferences().size() < 2) { equivalence.getDifferences().clear(); comparison.getEquivalences().remove(equivalence); } } }