From 1df4f51adf614210ca4a9b9728327090ec5ea264 Mon Sep 17 00:00:00 2001
From: Gaetan Boismal <gaetan.boismal@forgerock.com>
Date: Fri, 11 Sep 2015 20:33:53 +0000
Subject: [PATCH] OPENDJ-1666 PR-19 CREST-3.0.0 Migration

---
 opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java | 1537 +++++++++++++++++++++++++++-------------------------------
 1 files changed, 711 insertions(+), 826 deletions(-)

diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java
index 264918a..15d7996 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java
@@ -15,6 +15,18 @@
  */
 package org.forgerock.opendj.rest2ldap;
 
+import static java.util.Arrays.asList;
+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.rest2ldap.ReadOnUpdatePolicy.CONTROLS;
+import static org.forgerock.opendj.rest2ldap.Rest2LDAP.asResourceException;
+import static org.forgerock.opendj.rest2ldap.Utils.i18n;
+import static org.forgerock.opendj.rest2ldap.Utils.toFilter;
+
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -22,10 +34,13 @@
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Set;
+import java.util.concurrent.atomic.AtomicReference;
 
-import org.forgerock.json.fluent.JsonPointer;
-import org.forgerock.json.fluent.JsonValue;
+import org.forgerock.http.Context;
+import org.forgerock.json.JsonPointer;
+import org.forgerock.json.JsonValue;
 import org.forgerock.json.resource.ActionRequest;
+import org.forgerock.json.resource.ActionResponse;
 import org.forgerock.json.resource.CollectionResourceProvider;
 import org.forgerock.json.resource.CreateRequest;
 import org.forgerock.json.resource.DeleteRequest;
@@ -33,27 +48,25 @@
 import org.forgerock.json.resource.PatchOperation;
 import org.forgerock.json.resource.PatchRequest;
 import org.forgerock.json.resource.PreconditionFailedException;
-import org.forgerock.json.resource.QueryFilter;
-import org.forgerock.json.resource.QueryFilterVisitor;
 import org.forgerock.json.resource.QueryRequest;
-import org.forgerock.json.resource.QueryResult;
-import org.forgerock.json.resource.QueryResultHandler;
+import org.forgerock.json.resource.QueryResourceHandler;
+import org.forgerock.json.resource.QueryResponse;
 import org.forgerock.json.resource.ReadRequest;
-import org.forgerock.json.resource.Resource;
 import org.forgerock.json.resource.ResourceException;
-import org.forgerock.json.resource.ResultHandler;
-import org.forgerock.json.resource.ServerContext;
+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;
 import org.forgerock.opendj.ldap.AttributeDescription;
 import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.Connection;
 import org.forgerock.opendj.ldap.DN;
 import org.forgerock.opendj.ldap.DecodeException;
 import org.forgerock.opendj.ldap.DecodeOptions;
 import org.forgerock.opendj.ldap.Entry;
-import org.forgerock.opendj.ldap.LdapException;
 import org.forgerock.opendj.ldap.Filter;
+import org.forgerock.opendj.ldap.LdapException;
 import org.forgerock.opendj.ldap.Modification;
 import org.forgerock.opendj.ldap.SearchResultHandler;
 import org.forgerock.opendj.ldap.SearchScope;
@@ -72,45 +85,21 @@
 import org.forgerock.opendj.ldap.responses.SearchResultEntry;
 import org.forgerock.opendj.ldap.responses.SearchResultReference;
 import org.forgerock.opendj.ldif.ChangeRecord;
-import org.forgerock.util.promise.ExceptionHandler;
+import org.forgerock.util.AsyncFunction;
 import org.forgerock.util.Function;
-import org.forgerock.util.promise.NeverThrowsException;
+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 static java.util.Arrays.*;
-
-import static org.forgerock.opendj.ldap.Filter.*;
-import static org.forgerock.opendj.ldap.requests.Requests.*;
-import static org.forgerock.opendj.rest2ldap.ReadOnUpdatePolicy.*;
-import static org.forgerock.opendj.rest2ldap.Rest2LDAP.*;
-import static org.forgerock.opendj.rest2ldap.Utils.*;
+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 LDAPCollectionResourceProvider implements CollectionResourceProvider {
-    private static class ResultHandlerFromPromise<T> implements ResultHandler<T> {
-        private final PromiseImpl<T, ResourceException> promise;
-
-        ResultHandlerFromPromise() {
-            promise = PromiseImpl.create();
-        }
-
-        @Override
-        public void handleError(ResourceException error) {
-            promise.handleException(error);
-
-        }
-
-        @Override
-        public void handleResult(T result) {
-            promise.handleResult(result);
-        }
-    }
-
     /** Dummy exception used for signalling search success. */
     private static final ResourceException SUCCESS = new UncategorizedException(0, null, null);
 
@@ -136,511 +125,663 @@
     }
 
     @Override
-    public void actionCollection(final ServerContext context, final ActionRequest request,
-            final ResultHandler<JsonValue> handler) {
-        handler.handleError(new NotSupportedException("Not yet implemented"));
+    public Promise<ActionResponse, ResourceException> actionCollection(
+            final Context context, final ActionRequest request) {
+        return Promises.<ActionResponse, ResourceException> newExceptionPromise(
+                                                            new NotSupportedException("Not yet implemented"));
     }
 
     @Override
-    public void actionInstance(final ServerContext context, final String resourceId,
-            final ActionRequest request, final ResultHandler<JsonValue> handler) {
-        handler.handleError(new NotSupportedException("Not yet implemented"));
+    public Promise<ActionResponse, ResourceException> actionInstance(
+            final Context context, final String resourceId, final ActionRequest request) {
+        return Promises.<ActionResponse, ResourceException> newExceptionPromise(
+                                                            new NotSupportedException("Not yet implemented"));
     }
 
     @Override
-    public void createInstance(final ServerContext context, final CreateRequest request,
-            final ResultHandler<Resource> handler) {
-        final Context c = wrap(context);
-        final ResultHandler<Resource> h = wrap(c, handler);
+    public Promise<ResourceResponse, ResourceException> createInstance(
+            final Context context, final CreateRequest request) {
+        final RequestState requestState = wrap(context);
 
-        // Get the connection, then determine entry content, then perform add.
-        c.run(h, new Runnable() {
-            @Override
-            public void run() {
-                // Calculate entry content.
-                attributeMapper.create(c, new JsonPointer(), request.getContent(),
-                    new ResultHandler<List<Attribute>>() {
-                        @Override
-                        public void handleError(final ResourceException error) {
-                            h.handleError(error);
-                        }
-
-                        @Override
-                        public void handleResult(final List<Attribute> result) {
-                            // Perform add operation.
-                            final AddRequest addRequest = newAddRequest(DN.rootDN());
-                            for (final Attribute attribute : additionalLDAPAttributes) {
-                                addRequest.addAttribute(attribute);
-                            }
-                            for (final Attribute attribute : result) {
-                                addRequest.addAttribute(attribute);
-                            }
-                            try {
-                                nameStrategy.setResourceId(c, getBaseDN(c), request.getNewResourceId(), addRequest);
-                            } catch (final ResourceException e) {
-                                h.handleError(e);
-                                return;
-                            }
-                            if (config.readOnUpdatePolicy() == CONTROLS) {
-                                final String[] attributes = getLDAPAttributes(c, request.getFields());
-                                addRequest.addControl(PostReadRequestControl.newControl(false, attributes));
-                            }
-                            c.getConnection().applyChangeAsync(addRequest)
-                                        .thenOnResult(postUpdateResultHandler(c, h))
-                                        .thenOnException(postUpdateExceptionHandler(h));
-                        }
-                    });
-            }
-        });
-    }
-
-    @Override
-    public void deleteInstance(final ServerContext context, final String resourceId,
-            final DeleteRequest request, final ResultHandler<Resource> handler) {
-        final Context c = wrap(context);
-        final ResultHandler<Resource> h = wrap(c, handler);
-
-        // Get connection, search if needed, then delete.
-        c.run(h, doUpdate(c, resourceId, request.getRevision(), new ResultHandler<DN>() {
-            @Override
-            public void handleError(final ResourceException error) {
-                h.handleError(error);
-            }
-
-            @Override
-            public void handleResult(final DN dn) {
-                try {
-                    final ChangeRecord deleteRequest = newDeleteRequest(dn);
-                    if (config.readOnUpdatePolicy() == CONTROLS) {
-                        final String[] attributes = getLDAPAttributes(c, request.getFields());
-                        deleteRequest.addControl(PreReadRequestControl.newControl(false, attributes));
-                    }
-                    if (config.useSubtreeDelete()) {
-                        deleteRequest.addControl(SubtreeDeleteRequestControl.newControl(true));
-                    }
-                    addAssertionControl(deleteRequest, request.getRevision());
-                    c.getConnection().applyChangeAsync(deleteRequest).thenOnResult(postUpdateResultHandler(c, h))
-                                                                     .thenOnException(postUpdateExceptionHandler(h));
-                } catch (final Exception e) {
-                    h.handleError(asResourceException(e));
-                }
-            }
-        }));
-    }
-
-    @Override
-    public void patchInstance(final ServerContext context, final String resourceId, final PatchRequest request,
-            final ResultHandler<Resource> handler) {
-        final Context c = wrap(context);
-        final ResultHandler<Resource> h = wrap(c, handler);
-
-        if (request.getPatchOperations().isEmpty()) {
-            /*
-             * This patch is a no-op so just read the entry and check its version.
-             */
-            c.run(h, new Runnable() {
+        return requestState.getConnection().thenAsync(
+            new AsyncFunction<Connection, ResourceResponse, ResourceException>() {
                 @Override
-                public void run() {
-                    final String[] attributes = getLDAPAttributes(c, request.getFields());
-                    final SearchRequest searchRequest = nameStrategy.createSearchRequest(c, getBaseDN(c), resourceId)
-                            .addAttribute(attributes);
-                    c.getConnection().searchSingleEntryAsync(searchRequest)
-                            .thenOnResult(postEmptyPatchResultHandler(c, request, h))
-                            .thenOnException(postEmptyPatchExceptionHandler(h));
-                }
-            });
-        } else {
-            /*
-             * Get the connection, search if needed, then determine modifications, then perform modify.
-             */
-            c.run(h, doUpdate(c, resourceId, request.getRevision(), new ResultHandler<DN>() {
-                @Override
-                public void handleError(final ResourceException error) {
-                    h.handleError(error);
-                }
-
-                @Override
-                public void handleResult(final DN dn) {
-                    // Convert the patch operations to LDAP modifications.
-                    List<Promise<List<Modification>, ResourceException>> promises =
-                            new ArrayList<>(request.getPatchOperations().size());
-                    for (final PatchOperation operation : request.getPatchOperations()) {
-                        final ResultHandlerFromPromise<List<Modification>> handler = new ResultHandlerFromPromise<>();
-                        attributeMapper.patch(c, new JsonPointer(), operation, handler);
-                        promises.add(handler.promise);
-                    }
-
-                    Promises.when(promises).thenOnResult(
-                        new org.forgerock.util.promise.ResultHandler<List<List<Modification>>>() {
-                            @Override
-                            public void handleResult(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(c, request.getFields()));
-                                    if (modifyRequest.getModifications().isEmpty()) {
-                                        /*
-                                         * This patch is a no-op so just read the entry and check its version.
-                                         */
-                                        c.getConnection().readEntryAsync(dn, attributes)
-                                                .thenOnResult(postEmptyPatchResultHandler(c, request, h))
-                                                .thenOnException(postEmptyPatchExceptionHandler(h));
-                                    } 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());
-                                        c.getConnection().applyChangeAsync(modifyRequest)
-                                                .thenOnResult(postUpdateResultHandler(c, h))
-                                                .thenOnException(postUpdateExceptionHandler(h));
-                                    }
-                                } catch (final Exception e) {
-                                    h.handleError(asResourceException(e));
-                                }
-                            }
-                        }).thenOnException(new ExceptionHandler<ResourceException>() {
-                            @Override
-                            public void handleException(ResourceException exception) {
-                                h.handleError(asResourceException(exception));
-                            }
-                        });
-                }
-            }));
-        }
-    }
-
-    @Override
-    public void queryCollection(final ServerContext context, final QueryRequest request,
-            final QueryResultHandler handler) {
-        final Context c = wrap(context);
-        final QueryResultHandler h = wrap(c, handler);
-
-        /*
-         * Get the connection, then calculate the search filter, then perform the search.
-         */
-        c.run(h, new Runnable() {
-            @Override
-            public void run() {
-                // Calculate the filter (this may require the connection).
-                getLDAPFilter(c, request.getQueryFilter(), new ResultHandler<Filter>() {
-                    /**
-                     * 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;
-                    private int pendingResourceCount;
-                    private boolean resultSent;
-                    private int totalResourceCount;
-
-                    @Override
-                    public void handleError(final ResourceException error) {
-                        h.handleError(error);
-                    }
-
-                    @Override
-                    public void handleResult(final Filter ldapFilter) {
-                        /*
-                         * Avoid performing a search if the filter could not be mapped or if it will never match.
-                         */
-                        if (ldapFilter == null || ldapFilter == alwaysFalse()) {
-                            h.handleResult(new QueryResult());
-                        } else {
-                            // Perform the search.
-                            final String[] attributes = getLDAPAttributes(c, request.getFields());
-                            final Filter searchFilter =
-                                    ldapFilter == Filter.alwaysTrue() ? Filter.objectClassPresent() : ldapFilter;
-                            final SearchRequest searchRequest =
-                                    newSearchRequest(getBaseDN(c), SearchScope.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.
-                             */
-                            final int pageResultStartIndex;
-                            final int pageSize = request.getPageSize();
-                            if (request.getPageSize() > 0) {
-                                final int pageResultEndIndex;
-                                if (request.getPagedResultsOffset() > 0) {
-                                    pageResultStartIndex = request.getPagedResultsOffset() * pageSize;
-                                    pageResultEndIndex = pageResultStartIndex + pageSize;
-                                } else {
-                                    pageResultStartIndex = 0;
-                                    pageResultEndIndex = pageSize;
-                                }
-                                final ByteString cookie =
-                                        request.getPagedResultsCookie() != null ? ByteString
-                                                .valueOfBase64(request.getPagedResultsCookie())
-                                                : ByteString.empty();
-                                final SimplePagedResultsControl control =
-                                        SimplePagedResultsControl.newControl(true, pageResultEndIndex, cookie);
-                                searchRequest.addControl(control);
-                            } else {
-                                pageResultStartIndex = 0;
-                            }
-
-                            c.getConnection().searchAsync(searchRequest, new SearchResultHandler() {
+                public Promise<ResourceResponse, ResourceException> apply(final Connection connection)
+                        throws ResourceException {
+                    // Calculate entry content.
+                    return attributeMapper.create(requestState, new JsonPointer(), request.getContent())
+                            .thenAsync(new AsyncFunction<List<Attribute>, ResourceResponse, ResourceException>() {
                                 @Override
-                                public boolean handleEntry(final SearchResultEntry entry) {
-                                    /*
-                                     * Search result entries will be returned before the search result/error so the
-                                     * only reason pendingResult will be non-null is if a mapping error has occurred.
-                                     */
-                                    synchronized (sequenceLock) {
-                                        if (pendingResult != null) {
-                                            return false;
-                                        }
-                                        if (totalResourceCount++ < pageResultStartIndex) {
-                                            // Haven't reached paged results threshold yet.
-                                            return true;
-                                        }
-                                        pendingResourceCount++;
+                                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);
                                     }
-
-                                    /*
-                                     * FIXME: secondary asynchronous searches will complete in a non-deterministic
-                                     * order and may cause the JSON resources to be returned in a different order to the
-                                     * order in which the primary LDAP search results were received. This is benign at
-                                     * the moment, but will need resolving when we implement server side sorting. A
-                                     * possible fix will be to use a queue of pending resources (promises?). However,
-                                     * the queue cannot be unbounded in case it grows very large, but it cannot be
-                                     * bounded either since that could cause a deadlock between rest2ldap and the LDAP
-                                     * server (imagine the case where the server has a single worker thread which is
-                                     * occupied processing the primary search).
-                                     * The best solution is probably to process the primary search results in batches
-                                     * using the paged results control.
-                                     */
-                                    final String id = nameStrategy.getResourceId(c, entry);
-                                    final String revision = getRevisionFromEntry(entry);
-                                    attributeMapper.read(c, new JsonPointer(), entry, new ResultHandler<JsonValue>() {
-                                        @Override
-                                        public void handleError(final ResourceException e) {
-                                            synchronized (sequenceLock) {
-                                                pendingResourceCount--;
-                                                completeIfNecessary(e);
-                                            }
-                                        }
-
-                                        @Override
-                                        public void handleResult(final JsonValue result) {
-                                            synchronized (sequenceLock) {
-                                                pendingResourceCount--;
-                                                if (!resultSent) {
-                                                    h.handleResource(new Resource(id, revision, result));
-                                                }
-                                                completeIfNecessary();
-                                            }
-                                        }
-                                    });
-                                    return true;
-                                }
-
-                                @Override
-                                public boolean handleReference(final SearchResultReference reference) {
-                                    // TODO: should this be classed as an error since rest2ldap
-                                    // assumes entries are all colocated?
-                                    return true;
-                                }
-
-                            }).thenOnResult(new org.forgerock.util.promise.ResultHandler<Result>() {
-                                @Override
-                                public void handleResult(Result result) {
-                                    synchronized (sequenceLock) {
-                                        if (request.getPageSize() > 0) {
-                                            try {
-                                                final SimplePagedResultsControl control =
-                                                    result.getControl(SimplePagedResultsControl.DECODER,
-                                                        DECODE_OPTIONS);
-                                                if (control != null && !control.getCookie().isEmpty()) {
-                                                    cookie = control.getCookie().toBase64String();
-                                                }
-                                            } catch (final DecodeException e) {
-                                                // FIXME: need some logging.
-                                            }
-                                        }
-                                        completeIfNecessary(SUCCESS);
+                                    for (final Attribute attribute : attributes) {
+                                        addRequest.addAttribute(attribute);
                                     }
-                                }
-                            }).thenOnException(new ExceptionHandler<LdapException>() {
-                                @Override
-                                public void handleException(LdapException exception) {
-                                    synchronized (sequenceLock) {
-                                        completeIfNecessary(asResourceException(exception));
+                                    try {
+                                        nameStrategy.setResourceId(requestState, getBaseDN(requestState),
+                                                request.getNewResourceId(), addRequest);
+                                    } catch (final ResourceException e) {
+                                        return Promises.newExceptionPromise(e);
                                     }
+                                    if (config.readOnUpdatePolicy() == CONTROLS) {
+                                        addRequest.addControl(PostReadRequestControl.newControl(
+                                                false, getLDAPAttributes(requestState, request.getFields())));
+                                    }
+                                    return connection.applyChangeAsync(addRequest)
+                                                     .thenAsync(postUpdateResultAsyncFunction(requestState),
+                                                                ldapExceptionToResourceException());
                                 }
                             });
-                        }
-                    }
+                }
+            }).thenFinally(close(requestState));
+    }
 
-                    /**
-                     * This method must be invoked with the sequenceLock held.
-                     */
-                    private void completeIfNecessary(final ResourceException e) {
-                        if (pendingResult == null) {
-                            pendingResult = e;
-                        }
-                        completeIfNecessary();
-                    }
-
-                    /**
-                     * Close out the query result set if there are no more
-                     * pending resources and the LDAP result has been received.
-                     * This method must be invoked with the sequenceLock held.
-                     */
-                    private void completeIfNecessary() {
-                        if (pendingResourceCount == 0 && pendingResult != null && !resultSent) {
-                            if (pendingResult == SUCCESS) {
-                                h.handleResult(new QueryResult(cookie, -1));
-                            } else {
-                                h.handleError(pendingResult);
+    @Override
+    public Promise<ResourceResponse, ResourceException> deleteInstance(
+            final Context context, final String resourceId, final DeleteRequest request) {
+        final RequestState requestState = wrap(context);
+        final AtomicReference<Connection> connectionHolder = new AtomicReference<>();
+        return requestState.getConnection()
+                .thenOnResult(saveConnection(connectionHolder))
+                .thenAsync(doUpdateFunction(requestState, 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(requestState, request.getFields());
+                                deleteRequest.addControl(PreReadRequestControl.newControl(false, attributes));
                             }
-                            resultSent = true;
+                            if (config.useSubtreeDelete()) {
+                                deleteRequest.addControl(SubtreeDeleteRequestControl.newControl(true));
+                            }
+                            addAssertionControl(deleteRequest, request.getRevision());
+                            return connectionHolder.get().applyChangeAsync(deleteRequest)
+                                                         .thenAsync(postUpdateResultAsyncFunction(requestState),
+                                                                    ldapExceptionToResourceException());
+
+                        } catch (final Exception e) {
+                            return Promises.newExceptionPromise((asResourceException(e)));
+                        }
+                    }
+                }).thenFinally(close(requestState));
+    }
+
+    @Override
+    public Promise<ResourceResponse, ResourceException> patchInstance(
+            final Context context, final String resourceId, final PatchRequest request) {
+        final RequestState requestState = wrap(context);
+
+        if (request.getPatchOperations().isEmpty()) {
+            return emptyPatchInstance(requestState, resourceId, request);
+        }
+
+        final AtomicReference<Connection> connectionHolder = new AtomicReference<>();
+        return requestState.getConnection()
+                .thenOnResult(saveConnection(connectionHolder))
+                .thenAsync(doUpdateFunction(requestState, resourceId, request.getRevision()))
+                .thenAsync(new AsyncFunction<DN, 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(attributeMapper.patch(requestState, new JsonPointer(), operation));
+                        }
+
+                        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(requestState, request.getFields()));
+                                            if (modifyRequest.getModifications().isEmpty()) {
+                                                // This patch is a no-op so just read the entry and check its version.
+                                                return connectionHolder.get()
+                                                        .readEntryAsync(dn, attributes)
+                                                        .thenAsync(postEmptyPatchAsyncFunction(requestState, request),
+                                                                   ldapExceptionToResourceException());
+                                            } 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 connectionHolder.get()
+                                                        .applyChangeAsync(modifyRequest)
+                                                        .thenAsync(postUpdateResultAsyncFunction(requestState),
+                                                                   ldapExceptionToResourceException());
+                                            }
+                                        } catch (final Exception e) {
+                                            return Promises.newExceptionPromise(asResourceException(e));
+                                        }
+                                    }
+                                });
+                    }
+                }).thenFinally(close(requestState));
+    }
+
+    /** Just read the entry and check its version. */
+    private Promise<ResourceResponse, ResourceException> emptyPatchInstance(
+            final RequestState requestState, final String resourceId, final PatchRequest request) {
+        return requestState.getConnection()
+                .thenAsync(new AsyncFunction<Connection, ResourceResponse, ResourceException>() {
+                    @Override
+                    public Promise<ResourceResponse, ResourceException> apply(final Connection connection)
+                            throws ResourceException {
+                        final String[] attributes = getLDAPAttributes(requestState, request.getFields());
+                        final SearchRequest searchRequest =
+                                nameStrategy.createSearchRequest(requestState, getBaseDN(requestState), resourceId)
+                                            .addAttribute(attributes);
+                        return connection.searchSingleEntryAsync(searchRequest)
+                                         .thenAsync(postEmptyPatchAsyncFunction(requestState, request),
+                                                    ldapExceptionToResourceException());
+                    }
+                });
+    }
+
+    private AsyncFunction<SearchResultEntry, ResourceResponse, ResourceException> postEmptyPatchAsyncFunction(
+            final RequestState requestState, final PatchRequest request) {
+        return new AsyncFunction<SearchResultEntry, ResourceResponse, ResourceException>() {
+            @Override
+            public Promise<ResourceResponse, ResourceException> apply(SearchResultEntry entry)
+                    throws ResourceException {
+                try {
+                    // Fail if there is a version mismatch.
+                    ensureMVCCVersionMatches(entry, request.getRevision());
+                    return adaptEntry(requestState, entry);
+                } catch (final Exception e) {
+                    return Promises.newExceptionPromise(asResourceException(e));
+                }
+            }
+        };
+    }
+
+    @Override
+    public Promise<QueryResponse, ResourceException> queryCollection(
+            final Context context, final QueryRequest request, final QueryResourceHandler resourceHandler) {
+        final RequestState requestState = wrap(context);
+
+        return requestState.getConnection()
+                .thenAsync(new AsyncFunction<Connection, QueryResponse, ResourceException>() {
+                    @Override
+                    public Promise<QueryResponse, ResourceException> apply(final Connection connection)
+                            throws ResourceException {
+                        // Calculate the filter (this may require the connection).
+                        return getLDAPFilter(requestState, request.getQueryFilter())
+                                            .thenAsync(runQuery(request, resourceHandler, requestState, connection));
+                    }
+                })
+                .thenFinally(close(requestState));
+    }
+
+    private Promise<Filter, ResourceException> getLDAPFilter(
+            final RequestState requestState, final QueryFilter<JsonPointer> queryFilter) {
+        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) {
+                        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>() {
+                            @Override
+                            public Filter apply(final List<Filter> value) {
+                                // Check for unmapped filter components and optimize.
+                                final Iterator<Filter> i = value.iterator();
+                                while (i.hasNext()) {
+                                    final Filter f = i.next();
+                                    if (f == alwaysFalse()) {
+                                        return alwaysFalse();
+                                    } else if (f == alwaysTrue()) {
+                                        i.remove();
+                                    }
+                                }
+                                switch (value.size()) {
+                                case 0:
+                                    return alwaysTrue();
+                                case 1:
+                                    return value.get(0);
+                                default:
+                                    return Filter.and(value);
+                                }
+                            }
+                        });
+                    }
+
+                    @Override
+                    public Promise<Filter, ResourceException> visitBooleanLiteralFilter(
+                            final Void unused, final boolean value) {
+                        return Promises.newResultPromise(toFilter(value));
+                    }
+
+                    @Override
+                    public Promise<Filter, ResourceException> visitContainsFilter(
+                            final Void unused, final JsonPointer field, final Object valueAssertion) {
+                        return attributeMapper.getLDAPFilter(
+                                requestState, new JsonPointer(), field, FilterType.CONTAINS, null, valueAssertion);
+                    }
+
+                    @Override
+                    public Promise<Filter, ResourceException> visitEqualsFilter(
+                            final Void unused, final JsonPointer field, final Object valueAssertion) {
+                        return attributeMapper.getLDAPFilter(
+                                requestState, new JsonPointer(), field, FilterType.EQUAL_TO, null, valueAssertion);
+                    }
+
+                    @Override
+                    public Promise<Filter, ResourceException> visitExtendedMatchFilter(final Void unused,
+                            final JsonPointer field, final String operator, final Object valueAssertion) {
+                        return attributeMapper.getLDAPFilter(
+                                requestState, new JsonPointer(), field, FilterType.EXTENDED, operator, valueAssertion);
+                    }
+
+                    @Override
+                    public Promise<Filter, ResourceException> visitGreaterThanFilter(
+                            final Void unused, final JsonPointer field, final Object valueAssertion) {
+                        return attributeMapper.getLDAPFilter(
+                                requestState, new JsonPointer(), field, FilterType.GREATER_THAN, null, valueAssertion);
+                    }
+
+                    @Override
+                    public Promise<Filter, ResourceException> visitGreaterThanOrEqualToFilter(
+                            final Void unused, final JsonPointer field, final Object valueAssertion) {
+                        return attributeMapper.getLDAPFilter(requestState, new JsonPointer(), field,
+                                FilterType.GREATER_THAN_OR_EQUAL_TO, null, valueAssertion);
+                    }
+
+                    @Override
+                    public Promise<Filter, ResourceException> visitLessThanFilter(
+                            final Void unused, final JsonPointer field, final Object valueAssertion) {
+                        return attributeMapper.getLDAPFilter(
+                                requestState, new JsonPointer(), field, FilterType.LESS_THAN, null, valueAssertion);
+                    }
+
+                    @Override
+                    public Promise<Filter, ResourceException> visitLessThanOrEqualToFilter(
+                            final Void unused, final JsonPointer field, final Object valueAssertion) {
+                        return attributeMapper.getLDAPFilter(requestState, new JsonPointer(), field,
+                                FilterType.LESS_THAN_OR_EQUAL_TO, null, valueAssertion);
+                    }
+
+                    @Override
+                    public Promise<Filter, ResourceException> visitNotFilter(
+                            final Void unused, final QueryFilter<JsonPointer> subFilter) {
+                        return subFilter.accept(this, unused).then(new Function<Filter, Filter, ResourceException>() {
+                            @Override
+                            public Filter apply(final Filter value) {
+                                if (value == null || value == alwaysFalse()) {
+                                    return alwaysTrue();
+                                } else if (value == alwaysTrue()) {
+                                    return alwaysFalse();
+                                } else {
+                                    return Filter.not(value);
+                                }
+                            }
+                        });
+                    }
+
+                    @Override
+                    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>() {
+                            @Override
+                            public Filter apply(final List<Filter> value) {
+                                // Check for unmapped filter components and optimize.
+                                final Iterator<Filter> i = value.iterator();
+                                while (i.hasNext()) {
+                                    final Filter f = i.next();
+                                    if (f == alwaysFalse()) {
+                                        i.remove();
+                                    } else if (f == alwaysTrue()) {
+                                        return alwaysTrue();
+                                    }
+                                }
+                                switch (value.size()) {
+                                case 0:
+                                    return alwaysFalse();
+                                case 1:
+                                    return value.get(0);
+                                default:
+                                    return Filter.or(value);
+                                }
+                            }
+                        });
+                    }
+
+                    @Override
+                    public Promise<Filter, ResourceException> visitPresentFilter(
+                            final Void unused, final JsonPointer field) {
+                        return attributeMapper.getLDAPFilter(
+                                requestState, new JsonPointer(), field, FilterType.PRESENT, null, null);
+                    }
+
+                    @Override
+                    public Promise<Filter, ResourceException> visitStartsWithFilter(
+                            final Void unused, final JsonPointer field, final Object valueAssertion) {
+                        return attributeMapper.getLDAPFilter(
+                                requestState, new JsonPointer(), field, FilterType.STARTS_WITH, null, valueAssertion);
+                    }
+
+                };
+        // Note that the returned LDAP filter may be null if it could not be mapped by any attribute mappers.
+        return queryFilter.accept(visitor, null);
+    }
+
+    private AsyncFunction<Filter, QueryResponse, ResourceException> runQuery(final QueryRequest request,
+            final QueryResourceHandler resourceHandler, final RequestState requestState, 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.
+             */
+            private final Object sequenceLock = new Object();
+            private String cookie;
+            private ResourceException pendingResult;
+            private int pendingResourceCount;
+            private boolean resultSent;
+            private int totalResourceCount;
+
+            @Override
+            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());
+                }
+                final PromiseImpl<QueryResponse, ResourceException> promise = PromiseImpl.create();
+                // Perform the search.
+                final String[] attributes = getLDAPAttributes(requestState, request.getFields());
+                final Filter searchFilter = ldapFilter == Filter.alwaysTrue() ? Filter.objectClassPresent()
+                                                                              : ldapFilter;
+                final SearchRequest searchRequest = newSearchRequest(
+                        getBaseDN(requestState), SearchScope.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.
+                final int pageResultStartIndex;
+                final int pageSize = request.getPageSize();
+                if (request.getPageSize() > 0) {
+                    final int pageResultEndIndex;
+                    if (request.getPagedResultsOffset() > 0) {
+                        pageResultStartIndex = request.getPagedResultsOffset() * pageSize;
+                        pageResultEndIndex = pageResultStartIndex + pageSize;
+                    } else {
+                        pageResultStartIndex = 0;
+                        pageResultEndIndex = pageSize;
+                    }
+                    final ByteString cookie = request.getPagedResultsCookie() != null
+                            ? ByteString.valueOfBase64(request.getPagedResultsCookie()) : ByteString.empty();
+                    final SimplePagedResultsControl control =
+                            SimplePagedResultsControl.newControl(true, pageResultEndIndex, cookie);
+                    searchRequest.addControl(control);
+                } else {
+                    pageResultStartIndex = 0;
+                }
+
+                connection.searchAsync(searchRequest, new SearchResultHandler() {
+                    @Override
+                    public boolean handleEntry(final SearchResultEntry entry) {
+                        // Search result entries will be returned before the search result/error so the only reason
+                        // pendingResult will be non-null is if a mapping error has occurred.
+                        synchronized (sequenceLock) {
+                            if (pendingResult != null) {
+                                return false;
+                            }
+                            if (totalResourceCount++ < pageResultStartIndex) {
+                                // Haven't reached paged results threshold yet.
+                                return true;
+                            }
+                            pendingResourceCount++;
+                        }
+
+                        /*
+                         * FIXME: secondary asynchronous searches will complete in a non-deterministic order and
+                         * may cause the JSON resources to be returned in a different order to the order in which
+                         * the primary LDAP search results were received. This is benign at the moment, but will
+                         * need resolving when we implement server side sorting. A possible fix will be to use a
+                         * queue of pending resources (promises?). However, the queue cannot be unbounded in case
+                         * it grows very large, but it cannot be bounded either since that could cause a deadlock
+                         * between rest2ldap and the LDAP server (imagine the case where the server has a single
+                         * worker thread which is occupied processing the primary search).
+                         * The best solution is probably to process the primary search results in batches using
+                         * the paged results control.
+                         */
+                        final String id = nameStrategy.getResourceId(requestState, entry);
+                        final String revision = getRevisionFromEntry(entry);
+                        attributeMapper.read(requestState, new JsonPointer(), 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);
+                                               }
+                                           }
+                                       });
+                        return true;
+                    }
+
+                    @Override
+                    public boolean handleReference(final SearchResultReference reference) {
+                        // TODO: should this be classed as an error since
+                        // rest2ldap assumes entries are all colocated?
+                        return true;
+                    }
+
+                }).thenOnResult(new ResultHandler<Result>() {
+                    @Override
+                    public void handleResult(Result result) {
+                        synchronized (sequenceLock) {
+                            if (request.getPageSize() > 0) {
+                                try {
+                                    final SimplePagedResultsControl control =
+                                            result.getControl(SimplePagedResultsControl.DECODER, DECODE_OPTIONS);
+                                    if (control != null && !control.getCookie().isEmpty()) {
+                                        cookie = control.getCookie().toBase64String();
+                                    }
+                                } catch (final DecodeException e) {
+                                    // FIXME: need some logging.
+                                }
+                            }
+                            completeIfNecessary(SUCCESS, promise);
+                        }
+                    }
+                }).thenOnException(new ExceptionHandler<LdapException>() {
+                    @Override
+                    public void handleException(LdapException exception) {
+                        synchronized (sequenceLock) {
+                            completeIfNecessary(asResourceException(exception), promise);
                         }
                     }
                 });
+
+                return promise;
             }
-        });
-    }
 
-    @Override
-    public void readInstance(final ServerContext context, final String resourceId, final ReadRequest request,
-        final ResultHandler<Resource> handler) {
-        final Context c = wrap(context);
-        final ResultHandler<Resource> h = wrap(c, handler);
-
-        // Get connection then perform the search.
-        c.run(h, new Runnable() {
-            @Override
-            public void run() {
-                // Do the search.
-                final String[] attributes = getLDAPAttributes(c, request.getFields());
-                final SearchRequest request =
-                    nameStrategy.createSearchRequest(c, getBaseDN(c), resourceId).addAttribute(attributes);
-
-                c.getConnection().searchSingleEntryAsync(request).thenOnResult(
-                    new org.forgerock.util.promise.ResultHandler<SearchResultEntry>() {
-                        @Override
-                        public void handleResult(final SearchResultEntry entry) {
-                            adaptEntry(c, entry, h);
-                        }
-                    }).thenOnException(new ExceptionHandler<LdapException>() {
-                        @Override
-                        public void handleException(final LdapException exception) {
-                            h.handleError(asResourceException(exception));
-                        }
-                    });
-            };
-        });
-    }
-
-    @Override
-    public void updateInstance(final ServerContext context, final String resourceId, final UpdateRequest request,
-            final ResultHandler<Resource> handler) {
-        /*
-         * Update operations are a bit awkward because there is no direct
-         * mapping to LDAP. We need to convert the update request into an LDAP
-         * modify operation which means reading the current LDAP entry,
-         * generating the new entry content, then comparing the two in order to
-         * obtain a set of changes. We also need to handle read-only fields
-         * correctly: if a read-only field is included with the new resource
-         * then it must match exactly the value of the existing field.
-         */
-        final Context c = wrap(context);
-        final ResultHandler<Resource> h = wrap(c, handler);
-
-        // Get connection then, search for the existing entry, then modify.
-        c.run(h, new Runnable() {
-            @Override
-            public void run() {
-                final String[] attributes = getLDAPAttributes(c, Collections.<JsonPointer> emptyList());
-                final SearchRequest searchRequest = nameStrategy.createSearchRequest(c, getBaseDN(c), resourceId)
-                        .addAttribute(attributes);
-
-                c.getConnection().searchSingleEntryAsync(searchRequest)
-                        .thenOnResult(new org.forgerock.util.promise.ResultHandler<SearchResultEntry>() {
-                            @Override
-                            public void handleResult(final SearchResultEntry entry) {
-                                try {
-                                    // Fail-fast if there is a version mismatch.
-                                    ensureMVCCVersionMatches(entry, request.getRevision());
-
-                                    // Create the modify request.
-                                    final ModifyRequest modifyRequest = newModifyRequest(entry.getName());
-                                    if (config.readOnUpdatePolicy() == CONTROLS) {
-                                        final String[] attributes = getLDAPAttributes(c, request.getFields());
-                                        modifyRequest.addControl(PostReadRequestControl.newControl(false, attributes));
-                                    }
-                                    if (config.usePermissiveModify()) {
-                                        modifyRequest.addControl(PermissiveModifyRequestControl.newControl(true));
-                                    }
-                                    addAssertionControl(modifyRequest, request.getRevision());
-
-                                    /*
-                                     * Determine the set of changes that need to be performed.
-                                     */
-                                    attributeMapper.update(c, new JsonPointer(), entry, request.getNewContent(),
-                                            new ResultHandler<List<Modification>>() {
-                                                @Override
-                                                public void handleError(final ResourceException error) {
-                                                    h.handleError(error);
-                                                }
-
-                                                @Override
-                                                public void handleResult(final List<Modification> result) {
-                                                    // Perform the modify operation.
-                                                    if (result.isEmpty()) {
-                                                        /*
-                                                         * No changes to be performed, so just return
-                                                         * the entry that we read.
-                                                         */
-                                                        adaptEntry(c, entry, h);
-                                                    } else {
-                                                        modifyRequest.getModifications().addAll(result);
-                                                        c.getConnection().applyChangeAsync(modifyRequest)
-                                                                .thenOnResult(postUpdateResultHandler(c, h))
-                                                                .thenOnException(postUpdateExceptionHandler(h));
-                                                    }
-                                                }
-                                            });
-                                } catch (final Exception e) {
-                                    h.handleError(asResourceException(e));
-                                }
-                            }
-                        }).thenOnException(new ExceptionHandler<LdapException>() {
-                            @Override
-                            public void handleException(final LdapException exception) {
-                                h.handleError(asResourceException(exception));
-                            }
-                        });
+            /** This method must be invoked with the sequenceLock held. */
+            private void completeIfNecessary(
+                    final ResourceException e, final PromiseImpl<QueryResponse, ResourceException> handler) {
+                if (pendingResult == null) {
+                    pendingResult = e;
+                }
+                completeIfNecessary(handler);
             }
-        });
-    }
 
-    private void adaptEntry(final Context c, final Entry entry, final ResultHandler<Resource> handler) {
-        final String actualResourceId = nameStrategy.getResourceId(c, entry);
-        final String revision = getRevisionFromEntry(entry);
-        attributeMapper.read(c, new JsonPointer(), entry, transform(
-                new Function<JsonValue, Resource, NeverThrowsException>() {
-                    @Override
-                    public Resource apply(final JsonValue value) {
-                        return new Resource(actualResourceId, revision, new JsonValue(value));
+            /**
+             * Close out the query result set if there are no more pending
+             * resources and the LDAP result has been received.
+             * This method must be invoked with the sequenceLock held.
+             */
+            private void completeIfNecessary(final PromiseImpl<QueryResponse, ResourceException> handler) {
+                if (pendingResourceCount == 0 && pendingResult != null && !resultSent) {
+                    if (pendingResult == SUCCESS) {
+                        handler.handleResult(Responses.newQueryResponse(cookie));
+                    } else {
+                        handler.handleException(pendingResult);
                     }
-                }, handler));
+                    resultSent = true;
+                }
+            }
+        };
+    }
+
+    @Override
+    public Promise<ResourceResponse, ResourceException> readInstance(
+            final Context context, final String resourceId, final ReadRequest request) {
+        final RequestState requestState = wrap(context);
+
+        return requestState.getConnection()
+                .thenAsync(new AsyncFunction<Connection, ResourceResponse, ResourceException>() {
+                    @Override
+                    public Promise<ResourceResponse, ResourceException> apply(Connection connection)
+                            throws ResourceException {
+                        // Do the search.
+                        final String[] attributes = getLDAPAttributes(requestState, request.getFields());
+                        final SearchRequest request =
+                                nameStrategy.createSearchRequest(requestState, getBaseDN(requestState), resourceId)
+                                            .addAttribute(attributes);
+
+                        return connection.searchSingleEntryAsync(request)
+                                    .thenAsync(
+                                        new AsyncFunction<SearchResultEntry, ResourceResponse, ResourceException>() {
+                                            public Promise<ResourceResponse, ResourceException> apply(
+                                                    SearchResultEntry entry) throws ResourceException {
+                                                return adaptEntry(requestState, entry);
+                                            }
+                                        },
+                                        ldapExceptionToResourceException());
+                    }
+                })
+                .thenFinally(close(requestState));
+    }
+
+    @Override
+    public Promise<ResourceResponse, ResourceException> updateInstance(
+            final Context context, final String resourceId, final UpdateRequest request) {
+        final RequestState requestState = wrap(context);
+        final AtomicReference<Connection> connectionHolder = new AtomicReference<>();
+
+        return requestState.getConnection().thenOnResult(saveConnection(connectionHolder))
+                .thenAsync(new AsyncFunction<Connection, ResourceResponse, ResourceException>() {
+                    @Override
+                    public Promise<ResourceResponse, ResourceException> apply(final Connection connection)
+                            throws ResourceException {
+                        final String[] attributes = getLDAPAttributes(
+                                requestState, Collections.<JsonPointer> emptyList());
+                        final SearchRequest searchRequest = nameStrategy.createSearchRequest(
+                                requestState, getBaseDN(requestState), resourceId).addAttribute(attributes);
+
+                        return connection.searchSingleEntryAsync(searchRequest)
+                                .thenAsync(new AsyncFunction<SearchResultEntry, ResourceResponse, ResourceException>() {
+                                    @Override
+                                    public Promise<ResourceResponse, ResourceException> apply(
+                                            final SearchResultEntry entry) {
+                                        try {
+                                            // Fail-fast if there is a version mismatch.
+                                            ensureMVCCVersionMatches(entry, request.getRevision());
+
+                                            // Create the modify request.
+                                            final ModifyRequest modifyRequest = newModifyRequest(entry.getName());
+                                            if (config.readOnUpdatePolicy() == CONTROLS) {
+                                                final String[] attributes =
+                                                        getLDAPAttributes(requestState, request.getFields());
+                                                modifyRequest.addControl(
+                                                        PostReadRequestControl.newControl(false, attributes));
+                                            }
+                                            if (config.usePermissiveModify()) {
+                                                modifyRequest.addControl(
+                                                        PermissiveModifyRequestControl.newControl(true));
+                                            }
+                                            addAssertionControl(modifyRequest, request.getRevision());
+
+                                            // Determine the set of changes that need to be performed.
+                                            return attributeMapper.update(
+                                                        requestState, 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(requestState, entry);
+                                                            }
+                                                            // Perform the modify operation.
+                                                            modifyRequest.getModifications().addAll(modifications);
+                                                            return connection.applyChangeAsync(modifyRequest).thenAsync(
+                                                                    postUpdateResultAsyncFunction(requestState),
+                                                                    ldapExceptionToResourceException());
+                                                        }
+                                                    });
+                                        } catch (final Exception e) {
+                                            return Promises.newExceptionPromise(asResourceException(e));
+                                        }
+                                    }
+                                }, ldapExceptionToResourceException());
+                    }
+                }).thenFinally(close(requestState));
+    }
+
+    private Promise<ResourceResponse, ResourceException> adaptEntry(
+            final RequestState requestState, final Entry entry) {
+        final String actualResourceId = nameStrategy.getResourceId(requestState, entry);
+        final String revision = getRevisionFromEntry(entry);
+        return attributeMapper.read(requestState, new JsonPointer(), entry)
+                              .then(new Function<JsonValue, ResourceResponse, ResourceException>() {
+                                  @Override
+                                  public ResourceResponse apply(final JsonValue value) {
+                                      return Responses.newResourceResponse(
+                                              actualResourceId, revision, new JsonValue(value));
+                                  }
+                              });
     }
 
     private void addAssertionControl(final ChangeRecord request, final String expectedRevision)
@@ -652,39 +793,39 @@
         }
     }
 
-    private Runnable doUpdate(final Context c, final String resourceId, final String revision,
-            final ResultHandler<DN> updateHandler) {
-        return new Runnable() {
+    private AsyncFunction<Connection, DN, ResourceException> doUpdateFunction(
+            final RequestState requestState, final String resourceId, final String revision) {
+        return new AsyncFunction<Connection, DN, ResourceException>() {
             @Override
-            public void run() {
+            public Promise<DN, ResourceException> apply(Connection connection) {
                 final String ldapAttribute =
-                        (etagAttribute != null && revision != null) ? etagAttribute.toString()
-                                : "1.1";
+                        (etagAttribute != null && revision != null) ? etagAttribute.toString() : "1.1";
                 final SearchRequest searchRequest =
-                        nameStrategy.createSearchRequest(c, getBaseDN(c), resourceId).addAttribute(
-                                ldapAttribute);
+                        nameStrategy.createSearchRequest(requestState, getBaseDN(requestState), resourceId)
+                                    .addAttribute(ldapAttribute);
                 if (searchRequest.getScope().equals(SearchScope.BASE_OBJECT)) {
                     // There's no point in doing a search because we already know the DN.
-                    updateHandler.handleResult(searchRequest.getName());
+                    return Promises.newResultPromise(searchRequest.getName());
                 } else {
-                    c.getConnection().searchSingleEntryAsync(searchRequest)
-                            .thenOnResult(new org.forgerock.util.promise.ResultHandler<SearchResultEntry>() {
+                    return connection.searchSingleEntryAsync(searchRequest)
+                            .thenAsync(new AsyncFunction<SearchResultEntry, DN, ResourceException>() {
                                 @Override
-                                public void handleResult(final SearchResultEntry entry) {
+                                public Promise<DN, ResourceException> apply(SearchResultEntry entry)
+                                        throws ResourceException {
                                     try {
                                         // Fail-fast if there is a version mismatch.
                                         ensureMVCCVersionMatches(entry, revision);
-
                                         // Perform update operation.
-                                        updateHandler.handleResult(entry.getName());
+                                        return Promises.newResultPromise(entry.getName());
                                     } catch (final Exception e) {
-                                        updateHandler.handleError(asResourceException(e));
+                                        return Promises.newExceptionPromise(asResourceException(e));
                                     }
                                 }
-                            }).thenOnException(new ExceptionHandler<LdapException>() {
+                            }, new AsyncFunction<LdapException, DN, ResourceException>() {
                                 @Override
-                                public void handleException(final LdapException exception) {
-                                    updateHandler.handleError(asResourceException(exception));
+                                public Promise<DN, ResourceException> apply(LdapException ldapException)
+                                        throws ResourceException {
+                                    return Promises.newExceptionPromise(asResourceException(ldapException));
                                 }
                             });
                 }
@@ -715,7 +856,7 @@
         }
     }
 
-    private DN getBaseDN(final Context context) {
+    private DN getBaseDN(final RequestState requestState) {
         return baseDN;
     }
 
@@ -723,274 +864,49 @@
      * Determines the set of LDAP attributes to request in an LDAP read (search,
      * post-read), based on the provided list of JSON pointers.
      *
+     * @param requestState
+     *          The request state.
      * @param requestedAttributes
-     *            The list of resource attributes to be read.
+     *          The list of resource attributes to be read.
      * @return The set of LDAP attributes associated with the resource
      *         attributes.
      */
-    private String[] getLDAPAttributes(final Context c,
-            final Collection<JsonPointer> requestedAttributes) {
+    private String[] getLDAPAttributes(
+            final RequestState requestState, final Collection<JsonPointer> requestedAttributes) {
         // Get all the LDAP attributes required by the attribute mappers.
         final Set<String> requestedLDAPAttributes;
         if (requestedAttributes.isEmpty()) {
             // Full read.
             requestedLDAPAttributes = new LinkedHashSet<>();
-            attributeMapper.getLDAPAttributes(c, new JsonPointer(), new JsonPointer(),
+            attributeMapper.getLDAPAttributes(requestState, new JsonPointer(), new JsonPointer(),
                     requestedLDAPAttributes);
         } else {
             // Partial read.
             requestedLDAPAttributes = new LinkedHashSet<>(requestedAttributes.size());
             for (final JsonPointer requestedAttribute : requestedAttributes) {
-                attributeMapper.getLDAPAttributes(c, new JsonPointer(), requestedAttribute,
+                attributeMapper.getLDAPAttributes(requestState, new JsonPointer(), requestedAttribute,
                         requestedLDAPAttributes);
             }
         }
 
         // Get the LDAP attributes required by the Etag and name stategies.
-        nameStrategy.getLDAPAttributes(c, requestedLDAPAttributes);
+        nameStrategy.getLDAPAttributes(requestState, requestedLDAPAttributes);
         if (etagAttribute != null) {
             requestedLDAPAttributes.add(etagAttribute.toString());
         }
         return requestedLDAPAttributes.toArray(new String[requestedLDAPAttributes.size()]);
     }
 
-    private void getLDAPFilter(final Context c, final QueryFilter queryFilter, final ResultHandler<Filter> h) {
-        final QueryFilterVisitor<Void, ResultHandler<Filter>> visitor =
-                new QueryFilterVisitor<Void, ResultHandler<Filter>>() {
-                    @Override
-                    public Void visitAndFilter(final ResultHandler<Filter> p, final List<QueryFilter> subFilters) {
-                        List<Promise<Filter, ResourceException>> promises = new ArrayList<>(subFilters.size());
-                        for (final QueryFilter subFilter : subFilters) {
-                            final ResultHandlerFromPromise<Filter> handler = new ResultHandlerFromPromise<>();
-                            subFilter.accept(this, handler);
-                            promises.add(handler.promise);
-                        }
-
-                        Promises.when(promises)
-                                .then(new org.forgerock.util.Function<List<Filter>, Filter, ResourceException>() {
-                                    @Override
-                                    public Filter apply(final List<Filter> value) {
-                                        // Check for unmapped filter components and optimize.
-                                        final Iterator<Filter> i = value.iterator();
-                                        while (i.hasNext()) {
-                                            final Filter f = i.next();
-                                            if (f == alwaysFalse()) {
-                                                return alwaysFalse();
-                                            } else if (f == alwaysTrue()) {
-                                                i.remove();
-                                            }
-                                        }
-                                        switch (value.size()) {
-                                        case 0:
-                                            return alwaysTrue();
-                                        case 1:
-                                            return value.get(0);
-                                        default:
-                                            return Filter.and(value);
-                                        }
-                                    }
-                                }).thenOnResult(new org.forgerock.util.promise.ResultHandler<Filter>() {
-                                    @Override
-                                    public void handleResult(Filter result) {
-                                        p.handleResult(result);
-                                    }
-                                }).thenOnException(new ExceptionHandler<ResourceException>() {
-                                    @Override
-                                    public void handleException(ResourceException exception) {
-                                        p.handleError(exception);
-                                    }
-                                });
-
-                        return null;
-                    }
-
-                    @Override
-                    public Void visitBooleanLiteralFilter(final ResultHandler<Filter> p,
-                            final boolean value) {
-                        p.handleResult(toFilter(value));
-                        return null;
-                    }
-
-                    @Override
-                    public Void visitContainsFilter(final ResultHandler<Filter> p,
-                            final JsonPointer field, final Object valueAssertion) {
-                        attributeMapper.getLDAPFilter(c, new JsonPointer(), field,
-                                FilterType.CONTAINS, null, valueAssertion, p);
-                        return null;
-                    }
-
-                    @Override
-                    public Void visitEqualsFilter(final ResultHandler<Filter> p,
-                            final JsonPointer field, final Object valueAssertion) {
-                        attributeMapper.getLDAPFilter(c, new JsonPointer(), field,
-                                FilterType.EQUAL_TO, null, valueAssertion, p);
-                        return null;
-                    }
-
-                    @Override
-                    public Void visitExtendedMatchFilter(final ResultHandler<Filter> p,
-                            final JsonPointer field, final String operator,
-                            final Object valueAssertion) {
-                        attributeMapper.getLDAPFilter(c, new JsonPointer(), field,
-                                FilterType.EXTENDED, operator, valueAssertion, p);
-                        return null;
-                    }
-
-                    @Override
-                    public Void visitGreaterThanFilter(final ResultHandler<Filter> p,
-                            final JsonPointer field, final Object valueAssertion) {
-                        attributeMapper.getLDAPFilter(c, new JsonPointer(), field,
-                                FilterType.GREATER_THAN, null, valueAssertion, p);
-                        return null;
-                    }
-
-                    @Override
-                    public Void visitGreaterThanOrEqualToFilter(final ResultHandler<Filter> p,
-                            final JsonPointer field, final Object valueAssertion) {
-                        attributeMapper.getLDAPFilter(c, new JsonPointer(), field,
-                                FilterType.GREATER_THAN_OR_EQUAL_TO, null, valueAssertion, p);
-                        return null;
-                    }
-
-                    @Override
-                    public Void visitLessThanFilter(final ResultHandler<Filter> p,
-                            final JsonPointer field, final Object valueAssertion) {
-                        attributeMapper.getLDAPFilter(c, new JsonPointer(), field,
-                                FilterType.LESS_THAN, null, valueAssertion, p);
-                        return null;
-                    }
-
-                    @Override
-                    public Void visitLessThanOrEqualToFilter(final ResultHandler<Filter> p,
-                            final JsonPointer field, final Object valueAssertion) {
-                        attributeMapper.getLDAPFilter(c, new JsonPointer(), field,
-                                FilterType.LESS_THAN_OR_EQUAL_TO, null, valueAssertion, p);
-                        return null;
-                    }
-
-                    @Override
-                    public Void visitNotFilter(final ResultHandler<Filter> p,
-                            final QueryFilter subFilter) {
-                        subFilter.accept(this, transform(new Function<Filter, Filter, NeverThrowsException>() {
-                            @Override
-                            public Filter apply(final Filter value) {
-                                if (value == null || value == alwaysFalse()) {
-                                    return alwaysTrue();
-                                } else if (value == alwaysTrue()) {
-                                    return alwaysFalse();
-                                } else {
-                                    return Filter.not(value);
-                                }
-                            }
-                        }, p));
-                        return null;
-                    }
-
-                    @Override
-                    public Void visitOrFilter(final ResultHandler<Filter> p, final List<QueryFilter> subFilters) {
-                        List<Promise<Filter, ResourceException>> promises = new ArrayList<>(subFilters.size());
-                        for (final QueryFilter subFilter : subFilters) {
-                            final ResultHandlerFromPromise<Filter> handler = new ResultHandlerFromPromise<>();
-                            subFilter.accept(this, handler);
-                            promises.add(handler.promise);
-                        }
-
-                        Promises.when(promises)
-                                .then(new org.forgerock.util.Function<List<Filter>, Filter, ResourceException>() {
-                                    @Override
-                                    public Filter apply(final List<Filter> value) {
-                                        // Check for unmapped filter components and optimize.
-                                        final Iterator<Filter> i = value.iterator();
-                                        while (i.hasNext()) {
-                                            final Filter f = i.next();
-                                            if (f == alwaysFalse()) {
-                                                i.remove();
-                                            } else if (f == alwaysTrue()) {
-                                                return alwaysTrue();
-                                            }
-                                        }
-                                        switch (value.size()) {
-                                        case 0:
-                                            return alwaysFalse();
-                                        case 1:
-                                            return value.get(0);
-                                        default:
-                                            return Filter.or(value);
-                                        }
-                                    }
-                                }).thenOnResult(new org.forgerock.util.promise.ResultHandler<Filter>() {
-                                    @Override
-                                    public void handleResult(Filter result) {
-                                        p.handleResult(result);
-                                    }
-                                }).thenOnException(new ExceptionHandler<ResourceException>() {
-                                    @Override
-                                    public void handleException(ResourceException exception) {
-                                        p.handleError(exception);
-                                    }
-                                });
-
-                        return null;
-                    }
-
-                    @Override
-                    public Void visitPresentFilter(final ResultHandler<Filter> p,
-                            final JsonPointer field) {
-                        attributeMapper.getLDAPFilter(c, new JsonPointer(), field,
-                                FilterType.PRESENT, null, null, p);
-                        return null;
-                    }
-
-                    @Override
-                    public Void visitStartsWithFilter(final ResultHandler<Filter> p,
-                            final JsonPointer field, final Object valueAssertion) {
-                        attributeMapper.getLDAPFilter(c, new JsonPointer(), field,
-                                FilterType.STARTS_WITH, null, valueAssertion, p);
-                        return null;
-                    }
-
-                };
-        /*
-         * Note that the returned LDAP filter may be null if it could not be mapped by any attribute mappers.
-         */
-        queryFilter.accept(visitor, h);
-    }
-
     private String getRevisionFromEntry(final Entry entry) {
         return etagAttribute != null ? entry.parseAttribute(etagAttribute).asString() : null;
     }
 
-    private org.forgerock.util.promise.ResultHandler<SearchResultEntry> postEmptyPatchResultHandler(
-            final Context c, final PatchRequest request, final ResultHandler<Resource> h) {
-        return new org.forgerock.util.promise.ResultHandler<SearchResultEntry>() {
-            @Override
-            public void handleResult(final SearchResultEntry entry) {
-                try {
-                    // Fail if there is a version mismatch.
-                    ensureMVCCVersionMatches(entry, request.getRevision());
-                    adaptEntry(c, entry, h);
-                } catch (final Exception e) {
-                    h.handleError(asResourceException(e));
-                }
-            }
-        };
-    }
-
-    private ExceptionHandler<LdapException> postEmptyPatchExceptionHandler(final ResultHandler<Resource> h) {
-        return new ExceptionHandler<LdapException>() {
-            @Override
-            public void handleException(final LdapException exception) {
-                h.handleError(asResourceException(exception));
-            }
-        };
-    }
-
-    private org.forgerock.util.promise.ResultHandler<Result> postUpdateResultHandler(
-            final Context c, final ResultHandler<Resource> handler) {
+    private AsyncFunction<Result, ResourceResponse, ResourceException> postUpdateResultAsyncFunction(
+            final RequestState requestState) {
         // The handler which will be invoked for the LDAP add result.
-        return new org.forgerock.util.promise.ResultHandler<Result>() {
+        return new AsyncFunction<Result, ResourceResponse, ResourceException>() {
             @Override
-            public void handleResult(final Result result) {
+            public Promise<ResourceResponse, ResourceException> apply(Result result) throws ResourceException {
                 // FIXME: handle USE_SEARCH policy.
                 Entry entry;
                 try {
@@ -1012,76 +928,45 @@
                     entry = null;
                 }
                 if (entry != null) {
-                    adaptEntry(c, entry, handler);
+                    return adaptEntry(requestState, entry);
                 } else {
-                    final Resource resource = new Resource(null, null, new JsonValue(Collections.emptyMap()));
-                    handler.handleResult(resource);
+                    return Promises.newResultPromise(
+                            Responses.newResourceResponse(null, null, new JsonValue(Collections.emptyMap())));
                 }
             }
-
         };
     }
 
-    private ExceptionHandler<LdapException> postUpdateExceptionHandler(final ResultHandler<Resource> handler) {
+    private AsyncFunction<LdapException, ResourceResponse, ResourceException> ldapExceptionToResourceException() {
         // The handler which will be invoked for the LDAP add result.
-        return new ExceptionHandler<LdapException>() {
+        return new AsyncFunction<LdapException, ResourceResponse, ResourceException>() {
             @Override
-            public void handleException(final LdapException exception) {
-                handler.handleError(asResourceException(exception));
+            public Promise<ResourceResponse, ResourceException> apply(final LdapException ldapException)
+                    throws ResourceException {
+                return Promises.newExceptionPromise(asResourceException(ldapException));
             }
         };
     }
 
-    private QueryResultHandler wrap(final Context c, final QueryResultHandler handler) {
-        return new QueryResultHandler() {
-            @Override
-            public void handleError(final ResourceException error) {
-                try {
-                    handler.handleError(error);
-                } finally {
-                    c.close();
-                }
-            }
+    private RequestState wrap(final Context context) {
+        return new RequestState(config, context);
+    }
 
+    private Runnable close(final RequestState requestState) {
+        return new Runnable() {
             @Override
-            public boolean handleResource(final Resource resource) {
-                return handler.handleResource(resource);
-            }
-
-            @Override
-            public void handleResult(final QueryResult result) {
-                try {
-                    handler.handleResult(result);
-                } finally {
-                    c.close();
-                }
+            public void run() {
+                requestState.close();
             }
         };
     }
 
-    private <V> ResultHandler<V> wrap(final Context c, final ResultHandler<V> handler) {
-        return new ResultHandler<V>() {
+    private ResultHandler<Connection> saveConnection(final AtomicReference<Connection> connectionHolder) {
+        return new ResultHandler<Connection>() {
             @Override
-            public void handleError(final ResourceException error) {
-                try {
-                    handler.handleError(error);
-                } finally {
-                    c.close();
-                }
-            }
-
-            @Override
-            public void handleResult(final V result) {
-                try {
-                    handler.handleResult(result);
-                } finally {
-                    c.close();
-                }
+            public void handleResult(Connection connection) {
+                connectionHolder.set(connection);
             }
         };
     }
-
-    private Context wrap(final ServerContext context) {
-        return new Context(config, context);
-    }
 }

--
Gitblit v1.10.0