/** * Copyright (c) 2003-2011 IBM Corporation 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: * IBM - Initial API and implementation */ package org.eclipse.emf.ecore.change.util; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; import org.eclipse.emf.common.notify.Adapter; import org.eclipse.emf.common.notify.Notification; import org.eclipse.emf.common.notify.Notifier; import org.eclipse.emf.common.util.BasicEList; import org.eclipse.emf.common.util.EList; import org.eclipse.emf.common.util.URI; import org.eclipse.emf.ecore.EObject; import org.eclipse.emf.ecore.EReference; import org.eclipse.emf.ecore.EStructuralFeature; import org.eclipse.emf.ecore.InternalEObject; import org.eclipse.emf.ecore.change.ChangeDescription; import org.eclipse.emf.ecore.change.FeatureChange; import org.eclipse.emf.ecore.change.ResourceChange; 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.InternalEList; /** * A change recorder for the tree contents of a collection of EObjects. It monitors the specified objects and * then produces a {@link ChangeDescription change model} representing the changes needed to reverse (undo) all * the model changes made while recording. */ public class ChangeRecorder extends BasicChangeRecorder implements Adapter.Internal { protected List<Notifier> targetObjects = new BasicEList.FastCompare<Notifier>(); protected List<Notifier> originalTargetObjects = new BasicEList.FastCompare<Notifier>(); protected boolean loadingTargets; protected boolean resolveProxies; protected Map<EObject, URI> eObjectToProxyURIMap; public ChangeRecorder() { super(); } public ChangeRecorder(EObject rootObject) { beginRecording(Collections.singleton(rootObject)); } public ChangeRecorder(Resource resource) { beginRecording(Collections.singleton(resource)); } public ChangeRecorder(ResourceSet resourceSet) { beginRecording(Collections.singleton(resourceSet)); } public ChangeRecorder(Collection<?> rootObjects) { beginRecording(rootObjects); } public boolean isResolveProxies() { return resolveProxies; } public void setResolveProxies(boolean resolveProxies) { this.resolveProxies = resolveProxies; } /** * @since 2.7 */ public Map<EObject, URI> getEObjectToProxyURIMap() { return eObjectToProxyURIMap; } /** * When this is set to a non-null value, * the original proxy URI of each object will be recorded * as the change recorder is attached to each object. * This is important for calling {@link ChangeDescription#copyAndReverse(Map)}. * @since 2.7 */ public void setEObjectToProxyURIMap(Map<EObject, URI> eObjectToProxyURIMap) { this.eObjectToProxyURIMap = eObjectToProxyURIMap; } @Override public void dispose() { setRecording(false); Notifier[] notifiers = targetObjects.toArray(new Notifier [targetObjects.size()]); targetObjects.clear(); for (int i = 0, length = notifiers.length; i < length; i++) { removeAdapter(notifiers[i]); } originalTargetObjects.clear(); super.dispose(); } protected void removeAdapter(Notifier notifier) { notifier.eAdapters().remove(this); } /** * Begins recording any changes made to the elements of the specified collection. * @param rootObjects A collection of instances of {@link Notifier} */ public void beginRecording(Collection<?> rootObjects) { beginRecording(null, rootObjects); } /** * Begins recording any changes made to the elements of the specified collection, * adding the changes to an existing {@link ChangeDescription}. * This allows clients to resume a previous recording. * <p> * Unpredictable (and probably bad) results may happen if the change description is * inconsistent with the current state of the application. * </p> * @param changeDescription A change description with changes made during a previous * recording or <tt>null</tt> if a new change description should be instantiated. * @param rootObjects A collection of instances of {@link Notifier} * @since 2.1.0 */ public void beginRecording(ChangeDescription changeDescription, Collection<?> rootObjects) { List<EObject> insertedObjects = changeDescription == null ? null : changeDescription.getObjectsToDetach(); if (changeDescription == null) { changeDescription = createChangeDescription(); } setChangeDescription(changeDescription); loadingTargets = true; for (Object rootObject : rootObjects) { Notifier notifier = (Notifier)rootObject; addAdapter(notifier); } loadingTargets = false; if (changeDescription != null) { prepareChangeDescriptionForResume(); } if (insertedObjects != null) { originalTargetObjects.removeAll(insertedObjects); } setRecording(true); } /** * Prepares this ChangeRecorder's {@link #changeDescription} for the scenarios where the user * is resuming a previous recording. * @see #beginRecording(ChangeDescription, Collection) * @since 2.1.0 */ protected void prepareChangeDescriptionForResume() { loadingTargets = true; ChangeDescription changeDescription = getChangeDescription(); for (Notifier notifier : changeDescription.getObjectsToAttach()) { addAdapter(notifier); } loadingTargets = false; changeDescription.getObjectsToAttach().clear(); // Make sure that all the old values are cached. for (List<FeatureChange> featureChanges : changeDescription.getObjectChanges().values()) { for (FeatureChange featureChange : featureChanges) { featureChange.getValue(); } } for (ResourceChange resourceChange : changeDescription.getResourceChanges()) { resourceChange.getValue(); } } @Override protected void consolidateChanges() { ChangeDescription changeDescription = getChangeDescription(); List<EObject> orphanedObjects = changeDescription.getObjectsToAttach(); for (Object target : targetObjects) { if (target instanceof EObject) { EObject eObject = (EObject)target; if (isOrphan(eObject)) { if (originalTargetObjects.contains(eObject)) { orphanedObjects.add(eObject); } else { changeDescription.getObjectChanges().removeKey(eObject); } } } } super.consolidateChanges(); } protected boolean isOrphan(EObject eObject) { return ((InternalEObject)eObject).eInternalContainer() == null && eObject.eResource() == null; } public void notifyChanged(Notification notification) { Object notifier = notification.getNotifier(); if (notifier instanceof EObject) { Object feature = notification.getFeature(); if (feature instanceof EReference) { EReference eReference = (EReference)feature; handleFeature(eReference, eReference.isContainment() ? eReference : null, notification, (EObject)notifier); } else if (feature != null) { handleFeature((EStructuralFeature)feature, null, notification, (EObject) notifier); } } else if (notifier instanceof Resource) { int featureID = notification.getFeatureID(Resource.class); switch (featureID) { case Resource.RESOURCE__CONTENTS: { if (!((Resource.Internal)notification.getNotifier()).isLoading()) { handleResource(notification); } break; } case Resource.RESOURCE__IS_LOADED: { loadingTargets = true; @SuppressWarnings("unchecked") EList<InternalEObject> contents = (EList<InternalEObject>)(EList<?>)((Resource)notification.getNotifier()).getContents(); for (InternalEObject content : contents) { // Don't process it as a load if the bidirectional inverse has not been set. // This situation happens when objects are added to an empty not-yet-loaded resource. // if (content.eDirectResource() != null) { addAdapter(content); } } loadingTargets = false; break; } } } else if (notifier instanceof ResourceSet) { if (notification.getFeatureID(ResourceSet.class) == ResourceSet.RESOURCE_SET__RESOURCES) { switch (notification.getEventType()) { case Notification.ADD: case Notification.SET: //case Notification.REMOVE: { Resource resource = (Resource)notification.getNewValue(); loadingTargets = true; addAdapter(resource); loadingTargets = false; break; } case Notification.ADD_MANY: //case Notification.REMOVE_MANY: { @SuppressWarnings("unchecked") Collection<Resource> resources = (Collection<Resource>)notification.getNewValue(); loadingTargets = true; for (Resource resource : resources) { addAdapter(resource); } loadingTargets = false; } } } } } protected boolean shouldRecord(EStructuralFeature feature, EReference containment, Notification notification, EObject eObject) { return shouldRecord(feature, eObject) && notification.getEventType() != Notification.RESOLVE; } protected void handleFeature(EStructuralFeature feature, EReference containment, Notification notification, EObject eObject) { boolean shouldRecord = shouldRecord(feature, containment, notification, eObject); List<FeatureChange> changes = null; FeatureChange change = null; if (shouldRecord) { changes = getFeatureChanges(eObject); change = getFeatureChange(changes, feature); } switch (notification.getEventType()) { case Notification.RESOLVE: case Notification.SET: case Notification.UNSET: { if (change == null && changes != null) { if (feature.isMany()) { List<Object> oldValue = new BasicEList<Object>((Collection<?>)eObject.eGet(feature)); int index = notification.getPosition(); if (index != Notification.NO_INDEX) { oldValue.set(index, notification.getOldValue()); } change = createFeatureChange(eObject, feature, oldValue, notification.wasSet()); } else { Object oldValue = notification.getOldValue(); change = createFeatureChange(eObject, feature, oldValue, notification.wasSet()); } ((InternalEList<FeatureChange>)changes).addUnique(change); } if (containment != null) { Object newValue = notification.getNewValue(); if (newValue != null && newValue != Boolean.TRUE && newValue != Boolean.FALSE) { addAdapter((Notifier)newValue); } } break; } case Notification.ADD: { if (change == null && changes != null) { List<Object> oldValue = new BasicEList<Object>((Collection<?>)eObject.eGet(feature)); oldValue.remove(notification.getPosition()); change = createFeatureChange(eObject, feature, oldValue, notification.wasSet()); ((InternalEList<FeatureChange>)changes).addUnique(change); } if (containment != null) { Notifier newValue = (Notifier)notification.getNewValue(); addAdapter(newValue); } break; } case Notification.ADD_MANY: { if (change == null && changes != null) { List<Object> oldValue = new BasicEList<Object>((Collection<?>)eObject.eGet(feature)); int position = notification.getPosition(); for (int i = ((Collection<?>)notification.getNewValue()).size(); --i >= 0;) { oldValue.remove(position); } change = createFeatureChange(eObject, feature, oldValue, notification.wasSet()); ((InternalEList<FeatureChange>)changes).addUnique(change); } if (containment != null) { @SuppressWarnings("unchecked") Collection<Notifier> newValues = (Collection<Notifier>)notification.getNewValue(); for (Notifier newValue : newValues) { addAdapter(newValue); } } break; } case Notification.REMOVE: { if (change == null && changes != null) { List<Object> oldValue = new BasicEList<Object>((Collection<?>)eObject.eGet(feature)); // If there's no position, the list is being cleared. // int position = notification.getPosition(); if (position == Notification.NO_INDEX) { position = 0; } oldValue.add(position, notification.getOldValue()); change = createFeatureChange(eObject, feature, oldValue, notification.wasSet()); ((InternalEList<FeatureChange>)changes).addUnique(change); } break; } case Notification.REMOVE_MANY: { if (change == null && changes != null) { @SuppressWarnings("unchecked") List<Object> removedValues = (List<Object>)notification.getOldValue(); List<Object> oldValue = new BasicEList<Object>((Collection<?>)eObject.eGet(feature)); int[] positions = (int[])notification.getNewValue(); if (positions == null) { oldValue.addAll(removedValues); } else { for (int i = 0; i < positions.length; ++i) { oldValue.add(positions[i], removedValues.get(i)); } } change = createFeatureChange(eObject, feature, oldValue, notification.wasSet()); ((InternalEList<FeatureChange>)changes).addUnique(change); } break; } case Notification.MOVE: { if (change == null && changes != null) { EList<Object> oldValue = new BasicEList<Object>((Collection<?>)eObject.eGet(feature)); int position = notification.getPosition(); int oldPosition = (Integer)notification.getOldValue(); oldValue.move(oldPosition, position); change = createFeatureChange(eObject, feature, oldValue, notification.wasSet()); ((InternalEList<FeatureChange>)changes).addUnique(change); } break; } } } protected void handleResource(Notification notification) { Resource resource = null; ResourceChange change = null; if (isRecording()) { resource = (Resource)notification.getNotifier(); change = getResourceChange(resource); } int eventType = notification.getEventType(); switch (eventType) { case Notification.SET: case Notification.UNSET: { if (change == null && resource != null) { EList<Object> oldValue = new BasicEList<Object>(resource.getContents()); int index = notification.getPosition(); if (index != Notification.NO_INDEX) { oldValue.set(index, notification.getOldValue()); } change = createResourceChange(resource, oldValue); getResourceChanges().add(change); Notifier newValue = (Notifier)notification.getNewValue(); if (newValue != null) { addAdapter(newValue); } } break; } case Notification.ADD: { if (change == null && resource != null) { EList<Object> oldValue = new BasicEList<Object>(resource.getContents()); oldValue.remove(notification.getPosition()); change = createResourceChange(resource, oldValue); getResourceChanges().add(change); } Notifier newValue = (Notifier)notification.getNewValue(); addAdapter(newValue); break; } case Notification.ADD_MANY: { if (change == null && resource != null) { EList<Object> oldValue = new BasicEList<Object>(resource.getContents()); int position = notification.getPosition(); for (int i = ((Collection<?>)notification.getNewValue()).size(); --i >= 0;) { oldValue.remove(position); } change = createResourceChange(resource, oldValue); getResourceChanges().add(change); } @SuppressWarnings("unchecked") Collection<Notifier> newValues = (Collection<Notifier>)notification.getNewValue(); for (Notifier newValue : newValues) { addAdapter(newValue); } break; } case Notification.REMOVE: { if (change == null && resource != null) { EList<Object> oldValue = new BasicEList<Object>(resource.getContents()); // If there's no position, the list is being cleared. // int position = notification.getPosition(); if (position == Notification.NO_INDEX) { position = 0; } oldValue.add(position, notification.getOldValue()); change = createResourceChange(resource, oldValue); getResourceChanges().add(change); } break; } case Notification.REMOVE_MANY: { if (change == null && resource != null) { @SuppressWarnings("unchecked") List<Object> removedValues = (List<Object>)notification.getOldValue(); EList<Object> oldValue = new BasicEList<Object>(resource.getContents()); int[] positions = (int[])notification.getNewValue(); if (positions == null) { oldValue.addAll(removedValues); } else { for (int i = 0; i < positions.length; ++i) { oldValue.add(positions[i], removedValues.get(i)); } } change = createResourceChange(resource, oldValue); getResourceChanges().add(change); } break; } case Notification.MOVE: { if (change == null && resource != null) { EList<Object> oldValue = new BasicEList<Object>(resource.getContents()); int position = notification.getPosition(); int oldPosition = (Integer)notification.getOldValue(); oldValue.move(oldPosition, position); change = createResourceChange(resource, oldValue); getResourceChanges().add(change); } break; } } } /** * Handles installation of the adapter * by adding the adapter to each of the directly contained objects. */ public void setTarget(Notifier target) { if (!targetObjects.add(target)) { throw new IllegalStateException("The target should not be set more than once"); } if (loadingTargets) { originalTargetObjects.add(target); } if (target instanceof EObject) { EObject targetEObject = (EObject)target; if (resolveProxies) { for (EObject eObject : targetEObject.eContents()) { addAdapter(eObject); } } else { Iterator<EObject> contents = ((InternalEList<EObject>)targetEObject.eContents()).basicIterator(); while (contents.hasNext()) { // Avoid adding the adapter to unresolved proxies // so that the proxies don't look like objects that have become orphans. // EObject eObject = contents.next(); if (!eObject.eIsProxy()) { addAdapter(eObject); } } } handleTarget(targetEObject); } else { Iterator<?> contents = target instanceof ResourceSet ? ((ResourceSet)target).getResources().iterator() : target instanceof Resource ? ((Resource)target).getContents().iterator() : null; if (contents != null) { while (contents.hasNext()) { Notifier notifier = (Notifier)contents.next(); addAdapter(notifier); } } } } protected void handleTarget(EObject targetEObject) { if (loadingTargets && eObjectToProxyURIMap != null) { eObjectToProxyURIMap.put(targetEObject, EcoreUtil.getURI(targetEObject)); } } public void unsetTarget(Notifier oldTarget) { targetObjects.remove(oldTarget); } protected void addAdapter(Notifier notifier) { if (notifier != getChangeDescription()) { EList<Adapter> eAdapters = notifier.eAdapters(); if (!eAdapters.contains(this)) { eAdapters.add(this); } } } public Notifier getTarget() { return null; } public boolean isAdapterForType(Object type) { return false; } }