/* * 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.groovy; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.TreeMap; 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.config.SyncPipePluginConfig; import com.unboundid.directory.sdk.sync.scripting.ScriptedSyncPipePlugin; import com.unboundid.directory.sdk.sync.types.PostStepResult; import com.unboundid.directory.sdk.sync.types.SyncOperation; import com.unboundid.directory.sdk.sync.types.SyncServerContext; import com.unboundid.ldap.sdk.Entry; import com.unboundid.ldap.sdk.LDAPException; import com.unboundid.ldap.sdk.ResultCode; import com.unboundid.util.args.ArgumentException; import com.unboundid.util.args.ArgumentParser; import com.unboundid.util.args.BooleanValueArgument; import com.unboundid.util.args.StringArgument; /** * This class provides a simple example of a scripted sync pipe plugin which * will convert source attribute values to destination attribute values * using a fixed mapping. Attribute values that do not appear in the mapping * will be copied across unmodified. This can be used to convert values that * have a different format on the source and destination that cannot be * handled by the built-in attribute mappings, for instance, if the source * stores a boolean as 'Y' or 'N', but the destination stores it as 'true' or * 'false'. It takes the following arguments: * <UL> * <LI>source-attribute -- The single source attribute to translate values * from.</LI> * <LI>destination-attribute -- The single destination attribute to * translate values into.</LI> * <LI>source-value -- An ordered list of source attribute values.</LI> * <LI>destination-value -- An ordered list of equivalent destination * attribute values.</LI> * <LI>case-sensitive -- Whether a case-sensitive comparison is used when * determining if a source attribute value matches.</LI> * </UL> * * Source attribute values that appear in the source-value list will be * replaced with the destination attribute value, that appears at the same * place in the list. * <p> * For example, this can be used to convert values that * have a different format on the source and destination that cannot be * handled by the built-in attribute mappings, for instance, if the source * attribute 'enabled' stores a boolean as 'Y' or 'N', but the destination * attribute 'is-enabled' stores it as 'true' or 'false', then you could * create a plugin to handle this as follows: * <p> * <code> * dsconfig create-sync-pipe-plugin --plugin-name "Scripted Sync Pipe Plugin" * --type groovy-scripted * --set script-class:com.unboundid.directory.sdk.examples. * groovy.GroovyScriptedSyncPipePlugin * --set script-argument:source-attribute=enabled * --set script-argument:destination-attribute=is-enabled * --set script-argument:source-value=y * --set script-argument:destination-value=true * --set script-argument:source-value=n * --set script-argument:destination-value=false * --set script-argument:case-sensitive=false * </code> * </p> * And then add it into the Sync Pipe as follows: * <p> * <code> * dsconfig set-sync-pipe-prop --pipe-name "Example Sync Pipe" * --add "plugin:Scripted Sync Pipe Plugin" * </code> * </p> */ public final class ExampleScriptedSyncPipePlugin extends ScriptedSyncPipePlugin { private static final String ARG_NAME_SRC_ATTRIBUTE = "source-attribute"; private static final String ARG_NAME_DEST_ATTRIBUTE = "destination-attribute"; private static final String ARG_NAME_SRC_VALUE = "source-value"; private static final String ARG_NAME_DEST_VALUE = "destination-value"; private static final String ARG_NAME_CASE_SENSITIVE = "case-sensitive"; // 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(); // Maps a source attribute value to a destination attribute value. private Map<String,String> sourceValueToDestinationValue; // The name of the source attribute. private String sourceAttribute; // The name of the destination attribute. private String destinationAttribute; /** * Creates a new instance of this scripted sync pipe plugin. All sync pipe * plugins implementations must include a default constructor, but any * initialization should generally be done in the * {@code initializeSyncPipePlugin} method. */ public ExampleScriptedSyncPipePlugin() { // No implementation required. } /** * 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_SRC_ATTRIBUTE; boolean required = true; int maxOccurrences = 1; String placeholder = "{src-attr}"; String description = "The name of the source attribute to map " + "values for."; 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); // Add an argument that allows you to specify the destination attribute. shortIdentifier = null; longIdentifier = ARG_NAME_DEST_ATTRIBUTE; required = true; maxOccurrences = 1; placeholder = "{dest-attr}"; description = "The name that the destination attribute to map values " + "into."; 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); // Add an argument that allows you to specify a source attribute value. shortIdentifier = null; longIdentifier = ARG_NAME_SRC_VALUE; required = false; maxOccurrences = Integer.MAX_VALUE; placeholder = "{src-value}"; description = "A value of the source attribute."; parser.addArgument(new StringArgument(shortIdentifier, longIdentifier, required, maxOccurrences, placeholder, description)); // Add an argument that allows you to specify a destination attribute value. shortIdentifier = null; longIdentifier = ARG_NAME_DEST_VALUE; required = false; maxOccurrences = Integer.MAX_VALUE; placeholder = "{dest-value}"; description = "A value of the destination attribute."; parser.addArgument(new StringArgument(shortIdentifier, longIdentifier, required, maxOccurrences, placeholder, description)); // Add an argument that allows you to specify a destination attribute value. shortIdentifier = null; longIdentifier = ARG_NAME_CASE_SENSITIVE; required = false; maxOccurrences = Integer.MAX_VALUE; placeholder = "{true|false}"; description = "Specifies whether matching of the source attribute " + "should be done case-sensitively."; boolean isCaseSensitiveDefault = false; // Note: Using BooleanValueArgument is preferable to BooleanArgument here // because of the way that the value is set in the server's configuration. parser.addArgument(new BooleanValueArgument(shortIdentifier, longIdentifier, required, placeholder, description, isCaseSensitiveDefault)); } /** * Initializes this sync pipe plugin. * * @param serverContext A handle to the server context for the server in * which this extension is running. * @param config The general configuration for this sync pipe plugin. * @param parser The argument parser which has been initialized from * the configuration for this sync pipe plugin. * * @throws LDAPException If a problem occurs while initializing this sync * pipe plugin. */ @Override() public void initializeSyncPipePlugin( final SyncServerContext serverContext, final SyncPipePluginConfig config, final ArgumentParser parser) throws LDAPException { serverContext.debugInfo("Beginning sync pipe plugin initialization"); this.serverContext = serverContext; setConfig(config, parser, false); } /** * 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 sync pipe plugin. * @param parser The argument parser which has been initialized from * the configuration for this sync pipe plugin. * @param validateOnly If true, then the configuration is only validated * and not updated. * * @throws LDAPException If a problem occurs while initializing this sync * pipe plugin. */ private void setConfig( final SyncPipePluginConfig config, final ArgumentParser parser, final boolean validateOnly) throws LDAPException { boolean isCaseSensitive = ((BooleanValueArgument)parser.getNamedArgument( ARG_NAME_CASE_SENSITIVE)).getValue(); Map<String,String> valueMap; if (isCaseSensitive) { valueMap = new TreeMap<String,String>(); } else { valueMap = new TreeMap<String,String>(String.CASE_INSENSITIVE_ORDER); } String srcAttribute = ((StringArgument)parser.getNamedArgument( ARG_NAME_SRC_ATTRIBUTE)).getValue(); String destAttribute = ((StringArgument)parser.getNamedArgument( ARG_NAME_DEST_ATTRIBUTE)).getValue(); List<String> sourceValues = ((StringArgument)parser.getNamedArgument( ARG_NAME_SRC_VALUE)).getValues(); List<String> destValues = ((StringArgument)parser.getNamedArgument( ARG_NAME_DEST_VALUE)).getValues(); if (sourceValues.size() != destValues.size()) { throw new LDAPException(ResultCode.PARAM_ERROR, "The same number of " + "values must be provided for the " + ARG_NAME_SRC_ATTRIBUTE + " and the " + ARG_NAME_DEST_ATTRIBUTE + "."); } Iterator<String> sourceValueIter = sourceValues.iterator(); Iterator<String> destValueIter = destValues.iterator(); while (sourceValueIter.hasNext()) { valueMap.put(sourceValueIter.next(), destValueIter.next()); } if (sourceValues.size() != valueMap.size()) { throw new LDAPException(ResultCode.PARAM_ERROR, "Duplicate values were " + "provided for " + ARG_NAME_SRC_ATTRIBUTE + "."); } if (!validateOnly) { configWriteLock.lock(); try { this.sourceAttribute = srcAttribute; this.destinationAttribute = destAttribute; this.sourceValueToDestinationValue = valueMap; } finally { configWriteLock.unlock(); } } } /** * Indicates whether the configuration contained in the provided argument * parser represents a valid configuration for this extension. * * @param config The general configuration for this sync pipe * 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 SyncPipePluginConfig config, final ArgumentParser parser, final List<String> unacceptableReasons) { try { setConfig(config, parser, true); } catch (LDAPException e) { unacceptableReasons.add(e.getExceptionMessage()); return false; } return true; } /** * Attempts to apply the configuration contained in the provided argument * parser. * * @param config The general configuration for this sync pipe * plugin. * @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 SyncPipePluginConfig config, final ArgumentParser parser, final List<String> adminActionsRequired, final List<String> messages) { try { setConfig(config, parser, false); } catch (LDAPException e) { messages.add(e.getExceptionMessage()); return e.getResultCode(); } return ResultCode.SUCCESS; } /** * Performs any cleanup which may be necessary when this sync pipe plugin * is to be taken out of service. */ @Override() public void finalizeSyncPipePlugin() { // No finalization is required. } /** * This method is called immediately after the attributes and DN in * the source entry are mapped into the equivalent destination entry. * Once this mapping is complete, this equivalent destination entry is then * compared to the actual destination entry to determine which of the modified * attributes need to be updated. * <p> * This method is typically used to manipulate the equivalent destination * entry before these necessary changes are calculated. It can also be used * to filter out certain changes (by returning * {@link PostStepResult#ABORT_OPERATION}), but this is typically done in * the {@link #preMapping method}. * <p> * The set of source attributes that should be synchronized at the destination * can be manipulated by calling * {@link SyncOperation#addModifiedSourceAttribute} and * {@link SyncOperation#removeModifiedSourceAttribute} on * {@code operation}. * <p> * Additional steps must be taken if this plugin adds destination attributes * in {@code equivalentDestinationEntry} that need to be modified at the * destination. For operations with an operation type of * {@link com.unboundid.directory.sdk.sync.types.SyncOperationType#CREATE}, * any updates made to * {@code equivalentDestinationEntry} will be included in the * entry created at the destination. However, for operations with an * operation type of * {@link com.unboundid.directory.sdk.sync.types.SyncOperationType#MODIFY}, * destination attributes * added by this plugin that need to be modified must be updated * explicitly by calling * {@link SyncOperation#addModifiedDestinationAttribute}. * <p> * With the exception of aborting changes or skipping the mapping step * completely, most plugins will override this method instead of * {@link #preMapping} because this method has access to the fully mapped * destination entry. * * @param sourceEntry The entry that was fetched from the * source. * @param equivalentDestinationEntry The destination entry that is * equivalent to the source. This entry * will include all attributes mapped * from the source entry. * @param operation The operation that is being * synchronized. * * @return The result of the plugin processing. */ @Override public PostStepResult postMapping(final Entry sourceEntry, final Entry equivalentDestinationEntry, final SyncOperation operation) { configReadLock.lock(); String[] srcValues = sourceEntry.getAttributeValues(sourceAttribute); try { if (srcValues != null) { ArrayList<String> destValues = new ArrayList<String>(srcValues.length); for (String srcValue: srcValues) { String destValue = sourceValueToDestinationValue.get(srcValue); if (destValue == null) { // Defaults to the source value if there isn't a mapping. destValue = srcValue; } destValues.add(destValue); // Set the destination attribute in the entry. equivalentDestinationEntry.setAttribute(destinationAttribute, destValues.toArray(new String[0])); } } else { equivalentDestinationEntry.removeAttribute(destinationAttribute); } // If we don't set this here, then the Sync Pipe won't know that it needs // to update the destination attribute. operation.addModifiedDestinationAttribute(destinationAttribute); // This keeps the source attribute from being copied over as is into // a destination attribute with the same name. operation.removeModifiedSourceAttribute(sourceAttribute); operation.logInfo("Mapped values for " + sourceAttribute + " to " + destinationAttribute + " in plugin."); return PostStepResult.CONTINUE; } finally { configReadLock.unlock(); } } }