/*
* 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 2022-2024 Ping Identity Corporation
*/
package com.unboundid.directory.sdk.examples;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import com.unboundid.directory.sdk.sync.api.SCIM2AttributeMapping;
import com.unboundid.directory.sdk.sync.config.SCIM2AttributeMappingConfig;
import com.unboundid.directory.sdk.sync.types.SyncServerContext;
import com.unboundid.ldap.sdk.Attribute;
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.StringArgument;
import com.unboundid.util.json.JSONArray;
import com.unboundid.util.json.JSONField;
import com.unboundid.util.json.JSONObject;
import com.unboundid.util.json.JSONString;
import com.unboundid.util.json.JSONValue;
/**
* This class provides a simple implementation of a SCIM 2.0 attribute mapping
* for use by the Synchronization Server's SCIMv2 sync destination. It takes
* an LDAP attribute whose value is a comma-delimited string and converts it to
* a SCIM attribute whose value is an array of those strings. This extension
* takes the following configuration argument:
* <UL>
* <LI>
* ldap-attribute -- The name of the LDAP attribute whose value is the
* comma-delimited string to be converted into SCIM array values. This is
* a required argument.
* </LI>
* </UL>
*/
public final class ExampleSCIM2AttributeMapping
extends SCIM2AttributeMapping
{
/**
* The name of the argument used to specify the source LDAP attribute.
*/
public static final String ARGUMENT_NAME_LDAP_ATTRIBUTE = "ldap-attribute";
// The server context for this extension.
private SyncServerContext serverContext = null;
// The name of the source LDAP attribute.
private String ldapAttributeName = null;
// The name of the destination SCIM attribute.
private String scimAttributeName = null;
/**
* Creates a new instance of this SCIM2 attribute mapping. All attribute
* mapping implementations must include a default constructor, but any
* initialization should generally be performed in the
* {@code initializeAttributeMapping} method.
*/
public ExampleSCIM2AttributeMapping()
{
// No implementation required.
}
/**
* Retrieves a human-readable name for this extension.
*
* @return A human-readable name for this extension.
*/
@Override()
public String getExtensionName()
{
return "Example SCIM2 Attribute Mapping";
}
/**
* 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 SCIM2 attribute mapping serves as an example that may be used " +
"to demonstrate the process for creating a third-party SCIM2 " +
"attribute mapping. It converts an LDAP attribute whose value " +
"is a comma-delimited string and converts it to a SCIM attribute " +
"whose value is an array of those strings."
};
}
/**
* 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 Map<List<String>,String> examples = new LinkedHashMap<>();
final List<String> exampleArgumentList =
Collections.singletonList("ldap-attribute=sourceAttributeName");
final String description = "Configures the example SCIM2 attribute " +
"mapping so that it will convert values of the " +
"'sourceAttributeName' source attribute from a comma-delimited list " +
"to a SCIM array.";
examples.put(exampleArgumentList, description);
return examples;
}
/**
* Updates the provided argument parser to define any configuration arguments
* which may be used by this extension. 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 extension.
*
* @throws ArgumentException If a problem is encountered while updating the
* provided argument parser.
*/
@Override()
public void defineConfigArguments(final ArgumentParser parser)
throws ArgumentException
{
final Character shortIdentifier = null;
final String longIdentifier = ARGUMENT_NAME_LDAP_ATTRIBUTE;
final boolean isRequired = true;
final int maxOccurrences = 1;
final String valuePlaceholder = "{attributeName}";
final String description = "The name of the source LDAP attribute whose " +
"value should be converted from a comma-delimited list to a SCIM " +
"array.";
final StringArgument ldapAttributeArg = new StringArgument(shortIdentifier,
longIdentifier, isRequired, maxOccurrences, valuePlaceholder,
description);
parser.addArgument(ldapAttributeArg);
}
/**
* Performs any necessary initialization for this SCIM2 attribute mapping.
*
* @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 SCIM2 attribute mapping.
*
* @throws LDAPException If a problem occurs while initializing this SCIM2
* attribute mapping.
*/
public void initializeAttributeMapping(
final SyncServerContext serverContext,
final SCIM2AttributeMappingConfig config,
final ArgumentParser parser)
throws LDAPException
{
// The SCIM attribute name is provided in the config object.
scimAttributeName = config.getSCIM2AttributeName();
// Get the LDAP attribute name from the argument parser.
StringArgument ldapAttributeArgument =
parser.getStringArgument(ARGUMENT_NAME_LDAP_ATTRIBUTE);
if ((ldapAttributeArgument == null) ||
(! ldapAttributeArgument.isPresent()))
{
// This should never happen, as the argument was declared to be required.
throw new LDAPException(ResultCode.PARAM_ERROR,
"The required '" + ARGUMENT_NAME_LDAP_ATTRIBUTE +
"' argument was not provided.");
}
ldapAttributeName = ldapAttributeArgument.getValue();
}
/**
* Indicates whether the configuration represented by the provided argument
* parser is acceptable for use by this extension. The parser will have been
* used to parse any configuration available for this extension, and any
* automatic validation will have been performed. This method may be used to
* perform any more complex validation which cannot be performed automatically
* by the argument parser.
*
* @param config The general configuration for this extension.
* @param parser The argument parser that has been used to
* parse the proposed configuration for this
* extension.
* @param unacceptableReasons A list to which messages may be added to
* provide additional information about why the
* provided configuration is not acceptable.
*
* @return {@code true} if the configuration in the provided argument parser
* appears to be acceptable, or {@code false} if not.
*/
@Override()
public boolean isConfigurationAcceptable(
final SCIM2AttributeMappingConfig config,
final ArgumentParser parser,
final List<String> unacceptableReasons)
{
// No extended validation will be performed by default. The argument
// parser should automatically do all the necessary validation.
return true;
}
/**
* Attempts to apply the configuration from the provided argument parser to
* this extension.
*
* @param config The general configuration for this extension.
* @param parser The argument parser that has been used to
* parse the new configuration for this
* extension.
* @param adminActionsRequired A list to which messages may be added to
* provide additional information about any
* additional administrative actions that may
* be required to apply some of the
* configuration changes.
* @param messages A list to which messages may be added to
* provide additional information about the
* processing performed by this method.
*
* @return A result code providing information about the result of applying
* the configuration change. A result of {@code SUCCESS} should be
* used to indicate that all processing completed successfully. Any
* other result will indicate that a problem occurred during
* processing.
*/
@Override()
public ResultCode applyConfiguration(
final SCIM2AttributeMappingConfig config,
final ArgumentParser parser,
final List<String> adminActionsRequired,
final List<String> messages)
{
// The SCIM attribute name is provided in the config object.
scimAttributeName = config.getSCIM2AttributeName();
// Get the LDAP attribute name from the argument parser.
StringArgument ldapAttributeArgument =
parser.getStringArgument(ARGUMENT_NAME_LDAP_ATTRIBUTE);
if ((ldapAttributeArgument != null) && ldapAttributeArgument.isPresent())
{
ldapAttributeName = ldapAttributeArgument.getValue();
}
return ResultCode.SUCCESS;
}
/**
* Performs any necessary cleanup work when this attribute mapping it taken
* out of service.
*/
@Override()
public void finalizeAttributeMapping()
{
// No special cleanup is required for this attribute mapping implementation.
}
/**
* Indicates whether this SCIM2 attribute mapping targets a single-valued
* SCIM 2.0 attribute.
*
* @return {@code true} if this SCIM2 attribute mapping targets a
* single-valued SCIM 2.0 attribute, or {@code false} if it targets
* an attribute that may have multiple values or is defined as
* multivalued in the schema.
*/
@Override()
public boolean isSingleValued()
{
// This attribute will always return values as a SCIM array, so it's not
// single-valued.
return false;
}
/**
* Constructs the SCIM2 representation of an attribute from the information
* in the provided mapped LDAP representation of an entry as created by a
* sync class.
*
* @param mappedLDAPEntry The mapped LDAP representation of an entry as
* created by a sync class. It must not be
* {@code null}.
*
* @return A JSON field that represents the SCIM 2.0 representation of the
* attribute. It may be {@code null} if the provided entry does not
* contain enough information to create the SCIM2 attribute (e.g.,
* if the associated LDAP attribute does not exist in the entry).
*
* @throws LDAPException If a problem occurs while attempting to create the
* SCIM 2.0 representation of the attribute from the
* provided LDAP entry.
*/
@Override()
public JSONField createSCIM2Representation(final Entry mappedLDAPEntry)
throws LDAPException
{
// Get the source attribute from the mapped LDAP entry. If there isn't
// one, then return null to indicate that no corresponding SCIM attribute is
// needed.
final Attribute sourceAttribute =
mappedLDAPEntry.getAttribute(ldapAttributeName);
if (sourceAttribute == null)
{
return null;
}
// Break the source attribute value into the SCIM array of strings.
final List<JSONString> arrayValues = new ArrayList<>();
for (final String sourceAttributeValue : sourceAttribute.getValues())
{
final String[] commaDelimitedListItems = sourceAttributeValue.split(",");
for (final String item : commaDelimitedListItems)
{
arrayValues.add(new JSONString(item));
}
}
return new JSONField(scimAttributeName, new JSONArray(arrayValues));
}
/**
* Updates the provided LDAP entry with information from the given JSON object
* containing the SCIM 2.0 representation of the entry.
*
* @param scim2EntryID The identifier for the SCIM2 entry.
* @param scim2Entry A JSON object containing the SCIM2
* representation of the entry.
* @param updatableLDAPEntry An LDAP entry that may be updated with content
* obtained from the SCIM2 representation of the
* entry.
*
* @throws LDAPException If a problem occurs while attempting to create the
* LDAP representation of the attribute from the
* provided JSON object.
*/
@Override()
public void createLDAPRepresentation(final String scim2EntryID,
final JSONObject scim2Entry,
final Entry updatableLDAPEntry)
throws LDAPException
{
// See if the SCIM entry has the destination attribute. If not, then we
// don't need to do anything.
final List<JSONValue> scimArrayValues =
scim2Entry.getFieldAsArray(scimAttributeName);
if ((scimArrayValues == null) || scimArrayValues.isEmpty())
{
return;
}
// Create a string that is simply a concatenation of the array values.
final StringBuilder ldapValueBuffer = new StringBuilder();
final Iterator<JSONValue> valueIterator = scimArrayValues.iterator();
while (valueIterator.hasNext())
{
final JSONValue scimArrayValue = valueIterator.next();
if (scimArrayValue instanceof JSONString)
{
ldapValueBuffer.append(((JSONString) scimArrayValue).stringValue());
}
else
{
// The SCIM array had an element that isn't a string. This is not
// expected and will be treated as an error.
throw new LDAPException(ResultCode.PARAM_ERROR,
"SCIM attribute '" + scimAttributeName + "' in entry '" +
scim2EntryID + "' has a non-string array value.");
}
if (valueIterator.hasNext())
{
ldapValueBuffer.append(',');
}
}
updatableLDAPEntry.setAttribute(ldapAttributeName,
ldapValueBuffer.toString());
}
}