Package org.eclipse.core.commands.operations

Source Code of org.eclipse.core.commands.operations.DefaultOperationHistory

/*******************************************************************************
* Copyright (c) 2005, 2007 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.core.commands.operations;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.eclipse.core.commands.ExecutionException;
import org.eclipse.core.commands.util.Tracing;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.ISafeRunnable;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.ListenerList;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.SafeRunner;
import org.eclipse.core.runtime.Status;

/**
* <p>
* A base implementation of IOperationHistory that implements a 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 considered. Likewise, if
* the operation available for redo cannot be redone, then no redo is available.
* </p>
* <p>
* Implementations for the direct undo and redo of a specified operation are
* available. If a strict linear undo is to be enforced, than an
* IOperationApprover should be installed that prevents undo and redo of any
* operation that is not the most recently undone or redone operation in all of
* its undo contexts.
* </p>
* <p>
* The data structures used by the DefaultOperationHistory are synchronized, and
* entry points that modify the undo and redo history concurrently are also
* synchronized. This means that the DefaultOperationHistory is relatively
* "thread-friendly" in its implementation. Outbound notifications or operation
* approval requests will occur on the thread that initiated the request.
* Clients may use DefaultOperationHistory API from any thread; however,
* listeners or operation approvers that receive notifications from the
* DefaultOperationHistory must be prepared to receive these notifications from
* a background thread. Any UI access occurring inside these notifications must
* be properly synchronized using the techniques specified by the client's
* widget library.
* </p>
*
* <p>
* This implementation is not intended to be subclassed.
* </p>
*
* @see org.eclipse.core.commands.operations.IOperationHistory
* @see org.eclipse.core.commands.operations.IOperationApprover
*
* @since 3.1
*/
public final class DefaultOperationHistory implements IOperationHistory {
  /**
   * This flag can be set to <code>true</code> if the history should print
   * information to <code>System.out</code> whenever notifications about
   * changes to the history occur. This flag should be used for debug purposes
   * only.
   */
  public static boolean DEBUG_OPERATION_HISTORY_NOTIFICATION = false;

  /**
   * This flag can be set to <code>true</code> if the history should print
   * information to <code>System.out</code> whenever an unexpected condition
   * arises. This flag should be used for debug purposes only.
   */
  public static boolean DEBUG_OPERATION_HISTORY_UNEXPECTED = false;

  /**
   * This flag can be set to <code>true</code> if the history should print
   * information to <code>System.out</code> whenever an undo context is
   * disposed. This flag should be used for debug purposes only.
   */
  public static boolean DEBUG_OPERATION_HISTORY_DISPOSE = false;

  /**
   * This flag can be set to <code>true</code> if the history should print
   * information to <code>System.out</code> during the open/close sequence.
   * This flag should be used for debug purposes only.
   */
  public static boolean DEBUG_OPERATION_HISTORY_OPENOPERATION = false;

  /**
   * This flag can be set to <code>true</code> if the history should print
   * information to <code>System.out</code> whenever an operation is not
   * approved. This flag should be used for debug purposes only.
   */
  public static boolean DEBUG_OPERATION_HISTORY_APPROVAL = false;

  static final int DEFAULT_LIMIT = 20;

  /**
   * the list of {@link IOperationApprover}s
   */
  ListenerList approvers = new ListenerList(ListenerList.IDENTITY);

  /**
   * a map of undo limits per context
   */
  private Map limits = Collections.synchronizedMap(new HashMap());

  /**
   * the list of {@link IOperationHistoryListener}s
   */
  ListenerList listeners = new ListenerList(ListenerList.IDENTITY);

  /**
   * the list of operations available for redo, LIFO
   */
  private List redoList = Collections.synchronizedList(new ArrayList());

  /**
   * the list of operations available for undo, LIFO
   */
  private List undoList = Collections.synchronizedList(new ArrayList());

  /**
   * a lock that is used to synchronize access between the undo and redo
   * history
   */
  final Object undoRedoHistoryLock = new Object();

  /**
   * An operation that is "absorbing" all other operations while it is open.
   * When this is not null, other operations added or executed are added to
   * this composite.
   *
   */
  private ICompositeOperation openComposite = null;

  /**
   * a lock that is used to synchronize access to the open composite.
   */
  final Object openCompositeLock = new Object();

  /**
   * Create an instance of DefaultOperationHistory.
   */
  public DefaultOperationHistory() {
    super();
  }

  /*
   * (non-Javadoc)
   *
   * @see org.eclipse.core.commands.operations.IOperationHistory#add(org.eclipse.core.commands.operations.IUndoableOperation)
   */
  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 context of the
     * new operation to the batch. The operation itself is disposed since we
     * will never undo or redo it. We consider it to be triggered by the
     * batching operation and assume that its undo will be triggered by the
     * batching operation undo.
     */
    synchronized (openCompositeLock) {
      if (openComposite != null && openComposite != operation) {
        openComposite.add(operation);
        return;
      }
    }

    if (checkUndoLimit(operation)) {
      synchronized (undoRedoHistoryLock) {
        undoList.add(operation);
      }
      notifyAdd(operation);

      // flush redo stack for related contexts
      IUndoContext[] contexts = operation.getContexts();
      for (int i = 0; i < contexts.length; i++) {
        flushRedo(contexts[i]);
      }
    } else {
      // Dispose the operation since we will not have a reference to it.
      operation.dispose();
    }
  }

  /**
   * <p>
   * Add the specified approver to the list of operation approvers consulted
   * by the operation history before an undo or redo is allowed to proceed.
   * This method has no effect if the instance being added is already in the
   * list.
   * </p>
   * <p>
   * Operation approvers must be prepared to receive these the operation
   * approval messages from a background thread. Any UI access occurring
   * inside the implementation must be properly synchronized using the
   * techniques specified by the client's widget library.
   * </p>
   *
   * @param approver
   *            the IOperationApprover to be added as an approver.
   *
   */

  public void addOperationApprover(IOperationApprover approver) {
    approvers.add(approver);
  }

  /**
   * <p>
   * Add the specified listener to the list of operation history listeners
   * that are notified about changes in the history or operations that are
   * executed, undone, or redone. This method has no effect if the instance
   * being added is already in the list.
   * </p>
   * <p>
   * Operation history listeners must be prepared to receive notifications
   * from a background thread. Any UI access occurring inside the
   * implementation must be properly synchronized using the techniques
   * specified by the client's widget library.
   * </p>
   *
   * @param listener
   *            the IOperationHistoryListener to be added as a listener.
   *
   * @see org.eclipse.core.commands.operations.IOperationHistoryListener
   * @see org.eclipse.core.commands.operations.OperationHistoryEvent
   */
  public void addOperationHistoryListener(IOperationHistoryListener listener) {
    listeners.add(listener);
  }

  /*
   * (non-Javadoc)
   *
   * @see org.eclipse.core.commands.operations.IOperationHistory#canRedo(org.eclipse.core.commands.operations.IUndoContext)
   */
  public boolean canRedo(IUndoContext context) {
    // null context is allowed and passed through
    IUndoableOperation operation = getRedoOperation(context);
    return (operation != null && operation.canRedo());
  }

  /*
   * (non-Javadoc)
   *
   * @see org.eclipse.core.commands.operations.IOperationHistory#canUndo(org.eclipse.core.commands.operations.IUndoContext)
   */
  public boolean canUndo(IUndoContext 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.
   *
   * Return a boolean indicating whether the redo should proceed.
   */
  private boolean checkRedoLimit(IUndoableOperation operation) {
    IUndoContext[] contexts = operation.getContexts();
    for (int i = 0; i < contexts.length; i++) {
      int limit = getLimit(contexts[i]);
      if (limit > 0) {
        forceRedoLimit(contexts[i], limit - 1);
      } else {
        // this context has a 0 limit
        operation.removeContext(contexts[i]);
      }
    }
    return operation.getContexts().length > 0;
  }

  /**
   * Check the undo limit before adding an operation. Return a boolean
   * indicating whether the undo should proceed.
   */
  private boolean checkUndoLimit(IUndoableOperation operation) {
    IUndoContext[] contexts = operation.getContexts();
    for (int i = 0; i < contexts.length; i++) {
      int limit = getLimit(contexts[i]);
      if (limit > 0) {
        forceUndoLimit(contexts[i], limit - 1);
      } else {
        // this context has a 0 limit
        operation.removeContext(contexts[i]);
      }
    }
    return operation.getContexts().length > 0;
  }

  /*
   * (non-Javadoc)
   *
   * @see org.eclipse.core.commands.operations.IOperationHistory#dispose(org.eclipse.core.commands.operations.IUndoContext,
   *      boolean, boolean, boolean)
   */
  public void dispose(IUndoContext context, boolean flushUndo,
      boolean flushRedo, boolean flushContext) {
    // dispose of any limit that was set for the context if it is not to be
    // used again.
    if (flushContext) {
      if (DEBUG_OPERATION_HISTORY_DISPOSE) {
        Tracing.printTrace("OPERATIONHISTORY", "Flushing context " //$NON-NLS-1$//$NON-NLS-2$
            + context);
      }
      flushUndo(context);
      flushRedo(context);
      limits.remove(context);
      return;
    }
    if (flushUndo) {
      flushUndo(context);
    }
    if (flushRedo) {
      flushRedo(context);
    }

  }

  /**
   * Perform the redo. All validity checks have already occurred.
   *
   * @param monitor
   * @param operation
   */
  private IStatus doRedo(IProgressMonitor monitor, IAdaptable info,
      IUndoableOperation operation) throws ExecutionException {

    IStatus status = getRedoApproval(operation, info);
    if (status.isOK()) {
      notifyAboutToRedo(operation);
      try {
        status = operation.redo(monitor, info);
      } catch (OperationCanceledException e) {
        status = Status.CANCEL_STATUS;
      } catch (ExecutionException e) {
        notifyNotOK(operation);
        if (DEBUG_OPERATION_HISTORY_UNEXPECTED) {
          Tracing.printTrace("OPERATIONHISTORY", //$NON-NLS-1$
              "ExecutionException while redoing " + operation); //$NON-NLS-1$
        }
        throw e;
      } catch (Exception e) {
        notifyNotOK(operation);
        if (DEBUG_OPERATION_HISTORY_UNEXPECTED) {
          Tracing.printTrace("OPERATIONHISTORY", //$NON-NLS-1$
              "Exception while redoing " + operation); //$NON-NLS-1$
        }
        throw new ExecutionException(
            "While redoing the operation, an exception occurred", e); //$NON-NLS-1$
      }
    }

    // if successful, the operation is removed from the redo history and
    // placed back in the undo history.
    if (status.isOK()) {
      boolean addedToUndo = true;
      synchronized (undoRedoHistoryLock) {
        redoList.remove(operation);
        if (checkUndoLimit(operation)) {
          undoList.add(operation);
        } else {
          addedToUndo = false;
        }
      }
      // dispose the operation since we could not add it to the
      // stack and will no longer have a reference to it.
      if (!addedToUndo) {
        operation.dispose();
      }

      // notify listeners must happen after history is updated
      notifyRedone(operation);
    } else {
      notifyNotOK(operation, status);
    }

    return status;
  }

  /**
   * Perform the undo. All validity checks have already occurred.
   *
   * @param monitor
   * @param operation
   */
  private IStatus doUndo(IProgressMonitor monitor, IAdaptable info,
      IUndoableOperation operation) throws ExecutionException {
    IStatus status = getUndoApproval(operation, info);
    if (status.isOK()) {
      notifyAboutToUndo(operation);
      try {
        status = operation.undo(monitor, info);
      } catch (OperationCanceledException e) {
        status = Status.CANCEL_STATUS;
      } catch (ExecutionException e) {
        notifyNotOK(operation);
        if (DEBUG_OPERATION_HISTORY_UNEXPECTED) {
          Tracing.printTrace("OPERATIONHISTORY", //$NON-NLS-1$
              "ExecutionException while undoing " + operation); //$NON-NLS-1$
        }
        throw e;
      } catch (Exception e) {
        notifyNotOK(operation);
        if (DEBUG_OPERATION_HISTORY_UNEXPECTED) {
          Tracing.printTrace("OPERATIONHISTORY", //$NON-NLS-1$
              "Exception while undoing " + operation); //$NON-NLS-1$
        }
        throw new ExecutionException(
            "While undoing the operation, an exception occurred", e); //$NON-NLS-1$
      }
    }
    // if successful, the operation is removed from the undo history and
    // placed in the redo history.
    if (status.isOK()) {
      boolean addedToRedo = true;
      synchronized (undoRedoHistoryLock) {
        undoList.remove(operation);
        if (checkRedoLimit(operation)) {
          redoList.add(operation);
        } else {
          addedToRedo = false;
        }
      }
      // dispose the operation since we could not add it to the
      // stack and will no longer have a reference to it.
      if (!addedToRedo) {
        operation.dispose();
      }
      // notification occurs after the undo and redo histories are
      // adjusted
      notifyUndone(operation);
    } else {
      notifyNotOK(operation, status);
    }
    return status;
  }

  /*
   * (non-Javadoc)
   *
   * @see org.eclipse.core.commands.operations.IOperationHistory#execute(org.eclipse.core.commands.operations.IUndoableOperation,
   *      org.eclipse.core.runtime.IProgressMonitor,
   *      org.eclipse.core.runtime.IAdaptable)
   */
  public IStatus execute(IUndoableOperation operation,
      IProgressMonitor monitor, IAdaptable info)
      throws ExecutionException {
    Assert.isNotNull(operation);

    // error if operation is invalid
    if (!operation.canExecute()) {
      return IOperationHistory.OPERATION_INVALID_STATUS;
    }

    // check with the operation approvers
    IStatus status = getExecuteApproval(operation, info);
    if (!status.isOK()) {
      // not approved. No notifications are sent, just return the status.
      return status;
    }

    /*
     * If we are in the middle of an open composite, then we will add this
     * operation to the open operation rather than add the operation to the
     * history. We will still execute it.
     */
    boolean merging = false;
    synchronized (openCompositeLock) {
      if (openComposite != null) {
        // the composite shouldn't be executed explicitly while it is
        // still
        // open
        if (openComposite == operation) {
          return IOperationHistory.OPERATION_INVALID_STATUS;
        }
        openComposite.add(operation);
        merging = true;
      }
    }

    /*
     * Execute the operation
     */
    if (!merging) {
      notifyAboutToExecute(operation);
    }
    try {
      status = operation.execute(monitor, info);
    } catch (OperationCanceledException e) {
      status = Status.CANCEL_STATUS;
    } catch (ExecutionException e) {
      notifyNotOK(operation);
      throw e;
    } catch (Exception e) {
      notifyNotOK(operation);
      throw new ExecutionException(
          "While executing the operation, an exception occurred", e); //$NON-NLS-1$
    }

    // if successful, the notify listeners are notified and the operation is
    // added to the history
    if (!merging) {
      if (status.isOK()) {
        notifyDone(operation);
        add(operation);
      } else {
        notifyNotOK(operation, status);
        // dispose the operation since we did not add it to the stack
        // and will no longer have a reference to it.
        operation.dispose();
      }
    }
    // all other severities are not interpreted. Simply return the status.
    return status;
  }

  /*
   * Filter the specified list to include only the specified undo context.
   */
  private IUndoableOperation[] filter(List list, IUndoContext 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.
     */

    List filtered = new ArrayList();
    Iterator iterator = list.iterator();
    synchronized (undoRedoHistoryLock) {
      while (iterator.hasNext()) {
        IUndoableOperation operation = (IUndoableOperation) iterator
            .next();
        if (operation.hasContext(context)) {
          filtered.add(operation);
        }
      }
    }
    return (IUndoableOperation[]) filtered
        .toArray(new IUndoableOperation[filtered.size()]);
  }

  /*
   * Flush the redo stack of all operations that have the given context.
   */
  private void flushRedo(IUndoContext context) {
    if (DEBUG_OPERATION_HISTORY_DISPOSE) {
      Tracing.printTrace("OPERATIONHISTORY", "Flushing redo history for " //$NON-NLS-1$ //$NON-NLS-2$
          + context);
    }

    Object[] filtered = filter(redoList, context);
    for (int i = 0; i < filtered.length; i++) {
      IUndoableOperation operation = (IUndoableOperation) filtered[i];
      if (context == GLOBAL_UNDO_CONTEXT
          || operation.getContexts().length == 1) {
        // remove the operation if it only has the context or we are
        // flushing all
        redoList.remove(operation);
        internalRemove(operation);
      } else {
        // remove the reference to the context.
        // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=161786
        // It is not enough to simply remove the context. There could
        // be one or more contexts that match the one we are trying to
        // dispose.
        IUndoContext[] contexts = operation.getContexts();
        for (int j = 0; j < contexts.length; j++) {
          if (contexts[j].matches(context)) {
            operation.removeContext(contexts[j]);
          }
        }
        if (operation.getContexts().length == 0) {
          redoList.remove(operation);
          internalRemove(operation);
        }
      }
    }
  }

  /*
   * Flush the undo stack of all operations that have the given context.
   */
  private void flushUndo(IUndoContext context) {
    if (DEBUG_OPERATION_HISTORY_DISPOSE) {
      Tracing.printTrace("OPERATIONHISTORY", "Flushing undo history for " //$NON-NLS-1$ //$NON-NLS-2$
          + context);
    }

    // Get all operations that have the context (or one that matches)
    Object[] filtered = filter(undoList, context);
    for (int i = 0; i < filtered.length; i++) {
      IUndoableOperation operation = (IUndoableOperation) filtered[i];
      if (context == GLOBAL_UNDO_CONTEXT
          || operation.getContexts().length == 1) {
        // remove the operation if it only has the context or we are
        // flushing all
        undoList.remove(operation);
        internalRemove(operation);
      } else {
        // remove the reference to the context.
        // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=161786
        // It is not enough to simply remove the context. There could
        // be one or more contexts that match the one we are trying to
        // dispose.
        IUndoContext[] contexts = operation.getContexts();
        for (int j = 0; j < contexts.length; j++) {
          if (contexts[j].matches(context)) {
            operation.removeContext(contexts[j]);
          }
        }
        if (operation.getContexts().length == 0) {
          undoList.remove(operation);
          internalRemove(operation);
        }
      }
    }
    /*
     * There may be an open composite. If it has this context, then the
     * context must be removed. If it has only this context or we are
     * flushing all operations, then null it out and notify that we are
     * ending it. We don't remove it since it was never added.
     */
    ICompositeOperation endedComposite = null;
    synchronized (openCompositeLock) {
      if (openComposite != null) {
        if (openComposite.hasContext(context)) {
          if (context == GLOBAL_UNDO_CONTEXT
              || openComposite.getContexts().length == 1) {
            endedComposite = openComposite;
            openComposite = null;
          } else {
            openComposite.removeContext(context);
          }
        }
      }
    }
    // notify outside of the synchronized block.
    if (endedComposite != null) {
      notifyNotOK(endedComposite);
    }
  }

  /*
   * Force the redo history for the given context to contain max or less
   * items.
   */
  private void forceRedoLimit(IUndoContext context, int max) {
    Object[] filtered = filter(redoList, context);
    int size = filtered.length;
    if (size > 0) {
      int index = 0;
      while (size > max) {
        IUndoableOperation removed = (IUndoableOperation) filtered[index];
        if (context == GLOBAL_UNDO_CONTEXT
            || removed.getContexts().length == 1) {
          /*
           * remove the operation if we are enforcing a global limit
           * or if the operation only has the specified context
           */
          redoList.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++;
      }
    }
  }

  /*
   * Force the undo history for the given context to contain max or less
   * items.
   */
  private void forceUndoLimit(IUndoContext context, int max) {
    Object[] filtered = filter(undoList, context);
    int size = filtered.length;
    if (size > 0) {
      int index = 0;
      while (size > max) {
        IUndoableOperation removed = (IUndoableOperation) filtered[index];
        if (context == GLOBAL_UNDO_CONTEXT
            || removed.getContexts().length == 1) {
          /*
           * remove the operation if we are enforcing a global limit
           * or if the operation only has the specified context
           */
          undoList.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.core.commands.operations.IOperationHistory#getLimit()
   */
  public int getLimit(IUndoContext context) {
    if (!limits.containsKey(context)) {
      return DEFAULT_LIMIT;
    }
    return ((Integer) (limits.get(context))).intValue();
  }

  /*
   * Consult the IOperationApprovers to see if the proposed redo should be
   * allowed.
   */
  private IStatus getRedoApproval(IUndoableOperation operation,
      IAdaptable info) {

    final Object[] approverArray = approvers.getListeners();

    for (int i = 0; i < approverArray.length; i++) {
      IOperationApprover approver = (IOperationApprover) approverArray[i];
      IStatus approval = approver.proceedRedoing(operation, this, info);
      if (!approval.isOK()) {
        if (DEBUG_OPERATION_HISTORY_APPROVAL) {
          Tracing.printTrace("OPERATIONHISTORY", //$NON-NLS-1$
              "Redo not approved by " + approver //$NON-NLS-1$
                  + "for operation " + operation //$NON-NLS-1$
                  + " approved by " + approval); //$NON-NLS-1$
        }
        return approval;
      }
    }
    return Status.OK_STATUS;
  }

  /*
   * (non-Javadoc)
   *
   * @see org.eclipse.core.commands.operations.IOperationHistory#getRedoHistory(org.eclipse.core.commands.operations.IUndoContext)
   */
  public IUndoableOperation[] getRedoHistory(IUndoContext context) {
    Assert.isNotNull(context);
    return filter(redoList, context);
  }

  /*
   * (non-Javadoc)
   *
   * @see org.eclipse.core.commands.operations.IOperationHistory#getOperation(org.eclipse.core.commands.operations.IUndoContext)
   */
  public IUndoableOperation getRedoOperation(IUndoContext context) {
    Assert.isNotNull(context);
    synchronized (undoRedoHistoryLock) {
      for (int i = redoList.size() - 1; i >= 0; i--) {
        IUndoableOperation operation = (IUndoableOperation) redoList
            .get(i);
        if (operation.hasContext(context)) {
          return operation;
        }
      }
    }
    return null;
  }

  /*
   * Consult the IOperationApprovers to see if the proposed undo should be
   * allowed.
   */
  private IStatus getUndoApproval(IUndoableOperation operation,
      IAdaptable info) {

    final Object[] approverArray = approvers.getListeners();

    for (int i = 0; i < approverArray.length; i++) {
      IOperationApprover approver = (IOperationApprover) approverArray[i];
      IStatus approval = approver.proceedUndoing(operation, this, info);
      if (!approval.isOK()) {
        if (DEBUG_OPERATION_HISTORY_APPROVAL) {
          Tracing.printTrace("OPERATIONHISTORY", //$NON-NLS-1$
              "Undo not approved by " + approver //$NON-NLS-1$
                  + "for operation " + operation //$NON-NLS-1$
                  + " with status " + approval); //$NON-NLS-1$
        }
        return approval;
      }
    }
    return Status.OK_STATUS;
  }

  /*
   * (non-Javadoc)
   *
   * @see org.eclipse.core.commands.operations.IOperationHistory#getUndoHistory(org.eclipse.core.commands.operations.IUndoContext)
   */
  public IUndoableOperation[] getUndoHistory(IUndoContext context) {
    Assert.isNotNull(context);
    return filter(undoList, context);
  }

  /*
   * (non-Javadoc)
   *
   * @see org.eclipse.core.commands.operations.IOperationHistory#getUndoOperation(org.eclipse.core.commands.operations.IUndoContext)
   */
  public IUndoableOperation getUndoOperation(IUndoContext context) {
    Assert.isNotNull(context);
    synchronized (undoRedoHistoryLock) {
      for (int i = undoList.size() - 1; i >= 0; i--) {
        IUndoableOperation operation = (IUndoableOperation) undoList
            .get(i);
        if (operation.hasContext(context)) {
          return operation;
        }
      }
    }
    return null;
  }

  /*
   * Consult the IOperationApprovers to see if the proposed execution should
   * be allowed.
   *
   * @since 3.2
   */
  private IStatus getExecuteApproval(IUndoableOperation operation,
      IAdaptable info) {

    final Object[] approverArray = approvers.getListeners();

    for (int i = 0; i < approverArray.length; i++) {
      if (approverArray[i] instanceof IOperationApprover2) {
        IOperationApprover2 approver = (IOperationApprover2) approverArray[i];
        IStatus approval = approver.proceedExecuting(operation, this,
            info);
        if (!approval.isOK()) {
          if (DEBUG_OPERATION_HISTORY_APPROVAL) {
            Tracing.printTrace("OPERATIONHISTORY", //$NON-NLS-1$
                "Execute not approved by " + approver //$NON-NLS-1$
                    + "for operation " + operation //$NON-NLS-1$
                    + " with status " + approval); //$NON-NLS-1$
          }
          return approval;
        }
      }
    }
    return Status.OK_STATUS;
  }

  /*
   * Remove the operation by disposing it and notifying listeners.
   */
  private void internalRemove(IUndoableOperation operation) {
    operation.dispose();
    notifyRemoved(operation);
  }

  /*
   * Notify listeners of an operation event.
   */
  private void notifyListeners(final OperationHistoryEvent event) {
    if (event.getOperation() instanceof IAdvancedUndoableOperation) {
      final IAdvancedUndoableOperation advancedOp = (IAdvancedUndoableOperation) event
          .getOperation();
      SafeRunner.run(new ISafeRunnable() {
        public void handleException(Throwable exception) {
          if (DEBUG_OPERATION_HISTORY_UNEXPECTED) {
            Tracing
                .printTrace(
                    "OPERATIONHISTORY", //$NON-NLS-1$
                    "Exception during notification callback " + exception); //$NON-NLS-1$
          }
        }

        public void run() throws Exception {
          advancedOp.aboutToNotify(event);
        }
      });
    }
    final Object[] listenerArray = listeners.getListeners();
    for (int i = 0; i < listenerArray.length; i++) {
      final IOperationHistoryListener listener = (IOperationHistoryListener) listenerArray[i];
      SafeRunner.run(new ISafeRunnable() {
        public void handleException(Throwable exception) {
          if (DEBUG_OPERATION_HISTORY_UNEXPECTED) {
            Tracing
                .printTrace(
                    "OPERATIONHISTORY", //$NON-NLS-1$
                    "Exception during notification callback " + exception); //$NON-NLS-1$
          }
        }

        public void run() throws Exception {
          listener.historyNotification(event);
        }
      });
    }
  }

  private void notifyAboutToExecute(IUndoableOperation operation) {
    if (DEBUG_OPERATION_HISTORY_NOTIFICATION) {
      Tracing.printTrace("OPERATIONHISTORY", "ABOUT_TO_EXECUTE " //$NON-NLS-1$ //$NON-NLS-2$
          + operation);
    }

    notifyListeners(new OperationHistoryEvent(
        OperationHistoryEvent.ABOUT_TO_EXECUTE, this, operation));
  }

  /*
   * Notify listeners that an operation is about to redo.
   */
  private void notifyAboutToRedo(IUndoableOperation operation) {
    if (DEBUG_OPERATION_HISTORY_NOTIFICATION) {
      Tracing.printTrace("OPERATIONHISTORY", "ABOUT_TO_REDO " //$NON-NLS-1$ //$NON-NLS-2$
          + operation);
    }

    notifyListeners(new OperationHistoryEvent(
        OperationHistoryEvent.ABOUT_TO_REDO, this, operation));
  }

  /*
   * Notify listeners that an operation is about to undo.
   */
  private void notifyAboutToUndo(IUndoableOperation operation) {
    if (DEBUG_OPERATION_HISTORY_NOTIFICATION) {
      Tracing.printTrace("OPERATIONHISTORY", "ABOUT_TO_UNDO " //$NON-NLS-1$ //$NON-NLS-2$
          + operation);
    }

    notifyListeners(new OperationHistoryEvent(
        OperationHistoryEvent.ABOUT_TO_UNDO, this, operation));
  }

  /*
   * Notify listeners that an operation has been added.
   */
  private void notifyAdd(IUndoableOperation operation) {
    if (DEBUG_OPERATION_HISTORY_NOTIFICATION) {
      Tracing.printTrace("OPERATIONHISTORY", "OPERATION_ADDED " //$NON-NLS-1$ //$NON-NLS-2$
          + operation);
    }

    notifyListeners(new OperationHistoryEvent(
        OperationHistoryEvent.OPERATION_ADDED, this, operation));
  }

  /*
   * Notify listeners that an operation is done executing.
   */
  private void notifyDone(IUndoableOperation operation) {
    if (DEBUG_OPERATION_HISTORY_NOTIFICATION) {
      Tracing.printTrace("OPERATIONHISTORY", "DONE " + operation); //$NON-NLS-1$ //$NON-NLS-2$
    }

    notifyListeners(new OperationHistoryEvent(OperationHistoryEvent.DONE,
        this, operation));
  }

  /*
   * Notify listeners that an operation did not succeed after an attempt to
   * execute, undo, or redo was made.
   */
  private void notifyNotOK(IUndoableOperation operation) {
    notifyNotOK(operation, null);
  }

  /*
   * Notify listeners that an operation did not succeed after an attempt to
   * execute, undo, or redo was made. Include the status associated with the
   * attempt.
   *
   * @since 3.2
   */
  private void notifyNotOK(IUndoableOperation operation, IStatus status) {
    if (DEBUG_OPERATION_HISTORY_NOTIFICATION) {
      Tracing.printTrace("OPERATIONHISTORY", "OPERATION_NOT_OK " //$NON-NLS-1$ //$NON-NLS-2$
          + operation);
    }

    notifyListeners(new OperationHistoryEvent(
        OperationHistoryEvent.OPERATION_NOT_OK, this, operation, status));
  }

  /*
   * Notify listeners that an operation was redone.
   */
  private void notifyRedone(IUndoableOperation operation) {
    if (DEBUG_OPERATION_HISTORY_NOTIFICATION) {
      Tracing.printTrace("OPERATIONHISTORY", "REDONE " + operation); //$NON-NLS-1$ //$NON-NLS-2$
    }

    notifyListeners(new OperationHistoryEvent(OperationHistoryEvent.REDONE,
        this, operation));
  }

  /*
   * Notify listeners that an operation has been removed from the history.
   */
  private void notifyRemoved(IUndoableOperation operation) {
    if (DEBUG_OPERATION_HISTORY_NOTIFICATION) {
      Tracing.printTrace("OPERATIONHISTORY", "OPERATION_REMOVED " //$NON-NLS-1$ //$NON-NLS-2$
          + operation);
    }

    notifyListeners(new OperationHistoryEvent(
        OperationHistoryEvent.OPERATION_REMOVED, this, operation));
  }

  /*
   * Notify listeners that an operation has been undone.
   */
  private void notifyUndone(IUndoableOperation operation) {
    if (DEBUG_OPERATION_HISTORY_NOTIFICATION) {
      Tracing.printTrace("OPERATIONHISTORY", "UNDONE " + operation); //$NON-NLS-1$ //$NON-NLS-2$
    }

    notifyListeners(new OperationHistoryEvent(OperationHistoryEvent.UNDONE,
        this, operation));
  }

  /*
   * Notify listeners that an operation has been undone.
   */
  private void notifyChanged(IUndoableOperation operation) {
    if (DEBUG_OPERATION_HISTORY_NOTIFICATION) {
      Tracing.printTrace("OPERATIONHISTORY", "OPERATION_CHANGED " //$NON-NLS-1$//$NON-NLS-2$
          + operation);
    }

    notifyListeners(new OperationHistoryEvent(
        OperationHistoryEvent.OPERATION_CHANGED, this, operation));
  }

  /*
   * (non-Javadoc)
   *
   * @see org.eclipse.core.commands.operations.IOperationHistory#redo(org.eclipse.core.commands.operations.IUndoContext,
   *      org.eclipse.core.runtime.IProgressMonitor,
   *      org.eclipse.core.runtime.IAdaptable)
   */
  public IStatus redo(IUndoContext context, IProgressMonitor monitor,
      IAdaptable info) throws ExecutionException {
    Assert.isNotNull(context);
    IUndoableOperation operation = getRedoOperation(context);

    // info if there is no operation
    if (operation == null) {
      return IOperationHistory.NOTHING_TO_REDO_STATUS;
    }

    // error if operation is invalid
    if (!operation.canRedo()) {
      if (DEBUG_OPERATION_HISTORY_UNEXPECTED) {
        Tracing.printTrace("OPERATIONHISTORY", //$NON-NLS-1$
            "Redo operation not valid - " + operation); //$NON-NLS-1$
      }

      return IOperationHistory.OPERATION_INVALID_STATUS;
    }

    return doRedo(monitor, info, operation);
  }

  /*
   * (non-Javadoc)
   *
   * @see org.eclipse.core.commands.operations.IOperationHistory#redoOperation(org.eclipse.core.commands.operations.IUndoableOperation,
   *      org.eclipse.core.runtime.IProgressMonitor,
   *      org.eclipse.core.runtime.IAdaptable)
   */

  public IStatus redoOperation(IUndoableOperation operation,
      IProgressMonitor monitor, IAdaptable info)
      throws ExecutionException {
    Assert.isNotNull(operation);
    IStatus status;
    if (operation.canRedo()) {
      status = doRedo(monitor, info, operation);
    } else {
      if (DEBUG_OPERATION_HISTORY_UNEXPECTED) {
        Tracing.printTrace("OPERATIONHISTORY", //$NON-NLS-1$
            "Redo operation not valid - " + operation); //$NON-NLS-1$
      }
      status = IOperationHistory.OPERATION_INVALID_STATUS;
    }
    return status;
  }

  /*
   * (non-Javadoc)
   *
   * @see org.eclipse.core.commands.operations.IOperationHistory#removeOperationApprover(org.eclipse.core.commands.operations.IOperationApprover)
   */
  public void removeOperationApprover(IOperationApprover approver) {
    approvers.remove(approver);
  }

  /*
   * (non-Javadoc)
   *
   * @see org.eclipse.core.commands.operations.IOperationHistory#removeOperationHistoryListener(org.eclipse.core.commands.operations.IOperationHistoryListener)
   */
  public void removeOperationHistoryListener(
      IOperationHistoryListener listener) {
    listeners.remove(listener);
  }

  /*
   * (non-Javadoc)
   *
   * @see org.eclipse.core.commands.operations.IOperationHistory#replaceOperation(org.eclipse.core.commands.operations.IUndoableOperation,
   *      org.eclipse.core.commands.operations.IUndoableOperation [])
   */
  public void replaceOperation(IUndoableOperation operation,
      IUndoableOperation[] replacements) {
    // check the undo history first.
    boolean inUndo = false;
    synchronized (undoRedoHistoryLock) {
      int index = undoList.indexOf(operation);
      if (index > -1) {
        inUndo = true;
        undoList.remove(operation);
        // notify listeners after the lock on undoList is released
        ArrayList allContexts = new ArrayList(replacements.length);
        for (int i = 0; i < replacements.length; i++) {
          IUndoContext[] opContexts = replacements[i].getContexts();
          for (int j = 0; j < opContexts.length; j++) {
            allContexts.add(opContexts[j]);
          }
          undoList.add(index, replacements[i]);
          // notify listeners after the lock on the history is
          // released
        }
        // recheck all the limits. We do this at the end so the index
        // doesn't change during replacement
        for (int i = 0; i < allContexts.size(); i++) {
          IUndoContext context = (IUndoContext) allContexts.get(i);
          forceUndoLimit(context, getLimit(context));
        }
      }
    }
    if (inUndo) {
      // notify listeners of operations added and removed
      internalRemove(operation);
      for (int i = 0; i < replacements.length; i++) {
        notifyAdd(replacements[i]);
      }
      return;
    }

    // operation was not in the undo history. Check the redo history.

    synchronized (undoRedoHistoryLock) {
      int index = redoList.indexOf(operation);
      if (index == -1) {
        return;
      }
      ArrayList allContexts = new ArrayList(replacements.length);
      redoList.remove(operation);
      // notify listeners after we release the lock on redoList
      for (int i = 0; i < replacements.length; i++) {
        IUndoContext[] opContexts = replacements[i].getContexts();
        for (int j = 0; j < opContexts.length; j++) {
          allContexts.add(opContexts[j]);
        }
        redoList.add(index, replacements[i]);
        // notify listeners after we release the lock on redoList
      }
      // recheck all the limits. We do this at the end so the index
      // doesn't change during replacement
      for (int i = 0; i < allContexts.size(); i++) {
        IUndoContext context = (IUndoContext) allContexts.get(i);
        forceRedoLimit(context, getLimit(context));
      }
    }
    // send listener notifications after we release the lock on the history
    internalRemove(operation);
    for (int i = 0; i < replacements.length; i++) {
      notifyAdd(replacements[i]);
    }
  }

  /*
   * (non-Javadoc)
   *
   * @see org.eclipse.core.commands.operations.IOperationHistory#setLimit(org.eclipse.core.commands.operations.IUndoContext,
   *      int)
   */
  public void setLimit(IUndoContext 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);
    limits.put(context, new Integer(limit));
    forceUndoLimit(context, limit);
    forceRedoLimit(context, limit);

  }

  /*
   * (non-Javadoc)
   *
   * @see org.eclipse.core.commands.operations.IOperationHistory#undo(org.eclipse.core.commands.operations.IUndoContext,
   *      org.eclipse.core.runtime.IProgressMonitor,
   *      org.eclipse.core.runtime.IAdaptable)
   */
  public IStatus undo(IUndoContext context, IProgressMonitor monitor,
      IAdaptable info) throws ExecutionException {
    Assert.isNotNull(context);
    IUndoableOperation operation = getUndoOperation(context);

    // info if there is no operation
    if (operation == null) {
      return IOperationHistory.NOTHING_TO_UNDO_STATUS;
    }

    // error if operation is invalid
    if (!operation.canUndo()) {
      if (DEBUG_OPERATION_HISTORY_UNEXPECTED) {
        Tracing.printTrace("OPERATIONHISTORY", //$NON-NLS-1$
            "Undo operation not valid - " + operation); //$NON-NLS-1$
      }
      return IOperationHistory.OPERATION_INVALID_STATUS;
    }

    return doUndo(monitor, info, operation);
  }

  /*
   * (non-Javadoc)
   *
   * @see org.eclipse.core.commands.operations.IOperationHistory#undoOperation(org.eclipse.core.commands.operations.IUndoableOperation,
   *      org.eclipse.core.runtime.IProgressMonitor,
   *      org.eclipse.core.runtime.IAdaptable)
   */
  public IStatus undoOperation(IUndoableOperation operation,
      IProgressMonitor monitor, IAdaptable info)
      throws ExecutionException {
    Assert.isNotNull(operation);
    IStatus status;
    if (operation.canUndo()) {
      status = doUndo(monitor, info, operation);
    } else {
      if (DEBUG_OPERATION_HISTORY_UNEXPECTED) {
        Tracing.printTrace("OPERATIONHISTORY", //$NON-NLS-1$
            "Undo operation not valid - " + operation); //$NON-NLS-1$
      }
      status = IOperationHistory.OPERATION_INVALID_STATUS;
    }
    return status;
  }

  /*
   * (non-Javadoc)
   *
   * @see org.eclipse.core.commands.operations.IOperationHistory#openOperation(org.eclipse.core.commands.operations.ICompositeOperation)
   */
  public void openOperation(ICompositeOperation operation, int mode) {
    synchronized (openCompositeLock) {
      if (openComposite != null && openComposite != operation) {
        // unexpected nesting of operations.
        if (DEBUG_OPERATION_HISTORY_UNEXPECTED) {
          Tracing.printTrace("OPERATIONHISTORY", //$NON-NLS-1$
              "Open operation called while another operation is open.  old: " //$NON-NLS-1$
                  + openComposite + "; new:  " + operation); //$NON-NLS-1$
        }

        throw new IllegalStateException(
            "Cannot open an operation while one is already open"); //$NON-NLS-1$
      }
      openComposite = operation;
    }
    if (DEBUG_OPERATION_HISTORY_OPENOPERATION) {
      Tracing.printTrace("OPERATIONHISTORY", "Opening operation " //$NON-NLS-1$ //$NON-NLS-2$
          + openComposite);
    }

    if (mode == EXECUTE) {
      notifyAboutToExecute(openComposite);
    }
  }

  /*
   * (non-Javadoc)
   *
   * @see org.eclipse.core.commands.operations.IOperationHistory#closeOperation(boolean,
   *      boolean)
   */
  public void closeOperation(boolean operationOK, boolean addToHistory,
      int mode) {
    ICompositeOperation endedComposite = null;

    synchronized (openCompositeLock) {
      if (DEBUG_OPERATION_HISTORY_UNEXPECTED) {
        if (openComposite == null) {
          Tracing.printTrace("OPERATIONHISTORY", //$NON-NLS-1$
              "Attempted to close operation when none was open"); //$NON-NLS-1$
          return;
        }
      }
      // notifications will occur outside the synchonized block
      if (openComposite != null) {
        if (DEBUG_OPERATION_HISTORY_OPENOPERATION) {
          Tracing.printTrace("OPERATIONHISTORY", "Closing operation " //$NON-NLS-1$ //$NON-NLS-2$
              + openComposite);
        }
        endedComposite = openComposite;
        openComposite = null;
      }
    }
    // any mode other than EXECUTE was triggered by a request to undo or
    // redo something already in the history, so undo and redo
    // notification will occur at the end of that sequence.
    if (endedComposite != null) {
      if (operationOK) {
        if (mode == EXECUTE) {
          notifyDone(endedComposite);
        }
        if (addToHistory) {
          add(endedComposite);
        }
      } else {
        if (mode == EXECUTE) {
          notifyNotOK(endedComposite);
        }
      }
    }
  }

  /*
   * (non-Javadoc)
   *
   * @see org.eclipse.core.commands.operations.IOperationHistory#operationChanged(org.eclipse.core.commands.operations.IUndoableOperation)
   */
  public void operationChanged(IUndoableOperation operation) {
    if (undoList.contains(operation) || redoList.contains(operation)) {
      notifyChanged(operation);
    }
  }
}
TOP

Related Classes of org.eclipse.core.commands.operations.DefaultOperationHistory

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.