/*
 * 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 static org.opends.server.util.CollectionUtils.*;

import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

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.Control;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.OperationType;
import org.opends.server.types.operation.*;

/**
 * This class defines a very simple plugin that causes request processing to end
 * immediately and send a specific result code to the client.  It will be
 * triggered by a control contained in the client request, and may be invoked
 * during either pre-parse or pre-operation processing.  Short circuits can
 * also be registered for operations regardless of controls.
 */
public class ShortCircuitPlugin
       extends DirectoryServerPlugin<PluginCfg>
{
  /**
   * The OID for the short circuit request control, which is used to flag
   * operations that should cause the operation processing to end immediately.
   */
  public static final String OID_SHORT_CIRCUIT_REQUEST =
       "1.3.6.1.4.1.26027.1.999.3";

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

        try
        {
          reader.readStartSequence();
          int resultCode = (int)reader.readInteger();
          String section = reader.readOctetStringAsString();
          reader.readEndSequence();

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

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

    }

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


    private int resultCode;
    private String section;

    /**
     * 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 resultCode
     *          The result code to return to the client.
     * @param section
     *          The section to use to determine when to short circuit.
     */
    public ShortCircuitRequestControl(boolean isCritical, int resultCode,
                                      String section)
    {
      super(OID_SHORT_CIRCUIT_REQUEST, isCritical);
      this.resultCode = resultCode;
      this.section = section;
    }

    /**
     * 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.writeStartSequence();
      writer.writeInteger(resultCode);
      writer.writeOctetString(section);
      writer.writeEndSequence();
      writer.writeEndSequence();
    }

    /**
     * Retrieves the resultCode.
     *
     * @return The resultCode.
     */
    public int getResultCode()
    {
      return resultCode;
    }

    /**
     * Retrieves the section.
     *
     * @return The section.
     */
    public String getSection()
    {
      return section;
    }
  }



  /**
   * 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 ShortCircuitPlugin()
  {
    super();
  }



  /** {@inheritDoc} */
  @Override
  public void initializePlugin(Set<PluginType> pluginTypes,
                               PluginCfg configuration)
         throws ConfigException
  {
    // This plugin may only be used as a pre-parse or pre-operation plugin.
    for (PluginType t : pluginTypes)
    {
      switch (t)
      {
        case PRE_PARSE_ABANDON:
        case PRE_PARSE_ADD:
        case PRE_PARSE_BIND:
        case PRE_PARSE_COMPARE:
        case PRE_PARSE_DELETE:
        case PRE_PARSE_EXTENDED:
        case PRE_PARSE_MODIFY:
        case PRE_PARSE_MODIFY_DN:
        case PRE_PARSE_SEARCH:
        case PRE_PARSE_UNBIND:
        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 the short circuit plugin."));
      }
    }
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PreParse
         doPreParse(PreParseAbandonOperation abandonOperation)
  {
    int resultCode = shortCircuitInternal(abandonOperation, "PreParse");
    if (resultCode >= 0)
    {
      return PluginResult.PreParse.stopProcessing(
          ResultCode.valueOf(resultCode),
          LocalizableMessage.raw("Short-circuit in pre-parse"));
    }
    else
    {
      return PluginResult.PreParse.continueOperationProcessing();
    }
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PreParse doPreParse(PreParseAddOperation addOperation)
  {
    int resultCode = shortCircuitInternal(addOperation, "PreParse");
    if (resultCode >= 0)
    {
      return PluginResult.PreParse.stopProcessing(
          ResultCode.valueOf(resultCode),
          LocalizableMessage.raw("Short-circuit in pre-parse"));
    }
    else
    {
      return PluginResult.PreParse.continueOperationProcessing();
    }
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PreParse doPreParse(PreParseBindOperation bindOperation)
  {
    int resultCode = shortCircuitInternal(bindOperation, "PreParse");
    if (resultCode >= 0)
    {
      return PluginResult.PreParse.stopProcessing(
          ResultCode.valueOf(resultCode),
          LocalizableMessage.raw("Short-circuit in pre-parse"));
    }
    else
    {
      return PluginResult.PreParse.continueOperationProcessing();
    }
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PreParse
       doPreParse(PreParseCompareOperation compareOperation)
  {
    int resultCode = shortCircuitInternal(compareOperation, "PreParse");
    if (resultCode >= 0)
    {
      return PluginResult.PreParse.stopProcessing(
          ResultCode.valueOf(resultCode),
          LocalizableMessage.raw("Short-circuit in pre-parse"));
    }
    else
    {
      return PluginResult.PreParse.continueOperationProcessing();
    }
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PreParse
       doPreParse(PreParseDeleteOperation deleteOperation)
  {
    int resultCode = shortCircuitInternal(deleteOperation, "PreParse");
    if (resultCode >= 0)
    {
      return PluginResult.PreParse.stopProcessing(
          ResultCode.valueOf(resultCode),
          LocalizableMessage.raw("Short-circuit in pre-parse"));
    }
    else
    {
      return PluginResult.PreParse.continueOperationProcessing();
    }
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PreParse
       doPreParse(PreParseExtendedOperation extendedOperation)
  {
    int resultCode = shortCircuitInternal(extendedOperation, "PreParse");
    if (resultCode >= 0)
    {
      return PluginResult.PreParse.stopProcessing(
          ResultCode.valueOf(resultCode),
          LocalizableMessage.raw("Short-circuit in pre-parse"));
    }
    else
    {
      return PluginResult.PreParse.continueOperationProcessing();
    }
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PreParse
       doPreParse(PreParseModifyOperation modifyOperation)
  {
    int resultCode = shortCircuitInternal(modifyOperation, "PreParse");
    if (resultCode >= 0)
    {
      return PluginResult.PreParse.stopProcessing(
          ResultCode.valueOf(resultCode),
          LocalizableMessage.raw("Short-circuit in pre-parse"));
    }
    else
    {
      return PluginResult.PreParse.continueOperationProcessing();
    }
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PreParse
       doPreParse(PreParseModifyDNOperation modifyDNOperation)
  {
    int resultCode = shortCircuitInternal(modifyDNOperation, "PreParse");
    if (resultCode >= 0)
    {
      return PluginResult.PreParse.stopProcessing(
          ResultCode.valueOf(resultCode),
          LocalizableMessage.raw("Short-circuit in pre-parse"));
    }
    else
    {
      return PluginResult.PreParse.continueOperationProcessing();
    }
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PreParse
       doPreParse(PreParseSearchOperation searchOperation)
  {
    int resultCode = shortCircuitInternal(searchOperation, "PreParse");
    if (resultCode >= 0)
    {
      return PluginResult.PreParse.stopProcessing(
          ResultCode.valueOf(resultCode),
          LocalizableMessage.raw("Short-circuit in pre-parse"));
    }
    else
    {
      return PluginResult.PreParse.continueOperationProcessing();
    }
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PreParse
       doPreParse(PreParseUnbindOperation unbindOperation)
  {
    int resultCode = shortCircuitInternal(unbindOperation, "PreParse");
    if (resultCode >= 0)
    {
      return PluginResult.PreParse.stopProcessing(
          ResultCode.valueOf(resultCode),
          LocalizableMessage.raw("Short-circuit in pre-parse"));
    }
    else
    {
      return PluginResult.PreParse.continueOperationProcessing();
    }
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PreOperation
       doPreOperation(PreOperationAddOperation addOperation)
  {
    int resultCode = shortCircuitInternal(addOperation, "PreOperation");
    if (resultCode >= 0)
    {
      return PluginResult.PreOperation.stopProcessing(
          ResultCode.valueOf(resultCode),
          LocalizableMessage.raw("Short-circuit in pre-operation"));
    }
    else
    {
      return PluginResult.PreOperation.continueOperationProcessing();
    }
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PreOperation
       doPreOperation(PreOperationBindOperation bindOperation)
  {
    int resultCode = shortCircuitInternal(bindOperation, "PreOperation");
    if (resultCode >= 0)
    {
      return PluginResult.PreOperation.stopProcessing(
          ResultCode.valueOf(resultCode),
          LocalizableMessage.raw("Short-circuit in pre-operation"));
    }
    else
    {
      return PluginResult.PreOperation.continueOperationProcessing();
    }
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PreOperation
       doPreOperation(PreOperationCompareOperation compareOperation)
  {
    int resultCode = shortCircuitInternal(compareOperation, "PreOperation");
    if (resultCode >= 0)
    {
      return PluginResult.PreOperation.stopProcessing(
          ResultCode.valueOf(resultCode),
          LocalizableMessage.raw("Short-circuit in pre-operation"));
    }
    else
    {
      return PluginResult.PreOperation.continueOperationProcessing();
    }
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PreOperation
       doPreOperation(PreOperationDeleteOperation deleteOperation)
  {
    int resultCode = shortCircuitInternal(deleteOperation, "PreOperation");
    if (resultCode >= 0)
    {
      return PluginResult.PreOperation.stopProcessing(
          ResultCode.valueOf(resultCode),
          LocalizableMessage.raw("Short-circuit in pre-operation"));
    }
    else
    {
      return PluginResult.PreOperation.continueOperationProcessing();
    }
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PreOperation
       doPreOperation(PreOperationExtendedOperation extendedOperation)
  {
    int resultCode = shortCircuitInternal(extendedOperation, "PreOperation");
    if (resultCode >= 0)
    {
      return PluginResult.PreOperation.stopProcessing(
          ResultCode.valueOf(resultCode),
          LocalizableMessage.raw("Short-circuit in pre-operation"));
    }
    else
    {
      return PluginResult.PreOperation.continueOperationProcessing();
    }
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PreOperation
       doPreOperation(PreOperationModifyOperation modifyOperation)
  {
    int resultCode = shortCircuitInternal(modifyOperation, "PreOperation");
    if (resultCode >= 0)
    {
      return PluginResult.PreOperation.stopProcessing(
          ResultCode.valueOf(resultCode),
          LocalizableMessage.raw("Short-circuit in pre-operation"));
    }
    else
    {
      return PluginResult.PreOperation.continueOperationProcessing();
    }
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PreOperation
       doPreOperation(PreOperationModifyDNOperation modifyDNOperation)
  {
    int resultCode = shortCircuitInternal(modifyDNOperation, "PreOperation");
    if (resultCode >= 0)
    {
      return PluginResult.PreOperation.stopProcessing(
          ResultCode.valueOf(resultCode),
          LocalizableMessage.raw("Short-circuit in pre-operation"));
    }
    else
    {
      return PluginResult.PreOperation.continueOperationProcessing();
    }
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PreOperation
       doPreOperation(PreOperationSearchOperation searchOperation)
  {
    int resultCode = shortCircuitInternal(searchOperation, "PreOperation");
    if (resultCode >= 0)
    {
      return PluginResult.PreOperation.stopProcessing(
          ResultCode.valueOf(resultCode),
          LocalizableMessage.raw("Short-circuit in pre-operation"));
    }
    else
    {
      return PluginResult.PreOperation.continueOperationProcessing();
    }
  }



  /**
   * Looks for a short-circuit request control in the operation and if one is
   * found with the correct section then generate the appropriate result.
   *
   * @param  operation  The operation to be processed.
   * @param  section    The section to match in the control value.
   *
   * @return  The result code that should be immediately sent to the client, or
   *          -1 if operation processing should continue as normal.
   */
  private int shortCircuitInternal(PluginOperation operation, String section)
  {
    try
    {
      ShortCircuitRequestControl control =
          operation.getRequestControl(ShortCircuitRequestControl.DECODER);
      if (control != null && section.equalsIgnoreCase(control.getSection()))
      {
        return control.resultCode;
      }
    }
    catch (Exception e)
    {
      System.err.println("***** ERROR:  Could not decode short circuit " +
          "control value:  " + e);
      e.printStackTrace();
      return -1;
    }

    // Check for registered short circuits.
    Integer resultCode = shortCircuits.get(
         operation.getOperationType() + "/" + section.toLowerCase());
    if (resultCode != null)
    {
      return resultCode;
    }

    // If we've gotten here, then we shouldn't short-circuit the operation
    // processing.
    return -1;
  }



  /**
   * Creates a short circuit request control with the specified result code and
   * section.
   *
   * @param  resultCode  The result code to return to the client.
   * @param  section     The section to use to determine when to short circuit.
   *
   * @return  The appropriate short circuit request control.
   */
  public static Control createShortCircuitControl(int resultCode,
                                                  String section)
  {
    return new ShortCircuitRequestControl(false, resultCode, section);
  }



  /**
   * Retrieves a list containing a short circuit control with the specified
   * result code and section.
   *
   * @param  resultCode  The result code to return to the client.
   * @param  section     The section to use to determine when to short circuit.
   *
   * @return  A list containing the appropriate short circuit request control.
   */
  public static List<Control> createShortCircuitControlList(int resultCode, String section)
  {
    return newArrayList(createShortCircuitControl(resultCode, section));
  }

  /** Registered short circuits for operations regardless of controls. */
  private static Map<String, Integer> shortCircuits = new ConcurrentHashMap<>();

  /**
   * Register a short circuit for the given operation type and plugin point.
   * @param operation The type of operation the short circuit applies to.
   * @param section The plugin point the short circuit applies to.
   * @param resultCode The result code to be returned for the short circuit.
   */
  public static void registerShortCircuit(OperationType operation, String section, int resultCode)
  {
    shortCircuits.put(operation + "/" + section.toLowerCase(), resultCode);
  }

  /**
   * Deregister a short circuit for the given operation type and plugin point.
   * @param operation The type of operation the short circuit applies to.
   * @param section The plugin point the short circuit applies to.
   */
  public static void deregisterShortCircuit(OperationType operation, String section)
  {
    shortCircuits.remove(operation + "/" + section.toLowerCase());
  }
}
