UnboundID Server SDK

Ping Identity
UnboundID Server SDK Documentation

ExampleSyncDestination.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.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import com.unboundid.directory.sdk.sync.api.SyncDestination;
import com.unboundid.directory.sdk.sync.config.SyncDestinationConfig;
import com.unboundid.directory.sdk.sync.types.EndpointException;
import com.unboundid.directory.sdk.sync.types.SyncOperation;
import com.unboundid.directory.sdk.sync.types.SyncServerContext;
import com.unboundid.util.args.ArgumentException;
import com.unboundid.util.args.ArgumentParser;
import com.unboundid.util.args.DNArgument;
import com.unboundid.util.args.IntegerArgument;
import com.unboundid.util.args.StringArgument;
import com.unboundid.ldap.sdk.Attribute;
import com.unboundid.ldap.sdk.ChangeLogEntry;
import com.unboundid.ldap.sdk.DN;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPConnection;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPThreadLocalConnectionPool;
import com.unboundid.ldap.sdk.Modification;
import com.unboundid.ldap.sdk.RDN;
import com.unboundid.ldap.sdk.ResultCode;


/**
 * This example provides an implementation of SyncDestination which pushes
 * changes to a target LDAP directory server. This can optionally be used as the
 * destination of a Sync Pipe in notification mode. Its configuration arguments
 * require the host, port, and credentials for a target LDAP directory, to which
 * changes are pushed straight through. The fetchEntry() method is implemented
 * for reference, although it is not used when a Sync Pipe is in
 * <i>notification</i> mode.
 */
public class ExampleSyncDestination extends SyncDestination
{

  // The general configuration for this Sync Destination
  private volatile SyncDestinationConfig config;

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

  // A pool of connections to the destination LDAP server
  private LDAPThreadLocalConnectionPool connectionPool;


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


  /**
   * 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 implementation serves as an example that may be used to " +
        "demonstrate the process for creating a third-party Sync Destination " +
        "extension. It will forward notifications from the " +
        "Data Sync Server on to another LDAP directory server."
      };
  }


  /**
   * {@inheritDoc}
   */
  @Override
  public void defineConfigArguments(final ArgumentParser parser)
                  throws ArgumentException
  {
    StringArgument hostArg = new StringArgument(
                                 null, "host", true, 1, "{hostname}",
                                 "The hostname of the target directory " +
                                 "server to which notifications should be " +
                                 "sent.");

    IntegerArgument portArg = new IntegerArgument(
                                 null, "port", true, 1, "{port}",
                                 "The port number of the target directory " +
                                 "server to which notifications should be " +
                                 "sent.");

    DNArgument bindDNArg = new DNArgument(
                                 null, "bind-dn", true, 1, "{bind-DN}",
                                 "The bind DN to use for requests to the " +
                                 "target directory server.");

    StringArgument bindPasswordArg = new StringArgument(
                                 null, "bind-password", true, 1,
                                 "{bind-password}", "The bind password to " +
                                 "use for requests to the target directory " +
                                 "server.");

    parser.addArgument(hostArg);
    parser.addArgument(portArg);
    parser.addArgument(bindDNArg);
    parser.addArgument(bindPasswordArg);
  }


  /**
   * 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("host=sample.host.com", "port=1389",
                    "bind-dn=cn=Sync User", "bind-password=p@ssW0rd"),
          "Push changes to ldap://sample.host.com:1389, using the " +
          "Sync User account.");

    return exampleMap;
  }


  /**
   * Initializes this sync destination. This hook is called when a Sync Pipe
   * first starts up, or when the <i>resync</i> process first starts up. Any
   * initialization should be performed here. This method should generally store
   * the {@link SyncServerContext} in a class
   * member so that it can be used elsewhere in the implementation.
   *
   * @param  serverContext  A handle to the server context for the server in
   *                        which this extension is running. Extensions should
   *                        typically store this in a class member.
   * @param  config         The general configuration for this object.
   * @param  parser         The argument parser which has been initialized from
   *                        the configuration for this sync destination.
   * @throws  EndpointException
   *                        if a problem occurs while initializing this
   *                        sync destination.
   */
  @Override()
  public void initializeSyncDestination(
                                    final SyncServerContext serverContext,
                                    final SyncDestinationConfig config,
                                    final ArgumentParser parser)
                                           throws EndpointException
  {
    this.serverContext = serverContext;
    this.config        = config;

    StringArgument hostArg = (StringArgument)
                              parser.getNamedArgument("host");

    IntegerArgument portArg = (IntegerArgument)
                              parser.getNamedArgument("port");

    DNArgument bindDNArg = (DNArgument)
                              parser.getNamedArgument("bind-dn");

    StringArgument passwordArg = (StringArgument)
                              parser.getNamedArgument("bind-password");

    String host = hostArg.getValue();
    int port = portArg.getValue();
    DN bindDN = bindDNArg.getValue();
    String bindPassword = passwordArg.getValue();

    try
    {
      LDAPConnection conn = new LDAPConnection(host,
                                               port,
                                               bindDN.toString(),
                                               bindPassword);
      connectionPool = new LDAPThreadLocalConnectionPool(conn);
    }
    catch(LDAPException e)
    {
      throw new EndpointException(e);
    }
  }


  /**
   * This hook is called when a Sync Pipe shuts down, or when the <i>resync</i>
   * process shuts down. Any clean-up of this sync destination should be
   * performed here.
   */
  @Override
  public void finalizeSyncDestination()
  {
    if(connectionPool != null)
    {
      connectionPool.close();
    }
  }



  /**
   * Return the URL or path identifying the destination endpoint
   * to which this extension is transmitting data. This is used for logging
   * purposes only, so it could just be a server name or hostname and port, etc.
   *
   * @return the path to the destination endpoint
   */
  @Override
  public String getCurrentEndpointURL()
  {
    if(connectionPool == null)
    {
      return "not connected";
    }

    LDAPConnection conn = null;
    try
    {
      conn = connectionPool.getConnection();
      return conn.getConnectedAddress() + ":" + conn.getConnectedPort();
    }
    catch (LDAPException e)
    {
      return "not connected";
    }
    finally
    {
      connectionPool.releaseConnection(conn);
    }
  }



  /**
   * Return a full destination entry (in LDAP form) from the destination
   * endpoint, corresponding to the source {@link Entry} that is passed in.
   * This method should perform any queries necessary to gather the latest
   * values for all the attributes to be synchronized and return them in an
   * Entry.
   * <p>
   * This method only needs to be implemented if the 'synchronization-mode' on
   * the Sync Pipe is set to 'standard'. If it is set to 'notification', this
   * method will never be called, and the pipe will pass changes straight
   * through to one of {@link #createEntry}, {@link #modifyEntry}, or
   * {@link #deleteEntry}.
   * <p>
   * Note that the if the source entry was renamed (see
   * {@link SyncOperation#isModifyDN}), the
   * <code>destEntryMappedFromSrc</code> will have the new DN; the old DN can
   * be obtained by calling
   * {@link SyncOperation#getDestinationEntryBeforeChange()} and getting the DN
   * from there. This method should return the entry in its existing form
   * (i.e. with the old DN, before it is changed).
   * <p>
   * This method <b>must be thread safe</b>, as it will be called repeatedly and
   * concurrently by each of the Sync Pipe worker threads as they process
   * entries.
   * @param destEntryMappedFromSrc
   *          the LDAP entry which corresponds to the destination "entry" to
   *          fetch
   * @param  operation
   *          the sync operation for this change
   * @return a list containing the full LDAP entries that matched this search
   *          (there may be more than one), or an empty list if no such entry
   *          exists
   * @throws EndpointException
   *           if there is an error fetching the entry
   */
  @Override
  public List<Entry> fetchEntry(final Entry destEntryMappedFromSrc,
                                       final SyncOperation operation)
                                          throws EndpointException
  {
    List<Entry> entries = new ArrayList<Entry>();
    try
    {
      Entry e = connectionPool.getEntry(destEntryMappedFromSrc.getDN());
      if(e != null)
      {
        entries.add(e);
      }
      return entries;
    }
    catch(LDAPException e)
    {
      if(e.getResultCode().equals(ResultCode.NO_SUCH_OBJECT))
      {
        return Collections.emptyList();
      }
      throw new EndpointException(e);
    }
  }



  /**
   * Creates a full destination "entry", corresponding to the LDAP
   * {@link Entry} that is passed in. This method is responsible for
   * transforming the contents of the entry into the desired format and
   * transmitting it to the target destination. It should perform any inserts or
   * updates necessary to make sure the entry is fully created on the
   * destination endpoint.
   * <p>
   * This method <b>must be thread safe</b>, as it will be called repeatedly and
   * concurrently by the Sync Pipe worker threads as they process CREATE
   * operations.
   * @param entryToCreate
   *          the LDAP entry which corresponds to the destination
   *          "entry" to create
   * @param  operation
   *          the sync operation for this change
   * @throws EndpointException
   *           if there is an error creating the entry
   */
  @Override
  public void createEntry(final Entry entryToCreate,
                                       final SyncOperation operation)
                                           throws EndpointException
  {
    if (shouldIgnore(entryToCreate, operation))
    {
      return;
    }
    try
    {
      connectionPool.add(entryToCreate);
      operation.logInfo("Created entry: " + entryToCreate.getDN());
    }
    catch(LDAPException e)
    {
      throw new EndpointException(e);
    }
  }



  /**
   * Modify an "entry" on the destination, corresponding to the LDAP
   * {@link Entry} that is passed in. This method is responsible for
   * transforming the contents of the entry into the desired format and
   * transmitting it to the target destination. It may perform multiple updates
   * (including inserting or deleting other attributes) in order to fully
   * synchronize the entire entry on the destination endpoint.
   * <p>
   * Note that the if the source entry was renamed (see
   * {@link SyncOperation#isModifyDN}), the
   * <code>fetchedDestEntry</code> will have the old DN; the new DN can
   * be obtained by calling
   * {@link SyncOperation#getDestinationEntryAfterChange()} and getting the DN
   * from there.
   * <p>
   * This method <b>must be thread safe</b>, as it will be called repeatedly and
   * concurrently by the Sync Pipe worker threads as they process MODIFY
   * operations.
   * @param entryToModify
   *          the LDAP entry which corresponds to the destination
   *          "entry" to modify. If the synchronization mode is 'standard',
   *          this will be the entry that was returned by {@link #fetchEntry};
   *          otherwise if the synchronization mode is 'notification', this
   *          will be the destination entry mapped from the source entry, before
   *          changes are applied.
   * @param modsToApply
   *          a list of Modification objects which should be applied; these will
   *          have any configured attribute mappings already applied
   * @param  operation
   *          the sync operation for this change
   * @throws EndpointException
   *           if there is an error modifying the entry
   */
  @Override
  public void modifyEntry(final Entry entryToModify,
                          final List<Modification> modsToApply,
                          final SyncOperation operation)
                                                 throws EndpointException
  {
    if (shouldIgnore(entryToModify, operation))
    {
      return;
    }
    if(operation.isModifyDN())
    {
      Entry destEntryAfterChange = operation.getDestinationEntryAfterChange();
      try
      {
        String newRDN = destEntryAfterChange.getRDN().toString();
        String newSuperior = destEntryAfterChange.getParentDNString();
        boolean deleteOldRdn = operation.getChangeLogEntry()
                .getAttributeValueAsBoolean(ChangeLogEntry.ATTR_DELETE_OLD_RDN);

        connectionPool.modifyDN(entryToModify.getDN(),
                                newRDN, deleteOldRdn, newSuperior);
        operation.logInfo("Modified DN " + entryToModify.getDN() +
                          " to " + destEntryAfterChange.getDN());
      }
      catch(LDAPException e)
      {
        throw new EndpointException(e);
      }
    }
    else if(!modsToApply.isEmpty())
    {
      try
      {
        connectionPool.modify(entryToModify.getDN(), modsToApply);
        operation.logInfo("Modified entry: " + entryToModify.getDN());
      }
      catch(LDAPException e)
      {
        throw new EndpointException(e);
      }
    }
  }



  /**
   * Delete a full "entry" from the destination, corresponding to the LDAP
   * {@link Entry} that is passed in. This method may perform multiple deletes
   * or updates if necessary to fully delete the entry from the destination
   * endpoint.
   * <p>
   * This method <b>must be thread safe</b>, as it will be called repeatedly and
   * concurrently by the Sync Pipe worker threads as they process DELETE
   * operations.
   * @param entryToDelete
   *          the LDAP entry which corresponds to the destination
   *          "entry" to delete. If the synchronization mode is 'standard',
   *          this will be the entry that was returned by {@link #fetchEntry};
   *          otherwise if the synchronization mode is 'notification', this
   *          will be the mapped destination entry.
   * @param  operation
   *          the sync operation for this change
   * @throws EndpointException
   *           if there is an error deleting the entry
   */
  @Override
  public void deleteEntry(final Entry entryToDelete,
                                       final SyncOperation operation)
                                            throws EndpointException
  {
    if (shouldIgnore(entryToDelete, operation))
    {
      return;
    }
    try
    {
      connectionPool.delete(entryToDelete.getDN());
      operation.logInfo("Deleted entry: " + entryToDelete.getDN());
    }
    catch(LDAPException e)
    {
      throw new EndpointException(e);
    }
  }



  /**
   * Entries that have 'cn=ignore user' as the RDN will be ignored. This method
   * demonstrates using SyncOperation#setIgnored() to direct the Sync Server to
   * ignore certain types of changes and isn't tied to a true use case.
   *
   * @param entry
   *          The entry to to be checked.
   * @param operation
   *          The sync operation.
   * @return {@code true} if the entry should be ignored.
   * @throws EndpointException
   *           If the RDN could not be retrieved.
   */
  protected boolean shouldIgnore(final Entry entry,
      final SyncOperation operation) throws EndpointException {
    try
    {
      final RDN rdn = entry.getRDN();
      for (Attribute attribute : rdn.getAttributes())
      {
        if (attribute.getName().equalsIgnoreCase("cn") &&
            attribute.getValue().equals("ignore user"))
        {
          // Setting this prevents this operation from being included in the
          // monitor statistics.
          operation.setIgnored();
          return true;
        }
      }
    }
    catch(LDAPException e)
    {
      throw new EndpointException(e);
    }
    return false;
  }
}