/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
 * add the following below this CDDL HEADER, with the fields enclosed
 * by brackets "[]" replaced with your own identifying information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2008 Sun Microsystems, Inc.
 */
package org.opends.server.core;

import static org.opends.messages.CoreMessages.*;
import static org.opends.server.util.Validator.ensureNotNull;

import java.util.Collection;
import java.util.Observable;
import java.util.Observer;
import java.util.TreeMap;

import org.opends.messages.MessageBuilder;
import org.opends.server.admin.std.server.WorkflowCfg;
import org.opends.server.types.*;
import org.opends.server.workflowelement.WorkflowElement;
import org.opends.server.workflowelement.ObservableWorkflowElementState;


/**
 * This class implements the workflow interface. Each task in the workflow
 * is implemented by a WorkflowElement. All the tasks in the workflow are
 * structured in a tree of tasks and the root node of the task tree is
 * stored in the Workflow class itself. To execute a workflow, one just need
 * to call the execute method on the root node of the task tree. Then each
 * task in turn will execute its subordinate nodes and synchronizes them
 * as needed.
 */
public class WorkflowImpl implements Workflow, Observer
{
  // The workflow identifier used by the configuration.
  private final String workflowID;

  // The root of the workflow task tree.
  private WorkflowElement<?> rootWorkflowElement = null;

  // The root workflow element identifier.
  private String rootWorkflowElementID = null;

  // The base DN of the data handled by the workflow.
  private final DN baseDN;

  // Flag indicating whether the workflow root node of the task tree is
  // handling a private local backend.
  //
  // A private local backend is used by the server to store "private data"
  // such as schemas, tasks, monitoring data, configuration data... Such
  // private data are not returned upon a subtree search on the root DSE.
  // Also it is not planned to have anything but a single node task tree
  // to handle private local backend. So workflows used for proxy and
  // virtual will always be made public (ie. not private). So, unless the
  // rootWorkflowElement is handling a private local backend, the isPrivate
  // flag will always return false.
  private boolean isPrivate = false;

  // The set of workflows registered with the server.
  private static TreeMap<String, Workflow> registeredWorkflows =
    new TreeMap<String, Workflow>();

  // A lock to protect concurrent access to the registeredWorkflows.
  private static Object registeredWorkflowsLock = new Object();

  // A reference counter used to count the number of workflow nodes that
  // were registered with a network group. A workflow can be disabled or
  // deleted only when its reference counter value is 0.
  private int referenceCounter = 0;
  private Object referenceCounterLock = new Object();


  /**
   * Creates a new instance of a workflow implementation. To define a workflow
   * one needs to provide a task tree root node (the rootWorkflowElement) and
   * a base DN to identify the data set upon which the tasks can be applied.
   *
   * The rootWorkflowElement must not be null.
   *
   * @param workflowId          workflow internal identifier
   * @param baseDN              identifies the data handled by the workflow
   * @param rootWorkflowElementID  the identifier of the root workflow element
   * @param rootWorkflowElement the root node of the workflow task tree
   */
  public WorkflowImpl(
      String             workflowId,
      DN                 baseDN,
      String             rootWorkflowElementID,
      WorkflowElement<?> rootWorkflowElement
      )
  {
    this.workflowID = workflowId;
    this.baseDN = baseDN;
    this.rootWorkflowElement = rootWorkflowElement;
    if (this.rootWorkflowElement != null)
    {
      this.isPrivate = rootWorkflowElement.isPrivate();
      this.rootWorkflowElementID = rootWorkflowElementID;

      // The workflow wants to be notified when the workflow element state
      // is changing from enabled to disabled and vice versa.
      WorkflowElement.registereForStateUpdate(
        rootWorkflowElement, null, this);
    }
    else
    {
      // The root workflow element has not been loaded, let's register
      // the workflow with the list of objects that want to be notify
      // when the workflow element is created.
      WorkflowElement.registereForStateUpdate(
        null, rootWorkflowElementID, this);
    }
  }


  /**
   * Performs any finalization that might be required when this
   * workflow is unloaded.  No action is taken in the default
   * implementation.
   */
  public void finalizeWorkflow()
  {
    // No action is required by default.
  }


  /**
   * Gets the base DN of the data set being handled by the workflow.
   *
   * @return the workflow base DN
   */
  public DN getBaseDN()
  {
    return baseDN;
  }


  /**
   * Gets the workflow internal identifier.
   *
   * @return the workflow internal identifier
   */
  public String getWorkflowId()
  {
    return workflowID;
  }


  /**
   * Indicates whether the root node of the workflow task tree is
   * handling a private local backend.
   *
   * @return <code>true</code> if the workflow encapsulates a private local
   *         backend
   */
  public boolean isPrivate()
  {
    return isPrivate;
  }


  /**
   * Executes all the tasks defined by the workflow task tree for a given
   * operation.
   *
   * @param operation  the operation to execute
   *
   * @throws CanceledOperationException if this operation should
   * be canceled.
   */
  public void execute(Operation operation)
    throws CanceledOperationException
  {
    if (rootWorkflowElement != null)
    {
      rootWorkflowElement.execute(operation);
    }
    else
    {
      // No root workflow element? It's a configuration error.
      operation.setResultCode(ResultCode.OPERATIONS_ERROR);
      MessageBuilder message = new MessageBuilder(
        ERR_ROOT_WORKFLOW_ELEMENT_NOT_DEFINED.get(workflowID));
      operation.setErrorMessage(message);
    }
  }


  /**
   * Registers the current workflow (this) with the server.
   *
   * @throws  DirectoryException  If the workflow ID for the provided workflow
   *                              conflicts with the workflow ID of an existing
   *                              workflow.
   */
  public void register()
      throws DirectoryException
  {
    ensureNotNull(workflowID);

    synchronized (registeredWorkflowsLock)
    {
      // The workflow must not be already registered
      if (registeredWorkflows.containsKey(workflowID))
      {
        throw new DirectoryException(
            ResultCode.UNWILLING_TO_PERFORM,
            ERR_REGISTER_WORKFLOW_ALREADY_EXISTS.get(workflowID));
      }

      TreeMap<String, Workflow> newRegisteredWorkflows =
        new TreeMap<String, Workflow>(registeredWorkflows);
      newRegisteredWorkflows.put(workflowID, this);
      registeredWorkflows = newRegisteredWorkflows;
    }
  }


  /**
   * Deregisters the current workflow (this) with the server.
   */
  public void deregister()
  {
    ensureNotNull(workflowID);

    // Deregister the workflow with the list of objects to notify when
    // a workflow element is created or deleted.
    WorkflowElement.deregistereForStateUpdate(
      null, rootWorkflowElementID, this);

    // Deregister the workflow with the list of registered workflows.
    synchronized (registeredWorkflowsLock)
    {
      TreeMap<String, Workflow> newWorkflows =
        new TreeMap<String, Workflow>(registeredWorkflows);
      newWorkflows.remove(workflowID);
      registeredWorkflows = newWorkflows;
    }
  }


  /**
   * Deregisters a workflow with the server. The workflow to deregister
   * is identified with its identifier.
   *
   * @param workflowID  the identifier of the workflow to deregister
   *
   * @return the workflow that has been deregistered,
   *         <code>null</code> if no workflow has been found.
   */
  public WorkflowImpl deregister(String workflowID)
  {
    WorkflowImpl workflowToDeregister = null;

    synchronized (registeredWorkflowsLock)
    {
      if (registeredWorkflows.containsKey(workflowID))
      {
        workflowToDeregister =
          (WorkflowImpl) registeredWorkflows.get(workflowID);
        workflowToDeregister.deregister();
      }
    }

    return workflowToDeregister;
  }


  /**
   * Deregisters all Workflows that have been registered.  This should be
   * called when the server is shutting down.
   */
  public static void deregisterAllOnShutdown()
  {
    synchronized (registeredWorkflowsLock)
    {
      registeredWorkflows =
        new TreeMap<String, Workflow>();
    }
  }


  /**
   * Gets a workflow that was registered with the server.
   *
   * @param workflowID  the ID of the workflow to get
   * @return the requested workflow
   */
  public static Workflow getWorkflow(
      String workflowID)
  {
    return registeredWorkflows.get(workflowID);
  }


  /**
   * Gets all the workflows that were registered with the server.
   *
   * @return the list of registered workflows
   */
  public static Collection<Workflow> getWorkflows()
  {
    return registeredWorkflows.values();
  }


  /**
   * Gets the root workflow element for test purpose only.
   *
   * @return the root workflow element.
   */
  WorkflowElement<?> getRootWorkflowElement()
  {
    return rootWorkflowElement;
  }


  /**
   * Resets all the registered workflows.
   */
  public static void resetConfig()
  {
    synchronized (registeredWorkflowsLock)
    {
      registeredWorkflows = new TreeMap<String, Workflow>();
    }
  }


  /**
   * Updates the workflow configuration. This method should be invoked
   * whenever an existing workflow is modified.
   *
   * @param configuration  the new workflow configuration
   */
  public void updateConfig(WorkflowCfg configuration)
  {
    // The only parameter that can be changed is the root workflow element.
    String rootWorkflowElementID = configuration.getWorkflowElement();
    WorkflowElement<?> rootWorkflowElement =
      DirectoryServer.getWorkflowElement(rootWorkflowElementID);

    // Update the ID of the new root workflow element
    // and deregister the workflow with the list of objects to notify
    // when the former root workflow element is created
    String previousRootWorkflowElement = this.rootWorkflowElementID;
    WorkflowElement.deregistereForStateUpdate(
      null, previousRootWorkflowElement, this);
    this.rootWorkflowElementID = rootWorkflowElementID;

    // Does the new root workflow element exist?
    if (rootWorkflowElement == null)
    {
      // The new root workflow element does not exist yet then do nothing
      // but register with the list of object to notify when the workflow
      // element is created (and deregister first in case the workflow
      // was already registered)
      WorkflowElement.registereForStateUpdate(
        null, rootWorkflowElementID, this);
      rootWorkflowElement = null;
    }
    else
    {
      // The new root workflow element exists, let's use it and don't forget
      // to register with the list of objects to notify when the workflow
      // element is deleted.
      this.rootWorkflowElement = rootWorkflowElement;
      WorkflowElement.registereForStateUpdate(
        rootWorkflowElement, null, this);
    }
  }


  /**
   * {@inheritDoc}
   */
  public void update(Observable observable, Object arg)
  {
    if (observable instanceof ObservableWorkflowElementState)
    {
      ObservableWorkflowElementState weState =
        (ObservableWorkflowElementState) observable;
      updateRootWorkflowElementState(weState);
    }
  }


  /**
   * Display the workflow object.
   * @return a string identifying the workflow.
   */
  public String toString()
  {
    String id = "Workflow " + workflowID;
    return id;
  }


  /**
   * Takes into account an update of the root workflow element.
   * <p>
   * This method is called when a workflow element is created or
   * deleted. If the workflow element is the root workflow element
   * then the workflow processes it as follow:
   * <br>
   * If the workflow element is disabled then the workflow reset
   * its root workflow element (ie. the workflow has no more workflow
   * element, hence the workflow cannot process requests anymore).
   * <br>
   * If the workflow element is enabled then the workflow adopts the
   * workflow element as its new root workflow element. The workflow
   * then can process requests again.
   *
   * @param weState  the new state of the root workflow element
   */
  private void updateRootWorkflowElementState(
      ObservableWorkflowElementState weState)
  {
    // Check that the workflow element maps the root workflow element.
    // If not then ignore the workflow element.
    WorkflowElement<?> we = weState.getObservedWorkflowElement();
    String newWorkflowElementID = we.getWorkflowElementID();
    if (! rootWorkflowElementID.equalsIgnoreCase(newWorkflowElementID))
    {
      return;
    }

    // The workflow element maps the root workflow element, let's process it.
    if (weState.workflowElementIsEnabled())
    {
      // The root workflow element is enabled, let's use it
      // and don't forget to register the workflow with the list
      // of objects to notify when the root workflow element
      // is disabled...
      rootWorkflowElement = weState.getObservedWorkflowElement();
      WorkflowElement.registereForStateUpdate(
        rootWorkflowElement, null, this);
      WorkflowElement.deregistereForStateUpdate(
        null, rootWorkflowElementID, this);
    }
    else
    {
      // The root workflow element has been disabled. Reset the current
      // reference to the root workflow element and register the workflow
      // with the list of objects to notify when new workflow elements
      // are created.
      WorkflowElement.registereForStateUpdate(
        null, rootWorkflowElement.getWorkflowElementID(), this);
      rootWorkflowElement = null;
    }
  }


  /**
   * Increments the workflow reference counter.
   * <p>
   * As long as the counter value is not 0 the workflow cannot be
   * disabled nor deleted.
   */
  public void incrementReferenceCounter()
  {
    synchronized (referenceCounterLock)
    {
      referenceCounter++;
    }
  }


  /**
   * Decrements the workflow reference counter.
   * <p>
   * As long as the counter value is not 0 the workflow cannot be
   * disabled nor deleted.
   */
  public void decrementReferenceCounter()
  {
    synchronized (referenceCounterLock)
    {
      if (referenceCounter == 0)
      {
        // the counter value is 0, we should not need to decrement anymore
        throw new AssertionError(
          "Reference counter of the workflow " + workflowID
          + " is already set to 0, cannot decrement it anymore"
          );
      }
      referenceCounter--;
    }
  }


  /**
   * Gets the value of the reference counter of the workflow.
   *
   * @return the reference counter of the workflow
   */
  public int getReferenceCounter()
  {
    return referenceCounter;
  }
}
