/*
 * 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.i18n.slf4j.LocalizedLogger;
import org.forgerock.opendj.config.server.ConfigException;
import org.forgerock.opendj.io.ASN1Writer;
import org.forgerock.opendj.ldap.ByteString;
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.DisconnectReason;
import org.opends.server.types.operation.*;
import org.opends.server.util.CollectionUtils;

/**
 * This class defines a very simple plugin that terminates the client connection
 * at any point in the plugin processing if the client request contains an
 * appropriate control with a value string that matches the plugin type.  The
 * valid sections for this plugin include:
 * <BR>
 * <UL>
 *   <LI>PreParse -- available for all types of operations</LI>
 *   <LI>PreOperation -- available for all types of operations except abandon
 *       and unbind</LI>
 *   <LI>PostOperation -- available for all types of operations</LI>
 *   <LI>PostResponse -- available for all types of operations except abandon
 *       and unbind</LI>
 * </UL>
 */
public class DisconnectClientPlugin
       extends DirectoryServerPlugin<PluginCfg>
{

  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();

  /**
   * The OID for the disconnect request control, which is used to flag
   * operations that should cause the client connection to be terminated.
   */
  public static final String OID_DISCONNECT_REQUEST =
       "1.3.6.1.4.1.26027.1.999.2";


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

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

    }

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


    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 section
     *          The section to use for the disconnect.
     */
    public DisconnectClientControl(boolean isCritical, String section)
    {
      super(OID_DISCONNECT_REQUEST, isCritical);
      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 java.io.IOException If a problem occurs while writing to the stream.
     */
    @Override
    protected void writeValue(ASN1Writer writer) throws IOException {
      writer.writeOctetString(section);
    }

    /**
     * Retrieves the delay duration.
     *
     * @return The delay duration.
     */
    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 DisconnectClientPlugin()
  {
    super();
  }



  /** {@inheritDoc} */
  @Override
  public void initializePlugin(Set<PluginType> pluginTypes,
                               PluginCfg configuration)
         throws ConfigException
  {
    // This plugin may only be used as a pre-parse, pre-operation,
    // post-operation, or post-response 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:
        case POST_OPERATION_ABANDON:
        case POST_OPERATION_ADD:
        case POST_OPERATION_BIND:
        case POST_OPERATION_COMPARE:
        case POST_OPERATION_DELETE:
        case POST_OPERATION_EXTENDED:
        case POST_OPERATION_MODIFY:
        case POST_OPERATION_MODIFY_DN:
        case POST_OPERATION_SEARCH:
        case POST_OPERATION_UNBIND:
        case POST_RESPONSE_ADD:
        case POST_RESPONSE_BIND:
        case POST_RESPONSE_COMPARE:
        case POST_RESPONSE_DELETE:
        case POST_RESPONSE_EXTENDED:
        case POST_RESPONSE_MODIFY:
        case POST_RESPONSE_MODIFY_DN:
        case POST_RESPONSE_SEARCH:
          // This is fine.
          break;
        default:
          throw new ConfigException(LocalizableMessage.raw("Invalid plugin type " + t +
                                    " for the disconnect plugin."));
      }
    }
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PreParse doPreParse(
      PreParseAbandonOperation abandonOperation)
  {
    disconnectInternal(abandonOperation, "PreParse");
    return PluginResult.PreParse.continueOperationProcessing();
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PreParse doPreParse(PreParseAddOperation addOperation)
      throws CanceledOperationException {
    if (disconnectInternal(addOperation, "PreParse"))
    {
      addOperation.checkIfCanceled(false);
    }
    return PluginResult.PreParse.continueOperationProcessing();
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PreParse doPreParse(PreParseBindOperation bindOperation)
  {
    disconnectInternal(bindOperation, "PreParse");
    return PluginResult.PreParse.continueOperationProcessing();
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PreParse
       doPreParse(PreParseCompareOperation compareOperation)
       throws CanceledOperationException {
    if (disconnectInternal(compareOperation, "PreParse"))
    {
      compareOperation.checkIfCanceled(false);
    }
    return PluginResult.PreParse.continueOperationProcessing();
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PreParse
       doPreParse(PreParseDeleteOperation deleteOperation)
       throws CanceledOperationException {
    if (disconnectInternal(deleteOperation, "PreParse"))
    {
      deleteOperation.checkIfCanceled(false);
    }
    return PluginResult.PreParse.continueOperationProcessing();
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PreParse
       doPreParse(PreParseExtendedOperation extendedOperation)
       throws CanceledOperationException {
    if (disconnectInternal(extendedOperation, "PreParse"))
    {
      extendedOperation.checkIfCanceled(false);
    }
    return PluginResult.PreParse.continueOperationProcessing();
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PreParse
       doPreParse(PreParseModifyOperation modifyOperation)
       throws CanceledOperationException {
    if (disconnectInternal(modifyOperation, "PreParse"))
    {
      modifyOperation.checkIfCanceled(false);
    }
    return PluginResult.PreParse.continueOperationProcessing();
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PreParse
       doPreParse(PreParseModifyDNOperation modifyDNOperation)
       throws CanceledOperationException {
    if (disconnectInternal(modifyDNOperation, "PreParse"))
    {
      modifyDNOperation.checkIfCanceled(false);
    }
    return PluginResult.PreParse.continueOperationProcessing();
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PreParse
       doPreParse(PreParseSearchOperation searchOperation)
       throws CanceledOperationException {
    if (disconnectInternal(searchOperation, "PreParse"))
    {
      searchOperation.checkIfCanceled(false);
    }
    return PluginResult.PreParse.continueOperationProcessing();
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PreParse
       doPreParse(PreParseUnbindOperation unbindOperation)
  {
    disconnectInternal(unbindOperation, "PreParse");
    return PluginResult.PreParse.continueOperationProcessing();
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PreOperation
       doPreOperation(PreOperationAddOperation addOperation)
       throws CanceledOperationException {
    if (disconnectInternal(addOperation, "PreOperation"))
    {
      addOperation.checkIfCanceled(false);
    }
    return PluginResult.PreOperation.continueOperationProcessing();
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PreOperation
       doPreOperation(PreOperationBindOperation bindOperation)
  {
    disconnectInternal(bindOperation, "PreOperation");
    return PluginResult.PreOperation.continueOperationProcessing();
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PreOperation
       doPreOperation(PreOperationCompareOperation compareOperation)
       throws CanceledOperationException {
    if (disconnectInternal(compareOperation, "PreOperation"))
    {
      compareOperation.checkIfCanceled(false);
    }
    return PluginResult.PreOperation.continueOperationProcessing();
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PreOperation
       doPreOperation(PreOperationDeleteOperation deleteOperation)
       throws CanceledOperationException {
    if (disconnectInternal(deleteOperation, "PreOperation"))
    {
      deleteOperation.checkIfCanceled(false);
    }
    return PluginResult.PreOperation.continueOperationProcessing();
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PreOperation
       doPreOperation(PreOperationExtendedOperation extendedOperation)
       throws CanceledOperationException {
    if (disconnectInternal(extendedOperation, "PreOperation"))
    {
      extendedOperation.checkIfCanceled(false);
    }
    return PluginResult.PreOperation.continueOperationProcessing();
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PreOperation
       doPreOperation(PreOperationModifyOperation modifyOperation)
       throws CanceledOperationException {
    if (disconnectInternal(modifyOperation, "PreOperation"))
    {
      modifyOperation.checkIfCanceled(false);
    }
    return PluginResult.PreOperation.continueOperationProcessing();
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PreOperation
       doPreOperation(PreOperationModifyDNOperation modifyDNOperation)
       throws CanceledOperationException {
    if (disconnectInternal(modifyDNOperation, "PreOperation"))
    {
      modifyDNOperation.checkIfCanceled(false);
    }
    return PluginResult.PreOperation.continueOperationProcessing();
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PreOperation
       doPreOperation(PreOperationSearchOperation searchOperation)
       throws CanceledOperationException {
    if (disconnectInternal(searchOperation, "PreOperation"))
    {
      searchOperation.checkIfCanceled(false);
    }
    return PluginResult.PreOperation.continueOperationProcessing();
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PostOperation
       doPostOperation(PostOperationAbandonOperation abandonOperation)
  {
    disconnectInternal(abandonOperation, "PostOperation");
    return PluginResult.PostOperation.continueOperationProcessing();
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PostOperation
       doPostOperation(PostOperationAddOperation addOperation)
  {
    disconnectInternal(addOperation, "PostOperation");
    return PluginResult.PostOperation.continueOperationProcessing();
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PostOperation
       doPostOperation(PostOperationBindOperation bindOperation)
  {
    disconnectInternal(bindOperation, "PostOperation");
    return PluginResult.PostOperation.continueOperationProcessing();
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PostOperation
       doPostOperation(PostOperationCompareOperation compareOperation)
  {
    disconnectInternal(compareOperation, "PostOperation");
    return PluginResult.PostOperation.continueOperationProcessing();
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PostOperation
       doPostOperation(PostOperationDeleteOperation deleteOperation)
  {
    disconnectInternal(deleteOperation, "PostOperation");
    return PluginResult.PostOperation.continueOperationProcessing();
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PostOperation
       doPostOperation(PostOperationExtendedOperation extendedOperation)
  {
    disconnectInternal(extendedOperation, "PostOperation");
    return PluginResult.PostOperation.continueOperationProcessing();
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PostOperation
       doPostOperation(PostOperationModifyOperation modifyOperation)
  {
    disconnectInternal(modifyOperation, "PostOperation");
    return PluginResult.PostOperation.continueOperationProcessing();
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PostOperation
       doPostOperation(PostOperationModifyDNOperation modifyDNOperation)
  {
    disconnectInternal(modifyDNOperation, "PostOperation");
    return PluginResult.PostOperation.continueOperationProcessing();
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PostOperation
       doPostOperation(PostOperationSearchOperation searchOperation)
  {
    disconnectInternal(searchOperation, "PostOperation");
    return PluginResult.PostOperation.continueOperationProcessing();
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PostOperation
       doPostOperation(PostOperationUnbindOperation unbindOperation)
  {
    disconnectInternal(unbindOperation, "PostOperation");
    return PluginResult.PostOperation.continueOperationProcessing();
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PostResponse
       doPostResponse(PostResponseAddOperation addOperation)
  {
    disconnectInternal(addOperation, "PostResponse");
    return PluginResult.PostResponse.continueOperationProcessing();
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PostResponse
       doPostResponse(PostResponseBindOperation bindOperation)
  {
    disconnectInternal(bindOperation, "PostResponse");
    return PluginResult.PostResponse.continueOperationProcessing();
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PostResponse
       doPostResponse(PostResponseCompareOperation compareOperation)
  {
    disconnectInternal(compareOperation, "PostResponse");
    return PluginResult.PostResponse.continueOperationProcessing();
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PostResponse
       doPostResponse(PostResponseDeleteOperation deleteOperation)
  {
    disconnectInternal(deleteOperation, "PostResponse");
    return PluginResult.PostResponse.continueOperationProcessing();
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PostResponse
       doPostResponse(PostResponseExtendedOperation extendedOperation)
  {
    disconnectInternal(extendedOperation, "PostResponse");
    return PluginResult.PostResponse.continueOperationProcessing();
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PostResponse
       doPostResponse(PostResponseModifyOperation modifyOperation)
  {
    disconnectInternal(modifyOperation, "PostResponse");
    return PluginResult.PostResponse.continueOperationProcessing();
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PostResponse
       doPostResponse(PostResponseModifyDNOperation modifyDNOperation)
  {
    disconnectInternal(modifyDNOperation, "PostResponse");
    return PluginResult.PostResponse.continueOperationProcessing();
  }



  /** {@inheritDoc} */
  @Override
  public PluginResult.PostResponse
       doPostResponse(PostResponseSearchOperation searchOperation)
  {
    disconnectInternal(searchOperation, "PostResponse");
    return PluginResult.PostResponse.continueOperationProcessing();
  }



  /**
   * Looks for a disconnect request control in the operation and if one is found
   * with the correct section then terminate the client connection.
   *
   * @param  operation  The operation to be processed.
   * @param  section    The section to match in the control value.
   *
   * @return  <CODE>true</CODE> if the client connection was terminated, or
   *          <CODE>false</CODE> if it was not.
   */
  private boolean disconnectInternal(PluginOperation operation,
                                     String section)
  {
    try
    {
      DisconnectClientControl control =
          operation.getRequestControl(DisconnectClientControl.DECODER);

      if (control != null && control.getSection().equalsIgnoreCase(section))
      {
        operation.disconnectClient(DisconnectReason.CLOSED_BY_PLUGIN, true,
            LocalizableMessage.raw("Closed by disconnect client plugin (section " +
                section + ")"));

        return true;
      }
    }
    catch (Exception e)
    {
      logger.error(LocalizableMessage.raw("Unable to decode the disconnect client control:  " + e));
    }

    // If we've gotten here, then we shouldn't disconnect the client.
    return false;
  }



  /**
   * Creates a disconnect request control with the specified section.
   *
   * @param  section  The section to use for the disconnect.
   *
   * @return  The appropriate disconnect request control.
   */
  public static Control createDisconnectControl(String section)
  {
    return new DisconnectClientControl(false, section);
  }



  /**
   * Retrieves a list containing a disconnect control with the specified
   * section.
   *
   * @param  section  The section to use for the disconnect.
   *
   * @return  A list containing the appropriate disconnect request control.
   */
  public static List<Control> createDisconnectControlList(String section)
  {
    Control c = new DisconnectClientControl(false, section);
    return CollectionUtils.newArrayList(c);
  }
}
