@Override
  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();
    }
  }
  @Override
  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<IUndoContext> 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 = 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<IUndoContext> 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 = 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]);
    }
  }