org.eclipse.emf.compare.ide.ui.internal.logical.EMFResourceMappingMerger.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.emf.compare.ide.ui.internal.logical.EMFResourceMappingMerger.java

Source

/*******************************************************************************
 * Copyright (c) 2014, 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
 *     Philip Langer - bugs 461713, 465331, 470268, 476363, 476417, 486940, refactorings
 *     Alexandra Buzila - bugs 470332, 478620
 *******************************************************************************/
package org.eclipse.emf.compare.ide.ui.internal.logical;

import static org.eclipse.emf.compare.utils.EMFComparePredicates.fromSide;
import static org.eclipse.emf.compare.utils.EMFComparePredicates.hasConflict;

import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.common.io.ByteStreams;

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IStorage;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.resources.mapping.ResourceMapping;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.MultiStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.core.runtime.jobs.ISchedulingRule;
import org.eclipse.core.runtime.jobs.MultiRule;
import org.eclipse.emf.common.notify.Notification;
import org.eclipse.emf.common.notify.Notifier;
import org.eclipse.emf.common.notify.impl.AdapterImpl;
import org.eclipse.emf.common.util.BasicDiagnostic;
import org.eclipse.emf.common.util.BasicMonitor;
import org.eclipse.emf.common.util.Diagnostic;
import org.eclipse.emf.common.util.Monitor;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.compare.Comparison;
import org.eclipse.emf.compare.Conflict;
import org.eclipse.emf.compare.ConflictKind;
import org.eclipse.emf.compare.Diff;
import org.eclipse.emf.compare.DifferenceSource;
import org.eclipse.emf.compare.DifferenceState;
import org.eclipse.emf.compare.EMFCompare;
import org.eclipse.emf.compare.EMFCompare.Builder;
import org.eclipse.emf.compare.Match;
import org.eclipse.emf.compare.graph.IGraph;
import org.eclipse.emf.compare.graph.PruningIterator;
import org.eclipse.emf.compare.ide.ui.internal.EMFCompareIDEUIMessages;
import org.eclipse.emf.compare.ide.ui.internal.EMFCompareIDEUIPlugin;
import org.eclipse.emf.compare.ide.ui.logical.IModelMinimizer;
import org.eclipse.emf.compare.ide.ui.logical.SynchronizationModel;
import org.eclipse.emf.compare.ide.utils.ResourceUtil;
import org.eclipse.emf.compare.ide.utils.StorageTraversal;
import org.eclipse.emf.compare.internal.merge.MergeDependenciesUtil;
import org.eclipse.emf.compare.internal.merge.MergeMode;
import org.eclipse.emf.compare.merge.BatchMerger;
import org.eclipse.emf.compare.merge.IBatchMerger;
import org.eclipse.emf.compare.merge.IMerger;
import org.eclipse.emf.compare.merge.IMerger.Registry;
import org.eclipse.emf.compare.rcp.EMFCompareRCPPlugin;
import org.eclipse.emf.compare.rcp.internal.extension.impl.EMFCompareBuilderConfigurator;
import org.eclipse.emf.compare.scope.IComparisonScope;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.resource.ResourceSet;
import org.eclipse.team.core.diff.IDiff;
import org.eclipse.team.core.mapping.IMergeContext;
import org.eclipse.team.core.mapping.IResourceMappingMerger;
import org.eclipse.team.core.mapping.provider.MergeStatus;

/*
 * Illegally implements IResourceMappingMerger, but we need none of the behavior from the abstract
 * ResourceMappingMerger. Filtered in the API filters.
 */
/**
 * A customized merger for the {@link EMFResourceMapping}s. This will use EMF Compare to recompute the logical
 * model of the mappings it needs to merge, then merge everything to the left model if there are no conflicts,
 * stopping dead if there is any conflict.
 * <p>
 * Mapping mergers are usually retrieved through an adapter registered on the ModelProvider. In this case,
 * {@code org.eclipse.core.runtime.Platform.getAdapterManager().getAdapter(emfModelProvider, IResourceMappingMerger.class)}
 * .
 * </p>
 * 
 * @author <a href="mailto:laurent.goubet@obeo.fr">Laurent Goubet</a>
 * @see EMFLogicalModelAdapterFactory
 */
public class EMFResourceMappingMerger implements IResourceMappingMerger {

    /** The merger registry. */
    private static final Registry MERGER_REGISTRY = EMFCompareRCPPlugin.getDefault().getMergerRegistry();

    /** {@inheritDoc} */
    public IStatus merge(IMergeContext mergeContext, IProgressMonitor monitor) throws CoreException {
        final ResourceMapping[] emfMappings = getEMFMappings(mergeContext);
        log(IStatus.OK, "EMFResourceMappingMerger.startingModelMerge", emfMappings); //$NON-NLS-1$
        IStatus shortcutStatus = Status.OK_STATUS;
        if (emfMappings.length <= 0) {
            shortcutStatus = new Status(IStatus.ERROR, EMFCompareIDEUIPlugin.PLUGIN_ID,
                    EMFCompareIDEUIMessages.getString("EMFResourceMappingMerger.mergeFailedGeneric")); //$NON-NLS-1$
        } else {
            shortcutStatus = validateMappings(emfMappings);
        }
        if (shortcutStatus.getSeverity() != IStatus.OK) {
            return shortcutStatus;
        }

        // Use a sub-monitor with 10 ticks per child
        // For the time being, Cancel is not supported here because reverting changes is problematic
        SubMonitor subMonitor = SubMonitor.convert(monitor, emfMappings.length);
        try {
            final Set<ResourceMapping> failingMappings = new HashSet<ResourceMapping>();
            for (ResourceMapping mapping : emfMappings) {
                mergeMapping(mapping, mergeContext, failingMappings, subMonitor.newChild(1));
            }

            IStatus status = Status.OK_STATUS;
            if (!failingMappings.isEmpty()) {
                final ResourceMapping[] failingArray = failingMappings
                        .toArray(new ResourceMapping[failingMappings.size()]);
                status = new MergeStatus(EMFCompareIDEUIPlugin.PLUGIN_ID,
                        EMFCompareIDEUIMessages.getString("EMFResourceMappingMerger.mergeFailedConflicts"), //$NON-NLS-1$
                        failingArray);
            } else {
                log(IStatus.OK, "EMFResourceMappingMerger.successfulModelMerge", emfMappings); //$NON-NLS-1$
            }
            return status;
        } finally {
            if (monitor != null) {
                monitor.done();
            }
        }
    }

    /**
     * Logs the EMFCompareIDEUIPlugin message for the given {@code key} with the given severity
     * {@code statusCode} and {@code emfMappings}.
     * 
     * @param statusCode
     *            Status code must be one of {@link IStatus}.
     * @param key
     *            Message key for EMFCompareIDEUIPlugin.
     * @param emfMappings
     *            The resource mappings to log.
     */
    private void log(int statusCode, String key, ResourceMapping[] emfMappings) {
        final List<IResource> iResources = getInvolvedIResources(emfMappings);
        final String message = EMFCompareIDEUIMessages.getString(key, String.valueOf(iResources.size()));
        log(statusCode, message, iResources);
    }

    /**
     * Logs the given {@code message} and the given {@code iResources} with the given severity
     * {@code statusCode}.
     * <p>
     * The logged status is a {@link MultiStatus}, having the given {@code message} as a parent status and the
     * names of the provided {@code iResources} as child statuses.
     * </p>
     * 
     * @param statusCode
     *            Status code must be one of {@link IStatus}.
     * @param message
     *            The message to be logged.
     * @param iResources
     *            The resources to be added as child status.
     */
    private void log(int statusCode, String message, Collection<IResource> iResources) {
        final MultiStatus multiStatus = new MultiStatus(EMFCompareIDEUIPlugin.PLUGIN_ID, 0, message, null);
        for (IResource iResource : iResources) {
            final Status childStatus = new Status(statusCode, EMFCompareIDEUIPlugin.PLUGIN_ID,
                    iResource.getFullPath().toOSString());
            multiStatus.add(childStatus);
        }
        log(multiStatus);
    }

    /**
     * Returns the {@link IResource resources} involved in the given {@code emfMappings}.
     * 
     * @param emfMappings
     *            The resource mappings to get the involved resources from.
     * @return The resources involved in {@code emfMappings}.
     */
    private List<IResource> getInvolvedIResources(ResourceMapping[] emfMappings) {
        final List<IResource> iResources = new ArrayList<IResource>();
        for (ResourceMapping mapping : emfMappings) {
            if (mapping instanceof EMFResourceMapping) {
                final SynchronizationModel syncModel = ((EMFResourceMapping) mapping).getLatestModel();
                for (IResource iResource : syncModel.getResources()) {
                    iResources.add(iResource);
                }
            }
        }
        return iResources;
    }

    /**
     * Logs the given {@code status} to the log of {@link EMFCompareIDEUIPlugin}.
     * 
     * @param status
     *            The {@link IStatus} to log.
     */
    private void log(IStatus status) {
        EMFCompareIDEUIPlugin.getDefault().getLog().log(status);
    }

    /**
     * Merges one mapping.
     * 
     * @param mapping
     *            The mapping to merge
     * @param mergeContext
     *            The merge context
     * @param failingMappings
     *            The set of failing mappings
     * @param monitor
     *            The progress monitor to use, 10 ticks will be consumed
     */
    private void mergeMapping(ResourceMapping mapping, final IMergeContext mergeContext,
            final Set<ResourceMapping> failingMappings, IProgressMonitor monitor) throws CoreException {
        final SubMonitor subMonitor = SubMonitor.convert(monitor, 10);
        // validateMappings() has made sure we only have EMFResourceMappings
        final SynchronizationModel syncModel = ((EMFResourceMapping) mapping).getLatestModel();
        // we may have non-existing storages in the left traversal, so let's get rid of them
        removeNonExistingStorages(syncModel.getLeftTraversal());
        // get the involved resources before we run the minimizer
        final Set<IResource> resources = Sets.newLinkedHashSet(syncModel.getResources());

        final IModelMinimizer minimizer = new IdenticalResourceMinimizer();
        minimizer.minimize(syncModel, subMonitor.newChild(1)); // 10%
        final IComparisonScope scope = ComparisonScopeBuilder.create(syncModel, subMonitor.newChild(3)); // 40%

        final Builder builder = EMFCompare.builder();
        EMFCompareBuilderConfigurator.createDefault().configure(builder);

        final Comparison comparison = builder.build().compare(scope,
                BasicMonitor.toMonitor(SubMonitor.convert(subMonitor.newChild(1), 10))); // 50%

        if (hasRealConflict(comparison)) {
            final Set<URI> conflictingURIs = performPreMerge(comparison, subMonitor.newChild(3)); // 80%
            save(scope.getLeft(), syncModel.getLeftTraversal(), syncModel.getRightTraversal(),
                    syncModel.getOriginTraversal());
            failingMappings.add(mapping);
            markResourcesAsMerged(mergeContext, resources, conflictingURIs, subMonitor.newChild(2)); // 100%
        } else {
            final ResourceAdditionAndDeletionTracker resourceTracker = new ResourceAdditionAndDeletionTracker();
            try {
                scope.getLeft().eAdapters().add(resourceTracker);
                performBatchMerge(comparison, subMonitor.newChild(3)); // 80%
                save(scope.getLeft(), syncModel.getLeftTraversal(), syncModel.getRightTraversal(),
                        syncModel.getOriginTraversal());
                delegateMergeOfUnmergedResourcesAndMarkDiffsAsMerged(syncModel, mergeContext, resourceTracker,
                        subMonitor.newChild(2)); // 100%
            } finally {
                scope.getLeft().eAdapters().remove(resourceTracker);
            }
        }

        subMonitor.setWorkRemaining(0);
    }

    /**
     * Removes storages that do not exist from the specified {@code traversal}.
     * <p>
     * In the current implementation, the check for existence is based on the assumption that the storage is
     * an {@link IFile}. This is fine, since we currently need it on the local side only anyways.
     * </p>
     * 
     * @param traversal
     *            The traversal to remove non-existing storages from.
     */
    private void removeNonExistingStorages(StorageTraversal traversal) {
        for (IStorage storage : Sets.newHashSet(traversal.getStorages())) {
            if (storage instanceof IFile && !((IFile) storage).exists()) {
                traversal.removeStorage(storage);
            }
        }
    }

    /**
     * Performs a pre-merge of the given {@code comparison}.
     * <p>
     * A pre-merge is a merge that performs all non-conflicting changes but omits conflicting changes or
     * changes that depend on conflicting changes.
     * </p>
     * 
     * @param comparison
     *            The comparison to pre-merge.
     * @param subMonitor
     *            The progress monitor to use.
     * @return the set of the uri for resources on which conflicts were not auto-mergeable.
     */
    private Set<URI> performPreMerge(Comparison comparison, SubMonitor subMonitor) {
        final IGraph<Diff> differencesGraph = MergeDependenciesUtil.mapDifferences(comparison, MERGER_REGISTRY,
                true, MergeMode.RIGHT_TO_LEFT);
        final PruningIterator<Diff> iterator = differencesGraph.breadthFirstIterator();
        final Monitor emfMonitor = BasicMonitor.toMonitor(subMonitor);

        final Set<URI> conflictingURIs = new LinkedHashSet<URI>();
        while (iterator.hasNext()) {
            final Diff next = iterator.next();
            if (hasConflict(ConflictKind.REAL).apply(next)) {
                iterator.prune();
                conflictingURIs.addAll(collectConflictingResources(differencesGraph.depthFirstIterator(next)));
            } else if (next.getState() != DifferenceState.MERGED) {
                final IMerger merger = MERGER_REGISTRY.getHighestRankingMerger(next);
                merger.copyRightToLeft(next, emfMonitor);
            }
        }
        return conflictingURIs;
    }

    /**
     * Iterates over the given diffs to collect the resources they impact. This will be called in case of
     * conflicts in order to know exactly which resources should be marked as conflicting.
     * 
     * @param diffIterator
     *            Iterator over the conflicting differences and their dependent diffs.
     * @return The uris of all resources impacted by conflicting differences.
     */
    private Set<URI> collectConflictingResources(Iterator<Diff> diffIterator) {
        final Set<URI> conflictingURIs = new LinkedHashSet<URI>();
        while (diffIterator.hasNext()) {
            final Diff diff = diffIterator.next();
            final ImmutableSet.Builder<Diff> builder = ImmutableSet.builder();
            if (diff.getConflict() != null) {
                builder.addAll(diff.getConflict().getDifferences()).add(diff);
                for (Diff conflictingDiff : builder.build()) {
                    final Match next = conflictingDiff.getMatch();
                    final URI leftURI = resourceURIorNull(next.getLeft());
                    final URI rightURI = resourceURIorNull(next.getRight());
                    final URI originURI = resourceURIorNull(next.getOrigin());
                    if (leftURI != null) {
                        conflictingURIs.add(leftURI);
                    }
                    if (rightURI != null) {
                        conflictingURIs.add(rightURI);
                    }
                    if (originURI != null) {
                        conflictingURIs.add(originURI);
                    }
                }
            }
        }
        return conflictingURIs;
    }

    /**
     * Returns either the URI of the resource containing the given EObject, or <code>null</code> if none.
     * 
     * @param eObject
     *            The EObject for which we need a resource URI.
     * @return The URI of the resource containing the given EObject, <code>null</code> if none.
     */
    private URI resourceURIorNull(EObject eObject) {
        if (eObject != null) {
            final Resource res = eObject.eResource();
            if (res != null) {
                return res.getURI();
            }
        }
        return null;
    }

    /**
     * Marks the resources as merged if their URIs are not included in the given set of known
     * {@code conflictingURIs}.
     * 
     * @param context
     *            The current merge context.
     * @param resources
     *            The resources to be marked as merged.
     * @param conflictingURIs
     *            The set of known {@code conflictingURIs}.
     * @param subMonitor
     *            Monitor on which to report progress to the user.
     */
    private void markResourcesAsMerged(IMergeContext context, Set<IResource> resources, Set<URI> conflictingURIs,
            SubMonitor subMonitor) {
        for (IResource resource : resources) {
            if (resource instanceof IFile) {
                final IFile iFile = (IFile) resource;
                final URI uri = ResourceUtil.createURIFor(iFile);
                if (!conflictingURIs.contains(uri)) {
                    markAsMerged(context, resource, subMonitor);
                }
            }
        }
    }

    /**
     * Mark the resource at the given URI as merged.
     * 
     * @param context
     *            The current merge context.
     * @param resource
     *            The resource to mark as merged.
     * @param subMonitor
     *            Monitor on which to report progress to the user.
     */
    private void markAsMerged(IMergeContext context, IResource resource, SubMonitor subMonitor) {
        IDiff diff = context.getDiffTree().getDiff(resource);
        if (diff != null) {
            try {
                context.markAsMerged(diff, true, subMonitor);
            } catch (CoreException e) {
                EMFCompareIDEUIPlugin.getDefault().log(e);
            }
        }
    }

    /**
     * Performs a batch merge of the given {@code comparison}.
     * 
     * @param comparison
     *            The comparison to merge.
     * @param subMonitor
     *            The progress monitor to use.
     */
    private void performBatchMerge(Comparison comparison, SubMonitor subMonitor) {
        final IBatchMerger merger = new BatchMerger(MERGER_REGISTRY, fromSide(DifferenceSource.RIGHT));
        merger.copyAllRightToLeft(comparison.getDifferences(), BasicMonitor.toMonitor(subMonitor));
    }

    /**
     * Delegates the merge of so far non-merged resource additions and deletions and marks all other already
     * merged resources as merged.
     * 
     * @param syncModel
     *            The synchronization model to obtain the storages.
     * @param mergeContext
     *            The merge context.
     * @param resourceTracker
     *            The tracker that tracked already merged file additions and deletions.
     * @param subMonitor
     *            The progress monitor to use.
     */
    private void delegateMergeOfUnmergedResourcesAndMarkDiffsAsMerged(SynchronizationModel syncModel,
            IMergeContext mergeContext, ResourceAdditionAndDeletionTracker resourceTracker, SubMonitor subMonitor)
            throws CoreException {

        // mark already deleted files as merged
        for (IFile deletedFile : resourceTracker.getDeletedIFiles()) {
            final IDiff diff = mergeContext.getDiffTree().getDiff(deletedFile);
            markAsMerged(diff, mergeContext, subMonitor);
        }

        // for all left storages, delegate the merge of a deletion that has not been performed yet and mark
        // all already performed diffs as merged
        for (IStorage storage : syncModel.getLeftTraversal().getStorages()) {
            final IPath fullPath = ResourceUtil.getFixedPath(storage);
            if (fullPath == null) {
                EMFCompareIDEUIPlugin.getDefault().getLog()
                        .log(new Status(IStatus.WARNING, EMFCompareIDEUIPlugin.PLUGIN_ID,
                                EMFCompareIDEUIMessages.getString("EMFResourceMappingMerger.mergeIncomplete"))); //$NON-NLS-1$
            } else {
                final IDiff diff = mergeContext.getDiffTree().getDiff(fullPath);
                if (diff != null) {
                    if (IDiff.REMOVE == diff.getKind() && !resourceTracker.containsRemovedResource(fullPath)) {
                        merge(diff, mergeContext, subMonitor.newChild(1));
                    } else {
                        markAsMerged(diff, mergeContext, subMonitor.newChild(1));
                    }
                }
            }
        }

        // delegate all additions from the right storages that have not been performed yet
        // or, if they have been merged, mark the diff as merged
        for (IStorage rightStorage : syncModel.getRightTraversal().getStorages()) {
            final IPath fullPath = ResourceUtil.getFixedPath(rightStorage);
            if (fullPath != null) {
                final IDiff diff = mergeContext.getDiffTree().getDiff(fullPath);
                if (diff != null && IDiff.ADD == diff.getKind()) {
                    if (!resourceTracker.containsAddedResource(fullPath)) {
                        IFile file = ResourcesPlugin.getWorkspace().getRoot().getFile(fullPath);
                        IProject project = file.getProject();
                        if (project.isAccessible()) {
                            merge(diff, mergeContext, subMonitor.newChild(1));
                        } else {
                            // The project that will contain the resource is not accessible.
                            // We have to copy the file "manually" from the right side to the left side.
                            try {
                                InputStream inputStream = rightStorage.getContents();
                                FileOutputStream outputStream = new FileOutputStream(
                                        ResourceUtil.getAbsolutePath(rightStorage).toFile());
                                ByteStreams.copy(inputStream, outputStream);
                                inputStream.close();
                                outputStream.close();
                            } catch (FileNotFoundException e) {
                                EMFCompareIDEUIPlugin.getDefault().log(e);
                                // TODO Should we throw the exception here to interrupt the merge ?
                            } catch (IOException e) {
                                EMFCompareIDEUIPlugin.getDefault().log(e);
                                // TODO Should we throw the exception here to interrupt the merge ?
                            }
                        }
                    } else {
                        markAsMerged(diff, mergeContext, subMonitor.newChild(1));
                    }
                }
            }
        }
    }

    /**
     * Merges the given {@code diff}.
     * 
     * @param diff
     *            The difference to be merged.
     * @param mergeContext
     *            The merge context.
     * @param subMonitor
     *            The process monitor to use.
     */
    private void merge(IDiff diff, IMergeContext mergeContext, SubMonitor subMonitor) {
        try {
            mergeContext.merge(diff, false, subMonitor);
        } catch (CoreException e) {
            EMFCompareIDEUIPlugin.getDefault().log(e);
        }
    }

    /**
     * Marks the given {@code diff} as merged.
     * 
     * @param diff
     *            The difference to be marked as merge.
     * @param mergeContext
     *            The merge context.
     * @param subMonitor
     *            The progress monitor to use.
     */
    private void markAsMerged(final IDiff diff, IMergeContext mergeContext, SubMonitor subMonitor) {
        try {
            mergeContext.markAsMerged(diff, true, subMonitor);
        } catch (CoreException e) {
            EMFCompareIDEUIPlugin.getDefault().log(e);
        }
    }

    /**
     * Validates the given array of mappings. Basically, this merger can only operate if all of its target
     * mappings are instances of EMFResourceMappings that were properly initialized.
     * 
     * @param mappings
     *            The mappings we are to validate.
     * @return {@link Status#OK_STATUS} if the given mappings are valid, a status describing the failure
     *         otherwise.
     */
    private IStatus validateMappings(ResourceMapping[] mappings) {
        for (ResourceMapping mapping : mappings) {
            if (mapping instanceof EMFResourceMapping && mapping.getModelObject() instanceof SynchronizationModel) {
                final SynchronizationModel model = (SynchronizationModel) mapping.getModelObject();
                if (model.getDiagnostic().getSeverity() >= Diagnostic.WARNING) {
                    return BasicDiagnostic.toIStatus(model.getDiagnostic());
                }
            } else {
                return new Status(IStatus.ERROR, EMFCompareIDEUIPlugin.PLUGIN_ID,
                        EMFCompareIDEUIMessages.getString("EMFResourceMappingMerger.mergeFailedInvalidMapping")); //$NON-NLS-1$
            }
        }
        return Status.OK_STATUS;
    }

    /**
     * Saves the given notifier to disk after a successful merge.
     * 
     * @param notifier
     *            The notifier.
     * @param leftTraversal
     *            The traversal corresponding to the left side.
     * @param rightTraversal
     *            The traversal corresponding to the right side.
     * @param originTraversal
     *            The traversal corresponding to the common ancestor of both other side. Can be
     *            <code>null</code>.
     */
    private void save(Notifier notifier, StorageTraversal leftTraversal, StorageTraversal rightTraversal,
            StorageTraversal originTraversal) {
        if (notifier instanceof ResourceSet) {
            ResourceUtil.saveAllResources((ResourceSet) notifier,
                    ImmutableMap.of(Resource.OPTION_SAVE_ONLY_IF_CHANGED,
                            Resource.OPTION_SAVE_ONLY_IF_CHANGED_MEMORY_BUFFER),
                    leftTraversal, rightTraversal, originTraversal);
        } else if (notifier instanceof Resource) {
            ResourceUtil.saveResource((Resource) notifier, ImmutableMap.of(Resource.OPTION_SAVE_ONLY_IF_CHANGED,
                    Resource.OPTION_SAVE_ONLY_IF_CHANGED_MEMORY_BUFFER));
        }
    }

    /**
     * Checks whether this comparison presents a real conflict.
     * 
     * @param comparison
     *            The comparison to check for conflicts.
     * @return <code>true</code> if there's at least one {@link ConflictKind#REAL real conflict} within this
     *         comparison.
     */
    private boolean hasRealConflict(Comparison comparison) {
        for (Conflict conflict : comparison.getConflicts()) {
            if (conflict.getKind() == ConflictKind.REAL) {
                return true;
            }
        }
        return false;
    }

    /** {@inheritDoc} */
    public ISchedulingRule getMergeRule(IMergeContext context) {
        final ResourceMapping[] emfMappings = getEMFMappings(context);
        final Set<IProject> impactedProjects = new LinkedHashSet<IProject>();
        for (ResourceMapping mapping : emfMappings) {
            impactedProjects.addAll(Arrays.asList(mapping.getProjects()));
        }
        return MultiRule.combine(impactedProjects.toArray(new ISchedulingRule[impactedProjects.size()]));
    }

    /**
     * Retrieves all mappings from the given merge context that were created from the EMFModelProvider.
     * 
     * @param context
     *            The context from which to retrieve resource mappings.
     * @return All resource mappings from this context that were created from the EMFModelProvider.
     */
    private ResourceMapping[] getEMFMappings(IMergeContext context) {
        return context.getScope().getMappings(EMFModelProvider.PROVIDER_ID);
    }

    /** {@inheritDoc} */
    public IStatus validateMerge(IMergeContext mergeContext, IProgressMonitor monitor) {
        return Status.OK_STATUS;
    }

    private static class ResourceAdditionAndDeletionTracker extends AdapterImpl {

        private final Set<String> urisOfAddedResources = new HashSet<String>();

        private final Set<String> urisOfDeletedResources = new HashSet<String>();

        private final Set<IFile> deletedIFiles = new HashSet<IFile>();

        @Override
        public void notifyChanged(Notification msg) {
            if (!isAdditionOrDeletionOfResourceNotification(msg)) {
                return;
            }

            final Resource newResource = (Resource) msg.getNewValue();
            final URI uri = newResource.getURI();
            if (uri.isPlatformResource()) {
                final String path = uri.toPlatformString(true);
                final IFile file = ResourcesPlugin.getWorkspace().getRoot().getFile(new Path(path));
                if (msg.getEventType() == Notification.ADD) {
                    trackResourceAddition(file);
                } else if (msg.getEventType() == Notification.REMOVE) {
                    trackResourceDeletion(file);
                }
            }
        }

        private boolean isAdditionOrDeletionOfResourceNotification(Notification msg) {
            return (msg.getEventType() == Notification.ADD || msg.getEventType() == Notification.REMOVE)
                    && msg.getNewValue() instanceof Resource;
        }

        private void trackResourceAddition(IFile file) {
            urisOfAddedResources.add(getStringRepresentation(file.getFullPath()));
        }

        private void trackResourceDeletion(IFile file) {
            deletedIFiles.add(file);
            urisOfDeletedResources.add(getStringRepresentation(file.getFullPath()));
        }

        private String getStringRepresentation(IPath path) {
            return path.toPortableString();
        }

        public boolean containsAddedResource(IPath path) {
            return urisOfAddedResources.contains(getStringRepresentation(path));
        }

        public boolean containsRemovedResource(IPath path) {
            return urisOfDeletedResources.contains(getStringRepresentation(path));
        }

        public Set<IFile> getDeletedIFiles() {
            return Collections.unmodifiableSet(deletedIFiles);
        }
    }
}