UnboundID Server SDK

Ping Identity
UnboundID Server SDK Documentation

ExampleOneToManySCIMSubResourceTypeHandler.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
 *
 *
 *      Copyright 2021 Ping Identity Corporation
 */
package com.unboundid.directory.sdk.examples;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;
import com.unboundid.directory.sdk.common.types.LogSeverity;
import com.unboundid.directory.sdk.scim2.api.SCIMSubResourceTypeHandler;
import com.unboundid.directory.sdk.scim2.config.SCIMSubResourceTypeHandlerConfig;
import com.unboundid.directory.sdk.scim2.types.SCIMCreateRequest;
import com.unboundid.directory.sdk.scim2.types.SCIMDeleteRequest;
import com.unboundid.directory.sdk.scim2.types.SCIMLDAPAttributeMapper;
import com.unboundid.directory.sdk.scim2.types.SCIMLDAPInterface;
import com.unboundid.directory.sdk.scim2.types.SCIMModifyRequest;
import com.unboundid.directory.sdk.scim2.types.SCIMReplaceRequest;
import com.unboundid.directory.sdk.scim2.types.SCIMRetrieveRequest;
import com.unboundid.directory.sdk.scim2.types.SCIMSearchRequest;
import com.unboundid.directory.sdk.scim2.types.SCIMSearchResultListener;
import com.unboundid.directory.sdk.scim2.types.SCIMServerContext;
import com.unboundid.ldap.sdk.AddRequest;
import com.unboundid.ldap.sdk.Control;
import com.unboundid.ldap.sdk.DeleteRequest;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.Filter;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.Modification;
import com.unboundid.ldap.sdk.ModifyRequest;
import com.unboundid.ldap.sdk.ResultCode;
import com.unboundid.ldap.sdk.SearchRequest;
import com.unboundid.ldap.sdk.SearchResult;
import com.unboundid.ldap.sdk.SearchResultEntry;
import com.unboundid.ldap.sdk.SearchResultListener;
import com.unboundid.ldap.sdk.SearchResultReference;
import com.unboundid.ldap.sdk.SearchScope;
import com.unboundid.ldap.sdk.controls.PostReadRequestControl;
import com.unboundid.ldap.sdk.controls.PostReadResponseControl;
import com.unboundid.ldap.sdk.schema.Schema;
import com.unboundid.ldap.sdk.unboundidds.controls.NameWithEntryUUIDRequestControl;
import com.unboundid.scim2.common.GenericScimResource;
import com.unboundid.scim2.common.ScimResource;
import com.unboundid.scim2.common.exceptions.ResourceNotFoundException;
import com.unboundid.scim2.common.exceptions.ScimException;
import com.unboundid.scim2.common.exceptions.ServerErrorException;
import com.unboundid.scim2.common.messages.PatchRequest;
import com.unboundid.scim2.common.types.SchemaResource;
import com.unboundid.scim2.common.utils.JsonUtils;
import com.unboundid.util.StaticUtils;
import com.unboundid.util.args.ArgumentParser;

import javax.servlet.http.HttpServletRequest;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.StringJoiner;

/**
 * This is an example SCIM Sub-Resource Type Handler that demonstrates how to
 * implement a <i>one-to-many</i> SCIM sub-resource type, which is a SCIM
 * sub-resource type that may have zero to many sub-resource per parent SCIM
 * resource.
 * <p>
 * Sub-resources provided by this example represent a user's "gadgets" and use
 * the core SCIM schema "urn:pingidentity:schemas:2.0:GadgetExample". Each
 * sub-resource is stored in the LDAP directory as a child entry of the LDAP
 * entry representing the parent SCIM resource using the "device" object class.
 * For example, given a SCIM resource backed by the LDAP entry
 * "entryUUID=...,ou=people,dc=example,dc=com" (entryUUID omitted for brevity),
 * a sub-resource would be stored in the LDAP entry
 * "entryUUID=...,entryUUID=...,ou=people,dc=example,dc=com".
 *
 * This example SCIM Sub-Resource Type Handler requires that its SCIM schema
 * be defined in the server configuration using the following commands:
 * <pre>
 *     dsconfig create-scim-schema \
 *       --schema-name urn:pingidentity:schemas:2.0:GadgetExample \
 *       --set display-name:Gadget
 *     dsconfig create-scim-attribute \
 *       --schema-name urn:pingidentity:schemas:2.0:GadgetExample \
 *       --attribute-name name --set required:true
 *     dsconfig create-scim-attribute \
 *       --schema-name urn:pingidentity:schemas:2.0:GadgetExample \
 *       --attribute-name description
 *     dsconfig create-scim-attribute \
 *       --schema-name urn:pingidentity:schemas:2.0:GadgetExample \
 *       --attribute-name serialNumber
 * </pre>
 */
public class ExampleOneToManySCIMSubResourceTypeHandler
    extends SCIMSubResourceTypeHandler
{
  /**
   * The schema URN of this sub-resource type's core schema.
   */
  public static final String SCIM_SCHEMA_ID =
      "urn:pingidentity:schemas:2.0:GadgetExample";

  /**
   * The name of this sub-resource type's core schema.
   */
  public static final String SCIM_SCHEMA_NAME = "Gadget";

  // The LDAP attribute used for SCIM sub-resource IDs.
  private static final String SUBRESOURCE_ID_ATTRIBUTE = "entryUUID";

  // The LDAP object class used for SCIM sub-resource entries.
  private static final String LDAP_OBJECT_CLASS = "device";

  private static final ObjectMapper objectMapper =
      JsonUtils.createObjectMapper();

  private SCIMServerContext serverContext;
  private SchemaResource coreSchema;
  private String parentResourceType;
  private SCIMLDAPInterface ldapInterface;
  private SCIMLDAPAttributeMapper attributeMapper;

  /**
   * {@inheritDoc}
   */
  @Override
  public String getExtensionName()
  {
    return "Example One-To-Many SCIM Sub-Resource Type Handler";
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public String[] getExtensionDescription()
  {
    return new String[] {
        "This is an example SCIM Sub-Resource Type Handler that exposes " +
            "sub-resources representing a user's gadgets.",
        "The attributes of each gadget sub-resource is stored as a child " +
            "LDAP entry of the parent resource's entry using the 'device' " +
            "object class."
    };
  }

  /**
   * Initializes this SCIM Sub-Resource Type Handler, loading the SCIM schema
   * from the server configuration, setting up helper objects, and checking
   * that the LDAP server supports certain required controls.
   *
   * @param serverContext  A handle to the server context for the server in
   *                       which this extension is running.
   * @param config         The configuration for this SCIM Sub-Resource Type
   *                       Handler.
   * @param parser         The argument parser which has been initialized from
   *                       the configuration for this SCIM Sub-Resource Type
   *                       Handler.
   * @throws Exception     If a problem occurs while initializing this SCIM
   *                       Sub-Resource Type Handler.
   */
  @Override
  public void initializeHandler(final SCIMServerContext serverContext,
                                final SCIMSubResourceTypeHandlerConfig config,
                                final ArgumentParser parser) throws Exception
  {
    this.serverContext = serverContext;

    this.coreSchema = getSchemaFromConfiguration();

    parentResourceType = config.getParentSCIMResourceTypeName();

    try
    {
      ldapInterface = serverContext.getSCIMLDAPInterface(parentResourceType);

      attributeMapper = serverContext.getLDAPAttributeMapper(
          ldapInterface.getSchema(), objectMapper);

      checkForRequiredControls();
    }
    catch (ScimException e)
    {
      String error = String.format("Initialization error: %s; Stack trace: %s",
          e.getMessage(), StaticUtils.getStackTrace(e));
      serverContext.logTraceMessage(LogSeverity.DEBUG, error);
      throw new LDAPException(ResultCode.LOCAL_ERROR, e);
    }
  }

  /**
   * Gets the SCIM Sub-Resource Type's core schema.
   *
   * @return The core schema for the SCIM Sub-Resource Type.
   */
  @Override
  public SchemaResource getCoreSchema()
  {
    return coreSchema;
  }

  /**
   * Indicates that this SCIM Sub-Resource Type Handler supports multiple
   * sub-resources per parent resource.
   *
   * @return Always returns true to indicate that this SCIM Sub-Resource Type
   *         Handler supports multiple sub-resources per parent resource.
   */
  @Override
  public boolean supportsOneToMany()
  {
    // This handler supports multiple sub-resources per parent SCIM resource.
    return true;
  }

  /**
   * Handles SCIM create requests.
   *
   * @param request         A SCIM create request.
   * @return                The created sub-resource.
   * @throws ScimException  If an error occurs.
   */
  @Override
  public ScimResource create(final SCIMCreateRequest request)
      throws ScimException
  {
    // Get the parent LDAP entry's DN. This will be used as the base DN of the
    // LDAP entry to create.
    String parentDN =
        getParentEntryDN(request.getHttpServletRequest(),
            request.getResourceId());

    // Use the SCIM request body to create a LDAP entry for the new
    // sub-resource.
    Entry entryToCreate =
        scimResourceToLDAPEntry(attributeMapper, newEntryDN(parentDN),
            request.getResourceToCreate());
    Entry createdEntry =
        addEntry(request.getHttpServletRequest(), entryToCreate);

    // Convert the new entry to a ScimResource.
    return ldapEntryToSCIMResource(attributeMapper, createdEntry);
  }

  /**
   * Handles SCIM retrieve requests.
   *
   * @param request         A SCIM retrieve request.
   * @return                The retrieved sub-resource.
   * @throws ScimException  If an error occurs.
   */
  @Override
  public ScimResource retrieve(final SCIMRetrieveRequest request)
      throws ScimException
  {
    // Get the sub-resource's LDAP entry.
    Entry entry =
        getSubResourceEntry(request.getHttpServletRequest(),
            request.getResourceId(), request.getSubResourceId());

    // Convert the entry to a ScimResource.
    return ldapEntryToSCIMResource(attributeMapper, entry);
  }

  /**
   * Handles SCIM modify requests.
   *
   * @param request         A SCIM modify request.
   * @return                The modified sub-resource.
   * @throws ScimException  If an error occurs.
   */
  @Override
  public ScimResource modify(final SCIMModifyRequest request)
      throws ScimException
  {
    // Get the sub-resource's current LDAP entry.
    Entry currentEntry =
        getSubResourceEntry(request.getHttpServletRequest(),
            request.getResourceId(), request.getSubResourceId());

    // Convert the current entry to a ScimResource and apply the
    // PatchRequest to it.
    ScimResource currentSubResource =
        ldapEntryToSCIMResource(attributeMapper, currentEntry);
    PatchRequest patchRequest = request.getPatchRequest();
    patchRequest.apply(currentSubResource.asGenericScimResource());

    // Convert the modified ScimResource to an LDAP entry with the changes.
    Entry updatedEntry =
        scimResourceToLDAPEntry(attributeMapper, currentEntry.getDN(),
            currentSubResource);

    // Use the current entry and the updated entry to generate a set of
    // modifications and apply them over LDAP, then convert the result to a
    // ScimResource.
    return update(request.getHttpServletRequest(), currentEntry, updatedEntry);
  }

  /**
   * Handles SCIM replace requests.
   *
   * @param request         A SCIM replace request.
   * @return                The replaced sub-resource.
   * @throws ScimException  If an error occurs.
   */
  @Override
  public ScimResource replace(final SCIMReplaceRequest request)
      throws ScimException
  {
    // Get the sub-resource's current LDAP entry.
    Entry currentEntry =
        getSubResourceEntry(request.getHttpServletRequest(),
            request.getResourceId(), request.getSubResourceId());

    // Create an LDAP entry representation of the SCIM request body containing
    // the changes.
    Entry updatedEntry =
        scimResourceToLDAPEntry(attributeMapper, currentEntry.getDN(),
            request.getRequestResource());

    // Use the current entry and the updated entry to generate a set of
    // modifications and apply them over LDAP, then convert the result to a
    // ScimResource.
    return update(request.getHttpServletRequest(), currentEntry, updatedEntry);
  }

  /**
   * Handles SCIM delete requests.
   *
   * @param request         A SCIM delete request.
   * @throws ScimException  If an error occurs.
   */
  @Override
  public void delete(final SCIMDeleteRequest request) throws ScimException
  {
    // Get the sub-resource's current LDAP entry.
    Entry entry =
        getSubResourceEntry(request.getHttpServletRequest(),
            request.getResourceId(), request.getSubResourceId());

    // Delete the LDAP entry.
    DeleteRequest deleteRequest = new DeleteRequest(entry.getDN());
    ldapInterface.delete(request.getHttpServletRequest(), deleteRequest);
  }

  /**
   * Handles SCIM search requests.
   *
   * @param request         A SCIM search request.
   * @throws ScimException  If an error occurs.
   */
  @Override
  public void search(final SCIMSearchRequest request) throws ScimException
  {
    // Get the parent LDAP entry's DN. This will be used as the base DN for
    // the sub-resource search.
    String parentDN =
        getParentEntryDN(request.getHttpServletRequest(),
            request.getResourceId());

    // Create a SCIMSearchResultListenerAdapter to wrap the
    // SCIMSearchResultListener provided by the server. This will return
    // results back to the server as they are received over the LDAP interface.
    SCIMSearchResultListenerAdapter ldapSearchResultListener =
        new SCIMSearchResultListenerAdapter(ldapInterface.getSchema(),
            objectMapper, serverContext, request.getSearchResultListener());

    // Provide an LDAP filter that simply returns all child entries with the
    // "device" object class rather than attempting to map the SCIM filter from
    // the SCIMSearchRequest to an LDAP filter. When result sets are expected
    // to be small, this is acceptable, because the server will apply the SCIM
    // filter to the SCIMSearchResultListener's result set.
    Filter filter =
        Filter.createEqualityFilter("objectClass", LDAP_OBJECT_CLASS);
    SearchRequest searchRequest =
        new SearchRequest(ldapSearchResultListener, parentDN,
            SearchScope.SUB, filter, "*", "+");
    ldapInterface.search(request.getHttpServletRequest(), searchRequest);
  }

  /**
   * Throws an exception if the LDAP server backing this SCIM Sub-Resource Type
   * does not support the LDAP controls used by this handler.
   *
   * @throws ScimException  If the LDAP server does not support the LDAP
   *                        controls used by this handler.
   */
  private void checkForRequiredControls() throws ScimException
  {
    Set<String> missingRequiredControls = new LinkedHashSet<>();

    if (!ldapInterface.getSupportedControls().contains(
        PostReadRequestControl.POST_READ_REQUEST_OID))
    {
      missingRequiredControls.add("Post-Read Request Control");
    }

    if (!ldapInterface.getSupportedControls().contains(
        NameWithEntryUUIDRequestControl.NAME_WITH_ENTRY_UUID_REQUEST_OID))
    {
      missingRequiredControls.add("Name with entryUUID Request Control");
    }

    if (!missingRequiredControls.isEmpty())
    {
      String errorPrefix = "Cannot initialize extension because the LDAP " +
          "server does not support the following control(s): ";
      StringJoiner joiner = new StringJoiner(",", errorPrefix, "");
      for (String control : missingRequiredControls)
      {
        joiner.add(control);
      }

      throw new ServerErrorException(joiner.toString());
    }
  }

  /**
   * Retrieves this handler's SCIM schema from the server configuration.
   *
   * @return                The SCIM schema used by this handler.
   * @throws LDAPException  If the SCIM schema does not exist.
   */
  private SchemaResource getSchemaFromConfiguration() throws LDAPException
  {
    Optional<SchemaResource> configuredSchema =
        serverContext.getSCIMSchemas()
                     .stream()
                     .filter(schema -> schema.getId().equals(SCIM_SCHEMA_ID))
                     .findFirst();
    return configuredSchema.orElseThrow(
        () -> new LDAPException(ResultCode.LOCAL_ERROR,
            String.format("Could not find required SCIM schema '%s'",
                SCIM_SCHEMA_ID)));
  }

  /**
   * Retrieves the DN of the LDAP entry representing a SCIM parent resource.
   *
   * @param httpServletRequest  The HTTP servlet request associated with the
   *                            current operation.
   * @param scimResourceID      The SCIM resource ID.
   *                            This must not be null.
   *
   * @return                    The SCIM parent resource's LDAP entry DN.
   * @throws ScimException      If the entry cannot be retrieved.
   */
  private String getParentEntryDN(final HttpServletRequest httpServletRequest,
                                  final String scimResourceID)
      throws ScimException
  {
    Filter filter = Filter.createEqualityFilter(
        serverContext.getIDAttribute(parentResourceType), scimResourceID);
    SearchRequest searchRequest =
        new SearchRequest(ldapInterface.getBaseDN(), SearchScope.SUB,
            filter, "1.1");
    Entry entry =
        ldapInterface.searchForEntry(httpServletRequest, searchRequest);
    return entry.getDN();
  }

  /**
   * Retrieves the LDAP entry representing a SCIM sub-resource.
   *
   * @param httpServletRequest  The HTTP servlet request associated with the
   *                            current operation.
   * @param resourceId          The parent SCIM resource ID.
   * @param subResourceId       The SCIM sub-resource ID.
   * @return                    The SCIM sub-resource's LDAP entry.
   * @throws ScimException      If the entry cannot be retrieved.
   */
  private Entry getSubResourceEntry(final HttpServletRequest httpServletRequest,
                                    final String resourceId,
                                    final String subResourceId)
      throws ScimException
  {
    String parentDN = getParentEntryDN(httpServletRequest, resourceId);

    Filter filter =
        Filter.createEqualityFilter(SUBRESOURCE_ID_ATTRIBUTE, subResourceId);
    SearchRequest searchRequest =
        new SearchRequest(parentDN, SearchScope.SUB, filter, "*", "+");
    SearchResult result =
        ldapInterface.search(httpServletRequest, searchRequest);
    if (result.getEntryCount() == 0)
    {
      throw new ResourceNotFoundException(
          String.format("SCIM sub-resource %s not found", subResourceId));
    }
    return result.getSearchEntries().get(0);
  }

  /**
   * Gets a DN to be used for creating a new SCIM sub-resource's LDAP entry.
   *
   * @param parentDN  The parent SCIM resource's LDAP DN.
   *
   * @return          The new DN.
   */
  private String newEntryDN(final String parentDN)
  {
    // Use a placeholder entryUUID value. When adding an entry, the Name with
    // EntryUUID request control will cause the LDAP server to generate a
    // unique value.
    return "entryUUID=00000000-0000-0000-0000-000000000000," + parentDN;
  }

  /**
   * Adds a new entry via LDAP.
   *
   * @param httpServletRequest  The HTTP servlet request associated with the
   *                            current operation.
   * @param entryToAdd          The LDAP entry to add. This entry is expected
   *                            to use entryUUID as its RDN attribute. Use
   *                            {@link #newEntryDN(String)} to create an
   *                            appropriate DN for the entry to be added.
   *
   * @return                    The newly created LDAP entry.
   * @throws ScimException      If the entry cannot be added.
   */
  private Entry addEntry(final HttpServletRequest httpServletRequest,
                         final Entry entryToAdd)
      throws ScimException
  {
    AddRequest addRequest = new AddRequest(entryToAdd);
    addRequest.addControl(new NameWithEntryUUIDRequestControl());
    addRequest.addControl(new PostReadRequestControl("*", "+"));

    LDAPResult result = ldapInterface.add(httpServletRequest, addRequest);

    PostReadResponseControl postReadResponseControl =
        getPostReadResponseControl(result);

    return postReadResponseControl.getEntry();
  }

  /**
   * Updates a SCIM sub-resource using two LDAP representations of the
   * sub-resource: The LDAP entry without changes, and the LDAP entry with
   * changes.
   *
   * @param httpServletRequest  The HTTP servlet request associated with the
   *                            current operation.
   * @param currentEntry        The current LDAP entry.
   * @param updatedEntry        The same LDAP entry with changes.
   *
   * @return                    The SCIM resource after applying changes. If no
   *                            modifications needed to be made, then an
   *                            unchanged resource will be returned.
   * @throws ScimException      If the update fails.
   */
  private ScimResource update(final HttpServletRequest httpServletRequest,
                              final Entry currentEntry,
                              final Entry updatedEntry)
      throws ScimException
  {
    // Generate a list of LDAP modifications.
    List<Modification> ldapMods =
        Entry.diff(currentEntry, updatedEntry, true,
            "cn", "description", "serialNumber");

    // If there are modifications to make, continue and perform and LDAP modify.
    if (!ldapMods.isEmpty())
    {
      ModifyRequest modifyRequest =
          new ModifyRequest(currentEntry.getDN(), ldapMods);
      modifyRequest.addControl(new PostReadRequestControl("*", "+"));

      LDAPResult result =
          ldapInterface.modify(httpServletRequest, modifyRequest);

      // Use the Post-Read Response Control to get the updated LDAP entry.
      PostReadResponseControl postReadResponseControl =
          getPostReadResponseControl(result);

      return ldapEntryToSCIMResource(attributeMapper,
          postReadResponseControl.getEntry());
    }

    // If there were no modifications to make, return the unchanged resource.
    return ldapEntryToSCIMResource(attributeMapper, currentEntry);
  }

  /**
   * Extracts the Post-Read Response Control from an LDAP result.
   *
   * @param result          An LDAP result containing a Post-Read Response
   *                        Control.
   *
   * @return                The Post-Read Response Control.
   * @throws ScimException  If a Post-Read Response Control cannot be obtained.
   */
  private PostReadResponseControl getPostReadResponseControl(
      final LDAPResult result) throws ScimException
  {
    Control control =
        result.getResponseControl(PostReadRequestControl.POST_READ_REQUEST_OID);

    try
    {
      if (control == null)
      {
        throw new LDAPException(ResultCode.CONTROL_NOT_FOUND,
            "LDAP result did not include Post-Read Response Control");
      }

      if (control instanceof PostReadResponseControl)
      {
        return (PostReadResponseControl) control;
      }

      return new PostReadResponseControl(control.getOID(), control.isCritical(),
          control.getValue());
    }
    catch (LDAPException e)
    {
      String error =
          String.format("Error reading Post-Read Response Control " +
              "from LDAP result: %s; Stack trace: %s",
              e.getExceptionMessage(), StaticUtils.getStackTrace(e));
      serverContext.logTraceMessage(LogSeverity.SEVERE_ERROR, error);
      throw new ServerErrorException(error, null, e);
    }
  }

  /**
   * Converts a SCIM gadget sub-resource object to an LDAP device entry.
   *
   * @param attributeMapper  An SCIMLDAPAttributeMapper instance.
   * @param dn               The DN to use for the LDAP entry.
   * @param resource         The SCIM resource.
   *
   * @return                 The LDAP entry.
   * @throws ScimException   If an attribute value cannot be converted.
   */
  private static Entry scimResourceToLDAPEntry(
      final SCIMLDAPAttributeMapper attributeMapper,
      final String dn,
      final ScimResource resource)
      throws ScimException
  {
    ObjectNode fields = resource.asGenericScimResource().getObjectNode();

    Entry entry = new Entry(dn);

    entry.setAttribute("objectClass", "top", LDAP_OBJECT_CLASS);

    entry.setAttribute(attributeMapper.toLdapAttribute(
        fields.path("name"), "name", "cn"));

    if (!fields.path("description").isMissingNode())
    {
      entry.setAttribute(attributeMapper.toLdapAttribute(
          fields.path("description"), "description", "description"));
    }

    if (!fields.path("serialNumber").isMissingNode())
    {
      entry.setAttribute(attributeMapper.toLdapAttribute(
          fields.path("serialNumber"), "serialNumber", "serialNumber"));
    }

    return entry;
  }

  /**
   * Converts an LDAP device entry to a SCIM gadget sub-resource object.
   *
   * @param attributeMapper  An SCIMLDAPAttributeMapper instance.
   * @param entry            The LDAP entry.
   *
   * @return                 The SCIM resource.
   * @throws ScimException   If an attribute value cannot be converted.
   */
  private static ScimResource ldapEntryToSCIMResource(
      final SCIMLDAPAttributeMapper attributeMapper,
      final Entry entry)
      throws ScimException
  {
    ObjectNode objectNode = JsonUtils.getJsonNodeFactory().objectNode();

    TextNode name =
        attributeMapper.toTextNode(entry.getAttribute("cn"));
    objectNode.replace("name", name);

    if (entry.getAttribute("description") != null)
    {
      TextNode description =
          attributeMapper.toTextNode(entry.getAttribute("description"));
      objectNode.replace("description", description);
    }

    if (entry.getAttribute("serialNumber") != null)
    {
      TextNode serialNumber =
          attributeMapper.toTextNode(entry.getAttribute("serialNumber"));
      objectNode.replace("serialNumber", serialNumber);
    }

    GenericScimResource resource = new GenericScimResource(objectNode);
    resource.setSchemaUrns(Collections.singletonList(SCIM_SCHEMA_ID));

    if (entry.getAttributeValue(SUBRESOURCE_ID_ATTRIBUTE) != null)
    {
      resource.setId(entry.getAttributeValue(SUBRESOURCE_ID_ATTRIBUTE));
    }

    return resource;
  }


  /**
   * An LDAP search result listener implementation that maps its search results
   * to a {@link SCIMSearchResultListener}.
   */
  private static class SCIMSearchResultListenerAdapter
      implements SearchResultListener
  {
    private static final long serialVersionUID = 3211616264384619807L;

    private final SCIMServerContext serverContext;
    private final SCIMLDAPAttributeMapper attributeMapper;
    private final SCIMSearchResultListener scimSearchResultListener;

    /**
     * Constructs a SCIMSearchResultListenerAdapter.
     *
     * @param ldapSchema                The LDAP server schema.
     * @param objectMapper              A Jackson object mapper.
     * @param serverContext             The extension's server context.
     * @param scimSearchResultListener  The SCIM search result listener
     *                                  associated with the current operation.
     */
    SCIMSearchResultListenerAdapter(
        final Schema ldapSchema,
        final ObjectMapper objectMapper,
        final SCIMServerContext serverContext,
        final SCIMSearchResultListener scimSearchResultListener)
    {
      this.serverContext = serverContext;
      this.attributeMapper =
          serverContext.getLDAPAttributeMapper(ldapSchema, objectMapper);
      this.scimSearchResultListener = scimSearchResultListener;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void searchEntryReturned(final SearchResultEntry searchResultEntry)
    {
      try
      {
        scimSearchResultListener.searchResultReturned(
            ldapEntryToSCIMResource(attributeMapper, searchResultEntry));
      }
      catch (ScimException e)
      {
        serverContext.logTraceMessage(LogSeverity.SEVERE_ERROR,
            String.format("Unable to map search result entry '%s' " +
                "to a ScimResource", searchResultEntry.getDN()));
      }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void searchReferenceReturned(
        final SearchResultReference searchResultReference)
    {
      // No implementation needed.
    }
  }
}