/** | |
* Copyright (c) 2008 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 Corporation - initial API and implementation | |
*/ | |
package org.eclipse.egf.core.domain; | |
import java.lang.ref.WeakReference; | |
import java.util.HashMap; | |
import java.util.HashSet; | |
import java.util.Map; | |
import java.util.Set; | |
import java.util.WeakHashMap; | |
import org.eclipse.core.commands.operations.IOperationHistory; | |
import org.eclipse.core.commands.operations.IOperationHistoryListener; | |
import org.eclipse.core.commands.operations.IUndoContext; | |
import org.eclipse.core.commands.operations.IUndoableOperation; | |
import org.eclipse.core.commands.operations.OperationHistoryEvent; | |
import org.eclipse.core.commands.operations.UndoContext; | |
import org.eclipse.egf.core.l10n.EGFCoreMessages; | |
import org.eclipse.emf.common.command.CommandStack; | |
import org.eclipse.emf.common.notify.Notification; | |
import org.eclipse.emf.ecore.resource.Resource; | |
import org.eclipse.emf.ecore.resource.ResourceSet; | |
import org.eclipse.emf.transaction.NotificationFilter; | |
import org.eclipse.emf.transaction.ResourceSetChangeEvent; | |
import org.eclipse.emf.transaction.ResourceSetListener; | |
import org.eclipse.emf.transaction.ResourceSetListenerImpl; | |
import org.eclipse.emf.transaction.TransactionalEditingDomain; | |
import org.eclipse.emf.transaction.TransactionalEditingDomainEvent; | |
import org.eclipse.emf.transaction.TransactionalEditingDomainListenerImpl; | |
import org.eclipse.emf.transaction.TransactionalEditingDomain.Lifecycle; | |
import org.eclipse.emf.transaction.util.TransactionUtil; | |
import org.eclipse.emf.workspace.IWorkspaceCommandStack; | |
import org.eclipse.emf.workspace.ResourceUndoContext; | |
import org.eclipse.osgi.util.NLS; | |
/** | |
* Manages the <code>isModified</code> state of resources in a given editing | |
* domain as operations are executed, undone and redone on the operation | |
* history. | |
* <P> | |
* This allows clients to use the <code>isModified</code> state of a resource to | |
* determine whether or not the resource is dirty and can be saved. | |
* | |
* @author ldamus | |
* @since 1.2 | |
* comes from org.eclipse.gmf.runtime.emf.core.resources.GMFResourceModificationManager | |
*/ | |
public class ResourceModificationManager { | |
/** | |
* Keeps track of the modification manager for each editing domain. Only one | |
* modification manager can ever be created for a given editing domain. Keys | |
* are WeakReferences because the modification manager has a reference back | |
* to its editing domain key. | |
*/ | |
private static Map<TransactionalEditingDomain, WeakReference<ResourceModificationManager>> managerRegistry = new WeakHashMap<TransactionalEditingDomain, WeakReference<ResourceModificationManager>>(); | |
/** | |
* Creates a new resource modification manager for <code>domain</code>, if | |
* the <code>domain</code>'s command stack is integrated with an | |
* <code>IOperationHistory</code>. The <code>isModified</code> state of a | |
* resource in <code>domain</code> will be set to <code>false</code> when | |
* the last operation affecting that resource is undone on the history. | |
* | |
* @param domain | |
* the editing domain | |
* @return the resource modification manager, or <code>null</code> if | |
* <code>domain</code> is not integrated with an operation history | |
*/ | |
public static synchronized ResourceModificationManager manage(TransactionalEditingDomain domain) { | |
// make sure we only instantiate one manager per editing domain | |
WeakReference<ResourceModificationManager> reference = managerRegistry.get(domain); | |
ResourceModificationManager result = reference != null ? reference.get() : null; | |
if (result == null) { | |
CommandStack stack = domain.getCommandStack(); | |
if (stack instanceof IWorkspaceCommandStack) { | |
IOperationHistory history = ((IWorkspaceCommandStack) stack).getOperationHistory(); | |
if (history != null) { | |
final ResourceModificationManager manager = new ResourceModificationManager(domain, history); | |
managerRegistry.put(domain, new WeakReference<ResourceModificationManager>(manager)); | |
result = manager; | |
// dispose the modification manager when the domain is | |
// disposed | |
Lifecycle lifecycle = TransactionUtil.getAdapter(domain, Lifecycle.class); | |
if (lifecycle != null) { | |
lifecycle.addTransactionalEditingDomainListener(new TransactionalEditingDomainListenerImpl() { | |
@Override | |
public void editingDomainDisposing(TransactionalEditingDomainEvent event) { | |
manager.dispose(); | |
} | |
}); | |
} | |
} | |
} | |
} | |
return result; | |
} | |
/** | |
* A filter matching "resource is no longer modified" events. | |
*/ | |
private static final NotificationFilter RESOURCE_UNMODIFIED = new NotificationFilter.Custom() { | |
@Override | |
public boolean matches(Notification notification) { | |
return (notification.getNotifier() instanceof Resource) && (notification.getFeatureID(Resource.class) == Resource.RESOURCE__IS_MODIFIED) && notification.getOldBooleanValue() && !notification.getNewBooleanValue(); | |
} | |
}; | |
private TransactionalEditingDomain domain; | |
private IOperationHistory history; | |
private ResourceSetListener domainListener; | |
private IOperationHistoryListener historyListener; | |
private Map<Resource, IUndoContext> saveContexts; | |
private IUndoableOperation currentOperation; | |
/** | |
* Private constructor to prevent instantiation by clients. Clients must use {@link #manage(TransactionalEditingDomain)} to construct a new instance. | |
* | |
* @param domain | |
* the editing domain | |
* @param history | |
* the operation history | |
*/ | |
private ResourceModificationManager(TransactionalEditingDomain domain, IOperationHistory history) { | |
this.domain = domain; | |
this.history = history; | |
domain.addResourceSetListener(getDomainListener()); | |
history.addOperationHistoryListener(getHistoryListener()); | |
} | |
/** | |
* Gets the resource set listener listener, which manages the save-point | |
* context for operations executed on the history when the resource is saved | |
* or unloaded. | |
* | |
* @return the resource set listener | |
*/ | |
private ResourceSetListener getDomainListener() { | |
if (domainListener == null) { | |
domainListener = new ResourceSetListenerImpl(RESOURCE_UNMODIFIED.or(NotificationFilter.RESOURCE_UNLOADED)) { | |
@Override | |
public void resourceSetChanged(ResourceSetChangeEvent event) { | |
for (Notification n : event.getNotifications()) { | |
Resource resource = (Resource) n.getNotifier(); | |
switch (n.getFeatureID(Resource.class)) { | |
case Resource.RESOURCE__IS_MODIFIED: | |
applySaveContext(resource); | |
break; | |
case Resource.RESOURCE__IS_LOADED: | |
disposeSaveContext(resource); | |
break; | |
} | |
} | |
} | |
@Override | |
public boolean isPostcommitOnly() { | |
return true; | |
} | |
}; | |
} | |
return domainListener; | |
} | |
/** | |
* Gets the operation history listener, which manages the | |
* <code>isModified</code> state of the resources. | |
* | |
* @return the operation history listener | |
*/ | |
private IOperationHistoryListener getHistoryListener() { | |
if (historyListener == null) { | |
historyListener = new IOperationHistoryListener() { | |
public void historyNotification(OperationHistoryEvent event) { | |
int type = event.getEventType(); | |
switch (type) { | |
case OperationHistoryEvent.ABOUT_TO_EXECUTE: | |
case OperationHistoryEvent.ABOUT_TO_UNDO: | |
case OperationHistoryEvent.ABOUT_TO_REDO: | |
// Remember the operation in order to apply the | |
// save context to it if the isModified is set to false | |
// during execute, undo or redo. For undo, the save | |
// context goes on next undoable operation on the | |
// history. | |
currentOperation = event.getOperation(); | |
break; | |
case OperationHistoryEvent.OPERATION_NOT_OK: | |
currentOperation = null; | |
break; | |
case OperationHistoryEvent.DONE: { | |
currentOperation = null; | |
IUndoableOperation operation = event.getOperation(); | |
Set<Resource> affectedResources = getAffectedResourcesInDomain(operation); | |
for (Resource r : affectedResources) { | |
ResourceUndoContext context = new ResourceUndoContext(domain, r); | |
IUndoableOperation[] undoHistory = history.getUndoHistory(context); | |
if (undoHistory.length >= history.getLimit(context)) { | |
// We've reached the limit for this context; | |
// initialize the save context to indicate that | |
// we can't undo to the last saved state | |
getSaveContext(r); | |
} | |
} | |
break; | |
} | |
case OperationHistoryEvent.UNDONE: | |
case OperationHistoryEvent.REDONE: { | |
currentOperation = null; | |
IUndoableOperation operation = event.getOperation(); | |
Set<Resource> affectedResources = getAffectedResourcesInDomain(operation); | |
for (Resource r : affectedResources) { | |
IUndoContext saveContext = getSaveContexts().get(r); | |
IUndoableOperation nextUndoableOperation = getNextUndoableOperation(r); | |
boolean atStart = saveContext == null && nextUndoableOperation == null; | |
boolean atSaveContext = saveContext != null && nextUndoableOperation != null && nextUndoableOperation.hasContext(saveContext); | |
if (atStart || atSaveContext) { | |
r.setModified(false); | |
} | |
} | |
} | |
} | |
} | |
}; | |
} | |
return historyListener; | |
} | |
private Map<Resource, IUndoContext> getSaveContexts() { | |
if (saveContexts == null) { | |
saveContexts = new HashMap<Resource, IUndoContext>(); | |
} | |
return saveContexts; | |
} | |
private IUndoableOperation getNextUndoableOperation(Resource resource) { | |
return history.getUndoOperation(new ResourceUndoContext(domain, resource)); | |
} | |
private IUndoContext getSaveContext(final Resource resource) { | |
IUndoContext saveContext = getSaveContexts().get(resource); | |
if (saveContext == null) { | |
saveContext = new UndoContext() { | |
@Override | |
public String getLabel() { | |
return NLS.bind(EGFCoreMessages.saveContextLabel, resource.getURI()); | |
} | |
@Override | |
public String toString() { | |
return getLabel(); | |
} | |
}; | |
getSaveContexts().put(resource, saveContext); | |
} | |
return saveContext; | |
} | |
private Set<Resource> getAffectedResourcesInDomain(IUndoableOperation operation) { | |
Set<Resource> result = new HashSet<Resource>(); | |
Set<Resource> affectedResources = ResourceUndoContext.getAffectedResources(operation); | |
for (Resource resource : affectedResources) { | |
ResourceSet resourceSet = resource.getResourceSet(); | |
if (domain.getResourceSet().equals(resourceSet)) { | |
result.add(resource); | |
} | |
} | |
return result; | |
} | |
private void applySaveContext(Resource resource) { | |
IUndoContext saveContext = getSaveContexts().get(resource); | |
if (saveContext != null) { | |
// Remove the save context from existing operations | |
IUndoableOperation[] undoableOperations = history.getUndoHistory(saveContext); | |
for (IUndoableOperation op : undoableOperations) { | |
op.removeContext(saveContext); | |
} | |
IUndoableOperation[] redoableOperations = history.getRedoHistory(saveContext); | |
for (IUndoableOperation op : redoableOperations) { | |
op.removeContext(saveContext); | |
} | |
} | |
IUndoableOperation operation = null; | |
IUndoableOperation nextUndoable = getNextUndoableOperation(resource); | |
if (currentOperation != null) { | |
if (currentOperation == nextUndoable) { | |
// we're undoing; get the previous operation on the history | |
IUndoableOperation[] undoableOperations = history.getUndoHistory(new ResourceUndoContext(domain, resource)); | |
for (int i = undoableOperations.length - 1; i >= 0; i--) { | |
if (currentOperation != undoableOperations[i]) { | |
operation = undoableOperations[i]; | |
break; | |
} | |
} | |
} else { | |
operation = currentOperation; | |
} | |
} else { | |
operation = nextUndoable; | |
} | |
if (operation != null) { | |
// apply the save context | |
operation.addContext(getSaveContext(resource)); | |
} else { | |
// clear the save context; required if we save after undoing the | |
// last thing on the stack | |
getSaveContexts().remove(resource); | |
} | |
} | |
private void disposeSaveContext(Resource resource) { | |
IUndoContext saveContext = getSaveContexts().get(resource); | |
if (saveContext != null) { | |
history.dispose(saveContext, true, true, true); | |
getSaveContexts().remove(resource); | |
} | |
} | |
private void dispose() { | |
managerRegistry.remove(domain); | |
if (saveContexts != null) { | |
for (Resource r : saveContexts.keySet()) { | |
disposeSaveContext(r); | |
} | |
} | |
if (domainListener != null) { | |
domain.removeResourceSetListener(domainListener); | |
} | |
if (historyListener != null) { | |
history.removeOperationHistoryListener(historyListener); | |
} | |
currentOperation = null; | |
domain = null; | |
domainListener = null; | |
history = null; | |
historyListener = null; | |
saveContexts = null; | |
} | |
} |