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-servlet/pom.xml                                                                            |  262 --
 opendj-server-legacy/src/main/java/org/opends/server/protocols/http/LdapHttpApplication.java                |  177 ++
 opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java                        |  273 +-
 opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/TestUtils.java                                |   20 
 opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/HttpAuthenticationFilter.java                 |  384 ++++
 opendj-rest2ldap/pom.xml                                                                                    |   10 
 opendj-server-legacy/src/messages/org/opends/messages/protocol.properties                                   |    1 
 opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAPHttpApplication.java                 |  167 +
 opendj-server-legacy/src/test/java/org/opends/server/protocols/http/CollectClientConnectionsFilterTest.java |   72 
 pom.xml                                                                                                     |    3 
 opendj-server-legacy/pom.xml                                                                                |   24 
 opendj-server-legacy/src/main/java/org/opends/server/protocols/http/HTTPClientConnection.java               |   64 
 opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/RequestState.java                             |   64 
 opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/web.xml                                                    |   63 
 opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthenticatedConnectionContext.java           |   45 
 opendj-rest2ldap-servlet/src/main/webapp/META-INF/services/org.forgerock.http.HttpApplication               |   16 
 opendj-server-legacy/src/main/assembly/opendj-archive-component.xml                                         |    1 
 opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferenceAttributeMapper.java                 |  330 +-
 opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/FilterType.java                               |   10 
 opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Utils.java                                    |  126 -
 opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractLDAPAttributeMapper.java              |  269 +-
 opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectAttributeMapper.java                    |  171 +-
 opendj-server-legacy/src/main/java/org/opends/server/protocols/http/HTTPConnectionHandler.java              |  129 -
 opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/JSONConstantAttributeMapper.java              |   55 
 /dev/null                                                                                                   |   20 
 opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimpleAttributeMapper.java                    |   45 
 opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java           | 1537 ++++++++---------
 opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java                                |   27 
 opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/opendj-rest2ldap-config.json                       |    0 
 opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AttributeMapper.java                          |  128 
 opendj-server-legacy/src/main/java/org/opends/server/protocols/http/CollectClientConnectionsFilter.java     |  556 ++----
 opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/NameStrategy.java                             |   26 
 32 files changed, 2,562 insertions(+), 2,513 deletions(-)

diff --git a/opendj-rest2ldap-servlet/pom.xml b/opendj-rest2ldap-servlet/pom.xml
index c6c5249..7d745cf 100644
--- a/opendj-rest2ldap-servlet/pom.xml
+++ b/opendj-rest2ldap-servlet/pom.xml
@@ -12,187 +12,91 @@
   ! Header, with the fields enclosed by brackets [] replaced by your own identifying
   ! information: "Portions Copyright [year] [name of copyright owner]".
   !
-  ! Copyright 2013 ForgeRock AS.
+  ! Copyright 2013-2015 ForgeRock AS.
   !
 -->
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
-    <modelVersion>4.0.0</modelVersion>
-    <parent>
-        <artifactId>opendj-project</artifactId>
-        <groupId>org.forgerock.opendj</groupId>
-        <version>3.0.0-SNAPSHOT</version>
-    </parent>
-    <artifactId>opendj-rest2ldap-servlet</artifactId>
-    <name>OpenDJ Commons REST LDAP Gateway</name>
-    <description>
-        Provides integration between the OpenDJ Commons REST Adapter and Servlet APIs.
-    </description>
-    <packaging>bundle</packaging>
-    <properties>
-        <jacksonVersion>1.9.2</jacksonVersion>
-        <checkstyleHeaderLocation>org/forgerock/checkstyle/default-java-header</checkstyleHeaderLocation>
-    </properties>
-    <dependencies>
-        <dependency>
-            <groupId>org.forgerock.opendj</groupId>
-            <artifactId>opendj-rest2ldap</artifactId>
-            <version>${project.version}</version>
-        </dependency>
-        <dependency>
-            <groupId>org.forgerock.opendj</groupId>
-            <artifactId>opendj-grizzly</artifactId>
-            <version>${project.version}</version>
-            <optional>true</optional>
-        </dependency>
-        <dependency>
-            <!-- Required for compilation -->
-            <groupId>org.forgerock.commons</groupId>
-            <artifactId>json-resource-servlet</artifactId>
-            <version>${forgerockRestVersion}</version>
-        </dependency>
-        <dependency>
-            <!-- Required for runtime via WAR overlay -->
-            <groupId>org.forgerock.commons</groupId>
-            <artifactId>json-resource-servlet</artifactId>
-            <version>${forgerockRestVersion}</version>
-            <type>war</type>
-            <classifier>servlet</classifier>
-            <scope>runtime</scope>
-        </dependency>
-        <dependency>
-            <groupId>org.slf4j</groupId>
-            <artifactId>slf4j-api</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>org.slf4j</groupId>
-            <artifactId>slf4j-jdk14</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>javax.servlet</groupId>
-            <artifactId>javax.servlet-api</artifactId>
-            <version>3.0.1</version>
-            <scope>provided</scope>
-        </dependency>
-        <dependency>
-            <groupId>org.codehaus.jackson</groupId>
-            <artifactId>jackson-core-asl</artifactId>
-            <version>${jacksonVersion}</version>
-        </dependency>
-        <dependency>
-            <groupId>org.codehaus.jackson</groupId>
-            <artifactId>jackson-mapper-asl</artifactId>
-            <version>${jacksonVersion}</version>
-        </dependency>
-    </dependencies>
-    <build>
-        <resources>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <parent>
+    <artifactId>opendj-project</artifactId>
+    <groupId>org.forgerock.opendj</groupId>
+    <version>3.0.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>opendj-rest2ldap-servlet</artifactId>
+  <name>OpenDJ Commons REST LDAP Gateway</name>
+  <description>
+    Provides integration between the OpenDJ Commons REST Adapter and Servlet APIs.
+  </description>
+  <packaging>war</packaging>
+
+  <properties>
+    <!-- When released, with the 'binary.license.url' property set,
+         this artifact will contain an additional binary license -->
+    <include.binary.license>${project.build.directory}/${project.build.finalName}/WEB-INF/legal-notices</include.binary.license>
+    <checkstyleHeaderLocation>org/forgerock/checkstyle/default-java-header</checkstyleHeaderLocation>
+  </properties>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.forgerock.http</groupId>
+      <artifactId>chf-http-servlet</artifactId>
+      <version>0.0.1-SNAPSHOT</version>
+    </dependency>
+
+    <dependency>
+      <groupId>org.forgerock.opendj</groupId>
+      <artifactId>opendj-rest2ldap</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>org.forgerock.opendj</groupId>
+      <artifactId>opendj-grizzly</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+  </dependencies>
+
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.eclipse.jetty</groupId>
+        <artifactId>jetty-maven-plugin</artifactId>
+        <version>9.2.11.v20150529</version>
+        <configuration>
+          <scanIntervalSeconds>10</scanIntervalSeconds>
+          <webAppConfig>
+            <contextPath>/</contextPath>
+          </webAppConfig>
+        </configuration>
+      </plugin>
+
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-war-plugin</artifactId>
+        <configuration>
+          <webResources> 
             <resource>
-                <directory>src/main/resources</directory>
-                <filtering>true</filtering>
+              <targetPath>WEB-INF/legal-notices</targetPath>
+              <directory>../legal-notices</directory>
+              <excludes>
+                <!-- The web-app does not include the documentation -->
+                <exclude>CC-BY-NC-ND.txt</exclude>
+              </excludes>
             </resource>
-        </resources>
-        <plugins>
-            <plugin>
-                <groupId>org.apache.felix</groupId>
-                <artifactId>maven-bundle-plugin</artifactId>
-                <extensions>true</extensions>
-            </plugin>
-            <!--  include opendj-grizzly and its dependencies in war -->
-            <plugin>
-              <groupId>org.apache.maven.plugins</groupId>
-              <artifactId>maven-dependency-plugin</artifactId>
-              <executions>
-                <execution>
-                  <id>copy-grizzly</id>
-                  <phase>package</phase>
-                  <goals>
-                    <goal>copy-dependencies</goal>
-                  </goals>
-                  <configuration>
-                    <includeArtifactIds>opendj-grizzly</includeArtifactIds>
-                    <outputDirectory>${project.build.directory}/${project.build.finalName}/WEB-INF/lib</outputDirectory>
-                    <overWriteReleases>false</overWriteReleases>
-                    <overWriteSnapshots>false</overWriteSnapshots>
-                    <overWriteIfNewer>true</overWriteIfNewer>
-                  </configuration>
-                </execution>
-              </executions>
-            </plugin>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-war-plugin</artifactId>
-                <executions>
-                    <execution>
-                        <id>war</id>
-                        <phase>package</phase>
-                        <goals>
-                            <goal>war</goal>
-                        </goals>
-                        <configuration>
-                            <classifier>servlet</classifier>
-                            <overlays>
-                                <overlay>
-                                    <groupId>org.forgerock.commons</groupId>
-                                    <artifactId>json-resource-servlet</artifactId>
-                                    <classifier>servlet</classifier>
-                                    <includes>
-                                        <include>WEB-INF/*.xml</include>
-                                    </includes>
-                                </overlay>
-                            </overlays>
-                        </configuration>
-                    </execution>
-                </executions>
-            </plugin>
-            <plugin>
-                <groupId>org.eclipse.jetty</groupId>
-                <artifactId>jetty-maven-plugin</artifactId>
-                <version>9.0.4.v20130625</version>
-            </plugin>
-        </plugins>
-        <pluginManagement>
-          <plugins>
-            <!--This plugin's configuration is used to store Eclipse m2e settings only. It has no influence on the Maven build itself.-->
-            <plugin>
-              <groupId>org.eclipse.m2e</groupId>
-              <artifactId>lifecycle-mapping</artifactId>
-              <version>1.0.0</version>
-              <configuration>
-                <lifecycleMappingMetadata>
-                  <pluginExecutions>
-                    <pluginExecution>
-                      <pluginExecutionFilter>
-                        <groupId>org.apache.maven.plugins</groupId>
-                        <artifactId>maven-dependency-plugin</artifactId>
-                        <versionRange>[2.6,)</versionRange>
-                        <goals>
-                          <goal>copy-dependencies</goal>
-                        </goals>
-                      </pluginExecutionFilter>
-                      <action>
-                        <ignore />
-                      </action>
-                    </pluginExecution>
-                  </pluginExecutions>
-                </lifecycleMappingMetadata>
-              </configuration>
-            </plugin>
-          </plugins>
-        </pluginManagement>
-    </build>
-    <reporting>
-        <plugins>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-project-info-reports-plugin</artifactId>
-                <reportSets>
-                    <reportSet>
-                        <reports>
-                            <report>dependencies</report>
-                        </reports>
-                    </reportSet>
-                </reportSets>
-            </plugin>
-        </plugins>
-    </reporting>
+            <resource>
+              <targetPath>/</targetPath>
+              <directory>src/main/webapp/</directory>
+              <excludes>
+                <exclude>web.xml</exclude>
+              </excludes>
+            </resource>
+          </webResources>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
 </project>
diff --git a/opendj-rest2ldap-servlet/src/main/java/org/forgerock/opendj/rest2ldap/servlet/Rest2LDAPAuthnFilter.java b/opendj-rest2ldap-servlet/src/main/java/org/forgerock/opendj/rest2ldap/servlet/Rest2LDAPAuthnFilter.java
deleted file mode 100644
index 70ed0f6..0000000
--- a/opendj-rest2ldap-servlet/src/main/java/org/forgerock/opendj/rest2ldap/servlet/Rest2LDAPAuthnFilter.java
+++ /dev/null
@@ -1,485 +0,0 @@
-/*
- * The contents of this file are subject to the terms of the Common Development and
- * Distribution License (the License). You may not use this file except in compliance with the
- * License.
- *
- * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
- * specific language governing permission and limitations under the License.
- *
- * When distributing Covered Software, include this CDDL Header Notice in each file and include
- * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
- * Header, with the fields enclosed by brackets [] replaced by your own identifying
- * information: "Portions copyright [year] [name of copyright owner]".
- *
- * Copyright 2013-2015 ForgeRock AS.
- */
-package org.forgerock.opendj.rest2ldap.servlet;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.Collections;
-import java.util.LinkedHashMap;
-import java.util.Map;
-import java.util.StringTokenizer;
-import java.util.concurrent.atomic.AtomicReference;
-
-import javax.servlet.Filter;
-import javax.servlet.FilterChain;
-import javax.servlet.FilterConfig;
-import javax.servlet.ServletException;
-import javax.servlet.ServletRequest;
-import javax.servlet.ServletResponse;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-import org.codehaus.jackson.JsonParser;
-import org.codehaus.jackson.map.ObjectMapper;
-import org.forgerock.json.fluent.JsonValue;
-import org.forgerock.json.fluent.JsonValueException;
-import org.forgerock.json.resource.ResourceException;
-import org.forgerock.json.resource.servlet.ServletApiVersionAdapter;
-import org.forgerock.json.resource.servlet.ServletSynchronizer;
-import org.forgerock.opendj.ldap.AuthenticationException;
-import org.forgerock.opendj.ldap.AuthorizationException;
-import org.forgerock.opendj.ldap.ByteString;
-import org.forgerock.opendj.ldap.Connection;
-import org.forgerock.opendj.ldap.ConnectionFactory;
-import org.forgerock.opendj.ldap.Connections;
-import org.forgerock.opendj.ldap.DN;
-import org.forgerock.opendj.ldap.EntryNotFoundException;
-import org.forgerock.opendj.ldap.LdapException;
-import org.forgerock.opendj.ldap.MultipleEntriesFoundException;
-import org.forgerock.opendj.ldap.ResultCode;
-import org.forgerock.opendj.ldap.SearchScope;
-import org.forgerock.opendj.ldap.requests.BindRequest;
-import org.forgerock.opendj.ldap.requests.SearchRequest;
-import org.forgerock.opendj.ldap.responses.BindResult;
-import org.forgerock.opendj.ldap.responses.SearchResultEntry;
-import org.forgerock.opendj.ldap.schema.Schema;
-import org.forgerock.opendj.rest2ldap.Rest2LDAP;
-import org.forgerock.util.AsyncFunction;
-import org.forgerock.util.promise.ExceptionHandler;
-import org.forgerock.util.promise.Promise;
-import org.forgerock.util.promise.ResultHandler;
-
-import static org.forgerock.json.resource.SecurityContext.*;
-import static org.forgerock.json.resource.servlet.SecurityContextFactory.*;
-import static org.forgerock.opendj.ldap.LdapException.*;
-import static org.forgerock.opendj.ldap.requests.Requests.*;
-import static org.forgerock.opendj.rest2ldap.Rest2LDAP.*;
-import static org.forgerock.opendj.rest2ldap.servlet.Rest2LDAPContextFactory.*;
-
-/**
- * An LDAP based authentication Servlet filter.
- * <p>
- * TODO: this is a work in progress. In particular, in order to embed this into
- * the OpenDJ HTTP listener it will need to provide a configuration API.
- */
-public final class Rest2LDAPAuthnFilter implements Filter {
-    /** Indicates how authentication should be performed. */
-    private static enum AuthenticationMethod {
-        SASL_PLAIN, SEARCH_SIMPLE, SIMPLE;
-    }
-
-    private static final String INIT_PARAM_CONFIG_FILE = "config-file";
-    private static final ObjectMapper JSON_MAPPER = new ObjectMapper().configure(
-            JsonParser.Feature.ALLOW_COMMENTS, true);
-
-    private String altAuthenticationPasswordHeader;
-    private String altAuthenticationUsernameHeader;
-    private AuthenticationMethod authenticationMethod = AuthenticationMethod.SEARCH_SIMPLE;
-    private ConnectionFactory bindLDAPConnectionFactory;
-    /** Indicates whether or not authentication should be performed. */
-    private boolean isEnabled;
-    private boolean reuseAuthenticatedConnection = true;
-    private String saslAuthzIdTemplate;
-    private final Schema schema = Schema.getDefaultSchema();
-    private DN searchBaseDN;
-    private String searchFilterTemplate;
-    private ConnectionFactory searchLDAPConnectionFactory;
-    private SearchScope searchScope = SearchScope.WHOLE_SUBTREE;
-    private boolean supportAltAuthentication;
-    private boolean supportHTTPBasicAuthentication = true;
-    private ServletApiVersionAdapter syncFactory;
-
-    @Override
-    public void destroy() {
-        if (searchLDAPConnectionFactory != null) {
-            searchLDAPConnectionFactory.close();
-        }
-        if (bindLDAPConnectionFactory != null) {
-            bindLDAPConnectionFactory.close();
-        }
-    }
-
-    @Override
-    public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain)
-            throws IOException, ServletException {
-        // Skip this filter if authentication has not been configured.
-        if (!isEnabled) {
-            chain.doFilter(request, response);
-            return;
-        }
-
-        // First of all parse the HTTP headers for authentication credentials.
-        if (!(request instanceof HttpServletRequest) || !(response instanceof HttpServletResponse)) {
-            // This should never happen.
-            throw new ServletException("non-HTTP request or response");
-        }
-
-        // TODO: support logout, sessions, reauth?
-        final HttpServletRequest req = (HttpServletRequest) request;
-        final HttpServletResponse res = (HttpServletResponse) response;
-
-        /*
-         * Store the authenticated connection so that it can be re-used by the
-         * servlet if needed. However, make sure that it is closed on
-         * completion.
-         */
-        final AtomicReference<Connection> savedConnection = new AtomicReference<>();
-        final ServletSynchronizer sync = syncFactory.createServletSynchronizer(req, res);
-
-        sync.addAsyncListener(new Runnable() {
-            @Override
-            public void run() {
-                closeConnection(savedConnection);
-            }
-        });
-
-        try {
-            final String headerUsername = supportAltAuthentication ? req.getHeader(altAuthenticationUsernameHeader)
-                    : null;
-            final String headerPassword = supportAltAuthentication ? req.getHeader(altAuthenticationPasswordHeader)
-                    : null;
-            final String headerAuthorization = supportHTTPBasicAuthentication ? req.getHeader("Authorization") : null;
-
-            final String username;
-            final char[] password;
-            if (headerUsername != null) {
-                if (headerPassword == null || headerUsername.isEmpty() || headerPassword.isEmpty()) {
-                    throw ResourceException.getException(401);
-                }
-                username = headerUsername;
-                password = headerPassword.toCharArray();
-            } else if (headerAuthorization != null) {
-                final StringTokenizer st = new StringTokenizer(headerAuthorization);
-                final String method = st.nextToken();
-                if (method == null || !HttpServletRequest.BASIC_AUTH.equalsIgnoreCase(method)) {
-                    throw ResourceException.getException(401);
-                }
-                final String b64Credentials = st.nextToken();
-                if (b64Credentials == null) {
-                    throw ResourceException.getException(401);
-                }
-                final String credentials = ByteString.valueOfBase64(b64Credentials).toString();
-                final String[] usernameAndPassword = credentials.split(":");
-                if (usernameAndPassword.length != 2) {
-                    throw ResourceException.getException(401);
-                }
-                username = usernameAndPassword[0];
-                password = usernameAndPassword[1].toCharArray();
-            } else {
-                throw ResourceException.getException(401);
-            }
-
-            // If we've got here then we have a username and password.
-            switch (authenticationMethod) {
-            case SIMPLE: {
-                final Map<String, Object> authzid;
-                authzid = new LinkedHashMap<>(2);
-                authzid.put(AUTHZID_DN, username);
-                authzid.put(AUTHZID_ID, username);
-                doBind(req, res, newSimpleBindRequest(username, password), chain, savedConnection, sync, username,
-                        authzid);
-                break;
-            }
-            case SASL_PLAIN: {
-                final Map<String, Object> authzid;
-                final String bindId;
-                if (saslAuthzIdTemplate.startsWith("dn:")) {
-                    final String bindDN = DN.format(saslAuthzIdTemplate.substring(3), schema, username).toString();
-                    bindId = "dn:" + bindDN;
-                    authzid = new LinkedHashMap<>(2);
-                    authzid.put(AUTHZID_DN, bindDN);
-                    authzid.put(AUTHZID_ID, username);
-                } else {
-                    bindId = String.format(saslAuthzIdTemplate, username);
-                    authzid = Collections.singletonMap(AUTHZID_ID, (Object) username);
-                }
-                doBind(req, res, newPlainSASLBindRequest(bindId, password), chain, savedConnection, sync, username,
-                        authzid);
-                break;
-            }
-            default: // SEARCH_SIMPLE
-            {
-                /*
-                 * First do a search to find the user's entry and then perform a
-                 * bind request using the user's DN.
-                 */
-                final org.forgerock.opendj.ldap.Filter filter = org.forgerock.opendj.ldap.Filter.format(
-                        searchFilterTemplate, username);
-                final SearchRequest searchRequest = newSearchRequest(searchBaseDN, searchScope, filter, "1.1");
-                searchLDAPConnectionFactory.getConnectionAsync()
-                        .thenAsync(new AsyncFunction<Connection, SearchResultEntry, LdapException>() {
-                            @Override
-                            public Promise<SearchResultEntry, LdapException> apply(Connection connection)
-                                    throws LdapException {
-                                savedConnection.set(connection);
-                                // Do the search.
-                                return connection.searchSingleEntryAsync(searchRequest);
-                            }
-                        }).thenOnResult(new ResultHandler<SearchResultEntry>() {
-                            @Override
-                            public void handleResult(final SearchResultEntry result) {
-                                savedConnection.get().close();
-                                final String bindDN = result.getName().toString();
-                                final Map<String, Object> authzid = new LinkedHashMap<>(2);
-                                authzid.put(AUTHZID_DN, bindDN);
-                                authzid.put(AUTHZID_ID, username);
-                                doBind(req, res, newSimpleBindRequest(bindDN, password), chain, savedConnection, sync,
-                                        username, authzid);
-                            }
-                        }).thenOnException(new ExceptionHandler<LdapException>() {
-                            @Override
-                            public void handleException(final LdapException exception) {
-                                LdapException normalizedError = exception;
-                                if (savedConnection.get() != null) {
-                                    savedConnection.get().close();
-                                    /*
-                                     * The search error should not be passed
-                                     * as-is back to the user.
-                                     */
-                                    if (exception instanceof EntryNotFoundException
-                                            || exception instanceof MultipleEntriesFoundException) {
-                                        normalizedError = newLdapException(ResultCode.INVALID_CREDENTIALS, exception);
-                                    } else if (exception instanceof AuthenticationException
-                                            || exception instanceof AuthorizationException) {
-                                        normalizedError =
-                                            newLdapException(ResultCode.CLIENT_SIDE_LOCAL_ERROR, exception);
-                                    } else {
-                                        normalizedError = exception;
-                                    }
-                                }
-                                sync.signalAndComplete(asResourceException(normalizedError));
-                            }
-                        });
-                break;
-            }
-            }
-            sync.awaitIfNeeded();
-            if (!sync.isAsync()) {
-                chain.doFilter(request, response);
-            }
-        } catch (final Throwable t) {
-            sync.signalAndComplete(t);
-        } finally {
-            if (!sync.isAsync()) {
-                closeConnection(savedConnection);
-            }
-        }
-    }
-
-    @Override
-    public void init(final FilterConfig config) throws ServletException {
-        // FIXME: make it possible to configure the filter externally, especially
-        // connection factories.
-        final String configFileName = config.getInitParameter(INIT_PARAM_CONFIG_FILE);
-        if (configFileName == null) {
-            throw new ServletException("Authentication filter initialization parameter '"
-                    + INIT_PARAM_CONFIG_FILE + "' not specified");
-        }
-        final InputStream configFile =
-                config.getServletContext().getResourceAsStream(configFileName);
-        if (configFile == null) {
-            throw new ServletException("Servlet filter configuration file '" + configFileName
-                    + "' not found");
-        }
-        try {
-            // Parse the config file.
-            final Object content = JSON_MAPPER.readValue(configFile, Object.class);
-            if (!(content instanceof Map)) {
-                throw new ServletException("Servlet filter configuration file '" + configFileName
-                        + "' does not contain a valid JSON configuration");
-            }
-
-            // Parse the authentication configuration.
-            final JsonValue configuration = new JsonValue(content);
-            final JsonValue authnConfig = configuration.get("authenticationFilter");
-            if (!authnConfig.isNull()) {
-                supportHTTPBasicAuthentication =
-                        authnConfig.get("supportHTTPBasicAuthentication").required().asBoolean();
-
-                // Alternative HTTP authentication.
-                supportAltAuthentication =
-                        authnConfig.get("supportAltAuthentication").required().asBoolean();
-                if (supportAltAuthentication) {
-                    altAuthenticationUsernameHeader =
-                            authnConfig.get("altAuthenticationUsernameHeader").required()
-                                    .asString();
-                    altAuthenticationPasswordHeader =
-                            authnConfig.get("altAuthenticationPasswordHeader").required()
-                                    .asString();
-                }
-
-                // Should the authenticated connection should be cached for use by subsequent LDAP operations?
-                reuseAuthenticatedConnection =
-                        authnConfig.get("reuseAuthenticatedConnection").required().asBoolean();
-
-                // Parse the authentication method and associated parameters.
-                authenticationMethod = parseAuthenticationMethod(authnConfig);
-                switch (authenticationMethod) {
-                case SIMPLE:
-                    // Nothing to do.
-                    break;
-                case SASL_PLAIN:
-                    saslAuthzIdTemplate =
-                            authnConfig.get("saslAuthzIdTemplate").required().asString();
-                    break;
-                case SEARCH_SIMPLE:
-                    searchBaseDN =
-                            DN.valueOf(authnConfig.get("searchBaseDN").required().asString(),
-                                    schema);
-                    searchScope = parseSearchScope(authnConfig);
-                    searchFilterTemplate =
-                            authnConfig.get("searchFilterTemplate").required().asString();
-
-                    // Parse the LDAP connection factory to be used for searches.
-                    final String ldapFactoryName =
-                            authnConfig.get("searchLDAPConnectionFactory").required().asString();
-                    searchLDAPConnectionFactory =
-                            Rest2LDAP.configureConnectionFactory(configuration.get(
-                                    "ldapConnectionFactories").required(), ldapFactoryName);
-                    break;
-                }
-
-                // Parse the LDAP connection factory to be used for binds.
-                final String ldapFactoryName =
-                        authnConfig.get("bindLDAPConnectionFactory").required().asString();
-                bindLDAPConnectionFactory =
-                        Rest2LDAP.configureConnectionFactory(configuration.get(
-                                "ldapConnectionFactories").required(), ldapFactoryName);
-
-                // Set the completion handler factory based on the Servlet API version.
-                syncFactory = ServletApiVersionAdapter.getInstance(config.getServletContext());
-
-                isEnabled = true;
-            }
-        } catch (final ServletException e) {
-            // Rethrow.
-            throw e;
-        } catch (final Exception e) {
-            throw new ServletException("Servlet filter configuration file '" + configFileName
-                    + "' could not be read: " + e.getMessage());
-        } finally {
-            try {
-                configFile.close();
-            } catch (final Exception e) {
-                // Ignore.
-            }
-        }
-    }
-
-    private void closeConnection(final AtomicReference<Connection> savedConnection) {
-        final Connection connection = savedConnection.get();
-        if (connection != null) {
-            connection.close();
-        }
-    }
-
-    /**
-     * Get a bind connection and then perform the bind operation, setting the
-     * cached connection and authorization credentials on completion.
-     */
-    private void doBind(final HttpServletRequest request, final ServletResponse response,
-            final BindRequest bindRequest, final FilterChain chain, final AtomicReference<Connection> savedConnection,
-            final ServletSynchronizer sync, final String authcid, final Map<String, Object> authzid) {
-        bindLDAPConnectionFactory.getConnectionAsync()
-                .thenAsync(new AsyncFunction<Connection, BindResult, LdapException>() {
-                    @Override
-                    public Promise<BindResult, LdapException> apply(Connection connection)
-                            throws LdapException {
-                        savedConnection.set(connection);
-                        return connection.bindAsync(bindRequest);
-                    }
-                }).thenOnResult(new ResultHandler<BindResult>() {
-                    @Override
-                    public void handleResult(final BindResult result) {
-                        /*
-                         * Cache the pre-authenticated connection and prevent
-                         * downstream components from closing it since this
-                         * filter will close it.
-                         */
-                        if (reuseAuthenticatedConnection) {
-                            request.setAttribute(ATTRIBUTE_AUTHN_CONNECTION,
-                                    Connections.uncloseable(savedConnection.get()));
-                        }
-
-                        // Pass through the authentication ID and authorization principals.
-                        request.setAttribute(ATTRIBUTE_AUTHCID, authcid);
-                        request.setAttribute(ATTRIBUTE_AUTHZID, authzid);
-
-                        // Invoke the remainder of the filter chain.
-                        sync.signal();
-                        if (sync.isAsync()) {
-                            try {
-                                chain.doFilter(request, response);
-
-                                /*
-                                 * Fix for OPENDJ-1105: Jetty 8 a bug where
-                                 * synchronous downstream completion (i.e. in
-                                 * the servlet) is ignored due to upstream
-                                 * active async context. The following code
-                                 * should be benign in other containers.
-                                 */
-                                if (response.isCommitted()) {
-                                    sync.signalAndComplete();
-                                }
-                            } catch (Throwable t) {
-                                sync.signalAndComplete(asResourceException(t));
-                            }
-                        }
-                    }
-                }).thenOnException(new ExceptionHandler<LdapException>() {
-                    @Override
-                    public void handleException(final LdapException exception) {
-                        sync.signalAndComplete(asResourceException(exception));
-                    }
-                });
-    }
-
-    private AuthenticationMethod parseAuthenticationMethod(final JsonValue configuration) {
-        if (configuration.isDefined("method")) {
-            final String method = configuration.get("method").asString();
-            if ("simple".equalsIgnoreCase(method)) {
-                return AuthenticationMethod.SIMPLE;
-            } else if ("sasl-plain".equalsIgnoreCase(method)) {
-                return AuthenticationMethod.SASL_PLAIN;
-            } else if ("search-simple".equalsIgnoreCase(method)) {
-                return AuthenticationMethod.SEARCH_SIMPLE;
-            } else {
-                throw new JsonValueException(configuration,
-                        "Illegal authentication method: must be either 'simple', "
-                                + "'sasl-plain', or 'search-simple'");
-            }
-        } else {
-            return AuthenticationMethod.SEARCH_SIMPLE;
-        }
-    }
-
-    private SearchScope parseSearchScope(final JsonValue configuration) {
-        if (configuration.isDefined("searchScope")) {
-            final String scope = configuration.get("searchScope").asString();
-            if ("sub".equalsIgnoreCase(scope)) {
-                return SearchScope.WHOLE_SUBTREE;
-            } else if ("one".equalsIgnoreCase(scope)) {
-                return SearchScope.SINGLE_LEVEL;
-            } else {
-                throw new JsonValueException(configuration,
-                        "Illegal search scope: must be either 'sub' or 'one'");
-            }
-        } else {
-            return SearchScope.WHOLE_SUBTREE;
-        }
-    }
-
-}
diff --git a/opendj-rest2ldap-servlet/src/main/java/org/forgerock/opendj/rest2ldap/servlet/Rest2LDAPConnectionFactoryProvider.java b/opendj-rest2ldap-servlet/src/main/java/org/forgerock/opendj/rest2ldap/servlet/Rest2LDAPConnectionFactoryProvider.java
deleted file mode 100644
index 4658711..0000000
--- a/opendj-rest2ldap-servlet/src/main/java/org/forgerock/opendj/rest2ldap/servlet/Rest2LDAPConnectionFactoryProvider.java
+++ /dev/null
@@ -1,160 +0,0 @@
-/*
- * The contents of this file are subject to the terms of the Common Development and
- * Distribution License (the License). You may not use this file except in compliance with the
- * License.
- *
- * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
- * specific language governing permission and limitations under the License.
- *
- * When distributing Covered Software, include this CDDL Header Notice in each file and include
- * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
- * Header, with the fields enclosed by brackets [] replaced by your own identifying
- * information: "Portions copyright [year] [name of copyright owner]".
- *
- * Copyright 2013 ForgeRock AS.
- */
-package org.forgerock.opendj.rest2ldap.servlet;
-
-import static org.forgerock.json.resource.Resources.*;
-import static org.forgerock.opendj.rest2ldap.Rest2LDAP.*;
-
-import java.io.InputStream;
-import java.util.Map;
-
-import javax.servlet.ServletConfig;
-import javax.servlet.ServletException;
-
-import org.codehaus.jackson.JsonParser;
-import org.codehaus.jackson.map.ObjectMapper;
-import org.forgerock.json.fluent.JsonValue;
-import org.forgerock.json.resource.CollectionResourceProvider;
-import org.forgerock.json.resource.Connection;
-import org.forgerock.json.resource.ConnectionFactory;
-import org.forgerock.json.resource.FutureResult;
-import org.forgerock.json.resource.ResourceException;
-import org.forgerock.json.resource.ResultHandler;
-import org.forgerock.json.resource.Router;
-import org.forgerock.opendj.rest2ldap.AuthorizationPolicy;
-import org.forgerock.opendj.rest2ldap.Rest2LDAP;
-import org.forgerock.opendj.rest2ldap.Rest2LDAP.Builder;
-import org.forgerock.util.Utils;
-
-/**
- * The connection factory provider which is used by the OpenDJ Commons REST LDAP
- * Gateway.
- */
-public final class Rest2LDAPConnectionFactoryProvider {
-    private static final String INIT_PARAM_CONFIG_FILE = "config-file";
-    private static final ObjectMapper JSON_MAPPER = new ObjectMapper().configure(
-            JsonParser.Feature.ALLOW_COMMENTS, true);
-
-    /**
-     * Returns a JSON resource connection factory configured using the
-     * configuration file named in the {@code config-file} Servlet
-     * initialization parameter. See the sample configuration file for a
-     * detailed description of its content.
-     *
-     * @param config
-     *            The Servlet configuration.
-     * @return The configured JSON resource connection factory.
-     * @throws ServletException
-     *             If the connection factory could not be initialized.
-     * @see Rest2LDAP#configureConnectionFactory(JsonValue, String)
-     * @see Builder#configureMapping(JsonValue)
-     */
-    public static ConnectionFactory getConnectionFactory(final ServletConfig config)
-            throws ServletException {
-        final String configFileName = config.getInitParameter(INIT_PARAM_CONFIG_FILE);
-        if (configFileName == null) {
-            throw new ServletException("Servlet initialization parameter '"
-                    + INIT_PARAM_CONFIG_FILE + "' not specified");
-        }
-        final InputStream configFile =
-                config.getServletContext().getResourceAsStream(configFileName);
-        if (configFile == null) {
-            throw new ServletException("Servlet configuration file '" + configFileName
-                    + "' not found");
-        }
-        try {
-            // Parse the config file.
-            final Object content = JSON_MAPPER.readValue(configFile, Object.class);
-            if (!(content instanceof Map)) {
-                throw new ServletException("Servlet configuration file '" + configFileName
-                        + "' does not contain a valid JSON configuration");
-            }
-            final JsonValue configuration = new JsonValue(content);
-
-            // Parse the authorization configuration.
-            final AuthorizationPolicy authzPolicy =
-                    configuration.get("servlet").get("authorizationPolicy").required().asEnum(
-                            AuthorizationPolicy.class);
-            final String proxyAuthzTemplate =
-                    configuration.get("servlet").get("proxyAuthzIdTemplate").asString();
-
-            // Parse the connection factory if present.
-            final String ldapFactoryName =
-                    configuration.get("servlet").get("ldapConnectionFactory").asString();
-            final org.forgerock.opendj.ldap.ConnectionFactory ldapFactory;
-            if (ldapFactoryName != null) {
-                ldapFactory =
-                        configureConnectionFactory(configuration.get("ldapConnectionFactories")
-                                .required(), ldapFactoryName);
-            } else {
-                ldapFactory = null;
-            }
-
-            // Create the router.
-            final Router router = new Router();
-            final JsonValue mappings = configuration.get("servlet").get("mappings").required();
-            for (final String mappingUrl : mappings.keys()) {
-                final JsonValue mapping = mappings.get(mappingUrl);
-                final CollectionResourceProvider provider =
-                        Rest2LDAP.builder().ldapConnectionFactory(ldapFactory).authorizationPolicy(
-                                authzPolicy).proxyAuthzIdTemplate(proxyAuthzTemplate)
-                                .configureMapping(mapping).build();
-                router.addRoute(mappingUrl, provider);
-            }
-            final ConnectionFactory factory = newInternalConnectionFactory(router);
-            if (ldapFactory != null) {
-                /*
-                 * Return a wrapper which will release resources associated with
-                 * the LDAP connection factory (pooled connections, transport,
-                 * etc).
-                 */
-                return new ConnectionFactory() {
-                    @Override
-                    public FutureResult<Connection> getConnectionAsync(
-                            ResultHandler<? super Connection> handler) {
-                        return factory.getConnectionAsync(handler);
-                    }
-
-                    @Override
-                    public Connection getConnection() throws ResourceException {
-                        return factory.getConnection();
-                    }
-
-                    @Override
-                    public void close() {
-                        ldapFactory.close();
-                    }
-                };
-            } else {
-                return factory;
-            }
-        } catch (final ServletException e) {
-            // Rethrow.
-            throw e;
-        } catch (final Exception e) {
-            throw new ServletException("Servlet configuration file '" + configFileName
-                    + "' could not be read: " + e.getMessage());
-        } finally {
-            Utils.closeSilently(configFile);
-        }
-    }
-
-    /** Prevent instantiation. */
-    private Rest2LDAPConnectionFactoryProvider() {
-        // Nothing to do.
-    }
-
-}
diff --git a/opendj-rest2ldap-servlet/src/main/java/org/forgerock/opendj/rest2ldap/servlet/Rest2LDAPContextFactory.java b/opendj-rest2ldap-servlet/src/main/java/org/forgerock/opendj/rest2ldap/servlet/Rest2LDAPContextFactory.java
deleted file mode 100644
index d16dc3d..0000000
--- a/opendj-rest2ldap-servlet/src/main/java/org/forgerock/opendj/rest2ldap/servlet/Rest2LDAPContextFactory.java
+++ /dev/null
@@ -1,176 +0,0 @@
-/*
- * The contents of this file are subject to the terms of the Common Development and
- * Distribution License (the License). You may not use this file except in compliance with the
- * License.
- *
- * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
- * specific language governing permission and limitations under the License.
- *
- * When distributing Covered Software, include this CDDL Header Notice in each file and include
- * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
- * Header, with the fields enclosed by brackets [] replaced by your own identifying
- * information: "Portions copyright [year] [name of copyright owner]".
- *
- * Copyright 2012-2014 ForgeRock AS.
- */
-package org.forgerock.opendj.rest2ldap.servlet;
-
-import javax.servlet.http.HttpServletRequest;
-
-import org.forgerock.json.resource.Context;
-import org.forgerock.json.resource.InternalServerErrorException;
-import org.forgerock.json.resource.ResourceException;
-import org.forgerock.json.resource.RootContext;
-import org.forgerock.json.resource.SecurityContext;
-import org.forgerock.json.resource.servlet.HttpServletContextFactory;
-import org.forgerock.json.resource.servlet.SecurityContextFactory;
-import org.forgerock.opendj.ldap.Connection;
-import org.forgerock.opendj.rest2ldap.AuthenticatedConnectionContext;
-
-/**
- * An HTTP servlet context factory which will create a {@link Context} chain
- * comprising of a {@link SecurityContext} and optionally an
- * {@link AuthenticatedConnectionContext}.
- * <p>
- * This class provides integration between Rest2LDAP HTTP Servlets and the
- * {@link Rest2LDAPAuthnFilter}, by providing a mechanism allowing the filter to
- * pass a pre-authenticated LDAP connection through to the underlying Rest2LDAP
- * implementation for use when performing subsequent LDAP operations. The
- * following code illustrates how an authentication Servlet filter can populate
- * the attributes:
- *
- * <pre>
- * public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
- *     // Authenticate the user.
- *     String username = getUserName(request);
- *     String password = getPassword(request);
- *     final Connection connection = getLDAPConnection();
- *
- *     // Publish the authenticated connection.
- *     try {
- *         connection.bind(username, password.toCharArray());
- *         request.setAttribute(ATTRIBUTE_AUTHN_CONNECTION, connection);
- *     } catch (LdapException e) {
- *         // Fail the HTTP request.
- *         response.setStatus(...);
- *         return;
- *     }
- *
- *     // Invoke the rest of the filter chain and then release the LDAP connection once
- *     // processing has completed. Note that this assumes that the filter chain is
- *     // processes requests synchronous.
- *     try {
- *         chain.doFilter(request, response);
- *     } finally {
- *         connection.close();
- *     }
- * }
- * </pre>
- */
-public final class Rest2LDAPContextFactory implements HttpServletContextFactory {
-
-    /**
-     * The name of the HTTP Servlet Request attribute where this factory expects
-     * to find the authenticated user's authentication ID. The name of this
-     * attribute is {@code org.forgerock.security.authcid} and it MUST contain a
-     * {@code String} if it is present.
-     *
-     * @see AuthenticatedConnectionContext
-     */
-    public static final String ATTRIBUTE_AUTHN_CONNECTION =
-            "org.forgerock.opendj.rest2ldap.authn-connection";
-
-    /** Singleton instance. */
-    private static final Rest2LDAPContextFactory INSTANCE = new Rest2LDAPContextFactory();
-
-    /**
-     * Returns the singleton context factory which can be used for obtaining
-     * context information from a HTTP servlet request.
-     * <p>
-     * This method is named {@code getHttpServletContextFactory} so that it can
-     * easily be used for
-     * {@link org.forgerock.json.resource.servlet.HttpServlet#getHttpServletContextFactory
-     * configuring} JSON Resource Servlets.
-     *
-     * @return The singleton context factory.
-     */
-    public static Rest2LDAPContextFactory getHttpServletContextFactory() {
-        return INSTANCE;
-    }
-
-    private Rest2LDAPContextFactory() {
-        // Prevent instantiation.
-    }
-
-    /**
-     * Creates a new {@link Context} chain comprising of the provided parent
-     * context(s), a {@link SecurityContext} obtained using a
-     * {@link SecurityContextFactory} , and optionally a
-     * {@code AuthenticatedConnectionContext}. The authenticated connection will
-     * be obtained from the {@link #ATTRIBUTE_AUTHN_CONNECTION} attribute
-     * contained in the provided HTTP servlet request. If the attribute is not
-     * present then the {@code AuthenticatedConnectionContext} will not be
-     * created.
-     *
-     * @param parent
-     *            The parent context.
-     * @param request
-     *            The HTTP servlet request from which the security and
-     *            authenticated connection attributes should be obtained.
-     * @return A new {@link Context} chain comprising of the provided parent
-     *         context(s), a {@link SecurityContext} obtained using a
-     *         {@link SecurityContextFactory} , and optionally a
-     *         {@code AuthenticatedConnectionContext}.
-     * @throws ResourceException
-     *             If one of the attributes was present but had the wrong type.
-     */
-    public Context createContext(final Context parent, final HttpServletRequest request)
-            throws ResourceException {
-        // First get the security context.
-        final Context securityContext =
-                SecurityContextFactory.getHttpServletContextFactory()
-                        .createContext(parent, request);
-
-        // Now append the pre-authenticated connection context if required.
-        final Connection connection;
-        try {
-            connection = (Connection) request.getAttribute(ATTRIBUTE_AUTHN_CONNECTION);
-        } catch (final ClassCastException e) {
-            throw new InternalServerErrorException(
-                    "The rest2ldap authenticated connection context could not be "
-                            + "created because the connection attribute, "
-                            + ATTRIBUTE_AUTHN_CONNECTION + ", contained in the HTTP "
-                            + "servlet request did not have the correct type", e);
-        }
-        if (connection != null) {
-            return new AuthenticatedConnectionContext(securityContext, connection);
-        } else {
-            return securityContext;
-        }
-    }
-
-    /**
-     * Creates a new {@link Context} chain comprising of a {@link RootContext},
-     * a {@link SecurityContext} obtained using a {@link SecurityContextFactory}
-     * , and optionally a {@code AuthenticatedConnectionContext}. The
-     * authenticated connection will be obtained from the
-     * {@link #ATTRIBUTE_AUTHN_CONNECTION} attribute contained in the provided
-     * HTTP servlet request. If the attribute is not present then the
-     * {@code AuthenticatedConnectionContext} will not be created.
-     *
-     * @param request
-     *            The HTTP servlet request from which the security and
-     *            authenticated connection attributes should be obtained.
-     * @return A new {@link Context} chain comprising of a {@link RootContext},
-     *         a {@link SecurityContext} obtained using a
-     *         {@link SecurityContextFactory} , and optionally a
-     *         {@code AuthenticatedConnectionContext}.
-     * @throws ResourceException
-     *             If one of the attributes was present but had the wrong type.
-     */
-    @Override
-    public Context createContext(final HttpServletRequest request) throws ResourceException {
-        return createContext(new RootContext(), request);
-    }
-
-}
diff --git a/opendj-rest2ldap-servlet/src/main/java/org/forgerock/opendj/rest2ldap/servlet/package-info.java b/opendj-rest2ldap-servlet/src/main/java/org/forgerock/opendj/rest2ldap/servlet/package-info.java
deleted file mode 100644
index 53ce09f..0000000
--- a/opendj-rest2ldap-servlet/src/main/java/org/forgerock/opendj/rest2ldap/servlet/package-info.java
+++ /dev/null
@@ -1,20 +0,0 @@
-/*
- * The contents of this file are subject to the terms of the Common Development and
- * Distribution License (the License). You may not use this file except in compliance with the
- * License.
- *
- * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
- * specific language governing permission and limitations under the License.
- *
- * When distributing Covered Software, include this CDDL Header Notice in each file and include
- * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
- * Header, with the fields enclosed by brackets [] replaced by your own identifying
- * information: "Portions copyright [year] [name of copyright owner]".
- *
- * Copyright 2013 ForgeRock AS.
- */
-
-/**
- * Provides integration between the OpenDJ Commons REST Adapter and Servlet APIs.
- */
-package org.forgerock.opendj.rest2ldap.servlet;
diff --git a/opendj-rest2ldap-servlet/src/main/webapp/META-INF/services/org.forgerock.http.HttpApplication b/opendj-rest2ldap-servlet/src/main/webapp/META-INF/services/org.forgerock.http.HttpApplication
new file mode 100644
index 0000000..40ee013
--- /dev/null
+++ b/opendj-rest2ldap-servlet/src/main/webapp/META-INF/services/org.forgerock.http.HttpApplication
@@ -0,0 +1,16 @@
+#
+# The contents of this file are subject to the terms of the Common Development and
+# Distribution License (the License). You may not use this file except in compliance with the
+# License.
+#
+# You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+# specific language governing permission and limitations under the License.
+#
+# When distributing Covered Software, include this CDDL Header Notice in each file and include
+# the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+# Header, with the fields enclosed by brackets [] replaced by your own identifying
+# information: "Portions copyright [year] [name of copyright owner]".
+#
+# Copyright 2015 ForgeRock AS.
+#
+org.forgerock.opendj.rest2ldap.Rest2LDAPHttpApplication
diff --git a/opendj-rest2ldap-servlet/src/main/webapp/opendj-rest2ldap-servlet.json b/opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/opendj-rest2ldap-config.json
similarity index 100%
rename from opendj-rest2ldap-servlet/src/main/webapp/opendj-rest2ldap-servlet.json
rename to opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/opendj-rest2ldap-config.json
diff --git a/opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/web.xml b/opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/web.xml
index 2ba6b1c..42655ef 100644
--- a/opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/web.xml
+++ b/opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/web.xml
@@ -1,49 +1,34 @@
+<!--
+  ! The contents of this file are subject to the terms of the Common Development and
+  ! Distribution License (the License). You may not use this file except in compliance with the
+  ! License.
+  !
+  ! You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+  ! specific language governing permission and limitations under the License.
+  !
+  ! When distributing Covered Software, include this CDDL Header Notice in each file and include
+  ! the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+  ! Header, with the fields enclosed by brackets [] replaced by your own identifying
+  ! information: "Portions Copyright [year] [name of copyright owner]".
+  !
+  ! Copyright 2015 ForgeRock AS.
+  !
+-->
 <?xml version="1.0" encoding="UTF-8"?>
 <web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
-    version="2.5">
+    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
+    version="3.0">
 
-    <display-name>Json Resource Servlet</display-name>
+    <display-name>OpenDJ REST LDAP Gateway</display-name>
 
     <servlet>
-        <servlet-name>OpenDJ Commons REST LDAP Gateway</servlet-name>
-        <servlet-class>org.forgerock.json.resource.servlet.HttpServlet</servlet-class>
-
-        <init-param>
-            <param-name>connection-factory-class</param-name>
-            <param-value>org.forgerock.opendj.rest2ldap.servlet.Rest2LDAPConnectionFactoryProvider</param-value>
-        </init-param>
-
-        <init-param>
-            <param-name>config-file</param-name>
-            <param-value>/opendj-rest2ldap-servlet.json</param-value>
-        </init-param>
-        
-         <init-param>
-            <param-name>context-factory-class</param-name>
-            <param-value>org.forgerock.opendj.rest2ldap.servlet.Rest2LDAPContextFactory</param-value>
-        </init-param>
+        <servlet-name>OpenDJ REST LDAP Gateway</servlet-name>
+        <servlet-class>org.forgerock.http.servlet.HttpFrameworkServlet</servlet-class>
+        <async-supported>true</async-supported>
     </servlet>
-    
+
     <servlet-mapping>
-        <servlet-name>OpenDJ Commons REST LDAP Gateway</servlet-name>
+        <servlet-name>OpenDJ REST LDAP Gateway</servlet-name>
         <url-pattern>/*</url-pattern>
     </servlet-mapping>
-
-    <filter>
-        <filter-name>OpenDJ Commons REST LDAP Authentication Filter</filter-name>
-        <filter-class>org.forgerock.opendj.rest2ldap.servlet.Rest2LDAPAuthnFilter</filter-class>
-
-        <init-param>
-            <param-name>config-file</param-name>
-            <param-value>/opendj-rest2ldap-servlet.json</param-value>
-        </init-param>
-    </filter>
-
-    <filter-mapping>
-        <filter-name>OpenDJ Commons REST LDAP Authentication Filter</filter-name>
-        <url-pattern>/*</url-pattern>
-    </filter-mapping>
-
 </web-app>
- 
\ No newline at end of file
diff --git a/opendj-rest2ldap/pom.xml b/opendj-rest2ldap/pom.xml
index c800614..06605ec 100644
--- a/opendj-rest2ldap/pom.xml
+++ b/opendj-rest2ldap/pom.xml
@@ -12,7 +12,7 @@
   ! Header, with the fields enclosed by brackets [] replaced by your own identifying
   ! information: "Portions Copyright [year] [name of copyright owner]".
   !
-  ! Copyright 2012 ForgeRock AS.
+  ! Copyright 2012-2015 ForgeRock AS.
   !
 -->
 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
@@ -39,13 +39,13 @@
         </dependency>
         <dependency>
             <groupId>org.forgerock.commons</groupId>
-            <artifactId>json-fluent</artifactId>
+            <artifactId>json-resource</artifactId>
             <version>${forgerockRestVersion}</version>
         </dependency>
         <dependency>
-            <groupId>org.forgerock.commons</groupId>
-            <artifactId>json-resource</artifactId>
-            <version>${forgerockRestVersion}</version>
+          <groupId>org.forgerock.commons</groupId>
+          <artifactId>json-resource-http</artifactId>
+          <version>${forgerockRestVersion}</version>
         </dependency>
         <dependency>
             <groupId>org.forgerock</groupId>
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractLDAPAttributeMapper.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractLDAPAttributeMapper.java
index 3e7803e..dcf269f 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractLDAPAttributeMapper.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractLDAPAttributeMapper.java
@@ -21,7 +21,6 @@
 import static org.forgerock.opendj.rest2ldap.Rest2LDAP.asResourceException;
 import static org.forgerock.opendj.rest2ldap.Utils.i18n;
 import static org.forgerock.opendj.rest2ldap.Utils.isNullOrEmpty;
-import static org.forgerock.opendj.rest2ldap.Utils.transform;
 import static org.forgerock.opendj.rest2ldap.WritabilityPolicy.READ_WRITE;
 
 import java.util.ArrayList;
@@ -29,13 +28,12 @@
 import java.util.List;
 import java.util.Set;
 
-import org.forgerock.json.fluent.JsonPointer;
-import org.forgerock.json.fluent.JsonValue;
+import org.forgerock.json.JsonPointer;
+import org.forgerock.json.JsonValue;
 import org.forgerock.json.resource.BadRequestException;
 import org.forgerock.json.resource.NotSupportedException;
 import org.forgerock.json.resource.PatchOperation;
 import org.forgerock.json.resource.ResourceException;
-import org.forgerock.json.resource.ResultHandler;
 import org.forgerock.opendj.ldap.Attribute;
 import org.forgerock.opendj.ldap.AttributeDescription;
 import org.forgerock.opendj.ldap.Entry;
@@ -43,14 +41,14 @@
 import org.forgerock.opendj.ldap.Modification;
 import org.forgerock.opendj.ldap.ModificationType;
 import org.forgerock.util.Function;
-import org.forgerock.util.promise.NeverThrowsException;
+import org.forgerock.util.promise.Promise;
+import org.forgerock.util.promise.Promises;
 
 /**
  * An abstract LDAP attribute mapper which provides a simple mapping from a JSON
  * value to a single LDAP attribute.
  */
-abstract class AbstractLDAPAttributeMapper<T extends AbstractLDAPAttributeMapper<T>> extends
-        AttributeMapper {
+abstract class AbstractLDAPAttributeMapper<T extends AbstractLDAPAttributeMapper<T>> extends AttributeMapper {
     List<Object> defaultJSONValues = emptyList();
     final AttributeDescription ldapAttributeName;
     private boolean isRequired;
@@ -101,25 +99,45 @@
     }
 
     @Override
-    void create(final Context c, final JsonPointer path, final JsonValue v,
-            final ResultHandler<List<Attribute>> h) {
-        getNewLDAPAttributes(c, path, v, createAttributeHandler(path, h));
+    Promise<List<Attribute>, ResourceException> create(
+            final RequestState requestState, final JsonPointer path, final JsonValue v) {
+        return getNewLDAPAttributes(requestState, path, v).then(
+            new Function<Attribute, List<Attribute>, ResourceException>() {
+                @Override
+                public List<Attribute> apply(Attribute newLDAPAttribute) throws ResourceException {
+                    if (!writabilityPolicy.canCreate(ldapAttributeName)) {
+                        if (!newLDAPAttribute.isEmpty() && !writabilityPolicy.discardWrites()) {
+                            throw new BadRequestException(i18n("The request cannot be processed because it attempts "
+                                    + "to create the read-only field '%s'", path));
+                        }
+                        return Collections.emptyList();
+                    } else if (newLDAPAttribute.isEmpty()) {
+                        if (isRequired) {
+                            throw new BadRequestException(i18n("The request cannot be processed because it attempts "
+                                    + "to remove the required field '%s'", path));
+                        }
+                        return Collections.emptyList();
+                    }
+
+                    return singletonList(newLDAPAttribute);
+                }
+            });
     }
 
     @Override
-    void getLDAPAttributes(final Context c, final JsonPointer path, final JsonPointer subPath,
-            final Set<String> ldapAttributes) {
+    void getLDAPAttributes(final RequestState requestState, final JsonPointer path,
+            final JsonPointer subPath, final Set<String> ldapAttributes) {
         ldapAttributes.add(ldapAttributeName.toString());
     }
 
-    abstract void getNewLDAPAttributes(Context c, JsonPointer path, List<Object> newValues,
-            ResultHandler<Attribute> h);
+    abstract Promise<Attribute, ResourceException> getNewLDAPAttributes(
+                RequestState requestState, JsonPointer path, List<Object> newValues);
 
     abstract T getThis();
 
     @Override
-    void patch(final Context c, final JsonPointer path, final PatchOperation operation,
-            final ResultHandler<List<Modification>> h) {
+    Promise<List<Modification>, ResourceException> patch(
+                final RequestState requestState, final JsonPointer path, final PatchOperation operation) {
         try {
             final JsonPointer field = operation.getField();
             final JsonValue v = operation.getValue();
@@ -235,33 +253,95 @@
             if (newValues.isEmpty()) {
                 // Deleting the attribute.
                 if (isRequired) {
-                    h.handleError(new BadRequestException(i18n(
-                            "The request cannot be processed because it attempts to remove "
-                                    + "the required field '%s'", path)));
+                    return Promises.<List<Modification>, ResourceException> newExceptionPromise(
+                            new BadRequestException(i18n(
+                                "The request cannot be processed because it attempts to remove the required field '%s'",
+                                path)));
                 } else {
-                    h.handleResult(singletonList(new Modification(modType,
-                            emptyAttribute(ldapAttributeName))));
+                    return Promises.newResultPromise(
+                        singletonList(new Modification(modType, emptyAttribute(ldapAttributeName))));
                 }
             } else {
-                getNewLDAPAttributes(c, path, newValues, transform(
-                        new Function<Attribute, List<Modification>, NeverThrowsException>() {
+                return getNewLDAPAttributes(requestState, path, newValues)
+                        .then(new Function<Attribute, List<Modification>, ResourceException>() {
                             @Override
                             public List<Modification> apply(final Attribute value) {
                                 return singletonList(new Modification(modType, value));
                             }
-                        }, h));
+                        });
             }
         } catch (final RuntimeException e) {
-            h.handleError(asResourceException(e));
+            return Promises.newExceptionPromise(asResourceException(e));
         } catch (final ResourceException e) {
-            h.handleError(e);
+            return Promises.newExceptionPromise(e);
         }
     }
 
     @Override
-    void update(final Context c, final JsonPointer path, final Entry e, final JsonValue v,
-            final ResultHandler<List<Modification>> h) {
-        getNewLDAPAttributes(c, path, v, updateAttributeHandler(path, e, h));
+    Promise<List<Modification>, ResourceException> update(
+            final RequestState requestState, final JsonPointer path, final Entry e, final JsonValue v) {
+        return getNewLDAPAttributes(requestState, path, v).then(
+            new Function<Attribute, List<Modification>, ResourceException>() {
+                @Override
+                public List<Modification> apply(final Attribute newLDAPAttribute) throws ResourceException {
+                    // Get the existing LDAP attribute.
+                    final Attribute tmp = e.getAttribute(ldapAttributeName);
+                    final Attribute oldLDAPAttribute = tmp != null ? tmp : emptyAttribute(ldapAttributeName);
+                    /*
+                     * If the attribute is read-only then handle the following cases:
+                     * 1) new values are provided and they are the same as the existing values
+                     * 2) no new values are provided.
+                     */
+                    if (!writabilityPolicy.canWrite(ldapAttributeName)) {
+                        if (newLDAPAttribute.isEmpty()
+                                || newLDAPAttribute.equals(oldLDAPAttribute)
+                                || writabilityPolicy.discardWrites()) {
+                            // No change.
+                            return Collections.emptyList();
+                        }
+                        throw new BadRequestException(i18n(
+                            "The request cannot be processed because it attempts to modify the read-only field '%s'",
+                            path));
+                    }
+
+                    if (oldLDAPAttribute.isEmpty() && newLDAPAttribute.isEmpty()) {
+                        // No change.
+                        return Collections.emptyList();
+                    } else if (oldLDAPAttribute.isEmpty()) {
+                        // The attribute is being added.
+                        return singletonList(new Modification(ModificationType.REPLACE, newLDAPAttribute));
+                    } else if (newLDAPAttribute.isEmpty()) {
+                        // The attribute is being deleted - this is not allowed if the attribute is required.
+                        if (isRequired) {
+                            throw new BadRequestException(i18n(
+                                "The request cannot be processed because it attempts to remove the required field '%s'",
+                                path));
+                        }
+                        return singletonList(new Modification(ModificationType.REPLACE, newLDAPAttribute));
+                    } else {
+                        /*
+                         * We could do a replace, but try to save bandwidth and send diffs instead.
+                         * Perform deletes first in case we don't have an appropriate normalizer:
+                         * permissive add(x) followed by delete(x) is destructive, whereas
+                         * delete(x) followed by add(x) is idempotent when adding/removing the same value.
+                         */
+                        final List<Modification> modifications = new ArrayList<>(2);
+
+                        final Attribute deletedValues = new LinkedAttribute(oldLDAPAttribute);
+                        deletedValues.removeAll(newLDAPAttribute);
+                        if (!deletedValues.isEmpty()) {
+                            modifications.add(new Modification(ModificationType.DELETE, deletedValues));
+                        }
+
+                        final Attribute addedValues = new LinkedAttribute(newLDAPAttribute);
+                        addedValues.removeAll(oldLDAPAttribute);
+                        if (!addedValues.isEmpty()) {
+                            modifications.add(new Modification(ModificationType.ADD, addedValues));
+                        }
+                        return modifications;
+                    }
+                }
+            });
     }
 
     private List<Object> asList(final JsonValue v, final List<Object> defaultValues) {
@@ -290,142 +370,21 @@
         }
     }
 
-    private ResultHandler<Attribute> createAttributeHandler(final JsonPointer path,
-            final ResultHandler<List<Attribute>> h) {
-        return new ResultHandler<Attribute>() {
-            @Override
-            public void handleError(final ResourceException error) {
-                h.handleError(error);
-            }
-
-            @Override
-            public void handleResult(final Attribute newLDAPAttribute) {
-                if (!writabilityPolicy.canCreate(ldapAttributeName)) {
-                    if (newLDAPAttribute.isEmpty() || writabilityPolicy.discardWrites()) {
-                        h.handleResult(Collections.<Attribute> emptyList());
-                    } else {
-                        h.handleError(new BadRequestException(i18n(
-                                "The request cannot be processed because it attempts to create "
-                                        + "the read-only field '%s'", path)));
-                    }
-                } else if (newLDAPAttribute.isEmpty()) {
-                    if (isRequired) {
-                        h.handleError(new BadRequestException(i18n(
-                                "The request cannot be processed because it attempts to remove "
-                                        + "the required field '%s'", path)));
-                        return;
-                    } else {
-                        h.handleResult(Collections.<Attribute> emptyList());
-                    }
-                } else {
-                    h.handleResult(singletonList(newLDAPAttribute));
-                }
-            }
-        };
-    }
-
-    private void getNewLDAPAttributes(final Context c, final JsonPointer path, final JsonValue v,
-            final ResultHandler<Attribute> attributeHandler) {
+    private Promise<Attribute, ResourceException> getNewLDAPAttributes(
+            final RequestState requestState, final JsonPointer path, final JsonValue v) {
         try {
             // Ensure that the value is of the correct type.
             checkSchema(path, v);
             final List<Object> newValues = asList(v, defaultJSONValues);
             if (newValues.isEmpty()) {
                 // Skip sub-class implementation if there are no values.
-                attributeHandler.handleResult(emptyAttribute(ldapAttributeName));
+                return Promises.newResultPromise(emptyAttribute(ldapAttributeName));
             } else {
-                getNewLDAPAttributes(c, path, newValues, attributeHandler);
+                return getNewLDAPAttributes(requestState, path, newValues);
             }
         } catch (final Exception ex) {
-            attributeHandler.handleError(asResourceException(ex));
+            return Promises.newExceptionPromise(asResourceException(ex));
         }
     }
 
-    private ResultHandler<Attribute> updateAttributeHandler(final JsonPointer path, final Entry e,
-            final ResultHandler<List<Modification>> h) {
-        // Get the existing LDAP attribute.
-        final Attribute tmp = e.getAttribute(ldapAttributeName);
-        final Attribute oldLDAPAttribute = tmp != null ? tmp : emptyAttribute(ldapAttributeName);
-        return new ResultHandler<Attribute>() {
-            @Override
-            public void handleError(final ResourceException error) {
-                h.handleError(error);
-            }
-
-            @Override
-            public void handleResult(final Attribute newLDAPAttribute) {
-                /*
-                 * If the attribute is read-only then handle the following
-                 * cases:
-                 *
-                 * 1) new values are provided and they are the same as the
-                 * existing values
-                 *
-                 * 2) no new values are provided.
-                 */
-                if (!writabilityPolicy.canWrite(ldapAttributeName)) {
-                    if (newLDAPAttribute.isEmpty() || newLDAPAttribute.equals(oldLDAPAttribute)
-                            || writabilityPolicy.discardWrites()) {
-                        // No change.
-                        h.handleResult(Collections.<Modification> emptyList());
-                    } else {
-                        h.handleError(new BadRequestException(i18n(
-                                "The request cannot be processed because it attempts to modify "
-                                        + "the read-only field '%s'", path)));
-                    }
-                } else {
-                    // Compute the changes to the attribute.
-                    final List<Modification> modifications;
-                    if (oldLDAPAttribute.isEmpty() && newLDAPAttribute.isEmpty()) {
-                        // No change.
-                        modifications = Collections.<Modification> emptyList();
-                    } else if (oldLDAPAttribute.isEmpty()) {
-                        // The attribute is being added.
-                        modifications =
-                                singletonList(new Modification(ModificationType.REPLACE,
-                                        newLDAPAttribute));
-                    } else if (newLDAPAttribute.isEmpty()) {
-                        /*
-                         * The attribute is being deleted - this is not allowed
-                         * if the attribute is required.
-                         */
-                        if (isRequired) {
-                            h.handleError(new BadRequestException(i18n(
-                                    "The request cannot be processed because it attempts to remove "
-                                            + "the required field '%s'", path)));
-                            return;
-                        } else {
-                            modifications =
-                                    singletonList(new Modification(ModificationType.REPLACE,
-                                            newLDAPAttribute));
-                        }
-                    } else {
-                        /*
-                         * We could do a replace, but try to save bandwidth and
-                         * send diffs instead. Perform deletes first in case we
-                         * don't have an appropriate normalizer: permissive
-                         * add(x) followed by delete(x) is destructive, whereas
-                         * delete(x) followed by add(x) is idempotent when
-                         * adding/removing the same value.
-                         */
-                        modifications = new ArrayList<>(2);
-
-                        final Attribute deletedValues = new LinkedAttribute(oldLDAPAttribute);
-                        deletedValues.removeAll(newLDAPAttribute);
-                        if (!deletedValues.isEmpty()) {
-                            modifications.add(new Modification(ModificationType.DELETE,
-                                    deletedValues));
-                        }
-
-                        final Attribute addedValues = new LinkedAttribute(newLDAPAttribute);
-                        addedValues.removeAll(oldLDAPAttribute);
-                        if (!addedValues.isEmpty()) {
-                            modifications.add(new Modification(ModificationType.ADD, addedValues));
-                        }
-                    }
-                    h.handleResult(modifications);
-                }
-            }
-        };
-    }
 }
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AttributeMapper.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AttributeMapper.java
index 53a7602..d0fd363 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AttributeMapper.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AttributeMapper.java
@@ -11,26 +11,24 @@
  * Header, with the fields enclosed by brackets [] replaced by your own identifying
  * information: "Portions Copyright [year] [name of copyright owner]".
  *
- * Copyright 2012-2013 ForgeRock AS.
+ * Copyright 2012-2015 ForgeRock AS.
  */
 package org.forgerock.opendj.rest2ldap;
 
 import java.util.List;
 import java.util.Set;
 
-import org.forgerock.json.fluent.JsonPointer;
-import org.forgerock.json.fluent.JsonValue;
+import org.forgerock.json.JsonPointer;
+import org.forgerock.json.JsonValue;
 import org.forgerock.json.resource.PatchOperation;
-import org.forgerock.json.resource.ResultHandler;
+import org.forgerock.json.resource.ResourceException;
 import org.forgerock.opendj.ldap.Attribute;
 import org.forgerock.opendj.ldap.Entry;
 import org.forgerock.opendj.ldap.Filter;
 import org.forgerock.opendj.ldap.Modification;
+import org.forgerock.util.promise.Promise;
 
-/**
- * An attribute mapper is responsible for converting JSON values to and from
- * LDAP attributes.
- */
+/** An attribute mapper is responsible for converting JSON values to and from LDAP attributes. */
 public abstract class AttributeMapper {
     /*
      * This interface is an abstract class so that methods can be made package
@@ -42,19 +40,18 @@
     }
 
     /**
-     * Maps a JSON value to one or more LDAP attributes, invoking a completion
-     * handler once the transformation has completed. This method is invoked
-     * when a REST resource is created using a create request.
+     * Maps a JSON value to one or more LDAP attributes, returning a promise
+     * once the transformation has completed. This method is invoked when a REST
+     * resource is created using a create request.
      * <p>
      * If the JSON value corresponding to this mapper is not present in the
      * resource then this method will be invoked with a value of {@code null}.
      * It is the responsibility of the mapper implementation to take appropriate
      * action in this case, perhaps by substituting default LDAP values, or by
-     * rejecting the update by invoking the result handler's
-     * {@link ResultHandler#handleError handleError} method.
+     * returning a failed promise with an appropriate {@link ResourceException}.
      *
-     * @param c
-     *            The context.
+     * @param requestState
+     *            The request state.
      * @param path
      *            The pointer from the root of the JSON resource to this
      *            attribute mapper. This may be used when constructing error
@@ -63,10 +60,10 @@
      *            The JSON value to be converted to LDAP attributes, which may
      *            be {@code null} indicating that the JSON value was not present
      *            in the resource.
-     * @param h
-     *            The result handler.
+     * @return A {@link Promise} containing the result of the operation.
      */
-    abstract void create(Context c, JsonPointer path, JsonValue v, ResultHandler<List<Attribute>> h);
+    abstract Promise<List<Attribute>, ResourceException> create(
+            RequestState requestState, JsonPointer path, JsonValue v);
 
     /**
      * Adds the names of the LDAP attributes required by this attribute mapper
@@ -75,8 +72,8 @@
      * Implementations should only add the names of attributes found in the LDAP
      * entry directly associated with the resource.
      *
-     * @param c
-     *            The context.
+     * @param requestState
+     *            The request state.
      * @param path
      *            The pointer from the root of the JSON resource to this
      *            attribute mapper. This may be used when constructing error
@@ -89,21 +86,20 @@
      *            The set into which the required LDAP attribute names should be
      *            put.
      */
-    abstract void getLDAPAttributes(Context c, JsonPointer path, JsonPointer subPath,
-            Set<String> ldapAttributes);
+    abstract void getLDAPAttributes(
+            RequestState requestState, JsonPointer path, JsonPointer subPath, Set<String> ldapAttributes);
 
     /**
      * Transforms the provided REST comparison filter parameters to an LDAP
-     * filter representation, invoking a completion handler once the
-     * transformation has completed.
+     * filter representation, returning a promise once the transformation has
+     * completed.
      * <p>
-     * If an error occurred while constructing the LDAP filter, then the result
-     * handler's {@link ResultHandler#handleError handleError} method must be
-     * invoked with an appropriate exception indicating the problem which
-     * occurred.
+     * If an error occurred while constructing the LDAP filter, then a failed
+     * promise must be returned with an appropriate {@link ResourceException}
+     * indicating the problem which occurred.
      *
-     * @param c
-     *            The context.
+     * @param requestState
+     *            The request state.
      * @param path
      *            The pointer from the root of the JSON resource to this
      *            attribute mapper. This may be used when constructing error
@@ -121,19 +117,18 @@
      * @param valueAssertion
      *            The value assertion, or {@code null} if {@code type} is
      *            {@link FilterType#PRESENT}.
-     * @param h
-     *            The result handler.
+     * @return A {@link Promise} containing the result of the operation.
      */
-    abstract void getLDAPFilter(Context c, JsonPointer path, JsonPointer subPath, FilterType type,
-            String operator, Object valueAssertion, ResultHandler<Filter> h);
+    abstract Promise<Filter, ResourceException> getLDAPFilter(RequestState requestState, JsonPointer path,
+            JsonPointer subPath, FilterType type, String operator, Object valueAssertion);
 
     /**
-     * Maps a JSON patch operation to one or more LDAP modifications, invoking a
-     * completion handler once the transformation has completed. This method is
-     * invoked when a REST resource is modified using a patch request.
+     * Maps a JSON patch operation to one or more LDAP modifications, returning
+     * a promise once the transformation has completed. This method is invoked
+     * when a REST resource is modified using a patch request.
      *
-     * @param c
-     *            The context.
+     * @param requestState
+     *            The request state.
      * @param path
      *            The pointer from the root of the JSON resource to this
      *            attribute mapper. This may be used when constructing error
@@ -143,67 +138,60 @@
      *            modifications. The targeted JSON field will be relative to
      *            this attribute mapper, or root if all attributes associated
      *            with this mapper have been targeted.
-     * @param h
-     *            The result handler.
+     * @return A {@link Promise} containing the result of the operation.
      */
-    abstract void patch(Context c, JsonPointer path, PatchOperation operation,
-            ResultHandler<List<Modification>> h);
+    abstract Promise<List<Modification>, ResourceException> patch(
+            RequestState requestState, JsonPointer path, PatchOperation operation);
 
     /**
-     * Maps one or more LDAP attributes to their JSON representation, invoking a
-     * completion handler once the transformation has completed.
+     * Maps one or more LDAP attributes to their JSON representation, returning
+     * a promise once the transformation has completed.
      * <p>
      * This method is invoked whenever an LDAP entry is converted to a REST
      * resource, i.e. when responding to read, query, create, put, or patch
      * requests.
      * <p>
      * If the LDAP attributes are not present in the entry, perhaps because they
-     * are optional, then implementations should invoke the result handler's
-     * {@link ResultHandler#handleResult handleResult} method with a result of
-     * {@code null}. If the LDAP attributes cannot be mapped for any other
-     * reason, perhaps because they are required but missing, or they contain
-     * unexpected content, then the result handler's
-     * {@link ResultHandler#handleError handleError} method must be invoked with
-     * an appropriate exception indicating the problem which occurred.
+     * are optional, then implementations should return a successful promise
+     * with a result of {@code null}. If the LDAP attributes cannot be mapped
+     * for any other reason, perhaps because they are required but missing, or
+     * they contain unexpected content, then a failed promise must be returned
+     * with an appropriate exception indicating the problem which occurred.
      *
-     * @param c
-     *            The context.
+     * @param requestState
+     *            The request state.
      * @param path
      *            The pointer from the root of the JSON resource to this
      *            attribute mapper. This may be used when constructing error
      *            messages.
      * @param e
      *            The LDAP entry to be converted to JSON.
-     * @param h
-     *            The result handler.
+     * @return A {@link Promise} containing the result of the operation.
      */
-    abstract void read(Context c, JsonPointer path, Entry e, ResultHandler<JsonValue> h);
+    abstract Promise<JsonValue, ResourceException> read(RequestState requestState, JsonPointer path, Entry e);
 
     /**
-     * Maps a JSON value to one or more LDAP modifications, invoking a
-     * completion handler once the transformation has completed. This method is
-     * invoked when a REST resource is modified using an update request.
+     * Maps a JSON value to one or more LDAP modifications, returning a promise
+     * once the transformation has completed. This method is invoked when a REST
+     * resource is modified using an update request.
      * <p>
      * If the JSON value corresponding to this mapper is not present in the
      * resource then this method will be invoked with a value of {@code null}.
      * It is the responsibility of the mapper implementation to take appropriate
      * action in this case, perhaps by substituting default LDAP values, or by
-     * rejecting the update by invoking the result handler's
-     * {@link ResultHandler#handleError handleError} method.
+     * returning a failed promise with an appropriate {@link ResourceException}.
      *
-     * @param c
-     *            The context.
+     * @param requestState
+     *            The request state.
      * @param v
      *            The JSON value to be converted to LDAP attributes, which may
      *            be {@code null} indicating that the JSON value was not present
      *            in the resource.
-     * @param h
-     *            The result handler.
+     * @return A {@link Promise} containing the result of the operation.
      */
-    abstract void update(Context c, JsonPointer path, Entry e, JsonValue v,
-            ResultHandler<List<Modification>> h);
+    abstract Promise<List<Modification>, ResourceException> update(
+            RequestState requestState, JsonPointer path, Entry e, JsonValue v);
 
-    // TODO: methods for obtaining schema information (e.g. name, description,
-    // type information).
+    // TODO: methods for obtaining schema information (e.g. name, description, type information).
     // TODO: methods for creating sort controls.
 }
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthenticatedConnectionContext.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthenticatedConnectionContext.java
index 19a5f93..9a2efb2 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthenticatedConnectionContext.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthenticatedConnectionContext.java
@@ -11,18 +11,14 @@
  * Header, with the fields enclosed by brackets [] replaced by your own identifying
  * information: "Portions copyright [year] [name of copyright owner]".
  *
- * Copyright 2013 ForgeRock AS.
+ * Copyright 2013-2015 ForgeRock AS.
  */
 package org.forgerock.opendj.rest2ldap;
 
-import static org.forgerock.opendj.rest2ldap.Utils.ensureNotNull;
-import static org.forgerock.opendj.rest2ldap.Utils.i18n;
+import static org.forgerock.opendj.rest2ldap.Utils.*;
 
-import org.forgerock.json.fluent.JsonValue;
-import org.forgerock.json.resource.Context;
-import org.forgerock.json.resource.InternalServerErrorException;
-import org.forgerock.json.resource.PersistenceConfig;
-import org.forgerock.json.resource.ResourceException;
+import org.forgerock.http.Context;
+import org.forgerock.http.context.AbstractContext;
 import org.forgerock.opendj.ldap.Connection;
 
 /**
@@ -32,7 +28,7 @@
  * servlet filter. It is the responsibility of the component which acquired the
  * connection to release once processing has completed.
  */
-public final class AuthenticatedConnectionContext extends Context {
+public final class AuthenticatedConnectionContext extends AbstractContext {
     /*
      * TODO: this context does not support persistence because there is no
      * obvious way to restore the connection. We could just persist the context
@@ -53,7 +49,7 @@
      *            re-used for subsequent LDAP operations.
      */
     public AuthenticatedConnectionContext(final Context parent, final Connection connection) {
-        super(ensureNotNull(parent));
+        super(ensureNotNull(parent), "authenticated connection");
         this.connection = connection;
     }
 
@@ -69,38 +65,13 @@
      *            The cached pre-authenticated LDAP connection which should be
      *            re-used for subsequent LDAP operations.
      */
-    public AuthenticatedConnectionContext(final String id, final Context parent,
+    AuthenticatedConnectionContext(final String id, final Context parent,
             final Connection connection) {
-        super(id, ensureNotNull(parent));
+        super(id, "authenticated connection", ensureNotNull(parent));
         this.connection = connection;
     }
 
     /**
-     * Restore from JSON representation.
-     *
-     * @param savedContext
-     *            The JSON representation from which this context's attributes
-     *            should be parsed.
-     * @param config
-     *            The persistence configuration.
-     * @throws ResourceException
-     *             If the JSON representation could not be parsed.
-     */
-    AuthenticatedConnectionContext(final JsonValue savedContext, final PersistenceConfig config)
-            throws ResourceException {
-        super(savedContext, config);
-        throw new InternalServerErrorException(i18n("Cached LDAP connections cannot be restored"));
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    protected void saveToJson(final JsonValue savedContext, final PersistenceConfig config)
-            throws ResourceException {
-        super.saveToJson(savedContext, config);
-        throw new InternalServerErrorException(i18n("Cached LDAP connections cannot be persisted"));
-    }
-
-    /**
      * Returns the cached pre-authenticated LDAP connection which should be
      * re-used for subsequent LDAP operations.
      *
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/FilterType.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/FilterType.java
index 90b8b9b..6e226db 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/FilterType.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/FilterType.java
@@ -11,15 +11,13 @@
  * Header, with the fields enclosed by brackets [] replaced by your own identifying
  * information: "Portions Copyright [year] [name of copyright owner]".
  *
- * Copyright 2013 ForgeRock AS.
+ * Copyright 2013-2015 ForgeRock AS.
  */
 package org.forgerock.opendj.rest2ldap;
 
-import org.forgerock.json.resource.QueryFilter;
+import org.forgerock.util.query.QueryFilter;
 
-/**
- * An enumeration of the commons REST query comparison filter types.
- */
+/** An enumeration of the commons REST query comparison filter types. */
 enum FilterType {
 
     /**
@@ -83,5 +81,5 @@
      *
      * @see QueryFilter#startsWith
      */
-    STARTS_WITH;
+    STARTS_WITH
 }
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/HttpAuthenticationFilter.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/HttpAuthenticationFilter.java
new file mode 100644
index 0000000..2c5bee5
--- /dev/null
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/HttpAuthenticationFilter.java
@@ -0,0 +1,384 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2013-2015 ForgeRock AS.
+ */
+package org.forgerock.opendj.rest2ldap;
+
+import static org.forgerock.json.resource.SecurityContext.AUTHZID_DN;
+import static org.forgerock.json.resource.SecurityContext.AUTHZID_ID;
+import static org.forgerock.opendj.ldap.Connections.uncloseable;
+import static org.forgerock.opendj.ldap.LdapException.newLdapException;
+import static org.forgerock.opendj.ldap.requests.Requests.newPlainSASLBindRequest;
+import static org.forgerock.opendj.ldap.requests.Requests.newSearchRequest;
+import static org.forgerock.opendj.ldap.requests.Requests.newSimpleBindRequest;
+import static org.forgerock.opendj.rest2ldap.Rest2LDAP.asResourceException;
+import static org.forgerock.util.Utils.closeSilently;
+
+import java.io.Closeable;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.StringTokenizer;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.forgerock.http.Context;
+import org.forgerock.http.Handler;
+import org.forgerock.http.protocol.Request;
+import org.forgerock.http.protocol.Response;
+import org.forgerock.http.protocol.Status;
+import org.forgerock.json.JsonValue;
+import org.forgerock.json.JsonValueException;
+import org.forgerock.json.resource.ResourceException;
+import org.forgerock.json.resource.SecurityContext;
+import org.forgerock.opendj.ldap.AuthenticationException;
+import org.forgerock.opendj.ldap.AuthorizationException;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.opendj.ldap.ConnectionFactory;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.EntryNotFoundException;
+import org.forgerock.opendj.ldap.Filter;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.MultipleEntriesFoundException;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.SearchScope;
+import org.forgerock.opendj.ldap.requests.BindRequest;
+import org.forgerock.opendj.ldap.requests.Requests;
+import org.forgerock.opendj.ldap.responses.BindResult;
+import org.forgerock.opendj.ldap.responses.SearchResultEntry;
+import org.forgerock.opendj.ldap.schema.Schema;
+import org.forgerock.util.AsyncFunction;
+import org.forgerock.util.promise.NeverThrowsException;
+import org.forgerock.util.promise.Promise;
+import org.forgerock.util.promise.Promises;
+
+/** An LDAP based HTTP authentication filter. */
+final class HttpAuthenticationFilter implements org.forgerock.http.Filter, Closeable {
+
+    /** Indicates how authentication should be performed. */
+    private enum AuthenticationMethod {
+        SASL_PLAIN,
+        SEARCH_SIMPLE,
+        SIMPLE
+    }
+
+    private final Schema schema = Schema.getDefaultSchema();
+    private final String altAuthenticationPasswordHeader;
+    private final String altAuthenticationUsernameHeader;
+    private final AuthenticationMethod authenticationMethod;
+    private final ConnectionFactory bindLDAPConnectionFactory;
+    private final boolean reuseAuthenticatedConnection;
+    private final String saslAuthzIdTemplate;
+    private final DN searchBaseDN;
+    private final String searchFilterTemplate;
+    private final ConnectionFactory searchLDAPConnectionFactory;
+    private final SearchScope searchScope;
+    private final boolean supportAltAuthentication;
+
+    private final boolean supportHTTPBasicAuthentication;
+
+    HttpAuthenticationFilter(final JsonValue configuration) {
+        // Parse the authentication configuration.
+        final JsonValue authnConfig = configuration.get("authenticationFilter");
+        supportHTTPBasicAuthentication = authnConfig.get("supportHTTPBasicAuthentication").required().asBoolean();
+
+        // Alternative HTTP authentication.
+        supportAltAuthentication = authnConfig.get("supportAltAuthentication").required().asBoolean();
+        if (supportAltAuthentication) {
+            altAuthenticationUsernameHeader = authnConfig.get("altAuthenticationUsernameHeader").required().asString();
+            altAuthenticationPasswordHeader = authnConfig.get("altAuthenticationPasswordHeader").required().asString();
+        } else {
+            altAuthenticationUsernameHeader = null;
+            altAuthenticationPasswordHeader = null;
+        }
+
+        // Should the authenticated connection should be cached for use by subsequent LDAP operations?
+        reuseAuthenticatedConnection = authnConfig.get("reuseAuthenticatedConnection").required().asBoolean();
+
+        // Parse the authentication method and associated parameters.
+        authenticationMethod = parseAuthenticationMethod(authnConfig);
+        switch (authenticationMethod) {
+        case SASL_PLAIN:
+            saslAuthzIdTemplate = authnConfig.get("saslAuthzIdTemplate").required().asString();
+            searchBaseDN = null;
+            searchScope = null;
+            searchFilterTemplate = null;
+            searchLDAPConnectionFactory = null;
+            break;
+        case SEARCH_SIMPLE:
+            searchBaseDN = DN.valueOf(authnConfig.get("searchBaseDN").required().asString(), schema);
+            searchScope = parseSearchScope(authnConfig);
+            searchFilterTemplate = authnConfig.get("searchFilterTemplate").required().asString();
+
+            // Parse the LDAP connection factory to be used for searches.
+            final String ldapFactoryName = authnConfig.get("searchLDAPConnectionFactory").required().asString();
+            searchLDAPConnectionFactory = Rest2LDAP
+                .configureConnectionFactory(configuration.get("ldapConnectionFactories").required(), ldapFactoryName);
+
+            saslAuthzIdTemplate = null;
+            break;
+        case SIMPLE:
+        default:
+            saslAuthzIdTemplate = null;
+            searchBaseDN = null;
+            searchScope = null;
+            searchFilterTemplate = null;
+            searchLDAPConnectionFactory = null;
+            break;
+        }
+
+        // Parse the LDAP connection factory to be used for binds.
+        final String ldapFactoryName = authnConfig.get("bindLDAPConnectionFactory").required().asString();
+        bindLDAPConnectionFactory = Rest2LDAP.configureConnectionFactory(
+            configuration.get("ldapConnectionFactories").required(), ldapFactoryName);
+    }
+
+    private static AuthenticationMethod parseAuthenticationMethod(final JsonValue configuration) {
+        if (configuration.isDefined("method")) {
+            final String method = configuration.get("method").asString();
+            if ("simple".equalsIgnoreCase(method)) {
+                return AuthenticationMethod.SIMPLE;
+            } else if ("sasl-plain".equalsIgnoreCase(method)) {
+                return AuthenticationMethod.SASL_PLAIN;
+            } else if ("search-simple".equalsIgnoreCase(method)) {
+                return AuthenticationMethod.SEARCH_SIMPLE;
+            } else {
+                throw new JsonValueException(configuration,
+                    "Illegal authentication method: must be either 'simple', 'sasl-plain', or 'search-simple'");
+            }
+        } else {
+            return AuthenticationMethod.SEARCH_SIMPLE;
+        }
+    }
+
+    private static SearchScope parseSearchScope(final JsonValue configuration) {
+        if (configuration.isDefined("searchScope")) {
+            final String scope = configuration.get("searchScope").asString();
+            if ("sub".equalsIgnoreCase(scope)) {
+                return SearchScope.WHOLE_SUBTREE;
+            } else if ("one".equalsIgnoreCase(scope)) {
+                return SearchScope.SINGLE_LEVEL;
+            } else {
+                throw new JsonValueException(configuration, "Illegal search scope: must be either 'sub' or 'one'");
+            }
+        } else {
+            return SearchScope.WHOLE_SUBTREE;
+        }
+    }
+
+    @Override
+    public Promise<Response, NeverThrowsException> filter(final Context context, final Request request,
+                                                          final Handler next) {
+        // Store the authenticated connection so that it can be re-used by the handler if needed.
+        // However, make sure that it is closed on completion.
+        try {
+            final String headerUsername =
+                supportAltAuthentication ? request.getHeaders().getFirst(altAuthenticationUsernameHeader) : null;
+            final String headerPassword =
+                supportAltAuthentication ? request.getHeaders().getFirst(altAuthenticationPasswordHeader) : null;
+            final String headerAuthorization =
+                supportHTTPBasicAuthentication ? request.getHeaders().getFirst("Authorization") : null;
+
+            final String username;
+            final char[] password;
+            if (headerUsername != null) {
+                if (headerPassword == null || headerUsername.isEmpty() || headerPassword.isEmpty()) {
+                    throw ResourceException.getException(401);
+                }
+                username = headerUsername;
+                password = headerPassword.toCharArray();
+            } else if (headerAuthorization != null) {
+                final StringTokenizer st = new StringTokenizer(headerAuthorization);
+                final String method = st.nextToken();
+                if (method == null || !"BASIC".equalsIgnoreCase(method)) {
+                    throw ResourceException.getException(401);
+                }
+                final String b64Credentials = st.nextToken();
+                if (b64Credentials == null) {
+                    throw ResourceException.getException(401);
+                }
+                final String credentials = ByteString.valueOfBase64(b64Credentials).toString();
+                final String[] usernameAndPassword = credentials.split(":");
+                if (usernameAndPassword.length != 2) {
+                    throw ResourceException.getException(401);
+                }
+                username = usernameAndPassword[0];
+                password = usernameAndPassword[1].toCharArray();
+            } else {
+                throw ResourceException.getException(401);
+            }
+
+            // If we've got here then we have a username and password.
+            switch (authenticationMethod) {
+            case SIMPLE: {
+                final Map<String, Object> authzid;
+                authzid = new LinkedHashMap<>(2);
+                authzid.put(AUTHZID_DN, username);
+                authzid.put(AUTHZID_ID, username);
+                return doBind(
+                    context, request, next, Requests.newSimpleBindRequest(username, password), username, authzid);
+            }
+            case SASL_PLAIN: {
+                final Map<String, Object> authzid;
+                final String bindId;
+                if (saslAuthzIdTemplate.startsWith("dn:")) {
+                    final String bindDN = DN.format(saslAuthzIdTemplate.substring(3), schema, username).toString();
+                    bindId = "dn:" + bindDN;
+                    authzid = new LinkedHashMap<>(2);
+                    authzid.put(AUTHZID_DN, bindDN);
+                    authzid.put(AUTHZID_ID, username);
+                } else {
+                    bindId = String.format(saslAuthzIdTemplate, username);
+                    authzid = Collections.singletonMap(AUTHZID_ID, (Object) username);
+                }
+                return doBind(context, request, next, newPlainSASLBindRequest(bindId, password), username, authzid);
+            }
+            default: // SEARCH_SIMPLE
+                final AtomicReference<Connection> savedConnection = new AtomicReference<>();
+                return searchLDAPConnectionFactory.getConnectionAsync()
+                    .thenAsync(doSearchForUser(username, savedConnection))
+                    .thenAsync(doBindAfterSearch(context, request, next, username, password, savedConnection),
+                        returnErrorAfterFailedSearch(savedConnection));
+            }
+        } catch (final Throwable t) {
+            return asErrorResponse(t);
+        }
+    }
+
+    private AsyncFunction<Connection, SearchResultEntry, LdapException> doSearchForUser(
+            final String username, final AtomicReference<Connection> savedConnection) {
+        return new AsyncFunction<Connection, SearchResultEntry, LdapException>() {
+            @Override
+            public Promise<SearchResultEntry, LdapException> apply(final Connection connection) throws LdapException {
+                savedConnection.set(connection);
+                final Filter filter = Filter.format(searchFilterTemplate, username);
+                return connection.searchSingleEntryAsync(newSearchRequest(searchBaseDN, searchScope, filter, "1.1"));
+            }
+        };
+    }
+
+    private AsyncFunction<SearchResultEntry, Response, NeverThrowsException> doBindAfterSearch(
+            final Context context, final Request request, final Handler next, final String username,
+            final char[] password, final AtomicReference<Connection> savedConnection) {
+        return new AsyncFunction<SearchResultEntry, Response, NeverThrowsException>() {
+            @Override
+            public Promise<Response, NeverThrowsException> apply(final SearchResultEntry entry) {
+                closeConnection(savedConnection);
+                final String bindDN = entry.getName().toString();
+                final Map<String, Object> authzid = new LinkedHashMap<>(2);
+                authzid.put(AUTHZID_DN, bindDN);
+                authzid.put(AUTHZID_ID, username);
+                return doBind(context, request, next, newSimpleBindRequest(bindDN, password), username, authzid);
+            }
+        };
+    }
+
+    /**
+     * Get a bind connection and then perform the bind operation, setting the cached connection and authorization
+     * credentials on completion.
+     */
+    private Promise<Response, NeverThrowsException> doBind(
+            final Context context, final Request request, final Handler next, final BindRequest bindRequest,
+            final String authcid, final Map<String, Object> authzid) {
+        final AtomicReference<Connection> savedConnection = new AtomicReference<>();
+        return bindLDAPConnectionFactory.getConnectionAsync()
+            .thenAsync(new AsyncFunction<Connection, BindResult, LdapException>() {
+                @Override
+                public Promise<BindResult, LdapException> apply(final Connection connection) throws LdapException {
+                    savedConnection.set(connection);
+                    return connection.bindAsync(bindRequest);
+                }
+            })
+            .thenAsync(doChain(context, request, next, authcid, authzid, savedConnection),
+                       returnErrorAfterFailedBind())
+            .thenFinally(new Runnable() {
+                @Override
+                public void run() {
+                    closeConnection(savedConnection);
+                }
+            });
+    }
+
+    private AsyncFunction<BindResult, Response, NeverThrowsException> doChain(
+            final Context context, final Request request, final Handler next, final String authcid,
+            final Map<String, Object> authzid, final AtomicReference<Connection> savedConnection) {
+        return new AsyncFunction<BindResult, Response, NeverThrowsException>() {
+            @Override
+            public Promise<Response, NeverThrowsException> apply(final BindResult result) {
+                // Pass through the authentication ID and authorization principals.
+                Context forwardedContext = new SecurityContext(context, authcid, authzid);
+
+                // Cache the pre-authenticated connection and prevent downstream
+                // components from closing it since this filter will close it.
+                if (reuseAuthenticatedConnection) {
+                    forwardedContext = new AuthenticatedConnectionContext(
+                            forwardedContext, uncloseable(savedConnection.get()));
+                }
+
+                return next.handle(forwardedContext, request);
+            }
+        };
+    }
+
+    private AsyncFunction<LdapException, Response, NeverThrowsException> returnErrorAfterFailedSearch(
+            final AtomicReference<Connection> savedConnection) {
+        return new AsyncFunction<LdapException, Response, NeverThrowsException>() {
+            @Override
+            public Promise<Response, NeverThrowsException> apply(final LdapException e) {
+                if (closeConnection(savedConnection)) {
+                    // The search error should not be passed as-is back to the user.
+                    if (e instanceof EntryNotFoundException || e instanceof MultipleEntriesFoundException) {
+                        return asErrorResponse(newLdapException(ResultCode.INVALID_CREDENTIALS, e));
+                    } else if (e instanceof AuthenticationException || e instanceof AuthorizationException) {
+                        return asErrorResponse(newLdapException(ResultCode.CLIENT_SIDE_LOCAL_ERROR, e));
+                    } else {
+                        return asErrorResponse(e);
+                    }
+                } else {
+                    return asErrorResponse(e);
+                }
+            }
+        };
+    }
+
+    private AsyncFunction<LdapException, Response, NeverThrowsException> returnErrorAfterFailedBind() {
+        return new AsyncFunction<LdapException, Response, NeverThrowsException>() {
+            @Override
+            public Promise<Response, NeverThrowsException> apply(final LdapException e) {
+                return asErrorResponse(e);
+            }
+        };
+    }
+
+    private Promise<Response, NeverThrowsException> asErrorResponse(final Throwable t) {
+        final ResourceException e = asResourceException(t);
+        final Response response =
+            new Response().setStatus(Status.valueOf(e.getCode())).setEntity(e.toJsonValue().getObject());
+        return Promises.newResultPromise(response);
+    }
+
+    private boolean closeConnection(final AtomicReference<Connection> savedConnection) {
+        final Connection connection = savedConnection.get();
+        if (connection != null) {
+            connection.close();
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public void close() {
+        closeSilently(searchLDAPConnectionFactory, bindLDAPConnectionFactory);
+    }
+}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/JSONConstantAttributeMapper.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/JSONConstantAttributeMapper.java
index dc1faca..f916a0d 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/JSONConstantAttributeMapper.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/JSONConstantAttributeMapper.java
@@ -11,7 +11,7 @@
  * Header, with the fields enclosed by brackets [] replaced by your own identifying
  * information: "Portions Copyright [year] [name of copyright owner]".
  *
- * Copyright 2012-2013 ForgeRock AS.
+ * Copyright 2012-2015 ForgeRock AS.
  */
 package org.forgerock.opendj.rest2ldap;
 
@@ -26,15 +26,17 @@
 import java.util.List;
 import java.util.Set;
 
-import org.forgerock.json.fluent.JsonPointer;
-import org.forgerock.json.fluent.JsonValue;
+import org.forgerock.json.JsonPointer;
+import org.forgerock.json.JsonValue;
 import org.forgerock.json.resource.BadRequestException;
 import org.forgerock.json.resource.PatchOperation;
-import org.forgerock.json.resource.ResultHandler;
+import org.forgerock.json.resource.ResourceException;
 import org.forgerock.opendj.ldap.Attribute;
 import org.forgerock.opendj.ldap.Entry;
 import org.forgerock.opendj.ldap.Filter;
 import org.forgerock.opendj.ldap.Modification;
+import org.forgerock.util.promise.Promise;
+import org.forgerock.util.promise.Promises;
 
 /**
  * An attribute mapper which maps a single JSON attribute to a fixed value.
@@ -52,27 +54,25 @@
     }
 
     @Override
-    void create(final Context c, final JsonPointer path, final JsonValue v,
-            final ResultHandler<List<Attribute>> h) {
+    Promise<List<Attribute>, ResourceException> create(
+            final RequestState requestState, final JsonPointer path, final JsonValue v) {
         if (!isNullOrEmpty(v) && !v.getObject().equals(value.getObject())) {
-            h.handleError(new BadRequestException(i18n(
-                    "The request cannot be processed because it attempts to create "
-                            + "the read-only field '%s'", path)));
+            return Promises.<List<Attribute>, ResourceException> newExceptionPromise(new BadRequestException(i18n(
+                    "The request cannot be processed because it attempts to create the read-only field '%s'", path)));
         } else {
-            h.handleResult(Collections.<Attribute> emptyList());
+            return Promises.newResultPromise(Collections.<Attribute> emptyList());
         }
     }
 
     @Override
-    void getLDAPAttributes(final Context c, final JsonPointer path, final JsonPointer subPath,
+    void getLDAPAttributes(final RequestState requestState, final JsonPointer path, final JsonPointer subPath,
             final Set<String> ldapAttributes) {
         // Nothing to do.
     }
 
     @Override
-    void getLDAPFilter(final Context c, final JsonPointer path, final JsonPointer subPath,
-            final FilterType type, final String operator, final Object valueAssertion,
-            final ResultHandler<Filter> h) {
+    Promise<Filter, ResourceException> getLDAPFilter(final RequestState requestState, final JsonPointer path,
+            final JsonPointer subPath, final FilterType type, final String operator, final Object valueAssertion) {
         final Filter filter;
         final JsonValue subValue = value.get(subPath);
         if (subValue == null) {
@@ -105,32 +105,29 @@
             // This attribute mapper is a candidate but it does not match.
             filter = alwaysFalse();
         }
-        h.handleResult(filter);
+        return Promises.newResultPromise(filter);
     }
 
     @Override
-    void patch(final Context c, final JsonPointer path, final PatchOperation operation,
-            final ResultHandler<List<Modification>> h) {
-        h.handleError(new BadRequestException(i18n(
-                "The request cannot be processed because it attempts to patch "
-                        + "the read-only field '%s'", path)));
+    Promise<List<Modification>, ResourceException> patch(final RequestState requestState, final JsonPointer path,
+            final PatchOperation operation) {
+        return Promises.<List<Modification>, ResourceException> newExceptionPromise(new BadRequestException(i18n(
+                "The request cannot be processed because it attempts to patch the read-only field '%s'", path)));
     }
 
     @Override
-    void read(final Context c, final JsonPointer path, final Entry e,
-            final ResultHandler<JsonValue> h) {
-        h.handleResult(value.copy());
+    Promise<JsonValue, ResourceException> read(final RequestState requestState, final JsonPointer path, final Entry e) {
+        return Promises.newResultPromise(value.copy());
     }
 
     @Override
-    void update(final Context c, final JsonPointer path, final Entry e, final JsonValue v,
-            final ResultHandler<List<Modification>> h) {
+    Promise<List<Modification>, ResourceException> update(
+            final RequestState requestState, final JsonPointer path, final Entry e, final JsonValue v) {
         if (!isNullOrEmpty(v) && !v.getObject().equals(value.getObject())) {
-            h.handleError(new BadRequestException(i18n(
-                    "The request cannot be processed because it attempts to modify "
-                            + "the read-only field '%s'", path)));
+            return Promises.<List<Modification>, ResourceException> newExceptionPromise(new BadRequestException(i18n(
+                    "The request cannot be processed because it attempts to modify the read-only field '%s'", path)));
         } else {
-            h.handleResult(Collections.<Modification> emptyList());
+            return Promises.newResultPromise(Collections.<Modification> emptyList());
         }
     }
 
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);
-    }
 }
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/NameStrategy.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/NameStrategy.java
index 323f962..69e764e 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/NameStrategy.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/NameStrategy.java
@@ -11,7 +11,7 @@
  * Header, with the fields enclosed by brackets [] replaced by your own identifying
  * information: "Portions copyright [year] [name of copyright owner]".
  *
- * Copyright 2013 ForgeRock AS.
+ * Copyright 2013-2015 ForgeRock AS.
  */
 
 package org.forgerock.opendj.rest2ldap;
@@ -40,8 +40,8 @@
      * Returns a search request which can be used to obtain the specified REST
      * resource.
      *
-     * @param c
-     *            The context.
+     * @param requestState
+     *            The request state.
      * @param baseDN
      *            The search base DN.
      * @param resourceId
@@ -49,40 +49,40 @@
      * @return A search request which can be used to obtain the specified REST
      *         resource.
      */
-    abstract SearchRequest createSearchRequest(Context c, DN baseDN, String resourceId);
+    abstract SearchRequest createSearchRequest(RequestState requestState, DN baseDN, String resourceId);
 
     /**
      * Adds the name of any LDAP attribute required by this name strategy to the
      * provided set.
      *
-     * @param c
-     *            The context.
+     * @param requestState
+     *            The request state.
      * @param ldapAttributes
      *            The set into which any required LDAP attribute name should be
      *            put.
      */
-    abstract void getLDAPAttributes(Context c, Set<String> ldapAttributes);
+    abstract void getLDAPAttributes(RequestState requestState, Set<String> ldapAttributes);
 
     /**
      * Retrieves the resource ID from the provided LDAP entry. Implementations
      * may use the entry DN as well as any attributes in order to determine the
      * resource ID.
      *
-     * @param c
-     *            The context.
+     * @param requestState
+     *            The request state.
      * @param entry
      *            The LDAP entry from which the resource ID should be obtained.
      * @return The resource ID.
      */
-    abstract String getResourceId(Context c, Entry entry);
+    abstract String getResourceId(RequestState requestState, Entry entry);
 
     /**
      * Sets the resource ID in the provided LDAP entry. Implementations are
      * responsible for setting the entry DN as well as any attributes associated
      * with the resource ID.
      *
-     * @param c
-     *            The context.
+     * @param requestState
+     *            The request state.
      * @param baseDN
      *            The baseDN to use when constructing the entry's DN.
      * @param resourceId
@@ -93,7 +93,7 @@
      * @throws ResourceException
      *             If the resource ID cannot be determined.
      */
-    abstract void setResourceId(Context c, DN baseDN, String resourceId, Entry entry)
+    abstract void setResourceId(RequestState requestState, DN baseDN, String resourceId, Entry entry)
             throws ResourceException;
 
 }
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectAttributeMapper.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectAttributeMapper.java
index 9b4124c..03cc53a 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectAttributeMapper.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectAttributeMapper.java
@@ -15,6 +15,12 @@
  */
 package org.forgerock.opendj.rest2ldap;
 
+import static org.forgerock.json.resource.PatchOperation.operation;
+import static org.forgerock.opendj.ldap.Filter.alwaysFalse;
+import static org.forgerock.opendj.rest2ldap.Rest2LDAP.asResourceException;
+import static org.forgerock.opendj.rest2ldap.Utils.i18n;
+import static org.forgerock.opendj.rest2ldap.Utils.toLowerCase;
+
 import java.util.AbstractMap.SimpleImmutableEntry;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -23,27 +29,20 @@
 import java.util.Map;
 import java.util.Set;
 
-import org.forgerock.json.fluent.JsonPointer;
-import org.forgerock.json.fluent.JsonValue;
+import org.forgerock.json.JsonPointer;
+import org.forgerock.json.JsonValue;
 import org.forgerock.json.resource.BadRequestException;
 import org.forgerock.json.resource.PatchOperation;
 import org.forgerock.json.resource.ResourceException;
-import org.forgerock.json.resource.ResultHandler;
 import org.forgerock.opendj.ldap.Attribute;
 import org.forgerock.opendj.ldap.Entry;
 import org.forgerock.opendj.ldap.Filter;
 import org.forgerock.opendj.ldap.Modification;
 import org.forgerock.util.Function;
-import org.forgerock.util.promise.NeverThrowsException;
+import org.forgerock.util.promise.Promise;
+import org.forgerock.util.promise.Promises;
 
-import static org.forgerock.json.resource.PatchOperation.*;
-import static org.forgerock.opendj.ldap.Filter.*;
-import static org.forgerock.opendj.rest2ldap.Rest2LDAP.*;
-import static org.forgerock.opendj.rest2ldap.Utils.*;
-
-/**
- * An attribute mapper which maps JSON objects to LDAP attributes.
- */
+/** An attribute mapper which maps JSON objects to LDAP attributes. */
 public final class ObjectAttributeMapper extends AttributeMapper {
 
     private static final class Mapping {
@@ -88,8 +87,8 @@
     }
 
     @Override
-    void create(final Context c, final JsonPointer path, final JsonValue v,
-            final ResultHandler<List<Attribute>> h) {
+    Promise<List<Attribute>, ResourceException> create(
+            final RequestState requestState, final JsonPointer path, final JsonValue v) {
         try {
             /*
              * First check that the JSON value is an object and that the fields
@@ -98,66 +97,67 @@
             final Map<String, Mapping> missingMappings = checkMapping(path, v);
 
             // Accumulate the results of the subordinate mappings.
-            final ResultHandler<List<Attribute>> handler = accumulator(h);
+            final List<Promise<List<Attribute>, ResourceException>> promises = new ArrayList<>();
 
             // Invoke mappings for which there are values provided.
             if (v != null && !v.isNull()) {
                 for (final Map.Entry<String, Object> me : v.asMap().entrySet()) {
                     final Mapping mapping = getMapping(me.getKey());
                     final JsonValue subValue = new JsonValue(me.getValue());
-                    mapping.mapper.create(c, path.child(me.getKey()), subValue, handler);
+                    promises.add(mapping.mapper.create(requestState, path.child(me.getKey()), subValue));
                 }
             }
 
             // Invoke mappings for which there were no values provided.
             for (final Mapping mapping : missingMappings.values()) {
-                mapping.mapper.create(c, path.child(mapping.name), null, handler);
+                promises.add(mapping.mapper.create(requestState, path.child(mapping.name), null));
             }
+
+            return Promises.when(promises)
+                           .then(this.<Attribute> accumulateResults());
         } catch (final Exception e) {
-            h.handleError(asResourceException(e));
+            return Promises.newExceptionPromise(asResourceException(e));
         }
     }
 
     @Override
-    void getLDAPAttributes(final Context c, final JsonPointer path, final JsonPointer subPath,
+    void getLDAPAttributes(final RequestState requestState, final JsonPointer path, final JsonPointer subPath,
             final Set<String> ldapAttributes) {
         if (subPath.isEmpty()) {
             // Request all subordinate mappings.
             for (final Mapping mapping : mappings.values()) {
-                mapping.mapper.getLDAPAttributes(c, path.child(mapping.name), subPath,
-                        ldapAttributes);
+                mapping.mapper.getLDAPAttributes(requestState, path.child(mapping.name), subPath, ldapAttributes);
             }
         } else {
             // Request single subordinate mapping.
             final Mapping mapping = getMapping(subPath);
             if (mapping != null) {
-                mapping.mapper.getLDAPAttributes(c, path.child(subPath.get(0)), subPath
-                        .relativePointer(), ldapAttributes);
+                mapping.mapper.getLDAPAttributes(
+                        requestState, path.child(subPath.get(0)), subPath.relativePointer(), ldapAttributes);
             }
         }
     }
 
     @Override
-    void getLDAPFilter(final Context c, final JsonPointer path, final JsonPointer subPath,
-            final FilterType type, final String operator, final Object valueAssertion,
-            final ResultHandler<Filter> h) {
+    Promise<Filter, ResourceException> getLDAPFilter(final RequestState requestState, final JsonPointer path,
+            final JsonPointer subPath, final FilterType type, final String operator, final Object valueAssertion) {
         final Mapping mapping = getMapping(subPath);
         if (mapping != null) {
-            mapping.mapper.getLDAPFilter(c, path.child(subPath.get(0)), subPath.relativePointer(),
-                    type, operator, valueAssertion, h);
+            return mapping.mapper.getLDAPFilter(requestState, path.child(subPath.get(0)),
+                    subPath.relativePointer(), type, operator, valueAssertion);
         } else {
             /*
              * Either the filter targeted the entire object (i.e. it was "/"),
              * or it targeted an unrecognized attribute within the object.
              * Either way, the filter will never match.
              */
-            h.handleResult(alwaysFalse());
+            return Promises.newResultPromise(alwaysFalse());
         }
     }
 
     @Override
-    void patch(final Context c, final JsonPointer path, final PatchOperation operation,
-            final ResultHandler<List<Modification>> h) {
+    Promise<List<Modification>, ResourceException> patch(
+            final RequestState requestState, final JsonPointer path, final PatchOperation operation) {
         try {
             final JsonPointer field = operation.getField();
             final JsonValue v = operation.getValue();
@@ -168,11 +168,10 @@
                  * by allowing the JSON value to be a partial object and
                  * add/remove/replace only the provided values.
                  */
-                final Map<String, Mapping> missingMappings = checkMapping(path, v);
+                checkMapping(path, v);
 
                 // Accumulate the results of the subordinate mappings.
-                final ResultHandler<List<Modification>> handler =
-                        accumulator(mappings.size() - missingMappings.size(), h);
+                final List<Promise<List<Modification>, ResourceException>> promises = new ArrayList<>();
 
                 // Invoke mappings for which there are values provided.
                 if (!v.isNull()) {
@@ -181,9 +180,12 @@
                         final JsonValue subValue = new JsonValue(me.getValue());
                         final PatchOperation subOperation =
                                 operation(operation.getOperation(), field /* empty */, subValue);
-                        mapping.mapper.patch(c, path.child(me.getKey()), subOperation, handler);
+                        promises.add(mapping.mapper.patch(requestState, path.child(me.getKey()), subOperation));
                     }
                 }
+
+                return Promises.when(promises)
+                               .then(this.<Modification> accumulateResults());
             } else {
                 /*
                  * The patch operation targets a subordinate field. Create a new
@@ -199,92 +201,92 @@
                 }
                 final PatchOperation subOperation =
                         operation(operation.getOperation(), field.relativePointer(), v);
-                mapping.mapper.patch(c, path.child(fieldName), subOperation, h);
+                return mapping.mapper.patch(requestState, path.child(fieldName), subOperation);
             }
         } catch (final Exception ex) {
-            h.handleError(asResourceException(ex));
+            return Promises.newExceptionPromise(asResourceException(ex));
         }
     }
 
     @Override
-    void read(final Context c, final JsonPointer path, final Entry e,
-            final ResultHandler<JsonValue> h) {
+    Promise<JsonValue, ResourceException> read(final RequestState requestState, final JsonPointer path, final Entry e) {
         /*
          * Use an accumulator which will aggregate the results from the
          * subordinate mappers into a single list. On completion, the
          * accumulator combines the results into a single JSON map object.
          */
-        final ResultHandler<Map.Entry<String, JsonValue>> handler =
-                accumulate(mappings.size(), transform(
-                        new Function<List<Map.Entry<String, JsonValue>>, JsonValue, NeverThrowsException>() {
-                            @Override
-                            public JsonValue apply(final List<Map.Entry<String, JsonValue>> value) {
-                                if (value.isEmpty()) {
-                                    /*
-                                     * No subordinate attributes, so omit the
-                                     * entire JSON object from the resource.
-                                     */
-                                    return null;
-                                } else {
-                                    // Combine the sub-attributes into a single JSON object.
-                                    final Map<String, Object> result = new LinkedHashMap<>(value.size());
-                                    for (final Map.Entry<String, JsonValue> e : value) {
-                                        result.put(e.getKey(), e.getValue().getObject());
-                                    }
-                                    return new JsonValue(result);
-                                }
-                            }
-                        }, h));
+        final List<Promise<Map.Entry<String, JsonValue>, ResourceException>> promises =
+                new ArrayList<>(mappings.size());
 
         for (final Mapping mapping : mappings.values()) {
-            mapping.mapper.read(c, path.child(mapping.name), e, transform(
-                    new Function<JsonValue, Map.Entry<String, JsonValue>, NeverThrowsException>() {
+            promises.add(mapping.mapper.read(requestState, path.child(mapping.name), e)
+                    .then(new Function<JsonValue, Map.Entry<String, JsonValue>, ResourceException>() {
                         @Override
                         public Map.Entry<String, JsonValue> apply(final JsonValue value) {
-                            return value != null ? new SimpleImmutableEntry<String, JsonValue>(
-                                    mapping.name, value) : null;
+                            return value != null ? new SimpleImmutableEntry<String, JsonValue>(mapping.name, value)
+                                                 : null;
                         }
-                    }, handler));
+                    }));
         }
+
+        return Promises.when(promises)
+                .then(new Function<List<Map.Entry<String, JsonValue>>, JsonValue, ResourceException>() {
+                    @Override
+                    public JsonValue apply(final List<Map.Entry<String, JsonValue>> value) {
+                        if (value.isEmpty()) {
+                            /*
+                             * No subordinate attributes, so omit the entire
+                             * JSON object from the resource.
+                             */
+                            return null;
+                        } else {
+                            // Combine the sub-attributes into a single JSON object.
+                            final Map<String, Object> result = new LinkedHashMap<>(value.size());
+                            for (final Map.Entry<String, JsonValue> e : value) {
+                                if (e != null) {
+                                    result.put(e.getKey(), e.getValue().getObject());
+                                }
+                            }
+                            return new JsonValue(result);
+                        }
+                    }
+                });
     }
 
     @Override
-    void update(final Context c, final JsonPointer path, final Entry e, final JsonValue v,
-            final ResultHandler<List<Modification>> h) {
+    Promise<List<Modification>, ResourceException> update(
+            final RequestState requestState, final JsonPointer path, final Entry e, final JsonValue v) {
         try {
-            /*
-             * First check that the JSON value is an object and that the fields
-             * it contains are known by this mapper.
-             */
+            // First check that the JSON value is an object and that the fields
+            // it contains are known by this mapper.
             final Map<String, Mapping> missingMappings = checkMapping(path, v);
 
             // Accumulate the results of the subordinate mappings.
-            final ResultHandler<List<Modification>> handler = accumulator(h);
+            final List<Promise<List<Modification>, ResourceException>> promises = new ArrayList<>();
 
             // Invoke mappings for which there are values provided.
             if (v != null && !v.isNull()) {
                 for (final Map.Entry<String, Object> me : v.asMap().entrySet()) {
                     final Mapping mapping = getMapping(me.getKey());
                     final JsonValue subValue = new JsonValue(me.getValue());
-                    mapping.mapper.update(c, path.child(me.getKey()), e, subValue, handler);
+                    promises.add(mapping.mapper.update(requestState, path.child(me.getKey()), e, subValue));
                 }
             }
 
             // Invoke mappings for which there were no values provided.
             for (final Mapping mapping : missingMappings.values()) {
-                mapping.mapper.update(c, path.child(mapping.name), e, null, handler);
+                promises.add(mapping.mapper.update(requestState, path.child(mapping.name), e, null));
             }
+
+            return Promises.when(promises)
+                           .then(this.<Modification> accumulateResults());
         } catch (final Exception ex) {
-            h.handleError(asResourceException(ex));
+            return Promises.newExceptionPromise(asResourceException(ex));
         }
     }
 
-    private <T> ResultHandler<List<T>> accumulator(final ResultHandler<List<T>> h) {
-        return accumulator(mappings.size(), h);
-    }
-
-    private <T> ResultHandler<List<T>> accumulator(final int size, final ResultHandler<List<T>> h) {
-        return accumulate(size, transform(new Function<List<List<T>>, List<T>, NeverThrowsException>() {
+    private <T> Function<List<List<T>>, List<T>, ResourceException> accumulateResults() {
+        return new Function<List<List<T>>, List<T>, ResourceException>() {
             @Override
             public List<T> apply(final List<List<T>> value) {
                 switch (value.size()) {
@@ -300,13 +302,10 @@
                     return attributes;
                 }
             }
-        }, h));
+        };
     }
 
-    /**
-     * Fail immediately if the JSON value has the wrong type or contains unknown
-     * attributes.
-     */
+    /** Fail immediately if the JSON value has the wrong type or contains unknown attributes. */
     private Map<String, Mapping> checkMapping(final JsonPointer path, final JsonValue v)
             throws ResourceException {
         final Map<String, Mapping> missingMappings = new LinkedHashMap<>(mappings);
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferenceAttributeMapper.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferenceAttributeMapper.java
index a0f515c..f259cb2 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferenceAttributeMapper.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferenceAttributeMapper.java
@@ -15,6 +15,12 @@
  */
 package org.forgerock.opendj.rest2ldap;
 
+import static org.forgerock.opendj.ldap.LdapException.newLdapException;
+import static org.forgerock.opendj.ldap.requests.Requests.newSearchRequest;
+import static org.forgerock.opendj.rest2ldap.Rest2LDAP.asResourceException;
+import static org.forgerock.opendj.rest2ldap.Utils.ensureNotNull;
+import static org.forgerock.opendj.rest2ldap.Utils.i18n;
+
 import java.util.ArrayList;
 import java.util.LinkedHashSet;
 import java.util.LinkedList;
@@ -23,19 +29,19 @@
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicReference;
 
-import org.forgerock.json.fluent.JsonPointer;
-import org.forgerock.json.fluent.JsonValue;
+import org.forgerock.json.JsonPointer;
+import org.forgerock.json.JsonValue;
 import org.forgerock.json.resource.BadRequestException;
 import org.forgerock.json.resource.ResourceException;
-import org.forgerock.json.resource.ResultHandler;
 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.Entry;
 import org.forgerock.opendj.ldap.EntryNotFoundException;
-import org.forgerock.opendj.ldap.LdapException;
 import org.forgerock.opendj.ldap.Filter;
+import org.forgerock.opendj.ldap.LdapException;
 import org.forgerock.opendj.ldap.LinkedAttribute;
 import org.forgerock.opendj.ldap.MultipleEntriesFoundException;
 import org.forgerock.opendj.ldap.ResultCode;
@@ -45,14 +51,13 @@
 import org.forgerock.opendj.ldap.responses.Result;
 import org.forgerock.opendj.ldap.responses.SearchResultEntry;
 import org.forgerock.opendj.ldap.responses.SearchResultReference;
+import org.forgerock.util.AsyncFunction;
 import org.forgerock.util.Function;
-import org.forgerock.util.promise.NeverThrowsException;
 import org.forgerock.util.promise.ExceptionHandler;
-
-import static org.forgerock.opendj.ldap.LdapException.*;
-import static org.forgerock.opendj.ldap.requests.Requests.*;
-import static org.forgerock.opendj.rest2ldap.Rest2LDAP.*;
-import static org.forgerock.opendj.rest2ldap.Utils.*;
+import org.forgerock.util.promise.Promise;
+import org.forgerock.util.promise.PromiseImpl;
+import org.forgerock.util.promise.Promises;
+import org.forgerock.util.promise.ResultHandler;
 
 /**
  * An attribute mapper which provides a mapping from a JSON value to a single DN
@@ -125,64 +130,67 @@
     }
 
     @Override
-    void getLDAPFilter(final Context c, final JsonPointer path, final JsonPointer subPath, final FilterType type,
-        final String operator, final Object valueAssertion, final ResultHandler<Filter> h) {
-        // Construct a filter which can be used to find referenced resources.
-        mapper.getLDAPFilter(c, path, subPath, type, operator, valueAssertion, new ResultHandler<Filter>() {
-            @Override
-            public void handleError(final ResourceException error) {
-                h.handleError(error); // Propagate.
-            }
+    Promise<Filter, ResourceException> getLDAPFilter(final RequestState requestState, final JsonPointer path,
+            final JsonPointer subPath, final FilterType type, final String operator, final Object valueAssertion) {
 
-            @Override
-            public void handleResult(final Filter result) {
-                // Search for all referenced entries and construct a filter.
-                final SearchRequest request = createSearchRequest(result);
-                final List<Filter> subFilters = new LinkedList<>();
+        return mapper.getLDAPFilter(requestState, path, subPath, type, operator, valueAssertion)
+                .thenAsync(new AsyncFunction<Filter, Filter, ResourceException>() {
+                    @Override
+                    public Promise<Filter, ResourceException> apply(final Filter result) {
+                        // Search for all referenced entries and construct a filter.
+                        final SearchRequest request = createSearchRequest(result);
+                        final List<Filter> subFilters = new LinkedList<>();
 
-                final ExceptionHandler<LdapException> exceptionHandler = new ExceptionHandler<LdapException>() {
-                    @Override
-                    public void handleException(LdapException exception) {
-                        h.handleError(asResourceException(exception)); // Propagate.
-                    }
-                };
+                        return requestState.getConnection().thenAsync(
+                                new AsyncFunction<Connection, Filter, ResourceException>() {
+                                    @Override
+                                    public Promise<Filter, ResourceException> apply(final Connection connection)
+                                            throws ResourceException {
+                                        return connection.searchAsync(request, new SearchResultHandler() {
+                                            @Override
+                                            public boolean handleEntry(final SearchResultEntry entry) {
+                                                if (subFilters.size() < SEARCH_MAX_CANDIDATES) {
+                                                    subFilters.add(Filter.equality(
+                                                            ldapAttributeName.toString(), entry.getName()));
+                                                    return true;
+                                                } else {
+                                                    // No point in continuing - maximum candidates reached.
+                                                    return false;
+                                                }
+                                            }
 
-                c.getConnection().searchAsync(request, new SearchResultHandler() {
-                    @Override
-                    public boolean handleEntry(final SearchResultEntry entry) {
-                        if (subFilters.size() < SEARCH_MAX_CANDIDATES) {
-                            subFilters.add(Filter.equality(ldapAttributeName.toString(), entry.getName()));
-                            return true;
-                        } else {
-                            // No point in continuing - maximum candidates reached.
-                            return false;
-                        }
+                                            @Override
+                                            public boolean handleReference(final SearchResultReference reference) {
+                                                // Ignore references.
+                                                return true;
+                                            }
+                                        }).then(new Function<Result, Filter, ResourceException>() {
+                                            @Override
+                                            public Filter apply(Result result) throws ResourceException {
+                                                if (subFilters.size() >= SEARCH_MAX_CANDIDATES) {
+                                                    throw asResourceException(
+                                                            newLdapException(ResultCode.ADMIN_LIMIT_EXCEEDED));
+                                                } else if (subFilters.size() == 1) {
+                                                    return subFilters.get(0);
+                                                } else {
+                                                    return Filter.or(subFilters);
+                                                }
+                                            }
+                                        }, new Function<LdapException, Filter, ResourceException>() {
+                                            @Override
+                                            public Filter apply(LdapException exception) throws ResourceException {
+                                                throw asResourceException(exception);
+                                            }
+                                        });
+                                    }
+                                });
                     }
-
-                    @Override
-                    public boolean handleReference(final SearchResultReference reference) {
-                        // Ignore references.
-                        return true;
-                    }
-                }).thenOnResult(new org.forgerock.util.promise.ResultHandler<Result>() {
-                    @Override
-                    public void handleResult(Result result) {
-                        if (subFilters.size() >= SEARCH_MAX_CANDIDATES) {
-                            exceptionHandler.handleException(newLdapException(ResultCode.ADMIN_LIMIT_EXCEEDED));
-                        } else if (subFilters.size() == 1) {
-                            h.handleResult(subFilters.get(0));
-                        } else {
-                            h.handleResult(Filter.or(subFilters));
-                        }
-                    }
-                }).thenOnException(exceptionHandler);
-            }
-        });
+                });
     }
 
     @Override
-    void getNewLDAPAttributes(final Context c, final JsonPointer path, final List<Object> newValues,
-        final ResultHandler<Attribute> h) {
+    Promise<Attribute, ResourceException> getNewLDAPAttributes(
+            final RequestState requestState, final JsonPointer path, final List<Object> newValues) {
         /*
          * For each value use the subordinate mapper to obtain the LDAP primary
          * key, the perform a search for each one to find the corresponding entries.
@@ -190,17 +198,12 @@
         final Attribute newLDAPAttribute = new LinkedAttribute(ldapAttributeName);
         final AtomicInteger pendingSearches = new AtomicInteger(newValues.size());
         final AtomicReference<ResourceException> exception = new AtomicReference<>();
+        final PromiseImpl<Attribute, ResourceException> promise = PromiseImpl.create();
 
         for (final Object value : newValues) {
-            mapper.create(c, path, new JsonValue(value), new ResultHandler<List<Attribute>>() {
-
+            mapper.create(requestState, path, new JsonValue(value)).thenOnResult(new ResultHandler<List<Attribute>>() {
                 @Override
-                public void handleError(final ResourceException error) {
-                    h.handleError(error);
-                }
-
-                @Override
-                public void handleResult(final List<Attribute> result) {
+                public void handleResult(List<Attribute> result) {
                     Attribute primaryKeyAttribute = null;
                     for (final Attribute attribute : result) {
                         if (attribute.getAttributeDescription().equals(primaryKey)) {
@@ -210,68 +213,72 @@
                     }
 
                     if (primaryKeyAttribute == null || primaryKeyAttribute.isEmpty()) {
-                        h.handleError(new BadRequestException(i18n(
-                            "The request cannot be processed because the reference "
-                                + "field '%s' contains a value which does not contain " + "a primary key", path)));
-                        return;
+                        promise.handleException(new BadRequestException(
+                                i18n("The request cannot be processed because the reference field '%s' contains "
+                                        + "a value which does not contain a primary key", path)));
                     }
 
                     if (primaryKeyAttribute.size() > 1) {
-                        h.handleError(new BadRequestException(i18n(
-                            "The request cannot be processed because the reference "
-                                + "field '%s' contains a value which contains multiple " + "primary keys", path)));
-                        return;
+                        promise.handleException(new BadRequestException(
+                                i18n("The request cannot be processed because the reference field '%s' contains "
+                                        + "a value which contains multiple primary keys", path)));
                     }
 
                     // Now search for the referenced entry in to get its DN.
                     final ByteString primaryKeyValue = primaryKeyAttribute.firstValue();
                     final Filter filter = Filter.equality(primaryKey.toString(), primaryKeyValue);
                     final SearchRequest search = createSearchRequest(filter);
-                    c.getConnection().searchSingleEntryAsync(search).thenOnResult(
-                            new org.forgerock.util.promise.ResultHandler<SearchResultEntry>() {
-                            @Override
-                            public void handleResult(final SearchResultEntry result) {
-                                synchronized (newLDAPAttribute) {
-                                    newLDAPAttribute.add(result.getName());
-                                }
-                                completeIfNecessary();
-                            }
-                        }).thenOnException(new ExceptionHandler<LdapException>() {
-                            @Override
-                            public void handleException(final LdapException error) {
-                                ResourceException re;
-                                try {
-                                    throw error;
-                                } catch (final EntryNotFoundException e) {
-                                    re = new BadRequestException(i18n(
-                                            "The request cannot be processed " + "because the resource '%s' "
-                                                    + "referenced in field '%s' does " + "not exist",
-                                            primaryKeyValue.toString(), path));
-                                } catch (final MultipleEntriesFoundException e) {
-                                    re = new BadRequestException(i18n(
-                                            "The request cannot be processed " + "because the resource '%s' "
-                                                    + "referenced in field '%s' is " + "ambiguous",
-                                            primaryKeyValue.toString(), path));
-                                } catch (final LdapException e) {
-                                    re = asResourceException(e);
-                                }
-                                exception.compareAndSet(null, re);
-                                completeIfNecessary();
-                            }
-                        });
+                    requestState.getConnection().thenOnResult(new ResultHandler<Connection>() {
+                        @Override
+                        public void handleResult(Connection connection) {
+                            connection.searchSingleEntryAsync(search)
+                                      .thenOnResult(new ResultHandler<SearchResultEntry>() {
+                                          @Override
+                                          public void handleResult(final SearchResultEntry result) {
+                                              synchronized (newLDAPAttribute) {
+                                                  newLDAPAttribute.add(result.getName());
+                                              }
+                                              completeIfNecessary();
+                                          }
+                                      }).thenOnException(new ExceptionHandler<LdapException>() {
+                                          @Override
+                                          public void handleException(final LdapException error) {
+                                              ResourceException re;
+                                              try {
+                                                  throw error;
+                                              } catch (final EntryNotFoundException e) {
+                                                  re = new BadRequestException(i18n(
+                                                          "The request cannot be processed because the resource "
+                                                          + "'%s' referenced in field '%s' does not exist",
+                                                          primaryKeyValue.toString(), path));
+                                              } catch (final MultipleEntriesFoundException e) {
+                                                  re = new BadRequestException(i18n(
+                                                          "The request cannot be processed because the resource "
+                                                          + "'%s' referenced in field '%s' is ambiguous",
+                                                          primaryKeyValue.toString(), path));
+                                              } catch (final LdapException e) {
+                                                  re = asResourceException(e);
+                                              }
+                                              exception.compareAndSet(null, re);
+                                              completeIfNecessary();
+                                          }
+                                      });
+                        }
+                    });
                 }
 
                 private void completeIfNecessary() {
                     if (pendingSearches.decrementAndGet() == 0) {
                         if (exception.get() != null) {
-                            h.handleError(exception.get());
+                            promise.handleException(exception.get());
                         } else {
-                            h.handleResult(newLDAPAttribute);
+                            promise.handleResult(newLDAPAttribute);
                         }
                     }
                 }
             });
         }
+        return promise;
     }
 
     @Override
@@ -280,47 +287,47 @@
     }
 
     @Override
-    void read(final Context c, final JsonPointer path, final Entry e, final ResultHandler<JsonValue> h) {
+    Promise<JsonValue, ResourceException> read(final RequestState c, final JsonPointer path, final Entry e) {
         final Attribute attribute = e.getAttribute(ldapAttributeName);
         if (attribute == null || attribute.isEmpty()) {
-            h.handleResult(null);
+            return Promises.newResultPromise(null);
         } else if (attributeIsSingleValued()) {
             try {
                 final DN dn = attribute.parse().usingSchema(c.getConfig().schema()).asDN();
-                readEntry(c, path, dn, h);
+                return readEntry(c, path, dn);
             } catch (final Exception ex) {
                 // The LDAP attribute could not be decoded.
-                h.handleError(asResourceException(ex));
+                return Promises.newExceptionPromise(asResourceException(ex));
             }
         } else {
             try {
                 final Set<DN> dns = attribute.parse().usingSchema(c.getConfig().schema()).asSetOfDN();
-                final ResultHandler<JsonValue> handler =
-                    accumulate(dns.size(), transform(new Function<List<JsonValue>, JsonValue, NeverThrowsException>() {
-                        @Override
-                        public JsonValue apply(final List<JsonValue> value) {
-                            if (value.isEmpty()) {
-                                /*
-                                 * No values, so omit the entire JSON object
-                                 * from the resource.
-                                 */
-                                return null;
-                            } else {
-                                // Combine values into a single JSON array.
-                                final List<Object> result = new ArrayList<>(value.size());
-                                for (final JsonValue e : value) {
-                                    result.add(e.getObject());
-                                }
-                                return new JsonValue(result);
-                            }
-                        }
-                    }, h));
+
+                final List<Promise<JsonValue, ResourceException>> promises = new ArrayList<>(dns.size());
                 for (final DN dn : dns) {
-                    readEntry(c, path, dn, handler);
+                    promises.add(readEntry(c, path, dn));
                 }
+
+                return Promises.when(promises)
+                               .then(new Function<List<JsonValue>, JsonValue, ResourceException>() {
+                                   @Override
+                                   public JsonValue apply(final List<JsonValue> value) {
+                                       if (value.isEmpty()) {
+                                           // No values, so omit the entire JSON object from the resource.
+                                           return null;
+                                       } else {
+                                           // Combine values into a single JSON array.
+                                           final List<Object> result = new ArrayList<>(value.size());
+                                           for (final JsonValue e : value) {
+                                               result.add(e.getObject());
+                                           }
+                                           return new JsonValue(result);
+                                       }
+                                   }
+                               });
             } catch (final Exception ex) {
                 // The LDAP attribute could not be decoded.
-                h.handleError(asResourceException(ex));
+                return Promises.newExceptionPromise(asResourceException(ex));
             }
         }
     }
@@ -330,30 +337,31 @@
         return newSearchRequest(baseDN, scope, searchFilter, "1.1");
     }
 
-    private void readEntry(final Context c, final JsonPointer path, final DN dn,
-        final ResultHandler<JsonValue> handler) {
+    private Promise<JsonValue, ResourceException> readEntry(
+            final RequestState requestState, final JsonPointer path, final DN dn) {
         final Set<String> requestedLDAPAttributes = new LinkedHashSet<>();
-        mapper.getLDAPAttributes(c, path, new JsonPointer(), requestedLDAPAttributes);
-        c.getConnection().readEntryAsync(dn, requestedLDAPAttributes)
-                .thenOnResult(new org.forgerock.util.promise.ResultHandler<SearchResultEntry>() {
-                    @Override
-                    public void handleResult(final SearchResultEntry result) {
-                        mapper.read(c, path, result, handler);
-                    }
-                }).thenOnException(new ExceptionHandler<LdapException>() {
-                    @Override
-                    public void handleException(final LdapException error) {
-                        if (!(error instanceof EntryNotFoundException)) {
-                            handler.handleError(asResourceException(error));
-                        } else {
-                            /*
-                             * The referenced entry does not exist so ignore it
-                             * since it cannot be mapped.
-                             */
-                            handler.handleResult(null);
-                        }
-                    }
-                });
+        mapper.getLDAPAttributes(requestState, path, new JsonPointer(), requestedLDAPAttributes);
+        return requestState.getConnection().thenAsync(new AsyncFunction<Connection, JsonValue, ResourceException>() {
+            @Override
+            public Promise<JsonValue, ResourceException> apply(Connection connection) throws ResourceException {
+                return connection.readEntryAsync(dn, requestedLDAPAttributes)
+                        .thenAsync(new AsyncFunction<SearchResultEntry, JsonValue, ResourceException>() {
+                            @Override
+                            public Promise<JsonValue, ResourceException> apply(final SearchResultEntry result) {
+                                return mapper.read(requestState, path, result);
+                            }
+                        }, new AsyncFunction<LdapException, JsonValue, ResourceException>() {
+                            @Override
+                            public Promise<JsonValue, ResourceException> apply(final LdapException error) {
+                                if (!(error instanceof EntryNotFoundException)) {
+                                    return Promises.newExceptionPromise(asResourceException(error));
+                                } else {
+                                    // The referenced entry does not exist so ignore it since it cannot be mapped.
+                                    return Promises.newResultPromise(null);
+                                }
+                            }
+                        });
+            }
+        });
     }
-
 }
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Context.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/RequestState.java
similarity index 90%
rename from opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Context.java
rename to opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/RequestState.java
index a27e3e0..825736a 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Context.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/RequestState.java
@@ -21,10 +21,10 @@
 import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.CountDownLatch;
 
+import org.forgerock.http.Context;
 import org.forgerock.json.resource.InternalServerErrorException;
 import org.forgerock.json.resource.ResourceException;
 import org.forgerock.json.resource.SecurityContext;
-import org.forgerock.json.resource.ServerContext;
 import org.forgerock.opendj.ldap.AbstractAsynchronousConnection;
 import org.forgerock.opendj.ldap.Connection;
 import org.forgerock.opendj.ldap.ConnectionEventListener;
@@ -55,20 +55,21 @@
 import org.forgerock.opendj.ldap.responses.SearchResultEntry;
 import org.forgerock.opendj.ldap.responses.SearchResultReference;
 import org.forgerock.util.promise.ExceptionHandler;
+import org.forgerock.util.promise.Promise;
+import org.forgerock.util.promise.PromiseImpl;
+import org.forgerock.util.promise.Promises;
 import org.forgerock.util.promise.ResultHandler;
 
 import static org.forgerock.opendj.rest2ldap.Rest2LDAP.*;
 import static org.forgerock.opendj.rest2ldap.Utils.*;
 
 /**
- * Common context information passed to containers and mappers. A new context is
- * allocated for each REST request.
+ * Common request information passed to containers and mappers.
+ * A new @{code RequestState} is allocated for each REST request.
  */
-final class Context implements Closeable {
+final class RequestState implements Closeable {
 
-    /**
-     * A cached read request - see cachedReads for more information.
-     */
+    /** A cached read request - see cachedReads for more information. */
     private static final class CachedRead implements SearchResultHandler, LdapResultHandler<Result> {
         private SearchResultEntry cachedEntry;
         private final String cachedFilterString;
@@ -122,10 +123,7 @@
         }
 
         LdapPromise<Result> getPromise() {
-            /*
-             * Perform uninterrupted wait since this method is unlikely to block
-             * for a long time.
-             */
+            // Perform uninterrupted wait since this method is unlikely to block for a long time.
             boolean wasInterrupted = false;
             while (true) {
                 try {
@@ -185,11 +183,11 @@
     };
 
     private final Config config;
-    private final ServerContext context;
+    private final Context context;
     private Connection connection;
     private Control proxiedAuthzControl;
 
-    Context(final Config config, final ServerContext context) {
+    RequestState(final Config config, final Context context) {
         this.config = config;
         this.context = context;
 
@@ -215,31 +213,20 @@
         return config;
     }
 
-    Connection getConnection() {
-        return connection;
-    }
-
-    ServerContext getServerContext() {
+    Context getContext() {
         return context;
     }
 
     /**
      * Performs common processing required before handling an HTTP request,
-     * including calculating the proxied authorization request control, and
-     * obtaining an LDAP connection.
+     * including calculating the proxied authorization request control. Then
+     * return a promise containing a valid LDAP connection or a
+     * {@link ResourceException} if an error is detected.
      * <p>
      * This method should be called at most once per request.
-     *
-     * @param handler
-     *            The result handler which should be invoked if an error is
-     *            detected.
-     * @param runnable
-     *            The runnable which will be invoked once the common processing
-     *            has completed. Implementations will be able to call
-     *            {@link #getConnection()} to get the LDAP connection for use
-     *            with subsequent LDAP requests.
+     * @return A {@link Promise} containing a valid {@link Connection}
      */
-    void run(final org.forgerock.json.resource.ResultHandler<?> handler, final Runnable runnable) {
+    Promise<Connection, ResourceException> getConnection() {
         /*
          * Compute the proxied authorization control from the content of the
          * security context if present. Only do this if we are not using a
@@ -254,13 +241,11 @@
                             securityContext.getAuthorizationId(), config.schema());
                     proxiedAuthzControl = ProxiedAuthV2RequestControl.newControl(authzId);
                 } catch (final ResourceException e) {
-                    handler.handleError(e);
-                    return;
+                    return Promises.newExceptionPromise(e);
                 }
             } else {
-                handler.handleError(new InternalServerErrorException(
+                return Promises.<Connection, ResourceException> newExceptionPromise(new InternalServerErrorException(
                         i18n("The request could not be authorized because it did not contain a security context")));
-                return;
             }
         }
 
@@ -270,23 +255,24 @@
          * to re-use the LDAP connection which was used for authentication.
          */
         if (connection != null) {
-            // Invoke the handler immediately since a connection is available.
-            runnable.run();
+            return Promises.newResultPromise(connection);
         } else if (config.connectionFactory() != null) {
+            final PromiseImpl<Connection, ResourceException> promise = PromiseImpl.create();
             config.connectionFactory().getConnectionAsync().thenOnResult(new ResultHandler<Connection>() {
                 @Override
                 public final void handleResult(final Connection result) {
                     connection = wrap(result);
-                    runnable.run();
+                    promise.handleResult(connection);
                 }
             }).thenOnException(new ExceptionHandler<LdapException>() {
                 @Override
                 public final void handleException(final LdapException exception) {
-                    handler.handleError(asResourceException(exception));
+                    promise.handleException(asResourceException(exception));
                 }
             });
+            return promise;
         } else {
-            handler.handleError(new InternalServerErrorException(
+            return Promises.<Connection, ResourceException> newExceptionPromise(new InternalServerErrorException(
                     i18n("The request could not be processed because there was no LDAP connection available for use")));
         }
     }
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java
index 74d250d..a6e3501 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java
@@ -31,8 +31,8 @@
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
 
-import org.forgerock.json.fluent.JsonValue;
-import org.forgerock.json.fluent.JsonValueException;
+import org.forgerock.json.JsonValue;
+import org.forgerock.json.JsonValueException;
 import org.forgerock.json.resource.BadRequestException;
 import org.forgerock.json.resource.CollectionResourceProvider;
 import org.forgerock.json.resource.ResourceException;
@@ -49,11 +49,11 @@
 import org.forgerock.opendj.ldap.DN;
 import org.forgerock.opendj.ldap.Entry;
 import org.forgerock.opendj.ldap.EntryNotFoundException;
-import org.forgerock.opendj.ldap.LdapException;
 import org.forgerock.opendj.ldap.FailoverLoadBalancingAlgorithm;
 import org.forgerock.opendj.ldap.Filter;
 import org.forgerock.opendj.ldap.LDAPConnectionFactory;
 import org.forgerock.opendj.ldap.LDAPOptions;
+import org.forgerock.opendj.ldap.LdapException;
 import org.forgerock.opendj.ldap.LinkedAttribute;
 import org.forgerock.opendj.ldap.MultipleEntriesFoundException;
 import org.forgerock.opendj.ldap.RDN;
@@ -74,7 +74,6 @@
  * collections.
  */
 public final class Rest2LDAP {
-
     /**
      * Indicates whether or not LDAP client connections should use SSL or
      * StartTLS.
@@ -715,23 +714,23 @@
         }
 
         @Override
-        SearchRequest createSearchRequest(final Context c, final DN baseDN, final String resourceId) {
+        SearchRequest createSearchRequest(final RequestState requestState, final DN baseDN, final String resourceId) {
             return newSearchRequest(baseDN, SearchScope.SINGLE_LEVEL, Filter.equality(idAttribute
                     .toString(), resourceId));
         }
 
         @Override
-        void getLDAPAttributes(final Context c, final Set<String> ldapAttributes) {
+        void getLDAPAttributes(final RequestState requestState, final Set<String> ldapAttributes) {
             ldapAttributes.add(idAttribute.toString());
         }
 
         @Override
-        String getResourceId(final Context c, final Entry entry) {
+        String getResourceId(final RequestState requestState, final Entry entry) {
             return entry.parseAttribute(idAttribute).asString();
         }
 
         @Override
-        void setResourceId(final Context c, final DN baseDN, final String resourceId,
+        void setResourceId(final RequestState requestState, final DN baseDN, final String resourceId,
                 final Entry entry) throws ResourceException {
             if (isServerProvided) {
                 if (resourceId != null) {
@@ -756,23 +755,23 @@
         }
 
         @Override
-        SearchRequest createSearchRequest(final Context c, final DN baseDN, final String resourceId) {
+        SearchRequest createSearchRequest(final RequestState requestState, final DN baseDN, final String resourceId) {
             return newSearchRequest(baseDN.child(rdn(resourceId)), SearchScope.BASE_OBJECT, Filter
                     .objectClassPresent());
         }
 
         @Override
-        void getLDAPAttributes(final Context c, final Set<String> ldapAttributes) {
+        void getLDAPAttributes(final RequestState requestState, final Set<String> ldapAttributes) {
             ldapAttributes.add(attribute.toString());
         }
 
         @Override
-        String getResourceId(final Context c, final Entry entry) {
+        String getResourceId(final RequestState requestState, final Entry entry) {
             return entry.parseAttribute(attribute).asString();
         }
 
         @Override
-        void setResourceId(final Context c, final DN baseDN, final String resourceId,
+        void setResourceId(final RequestState requestState, final DN baseDN, final String resourceId,
                 final Entry entry) throws ResourceException {
             if (resourceId != null) {
                 entry.setName(baseDN.child(rdn(resourceId)));
@@ -987,10 +986,6 @@
         return simple(AttributeDescription.valueOf(attribute));
     }
 
-    private static ConnectionFactory configureConnectionFactory(final JsonValue configuration) {
-        return configureConnectionFactory(configuration, (ClassLoader) null);
-    }
-
     private static ConnectionFactory configureConnectionFactory(final JsonValue configuration,
                                                                 final ClassLoader providerClassLoader) {
         // Parse pool parameters,
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAPHttpApplication.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAPHttpApplication.java
new file mode 100644
index 0000000..5b7588d
--- /dev/null
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAPHttpApplication.java
@@ -0,0 +1,167 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2015 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.rest2ldap;
+
+import static org.forgerock.http.util.Json.*;
+import static org.forgerock.opendj.rest2ldap.Rest2LDAP.configureConnectionFactory;
+import static org.forgerock.util.Utils.*;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+
+import org.forgerock.http.Context;
+import org.forgerock.http.Handler;
+import org.forgerock.http.HttpApplication;
+import org.forgerock.http.HttpApplicationException;
+import org.forgerock.http.handler.Handlers;
+import org.forgerock.http.io.Buffer;
+import org.forgerock.http.protocol.Request;
+import org.forgerock.http.protocol.Response;
+import org.forgerock.json.JsonValue;
+import org.forgerock.json.resource.CollectionResourceProvider;
+import org.forgerock.json.resource.RequestHandler;
+import org.forgerock.json.resource.Router;
+import org.forgerock.json.resource.http.CrestHttp;
+import org.forgerock.opendj.ldap.ConnectionFactory;
+import org.forgerock.util.Factory;
+import org.forgerock.util.Reject;
+import org.forgerock.util.promise.NeverThrowsException;
+import org.forgerock.util.promise.Promise;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Rest2ldap HTTP application. */
+public final class Rest2LDAPHttpApplication implements HttpApplication {
+    private static final Logger LOG = LoggerFactory.getLogger(Rest2LDAPHttpApplication.class);
+
+    private static final class HttpHandler implements Handler, Closeable {
+        private final ConnectionFactory ldapConnectionFactory;
+        private final Handler delegate;
+
+        HttpHandler(final JsonValue configuration) {
+            ldapConnectionFactory = createLdapConnectionFactory(configuration);
+            try {
+                delegate = CrestHttp.newHttpHandler(createRouter(configuration, ldapConnectionFactory));
+            } catch (final RuntimeException e) {
+                closeSilently(ldapConnectionFactory);
+                throw e;
+            }
+        }
+
+        private static RequestHandler createRouter(
+                final JsonValue configuration, final ConnectionFactory ldapConnectionFactory) {
+            final AuthorizationPolicy authzPolicy = configuration.get("servlet")
+                    .get("authorizationPolicy")
+                    .required()
+                    .asEnum(AuthorizationPolicy.class);
+            final String proxyAuthzTemplate = configuration.get("servlet").get("proxyAuthzIdTemplate").asString();
+            final JsonValue mappings = configuration.get("servlet").get("mappings").required();
+
+            final Router router = new Router();
+            for (final String mappingUrl : mappings.keys()) {
+                final JsonValue mapping = mappings.get(mappingUrl);
+                final CollectionResourceProvider provider = Rest2LDAP.builder()
+                        .ldapConnectionFactory(ldapConnectionFactory)
+                        .authorizationPolicy(authzPolicy)
+                        .proxyAuthzIdTemplate(proxyAuthzTemplate)
+                        .configureMapping(mapping)
+                        .build();
+                router.addRoute(Router.uriTemplate(mappingUrl), provider);
+            }
+            return router;
+        }
+
+        private static ConnectionFactory createLdapConnectionFactory(final JsonValue configuration) {
+            final String ldapFactoryName = configuration.get("servlet").get("ldapConnectionFactory").asString();
+            if (ldapFactoryName != null) {
+                return configureConnectionFactory(
+                        configuration.get("ldapConnectionFactories").required(), ldapFactoryName);
+            }
+            return null;
+        }
+
+        @Override
+        public void close() {
+            closeSilently(ldapConnectionFactory);
+        }
+
+        @Override
+        public Promise<Response, NeverThrowsException> handle(final Context context, final Request request) {
+            return delegate.handle(context, request);
+        }
+    }
+
+    private final URL configurationUrl;
+    private HttpHandler handler;
+    private HttpAuthenticationFilter filter;
+
+    /**
+     * Default constructor called by the HTTP Framework which will use the
+     * default configuration file location.
+     */
+    public Rest2LDAPHttpApplication() {
+        this.configurationUrl = getClass().getResource("/opendj-rest2ldap-config.json");
+    }
+
+    /**
+     * Creates a new Rest2LDAP HTTP application using the provided configuration URL.
+     *
+     * @param configurationURL
+     *            The URL to the JSON configuration file.
+     */
+    public Rest2LDAPHttpApplication(final URL configurationURL) {
+        Reject.ifNull(configurationURL, "The configuration URL must not be null");
+        this.configurationUrl = configurationURL;
+    }
+
+    private static JsonValue readJson(final URL resource) throws IOException {
+        try (InputStream in = resource.openStream()) {
+            return new JsonValue(readJsonLenient(in));
+        }
+    }
+
+    @Override
+    public Handler start() throws HttpApplicationException {
+        try {
+            final JsonValue configuration = readJson(configurationUrl);
+            handler = new HttpHandler(configuration);
+            filter = new HttpAuthenticationFilter(configuration);
+            return Handlers.chainOf(handler, filter);
+        } catch (final Exception e) {
+            // TODO i18n, once supported in opendj-rest2ldap
+            final String errorMsg = "Unable to start Rest2Ldap Http Application";
+            LOG.error(errorMsg, e);
+            stop();
+            throw new HttpApplicationException(errorMsg, e);
+        }
+    }
+
+    @Override
+    public Factory<Buffer> getBufferFactory() {
+        // Use container default buffer factory.
+        return null;
+    }
+
+    @Override
+    public void stop() {
+        closeSilently(handler, filter);
+        handler = null;
+        filter = null;
+    }
+}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimpleAttributeMapper.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimpleAttributeMapper.java
index a8f99bd..0035d55 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimpleAttributeMapper.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimpleAttributeMapper.java
@@ -11,7 +11,7 @@
  * Header, with the fields enclosed by brackets [] replaced by your own identifying
  * information: "Portions Copyright [year] [name of copyright owner]".
  *
- * Copyright 2012-2014 ForgeRock AS.
+ * Copyright 2012-2015 ForgeRock AS.
  */
 package org.forgerock.opendj.rest2ldap;
 
@@ -19,10 +19,10 @@
 import java.util.List;
 import java.util.Set;
 
-import org.forgerock.json.fluent.JsonPointer;
-import org.forgerock.json.fluent.JsonValue;
+import org.forgerock.json.JsonPointer;
+import org.forgerock.json.JsonValue;
 import org.forgerock.json.resource.BadRequestException;
-import org.forgerock.json.resource.ResultHandler;
+import org.forgerock.json.resource.ResourceException;
 import org.forgerock.opendj.ldap.Attribute;
 import org.forgerock.opendj.ldap.AttributeDescription;
 import org.forgerock.opendj.ldap.ByteString;
@@ -30,12 +30,15 @@
 import org.forgerock.opendj.ldap.Filter;
 import org.forgerock.util.Function;
 import org.forgerock.util.promise.NeverThrowsException;
+import org.forgerock.util.promise.Promise;
 
 import static java.util.Collections.*;
 
 import static org.forgerock.opendj.ldap.Filter.*;
 import static org.forgerock.opendj.rest2ldap.Rest2LDAP.*;
 import static org.forgerock.opendj.rest2ldap.Utils.*;
+import static org.forgerock.util.promise.Promises.newExceptionPromise;
+import static org.forgerock.util.promise.Promises.newResultPromise;
 
 /**
  * An attribute mapper which provides a simple mapping from a JSON value to a
@@ -111,34 +114,33 @@
     }
 
     @Override
-    void getLDAPFilter(final Context c, final JsonPointer path, final JsonPointer subPath,
-            final FilterType type, final String operator, final Object valueAssertion,
-            final ResultHandler<Filter> h) {
+    Promise<Filter, ResourceException> getLDAPFilter(final RequestState requestState, final JsonPointer path,
+            final JsonPointer subPath, final FilterType type, final String operator, final Object valueAssertion) {
         if (subPath.isEmpty()) {
             try {
                 final ByteString va =
                         valueAssertion != null ? encoder().apply(valueAssertion) : null;
-                h.handleResult(toFilter(type, ldapAttributeName.toString(), va));
+                return newResultPromise(toFilter(type, ldapAttributeName.toString(), va));
             } catch (final Exception e) {
                 // Invalid assertion value - bad request.
-                h.handleError(new BadRequestException(i18n(
+                return newExceptionPromise((ResourceException) new BadRequestException(i18n(
                         "The request cannot be processed because it contained an "
-                                + "illegal filter assertion value '%s' for field '%s'", String
-                                .valueOf(valueAssertion), path), e));
+                                + "illegal filter assertion value '%s' for field '%s'",
+                        String.valueOf(valueAssertion), path), e));
             }
         } else {
             // This attribute mapper does not support partial filtering.
-            h.handleResult(alwaysFalse());
+            return newResultPromise(alwaysFalse());
         }
     }
 
     @Override
-    void getNewLDAPAttributes(final Context c, final JsonPointer path,
-            final List<Object> newValues, final ResultHandler<Attribute> h) {
+    Promise<Attribute, ResourceException> getNewLDAPAttributes(
+            final RequestState requestState, final JsonPointer path, final List<Object> newValues) {
         try {
-            h.handleResult(jsonToAttribute(newValues, ldapAttributeName, encoder()));
+            return newResultPromise(jsonToAttribute(newValues, ldapAttributeName, encoder()));
         } catch (final Exception ex) {
-            h.handleError(new BadRequestException(i18n(
+            return newExceptionPromise((ResourceException) new BadRequestException(i18n(
                     "The request cannot be processed because an error occurred while "
                             + "encoding the values for the field '%s': %s", path, ex.getMessage())));
         }
@@ -150,8 +152,7 @@
     }
 
     @Override
-    void read(final Context c, final JsonPointer path, final Entry e,
-            final ResultHandler<JsonValue> h) {
+    Promise<JsonValue, ResourceException> read(final RequestState requestState, final JsonPointer path, final Entry e) {
         try {
             final Object value;
             if (attributeIsSingleValued()) {
@@ -161,16 +162,16 @@
             } else {
                 final Set<Object> s =
                         e.parseAttribute(ldapAttributeName).asSetOf(decoder(), defaultJSONValues);
-                value = s.isEmpty() ? null : new ArrayList<Object>(s);
+                value = s.isEmpty() ? null : new ArrayList<>(s);
             }
-            h.handleResult(value != null ? new JsonValue(value) : null);
+            return newResultPromise(value != null ? new JsonValue(value) : null);
         } catch (final Exception ex) {
             // The LDAP attribute could not be decoded.
-            h.handleError(asResourceException(ex));
+            return newExceptionPromise(asResourceException(ex));
         }
     }
 
-    private Function<ByteString, ? extends Object, NeverThrowsException> decoder() {
+    private Function<ByteString, ?, NeverThrowsException> decoder() {
         return decoder == null ? byteStringToJson(ldapAttributeName) : decoder;
     }
 
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Utils.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Utils.java
index 0a441de..2cc8b2a 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Utils.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Utils.java
@@ -25,18 +25,14 @@
 import static org.forgerock.opendj.ldap.schema.CoreSchema.getBooleanSyntax;
 import static org.forgerock.opendj.ldap.schema.CoreSchema.getGeneralizedTimeSyntax;
 import static org.forgerock.opendj.ldap.schema.CoreSchema.getIntegerSyntax;
-import static org.forgerock.opendj.rest2ldap.Rest2LDAP.asResourceException;
 
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 import java.util.Locale;
-import java.util.concurrent.atomic.AtomicInteger;
 
-import org.forgerock.json.fluent.JsonValue;
-import org.forgerock.json.resource.ResourceException;
-import org.forgerock.json.resource.ResultHandler;
+import org.forgerock.json.JsonValue;
 import org.forgerock.opendj.ldap.Attribute;
 import org.forgerock.opendj.ldap.AttributeDescription;
 import org.forgerock.opendj.ldap.ByteString;
@@ -51,64 +47,6 @@
  * Internal utility methods.
  */
 final class Utils {
-    /**
-     * Implementation class for {@link #accumulate}.
-     *
-     * @param <V>
-     *            The type of result.
-     */
-    private static final class AccumulatingResultHandler<V> implements ResultHandler<V> {
-        /** Guarded by latch. */
-        private ResourceException exception;
-        private final ResultHandler<List<V>> handler;
-        private final AtomicInteger latch;
-        private final List<V> results;
-
-        private AccumulatingResultHandler(final int size, final ResultHandler<List<V>> handler) {
-            if (size < 0) {
-                throw new IllegalStateException();
-            }
-            this.latch = new AtomicInteger(size);
-            this.results = new ArrayList<>(size);
-            this.handler = handler;
-            if (size == 0) {
-                // Invoke immediately.
-                handler.handleResult(results);
-            }
-        }
-
-        @Override
-        public void handleError(final ResourceException e) {
-            exception = e;
-            latch(); // Volatile write publishes exception.
-        }
-
-        @Override
-        public void handleResult(final V result) {
-            if (result != null) {
-                synchronized (results) {
-                    results.add(result);
-                }
-            }
-            latch();
-        }
-
-        private void latch() {
-            /*
-             * Invoke the handler once all results have been received. Avoid
-             * failing-fast when an error occurs because some in-flight tasks
-             * may depend on resources (e.g. connections) which are
-             * automatically closed on completion.
-             */
-            if (latch.decrementAndGet() == 0) {
-                if (exception != null) {
-                    handler.handleError(exception);
-                } else {
-                    handler.handleResult(results);
-                }
-            }
-        }
-    }
 
     private static final Function<Object, ByteString, NeverThrowsException> BASE64_TO_BYTESTRING =
             new Function<Object, ByteString, NeverThrowsException>() {
@@ -126,30 +64,6 @@
                 }
             };
 
-    /**
-     * Returns a result handler which can be used to collect the results of
-     * {@code size} asynchronous operations. Once all results have been received
-     * {@code handler} will be invoked with a list containing the results.
-     * Accumulation ignores {@code null} results, so the result list may be
-     * smaller than {@code size}. The returned result handler does not
-     * fail-fast: it will wait until all results have been received even if an
-     * error has been detected. This ensures that asynchronous operations can
-     * use resources such as connections which are automatically released
-     * (closed) upon completion of the final operation.
-     *
-     * @param <V>
-     *            The type of result to be collected.
-     * @param size
-     *            The number of expected results.
-     * @param handler
-     *            The result handler to be invoked when all results have been
-     *            received.
-     * @return A result handler which can be used to collect the results of
-     *         {@code size} asynchronous operations.
-     */
-    static <V> ResultHandler<V> accumulate(final int size, final ResultHandler<List<V>> handler) {
-        return new AccumulatingResultHandler<>(size, handler);
-    }
 
     static Object attributeToJson(final Attribute a) {
         final Function<ByteString, Object, NeverThrowsException> f = byteStringToJson(a.getAttributeDescription());
@@ -294,44 +208,6 @@
         return s != null ? s.toLowerCase(Locale.ENGLISH) : null;
     }
 
-    /**
-     * Returns a result handler which accepts results of type {@code M}, applies
-     * the function {@code f} in order to convert the result to an object of
-     * type {@code N}, and subsequently invokes {@code handler}. If an
-     * unexpected error occurs while performing the transformation, the
-     * exception is converted to a {@code ResourceException} before invoking
-     * {@code handler.handleError()}.
-     *
-     * @param <M>
-     *            The type of result expected by the returned handler.
-     * @param <N>
-     *            The type of result expected by {@code handler}.
-     * @param f
-     *            A function which converts the result of type {@code M} to type
-     *            {@code N}.
-     * @param handler
-     *            A result handler which accepts results of type {@code N}.
-     * @return A result handler which accepts results of type {@code M}.
-     */
-    static <M, N> ResultHandler<M> transform(final Function<M, N, NeverThrowsException> f,
-            final ResultHandler<N> handler) {
-        return new ResultHandler<M>() {
-            @Override
-            public void handleError(final ResourceException error) {
-                handler.handleError(error);
-            }
-
-            @Override
-            public void handleResult(final M result) {
-                try {
-                    handler.handleResult(f.apply(result));
-                } catch (final Throwable t) {
-                    handler.handleError(asResourceException(t));
-                }
-            }
-        };
-    }
-
     private static <T> List<T> asList(final Collection<T> c) {
         if (c instanceof List) {
             return (List<T>) c;
diff --git a/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java
index 6311a92..9c9d9c4 100644
--- a/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java
+++ b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java
@@ -17,10 +17,9 @@
 
 import static java.util.Arrays.asList;
 import static org.fest.assertions.Assertions.assertThat;
-import static org.fest.assertions.Fail.fail;
-import static org.forgerock.json.fluent.JsonValue.field;
-import static org.forgerock.json.fluent.JsonValue.json;
-import static org.forgerock.json.fluent.JsonValue.object;
+import static org.forgerock.json.JsonValue.field;
+import static org.forgerock.json.JsonValue.json;
+import static org.forgerock.json.JsonValue.object;
 import static org.forgerock.json.resource.PatchOperation.add;
 import static org.forgerock.json.resource.PatchOperation.increment;
 import static org.forgerock.json.resource.PatchOperation.remove;
@@ -46,22 +45,22 @@
 import java.util.LinkedList;
 import java.util.List;
 
-import org.forgerock.json.fluent.JsonValue;
+import org.forgerock.json.JsonPointer;
+import org.forgerock.json.JsonValue;
 import org.forgerock.json.resource.BadRequestException;
 import org.forgerock.json.resource.Connection;
 import org.forgerock.json.resource.NotFoundException;
 import org.forgerock.json.resource.NotSupportedException;
 import org.forgerock.json.resource.PreconditionFailedException;
-import org.forgerock.json.resource.QueryFilter;
-import org.forgerock.json.resource.QueryResult;
+import org.forgerock.json.resource.QueryResponse;
 import org.forgerock.json.resource.Requests;
-import org.forgerock.json.resource.Resource;
+import org.forgerock.json.resource.ResourceResponse;
 import org.forgerock.opendj.ldap.ConnectionFactory;
 import org.forgerock.opendj.ldap.IntermediateResponseHandler;
+import org.forgerock.opendj.ldap.LdapResultHandler;
 import org.forgerock.opendj.ldap.MemoryBackend;
 import org.forgerock.opendj.ldap.RequestContext;
 import org.forgerock.opendj.ldap.RequestHandler;
-import org.forgerock.opendj.ldap.LdapResultHandler;
 import org.forgerock.opendj.ldap.SearchResultHandler;
 import org.forgerock.opendj.ldap.requests.AddRequest;
 import org.forgerock.opendj.ldap.requests.BindRequest;
@@ -79,11 +78,10 @@
 import org.forgerock.opendj.ldif.LDIFEntryReader;
 import org.forgerock.opendj.rest2ldap.Rest2LDAP.Builder;
 import org.forgerock.testng.ForgeRockTestCase;
+import org.forgerock.util.query.QueryFilter;
 import org.testng.annotations.Test;
 
-/**
- * Tests that CREST requests are correctly mapped to LDAP.
- */
+/** Tests that CREST requests are correctly mapped to LDAP. */
 @SuppressWarnings({ "javadoc" })
 @Test
 public final class BasicRequestsTest extends ForgeRockTestCase {
@@ -91,39 +89,38 @@
     // so that we can check that the request handler is returning everything.
     // FIXME: factor out test for re-use as common test suite (e.g. for InMemoryBackend).
 
+    private static final QueryFilter<JsonPointer> NO_FILTER = QueryFilter.alwaysTrue();
+
     @Test
     public void testQueryAll() throws Exception {
         final Connection connection = newConnection();
-        final List<Resource> resources = new LinkedList<>();
-        final QueryResult result =
-                connection.query(ctx(), Requests.newQueryRequest("").setQueryFilter(
-                        QueryFilter.alwaysTrue()), resources);
+        final List<ResourceResponse> resources = new LinkedList<>();
+        final QueryResponse result = connection.query(
+            ctx(), Requests.newQueryRequest("").setQueryFilter(NO_FILTER), resources);
         assertThat(resources).hasSize(5);
         assertThat(result.getPagedResultsCookie()).isNull();
-        assertThat(result.getRemainingPagedResults()).isEqualTo(-1);
+        assertThat(result.getTotalPagedResults()).isEqualTo(-1);
     }
 
     @Test
     public void testQueryNone() throws Exception {
         final Connection connection = newConnection();
-        final List<Resource> resources = new LinkedList<>();
-        final QueryResult result =
-                connection.query(ctx(), Requests.newQueryRequest("").setQueryFilter(
-                        QueryFilter.alwaysFalse()), resources);
+        final List<ResourceResponse> resources = new LinkedList<>();
+        final QueryResponse result = connection.query(
+            ctx(), Requests.newQueryRequest("").setQueryFilter(QueryFilter.<JsonPointer>alwaysFalse()), resources);
         assertThat(resources).hasSize(0);
         assertThat(result.getPagedResultsCookie()).isNull();
-        assertThat(result.getRemainingPagedResults()).isEqualTo(-1);
+        assertThat(result.getTotalPagedResults()).isEqualTo(-1);
     }
 
     @Test
     public void testQueryPageResultsCookie() throws Exception {
         final Connection connection = newConnection();
-        final List<Resource> resources = new ArrayList<>();
+        final List<ResourceResponse> resources = new ArrayList<>();
 
         // Read first page.
-        QueryResult result =
-                connection.query(ctx(), newQueryRequest("")
-                        .setQueryFilter(QueryFilter.alwaysTrue()).setPageSize(2), resources);
+        QueryResponse result = connection.query(
+                ctx(), newQueryRequest("").setQueryFilter(NO_FILTER).setPageSize(2), resources);
         assertThat(result.getPagedResultsCookie()).isNotNull();
         assertThat(resources).hasSize(2);
         assertThat(resources.get(0).getId()).isEqualTo("test1");
@@ -133,10 +130,8 @@
         resources.clear();
 
         // Read second page.
-        result =
-                connection.query(ctx(), newQueryRequest("")
-                        .setQueryFilter(QueryFilter.alwaysTrue()).setPageSize(2)
-                        .setPagedResultsCookie(cookie), resources);
+        result = connection.query(ctx(),
+                newQueryRequest("").setQueryFilter(NO_FILTER).setPageSize(2).setPagedResultsCookie(cookie), resources);
         assertThat(result.getPagedResultsCookie()).isNotNull();
         assertThat(resources).hasSize(2);
         assertThat(resources.get(0).getId()).isEqualTo("test3");
@@ -146,10 +141,8 @@
         resources.clear();
 
         // Read third page.
-        result =
-                connection.query(ctx(), newQueryRequest("")
-                        .setQueryFilter(QueryFilter.alwaysTrue()).setPageSize(2)
-                        .setPagedResultsCookie(cookie), resources);
+        result = connection.query(ctx(),
+                newQueryRequest("").setQueryFilter(NO_FILTER).setPageSize(2).setPagedResultsCookie(cookie), resources);
         assertThat(result.getPagedResultsCookie()).isNull();
         assertThat(resources).hasSize(1);
         assertThat(resources.get(0).getId()).isEqualTo("test5");
@@ -158,42 +151,29 @@
     @Test
     public void testQueryPageResultsIndexed() throws Exception {
         final Connection connection = newConnection();
-        final List<Resource> resources = new ArrayList<>();
-        QueryResult result =
-                connection.query(ctx(), newQueryRequest("")
-                        .setQueryFilter(QueryFilter.alwaysTrue()).setPageSize(2)
-                        .setPagedResultsOffset(1), resources);
+        final List<ResourceResponse> resources = new ArrayList<>();
+        QueryResponse result = connection.query(ctx(),
+                newQueryRequest("").setQueryFilter(NO_FILTER).setPageSize(2).setPagedResultsOffset(1), resources);
         assertThat(result.getPagedResultsCookie()).isNotNull();
         assertThat(resources).hasSize(2);
         assertThat(resources.get(0).getId()).isEqualTo("test3");
         assertThat(resources.get(1).getId()).isEqualTo("test4");
     }
 
-    @Test
+    @Test(expectedExceptions = NotFoundException.class)
     public void testDelete() throws Exception {
         final Connection connection = newConnection();
-        final Resource resource = connection.delete(ctx(), newDeleteRequest("/test1"));
+        final ResourceResponse resource = connection.delete(ctx(), newDeleteRequest("/test1"));
         checkResourcesAreEqual(resource, getTestUser1(12345));
-        try {
-            connection.read(ctx(), newReadRequest("/test1"));
-            fail("Read succeeded unexpectedly");
-        } catch (final NotFoundException e) {
-            // Expected.
-        }
+        connection.read(ctx(), newReadRequest("/test1"));
     }
 
-    @Test
+    @Test(expectedExceptions = NotFoundException.class)
     public void testDeleteMVCCMatch() throws Exception {
         final Connection connection = newConnection();
-        final Resource resource =
-                connection.delete(ctx(), newDeleteRequest("/test1").setRevision("12345"));
+        final ResourceResponse resource = connection.delete(ctx(), newDeleteRequest("/test1").setRevision("12345"));
         checkResourcesAreEqual(resource, getTestUser1(12345));
-        try {
-            connection.read(ctx(), newReadRequest("/test1"));
-            fail("Read succeeded unexpectedly");
-        } catch (final NotFoundException e) {
-            // Expected.
-        }
+        connection.read(ctx(), newReadRequest("/test1"));
     }
 
     @Test(expectedExceptions = PreconditionFailedException.class)
@@ -211,11 +191,10 @@
     @Test
     public void testPatch() throws Exception {
         final Connection connection = newConnection();
-        final Resource resource1 =
-                connection.patch(ctx(), newPatchRequest("/test1", add("/name/displayName",
-                        "changed")));
+        final ResourceResponse resource1 =
+                connection.patch(ctx(), newPatchRequest("/test1", add("/name/displayName", "changed")));
         checkResourcesAreEqual(resource1, getTestUser1Updated(12345));
-        final Resource resource2 = connection.read(ctx(), newReadRequest("/test1"));
+        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
         checkResourcesAreEqual(resource2, getTestUser1Updated(12345));
     }
 
@@ -223,7 +202,7 @@
     public void testPatchEmpty() throws Exception {
         final List<Request> requests = new LinkedList<>();
         final Connection connection = newConnection(requests);
-        final Resource resource1 = connection.patch(ctx(), newPatchRequest("/test1"));
+        final ResourceResponse resource1 = connection.patch(ctx(), newPatchRequest("/test1"));
         checkResourcesAreEqual(resource1, getTestUser1(12345));
 
         /*
@@ -233,7 +212,7 @@
         assertThat(requests).hasSize(1);
         assertThat(requests.get(0)).isInstanceOf(SearchRequest.class);
 
-        final Resource resource2 = connection.read(ctx(), newReadRequest("/test1"));
+        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
         checkResourcesAreEqual(resource2, getTestUser1(12345));
     }
 
@@ -242,11 +221,11 @@
         final Connection connection = newConnection();
         final JsonValue newContent = getTestUser1(12345);
         newContent.put("description", asList("one", "two"));
-        final Resource resource1 =
+        final ResourceResponse resource1 =
                 connection.patch(ctx(), newPatchRequest("/test1", add("/description", asList("one",
                         "two"))));
         checkResourcesAreEqual(resource1, newContent);
-        final Resource resource2 = connection.read(ctx(), newReadRequest("/test1"));
+        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
         checkResourcesAreEqual(resource2, newContent);
     }
 
@@ -255,29 +234,25 @@
         final Connection connection = newConnection();
         final JsonValue newContent = getTestUser1(12345);
         newContent.put("description", asList("one", "two"));
-        final Resource resource1 =
-                connection.patch(ctx(), newPatchRequest("/test1", add("/description/-", "one"),
-                        add("/description/-", "two")));
+        final ResourceResponse resource1 = connection.patch(
+            ctx(), newPatchRequest("/test1", add("/description/-", "one"), add("/description/-", "two")));
         checkResourcesAreEqual(resource1, newContent);
-        final Resource resource2 = connection.read(ctx(), newReadRequest("/test1"));
+        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
         checkResourcesAreEqual(resource2, newContent);
     }
 
     @Test(expectedExceptions = BadRequestException.class)
     public void testPatchConstantAttribute() throws Exception {
-        final Connection connection = newConnection();
-        connection.patch(ctx(), newPatchRequest("/test1", add("/schemas", asList("junk"))));
+        newConnection().patch(ctx(), newPatchRequest("/test1", add("/schemas", asList("junk"))));
     }
 
     @Test
     public void testPatchDeleteOptionalAttribute() throws Exception {
         final Connection connection = newConnection();
-        connection.patch(ctx(),
-                newPatchRequest("/test1", add("/description", asList("one", "two"))));
-        final Resource resource1 =
-                connection.patch(ctx(), newPatchRequest("/test1", remove("/description")));
+        connection.patch(ctx(), newPatchRequest("/test1", add("/description", asList("one", "two"))));
+        final ResourceResponse resource1 = connection.patch(ctx(), newPatchRequest("/test1", remove("/description")));
         checkResourcesAreEqual(resource1, getTestUser1(12345));
-        final Resource resource2 = connection.read(ctx(), newReadRequest("/test1"));
+        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
         checkResourcesAreEqual(resource2, getTestUser1(12345));
     }
 
@@ -288,33 +263,31 @@
         newContent.put("singleNumber", 100);
         newContent.put("multiNumber", asList(200, 300));
 
-        final Resource resource1 =
-                connection.patch(ctx(), newPatchRequest("/test1", add("/singleNumber", 0), add(
-                        "/multiNumber", asList(100, 200)), increment("/singleNumber", 100),
-                        increment("/multiNumber", 100)));
+        final ResourceResponse resource1 = connection.patch(ctx(), newPatchRequest("/test1",
+            add("/singleNumber", 0),
+            add("/multiNumber", asList(100, 200)),
+            increment("/singleNumber", 100),
+            increment("/multiNumber", 100)));
         checkResourcesAreEqual(resource1, newContent);
-        final Resource resource2 = connection.read(ctx(), newReadRequest("/test1"));
+        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
         checkResourcesAreEqual(resource2, newContent);
     }
 
     @Test(expectedExceptions = BadRequestException.class)
     public void testPatchMissingRequiredAttribute() throws Exception {
-        final Connection connection = newConnection();
-        connection.patch(ctx(), newPatchRequest("/test1", remove("/name/surname")));
+        newConnection().patch(ctx(), newPatchRequest("/test1", remove("/name/surname")));
     }
 
     @Test
     public void testPatchModifyOptionalAttribute() throws Exception {
         final Connection connection = newConnection();
-        connection.patch(ctx(),
-                newPatchRequest("/test1", add("/description", asList("one", "two"))));
-        final Resource resource1 =
-                connection.patch(ctx(), newPatchRequest("/test1", add("/description",
-                        asList("three"))));
+        connection.patch(ctx(), newPatchRequest("/test1", add("/description", asList("one", "two"))));
+        final ResourceResponse resource1 =
+                connection.patch(ctx(), newPatchRequest("/test1", add("/description", asList("three"))));
         final JsonValue newContent = getTestUser1(12345);
         newContent.put("description", asList("one", "two", "three"));
         checkResourcesAreEqual(resource1, newContent);
-        final Resource resource2 = connection.read(ctx(), newReadRequest("/test1"));
+        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
         checkResourcesAreEqual(resource2, newContent);
     }
 
@@ -340,63 +313,62 @@
     @Test
     public void testPatchMVCCMatch() throws Exception {
         final Connection connection = newConnection();
-        final Resource resource1 =
-                connection.patch(ctx(), newPatchRequest("/test1",
-                        add("/name/displayName", "changed")).setRevision("12345"));
+        final ResourceResponse resource1 = connection.patch(
+            ctx(), newPatchRequest("/test1", add("/name/displayName", "changed")).setRevision("12345"));
         checkResourcesAreEqual(resource1, getTestUser1Updated(12345));
-        final Resource resource2 = connection.read(ctx(), newReadRequest("/test1"));
+        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
         checkResourcesAreEqual(resource2, getTestUser1Updated(12345));
     }
 
     @Test(expectedExceptions = PreconditionFailedException.class)
     public void testPatchMVCCNoMatch() throws Exception {
         final Connection connection = newConnection();
-        connection.patch(ctx(), newPatchRequest("/test1", add("/name/displayName", "changed"))
-                .setRevision("12346"));
+        connection.patch(ctx(), newPatchRequest("/test1", add("/name/displayName", "changed")).setRevision("12346"));
     }
 
     @Test(expectedExceptions = NotFoundException.class)
     public void testPatchNotFound() throws Exception {
-        final Connection connection = newConnection();
-        connection.patch(ctx(), newPatchRequest("/missing", add("/name/displayName", "changed")));
+        newConnection().patch(ctx(), newPatchRequest("/missing", add("/name/displayName", "changed")));
     }
 
     @Test(expectedExceptions = BadRequestException.class)
     public void testPatchReadOnlyAttribute() throws Exception {
-        final Connection connection = newConnection();
         // Etag is read-only.
-        connection.patch(ctx(), newPatchRequest("/test1", add("_rev", "99999")));
+        newConnection().patch(ctx(), newPatchRequest("/test1", add("_rev", "99999")));
     }
 
     @Test
     public void testPatchReplacePartialObject() throws Exception {
         final Connection connection = newConnection();
-        final JsonValue expected =
-                json(object(field("schemas", asList("urn:scim:schemas:core:1.0")), field("_id",
-                        "test1"), field("_rev", "12345"), field("name", object(field("displayName",
-                        "Humpty"), field("surname", "Dumpty")))));
-        final Resource resource1 =
-                connection.patch(ctx(), newPatchRequest("/test1", replace("/name", object(field(
-                        "displayName", "Humpty"), field("surname", "Dumpty")))));
+        final JsonValue expected = json(object(
+            field("schemas", asList("urn:scim:schemas:core:1.0")),
+            field("_id", "test1"),
+            field("_rev", "12345"),
+            field("name", object(field("displayName", "Humpty"),
+                                 field("surname", "Dumpty")))));
+        final ResourceResponse resource1 = connection.patch(ctx(), newPatchRequest("/test1",
+            replace("/name", object(field("displayName", "Humpty"), field("surname", "Dumpty")))));
         checkResourcesAreEqual(resource1, expected);
-        final Resource resource2 = connection.read(ctx(), newReadRequest("/test1"));
+        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
         checkResourcesAreEqual(resource2, expected);
     }
 
     @Test
     public void testPatchReplaceWholeObject() throws Exception {
         final Connection connection = newConnection();
-        final JsonValue newContent =
-                json(object(field("name", object(field("displayName", "Humpty"), field("surname",
-                        "Dumpty")))));
-        final JsonValue expected =
-                json(object(field("schemas", asList("urn:scim:schemas:core:1.0")), field("_id",
-                        "test1"), field("_rev", "12345"), field("name", object(field("displayName",
-                        "Humpty"), field("surname", "Dumpty")))));
-        final Resource resource1 =
+        final JsonValue newContent = json(object(
+            field("name", object(field("displayName", "Humpty"),
+                                 field("surname", "Dumpty")))));
+        final JsonValue expected = json(object(
+            field("schemas", asList("urn:scim:schemas:core:1.0")),
+            field("_id", "test1"),
+            field("_rev", "12345"),
+            field("name", object(field("displayName", "Humpty"),
+                                 field("surname", "Dumpty")))));
+        final ResourceResponse resource1 =
                 connection.patch(ctx(), newPatchRequest("/test1", replace("/", newContent)));
         checkResourcesAreEqual(resource1, expected);
-        final Resource resource2 = connection.read(ctx(), newReadRequest("/test1"));
+        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
         checkResourcesAreEqual(resource2, expected);
     }
 
@@ -439,7 +411,7 @@
 
     @Test
     public void testRead() throws Exception {
-        final Resource resource = newConnection().read(ctx(), newReadRequest("/test1"));
+        final ResourceResponse resource = newConnection().read(ctx(), newReadRequest("/test1"));
         checkResourcesAreEqual(resource, getTestUser1(12345));
     }
 
@@ -450,15 +422,14 @@
 
     @Test
     public void testReadSelectAllFields() throws Exception {
-        final Resource resource =
-                newConnection().read(ctx(), newReadRequest("/test1").addField("/"));
+        final ResourceResponse resource = newConnection().read(ctx(), newReadRequest("/test1").addField("/"));
         checkResourcesAreEqual(resource, getTestUser1(12345));
     }
 
     @Test
     public void testReadSelectPartial() throws Exception {
-        final Resource resource =
-                newConnection().read(ctx(), newReadRequest("/test1").addField("/name/surname"));
+        final ResourceResponse resource = newConnection().read(
+            ctx(), newReadRequest("/test1").addField("/name/surname"));
         assertThat(resource.getId()).isEqualTo("test1");
         assertThat(resource.getRevision()).isEqualTo("12345");
         assertThat(resource.getContent().get("_id").asString()).isNull();
@@ -470,8 +441,8 @@
     /** Disabled - see CREST-86 (Should JSON resource fields be case insensitive?) */
     @Test(enabled = false)
     public void testReadSelectPartialInsensitive() throws Exception {
-        final Resource resource =
-                newConnection().read(ctx(), newReadRequest("/test1").addField("/name/SURNAME"));
+        final ResourceResponse resource = newConnection().read(
+            ctx(), newReadRequest("/test1").addField("/name/SURNAME"));
         assertThat(resource.getId()).isEqualTo("test1");
         assertThat(resource.getRevision()).isEqualTo("12345");
         assertThat(resource.getContent().get("_id").asString()).isNull();
@@ -483,10 +454,10 @@
     @Test
     public void testUpdate() throws Exception {
         final Connection connection = newConnection();
-        final Resource resource1 =
-                connection.update(ctx(), newUpdateRequest("/test1", getTestUser1Updated(12345)));
+        final ResourceResponse resource1 = connection.update(
+            ctx(), newUpdateRequest("/test1", getTestUser1Updated(12345)));
         checkResourcesAreEqual(resource1, getTestUser1Updated(12345));
-        final Resource resource2 = connection.read(ctx(), newReadRequest("/test1"));
+        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
         checkResourcesAreEqual(resource2, getTestUser1Updated(12345));
     }
 
@@ -494,18 +465,15 @@
     public void testUpdateNoChange() throws Exception {
         final List<Request> requests = new LinkedList<>();
         final Connection connection = newConnection(requests);
-        final Resource resource1 =
-                connection.update(ctx(), newUpdateRequest("/test1", getTestUser1(12345)));
+        final ResourceResponse resource1 = connection.update(ctx(), newUpdateRequest("/test1", getTestUser1(12345)));
 
-        /*
-         * Check that no modify operation was sent (only a single search should
-         * be sent in order to get the current resource).
-         */
+        // Check that no modify operation was sent
+        // (only a single search should be sent in order to get the current resource).
         assertThat(requests).hasSize(1);
         assertThat(requests.get(0)).isInstanceOf(SearchRequest.class);
 
         checkResourcesAreEqual(resource1, getTestUser1(12345));
-        final Resource resource2 = connection.read(ctx(), newReadRequest("/test1"));
+        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
         checkResourcesAreEqual(resource2, getTestUser1(12345));
     }
 
@@ -514,9 +482,9 @@
         final Connection connection = newConnection();
         final JsonValue newContent = getTestUser1Updated(12345);
         newContent.put("description", asList("one", "two"));
-        final Resource resource1 = connection.update(ctx(), newUpdateRequest("/test1", newContent));
+        final ResourceResponse resource1 = connection.update(ctx(), newUpdateRequest("/test1", newContent));
         checkResourcesAreEqual(resource1, newContent);
-        final Resource resource2 = connection.read(ctx(), newReadRequest("/test1"));
+        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
         checkResourcesAreEqual(resource2, newContent);
     }
 
@@ -535,9 +503,9 @@
         newContent.put("description", asList("one", "two"));
         connection.update(ctx(), newUpdateRequest("/test1", newContent));
         newContent.remove("description");
-        final Resource resource1 = connection.update(ctx(), newUpdateRequest("/test1", newContent));
+        final ResourceResponse resource1 = connection.update(ctx(), newUpdateRequest("/test1", newContent));
         checkResourcesAreEqual(resource1, newContent);
-        final Resource resource2 = connection.read(ctx(), newReadRequest("/test1"));
+        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
         checkResourcesAreEqual(resource2, newContent);
     }
 
@@ -556,20 +524,19 @@
         newContent.put("description", asList("one", "two"));
         connection.update(ctx(), newUpdateRequest("/test1", newContent));
         newContent.put("description", asList("three"));
-        final Resource resource1 = connection.update(ctx(), newUpdateRequest("/test1", newContent));
+        final ResourceResponse resource1 = connection.update(ctx(), newUpdateRequest("/test1", newContent));
         checkResourcesAreEqual(resource1, newContent);
-        final Resource resource2 = connection.read(ctx(), newReadRequest("/test1"));
+        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
         checkResourcesAreEqual(resource2, newContent);
     }
 
     @Test
     public void testUpdateMVCCMatch() throws Exception {
         final Connection connection = newConnection();
-        final Resource resource1 =
-                connection.update(ctx(), newUpdateRequest("/test1", getTestUser1Updated(12345))
-                        .setRevision("12345"));
+        final ResourceResponse resource1 =
+                connection.update(ctx(), newUpdateRequest("/test1", getTestUser1Updated(12345)).setRevision("12345"));
         checkResourcesAreEqual(resource1, getTestUser1Updated(12345));
-        final Resource resource2 = connection.read(ctx(), newReadRequest("/test1"));
+        final ResourceResponse resource2 = connection.read(ctx(), newReadRequest("/test1"));
         checkResourcesAreEqual(resource2, getTestUser1Updated(12345));
     }
 
@@ -643,8 +610,8 @@
                         .attribute("multiNumber", simple("multiNumber").decoder(byteStringToInteger())));
     }
 
-    private void checkResourcesAreEqual(final Resource actual, final JsonValue expected) {
-        final Resource expectedResource = asResource(expected);
+    private void checkResourcesAreEqual(final ResourceResponse actual, final JsonValue expected) {
+        final ResourceResponse expectedResource = asResource(expected);
         assertThat(actual.getId()).isEqualTo(expectedResource.getId());
         assertThat(actual.getRevision()).isEqualTo(expectedResource.getRevision());
         assertThat(actual.getContent().getObject()).isEqualTo(
@@ -790,14 +757,20 @@
     }
 
     private JsonValue getTestUser1(final int rev) {
-        return content(object(field("schemas", asList("urn:scim:schemas:core:1.0")), field("_id",
-                "test1"), field("_rev", String.valueOf(rev)), field("name", object(field(
-                "displayName", "test user 1"), field("surname", "user 1")))));
+        return content(object(
+                field("schemas", asList("urn:scim:schemas:core:1.0")),
+                field("_id", "test1"),
+                field("_rev", String.valueOf(rev)),
+                field("name", object(field("displayName", "test user 1"),
+                                     field("surname", "user 1")))));
     }
 
     private JsonValue getTestUser1Updated(final int rev) {
-        return content(object(field("schemas", asList("urn:scim:schemas:core:1.0")), field("_id",
-                "test1"), field("_rev", String.valueOf(rev)), field("name", object(field(
-                "displayName", "changed"), field("surname", "user 1")))));
+        return content(object(
+                field("schemas", asList("urn:scim:schemas:core:1.0")),
+                field("_id", "test1"),
+                field("_rev", String.valueOf(rev)),
+                field("name", object(field("displayName", "changed"),
+                                     field("surname", "user 1")))));
     }
 }
diff --git a/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/TestUtils.java b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/TestUtils.java
index cef00f4..a3aa64c 100644
--- a/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/TestUtils.java
+++ b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/TestUtils.java
@@ -15,20 +15,18 @@
  */
 package org.forgerock.opendj.rest2ldap;
 
-import static org.forgerock.json.fluent.JsonValue.json;
+import static org.forgerock.json.JsonValue.json;
 
 import java.util.ArrayList;
 import java.util.List;
 
-import org.forgerock.json.fluent.JsonPointer;
-import org.forgerock.json.fluent.JsonValue;
-import org.forgerock.json.resource.Resource;
-import org.forgerock.json.resource.RootContext;
+import org.forgerock.http.context.RootContext;
+import org.forgerock.json.JsonPointer;
+import org.forgerock.json.JsonValue;
+import org.forgerock.json.resource.ResourceResponse;
+import org.forgerock.json.resource.Responses;
 
-/**
- * Unit test utility methods, including fluent methods for creating JSON
- * objects.
- */
+/** Unit test utility methods, including fluent methods for creating JSON objects. */
 public final class TestUtils {
 
     /**
@@ -39,8 +37,8 @@
      *            The JSON content.
      * @return A {@code Resource} containing the provided JSON content.
      */
-    public static Resource asResource(final JsonValue content) {
-        return new Resource(content.get("_id").asString(), content.get("_rev").asString(), content);
+    public static ResourceResponse asResource(final JsonValue content) {
+        return Responses.newResourceResponse(content.get("_id").asString(), content.get("_rev").asString(), content);
     }
 
     /**
diff --git a/opendj-server-legacy/pom.xml b/opendj-server-legacy/pom.xml
index 89be8db..f5cf65a 100644
--- a/opendj-server-legacy/pom.xml
+++ b/opendj-server-legacy/pom.xml
@@ -89,7 +89,7 @@
     <!-- ForgeRock libraries -->
     <dependency>
       <groupId>org.forgerock.opendj</groupId>
-      <artifactId>opendj-rest2ldap-servlet</artifactId>
+      <artifactId>opendj-rest2ldap</artifactId>
       <version>${project.version}</version>
     </dependency>
     <dependency>
@@ -129,6 +129,28 @@
       <groupId>org.slf4j</groupId>
       <artifactId>slf4j-jdk14</artifactId>
     </dependency>
+    
+    <dependency>
+      <groupId>org.forgerock.commons</groupId>
+      <artifactId>json-resource</artifactId>
+      <version>${forgerockRestVersion}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.forgerock.commons</groupId>
+      <artifactId>json-resource-http</artifactId>
+      <version>${forgerockRestVersion}</version>
+    </dependency>
+    
+    <dependency>
+      <groupId>org.forgerock.http</groupId>
+      <artifactId>chf-http-core</artifactId>
+      <version>${forgerockHttpVersion}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.forgerock.http</groupId>
+      <artifactId>chf-http-servlet</artifactId>
+      <version>${forgerockHttpVersion}</version>
+    </dependency>
 
     <!-- servlet and mail -->
     <dependency>
diff --git a/opendj-server-legacy/src/main/assembly/opendj-archive-component.xml b/opendj-server-legacy/src/main/assembly/opendj-archive-component.xml
index 7e5925c..1bf90a3 100644
--- a/opendj-server-legacy/src/main/assembly/opendj-archive-component.xml
+++ b/opendj-server-legacy/src/main/assembly/opendj-archive-component.xml
@@ -35,7 +35,6 @@
       <outputFileNameMapping>${artifact.artifactId}.${artifact.extension}</outputFileNameMapping>
       <excludes>
         <exclude>javax.activation:activation</exclude>
-        <exclude>org.forgerock.commons:json-resource-servlet:war</exclude>
         <exclude>org.forgerock.opendj:opendj-server-legacy</exclude>
       </excludes>
     </dependencySet>
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/CollectClientConnectionsFilter.java b/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/CollectClientConnectionsFilter.java
index 950b5c5..fbbc2c4 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/CollectClientConnectionsFilter.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/CollectClientConnectionsFilter.java
@@ -25,6 +25,7 @@
  */
 package org.opends.server.protocols.http;
 
+import java.io.Closeable;
 import java.io.IOException;
 import java.net.InetAddress;
 import java.net.UnknownHostException;
@@ -32,68 +33,45 @@
 import java.text.ParseException;
 import java.util.Collection;
 
-import javax.servlet.*;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpServletResponseWrapper;
-
+import org.forgerock.http.Context;
+import org.forgerock.http.Handler;
+import org.forgerock.http.protocol.Request;
+import org.forgerock.http.protocol.Response;
+import org.forgerock.http.protocol.Status;
 import org.forgerock.i18n.LocalizableMessage;
 import org.forgerock.i18n.slf4j.LocalizedLogger;
 import org.forgerock.json.resource.ResourceException;
-import org.forgerock.opendj.ldap.*;
+import org.forgerock.opendj.adapter.server3x.Adapters;
+import org.forgerock.opendj.ldap.AddressMask;
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.opendj.ldap.DN;
 import org.forgerock.opendj.ldap.Filter;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.ResultCode;
 import org.forgerock.opendj.ldap.requests.BindRequest;
 import org.forgerock.opendj.ldap.requests.Requests;
 import org.forgerock.opendj.ldap.requests.SearchRequest;
 import org.forgerock.opendj.ldap.responses.BindResult;
 import org.forgerock.opendj.ldap.responses.SearchResultEntry;
+import org.forgerock.opendj.rest2ldap.AuthenticatedConnectionContext;
 import org.forgerock.opendj.rest2ldap.Rest2LDAP;
-import org.forgerock.opendj.rest2ldap.servlet.Rest2LDAPContextFactory;
 import org.forgerock.util.AsyncFunction;
-import org.forgerock.util.promise.ExceptionHandler;
+import org.forgerock.util.promise.NeverThrowsException;
 import org.forgerock.util.promise.Promise;
-import org.forgerock.util.promise.ResultHandler;
+import org.forgerock.util.promise.Promises;
 import org.opends.server.admin.std.server.ConnectionHandlerCfg;
 import org.opends.server.schema.SchemaConstants;
 import org.opends.server.types.DisconnectReason;
 import org.opends.server.util.Base64;
 
-import static org.forgerock.opendj.adapter.server3x.Adapters.*;
-import static org.forgerock.opendj.ldap.LdapException.*;
-import static org.forgerock.util.promise.Promises.*;
 import static org.opends.messages.ProtocolMessages.*;
 import static org.opends.server.loggers.AccessLogger.*;
 import static org.opends.server.util.StaticUtils.*;
 
-/**
- * Servlet {@link Filter} that collects information about client connections.
- */
-final class CollectClientConnectionsFilter implements javax.servlet.Filter
+/** Servlet {@link Filter} that collects information about client connections. */
+final class CollectClientConnectionsFilter implements org.forgerock.http.Filter, Closeable
 {
 
-  /** This class holds all the necessary data to complete an HTTP request. */
-  private static final class HTTPRequestContext
-  {
-    private AsyncContext asyncContext;
-    private HttpServletRequest request;
-    private HttpServletResponse response;
-    private FilterChain chain;
-
-    private HTTPClientConnection clientConnection;
-    private Connection connection;
-
-    /** Whether to pretty print the resulting JSON. */
-    private boolean prettyPrint;
-    /** Used for the bind request when credentials are specified. */
-    private String userName;
-    /**
-     * Used for the bind request when credentials are specified. For security
-     * reasons, the password must be discarded as soon as possible after it's
-     * been used.
-     */
-    private String password;
-  }
-
   /** HTTP Header sent by the client with HTTP basic authentication. */
   static final String HTTP_BASIC_AUTH_HEADER = "Authorization";
 
@@ -118,269 +96,242 @@
    *          authentication
    */
   public CollectClientConnectionsFilter(
-      HTTPConnectionHandler connectionHandler,
-      HTTPAuthenticationConfig authenticationConfig)
+      HTTPConnectionHandler connectionHandler, HTTPAuthenticationConfig authenticationConfig)
   {
     this.connectionHandler = connectionHandler;
     this.authConfig = authenticationConfig;
   }
 
-  /** {@inheritDoc} */
   @Override
-  public void init(FilterConfig filterConfig) throws ServletException
+  public Promise<Response, NeverThrowsException> filter(Context context, Request request, Handler next)
   {
-    // nothing to do
-  }
+    final HTTPClientConnection clientConnection = new HTTPClientConnection(this.connectionHandler, context, request);
+    connectionHandler.addClientConnection(clientConnection);
 
-  /** {@inheritDoc} */
-  @Override
-  public void doFilter(ServletRequest req, ServletResponse resp,
-      FilterChain chain)
-  {
-    final HttpServletRequest request = (HttpServletRequest) req;
-    final HttpServletResponse response = (HttpServletResponse) resp;
-
-    final HTTPRequestContext ctx = new HTTPRequestContext();
-
-    ctx.request = request;
-    ctx.response = new HttpServletResponseWrapper(response)
+    if (connectionHandler.keepStats())
     {
-
-      /** {@inheritDoc} */
-      @Override
-      public void setStatus(int sc)
-      {
-        ctx.clientConnection.log(sc);
-        super.setStatus(sc);
-      }
-
-      /** {@inheritDoc} */
-      @SuppressWarnings("deprecation")
-      @Override
-      public void setStatus(int sc, String sm)
-      {
-        ctx.clientConnection.log(sc);
-        super.setStatus(sc, sm);
-      }
-    };
-    ctx.chain = chain;
-    ctx.prettyPrint =
-        Boolean.parseBoolean(request.getParameter("_prettyPrint"));
-
-    final HTTPClientConnection clientConnection =
-        new HTTPClientConnection(this.connectionHandler, request);
-    this.connectionHandler.addClientConnection(clientConnection);
-
-    ctx.clientConnection = clientConnection;
-
-    if (this.connectionHandler.keepStats()) {
-      this.connectionHandler.getStatTracker().addRequest(
-          ctx.clientConnection.getMethod());
+      connectionHandler.getStatTracker().addRequest(request.getMethod());
     }
 
     try
     {
-      if (!canProcessRequest(request, clientConnection))
+      if (!canProcessRequest(clientConnection))
       {
-        return;
+        return resourceExceptionToPromise(ResourceException.getException(ResourceException.INTERNAL_ERROR));
       }
-      // logs the connect after all the possible disconnect reasons have been
-      // checked.
+      // Logs the connect after all the possible disconnect reasons have been checked.
       logConnect(clientConnection);
-
-      ctx.connection = new SdkConnectionAdapter(clientConnection);
+      final Connection connection = new SdkConnectionAdapter(clientConnection);
 
       final String[] userCredentials = extractUsernamePassword(request);
       if (userCredentials != null && userCredentials.length == 2)
       {
-        ctx.userName = userCredentials[0];
-        ctx.password = userCredentials[1];
-        ctx.asyncContext = getAsyncContext(request);
+        final String userName = userCredentials[0];
+        final String password = userCredentials[1];
 
-        newRootConnection().searchSingleEntryAsync(buildSearchRequest(ctx.userName)).thenAsync(
-            new AsyncFunction<SearchResultEntry, BindResult, LdapException>() {
-              @Override
-              public Promise<BindResult, LdapException> apply(SearchResultEntry resultEntry) throws LdapException
-              {
-                final DN bindDN = resultEntry.getName();
-                if (bindDN == null)
-                {
-                  sendAuthenticationFailure(ctx);
-                  return newExceptionPromise(newLdapException(ResultCode.CANCELLED));
-                }
-                else
-                {
-                  final BindRequest bindRequest =
-                      Requests.newSimpleBindRequest(bindDN.toString(), ctx.password.getBytes(Charset.forName("UTF-8")));
-                  // We are done with the password at this stage,
-                  // wipe it from memory for security reasons
-                  ctx.password = null;
-                  return ctx.connection.bindAsync(bindRequest);
-                }
-              }
-
-            }
-        ).thenOnResult(new ResultHandler<BindResult>() {
-          @Override
-          public void handleResult(BindResult result)
-          {
-            ctx.clientConnection.setAuthUser(ctx.userName);
-            try
-            {
-              doFilter(ctx);
-            }
-            catch (Exception e)
-            {
-              onException(e, ctx);
-            }
-          }
-        }).thenOnException(new ExceptionHandler<LdapException>(){
-          @Override
-          public void handleException(LdapException exception)
-          {
-            final ResultCode rc = exception.getResult().getResultCode();
-            if (ResultCode.CLIENT_SIDE_NO_RESULTS_RETURNED.equals(rc)
-                || ResultCode.CLIENT_SIDE_UNEXPECTED_RESULTS_RETURNED.equals(rc))
-            {
-              // Avoid information leak:
-              // do not hint to the user that it is the username that is invalid
-              sendAuthenticationFailure(ctx);
-            }
-            else
-            {
-              onException(exception, ctx);
-            }
-          }
-        });
+        return Adapters.newRootConnection()
+            .searchSingleEntryAsync(buildSearchRequest(userName))
+            .thenAsync(doBindAfterSearch(context, request, next, userName, password, clientConnection, connection),
+                       returnErrorAfterFailedSearch(clientConnection));
       }
       else if (this.connectionHandler.acceptUnauthenticatedRequests())
       {
-        // use unauthenticated user
-        doFilter(ctx);
+        // Use unauthenticated user
+        return doFilter(context, request, next, connection);
       }
       else
       {
-        sendAuthenticationFailure(ctx);
+        return authenticationFailure(clientConnection);
       }
     }
     catch (Exception e)
     {
-      onException(e, ctx);
+      return asErrorResponse(e, clientConnection);
     }
   }
 
-  private void doFilter(HTTPRequestContext ctx)
-      throws Exception
+  private boolean canProcessRequest(final HTTPClientConnection connection) throws UnknownHostException
   {
-    /*
-     * WARNING: This action triggers 3-4 others: Set the connection for use with
-     * this request on the HttpServletRequest. It will make
-     * Rest2LDAPContextFactory create an AuthenticatedConnectionContext which
-     * will in turn ensure Rest2LDAP uses the supplied Connection object.
-     */
-    ctx.request.setAttribute(
-        Rest2LDAPContextFactory.ATTRIBUTE_AUTHN_CONNECTION, ctx.connection);
+    final InetAddress clientAddr = connection.getRemoteAddress();
 
-    // send the request further down the filter chain or pass to servlet
-    ctx.chain.doFilter(ctx.request, ctx.response);
-  }
-
-  private void sendAuthenticationFailure(HTTPRequestContext ctx)
-  {
-    final int statusCode = HttpServletResponse.SC_UNAUTHORIZED;
-    try
+    // Check to see if the core server rejected the connection (e.g. already too many connections established).
+    if (connection.getConnectionID() < 0)
     {
-      // The user could not be authenticated. Send an HTTP Basic authentication
-      // challenge if HTTP Basic authentication is enabled.
-      ResourceException unauthorizedException =
-          ResourceException.getException(statusCode, "Invalid Credentials");
-      sendErrorReponse(ctx.response, ctx.prettyPrint, unauthorizedException);
-
-      ctx.clientConnection.disconnect(DisconnectReason.INVALID_CREDENTIALS,
-          false, null);
-    }
-    finally
-    {
-      ctx.clientConnection.log(statusCode);
-
-      if (ctx.asyncContext != null)
-      {
-        ctx.asyncContext.complete();
-      }
-    }
-  }
-
-  private void onException(Exception e, HTTPRequestContext ctx)
-  {
-    ResourceException ex = Rest2LDAP.asResourceException(e);
-    try
-    {
-      logger.traceException(e);
-
-      sendErrorReponse(ctx.response, ctx.prettyPrint, ex);
-
-      LocalizableMessage message =
-          INFO_CONNHANDLER_UNABLE_TO_REGISTER_CLIENT.get(ctx.clientConnection
-              .getClientHostPort(), ctx.clientConnection.getServerHostPort(),
-              getExceptionMessage(e));
-      logger.debug(message);
-
-      ctx.clientConnection.disconnect(DisconnectReason.SERVER_ERROR, false,
-          message);
-    }
-    finally
-    {
-      ctx.clientConnection.log(ex.getCode());
-
-      if (ctx.asyncContext != null)
-      {
-        ctx.asyncContext.complete();
-      }
-    }
-  }
-
-  private boolean canProcessRequest(HttpServletRequest request,
-      final HTTPClientConnection clientConnection) throws UnknownHostException
-  {
-    InetAddress clientAddr = InetAddress.getByName(request.getRemoteAddr());
-
-    // Check to see if the core server rejected the
-    // connection (e.g., already too many connections
-    // established).
-    if (clientConnection.getConnectionID() < 0)
-    {
-      clientConnection.disconnect(DisconnectReason.ADMIN_LIMIT_EXCEEDED, true,
-          ERR_CONNHANDLER_REJECTED_BY_SERVER.get());
+      connection.disconnect(
+          DisconnectReason.ADMIN_LIMIT_EXCEEDED, true, ERR_CONNHANDLER_REJECTED_BY_SERVER.get());
       return false;
     }
 
-    // Check to see if the client is on the denied list.
-    // If so, then reject it immediately.
-    ConnectionHandlerCfg config = this.connectionHandler.getCurrentConfig();
-    Collection<AddressMask> allowedClients = config.getAllowedClient();
-    Collection<AddressMask> deniedClients = config.getDeniedClient();
+    // Check to see if the client is on the denied list. If so, then reject it immediately.
+    final ConnectionHandlerCfg config = this.connectionHandler.getCurrentConfig();
+    final Collection<AddressMask> deniedClients = config.getDeniedClient();
     if (!deniedClients.isEmpty()
         && AddressMask.matchesAny(deniedClients, clientAddr))
     {
-      clientConnection.disconnect(DisconnectReason.CONNECTION_REJECTED, false,
-          ERR_CONNHANDLER_DENIED_CLIENT.get(clientConnection
-              .getClientHostPort(), clientConnection.getServerHostPort()));
+      connection.disconnect(DisconnectReason.CONNECTION_REJECTED, false,
+          ERR_CONNHANDLER_DENIED_CLIENT.get(connection.getClientHostPort(), connection.getServerHostPort()));
       return false;
     }
-    // Check to see if there is an allowed list and if
-    // there is whether the client is on that list. If
-    // not, then reject the connection.
+
+    // Check to see if there is an allowed list and if there is whether the client is on that list.
+    // If not, then reject the connection.
+    final Collection<AddressMask> allowedClients = config.getAllowedClient();
     if (!allowedClients.isEmpty()
         && !AddressMask.matchesAny(allowedClients, clientAddr))
     {
-      clientConnection.disconnect(DisconnectReason.CONNECTION_REJECTED, false,
-          ERR_CONNHANDLER_DISALLOWED_CLIENT.get(clientConnection
-              .getClientHostPort(), clientConnection.getServerHostPort()));
+      connection.disconnect(DisconnectReason.CONNECTION_REJECTED, false,
+          ERR_CONNHANDLER_DISALLOWED_CLIENT.get(connection.getClientHostPort(), connection.getServerHostPort()));
       return false;
     }
     return true;
   }
 
+  private SearchRequest buildSearchRequest(String userName)
+  {
+    // Use configured rights to find the user DN
+    final Filter filter = Filter.format(authConfig.getSearchFilterTemplate(), userName);
+    return Requests.newSearchRequest(
+        authConfig.getSearchBaseDN(), authConfig.getSearchScope(), filter, SchemaConstants.NO_ATTRIBUTES);
+  }
+
+  private AsyncFunction<SearchResultEntry, Response, NeverThrowsException> doBindAfterSearch(
+      final Context context, final Request request, final Handler next, final String userName, final String password,
+      final HTTPClientConnection clientConnection, final Connection connection)
+  {
+    return new AsyncFunction<SearchResultEntry, Response, NeverThrowsException>()
+    {
+      @Override
+      public Promise<Response, NeverThrowsException> apply(final SearchResultEntry resultEntry)
+      {
+        final DN bindDN = resultEntry.getName();
+        if (bindDN == null)
+        {
+          return authenticationFailure(clientConnection);
+        }
+
+        final BindRequest bindRequest =
+            Requests.newSimpleBindRequest(bindDN.toString(), password.getBytes(Charset.forName("UTF-8")));
+        return connection.bindAsync(bindRequest)
+                         .thenAsync(doChain(context, request, next, userName, clientConnection, connection),
+                                    returnErrorAfterFailedBind(clientConnection));
+      }
+    };
+  }
+
+  private AsyncFunction<BindResult, Response, NeverThrowsException> doChain(
+      final Context context, final Request request, final Handler next, final String userName,
+      final HTTPClientConnection clientConnection, final Connection connection)
+  {
+    return new AsyncFunction<BindResult, Response, NeverThrowsException>()
+    {
+      @Override
+      public Promise<Response, NeverThrowsException> apply(BindResult value) throws NeverThrowsException
+      {
+        clientConnection.setAuthUser(userName);
+        try
+        {
+          return doFilter(context, request, next, connection);
+        }
+        catch (Exception e)
+        {
+          return asErrorResponse(e, clientConnection);
+        }
+      }
+    };
+  }
+
+  private Promise<Response, NeverThrowsException> doFilter(
+      final Context context, final Request request, final Handler next, final Connection connection) throws Exception
+  {
+    final Context forwardedContext = new AuthenticatedConnectionContext(context, connection);
+    // Send the request further down the filter chain or pass to servlet
+    return next.handle(forwardedContext, request);
+  }
+
+  private AsyncFunction<? super LdapException, Response, NeverThrowsException> returnErrorAfterFailedSearch(
+      final HTTPClientConnection clientConnection)
+  {
+    return new AsyncFunction<LdapException, Response, NeverThrowsException>()
+    {
+      @Override
+      public Promise<Response, NeverThrowsException> apply(final LdapException exception)
+      {
+        final ResultCode rc = exception.getResult().getResultCode();
+        if (ResultCode.CLIENT_SIDE_NO_RESULTS_RETURNED.equals(rc)
+         || ResultCode.CLIENT_SIDE_UNEXPECTED_RESULTS_RETURNED.equals(rc))
+        {
+          // Avoid information leak:
+          // do not hint to the user that it is the username that is invalid
+          return authenticationFailure(clientConnection);
+        }
+        else
+        {
+          return asErrorResponse(exception, clientConnection);
+        }
+      }
+    };
+  }
+
+  private AsyncFunction<LdapException, Response, NeverThrowsException> returnErrorAfterFailedBind(
+      final HTTPClientConnection clientConnection)
+  {
+    return new AsyncFunction<LdapException, Response, NeverThrowsException>()
+    {
+      @Override
+      public Promise<Response, NeverThrowsException> apply(final LdapException e)
+      {
+        return asErrorResponse(e, clientConnection);
+      }
+    };
+  }
+
+  private Promise<Response, NeverThrowsException> authenticationFailure(final HTTPClientConnection clientConnection)
+  {
+    return asErrorResponse(ResourceException.getException(401, "Invalid Credentials"), clientConnection,
+        DisconnectReason.INVALID_CREDENTIALS, false);
+  }
+
+  private Promise<Response, NeverThrowsException> asErrorResponse(
+      final Throwable t, final HTTPClientConnection clientConnection)
+  {
+    return asErrorResponse(t, clientConnection, DisconnectReason.SERVER_ERROR, true);
+  }
+
+  private Promise<Response, NeverThrowsException> asErrorResponse(final Throwable t,
+      final HTTPClientConnection clientConnection, final DisconnectReason reason, final boolean logError)
+  {
+    final ResourceException ex = Rest2LDAP.asResourceException(t);
+    try
+    {
+      LocalizableMessage message = null;
+      if (logError)
+      {
+        logger.traceException(ex);
+        message = INFO_CONNHANDLER_UNABLE_TO_REGISTER_CLIENT.get(
+            clientConnection.getClientHostPort(), clientConnection.getServerHostPort(), getExceptionMessage(ex));
+        logger.debug(message);
+      }
+      clientConnection.disconnect(reason, false, message);
+    }
+    finally
+    {
+      clientConnection.log(ex.getCode());
+    }
+
+    return resourceExceptionToPromise(ex);
+  }
+
+  Promise<Response, NeverThrowsException> resourceExceptionToPromise(final ResourceException e)
+  {
+    final Response response = new Response().setStatus(Status.valueOf(e.getCode()))
+                                            .setEntity(e.toJsonValue().getObject());
+    if (e.getCode() == 401 && authConfig.isBasicAuthenticationSupported())
+    {
+      response.getHeaders().add("WWW-Authenticate", "Basic realm=\"org.forgerock.opendj\"");
+    }
+    return Promises.newResultPromise(response);
+  }
+
   /**
    * Extracts the username and password from the request using one of the
    * enabled authentication mechanism: HTTP Basic authentication or HTTP Custom
@@ -395,8 +346,7 @@
    * @throws ResourceException
    *           if any error occur
    */
-  String[] extractUsernamePassword(HttpServletRequest request)
-      throws ResourceException
+  String[] extractUsernamePassword(Request request) throws ResourceException
   {
     // TODO Use session to reduce hits with search + bind?
     // Use proxied authorization control for session.
@@ -404,10 +354,8 @@
     // Security: How can we remove the password held in the request headers?
     if (authConfig.isCustomHeadersAuthenticationSupported())
     {
-      final String userName =
-          request.getHeader(authConfig.getCustomHeaderUsername());
-      final String password =
-          request.getHeader(authConfig.getCustomHeaderPassword());
+      final String userName = request.getHeaders().getFirst(authConfig.getCustomHeaderUsername());
+      final String password = request.getHeaders().getFirst(authConfig.getCustomHeaderPassword());
       if (userName != null && password != null)
       {
         return new String[] { userName, password };
@@ -416,7 +364,7 @@
 
     if (authConfig.isBasicAuthenticationSupported())
     {
-      String httpBasicAuthHeader = request.getHeader(HTTP_BASIC_AUTH_HEADER);
+      String httpBasicAuthHeader = request.getHeaders().getFirst(HTTP_BASIC_AUTH_HEADER);
       if (httpBasicAuthHeader != null)
       {
         String[] userCredentials = parseUsernamePassword(httpBasicAuthHeader);
@@ -431,77 +379,6 @@
   }
 
   /**
-   * Sends an error response back to the client. If the error response is
-   * "Unauthorized", then it will send a challenge for HTTP Basic authentication
-   * if HTTP Basic authentication is enabled.
-   *
-   * @param response
-   *          where to send the Unauthorized status code.
-   * @param prettyPrint
-   *          whether to format the JSON document output
-   * @param re
-   *          the resource exception with the error response content
-   */
-  void sendErrorReponse(HttpServletResponse response, boolean prettyPrint,
-      ResourceException re)
-  {
-    response.setStatus(re.getCode());
-
-    if (re.getCode() == HttpServletResponse.SC_UNAUTHORIZED
-        && authConfig.isBasicAuthenticationSupported())
-    {
-      response.setHeader("WWW-Authenticate",
-          "Basic realm=\"org.forgerock.opendj\"");
-    }
-
-    try
-    {
-      // Send error JSON document out
-      response.setHeader("Content-Type", "application/json");
-      response.getOutputStream().println(toJSON(prettyPrint, re));
-    }
-    catch (IOException ignore)
-    {
-      // nothing else we can do in this case
-      logger.traceException(ignore);
-    }
-  }
-
-  /**
-   * Returns a JSON representation of the {@link ResourceException}.
-   *
-   * @param prettyPrint
-   *          whether to format the resulting JSON document
-   * @param re
-   *          the resource exception to convert to a JSON document
-   * @return a String containing the JSON representation of the
-   *         {@link ResourceException}.
-   */
-  private String toJSON(boolean prettyPrint, ResourceException re)
-  {
-    final String indent = "\n    ";
-    final StringBuilder sb = new StringBuilder();
-    sb.append("{");
-    if (prettyPrint) {
-      sb.append(indent);
-    }
-    sb.append("\"code\": ").append(re.getCode()).append(",");
-    if (prettyPrint) {
-      sb.append(indent);
-    }
-    sb.append("\"message\": \"").append(re.getMessage()).append("\",");
-    if (prettyPrint) {
-      sb.append(indent);
-    }
-    sb.append("\"reason\": \"").append(re.getReason()).append("\"");
-    if (prettyPrint) {
-      sb.append("\n");
-    }
-    sb.append("}");
-    return sb.toString();
-  }
-
-  /**
    * Parses username and password from the authentication header used in HTTP
    * basic authentication.
    *
@@ -520,12 +397,12 @@
       // We received authentication info
       // Example received header:
       // "Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="
-      String base64UserPassword = authHeader.substring("basic".length() + 1);
+      String base64UserCredentials = authHeader.substring("basic".length() + 1);
       try
       {
         // Example usage of base64:
         // Base64("Aladdin:open sesame") = "QWxhZGRpbjpvcGVuIHNlc2FtZQ=="
-        String userCredentials = new String(Base64.decode(base64UserPassword));
+        String userCredentials = new String(Base64.decode(base64UserCredentials));
         String[] split = userCredentials.split(":");
         if (split.length == 2)
         {
@@ -540,25 +417,6 @@
     return null;
   }
 
-  private AsyncContext getAsyncContext(ServletRequest request)
-  {
-    return request.isAsyncStarted() ? request.getAsyncContext() : request
-        .startAsync();
-  }
-
-  private SearchRequest buildSearchRequest(String userName)
-  {
-    // use configured rights to find the user DN
-    final Filter filter =
-        Filter.format(authConfig.getSearchFilterTemplate(), userName);
-    return Requests.newSearchRequest(authConfig.getSearchBaseDN(), authConfig
-        .getSearchScope(), filter, SchemaConstants.NO_ATTRIBUTES);
-  }
-
-  /** {@inheritDoc} */
   @Override
-  public void destroy()
-  {
-    // nothing to do
-  }
+  public void close() throws IOException {}
 }
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/HTTPClientConnection.java b/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/HTTPClientConnection.java
index 5251ee6..b2447ef 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/HTTPClientConnection.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/HTTPClientConnection.java
@@ -25,6 +25,12 @@
  */
 package org.opends.server.protocols.http;
 
+import static org.forgerock.opendj.adapter.server3x.Converters.from;
+import static org.forgerock.opendj.adapter.server3x.Converters.getResponseResult;
+import static org.forgerock.opendj.ldap.LdapException.newLdapException;
+import static org.opends.messages.ProtocolMessages.WARN_CLIENT_DISCONNECT_IN_PROGRESS;
+import static org.opends.server.loggers.AccessLogger.logDisconnect;
+
 import java.net.InetAddress;
 import java.net.UnknownHostException;
 import java.util.ArrayList;
@@ -34,16 +40,19 @@
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicLong;
 
-import javax.servlet.http.HttpServletRequest;
-
+import org.forgerock.http.Context;
+import org.forgerock.http.MutableUri;
+import org.forgerock.http.context.ClientContext;
+import org.forgerock.http.context.AttributesContext;
+import org.forgerock.http.protocol.Request;
 import org.forgerock.i18n.LocalizableMessage;
 import org.forgerock.i18n.LocalizableMessageBuilder;
 import org.forgerock.i18n.slf4j.LocalizedLogger;
 import org.forgerock.opendj.ldap.LdapException;
-import org.forgerock.opendj.ldap.spi.LdapPromiseImpl;
 import org.forgerock.opendj.ldap.ResultCode;
 import org.forgerock.opendj.ldap.SearchResultHandler;
 import org.forgerock.opendj.ldap.responses.Result;
+import org.forgerock.opendj.ldap.spi.LdapPromiseImpl;
 import org.opends.server.api.ClientConnection;
 import org.opends.server.core.AddOperation;
 import org.opends.server.core.BindOperation;
@@ -79,11 +88,6 @@
 import org.opends.server.types.SearchResultEntry;
 import org.opends.server.types.SearchResultReference;
 
-import static org.forgerock.opendj.adapter.server3x.Converters.*;
-import static org.forgerock.opendj.ldap.LdapException.*;
-import static org.opends.messages.ProtocolMessages.*;
-import static org.opends.server.loggers.AccessLogger.*;
-
 /**
  * This class defines an HTTP client connection, which is a type of client
  * connection that will be accepted by an instance of the HTTP connection
@@ -234,7 +238,7 @@
   private final InetAddress localAddress;
 
   /** Whether this connection is secure. */
-  private final boolean isSecure;
+  private boolean isSecure;
 
   /** Security-Strength Factor extracted from the request attribute. */
   private final int securityStrengthFactor;
@@ -244,26 +248,29 @@
    *
    * @param connectionHandler
    *          the connection handler that accepted this connection
-   * @param request
-   *          represents this client connection.
+   * @param context
+   *          represents the context of this client connection.
    */
-  public HTTPClientConnection(HTTPConnectionHandler connectionHandler, HttpServletRequest request)
+  public HTTPClientConnection(HTTPConnectionHandler connectionHandler, Context context, Request request)
   {
     this.connectionHandler = connectionHandler;
-
+    final ClientContext clientCtx = context.asContext(ClientContext.class);
     // Memorize all the fields we need from the request before Grizzly decides to recycle it
-    this.clientAddress = request.getRemoteAddr();
-    this.clientPort = request.getRemotePort();
-    this.serverAddress = request.getLocalAddr();
-    this.serverPort = request.getLocalPort();
-    this.remoteAddress = toInetAddress(request.getRemoteAddr());
-    this.localAddress = toInetAddress(request.getLocalAddr());
-    this.isSecure = request.isSecure();
-    this.securityStrengthFactor = calcSSF(request.getAttribute(SERVLET_SSF_CONSTANT));
+    this.clientAddress = clientCtx.getRemoteAddress();
+    this.remoteAddress = toInetAddress(clientAddress);
+    this.clientPort = clientCtx.getRemotePort();
+    this.isSecure = clientCtx.isSecure();
+
+    final MutableUri uri = request.getUri();
+    this.serverAddress = uri.getHost();
+    this.localAddress = toInetAddress(serverAddress);
+    this.serverPort = uri.getPort();
+    this.securityStrengthFactor = calcSSF(
+            context.asContext(AttributesContext.class).getAttributes().get(SERVLET_SSF_CONSTANT));
     this.method = request.getMethod();
-    this.query = computeQuery(request);
-    this.protocol = request.getProtocol();
-    this.userAgent = request.getHeader("User-Agent");
+    this.query = uri.getQuery();
+    this.protocol = request.getVersion();
+    this.userAgent = clientCtx.getUserAgent();
 
     this.statTracker = this.connectionHandler.getStatTracker();
 
@@ -277,15 +284,6 @@
     this.connectionID = DirectoryServer.newConnectionAccepted(this);
   }
 
-  private String computeQuery(HttpServletRequest request)
-  {
-    if (request.getQueryString() != null)
-    {
-      return request.getRequestURI() + "?" + request.getQueryString();
-    }
-    return request.getRequestURI();
-  }
-
   @Override
   public String getAuthUser()
   {
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/HTTPConnectionHandler.java b/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/HTTPConnectionHandler.java
index e76f317..934b78f 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/HTTPConnectionHandler.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/HTTPConnectionHandler.java
@@ -25,46 +25,38 @@
  */
 package org.opends.server.protocols.http;
 
-import static org.opends.messages.ConfigMessages.*;
+import static org.opends.messages.ConfigMessages.WARN_CONFIG_LOGGER_NO_ACTIVE_HTTP_ACCESS_LOGGERS;
 import static org.opends.messages.ProtocolMessages.*;
-import static org.opends.server.util.ServerConstants.*;
-import static org.opends.server.util.StaticUtils.*;
+import static org.opends.server.util.ServerConstants.ALERT_DESCRIPTION_HTTP_CONNECTION_HANDLER_CONSECUTIVE_FAILURES;
+import static org.opends.server.util.ServerConstants.ALERT_TYPE_HTTP_CONNECTION_HANDLER_CONSECUTIVE_FAILURES;
+import static org.opends.server.util.StaticUtils.getExceptionMessage;
+import static org.opends.server.util.StaticUtils.isAddressInUse;
+import static org.opends.server.util.StaticUtils.stackTraceToSingleLineString;
 
-import java.io.File;
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLEngine;
 import java.io.IOException;
 import java.net.InetAddress;
-import java.util.*;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.TimeUnit;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
-import javax.net.ssl.KeyManager;
-import javax.net.ssl.SSLContext;
-import javax.net.ssl.SSLEngine;
-import javax.servlet.DispatcherType;
-import javax.servlet.Filter;
-import javax.servlet.ServletException;
-
-import org.codehaus.jackson.JsonParseException;
-import org.codehaus.jackson.JsonParser;
-import org.codehaus.jackson.map.JsonMappingException;
-import org.codehaus.jackson.map.ObjectMapper;
+import org.forgerock.http.servlet.HttpFrameworkServlet;
 import org.forgerock.i18n.LocalizableMessage;
 import org.forgerock.i18n.slf4j.LocalizedLogger;
-import org.forgerock.json.fluent.JsonValue;
-import org.forgerock.json.resource.CollectionResourceProvider;
-import org.forgerock.json.resource.ConnectionFactory;
-import org.forgerock.json.resource.Resources;
-import org.forgerock.json.resource.Router;
-import org.forgerock.json.resource.servlet.HttpServlet;
 import org.forgerock.opendj.config.server.ConfigChangeResult;
 import org.forgerock.opendj.config.server.ConfigException;
 import org.forgerock.opendj.ldap.ResultCode;
-import org.forgerock.opendj.ldap.SearchScope;
-import org.forgerock.opendj.rest2ldap.AuthorizationPolicy;
-import org.forgerock.opendj.rest2ldap.Rest2LDAP;
-import org.forgerock.opendj.rest2ldap.servlet.Rest2LDAPContextFactory;
 import org.glassfish.grizzly.http.HttpProbe;
 import org.glassfish.grizzly.http.server.HttpServer;
 import org.glassfish.grizzly.http.server.NetworkListener;
@@ -78,13 +70,21 @@
 import org.opends.server.admin.server.ConfigurationChangeListener;
 import org.opends.server.admin.std.server.ConnectionHandlerCfg;
 import org.opends.server.admin.std.server.HTTPConnectionHandlerCfg;
-import org.opends.server.api.*;
+import org.opends.server.api.AlertGenerator;
+import org.opends.server.api.ClientConnection;
+import org.opends.server.api.ConnectionHandler;
+import org.opends.server.api.KeyManagerProvider;
+import org.opends.server.api.ServerShutdownListener;
+import org.opends.server.api.TrustManagerProvider;
 import org.opends.server.core.DirectoryServer;
 import org.opends.server.extensions.NullKeyManagerProvider;
 import org.opends.server.extensions.NullTrustManagerProvider;
 import org.opends.server.loggers.HTTPAccessLogger;
 import org.opends.server.monitors.ClientConnectionMonitorProvider;
-import org.opends.server.types.*;
+import org.opends.server.types.DN;
+import org.opends.server.types.DirectoryException;
+import org.opends.server.types.HostPort;
+import org.opends.server.types.InitializationException;
 import org.opends.server.util.SelectableCertificateKeyManager;
 import org.opends.server.util.StaticUtils;
 
@@ -107,8 +107,6 @@
   /** SSL instance name used in context creation. */
   private static final String SSL_CONTEXT_INSTANCE_NAME = "TLS";
 
-  private static final ObjectMapper JSON_MAPPER = new ObjectMapper().configure(JsonParser.Feature.ALLOW_COMMENTS, true);
-
   /** The initialization configuration. */
   private HTTPConnectionHandlerCfg initConfig;
 
@@ -779,81 +777,12 @@
 
   private void createAndRegisterServlet(final String servletName, final String... urlPatterns) throws Exception
   {
-    // Parse and use JSON config
-    File jsonConfigFile = getFileForPath(this.currentConfig.getConfigFile());
-    final JsonValue configuration = parseJsonConfiguration(jsonConfigFile).recordKeyAccesses();
-    final HTTPAuthenticationConfig authenticationConfig = getAuthenticationConfig(configuration);
-    final ConnectionFactory connFactory = getConnectionFactory(configuration);
-    configuration.verifyAllKeysAccessed();
-
-    Filter filter = new CollectClientConnectionsFilter(this, authenticationConfig);
-    // Used for hooking our HTTPClientConnection in Rest2LDAP
-    final HttpServlet servlet = new HttpServlet(connFactory, Rest2LDAPContextFactory.getHttpServletContextFactory());
-
     // Create and deploy the Web app context
     final WebappContext ctx = new WebappContext(servletName);
-    ctx.addFilter("collectClientConnections", filter)
-       .addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, urlPatterns);
-    ctx.addServlet(servletName, servlet).addMapping(urlPatterns);
+    ctx.addServlet(servletName, new HttpFrameworkServlet(new LdapHttpApplication(this))).addMapping(urlPatterns);
     ctx.deploy(this.httpServer);
   }
 
-  private HTTPAuthenticationConfig getAuthenticationConfig(final JsonValue configuration)
-  {
-    final HTTPAuthenticationConfig result = new HTTPAuthenticationConfig();
-
-    final JsonValue val = configuration.get("authenticationFilter");
-    result.setBasicAuthenticationSupported(asBool(val, "supportHTTPBasicAuthentication"));
-    result.setCustomHeadersAuthenticationSupported(asBool(val, "supportAltAuthentication"));
-    result.setCustomHeaderUsername(val.get("altAuthenticationUsernameHeader").asString());
-    result.setCustomHeaderPassword(val.get("altAuthenticationPasswordHeader").asString());
-
-    final String searchBaseDN = asString(val, "searchBaseDN");
-    result.setSearchBaseDN(org.forgerock.opendj.ldap.DN.valueOf(searchBaseDN));
-    result.setSearchScope(SearchScope.valueOf(asString(val, "searchScope")));
-    result.setSearchFilterTemplate(asString(val, "searchFilterTemplate"));
-
-    return result;
-  }
-
-  private String asString(JsonValue value, String key)
-  {
-    return value.get(key).required().asString();
-  }
-
-  private boolean asBool(JsonValue value, String key)
-  {
-    return value.get(key).defaultTo(false).asBoolean();
-  }
-
-  private ConnectionFactory getConnectionFactory(final JsonValue configuration)
-  {
-    final Router router = new Router();
-    final JsonValue mappings = configuration.get("servlet").get("mappings").required();
-    for (final String mappingUrl : mappings.keys())
-    {
-      final JsonValue mapping = mappings.get(mappingUrl);
-      final CollectionResourceProvider provider = Rest2LDAP.builder()
-                                                           .authorizationPolicy(AuthorizationPolicy.REUSE)
-                                                           .configureMapping(mapping).build();
-      router.addRoute(mappingUrl, provider);
-    }
-    return Resources.newInternalConnectionFactory(router);
-  }
-
-  private JsonValue parseJsonConfiguration(File configFile)
-      throws IOException, JsonParseException, JsonMappingException, ServletException
-  {
-    // Parse the config file.
-    final Object content = JSON_MAPPER.readValue(configFile, Object.class);
-    if (!(content instanceof Map))
-    {
-      throw new ServletException(
-          "Servlet configuration file '" + configFile + "' does not contain a valid JSON configuration");
-    }
-    return new JsonValue(content);
-  }
-
   private void stopHttpServer()
   {
     if (this.httpServer != null)
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/LdapHttpApplication.java b/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/LdapHttpApplication.java
new file mode 100644
index 0000000..efd5942
--- /dev/null
+++ b/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/LdapHttpApplication.java
@@ -0,0 +1,177 @@
+/*
+ * CDDL HEADER START
+ *
+ * The contents of this file are subject to the terms of the
+ * Common Development and Distribution License, Version 1.0 only
+ * (the "License").  You may not use this file except in compliance
+ * with the License.
+ *
+ * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
+ * or http://forgerock.org/license/CDDLv1.0.html.
+ * See the License for the specific language governing permissions
+ * and limitations under the License.
+ *
+ * When distributing Covered Code, include this CDDL HEADER in each
+ * file and include the License file at legal-notices/CDDLv1_0.txt.
+ * If applicable, add the following below this CDDL HEADER, with the
+ * fields enclosed by brackets "[]" replaced with your own identifying
+ * information:
+ *      Portions Copyright [yyyy] [name of copyright owner]
+ *
+ * CDDL HEADER END
+ *
+ *
+ *      Copyright 2015 ForgeRock AS
+ */
+package org.opends.server.protocols.http;
+
+import static org.forgerock.util.Utils.closeSilently;
+import static org.opends.server.util.StaticUtils.getFileForPath;
+
+import java.io.File;
+import java.io.FileReader;
+import java.util.Map;
+
+import org.forgerock.http.Context;
+import org.forgerock.http.Handler;
+import org.forgerock.http.HttpApplication;
+import org.forgerock.http.HttpApplicationException;
+import org.forgerock.http.handler.Handlers;
+import org.forgerock.http.io.Buffer;
+import org.forgerock.http.protocol.Request;
+import org.forgerock.http.protocol.Response;
+import org.forgerock.http.util.Json;
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.slf4j.LocalizedLogger;
+import org.forgerock.json.JsonValue;
+import org.forgerock.json.resource.CollectionResourceProvider;
+import org.forgerock.json.resource.RequestHandler;
+import org.forgerock.json.resource.Router;
+import org.forgerock.json.resource.http.CrestHttp;
+import org.forgerock.opendj.ldap.SearchScope;
+import org.forgerock.opendj.rest2ldap.AuthorizationPolicy;
+import org.forgerock.opendj.rest2ldap.Rest2LDAP;
+import org.forgerock.util.Factory;
+import org.forgerock.util.promise.NeverThrowsException;
+import org.forgerock.util.promise.Promise;
+import org.opends.messages.ProtocolMessages;
+
+/** Main class of the HTTP Connection Handler web application */
+class LdapHttpApplication implements HttpApplication
+{
+  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
+
+  /** Http Handler which sets a connection to an OpenDJ server. */
+  private static class LdapHttpHandler implements Handler
+  {
+
+    private final Handler delegate;
+
+    /**
+     * Build a new {@code LdapHttpHandler}.
+     *
+     * @param configuration
+     *            The configuration which will be used to set
+     *            the connection and the mappings to the OpenDJ server.
+     */
+    public LdapHttpHandler(final JsonValue configuration)
+    {
+      delegate = CrestHttp.newHttpHandler(createRouter(configuration));
+    }
+
+    private RequestHandler createRouter(final JsonValue configuration)
+    {
+      final JsonValue mappings = configuration.get("servlet").get("mappings").required();
+      final Router router = new Router();
+
+      for (final String mappingUrl : mappings.keys()) {
+        final JsonValue mapping = mappings.get(mappingUrl);
+        final CollectionResourceProvider provider = Rest2LDAP.builder()
+                .authorizationPolicy(AuthorizationPolicy.REUSE)
+                .configureMapping(mapping)
+                .build();
+        router.addRoute(Router.uriTemplate(mappingUrl), provider);
+      }
+      return router;
+    }
+    @Override
+    public final Promise<Response, NeverThrowsException> handle(final Context context, final Request request)
+    {
+      return delegate.handle(context, request);
+    }
+
+  }
+
+  private HTTPConnectionHandler connectionHandler;
+  private LdapHttpHandler handler;
+  private CollectClientConnectionsFilter filter;
+
+  LdapHttpApplication(HTTPConnectionHandler connectionHandler)
+  {
+    this.connectionHandler = connectionHandler;
+  }
+
+  @Override
+  public Handler start() throws HttpApplicationException
+  {
+    try
+    {
+      final File configFile = getFileForPath(connectionHandler.getCurrentConfig().getConfigFile());
+      final Map<String, Object> jsonElems = Json.readJsonLenient(new FileReader(configFile));
+      final JsonValue configuration = new JsonValue(jsonElems).recordKeyAccesses();
+      handler = new LdapHttpHandler(configuration);
+      filter = new CollectClientConnectionsFilter(connectionHandler, getAuthenticationConfig(configuration));
+      configuration.verifyAllKeysAccessed();
+      return Handlers.chainOf(handler, filter);
+    }
+    catch (final Exception e)
+    {
+      final LocalizableMessage errorMsg = ProtocolMessages.ERR_INITIALIZE_HTTP_CONNECTION_HANDLER.get();
+      logger.error(errorMsg, e);
+      stop();
+      throw new HttpApplicationException(errorMsg.toString(), e);
+    }
+  }
+
+  private HTTPAuthenticationConfig getAuthenticationConfig(final JsonValue configuration)
+  {
+    final HTTPAuthenticationConfig result = new HTTPAuthenticationConfig();
+
+    final JsonValue val = configuration.get("authenticationFilter");
+    result.setBasicAuthenticationSupported(asBool(val, "supportHTTPBasicAuthentication"));
+    result.setCustomHeadersAuthenticationSupported(asBool(val, "supportAltAuthentication"));
+    result.setCustomHeaderUsername(val.get("altAuthenticationUsernameHeader").asString());
+    result.setCustomHeaderPassword(val.get("altAuthenticationPasswordHeader").asString());
+
+    final String searchBaseDN = asString(val, "searchBaseDN");
+    result.setSearchBaseDN(org.forgerock.opendj.ldap.DN.valueOf(searchBaseDN));
+    result.setSearchScope(SearchScope.valueOf(asString(val, "searchScope")));
+    result.setSearchFilterTemplate(asString(val, "searchFilterTemplate"));
+
+    return result;
+  }
+
+  private String asString(JsonValue value, String key)
+  {
+    return value.get(key).required().asString();
+  }
+
+  private boolean asBool(JsonValue value, String key)
+  {
+    return value.get(key).defaultTo(false).asBoolean();
+  }
+
+  @Override
+  public Factory<Buffer> getBufferFactory()
+  {
+    return null;
+  }
+
+  @Override
+  public void stop()
+  {
+    closeSilently(filter);
+    handler = null;
+    filter = null;
+  }
+}
diff --git a/opendj-server-legacy/src/messages/org/opends/messages/protocol.properties b/opendj-server-legacy/src/messages/org/opends/messages/protocol.properties
index 91d6c42..9560963 100644
--- a/opendj-server-legacy/src/messages/org/opends/messages/protocol.properties
+++ b/opendj-server-legacy/src/messages/org/opends/messages/protocol.properties
@@ -916,3 +916,4 @@
   Verify that the keystore is properly configured
 ERR_INVALID_KEYSTORE_1527=No usable key was found for '%s'. Verify the keystore content
 INFO_DISABLE_CONNECTION_1528=Disabling %s
+ERR_INITIALIZE_HTTP_CONNECTION_HANDLER_1529=Failed to initialize Http Connection Handler
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/protocols/http/CollectClientConnectionsFilterTest.java b/opendj-server-legacy/src/test/java/org/opends/server/protocols/http/CollectClientConnectionsFilterTest.java
index c207865..ad82137 100644
--- a/opendj-server-legacy/src/test/java/org/opends/server/protocols/http/CollectClientConnectionsFilterTest.java
+++ b/opendj-server-legacy/src/test/java/org/opends/server/protocols/http/CollectClientConnectionsFilterTest.java
@@ -26,18 +26,17 @@
 package org.opends.server.protocols.http;
 
 import static org.assertj.core.api.Assertions.*;
-import static org.mockito.Mockito.*;
 import static org.opends.server.protocols.http.CollectClientConnectionsFilter.*;
 
 import java.io.IOException;
 
-import javax.servlet.ServletOutputStream;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
+import org.assertj.core.api.SoftAssertions;
+import org.forgerock.http.protocol.Request;
+import org.forgerock.http.protocol.Response;
 import org.forgerock.json.resource.ResourceException;
 import org.opends.server.DirectoryServerTestCase;
 import org.opends.server.util.Base64;
+import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.DataProvider;
 import org.testng.annotations.Test;
 
@@ -48,9 +47,15 @@
   private static final String PASSWORD = "open sesame";
   private static final String BASE64_USERPASS = Base64.encode((USERNAME + ":" + PASSWORD).getBytes());
 
-  private HTTPAuthenticationConfig authConfig = new HTTPAuthenticationConfig();
+  private HTTPAuthenticationConfig authConfig;
+  private CollectClientConnectionsFilter filter;
 
-  private CollectClientConnectionsFilter filter = new CollectClientConnectionsFilter(null, authConfig);
+  @BeforeMethod
+  private void createConfigAndFilter()
+  {
+    authConfig = new HTTPAuthenticationConfig();
+    filter = new CollectClientConnectionsFilter(null, authConfig);
+  }
 
   @DataProvider(name = "Invalid HTTP basic auth strings")
   public Object[][] getInvalidHttpBasicAuthStrings()
@@ -80,45 +85,35 @@
   public void sendUnauthorizedResponseWithHttpBasicAuthWillChallengeUserAgent() throws Exception
   {
     authConfig.setBasicAuthenticationSupported(true);
+    final Response response = sendUnauthorizedResponseWithHTTPBasicAuthChallenge();
 
-    ServletOutputStream oStream = mock(ServletOutputStream.class);
-    HttpServletResponse response = mock(HttpServletResponse.class);
-    when(response.getOutputStream()).thenReturn(oStream);
-    sendUnauthorizedResponseWithHTTPBasicAuthChallenge(response);
-
-    verify(response).setStatus(HttpServletResponse.SC_UNAUTHORIZED);
-    verify(response).setHeader("WWW-Authenticate", "Basic realm=\"org.forgerock.opendj\"");
-    verifyUnauthorizedOutputMessage(response, oStream);
+    assertThat(response.getHeaders().getFirst("WWW-Authenticate")).isEqualTo("Basic realm=\"org.forgerock.opendj\"");
+    verifyUnauthorizedOutputMessage(response);
   }
 
   @Test
   public void sendUnauthorizedResponseWithoutHttpBasicAuthWillNotChallengeUserAgent() throws Exception
   {
-    authConfig.setBasicAuthenticationSupported(true);
+    authConfig.setBasicAuthenticationSupported(false);
+    final Response response = sendUnauthorizedResponseWithHTTPBasicAuthChallenge();
 
-    HttpServletResponse response = mock(HttpServletResponse.class);
-    ServletOutputStream oStream = mock(ServletOutputStream.class);
-    when(response.getOutputStream()).thenReturn(oStream);
-    sendUnauthorizedResponseWithHTTPBasicAuthChallenge(response);
-
-    verify(response).setStatus(HttpServletResponse.SC_UNAUTHORIZED);
-    verifyUnauthorizedOutputMessage(response, oStream);
+    assertThat(response.getHeaders().getFirst("WWW-Authenticate")).isNull();
+    verifyUnauthorizedOutputMessage(response);
   }
 
-  private void sendUnauthorizedResponseWithHTTPBasicAuthChallenge(HttpServletResponse response)
+  private Response sendUnauthorizedResponseWithHTTPBasicAuthChallenge() throws Exception
   {
-    filter.sendErrorReponse(
-        response, true, ResourceException.getException(HttpServletResponse.SC_UNAUTHORIZED, "Invalid Credentials"));
+    return filter.resourceExceptionToPromise(ResourceException.getException(401, "Invalid Credentials")).get();
   }
 
-  private void verifyUnauthorizedOutputMessage(HttpServletResponse response, ServletOutputStream oStream)
-      throws IOException
+  private void verifyUnauthorizedOutputMessage(Response response) throws IOException
   {
-    verify(response).getOutputStream();
-    verify(oStream).println(
-        "{\n" + "    \"code\": 401,\n"
-              + "    \"message\": \"Invalid Credentials\",\n"
-              + "    \"reason\": \"Unauthorized\"\n" + "}");
+    final SoftAssertions softly = new SoftAssertions();
+    softly.assertThat(response.getStatus().getCode()).isEqualTo(401);
+    softly.assertThat(response.getStatus().getReasonPhrase()).isEqualTo("Unauthorized");
+    softly.assertThat(response.getEntity().getJson().toString()).isEqualTo(
+            "{code=401, reason=Unauthorized, message=Invalid Credentials}");
+    softly.assertAll();
   }
 
   @Test
@@ -126,9 +121,8 @@
   {
     authConfig.setBasicAuthenticationSupported(true);
 
-    HttpServletRequest request = mock(HttpServletRequest.class);
-    when(request.getHeader(HTTP_BASIC_AUTH_HEADER)).thenReturn("Basic " + BASE64_USERPASS);
-
+    final Request request = new Request();
+    request.getHeaders().add(HTTP_BASIC_AUTH_HEADER, "Basic " + BASE64_USERPASS);
     assertThat(filter.extractUsernamePassword(request)).containsExactly(USERNAME, PASSWORD);
   }
 
@@ -142,9 +136,9 @@
     authConfig.setCustomHeaderUsername(customHeaderUsername);
     authConfig.setCustomHeaderPassword(customHeaderPassword);
 
-    HttpServletRequest request = mock(HttpServletRequest.class);
-    when(request.getHeader(customHeaderUsername)).thenReturn(USERNAME);
-    when(request.getHeader(customHeaderPassword)).thenReturn(PASSWORD);
+    final Request request = new Request();
+    request.getHeaders().add(customHeaderUsername, USERNAME);
+    request.getHeaders().add(customHeaderPassword, PASSWORD);
 
     assertThat(filter.extractUsernamePassword(request)).containsExactly(USERNAME, PASSWORD);
   }
diff --git a/pom.xml b/pom.xml
index 7afa5ad..0d266c3 100644
--- a/pom.xml
+++ b/pom.xml
@@ -114,7 +114,8 @@
     <i18nFrameworkVersion>1.4.2-SNAPSHOT</i18nFrameworkVersion>
     <grizzlyFrameworkVersion>2.3.14</grizzlyFrameworkVersion>
     <slf4jVersion>1.7.5</slf4jVersion>
-    <forgerockRestVersion>2.1.0-SNAPSHOT</forgerockRestVersion>
+    <forgerockRestVersion>3.0.0-SNAPSHOT</forgerockRestVersion>
+    <forgerockHttpVersion>0.0.1-SNAPSHOT</forgerockHttpVersion>
     <frDocPluginVersion>3.1.0-SNAPSHOT</frDocPluginVersion>
 
     <!-- OSGi bundles properties -->

--
Gitblit v1.10.0