From a08c81f677247ec9eb7721a86250c663065e9930 Mon Sep 17 00:00:00 2001
From: Matthew Swift <matthew.swift@forgerock.com>
Date: Wed, 22 Jun 2016 22:12:03 +0000
Subject: [PATCH] OPENDJ-2871 Add support for sub-resources and inheritance

---
 opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceImpl.java |  993 +++++++++++++++++++++++++++++++---------------------------
 1 files changed, 527 insertions(+), 466 deletions(-)

diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceImpl.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceImpl.java
index 4f5403a..a1da30e 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceImpl.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceImpl.java
@@ -16,20 +16,30 @@
 package org.forgerock.opendj.rest2ldap;
 
 import static org.forgerock.i18n.LocalizableMessage.raw;
-import static org.forgerock.json.resource.Responses.newResourceResponse;
+import static org.forgerock.opendj.ldap.ResultCode.Enum.NOT_ALLOWED_ON_NONLEAF;
+import static org.forgerock.opendj.ldap.SearchScope.BASE_OBJECT;
+import static org.forgerock.opendj.ldap.responses.Responses.newResult;
+import static org.forgerock.opendj.ldap.spi.LdapPromises.newSuccessfulLdapPromise;
+import static org.forgerock.opendj.rest2ldap.FilterType.*;
+import static org.forgerock.opendj.rest2ldap.Rest2Ldap.*;
 import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.*;
-import static java.util.Arrays.asList;
+import static org.forgerock.json.resource.ResourceException.FORBIDDEN;
+import static org.forgerock.json.resource.ResourceException.newResourceException;
+import static org.forgerock.json.resource.Responses.newActionResponse;
+import static org.forgerock.json.resource.Responses.newQueryResponse;
+import static org.forgerock.json.resource.Responses.newResourceResponse;
+import static org.forgerock.opendj.ldap.ByteString.valueOfBytes;
 import static org.forgerock.opendj.ldap.Filter.alwaysFalse;
 import static org.forgerock.opendj.ldap.Filter.alwaysTrue;
-import static org.forgerock.opendj.ldap.requests.Requests.newAddRequest;
-import static org.forgerock.opendj.ldap.requests.Requests.newDeleteRequest;
-import static org.forgerock.opendj.ldap.requests.Requests.newModifyRequest;
-import static org.forgerock.opendj.ldap.requests.Requests.newSearchRequest;
+import static org.forgerock.opendj.ldap.SearchScope.SINGLE_LEVEL;
+import static org.forgerock.opendj.ldap.requests.Requests.*;
 import static org.forgerock.opendj.rest2ldap.ReadOnUpdatePolicy.CONTROLS;
-import static org.forgerock.opendj.rest2ldap.Rest2Ldap.asResourceException;
 import static org.forgerock.opendj.rest2ldap.Utils.newBadRequestException;
 import static org.forgerock.opendj.rest2ldap.Utils.newNotSupportedException;
 import static org.forgerock.opendj.rest2ldap.Utils.toFilter;
+import static org.forgerock.util.Utils.asEnum;
+import static org.forgerock.util.promise.Promises.newResultPromise;
+import static org.forgerock.util.promise.Promises.when;
 
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
@@ -40,6 +50,7 @@
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Set;
+import java.util.concurrent.atomic.AtomicReference;
 
 import org.forgerock.i18n.LocalizableMessage;
 import org.forgerock.i18n.slf4j.LocalizedLogger;
@@ -48,7 +59,7 @@
 import org.forgerock.json.JsonValueException;
 import org.forgerock.json.resource.ActionRequest;
 import org.forgerock.json.resource.ActionResponse;
-import org.forgerock.json.resource.CollectionResourceProvider;
+import org.forgerock.json.resource.BadRequestException;
 import org.forgerock.json.resource.CreateRequest;
 import org.forgerock.json.resource.DeleteRequest;
 import org.forgerock.json.resource.NotSupportedException;
@@ -61,7 +72,6 @@
 import org.forgerock.json.resource.ReadRequest;
 import org.forgerock.json.resource.ResourceException;
 import org.forgerock.json.resource.ResourceResponse;
-import org.forgerock.json.resource.Responses;
 import org.forgerock.json.resource.UncategorizedException;
 import org.forgerock.json.resource.UpdateRequest;
 import org.forgerock.opendj.ldap.Attribute;
@@ -72,9 +82,12 @@
 import org.forgerock.opendj.ldap.DecodeException;
 import org.forgerock.opendj.ldap.DecodeOptions;
 import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.opendj.ldap.EntryNotFoundException;
 import org.forgerock.opendj.ldap.Filter;
 import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.LdapPromise;
 import org.forgerock.opendj.ldap.Modification;
+import org.forgerock.opendj.ldap.ResultCode;
 import org.forgerock.opendj.ldap.SearchResultHandler;
 import org.forgerock.opendj.ldap.SearchScope;
 import org.forgerock.opendj.ldap.controls.AssertionRequestControl;
@@ -88,7 +101,6 @@
 import org.forgerock.opendj.ldap.requests.AddRequest;
 import org.forgerock.opendj.ldap.requests.ModifyRequest;
 import org.forgerock.opendj.ldap.requests.PasswordModifyExtendedRequest;
-import org.forgerock.opendj.ldap.requests.Requests;
 import org.forgerock.opendj.ldap.requests.SearchRequest;
 import org.forgerock.opendj.ldap.responses.PasswordModifyExtendedResult;
 import org.forgerock.opendj.ldap.responses.Result;
@@ -104,72 +116,69 @@
 import org.forgerock.util.promise.ExceptionHandler;
 import org.forgerock.util.promise.Promise;
 import org.forgerock.util.promise.PromiseImpl;
-import org.forgerock.util.promise.Promises;
 import org.forgerock.util.promise.ResultHandler;
 import org.forgerock.util.query.QueryFilter;
 import org.forgerock.util.query.QueryFilterVisitor;
 
-/**
- * A {@code CollectionResourceProvider} implementation which maps a JSON
- * resource collection to LDAP entries beneath a base DN.
- */
-final class SubResourceImpl implements CollectionResourceProvider {
-
+/** Implements the core CREST operations supported by singleton and collection sub-resources. */
+final class SubResourceImpl {
     private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
 
     /** Dummy exception used for signalling search success. */
     private static final ResourceException SUCCESS = new UncategorizedException(0, null, null);
 
-    /** Empty decode options required for decoding response controls. */
-    private static final DecodeOptions DECODE_OPTIONS = new DecodeOptions();
+    private static final JsonPointer ROOT = new JsonPointer();
 
-    private final List<Attribute> additionalLDAPAttributes;
-    private final PropertyMapper propertyMapper;
-    private final DN baseDn; // TODO: support template variables.
-    private final Config config;
+    private final DN baseDn;
     private final AttributeDescription etagAttribute;
     private final NamingStrategy namingStrategy;
+    private final DecodeOptions decodeOptions;
+    private final ReadOnUpdatePolicy readOnUpdatePolicy;
+    private final boolean useSubtreeDelete;
+    private final boolean usePermissiveModify;
+    private final Resource resource;
+    private final Attribute glueObjectClasses;
 
-    SubResourceImpl(final DN baseDn, final PropertyMapper mapper,
-                    final NamingStrategy namingStrategy, final AttributeDescription etagAttribute,
-                    final Config config, final List<Attribute> additionalLDAPAttributes) {
+    SubResourceImpl(final Rest2Ldap rest2Ldap, final DN baseDn, final Attribute glueObjectClasses,
+                    final NamingStrategy namingStrategy, final Resource resource) {
+        this.readOnUpdatePolicy = rest2Ldap.getOptions().get(READ_ON_UPDATE_POLICY);
+        this.useSubtreeDelete = rest2Ldap.getOptions().get(USE_SUBTREE_DELETE);
+        this.usePermissiveModify = rest2Ldap.getOptions().get(USE_PERMISSIVE_MODIFY);
+        this.etagAttribute = rest2Ldap.getOptions().get(USE_MVCC)
+                ? AttributeDescription.valueOf(rest2Ldap.getOptions().get(MVCC_ATTRIBUTE)) : null;
+        this.decodeOptions = rest2Ldap.getOptions().get(DECODE_OPTIONS);
         this.baseDn = baseDn;
-        this.propertyMapper = mapper;
-        this.config = config;
+        this.glueObjectClasses = glueObjectClasses;
         this.namingStrategy = namingStrategy;
-        this.etagAttribute = etagAttribute;
-        this.additionalLDAPAttributes = additionalLDAPAttributes;
+        this.resource = resource;
     }
 
-    @Override
-    public Promise<ActionResponse, ResourceException> actionCollection(
-            final Context context, final ActionRequest request) {
-        return Promises.<ActionResponse, ResourceException> newExceptionPromise(
-                                                            newNotSupportedException(ERR_NOT_YET_IMPLEMENTED.get()));
-    }
-
-    @Override
-    public Promise<ActionResponse, ResourceException> actionInstance(
+    Promise<ActionResponse, ResourceException> action(
             final Context context, final String resourceId, final ActionRequest request) {
-        String actionId = request.getAction();
-        if (actionId.equals("passwordModify")) {
-            return passwordModify(context, resourceId, request);
+        try {
+            final Action action = asEnum(request.getAction(), Action.class);
+            if (resource.hasSupportedAction(action)) {
+                switch (action) {
+                case PASSWORDMODIFY:
+                    return passwordModify(context, resourceId, request);
+                }
+            }
+        } catch (final IllegalArgumentException ignored) {
+            // fall-through
         }
-        return Promises.<ActionResponse, ResourceException> newExceptionPromise(
-                newNotSupportedException(ERR_ACTION_NOT_SUPPORTED.get(actionId)));
+        return newNotSupportedException(ERR_ACTION_NOT_SUPPORTED.get(request.getAction())).asPromise();
+
     }
 
     private Promise<ActionResponse, ResourceException> passwordModify(
             final Context context, final String resourceId, final ActionRequest request) {
         if (!context.containsContext(ClientContext.class)
                 || !context.asContext(ClientContext.class).isSecure()) {
-            return Promises.newExceptionPromise(ResourceException.newResourceException(
-                    ResourceException.FORBIDDEN, ERR_PASSWORD_MODIFY_SECURE_CONNECTION.get().toString()));
+            return newResourceException(FORBIDDEN, ERR_PASSWORD_MODIFY_SECURE_CONNECTION.get().toString()).asPromise();
         }
         if (!context.containsContext(SecurityContext.class)
                 || context.asContext(SecurityContext.class).getAuthenticationId() == null) {
-            return Promises.newExceptionPromise(ResourceException.newResourceException(
-                    ResourceException.FORBIDDEN, ERR_PASSWORD_MODIFY_USER_AUTHENTICATED.get().toString()));
+            return newResourceException(FORBIDDEN, ERR_PASSWORD_MODIFY_USER_AUTHENTICATED.get().toString()).asPromise();
         }
 
         final JsonValue jsonContent = request.getContent();
@@ -182,231 +191,307 @@
             final LocalizableMessage msg = ERR_PASSWORD_MODIFY_REQUEST_IS_INVALID.get();
             final ResourceException ex = newBadRequestException(msg, e);
             logger.error(msg, e);
-            return Promises.newExceptionPromise(ex);
+            return ex.asPromise();
         }
 
-        final Connection connection = context.asContext(AuthenticatedConnectionContext.class).getConnection();
-        List<JsonPointer> attrs = Collections.emptyList();
-        return connection.searchSingleEntryAsync(searchRequest(connection, resourceId, attrs))
-                         .thenAsync(new AsyncFunction<SearchResultEntry, ActionResponse, ResourceException>() {
-                             @Override
-                             public Promise<ActionResponse, ResourceException> apply(
-                                       final SearchResultEntry entry) {
-                                 PasswordModifyExtendedRequest pwdModifyRequest =
-                                         Requests.newPasswordModifyExtendedRequest();
-                                 pwdModifyRequest.setUserIdentity("dn: " + entry.getName());
-                                 pwdModifyRequest.setOldPassword(asBytes(oldPassword));
-                                 pwdModifyRequest.setNewPassword(asBytes(newPassword));
-                                 return connection.extendedRequestAsync(pwdModifyRequest)
-                                     .thenAsync(new AsyncFunction<PasswordModifyExtendedResult,
-                                             ActionResponse, ResourceException>() {
-                                         @Override
-                                         public Promise<ActionResponse, ResourceException> apply(
-                                                 PasswordModifyExtendedResult value) throws ResourceException {
-                                             JsonValue result = new JsonValue(new LinkedHashMap<>());
-                                             byte[] generatedPwd = value.getGeneratedPassword();
-                                             if (generatedPwd != null) {
-                                                 result = result.put("generatedPassword",
-                                                         ByteString.valueOfBytes(generatedPwd).toString());
-                                             }
-                                             return Responses.newActionResponse(result).asPromise();
-                                         }
-                                     }, Exceptions.<ActionResponse>toResourceException());
-                             }
-                         }, Exceptions.<ActionResponse>toResourceException());
+        final Connection connection = connectionFrom(context);
+        return resolveResourceDnAndType(context, connection, resourceId, null)
+                .thenAsync(new AsyncFunction<RoutingContext, PasswordModifyExtendedResult, ResourceException>() {
+                    @Override
+                    public Promise<PasswordModifyExtendedResult, ResourceException> apply(RoutingContext dnAndType) {
+                        final PasswordModifyExtendedRequest pwdModifyRequest = newPasswordModifyExtendedRequest()
+                                .setUserIdentity("dn: " + dnAndType.getDn())
+                                .setOldPassword(asBytes(oldPassword))
+                                .setNewPassword(asBytes(newPassword));
+                        return connection.extendedRequestAsync(pwdModifyRequest)
+                                         .thenCatchAsync(adaptLdapException(PasswordModifyExtendedResult.class));
+                    }
+                }).thenAsync(new AsyncFunction<PasswordModifyExtendedResult, ActionResponse, ResourceException>() {
+                    @Override
+                    public Promise<ActionResponse, ResourceException> apply(PasswordModifyExtendedResult r) {
+                        final JsonValue result = new JsonValue(new LinkedHashMap<>());
+                        final byte[] generatedPwd = r.getGeneratedPassword();
+                        if (generatedPwd != null) {
+                            result.put("generatedPassword", valueOfBytes(generatedPwd).toString());
+                        }
+                        return newActionResponse(result).asPromise();
+                    }
+                });
     }
 
     private byte[] asBytes(final String s) {
         return s != null ? s.getBytes(StandardCharsets.UTF_8) : null;
     }
 
-    @Override
-    public Promise<ResourceResponse, ResourceException> createInstance(final Context context,
-            final CreateRequest request) {
-        final Connection connection = context.asContext(AuthenticatedConnectionContext.class).getConnection();
-        // Calculate entry content.
-        return propertyMapper
-                .create(connection, new JsonPointer(), request.getContent())
-                .thenAsync(new AsyncFunction<List<Attribute>, ResourceResponse, ResourceException>() {
-                    @Override
-                    public Promise<ResourceResponse, ResourceException> apply(final List<Attribute> attributes) {
-                        // Perform add operation.
-                        final AddRequest addRequest = newAddRequest(DN.rootDN());
-                        for (final Attribute attribute : additionalLDAPAttributes) {
-                            addRequest.addAttribute(attribute);
-                        }
-                        for (final Attribute attribute : attributes) {
-                            addRequest.addAttribute(attribute);
-                        }
-                        try {
-                            namingStrategy.setResourceId(connection, getBaseDn(),
-                                                         request.getNewResourceId(),
-                                                         addRequest);
-                        } catch (final ResourceException e) {
-                            logger.error(raw(e.getLocalizedMessage()), e);
-                            return Promises.newExceptionPromise(e);
-                        }
-                        if (config.readOnUpdatePolicy() == CONTROLS) {
-                            addRequest.addControl(PostReadRequestControl.newControl(
-                                    false, getLdapAttributes(connection, request.getFields())));
-                        }
-                        return connection.applyChangeAsync(addRequest)
-                                         .thenAsync(
-                                                 postUpdateResultAsyncFunction(connection),
-                                                 Exceptions.<ResourceResponse>toResourceException());
-                    }
-                });
-    }
-
-    @Override
-    public Promise<ResourceResponse, ResourceException> deleteInstance(
-            final Context context, final String resourceId, final DeleteRequest request) {
-        final Connection connection = context.asContext(AuthenticatedConnectionContext.class).getConnection();
-        return doUpdateFunction(connection, resourceId, request.getRevision())
-                .thenAsync(new AsyncFunction<DN, ResourceResponse, ResourceException>() {
-                    @Override
-                    public Promise<ResourceResponse, ResourceException> apply(DN dn) throws ResourceException {
-                        try {
-                            final ChangeRecord deleteRequest = newDeleteRequest(dn);
-                            if (config.readOnUpdatePolicy() == CONTROLS) {
-                                final String[] attributes = getLdapAttributes(connection, request.getFields());
-                                deleteRequest.addControl(PreReadRequestControl.newControl(false, attributes));
-                            }
-                            if (config.useSubtreeDelete()) {
-                                deleteRequest.addControl(SubtreeDeleteRequestControl.newControl(true));
-                            }
-                            addAssertionControl(deleteRequest, request.getRevision());
-                            return connection.applyChangeAsync(deleteRequest)
-                                             .thenAsync(
-                                                     postUpdateResultAsyncFunction(connection),
-                                                     Exceptions.<ResourceResponse>toResourceException());
-
-                        } catch (final Exception e) {
-                            return Promises.newExceptionPromise(asResourceException(e));
-                        }
-                    }
-                });
-    }
-
-    @Override
-    public Promise<ResourceResponse, ResourceException> patchInstance(
-            final Context context, final String resourceId, final PatchRequest request) {
-        final Connection connection = context.asContext(AuthenticatedConnectionContext.class).getConnection();
-        if (request.getPatchOperations().isEmpty()) {
-            return emptyPatchInstance(connection, resourceId, request);
+    Promise<ResourceResponse, ResourceException> create(final Context context, final CreateRequest  request) {
+        // First determine the type of resource being created.
+        final Resource subType;
+        try {
+            subType = resource.resolveSubTypeFromJson(request.getContent());
+        } catch (final ResourceException e) {
+            return e.asPromise();
         }
-        return doUpdateFunction(connection, resourceId, request.getRevision())
-                .thenAsync(new AsyncFunction<DN, ResourceResponse, ResourceException>() {
+
+        // Now build the LDAP representation and add it.
+        final Connection connection = connectionFrom(context);
+        return subType.getPropertyMapper()
+                      .create(connection, subType, ROOT, request.getContent())
+                      .thenAsync(new AsyncFunction<List<Attribute>, ResourceResponse, ResourceException>() {
+                          @Override
+                          public Promise<ResourceResponse, ResourceException> apply(final List<Attribute> attributes) {
+                              // Perform add operation.
+                              final AddRequest addRequest = newAddRequest(DN.rootDN());
+                              addRequest.addAttribute(subType.getObjectClassAttribute());
+                              for (final Attribute attribute : attributes) {
+                                  addRequest.addAttribute(attribute);
+                              }
+                              try {
+                                  namingStrategy.encodeResourceId(baseDn, request.getNewResourceId(), addRequest);
+                              } catch (final ResourceException e) {
+                                  logger.error(raw(e.getLocalizedMessage()), e);
+                                  return e.asPromise();
+                              }
+                              if (readOnUpdatePolicy == CONTROLS) {
+                                  final Set<String> ldapAttributes =
+                                          getLdapAttributesForKnownType(request.getFields(), subType);
+                                  addRequest.addControl(PostReadRequestControl.newControl(false, ldapAttributes));
+                              }
+                              return connection.addAsync(addRequest)
+                                               .thenCatchAsync(lazilyAddGlueEntry(connection, addRequest))
+                                               .thenAsync(encodeUpdateResourceResponse(connection, subType),
+                                                          adaptLdapException(ResourceResponse.class));
+                          }
+                      });
+    }
+
+    /**
+     * A resource and sub-resource may be separated by a "glue" entry in LDAP. This method detects when a glue entry
+     * is missing, creates it, and then retries the original add operation. As a concrete example, consider the
+     * backend configuration entry "ds-cfg-backend-id=userRoot,cn=backends,cn=config". Since its indexes are located
+     * beneath "cn=Indexes,ds-cfg-backend-id=userRoot,cn=backends,cn=config" we need to add "cn=Indexes" before
+     * adding an index entry.
+     */
+    private AsyncFunction<LdapException, Result, LdapException> lazilyAddGlueEntry(final Connection connection,
+                                                                                   final AddRequest addRequest) {
+        return new AsyncFunction<LdapException, Result, LdapException>() {
+            @Override
+            public Promise<Result, LdapException> apply(final LdapException e) throws LdapException {
+                if (glueObjectClasses != null && e instanceof EntryNotFoundException) {
+                    // The parent glue entry may be missing - lazily create it.
+                    final AddRequest glueAddRequest = newAddRequest(baseDn);
+                    glueAddRequest.addAttribute(glueObjectClasses);
+                    glueAddRequest.addAttribute(baseDn.rdn().getFirstAVA().toAttribute());
+                    return connection.addAsync(glueAddRequest)
+                                     .thenAsync(new AsyncFunction<Result, Result, LdapException>() {
+                                         @Override
+                                         public Promise<Result, LdapException> apply(final Result value) {
+                                             return connection.addAsync(addRequest);
+                                         }
+                                     });
+                }
+                // Something else happened, so rethrow.
+                throw e;
+            }
+        };
+    }
+
+    private Connection connectionFrom(final Context context) {
+        return context.asContext(AuthenticatedConnectionContext.class).getConnection();
+    }
+
+    Promise<ResourceResponse, ResourceException> delete(
+            final Context context, final String resourceId, final DeleteRequest request) {
+        final Connection connection = connectionFrom(context);
+        return resolveResourceDnAndType(context, connection, resourceId, request.getRevision())
+                .thenAsync(new AsyncFunction<RoutingContext, ResourceResponse, ResourceException>() {
                     @Override
-                    public Promise<ResourceResponse, ResourceException> apply(final DN dn) throws ResourceException {
-                        // Convert the patch operations to LDAP modifications.
-                        List<Promise<List<Modification>, ResourceException>> promises =
-                                new ArrayList<>(request.getPatchOperations().size());
-                        for (final PatchOperation operation : request.getPatchOperations()) {
-                            promises.add(propertyMapper.patch(connection, new JsonPointer(), operation));
+                    public Promise<ResourceResponse, ResourceException> apply(final RoutingContext dnAndType)
+                            throws ResourceException {
+                        final ChangeRecord deleteRequest = newDeleteRequest(dnAndType.getDn());
+                        if (readOnUpdatePolicy == CONTROLS) {
+                            final Set<String> attributes =
+                                    getLdapAttributesForKnownType(request.getFields(), dnAndType.getType());
+                            deleteRequest.addControl(PreReadRequestControl.newControl(false, attributes));
                         }
-
-                        return Promises.when(promises).thenAsync(
-                                new AsyncFunction<List<List<Modification>>, ResourceResponse, ResourceException>() {
-                                    @Override
-                                    public Promise<ResourceResponse, ResourceException> apply(
-                                            final List<List<Modification>> result) {
-                                        // The patch operations have been converted successfully.
-                                        try {
-                                            final ModifyRequest modifyRequest = newModifyRequest(dn);
-
-                                            // Add the modifications.
-                                            for (final List<Modification> modifications : result) {
-                                                if (modifications != null) {
-                                                    modifyRequest.getModifications().addAll(modifications);
-                                                }
-                                            }
-
-                                            final List<String> attributes =
-                                                    asList(getLdapAttributes(connection, request.getFields()));
-                                            if (modifyRequest.getModifications().isEmpty()) {
-                                                // This patch is a no-op so just read the entry and check its version.
-                                                return
-                                                   connection
-                                                     .readEntryAsync(dn, attributes)
-                                                     .thenAsync(postEmptyPatchAsyncFunction(connection, request),
-                                                                Exceptions.<ResourceResponse>toResourceException());
-                                            } else {
-                                                // Add controls and perform the modify request.
-                                                if (config.readOnUpdatePolicy() == CONTROLS) {
-                                                    modifyRequest.addControl(
-                                                            PostReadRequestControl.newControl(false, attributes));
-                                                }
-                                                if (config.usePermissiveModify()) {
-                                                    modifyRequest.addControl(
-                                                            PermissiveModifyRequestControl.newControl(true));
-                                                }
-                                                addAssertionControl(modifyRequest, request.getRevision());
-                                                return connection
-                                                        .applyChangeAsync(modifyRequest)
-                                                        .thenAsync(
-                                                                postUpdateResultAsyncFunction(connection),
-                                                                Exceptions.<ResourceResponse>toResourceException());
-                                            }
-                                        } catch (final Exception e) {
-                                            return Promises.newExceptionPromise(asResourceException(e));
-                                        }
-                                    }
-                                });
+                        if (resource.mayHaveSubResources() && useSubtreeDelete) {
+                            // Non-critical so that we can detect failure and retry without the control. Some backends,
+                            // such as cn=config, do not support the subtree delete control.
+                            deleteRequest.addControl(SubtreeDeleteRequestControl.newControl(false));
+                        }
+                        addAssertionControl(deleteRequest, request.getRevision());
+                        return connection.applyChangeAsync(deleteRequest)
+                                         .thenCatchAsync(deleteSubtreeWithoutUsingSubtreeDeleteControl(connection,
+                                                                                                       deleteRequest))
+                                         .thenAsync(encodeUpdateResourceResponse(connection, dnAndType.getType()),
+                                                    adaptLdapException(ResourceResponse.class));
                     }
                 });
     }
 
-    /** Just read the entry and check its version. */
-    private Promise<ResourceResponse, ResourceException> emptyPatchInstance(final Connection connection,
-            final String resourceId, final PatchRequest request) {
-        final SearchRequest searchRequest = searchRequest(connection, resourceId, request.getFields());
-        return connection
-                .searchSingleEntryAsync(searchRequest)
-                .thenAsync(postEmptyPatchAsyncFunction(connection, request),
-                           Exceptions.<ResourceResponse>toResourceException());
+    /**
+     * Detects whether a delete request failed because the targeted entry has children and the subtree delete control
+     * could not be applied (e.g. due to ACIs or lack of support in the backend). On failure, fall-back to a search
+     * and then a recursive bottom up delete of all subordinate entries, before finally retrying the original delete
+     * request.
+     */
+    private AsyncFunction<LdapException, Result, LdapException> deleteSubtreeWithoutUsingSubtreeDeleteControl(
+            final Connection connection, final ChangeRecord deleteRequest) {
+        return new AsyncFunction<LdapException, Result, LdapException>() {
+            @Override
+            public Promise<Result, LdapException> apply(final LdapException e) throws LdapException {
+                if (e.getResult().getResultCode().asEnum() != NOT_ALLOWED_ON_NONLEAF
+                        || !resource.mayHaveSubResources()) {
+                    throw e;
+                }
+
+                // Perform a subtree search and then delete entries one by one.
+                final SearchRequest subordinates = newSearchRequest(deleteRequest.getName(),
+                                                                    SearchScope.SUBORDINATES,
+                                                                    Filter.objectClassPresent(),
+                                                                    "1.1");
+
+                // This list does not need synchronization because search result notification is synchronized.
+                final List<DN> subordinateEntries = new ArrayList<>();
+                return connection.searchAsync(subordinates, new SearchResultHandler() {
+                    @Override
+                    public boolean handleEntry(final SearchResultEntry entry) {
+                        subordinateEntries.add(entry.getName());
+                        return true;
+                    }
+
+                    @Override
+                    public boolean handleReference(final SearchResultReference reference) {
+                        return false;
+                    }
+                }).thenAsync(new AsyncFunction<Result, Result, LdapException>() {
+                    @Override
+                    public Promise<Result, LdapException> apply(final Result result) {
+                        // Sort the entries in hierarchical order and then delete them in reverse, thus
+                        // always deleting children before parents.
+                        Collections.sort(subordinateEntries);
+                        LdapPromise<Result> promise = newSuccessfulLdapPromise(newResult(ResultCode.SUCCESS));
+                        for (int i = subordinateEntries.size() - 1; i >= 0; i--) {
+                            final ChangeRecord subordinateDelete = newDeleteRequest(subordinateEntries.get(i));
+                            promise = promise.thenAsync(new AsyncFunction<Result, Result, LdapException>() {
+                                @Override
+                                public Promise<Result, LdapException> apply(final Result result) {
+                                    return connection.applyChangeAsync(subordinateDelete);
+                                }
+                            });
+                        }
+                        // And finally retry the original delete request.
+                        return promise.thenAsync(new AsyncFunction<Result, Result, LdapException>() {
+                            @Override
+                            public Promise<Result, LdapException> apply(final Result result) {
+                                return connection.applyChangeAsync(deleteRequest);
+                            }
+                        });
+                    }
+                });
+            }
+        };
     }
 
-    private AsyncFunction<SearchResultEntry, ResourceResponse, ResourceException> postEmptyPatchAsyncFunction(
-            final Connection connection, final PatchRequest request) {
-        return new AsyncFunction<SearchResultEntry, ResourceResponse, ResourceException>() {
+    Promise<ResourceResponse, ResourceException> patch(
+            final Context context, final String resourceId, final PatchRequest request) {
+        final Connection connection = connectionFrom(context);
+        final AtomicReference<RoutingContext> dnAndTypeHolder = new AtomicReference<>();
+        return resolveResourceDnAndType(context, connection, resourceId, request.getRevision())
+                .thenAsync(new AsyncFunction<RoutingContext, List<List<Modification>>, ResourceException>() {
+                    @Override
+                    public Promise<List<List<Modification>>, ResourceException> apply(final RoutingContext dnAndType)
+                            throws ResourceException {
+                        dnAndTypeHolder.set(dnAndType);
+
+                        // Convert the patch operations to LDAP modifications.
+                        final List<Promise<List<Modification>, ResourceException>> promises =
+                                new ArrayList<>(request.getPatchOperations().size());
+                        final Resource subType = dnAndType.getType();
+                        final PropertyMapper propertyMapper = subType.getPropertyMapper();
+                        for (final PatchOperation operation : request.getPatchOperations()) {
+                            promises.add(propertyMapper.patch(connection, subType, ROOT, operation));
+                        }
+                        return when(promises);
+                    }
+                }).thenAsync(new AsyncFunction<List<List<Modification>>, ResourceResponse, ResourceException>() {
+                    @Override
+                    public Promise<ResourceResponse, ResourceException> apply(final List<List<Modification>> result)
+                            throws ResourceException {
+                        // The patch operations have been converted successfully.
+                        final RoutingContext dnAndType = dnAndTypeHolder.get();
+                        final ModifyRequest modifyRequest = newModifyRequest(dnAndType.getDn());
+
+                        // Add the modifications.
+                        for (final List<Modification> modifications : result) {
+                            if (modifications != null) {
+                                modifyRequest.getModifications().addAll(modifications);
+                            }
+                        }
+
+                        final Resource subType = dnAndType.getType();
+                        final Set<String> attributes = getLdapAttributesForKnownType(request.getFields(), subType);
+                        if (modifyRequest.getModifications().isEmpty()) {
+                            // This patch is a no-op so just read the entry and check its version.
+                            return connection.readEntryAsync(dnAndType.getDn(), attributes)
+                                             .thenAsync(encodeEmptyPatchResourceResponse(connection, subType, request),
+                                                        adaptLdapException(ResourceResponse.class));
+                        } else {
+                            // Add controls and perform the modify request.
+                            if (readOnUpdatePolicy == CONTROLS) {
+                                modifyRequest.addControl(PostReadRequestControl.newControl(false, attributes));
+                            }
+                            if (usePermissiveModify) {
+                                modifyRequest.addControl(PermissiveModifyRequestControl.newControl(true));
+                            }
+                            addAssertionControl(modifyRequest, request.getRevision());
+                            return connection.applyChangeAsync(modifyRequest)
+                                             .thenAsync(encodeUpdateResourceResponse(connection, subType),
+                                                        adaptLdapException(ResourceResponse.class));
+                        }
+                    }
+                });
+    }
+
+    private AsyncFunction<Entry, ResourceResponse, ResourceException> encodeEmptyPatchResourceResponse(
+            final Connection connection, final Resource resource, final PatchRequest request) {
+        return new AsyncFunction<Entry, ResourceResponse, ResourceException>() {
             @Override
-            public Promise<ResourceResponse, ResourceException> apply(SearchResultEntry entry)
-                    throws ResourceException {
+            public Promise<ResourceResponse, ResourceException> apply(Entry entry) throws ResourceException {
                 try {
-                    // Fail if there is a version mismatch.
                     ensureMvccVersionMatches(entry, request.getRevision());
-                    return adaptEntry(connection, entry);
+                    return encodeResourceResponse(connection, resource, entry);
                 } catch (final Exception e) {
-                    return Promises.newExceptionPromise(asResourceException(e));
+                    return asResourceException(e).asPromise();
                 }
             }
         };
     }
 
-    @Override
-    public Promise<QueryResponse, ResourceException> queryCollection(
+    Promise<QueryResponse, ResourceException> query(
             final Context context, final QueryRequest request, final QueryResourceHandler resourceHandler) {
-        final Connection connection = context.asContext(AuthenticatedConnectionContext.class).getConnection();
-        // Calculate the filter (this may require the connection).
+        final Connection connection = connectionFrom(context);
         return getLdapFilter(connection, request.getQueryFilter())
                 .thenAsync(runQuery(request, resourceHandler, connection));
     }
 
-    private Promise<Filter, ResourceException> getLdapFilter(final Connection connection,
-                                                             final QueryFilter<JsonPointer> queryFilter) {
+    // FIXME: supporting assertions against sub-type properties.
+    private Promise<Filter, ResourceException> getLdapFilter(
+            final Connection connection, final QueryFilter<JsonPointer> queryFilter) {
+        if (queryFilter == null) {
+            return new BadRequestException(ERR_QUERY_BY_ID_OR_EXPRESSION_NOT_SUPPORTED.get().toString()).asPromise();
+        }
+        final PropertyMapper propertyMapper = resource.getPropertyMapper();
         final QueryFilterVisitor<Promise<Filter, ResourceException>, Void, JsonPointer> visitor =
                 new QueryFilterVisitor<Promise<Filter, ResourceException>, Void, JsonPointer>() {
-
                     @Override
-                    public Promise<Filter, ResourceException> visitAndFilter(final Void unused,
-                            final List<QueryFilter<JsonPointer>> subFilters) {
+                    public Promise<Filter, ResourceException> visitAndFilter(
+                            final Void unused, final List<QueryFilter<JsonPointer>> subFilters) {
                         final List<Promise<Filter, ResourceException>> promises = new ArrayList<>(subFilters.size());
                         for (final QueryFilter<JsonPointer> subFilter : subFilters) {
                             promises.add(subFilter.accept(this, unused));
                         }
 
-                        return Promises.when(promises).then(new Function<List<Filter>, Filter, ResourceException>() {
+                        return when(promises).then(new Function<List<Filter>, Filter, ResourceException>() {
                             @Override
                             public Filter apply(final List<Filter> value) {
                                 // Check for unmapped filter components and optimize.
@@ -434,56 +519,58 @@
                     @Override
                     public Promise<Filter, ResourceException> visitBooleanLiteralFilter(
                             final Void unused, final boolean value) {
-                        return Promises.newResultPromise(toFilter(value));
+                        return newResultPromise(toFilter(value));
                     }
 
                     @Override
                     public Promise<Filter, ResourceException> visitContainsFilter(
                             final Void unused, final JsonPointer field, final Object valueAssertion) {
                         return propertyMapper.getLdapFilter(
-                                connection, new JsonPointer(), field, FilterType.CONTAINS, null, valueAssertion);
+                                connection, resource, ROOT, field, CONTAINS, null, valueAssertion);
                     }
 
                     @Override
                     public Promise<Filter, ResourceException> visitEqualsFilter(
                             final Void unused, final JsonPointer field, final Object valueAssertion) {
                         return propertyMapper.getLdapFilter(
-                                connection, new JsonPointer(), field, FilterType.EQUAL_TO, null, valueAssertion);
+                                connection, resource, ROOT, field, EQUAL_TO, null, valueAssertion);
                     }
 
                     @Override
                     public Promise<Filter, ResourceException> visitExtendedMatchFilter(final Void unused,
-                            final JsonPointer field, final String operator, final Object valueAssertion) {
+                                                                                       final JsonPointer field,
+                                                                                       final String operator,
+                                                                                       final Object valueAssertion) {
                         return propertyMapper.getLdapFilter(
-                                connection, new JsonPointer(), field, FilterType.EXTENDED, operator, valueAssertion);
+                                connection, resource, ROOT, field, EXTENDED, operator, valueAssertion);
                     }
 
                     @Override
                     public Promise<Filter, ResourceException> visitGreaterThanFilter(
                             final Void unused, final JsonPointer field, final Object valueAssertion) {
                         return propertyMapper.getLdapFilter(
-                                connection, new JsonPointer(), field, FilterType.GREATER_THAN, null, valueAssertion);
+                                connection, resource, ROOT, field, GREATER_THAN, null, valueAssertion);
                     }
 
                     @Override
                     public Promise<Filter, ResourceException> visitGreaterThanOrEqualToFilter(
                             final Void unused, final JsonPointer field, final Object valueAssertion) {
-                        return propertyMapper.getLdapFilter(connection, new JsonPointer(), field,
-                                                            FilterType.GREATER_THAN_OR_EQUAL_TO, null, valueAssertion);
+                        return propertyMapper.getLdapFilter(
+                                connection, resource, ROOT, field, GREATER_THAN_OR_EQUAL_TO, null, valueAssertion);
                     }
 
                     @Override
                     public Promise<Filter, ResourceException> visitLessThanFilter(
                             final Void unused, final JsonPointer field, final Object valueAssertion) {
                         return propertyMapper.getLdapFilter(
-                                connection, new JsonPointer(), field, FilterType.LESS_THAN, null, valueAssertion);
+                                connection, resource, ROOT, field, LESS_THAN, null, valueAssertion);
                     }
 
                     @Override
                     public Promise<Filter, ResourceException> visitLessThanOrEqualToFilter(
                             final Void unused, final JsonPointer field, final Object valueAssertion) {
-                        return propertyMapper.getLdapFilter(connection, new JsonPointer(), field,
-                                                            FilterType.LESS_THAN_OR_EQUAL_TO, null, valueAssertion);
+                        return propertyMapper.getLdapFilter(
+                                connection, resource, ROOT, field, LESS_THAN_OR_EQUAL_TO, null, valueAssertion);
                     }
 
                     @Override
@@ -504,14 +591,14 @@
                     }
 
                     @Override
-                    public Promise<Filter, ResourceException> visitOrFilter(final Void unused,
-                            final List<QueryFilter<JsonPointer>> subFilters) {
+                    public Promise<Filter, ResourceException> visitOrFilter(
+                            final Void unused, final List<QueryFilter<JsonPointer>> subFilters) {
                         final List<Promise<Filter, ResourceException>> promises = new ArrayList<>(subFilters.size());
                         for (final QueryFilter<JsonPointer> subFilter : subFilters) {
                             promises.add(subFilter.accept(this, unused));
                         }
 
-                        return Promises.when(promises).then(new Function<List<Filter>, Filter, ResourceException>() {
+                        return when(promises).then(new Function<List<Filter>, Filter, ResourceException>() {
                             @Override
                             public Filter apply(final List<Filter> value) {
                                 // Check for unmapped filter components and optimize.
@@ -539,30 +626,25 @@
                     @Override
                     public Promise<Filter, ResourceException> visitPresentFilter(
                             final Void unused, final JsonPointer field) {
-                        return propertyMapper.getLdapFilter(
-                                connection, new JsonPointer(), field, FilterType.PRESENT, null, null);
+                        return propertyMapper.getLdapFilter(connection, resource, ROOT, field, PRESENT, null, null);
                     }
 
                     @Override
                     public Promise<Filter, ResourceException> visitStartsWithFilter(
                             final Void unused, final JsonPointer field, final Object valueAssertion) {
                         return propertyMapper.getLdapFilter(
-                                connection, new JsonPointer(), field, FilterType.STARTS_WITH, null, valueAssertion);
+                                connection, resource, ROOT, field, STARTS_WITH, null, valueAssertion);
                     }
-
                 };
         // Note that the returned LDAP filter may be null if it could not be mapped by any property mappers.
         return queryFilter.accept(visitor, null);
     }
 
-    private AsyncFunction<Filter, QueryResponse, ResourceException> runQuery(final QueryRequest request,
-            final QueryResourceHandler resourceHandler, final Connection connection) {
+    private AsyncFunction<Filter, QueryResponse, ResourceException> runQuery(
+            final QueryRequest request, final QueryResourceHandler resourceHandler, final Connection connection) {
         return new AsyncFunction<Filter, QueryResponse, ResourceException>() {
-            /**
-             * The following fields are guarded by sequenceLock. In addition,
-             * the sequenceLock ensures that we send one JSON resource at a time
-             * back to the client.
-             */
+            // The following fields are guarded by sequenceLock. In addition, the sequenceLock ensures that
+            // we send one JSON resource at a time back to the client.
             private final Object sequenceLock = new Object();
             private String cookie;
             private ResourceException pendingResult;
@@ -574,18 +656,17 @@
             public Promise<QueryResponse, ResourceException> apply(final Filter ldapFilter) {
                 if (ldapFilter == null || ldapFilter == alwaysFalse()) {
                     // Avoid performing a search if the filter could not be mapped or if it will never match.
-                    return Promises.newResultPromise(Responses.newQueryResponse());
+                    return newQueryResponse().asPromise();
                 }
                 final PromiseImpl<QueryResponse, ResourceException> promise = PromiseImpl.create();
                 // Perform the search.
-                final String[] attributes = getLdapAttributes(connection, request.getFields());
+                final String[] attributes = getLdapAttributesForUnknownType(request.getFields()).toArray(new String[0]);
                 final Filter searchFilter = ldapFilter == Filter.alwaysTrue() ? Filter.objectClassPresent()
-                                                                              : ldapFilter;
-                final SearchRequest searchRequest = newSearchRequest(
-                        getBaseDn(), SearchScope.SINGLE_LEVEL, searchFilter, attributes);
+                        : ldapFilter;
+                final SearchRequest searchRequest = newSearchRequest(baseDn, SINGLE_LEVEL, searchFilter, attributes);
 
-                // Add the page results control. We can support the page offset by
-                // reading the next offset pages, or offset x page size resources.
+                // Add the page results control. We can support the page offset by reading the next offset pages, or
+                // offset x page size resources.
                 final int pageResultStartIndex;
                 final int pageSize = request.getPageSize();
                 if (request.getPageSize() > 0) {
@@ -634,37 +715,39 @@
                          * The best solution is probably to process the primary search results in batches using
                          * the paged results control.
                          */
-                        final String id = namingStrategy.getResourceId(connection, entry);
+                        final String id = namingStrategy.decodeResourceId(entry);
                         final String revision = getRevisionFromEntry(entry);
-                        propertyMapper.read(connection, new JsonPointer(), entry)
+                        final Resource subType = resource.resolveSubTypeFromObjectClasses(entry);
+                        final PropertyMapper propertyMapper = subType.getPropertyMapper();
+                        propertyMapper.read(connection, subType, ROOT, entry)
                                       .thenOnResult(new ResultHandler<JsonValue>() {
-                                           @Override
-                                           public void handleResult(final JsonValue result) {
-                                               synchronized (sequenceLock) {
-                                                   pendingResourceCount--;
-                                                   if (!resultSent) {
-                                                       resourceHandler.handleResource(
-                                                               Responses.newResourceResponse(id, revision, result));
-                                                   }
-                                                   completeIfNecessary(promise);
-                                               }
-                                           }
-                                       }).thenOnException(new ExceptionHandler<ResourceException>() {
-                                           @Override
-                                           public void handleException(ResourceException exception) {
-                                               synchronized (sequenceLock) {
-                                                   pendingResourceCount--;
-                                                   completeIfNecessary(exception, promise);
-                                               }
-                                           }
-                                       });
+                                          @Override
+                                          public void handleResult(final JsonValue result) {
+                                              synchronized (sequenceLock) {
+                                                  pendingResourceCount--;
+                                                  if (!resultSent) {
+                                                      resourceHandler.handleResource(
+                                                              newResourceResponse(id, revision, result));
+                                                  }
+                                                  completeIfNecessary(promise);
+                                              }
+                                          }
+                                      })
+                                      .thenOnException(new ExceptionHandler<ResourceException>() {
+                                          @Override
+                                          public void handleException(ResourceException exception) {
+                                              synchronized (sequenceLock) {
+                                                  pendingResourceCount--;
+                                                  completeIfNecessary(exception, promise);
+                                              }
+                                          }
+                                      });
                         return true;
                     }
 
                     @Override
                     public boolean handleReference(final SearchResultReference reference) {
-                        // TODO: should this be classed as an error since
-                        // rest2ldap assumes entries are all colocated?
+                        // TODO: should this be classed as an error since rest2ldap assumes entries are all colocated?
                         return true;
                     }
 
@@ -675,7 +758,7 @@
                             if (request.getPageSize() > 0) {
                                 try {
                                     final SimplePagedResultsControl control =
-                                            result.getControl(SimplePagedResultsControl.DECODER, DECODE_OPTIONS);
+                                            result.getControl(SimplePagedResultsControl.DECODER, decodeOptions);
                                     if (control != null && !control.getCookie().isEmpty()) {
                                         cookie = control.getCookie().toBase64String();
                                     }
@@ -688,9 +771,14 @@
                     }
                 }).thenOnException(new ExceptionHandler<LdapException>() {
                     @Override
-                    public void handleException(LdapException exception) {
+                    public void handleException(final LdapException e) {
                         synchronized (sequenceLock) {
-                            completeIfNecessary(asResourceException(exception), promise);
+                            if (glueObjectClasses != null && e instanceof EntryNotFoundException) {
+                                // Glue entry does not exist, so treat this as an empty result set.
+                                completeIfNecessary(SUCCESS, promise);
+                            } else {
+                                completeIfNecessary(asResourceException(e), promise);
+                            }
                         }
                     }
                 });
@@ -715,7 +803,7 @@
             private void completeIfNecessary(final PromiseImpl<QueryResponse, ResourceException> handler) {
                 if (pendingResourceCount == 0 && pendingResult != null && !resultSent) {
                     if (pendingResult == SUCCESS) {
-                        handler.handleResult(Responses.newQueryResponse(cookie));
+                        handler.handleResult(newQueryResponse(cookie));
                     } else {
                         handler.handleException(pendingResult);
                     }
@@ -725,93 +813,81 @@
         };
     }
 
-    @Override
-    public Promise<ResourceResponse, ResourceException> readInstance(
+    Promise<ResourceResponse, ResourceException> read(
             final Context context, final String resourceId, final ReadRequest request) {
-        final Connection connection = context.asContext(AuthenticatedConnectionContext.class).getConnection();
-        // Do the search.
-        SearchRequest searchRequest = searchRequest(connection, resourceId, request.getFields());
-        return connection
-                .searchSingleEntryAsync(searchRequest)
-                .thenAsync(new AsyncFunction<SearchResultEntry, ResourceResponse, ResourceException>() {
-                    @Override
-                    public Promise<ResourceResponse, ResourceException> apply(SearchResultEntry entry)
-                            throws ResourceException {
-                        return adaptEntry(connection, entry);
-                    }
-                }, Exceptions.<ResourceResponse>toResourceException());
+        final Connection connection = connectionFrom(context);
+        return connection.searchSingleEntryAsync(searchRequestForUnknownType(resourceId, request.getFields()))
+                         .thenCatchAsync(adaptLdapException(SearchResultEntry.class))
+                         .thenAsync(new AsyncFunction<SearchResultEntry, ResourceResponse, ResourceException>() {
+                             @Override
+                             public Promise<ResourceResponse, ResourceException> apply(SearchResultEntry entry) {
+                                 final Resource subType = resource.resolveSubTypeFromObjectClasses(entry);
+                                 return encodeResourceResponse(connection, subType, entry);
+                             }
+                         });
     }
 
-    @Override
-    public Promise<ResourceResponse, ResourceException> updateInstance(
+    Promise<ResourceResponse, ResourceException> update(
             final Context context, final String resourceId, final UpdateRequest request) {
-        final Connection connection = context.asContext(AuthenticatedConnectionContext.class).getConnection();
-        List<JsonPointer> attrs = Collections.emptyList();
-        SearchRequest searchRequest = searchRequest(connection, resourceId, attrs);
+        final Connection connection = connectionFrom(context);
+        final AtomicReference<Entry> entryHolder = new AtomicReference<>();
+        final AtomicReference<Resource> subTypeHolder = new AtomicReference<>();
         return connection
-                .searchSingleEntryAsync(searchRequest)
-                .thenAsync(new AsyncFunction<SearchResultEntry, ResourceResponse, ResourceException>() {
+                .searchSingleEntryAsync(searchRequestForUnknownType(resourceId, Collections.<JsonPointer>emptyList()))
+                .thenCatchAsync(adaptLdapException(SearchResultEntry.class))
+                .thenAsync(new AsyncFunction<SearchResultEntry, List<Modification>, ResourceException>() {
                     @Override
-                    public Promise<ResourceResponse, ResourceException> apply(
-                            final SearchResultEntry entry) {
-                        try {
-                            // Fail-fast if there is a version mismatch.
-                            ensureMvccVersionMatches(entry, request.getRevision());
+                    public Promise<List<Modification>, ResourceException> apply(final SearchResultEntry entry)
+                            throws ResourceException {
+                        entryHolder.set(entry);
 
-                            // Create the modify request.
-                            final ModifyRequest modifyRequest = newModifyRequest(entry.getName());
-                            if (config.readOnUpdatePolicy() == CONTROLS) {
-                                final String[] attributes =
-                                        getLdapAttributes(connection, request.getFields());
-                                modifyRequest.addControl(
-                                        PostReadRequestControl.newControl(false, attributes));
-                            }
-                            if (config.usePermissiveModify()) {
-                                modifyRequest.addControl(
-                                        PermissiveModifyRequestControl.newControl(true));
-                            }
-                            addAssertionControl(modifyRequest, request.getRevision());
+                        // Fail-fast if there is a version mismatch.
+                        ensureMvccVersionMatches(entry, request.getRevision());
 
-                            // Determine the set of changes that need to be performed.
-                            return propertyMapper.update(
-                                    connection, new JsonPointer(), entry, request.getContent())
-                                                 .thenAsync(new AsyncFunction<
-                                            List<Modification>, ResourceResponse, ResourceException>() {
-                                        @Override
-                                        public Promise<ResourceResponse, ResourceException> apply(
-                                                List<Modification> modifications)
-                                                throws ResourceException {
-                                            if (modifications.isEmpty()) {
-                                                // No changes to be performed so just return
-                                                // the entry that we read.
-                                                return adaptEntry(connection, entry);
-                                            }
-                                            // Perform the modify operation.
-                                            modifyRequest.getModifications().addAll(modifications);
-                                            return connection
-                                                    .applyChangeAsync(modifyRequest)
-                                                    .thenAsync(
-                                                            postUpdateResultAsyncFunction(connection),
-                                                            Exceptions.<ResourceResponse>toResourceException());
-                                        }
-                                    });
-                        } catch (final Exception e) {
-                            return Promises.newExceptionPromise(asResourceException(e));
-                        }
+                        // Determine the type of resource and set of changes that need to be performed.
+                        final Resource subType = resource.resolveSubTypeFromObjectClasses(entry);
+                        subTypeHolder.set(subType);
+                        final PropertyMapper propertyMapper = subType.getPropertyMapper();
+                        return propertyMapper.update(connection, subType , ROOT, entry, request.getContent());
                     }
-                }, Exceptions.<ResourceResponse>toResourceException());
+                }).thenAsync(new AsyncFunction<List<Modification>, ResourceResponse, ResourceException>() {
+                    @Override
+                    public Promise<ResourceResponse, ResourceException> apply(List<Modification> modifications)
+                            throws ResourceException {
+                        final Resource subType = subTypeHolder.get();
+                        if (modifications.isEmpty()) {
+                            // No changes to be performed so just return the entry that we read.
+                            return encodeResourceResponse(connection, subType, entryHolder.get());
+                        }
+                        // Perform the modify operation.
+                        final ModifyRequest modifyRequest = newModifyRequest(entryHolder.get().getName());
+                        if (readOnUpdatePolicy == CONTROLS) {
+                            final Set<String> attributes = getLdapAttributesForKnownType(request.getFields(), subType);
+                            modifyRequest.addControl(PostReadRequestControl.newControl(false, attributes));
+                        }
+                        if (usePermissiveModify) {
+                            modifyRequest.addControl(PermissiveModifyRequestControl.newControl(true));
+                        }
+                        addAssertionControl(modifyRequest, request.getRevision());
+                        modifyRequest.getModifications().addAll(modifications);
+                        return connection.applyChangeAsync(modifyRequest)
+                                         .thenAsync(encodeUpdateResourceResponse(connection, subType),
+                                                    adaptLdapException(ResourceResponse.class));
+                    }
+                });
     }
 
-    private Promise<ResourceResponse, ResourceException> adaptEntry(final Connection connection, final Entry entry) {
-        final String actualResourceId = namingStrategy.getResourceId(connection, entry);
-        final String revision = getRevisionFromEntry(entry);
-        return propertyMapper.read(connection, new JsonPointer(), entry)
+    private Promise<ResourceResponse, ResourceException> encodeResourceResponse(
+            final Connection connection, final Resource resource, final Entry entry) {
+        final PropertyMapper propertyMapper = resource.getPropertyMapper();
+        return propertyMapper.read(connection, resource, ROOT, entry)
                              .then(new Function<JsonValue, ResourceResponse, ResourceException>() {
-                                  @Override
-                                  public ResourceResponse apply(final JsonValue value) {
-                                      return newResourceResponse(
-                                              actualResourceId, revision, new JsonValue(value));
-                                  }
+                                 @Override
+                                 public ResourceResponse apply(final JsonValue value) {
+                                     final String revision = getRevisionFromEntry(entry);
+                                     final String actualResourceId = namingStrategy.decodeResourceId(entry);
+                                     return newResourceResponse(actualResourceId, revision, new JsonValue(value));
+                                 }
                              });
     }
 
@@ -819,40 +895,34 @@
             throws ResourceException {
         if (expectedRevision != null) {
             ensureMvccSupported();
-            request.addControl(AssertionRequestControl.newControl(true, Filter.equality(
-                    etagAttribute.toString(), expectedRevision)));
+            final Filter filter = Filter.equality(etagAttribute.toString(), expectedRevision);
+            request.addControl(AssertionRequestControl.newControl(true, filter));
         }
     }
 
-    private Promise<DN, ResourceException> doUpdateFunction(final Connection connection, final String resourceId,
-            final String revision) {
-        final String ldapAttribute = (etagAttribute != null && revision != null) ? etagAttribute.toString() : "1.1";
-        final SearchRequest searchRequest = namingStrategy.createSearchRequest(connection, getBaseDn(), resourceId)
-                                                          .addAttribute(ldapAttribute);
-        if (searchRequest.getScope().equals(SearchScope.BASE_OBJECT)) {
-            // There's no point in doing a search because we already know the DN.
-            return Promises.newResultPromise(searchRequest.getName());
+    private Promise<RoutingContext, ResourceException> resolveResourceDnAndType(
+            final Context context, final Connection connection, final String resourceId, final String revision) {
+        final SearchRequest searchRequest = namingStrategy.createSearchRequest(baseDn, resourceId);
+        if (searchRequest.getScope().equals(BASE_OBJECT) && !resource.hasSubTypes()) {
+            // There's no point in doing a search because we already know the DN and sub-resources.
+            return newResultPromise(new RoutingContext(context, searchRequest.getName(), resource));
         }
-        return connection
-                .searchSingleEntryAsync(searchRequest)
-                .thenAsync(new AsyncFunction<SearchResultEntry, DN, ResourceException>() {
-                    @Override
-                    public Promise<DN, ResourceException> apply(SearchResultEntry entry) throws ResourceException {
-                        try {
-                            // Fail-fast if there is a version mismatch.
-                            ensureMvccVersionMatches(entry, revision);
-                            // Perform update operation.
-                            return Promises.newResultPromise(entry.getName());
-                        } catch (final Exception e) {
-                            return Promises.newExceptionPromise(asResourceException(e));
-                        }
-                    }
-                }, new AsyncFunction<LdapException, DN, ResourceException>() {
-                    @Override
-                    public Promise<DN, ResourceException> apply(LdapException ldapException) throws ResourceException {
-                        return Promises.newExceptionPromise(asResourceException(ldapException));
-                    }
-                });
+        if (etagAttribute != null && revision != null) {
+            searchRequest.addAttribute(etagAttribute.toString());
+        }
+        // The resource type will be resolved from the LDAP entry's objectClass.
+        searchRequest.addAttribute("objectClass");
+        return connection.searchSingleEntryAsync(searchRequest)
+                         .thenAsync(new AsyncFunction<SearchResultEntry, RoutingContext, ResourceException>() {
+                             @Override
+                             public Promise<RoutingContext, ResourceException> apply(final SearchResultEntry entry)
+                                     throws ResourceException {
+                                 // Fail-fast if there is a version mismatch.
+                                 ensureMvccVersionMatches(entry, revision);
+                                 final Resource subType = resource.resolveSubTypeFromObjectClasses(entry);
+                                 return newResultPromise(new RoutingContext(context, entry.getName(), subType));
+                             }
+                         }, adaptLdapException(RoutingContext.class));
     }
 
     private void ensureMvccSupported() throws NotSupportedException {
@@ -874,101 +944,92 @@
         }
     }
 
-    private DN getBaseDn() {
-        return baseDn;
+    private Set<String> getLdapAttributesForUnknownType(final Collection<JsonPointer> fields) {
+        final Set<String> ldapAttributes = getLdapAttributesForKnownType(fields, resource);
+        getLdapAttributesForUnknownType(fields, resource, ldapAttributes);
+        return ldapAttributes;
     }
 
-    /**
-     * Determines the set of LDAP attributes to request in an LDAP read (search,
-     * post-read), based on the provided list of JSON pointers.
-     *
-     * @param connection
-     *          The request state.
-     * @param requestedAttributes
-     *          The list of resource attributes to be read.
-     * @return The set of LDAP attributes associated with the resource
-     *         attributes.
-     */
-    private String[] getLdapAttributes(final Connection connection, final Collection<JsonPointer> requestedAttributes) {
-        // Get all the LDAP attributes required by the property mappers.
-        final Set<String> requestedLDAPAttributes;
-        if (requestedAttributes.isEmpty()) {
+    private void getLdapAttributesForUnknownType(final Collection<JsonPointer> fields, final Resource resource,
+                                                 final Set<String> ldapAttributes) {
+        for (final Resource subType : resource.getSubTypes()) {
+            addLdapAttributesForFields(fields, subType, ldapAttributes);
+            getLdapAttributesForUnknownType(fields, subType, ldapAttributes);
+        }
+    }
+
+    private Set<String> getLdapAttributesForKnownType(final Collection<JsonPointer> fields, final Resource resource) {
+        // Includes the LDAP attributes required by the type, etag, and name strategies.
+        final Set<String> ldapAttributes = new LinkedHashSet<>();
+        ldapAttributes.add("objectClass");
+        final String resourceIdLdapAttribute = namingStrategy.getResourceIdLdapAttribute();
+        if (resourceIdLdapAttribute != null) {
+            ldapAttributes.add(resourceIdLdapAttribute);
+        }
+        if (etagAttribute != null) {
+            ldapAttributes.add(etagAttribute.toString());
+        }
+        addLdapAttributesForFields(fields, resource, ldapAttributes);
+        return ldapAttributes;
+    }
+
+    /** Includes the LDAP attributes required for the specified JSON fields for all sub-types. */
+    private void addLdapAttributesForFields(final Collection<JsonPointer> fields, final Resource resource,
+                                            final Set<String> ldapAttributes) {
+        final PropertyMapper propertyMapper = resource.getPropertyMapper();
+        if (fields.isEmpty()) {
             // Full read.
-            requestedLDAPAttributes = new LinkedHashSet<>();
-            propertyMapper.getLdapAttributes(connection, new JsonPointer(), new JsonPointer(),
-                                             requestedLDAPAttributes);
+            propertyMapper.getLdapAttributes(ROOT, ROOT, ldapAttributes);
         } else {
             // Partial read.
-            requestedLDAPAttributes = new LinkedHashSet<>(requestedAttributes.size());
-            for (final JsonPointer requestedAttribute : requestedAttributes) {
-                propertyMapper.getLdapAttributes(connection, new JsonPointer(), requestedAttribute,
-                                                 requestedLDAPAttributes);
+            for (final JsonPointer field : fields) {
+                propertyMapper.getLdapAttributes(ROOT, field, ldapAttributes);
             }
         }
-
-        // Get the LDAP attributes required by the Etag and name stategies.
-        namingStrategy.getLdapAttributes(connection, requestedLDAPAttributes);
-        if (etagAttribute != null) {
-            requestedLDAPAttributes.add(etagAttribute.toString());
-        }
-        return requestedLDAPAttributes.toArray(new String[requestedLDAPAttributes.size()]);
     }
 
     private String getRevisionFromEntry(final Entry entry) {
         return etagAttribute != null ? entry.parseAttribute(etagAttribute).asString() : null;
     }
 
-    private AsyncFunction<Result, ResourceResponse, ResourceException> postUpdateResultAsyncFunction(
-            final Connection connection) {
-        // The handler which will be invoked for the LDAP add result.
+    private AsyncFunction<Result, ResourceResponse, ResourceException> encodeUpdateResourceResponse(
+            final Connection connection, final Resource resource) {
         return new AsyncFunction<Result, ResourceResponse, ResourceException>() {
             @Override
-            public Promise<ResourceResponse, ResourceException> apply(Result result) throws ResourceException {
+            public Promise<ResourceResponse, ResourceException> apply(Result result) {
                 // FIXME: handle USE_SEARCH policy.
-                Entry entry;
                 try {
                     final PostReadResponseControl postReadControl =
-                        result.getControl(PostReadResponseControl.DECODER, config.decodeOptions());
+                            result.getControl(PostReadResponseControl.DECODER, decodeOptions);
                     if (postReadControl != null) {
-                        entry = postReadControl.getEntry();
-                    } else {
-                        final PreReadResponseControl preReadControl =
-                            result.getControl(PreReadResponseControl.DECODER, config.decodeOptions());
-                        if (preReadControl != null) {
-                            entry = preReadControl.getEntry();
-                        } else {
-                            entry = null;
-                        }
+                        return encodeResourceResponse(connection, resource, postReadControl.getEntry());
+                    }
+                    final PreReadResponseControl preReadControl =
+                            result.getControl(PreReadResponseControl.DECODER, decodeOptions);
+                    if (preReadControl != null) {
+                        return encodeResourceResponse(connection, resource, preReadControl.getEntry());
                     }
                 } catch (final DecodeException e) {
                     logger.error(ERR_DECODING_CONTROL.get(e.getLocalizedMessage()), e);
-                    entry = null;
                 }
-                if (entry != null) {
-                    return adaptEntry(connection, entry);
-                } else {
-                    return Promises.newResultPromise(
-                            newResourceResponse(null, null, new JsonValue(Collections.emptyMap())));
-                }
+                // Return an empty resource response.
+                return newResourceResponse(null, null, new JsonValue(Collections.emptyMap())).asPromise();
             }
         };
     }
 
-    private SearchRequest searchRequest(
-            final Connection connection, final String resourceId, final List<JsonPointer> requestedAttributes) {
-        final String[] attributes = getLdapAttributes(connection, requestedAttributes);
-        return namingStrategy.createSearchRequest(connection, getBaseDn(), resourceId).addAttribute(attributes);
+    private SearchRequest searchRequestForUnknownType(final String resourceId, final List<JsonPointer> fields) {
+        final String[] attributes = getLdapAttributesForUnknownType(fields).toArray(new String[0]);
+        return namingStrategy.createSearchRequest(baseDn, resourceId).addAttribute(attributes);
     }
 
-    private static final class Exceptions {
-        private static <R> AsyncFunction<LdapException, R, ResourceException> toResourceException() {
-            // The handler which will be invoked for the LDAP add result.
-            return new AsyncFunction<LdapException, R, ResourceException>() {
-                @Override
-                public Promise<R, ResourceException> apply(final LdapException ldapException) throws ResourceException {
-                    return Promises.newExceptionPromise(asResourceException(ldapException));
-                }
-            };
-        }
+    @SuppressWarnings("unused")
+    private static <R> AsyncFunction<LdapException, R, ResourceException> adaptLdapException(final Class<R> clazz) {
+        return new AsyncFunction<LdapException, R, ResourceException>() {
+            @Override
+            public Promise<R, ResourceException> apply(final LdapException ldapException) {
+                return asResourceException(ldapException).asPromise();
+            }
+        };
     }
 }

--
Gitblit v1.10.0