UnboundID Server SDK

Ping Identity
UnboundID Server SDK Documentation

ExampleScriptedLDAPSyncDestinationPlugin.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.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.ScriptedLDAPSyncDestinationPlugin;
import com.unboundid.directory.sdk.sync.config.LDAPSyncDestinationPluginConfig;
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.ldap.sdk.DeleteRequest;
import com.unboundid.ldap.sdk.UpdatableLDAPRequest;
import com.unboundid.ldap.sdk.unboundidds.controls.SoftDeleteRequestControl;
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 destination
 * plugin which manipulates an attribute that stores a reference to another
 * entry.  In the LDAP destination server, the reference is stored as a DN, but
 * the source 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>
 *   <LI>{@code preCreate} -- When an entry is created at the destination,
 *       the key attribute values in the reference attribute must be replaced
 *       with DNs (i.e. the reverse of what happens in {@code postFetch} by
 *       retrieving the referenced entries from the destination server using
 *       the key attribute value.
 *   </LI>
 *   <LI>{@code preModify} -- If the reference attribute is modified,
 *       the key attribute values in the reference attribute must be replaced
 *       with DNs (i.e. the reverse of what happens in {@code postFetch} by
 *       retrieving the referenced entries from the destination server using
 *       the key attribute value.
 *   </LI>
 * </UL>
 * In addition, {@code transformRequest} has been implemented to convert
 * hard deletes into soft deletes.
 *
 * It takes the following arguments:
 * <UL>
 *   <LI>reference-attribute -- The destination 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 ExampleScriptedLDAPSyncDestinationPlugin
     extends ScriptedLDAPSyncDestinationPlugin
{

  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 destination 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 destination " +
         "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 destination 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 destination
   *                        plugin.
   *
   * @throws  LDAPException  If a problem occurs while initializing this ldap
   *                         sync destination plugin.
   */
  @Override
  public void initializeLDAPSyncDestinationPlugin(
       final SyncServerContext serverContext,
       final LDAPSyncDestinationPluginConfig 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
    *                              destination 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 LDAPSyncDestinationPluginConfig 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
   *                               destination.
   * @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 LDAPSyncDestinationPluginConfig 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
   *                        destination plugin.
   * @param  parser         The argument parser which has been initialized from
   *                        the configuration for this LDAP sync destination
   *                        plugin.
   */
  private void setConfig(
                   final LDAPSyncDestinationPluginConfig 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 an attempt to fetch a destination entry.  An
   * connection to the destination server is provided along with the
   * {@code SearchRequest} that was sent to the server.  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.
   * <p>
   * This method might be called multiple times for a single synchronization
   * operation, specifically when there are multiple search criteria or
   * multiple base DNs defined for the Sync Destination.
   * <p>
   * This method will not be called if the search fails, for instance, if
   * the base DN of the search does not exist.
   *
   * @param  destinationConnection  A connection to the destination server.
   * @param  searchRequest          The search request that the LDAP Sync
   *                                Destination used to fetch the entry.
   * @param  fetchedEntries         A list of entries that have been fetched.
   *                                When the search criteria matches multiple
   *                                entries, they will all be returned.  Entries
   *                                in this list can be edited directly, and the
   *                                list can be edited as well.
   * @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 destinationConnection,
                                  final SearchRequest searchRequest,
                                  final List<Entry> fetchedEntries,
                                  final SyncOperation operation)
       throws LDAPException
  {
    configReadLock.lock();

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

        List<String> keyValues = new ArrayList<String>(referenceDns.length);
        for (int i = 0; i < referenceDns.length; i++)
        {
          String referenceDn = referenceDns[i];
          Entry referencedEntry = destinationConnection.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();
    }
  }



  /**
   * This method is called before a destination entry is created.  A
   * connection to the destination server is provided along with the
   * {@code Entry} that will be sent to the server.  This method is
   * overridden by plugins that need to alter the entry before it is created
   * at the server.
   *
   * @param  destinationConnection  A connection to the destination server.
   * @param  entryToCreate          The entry that will be created at the
   *                                destination.  A plugin that wishes to
   *                                create the entry should be sure to return
   *                                {@code PreStepResult#SKIP_CURRENT_STEP}.
   * @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 PreStepResult preCreate(final LDAPInterface destinationConnection,
                                 final Entry entryToCreate,
                                 final SyncOperation operation)
       throws LDAPException
  {
    try
    {
      configReadLock.lock();

      String[] referenceValues =
           entryToCreate.getAttributeValues(referenceAttribute);
      if (referenceValues == null)
      {
        return PreStepResult.CONTINUE;
      }

      List<String> dnValues = new ArrayList<String>(referenceValues.length);
      for (int i = 0; i < referenceValues.length; i++)
      {
        String referenceValue = referenceValues[i];

        String dnValue = lookupDnByKeyValue(destinationConnection,
             entryToCreate, operation, referenceValue);
        if (dnValue != null)
        {
          dnValues.add(dnValue);
        }
      }

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

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



  /**
   * This method is called before a destination entry is modified.  A
   * connection to the destination server is provided along with the
   * {@code Entry} that will be sent to the server.  This method is
   * overridden by plugins that need to perform some processing on an entry
   * before it is modified.
   *
   * @param  destinationConnection  A connection to the destination server.
   * @param  entryToModify          The entry that will be modified at the
   *                                destination.  A plugin that wishes to
   *                                modify the entry should be sure to return
   *                                {@code PreStepResult#SKIP_CURRENT_STEP}.
   * @param  modsToApply            A modifiable list of the modifications to
   *                                apply at the server.
   * @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 PreStepResult preModify(final LDAPInterface destinationConnection,
                                 final Entry entryToModify,
                                 final List<Modification> modsToApply,
                                 final SyncOperation operation)
       throws LDAPException
  {
    try
    {
      configReadLock.lock();

      List<Modification> updatedModsToApply =
           new ArrayList<Modification>(modsToApply.size());

      for (Modification modification : modsToApply)
      {
        // Let any modification to non-referenceAttribute attributes pass
        // straight through.
        if (!referenceAttribute.equalsIgnoreCase(
        modification.getAttributeName()))
        {
          updatedModsToApply.add(modification);
          continue;
        }

        // For the referenceAttribute attribute, convert keys to DNs.
        String[] keyValues = modification.getValues();
        List<String> dnValues = new ArrayList<String>(keyValues.length);
        for (String keyValue : keyValues)
        {
          String dnValue = lookupDnByKeyValue(destinationConnection,
                                              entryToModify,
                                              operation,
                                              keyValue);
          if (dnValue != null)
          {
            dnValues.add(dnValue);
          }
        }

        // If we started off with no values, or ended up with at least one
        // DN, then let this mod pass through.
        if ((keyValues.length == 0) || !dnValues.isEmpty())
        {
          String[] dnValuesArr = dnValues.toArray(new String[0]);
          Modification updatedMod =
               new Modification(modification.getModificationType(),
                                modification.getAttributeName(), dnValuesArr);
          updatedModsToApply.add(updatedMod);
        }
      }

      // We can't edit the mods in place, so we just replace them with the
      // list that we built up.
      modsToApply.clear();
      modsToApply.addAll(updatedModsToApply);

      if (modsToApply.isEmpty())
      {
        // If there are no mods, then skip the current step.
        return PreStepResult.SKIP_CURRENT_STEP;
      }
      else
      {
        return PreStepResult.CONTINUE;
      }
    }
    finally
    {
      configReadLock.unlock();
    }
  }



  /**
   * This method is called prior to executing any add, modify, delete, or
   * search from the destination but after the respective pre method (e.g
   * preFetch or preModify). A connection to the destination server is provided
   * along with the {@code UpdatableLDAPRequest} that will be sent to the
   * server. this method is overridden by plugins that need to modify the
   * LDAP request prior to execution. For example, attaching a {@code Control}
   * to the request. Callers of this method can use {@code instanceof}
   * to determine which type of LDAP request is being made.
   *
   * @param destinationConnection A connection to the destination server.
   * @param request               The LDAP request that will be sent to
   *                              the destination server.
   * @param operation             The synchronization operation for this
   *                              change.
   *
   * @return  The result of the plugin processing. Be very careful when
   *          returning {@code PreStepResult#RETRY_OPERATION_UNLIMITED} as this
   *          can stall all in flight operations until this operation completes.
   *          This return value should only be used in situations where a
   *          remote service (e.g., the LDAP server) is unavailable. In this
   *          case, it's preferable to just throw the underlying LDAPException,
   *          which the Data Sync Server will handle correctly based on
   *          the type of the operation.
   *
   * @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 PreStepResult transformRequest(final LDAPInterface destinationConnection,
                                        final UpdatableLDAPRequest request,
                                        final SyncOperation operation)
  {
    // If the request is a delete request then attach
    // the soft delete request control.
    if (request instanceof DeleteRequest)
    {
      request.addControl(new SoftDeleteRequestControl());
    }

    return PreStepResult.CONTINUE;
  }



  /**
   * Returns the DN of the entry with a {@code referencedEntryKeyAttribute}
   * value of {@code referenceValue}.
   *
   * @param destinationConnection  A connection to the destination server.
   * @param entry                  The referencing entry.
   * @param operation              The synchronization operation.
   * @param referenceValue         The value of the reference attribute.
   *
   * @return  The DN of the referenced entry or {@code null} if there is not
   *          exactly one referenced entry.
   *
   * @throws  LDAPException  If there is a problem retrieving the remote entry.
   */
  private String lookupDnByKeyValue(final LDAPInterface destinationConnection,
                                    final Entry entry,
                                    final SyncOperation operation,
                                    final String referenceValue)
       throws LDAPException
  {
    Filter filter = Filter.createEqualityFilter(referencedEntryKeyAttribute,
                                                referenceValue);

    SearchResult searchResult = destinationConnection.search(
         searchBaseDn.toString(),
         SearchScope.SUB,
         filter,
         "1.1"); // Don't return any attributes.

    List<SearchResultEntry> entries = searchResult.getSearchEntries();
    if (entries.isEmpty())
    {
      operation.logError("For entry '" +
          entry.getDN() + "', " +
          "the example plugin could not find any entry that matched " +
          filter.toString() + ".  This value of the " + referenceAttribute +
          " attribute will not be included in the entry.");
      return null;
    }
    else if (entries.size() > 1)
    {
      operation.logError("For entry '" +
          entry.getDN() + "', " +
          "the example plugin found " + entries.size() + " entries that " +
          "matched " + filter.toString() + " instead of a single one.  " +
          "This value of the " + referenceAttribute +
          " attribute will not be included in the entry.");
      return null;
    }
    else
    {
      String dnValue = entries.get(0).getDN();
      if (serverContext.debugEnabled())
      {
        serverContext.debugVerbose("When creating entry " +
            entry.getDN() + " mapped " + referenceValue + " to " +
            dnValue + " for the " + referenceAttribute + " attribute.");
      }
      return dnValue;
    }
  }
}