/*****************************************************************************
* Copyright (c) 2011 Atos.
*
*
* 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:
* Mathieu Velten (Atos) - Initial API and implementation
* Arthur Daussy (Atos) - 363826: [Model Explorer] Drag and drop and undo, incorrect behavior
*
*****************************************************************************/
package org.eclipse.papyrus.commands;
import java.util.Collection;
import java.util.EventObject;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.core.commands.ExecutionException;
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.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Status;
import org.eclipse.emf.common.command.Command;
import org.eclipse.emf.common.command.CommandStackListener;
import org.eclipse.emf.common.notify.Notification;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.transaction.NotificationFilter;
import org.eclipse.emf.transaction.ResourceSetChangeEvent;
import org.eclipse.emf.transaction.ResourceSetListenerImpl;
import org.eclipse.emf.transaction.RollbackException;
import org.eclipse.emf.transaction.Transaction;
import org.eclipse.emf.transaction.impl.AbstractTransactionalCommandStack;
import org.eclipse.emf.transaction.impl.EMFCommandTransaction;
import org.eclipse.emf.transaction.impl.InternalTransaction;
import org.eclipse.emf.transaction.impl.InternalTransactionalEditingDomain;
import org.eclipse.emf.transaction.impl.TriggerCommandTransaction;
import org.eclipse.emf.transaction.util.TriggerCommand;
import org.eclipse.emf.workspace.EMFCommandOperation;
import org.eclipse.emf.workspace.IResourceUndoContextPolicy;
import org.eclipse.emf.workspace.IWorkspaceCommandStack;
import org.eclipse.emf.workspace.ResourceUndoContext;
import org.eclipse.emf.workspace.impl.EMFOperationTransaction;
import org.eclipse.emf.workspace.impl.WorkspaceCommandStackImpl;
import org.eclipse.emf.workspace.internal.EMFWorkspacePlugin;
import org.eclipse.emf.workspace.internal.EMFWorkspaceStatusCodes;
import org.eclipse.emf.workspace.internal.Tracing;
import org.eclipse.emf.workspace.internal.l10n.Messages;
import org.eclipse.gmf.runtime.emf.commands.core.command.EditingDomainUndoContext;
import org.eclipse.osgi.util.NLS;
/**
* Copied from WorkspaceCommandStackImpl but modify in order to change the
* IUndoContext. We want to make it point the the TransactionalEditingDomain. To
* see what really change in this class from original implementation look for
* "HAS CHANGE FROM ORIGINAL IMPLEMENTATION" in Java Doc.
*
*/
public class NotifyingWorkspaceCommandStack extends AbstractTransactionalCommandStack// AbstractTransactionalCommandStack
implements IWorkspaceCommandStack {
private final IOperationHistory history;
private DomainListener domainListener;
private IResourceUndoContextPolicy undoContextPolicy = IResourceUndoContextPolicy.DEFAULT;
private IUndoableOperation currentOperation;
private Set<Resource> historyAffectedResources;
/**
* HAS CHANGE FROM ORIGINAL IMPLEMENTATION TO USE {@link EditingDomainUndoContext}
*/
private IUndoContext defaultContext = null;
private IUndoContext savedContext = null;
private IUndoableOperation mostRecentOperation;
/**
* Initializes me with the operation history to which I delegate command
* execution.
*
* @param history
* my operation history
*/
public NotifyingWorkspaceCommandStack(IOperationHistory history) {
super();
this.history = history;
domainListener = new DomainListener();
defaultContext = new UndoContext() {
@Override
public String getLabel() {
return getDefaultUndoContextLabel();
}
@Override
public String toString() {
return getLabel();
}
};
}
/**
* map with registered listeners and the corresponding proxy registered to
* actual map
*/
private Map<CommandStackListener, IOperationHistoryListener> proxyOperationListeners = new HashMap<CommandStackListener, IOperationHistoryListener>();
@Override
public void addCommandStackListener(final CommandStackListener listener) {
removeCommandStackListener(listener);
IOperationHistoryListener proxy = new IOperationHistoryListener() {
public void historyNotification(OperationHistoryEvent event) {
int type = event.getEventType();
// emf stack only needs to be notified when an operation is
// finished
if(OperationHistoryEvent.DONE == type || OperationHistoryEvent.REDONE == type || OperationHistoryEvent.UNDONE == type) {
listener.commandStackChanged(new EventObject(NotifyingWorkspaceCommandStack.this));
}
}
};
getOperationHistory().addOperationHistoryListener(proxy);
proxyOperationListeners.put(listener, proxy);
}
@Override
public void removeCommandStackListener(CommandStackListener listener) {
IOperationHistoryListener proxy = proxyOperationListeners.remove(listener);
if(proxy != null) {
getOperationHistory().removeOperationHistoryListener(proxy);
}
}
/**
* Extends the superclass implementation to add/remove listeners on the
* editing domain. HAS CHANGE FROM ORIGINAL IMPLEMENTATION TO USE {@link EditingDomainUndoContext}
*/
@Override
public void setEditingDomain(InternalTransactionalEditingDomain domain) {
InternalTransactionalEditingDomain oldDomain = getDomain();
if(oldDomain != null) {
oldDomain.removeResourceSetListener(domainListener);
history.removeOperationHistoryListener(domainListener);
}
super.setEditingDomain(domain);
/*
* HAS CHANGE FROM ORIGINAL IMPLEMENTATION TO USE {@link
* EditingDomainUndoContext}
*/
if(getDomain() != null) {
boolean domainHasChanged = oldDomain == null || !oldDomain.equals(getDomain());
if(domainHasChanged) {
defaultContext = new EditingDomainUndoContext(domain, getDefaultUndoContextLabel());
}
}
if(domain != null) {
history.addOperationHistoryListener(domainListener);
domain.addResourceSetListener(domainListener);
}
}
// Documentation copied from the method specification
public final IOperationHistory getOperationHistory() {
return history;
}
// Documentation copied from the method specification
public final IUndoContext getDefaultUndoContext() {
return defaultContext;
}
/**
* Obtains the label to display for the default undo context that I apply to
* operations executed through me as {@link Command}s. Subclasses may
* override to customize the label.
*
* @return my default undo context label
*
* @since 1.2
*/
protected String getDefaultUndoContextLabel() {
String domainID = (getDomain() == null) ? null : getDomain().getID();
if(domainID == null) {
domainID = String.valueOf(domainID); // guaranteed to be safe
}
return NLS.bind(Messages.cmdStkCtxLabel, domainID);
}
private final IUndoContext getSavedContext() {
if(savedContext == null) {
savedContext = new UndoContext() {
@Override
public String getLabel() {
return getSavepointUndoContextLabel();
}
@Override
public String toString() {
return getLabel();
}
};
}
return savedContext;
}
/**
* Obtains the label to display for the save-point undo context that I apply
* to the last operation in my {@linkplain #getDefaultUndoContext() default
* undo context} that was executed at the time save was performed (as
* indicated by invocation of the {@link #saveIsDone()} method). Subclasses
* may override to customize the label.
*
* @return my save-point undo context label
*
* @since 1.2
*/
protected String getSavepointUndoContextLabel() {
String domainID = (getDomain() == null) ? null : getDomain().getID();
if(domainID == null) {
domainID = String.valueOf(domainID); // guaranteed to be safe
}
return NLS.bind(Messages.cmdStkSaveCtxLabel, domainID);
}
/**
* {@inheritDoc}
*
* @since 1.1
*/
@Override
protected void doExecute(Command command, Map<?, ?> options) throws InterruptedException, RollbackException {
EMFCommandOperation oper = new EMFCommandOperation(getDomain(), command, options);
// add the appropriate context
oper.addContext(getDefaultUndoContext());
try {
IStatus status = history.execute(oper, new NullProgressMonitor(), null);
if(status.getSeverity() >= IStatus.ERROR) {
// the transaction must have rolled back if the status was
// error or worse
RollbackException exc = new RollbackException(status);
Tracing.throwing(WorkspaceCommandStackImpl.class, "execute", exc); //$NON-NLS-1$
throw exc;
}
notifyListeners();
} catch (ExecutionException e) {
Tracing.catching(WorkspaceCommandStackImpl.class, "execute", e); //$NON-NLS-1$
command.dispose();
if(e.getCause() instanceof RollbackException) {
// throw the rollback
RollbackException exc = (RollbackException)e.getCause();
Tracing.throwing(WorkspaceCommandStackImpl.class, "execute", exc); //$NON-NLS-1$
throw exc;
} else if(e.getCause() instanceof RuntimeException) {
// throw the programming error
RuntimeException exc = (RuntimeException)e.getCause();
Tracing.throwing(WorkspaceCommandStackImpl.class, "execute", exc); //$NON-NLS-1$
throw exc;
} else {
// log the problem. We can't rethrow whatever it was
handleError(e);
}
}
}
/**
* Queries whether we can undo my default undo context in my operation
* history.
*/
@Override
public boolean canUndo() {
return getOperationHistory().canUndo(getDefaultUndoContext());
}
/**
* Undoes my default undo context in my operation history.
*/
@Override
public void undo() {
try {
getOperationHistory().undo(getDefaultUndoContext(), new NullProgressMonitor(), null);
} catch (ExecutionException e) {
Tracing.catching(WorkspaceCommandStackImpl.class, "undo", e); //$NON-NLS-1$
// can't throw anything from this method
handleError(e);
} finally {
// notify even if there was an error; clients should check to see
// that the command stack is flushed
notifyListeners();
}
}
/**
* Queries whether we can redo my default undo context in my operation
* history.
*/
@Override
public boolean canRedo() {
return getOperationHistory().canRedo(getDefaultUndoContext());
}
/**
* Redoes my default undo context in my operation history.
*/
@Override
public void redo() {
try {
getOperationHistory().redo(getDefaultUndoContext(), new NullProgressMonitor(), null);
} catch (ExecutionException e) {
Tracing.catching(WorkspaceCommandStackImpl.class, "redo", e); //$NON-NLS-1$
// can't throw anything from this method
handleError(e);
} finally {
// notify even if there was an error; clients should check to see
// that the command stack is flushed
notifyListeners();
}
}
/**
* Disposes my default undo context in my operation history.
*/
@Override
public void flush() {
getOperationHistory().dispose(getDefaultUndoContext(), true, true, true);
if(savedContext != null) {
getOperationHistory().dispose(getSavedContext(), true, true, true);
savedContext = null;
}
}
/**
* Gets the command from the most recently executed, done, or redone
* operation.
*/
@Override
public Command getMostRecentCommand() {
Command result = null;
if(mostRecentOperation instanceof EMFCommandOperation) {
result = ((EMFCommandOperation)mostRecentOperation).getCommand();
}
return result;
}
/**
* Gets the command from the top of the undo history, if any.
*/
@Override
public Command getUndoCommand() {
Command result = null;
IUndoableOperation topOperation = getOperationHistory().getUndoOperation(getDefaultUndoContext());
if(topOperation instanceof EMFCommandOperation) {
result = ((EMFCommandOperation)topOperation).getCommand();
}
return result;
}
/**
* Gets the command from the top of the redo history, if any.
*/
@Override
public Command getRedoCommand() {
Command result = null;
IUndoableOperation topOperation = getOperationHistory().getRedoOperation(getDefaultUndoContext());
if(topOperation instanceof EMFCommandOperation) {
result = ((EMFCommandOperation)topOperation).getCommand();
}
return result;
}
// Documentation copied from the method specification
public EMFCommandTransaction createTransaction(Command command, Map<?, ?> options) throws InterruptedException {
EMFCommandTransaction result;
if(command instanceof TriggerCommand) {
result = new TriggerCommandTransaction((TriggerCommand)command, getDomain(), options);
} else {
result = new EMFOperationTransaction(command, getDomain(), options);
}
result.start();
return result;
}
// Documentation copied from the method specification
public void executeTriggers(Command command, List<Command> triggers, Map<?, ?> options) throws InterruptedException, RollbackException {
if(!triggers.isEmpty()) {
TriggerCommand trigger = (command == null) ? new TriggerCommand(triggers) : new TriggerCommand(command, triggers);
InternalTransaction tx = createTransaction(trigger, makeTriggerTransactionOptions(options));
try {
trigger.execute();
InternalTransaction parent = (InternalTransaction)tx.getParent();
// shouldn't be null if we're executing triggers!
if(parent != null) {
parent.addTriggers(trigger);
}
// commit the transaction now
tx.commit();
} catch (RuntimeException e) {
Tracing.catching(WorkspaceCommandStackImpl.class, "executeTriggers", e); //$NON-NLS-1$
IStatus status;
if(e instanceof OperationCanceledException) {
status = Status.CANCEL_STATUS;
} else {
status = new Status(IStatus.ERROR, EMFWorkspacePlugin.getPluginId(), EMFWorkspaceStatusCodes.PRECOMMIT_FAILED, Messages.precommitFailed, e);
}
RollbackException rbe = new RollbackException(status);
Tracing.throwing(WorkspaceCommandStackImpl.class, "executeTriggers", rbe); //$NON-NLS-1$
throw rbe;
} finally {
if((tx != null) && (tx.isActive())) {
// roll back because an uncaught exception occurred
rollback(tx);
}
}
}
}
// Documentation copied from the method specification
public void dispose() {
setEditingDomain(null); // remove listeners
domainListener = null;
historyAffectedResources = null;
mostRecentOperation = null;
// remove listeners registered in opertationHistory
Collection<IOperationHistoryListener> values = proxyOperationListeners.values();
for(IOperationHistoryListener proxy : values) {
getOperationHistory().removeOperationHistoryListener(proxy);
}
proxyOperationListeners.clear();
}
/**
* Obtains my resource undo-context policy.
*
* @return my resource undo-context policy
*
* @since 1.3
*/
public IResourceUndoContextPolicy getResourceUndoContextPolicy() {
return undoContextPolicy;
}
/**
* Sets my resource undo-context policy.
*
* @param policy
* my new policy, or <code>null</code> to restore the default
*
* @since 1.3
*/
public void setResourceUndoContextPolicy(IResourceUndoContextPolicy policy) {
this.undoContextPolicy = policy;
}
/**
* A listener on the editing domain and operation history that tracks which
* resources are changed by an operation and attaches the appropriate {@link ResourceUndoContext} to it when it completes.
*
* @author Christian W. Damus (cdamus)
*/
private class DomainListener extends ResourceSetListenerImpl implements IOperationHistoryListener {
public void historyNotification(OperationHistoryEvent event) {
final IUndoableOperation operation = event.getOperation();
switch(event.getEventType()) {
case OperationHistoryEvent.ABOUT_TO_EXECUTE:
// set up to remember affected resources in case we make EMF
// changes
currentOperation = operation;
historyAffectedResources = new java.util.HashSet<Resource>();
break;
case OperationHistoryEvent.DONE:
if((historyAffectedResources != null) && !historyAffectedResources.isEmpty()) {
// add my undo context to the operation that has
// completed, but only if the operation actually changed
// any of my resources (in case this history is shared
// with other domains)
for(Resource next : historyAffectedResources) {
operation.addContext(new ResourceUndoContext(getDomain(), next));
}
}
currentOperation = null;
historyAffectedResources = null;
if(operation.hasContext(getDefaultUndoContext())) {
mostRecentOperation = operation;
}
break;
case OperationHistoryEvent.OPERATION_NOT_OK:
// just forget about the context because this operation
// failed
currentOperation = null;
historyAffectedResources = null;
break;
case OperationHistoryEvent.UNDONE:
case OperationHistoryEvent.REDONE:
if(operation.hasContext(getDefaultUndoContext())) {
mostRecentOperation = operation;
}
break;
case OperationHistoryEvent.OPERATION_REMOVED:
if(operation == mostRecentOperation) {
mostRecentOperation = null;
}
break;
}
}
@Override
public void resourceSetChanged(ResourceSetChangeEvent event) {
IUndoableOperation operation = null;
Set<Resource> unloaded = getUnloadedResources(event.getNotifications());
if(unloaded != null) {
// dispose their undo contexts
for(Resource next : unloaded) {
getOperationHistory().dispose(new ResourceUndoContext(getDomain(), next), true, true, true);
}
}
Transaction tx = event.getTransaction();
if(tx != null) {
operation = (IUndoableOperation)tx.getOptions().get(EMFWorkspacePlugin.OPTION_OWNING_OPERATION);
}
if(operation == null) {
operation = currentOperation;
}
if(operation != null) {
Set<Resource> affectedResources = getResourceUndoContextPolicy().getContextResources(operation, event.getNotifications());
if(unloaded != null) {
// don't add these resources to the operation
affectedResources.removeAll(unloaded);
}
if(!affectedResources.isEmpty()) {
// add any resource undo contexts to this operation that are
// not already applied
for(Resource next : affectedResources) {
ResourceUndoContext ctx = new ResourceUndoContext(getDomain(), next);
if(!operation.hasContext(ctx)) {
operation.addContext(ctx);
}
}
}
if(historyAffectedResources != null) {
// there is an operation executing on our history that is
// affecting my editing domain. Remember the affected
// resources.
historyAffectedResources.addAll(affectedResources);
}
}
}
/**
* Finds resources that have sent unload notifications.
*
* @param notifications
* notifications received from a transaction
* @return a set of resources that the notifications indicate have been
* unloaded, or <code>null</code> if none
*/
private Set<Resource> getUnloadedResources(Collection<Notification> notifications) {
Set<Resource> result = null;
for(Notification next : notifications) {
if(NotificationFilter.RESOURCE_UNLOADED.matches(next)) {
if(result == null) {
result = new java.util.HashSet<Resource>();
}
result.add((Resource)next.getNotifier());
}
}
return result;
}
@Override
public boolean isPostcommitOnly() {
// only interested in post-commit "resourceSetChanged" event
return true;
}
}
@Override
public boolean isSaveNeeded() {
// We override the execute method and never call the super
// implementation
// so we have to implement the isSaveNeeded method ourselves.
IUndoableOperation nextUndoableOperation = history.getUndoOperation(getDefaultUndoContext());
if(nextUndoableOperation == null) {
return savedContext != null;
}
return savedContext != null ? !nextUndoableOperation.hasContext(getSavedContext()) : true;
}
@Override
public void saveIsDone() {
// We override the execute method and never call the super
// implementation
// so we have to implement the saveIsDone method ourselves.
if(savedContext != null) {
// The save context is only stored on one operation. We must
// remove it from any other operation that may have contained it
// before.
IUndoableOperation[] undoableOperations = history.getUndoHistory(getSavedContext());
for(int i = 0; i < undoableOperations.length; i++) {
undoableOperations[i].removeContext(getSavedContext());
}
IUndoableOperation[] redoableOperations = history.getRedoHistory(getSavedContext());
for(int i = 0; i < redoableOperations.length; i++) {
redoableOperations[i].removeContext(getSavedContext());
}
}
IUndoableOperation nextUndoableOperation = history.getUndoOperation(getDefaultUndoContext());
if(nextUndoableOperation == null) {
return;
}
nextUndoableOperation.addContext(getSavedContext());
}
}