UnboundID Server SDK

Ping Identity
UnboundID Server SDK Documentation

ExampleExtendedOperationHandler.java

/*
 * 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
 * docs/licenses/cddl.txt
 * or http://www.opensource.org/licenses/cddl1.php.
 * 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
 * docs/licenses/cddl.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
 *
 *
 *      Portions Copyright 2010-2024 Ping Identity Corporation
 */
package com.unboundid.directory.sdk.examples;



import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.unboundid.asn1.ASN1Element;
import com.unboundid.asn1.ASN1OctetString;
import com.unboundid.asn1.ASN1Sequence;
import com.unboundid.asn1.ASN1Set;
import com.unboundid.directory.sdk.common.operation.ExtendedRequest;
import com.unboundid.directory.sdk.common.operation.UpdatableExtendedResult;
import com.unboundid.directory.sdk.common.types.InternalConnection;
import com.unboundid.directory.sdk.common.types.OperationContext;
import com.unboundid.directory.sdk.ds.api.ExtendedOperationHandler;
import com.unboundid.directory.sdk.ds.config.ExtendedOperationHandlerConfig;
import com.unboundid.directory.sdk.ds.types.DirectoryServerContext;
import com.unboundid.ldap.sdk.Attribute;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.ResultCode;
import com.unboundid.util.StaticUtils;
import com.unboundid.util.args.ArgumentException;
import com.unboundid.util.args.ArgumentParser;
import com.unboundid.util.args.StringArgument;



/**
 * This class provides a simple example of an extended operation handler which
 * may be used to retrieve a specified entry from the directory.  The extended
 * request must have a value, which will be encoded as follows:
 * <PRE>
 *   GetEntryRequest ::= SEQUENCE {
 *        dn             OCTET STRING
 *        attributes     SEQUENCE OF OCTET STRING OPTIONAL }
 * </PRE>
 * That is, the request value must include the DN of the entry to request, and
 * may also include a list of the attributes to return from that entry.  If the
 * entry exists, then the extended result will have a value with the following
 * encoding:
 * <PRE>
 *   GetEntryResult ::= SEQUENCE OF SEQUENCE {
 *        attributeDescription     OCTET STRING
 *        values                   SET OF OCTET STRING }
 * </PRE>
 * That is, the value will be a sequence of the attributes contained in that
 * entry.
 * <BR><BR>
 * This extended operation handler takes one configuration argument:
 * <UL>
 *   <LI>request-oid -- The OID that clients can use to request this extended
 *       operation.</LI>
 * </UL>
 */
public final class ExampleExtendedOperationHandler
       extends ExtendedOperationHandler
{
  /**
   * The name of the argument that will be used to specify the request OID for
   * the extended request.
   */
  private static final String ARG_NAME_REQUEST_OID = "request-oid";



  // The server context for the server in which this extension is running.
  private DirectoryServerContext serverContext;

  // The request OID for the extended request.
  private String requestOID;



  /**
   * Creates a new instance of this extended operation handler.  All extended
   * operation handler implementations must include a default constructor, but
   * any initialization should generally be done in the
   * {@code initializeExtendedOperationHandler} method.
   */
  public ExampleExtendedOperationHandler()
  {
    // No implementation required.
  }



  /**
   * Retrieves a human-readable name for this extension.
   *
   * @return  A human-readable name for this extension.
   */
  @Override()
  public String getExtensionName()
  {
    return "Example Extended Operation Handler";
  }



  /**
   * Retrieves a human-readable description for this extension.  Each element
   * of the array that is returned will be considered a separate paragraph in
   * generated documentation.
   *
   * @return  A human-readable description for this extension, or {@code null}
   *          or an empty array if no description should be available.
   */
  @Override()
  public String[] getExtensionDescription()
  {
    return new String[]
    {
      "This extended operation handler serves as an example that may be used " +
           "to demonstrate the process for creating a third-party " +
           "extended operation handler.  It may be used to retrieve all or " +
           "a select set of attributes from a specified entry in the server."
    };
  }



  /**
   * Updates the provided argument parser to define any configuration arguments
   * which may be used by this extended operation handler.  The argument parser
   * may also be updated to define relationships between arguments (e.g., to
   * specify required, exclusive, or dependent argument sets).
   *
   * @param  parser  The argument parser to be updated with the configuration
   *                 arguments which may be used by this extended operation
   *                 handler.
   *
   * @throws  ArgumentException  If a problem is encountered while updating the
   *                             provided argument parser.
   */
  @Override()
  public void defineConfigArguments(final ArgumentParser parser)
         throws ArgumentException
  {
    // Add an argument that allows you to specify the request OID.
    Character shortIdentifier = null;
    String    longIdentifier  = ARG_NAME_REQUEST_OID;
    boolean   required        = true;
    int       maxOccurrences  = 1;
    String    placeholder     = "{oid}";
    String    description     = "The OID that clients should use to request " +
         "this extended operation.";

    parser.addArgument(new StringArgument(shortIdentifier, longIdentifier,
         required, maxOccurrences, placeholder, description));
  }



  /**
   * Initializes this extended operation handler.
   *
   * @param  serverContext  A handle to the server context for the server in
   *                        which this extension is running.
   * @param  config         The general configuration for this extended
   *                        operation handler.
   * @param  parser         The argument parser which has been initialized from
   *                        the configuration for this extended operation
   *                        handler.
   *
   * @throws  LDAPException  If a problem occurs while initializing this
   *                         extended operation handler.
   */
  @Override()
  public void initializeExtendedOperationHandler(
                   final DirectoryServerContext serverContext,
                   final ExtendedOperationHandlerConfig config,
                   final ArgumentParser parser)
         throws LDAPException
  {
    serverContext.debugInfo(
         "Beginning extended operation handler initialization");

    this.serverContext = serverContext;

    // Get the request OID that should be used for this extended request.
    final StringArgument arg =
         (StringArgument) parser.getNamedArgument(ARG_NAME_REQUEST_OID);
    requestOID = arg.getValue();
  }



  /**
   * Indicates whether the configuration contained in the provided argument
   * parser represents a valid configuration for this extension.
   *
   * @param  config               The general configuration for this extended
   *                              operation handler.
   * @param  parser               The argument parser which has been initialized
   *                              with the proposed configuration.
   * @param  unacceptableReasons  A list that can be updated with reasons that
   *                              the proposed configuration is not acceptable.
   *
   * @return  {@code true} if the proposed configuration is acceptable, or
   *          {@code false} if not.
   */
  @Override()
  public boolean isConfigurationAcceptable(
                      final ExtendedOperationHandlerConfig config,
                      final ArgumentParser parser,
                      final List<String> unacceptableReasons)
  {
    boolean acceptable = true;

    // Make sure that the request OID is a valid numeric OID.
    final StringArgument arg =
         (StringArgument) parser.getNamedArgument(ARG_NAME_REQUEST_OID);
    final String oid = arg.getValue();


    if (! StaticUtils.isNumericOID(oid))
    {
      unacceptableReasons.add("The provided value '" + oid +
           "' is not a valid numeric OID.");
      acceptable = false;
    }

    return acceptable;
  }



  /**
   * Attempts to apply the configuration contained in the provided argument
   * parser.
   *
   * @param  config                The general configuration for this extended
   *                               operation handler.
   * @param  parser                The argument parser which has been
   *                               initialized with the new configuration.
   * @param  adminActionsRequired  A list that can be updated with information
   *                               about any administrative actions that may be
   *                               required before one or more of the
   *                               configuration changes will be applied.
   * @param  messages              A list that can be updated with information
   *                               about the result of applying the new
   *                               configuration.
   *
   * @return  A result code that provides information about the result of
   *          attempting to apply the configuration change.
   */
  @Override()
  public ResultCode applyConfiguration(
                         final ExtendedOperationHandlerConfig config,
                         final ArgumentParser parser,
                         final List<String> adminActionsRequired,
                         final List<String> messages)
  {
    // The only option that we support is the numeric OID.  However, that can't
    // be changed on the fly.  See if a new OID was configured, and if so then
    // indicate that admin action is required.
    final StringArgument arg =
         (StringArgument) parser.getNamedArgument(ARG_NAME_REQUEST_OID);
    final String newOID = arg.getValue();
    if (! requestOID.equals(newOID))
    {
      adminActionsRequired.add("The extended operation handler must be " +
           "disabled and re-enabled for changes to the request OID to take " +
           "effect.");
    }

    return ResultCode.SUCCESS;
  }



  /**
   * Performs any cleanup which may be necessary when this extended operation
   * handler is to be taken out of service.
   */
  @Override()
  public void finalizeExtendedOperationHandler()
  {
    // No finalization is required.
  }



  /**
   * Retrieves the name of the extended operation with the provided OID.
   *
   * @param  oid  The OID of the extended operation for which to retrieve the
   *              corresponding name.
   *
   * @return  The name of the extended operation with the specified OID, or
   *          {@code null} if the specified OID is not recognized by this
   *          extended operation handler.
   */
  @Override()
  public String getExtendedOperationName(final String oid)
  {
    if (oid.equals(requestOID))
    {
      return "Example Get Entry Extended Operation";
    }
    else
    {
      return null;
    }
  }



  /**
   * Retrieves the OIDs of the extended operation types supported by this
   * extended operation handler.
   *
   * @return  The OIDs of the extended operation types supported by this
   *          extended operation handler.  It must not be {@code null} or
   *          empty, and the contents of the set returned must not change over
   *          the life of this extended operation handler.
   */
  @Override()
  public Set<String> getSupportedExtensions()
  {
    final HashSet<String> supportedExtensions = new HashSet<String>(1);
    supportedExtensions.add(requestOID);
    return supportedExtensions;
  }



  /**
   * Retrieves the OIDs of any controls supported by this extended operation
   * handler.
   *
   * @return  The OIDs of any controls supported by this extended operation
   *          handler.  It may be {@code null} or empty if this extended
   *          operation handler does not support any controls.
   */
  @Override()
  public Set<String> getSupportedControls()
  {
    return Collections.emptySet();
  }



  /**
   * Retrieves the OIDs of any features supported by this extended operation
   * handler that should be advertised in the server root DSE.
   *
   * @return  The OIDs of any features supported by this extended operation
   *          handler.  It may be {@code null} or empty if this extended
   *          operation handler does not support any controls.
   */
  @Override()
  public Set<String> getSupportedFeatures()
  {
    return Collections.emptySet();
  }



  /**
   * Performs any processing appropriate for the provided extended request.
   *
   * @param  operationContext  The operation context for the extended operation.
   * @param  request           The extended request to be processed.
   * @param  result            The extended result to be updated with the result
   *                           of processing.
   */
  @Override()
  public void processExtendedOperation(final OperationContext operationContext,
                                       final ExtendedRequest request,
                                       final UpdatableExtendedResult result)
  {
    final boolean debugEnabled = serverContext.debugEnabled();
    if (debugEnabled)
    {
      serverContext.debugInfo("Starting processing for extended request " +
           request.toString());
    }


    // First, make sure that the request has a value and that we can decode it
    // as a sequence containing the target entry DN and optional set of
    // requested attributes.
    final ASN1OctetString requestValue = request.getRequestValue();
    if (requestValue == null)
    {
      if (debugEnabled)
      {
        serverContext.debugWarning(
             "The extended request does not have a value.");
      }

      result.setResultCode(ResultCode.PROTOCOL_ERROR);
      result.setDiagnosticMessage(
           "The provided request did not include a value.");
      return;
    }

    final String dn;
    final String[] attributes;
    try
    {
      final ASN1Sequence requestSequence =
           ASN1Sequence.decodeAsSequence(requestValue.getValue());
      final ASN1Element[] requestElements = requestSequence.elements();
      dn = requestElements[0].decodeAsOctetString().stringValue();
      if (requestElements.length == 2)
      {
        final ASN1Sequence attrSequence = requestElements[1].decodeAsSequence();
        final ASN1Element[] attrElements = attrSequence.elements();
        final ArrayList<String> attrList =
             new ArrayList<String>(attrElements.length);
        for (final ASN1Element e : attrElements)
        {
          attrList.add(e.decodeAsOctetString().stringValue());
        }
        attributes = new String[attrList.size()];
        attrList.toArray(attributes);
      }
      else
      {
        attributes = new String[0];
      }
    }
    catch (final Exception e)
    {
      serverContext.debugCaught(e);

      result.setResultCode(ResultCode.PROTOCOL_ERROR);
      result.setDiagnosticMessage("Unable to decode the request value:  " +
           StaticUtils.getExceptionMessage(e));
      return;
    }

    if (debugEnabled)
    {
      serverContext.debugInfo("Requested entry DN:  " + dn);
      serverContext.debugInfo("Requested attributes:  " +
           Arrays.toString(attributes));
    }


    // Get an internal connection authenticated as the user issuing the request
    // and use it to perform a search to get the target entry.
    final Entry entry;
    try
    {
      final InternalConnection conn =
           operationContext.getInternalUserConnection();
      entry = conn.getEntry(dn, attributes);
    }
    catch (final LDAPException le)
    {
      serverContext.debugCaught(le);
      result.setResultData(le);
      return;
    }

    if (entry == null)
    {
      result.setResultCode(ResultCode.NO_SUCH_OBJECT);
      result.setDiagnosticMessage("Requested entry " + dn +
           " does not exist or is not readable.");
      return;
    }


    // Encode the value for the result.
    final Collection<Attribute> entryAttributes = entry.getAttributes();
    final ArrayList<ASN1Element> attrElements =
         new ArrayList<ASN1Element>(entryAttributes.size());
    for (final Attribute a : entryAttributes)
    {
      final ArrayList<ASN1Element> valueElements =
           new ArrayList<ASN1Element>(a.size());
      for (final ASN1OctetString s : a.getRawValues())
      {
        valueElements.add(s);
      }

      attrElements.add(new ASN1Sequence(
           new ASN1OctetString(a.getName()),
           new ASN1Set(valueElements)));
    }

    final ASN1Sequence resultSequence = new ASN1Sequence(attrElements);


    // Prepare the result to return to the client.
    result.setResultCode(ResultCode.SUCCESS);
    result.setResultOID(requestOID);
    result.setResultValue(new ASN1OctetString(resultSequence.encode()));
  }



  /**
   * Retrieves a map containing examples of configurations that may be used for
   * this extension.  The map key should be a list of sample arguments, and the
   * corresponding value should be a description of the behavior that will be
   * exhibited by the extension when used with that configuration.
   *
   * @return  A map containing examples of configurations that may be used for
   *          this extension.  It may be {@code null} or empty if there should
   *          not be any example argument sets.
   */
  @Override()
  public Map<List<String>,String> getExamplesArgumentSets()
  {
    final LinkedHashMap<List<String>,String> exampleMap =
         new LinkedHashMap<List<String>,String>(1);

    exampleMap.put(
         Arrays.asList(ARG_NAME_REQUEST_OID + "=1.3.6.1.4.1.32473.1"),
         "Serve extended requests with a request OID of 1.3.6.1.4.1.32473.1 " +
              "by trying to decode the value as an entry DN and responding " +
              "with the corresponding entry in LDIF form.  Note that this " +
              "OID is in a range specifically reserved by IANA for " +
              "documentation examples and should not actually be used.");

    return exampleMap;
  }
}