From eb78aabadc36e75ca680d12d80de7dcb3f6b6b20 Mon Sep 17 00:00:00 2001
From: Matthew Swift <matthew.swift@forgerock.com>
Date: Tue, 24 Nov 2015 00:25:24 +0000
Subject: [PATCH] OPENDJ-1607 Merge HeartBeatConnectionFactory and AuthenticatedConnectionFactory into LDAPConnectionFactory

---
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/BindResultLdapPromiseImpl.java                    |   40 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/spi/BasicLDAPConnectionFactory.java                   |   40 
 opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/RewriterProxy.java                   |   27 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/io/LDAPReaderWriterTestCase.java                           |    4 
 opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/LDAPModify.java                         |   38 
 opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/GrizzlyUtils.java                               |    6 
 opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/SearchRate.java                         |    1 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/SearchResultLdapPromiseImpl.java                  |   22 
 opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/LDAPClientFilter.java                           |    2 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/LDAPListener.java                                     |   17 
 opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/PerformanceRunner.java                  |   20 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/LDAPConnectionFactoryImpl.java                    |   28 
 opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/Server.java                          |    4 
 opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/SimpleAuthAsync.java                 |   18 
 opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/LDAPSearch.java                         |   20 
 opendj-sdk/opendj-core/clirr-ignored-api-changes.xml                                                                 |   29 
 opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/SASLAuth.java                        |    4 
 opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/HttpAuthenticationFilter.java               |   11 
 opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/GrizzlyLDAPConnectionFactory.java               |  196 -
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/LdapPromises.java                                 |  510 ++++-
 opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/AuthRate.java                           |    8 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/LDAPConnectionFactory.java                            | 1198 +++++++++++++-
 opendj-sdk/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/GrizzlyLDAPConnectionTestCase.java              |   16 
 opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/PerformanceRunnerOptions.java           |    3 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/LdapPromiseWrapper.java                           |    4 
 opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/ConnectionFactoryProvider.java                          |   66 
 opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/Utils.java                              |  120 
 opendj-sdk/opendj-core/src/main/resources/com/forgerock/opendj/ldap/core.properties                                  |    3 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/StartTLSExtendedRequestImpl.java             |   12 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/ResultLdapPromiseImpl.java                        |   22 
 opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/Proxy.java                           |   29 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableStartTLSExtendedRequestImpl.java |   12 
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/HeartBeatConnectionFactoryTestCase.java               |  212 +
 opendj-sdk/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/GrizzlyLDAPListenerTestCase.java                |   10 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/Connections.java                                      |  141 -
 opendj-sdk/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/GrizzlyLDAPReaderWriterTestCase.java            |    4 
 opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java                              |   72 
 opendj-sdk/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/ConnectionFactoryTestCase.java                  |  130 
 opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/GrizzlyLDAPConnection.java                      |   79 
 opendj-sdk/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/GrizzlyLDAPConnectionFactoryTestCase.java       |    9 
 opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/SimpleAuth.java                      |    4 
 opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/ModRate.java                            |    1 
 opendj-sdk/opendj-ldap-toolkit/src/test/java/com/forgerock/opendj/ldap/tools/ConnectionFactoryProviderTest.java      |    4 
 opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/GrizzlyLDAPListener.java                        |    4 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/StartTLSExtendedRequest.java                 |   35 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/LDAPConnectionImpl.java                           |  324 ++++
 opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/LDAPCompare.java                        |   27 
 /dev/null                                                                                                            |  994 ------------
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/ExtendedResultLdapPromiseImpl.java                |   65 
 opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/AddRate.java                            |    1 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/CommonLDAPOptions.java                                |  166 +
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/LDAPServer.java                                       |    2 
 52 files changed, 2,663 insertions(+), 2,151 deletions(-)

diff --git a/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/AuthenticatedConnectionFactory.java b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/AuthenticatedConnectionFactory.java
deleted file mode 100644
index a761491..0000000
--- a/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/AuthenticatedConnectionFactory.java
+++ /dev/null
@@ -1,298 +0,0 @@
-/*
- * 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 2009-2010 Sun Microsystems, Inc.
- *      Portions Copyright 2011-2015 ForgeRock AS.
- */
-package com.forgerock.opendj.cli;
-
-import static org.forgerock.util.Utils.*;
-
-import java.util.concurrent.atomic.AtomicReference;
-
-import org.forgerock.opendj.ldap.AbstractConnectionWrapper;
-import org.forgerock.opendj.ldap.Connection;
-import org.forgerock.opendj.ldap.ConnectionFactory;
-import org.forgerock.opendj.ldap.IntermediateResponseHandler;
-import org.forgerock.opendj.ldap.LdapException;
-import org.forgerock.opendj.ldap.LdapPromise;
-import org.forgerock.opendj.ldap.requests.BindRequest;
-import org.forgerock.opendj.ldap.responses.BindResult;
-import org.forgerock.util.AsyncFunction;
-import org.forgerock.util.Function;
-import org.forgerock.util.Reject;
-import org.forgerock.util.promise.ExceptionHandler;
-import org.forgerock.util.promise.Promise;
-import org.forgerock.util.promise.ResultHandler;
-/**
- * An authenticated connection factory can be used to create pre-authenticated
- * connections to a Directory Server.
- * <p>
- * The connections returned by an authenticated connection factory support all
- * operations with the exception of Bind requests. Attempts to perform a Bind
- * will result in an {@code UnsupportedOperationException}.
- * <p>
- * In addition, the returned connections support retrieval of the
- * {@code BindResult} returned from the initial Bind request, or last rebind.
- * <p>
- * Support for connection re-authentication is provided through the
- * {@link #setRebindAllowed} method which, if set to {@code true}, causes
- * subsequent connections created using the factory to support the
- * {@code rebind} method.
- * <p>
- * If the Bind request fails for some reason (e.g. invalid credentials), then
- * the connection attempt will fail and an {@link LdapException} will be thrown.
- */
-public final class AuthenticatedConnectionFactory implements ConnectionFactory {
-
-    /**
-     * An authenticated connection supports all operations except Bind
-     * operations.
-     */
-    public static final class AuthenticatedConnection extends AbstractConnectionWrapper<Connection> {
-
-        private final BindRequest request;
-        private volatile BindResult result;
-
-        private AuthenticatedConnection(final Connection connection, final BindRequest request,
-                final BindResult result) {
-            super(connection);
-            this.request = request;
-            this.result = result;
-        }
-
-        /*
-         * Bind operations are not supported by pre-authenticated connections.
-         * These methods will always throw {@code UnsupportedOperationException}.
-         */
-        /** {@inheritDoc} */
-        @Override
-        public BindResult bind(BindRequest request) throws LdapException {
-            throw new UnsupportedOperationException();
-        }
-
-        /** {@inheritDoc} */
-        @Override
-        public BindResult bind(String name, char[] password) throws LdapException {
-            throw new UnsupportedOperationException();
-        }
-
-        /** {@inheritDoc} */
-        @Override
-        public LdapPromise<BindResult> bindAsync(BindRequest request,
-            IntermediateResponseHandler intermediateResponseHandler) {
-            throw new UnsupportedOperationException();
-        }
-
-        /**
-         * Returns an unmodifiable view of the Bind result which was returned
-         * from the server after authentication.
-         *
-         * @return The Bind result which was returned from the server after
-         *         authentication.
-         */
-        public BindResult getAuthenticatedBindResult() {
-            return result;
-        }
-
-        /**
-         * Re-authenticates to the Directory Server using the bind request
-         * associated with this connection. If re-authentication fails for some
-         * reason then this connection will be automatically closed.
-         *
-         * @return A promise representing the result of the operation.
-         * @throws UnsupportedOperationException
-         *             If this connection does not support rebind operations.
-         * @throws IllegalStateException
-         *             If this connection has already been closed, i.e. if
-         *             {@code isClosed() == true}.
-         */
-        public LdapPromise<BindResult> rebindAsync() {
-            if (request == null) {
-                throw new UnsupportedOperationException();
-            }
-
-            return connection.bindAsync(request)
-                      .thenOnResult(new ResultHandler<BindResult>() {
-                          @Override
-                          public void handleResult(final BindResult result) {
-                              // Save the result.
-                              AuthenticatedConnection.this.result = result;
-                          }
-                      }).thenOnException(new ExceptionHandler<LdapException>() {
-                          @Override
-                          public void handleException(final LdapException exception) {
-                              /*
-                               * This connection is now unauthenticated so prevent further use.
-                               */
-                              connection.close();
-                          }
-                      });
-        }
-
-        /**
-         * Returns the string representation of this authenticated connection.
-         *
-         * @return The string representation of this authenticated connection factory.
-         */
-        @Override
-        public String toString() {
-            StringBuilder builder = new StringBuilder();
-            builder.append("AuthenticatedConnection(");
-            builder.append(connection);
-            builder.append(')');
-            return builder.toString();
-        }
-
-    }
-
-    private final BindRequest request;
-    private final ConnectionFactory parentFactory;
-    private boolean allowRebinds;
-
-    /**
-     * Creates a new authenticated connection factory which will obtain
-     * connections using the provided connection factory and immediately perform
-     * the provided Bind request.
-     *
-     * @param factory
-     *            The connection factory to use for connecting to the Directory
-     *            Server.
-     * @param request
-     *            The Bind request to use for authentication.
-     * @throws NullPointerException
-     *             If {@code factory} or {@code request} was {@code null}.
-     */
-    public AuthenticatedConnectionFactory(final ConnectionFactory factory, final BindRequest request) {
-        Reject.ifNull(factory, request);
-        this.parentFactory = factory;
-
-        // FIXME: should do a defensive copy.
-        this.request = request;
-    }
-
-    @Override
-    public void close() {
-        parentFactory.close();
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    public Connection getConnection() throws LdapException {
-        final Connection connection = parentFactory.getConnection();
-        BindResult bindResult = null;
-        try {
-            bindResult = connection.bind(request);
-        } finally {
-            if (bindResult == null) {
-                connection.close();
-            }
-        }
-
-        /*
-         * If the bind didn't succeed then an exception will have been thrown
-         * and this line will not be reached.
-         */
-        return new AuthenticatedConnection(connection, request, bindResult);
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    public Promise<Connection, LdapException> getConnectionAsync() {
-        final AtomicReference<Connection> connectionHolder = new AtomicReference<>();
-        return parentFactory.getConnectionAsync()
-            .thenAsync(
-                    new AsyncFunction<Connection, BindResult, LdapException>() {
-                        @Override
-                        public Promise<BindResult, LdapException> apply(final Connection connection)
-                                throws LdapException {
-                            connectionHolder.set(connection);
-                            return connection.bindAsync(request);
-                        }
-                    }
-            ).then(
-                    new Function<BindResult, Connection, LdapException>() {
-                        @Override
-                        public Connection apply(BindResult result) throws LdapException {
-                            // FIXME: should make the result unmodifiable.
-                            return new AuthenticatedConnection(connectionHolder.get(), request, result);
-                        }
-                    },
-                    new Function<LdapException, Connection, LdapException>() {
-                        @Override
-                        public Connection apply(LdapException errorResult) throws LdapException {
-                            closeSilently(connectionHolder.get());
-                            throw errorResult;
-                        }
-                    }
-            );
-    }
-
-    /**
-     * Indicates whether or not rebind requests are to be supported by
-     * connections created by this authenticated connection factory.
-     * <p>
-     * Rebind requests are invoked using the connection's {@code rebind} method
-     * which will throw an {@code UnsupportedOperationException} if rebinds are
-     * not supported (the default).
-     *
-     * @return allowRebinds {@code true} if the {@code rebind} operation is to
-     *         be supported, otherwise {@code false}.
-     */
-    boolean isRebindAllowed() {
-        return allowRebinds;
-    }
-
-    /**
-     * Specifies whether or not rebind requests are to be supported by
-     * connections created by this authenticated connection factory.
-     * <p>
-     * Rebind requests are invoked using the connection's {@code rebind} method
-     * which will throw an {@code UnsupportedOperationException} if rebinds are
-     * not supported (the default).
-     *
-     * @param allowRebinds
-     *            {@code true} if the {@code rebind} operation is to be
-     *            supported, otherwise {@code false}.
-     * @return A reference to this connection factory.
-     */
-    AuthenticatedConnectionFactory setRebindAllowed(final boolean allowRebinds) {
-        this.allowRebinds = allowRebinds;
-        return this;
-    }
-
-    /**
-     * Returns the string representation of this authenticated connection factory.
-     *
-     * @return The string representation of this authenticated connection factory.
-     */
-    @Override
-    public String toString() {
-        final StringBuilder builder = new StringBuilder();
-        builder.append("AuthenticatedConnectionFactory(");
-        builder.append(parentFactory);
-        builder.append(')');
-        return builder.toString();
-    }
-
-}
diff --git a/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/ConnectionFactoryProvider.java b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/ConnectionFactoryProvider.java
index 33dca1b..1f21cb8 100644
--- a/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/ConnectionFactoryProvider.java
+++ b/opendj-sdk/opendj-cli/src/main/java/com/forgerock/opendj/cli/ConnectionFactoryProvider.java
@@ -27,10 +27,13 @@
 package com.forgerock.opendj.cli;
 
 import static com.forgerock.opendj.cli.ArgumentConstants.*;
-import static com.forgerock.opendj.cli.CliMessages.*;
 import static com.forgerock.opendj.cli.CliConstants.DEFAULT_LDAP_PORT;
+import static com.forgerock.opendj.cli.CliMessages.*;
 import static com.forgerock.opendj.cli.Utils.getHostNameForLdapUrl;
-import static org.forgerock.opendj.ldap.LDAPConnectionFactory.*;
+import static org.forgerock.opendj.ldap.LDAPConnectionFactory.AUTHN_BIND_REQUEST;
+import static org.forgerock.opendj.ldap.LDAPConnectionFactory.CONNECT_TIMEOUT;
+import static org.forgerock.opendj.ldap.LDAPConnectionFactory.SSL_CONTEXT;
+import static org.forgerock.opendj.ldap.LDAPConnectionFactory.SSL_USE_STARTTLS;
 
 import java.io.File;
 import java.io.FileInputStream;
@@ -65,6 +68,7 @@
 import org.forgerock.opendj.ldap.requests.PlainSASLBindRequest;
 import org.forgerock.opendj.ldap.requests.Requests;
 import org.forgerock.util.Options;
+import org.forgerock.util.time.Duration;
 
 /**
  * A connection factory designed for use with command line tools.
@@ -134,8 +138,6 @@
 
     /**  The basic connection factory. */
     private ConnectionFactory connFactory;
-    /** The authenticated connection factory. */
-    protected ConnectionFactory authenticatedConnFactory;
 
     /** The bind request to connect with. */
     private BindRequest bindRequest;
@@ -345,15 +347,34 @@
     }
 
     /**
-     * Checks if any conflicting arguments are present, build the connection with selected arguments and returns the
-     * connection factory. If the application is interactive, it will prompt the user for missing parameters.
+     * Constructs a connection factory for pre-authenticated connections. Checks if any conflicting arguments are
+     * present, build the connection with selected arguments and returns the connection factory. If the application is
+     * interactive, it will prompt the user for missing parameters.
      *
      * @return The connection factory.
      * @throws ArgumentException
-     *             If an error occurs during the parsing of the arguments.
-     *             (conflicting arguments or if an error occurs during building SSL context).
+     *         If an error occurs during the parsing of the arguments (conflicting arguments or if an error occurs
+     *         during building SSL context).
      */
-    public ConnectionFactory getConnectionFactory() throws ArgumentException {
+    public ConnectionFactory getAuthenticatedConnectionFactory() throws ArgumentException {
+        return getConnectionFactory(true);
+    }
+
+    /**
+     * Constructs a connection factory for unauthenticated connections. Checks if any conflicting arguments are present,
+     * build the connection with selected arguments and returns the connection factory. If the application is
+     * interactive, it will prompt the user for missing parameters.
+     *
+     * @return The connection factory.
+     * @throws ArgumentException
+     *         If an error occurs during the parsing of the arguments (conflicting arguments or if an error occurs
+     *         during building SSL context).
+     */
+    public ConnectionFactory getUnauthenticatedConnectionFactory() throws ArgumentException {
+        return getConnectionFactory(false);
+    }
+
+    private ConnectionFactory getConnectionFactory(boolean usePreAuthentication) throws ArgumentException {
         if (connFactory == null) {
             port = portArg.isPresent() ? portArg.getIntValue() : 0;
 
@@ -415,13 +436,14 @@
             }
 
             Options options = Options.defaultOptions();
-
             if (sslContext != null) {
                 options.set(SSL_CONTEXT, sslContext)
-                    .set(USE_STARTTLS, useStartTLSArg.isPresent());
+                    .set(SSL_USE_STARTTLS, useStartTLSArg.isPresent());
             }
-            options.set(CONNECT_TIMEOUT_IN_MILLISECONDS,
-                TimeUnit.MILLISECONDS.toMillis(getConnectTimeout()));
+            options.set(CONNECT_TIMEOUT, new Duration((long) getConnectTimeout(), TimeUnit.MILLISECONDS));
+            if (usePreAuthentication) {
+                options.set(AUTHN_BIND_REQUEST, getBindRequest());
+            }
             connFactory = new LDAPConnectionFactory(hostNameArg.getValue(), port, options);
         }
         return connFactory;
@@ -503,24 +525,6 @@
     }
 
     /**
-     * Returns the authenticated connection factory.
-     *
-     * @return The authenticated connection factory.
-     * @throws ArgumentException
-     *             If an error occurs during parsing the arguments.
-     */
-    public ConnectionFactory getAuthenticatedConnectionFactory() throws ArgumentException {
-        if (authenticatedConnFactory == null) {
-            authenticatedConnFactory = getConnectionFactory();
-            final BindRequest bindRequest = getBindRequest();
-            if (bindRequest != null) {
-                authenticatedConnFactory = new AuthenticatedConnectionFactory(authenticatedConnFactory, bindRequest);
-            }
-        }
-        return authenticatedConnFactory;
-    }
-
-    /**
      * Returns {@code true} if we can read on the provided path and
      * {@code false} otherwise.
      *
diff --git a/opendj-sdk/opendj-core/clirr-ignored-api-changes.xml b/opendj-sdk/opendj-core/clirr-ignored-api-changes.xml
index 84db7f9..dda2712 100644
--- a/opendj-sdk/opendj-core/clirr-ignored-api-changes.xml
+++ b/opendj-sdk/opendj-core/clirr-ignored-api-changes.xml
@@ -543,6 +543,35 @@
     <justification>OPENDJ-1802 ByteString.valueOf() => valueOfInt(), valueOfLong(), valueOfUtf8(), valueOfBytes(), valueOfObject()</justification>
   </difference>
   <difference>
+    <className>org/forgerock/opendj/ldap/Connections</className>
+    <differenceType>7002</differenceType>
+    <method>*newAuthenticatedConnectionFactory*</method>
+    <justification>OPENDJ-1607: merge authenticated and heart-beat connection factories into
+      LDAPConnectionFactory. Pre-authenticated connection support is now part of
+      LDAPConnectionFactory and can be enabled by specifying the AUTHN_BIND_REQUEST option.</justification>
+  </difference>
+  <difference>
+    <className>org/forgerock/opendj/ldap/Connections</className>
+    <differenceType>7002</differenceType>
+    <method>*newHeartBeatConnectionFactory*</method>
+    <justification>OPENDJ-1607: merge authenticated and heart-beat connection factories into
+      LDAPConnectionFactory. Heart-beat support is now part of
+      LDAPConnectionFactory and can be configured using the HEARTBEAT_* options.</justification>
+  </difference>
+  <difference>
+    <className>org/forgerock/opendj/ldap/AuthenticatedConnectionFactory*</className>
+    <differenceType>8001</differenceType>
+    <justification>OPENDJ-1607: merge authenticated and heart-beat connection factories into
+      LDAPConnectionFactory. Pre-authenticated connection support is now part of
+      LDAPConnectionFactory and can be enabled by specifying the AUTHN_BIND_REQUEST option.</justification>
+  </difference>
+  <difference>
+    <className>org/forgerock/opendj/ldap/requests/StartTLSExtendedRequest</className>
+    <differenceType>7012</differenceType>
+    <method>*addEnabled*(java.util.Collection)</method>
+    <justification>OPENDJ-1607: added Collection based addEnabled* methods</justification>
+  </difference>
+  <difference>
     <className>org/forgerock/opendj/ldap/AttributeParser</className>
     <differenceType>7014</differenceType>
     <method>java.util.Set asSetOf(org.forgerock.opendj.ldap.Function, java.lang.Object[])</method>
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AuthenticatedConnectionFactory.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AuthenticatedConnectionFactory.java
deleted file mode 100644
index bf15cd2..0000000
--- a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/AuthenticatedConnectionFactory.java
+++ /dev/null
@@ -1,181 +0,0 @@
-/*
- * 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 2009-2010 Sun Microsystems, Inc.
- *      Portions Copyright 2011-2015 ForgeRock AS.
- */
-
-package org.forgerock.opendj.ldap;
-
-import java.util.concurrent.atomic.AtomicReference;
-
-import org.forgerock.opendj.ldap.requests.BindRequest;
-import org.forgerock.opendj.ldap.responses.BindResult;
-import org.forgerock.util.AsyncFunction;
-import org.forgerock.util.Function;
-import org.forgerock.util.promise.Promise;
-
-import static org.forgerock.util.Utils.*;
-
-/**
- * An authenticated connection factory can be used to create pre-authenticated
- * connections to a Directory Server.
- * <p>
- * The connections returned by an authenticated connection factory support all
- * operations with the exception of Bind requests. Attempts to perform a Bind
- * will result in an {@code UnsupportedOperationException}.
- * <p>
- * If the Bind request fails for some reason (e.g. invalid credentials), then
- * the connection attempt will fail and an {@link LdapException} will be thrown.
- */
-final class AuthenticatedConnectionFactory implements ConnectionFactory {
-
-    /**
-     * An authenticated connection supports all operations except Bind
-     * operations.
-     */
-    public static final class AuthenticatedConnection extends AbstractConnectionWrapper<Connection> {
-
-        private AuthenticatedConnection(final Connection connection) {
-            super(connection);
-        }
-
-        /**
-         * Bind operations are not supported by pre-authenticated connections.
-         * These methods will always throw {@code UnsupportedOperationException}.
-         */
-        public LdapPromise<BindResult> bindAsync(final BindRequest request,
-                final IntermediateResponseHandler intermediateResponseHandler,
-                final LdapResultHandler<? super BindResult> resultHandler) {
-            throw new UnsupportedOperationException();
-        }
-
-        @Override
-        public BindResult bind(BindRequest request) throws LdapException {
-            throw new UnsupportedOperationException();
-        }
-
-        @Override
-        public BindResult bind(String name, char[] password) throws LdapException {
-            throw new UnsupportedOperationException();
-        }
-
-        @Override
-        public String toString() {
-            StringBuilder builder = new StringBuilder();
-            builder.append("AuthenticatedConnection(");
-            builder.append(connection);
-            builder.append(')');
-            return builder.toString();
-        }
-
-    }
-
-    private final BindRequest request;
-    private final ConnectionFactory parentFactory;
-
-    /**
-     * Creates a new authenticated connection factory which will obtain
-     * connections using the provided connection factory and immediately perform
-     * the provided Bind request.
-     *
-     * @param factory
-     *            The connection factory to use for connecting to the Directory
-     *            Server.
-     * @param request
-     *            The Bind request to use for authentication.
-     */
-    AuthenticatedConnectionFactory(final ConnectionFactory factory, final BindRequest request) {
-        this.parentFactory = factory;
-
-        // FIXME: should do a defensive copy.
-        this.request = request;
-    }
-
-    @Override
-    public void close() {
-        // Delegate.
-        parentFactory.close();
-    }
-
-    @Override
-    public Connection getConnection() throws LdapException {
-        final Connection connection = parentFactory.getConnection();
-        boolean bindSucceeded = false;
-        try {
-            connection.bind(request);
-            bindSucceeded = true;
-        } finally {
-            if (!bindSucceeded) {
-                connection.close();
-            }
-        }
-
-        /*
-         * If the bind didn't succeed then an exception will have been thrown
-         * and this line will not be reached.
-         */
-        return new AuthenticatedConnection(connection);
-    }
-
-    @Override
-    public Promise<Connection, LdapException> getConnectionAsync() {
-        final AtomicReference<Connection> connectionHolder = new AtomicReference<>();
-        return parentFactory.getConnectionAsync()
-            .thenAsync(
-                    new AsyncFunction<Connection, BindResult, LdapException>() {
-                        @Override
-                        public Promise<BindResult, LdapException> apply(final Connection connection)
-                                throws LdapException {
-                            connectionHolder.set(connection);
-                            return connection.bindAsync(request);
-                        }
-                    }
-            ).then(
-                    new Function<BindResult, Connection, LdapException>() {
-                        @Override
-                        public Connection apply(BindResult result) throws LdapException {
-                            return new AuthenticatedConnection(connectionHolder.get());
-                        }
-                    },
-                    new Function<LdapException, Connection, LdapException>() {
-                        @Override
-                        public Connection apply(LdapException error) throws LdapException {
-                            closeSilently(connectionHolder.get());
-                            throw error;
-                        }
-                    }
-            );
-    }
-
-    @Override
-    public String toString() {
-        final StringBuilder builder = new StringBuilder();
-        builder.append("AuthenticatedConnectionFactory(");
-        builder.append(parentFactory);
-        builder.append(", ");
-        builder.append(request);
-        builder.append(')');
-        return builder.toString();
-    }
-}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/CommonLDAPOptions.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/CommonLDAPOptions.java
index b42b5e3..0fd5d4a 100644
--- a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/CommonLDAPOptions.java
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/CommonLDAPOptions.java
@@ -25,12 +25,108 @@
 
 package org.forgerock.opendj.ldap;
 
+import static com.forgerock.opendj.util.StaticUtils.getProvider;
+
+import org.forgerock.opendj.ldap.spi.TransportProvider;
 import org.forgerock.util.Option;
+import org.forgerock.util.Options;
 
 /**
  * Common options for LDAP clients and listeners.
  */
 abstract class CommonLDAPOptions {
+    /**
+     * Specifies the class loader which will be used to load the
+     * {@link org.forgerock.opendj.ldap.spi.TransportProvider TransportProvider}.
+     * <p>
+     * By default the default class loader will be used.
+     * <p>
+     * The transport provider is loaded using {@code java.util.ServiceLoader},
+     * the JDK service-provider loading facility. The provider must be
+     * accessible from the same class loader that was initially queried to
+     * locate the configuration file; note that this is not necessarily the
+     * class loader from which the file was actually loaded. This method allows
+     * to provide a class loader to be used for loading the provider.
+     *
+     */
+    public static final Option<ClassLoader> TRANSPORT_PROVIDER_CLASS_LOADER = Option.of(ClassLoader.class, null);
+
+    /**
+     * Specifies the name of the provider to use for transport.
+     * <p>
+     * Transport providers implement {@link org.forgerock.opendj.ldap.spi.TransportProvider TransportProvider}
+     * interface.
+     * <p>
+     * The name should correspond to the name of an existing provider, as
+     * returned by {@code TransportProvider#getName()} method.
+     */
+    public static final Option<String> TRANSPORT_PROVIDER = Option.of(String.class, null);
+
+    /**
+     * Specifies the transport provider to use. This option is internal and only intended for testing.
+     */
+    static final Option<TransportProvider> TRANSPORT_PROVIDER_INSTANCE = Option.of(TransportProvider.class, null);
+
+    /**
+     * Specifies the value of the {@link java.net.SocketOptions#TCP_NODELAY
+     * TCP_NODELAY} socket option for new connections.
+     * <p>
+     * The default setting is {@code true} and may be configured using the
+     * {@code org.forgerock.opendj.io.tcpNoDelay} property.
+     */
+    public static final Option<Boolean> TCP_NO_DELAY = Option.withDefault(
+        getBooleanProperty("org.forgerock.opendj.io.tcpNoDelay", true));
+
+    /**
+     * Specifies the value of the {@link java.net.SocketOptions#SO_REUSEADDR
+     * SO_REUSEADDR} socket option for new connections.
+     * <p>
+     * The default setting is {@code true} and may be configured using the
+     * {@code org.forgerock.opendj.io.reuseAddress} property.
+     *
+     */
+    public static final Option<Boolean> SO_REUSE_ADDRESS = Option.withDefault(
+        getBooleanProperty("org.forgerock.opendj.io.reuseAddress", true));
+
+    /**
+     * Specifies the value of the {@link java.net.SocketOptions#SO_LINGER
+     * SO_LINGER} socket option for new connections.
+     * <p>
+     * The default setting is {@code -1} (disabled) and may be configured using
+     * the {@code org.forgerock.opendj.io.linger} property.
+     *
+     * @param linger
+     *            The value of the {@link java.net.SocketOptions#SO_LINGER
+     *            SO_LINGER} socket option for new connections, or -1 if linger
+     *            should be disabled.
+     * @return A reference to this set of options.
+     */
+    public static final Option<Integer> SO_LINGER_IN_SECONDS = Option.withDefault(
+        getIntProperty("org.forgerock.opendj.io.linger", -1));
+
+    /**
+     * Specifies the value of the {@link java.net.SocketOptions#SO_KEEPALIVE
+     * SO_KEEPALIVE} socket option for new connections.
+     * <p>
+     * The default setting is {@code true} and may be configured using the
+     * {@code org.forgerock.opendj.io.keepAlive} property.
+     *
+     */
+    public static final Option<Boolean> SO_KEEPALIVE = Option.withDefault(
+        getBooleanProperty("org.forgerock.opendj.io.keepAlive", true));
+
+    /** Sets the decoding options which will be used to control how requests and responses are decoded. */
+    public static final Option<DecodeOptions> LDAP_DECODE_OPTIONS = Option.withDefault(new DecodeOptions());
+
+    static TransportProvider getTransportProvider(final Options options) {
+        final TransportProvider transportProvider = options.get(TRANSPORT_PROVIDER_INSTANCE);
+        if (transportProvider != null) {
+            return transportProvider;
+        }
+        return getProvider(TransportProvider.class,
+                           options.get(TRANSPORT_PROVIDER),
+                           options.get(TRANSPORT_PROVIDER_CLASS_LOADER));
+    }
 
     static boolean getBooleanProperty(final String name, final boolean defaultValue) {
         final String value = System.getProperty(name);
@@ -45,74 +141,4 @@
             return defaultValue;
         }
     }
-
-    /** Sets the decoding options which will be used to control how requests and responses are decoded. */
-    public static final Option<DecodeOptions> DECODE_OPTIONS = Option.withDefault(new DecodeOptions());
-    /**
-     * Specifies the class loader which will be used to load the {@link TransportProvider}.
-     * <p>
-     * By default the default class loader will be used.
-     * <p>
-     * The transport provider is loaded using {@code java.util.ServiceLoader},
-     * the JDK service-provider loading facility. The provider must be
-     * accessible from the same class loader that was initially queried to
-     * locate the configuration file; note that this is not necessarily the
-     * class loader from which the file was actually loaded. This method allows
-     * to provide a class loader to be used for loading the provider.
-     *
-     */
-    public static final Option<ClassLoader> PROVIDER_CLASS_LOADER = Option.of(ClassLoader.class, null);
-    /**
-     * Specifies the name of the provider to use for transport.
-     * <p>
-     * Transport providers implement {@link TransportProvider} interface.
-     * <p>
-     * The name should correspond to the name of an existing provider, as
-     * returned by {@code TransportProvider#getName()} method.
-     */
-    public static final Option<String> TRANSPORT_PROVIDER = Option.of(String.class, null);
-    /**
-     * Specifies the value of the {@link java.net.SocketOptions#TCP_NODELAY
-     * TCP_NODELAY} socket option for new connections.
-     * <p>
-     * The default setting is {@code true} and may be configured using the
-     * {@code org.forgerock.opendj.io.tcpNoDelay} property.
-     */
-    public static final Option<Boolean> TCP_NO_DELAY = Option.withDefault(
-        getBooleanProperty("org.forgerock.opendj.io.tcpNoDelay", true));
-    /**
-     * Specifies the value of the {@link java.net.SocketOptions#SO_REUSEADDR
-     * SO_REUSEADDR} socket option for new connections.
-     * <p>
-     * The default setting is {@code true} and may be configured using the
-     * {@code org.forgerock.opendj.io.reuseAddress} property.
-     *
-     */
-    public static final Option<Boolean> REUSE_ADDRESS = Option.withDefault(
-        getBooleanProperty("org.forgerock.opendj.io.reuseAddress", true));
-    /**
-     * Specifies the value of the {@link java.net.SocketOptions#SO_LINGER
-     * SO_LINGER} socket option for new connections.
-     * <p>
-     * The default setting is {@code -1} (disabled) and may be configured using
-     * the {@code org.forgerock.opendj.io.linger} property.
-     *
-     * @param linger
-     *            The value of the {@link java.net.SocketOptions#SO_LINGER
-     *            SO_LINGER} socket option for new connections, or -1 if linger
-     *            should be disabled.
-     * @return A reference to this set of options.
-     */
-    public static final Option<Integer> LINGER = Option.withDefault(
-        getIntProperty("org.forgerock.opendj.io.linger", -1));
-    /**
-     * Specifies the value of the {@link java.net.SocketOptions#SO_KEEPALIVE
-     * SO_KEEPALIVE} socket option for new connections.
-     * <p>
-     * The default setting is {@code true} and may be configured using the
-     * {@code org.forgerock.opendj.io.keepAlive} property.
-     *
-     */
-    public static final Option<Boolean> KEEPALIVE = Option.withDefault(
-        getBooleanProperty("org.forgerock.opendj.io.keepAlive", true));
 }
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/Connections.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/Connections.java
index d7fe7eb..38c6fb7 100644
--- a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/Connections.java
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/Connections.java
@@ -34,8 +34,7 @@
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
 
-import org.forgerock.opendj.ldap.requests.BindRequest;
-import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.util.Options;
 import org.forgerock.util.Reject;
 import org.forgerock.util.promise.Promise;
 
@@ -45,35 +44,6 @@
  */
 public final class Connections {
     /**
-     * Creates a new authenticated connection factory which will obtain
-     * connections using the provided connection factory and immediately perform
-     * the provided Bind request.
-     * <p>
-     * The connections returned by an authenticated connection factory support
-     * all operations with the exception of Bind requests. Attempts to perform a
-     * Bind will result in an {@code UnsupportedOperationException}.
-     * <p>
-     * If the Bind request fails for some reason (e.g. invalid credentials),
-     * then the connection attempt will fail and an {@link LdapException}
-     * will be thrown.
-     *
-     * @param factory
-     *            The connection factory to use for connecting to the Directory
-     *            Server.
-     * @param request
-     *            The Bind request to use for authentication.
-     * @return The new connection pool.
-     * @throws NullPointerException
-     *             If {@code factory} or {@code request} was {@code null}.
-     */
-    public static ConnectionFactory newAuthenticatedConnectionFactory(
-            final ConnectionFactory factory, final BindRequest request) {
-        Reject.ifNull(factory, request);
-
-        return new AuthenticatedConnectionFactory(factory, request);
-    }
-
-    /**
      * Creates a new connection pool which creates new connections as needed
      * using the provided connection factory, but will reuse previously
      * allocated connections when they are available.
@@ -243,113 +213,6 @@
     }
 
     /**
-     * Creates a new heart-beat connection factory which will create connections
-     * using the provided connection factory and periodically ping any created
-     * connections in order to detect that they are still alive every 10 seconds
-     * using the default scheduler. Connections will be marked as having failed
-     * if a heart-beat takes longer than 3 seconds.
-     *
-     * @param factory
-     *            The connection factory to use for creating connections.
-     * @return The new heart-beat connection factory.
-     * @throws NullPointerException
-     *             If {@code factory} was {@code null}.
-     */
-    public static ConnectionFactory newHeartBeatConnectionFactory(final ConnectionFactory factory) {
-        return new HeartBeatConnectionFactory(factory, 10, 3, TimeUnit.SECONDS, null, null);
-    }
-
-    /**
-     * Creates a new heart-beat connection factory which will create connections
-     * using the provided connection factory and periodically ping any created
-     * connections in order to detect that they are still alive using the
-     * specified frequency and the default scheduler.
-     *
-     * @param factory
-     *            The connection factory to use for creating connections.
-     * @param interval
-     *            The interval between keepalive pings.
-     * @param timeout
-     *            The heart-beat timeout after which a connection will be marked
-     *            as failed.
-     * @param unit
-     *            The time unit for the interval between keepalive pings.
-     * @return The new heart-beat connection factory.
-     * @throws IllegalArgumentException
-     *             If {@code interval} was negative.
-     * @throws NullPointerException
-     *             If {@code factory} or {@code unit} was {@code null}.
-     */
-    public static ConnectionFactory newHeartBeatConnectionFactory(final ConnectionFactory factory,
-            final long interval, final long timeout, final TimeUnit unit) {
-        return new HeartBeatConnectionFactory(factory, interval, timeout, unit, null, null);
-    }
-
-    /**
-     * Creates a new heart-beat connection factory which will create connections
-     * using the provided connection factory and periodically ping any created
-     * connections using the specified search request in order to detect that
-     * they are still alive.
-     *
-     * @param factory
-     *            The connection factory to use for creating connections.
-     * @param interval
-     *            The interval between keepalive pings.
-     * @param timeout
-     *            The heart-beat timeout after which a connection will be marked
-     *            as failed.
-     * @param unit
-     *            The time unit for the interval between keepalive pings.
-     * @param heartBeat
-     *            The search request to use for keepalive pings.
-     * @return The new heart-beat connection factory.
-     * @throws IllegalArgumentException
-     *             If {@code interval} was negative.
-     * @throws NullPointerException
-     *             If {@code factory}, {@code unit}, or {@code heartBeat} was
-     *             {@code null}.
-     */
-    public static ConnectionFactory newHeartBeatConnectionFactory(final ConnectionFactory factory,
-            final long interval, final long timeout, final TimeUnit unit,
-            final SearchRequest heartBeat) {
-        return new HeartBeatConnectionFactory(factory, interval, timeout, unit, heartBeat, null);
-    }
-
-    /**
-     * Creates a new heart-beat connection factory which will create connections
-     * using the provided connection factory and periodically ping any created
-     * connections using the specified search request in order to detect that
-     * they are still alive.
-     *
-     * @param factory
-     *            The connection factory to use for creating connections.
-     * @param interval
-     *            The interval between keepalive pings.
-     * @param timeout
-     *            The heart-beat timeout after which a connection will be marked
-     *            as failed.
-     * @param unit
-     *            The time unit for the interval between keepalive pings.
-     * @param heartBeat
-     *            The search request to use for keepalive pings.
-     * @param scheduler
-     *            The scheduler which should for periodically sending keepalive
-     *            pings.
-     * @return The new heart-beat connection factory.
-     * @throws IllegalArgumentException
-     *             If {@code interval} was negative.
-     * @throws NullPointerException
-     *             If {@code factory}, {@code unit}, or {@code heartBeat} was
-     *             {@code null}.
-     */
-    public static ConnectionFactory newHeartBeatConnectionFactory(final ConnectionFactory factory,
-            final long interval, final long timeout, final TimeUnit unit,
-            final SearchRequest heartBeat, final ScheduledExecutorService scheduler) {
-        return new HeartBeatConnectionFactory(factory, interval, timeout, unit, heartBeat,
-                scheduler);
-    }
-
-    /**
      * Creates a new internal client connection which will route requests to the
      * provided {@code RequestHandler}.
      * <p>
@@ -646,7 +509,7 @@
             public void close(org.forgerock.opendj.ldap.requests.UnbindRequest request,
                     String reason) {
                 // Do nothing.
-            };
+            }
         };
     }
 
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/HeartBeatConnectionFactory.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/HeartBeatConnectionFactory.java
deleted file mode 100644
index 61fc7a2..0000000
--- a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/HeartBeatConnectionFactory.java
+++ /dev/null
@@ -1,994 +0,0 @@
-/*
- * 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 2009-2010 Sun Microsystems, Inc.
- *      Portions Copyright 2011-2015 ForgeRock AS.
- */
-package org.forgerock.opendj.ldap;
-
-
-
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Queue;
-import java.util.concurrent.ConcurrentLinkedQueue;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ScheduledFuture;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.concurrent.locks.AbstractQueuedSynchronizer;
-
-import org.forgerock.i18n.LocalizableMessage;
-import org.forgerock.i18n.slf4j.LocalizedLogger;
-import org.forgerock.opendj.ldap.requests.AbandonRequest;
-import org.forgerock.opendj.ldap.requests.AddRequest;
-import org.forgerock.opendj.ldap.requests.BindRequest;
-import org.forgerock.opendj.ldap.requests.CompareRequest;
-import org.forgerock.opendj.ldap.requests.DeleteRequest;
-import org.forgerock.opendj.ldap.requests.ExtendedRequest;
-import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
-import org.forgerock.opendj.ldap.requests.ModifyRequest;
-import org.forgerock.opendj.ldap.requests.Requests;
-import org.forgerock.opendj.ldap.requests.SearchRequest;
-import org.forgerock.opendj.ldap.requests.StartTLSExtendedRequest;
-import org.forgerock.opendj.ldap.requests.UnbindRequest;
-import org.forgerock.opendj.ldap.responses.BindResult;
-import org.forgerock.opendj.ldap.responses.CompareResult;
-import org.forgerock.opendj.ldap.responses.ExtendedResult;
-import org.forgerock.opendj.ldap.responses.Result;
-import org.forgerock.opendj.ldap.responses.SearchResultEntry;
-import org.forgerock.opendj.ldap.responses.SearchResultReference;
-import org.forgerock.opendj.ldap.spi.ConnectionState;
-import org.forgerock.opendj.ldap.spi.LdapPromiseImpl;
-import org.forgerock.util.Reject;
-import org.forgerock.util.AsyncFunction;
-import org.forgerock.util.promise.ExceptionHandler;
-import org.forgerock.util.promise.Promise;
-import org.forgerock.util.promise.PromiseImpl;
-import org.forgerock.util.promise.ResultHandler;
-
-import com.forgerock.opendj.util.ReferenceCountedObject;
-import org.forgerock.util.time.TimeService;
-
-import static org.forgerock.opendj.ldap.LdapException.*;
-import static org.forgerock.opendj.ldap.spi.LdapPromiseImpl.*;
-import static org.forgerock.opendj.ldap.spi.LdapPromises.*;
-import static org.forgerock.util.Utils.*;
-
-import static com.forgerock.opendj.ldap.CoreMessages.*;
-import static com.forgerock.opendj.util.StaticUtils.*;
-
-/**
- * An heart beat connection factory can be used to create connections that sends
- * a periodic search request to a Directory Server.
- * <p>
- * Before returning new connections to the application this factory will first
- * send an initial heart beat in order to determine that the remote server is
- * responsive. If the heart beat fails or times out then the connection is
- * closed immediately and an error returned to the client.
- * <p>
- * Once a connection has been established successfully (including the initial
- * heart beat), this factory will periodically send heart beats on the
- * connection based on the configured heart beat interval. If the heart beat
- * times out then the server is assumed to be down and an appropriate
- * {@link ConnectionException} generated and published to any registered
- * {@link ConnectionEventListener}s. Note however, that heart beats will only be
- * sent when the connection is determined to be reasonably idle: there is no
- * point in sending heart beats if the connection has recently received a
- * response. A connection is deemed to be idle if no response has been received
- * during a period equivalent to half the heart beat interval.
- * <p>
- * The LDAP protocol specifically precludes clients from performing operations
- * while bind or startTLS requests are being performed. Likewise, a bind or
- * startTLS request will cause active operations to be aborted. This factory
- * coordinates heart beats with bind or startTLS requests, ensuring that they
- * are not performed concurrently. Specifically, bind and startTLS requests are
- * queued up while a heart beat is pending, and heart beats are not sent at all
- * while there are pending bind or startTLS requests.
- */
-final class HeartBeatConnectionFactory implements ConnectionFactory {
-    /**
-     * A connection that sends heart beats and supports all operations.
-     */
-    private final class ConnectionImpl extends AbstractAsynchronousConnection implements ConnectionEventListener {
-
-        private class LdapPromiseImplWrapper<R> extends LdapPromiseImpl<R> {
-            protected LdapPromiseImplWrapper(final LdapPromise<R> wrappedPromise) {
-                super(new PromiseImpl<R, LdapException>() {
-                    @Override
-                    protected LdapException tryCancel(boolean mayInterruptIfRunning) {
-                        /*
-                         * FIXME: if the inner cancel succeeds then this promise will be
-                         * completed and we can never indicate that this cancel request
-                         * has succeeded.
-                         */
-                        wrappedPromise.cancel(mayInterruptIfRunning);
-                        return null;
-                    }
-                }, wrappedPromise.getRequestID());
-            }
-        }
-
-        /** The wrapped connection. */
-        private final Connection connection;
-
-        /**
-         * List of pending Bind or StartTLS requests which must be invoked once
-         * the current heart beat completes.
-         */
-        private final Queue<Runnable> pendingBindOrStartTLSRequests =
-                new ConcurrentLinkedQueue<>();
-
-        /**
-         * List of pending responses for all active operations. These will be
-         * signaled if no heart beat is detected within the permitted timeout period.
-         */
-        private final Queue<LdapResultHandler<?>> pendingResults = new ConcurrentLinkedQueue<>();
-
-        /** Internal connection state. */
-        private final ConnectionState state = new ConnectionState();
-
-        /** Coordinates heart-beats with Bind and StartTLS requests. */
-        private final Sync sync = new Sync();
-
-        /**
-         * Timestamp of last response received (any response, not just heart
-         * beats).
-         */
-        private volatile long lastResponseTimestamp = timeService.now(); // Assume valid at creation.
-
-        private ConnectionImpl(final Connection connection) {
-            this.connection = connection;
-            connection.addConnectionEventListener(this);
-        }
-
-        @Override
-        public LdapPromise<Void> abandonAsync(final AbandonRequest request) {
-            return connection.abandonAsync(request);
-        }
-
-        @Override
-        public LdapPromise<Result> addAsync(final AddRequest request,
-                final IntermediateResponseHandler intermediateResponseHandler) {
-            if (checkState()) {
-                return timestampPromise(connection.addAsync(request, intermediateResponseHandler));
-            } else {
-                return newConnectionErrorPromise();
-            }
-        }
-
-        @Override
-        public void addConnectionEventListener(final ConnectionEventListener listener) {
-            state.addConnectionEventListener(listener);
-        }
-
-        @Override
-        public LdapPromise<BindResult> bindAsync(final BindRequest request,
-                final IntermediateResponseHandler intermediateResponseHandler) {
-            if (checkState()) {
-                if (sync.tryLockShared()) {
-                    // Fast path
-                    return timestampBindOrStartTLSPromise(connection.bindAsync(request, intermediateResponseHandler));
-                } else {
-                    return enqueueBindOrStartTLSPromise(new AsyncFunction<Void, BindResult, LdapException>() {
-                        @Override
-                        public Promise<BindResult, LdapException> apply(Void value) throws LdapException {
-                            return timestampBindOrStartTLSPromise(connection.bindAsync(request,
-                                    intermediateResponseHandler));
-                        }
-                    });
-                }
-            } else {
-                return newConnectionErrorPromise();
-            }
-        }
-
-        @Override
-        public void close() {
-            handleConnectionClosed();
-            connection.close();
-        }
-
-        @Override
-        public void close(final UnbindRequest request, final String reason) {
-            handleConnectionClosed();
-            connection.close(request, reason);
-        }
-
-        @Override
-        public LdapPromise<CompareResult> compareAsync(final CompareRequest request,
-                final IntermediateResponseHandler intermediateResponseHandler) {
-            if (checkState()) {
-                return timestampPromise(connection.compareAsync(request, intermediateResponseHandler));
-            } else {
-                return newConnectionErrorPromise();
-            }
-        }
-
-        @Override
-        public LdapPromise<Result> deleteAsync(final DeleteRequest request,
-                final IntermediateResponseHandler intermediateResponseHandler) {
-            if (checkState()) {
-                return timestampPromise(connection.deleteAsync(request, intermediateResponseHandler));
-            } else {
-                return newConnectionErrorPromise();
-            }
-        }
-
-        @Override
-        public <R extends ExtendedResult> LdapPromise<R> extendedRequestAsync(final ExtendedRequest<R> request,
-                final IntermediateResponseHandler intermediateResponseHandler) {
-            if (checkState()) {
-                if (isStartTLSRequest(request)) {
-                    if (sync.tryLockShared()) {
-                        // Fast path
-                        return timestampBindOrStartTLSPromise(connection.extendedRequestAsync(request,
-                                intermediateResponseHandler));
-                    } else {
-                        return enqueueBindOrStartTLSPromise(new AsyncFunction<Void, R, LdapException>() {
-                            @Override
-                            public Promise<R, LdapException> apply(Void value) throws LdapException {
-                                return timestampBindOrStartTLSPromise(connection.extendedRequestAsync(
-                                        request, intermediateResponseHandler));
-                            }
-                        });
-                    }
-                } else {
-                    return timestampPromise(connection.extendedRequestAsync(request, intermediateResponseHandler));
-                }
-            } else {
-                return newConnectionErrorPromise();
-            }
-        }
-
-        @Override
-        public void handleConnectionClosed() {
-            if (state.notifyConnectionClosed()) {
-                failPendingResults(newLdapException(ResultCode.CLIENT_SIDE_USER_CANCELLED,
-                        HBCF_CONNECTION_CLOSED_BY_CLIENT.get()));
-                synchronized (validConnections) {
-                    connection.removeConnectionEventListener(this);
-                    validConnections.remove(this);
-                    if (validConnections.isEmpty()) {
-                        // This is the last active connection, so stop the heartbeat.
-                        heartBeatFuture.cancel(false);
-                    }
-                }
-                releaseScheduler();
-            }
-        }
-
-        @Override
-        public void handleConnectionError(final boolean isDisconnectNotification, final LdapException error) {
-            if (state.notifyConnectionError(isDisconnectNotification, error)) {
-                failPendingResults(error);
-            }
-        }
-
-        @Override
-        public void handleUnsolicitedNotification(final ExtendedResult notification) {
-            timestamp(notification);
-            state.notifyUnsolicitedNotification(notification);
-        }
-
-        @Override
-        public boolean isClosed() {
-            return state.isClosed();
-        }
-
-        @Override
-        public boolean isValid() {
-            return state.isValid() && connection.isValid();
-        }
-
-        @Override
-        public LdapPromise<Result> modifyAsync(final ModifyRequest request,
-                final IntermediateResponseHandler intermediateResponseHandler) {
-            if (checkState()) {
-                return timestampPromise(connection.modifyAsync(request, intermediateResponseHandler));
-            } else {
-                return newConnectionErrorPromise();
-            }
-        }
-
-        @Override
-        public LdapPromise<Result> modifyDNAsync(final ModifyDNRequest request,
-                final IntermediateResponseHandler intermediateResponseHandler) {
-            if (checkState()) {
-                return timestampPromise(connection.modifyDNAsync(request, intermediateResponseHandler));
-            } else {
-                return newConnectionErrorPromise();
-            }
-        }
-
-        @Override
-        public void removeConnectionEventListener(final ConnectionEventListener listener) {
-            state.removeConnectionEventListener(listener);
-        }
-
-        @Override
-        public LdapPromise<Result> searchAsync(final SearchRequest request,
-                final IntermediateResponseHandler intermediateResponseHandler,
-                final SearchResultHandler searchHandler) {
-            if (checkState()) {
-                final AtomicBoolean searchDone = new AtomicBoolean();
-                final SearchResultHandler entryHandler = new SearchResultHandler() {
-                    @Override
-                    public synchronized boolean handleEntry(SearchResultEntry entry) {
-                        if (!searchDone.get()) {
-                            timestamp(entry);
-                            if (searchHandler != null) {
-                                searchHandler.handleEntry(entry);
-                            }
-                        }
-                        return true;
-                    }
-
-                    @Override
-                    public synchronized boolean handleReference(SearchResultReference reference) {
-                        if (!searchDone.get()) {
-                            timestamp(reference);
-                            if (searchHandler != null) {
-                                searchHandler.handleReference(reference);
-                            }
-                        }
-                        return true;
-                    }
-                };
-
-                return timestampPromise(connection.searchAsync(request,
-                        intermediateResponseHandler, entryHandler).thenOnResultOrException(new Runnable() {
-                            @Override
-                            public void run() {
-                                searchDone.getAndSet(true);
-                            }
-                        }));
-            } else {
-                return newConnectionErrorPromise();
-            }
-        }
-
-        @Override
-        public String toString() {
-            final StringBuilder builder = new StringBuilder();
-            builder.append("HeartBeatConnection(");
-            builder.append(connection);
-            builder.append(')');
-            return builder.toString();
-        }
-
-        private void checkForHeartBeat() {
-            if (sync.isHeld()) {
-                /*
-                 * A heart beat or bind/startTLS is still in progress, but it
-                 * should have completed by now. Let's avoid aggressively
-                 * terminating the connection, because the heart beat may simply
-                 * have been delayed by a sudden surge of activity. Therefore,
-                 * only flag the connection as failed if no activity has been
-                 * seen on the connection since the heart beat was sent.
-                 */
-                final long currentTimeMillis = timeService.now();
-                if (lastResponseTimestamp < (currentTimeMillis - timeoutMS)) {
-                    logger.warn(LocalizableMessage.raw("No heartbeat detected for connection '%s'", connection));
-                    handleConnectionError(false, newHeartBeatTimeoutError());
-                }
-            }
-        }
-
-        private boolean checkState() {
-            return state.getConnectionError() == null;
-        }
-
-        private void failPendingResults(final LdapException error) {
-            /*
-             * Peek instead of pool because notification is responsible for
-             * removing the element from the queue.
-             */
-            LdapResultHandler<?> pendingResult;
-            while ((pendingResult = pendingResults.peek()) != null) {
-                pendingResult.handleException(error);
-            }
-        }
-
-        private void flushPendingBindOrStartTLSRequests() {
-            if (!pendingBindOrStartTLSRequests.isEmpty()) {
-                /*
-                 * The pending requests will acquire the shared lock, but we take
-                 * it here anyway to ensure that pending requests do not get blocked.
-                 */
-                if (sync.tryLockShared()) {
-                    try {
-                        Runnable pendingRequest;
-                        while ((pendingRequest = pendingBindOrStartTLSRequests.poll()) != null) {
-                            // Dispatch the waiting request. This will not block.
-                            pendingRequest.run();
-                        }
-                    } finally {
-                        sync.unlockShared();
-                    }
-                }
-            }
-        }
-
-        private boolean isStartTLSRequest(final ExtendedRequest<?> request) {
-            return request.getOID().equals(StartTLSExtendedRequest.OID);
-        }
-
-        private <R> LdapPromise<R> newConnectionErrorPromise() {
-            return newFailedLdapPromise(state.getConnectionError());
-        }
-
-        private void releaseBindOrStartTLSLock() {
-            sync.unlockShared();
-        }
-
-        private void releaseHeartBeatLock() {
-            sync.unlockExclusively();
-            flushPendingBindOrStartTLSRequests();
-        }
-
-        /**
-         * Sends a heart beat on this connection if required to do so.
-         *
-         * @return {@code true} if a heart beat was sent, otherwise {@code false}.
-         */
-        private boolean sendHeartBeat() {
-            /*
-             * Don't attempt to send a heart beat if the connection has already
-             * failed.
-             */
-            if (!state.isValid()) {
-                return false;
-            }
-
-            /*
-             * Only send the heart beat if the connection has been idle for some
-             * time.
-             */
-            final long currentTimeMillis = timeService.now();
-            if (currentTimeMillis < (lastResponseTimestamp + minDelayMS)) {
-                return false;
-            }
-
-            /*
-             * Don't send a heart beat if there is already a heart beat, bind,
-             * or startTLS in progress. Note that the bind/startTLS response
-             * will update the lastResponseTimestamp as if it were a heart beat.
-             */
-            if (sync.tryLockExclusively()) {
-                try {
-                    connection.searchAsync(heartBeatRequest, new SearchResultHandler() {
-                        @Override
-                        public boolean handleEntry(final SearchResultEntry entry) {
-                            timestamp(entry);
-                            return true;
-                        }
-
-                        @Override
-                        public boolean handleReference(final SearchResultReference reference) {
-                            timestamp(reference);
-                            return true;
-                        }
-                    }).thenOnResult(new org.forgerock.util.promise.ResultHandler<Result>() {
-                        @Override
-                        public void handleResult(Result result) {
-                            timestamp(result);
-                            releaseHeartBeatLock();
-                        }
-                    }).thenOnException(new ExceptionHandler<LdapException>() {
-                        @Override
-                        public void handleException(LdapException exception) {
-                            /*
-                             * Connection failure will be handled by connection
-                             * event listener. Ignore cancellation errors since
-                             * these indicate that the heart beat was aborted by
-                             * a client-side close.
-                             */
-                            if (!(exception instanceof CancelledResultException)) {
-                                /*
-                                 * Log at debug level to avoid polluting the
-                                 * logs with benign password policy related
-                                 * errors. See OPENDJ-1168 and OPENDJ-1167.
-                                 */
-                                logger.debug(LocalizableMessage.raw("Heartbeat failed for connection factory '%s'",
-                                        factory, exception));
-                                timestamp(exception);
-                            }
-                            releaseHeartBeatLock();
-                        }
-                    });
-                } catch (final IllegalStateException e) {
-                    /*
-                     * This may happen when we attempt to send the heart beat
-                     * just after the connection is closed but before we are
-                     * notified.
-                     */
-
-                    /*
-                     * Release the lock because we're never going to get a
-                     * response.
-                     */
-                    releaseHeartBeatLock();
-                }
-            }
-            /*
-             * Indicate that a the heartbeat should be checked even if a
-             * bind/startTLS is in progress, since these operations will
-             * effectively act as the heartbeat.
-             */
-            return true;
-        }
-
-        private <R> R timestamp(final R response) {
-            if (!(response instanceof ConnectionException)) {
-                lastResponseTimestamp = timeService.now();
-            }
-            return response;
-        }
-
-        private <R extends Result> LdapPromise<R> enqueueBindOrStartTLSPromise(
-                AsyncFunction<Void, R, LdapException> doRequest) {
-            /*
-             * A heart beat must be in progress so create a runnable task which
-             * will be executed when the heart beat completes.
-             */
-            final LdapPromiseImpl<Void> promise = newLdapPromiseImpl();
-            final LdapPromise<R> result = promise.thenAsync(doRequest);
-
-            // Enqueue and flush if the heart beat has completed in the mean
-            // time.
-            pendingBindOrStartTLSRequests.offer(new Runnable() {
-                @Override
-                public void run() {
-                    // FIXME: Handle cancel chaining.
-                    if (!result.isCancelled()) {
-                        sync.lockShared(); // Will not block.
-                        promise.handleResult((Void) null);
-                    }
-                }
-            });
-            flushPendingBindOrStartTLSRequests();
-            return result;
-        }
-
-        private <R extends Result> LdapPromise<R> timestampPromise(LdapPromise<R> wrappedPromise) {
-            final LdapPromiseImpl<R> outerPromise = new LdapPromiseImplWrapper<>(wrappedPromise);
-            pendingResults.add(outerPromise);
-            wrappedPromise.thenOnResult(new ResultHandler<R>() {
-                @Override
-                public void handleResult(R result) {
-                    outerPromise.handleResult(result);
-                    timestamp(result);
-                }
-            }).thenOnException(new ExceptionHandler<LdapException>() {
-                @Override
-                public void handleException(LdapException exception) {
-                    outerPromise.handleException(exception);
-                    timestamp(exception);
-                }
-            });
-            outerPromise.thenOnResultOrException(new Runnable() {
-                @Override
-                public void run() {
-                    pendingResults.remove(outerPromise);
-                }
-            });
-            if (!checkState()) {
-                outerPromise.handleException(state.getConnectionError());
-            }
-            return outerPromise;
-        }
-
-        private <R extends Result> LdapPromise<R> timestampBindOrStartTLSPromise(LdapPromise<R> wrappedPromise) {
-            return timestampPromise(wrappedPromise).thenOnResultOrException(new Runnable() {
-                @Override
-                public void run() {
-                    releaseBindOrStartTLSLock();
-                }
-            });
-        }
-    }
-
-    /**
-     * This synchronizer prevents Bind or StartTLS operations from being
-     * processed concurrently with heart-beats. This is required because the
-     * LDAP protocol specifically states that servers receiving a Bind operation
-     * should either wait for existing operations to complete or abandon them.
-     * The same presumably applies to StartTLS operations. Note that concurrent
-     * bind/StartTLS operations are not permitted.
-     * <p>
-     * This connection factory only coordinates Bind and StartTLS requests with
-     * heart-beats. It does not attempt to prevent or control attempts to send
-     * multiple concurrent Bind or StartTLS operations, etc.
-     * <p>
-     * This synchronizer can be thought of as cross between a read-write lock
-     * and a semaphore. Unlike a read-write lock there is no requirement that a
-     * thread releasing a lock must hold it. In addition, this synchronizer does
-     * not support reentrancy. A thread attempting to acquire exclusively more
-     * than once will deadlock, and a thread attempting to acquire shared more
-     * than once will succeed and be required to release an equivalent number of
-     * times.
-     * <p>
-     * The synchronizer has three states:
-     * <ul>
-     * <li>UNLOCKED(0) - the synchronizer may be acquired shared or exclusively
-     * <li>LOCKED_EXCLUSIVELY(-1) - the synchronizer is held exclusively and
-     * cannot be acquired shared or exclusively. An exclusive lock is held while
-     * a heart beat is in progress
-     * <li>LOCKED_SHARED(>0) - the synchronizer is held shared and cannot be
-     * acquired exclusively. N shared locks are held while N Bind or StartTLS
-     * operations are in progress.
-     * </ul>
-     */
-    private static final class Sync extends AbstractQueuedSynchronizer {
-        private static final int LOCKED_EXCLUSIVELY = -1;
-        /** Keep compiler quiet. */
-        private static final long serialVersionUID = -3590428415442668336L;
-
-        /** Lock states. Positive values indicate that the shared lock is taken. */
-        private static final int UNLOCKED = 0; // initial state
-
-        @Override
-        protected boolean isHeldExclusively() {
-            return getState() == LOCKED_EXCLUSIVELY;
-        }
-
-        boolean isHeld() {
-            return getState() != 0;
-        }
-
-        @Override
-        protected boolean tryAcquire(final int ignored) {
-            if (compareAndSetState(UNLOCKED, LOCKED_EXCLUSIVELY)) {
-                setExclusiveOwnerThread(Thread.currentThread());
-                return true;
-            }
-            return false;
-        }
-
-        @Override
-        protected int tryAcquireShared(final int readers) {
-            for (;;) {
-                final int state = getState();
-                if (state == LOCKED_EXCLUSIVELY) {
-                    return LOCKED_EXCLUSIVELY; // failed
-                }
-                final int newState = state + readers;
-                if (compareAndSetState(state, newState)) {
-                    return newState; // succeeded + more readers allowed
-                }
-            }
-        }
-
-        @Override
-        protected boolean tryRelease(final int ignored) {
-            if (getState() != LOCKED_EXCLUSIVELY) {
-                throw new IllegalMonitorStateException();
-            }
-            setExclusiveOwnerThread(null);
-            setState(UNLOCKED);
-            return true;
-        }
-
-        @Override
-        protected boolean tryReleaseShared(final int ignored) {
-            for (;;) {
-                final int state = getState();
-                if (state == UNLOCKED || state == LOCKED_EXCLUSIVELY) {
-                    throw new IllegalMonitorStateException();
-                }
-                final int newState = state - 1;
-                if (compareAndSetState(state, newState)) {
-                    /*
-                     * We could always return true here, but since there cannot
-                     * be waiting readers we can specialize for waiting writers.
-                     */
-                    return newState == UNLOCKED;
-                }
-            }
-        }
-
-        void lockShared() {
-            acquireShared(1);
-        }
-
-        boolean tryLockExclusively() {
-            return tryAcquire(0 /* unused */);
-        }
-
-        boolean tryLockShared() {
-            return tryAcquireShared(1) > 0;
-        }
-
-        void unlockExclusively() {
-            release(0 /* unused */);
-        }
-
-        void unlockShared() {
-            releaseShared(0 /* unused */);
-        }
-
-    }
-
-    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
-
-    /**
-     * Default heart beat which will target the root DSE but not return any
-     * results.
-     */
-    private static final SearchRequest DEFAULT_SEARCH = Requests.newSearchRequest("",
-            SearchScope.BASE_OBJECT, "(objectClass=*)", "1.1");
-
-    /**
-     * This is package private in order to allow unit tests to inject fake time
-     * stamps.
-     */
-    TimeService timeService = TimeService.SYSTEM;
-
-    /**
-     * Scheduled task which checks that all heart beats have been received
-     * within the timeout period.
-     */
-    private final Runnable checkHeartBeatRunnable = new Runnable() {
-        @Override
-        public void run() {
-            for (final ConnectionImpl connection : getValidConnections()) {
-                connection.checkForHeartBeat();
-            }
-        }
-    };
-
-    /**
-     * Underlying connection factory.
-     */
-    private final ConnectionFactory factory;
-
-    /**
-     * The heartbeat scheduled future - which may be null if heartbeats are not
-     * being sent (no valid connections).
-     */
-    private ScheduledFuture<?> heartBeatFuture;
-
-    /**
-     * The heartbeat search request.
-     */
-    private final SearchRequest heartBeatRequest;
-
-    /**
-     * The interval between successive heartbeats.
-     */
-    private final long interval;
-    private final TimeUnit intervalUnit;
-
-    /**
-     * Flag which indicates whether this factory has been closed.
-     */
-    private final AtomicBoolean isClosed = new AtomicBoolean();
-
-    /**
-     * The minimum amount of time the connection should remain idle (no
-     * responses) before starting to send heartbeats.
-     */
-    private final long minDelayMS;
-
-    /**
-     * Prevents the scheduler being released when there are remaining references
-     * (this factory or any connections). It is initially set to 1 because this
-     * factory has a reference.
-     */
-    private final AtomicInteger referenceCount = new AtomicInteger(1);
-
-    /**
-     * The heartbeat scheduler.
-     */
-    private final ReferenceCountedObject<ScheduledExecutorService>.Reference scheduler;
-
-
-    /**
-     * Scheduled task which sends heart beats for all valid idle connections.
-     */
-    private final Runnable sendHeartBeatRunnable = new Runnable() {
-        @Override
-        public void run() {
-            boolean heartBeatSent = false;
-            for (final ConnectionImpl connection : getValidConnections()) {
-                heartBeatSent |= connection.sendHeartBeat();
-            }
-            if (heartBeatSent) {
-                scheduler.get().schedule(checkHeartBeatRunnable, timeoutMS, TimeUnit.MILLISECONDS);
-            }
-        }
-    };
-
-    /**
-     * The heartbeat timeout in milli-seconds. The connection will be marked as
-     * failed if no heartbeat response is received within the timeout.
-     */
-    private final long timeoutMS;
-
-    /** List of valid connections to which heartbeats will be sent. */
-    private final List<ConnectionImpl> validConnections = new LinkedList<>();
-
-    HeartBeatConnectionFactory(final ConnectionFactory factory, final long interval,
-            final long timeout, final TimeUnit unit, final SearchRequest heartBeat,
-            final ScheduledExecutorService scheduler) {
-        Reject.ifNull(factory, unit);
-        Reject.ifFalse(interval >= 0, "negative interval");
-        Reject.ifFalse(timeout >= 0, "negative timeout");
-
-        this.heartBeatRequest = heartBeat != null ? heartBeat : DEFAULT_SEARCH;
-        this.interval = interval;
-        this.intervalUnit = unit;
-        this.factory = factory;
-        this.scheduler = DEFAULT_SCHEDULER.acquireIfNull(scheduler);
-        this.timeoutMS = unit.toMillis(timeout);
-        this.minDelayMS = unit.toMillis(interval) / 2;
-    }
-
-    @Override
-    public void close() {
-        if (isClosed.compareAndSet(false, true)) {
-            synchronized (validConnections) {
-                if (!validConnections.isEmpty()) {
-                    logger.debug(LocalizableMessage.raw(
-                            "HeartbeatConnectionFactory '%s' is closing while %d active connections remain",
-                            this, validConnections.size()));
-                }
-            }
-            releaseScheduler();
-            factory.close();
-        }
-    }
-
-    private void releaseScheduler() {
-        if (referenceCount.decrementAndGet() == 0) {
-            scheduler.release();
-        }
-    }
-
-    private void acquireScheduler() {
-        /*
-         * If the factory is not closed then we need to prevent the scheduler
-         * from being released while the connection attempt is in progress.
-         */
-        referenceCount.incrementAndGet();
-        if (isClosed.get()) {
-            releaseScheduler();
-            throw new IllegalStateException("Attempted to get a connection after factory close");
-        }
-    }
-
-    @Override
-    public Connection getConnection() throws LdapException {
-        /*
-         * Immediately send a heart beat in order to determine if the connected
-         * server is responding.
-         */
-        acquireScheduler(); // Protect scheduler.
-        Connection connection = null;
-        try {
-            connection = factory.getConnection();
-            connection.searchAsync(heartBeatRequest, null).getOrThrow(timeoutMS, TimeUnit.MILLISECONDS);
-            return registerConnection(new ConnectionImpl(connection));
-        } catch (final Exception e) {
-            closeSilently(connection);
-            releaseScheduler();
-            throw adaptHeartBeatError(e);
-        }
-    }
-
-    @Override
-    public Promise<Connection, LdapException> getConnectionAsync() {
-        acquireScheduler(); // Protect scheduler.
-
-        final AtomicReference<Connection> connectionHolder = new AtomicReference<>();
-        final PromiseImpl<Connection, LdapException> promise = PromiseImpl.create();
-
-        // Request a connection and return the promise representing the heartbeat.
-        factory.getConnectionAsync().thenAsync(new AsyncFunction<Connection, Result, LdapException>() {
-            @Override
-            public Promise<Result, LdapException> apply(final Connection connection) {
-                // Save the connection for later once the heart beat completes.
-                connectionHolder.set(connection);
-                scheduler.get().schedule(new Runnable() {
-                    @Override
-                    public void run() {
-                        if (promise.tryHandleException(newHeartBeatTimeoutError())) {
-                            closeSilently(connectionHolder.get());
-                            releaseScheduler();
-                        }
-                    }
-                }, timeoutMS, TimeUnit.MILLISECONDS);
-                return connection.searchAsync(heartBeatRequest, null);
-            }
-        }).thenOnResult(new ResultHandler<Result>() {
-            @Override
-            public void handleResult(Result result) {
-                final Connection connection = connectionHolder.get();
-                final ConnectionImpl connectionImpl = new ConnectionImpl(connection);
-                if (!promise.tryHandleResult(registerConnection(connectionImpl))) {
-                    connectionImpl.close();
-                }
-            }
-        }).thenOnException(new ExceptionHandler<LdapException>() {
-            @Override
-            public void handleException(LdapException exception) {
-                if (promise.tryHandleException(adaptHeartBeatError(exception))) {
-                    closeSilently(connectionHolder.get());
-                    releaseScheduler();
-                }
-            }
-        });
-
-        return promise;
-    }
-
-    @Override
-    public String toString() {
-        final StringBuilder builder = new StringBuilder();
-        builder.append("HeartBeatConnectionFactory(");
-        builder.append(factory);
-        builder.append(')');
-        return builder.toString();
-    }
-
-    private Connection registerConnection(final ConnectionImpl heartBeatConnection) {
-        synchronized (validConnections) {
-            if (validConnections.isEmpty()) {
-                /* This is the first active connection, so start the heart beat. */
-                heartBeatFuture =
-                        scheduler.get().scheduleWithFixedDelay(sendHeartBeatRunnable, 0, interval, intervalUnit);
-            }
-            validConnections.add(heartBeatConnection);
-            return heartBeatConnection;
-        }
-    }
-
-    private LdapException adaptHeartBeatError(final Exception error) {
-        if (error instanceof ConnectionException) {
-            return (LdapException) error;
-        } else if (error instanceof TimeoutResultException || error instanceof TimeoutException) {
-            return newHeartBeatTimeoutError();
-        } else if (error instanceof InterruptedException) {
-            return newLdapException(ResultCode.CLIENT_SIDE_USER_CANCELLED, error);
-        } else {
-            return newLdapException(ResultCode.CLIENT_SIDE_SERVER_DOWN, HBCF_HEARTBEAT_FAILED.get(), error);
-        }
-    }
-
-    private ConnectionImpl[] getValidConnections() {
-        final ConnectionImpl[] tmp;
-        synchronized (validConnections) {
-            tmp = validConnections.toArray(new ConnectionImpl[0]);
-        }
-        return tmp;
-    }
-
-    private LdapException newHeartBeatTimeoutError() {
-        return newLdapException(ResultCode.CLIENT_SIDE_SERVER_DOWN, HBCF_HEARTBEAT_TIMEOUT
-                .get(timeoutMS));
-    }
-}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/LDAPConnectionFactory.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/LDAPConnectionFactory.java
index 22246d1..d8ddbcc 100644
--- a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/LDAPConnectionFactory.java
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/LDAPConnectionFactory.java
@@ -27,142 +27,494 @@
 
 package org.forgerock.opendj.ldap;
 
-import static com.forgerock.opendj.util.StaticUtils.getProvider;
+import static com.forgerock.opendj.ldap.CoreMessages.HBCF_CONNECTION_CLOSED_BY_CLIENT;
+import static com.forgerock.opendj.ldap.CoreMessages.HBCF_HEARTBEAT_FAILED;
+import static com.forgerock.opendj.ldap.CoreMessages.HBCF_HEARTBEAT_TIMEOUT;
+import static com.forgerock.opendj.ldap.CoreMessages.LDAP_CONNECTION_CONNECT_TIMEOUT;
+import static com.forgerock.opendj.util.StaticUtils.DEFAULT_SCHEDULER;
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.forgerock.opendj.ldap.LdapException.newLdapException;
+import static org.forgerock.opendj.ldap.requests.Requests.newSearchRequest;
+import static org.forgerock.opendj.ldap.requests.Requests.newStartTLSExtendedRequest;
+import static org.forgerock.opendj.ldap.requests.Requests.unmodifiableSearchRequest;
+import static org.forgerock.opendj.ldap.responses.Responses.newBindResult;
+import static org.forgerock.opendj.ldap.responses.Responses.newGenericExtendedResult;
+import static org.forgerock.opendj.ldap.responses.Responses.newResult;
+import static org.forgerock.opendj.ldap.spi.LdapPromiseImpl.newLdapPromiseImpl;
+import static org.forgerock.opendj.ldap.spi.LdapPromises.newFailedLdapPromise;
+import static org.forgerock.util.Utils.closeSilently;
+import static org.forgerock.util.promise.Promises.newResultPromise;
 
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.locks.AbstractQueuedSynchronizer;
+
+import javax.net.ssl.SSLContext;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.i18n.slf4j.LocalizedLogger;
+import org.forgerock.opendj.ldap.requests.AbandonRequest;
+import org.forgerock.opendj.ldap.requests.AddRequest;
+import org.forgerock.opendj.ldap.requests.BindRequest;
+import org.forgerock.opendj.ldap.requests.CompareRequest;
+import org.forgerock.opendj.ldap.requests.DeleteRequest;
+import org.forgerock.opendj.ldap.requests.ExtendedRequest;
+import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.requests.StartTLSExtendedRequest;
+import org.forgerock.opendj.ldap.requests.UnbindRequest;
+import org.forgerock.opendj.ldap.responses.BindResult;
+import org.forgerock.opendj.ldap.responses.CompareResult;
+import org.forgerock.opendj.ldap.responses.ExtendedResult;
+import org.forgerock.opendj.ldap.responses.Result;
+import org.forgerock.opendj.ldap.responses.SearchResultEntry;
+import org.forgerock.opendj.ldap.responses.SearchResultReference;
+import org.forgerock.opendj.ldap.spi.ConnectionState;
 import org.forgerock.opendj.ldap.spi.LDAPConnectionFactoryImpl;
+import org.forgerock.opendj.ldap.spi.LDAPConnectionImpl;
+import org.forgerock.opendj.ldap.spi.LdapPromiseImpl;
 import org.forgerock.opendj.ldap.spi.TransportProvider;
+import org.forgerock.util.AsyncFunction;
+import org.forgerock.util.Function;
 import org.forgerock.util.Option;
 import org.forgerock.util.Options;
 import org.forgerock.util.Reject;
+import org.forgerock.util.promise.ExceptionHandler;
 import org.forgerock.util.promise.Promise;
+import org.forgerock.util.promise.PromiseImpl;
+import org.forgerock.util.promise.ResultHandler;
+import org.forgerock.util.time.Duration;
+import org.forgerock.util.time.TimeService;
 
-import javax.net.ssl.SSLContext;
-import java.util.Collections;
-import java.util.List;
+import com.forgerock.opendj.util.ReferenceCountedObject;
 
 /**
- * A factory class which can be used to obtain connections to an LDAP Directory
- * Server.
+ * A factory class which can be used to obtain connections to an LDAP Directory Server. A connection attempt comprises
+ * of the following steps:
+ * <ul>
+ * <li>first of all a TCP connection to the remote LDAP server is obtained. The attempt will fail if a connection is
+ *     not obtained within the configured {@link #CONNECT_TIMEOUT connect timeout}
+ * <li>if LDAPS (not StartTLS) is requested then an SSL handshake is performed. LDAPS is enabled by specifying the
+ *     {@link #SSL_CONTEXT} option along with {@link #SSL_USE_STARTTLS} set to {@code false}
+ * <li>if StartTLS is requested then a StartTLS request is sent and then an SSL handshake performed once the response
+ *     has been received. StartTLS is enabled by specifying the {@link #SSL_CONTEXT} option along with
+ *     {@link #SSL_USE_STARTTLS} set to {@code true}
+ * <li>an initial authentication request is sent if the {@link #AUTHN_BIND_REQUEST} option is specified
+ * <li>if heart-beat support is enabled via the {@link #HEARTBEAT_ENABLED} option, and none of steps 2-4 were performed,
+ *     then an initial heart-beat is sent in order to determine whether the directory service is available.
+ * <li>the connect attempt will fail if it does not complete within the configured connection timeout. If the SSL
+ *     handshake, StartTLS request, initial bind request, or initial heart-beat fail for any reason then the connection
+ *     attempt will be deemed to have failed and an appropriate error returned.
+ * </ul>
+ * Once a connection has been established heart-beats will be sent periodically on the connection based on the
+ * configured heart-beat interval. If the heart-beat times out then the server is assumed to be down and an appropriate
+ * {@link ConnectionException} generated and published to any registered {@link ConnectionEventListener}s. Note
+ * however, that heart-beats will only be sent when the connection is determined to be reasonably idle: there is no
+ * point in sending heart-beats if the connection has recently received a response. A connection is deemed to be idle
+ * if no response has been received during a period equivalent to half the heart-beat interval.
+ * <p>
+ * The LDAP protocol specifically precludes clients from performing operations while bind or startTLS requests are being
+ * performed. Likewise, a bind or startTLS request will cause active operations to be aborted. This factory coordinates
+ * heart-beats with bind or startTLS requests, ensuring that they are not performed concurrently. Specifically, bind and
+ * startTLS requests are queued up while a heart-beat is pending, and heart-beats are not sent at all while there are
+ * pending bind or startTLS requests.
  */
 public final class LDAPConnectionFactory extends CommonLDAPOptions implements ConnectionFactory {
+    /**
+     * Configures the connection factory to return pre-authenticated connections using the specified {@link
+     * BindRequest}. The connections returned by the connection factory will support all operations with the exception
+     * of Bind requests. Attempts to perform a Bind will result in an {@code UnsupportedOperationException}.
+     * <p>
+     * If the Bind request fails for some reason (e.g. invalid credentials), then the connection attempt will fail and
+     * an {@link LdapException} will be thrown.
+     */
+    public static final Option<BindRequest> AUTHN_BIND_REQUEST = Option.of(BindRequest.class, null);
+
+    /**
+     * Specifies the connect timeout spcified. If a connection is not established within the timeout period (incl. SSL
+     * negotiation, initial bind request, and/or heart-beat), then a {@link TimeoutResultException} error result will be
+     * returned.
+     * <p>
+     * The default operation timeout is 10 seconds and may be configured using the {@code
+     * org.forgerock.opendj.io.connectTimeout} property. A timeout setting of 0 causes the OS connect timeout to be
+     * used.
+     */
+    public static final Option<Duration> CONNECT_TIMEOUT =
+            Option.withDefault(new Duration((long) getIntProperty("org.forgerock.opendj.io.connectTimeout", 10000),
+                                            TimeUnit.MILLISECONDS));
+
+    /**
+     * Configures the connection factory to periodically send "heart-beat" or "keep-alive" requests to the Directory
+     * Server. This feature allows client applications to proactively detect network problems or unresponsive
+     * servers. In addition, frequent heartbeat requests may also prevent load-balancers or Directory Servers from
+     * closing otherwise idle connections.
+     * <p>
+     * Before returning new connections to the application the factory will first send an initial heart-beat request in
+     * order to determine that the remote server is responsive. If the heart-beat request fails or is too slow to
+     * respond then the connection is closed immediately and an error returned to the client.
+     * <p>
+     * Once a connection has been established successfully (including the initial heart-beat request), the connection
+     * factory will periodically send heart-beat requests on the connection based on the configured heart-beat interval.
+     * If the Directory Server is too slow to respond to the heart-beat then the server is assumed to be down and an
+     * appropriate {@link ConnectionException} generated and published to any registered
+     * {@link ConnectionEventListener}s. Note however, that heart-beat requests will only be sent when the connection
+     * is determined to be reasonably idle: there is no point in sending heart-beats if the connection has recently
+     * received a response. A connection is deemed to be idle if no response has been received during a period
+     * equivalent to half the heart-beat interval.
+     * <p>
+     * The LDAP protocol specifically precludes clients from performing operations while bind or startTLS requests are
+     * being performed. Likewise, a bind or startTLS request will cause active operations to be aborted. The LDAP
+     * connection factory coordinates heart-beats with bind or startTLS requests, ensuring that they are not performed
+     * concurrently. Specifically, bind and startTLS requests are queued up while a heart-beat is pending, and
+     * heart-beats are not sent at all while there are pending bind or startTLS requests.
+     */
+    public static final Option<Boolean> HEARTBEAT_ENABLED = Option.withDefault(false);
+
+    /**
+     * Specifies the time between successive heart-beat requests (default interval is 10 seconds). Heart-beats will only
+     * be sent if {@link #HEARTBEAT_ENABLED} is set to {@code true}.
+     *
+     * @see #HEARTBEAT_ENABLED
+     */
+    public static final Option<Duration> HEARTBEAT_INTERVAL = Option.withDefault(new Duration(10L, SECONDS));
+
+    /**
+     * Specifies the scheduler which will be used for periodically sending heart-beat requests. A system-wide scheduler
+     * will be used by default. Heart-beats will only be sent if {@link #HEARTBEAT_ENABLED} is set to {@code true}.
+     *
+     * @see #HEARTBEAT_ENABLED
+     */
+    public static final Option<ScheduledExecutorService> HEARTBEAT_SCHEDULER =
+            Option.of(ScheduledExecutorService.class, null);
+
+    /**
+     * Specifies the timeout for heart-beat requests, after which the remote Directory Server will be deemed to be
+     * unavailable (default timeout is 3 seconds). Heart-beats will only be sent if {@link #HEARTBEAT_ENABLED} is set to
+     * {@code true}. If a {@link #REQUEST_TIMEOUT request timeout} is also set then the lower of the two will be used
+     * for sending heart-beats.
+     *
+     * @see #HEARTBEAT_ENABLED
+     */
+    public static final Option<Duration> HEARTBEAT_TIMEOUT = Option.withDefault(new Duration(3L, SECONDS));
+
+    /**
+     * Specifies the operation timeout. If a response is not received from the Directory Server within the timeout
+     * period, then the operation will be abandoned and a {@link TimeoutResultException} error result returned. A
+     * timeout setting of 0 disables operation timeout limits.
+     * <p>
+     * The default operation timeout is 0 (no timeout) and may be configured using the {@code
+     * org.forgerock.opendj.io.requestTimeout} property or the deprecated {@code org.forgerock.opendj.io.timeout}
+     * property.
+     */
+    public static final Option<Duration> REQUEST_TIMEOUT =
+            Option.withDefault(new Duration((long) getIntProperty("org.forgerock.opendj.io.requestTimeout",
+                                                                  getIntProperty("org.forgerock.opendj.io.timeout", 0)),
+                                            TimeUnit.MILLISECONDS));
 
     /**
      * Specifies the SSL context which will be used when initiating connections with the Directory Server.
      * <p>
-     * By default no SSL context will be used, indicating that connections will not be secured.
-     * If an SSL context is set then connections will be secured using either SSL or StartTLS
-     * depending on {@link #USE_STARTTLS}.
+     * By default no SSL context will be used, indicating that connections will not be secured. If an SSL context is set
+     * then connections will be secured using either SSL or StartTLS depending on {@link #SSL_USE_STARTTLS}.
      */
     public static final Option<SSLContext> SSL_CONTEXT = Option.of(SSLContext.class, null);
+
+    /**
+     * Specifies the cipher suites enabled for secure connections with the Directory Server.
+     * <p>
+     * The suites must be supported by the SSLContext specified by option {@link #SSL_CONTEXT}. Only the suites listed
+     * in the parameter are enabled for use.
+     */
+    @SuppressWarnings({ "unchecked", "rawtypes" })
+    public static final Option<List<String>> SSL_ENABLED_CIPHER_SUITES =
+            (Option) Option.of(List.class, Collections.<String>emptyList());
+
+    /**
+     * Specifies the protocol versions enabled for secure connections with the Directory Server.
+     * <p>
+     * The protocols must be supported by the SSLContext specified by option {@link #SSL_CONTEXT}. Only the protocols
+     * listed in the parameter are enabled for use.
+     */
+    @SuppressWarnings({ "unchecked", "rawtypes" })
+    public static final Option<List<String>> SSL_ENABLED_PROTOCOLS =
+            (Option) Option.of(List.class, Collections.<String>emptyList());
+
     /**
      * Specifies whether SSL or StartTLS should be used for securing connections when an SSL context is specified.
      * <p>
      * By default SSL will be used in preference to StartTLS.
      */
-    public static final Option<Boolean> USE_STARTTLS = Option.withDefault(false);
-    /**
-     * Specifies the operation timeout in milliseconds. If a response is not
-     * received from the Directory Server within the timeout period, then the
-     * operation will be abandoned and a {@link TimeoutResultException} error
-     * result returned. A timeout setting of 0 disables operation timeout limits.
-     * <p>
-     * The default operation timeout is 0 (no timeout) and may be configured
-     * using the {@code org.forgerock.opendj.io.timeout} property.
-     */
-    public static final Option<Long> TIMEOUT_IN_MILLISECONDS = Option.of(Long.class,
-        (long) getIntProperty("org.forgerock.opendj.io.timeout", 0));
-    /**
-     * Specifies the connect timeout spcified in milliseconds. If a connection is not established
-     * within the timeout period, then a {@link TimeoutResultException} error result will be returned.
-     * <p>
-     * The default operation timeout is 10 seconds and may be configured using
-     * the {@code org.forgerock.opendj.io.connectTimeout} property.
-     * A timeout setting of 0 causes the OS connect timeout to be used.
-     */
-    public static final Option<Long> CONNECT_TIMEOUT_IN_MILLISECONDS = Option.of(Long.class,
-        (long) getIntProperty("org.forgerock.opendj.io.connectTimeout", 10000));
-    /**
-     * Specifies the cipher suites enabled for secure connections with the Directory Server.
-     * <p>
-     * The suites must be supported by the SSLContext specified by option {@link SSL_CONTEXT}.
-     * Only the suites listed in the parameter are enabled for use.
-     */
-    public static final Option<List<String>> ENABLED_CIPHER_SUITES = Option.withDefault(
-        Collections.<String>emptyList());
-    /**
-     * Specifies the protocol versions enabled for secure connections with the
-     * Directory Server.
-     * <p>
-     * The protocols must be supported by the SSLContext specified by option {@link SSL_CONTEXT}.
-     * Only the protocols listed in the parameter are enabled for use.
-     */
-    public static final Option<List<String>> ENABLED_PROTOCOLS =
-        Option.withDefault(Collections.<String>emptyList());
+    public static final Option<Boolean> SSL_USE_STARTTLS = Option.withDefault(false);
+
+    /** Default heart-beat which will target the root DSE but not return any results. */
+    private static final SearchRequest DEFAULT_HEARTBEAT =
+            unmodifiableSearchRequest(newSearchRequest("", SearchScope.BASE_OBJECT, "(objectClass=*)", "1.1"));
 
     /**
-     * We implement the factory using the pimpl idiom in order to avoid making
-     * too many implementation classes public.
+     * Specifies the parameters of the search request that will be used for heart-beats. The default heart-beat search
+     * request is a base object search against the root DSE requesting no attributes. Heart-beats will only be sent if
+     * {@link #HEARTBEAT_ENABLED} is set to {@code true}.
+     *
+     * @see #HEARTBEAT_ENABLED
      */
+    public static final Option<SearchRequest> HEARTBEAT_SEARCH_REQUEST =
+            Option.of(SearchRequest.class, DEFAULT_HEARTBEAT);
+
+    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
+
+    /** The overall timeout to use when establishing connections, including SSL, bind, and heart-beat. */
+    private final long connectTimeoutMS;
+
+    /**
+     * The minimum amount of time the connection should remain idle (no responses) before starting to send heartbeats.
+     */
+    private final long heartBeatDelayMS;
+
+    /** Indicates whether heartbeats should be performed. */
+    private final Boolean heartBeatEnabled;
+
+    /** The heartbeat search request. */
+    private final SearchRequest heartBeatRequest;
+
+    /**
+     * The heartbeat timeout in milli-seconds. The connection will be marked as failed if no heartbeat response is
+     * received within the timeout.
+     */
+    private final long heartBeatTimeoutMS;
+
+    /** The interval between successive heartbeats. */
+    private final long heartBeatintervalMS;
+
+    /** The factory responsible for handling the low-level network communication with the Directory Server. */
     private final LDAPConnectionFactoryImpl impl;
 
-    /**
-     * Transport provider that provides the implementation of this factory.
-     */
+    /** The optional bind request which will be used as the initial heartbeat if specified. */
+    private final BindRequest initialBindRequest;
+
+    /** Flag which indicates whether this factory has been closed. */
+    private final AtomicBoolean isClosed = new AtomicBoolean();
+
+    /** A copy of the original options. This is only useful for debugging. */
+    private final Options options;
+
+    /** Transport provider that provides the implementation of this factory. */
     private final TransportProvider provider;
 
     /**
-     * Creates a new LDAP connection factory which can be used to create LDAP
-     * connections to the Directory Server at the provided host and port
-     * number.
+     * Prevents the scheduler being released when there are remaining references (this factory or any connections). It
+     * is initially set to 1 because this factory has a reference.
+     */
+    private final AtomicInteger referenceCount = new AtomicInteger(1);
+
+    /** The heartbeat scheduler. */
+    private final ReferenceCountedObject<ScheduledExecutorService>.Reference scheduler;
+
+    /** Non-null if SSL or StartTLS should be used when creating new connections. */
+    private final SSLContext sslContext;
+
+    /** The list of permitted SSL ciphers for SSL negotiation. */
+    private final List<String> sslEnabledCipherSuites;
+
+    /** The list of permitted SSL protocols for SSL negotiation. */
+    private final List<String> sslEnabledProtocols;
+
+    /** Indicates whether a StartTLS request should be sent immediately after connecting. */
+    private final boolean sslUseStartTLS;
+
+    /** List of valid connections to which heartbeats will be sent. */
+    private final List<ConnectionImpl> validConnections = new LinkedList<>();
+
+    /** This is package private in order to allow unit tests to inject fake time stamps. */
+    TimeService timeService = TimeService.SYSTEM;
+
+    /** Scheduled task which sends heart beats for all valid idle connections. */
+    private final Runnable sendHeartBeatRunnable = new Runnable() {
+        @Override
+        public void run() {
+            boolean heartBeatSent = false;
+            for (final ConnectionImpl connection : getValidConnections()) {
+                heartBeatSent |= connection.sendHeartBeat();
+            }
+            if (heartBeatSent) {
+                scheduler.get().schedule(checkHeartBeatRunnable, heartBeatTimeoutMS, TimeUnit.MILLISECONDS);
+            }
+        }
+    };
+
+    /** Scheduled task which checks that all heart beats have been received within the timeout period. */
+    private final Runnable checkHeartBeatRunnable = new Runnable() {
+        @Override
+        public void run() {
+            for (final ConnectionImpl connection : getValidConnections()) {
+                connection.checkForHeartBeat();
+            }
+        }
+    };
+
+    /** The heartbeat scheduled future - which may be null if heartbeats are not being sent (no valid connections). */
+    private ScheduledFuture<?> heartBeatFuture;
+
+    /**
+     * Creates a new LDAP connection factory which can be used to create LDAP connections to the Directory Server at the
+     * provided host and port number.
      *
-     * @param host The host name.
-     * @param port The port number.
-     * @throws NullPointerException      If {@code host} was {@code null}.
-     * @throws ProviderNotFoundException if no provider is available or if the
-     *                                   provider requested using options is not found.
+     * @param host
+     *         The host name.
+     * @param port
+     *         The port number.
+     * @throws NullPointerException
+     *         If {@code host} was {@code null}.
+     * @throws ProviderNotFoundException
+     *         if no provider is available or if the provider requested using options is not found.
      */
     public LDAPConnectionFactory(final String host, final int port) {
         this(host, port, Options.defaultOptions());
     }
 
     /**
-     * Creates a new LDAP connection factory which can be used to create LDAP
-     * connections to the Directory Server at the provided host and port
-     * number.
+     * Creates a new LDAP connection factory which can be used to create LDAP connections to the Directory Server at the
+     * provided host and port number.
      *
-     * @param host    The host name.
-     * @param port    The port number.
-     * @param options The LDAP options to use when creating connections.
-     * @throws NullPointerException      If {@code host} or {@code options} was {@code null}.
-     * @throws ProviderNotFoundException if no provider is available or if the
-     *                                   provider requested using options is not found.
+     * @param host
+     *         The host name.
+     * @param port
+     *         The port number.
+     * @param options
+     *         The LDAP options to use when creating connections.
+     * @throws NullPointerException
+     *         If {@code host} or {@code options} was {@code null}.
+     * @throws ProviderNotFoundException
+     *         if no provider is available or if the provider requested using options is not found.
      */
     public LDAPConnectionFactory(final String host, final int port, final Options options) {
         Reject.ifNull(host, options);
-        this.provider = getProvider(TransportProvider.class, options.get(TRANSPORT_PROVIDER),
-                options.get(PROVIDER_CLASS_LOADER));
+
+        this.connectTimeoutMS = options.get(CONNECT_TIMEOUT).to(TimeUnit.MILLISECONDS);
+        Reject.ifTrue(connectTimeoutMS < 0, "connect timeout must be >= 0");
+        Reject.ifTrue(options.get(REQUEST_TIMEOUT).getValue() < 0, "request timeout must be >= 0");
+
+        this.heartBeatEnabled = options.get(HEARTBEAT_ENABLED);
+        this.heartBeatintervalMS = options.get(HEARTBEAT_INTERVAL).to(TimeUnit.MILLISECONDS);
+        this.heartBeatTimeoutMS = options.get(HEARTBEAT_TIMEOUT).to(TimeUnit.MILLISECONDS);
+        this.heartBeatDelayMS = heartBeatintervalMS / 2;
+        this.heartBeatRequest = options.get(HEARTBEAT_SEARCH_REQUEST);
+        if (heartBeatEnabled) {
+            Reject.ifTrue(heartBeatintervalMS <= 0, "heart-beat interval must be positive");
+            Reject.ifTrue(heartBeatTimeoutMS <= 0, "heart-beat timeout must be positive");
+        }
+
+        this.provider = getTransportProvider(options);
+        this.scheduler = DEFAULT_SCHEDULER.acquireIfNull(options.get(HEARTBEAT_SCHEDULER));
         this.impl = provider.getLDAPConnectionFactory(host, port, options);
+        this.initialBindRequest = options.get(AUTHN_BIND_REQUEST);
+        this.sslContext = options.get(SSL_CONTEXT);
+        this.sslUseStartTLS = options.get(SSL_USE_STARTTLS);
+        this.sslEnabledProtocols = options.get(SSL_ENABLED_PROTOCOLS);
+        this.sslEnabledCipherSuites = options.get(SSL_ENABLED_CIPHER_SUITES);
+
+        this.options = Options.copyOf(options);
     }
 
     @Override
     public void close() {
-        impl.close();
-    }
-
-    @Override
-    public Promise<Connection, LdapException> getConnectionAsync() {
-        return impl.getConnectionAsync();
+        if (isClosed.compareAndSet(false, true)) {
+            synchronized (validConnections) {
+                if (!validConnections.isEmpty()) {
+                    logger.debug(LocalizableMessage.raw(
+                            "HeartbeatConnectionFactory '%s' is closing while %d active connections remain",
+                            this,
+                            validConnections.size()));
+                }
+            }
+            releaseScheduler();
+            impl.close();
+        }
     }
 
     @Override
     public Connection getConnection() throws LdapException {
-        return impl.getConnection();
+        return getConnectionAsync().getOrThrowUninterruptibly();
+    }
+
+    @Override
+    public Promise<Connection, LdapException> getConnectionAsync() {
+        acquireScheduler(); // Protect scheduler.
+
+        // Register the connect timeout timer.
+        final PromiseImpl<Connection, LdapException> promise = PromiseImpl.create();
+        final AtomicReference<LDAPConnectionImpl> connectionHolder = new AtomicReference<>();
+        if (connectTimeoutMS > 0) {
+            scheduler.get().schedule(new Runnable() {
+                @Override
+                public void run() {
+                    if (promise.tryHandleException(newConnectTimeoutError())) {
+                        closeSilently(connectionHolder.get());
+                        releaseScheduler();
+                    }
+                }
+            }, connectTimeoutMS, TimeUnit.MILLISECONDS);
+        }
+
+        // Now connect, negotiate SSL, etc.
+        impl.getConnectionAsync()
+            // Save the connection.
+            .then(new Function<LDAPConnectionImpl, LDAPConnectionImpl, LdapException>() {
+                @Override
+                public LDAPConnectionImpl apply(final LDAPConnectionImpl connection) throws LdapException {
+                    connectionHolder.set(connection);
+                    return connection;
+                }
+            })
+            .thenAsync(performStartTLSIfNeeded())
+            .thenAsync(performSSLHandShakeIfNeeded(connectionHolder))
+            .thenAsync(performInitialBindIfNeeded(connectionHolder))
+            .thenAsync(performInitialHeartBeatIfNeeded(connectionHolder))
+            .thenOnResult(new ResultHandler<Result>() {
+                @Override
+                public void handleResult(Result result) {
+                    final LDAPConnectionImpl connection = connectionHolder.get();
+                    final ConnectionImpl connectionImpl = new ConnectionImpl(connection);
+                    if (!promise.tryHandleResult(registerConnection(connectionImpl))) {
+                        connectionImpl.close();
+                    }
+                }
+            })
+            .thenOnException(new ExceptionHandler<LdapException>() {
+                @Override
+                public void handleException(final LdapException e) {
+                    final LdapException connectException;
+                    if (e instanceof ConnectionException || e instanceof AuthenticationException) {
+                        connectException = e;
+                    } else if (e instanceof TimeoutResultException) {
+                        connectException = newHeartBeatTimeoutError();
+                    } else {
+                        connectException = newLdapException(ResultCode.CLIENT_SIDE_SERVER_DOWN,
+                                                            HBCF_HEARTBEAT_FAILED.get(),
+                                                            e);
+                    }
+                    if (promise.tryHandleException(connectException)) {
+                        closeSilently(connectionHolder.get());
+                        releaseScheduler();
+                    }
+                }
+            });
+
+        return promise;
     }
 
     /**
-     * Returns the host name of the Directory Server. The returned host name is
-     * the same host name that was provided during construction and may be an IP
-     * address. More specifically, this method will not perform a reverse DNS
+     * Returns the host name of the Directory Server. The returned host name is the same host name that was provided
+     * during construction and may be an IP address. More specifically, this method will not perform a reverse DNS
      * lookup.
      *
      * @return The host name of the Directory Server.
@@ -181,8 +533,7 @@
     }
 
     /**
-     * Returns the name of the transport provider, which provides the implementation
-     * of this factory.
+     * Returns the name of the transport provider, which provides the implementation of this factory.
      *
      * @return The name of actual transport provider.
      */
@@ -192,6 +543,683 @@
 
     @Override
     public String toString() {
-        return impl.toString();
+        return "LDAPConnectionFactory(provider=`" + getProviderName() + ", host='" + getHostName() + "', port="
+                + getPort() + ", options=" + options + ")";
+    }
+
+    private void acquireScheduler() {
+        /*
+         * If the factory is not closed then we need to prevent the scheduler from being released while the
+         * connection attempt is in progress.
+         */
+        referenceCount.incrementAndGet();
+        if (isClosed.get()) {
+            releaseScheduler();
+            throw new IllegalStateException("Attempted to get a connection on closed factory");
+        }
+    }
+
+    private ConnectionImpl[] getValidConnections() {
+        synchronized (validConnections) {
+            return validConnections.toArray(new ConnectionImpl[validConnections.size()]);
+        }
+    }
+
+    private LdapException newConnectTimeoutError() {
+        final LocalizableMessage msg = LDAP_CONNECTION_CONNECT_TIMEOUT.get(impl.getSocketAddress(), connectTimeoutMS);
+        return newLdapException(ResultCode.CLIENT_SIDE_CONNECT_ERROR, msg.toString());
+    }
+
+    private LdapException newHeartBeatTimeoutError() {
+        return newLdapException(ResultCode.CLIENT_SIDE_SERVER_DOWN, HBCF_HEARTBEAT_TIMEOUT.get(heartBeatTimeoutMS));
+    }
+
+    private AsyncFunction<Void, BindResult, LdapException> performInitialBindIfNeeded(
+            final AtomicReference<LDAPConnectionImpl> connectionHolder) {
+        return new AsyncFunction<Void, BindResult, LdapException>() {
+            @Override
+            public Promise<BindResult, LdapException> apply(final Void ignored) throws LdapException {
+                if (initialBindRequest != null) {
+                    return connectionHolder.get().bindAsync(initialBindRequest, null);
+                } else {
+                    return newResultPromise(newBindResult(ResultCode.SUCCESS));
+                }
+            }
+        };
+    }
+
+    private AsyncFunction<BindResult, Result, LdapException> performInitialHeartBeatIfNeeded(
+            final AtomicReference<LDAPConnectionImpl> connectionHolder) {
+        return new AsyncFunction<BindResult, Result, LdapException>() {
+            @Override
+            public Promise<Result, LdapException> apply(final BindResult ignored) throws LdapException {
+                // Only send an initial heartbeat if we haven't already interacted with the server.
+                if (heartBeatEnabled && sslContext == null && initialBindRequest == null) {
+                    return connectionHolder.get().searchAsync(heartBeatRequest, null, null);
+                } else {
+                    return newResultPromise(newResult(ResultCode.SUCCESS));
+                }
+            }
+        };
+    }
+
+    private AsyncFunction<ExtendedResult, Void, LdapException> performSSLHandShakeIfNeeded(
+            final AtomicReference<LDAPConnectionImpl> connectionHolder) {
+        return new AsyncFunction<ExtendedResult, Void, LdapException>() {
+            @Override
+            public Promise<Void, LdapException> apply(final ExtendedResult extendedResult) throws LdapException {
+                if (sslContext != null && !sslUseStartTLS) {
+                    return connectionHolder.get().enableTLS(sslContext, sslEnabledProtocols, sslEnabledCipherSuites);
+                } else {
+                    return newResultPromise(null);
+                }
+            }
+        };
+    }
+
+    private AsyncFunction<LDAPConnectionImpl, ExtendedResult, LdapException> performStartTLSIfNeeded() {
+        return new AsyncFunction<LDAPConnectionImpl, ExtendedResult, LdapException>() {
+            @Override
+            public Promise<ExtendedResult, LdapException> apply(final LDAPConnectionImpl connection)
+                    throws LdapException {
+                if (sslContext != null && sslUseStartTLS) {
+                    final StartTLSExtendedRequest startTLS = newStartTLSExtendedRequest(sslContext)
+                            .addEnabledCipherSuite(sslEnabledCipherSuites)
+                            .addEnabledProtocol(sslEnabledProtocols);
+                    return connection.extendedRequestAsync(startTLS, null);
+                } else {
+                    return newResultPromise((ExtendedResult) newGenericExtendedResult(ResultCode.SUCCESS));
+                }
+            }
+        };
+    }
+
+    private Connection registerConnection(final ConnectionImpl heartBeatConnection) {
+        synchronized (validConnections) {
+            if (heartBeatEnabled && validConnections.isEmpty()) {
+                // This is the first active connection, so start the heart beat.
+                heartBeatFuture = scheduler.get()
+                                           .scheduleWithFixedDelay(sendHeartBeatRunnable,
+                                                                   0,
+                                                                   heartBeatintervalMS,
+                                                                   TimeUnit.MILLISECONDS);
+            }
+            validConnections.add(heartBeatConnection);
+        }
+        return heartBeatConnection;
+    }
+
+    private void releaseScheduler() {
+        if (referenceCount.decrementAndGet() == 0) {
+            scheduler.release();
+        }
+    }
+
+    /**
+     * This synchronizer prevents Bind or StartTLS operations from being processed concurrently with heart-beats. This
+     * is required because the LDAP protocol specifically states that servers receiving a Bind operation should either
+     * wait for existing operations to complete or abandon them. The same presumably applies to StartTLS operations.
+     * Note that concurrent bind/StartTLS operations are not permitted.
+     * <p>
+     * This connection factory only coordinates Bind and StartTLS requests with heart-beats. It does not attempt to
+     * prevent or control attempts to send multiple concurrent Bind or StartTLS operations, etc.
+     * <p>
+     * This synchronizer can be thought of as cross between a read-write lock and a semaphore. Unlike a read-write lock
+     * there is no requirement that a thread releasing a lock must hold it. In addition, this synchronizer does not
+     * support reentrancy. A thread attempting to acquire exclusively more than once will deadlock, and a thread
+     * attempting to acquire shared more than once will succeed and be required to release an equivalent number of
+     * times.
+     * <p>
+     * The synchronizer has three states:
+     * <ul>
+     *     <li> UNLOCKED(0) - the synchronizer may be acquired shared or exclusively
+     *     <li> LOCKED_EXCLUSIVELY(-1) - the synchronizer is held exclusively and cannot be acquired shared or
+     *          exclusively. An exclusive lock is held while a heart beat is in progress
+     *     <li> LOCKED_SHARED(>0) - the synchronizer is held shared and cannot be acquired exclusively. N shared locks
+     *          are held while N Bind or StartTLS operations are in progress.
+     * </ul>
+     */
+    private static final class Sync extends AbstractQueuedSynchronizer {
+        /** Lock states. Positive values indicate that the shared lock is taken. */
+        private static final int LOCKED_EXCLUSIVELY = -1;
+        private static final int UNLOCKED = 0; // initial state
+
+        /** Keep compiler quiet. */
+        private static final long serialVersionUID = -3590428415442668336L;
+
+        boolean isHeld() {
+            return getState() != 0;
+        }
+
+        void lockShared() {
+            acquireShared(1);
+        }
+
+        boolean tryLockExclusively() {
+            return tryAcquire(0 /* unused */);
+        }
+
+        boolean tryLockShared() {
+            return tryAcquireShared(1) > 0;
+        }
+
+        void unlockExclusively() {
+            release(0 /* unused */);
+        }
+
+        void unlockShared() {
+            releaseShared(0 /* unused */);
+        }
+
+        @Override
+        protected boolean isHeldExclusively() {
+            return getState() == LOCKED_EXCLUSIVELY;
+        }
+
+        @Override
+        protected boolean tryAcquire(final int ignored) {
+            if (compareAndSetState(UNLOCKED, LOCKED_EXCLUSIVELY)) {
+                setExclusiveOwnerThread(Thread.currentThread());
+                return true;
+            }
+            return false;
+        }
+
+        @Override
+        protected int tryAcquireShared(final int readers) {
+            for (;;) {
+                final int state = getState();
+                if (state == LOCKED_EXCLUSIVELY) {
+                    return LOCKED_EXCLUSIVELY; // failed
+                }
+                final int newState = state + readers;
+                if (compareAndSetState(state, newState)) {
+                    return newState; // succeeded + more readers allowed
+                }
+            }
+        }
+
+        @Override
+        protected boolean tryRelease(final int ignored) {
+            if (getState() != LOCKED_EXCLUSIVELY) {
+                throw new IllegalMonitorStateException();
+            }
+            setExclusiveOwnerThread(null);
+            setState(UNLOCKED);
+            return true;
+        }
+
+        @Override
+        protected boolean tryReleaseShared(final int ignored) {
+            for (;;) {
+                final int state = getState();
+                if (state == UNLOCKED || state == LOCKED_EXCLUSIVELY) {
+                    throw new IllegalMonitorStateException();
+                }
+                final int newState = state - 1;
+                if (compareAndSetState(state, newState)) {
+                    /*
+                     * We could always return true here, but since there cannot be waiting readers we can specialize
+                     * for waiting writers.
+                     */
+                    return newState == UNLOCKED;
+                }
+            }
+        }
+
+    }
+
+    /** A connection that sends heart beats and supports all operations. */
+    private final class ConnectionImpl extends AbstractAsynchronousConnection implements ConnectionEventListener {
+        /** The wrapped connection. */
+        private final LDAPConnectionImpl connectionImpl;
+
+        /** List of pending Bind or StartTLS requests which must be invoked once the current heart beat completes. */
+        private final Queue<Runnable> pendingBindOrStartTLSRequests = new ConcurrentLinkedQueue<>();
+
+        /**
+         * List of pending responses for all active operations. These will be signaled if no heart beat is detected
+         * within the permitted timeout period.
+         */
+        private final Queue<LdapResultHandler<?>> pendingResults = new ConcurrentLinkedQueue<>();
+
+        /** Internal connection state. */
+        private final ConnectionState state = new ConnectionState();
+
+        /** Coordinates heart-beats with Bind and StartTLS requests. */
+        private final Sync sync = new Sync();
+
+        /** Timestamp of last response received (any response, not just heart beats). */
+        private volatile long lastResponseTimestamp = timeService.now();
+
+        private ConnectionImpl(final LDAPConnectionImpl connectionImpl) {
+            this.connectionImpl = connectionImpl;
+            connectionImpl.addConnectionEventListener(this);
+        }
+
+        @Override
+        public LdapPromise<Void> abandonAsync(final AbandonRequest request) {
+            return connectionImpl.abandonAsync(request);
+        }
+
+        @Override
+        public LdapPromise<Result> addAsync(
+                final AddRequest request, final IntermediateResponseHandler intermediateResponseHandler) {
+            if (hasConnectionErrorOccurred()) {
+                return newConnectionErrorPromise();
+            }
+            return timestampPromise(connectionImpl.addAsync(request, intermediateResponseHandler));
+        }
+
+        @Override
+        public void addConnectionEventListener(final ConnectionEventListener listener) {
+            state.addConnectionEventListener(listener);
+        }
+
+        @Override
+        public LdapPromise<BindResult> bindAsync(
+                final BindRequest request, final IntermediateResponseHandler intermediateResponseHandler) {
+            if (hasConnectionErrorOccurred()) {
+                return newConnectionErrorPromise();
+            }
+            if (sync.tryLockShared()) {
+                // Fast path
+                return timestampBindOrStartTLSPromise(connectionImpl.bindAsync(request, intermediateResponseHandler));
+            }
+            return enqueueBindOrStartTLSPromise(new AsyncFunction<Void, BindResult, LdapException>() {
+                @Override
+                public Promise<BindResult, LdapException> apply(Void value) throws LdapException {
+                    return timestampBindOrStartTLSPromise(connectionImpl.bindAsync(request,
+                                                                                   intermediateResponseHandler));
+                }
+            });
+        }
+
+        @Override
+        public void close() {
+            handleConnectionClosed();
+            connectionImpl.close();
+        }
+
+        @Override
+        public void close(final UnbindRequest request, final String reason) {
+            handleConnectionClosed();
+            connectionImpl.close(request, reason);
+        }
+
+        @Override
+        public LdapPromise<CompareResult> compareAsync(
+                final CompareRequest request, final IntermediateResponseHandler intermediateResponseHandler) {
+            if (hasConnectionErrorOccurred()) {
+                return newConnectionErrorPromise();
+            }
+            return timestampPromise(connectionImpl.compareAsync(request, intermediateResponseHandler));
+        }
+
+        @Override
+        public LdapPromise<Result> deleteAsync(
+                final DeleteRequest request, final IntermediateResponseHandler intermediateResponseHandler) {
+            if (hasConnectionErrorOccurred()) {
+                return newConnectionErrorPromise();
+            }
+            return timestampPromise(connectionImpl.deleteAsync(request, intermediateResponseHandler));
+        }
+
+        @Override
+        public <R extends ExtendedResult> LdapPromise<R> extendedRequestAsync(
+                final ExtendedRequest<R> request, final IntermediateResponseHandler intermediateResponseHandler) {
+            if (hasConnectionErrorOccurred()) {
+                return newConnectionErrorPromise();
+            }
+            if (!isStartTLSRequest(request)) {
+                return timestampPromise(connectionImpl.extendedRequestAsync(request, intermediateResponseHandler));
+            }
+            if (sync.tryLockShared()) {
+                // Fast path
+                return timestampBindOrStartTLSPromise(
+                        connectionImpl.extendedRequestAsync(request, intermediateResponseHandler));
+            }
+            return enqueueBindOrStartTLSPromise(new AsyncFunction<Void, R, LdapException>() {
+                @Override
+                public Promise<R, LdapException> apply(Void value) throws LdapException {
+                    return timestampBindOrStartTLSPromise(
+                            connectionImpl.extendedRequestAsync(request, intermediateResponseHandler));
+                }
+            });
+        }
+
+        @Override
+        public void handleConnectionClosed() {
+            if (state.notifyConnectionClosed()) {
+                failPendingResults(newLdapException(ResultCode.CLIENT_SIDE_USER_CANCELLED,
+                                                    HBCF_CONNECTION_CLOSED_BY_CLIENT.get()));
+                synchronized (validConnections) {
+                    connectionImpl.removeConnectionEventListener(this);
+                    validConnections.remove(this);
+                    if (heartBeatEnabled && validConnections.isEmpty()) {
+                        // This is the last active connection, so stop the heartbeat.
+                        heartBeatFuture.cancel(false);
+                    }
+                }
+                releaseScheduler();
+            }
+        }
+
+        @Override
+        public void handleConnectionError(final boolean isDisconnectNotification, final LdapException error) {
+            if (state.notifyConnectionError(isDisconnectNotification, error)) {
+                failPendingResults(error);
+            }
+        }
+
+        @Override
+        public void handleUnsolicitedNotification(final ExtendedResult notification) {
+            timestamp(notification);
+            state.notifyUnsolicitedNotification(notification);
+        }
+
+        @Override
+        public boolean isClosed() {
+            return state.isClosed();
+        }
+
+        @Override
+        public boolean isValid() {
+            return state.isValid() && connectionImpl.isValid();
+        }
+
+        @Override
+        public LdapPromise<Result> modifyAsync(
+                final ModifyRequest request, final IntermediateResponseHandler intermediateResponseHandler) {
+            if (hasConnectionErrorOccurred()) {
+                return newConnectionErrorPromise();
+            }
+            return timestampPromise(connectionImpl.modifyAsync(request, intermediateResponseHandler));
+        }
+
+        @Override
+        public LdapPromise<Result> modifyDNAsync(
+                final ModifyDNRequest request, final IntermediateResponseHandler intermediateResponseHandler) {
+            if (hasConnectionErrorOccurred()) {
+                return newConnectionErrorPromise();
+            }
+            return timestampPromise(connectionImpl.modifyDNAsync(request, intermediateResponseHandler));
+        }
+
+        @Override
+        public void removeConnectionEventListener(final ConnectionEventListener listener) {
+            state.removeConnectionEventListener(listener);
+        }
+
+        @Override
+        public LdapPromise<Result> searchAsync(
+                final SearchRequest request,
+                final IntermediateResponseHandler intermediateResponseHandler,
+                final SearchResultHandler searchHandler) {
+            if (hasConnectionErrorOccurred()) {
+                return newConnectionErrorPromise();
+            }
+
+            final AtomicBoolean searchDone = new AtomicBoolean();
+            final SearchResultHandler entryHandler = new SearchResultHandler() {
+                @Override
+                public synchronized boolean handleEntry(SearchResultEntry entry) {
+                    if (!searchDone.get()) {
+                        timestamp(entry);
+                        if (searchHandler != null) {
+                            searchHandler.handleEntry(entry);
+                        }
+                    }
+                    return true;
+                }
+
+                @Override
+                public synchronized boolean handleReference(SearchResultReference reference) {
+                    if (!searchDone.get()) {
+                        timestamp(reference);
+                        if (searchHandler != null) {
+                            searchHandler.handleReference(reference);
+                        }
+                    }
+                    return true;
+                }
+            };
+            return timestampPromise(connectionImpl.searchAsync(request, intermediateResponseHandler, entryHandler)
+                                                  .thenOnResultOrException(new Runnable() {
+                                                      @Override
+                                                      public void run() {
+                                                          searchDone.getAndSet(true);
+                                                      }
+                                                  }));
+        }
+
+        @Override
+        public String toString() {
+            return connectionImpl.toString();
+        }
+
+        private void checkForHeartBeat() {
+            if (sync.isHeld()) {
+                /*
+                 * A heart beat or bind/startTLS is still in progress, but it should have completed by now. Let's
+                 * avoid aggressively terminating the connection, because the heart beat may simply have been delayed
+                 * by a sudden surge of activity. Therefore, only flag the connection as failed if no activity has been
+                 * seen on the connection since the heart beat was sent.
+                 */
+                final long currentTimeMillis = timeService.now();
+                if (lastResponseTimestamp < (currentTimeMillis - heartBeatTimeoutMS)) {
+                    logger.warn(LocalizableMessage.raw("No heartbeat detected for connection '%s'", connectionImpl));
+                    handleConnectionError(false, newHeartBeatTimeoutError());
+                }
+            }
+        }
+
+        private boolean hasConnectionErrorOccurred() {
+            return state.getConnectionError() != null;
+        }
+
+        private <R extends Result> LdapPromise<R> enqueueBindOrStartTLSPromise(
+                AsyncFunction<Void, R, LdapException> doRequest) {
+            /*
+             * A heart beat must be in progress so create a runnable task which will be executed when the heart beat
+             * completes.
+             */
+            final LdapPromiseImpl<Void> promise = newLdapPromiseImpl();
+            final LdapPromise<R> result = promise.thenAsync(doRequest);
+
+            // Enqueue and flush if the heart beat has completed in the mean time.
+            pendingBindOrStartTLSRequests.offer(new Runnable() {
+                @Override
+                public void run() {
+                    // FIXME: Handle cancel chaining.
+                    if (!result.isCancelled()) {
+                        sync.lockShared(); // Will not block.
+                        promise.handleResult(null);
+                    }
+                }
+            });
+            flushPendingBindOrStartTLSRequests();
+            return result;
+        }
+
+        private void failPendingResults(final LdapException error) {
+            // Peek instead of pool because notification is responsible for removing the element from the queue.
+            LdapResultHandler<?> pendingResult;
+            while ((pendingResult = pendingResults.peek()) != null) {
+                pendingResult.handleException(error);
+            }
+        }
+
+        private void flushPendingBindOrStartTLSRequests() {
+            if (!pendingBindOrStartTLSRequests.isEmpty()) {
+                /*
+                 * The pending requests will acquire the shared lock, but we take it here anyway to ensure that
+                 * pending requests do not get blocked.
+                 */
+                if (sync.tryLockShared()) {
+                    try {
+                        Runnable pendingRequest;
+                        while ((pendingRequest = pendingBindOrStartTLSRequests.poll()) != null) {
+                            // Dispatch the waiting request. This will not block.
+                            pendingRequest.run();
+                        }
+                    } finally {
+                        sync.unlockShared();
+                    }
+                }
+            }
+        }
+
+        private boolean isStartTLSRequest(final ExtendedRequest<?> request) {
+            return request.getOID().equals(StartTLSExtendedRequest.OID);
+        }
+
+        private <R> LdapPromise<R> newConnectionErrorPromise() {
+            return newFailedLdapPromise(state.getConnectionError());
+        }
+
+        private void releaseBindOrStartTLSLock() {
+            sync.unlockShared();
+        }
+
+        private void releaseHeartBeatLock() {
+            sync.unlockExclusively();
+            flushPendingBindOrStartTLSRequests();
+        }
+
+        /**
+         * Sends a heart beat on this connection if required to do so.
+         *
+         * @return {@code true} if a heart beat was sent, otherwise {@code false}.
+         */
+        private boolean sendHeartBeat() {
+            // Don't attempt to send a heart beat if the connection has already failed.
+            if (!state.isValid()) {
+                return false;
+            }
+
+            // Only send the heart beat if the connection has been idle for some time.
+            final long currentTimeMillis = timeService.now();
+            if (currentTimeMillis < (lastResponseTimestamp + heartBeatDelayMS)) {
+                return false;
+            }
+
+            /* Don't send a heart beat if there is already a heart beat, bind, or startTLS in progress. Note that the
+             * bind/startTLS response will update the lastResponseTimestamp as if it were a heart beat.
+             */
+            if (sync.tryLockExclusively()) {
+                try {
+                    connectionImpl.searchAsync(heartBeatRequest, null, new SearchResultHandler() {
+                        @Override
+                        public boolean handleEntry(final SearchResultEntry entry) {
+                            timestamp(entry);
+                            return true;
+                        }
+
+                        @Override
+                        public boolean handleReference(final SearchResultReference reference) {
+                            timestamp(reference);
+                            return true;
+                        }
+                    }).thenOnResult(new org.forgerock.util.promise.ResultHandler<Result>() {
+                        @Override
+                        public void handleResult(Result result) {
+                            timestamp(result);
+                            releaseHeartBeatLock();
+                        }
+                    }).thenOnException(new ExceptionHandler<LdapException>() {
+                        @Override
+                        public void handleException(LdapException exception) {
+                            /*
+                             * Connection failure will be handled by connection event listener. Ignore cancellation
+                             * errors since these indicate that the heart beat was aborted by a client-side close.
+                             */
+                            if (!(exception instanceof CancelledResultException)) {
+                                /*
+                                 * Log at debug level to avoid polluting the logs with benign password policy related
+                                 * errors. See OPENDJ-1168 and OPENDJ-1167.
+                                 */
+                                logger.debug(LocalizableMessage.raw("Heartbeat failed for connection factory '%s'",
+                                                                    LDAPConnectionFactory.this,
+                                                                    exception));
+                                timestamp(exception);
+                            }
+                            releaseHeartBeatLock();
+                        }
+                    });
+                } catch (final IllegalStateException e) {
+                    /*
+                     * This may happen when we attempt to send the heart beat just after the connection is closed but
+                     * before we are notified. Release the lock because we're never going to get a response.
+                     */
+                    releaseHeartBeatLock();
+                }
+            }
+            /*
+             * Indicate that a the heartbeat should be checked even if a bind/startTLS is in progress, since these
+             * operations will effectively act as the heartbeat.
+             */
+            return true;
+        }
+
+        private <R> R timestamp(final R response) {
+            if (!(response instanceof ConnectionException)) {
+                lastResponseTimestamp = timeService.now();
+            }
+            return response;
+        }
+
+        private <R extends Result> LdapPromise<R> timestampBindOrStartTLSPromise(LdapPromise<R> wrappedPromise) {
+            return timestampPromise(wrappedPromise).thenOnResultOrException(new Runnable() {
+                @Override
+                public void run() {
+                    releaseBindOrStartTLSLock();
+                }
+            });
+        }
+
+        private <R extends Result> LdapPromise<R> timestampPromise(LdapPromise<R> wrappedPromise) {
+            final LdapPromiseImpl<R> outerPromise = new LdapPromiseImplWrapper<>(wrappedPromise);
+            pendingResults.add(outerPromise);
+            wrappedPromise.thenOnResult(new ResultHandler<R>() {
+                @Override
+                public void handleResult(R result) {
+                    outerPromise.handleResult(result);
+                    timestamp(result);
+                }
+            }).thenOnException(new ExceptionHandler<LdapException>() {
+                @Override
+                public void handleException(LdapException exception) {
+                    outerPromise.handleException(exception);
+                    timestamp(exception);
+                }
+            });
+            outerPromise.thenOnResultOrException(new Runnable() {
+                @Override
+                public void run() {
+                    pendingResults.remove(outerPromise);
+                }
+            });
+            if (hasConnectionErrorOccurred()) {
+                outerPromise.handleException(state.getConnectionError());
+            }
+            return outerPromise;
+        }
+
+        private class LdapPromiseImplWrapper<R> extends LdapPromiseImpl<R> {
+            protected LdapPromiseImplWrapper(final LdapPromise<R> wrappedPromise) {
+                super(new PromiseImpl<R, LdapException>() {
+                    @Override
+                    protected LdapException tryCancel(boolean mayInterruptIfRunning) {
+                        /*
+                         * FIXME: if the inner cancel succeeds then this promise will be completed and we can never
+                         * indicate that this cancel request has succeeded.
+                         */
+                        wrappedPromise.cancel(mayInterruptIfRunning);
+                        return null;
+                    }
+                }, wrappedPromise.getRequestID());
+            }
+        }
     }
 }
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/LDAPListener.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/LDAPListener.java
index 1543985..061fc90 100644
--- a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/LDAPListener.java
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/LDAPListener.java
@@ -100,13 +100,14 @@
      * Specifies the maximum queue length for incoming connections requests. If a
      * connection request arrives when the queue is full, the connection is refused.
      */
-    public static final Option<Integer> BACKLOG = Option.withDefault(50);
+    public static final Option<Integer> CONNECT_MAX_BACKLOG = Option.withDefault(50);
+
     /**
      * Specifies the maximum request size in bytes for incoming LDAP requests.
      * If an incoming request exceeds the limit then the connection will be aborted by the listener.
      * Default value is 5MiB.
      */
-    public static final Option<Integer> MAX_REQUEST_SIZE_BYTES = Option.withDefault(5 * 1024 * 1024);
+    public static final Option<Integer> REQUEST_MAX_SIZE_IN_BYTES = Option.withDefault(5 * 1024 * 1024);
 
     /**
      * We implement the factory using the pimpl idiom in order have
@@ -160,10 +161,8 @@
             final ServerConnectionFactory<LDAPClientContext, Integer> factory,
             final Options options) throws IOException {
         Reject.ifNull(factory, options);
-        final InetSocketAddress address = new InetSocketAddress(port);
-        this.provider = getProvider(TransportProvider.class, options.get(TRANSPORT_PROVIDER),
-            options.get(PROVIDER_CLASS_LOADER));
-        this.impl = provider.getLDAPListener(address, factory, options);
+        this.provider = getTransportProvider(options);
+        this.impl = provider.getLDAPListener(new InetSocketAddress(port), factory, options);
     }
 
     /**
@@ -208,8 +207,7 @@
             final ServerConnectionFactory<LDAPClientContext, Integer> factory,
             final Options options) throws IOException {
         Reject.ifNull(address, factory, options);
-        this.provider = getProvider(TransportProvider.class, options.get(TRANSPORT_PROVIDER),
-            options.get(PROVIDER_CLASS_LOADER));
+        this.provider = getTransportProvider(options);
         this.impl = provider.getLDAPListener(address, factory, options);
     }
 
@@ -260,8 +258,7 @@
             final Options options) throws IOException {
         Reject.ifNull(host, factory, options);
         final InetSocketAddress address = new InetSocketAddress(host, port);
-        this.provider = getProvider(TransportProvider.class, options.get(TRANSPORT_PROVIDER),
-            options.get(PROVIDER_CLASS_LOADER));
+        this.provider = getTransportProvider(options);
         this.impl = provider.getLDAPListener(address, factory, options);
     }
 
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/StartTLSExtendedRequest.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/StartTLSExtendedRequest.java
index 860db12..2dd3b88 100644
--- a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/StartTLSExtendedRequest.java
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/StartTLSExtendedRequest.java
@@ -22,11 +22,12 @@
  *
  *
  *      Copyright 2010 Sun Microsystems, Inc.
- *      Portions copyright 2012 ForgeRock AS.
+ *      Portions copyright 2012-2015 ForgeRock AS.
  */
 
 package org.forgerock.opendj.ldap.requests;
 
+import java.util.Collection;
 import java.util.List;
 
 import javax.net.ssl.SSLContext;
@@ -100,6 +101,22 @@
     StartTLSExtendedRequest addEnabledCipherSuite(String... suites);
 
     /**
+     * Adds the cipher suites enabled for secure connections with the Directory
+     * Server. The suites must be supported by the SSLContext specified in
+     * {@link #setSSLContext(SSLContext)}. Following a successful call to this
+     * method, only the suites listed in the protocols parameter are enabled for
+     * use.
+     *
+     * @param suites
+     *            Names of all the suites to enable.
+     * @return A reference to this LDAP connection options.
+     * @throws UnsupportedOperationException
+     *             If this start TLS extended request does not permit the
+     *             enabled cipher suites to be set.
+     */
+    StartTLSExtendedRequest addEnabledCipherSuite(Collection<String> suites);
+
+    /**
      * Adds the protocol versions enabled for secure connections with the
      * Directory Server. The protocols must be supported by the SSLContext
      * specified in {@link #setSSLContext(SSLContext)}. Following a successful
@@ -115,6 +132,22 @@
      */
     StartTLSExtendedRequest addEnabledProtocol(String... protocols);
 
+    /**
+     * Adds the protocol versions enabled for secure connections with the
+     * Directory Server. The protocols must be supported by the SSLContext
+     * specified in {@link #setSSLContext(SSLContext)}. Following a successful
+     * call to this method, only the protocols listed in the protocols parameter
+     * are enabled for use.
+     *
+     * @param protocols
+     *            Names of all the protocols to enable.
+     * @return A reference to this LDAP connection options.
+     * @throws UnsupportedOperationException
+     *             If this start TLS extended request does not permit the
+     *             enabled protocols to be set.
+     */
+    StartTLSExtendedRequest addEnabledProtocol(Collection<String> protocols);
+
     @Override
     <C extends Control> C getControl(ControlDecoder<C> decoder, DecodeOptions options)
             throws DecodeException;
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/StartTLSExtendedRequestImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/StartTLSExtendedRequestImpl.java
index 64d5a7a..56c38ad 100644
--- a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/StartTLSExtendedRequestImpl.java
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/StartTLSExtendedRequestImpl.java
@@ -27,6 +27,8 @@
 
 package org.forgerock.opendj.ldap.requests;
 
+import java.util.Arrays;
+import java.util.Collection;
 import java.util.LinkedList;
 import java.util.List;
 
@@ -113,6 +115,11 @@
 
     @Override
     public StartTLSExtendedRequest addEnabledCipherSuite(final String... suites) {
+        return addEnabledCipherSuite(Arrays.asList(suites));
+    }
+
+    @Override
+    public StartTLSExtendedRequest addEnabledCipherSuite(final Collection<String> suites) {
         for (final String suite : suites) {
             this.enabledCipherSuites.add(Reject.checkNotNull(suite));
         }
@@ -121,6 +128,11 @@
 
     @Override
     public StartTLSExtendedRequest addEnabledProtocol(final String... protocols) {
+        return addEnabledProtocol(Arrays.asList(protocols));
+    }
+
+    @Override
+    public StartTLSExtendedRequest addEnabledProtocol(final Collection<String> protocols) {
         for (final String protocol : protocols) {
             this.enabledProtocols.add(Reject.checkNotNull(protocol));
         }
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableStartTLSExtendedRequestImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableStartTLSExtendedRequestImpl.java
index ca9f4ea..77f86c0 100644
--- a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableStartTLSExtendedRequestImpl.java
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableStartTLSExtendedRequestImpl.java
@@ -22,10 +22,12 @@
  *
  *
  *      Copyright 2010 Sun Microsystems, Inc.
+ *      Portions copyright 2015 ForgeRock AS.
  */
 
 package org.forgerock.opendj.ldap.requests;
 
+import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 
@@ -44,11 +46,21 @@
     }
 
     @Override
+    public StartTLSExtendedRequest addEnabledCipherSuite(final Collection<String> suites) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
     public StartTLSExtendedRequest addEnabledCipherSuite(final String... suites) {
         throw new UnsupportedOperationException();
     }
 
     @Override
+    public StartTLSExtendedRequest addEnabledProtocol(final Collection<String> protocols) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
     public StartTLSExtendedRequest addEnabledProtocol(final String... protocols) {
         throw new UnsupportedOperationException();
     }
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/BindResultLdapPromiseImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/BindResultLdapPromiseImpl.java
index be7c33d..ffdf50d 100644
--- a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/BindResultLdapPromiseImpl.java
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/BindResultLdapPromiseImpl.java
@@ -22,12 +22,11 @@
  *
  *
  *      Copyright 2009-2010 Sun Microsystems, Inc.
- *      Portions copyright 2011-2014 ForgeRock AS.
+ *      Portions copyright 2011-2015 ForgeRock AS.
  */
 
 package org.forgerock.opendj.ldap.spi;
 
-import org.forgerock.opendj.ldap.Connection;
 import org.forgerock.opendj.ldap.IntermediateResponseHandler;
 import org.forgerock.opendj.ldap.LdapException;
 import org.forgerock.opendj.ldap.ResultCode;
@@ -43,28 +42,16 @@
 public final class BindResultLdapPromiseImpl extends ResultLdapPromiseImpl<BindRequest, BindResult> {
     private final BindClient bindClient;
 
-    BindResultLdapPromiseImpl(final int requestID, final BindRequest request, final BindClient bindClient,
-            final IntermediateResponseHandler intermediateResponseHandler,
-            final Connection connection) {
-        super(new PromiseImpl<BindResult, LdapException>() {
-            protected LdapException tryCancel(boolean mayInterruptIfRunning) {
-                /*
-                 * No other operations can be performed while a bind is active.
-                 * Therefore it is not possible to cancel bind or requests,
-                 * since doing so will leave the connection in a state which
-                 * prevents other operations from being performed.
-                 */
-                return null;
-            }
-        }, requestID, request, intermediateResponseHandler, connection);
+    BindResultLdapPromiseImpl(
+            final PromiseImpl<BindResult, LdapException> impl,
+            final int requestID,
+            final BindRequest request,
+            final BindClient bindClient,
+            final IntermediateResponseHandler intermediateResponseHandler) {
+        super(impl, requestID, request, intermediateResponseHandler);
         this.bindClient = bindClient;
     }
 
-    @Override
-    public boolean isBindOrStartTLS() {
-        return true;
-    }
-
     /**
      * Returns the bind client.
      *
@@ -75,9 +62,12 @@
     }
 
     @Override
-    BindResult newErrorResult(final ResultCode resultCode, final String diagnosticMessage,
-            final Throwable cause) {
-        return Responses.newBindResult(resultCode).setDiagnosticMessage(diagnosticMessage)
-                .setCause(cause);
+    public boolean isBindOrStartTLS() {
+        return true;
+    }
+
+    @Override
+    BindResult newErrorResult(final ResultCode resultCode, final String diagnosticMessage, final Throwable cause) {
+        return Responses.newBindResult(resultCode).setDiagnosticMessage(diagnosticMessage).setCause(cause);
     }
 }
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/ExtendedResultLdapPromiseImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/ExtendedResultLdapPromiseImpl.java
index 4c3e6a2..f05d7f9 100644
--- a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/ExtendedResultLdapPromiseImpl.java
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/ExtendedResultLdapPromiseImpl.java
@@ -22,53 +22,50 @@
  *
  *
  *      Copyright 2009-2010 Sun Microsystems, Inc.
- *      Portions copyright 2011-2014 ForgeRock AS.
+ *      Portions copyright 2011-2015 ForgeRock AS.
  */
 
 package org.forgerock.opendj.ldap.spi;
 
-import org.forgerock.opendj.ldap.Connection;
 import org.forgerock.opendj.ldap.DecodeException;
 import org.forgerock.opendj.ldap.DecodeOptions;
 import org.forgerock.opendj.ldap.IntermediateResponseHandler;
 import org.forgerock.opendj.ldap.LdapException;
 import org.forgerock.opendj.ldap.ResultCode;
 import org.forgerock.opendj.ldap.requests.ExtendedRequest;
-import org.forgerock.opendj.ldap.requests.Requests;
 import org.forgerock.opendj.ldap.requests.StartTLSExtendedRequest;
 import org.forgerock.opendj.ldap.responses.ExtendedResult;
 import org.forgerock.util.promise.PromiseImpl;
 
-import static org.forgerock.opendj.ldap.LdapException.*;
-
 /**
  * Extended result promise implementation.
  *
  * @param <S>
- *            The type of result returned by this promise.
+ *         The type of result returned by this promise.
  */
 public final class ExtendedResultLdapPromiseImpl<S extends ExtendedResult> extends
         ResultLdapPromiseImpl<ExtendedRequest<S>, S> {
-    ExtendedResultLdapPromiseImpl(final int requestID, final ExtendedRequest<S> request,
-            final IntermediateResponseHandler intermediateResponseHandler,
-            final Connection connection) {
-        super(new PromiseImpl<S, LdapException>() {
-            @Override
-            protected LdapException tryCancel(boolean mayInterruptIfRunning) {
-                if (!StartTLSExtendedRequest.OID.equals(request.getOID())) {
-                    connection.abandonAsync(Requests.newAbandonRequest(requestID));
-                    return newLdapException(ResultCode.CLIENT_SIDE_USER_CANCELLED);
-                }
+    ExtendedResultLdapPromiseImpl(
+            final PromiseImpl<S, LdapException> impl,
+            final int requestID,
+            final ExtendedRequest<S> request,
+            final IntermediateResponseHandler intermediateResponseHandler) {
+        super(impl, requestID, request, intermediateResponseHandler);
+    }
 
-                /*
-                 * No other operations can be performed while a startTLS is active.
-                 * Therefore it is not possible to cancel startTLS requests,
-                 * since doing so will leave the connection in a state which
-                 * prevents other operations from being performed.
-                 */
-                return null;
-            }
-        }, requestID, request, intermediateResponseHandler, connection);
+    /**
+     * Decode an extended result.
+     *
+     * @param result
+     *         Extended result to decode.
+     * @param options
+     *         Decoding options.
+     * @return The decoded extended result.
+     * @throws DecodeException
+     *         If a problem occurs during decoding.
+     */
+    public S decodeResult(final ExtendedResult result, final DecodeOptions options) throws DecodeException {
+        return getRequest().getResultDecoder().decodeExtendedResult(result, options);
     }
 
     @Override
@@ -76,24 +73,8 @@
         return StartTLSExtendedRequest.OID.equals(getRequest().getOID());
     }
 
-    /**
-     * Decode an extended result.
-     *
-     * @param result
-     *            Extended result to decode.
-     * @param options
-     *            Decoding options.
-     * @return The decoded extended result.
-     * @throws DecodeException
-     *             If a problem occurs during decoding.
-     */
-    public S decodeResult(final ExtendedResult result, final DecodeOptions options) throws DecodeException {
-        return getRequest().getResultDecoder().decodeExtendedResult(result, options);
-    }
-
     @Override
-    S newErrorResult(final ResultCode resultCode, final String diagnosticMessage,
-            final Throwable cause) {
+    S newErrorResult(final ResultCode resultCode, final String diagnosticMessage, final Throwable cause) {
         return getRequest().getResultDecoder().newExtendedErrorResult(resultCode, "", diagnosticMessage);
     }
 }
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/LDAPConnectionFactoryImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/LDAPConnectionFactoryImpl.java
index c76cf3f..9222504 100644
--- a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/LDAPConnectionFactoryImpl.java
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/LDAPConnectionFactoryImpl.java
@@ -21,17 +21,18 @@
  * CDDL HEADER END
  *
  *
- *      Copyright 2013-2014 ForgeRock AS.
+ *      Copyright 2013-2015 ForgeRock AS.
  */
 package org.forgerock.opendj.ldap.spi;
 
+import java.io.Closeable;
 import java.net.InetSocketAddress;
 
-import org.forgerock.opendj.ldap.ConnectionFactory;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.util.promise.Promise;
 
 /**
- * Interface for all classes that actually implement
- * {@code LDAPConnectionFactory}.
+ * Interface for all classes that implement {@code LDAPConnectionFactory}.
  * <p>
  * An implementation class is provided by a {@code TransportProvider}.
  * <p>
@@ -40,7 +41,7 @@
  * {@code TransportProvider} is declared in the provider-configuration file
  * {@code META-INF/services/org.forgerock.opendj.ldap.spi.TransportProvider}.
  */
-public interface LDAPConnectionFactoryImpl extends ConnectionFactory {
+public interface LDAPConnectionFactoryImpl extends Closeable {
 
     /**
      * Returns the address used by the connections created by this factory.
@@ -57,11 +58,24 @@
     String getHostName();
 
     /**
-     * Returns the remote port number used by the connections created by this
-     * factory.
+     * Returns the remote port number used by the connections created by this factory.
      *
      * @return The remote port number used by the connections.
      */
     int getPort();
 
+    /**
+     * Releases any resources associated with this connection factory implementation.
+     */
+    @Override
+    void close();
+
+    /**
+     * Asynchronously obtains a connection to the Directory Server associated
+     * with this connection factory. The returned {@code Promise} can be used to
+     * retrieve the completed connection.
+     *
+     * @return A promise which can be used to retrieve the connection.
+     */
+    Promise<LDAPConnectionImpl, LdapException> getConnectionAsync();
 }
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/LDAPConnectionImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/LDAPConnectionImpl.java
new file mode 100644
index 0000000..63a641a
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/LDAPConnectionImpl.java
@@ -0,0 +1,324 @@
+/*
+ * 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.forgerock.opendj.ldap.spi;
+
+import java.io.Closeable;
+import java.util.List;
+
+import javax.net.ssl.SSLContext;
+
+import org.forgerock.opendj.ldap.ConnectionEventListener;
+import org.forgerock.opendj.ldap.IntermediateResponseHandler;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.LdapPromise;
+import org.forgerock.opendj.ldap.SearchResultHandler;
+import org.forgerock.opendj.ldap.requests.AbandonRequest;
+import org.forgerock.opendj.ldap.requests.AddRequest;
+import org.forgerock.opendj.ldap.requests.BindRequest;
+import org.forgerock.opendj.ldap.requests.CompareRequest;
+import org.forgerock.opendj.ldap.requests.DeleteRequest;
+import org.forgerock.opendj.ldap.requests.ExtendedRequest;
+import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
+import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.requests.UnbindRequest;
+import org.forgerock.opendj.ldap.responses.BindResult;
+import org.forgerock.opendj.ldap.responses.CompareResult;
+import org.forgerock.opendj.ldap.responses.ExtendedResult;
+import org.forgerock.opendj.ldap.responses.Result;
+import org.forgerock.util.promise.Promise;
+
+/**
+ * LDAP connection interface which implementations of {@link LDAPConnectionFactoryImpl} should implement.
+ */
+public interface LDAPConnectionImpl extends Closeable {
+
+    /**
+     * Abandons the unfinished operation identified in the provided abandon request.
+     *
+     * @param request
+     *         The request identifying the operation to be abandoned.
+     * @return A promise whose result is Void.
+     * @throws UnsupportedOperationException
+     *         If this connection does not support abandon operations.
+     * @throws IllegalStateException
+     *         If this connection has already been closed, i.e. if {@code isClosed() == true}.
+     * @throws NullPointerException
+     *         If {@code request} was {@code null}.
+     * @see org.forgerock.opendj.ldap.Connection#abandonAsync(AbandonRequest)
+     */
+    LdapPromise<Void> abandonAsync(AbandonRequest request);
+
+    /**
+     * Asynchronously adds an entry to the Directory Server using the provided add request.
+     *
+     * @param request
+     *         The add request.
+     * @param intermediateResponseHandler
+     *         An intermediate response handler which can be used to process any intermediate responses as they are
+     *         received, may be {@code null}.
+     * @return A promise representing the result of the operation.
+     * @throws UnsupportedOperationException
+     *         If this connection does not support add operations.
+     * @throws IllegalStateException
+     *         If this connection has already been closed, i.e. if {@code isClosed() == true}.
+     * @throws NullPointerException
+     *         If {@code request} was {@code null}.
+     * @see org.forgerock.opendj.ldap.Connection#addAsync(AddRequest, IntermediateResponseHandler)
+     */
+    LdapPromise<Result> addAsync(AddRequest request, IntermediateResponseHandler intermediateResponseHandler);
+
+    /**
+     * Registers the provided connection event listener so that it will be notified when this connection is closed by
+     * the application, receives an unsolicited notification, or experiences a fatal error.
+     *
+     * @param listener
+     *         The listener which wants to be notified when events occur on this connection.
+     * @throws IllegalStateException
+     *         If this connection has already been closed, i.e. if {@code isClosed() == true}.
+     * @throws NullPointerException
+     *         If the {@code listener} was {@code null}.
+     * @see org.forgerock.opendj.ldap.Connection#addConnectionEventListener(ConnectionEventListener)
+     */
+    void addConnectionEventListener(ConnectionEventListener listener);
+
+    /**
+     * Asynchronously authenticates to the Directory Server using the provided bind request.
+     *
+     * @param request
+     *         The bind request.
+     * @param intermediateResponseHandler
+     *         An intermediate response handler which can be used to process any intermediate responses as they are
+     *         received, may be {@code null}.
+     * @return A promise representing the result of the operation.
+     * @throws UnsupportedOperationException
+     *         If this connection does not support bind operations.
+     * @throws IllegalStateException
+     *         If this connection has already been closed, i.e. if {@code isClosed() == true}.
+     * @throws NullPointerException
+     *         If {@code request} was {@code null}.
+     * @see org.forgerock.opendj.ldap.Connection#bindAsync(BindRequest, IntermediateResponseHandler)
+     */
+    LdapPromise<BindResult> bindAsync(BindRequest request, IntermediateResponseHandler intermediateResponseHandler);
+
+    /**
+     * Releases any resources associated with this connection.
+     * <p/>
+     * Calling {@code close} on a connection that is already closed has no effect.
+     * @see org.forgerock.opendj.ldap.Connection#close()
+     */
+    @Override
+    void close();
+
+    /**
+     * Releases any resources associated with this connection.
+     * <p/>
+     * Calling {@code close} on a connection that is already closed has no effect.
+     *
+     * @param request
+     *         The unbind request to use in the case where a physical connection is closed.
+     * @param reason
+     *         A reason describing why the connection was closed.
+     * @throws NullPointerException
+     *         If {@code request} was {@code null}.
+     * @see org.forgerock.opendj.ldap.Connection#close(UnbindRequest, String)
+     */
+    void close(UnbindRequest request, String reason);
+
+    /**
+     * Asynchronously compares an entry in the Directory Server using the provided compare request.
+     *
+     * @param request
+     *         The compare request.
+     * @param intermediateResponseHandler
+     *         An intermediate response handler which can be used to process any intermediate responses as they are
+     *         received, may be {@code null}.
+     * @return A promise representing the result of the operation.
+     * @throws UnsupportedOperationException
+     *         If this connection does not support compare operations.
+     * @throws IllegalStateException
+     *         If this connection has already been closed, i.e. if {@code isClosed() == true}.
+     * @throws NullPointerException
+     *         If {@code request} was {@code null}.
+     * @see org.forgerock.opendj.ldap.Connection#compareAsync(CompareRequest, IntermediateResponseHandler)
+     */
+    LdapPromise<CompareResult> compareAsync(
+            CompareRequest request, IntermediateResponseHandler intermediateResponseHandler);
+
+    /**
+     * Asynchronously deletes an entry from the Directory Server using the provided delete request.
+     *
+     * @param request
+     *         The delete request.
+     * @param intermediateResponseHandler
+     *         An intermediate response handler which can be used to process any intermediate responses as they are
+     *         received, may be {@code null}.
+     * @return A promise representing the result of the operation.
+     * @throws UnsupportedOperationException
+     *         If this connection does not support delete operations.
+     * @throws IllegalStateException
+     *         If this connection has already been closed, i.e. if {@code isClosed() == true}.
+     * @throws NullPointerException
+     *         If {@code request} was {@code null}.
+     * @see org.forgerock.opendj.ldap.Connection#deleteAsync(DeleteRequest, IntermediateResponseHandler)
+     */
+    LdapPromise<Result> deleteAsync(DeleteRequest request, IntermediateResponseHandler intermediateResponseHandler);
+
+    /**
+     * Installs the TLS/SSL security layer on the underlying connection. The TLS/SSL security layer will be installed
+     * beneath any existing connection security layers and can only be installed at most once.
+     *
+     * @param sslContext
+     *         The {@code SSLContext} which should be used to secure the
+     * @param protocols
+     *         Names of all the protocols to enable or {@code null} to use the default protocols.
+     * @param suites
+     *         Names of all the suites to enable or {@code null} to use the default cipher suites.
+     * @return A promise which will complete once the SSL handshake has completed.
+     * @throws IllegalStateException
+     *         If the TLS/SSL security layer has already been installed.
+     */
+    Promise<Void, LdapException> enableTLS(SSLContext sslContext, List<String> protocols, List<String> suites);
+
+    /**
+     * Asynchronously performs the provided extended request in the Directory Server.
+     *
+     * @param <R>
+     *         The type of result returned by the extended request.
+     * @param request
+     *         The extended request.
+     * @param intermediateResponseHandler
+     *         An intermediate response handler which can be used to process any intermediate responses as they are
+     *         received, may be {@code null}.
+     * @return A promise representing the result of the operation.
+     * @throws UnsupportedOperationException
+     *         If this connection does not support extended operations.
+     * @throws IllegalStateException
+     *         If this connection has already been closed, i.e. if {@code isClosed() == true}.
+     * @throws NullPointerException
+     *         If {@code request} was {@code null}.
+     * @see org.forgerock.opendj.ldap.Connection#extendedRequestAsync(ExtendedRequest, IntermediateResponseHandler)
+     */
+    <R extends ExtendedResult> LdapPromise<R> extendedRequestAsync(
+            ExtendedRequest<R> request, IntermediateResponseHandler intermediateResponseHandler);
+
+    /**
+     * Indicates whether or not this connection has been explicitly closed by calling {@code close}. This method will
+     * not return {@code true} if a fatal error has occurred on the connection unless {@code close} has been called.
+     *
+     * @return {@code true} if this connection has been explicitly closed by calling {@code close}, or {@code false}
+     * otherwise.
+     * @see org.forgerock.opendj.ldap.Connection#isClosed()
+     */
+    boolean isClosed();
+
+    /**
+     * Returns {@code true} if this connection has not been closed and no fatal errors have been detected. This method
+     * is guaranteed to return {@code false} only when it is called after the method {@code close} has been called.
+     *
+     * @return {@code true} if this connection is valid, {@code false} otherwise.
+     * @see org.forgerock.opendj.ldap.Connection#isValid()
+     */
+    boolean isValid();
+
+    /**
+     * Asynchronously modifies an entry in the Directory Server using the provided modify request.
+     *
+     * @param request
+     *         The modify request.
+     * @param intermediateResponseHandler
+     *         An intermediate response handler which can be used to process any intermediate responses as they are
+     *         received, may be {@code null}.
+     * @return A promise representing the result of the operation.
+     * @throws UnsupportedOperationException
+     *         If this connection does not support modify operations.
+     * @throws IllegalStateException
+     *         If this connection has already been closed, i.e. if {@code isClosed() == true}.
+     * @throws NullPointerException
+     *         If {@code request} was {@code null}.
+     * @see org.forgerock.opendj.ldap.Connection#modifyAsync(ModifyRequest, IntermediateResponseHandler)
+     */
+    LdapPromise<Result> modifyAsync(ModifyRequest request, IntermediateResponseHandler intermediateResponseHandler);
+
+    /**
+     * Asynchronously renames an entry in the Directory Server using the provided modify DN request.
+     *
+     * @param request
+     *         The modify DN request.
+     * @param intermediateResponseHandler
+     *         An intermediate response handler which can be used to process any intermediate responses as they are
+     *         received, may be {@code null}.
+     * @return A promise representing the result of the operation.
+     * @throws UnsupportedOperationException
+     *         If this connection does not support modify DN operations.
+     * @throws IllegalStateException
+     *         If this connection has already been closed, i.e. if {@code isClosed() == true}.
+     * @throws NullPointerException
+     *         If {@code request} was {@code null}.
+     * @see org.forgerock.opendj.ldap.Connection#modifyDNAsync(ModifyDNRequest, IntermediateResponseHandler)
+     */
+    LdapPromise<Result> modifyDNAsync(
+            ModifyDNRequest request, IntermediateResponseHandler intermediateResponseHandler);
+
+    /**
+     * Removes the provided connection event listener from this connection so that it will no longer be notified when
+     * this connection is closed by the application, receives an unsolicited notification, or experiences a fatal
+     * error.
+     *
+     * @param listener
+     *         The listener which no longer wants to be notified when events occur on this connection.
+     * @throws NullPointerException
+     *         If the {@code listener} was {@code null}.
+     * @see org.forgerock.opendj.ldap.Connection#removeConnectionEventListener(ConnectionEventListener)
+     */
+    void removeConnectionEventListener(ConnectionEventListener listener);
+
+    /**
+     * Asynchronously searches the Directory Server using the provided search request.
+     *
+     * @param request
+     *         The search request.
+     * @param intermediateResponseHandler
+     *         An intermediate response handler which can be used to process any intermediate responses as they are
+     *         received, may be {@code null}.
+     * @param entryHandler
+     *         A search result handler which can be used to asynchronously process the search result entries and
+     *         references as they are received, may be {@code null}.
+     * @return A promise representing the result of the operation.
+     * @throws UnsupportedOperationException
+     *         If this connection does not support search operations.
+     * @throws IllegalStateException
+     *         If this connection has already been closed, i.e. if {@code isClosed() == true}.
+     * @throws NullPointerException
+     *         If {@code request} was {@code null}.
+     * @see org.forgerock.opendj.ldap.Connection#searchAsync(SearchRequest, IntermediateResponseHandler,
+     * SearchResultHandler)
+     */
+    LdapPromise<Result> searchAsync(
+            SearchRequest request,
+            IntermediateResponseHandler intermediateResponseHandler,
+            SearchResultHandler entryHandler);
+}
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/LdapPromiseWrapper.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/LdapPromiseWrapper.java
index 14645c1..4916623 100644
--- a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/LdapPromiseWrapper.java
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/LdapPromiseWrapper.java
@@ -63,6 +63,7 @@
         this.requestID = requestID;
     }
 
+    @SuppressWarnings("unchecked")
     @Override
     public int getRequestID() {
         return wrappedPromise instanceof LdapPromise ? ((LdapPromise<R>) wrappedPromise).getRequestID()
@@ -185,7 +186,6 @@
     }
 
     @Override
-    // @Checkstyle:ignore
     public LdapPromise<R> thenFinally(Runnable onResultOrException) {
         wrappedPromise.thenFinally(onResultOrException);
         return this;
@@ -195,7 +195,7 @@
     // @Checkstyle:ignore
     public <EOUT extends Exception> Promise<R, EOUT> thenCatchAsync(
             AsyncFunction<? super LdapException, R, EOUT> onException) {
-        return null;
+        return wrappedPromise.thenCatchAsync(onException);
     }
 
     public P getWrappedPromise() {
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/LdapPromises.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/LdapPromises.java
index a7ce25d..ccc4561 100644
--- a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/LdapPromises.java
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/LdapPromises.java
@@ -25,6 +25,10 @@
  */
 package org.forgerock.opendj.ldap.spi;
 
+import static org.forgerock.opendj.ldap.LdapException.newLdapException;
+import static org.forgerock.opendj.ldap.responses.Responses.newCompareResult;
+import static org.forgerock.opendj.ldap.responses.Responses.newResult;
+
 import org.forgerock.opendj.ldap.Connection;
 import org.forgerock.opendj.ldap.IntermediateResponseHandler;
 import org.forgerock.opendj.ldap.LdapException;
@@ -36,164 +40,147 @@
 import org.forgerock.opendj.ldap.requests.CompareRequest;
 import org.forgerock.opendj.ldap.requests.ExtendedRequest;
 import org.forgerock.opendj.ldap.requests.Request;
+import org.forgerock.opendj.ldap.requests.Requests;
 import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.requests.StartTLSExtendedRequest;
+import org.forgerock.opendj.ldap.responses.BindResult;
 import org.forgerock.opendj.ldap.responses.CompareResult;
 import org.forgerock.opendj.ldap.responses.ExtendedResult;
 import org.forgerock.opendj.ldap.responses.Result;
 import org.forgerock.util.promise.Promise;
+import org.forgerock.util.promise.PromiseImpl;
 import org.forgerock.util.promise.Promises;
 
-import static org.forgerock.opendj.ldap.responses.Responses.*;
-
 /**
  * Utility methods for creating and composing {@link LdapPromise}s.
  */
 public final class LdapPromises {
-    /**
-     * Returns a {@link LdapPromise} representing an asynchronous task which has
-     * already succeeded with the provided result. Attempts to get the result
-     * will immediately return the result.
-     *
-     * @param <R>
-     *            The type of the task's result, or {@link Void} if the task
-     *            does not return anything (i.e. it only has side-effects).
-     * @param result
-     *            The result of the asynchronous task.
-     * @return A {@link LdapPromise} representing an asynchronous task which has
-     *         already succeeded with the provided result.
-     */
-    public static <R> LdapPromise<R> newSuccessfulLdapPromise(final R result) {
-        return wrap(Promises.<R, LdapException> newResultPromise(result), -1);
+    private LdapPromises() {
     }
 
     /**
-     * Returns a {@link LdapPromise} representing an asynchronous task,
-     * identified by the provided requestID, which has already succeeded with
-     * the provided result. Attempts to get the result will immediately return
-     * the result.
+     * Converts a {@link Promise} to a {@link LdapPromise}.
      *
      * @param <R>
-     *            The type of the task's result, or {@link Void} if the task
-     *            does not return anything (i.e. it only has side-effects).
-     * @param result
-     *            The result of the asynchronous task.
-     * @param requestID
-     *            The request ID of the succeeded task.
-     * @return A {@link LdapPromise} representing an asynchronous task which has
-     *         already succeeded with the provided result.
+     *         The type of the task's result, or {@link Void} if the task does not return anything (i.e. it only has
+     *         side-effects).
+     * @param wrappedPromise
+     *         The {@link Promise} to wrap.
+     * @return A {@link LdapPromise} representing the same asynchronous task as the {@link Promise} provided.
      */
-    public static <R> LdapPromise<R> newSuccessfulLdapPromise(final R result, int requestID) {
-        return wrap(Promises.<R, LdapException> newResultPromise(result), requestID);
-    }
-
-    /**
-     * Returns a {@link LdapPromise} representing an asynchronous task which has
-     * already failed with the provided error.
-     *
-     * @param <R>
-     *            The type of the task's result, or {@link Void} if the task
-     *            does not return anything (i.e. it only has side-effects).
-     * @param <E>
-     *            The type of the exception thrown by the task if it fails.
-     * @param error
-     *            The exception indicating why the asynchronous task has failed.
-     * @return A {@link LdapPromise} representing an asynchronous task which has
-     *         already failed with the provided error.
-     */
-    public static <R, E extends LdapException> LdapPromise<R> newFailedLdapPromise(final E error) {
-        return wrap(Promises.<R, LdapException> newExceptionPromise(error), -1);
-    }
-
-    /**
-     * Returns a {@link LdapPromise} representing an asynchronous task,
-     * identified by the provided requestID, which has already failed with the
-     * provided error.
-     *
-     * @param <R>
-     *            The type of the task's result, or {@link Void} if the task
-     *            does not return anything (i.e. it only has side-effects).
-     * @param <E>
-     *            The type of the exception thrown by the task if it fails.
-     * @param error
-     *            The exception indicating why the asynchronous task has failed.
-     * @param requestID
-     *            The request ID of the failed task.
-     * @return A {@link LdapPromise} representing an asynchronous task which has
-     *         already failed with the provided error.
-     */
-    public static <R, E extends LdapException> LdapPromise<R> newFailedLdapPromise(final E error, int requestID) {
-        return wrap(Promises.<R, LdapException> newExceptionPromise(error), requestID);
-    }
-
-    /**
-     * Creates a new {@link ResultLdapPromiseImpl} to handle  a standard request (add, delete, modify and modidyDN).
-     *
-     * @param <R>
-     *           The type of the task's request.
-     *
-     * @param requestID
-     *            Identifier of the request.
-     * @param request
-     *            The request sent to the server.
-     * @param intermediateResponseHandler
-     *            Handler that consumes intermediate responses from extended operations.
-     * @param connection
-     *            The connection to directory server.
-     *
-     * @return The new {@link ResultLdapPromiseImpl}.
-     */
-    public static <R extends Request> ResultLdapPromiseImpl<R, Result> newResultLdapPromise(final int requestID,
-            final R request, final IntermediateResponseHandler intermediateResponseHandler,
-            final Connection connection) {
-        return new ResultLdapPromiseImpl<R, Result>(requestID, request, intermediateResponseHandler, connection) {
-            @Override
-            protected Result newErrorResult(ResultCode resultCode, String diagnosticMessage, Throwable cause) {
-                return newResult(resultCode).setDiagnosticMessage(diagnosticMessage).setCause(cause);
-            }
-        };
+    public static <R> LdapPromise<R> asPromise(Promise<R, LdapException> wrappedPromise) {
+        return wrap(wrappedPromise, -1);
     }
 
     /**
      * Creates a new bind {@link BindResultLdapPromiseImpl}.
      *
      * @param requestID
-     *            Identifier of the request.
+     *         Identifier of the request.
      * @param request
-     *            The bind request sent to server.
+     *         The bind request sent to server.
      * @param bindClient
-     *            Client that binds to the server.
+     *         Client that binds to the server.
      * @param intermediateResponseHandler
-     *            Handler that consumes intermediate responses from extended operations.
+     *         Handler that consumes intermediate responses from extended operations.
      * @param connection
-     *            The connection to directory server.
-     *
+     *         The connection to directory server.
      * @return The new {@link BindResultLdapPromiseImpl}.
      */
-    public static BindResultLdapPromiseImpl newBindLdapPromise(final int requestID,
-            final BindRequest request, final BindClient bindClient,
-            final IntermediateResponseHandler intermediateResponseHandler, final Connection connection) {
-        return new BindResultLdapPromiseImpl(requestID, request, bindClient, intermediateResponseHandler, connection);
+    public static BindResultLdapPromiseImpl newBindLdapPromise(
+            final int requestID,
+            final BindRequest request,
+            final BindClient bindClient,
+            final IntermediateResponseHandler intermediateResponseHandler,
+            final Connection connection) {
+        return new BindResultLdapPromiseImpl(LdapPromises.<BindResult>newInnerBindOrStartTLSPromise(),
+                                             requestID,
+                                             request,
+                                             bindClient,
+                                             intermediateResponseHandler);
+    }
+
+    /**
+     * Creates a new bind {@link BindResultLdapPromiseImpl}.
+     *
+     * @param requestID
+     *         Identifier of the request.
+     * @param request
+     *         The bind request sent to server.
+     * @param bindClient
+     *         Client that binds to the server.
+     * @param intermediateResponseHandler
+     *         Handler that consumes intermediate responses from extended operations.
+     * @param connection
+     *         The connection to directory server.
+     * @return The new {@link BindResultLdapPromiseImpl}.
+     */
+    public static BindResultLdapPromiseImpl newBindLdapPromise(
+            final int requestID,
+            final BindRequest request,
+            final BindClient bindClient,
+            final IntermediateResponseHandler intermediateResponseHandler,
+            final LDAPConnectionImpl connection) {
+        return new BindResultLdapPromiseImpl(LdapPromises.<BindResult>newInnerBindOrStartTLSPromise(),
+                                             requestID,
+                                             request,
+                                             bindClient,
+                                             intermediateResponseHandler);
     }
 
     /**
      * Creates a new compare {@link ResultLdapPromiseImpl}.
      *
      * @param requestID
-     *            Identifier of the request.
+     *         Identifier of the request.
      * @param request
-     *            The compare request sent to the server.
+     *         The compare request sent to the server.
      * @param intermediateResponseHandler
-     *            Handler that consumes intermediate responses from extended operations.
+     *         Handler that consumes intermediate responses from extended operations.
      * @param connection
-     *            The connection to directory server.
-     *
+     *         The connection to directory server.
      * @return The new {@link ResultLdapPromiseImpl}.
      */
-    public static ResultLdapPromiseImpl<CompareRequest, CompareResult> newCompareLdapPromise(final int requestID,
-            final CompareRequest request, final IntermediateResponseHandler intermediateResponseHandler,
+    public static ResultLdapPromiseImpl<CompareRequest, CompareResult> newCompareLdapPromise(
+            final int requestID,
+            final CompareRequest request,
+            final IntermediateResponseHandler intermediateResponseHandler,
             final Connection connection) {
-        return new ResultLdapPromiseImpl<CompareRequest, CompareResult>(requestID, request, intermediateResponseHandler,
-                connection) {
+        final PromiseImpl<CompareResult, LdapException> innerPromise = newInnerPromise(connection, requestID);
+        return newCompareLdapPromise(innerPromise, requestID, request, intermediateResponseHandler);
+    }
+
+    /**
+     * Creates a new compare {@link ResultLdapPromiseImpl}.
+     *
+     * @param requestID
+     *         Identifier of the request.
+     * @param request
+     *         The compare request sent to the server.
+     * @param intermediateResponseHandler
+     *         Handler that consumes intermediate responses from extended operations.
+     * @param connection
+     *         The connection to directory server.
+     * @return The new {@link ResultLdapPromiseImpl}.
+     */
+    public static ResultLdapPromiseImpl<CompareRequest, CompareResult> newCompareLdapPromise(
+            final int requestID,
+            final CompareRequest request,
+            final IntermediateResponseHandler intermediateResponseHandler,
+            final LDAPConnectionImpl connection) {
+        final PromiseImpl<CompareResult, LdapException> innerPromise = newInnerPromise(connection, requestID);
+        return newCompareLdapPromise(innerPromise, requestID, request, intermediateResponseHandler);
+    }
+
+    private static ResultLdapPromiseImpl<CompareRequest, CompareResult> newCompareLdapPromise(
+            final PromiseImpl<CompareResult, LdapException> innerPromise,
+            final int requestID,
+            final CompareRequest request,
+            final IntermediateResponseHandler intermediateResponseHandler) {
+        return new ResultLdapPromiseImpl<CompareRequest, CompareResult>(innerPromise,
+                                                                        requestID,
+                                                                        request,
+                                                                        intermediateResponseHandler) {
             @Override
             protected CompareResult newErrorResult(ResultCode resultCode, String diagnosticMessage, Throwable cause) {
                 return newCompareResult(resultCode).setDiagnosticMessage(diagnosticMessage).setCause(cause);
@@ -205,69 +192,292 @@
      * Creates a new extended {@link ExtendedResultLdapPromiseImpl}.
      *
      * @param <S>
-     *            The type of result returned by this promise.
+     *         The type of result returned by this promise.
      * @param requestID
-     *            Identifier of the request.
+     *         Identifier of the request.
      * @param request
-     *            The extended request sent to the server.
+     *         The extended request sent to the server.
      * @param intermediateResponseHandler
-     *            Handler that consumes intermediate responses from extended operations.
+     *         Handler that consumes intermediate responses from extended operations.
      * @param connection
-     *            The connection to directory server.
-     *
+     *         The connection to directory server.
      * @return The new {@link ExtendedResultLdapPromiseImpl}.
      */
     public static <S extends ExtendedResult> ExtendedResultLdapPromiseImpl<S> newExtendedLdapPromise(
-            final int requestID, final ExtendedRequest<S> request,
-            final IntermediateResponseHandler intermediateResponseHandler, final Connection connection) {
-        return new ExtendedResultLdapPromiseImpl<>(requestID, request, intermediateResponseHandler, connection);
+            final int requestID,
+            final ExtendedRequest<S> request,
+            final IntermediateResponseHandler intermediateResponseHandler,
+            final Connection connection) {
+        final PromiseImpl<S, LdapException> innerPromise;
+        if (!StartTLSExtendedRequest.OID.equals(request.getOID())) {
+            innerPromise = newInnerBindOrStartTLSPromise();
+        } else {
+            innerPromise = newInnerPromise(connection, requestID);
+        }
+        return new ExtendedResultLdapPromiseImpl<>(innerPromise, requestID, request, intermediateResponseHandler);
+    }
+
+    /**
+     * Creates a new extended {@link ExtendedResultLdapPromiseImpl}.
+     *
+     * @param <S>
+     *         The type of result returned by this promise.
+     * @param requestID
+     *         Identifier of the request.
+     * @param request
+     *         The extended request sent to the server.
+     * @param intermediateResponseHandler
+     *         Handler that consumes intermediate responses from extended operations.
+     * @param connection
+     *         The connection to directory server.
+     * @return The new {@link ExtendedResultLdapPromiseImpl}.
+     */
+    public static <S extends ExtendedResult> ExtendedResultLdapPromiseImpl<S> newExtendedLdapPromise(
+            final int requestID,
+            final ExtendedRequest<S> request,
+            final IntermediateResponseHandler intermediateResponseHandler,
+            final LDAPConnectionImpl connection) {
+        final PromiseImpl<S, LdapException> innerPromise;
+        if (!StartTLSExtendedRequest.OID.equals(request.getOID())) {
+            innerPromise = newInnerBindOrStartTLSPromise();
+        } else {
+            innerPromise = newInnerPromise(connection, requestID);
+        }
+        return new ExtendedResultLdapPromiseImpl<>(innerPromise, requestID, request, intermediateResponseHandler);
+    }
+
+    /**
+     * Creates a new {@link ResultLdapPromiseImpl} to handle  a standard request (add, delete, modify and modidyDN).
+     *
+     * @param <R>
+     *         The type of the task's request.
+     * @param requestID
+     *         Identifier of the request.
+     * @param request
+     *         The request sent to the server.
+     * @param intermediateResponseHandler
+     *         Handler that consumes intermediate responses from extended operations.
+     * @param connection
+     *         The connection to directory server.
+     * @return The new {@link ResultLdapPromiseImpl}.
+     */
+    public static <R extends Request> ResultLdapPromiseImpl<R, Result> newResultLdapPromise(
+            final int requestID,
+            final R request,
+            final IntermediateResponseHandler intermediateResponseHandler,
+            final Connection connection) {
+        final PromiseImpl<Result, LdapException> innerPromise = newInnerPromise(connection, requestID);
+        return newResultLdapPromise(innerPromise, requestID, request, intermediateResponseHandler);
+    }
+
+    /**
+     * Creates a new {@link ResultLdapPromiseImpl} to handle  a standard request (add, delete, modify and modidyDN).
+     *
+     * @param <R>
+     *         The type of the task's request.
+     * @param requestID
+     *         Identifier of the request.
+     * @param request
+     *         The request sent to the server.
+     * @param intermediateResponseHandler
+     *         Handler that consumes intermediate responses from extended operations.
+     * @param connection
+     *         The connection to directory server.
+     * @return The new {@link ResultLdapPromiseImpl}.
+     */
+    public static <R extends Request> ResultLdapPromiseImpl<R, Result> newResultLdapPromise(
+            final int requestID,
+            final R request,
+            final IntermediateResponseHandler intermediateResponseHandler,
+            final LDAPConnectionImpl connection) {
+        final PromiseImpl<Result, LdapException> innerPromise = newInnerPromise(connection, requestID);
+        return newResultLdapPromise(innerPromise, requestID, request, intermediateResponseHandler);
+    }
+
+    private static <R extends Request> ResultLdapPromiseImpl<R, Result> newResultLdapPromise(
+            final PromiseImpl<Result, LdapException> innerPromise,
+            final int requestID,
+            final R request,
+            final IntermediateResponseHandler intermediateResponseHandler) {
+        return new ResultLdapPromiseImpl<R, Result>(innerPromise, requestID, request, intermediateResponseHandler) {
+            @Override
+            protected Result newErrorResult(ResultCode resultCode, String diagnosticMessage, Throwable cause) {
+                return newResult(resultCode).setDiagnosticMessage(diagnosticMessage).setCause(cause);
+            }
+        };
     }
 
     /**
      * Creates a new search {@link SearchResultLdapPromiseImpl}.
      *
      * @param requestID
-     *            Identifier of the request.
+     *         Identifier of the request.
      * @param request
-     *            The search request sent to the server.
+     *         The search request sent to the server.
      * @param resultHandler
-     *            Handler that consumes search result.
+     *         Handler that consumes search result.
      * @param intermediateResponseHandler
-     *            Handler that consumes intermediate responses from extended operations.
+     *         Handler that consumes intermediate responses from extended operations.
      * @param connection
-     *            The connection to directory server.
-     *
+     *         The connection to directory server.
      * @return The new {@link SearchResultLdapPromiseImpl}.
      */
-    public static SearchResultLdapPromiseImpl newSearchLdapPromise(final int requestID, final SearchRequest request,
-            final SearchResultHandler resultHandler, final IntermediateResponseHandler intermediateResponseHandler,
+    public static SearchResultLdapPromiseImpl newSearchLdapPromise(
+            final int requestID,
+            final SearchRequest request,
+            final SearchResultHandler resultHandler,
+            final IntermediateResponseHandler intermediateResponseHandler,
             final Connection connection) {
-        return new SearchResultLdapPromiseImpl(requestID, request, resultHandler, intermediateResponseHandler,
-                connection);
+        return new SearchResultLdapPromiseImpl(newInnerPromise(connection, requestID),
+                                               requestID,
+                                               request,
+                                               resultHandler,
+                                               intermediateResponseHandler);
     }
 
-
+    /**
+     * Creates a new search {@link SearchResultLdapPromiseImpl}.
+     *
+     * @param requestID
+     *         Identifier of the request.
+     * @param request
+     *         The search request sent to the server.
+     * @param resultHandler
+     *         Handler that consumes search result.
+     * @param intermediateResponseHandler
+     *         Handler that consumes intermediate responses from extended operations.
+     * @param connection
+     *         The connection to directory server.
+     * @return The new {@link SearchResultLdapPromiseImpl}.
+     */
+    public static SearchResultLdapPromiseImpl newSearchLdapPromise(
+            final int requestID,
+            final SearchRequest request,
+            final SearchResultHandler resultHandler,
+            final IntermediateResponseHandler intermediateResponseHandler,
+            final LDAPConnectionImpl connection) {
+        return new SearchResultLdapPromiseImpl(newInnerPromise(connection, requestID),
+                                               requestID,
+                                               request,
+                                               resultHandler,
+                                               intermediateResponseHandler);
+    }
 
     /**
-     * Converts a {@link Promise} to a {@link LdapPromise}.
+     * Returns a {@link LdapPromise} representing an asynchronous task which has already failed with the provided
+     * error.
      *
      * @param <R>
-     *            The type of the task's result, or {@link Void} if the task
-     *            does not return anything (i.e. it only has side-effects).
-     * @param wrappedPromise
-     *            The {@link Promise} to wrap.
-     * @return A {@link LdapPromise} representing the same asynchronous task as
-     *         the {@link Promise} provided.
+     *         The type of the task's result, or {@link Void} if the task does not return anything (i.e. it only has
+     *         side-effects).
+     * @param <E>
+     *         The type of the exception thrown by the task if it fails.
+     * @param error
+     *         The exception indicating why the asynchronous task has failed.
+     * @return A {@link LdapPromise} representing an asynchronous task which has already failed with the provided error.
      */
-    public static <R> LdapPromise<R> asPromise(Promise<R, LdapException> wrappedPromise) {
-        return wrap(wrappedPromise, -1);
+    public static <R, E extends LdapException> LdapPromise<R> newFailedLdapPromise(final E error) {
+        return wrap(Promises.<R, LdapException>newExceptionPromise(error), -1);
+    }
+
+    /**
+     * Returns a {@link LdapPromise} representing an asynchronous task, identified by the provided requestID, which has
+     * already failed with the provided error.
+     *
+     * @param <R>
+     *         The type of the task's result, or {@link Void} if the task does not return anything (i.e. it only has
+     *         side-effects).
+     * @param <E>
+     *         The type of the exception thrown by the task if it fails.
+     * @param error
+     *         The exception indicating why the asynchronous task has failed.
+     * @param requestID
+     *         The request ID of the failed task.
+     * @return A {@link LdapPromise} representing an asynchronous task which has already failed with the provided error.
+     */
+    public static <R, E extends LdapException> LdapPromise<R> newFailedLdapPromise(final E error, int requestID) {
+        return wrap(Promises.<R, LdapException>newExceptionPromise(error), requestID);
+    }
+
+    /**
+     * Returns a {@link LdapPromise} representing an asynchronous task which has already succeeded with the provided
+     * result. Attempts to get the result will immediately return the result.
+     *
+     * @param <R>
+     *         The type of the task's result, or {@link Void} if the task does not return anything (i.e. it only has
+     *         side-effects).
+     * @param result
+     *         The result of the asynchronous task.
+     * @return A {@link LdapPromise} representing an asynchronous task which has already succeeded with the provided
+     * result.
+     */
+    public static <R> LdapPromise<R> newSuccessfulLdapPromise(final R result) {
+        return wrap(Promises.<R, LdapException>newResultPromise(result), -1);
+    }
+
+    /**
+     * Returns a {@link LdapPromise} representing an asynchronous task, identified by the provided requestID, which has
+     * already succeeded with the provided result. Attempts to get the result will immediately return the result.
+     *
+     * @param <R>
+     *         The type of the task's result, or {@link Void} if the task does not return anything (i.e. it only has
+     *         side-effects).
+     * @param result
+     *         The result of the asynchronous task.
+     * @param requestID
+     *         The request ID of the succeeded task.
+     * @return A {@link LdapPromise} representing an asynchronous task which has already succeeded with the provided
+     * result.
+     */
+    public static <R> LdapPromise<R> newSuccessfulLdapPromise(final R result, int requestID) {
+        return wrap(Promises.<R, LdapException>newResultPromise(result), requestID);
+    }
+
+    private static <S extends Result> PromiseImpl<S, LdapException> newInnerBindOrStartTLSPromise() {
+        return new PromiseImpl<S, LdapException>() {
+            @Override
+            protected LdapException tryCancel(boolean mayInterruptIfRunning) {
+                /*
+                 * No other operations can be performed while a bind or StartTLS is active. Therefore it is not
+                 * possible to cancel bind or StartTLS requests, since doing so will leave the connection in a
+                 * state which prevents other operations from being performed.
+                 */
+                return null;
+            }
+        };
+    }
+
+    private static <S extends Result> PromiseImpl<S, LdapException> newInnerPromise(
+            final Connection connection, final int requestID) {
+        return new PromiseImpl<S, LdapException>() {
+            @Override
+            protected final LdapException tryCancel(final boolean mayInterruptIfRunning) {
+                /*
+                 * This will abandon the request, but will also recursively cancel this future. There is no risk of
+                 * an infinite loop because the state of this future has already been changed.
+                 */
+                connection.abandonAsync(Requests.newAbandonRequest(requestID));
+                return newLdapException(ResultCode.CLIENT_SIDE_USER_CANCELLED);
+            }
+        };
+    }
+
+    private static <S extends Result> PromiseImpl<S, LdapException> newInnerPromise(
+            final LDAPConnectionImpl connection, final int requestID) {
+        return new PromiseImpl<S, LdapException>() {
+            @Override
+            protected final LdapException tryCancel(final boolean mayInterruptIfRunning) {
+                /*
+                 * This will abandon the request, but will also recursively cancel this future. There is no risk of
+                 * an infinite loop because the state of this future has already been changed.
+                 */
+                connection.abandonAsync(Requests.newAbandonRequest(requestID));
+                return newLdapException(ResultCode.CLIENT_SIDE_USER_CANCELLED);
+            }
+        };
     }
 
     static <R> LdapPromise<R> wrap(Promise<R, LdapException> wrappedPromise, int requestID) {
         return new LdapPromiseWrapper<>(wrappedPromise, requestID);
     }
-
-    private LdapPromises() {
-    }
-
 }
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/ResultLdapPromiseImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/ResultLdapPromiseImpl.java
index 0a7422d..8883b97 100644
--- a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/ResultLdapPromiseImpl.java
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/ResultLdapPromiseImpl.java
@@ -21,18 +21,16 @@
  * CDDL HEADER END
  *
  *
- *      Copyright 2014 ForgeRock AS.
+ *      Copyright 2014-2015 ForgeRock AS.
  */
 
 package org.forgerock.opendj.ldap.spi;
 
-import org.forgerock.opendj.ldap.Connection;
 import org.forgerock.opendj.ldap.IntermediateResponseHandler;
 import org.forgerock.opendj.ldap.LdapException;
 import org.forgerock.opendj.ldap.LdapPromise;
 import org.forgerock.opendj.ldap.ResultCode;
 import org.forgerock.opendj.ldap.requests.Request;
-import org.forgerock.opendj.ldap.requests.Requests;
 import org.forgerock.opendj.ldap.responses.IntermediateResponse;
 import org.forgerock.opendj.ldap.responses.Result;
 import org.forgerock.util.promise.Promise;
@@ -58,24 +56,8 @@
     private IntermediateResponseHandler intermediateResponseHandler;
     private volatile long timestamp;
 
-    ResultLdapPromiseImpl(final int requestID, final R request,
-            final IntermediateResponseHandler intermediateResponseHandler, final Connection connection) {
-        this(new PromiseImpl<S, LdapException>() {
-            @Override
-            protected final LdapException tryCancel(final boolean mayInterruptIfRunning) {
-                /*
-                 * This will abandon the request, but will also recursively cancel this
-                 * future. There is no risk of an infinite loop because the state of
-                 * this future has already been changed.
-                 */
-                connection.abandonAsync(Requests.newAbandonRequest(requestID));
-                return newLdapException(ResultCode.CLIENT_SIDE_USER_CANCELLED);
-            }
-        }, requestID, request, intermediateResponseHandler, connection);
-    }
-
     ResultLdapPromiseImpl(final PromiseImpl<S, LdapException> impl, final int requestID, final R request,
-            final IntermediateResponseHandler intermediateResponseHandler, final Connection connection) {
+            final IntermediateResponseHandler intermediateResponseHandler) {
         super(impl, requestID);
         this.request = request;
         this.intermediateResponseHandler = intermediateResponseHandler;
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/SearchResultLdapPromiseImpl.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/SearchResultLdapPromiseImpl.java
index a169c66..e6ae3e6 100644
--- a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/SearchResultLdapPromiseImpl.java
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldap/spi/SearchResultLdapPromiseImpl.java
@@ -22,13 +22,13 @@
  *
  *
  *      Copyright 2009-2010 Sun Microsystems, Inc.
- *      Portions copyright 2011-2014 ForgeRock AS.
+ *      Portions copyright 2011-2015 ForgeRock AS.
  */
 
 package org.forgerock.opendj.ldap.spi;
 
-import org.forgerock.opendj.ldap.Connection;
 import org.forgerock.opendj.ldap.IntermediateResponseHandler;
+import org.forgerock.opendj.ldap.LdapException;
 import org.forgerock.opendj.ldap.ResultCode;
 import org.forgerock.opendj.ldap.SearchResultHandler;
 import org.forgerock.opendj.ldap.controls.ADNotificationRequestControl;
@@ -38,6 +38,7 @@
 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.promise.PromiseImpl;
 
 /**
  * Search result promise implementation.
@@ -47,13 +48,15 @@
     private SearchResultHandler searchResultHandler;
     private final boolean isPersistentSearch;
 
-    SearchResultLdapPromiseImpl(final int requestID, final SearchRequest request,
-        final SearchResultHandler resultHandler, final IntermediateResponseHandler intermediateResponseHandler,
-        final Connection connection) {
-        super(requestID, request, intermediateResponseHandler, connection);
+    SearchResultLdapPromiseImpl(
+            final PromiseImpl<Result, LdapException> impl,
+            final int requestID,
+            final SearchRequest request,
+            final SearchResultHandler resultHandler,
+            final IntermediateResponseHandler intermediateResponseHandler) {
+        super(impl, requestID, request, intermediateResponseHandler);
         this.searchResultHandler = resultHandler;
-        this.isPersistentSearch =
-            request.containsControl(PersistentSearchRequestControl.OID)
+        this.isPersistentSearch = request.containsControl(PersistentSearchRequestControl.OID)
                 || request.containsControl(ADNotificationRequestControl.OID);
     }
 
@@ -90,8 +93,7 @@
     }
 
     @Override
-    Result newErrorResult(final ResultCode resultCode, final String diagnosticMessage,
-            final Throwable cause) {
+    Result newErrorResult(final ResultCode resultCode, final String diagnosticMessage, final Throwable cause) {
         return Responses.newResult(resultCode).setDiagnosticMessage(diagnosticMessage).setCause(cause);
     }
 
diff --git a/opendj-sdk/opendj-core/src/main/resources/com/forgerock/opendj/ldap/core.properties b/opendj-sdk/opendj-core/src/main/resources/com/forgerock/opendj/ldap/core.properties
index 555a3be..535c4ea 100755
--- a/opendj-sdk/opendj-core/src/main/resources/com/forgerock/opendj/ldap/core.properties
+++ b/opendj-sdk/opendj-core/src/main/resources/com/forgerock/opendj/ldap/core.properties
@@ -1666,3 +1666,6 @@
 DOC_SUPPORTED_SUBTYPES_TITLE=Supported Language Subtypes
 DOC_SUPPORTED_SUBTYPES_INDEXTERM=Language subtypes
 DOC_LANGUAGE_SH=Serbo-Croatian
+
+LDAP_CONNECTION_CONNECT_TIMEOUT=The connection attempt to server %s has failed \
+ because the connection timeout period of %d ms was exceeded
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/io/LDAPReaderWriterTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/io/LDAPReaderWriterTestCase.java
index 8422faf..41335a4 100644
--- a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/io/LDAPReaderWriterTestCase.java
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/io/LDAPReaderWriterTestCase.java
@@ -26,7 +26,7 @@
 package org.forgerock.opendj.io;
 
 import static org.fest.assertions.Assertions.*;
-import static org.forgerock.opendj.ldap.LDAPConnectionFactory.DECODE_OPTIONS;
+import static org.forgerock.opendj.ldap.LDAPConnectionFactory.LDAP_DECODE_OPTIONS;
 
 import java.io.IOException;
 
@@ -265,7 +265,7 @@
                     ExtendedRequest<R> request) throws DecodeException, IOException {
                 CancelExtendedRequest cancelRequest =
                         CancelExtendedRequest.DECODER.decodeExtendedRequest(request,
-                            Options.defaultOptions().get(DECODE_OPTIONS));
+                            Options.defaultOptions().get(LDAP_DECODE_OPTIONS));
                 assertThat(cancelRequest.getOID()).isEqualTo(oidCancel);
                 assertThat(cancelRequest.getRequestID()).isEqualTo(requestID);
             }
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/HeartBeatConnectionFactoryTestCase.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/HeartBeatConnectionFactoryTestCase.java
index a6143bc..3668c6b 100644
--- a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/HeartBeatConnectionFactoryTestCase.java
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/HeartBeatConnectionFactoryTestCase.java
@@ -26,16 +26,43 @@
 
 package org.forgerock.opendj.ldap;
 
+import static org.fest.assertions.Assertions.assertThat;
+import static org.fest.assertions.Fail.fail;
+import static org.forgerock.opendj.ldap.LDAPConnectionFactory.*;
+import static org.forgerock.opendj.ldap.LdapException.newLdapException;
+import static org.forgerock.opendj.ldap.ResultCode.SUCCESS;
+import static org.forgerock.opendj.ldap.SearchScope.BASE_OBJECT;
+import static org.forgerock.opendj.ldap.TestCaseUtils.mockTimeService;
+import static org.forgerock.opendj.ldap.requests.Requests.newModifyRequest;
+import static org.forgerock.opendj.ldap.requests.Requests.newSearchRequest;
+import static org.forgerock.opendj.ldap.requests.Requests.newSimpleBindRequest;
+import static org.forgerock.opendj.ldap.responses.Responses.newBindResult;
+import static org.forgerock.opendj.ldap.responses.Responses.newResult;
+import static org.forgerock.opendj.ldap.spi.LdapPromiseImpl.newLdapPromiseImpl;
+import static org.forgerock.opendj.ldap.spi.LdapPromises.newBindLdapPromise;
+import static org.forgerock.opendj.ldap.spi.LdapPromises.newFailedLdapPromise;
+import static org.forgerock.opendj.ldap.spi.LdapPromises.newSearchLdapPromise;
+import static org.forgerock.opendj.ldap.spi.LdapPromises.newSuccessfulLdapPromise;
+import static org.forgerock.util.time.Duration.duration;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.same;
+import static org.mockito.Mockito.*;
+
 import java.util.LinkedList;
 import java.util.List;
-import java.util.concurrent.TimeUnit;
 import java.util.logging.Level;
 
 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.Result;
 import org.forgerock.opendj.ldap.spi.BindResultLdapPromiseImpl;
+import org.forgerock.opendj.ldap.spi.LDAPConnectionFactoryImpl;
+import org.forgerock.opendj.ldap.spi.LDAPConnectionImpl;
+import org.forgerock.opendj.ldap.spi.SearchResultLdapPromiseImpl;
+import org.forgerock.opendj.ldap.spi.TransportProvider;
+import org.forgerock.util.Options;
 import org.forgerock.util.promise.ExceptionHandler;
 import org.forgerock.util.promise.NeverThrowsException;
 import org.forgerock.util.promise.Promise;
@@ -49,19 +76,6 @@
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
 
-import static org.fest.assertions.Assertions.*;
-import static org.fest.assertions.Fail.*;
-import static org.forgerock.opendj.ldap.LdapException.*;
-import static org.forgerock.opendj.ldap.ResultCode.*;
-import static org.forgerock.opendj.ldap.SearchScope.*;
-import static org.forgerock.opendj.ldap.TestCaseUtils.*;
-import static org.forgerock.opendj.ldap.requests.Requests.*;
-import static org.forgerock.opendj.ldap.responses.Responses.*;
-import static org.forgerock.opendj.ldap.spi.LdapPromiseImpl.*;
-import static org.forgerock.opendj.ldap.spi.LdapPromises.*;
-import static org.mockito.Matchers.*;
-import static org.mockito.Mockito.*;
-
 /**
  * Tests the connection pool implementation..
  */
@@ -86,13 +100,12 @@
 
     // @formatter:on
 
-    private static final SearchRequest HEARTBEAT = newSearchRequest("dc=test", BASE_OBJECT,
-            "(objectclass=*)", "1.1");
+    private static final SearchRequest HEARTBEAT = newSearchRequest("dc=test", BASE_OBJECT, "(objectclass=*)", "1.1");
 
-    private Connection connection;
-    private ConnectionFactory factory;
+    private LDAPConnectionImpl ldapConnection;
+    private LDAPConnectionFactoryImpl ldapFactory;
     private Connection hbc;
-    private HeartBeatConnectionFactory hbcf;
+    private LDAPConnectionFactory hbcf;
     private List<ConnectionEventListener> listeners;
     private MockScheduler scheduler;
 
@@ -124,8 +137,8 @@
             assertThat(scheduler.isScheduled()).isFalse(); // No more connections to check.
             hbcf.close();
         } finally {
-            connection = null;
-            factory = null;
+            ldapConnection = null;
+            ldapFactory = null;
             hbcf = null;
             listeners = null;
             scheduler = null;
@@ -143,13 +156,19 @@
          * Send a heartbeat, trapping the search call-back so that we can send
          * the response once we have attempted a bind.
          */
-        when(connection.searchAsync(any(SearchRequest.class), any(SearchResultHandler.class))).thenReturn(
-            newSuccessfulLdapPromise(newResult(SUCCESS)));
+        final SearchResultLdapPromiseImpl heartBeatPromise =
+                newSearchLdapPromise(-1, HEARTBEAT, null, null, ldapConnection);
+        when(ldapConnection.searchAsync(same(HEARTBEAT),
+                                        any(IntermediateResponseHandler.class),
+                                        any(SearchResultHandler.class)))
+                .thenReturn(heartBeatPromise);
         when(hbcf.timeService.now()).thenReturn(11000L);
         scheduler.runAllTasks(); // Send the heartbeat.
 
         // Capture the heartbeat search result handler.
-        verify(connection, times(2)).searchAsync(same(HEARTBEAT), any(SearchResultHandler.class));
+        verify(ldapConnection, times(2)).searchAsync(same(HEARTBEAT),
+                                                     any(IntermediateResponseHandler.class),
+                                                     any(SearchResultHandler.class));
         assertThat(hbc.isValid()).isTrue(); // Not checked yet.
 
         /*
@@ -157,10 +176,11 @@
          * heart beat completes.
          */
         hbc.bindAsync(newSimpleBindRequest());
-        verify(connection, times(0)).bindAsync(any(BindRequest.class));
+        verify(ldapConnection, times(0)).bindAsync(any(BindRequest.class), any(IntermediateResponseHandler.class));
 
         // Send fake heartbeat response, releasing the bind request.
-        verify(connection, times(1)).bindAsync(any(BindRequest.class), any(IntermediateResponseHandler.class));
+        heartBeatPromise.getWrappedPromise().handleResult(newResult(SUCCESS));
+        verify(ldapConnection, times(1)).bindAsync(any(BindRequest.class), any(IntermediateResponseHandler.class));
     }
 
     @Test
@@ -230,27 +250,27 @@
         // Get a connection and check that it was pinged.
         hbc = hbcf.getConnection();
 
-        verifyHeartBeatSent(connection, 1);
+        verifyHeartBeatSent(ldapConnection, 1);
         assertThat(scheduler.isScheduled()).isTrue(); // heartbeater
         assertThat(listeners).hasSize(1);
         assertThat(hbc.isValid()).isTrue();
 
         // Invoke heartbeat before the connection is considered idle.
         scheduler.runAllTasks();
-        verifyHeartBeatSent(connection, 1); // No heartbeat sent - not idle yet.
+        verifyHeartBeatSent(ldapConnection, 1); // No heartbeat sent - not idle yet.
         assertThat(hbc.isValid()).isTrue();
 
         // Invoke heartbeat after the connection is considered idle.
         when(hbcf.timeService.now()).thenReturn(6000L);
         scheduler.runAllTasks();
-        verifyHeartBeatSent(connection, 2); // Heartbeat sent.
+        verifyHeartBeatSent(ldapConnection, 2); // Heartbeat sent.
         assertThat(hbc.isValid()).isTrue();
 
         // Now force the heartbeat to fail.
-        mockHeartBeatResponse(connection, listeners, ResultCode.CLIENT_SIDE_SERVER_DOWN);
+        mockHeartBeatResponse(ldapConnection, listeners, ResultCode.CLIENT_SIDE_SERVER_DOWN);
         when(hbcf.timeService.now()).thenReturn(11000L);
         scheduler.runAllTasks();
-        verifyHeartBeatSent(connection, 3);
+        verifyHeartBeatSent(ldapConnection, 3);
         assertThat(hbc.isValid()).isFalse();
 
         // Flush redundant timeout tasks.
@@ -262,8 +282,7 @@
         hbc.modifyAsync(newModifyRequest(DN.rootDN())).thenOnException(mockHandler);
         final ArgumentCaptor<LdapException> arg = ArgumentCaptor.forClass(LdapException.class);
         verify(mockHandler).handleException(arg.capture());
-        assertThat(arg.getValue().getResult().getResultCode()).isEqualTo(
-                ResultCode.CLIENT_SIDE_SERVER_DOWN);
+        assertThat(arg.getValue().getResult().getResultCode()).isEqualTo(ResultCode.CLIENT_SIDE_SERVER_DOWN);
 
         assertThat(hbc.isValid()).isFalse();
         assertThat(hbc.isClosed()).isFalse();
@@ -277,10 +296,10 @@
         hbc = hbcf.getConnection();
 
         // Now force the heartbeat to fail due to timeout.
-        mockHeartBeatResponse(connection, listeners, null /* no response */);
+        mockHeartBeatResponse(ldapConnection, listeners, null /* no response */);
         when(hbcf.timeService.now()).thenReturn(11000L);
         scheduler.runAllTasks(); // Send the heartbeat.
-        verifyHeartBeatSent(connection, 2);
+        verifyHeartBeatSent(ldapConnection, 2);
         assertThat(hbc.isValid()).isTrue(); // Not checked yet.
         when(hbcf.timeService.now()).thenReturn(12000L);
         scheduler.runAllTasks(); // Check for heartbeat.
@@ -302,16 +321,18 @@
         when(hbcf.timeService.now()).thenReturn(11000L);
         hbc.bindAsync(newSimpleBindRequest());
 
-        verify(connection, times(1)).bindAsync(any(BindRequest.class), any(IntermediateResponseHandler.class));
+        verify(ldapConnection, times(1)).bindAsync(any(BindRequest.class), any(IntermediateResponseHandler.class));
 
         // Verify no heartbeat is sent because there is a bind in progress.
         when(hbcf.timeService.now()).thenReturn(11001L);
         scheduler.runAllTasks(); // Invokes HBCF.ConnectionImpl.sendHeartBeat()
-        verify(connection, times(1)).searchAsync(same(HEARTBEAT), any(SearchResultHandler.class));
+        verify(ldapConnection, times(1)).searchAsync(same(HEARTBEAT),
+                                                     any(IntermediateResponseHandler.class),
+                                                     any(SearchResultHandler.class));
 
         // Send fake bind response, releasing the heartbeat.
         when(hbcf.timeService.now()).thenReturn(11099L);
-        ((PromiseImpl) promise.getWrappedPromise()).handleResult(newResult(SUCCESS));
+        promise.getWrappedPromise().handleResult(newBindResult(SUCCESS));
 
         // Check that bind response acts as heartbeat.
         assertThat(hbc.isValid()).isTrue();
@@ -329,12 +350,14 @@
         // Send another bind request which will timeout.
         when(hbcf.timeService.now()).thenReturn(20000L);
         hbc.bindAsync(newSimpleBindRequest());
-        verify(connection, times(1)).bindAsync(any(BindRequest.class), any(IntermediateResponseHandler.class));
+        verify(ldapConnection, times(1)).bindAsync(any(BindRequest.class), any(IntermediateResponseHandler.class));
 
         // Verify no heartbeat is sent because there is a bind in progress.
         when(hbcf.timeService.now()).thenReturn(20001L);
         scheduler.runAllTasks(); // Invokes HBCF.ConnectionImpl.sendHeartBeat()
-        verify(connection, times(1)).searchAsync(same(HEARTBEAT), any(SearchResultHandler.class));
+        verify(ldapConnection, times(1)).searchAsync(same(HEARTBEAT),
+                                                     any(IntermediateResponseHandler.class),
+                                                     any(SearchResultHandler.class));
 
         // Check that lack of bind response acts as heartbeat timeout.
         assertThat(hbc.isValid()).isTrue();
@@ -352,7 +375,7 @@
         hbc = hbcf.getConnection();
         hbc.bindAsync(newSimpleBindRequest());
 
-        verify(connection, times(1)).bindAsync(any(BindRequest.class), any(IntermediateResponseHandler.class));
+        verify(ldapConnection, times(1)).bindAsync(any(BindRequest.class), any(IntermediateResponseHandler.class));
 
         /*
          * Now attempt the heartbeat which should not happen because there is a
@@ -361,16 +384,20 @@
         when(hbcf.timeService.now()).thenReturn(11000L);
         // Attempt to send the heartbeat.
         scheduler.runAllTasks();
-        verify(connection, times(1)).searchAsync(same(HEARTBEAT), any(SearchResultHandler.class));
+        verify(ldapConnection, times(1)).searchAsync(same(HEARTBEAT),
+                                                     any(IntermediateResponseHandler.class),
+                                                     any(SearchResultHandler.class));
 
         // Send fake bind response, releasing the heartbeat.
-        ((PromiseImpl) promise.getWrappedPromise()).handleResult(newResult(SUCCESS));
+        promise.getWrappedPromise().handleResult(newBindResult(SUCCESS));
 
         // Attempt to send a heartbeat again.
         when(hbcf.timeService.now()).thenReturn(16000L);
         // Attempt to send the heartbeat.
         scheduler.runAllTasks();
-        verify(connection, times(2)).searchAsync(same(HEARTBEAT), any(SearchResultHandler.class));
+        verify(ldapConnection, times(2)).searchAsync(same(HEARTBEAT),
+                                                     any(IntermediateResponseHandler.class),
+                                                     any(SearchResultHandler.class));
     }
 
     @Test
@@ -392,36 +419,51 @@
 
     private void mockConnectionWithInitialHeartbeatResult(final ResultCode initialHeartBeatResult) {
         listeners = new LinkedList<>();
-        connection = mockConnection(listeners);
-        when(connection.isValid()).thenReturn(true);
-        mockHeartBeatResponse(connection, listeners, initialHeartBeatResult);
-
-        // Underlying connection factory.
-        factory = mockConnectionFactory(connection);
+        ldapConnection = mockLDAPConnectionImpl(listeners);
+        when(ldapConnection.isValid()).thenReturn(true);
+        mockHeartBeatResponse(ldapConnection, listeners, initialHeartBeatResult);
+        ldapFactory = mock(LDAPConnectionFactoryImpl.class);
+        when(ldapFactory.getConnectionAsync()).thenAnswer(new Answer<Promise<LDAPConnectionImpl, LdapException>>() {
+            @Override
+            public Promise<LDAPConnectionImpl, LdapException> answer(final InvocationOnMock invocation)
+                    throws Throwable {
+                return newSuccessfulLdapPromise(ldapConnection);
+            }
+        });
+        TransportProvider provider = mock(TransportProvider.class);
+        when(provider.getLDAPConnectionFactory(anyString(), anyInt(), any(Options.class))).thenReturn(ldapFactory);
+        scheduler = new MockScheduler();
 
         // Create heart beat connection factory.
-        scheduler = new MockScheduler();
-        hbcf = new HeartBeatConnectionFactory(factory, 10000, 100, TimeUnit.MILLISECONDS, HEARTBEAT, scheduler);
+        hbcf = new LDAPConnectionFactory("dummyHost",
+                                         1389,
+                                         Options.defaultOptions()
+                                                .set(TRANSPORT_PROVIDER_INSTANCE, provider)
+                                                .set(HEARTBEAT_ENABLED, true)
+                                                .set(HEARTBEAT_TIMEOUT, duration("100 ms"))
+                                                .set(HEARTBEAT_SEARCH_REQUEST, HEARTBEAT)
+                                                .set(HEARTBEAT_SCHEDULER, scheduler));
 
         // Set initial time stamp.
         hbcf.timeService = mockTimeService(0);
     }
 
     private BindResultLdapPromiseImpl mockBindAsyncResponse() {
-        final BindResultLdapPromiseImpl bindPromise = newBindLdapPromise(-1, null, null, null, connection);
-        doAnswer(new Answer<LdapPromise<BindResult>>() {
-            @Override
-            public LdapPromise<BindResult> answer(final InvocationOnMock invocation) throws Throwable {
-                return bindPromise;
-            }
-        }).when(connection).bindAsync(any(BindRequest.class), any(IntermediateResponseHandler.class));
-
+        final BindResultLdapPromiseImpl bindPromise = newBindLdapPromise(-1, null, null, null, ldapConnection);
+        when(ldapConnection.bindAsync(any(BindRequest.class), any(IntermediateResponseHandler.class)))
+                .thenReturn(bindPromise);
         return bindPromise;
     }
 
-    private Connection mockHeartBeatResponse(final Connection mockConnection,
-            final List<ConnectionEventListener> listeners, final ResultCode resultCode) {
-        Answer<LdapPromise<Result>> answer = new Answer<LdapPromise<Result>>() {
+    //
+    private void mockHeartBeatResponse(
+            final LDAPConnectionImpl mockConnection,
+            final List<ConnectionEventListener> listeners,
+            final ResultCode resultCode) {
+        // @Checkstyle:off
+        when(mockConnection.searchAsync(any(SearchRequest.class),
+                                        any(IntermediateResponseHandler.class),
+                                        any(SearchResultHandler.class))).thenAnswer(new Answer<LdapPromise<Result>>() {
             @Override
             public LdapPromise<Result> answer(final InvocationOnMock invocation) throws Throwable {
                 if (resultCode == null) {
@@ -440,16 +482,40 @@
                     return newSuccessfulLdapPromise(newResult(resultCode));
                 }
             }
-        };
-
-        doAnswer(answer).when(mockConnection).searchAsync(any(SearchRequest.class), any(SearchResultHandler.class));
-        doAnswer(answer).when(mockConnection).searchAsync(any(SearchRequest.class),
-            any(IntermediateResponseHandler.class), any(SearchResultHandler.class));
-
-        return mockConnection;
+        });
+        // @Checkstyle:on
     }
 
-    private void verifyHeartBeatSent(final Connection connection, final int times) {
-        verify(connection, times(times)).searchAsync(same(HEARTBEAT), any(SearchResultHandler.class));
+    private void verifyHeartBeatSent(final LDAPConnectionImpl connection, final int times) {
+        verify(connection, times(times)).searchAsync(same(HEARTBEAT),
+                                                     any(IntermediateResponseHandler.class),
+                                                     any(SearchResultHandler.class));
+    }
+
+    private static LDAPConnectionImpl mockLDAPConnectionImpl(final List<ConnectionEventListener> listeners) {
+        final LDAPConnectionImpl mockConnection = mock(LDAPConnectionImpl.class);
+
+        // Handle listener registration / deregistration in mock connection.
+        doAnswer(new Answer<Void>() {
+            @Override
+            public Void answer(final InvocationOnMock invocation) throws Throwable {
+                final ConnectionEventListener listener =
+                        (ConnectionEventListener) invocation.getArguments()[0];
+                listeners.add(listener);
+                return null;
+            }
+        }).when(mockConnection).addConnectionEventListener(any(ConnectionEventListener.class));
+
+        doAnswer(new Answer<Void>() {
+            @Override
+            public Void answer(final InvocationOnMock invocation) throws Throwable {
+                final ConnectionEventListener listener =
+                        (ConnectionEventListener) invocation.getArguments()[0];
+                listeners.remove(listener);
+                return null;
+            }
+        }).when(mockConnection).removeConnectionEventListener(any(ConnectionEventListener.class));
+
+        return mockConnection;
     }
 }
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/LDAPServer.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/LDAPServer.java
index 212b203..25c6e94 100644
--- a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/LDAPServer.java
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/LDAPServer.java
@@ -550,7 +550,7 @@
         }
         sslContext = new SSLContextBuilder().getSSLContext();
         listener = new LDAPListener(findFreeSocketAddress(), getInstance(),
-                        Options.defaultOptions().set(BACKLOG, 4096));
+                        Options.defaultOptions().set(CONNECT_MAX_BACKLOG, 4096));
         isRunning = true;
     }
 
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/spi/BasicLDAPConnectionFactory.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/spi/BasicLDAPConnectionFactory.java
index 4596658..ed038ae 100644
--- a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/spi/BasicLDAPConnectionFactory.java
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/spi/BasicLDAPConnectionFactory.java
@@ -28,37 +28,23 @@
 
 import java.net.InetSocketAddress;
 
-import org.forgerock.opendj.ldap.Connection;
 import org.forgerock.opendj.ldap.LdapException;
-import org.forgerock.opendj.ldap.ResultCode;
 import org.forgerock.util.Options;
 import org.forgerock.util.promise.Promise;
 
-import static org.forgerock.opendj.ldap.LdapException.*;
 import static org.forgerock.util.promise.Promises.*;
 import static org.mockito.Mockito.*;
 
 /**
  * Basic LDAP connection factory implementation to use for tests only.
  */
-public final class BasicLDAPConnectionFactory implements LDAPConnectionFactoryImpl {
-
-    private final Options options;
+final class BasicLDAPConnectionFactory implements LDAPConnectionFactoryImpl {
     private final String host;
     private final int port;
 
-    /**
-     * Creates a new LDAP connection factory which does nothing.
-     *  @param host
-     *            The address of the Directory Server to connect to.
-     * @param port
-     *            The port of the Directory Server to connect to.
-     * @param options
-     */
-    public BasicLDAPConnectionFactory(final String host, final int port, final Options options) {
+    BasicLDAPConnectionFactory(final String host, final int port, final Options options) {
         this.host = host;
         this.port = port;
-        this.options = Options.copyOf(options);
     }
 
     @Override
@@ -67,25 +53,11 @@
     }
 
     @Override
-    public Connection getConnection() throws LdapException {
-        try {
-            return getConnectionAsync().getOrThrow();
-        } catch (final InterruptedException e) {
-            throw newLdapException(ResultCode.CLIENT_SIDE_USER_CANCELLED, e);
-        }
+    public Promise<LDAPConnectionImpl, LdapException> getConnectionAsync() {
+        return newResultPromise(mock(LDAPConnectionImpl.class));
     }
 
     @Override
-    public Promise<Connection, LdapException> getConnectionAsync() {
-        return newResultPromise(mock(Connection.class));
-    }
-
-    /**
-     * Returns the address of the Directory Server.
-     *
-     * @return The address of the Directory Server.
-     */
-    @Override
     public InetSocketAddress getSocketAddress() {
         return new InetSocketAddress(host, port);
     }
@@ -104,8 +76,4 @@
     public String toString() {
         return getClass().getSimpleName() + "(" + host + ':' + port + ')';
     }
-
-    Options getLDAPOptions() {
-        return options;
-    }
 }
diff --git a/opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/GrizzlyLDAPConnection.java b/opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/GrizzlyLDAPConnection.java
index a2c2ab4..0c31689 100644
--- a/opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/GrizzlyLDAPConnection.java
+++ b/opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/GrizzlyLDAPConnection.java
@@ -26,11 +26,21 @@
  */
 package org.forgerock.opendj.grizzly;
 
+import static com.forgerock.opendj.grizzly.GrizzlyMessages.LDAP_CONNECTION_BIND_OR_START_TLS_CONNECTION_TIMEOUT;
+import static com.forgerock.opendj.grizzly.GrizzlyMessages.LDAP_CONNECTION_BIND_OR_START_TLS_REQUEST_TIMEOUT;
+import static com.forgerock.opendj.grizzly.GrizzlyMessages.LDAP_CONNECTION_REQUEST_TIMEOUT;
+import static org.forgerock.opendj.ldap.LDAPConnectionFactory.REQUEST_TIMEOUT;
+import static org.forgerock.opendj.ldap.LdapException.newLdapException;
+import static org.forgerock.opendj.ldap.ResultCode.CLIENT_SIDE_LOCAL_ERROR;
+import static org.forgerock.opendj.ldap.responses.Responses.newResult;
+import static org.forgerock.opendj.ldap.spi.LdapPromises.*;
+
 import java.io.IOException;
 import java.security.GeneralSecurityException;
 import java.util.List;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
 
@@ -40,7 +50,6 @@
 import org.forgerock.i18n.LocalizableMessage;
 import org.forgerock.i18n.slf4j.LocalizedLogger;
 import org.forgerock.opendj.io.LDAPWriter;
-import org.forgerock.opendj.ldap.AbstractAsynchronousConnection;
 import org.forgerock.opendj.ldap.ConnectionEventListener;
 import org.forgerock.opendj.ldap.Connections;
 import org.forgerock.opendj.ldap.IntermediateResponseHandler;
@@ -61,6 +70,7 @@
 import org.forgerock.opendj.ldap.requests.GenericBindRequest;
 import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
 import org.forgerock.opendj.ldap.requests.ModifyRequest;
+import org.forgerock.opendj.ldap.requests.Requests;
 import org.forgerock.opendj.ldap.requests.SearchRequest;
 import org.forgerock.opendj.ldap.requests.StartTLSExtendedRequest;
 import org.forgerock.opendj.ldap.requests.UnbindRequest;
@@ -71,24 +81,23 @@
 import org.forgerock.opendj.ldap.responses.Result;
 import org.forgerock.opendj.ldap.spi.BindResultLdapPromiseImpl;
 import org.forgerock.opendj.ldap.spi.ExtendedResultLdapPromiseImpl;
+import org.forgerock.opendj.ldap.spi.LDAPConnectionImpl;
 import org.forgerock.opendj.ldap.spi.ResultLdapPromiseImpl;
 import org.forgerock.opendj.ldap.spi.SearchResultLdapPromiseImpl;
 import org.forgerock.util.Options;
 import org.forgerock.util.Reject;
+import org.forgerock.util.promise.Promise;
+import org.forgerock.util.promise.PromiseImpl;
+import org.forgerock.util.time.Duration;
 import org.glassfish.grizzly.CompletionHandler;
+import org.glassfish.grizzly.EmptyCompletionHandler;
 import org.glassfish.grizzly.filterchain.Filter;
 import org.glassfish.grizzly.filterchain.FilterChain;
 import org.glassfish.grizzly.ssl.SSLEngineConfigurator;
 import org.glassfish.grizzly.ssl.SSLFilter;
 
-import static org.forgerock.opendj.ldap.LDAPConnectionFactory.*;
-import static org.forgerock.opendj.ldap.LdapException.*;
-import static org.forgerock.opendj.ldap.spi.LdapPromises.*;
-
-import static com.forgerock.opendj.grizzly.GrizzlyMessages.*;
-
 /** LDAP connection implementation. */
-final class GrizzlyLDAPConnection extends AbstractAsynchronousConnection implements TimeoutEventListener {
+final class GrizzlyLDAPConnection implements LDAPConnectionImpl, TimeoutEventListener {
     /**
      * A dummy SSL client engine configurator as SSLFilter only needs client
      * config. This prevents Grizzly from needlessly using JVM defaults which
@@ -112,6 +121,7 @@
     private final AtomicInteger nextMsgID = new AtomicInteger(1);
     private final GrizzlyLDAPConnectionFactory factory;
     private final ConcurrentHashMap<Integer, ResultLdapPromiseImpl<?, ?>> pendingRequests = new ConcurrentHashMap<>();
+    private final long requestTimeoutMS;
     private final Object stateLock = new Object();
     /** Guarded by stateLock. */
     private Result connectionInvalidReason;
@@ -133,6 +143,8 @@
             final GrizzlyLDAPConnectionFactory factory) {
         this.connection = connection;
         this.factory = factory;
+        final Duration requestTimeout = factory.getLDAPOptions().get(REQUEST_TIMEOUT);
+        this.requestTimeoutMS = requestTimeout.isUnlimited() ? 0 : requestTimeout.to(TimeUnit.MILLISECONDS);
     }
 
     @Override
@@ -306,6 +318,11 @@
     }
 
     @Override
+    public void close() {
+        close(Requests.newUnbindRequest(), null);
+    }
+
+    @Override
     public void close(final UnbindRequest request, final String reason) {
         // FIXME: I18N need to internationalize this message.
         Reject.ifNull(request);
@@ -548,17 +565,16 @@
 
     @Override
     public long handleTimeout(final long currentTime) {
-        final long timeout = factory.getLDAPOptions().get(TIMEOUT_IN_MILLISECONDS);
-        if (timeout <= 0) {
+        if (requestTimeoutMS <= 0) {
             return 0;
         }
 
-        long delay = timeout;
+        long delay = requestTimeoutMS;
         for (final ResultLdapPromiseImpl<?, ?> promise : pendingRequests.values()) {
             if (promise == null || !promise.checkForTimeout()) {
                 continue;
             }
-            final long diff = (promise.getTimestamp() + timeout) - currentTime;
+            final long diff = (promise.getTimestamp() + requestTimeoutMS) - currentTime;
             if (diff > 0) {
                 // Will expire in diff milliseconds.
                 delay = Math.min(delay, diff);
@@ -577,17 +593,17 @@
                 logger.debug(LocalizableMessage.raw("Failing bind or StartTLS request due to timeout %s"
                         + "(connection will be invalidated): ", promise));
                 final Result result = Responses.newResult(ResultCode.CLIENT_SIDE_TIMEOUT).setDiagnosticMessage(
-                        LDAP_CONNECTION_BIND_OR_START_TLS_REQUEST_TIMEOUT.get(timeout).toString());
+                        LDAP_CONNECTION_BIND_OR_START_TLS_REQUEST_TIMEOUT.get(requestTimeoutMS).toString());
                 promise.adaptErrorResult(result);
 
                 // Fail the connection.
                 final Result errorResult = Responses.newResult(ResultCode.CLIENT_SIDE_TIMEOUT).setDiagnosticMessage(
-                        LDAP_CONNECTION_BIND_OR_START_TLS_CONNECTION_TIMEOUT.get(timeout).toString());
+                        LDAP_CONNECTION_BIND_OR_START_TLS_CONNECTION_TIMEOUT.get(requestTimeoutMS).toString());
                 connectionErrorOccurred(errorResult);
             } else {
                 logger.debug(LocalizableMessage.raw("Failing request due to timeout: %s", promise));
                 final Result result = Responses.newResult(ResultCode.CLIENT_SIDE_TIMEOUT).setDiagnosticMessage(
-                        LDAP_CONNECTION_REQUEST_TIMEOUT.get(timeout).toString());
+                        LDAP_CONNECTION_REQUEST_TIMEOUT.get(requestTimeoutMS).toString());
                 promise.adaptErrorResult(result);
 
                 /*
@@ -607,7 +623,7 @@
 
     @Override
     public long getTimeout() {
-        return factory.getLDAPOptions().get(TIMEOUT_IN_MILLISECONDS);
+        return requestTimeoutMS;
     }
 
     /**
@@ -769,8 +785,37 @@
         bindOrStartTLSInProgress.set(state);
     }
 
+    @Override
+    public Promise<Void, LdapException> enableTLS(
+            final SSLContext sslContext,
+            final List<String> sslEnabledProtocols,
+            final List<String> sslEnabledCipherSuites) {
+        final PromiseImpl<Void, LdapException> promise = PromiseImpl.create();
+        final EmptyCompletionHandler<SSLEngine> completionHandler = new EmptyCompletionHandler<SSLEngine>() {
+            @Override
+            public void completed(final SSLEngine result) {
+                promise.handleResult(null);
+            }
+
+            @Override
+            public void failed(final Throwable throwable) {
+                final Result errorResult = newResult(CLIENT_SIDE_LOCAL_ERROR)
+                        .setCause(throwable).setDiagnosticMessage("SSL handshake failed");
+                connectionErrorOccurred(errorResult);
+                promise.handleException(newLdapException(errorResult));
+            }
+        };
+
+        try {
+            startTLS(sslContext, sslEnabledProtocols, sslEnabledCipherSuites, completionHandler);
+        } catch (final IOException e) {
+            completionHandler.failed(e);
+        }
+        return promise;
+    }
+
     void startTLS(final SSLContext sslContext, final List<String> protocols, final List<String> cipherSuites,
-            final CompletionHandler<SSLEngine> completionHandler) throws IOException {
+                  final CompletionHandler<SSLEngine> completionHandler) throws IOException {
         synchronized (stateLock) {
             if (isTLSEnabled()) {
                 throw new IllegalStateException("TLS already enabled");
diff --git a/opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/GrizzlyLDAPConnectionFactory.java b/opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/GrizzlyLDAPConnectionFactory.java
index 13135c1..912c1bf 100644
--- a/opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/GrizzlyLDAPConnectionFactory.java
+++ b/opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/GrizzlyLDAPConnectionFactory.java
@@ -27,32 +27,35 @@
 
 package org.forgerock.opendj.grizzly;
 
-import java.io.IOException;
+import static com.forgerock.opendj.grizzly.GrizzlyMessages.LDAP_CONNECTION_CONNECT_TIMEOUT;
+import static org.forgerock.opendj.grizzly.DefaultTCPNIOTransport.DEFAULT_TRANSPORT;
+import static org.forgerock.opendj.grizzly.GrizzlyUtils.buildFilterChain;
+import static org.forgerock.opendj.grizzly.GrizzlyUtils.configureConnection;
+import static org.forgerock.opendj.ldap.LDAPConnectionFactory.CONNECT_TIMEOUT;
+import static org.forgerock.opendj.ldap.LDAPConnectionFactory.LDAP_DECODE_OPTIONS;
+import static org.forgerock.opendj.ldap.LdapException.newLdapException;
+import static org.forgerock.opendj.ldap.TimeoutChecker.TIMEOUT_CHECKER;
+
 import java.net.InetSocketAddress;
-import java.util.List;
 import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
 
-import javax.net.ssl.SSLEngine;
-
 import org.forgerock.i18n.slf4j.LocalizedLogger;
-import org.forgerock.opendj.ldap.Connection;
 import org.forgerock.opendj.ldap.LdapException;
 import org.forgerock.opendj.ldap.ResultCode;
 import org.forgerock.opendj.ldap.TimeoutChecker;
 import org.forgerock.opendj.ldap.TimeoutEventListener;
-import org.forgerock.opendj.ldap.requests.Requests;
-import org.forgerock.opendj.ldap.requests.StartTLSExtendedRequest;
-import org.forgerock.opendj.ldap.responses.ExtendedResult;
 import org.forgerock.opendj.ldap.spi.LDAPConnectionFactoryImpl;
+import org.forgerock.opendj.ldap.spi.LDAPConnectionImpl;
+import org.forgerock.util.Option;
 import org.forgerock.util.Options;
-import org.forgerock.util.promise.ExceptionHandler;
 import org.forgerock.util.promise.Promise;
 import org.forgerock.util.promise.PromiseImpl;
-import org.forgerock.util.promise.ResultHandler;
+import org.forgerock.util.time.Duration;
 import org.glassfish.grizzly.CompletionHandler;
-import org.glassfish.grizzly.EmptyCompletionHandler;
+import org.glassfish.grizzly.Connection;
 import org.glassfish.grizzly.SocketConnectorHandler;
 import org.glassfish.grizzly.filterchain.FilterChain;
 import org.glassfish.grizzly.nio.transport.TCPNIOConnectorHandler;
@@ -60,14 +63,6 @@
 
 import com.forgerock.opendj.util.ReferenceCountedObject;
 
-import static org.forgerock.opendj.grizzly.DefaultTCPNIOTransport.*;
-import static org.forgerock.opendj.grizzly.GrizzlyUtils.*;
-import static org.forgerock.opendj.ldap.LDAPConnectionFactory.*;
-import static org.forgerock.opendj.ldap.LdapException.*;
-import static org.forgerock.opendj.ldap.TimeoutChecker.*;
-
-import static com.forgerock.opendj.grizzly.GrizzlyMessages.*;
-
 /**
  * LDAP connection factory implementation using Grizzly for transport.
  */
@@ -78,12 +73,11 @@
      * Adapts a Grizzly connection completion handler to an LDAP connection promise.
      */
     @SuppressWarnings("rawtypes")
-    private final class CompletionHandlerAdapter implements
-            CompletionHandler<org.glassfish.grizzly.Connection>, TimeoutEventListener {
-        private final PromiseImpl<Connection, LdapException> promise;
+    private final class CompletionHandlerAdapter implements CompletionHandler<Connection>, TimeoutEventListener {
+        private final PromiseImpl<LDAPConnectionImpl, LdapException> promise;
         private final long timeoutEndTime;
 
-        private CompletionHandlerAdapter(final PromiseImpl<Connection, LdapException> promise) {
+        private CompletionHandlerAdapter(final PromiseImpl<LDAPConnectionImpl, LdapException> promise) {
             this.promise = promise;
             final long timeoutMS = getTimeout();
             this.timeoutEndTime = timeoutMS > 0 ? System.currentTimeMillis() + timeoutMS : 0;
@@ -96,63 +90,13 @@
         }
 
         @Override
-        public void completed(final org.glassfish.grizzly.Connection result) {
+        public void completed(final Connection result) {
             // Adapt the connection.
             final GrizzlyLDAPConnection connection = adaptConnection(result);
-
-            // Plain connection.
-            if (options.get(SSL_CONTEXT) == null) {
-                thenOnResult(connection);
-                return;
-            }
-
-            // Start TLS or install SSL layer asynchronously.
-
-            // Give up immediately if the promise has been cancelled or timed out.
-            if (promise.isDone()) {
-                timeoutChecker.get().removeListener(this);
+            timeoutChecker.get().removeListener(this);
+            if (!promise.tryHandleResult(connection)) {
+                // The connection has been either cancelled or it has timed out.
                 connection.close();
-                return;
-            }
-
-            List<String> protocols = options.get(ENABLED_PROTOCOLS);
-            List<String> suites = options.get(ENABLED_CIPHER_SUITES);
-            if (options.get(USE_STARTTLS)) {
-                // Chain StartTLS extended request.
-                final StartTLSExtendedRequest startTLS =
-                        Requests.newStartTLSExtendedRequest(options.get(SSL_CONTEXT));
-                startTLS.addEnabledCipherSuite(suites.toArray(new String[suites.size()]));
-                startTLS.addEnabledProtocol(protocols.toArray(new String[protocols.size()]));
-
-                connection.extendedRequestAsync(startTLS).thenOnResult(new ResultHandler<ExtendedResult>() {
-                    @Override
-                    public void handleResult(final ExtendedResult result) {
-                        thenOnResult(connection);
-                    }
-                }).thenOnException(new ExceptionHandler<LdapException>() {
-                    @Override
-                    public void handleException(final LdapException error) {
-                        onException(connection, error);
-                    }
-                });
-            } else {
-                // Install SSL/TLS layer.
-                try {
-                    connection.startTLS(options.get(SSL_CONTEXT), protocols, suites,
-                        new EmptyCompletionHandler<SSLEngine>() {
-                            @Override
-                            public void completed(final SSLEngine result) {
-                                thenOnResult(connection);
-                            }
-
-                            @Override
-                            public void failed(final Throwable throwable) {
-                                onException(connection, throwable);
-                            }
-                        });
-                } catch (final IOException e) {
-                    onException(connection, e);
-                }
             }
         }
 
@@ -165,12 +109,11 @@
         }
 
         @Override
-        public void updated(final org.glassfish.grizzly.Connection result) {
+        public void updated(final Connection result) {
             // Ignore this.
         }
 
-        private GrizzlyLDAPConnection adaptConnection(
-                final org.glassfish.grizzly.Connection<?> connection) {
+        private GrizzlyLDAPConnection adaptConnection(final Connection<?> connection) {
             configureConnection(connection, logger, options);
 
             final GrizzlyLDAPConnection ldapConnection =
@@ -184,7 +127,6 @@
             if (!(t instanceof LdapException) && t instanceof ExecutionException) {
                 t = t.getCause() != null ? t.getCause() : t;
             }
-
             if (t instanceof LdapException) {
                 return (LdapException) t;
             } else {
@@ -192,21 +134,6 @@
             }
         }
 
-        private void onException(final GrizzlyLDAPConnection connection, final Throwable t) {
-            // Abort connection attempt due to error.
-            timeoutChecker.get().removeListener(this);
-            promise.handleException(adaptConnectionException(t));
-            connection.close();
-        }
-
-        private void thenOnResult(final GrizzlyLDAPConnection connection) {
-            timeoutChecker.get().removeListener(this);
-            if (!promise.tryHandleResult(connection)) {
-                // The connection has been either cancelled or it has timed out.
-                connection.close();
-            }
-        }
-
         @Override
         public long handleTimeout(final long currentTime) {
             if (timeoutEndTime == 0) {
@@ -222,7 +149,8 @@
 
         @Override
         public long getTimeout() {
-            return options.get(CONNECT_TIMEOUT_IN_MILLISECONDS);
+            final Duration duration = options.get(CONNECT_TIMEOUT);
+            return duration.isUnlimited() ? 0L : duration.to(TimeUnit.MILLISECONDS);
         }
     }
 
@@ -245,48 +173,32 @@
     private final AtomicBoolean isClosed = new AtomicBoolean();
 
     private final ReferenceCountedObject<TCPNIOTransport>.Reference transport;
-    private final ReferenceCountedObject<TimeoutChecker>.Reference timeoutChecker = TIMEOUT_CHECKER
-            .acquire();
+    private final ReferenceCountedObject<TimeoutChecker>.Reference timeoutChecker = TIMEOUT_CHECKER.acquire();
 
     /**
-     * Creates a new LDAP connection factory based on Grizzly which can be used
-     * to create connections to the Directory Server at the provided host and
-     * port address using provided connection options.
-     *  @param host
-     *            The hostname of the Directory Server to connect to.
+     * Grizzly TCP Transport NIO implementation to use for connections. If {@code null}, default transport will be
+     * used.
+     */
+    public static final Option<TCPNIOTransport> GRIZZLY_TRANSPORT = Option.of(TCPNIOTransport.class, null);
+
+    /**
+     * Creates a new LDAP connection factory based on Grizzly which can be used to create connections to the Directory
+     * Server at the provided host and port address using provided connection options.
+     *
+     * @param host
+     *         The hostname of the Directory Server to connect to.
      * @param port
-     *            The port number of the Directory Server to connect to.
+     *         The port number of the Directory Server to connect to.
      * @param options
-     *            The LDAP connection options to use when creating connections.
+     *         The LDAP connection options to use when creating connections.
      */
     public GrizzlyLDAPConnectionFactory(final String host, final int port, final Options options) {
-        this(host, port, options, null);
-    }
-
-    /**
-     * Creates a new LDAP connection factory based on Grizzly which can be used
-     * to create connections to the Directory Server at the provided host and
-     * port address using provided connection options and provided TCP
-     * transport.
-     *  @param host
-     *            The hostname of the Directory Server to connect to.
-     * @param port
-     *            The port number of the Directory Server to connect to.
-     * @param options
- *            The LDAP connection options to use when creating connections.
-     * @param transport
-*            Grizzly TCP Transport NIO implementation to use for
-*            connections. If {@code null}, default transport will be used.
-     */
-    public GrizzlyLDAPConnectionFactory(final String host, final int port, final Options options,
-                                        TCPNIOTransport transport) {
-        this.transport = DEFAULT_TRANSPORT.acquireIfNull(transport);
+        this.transport = DEFAULT_TRANSPORT.acquireIfNull(options.get(GRIZZLY_TRANSPORT));
         this.host = host;
         this.port = port;
         this.options = options;
-        this.clientFilter = new LDAPClientFilter(options.get(DECODE_OPTIONS), 0);
-        this.defaultFilterChain =
-                buildFilterChain(this.transport.get().getProcessor(), clientFilter);
+        this.clientFilter = new LDAPClientFilter(options.get(LDAP_DECODE_OPTIONS), 0);
+        this.defaultFilterChain = buildFilterChain(this.transport.get().getProcessor(), clientFilter);
     }
 
     @Override
@@ -297,21 +209,12 @@
     }
 
     @Override
-    public Connection getConnection() throws LdapException {
-        try {
-            return getConnectionAsync().getOrThrow();
-        } catch (final InterruptedException e) {
-            throw newLdapException(ResultCode.CLIENT_SIDE_USER_CANCELLED, e);
-        }
-    }
-
-    @Override
-    public Promise<Connection, LdapException> getConnectionAsync() {
+    public Promise<LDAPConnectionImpl, LdapException> getConnectionAsync() {
         acquireTransportAndTimeoutChecker(); // Protect resources.
-        final SocketConnectorHandler connectorHandler =
-                TCPNIOConnectorHandler.builder(transport.get()).processor(defaultFilterChain)
-                        .build();
-        final PromiseImpl<Connection, LdapException> promise = PromiseImpl.create();
+        final SocketConnectorHandler connectorHandler = TCPNIOConnectorHandler.builder(transport.get())
+                                                                              .processor(defaultFilterChain)
+                                                                              .build();
+        final PromiseImpl<LDAPConnectionImpl, LdapException> promise = PromiseImpl.create();
         connectorHandler.connect(getSocketAddress(), new CompletionHandlerAdapter(promise));
         return promise;
     }
@@ -331,11 +234,6 @@
         return port;
     }
 
-    @Override
-    public String toString() {
-        return getClass().getSimpleName() + "(" + host + ':' + port + ')';
-    }
-
     TimeoutChecker getTimeoutChecker() {
         return timeoutChecker.get();
     }
diff --git a/opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/GrizzlyLDAPListener.java b/opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/GrizzlyLDAPListener.java
index 837ff42..8af1e21 100644
--- a/opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/GrizzlyLDAPListener.java
+++ b/opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/GrizzlyLDAPListener.java
@@ -106,12 +106,12 @@
         this.connectionFactory = factory;
         this.options = Options.copyOf(options);
         final LDAPServerFilter serverFilter =
-                new LDAPServerFilter(this, options.get(DECODE_OPTIONS), options.get(MAX_REQUEST_SIZE_BYTES));
+                new LDAPServerFilter(this, options.get(LDAP_DECODE_OPTIONS), options.get(REQUEST_MAX_SIZE_IN_BYTES));
         final FilterChain ldapChain =
                 GrizzlyUtils.buildFilterChain(this.transport.get().getProcessor(), serverFilter);
         final TCPNIOBindingHandler bindingHandler =
                 TCPNIOBindingHandler.builder(this.transport.get()).processor(ldapChain).build();
-        this.serverConnection = bindingHandler.bind(address, options.get(BACKLOG));
+        this.serverConnection = bindingHandler.bind(address, options.get(CONNECT_MAX_BACKLOG));
 
         /*
          * Get the socket address now, ensuring that the host is the same as the
diff --git a/opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/GrizzlyUtils.java b/opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/GrizzlyUtils.java
index 5b4012a..27a451d 100644
--- a/opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/GrizzlyUtils.java
+++ b/opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/GrizzlyUtils.java
@@ -198,9 +198,9 @@
         final SocketChannel channel = (SocketChannel) ((TCPNIOConnection) connection).getChannel();
         final Socket socket = channel.socket();
         final boolean tcpNoDelay = options.get(TCP_NO_DELAY);
-        final boolean keepAlive = options.get(KEEPALIVE);
-        final boolean reuseAddress = options.get(REUSE_ADDRESS);
-        final int linger = options.get(LINGER);
+        final boolean keepAlive = options.get(SO_KEEPALIVE);
+        final boolean reuseAddress = options.get(SO_REUSE_ADDRESS);
+        final int linger = options.get(SO_LINGER_IN_SECONDS);
         try {
             socket.setTcpNoDelay(tcpNoDelay);
         } catch (final SocketException e) {
diff --git a/opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/LDAPClientFilter.java b/opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/LDAPClientFilter.java
index d84c3e6..ce328ef 100644
--- a/opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/LDAPClientFilter.java
+++ b/opendj-sdk/opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/LDAPClientFilter.java
@@ -368,7 +368,7 @@
         private <R extends ExtendedResult> void handleExtendedResult0(
                 final GrizzlyLDAPConnection conn, final ExtendedResultLdapPromiseImpl<R> promise,
                 final ExtendedResult result) throws DecodeException {
-            final R decodedResponse = promise.decodeResult(result, conn.getLDAPOptions().get(DECODE_OPTIONS));
+            final R decodedResponse = promise.decodeResult(result, conn.getLDAPOptions().get(LDAP_DECODE_OPTIONS));
 
             if (result.getResultCode() == ResultCode.SUCCESS
                     && promise.getRequest() instanceof StartTLSExtendedRequest) {
diff --git a/opendj-sdk/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/ConnectionFactoryTestCase.java b/opendj-sdk/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/ConnectionFactoryTestCase.java
index ae87862..8e49f98 100644
--- a/opendj-sdk/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/ConnectionFactoryTestCase.java
+++ b/opendj-sdk/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/ConnectionFactoryTestCase.java
@@ -27,8 +27,31 @@
 
 package org.forgerock.opendj.grizzly;
 
+import static java.util.Arrays.asList;
+import static java.util.Collections.singletonList;
+import static org.fest.assertions.Assertions.assertThat;
+import static org.forgerock.opendj.ldap.Connections.newFailoverLoadBalancer;
+import static org.forgerock.opendj.ldap.Connections.newFixedConnectionPool;
+import static org.forgerock.opendj.ldap.Connections.newRoundRobinLoadBalancer;
+import static org.forgerock.opendj.ldap.LDAPConnectionFactory.*;
+import static org.forgerock.opendj.ldap.LdapException.newLdapException;
+import static org.forgerock.opendj.ldap.TestCaseUtils.findFreeSocketAddress;
+import static org.forgerock.opendj.ldap.TestCaseUtils.getServerSocketAddress;
+import static org.forgerock.opendj.ldap.requests.Requests.newCRAMMD5SASLBindRequest;
+import static org.forgerock.opendj.ldap.requests.Requests.newDigestMD5SASLBindRequest;
+import static org.forgerock.opendj.ldap.requests.Requests.newSimpleBindRequest;
+import static org.forgerock.opendj.ldap.spi.LdapPromises.newSuccessfulLdapPromise;
+import static org.forgerock.util.Options.defaultOptions;
+import static org.forgerock.util.time.Duration.duration;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
+
 import java.net.InetSocketAddress;
-import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.concurrent.Callable;
 import java.util.concurrent.CountDownLatch;
@@ -46,18 +69,16 @@
 import org.forgerock.opendj.ldap.ConnectionPool;
 import org.forgerock.opendj.ldap.Connections;
 import org.forgerock.opendj.ldap.DN;
-import org.forgerock.opendj.ldap.LdapException;
-import org.forgerock.opendj.ldap.FailoverLoadBalancingAlgorithm;
-import org.forgerock.opendj.ldap.LdapPromise;
 import org.forgerock.opendj.ldap.IntermediateResponseHandler;
 import org.forgerock.opendj.ldap.LDAPClientContext;
 import org.forgerock.opendj.ldap.LDAPConnectionFactory;
 import org.forgerock.opendj.ldap.LDAPListener;
 import org.forgerock.opendj.ldap.LDAPServer;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.LdapPromise;
+import org.forgerock.opendj.ldap.LdapResultHandler;
 import org.forgerock.opendj.ldap.MockConnectionEventListener;
 import org.forgerock.opendj.ldap.ResultCode;
-import org.forgerock.opendj.ldap.LdapResultHandler;
-import org.forgerock.opendj.ldap.RoundRobinLoadBalancingAlgorithm;
 import org.forgerock.opendj.ldap.SSLContextBuilder;
 import org.forgerock.opendj.ldap.SdkTestCase;
 import org.forgerock.opendj.ldap.SearchScope;
@@ -66,9 +87,11 @@
 import org.forgerock.opendj.ldap.TestCaseUtils;
 import org.forgerock.opendj.ldap.TrustManagers;
 import org.forgerock.opendj.ldap.requests.BindRequest;
+import org.forgerock.opendj.ldap.requests.CRAMMD5SASLBindRequest;
 import org.forgerock.opendj.ldap.requests.DigestMD5SASLBindRequest;
 import org.forgerock.opendj.ldap.requests.Requests;
 import org.forgerock.opendj.ldap.requests.SearchRequest;
+import org.forgerock.opendj.ldap.requests.SimpleBindRequest;
 import org.forgerock.opendj.ldap.responses.BindResult;
 import org.forgerock.opendj.ldap.responses.Responses;
 import org.forgerock.opendj.ldap.responses.SearchResultEntry;
@@ -86,16 +109,6 @@
 import org.testng.annotations.DataProvider;
 import org.testng.annotations.Test;
 
-import static org.fest.assertions.Assertions.*;
-import static org.forgerock.opendj.ldap.Connections.*;
-import static org.forgerock.opendj.ldap.LdapException.*;
-import static org.forgerock.opendj.ldap.LDAPConnectionFactory.ENABLED_CIPHER_SUITES;
-import static org.forgerock.opendj.ldap.TestCaseUtils.*;
-import static org.forgerock.opendj.ldap.spi.LdapPromises.*;
-import static org.mockito.Matchers.*;
-import static org.mockito.Mockito.*;
-import static org.testng.Assert.*;
-
 /**
  * Tests the {@code ConnectionFactory} classes.
  */
@@ -147,54 +160,59 @@
             "uid=user.0,ou=people,o=test", SearchScope.BASE_OBJECT, "(objectclass=*)", "cn");
 
         InetSocketAddress serverAddress = getServerSocketAddress();
-        factories[0][0] =
-                Connections.newHeartBeatConnectionFactory(new LDAPConnectionFactory(
-                        serverAddress.getHostName(), serverAddress.getPort()),
-                        1000, 2000, TimeUnit.MILLISECONDS, request);
+        factories[0][0] = new LDAPConnectionFactory(serverAddress.getHostName(),
+                                                    serverAddress.getPort(),
+                                                    defaultOptions()
+                                                           .set(HEARTBEAT_ENABLED, true)
+                                                           .set(HEARTBEAT_INTERVAL, duration("1000 ms"))
+                                                           .set(HEARTBEAT_TIMEOUT, duration("2000 ms"))
+                                                           .set(HEARTBEAT_SEARCH_REQUEST, request));
 
         // InternalConnectionFactory
         factories[1][0] = Connections.newInternalConnectionFactory(LDAPServer.getInstance(), null);
 
         // AuthenticatedConnectionFactory
-        factories[2][0] =
-                Connections.newAuthenticatedConnectionFactory(new LDAPConnectionFactory(
-                                serverAddress.getHostName(), serverAddress.getPort()),
-                        Requests.newSimpleBindRequest("", new char[0]));
+        final SimpleBindRequest anon = newSimpleBindRequest("", new char[0]);
+        factories[2][0] = new LDAPConnectionFactory(serverAddress.getHostName(),
+                                                    serverAddress.getPort(),
+                                                    defaultOptions().set(AUTHN_BIND_REQUEST, anon));
 
         // AuthenticatedConnectionFactory with multi-stage SASL
-        factories[3][0] =
-                Connections.newAuthenticatedConnectionFactory(new LDAPConnectionFactory(
-                                serverAddress.getHostName(), serverAddress.getPort()),
-                        Requests.newCRAMMD5SASLBindRequest("id:user", "password".toCharArray()));
+        final CRAMMD5SASLBindRequest crammd5 = newCRAMMD5SASLBindRequest("id:user", "password".toCharArray());
+        factories[3][0] = new LDAPConnectionFactory(serverAddress.getHostName(),
+                                                    serverAddress.getPort(),
+                                                    defaultOptions().set(AUTHN_BIND_REQUEST, crammd5));
 
         // LDAPConnectionFactory with default options
         factories[4][0] = new LDAPConnectionFactory(serverAddress.getHostName(), serverAddress.getPort());
 
         // LDAPConnectionFactory with startTLS
-        SSLContext sslContext =
-                new SSLContextBuilder().setTrustManager(TrustManagers.trustAll()).getSSLContext();
-        Options options = Options.defaultOptions().set(ENABLED_CIPHER_SUITES,
-            new ArrayList<String>(Arrays.asList(
-                "SSL_DH_anon_EXPORT_WITH_DES40_CBC_SHA",
-                "SSL_DH_anon_EXPORT_WITH_RC4_40_MD5",
-                "SSL_DH_anon_WITH_3DES_EDE_CBC_SHA",
-                "SSL_DH_anon_WITH_DES_CBC_SHA",
-                "SSL_DH_anon_WITH_RC4_128_MD5",
-                "TLS_DH_anon_WITH_AES_128_CBC_SHA",
-                "TLS_DH_anon_WITH_AES_256_CBC_SHA")));
-        factories[5][0] = new LDAPConnectionFactory(serverAddress.getHostName(), serverAddress.getPort(), options);
+        SSLContext sslContext = new SSLContextBuilder().setTrustManager(TrustManagers.trustAll()).getSSLContext();
+        final Options startTlsOptions = defaultOptions()
+                                   .set(SSL_CONTEXT, sslContext)
+                                   .set(SSL_USE_STARTTLS, true)
+                                   .set(SSL_ENABLED_CIPHER_SUITES,
+                                        asList("SSL_DH_anon_EXPORT_WITH_DES40_CBC_SHA",
+                                                      "SSL_DH_anon_EXPORT_WITH_RC4_40_MD5",
+                                                      "SSL_DH_anon_WITH_3DES_EDE_CBC_SHA",
+                                                      "SSL_DH_anon_WITH_DES_CBC_SHA",
+                                                      "SSL_DH_anon_WITH_RC4_128_MD5",
+                                                      "TLS_DH_anon_WITH_AES_128_CBC_SHA",
+                                                      "TLS_DH_anon_WITH_AES_256_CBC_SHA"));
+        factories[5][0] = new LDAPConnectionFactory(serverAddress.getHostName(),
+                                                    serverAddress.getPort(),
+                                                    startTlsOptions);
 
         // startTLS + SASL confidentiality
         // Use IP address here so that DIGEST-MD5 host verification works if
         // local host name is not localhost (e.g. on some machines it might be
         // localhost.localdomain).
         // FIXME: enable QOP once OPENDJ-514 is fixed.
-        factories[6][0] =
-                Connections.newAuthenticatedConnectionFactory(new LDAPConnectionFactory(
-                        serverAddress.getHostName(), serverAddress.getPort(), options),
-                        Requests.newDigestMD5SASLBindRequest(
-                            "id:user", "password".toCharArray()).setCipher(
-                                DigestMD5SASLBindRequest.CIPHER_LOW));
+        final DigestMD5SASLBindRequest digestmd5 = newDigestMD5SASLBindRequest("id:user", "password".toCharArray())
+                .setCipher(DigestMD5SASLBindRequest.CIPHER_LOW);
+        factories[6][0] = new LDAPConnectionFactory(serverAddress.getHostName(),
+                                                    serverAddress.getPort(),
+                                                    Options.copyOf(startTlsOptions).set(AUTHN_BIND_REQUEST, digestmd5));
 
         // Connection pool and load balancing tests.
         InetSocketAddress offlineSocketAddress1 = findFreeSocketAddress();
@@ -213,7 +231,7 @@
                                 serverAddress.getPort()), "online");
 
         // Connection pools.
-        factories[7][0] = Connections.newFixedConnectionPool(onlineServer, 10);
+        factories[7][0] = newFixedConnectionPool(onlineServer, 10);
 
         // Round robin.
         factories[8][0] =
@@ -251,7 +269,7 @@
                                 offlineServer1, 10), Connections.newFixedConnectionPool(
                                 onlineServer, 10))));
 
-        factories[20][0] = Connections.newFixedConnectionPool(onlineServer, 10);
+        factories[20][0] = newFixedConnectionPool(onlineServer, 10);
 
         return factories;
     }
@@ -316,14 +334,14 @@
     }
 
     /**
-     * Verifies that LDAP connections take into consideration changes to the
-     * default schema post creation. See OPENDJ-159.
-     * <p>
-     * This test is disabled because it cannot be run in parallel with rest of
-     * the test suite, because it modifies the global default schema.
+     * Verifies that LDAP connections take into consideration changes to the default schema post creation. See
+     * OPENDJ-159.
+     * <p/>
+     * This test is disabled because it cannot be run in parallel with rest of the test suite, because it modifies the
+     * global default schema.
      *
      * @throws Exception
-     *             If an unexpected error occurred.
+     *         If an unexpected error occurred.
      */
     @Test(enabled = false)
     public void testSchemaUsage() throws Exception {
@@ -366,7 +384,7 @@
      * Tests connection pool closure.
      *
      * @throws Exception
-     *             If an unexpected exception occurred.
+     *         If an unexpected exception occurred.
      */
     @Test
     public void testConnectionPoolClose() throws Exception {
@@ -405,7 +423,7 @@
             }
         });
 
-        ConnectionPool pool = Connections.newFixedConnectionPool(mockFactory, size);
+        ConnectionPool pool = newFixedConnectionPool(mockFactory, size);
         Connection[] pooledConnections = new Connection[size];
         for (int i = 0; i < size; i++) {
             pooledConnections[i] = pool.getConnection();
diff --git a/opendj-sdk/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/GrizzlyLDAPConnectionFactoryTestCase.java b/opendj-sdk/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/GrizzlyLDAPConnectionFactoryTestCase.java
index 5916ae4..fc15db1 100644
--- a/opendj-sdk/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/GrizzlyLDAPConnectionFactoryTestCase.java
+++ b/opendj-sdk/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/GrizzlyLDAPConnectionFactoryTestCase.java
@@ -74,6 +74,7 @@
 import static org.forgerock.opendj.ldap.TestCaseUtils.*;
 import static org.forgerock.opendj.ldap.requests.Requests.*;
 import static org.forgerock.opendj.ldap.LDAPConnectionFactory.*;
+import static org.forgerock.util.time.Duration.duration;
 import static org.mockito.Matchers.*;
 import static org.mockito.Mockito.*;
 
@@ -108,7 +109,7 @@
     private final LDAPListener server = createServer();
     private final InetSocketAddress socketAddress = server.getSocketAddress();
     public final ConnectionFactory factory = new LDAPConnectionFactory(socketAddress.getHostName(),
-            socketAddress.getPort(), Options.defaultOptions().set(TIMEOUT_IN_MILLISECONDS, 1L));
+            socketAddress.getPort(), Options.defaultOptions().set(REQUEST_TIMEOUT, duration("1 ms")));
     private final ConnectionFactory pool = Connections.newFixedConnectionPool(factory, 10);
     private volatile ServerConnection<Integer> serverConnection;
 
@@ -123,7 +124,7 @@
     public void testClientSideConnectTimeout() throws Exception {
         // Use an non-local unreachable network address.
         final ConnectionFactory factory = new LDAPConnectionFactory("10.20.30.40", 1389,
-            Options.defaultOptions().set(CONNECT_TIMEOUT_IN_MILLISECONDS, 1L));
+            Options.defaultOptions().set(CONNECT_TIMEOUT, duration("1 ms")));
         try {
             for (int i = 0; i < ITERATIONS; i++) {
                 final PromiseImpl<LdapException, NeverThrowsException> promise = PromiseImpl.create();
@@ -317,8 +318,8 @@
     @Test
     public void testCreateLDAPConnectionFactoryWithCustomClassLoader() throws Exception {
         // test no exception is thrown, which means transport provider is correctly loaded
-        Options options =
-                Options.defaultOptions().set(PROVIDER_CLASS_LOADER, Thread.currentThread().getContextClassLoader());
+        Options options = Options.defaultOptions()
+                                 .set(TRANSPORT_PROVIDER_CLASS_LOADER, Thread.currentThread().getContextClassLoader());
         InetSocketAddress socketAddress = findFreeSocketAddress();
         LDAPConnectionFactory factory = new LDAPConnectionFactory(socketAddress.getHostName(),
                 socketAddress.getPort(), options);
diff --git a/opendj-sdk/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/GrizzlyLDAPConnectionTestCase.java b/opendj-sdk/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/GrizzlyLDAPConnectionTestCase.java
index dae54fd..cb59139 100644
--- a/opendj-sdk/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/GrizzlyLDAPConnectionTestCase.java
+++ b/opendj-sdk/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/GrizzlyLDAPConnectionTestCase.java
@@ -27,17 +27,16 @@
 package org.forgerock.opendj.grizzly;
 
 import static org.fest.assertions.Assertions.assertThat;
+import static org.forgerock.util.time.Duration.duration;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyZeroInteractions;
 
-import static org.forgerock.opendj.ldap.LDAPConnectionFactory.TIMEOUT_IN_MILLISECONDS;
+import static org.forgerock.opendj.ldap.LDAPConnectionFactory.REQUEST_TIMEOUT;
 import java.net.InetSocketAddress;
-import java.util.concurrent.TimeUnit;
 
 import org.forgerock.opendj.ldap.Connections;
 import org.forgerock.opendj.ldap.LdapException;
-import org.forgerock.opendj.ldap.LDAPConnectionFactory;
 import org.forgerock.opendj.ldap.LDAPListener;
 import org.forgerock.opendj.ldap.RequestHandler;
 import org.forgerock.opendj.ldap.ResultCode;
@@ -93,10 +92,11 @@
          * Use a very long time out in order to prevent the timeout thread from
          * triggering the timeout.
          */
-        LDAPConnectionFactory factory = new LDAPConnectionFactory(address.getHostName(),
-                address.getPort(), Options.defaultOptions().set(
-                    TIMEOUT_IN_MILLISECONDS, TimeUnit.SECONDS.toMillis((long) 100)));
-        GrizzlyLDAPConnection connection = (GrizzlyLDAPConnection) factory.getConnection();
+        GrizzlyLDAPConnectionFactory factory = new GrizzlyLDAPConnectionFactory(address.getHostName(),
+                                                                  address.getPort(),
+                                                                  Options.defaultOptions()
+                                                                         .set(REQUEST_TIMEOUT, duration("100 ms")));
+        GrizzlyLDAPConnection connection = (GrizzlyLDAPConnection) factory.getConnectionAsync().getOrThrow();
         try {
             SearchRequest request =
                     Requests.newSearchRequest("dc=test", SearchScope.BASE_OBJECT, "(objectClass=*)");
@@ -106,7 +106,7 @@
             SearchResultHandler searchHandler = mock(SearchResultHandler.class);
             @SuppressWarnings("unchecked")
             ExceptionHandler<LdapException> exceptionHandler = mock(ExceptionHandler.class);
-            connection.searchAsync(request, searchHandler).thenOnException(exceptionHandler);
+            connection.searchAsync(request, null, searchHandler).thenOnException(exceptionHandler);
 
             // Pass in a time which is guaranteed to trigger expiration.
             connection.handleTimeout(System.currentTimeMillis() + 1000000);
diff --git a/opendj-sdk/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/GrizzlyLDAPListenerTestCase.java b/opendj-sdk/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/GrizzlyLDAPListenerTestCase.java
index 672f48f..a463997 100644
--- a/opendj-sdk/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/GrizzlyLDAPListenerTestCase.java
+++ b/opendj-sdk/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/GrizzlyLDAPListenerTestCase.java
@@ -74,9 +74,11 @@
 
 import static org.fest.assertions.Assertions.*;
 import static org.fest.assertions.Fail.*;
+import static org.forgerock.opendj.ldap.Connections.newFixedConnectionPool;
 import static org.forgerock.opendj.ldap.LdapException.*;
 import static org.forgerock.opendj.ldap.LDAPListener.*;
 import static org.forgerock.opendj.ldap.TestCaseUtils.*;
+import static org.forgerock.util.Options.defaultOptions;
 import static org.mockito.Mockito.*;
 
 /**
@@ -242,8 +244,8 @@
     @Test
     public void testCreateLDAPListenerWithCustomClassLoader() throws Exception {
         // test no exception is thrown, which means transport provider is correctly loaded
-        Options options = Options.defaultOptions().set(PROVIDER_CLASS_LOADER,
-                Thread.currentThread().getContextClassLoader());
+        Options options = defaultOptions().set(TRANSPORT_PROVIDER_CLASS_LOADER,
+                                                       Thread.currentThread().getContextClassLoader());
         LDAPListener listener = new LDAPListener(findFreeSocketAddress(),
                 mock(ServerConnectionFactory.class), options);
         listener.close();
@@ -256,7 +258,7 @@
     @Test(expectedExceptions = { ProviderNotFoundException.class },
         expectedExceptionsMessageRegExp = "^The requested provider 'unknown' .*")
     public void testCreateLDAPListenerFailureProviderNotFound() throws Exception {
-        Options options = Options.defaultOptions().set(TRANSPORT_PROVIDER, "unknown");
+        Options options = defaultOptions().set(TRANSPORT_PROVIDER, "unknown");
         LDAPListener listener = new LDAPListener(findFreeSocketAddress(), mock(ServerConnectionFactory.class), options);
         listener.close();
     }
@@ -644,7 +646,7 @@
         final MockServerConnection serverConnection = new MockServerConnection();
         final MockServerConnectionFactory factory =
                 new MockServerConnectionFactory(serverConnection);
-        final Options options = Options.defaultOptions().set(MAX_REQUEST_SIZE_BYTES, 2048);
+        final Options options = defaultOptions().set(REQUEST_MAX_SIZE_IN_BYTES, 2048);
         final LDAPListener listener = new LDAPListener(findFreeSocketAddress(), factory, options);
 
         Connection connection = null;
diff --git a/opendj-sdk/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/GrizzlyLDAPReaderWriterTestCase.java b/opendj-sdk/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/GrizzlyLDAPReaderWriterTestCase.java
index a0888a7..549cb58 100644
--- a/opendj-sdk/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/GrizzlyLDAPReaderWriterTestCase.java
+++ b/opendj-sdk/opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/GrizzlyLDAPReaderWriterTestCase.java
@@ -32,7 +32,7 @@
 import org.forgerock.opendj.io.LDAPWriter;
 import org.forgerock.util.Options;
 import org.glassfish.grizzly.memory.HeapMemoryManager;
-import static org.forgerock.opendj.ldap.LDAPConnectionFactory.DECODE_OPTIONS;
+import static org.forgerock.opendj.ldap.LDAPConnectionFactory.LDAP_DECODE_OPTIONS;
 
 /**
  * Tests for LDAPWriter / LDAPReader classes using specific implementations of
@@ -47,7 +47,7 @@
 
     @Override
     protected LDAPReader<? extends ASN1Reader> getLDAPReader() {
-        return GrizzlyUtils.createReader(Options.defaultOptions().get(DECODE_OPTIONS), 0, new HeapMemoryManager());
+        return GrizzlyUtils.createReader(Options.defaultOptions().get(LDAP_DECODE_OPTIONS), 0, new HeapMemoryManager());
     }
 
     @Override
diff --git a/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/Proxy.java b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/Proxy.java
index 84d0e6a..0baf2db 100644
--- a/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/Proxy.java
+++ b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/Proxy.java
@@ -27,7 +27,10 @@
 
 package org.forgerock.opendj.examples;
 
+import static org.forgerock.opendj.ldap.LDAPConnectionFactory.AUTHN_BIND_REQUEST;
+import static org.forgerock.opendj.ldap.LDAPConnectionFactory.HEARTBEAT_ENABLED;
 import static org.forgerock.opendj.ldap.LDAPListener.*;
+import static org.forgerock.opendj.ldap.requests.Requests.newSimpleBindRequest;
 
 import java.io.IOException;
 import java.util.LinkedList;
@@ -43,7 +46,7 @@
 import org.forgerock.opendj.ldap.RequestHandlerFactory;
 import org.forgerock.opendj.ldap.RoundRobinLoadBalancingAlgorithm;
 import org.forgerock.opendj.ldap.ServerConnectionFactory;
-import org.forgerock.opendj.ldap.requests.Requests;
+import org.forgerock.opendj.ldap.requests.BindRequest;
 import org.forgerock.util.Options;
 
 /**
@@ -90,19 +93,25 @@
         // Create load balancer.
         // --- JCite pools ---
         final List<ConnectionFactory> factories = new LinkedList<>();
+        final BindRequest bindRequest = newSimpleBindRequest(proxyDN, proxyPassword.toCharArray());
+        final Options factoryOptions = Options.defaultOptions()
+                                              .set(HEARTBEAT_ENABLED, true)
+                                              .set(AUTHN_BIND_REQUEST, bindRequest);
+
         final List<ConnectionFactory> bindFactories = new LinkedList<>();
+        final Options bindFactoryOptions = Options.defaultOptions().set(HEARTBEAT_ENABLED, true);
+
         for (int i = 4; i < args.length; i += 2) {
             final String remoteAddress = args[i];
             final int remotePort = Integer.parseInt(args[i + 1]);
 
-            factories.add(Connections.newCachedConnectionPool(Connections
-                    .newAuthenticatedConnectionFactory(Connections
-                            .newHeartBeatConnectionFactory(new LDAPConnectionFactory(remoteAddress,
-                                    remotePort)), Requests.newSimpleBindRequest(proxyDN,
-                            proxyPassword.toCharArray()))));
-            bindFactories.add(Connections.newCachedConnectionPool(Connections
-                    .newHeartBeatConnectionFactory(new LDAPConnectionFactory(remoteAddress,
-                            remotePort))));
+            factories.add(Connections.newCachedConnectionPool(new LDAPConnectionFactory(remoteAddress,
+                                                                                        remotePort,
+                                                                                        factoryOptions)));
+
+            bindFactories.add(Connections.newCachedConnectionPool(new LDAPConnectionFactory(remoteAddress,
+                                                                                            remotePort,
+                                                                                            bindFactoryOptions)));
         }
         // --- JCite pools ---
 
@@ -135,7 +144,7 @@
 
         // --- JCite listener ---
         // Create listener.
-        final Options options = Options.defaultOptions().set(BACKLOG, 4096);
+        final Options options = Options.defaultOptions().set(CONNECT_MAX_BACKLOG, 4096);
         LDAPListener listener = null;
         try {
             listener = new LDAPListener(localAddress, localPort, connectionHandler, options);
diff --git a/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/RewriterProxy.java b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/RewriterProxy.java
index 90cb493..8ebe90d 100644
--- a/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/RewriterProxy.java
+++ b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/RewriterProxy.java
@@ -27,7 +27,9 @@
 
 package org.forgerock.opendj.examples;
 
-import static org.forgerock.opendj.ldap.LDAPListener.*;
+import static org.forgerock.opendj.ldap.Connections.newCachedConnectionPool;
+import static org.forgerock.opendj.ldap.LDAPListener.CONNECT_MAX_BACKLOG;
+import static org.forgerock.opendj.ldap.requests.Requests.newSimpleBindRequest;
 
 import java.io.IOException;
 import java.util.HashSet;
@@ -40,17 +42,17 @@
 import org.forgerock.opendj.ldap.ConnectionFactory;
 import org.forgerock.opendj.ldap.Connections;
 import org.forgerock.opendj.ldap.DN;
-import org.forgerock.opendj.ldap.LdapException;
 import org.forgerock.opendj.ldap.Filter;
 import org.forgerock.opendj.ldap.IntermediateResponseHandler;
 import org.forgerock.opendj.ldap.LDAPClientContext;
 import org.forgerock.opendj.ldap.LDAPConnectionFactory;
 import org.forgerock.opendj.ldap.LDAPListener;
+import org.forgerock.opendj.ldap.LdapException;
+import org.forgerock.opendj.ldap.LdapResultHandler;
 import org.forgerock.opendj.ldap.Modification;
 import org.forgerock.opendj.ldap.RequestContext;
 import org.forgerock.opendj.ldap.RequestHandler;
 import org.forgerock.opendj.ldap.RequestHandlerFactory;
-import org.forgerock.opendj.ldap.LdapResultHandler;
 import org.forgerock.opendj.ldap.SearchResultHandler;
 import org.forgerock.opendj.ldap.ServerConnectionFactory;
 import org.forgerock.opendj.ldap.controls.Control;
@@ -397,13 +399,14 @@
         final int remotePort = Integer.parseInt(args[5]);
 
         // Create connection factories.
-        final ConnectionFactory factory =
-                Connections.newCachedConnectionPool(Connections.newAuthenticatedConnectionFactory(
-                        new LDAPConnectionFactory(remoteAddress, remotePort), Requests
-                                .newSimpleBindRequest(proxyDN, proxyPassword.toCharArray())));
-        final ConnectionFactory bindFactory =
-                Connections.newCachedConnectionPool(new LDAPConnectionFactory(remoteAddress,
-                        remotePort));
+        final Options factoryOptions = Options.defaultOptions()
+                                   .set(LDAPConnectionFactory.AUTHN_BIND_REQUEST,
+                                        newSimpleBindRequest(proxyDN, proxyPassword.toCharArray()));
+        final ConnectionFactory factory = newCachedConnectionPool(new LDAPConnectionFactory(remoteAddress,
+                                                                                            remotePort,
+                                                                                            factoryOptions));
+        final ConnectionFactory bindFactory = newCachedConnectionPool(new LDAPConnectionFactory(remoteAddress,
+                                                                                                remotePort));
 
         /*
          * Create a server connection adapter which will create a new proxy
@@ -423,10 +426,10 @@
                 Connections.newServerConnectionFactory(proxyFactory);
 
         // Create listener.
-        final Options options = Options.defaultOptions().set(BACKLOG, 4096);
+        final Options listenerOptions = Options.defaultOptions().set(CONNECT_MAX_BACKLOG, 4096);
         LDAPListener listener = null;
         try {
-            listener = new LDAPListener(localAddress, localPort, connectionHandler, options);
+            listener = new LDAPListener(localAddress, localPort, connectionHandler, listenerOptions);
             System.out.println("Press any key to stop the server...");
             System.in.read();
         } catch (final IOException e) {
diff --git a/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/SASLAuth.java b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/SASLAuth.java
index f0e7b19..a4ccb0c 100644
--- a/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/SASLAuth.java
+++ b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/SASLAuth.java
@@ -32,7 +32,7 @@
  */
 package org.forgerock.opendj.examples;
 
-import static org.forgerock.opendj.ldap.LDAPConnectionFactory.USE_STARTTLS;
+import static org.forgerock.opendj.ldap.LDAPConnectionFactory.SSL_USE_STARTTLS;
 import static org.forgerock.opendj.ldap.LDAPConnectionFactory.SSL_CONTEXT;
 
 
@@ -129,7 +129,7 @@
         SSLContext sslContext =
                 new SSLContextBuilder().setTrustManager(TrustManagers.trustAll()).getSSLContext();
         options.set(SSL_CONTEXT, sslContext);
-        options.set(USE_STARTTLS, true);
+        options.set(SSL_USE_STARTTLS, true);
         return options;
     }
 
diff --git a/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/Server.java b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/Server.java
index 0f5a843..8b6e653 100644
--- a/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/Server.java
+++ b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/Server.java
@@ -27,7 +27,7 @@
 
 package org.forgerock.opendj.examples;
 
-import static org.forgerock.opendj.ldap.LDAPListener.BACKLOG;
+import static org.forgerock.opendj.ldap.LDAPListener.CONNECT_MAX_BACKLOG;
 
 import java.io.FileInputStream;
 import java.io.IOException;
@@ -103,7 +103,7 @@
         // Create listener.
         LDAPListener listener = null;
         try {
-            final Options options = Options.defaultOptions().set(BACKLOG, 4096);
+            final Options options = Options.defaultOptions().set(CONNECT_MAX_BACKLOG, 4096);
 
             if (keyStoreFileName != null) {
                 // Configure SSL/TLS and enable it when connections are
diff --git a/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/SimpleAuth.java b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/SimpleAuth.java
index 69ff5a4..72fc412 100644
--- a/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/SimpleAuth.java
+++ b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/SimpleAuth.java
@@ -153,7 +153,7 @@
             options.set(SSL_CONTEXT, sslContext);
         }
 
-        options.set(USE_STARTTLS, useStartTLS);
+        options.set(SSL_USE_STARTTLS, useStartTLS);
 
         return options;
     }
@@ -208,7 +208,7 @@
                 new SSLContextBuilder().setTrustManager(TrustManagers.trustAll())
                         .getSSLContext();
         options.set(SSL_CONTEXT, sslContext);
-        options.set(USE_STARTTLS, useStartTLS);
+        options.set(SSL_USE_STARTTLS, useStartTLS);
         return options;
     }
     // --- JCite trust all ---
diff --git a/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/SimpleAuthAsync.java b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/SimpleAuthAsync.java
index f354445..67b3eba 100644
--- a/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/SimpleAuthAsync.java
+++ b/opendj-sdk/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/SimpleAuthAsync.java
@@ -26,8 +26,9 @@
 
 package org.forgerock.opendj.examples;
 
+import static org.forgerock.opendj.ldap.TrustManagers.checkHostName;
 import static org.forgerock.util.Utils.closeSilently;
-import static org.forgerock.opendj.ldap.LDAPConnectionFactory.USE_STARTTLS;
+import static org.forgerock.opendj.ldap.LDAPConnectionFactory.SSL_USE_STARTTLS;
 import static org.forgerock.opendj.ldap.LDAPConnectionFactory.SSL_CONTEXT;
 
 import org.forgerock.opendj.ldap.Connection;
@@ -158,10 +159,9 @@
                                            final String storepass) {
         Options options = Options.defaultOptions();
         if (useSSL || useStartTLS) {
-            TrustManager trustManager = null;
             try {
-                trustManager = TrustManagers.checkValidityDates(
-                        TrustManagers.checkHostName(hostname,
+                TrustManager trustManager = TrustManagers.checkValidityDates(
+                        checkHostName(hostname,
                                 TrustManagers.checkUsingTrustStore(
                                         truststore, storepass.toCharArray(), null)));
                 if (trustManager != null) {
@@ -169,11 +169,12 @@
                             .setTrustManager(trustManager).getSSLContext();
                     options.set(SSL_CONTEXT, sslContext);
                 }
+                options.set(SSL_USE_STARTTLS, useStartTLS);
             } catch (Exception e) {
                 System.err.println(e.getMessage());
                 System.exit(ResultCode.CLIENT_SIDE_CONNECT_ERROR.intValue());
+                return null;
             }
-            options.set(USE_STARTTLS, useStartTLS);
         }
         return options;
     }
@@ -191,18 +192,19 @@
      * @return SSL context options to trust all certificates without checking.
      */
     private static Options getTrustAllOptions() {
-        Options options = Options.defaultOptions();
         try {
+            Options options = Options.defaultOptions();
             SSLContext sslContext =
                     new SSLContextBuilder().setTrustManager(TrustManagers.trustAll())
                             .getSSLContext();
             options.set(SSL_CONTEXT, sslContext);
-            options.set(USE_STARTTLS, useStartTLS);
+            options.set(SSL_USE_STARTTLS, useStartTLS);
+            return options;
         } catch (GeneralSecurityException e) {
             System.err.println(e.getMessage());
             System.exit(ResultCode.CLIENT_SIDE_CONNECT_ERROR.intValue());
+            return null;
         }
-        return options;
     }
 
     private static String  host;
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/AddRate.java b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/AddRate.java
index 503d7dd..f2a07e0 100644
--- a/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/AddRate.java
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/AddRate.java
@@ -501,6 +501,7 @@
             }
 
             connectionFactory = connectionFactoryProvider.getAuthenticatedConnectionFactory();
+            runner.setBindRequest(connectionFactoryProvider.getBindRequest());
             runner.validate(deleteMode, deleteSizeThreshold, deleteAgeThreshold, noPurgeArgument);
         } catch (final ArgumentException ae) {
             argParser.displayMessageAndUsageReference(getErrStream(), ERR_ERROR_PARSING_ARGS.get(ae.getMessage()));
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/AuthRate.java b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/AuthRate.java
index 75954aa..bf0f3fb 100644
--- a/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/AuthRate.java
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/AuthRate.java
@@ -202,6 +202,7 @@
                     break;
                 }
 
+                final BindRequest bindRequest = getBindRequest();
                 if (bindRequest instanceof SimpleBindRequest) {
                     final SimpleBindRequest o = (SimpleBindRequest) bindRequest;
                     if (br == null) {
@@ -315,7 +316,6 @@
         private SearchScope scope;
         private DereferenceAliasesPolicy dereferencesAliasesPolicy;
         private String[] attributes;
-        private BindRequest bindRequest;
         private int invalidCredPercent;
 
         private BindPerformanceRunner(final PerformanceRunnerOptions options)
@@ -479,11 +479,11 @@
                 return 0;
             }
 
-            connectionFactory = connectionFactoryProvider.getConnectionFactory();
+            connectionFactory = connectionFactoryProvider.getUnauthenticatedConnectionFactory();
+            runner.setBindRequest(connectionFactoryProvider.getBindRequest());
             runner.validate();
 
-            runner.bindRequest = connectionFactoryProvider.getBindRequest();
-            if (runner.bindRequest == null) {
+            if (runner.getBindRequest() == null) {
                 throw new ArgumentException(LocalizableMessage
                         .raw("Authentication information must be provided to use this tool"));
             }
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/LDAPCompare.java b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/LDAPCompare.java
index 29dbfaf..a76f212 100644
--- a/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/LDAPCompare.java
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/LDAPCompare.java
@@ -30,6 +30,8 @@
 import static com.forgerock.opendj.ldap.tools.ToolsMessages.*;
 import static com.forgerock.opendj.cli.Utils.filterExitCode;
 import static com.forgerock.opendj.cli.Utils.readBytesFromFile;
+import static com.forgerock.opendj.ldap.tools.Utils.printErrorMessage;
+import static com.forgerock.opendj.ldap.tools.Utils.printPasswordPolicyResults;
 import static org.forgerock.util.Utils.closeSilently;
 
 import java.io.BufferedReader;
@@ -52,6 +54,7 @@
 import org.forgerock.opendj.ldap.controls.AssertionRequestControl;
 import org.forgerock.opendj.ldap.controls.Control;
 import org.forgerock.opendj.ldap.controls.ProxiedAuthV2RequestControl;
+import org.forgerock.opendj.ldap.requests.BindRequest;
 import org.forgerock.opendj.ldap.requests.CompareRequest;
 import org.forgerock.opendj.ldap.requests.Requests;
 import org.forgerock.opendj.ldap.responses.Result;
@@ -149,6 +152,7 @@
         argParser.setShortToolDescription(REF_SHORT_DESC_LDAPCOMPARE.get());
         ConnectionFactoryProvider connectionFactoryProvider;
         ConnectionFactory connectionFactory;
+        BindRequest bindRequest;
 
         BooleanArgument continueOnError;
         BooleanArgument noop;
@@ -224,7 +228,8 @@
                 return 0;
             }
 
-            connectionFactory = connectionFactoryProvider.getAuthenticatedConnectionFactory();
+            connectionFactory = connectionFactoryProvider.getUnauthenticatedConnectionFactory();
+            bindRequest = connectionFactoryProvider.getBindRequest();
         } catch (final ArgumentException ae) {
             argParser.displayMessageAndUsageReference(getErrStream(), ERR_ERROR_PARSING_ARGS.get(ae.getMessage()));
             return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
@@ -359,16 +364,18 @@
         }
 
         Connection connection = null;
-        if (!noop.isPresent()) {
-            try {
-                connection = connectionFactory.getConnection();
-            } catch (final LdapException ere) {
-                errPrintln(LocalizableMessage.raw(ere.getMessage()));
-                return ere.getResult().getResultCode().intValue();
-            }
-        }
-
         try {
+            if (!noop.isPresent()) {
+                try {
+                    connection = connectionFactory.getConnection();
+                    if (bindRequest != null) {
+                        printPasswordPolicyResults(this, connection.bind(bindRequest));
+                    }
+                } catch (final LdapException ere) {
+                    return printErrorMessage(this, ere);
+                }
+            }
+
             int result;
             if (rdr == null) {
                 for (final String dn : dnStrings) {
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/LDAPModify.java b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/LDAPModify.java
index b19b1f0..1dd5c39 100644
--- a/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/LDAPModify.java
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/LDAPModify.java
@@ -30,6 +30,7 @@
 import static com.forgerock.opendj.ldap.tools.ToolsMessages.*;
 import static com.forgerock.opendj.cli.Utils.filterExitCode;
 import static com.forgerock.opendj.ldap.tools.Utils.printErrorMessage;
+import static com.forgerock.opendj.ldap.tools.Utils.printPasswordPolicyResults;
 import static org.forgerock.util.Utils.closeSilently;
 
 import java.io.FileInputStream;
@@ -56,6 +57,7 @@
 import org.forgerock.opendj.ldap.controls.PreReadResponseControl;
 import org.forgerock.opendj.ldap.controls.ProxiedAuthV2RequestControl;
 import org.forgerock.opendj.ldap.requests.AddRequest;
+import org.forgerock.opendj.ldap.requests.BindRequest;
 import org.forgerock.opendj.ldap.requests.DeleteRequest;
 import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
 import org.forgerock.opendj.ldap.requests.ModifyRequest;
@@ -94,7 +96,7 @@
                     printResult(opType, change.getName().toString(), r);
                     return r.getResultCode().intValue();
                 } catch (final LdapException ere) {
-                    return Utils.printErrorMessage(LDAPModify.this, ere);
+                    return printErrorMessage(LDAPModify.this, ere);
                 }
             }
             return ResultCode.SUCCESS.intValue();
@@ -130,7 +132,7 @@
                     printResult(opType, change.getName().toString(), r);
                     return r.getResultCode().intValue();
                 } catch (final LdapException ere) {
-                    return Utils.printErrorMessage(LDAPModify.this, ere);
+                    return printErrorMessage(LDAPModify.this, ere);
                 }
             }
             return ResultCode.SUCCESS.intValue();
@@ -148,7 +150,7 @@
                     printResult(opType, change.getName().toString(), r);
                     return r.getResultCode().intValue();
                 } catch (final LdapException ere) {
-                    return Utils.printErrorMessage(LDAPModify.this, ere);
+                    return printErrorMessage(LDAPModify.this, ere);
                 }
             }
             return ResultCode.SUCCESS.intValue();
@@ -253,6 +255,7 @@
         argParser.setShortToolDescription(REF_SHORT_DESC_LDAPMODIFY.get());
         ConnectionFactoryProvider connectionFactoryProvider;
         ConnectionFactory connectionFactory;
+        BindRequest bindRequest;
 
         BooleanArgument continueOnError;
         // TODO: Remove this due to new LDIF reader api?
@@ -367,7 +370,8 @@
                 return 0;
             }
 
-            connectionFactory = connectionFactoryProvider.getAuthenticatedConnectionFactory();
+            connectionFactory = connectionFactoryProvider.getUnauthenticatedConnectionFactory();
+            bindRequest = connectionFactoryProvider.getBindRequest();
         } catch (final ArgumentException ae) {
             argParser.displayMessageAndUsageReference(getErrStream(), ERR_ERROR_PARSING_ARGS.get(ae.getMessage()));
             return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
@@ -384,12 +388,6 @@
             return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
         }
 
-        // modifyOptions.setShowOperations(noop.isPresent());
-        // modifyOptions.setVerbose(verbose.isPresent());
-        // modifyOptions.setContinueOnError(continueOnError.isPresent());
-        // modifyOptions.setEncoding(encodingStr.getValue());
-        // modifyOptions.setDefaultAdd(defaultAdd.isPresent());
-
         controls = new LinkedList<>();
         if (controlStr.isPresent()) {
             for (final String ctrlString : controlStr.getValues()) {
@@ -453,20 +451,22 @@
             controls.add(control);
         }
 
-        if (!noop.isPresent()) {
-            try {
-                connection = connectionFactory.getConnection();
-            } catch (final LdapException ere) {
-                return Utils.printErrorMessage(this, ere);
-            }
-        }
-
-        Utils.printPasswordPolicyResults(this, connection);
 
         writer = new LDIFEntryWriter(getOutputStream());
         final VisitorImpl visitor = new VisitorImpl();
         ChangeRecordReader reader = null;
         try {
+            if (!noop.isPresent()) {
+                try {
+                    connection = connectionFactory.getConnection();
+                    if (bindRequest != null) {
+                        printPasswordPolicyResults(this, connection.bind(bindRequest));
+                    }
+                } catch (final LdapException ere) {
+                    return printErrorMessage(this, ere);
+                }
+            }
+
             if (filename.isPresent()) {
                 try {
                     reader = new LDIFChangeRecordReader(new FileInputStream(filename.getValue()));
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/LDAPSearch.java b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/LDAPSearch.java
index 8275fdb..ca62f2d 100644
--- a/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/LDAPSearch.java
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/LDAPSearch.java
@@ -63,6 +63,7 @@
 import org.forgerock.opendj.ldap.controls.SimplePagedResultsControl;
 import org.forgerock.opendj.ldap.controls.VirtualListViewRequestControl;
 import org.forgerock.opendj.ldap.controls.VirtualListViewResponseControl;
+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.Result;
@@ -83,6 +84,8 @@
 import com.forgerock.opendj.ldap.controls.AccountUsabilityResponseControl;
 import com.forgerock.opendj.util.StaticUtils;
 
+import static com.forgerock.opendj.ldap.tools.Utils.printErrorMessage;
+import static com.forgerock.opendj.ldap.tools.Utils.printPasswordPolicyResults;
 import static org.forgerock.util.Utils.*;
 
 import static com.forgerock.opendj.cli.ArgumentConstants.*;
@@ -241,6 +244,7 @@
         argParser.setShortToolDescription(REF_SHORT_DESC_LDAPSEARCH.get());
         ConnectionFactoryProvider connectionFactoryProvider;
         ConnectionFactory connectionFactory;
+        BindRequest bindRequest;
 
         BooleanArgument countEntries;
         BooleanArgument dontWrap;
@@ -444,7 +448,8 @@
                 return 0;
             }
 
-            connectionFactory = connectionFactoryProvider.getAuthenticatedConnectionFactory();
+            connectionFactory = connectionFactoryProvider.getUnauthenticatedConnectionFactory();
+            bindRequest = connectionFactoryProvider.getBindRequest();
         } catch (final ArgumentException ae) {
             argParser.displayMessageAndUsageReference(getErrStream(), ERR_ERROR_PARSING_ARGS.get(ae.getMessage()));
             return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
@@ -823,16 +828,13 @@
             return 0;
         }
 
-        Connection connection;
+        Connection connection = null;
         try {
             connection = connectionFactory.getConnection();
-        } catch (final LdapException ere) {
-            return Utils.printErrorMessage(this, ere);
-        }
+            if (bindRequest != null) {
+                printPasswordPolicyResults(this, connection.bind(bindRequest));
+            }
 
-        Utils.printPasswordPolicyResults(this, connection);
-
-        try {
             int filterIndex = 0;
             ldifWriter = new LDIFEntryWriter(getOutputStream()).setWrapColumn(wrapColumn);
             final LDAPSearchResultHandler resultHandler = new LDAPSearchResultHandler();
@@ -920,7 +922,7 @@
                 println();
             }
         } catch (final LdapException ere) {
-            return Utils.printErrorMessage(this, ere);
+            return printErrorMessage(this, ere);
         } finally {
             closeSilently(ldifWriter, connection);
         }
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/ModRate.java b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/ModRate.java
index 8743383..422c2a9 100644
--- a/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/ModRate.java
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/ModRate.java
@@ -228,6 +228,7 @@
             }
 
             connectionFactory = connectionFactoryProvider.getAuthenticatedConnectionFactory();
+            runner.setBindRequest(connectionFactoryProvider.getBindRequest());
             runner.validate();
         } catch (final ArgumentException ae) {
             argParser.displayMessageAndUsageReference(getErrStream(), ERR_ERROR_PARSING_ARGS.get(ae.getMessage()));
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/PerformanceRunner.java b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/PerformanceRunner.java
index 61bd58b..6d7c17b 100644
--- a/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/PerformanceRunner.java
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/PerformanceRunner.java
@@ -26,6 +26,7 @@
  */
 package com.forgerock.opendj.ldap.tools;
 
+import static com.forgerock.opendj.ldap.tools.Utils.printErrorMessage;
 import static java.util.Locale.ENGLISH;
 import static java.util.concurrent.TimeUnit.*;
 
@@ -54,13 +55,13 @@
 import org.forgerock.opendj.ldap.ConnectionFactory;
 import org.forgerock.opendj.ldap.LdapException;
 import org.forgerock.opendj.ldap.LdapResultHandler;
+import org.forgerock.opendj.ldap.requests.BindRequest;
 import org.forgerock.opendj.ldap.responses.ExtendedResult;
 import org.forgerock.opendj.ldap.responses.Result;
 import org.forgerock.util.promise.Promise;
 
 import com.forgerock.opendj.cli.ArgumentException;
 import com.forgerock.opendj.cli.ArgumentParser;
-import com.forgerock.opendj.cli.AuthenticatedConnectionFactory.AuthenticatedConnection;
 import com.forgerock.opendj.cli.BooleanArgument;
 import com.forgerock.opendj.cli.ConsoleApplication;
 import com.forgerock.opendj.cli.IntegerArgument;
@@ -581,10 +582,9 @@
                     }
                 } else {
                     connection = this.connection;
-                    if (!noRebind && connection instanceof AuthenticatedConnection) {
-                        final AuthenticatedConnection ac = (AuthenticatedConnection) connection;
+                    if (!noRebind && bindRequest != null) {
                         try {
-                            ac.rebindAsync().getOrThrow();
+                            connection.bindAsync(bindRequest).getOrThrow();
                         } catch (final InterruptedException e) {
                             // Ignore and check stop requested
                             continue;
@@ -684,6 +684,7 @@
     private long maxDurationTime;
     private boolean isAsync;
     private boolean noRebind;
+    private BindRequest bindRequest;
     private int statsInterval;
     private final IntegerArgument numThreadsArgument;
     private final IntegerArgument maxDurationArgument;
@@ -920,7 +921,8 @@
             stopRequested = true;
         } catch (final LdapException e) {
             stopRequested = true;
-            app.println(LocalizableMessage.raw(e.getResult().getDiagnosticMessage()));
+            printErrorMessage(app, e);
+            return e.getResult().getResultCode().intValue();
         } finally {
             closeSilently(connections);
         }
@@ -928,6 +930,14 @@
         return 0;
     }
 
+    void setBindRequest(final BindRequest request) {
+        this.bindRequest = request;
+    }
+
+    BindRequest getBindRequest() {
+        return bindRequest;
+    }
+
     protected void joinAllWorkerThreads() throws InterruptedException {
         for (final Thread t : workerThreads) {
             t.join();
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/PerformanceRunnerOptions.java b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/PerformanceRunnerOptions.java
index 6503ff1..f4585de 100644
--- a/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/PerformanceRunnerOptions.java
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/PerformanceRunnerOptions.java
@@ -21,7 +21,7 @@
  * CDDL HEADER END
  *
  *
- *      Portions Copyright 2014 ForgeRock AS
+ *      Portions Copyright 2014-2015 ForgeRock AS
  */
 
 package com.forgerock.opendj.ldap.tools;
@@ -42,7 +42,6 @@
     private boolean supportsGeneratorArgument = true;
 
     PerformanceRunnerOptions(ArgumentParser argParser, ConsoleApplication app) {
-        super();
         this.argParser = argParser;
         this.app = app;
     }
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/SearchRate.java b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/SearchRate.java
index 8f72044..356dabe 100644
--- a/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/SearchRate.java
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/SearchRate.java
@@ -284,6 +284,7 @@
             }
 
             connectionFactory = connectionFactoryProvider.getAuthenticatedConnectionFactory();
+            runner.setBindRequest(connectionFactoryProvider.getBindRequest());
             runner.validate();
         } catch (final ArgumentException ae) {
             argParser.displayMessageAndUsageReference(getErrStream(), ERR_ERROR_PARSING_ARGS.get(ae.getMessage()));
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/Utils.java b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/Utils.java
index 63e76c1..7467ccc 100644
--- a/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/Utils.java
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/Utils.java
@@ -26,13 +26,12 @@
  */
 package com.forgerock.opendj.ldap.tools;
 
-import static com.forgerock.opendj.ldap.tools.ToolsMessages.*;
 import static com.forgerock.opendj.cli.Utils.readBytesFromFile;
 import static com.forgerock.opendj.cli.Utils.secondsToTimeString;
+import static com.forgerock.opendj.ldap.tools.ToolsMessages.*;
 
 import org.forgerock.i18n.LocalizableMessage;
 import org.forgerock.opendj.ldap.ByteString;
-import org.forgerock.opendj.ldap.Connection;
 import org.forgerock.opendj.ldap.DecodeException;
 import org.forgerock.opendj.ldap.DecodeOptions;
 import org.forgerock.opendj.ldap.LdapException;
@@ -51,7 +50,6 @@
 
 import com.forgerock.opendj.cli.ConsoleApplication;
 import com.forgerock.opendj.ldap.controls.AccountUsabilityRequestControl;
-import com.forgerock.opendj.cli.AuthenticatedConnectionFactory.AuthenticatedConnection;
 import com.forgerock.opendj.util.StaticUtils;
 
 /**
@@ -196,84 +194,62 @@
         return ere.getResult().getResultCode().intValue();
     }
 
-    static void printPasswordPolicyResults(final ConsoleApplication app, final Connection connection) {
-        if (connection instanceof AuthenticatedConnection) {
-            final AuthenticatedConnection conn = (AuthenticatedConnection) connection;
-            final BindResult result = conn.getAuthenticatedBindResult();
-
-            try {
-                final AuthorizationIdentityResponseControl control =
-                        result.getControl(AuthorizationIdentityResponseControl.DECODER,
-                                new DecodeOptions());
-                if (control != null) {
-                    final LocalizableMessage message =
-                            INFO_BIND_AUTHZID_RETURNED.get(control.getAuthorizationID());
-                    app.println(message);
-                }
-            } catch (final DecodeException e) {
-                app.errPrintln(ERR_DECODE_CONTROL_FAILURE.get(e.getLocalizedMessage()));
+    static void printPasswordPolicyResults(final ConsoleApplication app, final BindResult result) {
+        try {
+            final AuthorizationIdentityResponseControl control = result.getControl(
+                    AuthorizationIdentityResponseControl.DECODER, new DecodeOptions());
+            if (control != null) {
+                app.println(INFO_BIND_AUTHZID_RETURNED.get(control.getAuthorizationID()));
             }
+        } catch (final DecodeException e) {
+            app.errPrintln(ERR_DECODE_CONTROL_FAILURE.get(e.getLocalizedMessage()));
+        }
 
-            try {
-                final PasswordExpiredResponseControl control =
-                        result.getControl(PasswordExpiredResponseControl.DECODER,
-                                new DecodeOptions());
-                if (control != null) {
-                    final LocalizableMessage message = INFO_BIND_PASSWORD_EXPIRED.get();
-                    app.println(message);
-                }
-            } catch (final DecodeException e) {
-                app.errPrintln(ERR_DECODE_CONTROL_FAILURE.get(e.getLocalizedMessage()));
+        try {
+            final PasswordExpiredResponseControl control = result.getControl(PasswordExpiredResponseControl.DECODER,
+                                                                             new DecodeOptions());
+            if (control != null) {
+                app.println(INFO_BIND_PASSWORD_EXPIRED.get());
             }
+        } catch (final DecodeException e) {
+            app.errPrintln(ERR_DECODE_CONTROL_FAILURE.get(e.getLocalizedMessage()));
+        }
 
-            try {
-                final PasswordExpiringResponseControl control =
-                        result.getControl(PasswordExpiringResponseControl.DECODER,
-                                new DecodeOptions());
-                if (control != null) {
-                    final LocalizableMessage timeString =
-                            secondsToTimeString(control.getSecondsUntilExpiration());
-                    final LocalizableMessage message = INFO_BIND_PASSWORD_EXPIRING.get(timeString);
-                    app.println(message);
-                }
-            } catch (final DecodeException e) {
-                app.errPrintln(ERR_DECODE_CONTROL_FAILURE.get(e.getLocalizedMessage()));
+        try {
+            final PasswordExpiringResponseControl control = result.getControl(PasswordExpiringResponseControl.DECODER,
+                                                                              new DecodeOptions());
+            if (control != null) {
+                final LocalizableMessage timeString = secondsToTimeString(control.getSecondsUntilExpiration());
+                app.println(INFO_BIND_PASSWORD_EXPIRING.get(timeString));
             }
+        } catch (final DecodeException e) {
+            app.errPrintln(ERR_DECODE_CONTROL_FAILURE.get(e.getLocalizedMessage()));
+        }
 
-            try {
-                final PasswordPolicyResponseControl control =
-                        result.getControl(PasswordPolicyResponseControl.DECODER,
-                                new DecodeOptions());
-                if (control != null) {
-                    final PasswordPolicyErrorType errorType = control.getErrorType();
-                    if (errorType == PasswordPolicyErrorType.PASSWORD_EXPIRED) {
-                        final LocalizableMessage message = INFO_BIND_PASSWORD_EXPIRED.get();
-                        app.println(message);
-                    } else if (errorType == PasswordPolicyErrorType.ACCOUNT_LOCKED) {
-                        final LocalizableMessage message = INFO_BIND_ACCOUNT_LOCKED.get();
-                        app.println(message);
-                    } else if (errorType == PasswordPolicyErrorType.CHANGE_AFTER_RESET) {
+        try {
+            final PasswordPolicyResponseControl control = result.getControl(PasswordPolicyResponseControl.DECODER,
+                                                                            new DecodeOptions());
+            if (control != null) {
+                final PasswordPolicyErrorType errorType = control.getErrorType();
+                if (errorType == PasswordPolicyErrorType.PASSWORD_EXPIRED) {
+                    app.println(INFO_BIND_PASSWORD_EXPIRED.get());
+                } else if (errorType == PasswordPolicyErrorType.ACCOUNT_LOCKED) {
+                    app.println(INFO_BIND_ACCOUNT_LOCKED.get());
+                } else if (errorType == PasswordPolicyErrorType.CHANGE_AFTER_RESET) {
 
-                        final LocalizableMessage message = INFO_BIND_MUST_CHANGE_PASSWORD.get();
-                        app.println(message);
-                    }
-
-                    final PasswordPolicyWarningType warningType = control.getWarningType();
-                    if (warningType == PasswordPolicyWarningType.TIME_BEFORE_EXPIRATION) {
-                        final LocalizableMessage timeString =
-                                secondsToTimeString(control.getWarningValue());
-                        final LocalizableMessage message =
-                                INFO_BIND_PASSWORD_EXPIRING.get(timeString);
-                        app.println(message);
-                    } else if (warningType == PasswordPolicyWarningType.GRACE_LOGINS_REMAINING) {
-                        final LocalizableMessage message =
-                                INFO_BIND_GRACE_LOGINS_REMAINING.get(control.getWarningValue());
-                        app.println(message);
-                    }
+                    app.println(INFO_BIND_MUST_CHANGE_PASSWORD.get());
                 }
-            } catch (final DecodeException e) {
-                app.errPrintln(ERR_DECODE_CONTROL_FAILURE.get(e.getLocalizedMessage()));
+
+                final PasswordPolicyWarningType warningType = control.getWarningType();
+                if (warningType == PasswordPolicyWarningType.TIME_BEFORE_EXPIRATION) {
+                    final LocalizableMessage timeString = secondsToTimeString(control.getWarningValue());
+                    app.println(INFO_BIND_PASSWORD_EXPIRING.get(timeString));
+                } else if (warningType == PasswordPolicyWarningType.GRACE_LOGINS_REMAINING) {
+                    app.println(INFO_BIND_GRACE_LOGINS_REMAINING.get(control.getWarningValue()));
+                }
             }
+        } catch (final DecodeException e) {
+            app.errPrintln(ERR_DECODE_CONTROL_FAILURE.get(e.getLocalizedMessage()));
         }
     }
 
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/test/java/com/forgerock/opendj/ldap/tools/ConnectionFactoryProviderTest.java b/opendj-sdk/opendj-ldap-toolkit/src/test/java/com/forgerock/opendj/ldap/tools/ConnectionFactoryProviderTest.java
index fc7bfc7..634a13b 100644
--- a/opendj-sdk/opendj-ldap-toolkit/src/test/java/com/forgerock/opendj/ldap/tools/ConnectionFactoryProviderTest.java
+++ b/opendj-sdk/opendj-ldap-toolkit/src/test/java/com/forgerock/opendj/ldap/tools/ConnectionFactoryProviderTest.java
@@ -21,7 +21,7 @@
  * CDDL HEADER END
  *
  *
- *      Copyright 2013-2014 ForgeRock AS.
+ *      Copyright 2013-2015 ForgeRock AS.
  */
 package com.forgerock.opendj.ldap.tools;
 
@@ -65,7 +65,7 @@
                 .getCanonicalPath();
         argParser.parseArguments(new String[] { "--useStartTLS", "--trustStorePath", trustStorePath });
 
-        ConnectionFactory factory = connectionFactoryProvider.getConnectionFactory();
+        ConnectionFactory factory = connectionFactoryProvider.getUnauthenticatedConnectionFactory();
 
         assertThat(factory).isNotNull();
     }
diff --git a/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/HttpAuthenticationFilter.java b/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/HttpAuthenticationFilter.java
index 033ae13..306cf3c 100644
--- a/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/HttpAuthenticationFilter.java
+++ b/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/HttpAuthenticationFilter.java
@@ -15,6 +15,7 @@
  */
 package org.forgerock.opendj.rest2ldap;
 
+import static org.forgerock.json.resource.ResourceException.newResourceException;
 import static org.forgerock.services.context.SecurityContext.AUTHZID_DN;
 import static org.forgerock.services.context.SecurityContext.AUTHZID_ID;
 import static org.forgerock.opendj.ldap.Connections.uncloseable;
@@ -194,7 +195,7 @@
             final char[] password;
             if (headerUsername != null) {
                 if (headerPassword == null || headerUsername.isEmpty() || headerPassword.isEmpty()) {
-                    throw ResourceException.getException(401);
+                    throw newResourceException(401);
                 }
                 username = headerUsername;
                 password = headerPassword.toCharArray();
@@ -202,21 +203,21 @@
                 final StringTokenizer st = new StringTokenizer(headerAuthorization);
                 final String method = st.nextToken();
                 if (method == null || !"BASIC".equalsIgnoreCase(method)) {
-                    throw ResourceException.getException(401);
+                    throw newResourceException(401);
                 }
                 final String b64Credentials = st.nextToken();
                 if (b64Credentials == null) {
-                    throw ResourceException.getException(401);
+                    throw newResourceException(401);
                 }
                 final String credentials = ByteString.valueOfBase64(b64Credentials).toString();
                 final String[] usernameAndPassword = credentials.split(":");
                 if (usernameAndPassword.length != 2) {
-                    throw ResourceException.getException(401);
+                    throw newResourceException(401);
                 }
                 username = usernameAndPassword[0];
                 password = usernameAndPassword[1].toCharArray();
             } else {
-                throw ResourceException.getException(401);
+                throw newResourceException(401);
             }
 
             // If we've got here then we have a username and password.
diff --git a/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java b/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java
index 541ecd1..24e1fa7 100644
--- a/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java
+++ b/opendj-sdk/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java
@@ -15,11 +15,13 @@
  */
 package org.forgerock.opendj.rest2ldap;
 
+import static org.forgerock.json.resource.ResourceException.newResourceException;
+import static org.forgerock.opendj.ldap.Connections.newCachedConnectionPool;
+import static org.forgerock.opendj.ldap.LDAPConnectionFactory.*;
 import static org.forgerock.opendj.ldap.requests.Requests.newSearchRequest;
 import static org.forgerock.opendj.ldap.schema.CoreSchema.getEntryUUIDAttributeType;
 import static org.forgerock.opendj.rest2ldap.ReadOnUpdatePolicy.CONTROLS;
 import static org.forgerock.opendj.rest2ldap.Utils.ensureNotNull;
-import static org.forgerock.opendj.ldap.LDAPConnectionFactory.*;
 
 import java.io.IOException;
 import java.security.GeneralSecurityException;
@@ -45,7 +47,6 @@
 import org.forgerock.opendj.ldap.ByteString;
 import org.forgerock.opendj.ldap.ConnectionException;
 import org.forgerock.opendj.ldap.ConnectionFactory;
-import org.forgerock.opendj.ldap.Connections;
 import org.forgerock.opendj.ldap.ConstraintViolationException;
 import org.forgerock.opendj.ldap.DN;
 import org.forgerock.opendj.ldap.Entry;
@@ -69,6 +70,7 @@
 import org.forgerock.opendj.ldap.schema.AttributeType;
 import org.forgerock.opendj.ldap.schema.Schema;
 import org.forgerock.util.Options;
+import org.forgerock.util.time.Duration;
 
 /** Provides core factory methods and builders for constructing LDAP resource collections. */
 public final class Rest2LDAP {
@@ -829,7 +831,7 @@
         } catch (final Throwable tmp) {
             resourceResultCode = ResourceException.INTERNAL_ERROR;
         }
-        return ResourceException.getException(resourceResultCode, t.getMessage(), t);
+        return newResourceException(resourceResultCode, t.getMessage(), t);
     }
 
     /**
@@ -976,36 +978,39 @@
 
     private static ConnectionFactory configureConnectionFactory(final JsonValue configuration,
                                                                 final ClassLoader providerClassLoader) {
+        final long heartBeatIntervalSeconds = configuration.get("heartBeatIntervalSeconds").defaultTo(30L).asLong();
+        final Duration heartBeatInterval = new Duration(Math.max(heartBeatIntervalSeconds, 1L), TimeUnit.SECONDS);
+
+        final long heartBeatTimeoutMillis = configuration.get("heartBeatTimeoutMilliSeconds").defaultTo(500L).asLong();
+        final Duration heartBeatTimeout = new Duration(Math.max(heartBeatTimeoutMillis, 100L), TimeUnit.MILLISECONDS);
+
+        final Options options = Options.defaultOptions()
+                                       .set(TRANSPORT_PROVIDER_CLASS_LOADER, providerClassLoader)
+                                       .set(HEARTBEAT_ENABLED, true)
+                                       .set(HEARTBEAT_INTERVAL, heartBeatInterval)
+                                       .set(HEARTBEAT_TIMEOUT, heartBeatTimeout)
         // Parse pool parameters,
         final int connectionPoolSize =
                 Math.max(configuration.get("connectionPoolSize").defaultTo(10).asInteger(), 1);
-        final int heartBeatIntervalSeconds =
-                Math.max(configuration.get("heartBeatIntervalSeconds").defaultTo(30).asInteger(), 1);
-        final int heartBeatTimeoutMilliSeconds =
-                Math.max(configuration.get("heartBeatTimeoutMilliSeconds").defaultTo(500)
-                        .asInteger(), 100);
 
         // Parse authentication parameters.
-        final BindRequest bindRequest;
         if (configuration.isDefined("authentication")) {
             final JsonValue authn = configuration.get("authentication");
             if (authn.isDefined("simple")) {
                 final JsonValue simple = authn.get("simple");
-                bindRequest =
+                final BindRequest bindRequest =
                         Requests.newSimpleBindRequest(simple.get("bindDN").required().asString(),
                                 simple.get("bindPassword").required().asString().toCharArray());
+                options.set(AUTHN_BIND_REQUEST, bindRequest);
             } else {
                 throw new IllegalArgumentException("Only simple authentication is supported");
             }
-        } else {
-            bindRequest = null;
         }
 
         // Parse SSL/StartTLS parameters.
         final ConnectionSecurity connectionSecurity =
                 configuration.get("connectionSecurity").defaultTo(ConnectionSecurity.NONE).asEnum(
                         ConnectionSecurity.class);
-        final Options options = Options.defaultOptions().set(PROVIDER_CLASS_LOADER, providerClassLoader);
         if (connectionSecurity != ConnectionSecurity.NONE) {
             try {
                 // Configure SSL.
@@ -1033,8 +1038,8 @@
                     break;
                 }
                 options.set(SSL_CONTEXT, builder.getSSLContext());
-                options.set(USE_STARTTLS,
-                    connectionSecurity == ConnectionSecurity.STARTTLS);
+                options.set(SSL_USE_STARTTLS,
+                            connectionSecurity == ConnectionSecurity.STARTTLS);
             } catch (GeneralSecurityException | IOException e) {
                 // Rethrow as unchecked exception.
                 throw new IllegalArgumentException(e);
@@ -1046,25 +1051,17 @@
         if (!primaryLDAPServers.isList() || primaryLDAPServers.size() == 0) {
             throw new IllegalArgumentException("No primaryLDAPServers");
         }
-        final ConnectionFactory primary =
-                parseLDAPServers(primaryLDAPServers, bindRequest, connectionPoolSize,
-                        heartBeatIntervalSeconds, heartBeatTimeoutMilliSeconds, options);
+        final ConnectionFactory primary = parseLDAPServers(primaryLDAPServers, connectionPoolSize, options);
 
         // Parse secondary data center(s).
         final JsonValue secondaryLDAPServers = configuration.get("secondaryLDAPServers");
-        final ConnectionFactory secondary;
+        ConnectionFactory secondary = null;
         if (secondaryLDAPServers.isList()) {
             if (secondaryLDAPServers.size() > 0) {
-                secondary =
-                        parseLDAPServers(secondaryLDAPServers, bindRequest, connectionPoolSize,
-                                heartBeatIntervalSeconds, heartBeatTimeoutMilliSeconds, options);
-            } else {
-                secondary = null;
+                secondary = parseLDAPServers(secondaryLDAPServers, connectionPoolSize, options);
             }
         } else if (!secondaryLDAPServers.isNull()) {
             throw new IllegalArgumentException("Invalid secondaryLDAPServers configuration");
-        } else {
-            secondary = null;
         }
 
         // Create fail-over.
@@ -1102,28 +1099,17 @@
         }
     }
 
-    private static ConnectionFactory parseLDAPServers(final JsonValue config,
-            final BindRequest bindRequest, final int connectionPoolSize,
-            final int heartBeatIntervalSeconds, final int heartBeatTimeoutMilliSeconds,
-            final Options options) {
+    private static ConnectionFactory parseLDAPServers(JsonValue config, int poolSize, Options options) {
         final List<ConnectionFactory> servers = new ArrayList<>(config.size());
         for (final JsonValue server : config) {
             final String host = server.get("hostname").required().asString();
             final int port = server.get("port").required().asInteger();
-            ConnectionFactory factory = new LDAPConnectionFactory(host, port, options);
-            factory =
-                    Connections.newHeartBeatConnectionFactory(factory,
-                            heartBeatIntervalSeconds * 1000, heartBeatTimeoutMilliSeconds,
-                            TimeUnit.MILLISECONDS);
-            if (bindRequest != null) {
-                factory = Connections.newAuthenticatedConnectionFactory(factory, bindRequest);
+            final ConnectionFactory factory = new LDAPConnectionFactory(host, port, options);
+            if (poolSize > 1) {
+                servers.add(newCachedConnectionPool(factory, 0, poolSize, 60L, TimeUnit.SECONDS));
+            } else {
+                servers.add(factory);
             }
-            if (connectionPoolSize > 1) {
-                factory =
-                        Connections.newCachedConnectionPool(factory, 0, connectionPoolSize, 60L,
-                                TimeUnit.SECONDS);
-            }
-            servers.add(factory);
         }
         if (servers.size() > 1) {
             return Connections.newLoadBalancer(new RoundRobinLoadBalancingAlgorithm(servers,

--
Gitblit v1.10.0