/*
* 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
*
*
* Copyright 2010-2016 UnboundID Corp.
*/
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();
}
}
}
|