blob: 8e0dbca357392afb4c3960863790a17858bdc7ee [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2005 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Common Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/cpl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.core.commands.operations;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import org.eclipse.core.internal.commands.util.Assert;
import org.eclipse.core.internal.commands.util.BatchingOperation;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
/**
* <p>
* A base implementation of IOperationHistory. DefaultOperationHistory implements a
* strict linear undo and redo model. The most recently added operation is
* available for undo, and the most recently undone operation is available for
* redo.
* </p>
* <p>
* If the operation eligible for undo is not in a state where it can be undone,
* then no undo is available. No other operations are consulted. Likewise, if
* the operation available for redo cannot be redone, then no redo is available.
* </p>
*
* @since 3.1
*/
public class DefaultOperationHistory implements IOperationHistory {
protected static final int DEFAULT_LIMIT = 20;
/**
* An operation info status describing the condition that there is no available
* operation for redo.
*/
public static final IStatus NOTHING_TO_REDO_STATUS = new OperationStatus(
IStatus.INFO, OperationStatus.NOTHING_TO_REDO,
"No operation to redo"); //$NON-NLS-1$
/**
* An operation info status describing the condition that there is no available
* operation for undo.
*/
public static final IStatus NOTHING_TO_UNDO_STATUS = new OperationStatus(
IStatus.INFO, OperationStatus.NOTHING_TO_UNDO,
"No operation to undo"); //$NON-NLS-1$
/**
* An operation error status describing the condition that the operation available
* for execution, undo or redo is not in a valid state for the action to be
* performed.
*/
public static final IStatus OPERATION_INVALID_STATUS = new OperationStatus(
IStatus.ERROR, OperationStatus.OPERATION_INVALID,
"Operation is not valid"); //$NON-NLS-1$
protected List fApprovers = new ArrayList();
private HashMap fLimits = new HashMap();
/**
* the list of {@link IOperationHistoryListener}s
*/
protected List fListeners = new ArrayList();
private List fRedo = new ArrayList();
private List fUndo = new ArrayList();
private BatchingOperation fBatchingComposite = null;
/*
* (non-Javadoc)
*
* @see org.eclipse.runtime.operations.IOperationHistory#add(org.eclipse.runtime.operations.IOperation)
*/
public void add(IUndoableOperation operation) {
Assert.isNotNull(operation);
/*
* If we are in the middle of executing an open batching operation, and this is not that
* operation, then we need only add the new operation to the batch. Listeners will be
* notified when the execution and adding of the batched operation is
* complete.
*/
if (fBatchingComposite != null && fBatchingComposite != operation) {
fBatchingComposite.add(operation);
return;
}
// flush redo stack for related contexts
UndoContext[] contexts = operation.getContexts();
for (int i = 0; i < contexts.length; i++) {
flushRedo(contexts[i]);
}
checkUndoLimit(operation);
fUndo.add(operation);
notifyAdd(operation);
}
/*
* (non-Javadoc)
*
* @see org.eclipse.runtime.operations.IOperationHistory#addOperationApprover(org.eclipse.runtime.operations.IOperationApprover)
*/
public void addOperationApprover(IOperationApprover approver) {
fApprovers.add(approver);
}
/*
* (non-Javadoc)
*
* @see org.eclipse.runtime.operations.IOperationHistory#addOperationHistoryListener(org.eclipse.runtime.operations.IOperationHistoryListener)
*/
public void addOperationHistoryListener(IOperationHistoryListener listener) {
fListeners.add(listener);
}
public boolean canRedo(UndoContext context) {
// null context is allowed and passed through
IUndoableOperation operation = getRedoOperation(context);
return (operation != null && operation.canRedo());
}
public boolean canUndo(UndoContext context) {
// null context is allowed and passed through
IUndoableOperation operation = getUndoOperation(context);
return (operation != null && operation.canUndo());
}
/**
* Check the redo limit before adding an operation. In theory the redo limit
* should never be reached, because the redo items are transferred from the undo
* history, which has the same limit. The redo history is cleared whenever a new
* operation is added. We check for completeness since implementations may change
* over time.
*/
private void checkRedoLimit(IUndoableOperation operation) {
UndoContext [] contexts = operation.getContexts();
for (int i=0; i<contexts.length; i++) {
int limit = getLimit(contexts[i]);
if (limit > 0) forceRedoLimit(contexts[i], limit - 1);
}
}
/**
* Check the undo limit before adding an operation.
*/
private void checkUndoLimit(IUndoableOperation operation) {
UndoContext [] contexts = operation.getContexts();
for (int i=0; i<contexts.length; i++) {
int limit = getLimit(contexts[i]);
if (limit > 0) forceUndoLimit(contexts[i], limit - 1);
}
}
/*
* null is valid argument indicating all contexts
*/
public void dispose(UndoContext context, boolean flushUndo,
boolean flushRedo) {
if (flushUndo)
flushUndo(context);
if (flushRedo)
flushRedo(context);
// we currently do not dispose of any limit that was set for the
// context since it may be used again.
}
/**
* Perform the redo. All validity checks have already occurred.
*
* @param monitor
* @param operation
*/
protected IStatus doRedo(IProgressMonitor monitor, IUndoableOperation operation,
boolean flushOnError) {
IStatus status = getRedoApproval(operation);
if (status.isOK()) {
notifyAboutToRedo(operation);
status = operation.redo(monitor);
}
// if successful, the operation is removed from the redo history and
// placed back in the undo history.
if (status.isOK()) {
fRedo.remove(operation);
// Only add the operation to the undo stack if it can indeed be undone.
// This conservatism is added to support the integration of existing
// frameworks (such as Refactoring) that produce undo and redo behavior
// on the fly and cannot guarantee that a successful redo means a
// successful undo will be available.
// See bug #84444
if (operation.canUndo()) {
checkUndoLimit(operation);
fUndo.add(operation);
}
// notify listeners must happen after history is updated
notifyRedone(operation);
} else {
notifyNotOK(operation);
if (flushOnError && status.getSeverity() == IStatus.ERROR) {
remove(operation);
}
}
return status;
}
/**
* Perform the undo. All validity checks have already occurred.
*
* @param monitor
* @param operation
*/
protected IStatus doUndo(IProgressMonitor monitor, IUndoableOperation operation,
boolean flushOnError) {
IStatus status = getUndoApproval(operation);
if (status.isOK()) {
notifyAboutToUndo(operation);
status = operation.undo(monitor);
}
// if successful, the operation is removed from the undo history and
// placed in the redo history.
if (status.isOK()) {
fUndo.remove(operation);
// Only add the operation to the redo stack if it can indeed be redone.
// This conservatism is added to support the integration of existing
// frameworks (such as Refactoring) that produce undo and redo behavior
// on the fly and cannot guarantee that a successful undo means a
// successful redo will be available.
// See bug #84444
if (operation.canRedo()) {
checkRedoLimit(operation);
fRedo.add(operation);
}
// notification occurs after the undo and redo histories are
// adjusted
notifyUndone(operation);
} else {
notifyNotOK(operation);
if (flushOnError && status.getSeverity() == IStatus.ERROR) {
remove(operation);
}
}
return status;
}
public IStatus execute(IUndoableOperation operation, IProgressMonitor monitor) {
Assert.isNotNull(operation);
// error if operation is invalid
if (!operation.canExecute()) {
return OPERATION_INVALID_STATUS;
}
/*
* If we are in the middle of an open batch, then we should add this operation to the
* batch first. We still want to execute it, but we do not want to notify listeners.
*/
boolean merging = false;
if (fBatchingComposite != null) {
// the composite shouldn't be executed explicitly while it is still open
if (fBatchingComposite == operation) {
return OPERATION_INVALID_STATUS;
}
fBatchingComposite.add(operation);
merging = true;
}
/*
* Execute the operation
*/
if (!merging) notifyAboutToExecute(operation);
IStatus status = operation.execute(monitor);
// if successful, the notify listeners are notified and the operation is
// added to the history
if (!merging) {
if (status.isOK()) {
notifyDone(operation);
// Only add the operation to the history if it can indeed be undone.
// This conservatism is added to support the integration of existing
// frameworks (such as Refactoring) that may be using a history to execute
// all operations, even those that are not undoable.
// See bug #84444
if (operation.canUndo()) {
add(operation);
}
} else {
notifyNotOK(operation);
}
}
// all other severities are not interpreted. Simply return the status.
return status;
}
private IUndoableOperation[] filter(List list, UndoContext context) {
/*
* This method is used whenever there is a need to filter the undo or
* redo history on a particular context. Currently there are no caches
* kept to optimize repeated requests for the same filter. If benchmarks
* show this to be a common pattern that causes performances problems,
* we could implement a filtered cache here that is nullified whenever
* the global history changes.
*/
// when the context is null, do not filter the list.
if (context == null)
return (IUndoableOperation[]) list.toArray(new IUndoableOperation[list.size()]);
// otherwise filter on the context
List filtered = new ArrayList();
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
IUndoableOperation operation = (IUndoableOperation) iterator.next();
if (operation.hasContext(context)) {
filtered.add(operation);
}
}
return (IUndoableOperation[]) filtered.toArray(new IUndoableOperation[filtered.size()]);
}
void flushRedo(UndoContext context) {
// null context indicates flushing all
Object[] filtered = filter(fRedo, context);
for (int i = 0; i < filtered.length; i++) {
IUndoableOperation operation = (IUndoableOperation) filtered[i];
if (context == null || operation.getContexts().length == 1) {
// remove the operation if it only has the context or we are flushing all
fRedo.remove(operation);
internalRemove(operation);
} else {
// remove the reference to the context
operation.removeContext(context);
}
}
}
void flushUndo(UndoContext context) {
// null context indicates flushing all
Object[] filtered = filter(fUndo, context);
for (int i = 0; i < filtered.length; i++) {
IUndoableOperation operation = (IUndoableOperation) filtered[i];
if (context == null || operation.getContexts().length == 1) {
// remove the operation if it only has the context or we are flushing all
fUndo.remove(operation);
internalRemove(operation);
} else {
// remove the reference to the context
operation.removeContext(context);
}
}
// there may be an open batch. Notify listeners that it did not complete.
// Since we did not add it, there's no need to notify of its removal.
if (fBatchingComposite != null) {
if (context == null || fBatchingComposite.hasContext(context)) {
notifyNotOK(fBatchingComposite.getPrimaryOperation());
fBatchingComposite = null;
}
}
}
private void forceRedoLimit(UndoContext context, int max) {
Object[] filtered = filter(fRedo, context);
int size = filtered.length;
if (size > 0) {
int index = 0;
while (size > max) {
IUndoableOperation removed = (IUndoableOperation)filtered[index];
if (context == null || removed.getContexts().length == 1) {
/* remove the operation if we are enforcing a global limit or if
* the operation only has the specified context
*/
fRedo.remove(removed);
internalRemove(removed);
} else {
/* if the operation has multiple contexts and we've reached the limit
* for only one of them, then just remove the context, not the operation.
*/
removed.removeContext(context);
}
size--;
index++;
}
}
}
private void forceUndoLimit(UndoContext context, int max) {
Object[] filtered = filter(fUndo, context);
int size = filtered.length;
if (size > 0) {
int index = 0;
while (size > max) {
IUndoableOperation removed = (IUndoableOperation)filtered[index];
if (context == null || removed.getContexts().length == 1) {
/* remove the operation if we are enforcing a global limit or if
* the operation only has the specified context
*/
fUndo.remove(removed);
internalRemove(removed);
} else {
/* if the operation has multiple contexts and we've reached the limit
* for only one of them, then just remove the context, not the operation.
*/
removed.removeContext(context);
}
size--;
index++;
}
}
}
/*
* (non-Javadoc)
*
* @see org.eclipse.runtime.operations.IOperationHistory#getLimit()
*/
public int getLimit(UndoContext context) {
if (!fLimits.containsKey(context)) {
return DEFAULT_LIMIT;
}
return ((Integer)(fLimits.get(context))).intValue();
}
protected IStatus getRedoApproval(IUndoableOperation operation) {
for (int i = 0; i < fApprovers.size(); i++) {
IStatus approval = ((IOperationApprover) fApprovers.get(i))
.proceedRedoing(operation, this);
if (!approval.isOK())
return approval;
}
return Status.OK_STATUS;
}
/*
* null is a valid argument indicating the entire history
*/
public IUndoableOperation[] getRedoHistory(UndoContext context) {
return filter(fRedo, context);
}
/*
* Peek the redo operation. Validity of the operation is not considered.
*/
public IUndoableOperation getRedoOperation(UndoContext context) {
if (context == null) {
if (fRedo.size() > 0) {
IUndoableOperation operation = (IUndoableOperation) fRedo.get(fRedo.size() - 1);
return operation;
}
return null;
}
for (int i = fRedo.size() - 1; i >= 0; i--) {
IUndoableOperation operation = (IUndoableOperation) fRedo.get(i);
if (operation.hasContext(context)) {
return operation;
}
}
return null;
}
protected IStatus getUndoApproval(IUndoableOperation operation) {
for (int i = 0; i < fApprovers.size(); i++) {
IStatus approval = ((IOperationApprover) fApprovers.get(i))
.proceedUndoing(operation, this);
if (!approval.isOK())
return approval;
}
return Status.OK_STATUS;
}
/*
* null is a valid argument indicating the entire history
*/
public IUndoableOperation[] getUndoHistory(UndoContext context) {
return filter(fUndo, context);
}
/*
* Validity of the returned operation is not considered.
*/
public IUndoableOperation getUndoOperation(UndoContext context) {
if (context == null) {
if (fUndo.size() > 0) {
IUndoableOperation operation = (IUndoableOperation) fUndo.get(fUndo.size() - 1);
return operation;
}
return null;
}
for (int i = fUndo.size() - 1; i >= 0; i--) {
IUndoableOperation operation = (IUndoableOperation) fUndo.get(i);
if (operation.hasContext(context)) {
return operation;
}
}
return null;
}
void internalRemove(IUndoableOperation operation) {
operation.dispose();
notifyRemoved(operation);
}
/*
* Notify listeners that an operation is about to execute.
*/
protected void notifyAboutToExecute(IUndoableOperation operation) {
IUndoableOperation op = getNotifyOperation(operation);
OperationHistoryEvent event = new OperationHistoryEvent(
OperationHistoryEvent.ABOUT_TO_EXECUTE, this, op);
preNotifyOperation(op, event);
for (int i = 0; i < fListeners.size(); i++)
((IOperationHistoryListener) fListeners.get(i))
.historyNotification(event);
}
/*
* Notify listeners that an operation is about to redo.
*/
protected void notifyAboutToRedo(IUndoableOperation operation) {
IUndoableOperation op = getNotifyOperation(operation);
OperationHistoryEvent event = new OperationHistoryEvent(
OperationHistoryEvent.ABOUT_TO_REDO, this, op);
preNotifyOperation(op, event);
for (int i = 0; i < fListeners.size(); i++)
((IOperationHistoryListener) fListeners.get(i))
.historyNotification(event);
}
/*
* Notify listeners that an operation is about to undo.
*/
protected void notifyAboutToUndo(IUndoableOperation operation) {
IUndoableOperation op = getNotifyOperation(operation);
OperationHistoryEvent event = new OperationHistoryEvent(
OperationHistoryEvent.ABOUT_TO_UNDO, this, op);
preNotifyOperation(op, event);
for (int i = 0; i < fListeners.size(); i++)
((IOperationHistoryListener) fListeners.get(i))
.historyNotification(event);
}
/*
* Notify listeners that an operation has been added.
*/
protected void notifyAdd(IUndoableOperation operation) {
IUndoableOperation op = getNotifyOperation(operation);
OperationHistoryEvent event = new OperationHistoryEvent(
OperationHistoryEvent.OPERATION_ADDED, this, op);
preNotifyOperation(op, event);
for (int i = 0; i < fListeners.size(); i++)
((IOperationHistoryListener) fListeners.get(i))
.historyNotification(event);
}
/*
* Notify listeners that an operation is done executing.
*/
protected void notifyDone(IUndoableOperation operation) {
IUndoableOperation op = getNotifyOperation(operation);
OperationHistoryEvent event = new OperationHistoryEvent(
OperationHistoryEvent.DONE, this, op);
preNotifyOperation(op, event);
for (int i = 0; i < fListeners.size(); i++)
((IOperationHistoryListener) fListeners.get(i))
.historyNotification(event);
}
/*
* Notify listeners that an operation did not succeed after
* an attempt to execute, undo, or redo was made.
*/
protected void notifyNotOK(IUndoableOperation operation) {
IUndoableOperation op = getNotifyOperation(operation);
OperationHistoryEvent event = new OperationHistoryEvent(
OperationHistoryEvent.OPERATION_NOT_OK, this, op);
preNotifyOperation(op, event);
for (int i = 0; i < fListeners.size(); i++)
((IOperationHistoryListener) fListeners.get(i))
.historyNotification(event);
}
/*
* Notify listeners that an operation was redone.
*/
protected void notifyRedone(IUndoableOperation operation) {
IUndoableOperation op = getNotifyOperation(operation);
OperationHistoryEvent event = new OperationHistoryEvent(
OperationHistoryEvent.REDONE, this, op);
preNotifyOperation(op, event);
for (int i = 0; i < fListeners.size(); i++)
((IOperationHistoryListener) fListeners.get(i))
.historyNotification(event);
}
/*
* Notify listeners that an operation has been removed from the
* history.
*/
protected void notifyRemoved(IUndoableOperation operation) {
IUndoableOperation op = getNotifyOperation(operation);
OperationHistoryEvent event = new OperationHistoryEvent(
OperationHistoryEvent.OPERATION_REMOVED, this, op);
preNotifyOperation(op, event);
for (int i = 0; i < fListeners.size(); i++)
((IOperationHistoryListener) fListeners.get(i))
.historyNotification(event);
}
/*
* Notify listeners that an operation has been undone.
*/
protected void notifyUndone(IUndoableOperation operation) {
IUndoableOperation op = getNotifyOperation(operation);
OperationHistoryEvent event = new OperationHistoryEvent(
OperationHistoryEvent.UNDONE, this, op);
preNotifyOperation(op, event);
for (int i = 0; i < fListeners.size(); i++)
((IOperationHistoryListener) fListeners.get(i))
.historyNotification(event);
}
/*
* A history notification is about to be sent. Notify the operation before hand
* if it implements IHistoryNotificationAwareOperation.
*
* This method is provided for legacy undo frameworks that rely on notification
* from their undo managers before any listeners are notified about changes
* in the operation.
*/
private void preNotifyOperation(IUndoableOperation operation, OperationHistoryEvent event) {
if (operation instanceof IHistoryNotificationAwareOperation) {
((IHistoryNotificationAwareOperation)operation).aboutToNotify(event);
}
}
/*
* A history notification is about to be sent. Answer the operation that should
* be used in the event.
*
* This method is provided so that notifications about batched operations will
* look like notifications about the primary operation.
*/
private IUndoableOperation getNotifyOperation(IUndoableOperation operation) {
if (operation instanceof BatchingOperation) {
return ((BatchingOperation)operation).getPrimaryOperation();
}
return operation;
}
public IStatus redo(UndoContext context, IProgressMonitor monitor) {
// null context is allowed and passed through when getting the operation
IUndoableOperation operation = getRedoOperation(context);
// info if there is no operation
if (operation == null)
return NOTHING_TO_REDO_STATUS;
// error if operation is invalid
if (!operation.canRedo())
return OPERATION_INVALID_STATUS;
return doRedo(monitor, operation, true);
}
public IStatus redoOperation(IUndoableOperation operation, IProgressMonitor monitor) {
Assert.isNotNull(operation);
IStatus status;
if (operation.canRedo()) {
status = getRedoApproval(operation);
if (status.isOK()) {
status = doRedo(monitor, operation, false);
}
} else {
status = OPERATION_INVALID_STATUS;
}
return status;
}
public void remove(IUndoableOperation operation) {
if (fUndo.contains(operation)) {
fUndo.remove(operation);
internalRemove(operation);
} else if (fRedo.contains(operation)) {
fRedo.remove(operation);
internalRemove(operation);
}
}
/*
* (non-Javadoc)
*
* @see org.eclipse.runtime.operations.IOperationHistory#removeOperationApprover(org.eclipse.runtime.operations.IOperationApprover)
*/
public void removeOperationApprover(IOperationApprover approver) {
fApprovers.remove(approver);
}
/*
* (non-Javadoc)
*
* @see org.eclipse.runtime.operations.IOperationHistory#removeOperationHistoryListener(org.eclipse.runtime.operations.IOperationHistoryListener)
*/
public void removeOperationHistoryListener(
IOperationHistoryListener listener) {
fListeners.remove(listener);
}
/*
* (non-Javadoc)
*
* @see org.eclipse.runtime.operations.IOperationHistory#setLimit(int)
*/
public void setLimit(UndoContext context, int limit) {
Assert.isTrue(limit > 0);
/*
* The limit checking methods interpret a null context as a global limit to be
* enforced. We do not wish to support a global limit in this implementation,
* so we throw an exception for a null context. The rest of the implementation
* can handle a null context, so subclasses can override this if a global limit
* is desired.
*/
Assert.isNotNull(context);
fLimits.put(context, new Integer(limit));
forceUndoLimit(context, limit);
forceRedoLimit(context, limit);
}
/*
* (non-Javadoc)
*
* @see org.eclipse.runtime.operations.IOperationHistory#undo(org.eclipse.core.runtime.IProgressMonitor)
*/
public IStatus undo(UndoContext context, IProgressMonitor monitor) {
// null context is allowed and passed through when getting the operation
IUndoableOperation operation = getUndoOperation(context);
// info if there is no operation
if (operation == null)
return NOTHING_TO_UNDO_STATUS;
// error if operation is invalid
if (!operation.canUndo())
return OPERATION_INVALID_STATUS;
return doUndo(monitor, operation, true);
}
/**
* {@inheritDoc}
*/
public IStatus undoOperation(IUndoableOperation operation, IProgressMonitor monitor) {
Assert.isNotNull(operation);
IStatus status;
if (operation.canUndo()) {
status = getUndoApproval(operation);
if (status.isOK()) {
status = doUndo(monitor, operation, false);
}
} else {
status = OPERATION_INVALID_STATUS;
}
return status;
}
/**
* {@inheritDoc}
*/
public void openOperation(IUndoableOperation operation) {
if (fBatchingComposite != null) {
closeOperation();
}
fBatchingComposite = new BatchingOperation(operation);
notifyAboutToExecute(operation);
}
/**
* {@inheritDoc}
*/
public void closeOperation() {
if (fBatchingComposite != null) {
notifyDone(fBatchingComposite.getPrimaryOperation());
add(fBatchingComposite);
fBatchingComposite = null;
}
}
}