/* * 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.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.concurrent.atomic.AtomicReference; import java.util.regex.Pattern; import com.unboundid.directory.sdk.sync.api.LDAPSyncSourcePlugin; 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.LDAPException; import com.unboundid.ldap.sdk.LDAPInterface; import com.unboundid.ldap.sdk.ResultCode; import com.unboundid.ldap.sdk.SearchRequest; import com.unboundid.ldap.sdk.SearchResultEntry; 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 an 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 ExampleLDAPSyncSourcePlugin extends LDAPSyncSourcePlugin { 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; /** * Retrieves a human-readable name for this extension. * * @return A human-readable name for this extension. */ @Override() public String getExtensionName() { return "Example LDAP Sync Source Plugin"; } /** * 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 is simple example of an LDAP sync source plugin which " + "manipulates an attribute that stores a reference to another entry. " + "In the LDAP source server, the reference attribute is stored as " + "the DN of the referenced entry, but in the sync destination (possibly " + "a relational database, the reference is stored as a key of the " + "referenced entry, e.g. the equivalent of uid." }; } /** * 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(); } } /** * 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_REFERENCE_ATTRIBUTE + "=manager", ARG_NAME_REFERENCED_ENTRY_KEY_ATTRIBUTE + "=employeeNumber", ARG_NAME_SEARCH_BASE_DN + "=dc=example,dc=com"), "Converts between a DN and employeeNumber representation of the " + "manager attribute at the source"); return exampleMap; } /** * {@inheritDoc} */ @Override public PreStepResult preFetch(final LDAPInterface sourceConnection, final SearchRequest searchRequest, final List<SearchResultEntry> searchResults, final SyncOperation operation) throws LDAPException { /* This example performs a search similarly to PingDataSync. It is possible to use preFetch to instead suppress a search based on variable criteria such as DN. */ searchResults.addAll( sourceConnection.search(searchRequest).getSearchEntries()); return PreStepResult.SKIP_CURRENT_STEP; } /** * 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(); } } /** * Appends a string representation of this LDAP sync source plugin 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("ExampleSyncPipePlugin(referenceAttribute='"); buffer.append(referenceAttribute); buffer.append("', referencedEntryKeyAttribute='"); buffer.append(referencedEntryKeyAttribute); buffer.append("', searchBaseDn='"); buffer.append(searchBaseDn); buffer.append("')"); } }