UnboundID Server SDK

Ping Identity
UnboundID Server SDK Documentation

ExampleSCIM2AttributeMapping.java

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