| package org.eclipse.emf.compare.merge; |
| |
| import static com.google.common.base.Preconditions.checkArgument; |
| import static com.google.common.base.Preconditions.checkNotNull; |
| import static com.google.common.collect.Iterables.all; |
| import static com.google.common.collect.Iterables.filter; |
| import static com.google.common.collect.Lists.newArrayList; |
| import static com.google.common.collect.Sets.newLinkedHashSet; |
| import static org.eclipse.emf.compare.DifferenceSource.LEFT; |
| import static org.eclipse.emf.compare.DifferenceSource.RIGHT; |
| |
| import com.google.common.base.Predicate; |
| |
| import java.util.Collection; |
| import java.util.List; |
| import java.util.Set; |
| |
| import org.eclipse.emf.common.notify.Notification; |
| import org.eclipse.emf.common.notify.impl.AdapterImpl; |
| import org.eclipse.emf.common.util.URI; |
| import org.eclipse.emf.compare.Comparison; |
| import org.eclipse.emf.compare.DifferenceSource; |
| import org.eclipse.emf.compare.MatchResource; |
| import org.eclipse.emf.compare.scope.IComparisonScope; |
| import org.eclipse.emf.ecore.resource.Resource; |
| import org.eclipse.emf.ecore.resource.ResourceSet; |
| import org.eclipse.emf.ecore.util.EcoreUtil; |
| |
| /** |
| * This adapter is supposed to be installed on a {@link Comparison}'s {@link ResourceSet}s and their |
| * {@link Resource}s to react to content changes. Participants can then react to such changes to jointly |
| * decide whether a resource must be marked for deletion. The same instance of adapter should be used for all |
| * the resources of a comparison's {@link ResourceSet}s. EMFCompare installs such an adapter on the comparison |
| * to make it easy to retrieve. |
| * |
| * @author <a href="mailto:laurent.delaigue@obeo.fr">Laurent Delaigue</a> |
| */ |
| public class ResourceChangeAdapter extends AdapterImpl { |
| |
| /** The comparison. */ |
| private final Comparison comparison; |
| |
| /** The compaison scope. */ |
| private final IComparisonScope scope; |
| |
| /** |
| * Set of resources to delete on save. These are the resources that are marked for deletion and which will |
| * be deleted when the comparison will be saved. |
| */ |
| private final Set<Resource> resourcesToDelete; |
| |
| /** |
| * Set of added resources. Those are the resources that have been added in a ResourceSet (left or right) |
| * by a merge operation, before the comparison has been saved. |
| */ |
| private final Set<Resource> addedResources; |
| |
| /** The participants to consult when a content change occurs. */ |
| private final List<IResourceChangeParticipant> participants; |
| |
| /** |
| * Constructor. |
| * |
| * @param comparison |
| * The comparison, cannot be <code>null</code>. |
| * @param scope |
| * The scope, cannot be <code>null</code>. Moreover, the left and right notifiers of the scope |
| * must be {@link ResourceSet}s. |
| */ |
| public ResourceChangeAdapter(Comparison comparison, IComparisonScope scope) { |
| this.comparison = checkNotNull(comparison); |
| this.scope = checkNotNull(scope); |
| checkArgument(scope.getLeft() instanceof ResourceSet); |
| checkArgument(scope.getRight() instanceof ResourceSet); |
| resourcesToDelete = newLinkedHashSet(); |
| addedResources = newLinkedHashSet(); |
| participants = newArrayList(); |
| } |
| |
| @SuppressWarnings("unchecked") |
| @Override |
| public void notifyChanged(Notification msg) { |
| if (!msg.isTouch()) { |
| if (msg.getNotifier() instanceof Resource |
| && msg.getFeatureID(Resource.class) == Resource.RESOURCE__CONTENTS) { |
| Resource resource = (Resource)msg.getNotifier(); |
| resourceContentsChanged(resource, msg); |
| } else if (msg.getNotifier() instanceof ResourceSet |
| && msg.getFeatureID(ResourceSet.class) == ResourceSet.RESOURCE_SET__RESOURCES) { |
| switch (msg.getEventType()) { |
| case Notification.ADD: |
| resourceAdded((Resource)msg.getNewValue()); |
| break; |
| case Notification.ADD_MANY: |
| for (Resource r : (List<Resource>)msg.getNewValue()) { |
| resourceAdded(r); |
| } |
| break; |
| default: |
| } |
| } |
| } |
| } |
| |
| @Override |
| public boolean isAdapterForType(Object type) { |
| return type == ResourceChangeAdapter.class; |
| } |
| |
| /** |
| * Register the given participant. |
| * |
| * @param participant |
| * The participant, must not be <code>null</code> |
| */ |
| public void addParticipant(IResourceChangeParticipant participant) { |
| participants.add(checkNotNull(participant)); |
| } |
| |
| /** |
| * Unregister the given participant, has no action if the participant was not previously registered. |
| * |
| * @param participant |
| * The participant to unregister |
| */ |
| public void removeParticipant(IResourceChangeParticipant participant) { |
| participants.remove(participant); |
| } |
| |
| /** |
| * Indicate whether a given Resource needs to be deleted. |
| * |
| * @param r |
| * The resource |
| * @return <code>true</code> if the given resource has been marked for deletion. |
| */ |
| public boolean mustDelete(Resource r) { |
| return resourcesToDelete.contains(r); |
| } |
| |
| /** |
| * Callback invoked when a resource has just been added to a resource set. By default, it walks over the |
| * interested participants and creates all the associated resources that these participants declare as |
| * associated to the given resource. |
| * |
| * @param resource |
| * The newly added resource |
| */ |
| protected void resourceAdded(Resource resource) { |
| addedResources.add(resource); |
| if (EcoreUtil.getAdapter(resource.eAdapters(), ResourceChangeAdapter.class) == null) { |
| resource.eAdapters().add(this); |
| } |
| for (IResourceChangeParticipant participant : filter(participants, interestedIn(resource))) { |
| for (URI relatedURI : participant.associatedResourceURIs(resource)) { |
| if (resource.getResourceSet().getResource(relatedURI, false) == null |
| && getResourceSetOnOtherSide(resource).getResource(relatedURI, false) != null) { |
| resource.getResourceSet().createResource(relatedURI); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Get the resource set on the other side of the given resource. |
| * |
| * @param r |
| * The resource, which must be either on the left or on the right of the comparison. |
| * @return The ResourceSet on the other side, never <code>null</code>. |
| * @throws IllegalArgumentException |
| * If the given resource is neither on the left nor on the right. |
| */ |
| protected ResourceSet getResourceSetOnOtherSide(Resource r) { |
| ResourceSet resourceSet = r.getResourceSet(); |
| if (scope.getLeft() == resourceSet) { |
| return (ResourceSet)scope.getRight(); |
| } else if (scope.getRight() == resourceSet) { |
| return (ResourceSet)scope.getLeft(); |
| } |
| throw new IllegalArgumentException("The given resource is neither on the left nor on the right"); //$NON-NLS-1$ |
| } |
| |
| /** |
| * React to a Resource contents change to determine if this change involves the deletion of one or several |
| * resources. A Resource must be deleted if: |
| * <ol> |
| * <li>Their contents is <code>null</code> or empty;</li> |
| * <li>It is not matched on the other side of the comparison;</li> |
| * <li>Every participant is OK to delete it.</li> |
| * </ol> |
| * Otherwise, it must not be deleted. When a resource is detected as 'to be deleted', all interested |
| * participants are asked for associated resources to delete along with it, and all these resources are |
| * marked for deletion without any further test. When a resource is detected as 'not to be deleted', and |
| * it had previously been marked for deletion (in the case of an undo for instance), then all interested |
| * participants are asked for associated resources which are all marked as 'not te be deleted'. |
| * |
| * @param resource |
| * The resource the contents of which have changed |
| * @param msg |
| * The notification of the change |
| */ |
| protected void resourceContentsChanged(Resource resource, Notification msg) { |
| if (resource.getContents() == null || resource.getContents().isEmpty()) { |
| if (isEmptyAndMissingOnOtherSide(resource)) { |
| Iterable<IResourceChangeParticipant> interestedParticipants = filter(participants, |
| interestedIn(resource)); |
| if (all(interestedParticipants, acceptDelete(resource))) { |
| resourcesToDelete.add(resource); |
| for (IResourceChangeParticipant participant : interestedParticipants) { |
| for (URI relatedURI : participant.associatedResourceURIs(resource)) { |
| Resource related = resource.getResourceSet().getResource(relatedURI, false); |
| if (related != null) { |
| resourcesToDelete.add(related); |
| } |
| } |
| } |
| } |
| } |
| } else { |
| if (resourcesToDelete.remove(resource)) { |
| Iterable<IResourceChangeParticipant> interestedParticipants = filter(participants, |
| interestedIn(resource)); |
| for (IResourceChangeParticipant participant : interestedParticipants) { |
| for (URI relatedURI : participant.associatedResourceURIs(resource)) { |
| Resource related = resource.getResourceSet().getResource(relatedURI, false); |
| if (related != null) { |
| resourcesToDelete.remove(related); |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| /** |
| * Indicate whether a resource is empty and is only on its side of the comparison (i.e. if it should be |
| * deleted unless a special restriction prevents it). |
| * |
| * @param resource |
| * The resource |
| * @return <code>true</code> if the resource is empty and is not matched on the other side of the |
| * comparison. |
| */ |
| public boolean isEmptyAndMissingOnOtherSide(Resource resource) { |
| if (resource.getContents().isEmpty()) { |
| ResourceMatch match = getResourceMatch(resource); |
| if (addedResources.contains(resource) || (match != null && match.isMissingOnOtherSide())) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Returns the MatchResource corresponding to the given <code>resource</code>. |
| * |
| * @param resource |
| * Resource for which we need a MatchResource. |
| * @return The MatchResource corresponding to the given <code>resource</code>, <code>null</code> if the |
| * resource is not in any side of this comparison (package, profiles, ...). |
| */ |
| protected ResourceMatch getResourceMatch(Resource resource) { |
| for (MatchResource matchRes : comparison.getMatchedResources()) { |
| if (matchRes.getLeft() == resource) { |
| return new ResourceMatch(matchRes, LEFT); |
| } else if (matchRes.getRight() == resource) { |
| return new ResourceMatch(matchRes, RIGHT); |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * A predicate for participants interested in a given resource. |
| * |
| * @param r |
| * The resource |
| * @return A predicate that returns <code>true</code> for participants interested in the given resource. |
| */ |
| private Predicate<IResourceChangeParticipant> interestedIn(final Resource r) { |
| return new Predicate<IResourceChangeParticipant>() { |
| public boolean apply(IResourceChangeParticipant input) { |
| return input.interestedIn(r); |
| } |
| }; |
| } |
| |
| /** |
| * A predicate for participants that accept the delete of a given resource. |
| * |
| * @param r |
| * The resource |
| * @return A predicate that returns <code>true</code> for participants that accept the delete of the |
| * resource. |
| */ |
| private Predicate<IResourceChangeParticipant> acceptDelete(final Resource r) { |
| return new Predicate<IResourceChangeParticipant>() { |
| public boolean apply(IResourceChangeParticipant input) { |
| return input.acceptDelete(r); |
| } |
| }; |
| } |
| |
| /** |
| * The match of a given Resource on a given side. |
| * |
| * @author <a href="mailto:laurent.delaigue@obeo.fr">Laurent Delaigue</a> |
| */ |
| private static final class ResourceMatch { |
| |
| /** The Matchresource. */ |
| private final MatchResource matchResource; |
| |
| /** The side. */ |
| private final DifferenceSource side; |
| |
| /** |
| * Constructor. |
| * |
| * @param matchResource |
| * The MatchResource, not <code>null</code> |
| * @param side |
| * The side, not <code>null</code> |
| */ |
| private ResourceMatch(MatchResource matchResource, DifferenceSource side) { |
| this.matchResource = checkNotNull(matchResource); |
| this.side = checkNotNull(side); |
| } |
| |
| /** |
| * Indicate whether the resource is missing on the other side. |
| * |
| * @return <code>true</code> if the matched resource not matched on the other side. |
| */ |
| public boolean isMissingOnOtherSide() { |
| switch (side) { |
| case LEFT: |
| return matchResource.getRight() == null; |
| case RIGHT: |
| return matchResource.getLeft() == null; |
| default: |
| } |
| throw new IllegalStateException(); |
| } |
| } |
| |
| /** |
| * A participant in a Resource content change, useful to indicate whether an empty resource must actually |
| * be deleted or not, and which other resources need to be deleted/undeleted along. |
| * |
| * @author <a href="mailto:laurent.delaigue@obeo.fr">Laurent Delaigue</a> |
| */ |
| public interface IResourceChangeParticipant { |
| /** |
| * Whether the participant is interested in the given resource. |
| * |
| * @param r |
| * The resource |
| * @return <code>true</code> if the participant is interested in (relevant for) the given resource. |
| */ |
| boolean interestedIn(Resource r); |
| |
| /** |
| * Whether the participant accepts the delete of the given resource. |
| * |
| * @param r |
| * The resource |
| * @return <code>true</code> if the participant is OK to delete the resource, <code>false</code> |
| * otherwise, which will block the deletion. |
| */ |
| boolean acceptDelete(Resource r); |
| |
| /** |
| * Provide the resources to (un)delete along with the given resource. This allows tools that want to |
| * atomically create/delete several resources at a time (for example, one sematin + one graphical |
| * resource) to deal with this atomicity. |
| * |
| * @param r |
| * The resource to (un)delete |
| * @return A collection of associated resources URI, must never be <code>null</code> but can be empty. |
| */ |
| Collection<URI> associatedResourceURIs(Resource r); |
| } |
| } |