/*
 * 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 legal-notices/CDDLv1_0.txt
 * or http://forgerock.org/license/CDDLv1.0.html.
 * 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 legal-notices/CDDLv1_0.txt.
 * 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 2006-2008 Sun Microsystems, Inc.
 *      Portions Copyright 2014-2015 ForgeRock AS
 */
package org.opends.server.plugins;



import java.io.IOException;
import java.util.List;
import java.util.Set;

import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.opendj.config.server.ConfigException;
import org.forgerock.opendj.io.ASN1;
import org.forgerock.opendj.io.ASN1Reader;
import org.forgerock.opendj.io.ASN1Writer;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.ResultCode;
import org.opends.server.admin.std.server.PluginCfg;
import org.opends.server.api.plugin.DirectoryServerPlugin;
import org.opends.server.api.plugin.PluginResult;
import org.opends.server.api.plugin.PluginType;
import org.opends.server.controls.ControlDecoder;
import org.opends.server.types.CanceledOperationException;
import org.opends.server.types.Control;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.operation.*;
import org.opends.server.util.CollectionUtils;

/**
 * This class defines a very simple pre-operation plugin that sleeps for up to
 * five seconds for add, compare, delete, extended, modify, modify DN, and
 * search operations (and therefore not for abandon, bind, and unbind
 * operations, since those operations cannot be cancelled).  While it is
 * sleeping, it also checks for a request to cancel the associated operation and
 * will respond to it accordingly.
 */
public class DelayPreOpPlugin
       extends DirectoryServerPlugin<PluginCfg>
{
  /**
   * The OID for the delay request control, which is used to flag operations
   * that should be delayed.
   */
  public static final String OID_DELAY_REQUEST = "1.3.6.1.4.1.26027.1.999.1";

  /**
   * The control used by this plugin.
   */
  public static class DelayRequestControl extends Control
  {

    /**
     * ControlDecoder implementation to decode this control from a ByteString.
     */
    private static final class Decoder
        implements ControlDecoder<DelayRequestControl>
    {
      /** {@inheritDoc} */
      @Override
      public DelayRequestControl decode(boolean isCritical, ByteString value)
          throws DirectoryException
      {
        ASN1Reader reader = ASN1.getReader(value);

        try
        {
          long delay = reader.readInteger();

          return new DelayRequestControl(isCritical, delay);
        }
        catch (Exception e)
        {
          // TODO: Need a better message
          throw new DirectoryException(ResultCode.PROTOCOL_ERROR, null, e);
        }
      }

      @Override
      public String getOID()
      {
        return OID_DELAY_REQUEST;
      }

    }

    /**
     * The Control Decoder that can be used to decode this control.
     */
    public static final ControlDecoder<DelayRequestControl> DECODER =
      new Decoder();


    private long delayDuration;

    /**
     * Constructs a new control of this class.
     *
     * @param isCritical
     *          Indicates whether support for this control should be considered
     *          a critical part of the server processing.
     * @param delayDuration
     *          The requested delay duration.
     */
    public DelayRequestControl(boolean isCritical, long delayDuration)
    {
      super(OID_DELAY_REQUEST, isCritical);
      this.delayDuration = delayDuration;
    }

    /**
     * Writes this control's value to an ASN.1 writer. The value (if any)
     * must be written as an ASN1OctetString.
     *
     * @param writer The ASN.1 writer to use.
     * @throws IOException If a problem occurs while writing to the stream.
     */
    @Override
    protected void writeValue(ASN1Writer writer) throws IOException {
      writer.writeStartSequence(ASN1.UNIVERSAL_OCTET_STRING_TYPE);
      writer.writeInteger(delayDuration);
      writer.writeEndSequence();
    }

    /**
     * Retrieves the delay duration.
     *
     * @return The delay duration.
     */
    public long getDelayDuration()
    {
      return delayDuration;
    }
  }



  /**
   * Creates a new instance of this Directory Server plugin.  Every
   * plugin must implement a default constructor (it is the only one
   * that will be used to create plugins defined in the
   * configuration), and every plugin constructor must call
   * <CODE>super()</CODE> as its first element.
   */
  public DelayPreOpPlugin()
  {
    super();
  }



  /** {@inheritDoc} */
  @Override
  public void initializePlugin(Set<PluginType> pluginTypes,
                               PluginCfg configuration)
         throws ConfigException
  {
    // This plugin may only be used as a pre-operation plugin.
    for (PluginType t : pluginTypes)
    {
      switch (t)
      {
        case PRE_OPERATION_ADD:
        case PRE_OPERATION_BIND:
        case PRE_OPERATION_COMPARE:
        case PRE_OPERATION_DELETE:
        case PRE_OPERATION_EXTENDED:
        case PRE_OPERATION_MODIFY:
        case PRE_OPERATION_MODIFY_DN:
        case PRE_OPERATION_SEARCH:
          // This is fine.
          break;
        default:
          throw new ConfigException(LocalizableMessage.raw("Invalid plugin type " + t +
                                    " for delay pre-op plugin."));
      }
    }
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PreOperation
       doPreOperation(PreOperationAddOperation addOperation)
      throws CanceledOperationException {
    return doPreOperationInternal(addOperation);
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PreOperation
         doPreOperation(PreOperationBindOperation bindOperation)
  {
    try
    {
      return doPreOperationInternal(bindOperation);
    }
    catch(CanceledOperationException coe)
    {
      // Bind ops can't be canceled. Just ignore.
      return PluginResult.PreOperation.continueOperationProcessing();
    }
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PreOperation
         doPreOperation(PreOperationCompareOperation compareOperation)
      throws CanceledOperationException {
    return doPreOperationInternal(compareOperation);
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PreOperation
       doPreOperation(PreOperationDeleteOperation deleteOperation)
      throws CanceledOperationException {
    return doPreOperationInternal(deleteOperation);
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PreOperation
       doPreOperation(PreOperationExtendedOperation extendedOperation)
      throws CanceledOperationException {
    return doPreOperationInternal(extendedOperation);
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PreOperation
       doPreOperation(PreOperationModifyOperation modifyOperation)
      throws CanceledOperationException {
    return doPreOperationInternal(modifyOperation);
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PreOperation
       doPreOperation(PreOperationModifyDNOperation modifyDNOperation)
      throws CanceledOperationException {
    return doPreOperationInternal(modifyDNOperation);
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PreOperation
       doPreOperation(PreOperationSearchOperation searchOperation)
      throws CanceledOperationException {
    return doPreOperationInternal(searchOperation);
  }



  /**
   * Looks for a delay request control in the operation, and if one is found
   * then sleep in 10 millisecond increments up to the length of time specified
   * in the control value.  If the operation receives a cancel request during
   * this time, then the control will stop sleeping immediately.
   *
   * @param  operation  The operation to be processed.
   *
   * @return  The result of the plugin processing.
   */
  private PluginResult.PreOperation
       doPreOperationInternal(PreOperationOperation operation)
      throws CanceledOperationException
  {
    DelayRequestControl control;
    try
    {
      control = operation.getRequestControl(DelayRequestControl.DECODER);
    }
    catch (Exception e)
    {
      return PluginResult.PreOperation.stopProcessing(
          ResultCode.PROTOCOL_ERROR,
          LocalizableMessage.raw("Unable to decode the delay request control:  " +
              e));
    }

    if(control != null)
    {
      long delayDuration = control.getDelayDuration();
      if (delayDuration <= 0)
      {
        return PluginResult.PreOperation.continueOperationProcessing();
      }

      long stopSleepTime = System.currentTimeMillis() + delayDuration;
      while (System.currentTimeMillis() < stopSleepTime)
      {
        operation.checkIfCanceled(false);

        try
        {
          Thread.sleep(10);
        } catch (Exception e) {}
      }
    }

    return PluginResult.PreOperation.continueOperationProcessing();
  }




  /**
   * Creates a delay request control with the specified delay.
   *
   * @param  delay  The length of time in milliseconds to sleep.
   *
   * @return  The appropriate delay request control.
   */
  public static Control createDelayControl(long delay)
  {
    return new DelayRequestControl(false, delay);
  }



  /**
   * Retrieves a list containing a delay request LDAP control with the specified
   * delay.
   *
   * @param  delay  The length of time in milliseconds to sleep.
   *
   * @return  A list containing the appropriate delay request LDAP control.
   */
  public static List<Control> createDelayControlList(long delay)
  {
    Control c = new DelayRequestControl(false, delay);
    return CollectionUtils.newArrayList(c);
  }
}
