/* * 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; } }