UnboundID Server SDK

Ping Identity
UnboundID Server SDK Documentation

ExamplePlacementAlgorithm.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-2023 Ping Identity Corporation
 */
package com.unboundid.directory.sdk.examples;



import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;

import com.unboundid.directory.sdk.common.operation.AddRequest;
import com.unboundid.directory.sdk.common.types.OperationContext;
import com.unboundid.directory.sdk.proxy.api.PlacementAlgorithm;
import com.unboundid.directory.sdk.proxy.config.PlacementAlgorithmConfig;
import com.unboundid.directory.sdk.proxy.types.BackendSet;
import com.unboundid.directory.sdk.proxy.types.ProxyServerContext;
import com.unboundid.ldap.sdk.Attribute;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.ResultCode;
import com.unboundid.util.args.ArgumentException;
import com.unboundid.util.args.ArgumentParser;
import com.unboundid.util.args.IntegerArgument;
import com.unboundid.util.args.StringArgument;



/**
 * This class provides a simple example of a placement algorithm which sends
 * entries to different backend sets based on a hash generated from the value of
 * a specified attribute.  If an entry has multiple values for the specified
 * attribute, then only the first value will be used.  If an entry does not
 * have the specified attribute, then a simple round-robin distribution will be
 * used.
 * <BR><BR>
 * It takes the following configuration arguments:
 * <UL>
 *   <LI>attribute-name -- The name of the attribute from which to generate the
 *       hash.</LI>
 *   <LI>max-characters -- The maximum number of characters to use from the
 *       value when generating the hash.  A value of zero indicates that all
 *       characters in the value should be used.</LI>
 * </UL>
 */
public final class ExamplePlacementAlgorithm
       extends PlacementAlgorithm
{
  /**
   * The name of the argument that will be used to specify the name of the
   * attribute for which to generate the hash.
   */
  private static final String ARG_NAME_ATTR = "attribute-name";



  /**
   * The name of the argument that will be used to specify the maximum number of
   * characters to use to generate the hash.
   */
  private static final String ARG_NAME_MAX_CHARS = "max-characters";



  // A counter that will be used to select backend sets for entries that don't
  // contain the target attribute.
  private final AtomicLong roundRobinCounter;

  // The maximum number of characters to use in the hash.
  private volatile int maxChars;

  // The list of backend sets currently in use.
  private volatile List<BackendSet> backendSets;

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

  // The name of the attribute to use to generate the hash.
  private volatile String attributeName;



  /**
   * Creates a new instance of this placement algorithm.  All placement
   * algorithm implementations must include a default constructor, but any
   * initialization should generally be done in the
   * {@code initializePlacementAlgorithm} method.
   */
  public ExamplePlacementAlgorithm()
  {
    roundRobinCounter = new AtomicLong(0L);
  }



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



  /**
   * 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 placement algorithm serves an example that may be used to " +
           "demonstrate the process for creating a third-party placement " +
           "algorithm.  It will generate a hash from the first value of a " +
           "specified attribute and will use that to select which backend " +
           "set should be used to hold the associated entry."
    };
  }



  /**
   * Updates the provided argument parser to define any configuration arguments
   * which may be used by this placement algorithm.  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 placement algorithm.
   *
   * @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 attribute name.
    Character shortIdentifier = null;
    String    longIdentifier  = ARG_NAME_ATTR;
    boolean   required        = true;
    int       maxOccurrences  = 1;
    String    placeholder     = "{attr}";
    String    description     = "The name of the attribute whose value " +
         "be used to generate a hash for use when selecting the backend set.";

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


    // Add an argument that allows you to specify the maximum number of
    // characters to use when generating the hash.
    shortIdentifier = null;
    longIdentifier  = ARG_NAME_MAX_CHARS;
    required        = true;
    maxOccurrences  = 1;
    placeholder     = "{value}";
    description     = "The maximum number of characters to use when " +
         "generating the hash.  A value of zero indicates that the entire " +
         "value should be used.";

    int lowerBound   = 0;
    int upperBound   = Integer.MAX_VALUE;
    int defaultValue = 0;

    parser.addArgument(new IntegerArgument(shortIdentifier, longIdentifier,
         required, maxOccurrences, placeholder, description, lowerBound,
         upperBound, defaultValue));
  }



  /**
   * Initializes this placement algorithm.
   *
   * @param  serverContext    A handle to the server context for the server in
   *                          which this extension is running.
   * @param  config           The general configuration for this placement
   *                          algorithm.
   * @param  parser           The argument parser which has been initialized
   *                          from the configuration for this placement
   *                          algorithm.
   * @param  balancingBaseDN  The balancing base DN for the associated
   *                          entry-balancing request processor.
   * @param  backendSets      The list of backend sets that will be used with
   *                          the entry-balancing request processor.
   *
   * @throws  LDAPException  If a problem occurs while initializing this
   *                         placement algorithm.
   */
  @Override()
  public void initializePlacementAlgorithm(
                   final ProxyServerContext serverContext,
                   final PlacementAlgorithmConfig config,
                   final ArgumentParser parser,
                   final String balancingBaseDN,
                   final List<BackendSet> backendSets)
         throws LDAPException
  {
    serverContext.debugInfo("Beginning placement algorithm initialization");

    this.serverContext = serverContext;
    this.backendSets   = backendSets;


    // Get the target attribute name.
    final StringArgument attrArg =
         (StringArgument) parser.getNamedArgument(ARG_NAME_ATTR);
    attributeName = attrArg.getValue();


    // Get the maximum number of characters to use from the value.
    final IntegerArgument maxCharsArg =
         (IntegerArgument) parser.getNamedArgument(ARG_NAME_MAX_CHARS);
    maxChars = maxCharsArg.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 placement
   *                              algorithm.
   * @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 PlacementAlgorithmConfig config,
                      final ArgumentParser parser,
                      final List<String> unacceptableReasons)
  {
    // No special validation is required.
    return true;
  }



  /**
   * Attempts to apply the configuration contained in the provided argument
   * parser.
   *
   * @param  config                The general configuration for this placement
   *                               algorithm.
   * @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 PlacementAlgorithmConfig config,
                                       final ArgumentParser parser,
                                       final List<String> adminActionsRequired,
                                       final List<String> messages)
  {
    // Get the new target attribute name.
    final StringArgument attrArg =
         (StringArgument) parser.getNamedArgument(ARG_NAME_ATTR);
    final String newAttr = attrArg.getValue();


    // Get the new maximum number of characters to use from the value.
    final IntegerArgument maxCharsArg =
         (IntegerArgument) parser.getNamedArgument(ARG_NAME_MAX_CHARS);
    final int newMax = maxCharsArg.getValue();


    attributeName = newAttr;
    maxChars      = newMax;

    return ResultCode.SUCCESS;
  }



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



  /**
   * Adapts to a change in the backend sets configured for use with the
   * associated entry-balancing request processor.
   *
   * @param  balancingBaseDN  The updated balancing base DN for the associated
   *                          entry-balancing request processor.
   * @param  backendSets      The updated list of backend sets for the
   *                          associated entry-balancing request processor.
   */
  @Override()
  public void applyBalancingConfigurationChange(final String balancingBaseDN,
                   final List<BackendSet> backendSets)
  {
    this.backendSets = backendSets;
  }



  /**
   * Determines the backend set that should be used to process the specified
   * add operation.
   *
   * @param  operationContext  The operation context for the add operation
   *                           to be processed.
   * @param  addRequest        The add request being processed.
   *
   * @return  The backend set in which the add should be processed, or
   *          {@code null} if there is no appropriate backend set.
   */
  @Override()
  public BackendSet selectBackendSet(final OperationContext operationContext,
                                     final AddRequest addRequest)
  {
    // Create a local copy of the configuration in case it changes while this
    // method is running.
    final int              max  = maxChars;
    final List<BackendSet> sets = backendSets;
    final String           attr = attributeName;


    // Get the target attribute from the entry to be added.  Find the first
    // value and use it to make the determination.
    final List<Attribute> attrList = addRequest.getEntry().getAttribute(attr);
    if (attrList != null)
    {
      for (final Attribute a : attrList)
      {
        final String value = a.getValue();
        if (value != null)
        {
          final String s;
          if ((max == 0) || (max <= value.length()))
          {
            s = value;
          }
          else
          {
            s = value.substring(0, max);
          }

          final int slot = (s.hashCode() % sets.size());
          return sets.get(slot);
        }
      }
    }


    // If we've gotten here, then the entry doesn't have a value for the
    // specified attribute, so we should use the round-robin counter.
    final int slot =
         (int) (roundRobinCounter.getAndIncrement() % sets.size());
    return sets.get(slot);
  }



  /**
   * 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_ATTR + "=cn",
              ARG_NAME_MAX_CHARS + "=10"),
         "Select the backend set to use for add operations based on a hash " +
              "of up to the first 10 characters of the first value of the " +
              "cn attribute.");

    return exampleMap;
  }



  /**
   * Appends a string representation of this LDAP health check to the provided
   * buffer.
   *
   * @param  buffer  The buffer to which the string representation should be
   *                 appended.
   */
  @Override()
  public void toString(final StringBuilder buffer)
  {
    buffer.append("ExamplePlacementAlgorithm(attributeName='");
    buffer.append(attributeName);
    buffer.append("', maxChars=");
    buffer.append(maxChars);
    buffer.append(')');
  }
}