UnboundID Server SDK

Ping Identity
UnboundID Server SDK Documentation

ExampleScriptedLDAPSyncSourcePlugin.groovy

/*
 * 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.groovy;



import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.regex.Pattern;

import com.unboundid.directory.sdk.sync.scripting.ScriptedLDAPSyncSourcePlugin;
import com.unboundid.directory.sdk.sync.config.LDAPSyncSourcePluginConfig;
import com.unboundid.directory.sdk.sync.types.PostStepResult;
import com.unboundid.directory.sdk.sync.types.PreStepResult;
import com.unboundid.directory.sdk.sync.types.SyncOperation;
import com.unboundid.directory.sdk.sync.types.SyncServerContext;
import com.unboundid.ldap.sdk.Attribute;
import com.unboundid.ldap.sdk.DN;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.Filter;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPInterface;
import com.unboundid.ldap.sdk.Modification;
import com.unboundid.ldap.sdk.ResultCode;
import com.unboundid.ldap.sdk.SearchRequest;
import com.unboundid.ldap.sdk.SearchResult;
import com.unboundid.ldap.sdk.SearchResultEntry;
import com.unboundid.ldap.sdk.SearchScope
import com.unboundid.util.args.ArgumentException;
import com.unboundid.util.args.ArgumentParser;
import com.unboundid.util.args.DNArgument;
import com.unboundid.util.args.StringArgument;



/**
 * This class provides a simple example of a scripted LDAP sync source plugin
 * which manipulates an attribute that stores a reference to another entry.
 * In the LDAP source server, the reference is stored as a DN, but the
 * destination instance (possible a relational database) stores the reference
 * using a key for the referenced entry (e.g. uid).
 * <UL>
 *   <LI>{@code postFetch} -- After an entry is returned from the source, the
 *       key value in the referenced attribute must be retrieved by doing
 *       a search for the referenced entry.
 *   </LI>
 * </UL>
 * It takes the following arguments:
 * <UL>
 *   <LI>reference-attribute -- The source attribute that stores the
 *                              DN of the referenced entry.</LI>
 *   <LI>referenced-entry-key-attribute -- The key attribute on the referenced
 *                              entry that is used to find the entry, e.g.
 *                              uid.</LI>
 *   <LI>search-base-dn -- The base DN to search for referenced entries using
 *                         the key.</LI>
 * </UL>
 */
public class ExampleScriptedLDAPSyncSourcePlugin
     extends ScriptedLDAPSyncSourcePlugin
{

  private static final String ARG_NAME_REFERENCE_ATTRIBUTE =
       "reference-attribute";
  private static final String ARG_NAME_REFERENCED_ENTRY_KEY_ATTRIBUTE =
       "referenced-entry-key-attribute";
  private static final String ARG_NAME_SEARCH_BASE_DN = "search-base-dn";

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

  // This lock ensures that the configuration is updated atomically and safely.
  private final ReadWriteLock configLock = new ReentrantReadWriteLock();
  private final Lock configReadLock = configLock.readLock();
  private final Lock configWriteLock = configLock.writeLock();

  private String referenceAttribute;

  private String referencedEntryKeyAttribute;

  private DN searchBaseDn;



  /**
   * Updates the provided argument parser to define any configuration arguments
   * which may be used by this sync pipe plugin.  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 sync pipe plugin.
   *
   * @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 source attribute.
    Character shortIdentifier = null;
    String    longIdentifier  = ARG_NAME_REFERENCE_ATTRIBUTE;
    boolean   required        = true;
    int       maxOccurrences  = 1;
    String    placeholder     = "{attr}";
    String    description     = "The name of the source attribute that " +
         "stores a reference to another entry as a DN.";

    StringArgument arg = new StringArgument(shortIdentifier, longIdentifier,
         required, maxOccurrences, placeholder, description);
    arg.setValueRegex(Pattern.compile("^[a-zA-Z][a-zA-Z0-9\\\\-]*\$"),
                      "A valid attribute name is required.");
    parser.addArgument(arg);

    shortIdentifier = null;
    longIdentifier  = ARG_NAME_REFERENCED_ENTRY_KEY_ATTRIBUTE;
    required        = true;
    maxOccurrences  = 1;
    placeholder     = "{attr}";
    description     = "The name of the key attribute on the source " +
         "entry that is referenced by the entry being synchronized.  For " +
         "instance, this could be 'uid' if the referring entry stored the " +
         "value of the uid attribute in the referenced entry.";

    arg = new StringArgument(shortIdentifier, longIdentifier,
         required, maxOccurrences, placeholder, description);
    arg.setValueRegex(Pattern.compile("^[a-zA-Z][a-zA-Z0-9\\\\-]*\$"),
                      "A valid attribute name is required.");
    parser.addArgument(arg);

    shortIdentifier = null;
    longIdentifier  = ARG_NAME_SEARCH_BASE_DN;
    required        = true;
    maxOccurrences  = 1;
    placeholder     = "{base-dn}";
    description     = "The base DN to use when searching for referenced " +
         "entries.  A subtree search will be issued with a base of the DN " +
         "specified here to find the referenced entries.";

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



  /**
   * Initializes this LDAP sync source plugin.  This method will be called
   * before any other methods in the class.
   *
   * @param  serverContext  A handle to the server context for the
   *                        Data Sync Server in which this extension is
   *                        running.  Extensions should typically store this
   *                        in a class member.
   * @param  config         The general configuration for this proxy
   *                        transformation.
   * @param  parser         The argument parser which has been initialized from
   *                        the configuration for this LDAP sync source
   *                        plugin.
   *
   * @throws  LDAPException  If a problem occurs while initializing this ldap
   *                         sync source plugin.
   */
  @Override
  public void initializeLDAPSyncSourcePlugin(
       final SyncServerContext serverContext,
       final LDAPSyncSourcePluginConfig config,
       final ArgumentParser parser)
       throws LDAPException
  {
    this.serverContext = serverContext;
    setConfig(config, parser);
  }



  /**
    * Indicates whether the configuration contained in the provided argument
    * parser represents a valid configuration for this extension.
    *
    * @param  config               The general configuration for this LDAP sync
    *                              source plugin.
    * @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 LDAPSyncSourcePluginConfig config,
       final ArgumentParser parser,
       final List<String> unacceptableReasons)
  {
    // The built-in ArgumentParser validation does all of the validation that
    // we need.
    return true;
  }



  /**
   * Attempts to apply the configuration contained in the provided argument
   * parser.
   *
   * @param  config                The general configuration for this LDAP sync
   *                               source.
   * @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 LDAPSyncSourcePluginConfig config,
       final ArgumentParser parser,
       final List<String> adminActionsRequired,
       final List<String> messages)
  {
    setConfig(config, parser);
    return ResultCode.SUCCESS;
  }



  /**
   * Sets the configuration for this plugin.  This is a centralized place
   * where the configuration is initialized or updated.
   *
   * @param  config         The general configuration for this LDAP sync
   *                        source plugin.
   * @param  parser         The argument parser which has been initialized from
   *                        the configuration for this LDAP sync source
   *                        plugin.
   */
  private void setConfig(
                   final LDAPSyncSourcePluginConfig config,
                   final ArgumentParser parser)
  {
    configWriteLock.lock();
    try
    {
      this.referenceAttribute =((StringArgument)parser.getNamedArgument(
            ARG_NAME_REFERENCE_ATTRIBUTE)).getValue();
      this.referencedEntryKeyAttribute =
           ((StringArgument)parser.getNamedArgument(
             ARG_NAME_REFERENCED_ENTRY_KEY_ATTRIBUTE)).getValue();
      this.searchBaseDn = ((DNArgument)parser.getNamedArgument(
            ARG_NAME_SEARCH_BASE_DN)).getValue();
    }
    finally
    {
      configWriteLock.unlock();
    }
  }



  /**
   * This method is called after fetching a source entry.  A
   * connection to the source server is provided.  This method is
   * overridden by plugins that need to manipulate the search results that
   * are returned to the Sync Pipe.  This can include filtering out certain
   * entries, remove information from the entries, or adding additional
   * information, possibly by doing a followup LDAP search.
   *
   * @param  sourceConnection       A connection to the source server.
   * @param  fetchedEntryRef        A reference to the entry that was fetched.
   *                                This entry can be edited in place, or the
   *                                reference can be changed to point to a
   *                                different entry that the plugin constructs.
   * @param  operation              The synchronization operation for this
   *                                change.
   *
   * @return  The result of the plugin processing.
   *
   * @throws  LDAPException  In general subclasses should not catch
   *                         LDAPExceptions that are thrown when
   *                         using the LDAPInterface unless there
   *                         are specific exceptions that are
   *                         expected.  The Data Sync Server
   *                         will handle LDAPExceptions in an
   *                         appropriate way based on the specific
   *                         cause of the exception.  For example,
   *                         some errors will result in the
   *                         SyncOperation being retried, and others
   *                         will trigger fail over to a different
   *                         server.  Plugins should only throw
   *                         LDAPException for errors related to
   *                         communication with the LDAP server.
   *                         Use the return code to indicate other
   *                         types of errors, which might require
   *                         retry.
   */
  @Override
  public PostStepResult postFetch(final LDAPInterface sourceConnection,
                                  final AtomicReference<Entry> fetchedEntryRef,
                                  final SyncOperation operation)
       throws LDAPException
  {
    try
    {
      configReadLock.lock();

      Entry entry = fetchedEntryRef.get();
      if (entry == null)
      {
        return PostStepResult.CONTINUE;
      }

      String[] referenceDns = entry.getAttributeValues(referenceAttribute);
      if (referenceDns == null)
      {
        serverContext.debugInfo("Entry " + entry.getDN() + " does not have " +
            "any values for the " + referencedEntryKeyAttribute +
            "attribute.");
        return PostStepResult.CONTINUE;
      }

      List<String> keyValues = new ArrayList<String>(referenceDns.length);
      for (int i = 0; i < referenceDns.length; i++)
      {
        String referenceDn = referenceDns[i];
        Entry referencedEntry = sourceConnection.getEntry(referenceDn,
           referencedEntryKeyAttribute);
        if (referencedEntry != null)
        {
          String keyValue =
               referencedEntry.getAttributeValue(referencedEntryKeyAttribute);
          if (keyValue != null)
          {
            keyValues.add(keyValue);
          }
          else
          {
            operation.logError("For fetched entry '" + entry.getDN() + "', " +
                "the example plugin could not find a corresponding " +
                referencedEntryKeyAttribute + " value for referenced entry " +
                referenceDn + " but the referenced entry does exist.");
          }
        }
        else
        {
          operation.logError("For fetched entry '" + entry.getDN() + "', " +
              "the example plugin could not find the entry, " +
              referenceDn + ", which is referenced by " + referenceAttribute +
              ".");
        }
      }

      if (keyValues.isEmpty())
      {
        operation.logInfo("Example plugin removed the " + referenceAttribute +
            " attribute because no entries could be found to match the list " +
            "DNs: " + Arrays.asList(referenceDns));
        entry.removeAttribute(referenceAttribute);
      }
      else
      {
        operation.logInfo("Example plugin replacing DN(s)=" +
            Arrays.asList(referenceDns) + " in attribute " +
            referenceAttribute + " with " + referencedEntryKeyAttribute +
            " value(s)=" + keyValues);
        entry.setAttribute(new Attribute(referenceAttribute, keyValues));
      }

      return PostStepResult.CONTINUE;
    }
    finally
    {
      configReadLock.unlock();
    }
  }
}