From 71178c4013389172080487b47ad407ae211c304b Mon Sep 17 00:00:00 2001
From: Matthew Swift <matthew.swift@forgerock.com>
Date: Thu, 28 Mar 2013 11:50:36 +0000
Subject: [PATCH] Fix OPENDJ-354: Implement a RequestHandler which provides an in-memory backend

---
 opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/com/forgerock/opendj/util/ConnectionDecorator.java                        |   10 
 opendj-sdk/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/LDAPServer.java                                 |    6 
 opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/AbstractConnection.java                         |   12 
 opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/controls/PostReadRequestControl.java            |   55 
 opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/requests/AbstractRequestImpl.java               |   52 +
 opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/FixedConnectionPool.java                        |    5 
 opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/controls/PreReadRequestControl.java             |   55 
 opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Connection.java                                 |   31 
 opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/requests/AbstractUnmodifiableRequest.java       |   55 
 opendj-sdk/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/MemoryBackendTestCase.java                      |  455 +++++++++++++
 opendj-sdk/opendj3/opendj-ldap-sdk/src/main/resources/org/forgerock/opendj/ldap/core.properties                            |   15 
 opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Modification.java                               |    5 
 opendj-sdk/opendj3/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/Server.java                        |  364 ----------
 opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/requests/Request.java                           |   13 
 opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/responses/AbstractResponseImpl.java             |   52 
 opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Entries.java                                    |  209 ++++++
 opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/ErrorResultException.java                       |    1 
 opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/responses/AbstractUnmodifiableResponseImpl.java |   52 
 opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/responses/Response.java                         |   13 
 opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/MemoryBackend.java                              |  461 +++++++++++++
 20 files changed, 1,409 insertions(+), 512 deletions(-)

diff --git a/opendj-sdk/opendj3/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/Server.java b/opendj-sdk/opendj3/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/Server.java
index 575f3ae..1837cfe 100644
--- a/opendj-sdk/opendj3/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/Server.java
+++ b/opendj-sdk/opendj3/opendj-ldap-sdk-examples/src/main/java/org/forgerock/opendj/examples/Server.java
@@ -22,61 +22,28 @@
  *
  *
  *      Copyright 2009-2010 Sun Microsystems, Inc.
- *      Portions copyright 2011-2012 ForgeRock AS
+ *      Portions copyright 2011-2013 ForgeRock AS
  */
 
 package org.forgerock.opendj.examples;
 
-import static org.forgerock.opendj.ldap.ErrorResultException.newErrorResult;
-
 import java.io.FileInputStream;
-import java.io.FileNotFoundException;
 import java.io.IOException;
-import java.io.InputStream;
-import java.util.NavigableMap;
-import java.util.concurrent.ConcurrentSkipListMap;
-import java.util.concurrent.locks.ReentrantReadWriteLock;
 
 import javax.net.ssl.SSLContext;
 
-import org.forgerock.opendj.ldap.CancelledResultException;
 import org.forgerock.opendj.ldap.Connections;
-import org.forgerock.opendj.ldap.DN;
-import org.forgerock.opendj.ldap.Entry;
 import org.forgerock.opendj.ldap.ErrorResultException;
-import org.forgerock.opendj.ldap.Filter;
-import org.forgerock.opendj.ldap.IntermediateResponseHandler;
 import org.forgerock.opendj.ldap.KeyManagers;
 import org.forgerock.opendj.ldap.LDAPClientContext;
 import org.forgerock.opendj.ldap.LDAPListener;
 import org.forgerock.opendj.ldap.LDAPListenerOptions;
-import org.forgerock.opendj.ldap.LinkedHashMapEntry;
-import org.forgerock.opendj.ldap.Matcher;
-import org.forgerock.opendj.ldap.Modification;
-import org.forgerock.opendj.ldap.ModificationType;
-import org.forgerock.opendj.ldap.RequestContext;
-import org.forgerock.opendj.ldap.RequestHandler;
+import org.forgerock.opendj.ldap.MemoryBackend;
 import org.forgerock.opendj.ldap.ResultCode;
-import org.forgerock.opendj.ldap.ResultHandler;
 import org.forgerock.opendj.ldap.SSLContextBuilder;
-import org.forgerock.opendj.ldap.SearchResultHandler;
-import org.forgerock.opendj.ldap.SearchScope;
 import org.forgerock.opendj.ldap.ServerConnection;
 import org.forgerock.opendj.ldap.ServerConnectionFactory;
 import org.forgerock.opendj.ldap.TrustManagers;
-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.responses.BindResult;
-import org.forgerock.opendj.ldap.responses.CompareResult;
-import org.forgerock.opendj.ldap.responses.ExtendedResult;
-import org.forgerock.opendj.ldap.responses.Responses;
-import org.forgerock.opendj.ldap.responses.Result;
 import org.forgerock.opendj.ldif.LDIFEntryReader;
 
 /**
@@ -95,260 +62,6 @@
  * </pre>
  */
 public final class Server {
-    private static final class MemoryBackend implements RequestHandler<RequestContext> {
-        private final ConcurrentSkipListMap<DN, Entry> entries;
-        private final ReentrantReadWriteLock entryLock = new ReentrantReadWriteLock();
-
-        private MemoryBackend(final ConcurrentSkipListMap<DN, Entry> entries) {
-            this.entries = entries;
-        }
-
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        public void handleAdd(final RequestContext requestContext, final AddRequest request,
-                final IntermediateResponseHandler intermediateResponseHandler,
-                final ResultHandler<? super Result> resultHandler) {
-            // TODO: controls.
-            entryLock.writeLock().lock();
-            try {
-                DN dn = request.getName();
-                if (entries.containsKey(dn)) {
-                    resultHandler.handleErrorResult(ErrorResultException.newErrorResult(
-                            ResultCode.ENTRY_ALREADY_EXISTS, "The entry " + dn.toString()
-                                    + " already exists"));
-                }
-
-                DN parent = dn.parent();
-                if (!entries.containsKey(parent)) {
-                    resultHandler.handleErrorResult(ErrorResultException.newErrorResult(
-                            ResultCode.NO_SUCH_OBJECT, "The parent entry " + parent.toString()
-                                    + " does not exist"));
-                } else {
-                    entries.put(dn, request);
-                    resultHandler.handleResult(Responses.newResult(ResultCode.SUCCESS));
-                }
-            } finally {
-                entryLock.writeLock().unlock();
-            }
-        }
-
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        public void handleBind(final RequestContext requestContext, final int version,
-                final BindRequest request,
-                final IntermediateResponseHandler intermediateResponseHandler,
-                final ResultHandler<? super BindResult> resultHandler) {
-            if (request.getAuthenticationType() != ((byte) 0x80)) {
-                // TODO: SASL authentication not implemented.
-                resultHandler.handleErrorResult(newErrorResult(ResultCode.PROTOCOL_ERROR,
-                        "non-SIMPLE authentication not supported: "
-                                + request.getAuthenticationType()));
-            } else {
-                // TODO: always succeed.
-                resultHandler.handleResult(Responses.newBindResult(ResultCode.SUCCESS));
-            }
-        }
-
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        public void handleCompare(final RequestContext requestContext,
-                final CompareRequest request,
-                final IntermediateResponseHandler intermediateResponseHandler,
-                final ResultHandler<? super CompareResult> resultHandler) {
-            // TODO:
-        }
-
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        public void handleDelete(final RequestContext requestContext, final DeleteRequest request,
-                final IntermediateResponseHandler intermediateResponseHandler,
-                final ResultHandler<? super Result> resultHandler) {
-            // TODO: controls.
-            entryLock.writeLock().lock();
-            try {
-                // TODO: check for children.
-                DN dn = request.getName();
-                if (!entries.containsKey(dn)) {
-                    resultHandler.handleErrorResult(ErrorResultException.newErrorResult(
-                            ResultCode.NO_SUCH_OBJECT, "The entry " + dn.toString()
-                                    + " does not exist"));
-                } else {
-                    entries.remove(dn);
-                    resultHandler.handleResult(Responses.newResult(ResultCode.SUCCESS));
-                }
-            } finally {
-                entryLock.writeLock().unlock();
-            }
-        }
-
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        public <R extends ExtendedResult> void handleExtendedRequest(
-                final RequestContext requestContext, final ExtendedRequest<R> request,
-                final IntermediateResponseHandler intermediateResponseHandler,
-                final ResultHandler<? super R> resultHandler) {
-            // TODO: not implemented.
-            resultHandler.handleErrorResult(newErrorResult(ResultCode.PROTOCOL_ERROR,
-                    "Extended request operation not supported"));
-        }
-
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        public void handleModify(final RequestContext requestContext, final ModifyRequest request,
-                final IntermediateResponseHandler intermediateResponseHandler,
-                final ResultHandler<? super Result> resultHandler) {
-            // TODO: controls.
-            // TODO: read lock is not really enough since concurrent updates may
-            // still occur to the same entry.
-            entryLock.readLock().lock();
-            try {
-                DN dn = request.getName();
-                Entry entry = entries.get(dn);
-                if (entry == null) {
-                    resultHandler.handleErrorResult(ErrorResultException.newErrorResult(
-                            ResultCode.NO_SUCH_OBJECT, "The entry " + dn.toString()
-                                    + " does not exist"));
-                }
-
-                Entry newEntry = new LinkedHashMapEntry(entry);
-                for (Modification mod : request.getModifications()) {
-                    ModificationType modType = mod.getModificationType();
-                    if (modType.equals(ModificationType.ADD)) {
-                        // TODO: Reject empty attribute and duplicate values.
-                        newEntry.addAttribute(mod.getAttribute(), null);
-                    } else if (modType.equals(ModificationType.DELETE)) {
-                        // TODO: Reject missing values.
-                        newEntry.removeAttribute(mod.getAttribute(), null);
-                    } else if (modType.equals(ModificationType.REPLACE)) {
-                        newEntry.replaceAttribute(mod.getAttribute());
-                    } else {
-                        resultHandler.handleErrorResult(newErrorResult(ResultCode.PROTOCOL_ERROR,
-                                "Modify request contains an unsupported modification type"));
-                        return;
-                    }
-                }
-
-                entries.put(dn, newEntry);
-                resultHandler.handleResult(Responses.newResult(ResultCode.SUCCESS));
-            } finally {
-                entryLock.readLock().unlock();
-            }
-        }
-
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        public void handleModifyDN(final RequestContext requestContext,
-                final ModifyDNRequest request,
-                final IntermediateResponseHandler intermediateResponseHandler,
-                final ResultHandler<? super Result> resultHandler) {
-            // TODO: not implemented.
-            resultHandler.handleErrorResult(newErrorResult(ResultCode.PROTOCOL_ERROR,
-                    "ModifyDN request operation not supported"));
-        }
-
-        /**
-         * {@inheritDoc}
-         */
-        @Override
-        public void handleSearch(final RequestContext requestContext, final SearchRequest request,
-                final IntermediateResponseHandler intermediateResponseHandler,
-                final SearchResultHandler resultHandler) {
-            // TODO: controls, limits, etc.
-            entryLock.readLock().lock();
-            try {
-                DN dn = request.getName();
-                Entry baseEntry = entries.get(dn);
-                if (baseEntry == null) {
-                    resultHandler.handleErrorResult(ErrorResultException.newErrorResult(
-                            ResultCode.NO_SUCH_OBJECT, "The entry " + dn.toString()
-                                    + " does not exist"));
-                    return;
-                }
-
-                SearchScope scope = request.getScope();
-                Filter filter = request.getFilter();
-                Matcher matcher = filter.matcher();
-
-                if (scope.equals(SearchScope.BASE_OBJECT)) {
-                    if (matcher.matches(baseEntry).toBoolean()) {
-                        sendEntry(request, resultHandler, baseEntry);
-                    }
-                } else if (scope.equals(SearchScope.SINGLE_LEVEL)) {
-                    NavigableMap<DN, Entry> subtree = entries.tailMap(dn, false);
-                    for (Entry entry : subtree.values()) {
-                        // Check for cancellation.
-                        requestContext.checkIfCancelled(false);
-
-                        DN childDN = entry.getName();
-                        if (childDN.isChildOf(dn)) {
-                            if (!matcher.matches(entry).toBoolean()) {
-                                continue;
-                            }
-
-                            if (!sendEntry(request, resultHandler, entry)) {
-                                // Caller has asked to stop sending results.
-                                break;
-                            }
-                        } else if (!childDN.isSubordinateOrEqualTo(dn)) {
-                            // The remaining entries will be out of scope.
-                            break;
-                        }
-                    }
-                } else if (scope.equals(SearchScope.WHOLE_SUBTREE)) {
-                    NavigableMap<DN, Entry> subtree = entries.tailMap(dn);
-                    for (Entry entry : subtree.values()) {
-                        // Check for cancellation.
-                        requestContext.checkIfCancelled(false);
-
-                        DN childDN = entry.getName();
-                        if (childDN.isSubordinateOrEqualTo(dn)) {
-                            if (!matcher.matches(entry).toBoolean()) {
-                                continue;
-                            }
-
-                            if (!sendEntry(request, resultHandler, entry)) {
-                                // Caller has asked to stop sending results.
-                                break;
-                            }
-                        } else {
-                            // The remaining entries will be out of scope.
-                            break;
-                        }
-                    }
-                } else {
-                    resultHandler.handleErrorResult(newErrorResult(ResultCode.PROTOCOL_ERROR,
-                            "Search request contains an unsupported search scope"));
-                    return;
-                }
-
-                resultHandler.handleResult(Responses.newResult(ResultCode.SUCCESS));
-            } catch (CancelledResultException e) {
-                resultHandler.handleErrorResult(e);
-            } finally {
-                entryLock.readLock().unlock();
-            }
-        }
-
-        private boolean sendEntry(SearchRequest request, SearchResultHandler resultHandler,
-                Entry entry) {
-            // TODO: check filter, strip attributes.
-            return resultHandler.handleEntry(Responses.newSearchResultEntry(entry));
-        }
-    }
 
     /**
      * Main method.
@@ -373,8 +86,14 @@
         final String certNickname = (args.length == 6) ? args[5] : null;
 
         // Create the memory backend.
-        final ConcurrentSkipListMap<DN, Entry> entries = readEntriesFromLDIF(ldifFileName);
-        final MemoryBackend backend = new MemoryBackend(entries);
+        final MemoryBackend backend;
+        try {
+            backend = new MemoryBackend(new LDIFEntryReader(new FileInputStream(ldifFileName)));
+        } catch (final IOException e) {
+            System.err.println(e.getMessage());
+            System.exit(ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue());
+            return; // Keep compiler quiet.
+        }
 
         // Create a server connection adapter.
         final ServerConnectionFactory<LDAPClientContext, Integer> connectionHandler =
@@ -395,11 +114,13 @@
                                                 .toCharArray(), null))).setTrustManager(
                                 TrustManagers.trustAll()).getSSLContext();
 
-                ServerConnectionFactory<LDAPClientContext, Integer> sslWrapper =
+                final ServerConnectionFactory<LDAPClientContext, Integer> sslWrapper =
                         new ServerConnectionFactory<LDAPClientContext, Integer>() {
 
+                            @Override
                             public ServerConnection<Integer> handleAccept(
-                                    LDAPClientContext clientContext) throws ErrorResultException {
+                                    final LDAPClientContext clientContext)
+                                    throws ErrorResultException {
                                 clientContext.enableTLS(sslContext, null, null, false, false);
                                 return connectionHandler.handleAccept(clientContext);
                             }
@@ -422,63 +143,6 @@
         }
     }
 
-    /**
-     * Reads the entries from the named LDIF file.
-     *
-     * @param ldifFileName
-     *            The name of the LDIF file.
-     * @return The entries.
-     */
-    private static ConcurrentSkipListMap<DN, Entry> readEntriesFromLDIF(final String ldifFileName) {
-        final ConcurrentSkipListMap<DN, Entry> entries;
-        // Read the LDIF.
-        InputStream ldif;
-        try {
-            ldif = new FileInputStream(ldifFileName);
-        } catch (final FileNotFoundException e) {
-            System.err.println(e.getMessage());
-            System.exit(ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue());
-            return null; // Satisfy compiler.
-        }
-
-        entries = new ConcurrentSkipListMap<DN, Entry>();
-        final LDIFEntryReader reader = new LDIFEntryReader(ldif);
-        try {
-            while (reader.hasNext()) {
-                Entry entry = reader.readEntry();
-                entries.put(entry.getName(), entry);
-            }
-        } catch (final IOException e) {
-            System.err.println(e.getMessage());
-            System.exit(ResultCode.CLIENT_SIDE_LOCAL_ERROR.intValue());
-            return null; // Satisfy compiler.
-        } finally {
-            try {
-                reader.close();
-            } catch (final IOException ignored) {
-                // Ignore.
-            }
-        }
-
-        // Quickly sanity check that every entry (except root entries) have a
-        // parent.
-        boolean isValid = true;
-        for (DN dn : entries.keySet()) {
-            if (dn.size() > 1) {
-                DN parent = dn.parent();
-                if (!entries.containsKey(parent)) {
-                    System.err
-                            .println("The entry \"" + dn.toString() + "\" does not have a parent");
-                    isValid = false;
-                }
-            }
-        }
-        if (!isValid) {
-            System.exit(1);
-        }
-        return entries;
-    }
-
     private Server() {
         // Not used.
     }
diff --git a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/com/forgerock/opendj/util/ConnectionDecorator.java b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/com/forgerock/opendj/util/ConnectionDecorator.java
index 88ea33e..e865863 100644
--- a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/com/forgerock/opendj/util/ConnectionDecorator.java
+++ b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/com/forgerock/opendj/util/ConnectionDecorator.java
@@ -289,6 +289,16 @@
      * The default implementation is to delegate.
      */
     @Override
+    public Result deleteSubtree(final String name) throws ErrorResultException {
+        return connection.deleteSubtree(name);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to delegate.
+     */
+    @Override
     public <R extends ExtendedResult> R extendedRequest(final ExtendedRequest<R> request)
             throws ErrorResultException {
         return connection.extendedRequest(request);
diff --git a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/AbstractConnection.java b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/AbstractConnection.java
index e898072..167528b 100644
--- a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/AbstractConnection.java
+++ b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/AbstractConnection.java
@@ -22,7 +22,7 @@
  *
  *
  *      Copyright 2009-2010 Sun Microsystems, Inc.
- *      Portions copyright 2011-2012 ForgeRock AS
+ *      Portions copyright 2011-2013 ForgeRock AS
  */
 
 package org.forgerock.opendj.ldap;
@@ -36,6 +36,7 @@
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
 
+import org.forgerock.opendj.ldap.controls.SubtreeDeleteRequestControl;
 import org.forgerock.opendj.ldap.requests.AddRequest;
 import org.forgerock.opendj.ldap.requests.DeleteRequest;
 import org.forgerock.opendj.ldap.requests.ExtendedRequest;
@@ -363,6 +364,15 @@
      * {@inheritDoc}
      */
     @Override
+    public Result deleteSubtree(final String name) throws ErrorResultException {
+        return delete(Requests.newDeleteRequest(name).addControl(
+                SubtreeDeleteRequestControl.newControl(true)));
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
     public <R extends ExtendedResult> R extendedRequest(final ExtendedRequest<R> request)
             throws ErrorResultException {
         return extendedRequest(request, null);
diff --git a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Connection.java b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Connection.java
index 64c3c35..0afa53e 100644
--- a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Connection.java
+++ b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Connection.java
@@ -597,6 +597,37 @@
     Result delete(String name) throws ErrorResultException;
 
     /**
+     * Deletes the named entry and all of its subordinates from the Directory
+     * Server.
+     * <p>
+     * This method is equivalent to the following code:
+     *
+     * <pre>
+     * DeleteRequest request = new DeleteRequest(name).addControl(
+     * connection.delete(request);
+     * </pre>
+     *
+     * @param name
+     *            The distinguished name of the subtree base entry to be
+     *            deleted.
+     * @return The result of the operation.
+     * @throws ErrorResultException
+     *             If the result code indicates that the request failed for some
+     *             reason.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code name} could not be decoded using the default
+     *             schema.
+     * @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 name} was {@code null}.
+     */
+    Result deleteSubtree(String name) throws ErrorResultException;
+
+    /**
      * Asynchronously deletes an entry from the Directory Server using the
      * provided delete request.
      *
diff --git a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Entries.java b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Entries.java
index de5fcb4..7511b6b 100644
--- a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Entries.java
+++ b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Entries.java
@@ -22,13 +22,21 @@
  *
  *
  *      Copyright 2010 Sun Microsystems, Inc.
- *      Portions copyright 2011-2012 ForgeRock AS
+ *      Portions copyright 2011-2013 ForgeRock AS
  */
 
 package org.forgerock.opendj.ldap;
 
 import static org.forgerock.opendj.ldap.AttributeDescription.objectClass;
+import static org.forgerock.opendj.ldap.CoreMessages.ERR_ENTRY_DUPLICATE_VALUES;
+import static org.forgerock.opendj.ldap.CoreMessages.ERR_ENTRY_INCREMENT_CANNOT_PARSE_AS_INT;
+import static org.forgerock.opendj.ldap.CoreMessages.ERR_ENTRY_INCREMENT_INVALID_VALUE_COUNT;
+import static org.forgerock.opendj.ldap.CoreMessages.ERR_ENTRY_INCREMENT_NO_SUCH_ATTRIBUTE;
+import static org.forgerock.opendj.ldap.CoreMessages.ERR_ENTRY_NO_SUCH_VALUE;
+import static org.forgerock.opendj.ldap.CoreMessages.ERR_ENTRY_UNKNOWN_MODIFICATION_TYPE;
+import static org.forgerock.opendj.ldap.ErrorResultException.newErrorResult;
 
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
@@ -37,6 +45,7 @@
 import java.util.Set;
 
 import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.opendj.ldap.controls.PermissiveModifyRequestControl;
 import org.forgerock.opendj.ldap.requests.ModifyRequest;
 import org.forgerock.opendj.ldap.requests.Requests;
 import org.forgerock.opendj.ldap.schema.ObjectClass;
@@ -179,14 +188,16 @@
         /**
          * {@inheritDoc}
          */
-        public AttributeParser parseAttribute(AttributeDescription attributeDescription) {
+        @Override
+        public AttributeParser parseAttribute(final AttributeDescription attributeDescription) {
             return entry.parseAttribute(attributeDescription);
         }
 
         /**
          * {@inheritDoc}
          */
-        public AttributeParser parseAttribute(String attributeDescription) {
+        @Override
+        public AttributeParser parseAttribute(final String attributeDescription) {
             return entry.parseAttribute(attributeDescription);
         }
 
@@ -251,6 +262,13 @@
 
     }
 
+    private static final Comparator<Entry> COMPARATOR = new Comparator<Entry>() {
+        @Override
+        public int compare(final Entry o1, final Entry o2) {
+            return o1.getName().compareTo(o2.getName());
+        }
+    };
+
     private static final Function<Attribute, Attribute, Void> UNMODIFIABLE_ATTRIBUTE_FUNCTION =
             new Function<Attribute, Attribute, Void>() {
 
@@ -261,12 +279,6 @@
 
             };
 
-    private static final Comparator<Entry> COMPARATOR = new Comparator<Entry>() {
-        public int compare(Entry o1, Entry o2) {
-            return o1.getName().compareTo(o2.getName());
-        }
-    };
-
     /**
      * Returns a {@code Comparator} which can be used to compare entries by name
      * using the natural order for DN comparisons (parent before children).
@@ -576,6 +588,126 @@
     }
 
     /**
+     * Applies the provided modification to an entry. This method implements
+     * "permissive" modify semantics, ignoring attempts to add duplicate values
+     * or attempts to remove values which do not exist.
+     *
+     * @param entry
+     *            The entry to be modified.
+     * @param change
+     *            The modification to be applied to the entry.
+     * @return A reference to the updated entry.
+     * @throws ErrorResultException
+     *             If an error occurred while performing the change such as an
+     *             attempt to increment a value which is not a number. The entry
+     *             will not have been modified.
+     */
+    public static Entry modifyEntry(final Entry entry, final Modification change)
+            throws ErrorResultException {
+        return modifyEntry(entry, change, null);
+    }
+
+    /**
+     * Applies the provided modification to an entry. This method implements
+     * "permissive" modify semantics, recording attempts to add duplicate values
+     * or attempts to remove values which do not exist in the provided
+     * collection if provided.
+     *
+     * @param entry
+     *            The entry to be modified.
+     * @param change
+     *            The modification to be applied to the entry.
+     * @param conflictingValues
+     *            A collection into which duplicate or missing values will be
+     *            added, or {@code null} if conflicting values should not be
+     *            saved.
+     * @return A reference to the updated entry.
+     * @throws ErrorResultException
+     *             If an error occurred while performing the change such as an
+     *             attempt to increment a value which is not a number. The entry
+     *             will not have been modified.
+     */
+    public static Entry modifyEntry(final Entry entry, final Modification change,
+            final Collection<? super ByteString> conflictingValues) throws ErrorResultException {
+        final ModificationType modType = change.getModificationType();
+        if (modType.equals(ModificationType.ADD)) {
+            entry.addAttribute(change.getAttribute(), conflictingValues);
+        } else if (modType.equals(ModificationType.DELETE)) {
+            entry.removeAttribute(change.getAttribute(), conflictingValues);
+        } else if (modType.equals(ModificationType.REPLACE)) {
+            entry.replaceAttribute(change.getAttribute());
+        } else if (modType.equals(ModificationType.INCREMENT)) {
+            incrementAttribute(entry, change.getAttribute());
+        } else {
+            throw newErrorResult(ResultCode.UNWILLING_TO_PERFORM,
+                    ERR_ENTRY_UNKNOWN_MODIFICATION_TYPE.get(String.valueOf(modType)).toString());
+        }
+        return entry;
+    }
+
+    /**
+     * Applies the provided modification request to an entry. This method will
+     * utilize "permissive" modify semantics if the request contains the
+     * {@link PermissiveModifyRequestControl}.
+     *
+     * @param entry
+     *            The entry to be modified.
+     * @param changes
+     *            The modification request to be applied to the entry.
+     * @return A reference to the updated entry.
+     * @throws ErrorResultException
+     *             If an error occurred while performing the changes such as an
+     *             attempt to add duplicate values, remove values which do not
+     *             exist, or increment a value which is not a number. The entry
+     *             may have been modified.
+     */
+    public static Entry modifyEntry(final Entry entry, final ModifyRequest changes)
+            throws ErrorResultException {
+        final boolean isPermissive = changes.containsControl(PermissiveModifyRequestControl.OID);
+        return modifyEntry0(entry, changes.getModifications(), isPermissive);
+    }
+
+    /**
+     * Applies the provided modifications to an entry using "permissive" modify
+     * semantics.
+     *
+     * @param entry
+     *            The entry to be modified.
+     * @param changes
+     *            The modification request to be applied to the entry.
+     * @return A reference to the updated entry.
+     * @throws ErrorResultException
+     *             If an error occurred while performing the changes such as an
+     *             attempt to increment a value which is not a number. The entry
+     *             may have been modified.
+     */
+    public static Entry modifyEntryPermissive(final Entry entry,
+            final Collection<Modification> changes) throws ErrorResultException {
+        return modifyEntry0(entry, changes, true);
+    }
+
+    /**
+     * Applies the provided modifications to an entry using "strict" modify
+     * semantics. Attempts to add duplicate values or attempts to remove values
+     * which do not exist will cause the update to fail.
+     *
+     * @param entry
+     *            The entry to be modified.
+     * @param changes
+     *            The modification request to be applied to the entry.
+     * @return A reference to the updated entry.
+     * @throws ErrorResultException
+     *             If an error occurred while performing the changes such as an
+     *             attempt to add duplicate values, remove values which do not
+     *             exist, or increment a value which is not a number. The entry
+     *             may have been modified.
+     */
+    public static Entry modifyEntryStrict(final Entry entry, final Collection<Modification> changes)
+            throws ErrorResultException {
+        return modifyEntry0(entry, changes, false);
+    }
+
+    /**
      * Returns a read-only view of {@code entry} and its attributes. Query
      * operations on the returned entry and its attributes "read-through" to the
      * underlying entry or attribute, and attempts to modify the returned entry
@@ -596,6 +728,65 @@
         }
     }
 
+    private static void incrementAttribute(final Entry entry, final Attribute change)
+            throws ErrorResultException {
+        // First parse the change.
+        final AttributeDescription deltaAd = change.getAttributeDescription();
+        if (change.size() != 1) {
+            throw newErrorResult(ResultCode.CONSTRAINT_VIOLATION,
+                    ERR_ENTRY_INCREMENT_INVALID_VALUE_COUNT.get(deltaAd.toString()).toString());
+        }
+        final long delta;
+        try {
+            delta = change.parse().asLong();
+        } catch (final Exception e) {
+            throw newErrorResult(ResultCode.CONSTRAINT_VIOLATION,
+                    ERR_ENTRY_INCREMENT_CANNOT_PARSE_AS_INT.get(deltaAd.toString()).toString());
+        }
+
+        // Now apply the increment to the attribute.
+        final Attribute oldAttribute = entry.getAttribute(deltaAd);
+        if (oldAttribute == null) {
+            throw newErrorResult(ResultCode.NO_SUCH_ATTRIBUTE,
+                    ERR_ENTRY_INCREMENT_NO_SUCH_ATTRIBUTE.get(deltaAd.toString()).toString());
+        }
+
+        // Re-use existing attribute description in case it differs in case, etc.
+        final Attribute newAttribute = new LinkedAttribute(oldAttribute.getAttributeDescription());
+        try {
+            for (final Long value : oldAttribute.parse().asSetOfLong()) {
+                newAttribute.add(value + delta);
+            }
+        } catch (final Exception e) {
+            throw newErrorResult(ResultCode.CONSTRAINT_VIOLATION,
+                    ERR_ENTRY_INCREMENT_CANNOT_PARSE_AS_INT.get(deltaAd.toString()).toString());
+        }
+        entry.replaceAttribute(newAttribute);
+    }
+
+    private static Entry modifyEntry0(final Entry entry, final Collection<Modification> changes,
+            final boolean isPermissive) throws ErrorResultException {
+        final Collection<ByteString> conflictingValues =
+                isPermissive ? null : new ArrayList<ByteString>(0);
+        for (final Modification change : changes) {
+            modifyEntry(entry, change, conflictingValues);
+            if (!isPermissive && !conflictingValues.isEmpty()) {
+                if (change.getModificationType().equals(ModificationType.ADD)) {
+                    // Duplicate values.
+                    throw newErrorResult(ResultCode.ATTRIBUTE_OR_VALUE_EXISTS,
+                            ERR_ENTRY_DUPLICATE_VALUES.get(
+                                    change.getAttribute().getAttributeDescriptionAsString())
+                                    .toString());
+                } else {
+                    // Missing values.
+                    throw newErrorResult(ResultCode.NO_SUCH_ATTRIBUTE, ERR_ENTRY_NO_SUCH_VALUE.get(
+                            change.getAttribute().getAttributeDescriptionAsString()).toString());
+                }
+            }
+        }
+        return entry;
+    }
+
     // Prevent instantiation.
     private Entries() {
         // Nothing to do.
diff --git a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/ErrorResultException.java b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/ErrorResultException.java
index 4444d3b..e9cf922 100644
--- a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/ErrorResultException.java
+++ b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/ErrorResultException.java
@@ -162,6 +162,7 @@
                 || rc == ResultCode.CLIENT_SIDE_ENCODING_ERROR) {
             return new ConnectionException(result);
         } else if (rc == ResultCode.ATTRIBUTE_OR_VALUE_EXISTS
+                || rc == ResultCode.NO_SUCH_ATTRIBUTE
                 || rc == ResultCode.CONSTRAINT_VIOLATION || rc == ResultCode.ENTRY_ALREADY_EXISTS
                 || rc == ResultCode.INVALID_ATTRIBUTE_SYNTAX || rc == ResultCode.INVALID_DN_SYNTAX
                 || rc == ResultCode.NAMING_VIOLATION || rc == ResultCode.NOT_ALLOWED_ON_NONLEAF
diff --git a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/FixedConnectionPool.java b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/FixedConnectionPool.java
index bc6cc11..42ad688 100644
--- a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/FixedConnectionPool.java
+++ b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/FixedConnectionPool.java
@@ -309,6 +309,11 @@
         }
 
         @Override
+        public Result deleteSubtree(final String name) throws ErrorResultException {
+            return checkState().deleteSubtree(name);
+        }
+
+        @Override
         public <R extends ExtendedResult> R extendedRequest(final ExtendedRequest<R> request)
                 throws ErrorResultException {
             return checkState().extendedRequest(request);
diff --git a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/MemoryBackend.java b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/MemoryBackend.java
new file mode 100644
index 0000000..92020ff
--- /dev/null
+++ b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/MemoryBackend.java
@@ -0,0 +1,461 @@
+/*
+ * 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 2013 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import static org.forgerock.opendj.ldap.Attributes.singletonAttribute;
+import static org.forgerock.opendj.ldap.Entries.modifyEntry;
+import static org.forgerock.opendj.ldap.ErrorResultException.newErrorResult;
+import static org.forgerock.opendj.ldap.responses.Responses.newBindResult;
+import static org.forgerock.opendj.ldap.responses.Responses.newCompareResult;
+import static org.forgerock.opendj.ldap.responses.Responses.newResult;
+import static org.forgerock.opendj.ldap.responses.Responses.newSearchResultEntry;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.NavigableMap;
+import java.util.concurrent.ConcurrentSkipListMap;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import org.forgerock.i18n.LocalizedIllegalArgumentException;
+import org.forgerock.opendj.ldap.controls.AssertionRequestControl;
+import org.forgerock.opendj.ldap.controls.PostReadRequestControl;
+import org.forgerock.opendj.ldap.controls.PostReadResponseControl;
+import org.forgerock.opendj.ldap.controls.PreReadRequestControl;
+import org.forgerock.opendj.ldap.controls.PreReadResponseControl;
+import org.forgerock.opendj.ldap.controls.SubtreeDeleteRequestControl;
+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.GenericBindRequest;
+import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
+import org.forgerock.opendj.ldap.requests.ModifyRequest;
+import org.forgerock.opendj.ldap.requests.Request;
+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.CompareResult;
+import org.forgerock.opendj.ldap.responses.ExtendedResult;
+import org.forgerock.opendj.ldap.responses.Result;
+import org.forgerock.opendj.ldap.schema.Schema;
+import org.forgerock.opendj.ldif.EntryReader;
+
+/**
+ * A simple in memory back-end which can be used for testing. It is not intended
+ * for production use due to various limitations. The back-end implementations
+ * supports the following:
+ * <ul>
+ * <li>add, bind (simple), compare, delete, modify, and search operations, but
+ * not modifyDN nor extended operations
+ * <li>assertion, pre-, and post- read controls, subtree delete control, and
+ * permissive modify control
+ * <li>thread safety - supports concurrent operations
+ * </ul>
+ * It does not support the following:
+ * <ul>
+ * <li>high performance
+ * <li>secure password storage
+ * <li>schema checking
+ * <li>persistence
+ * <li>indexing
+ * </ul>
+ * This class can be used in conjunction with the factories defined in
+ * {@link Connections} to create simpler servers as well as mock LDAP
+ * connections. For example, to create a mock LDAP connection factory:
+ *
+ * <pre>
+ * MemoryBackend backend = new MemoryBackend();
+ * Connection connection = newInternalConnectionFactory(newServerConnectionFactory(backend), null)
+ *         .getConnection();
+ * </pre>
+ */
+public final class MemoryBackend implements RequestHandler<RequestContext> {
+    private final DecodeOptions decodeOptions;
+    private final ConcurrentSkipListMap<DN, Entry> entries = new ConcurrentSkipListMap<DN, Entry>();
+    private final ReentrantReadWriteLock entryLock = new ReentrantReadWriteLock();
+    private final Schema schema;
+
+    /**
+     * Creates a new empty memory backend which will use the default schema.
+     */
+    public MemoryBackend() {
+        this(Schema.getDefaultSchema());
+    }
+
+    /**
+     * Creates a new memory backend which will use the default schema, and will
+     * contain the entries read from the provided entry reader.
+     *
+     * @param reader
+     *            The entry reader.
+     * @throws IOException
+     *             If an unexpected IO error occurred while reading the entries.
+     */
+    public MemoryBackend(final EntryReader reader) throws IOException {
+        this(Schema.getDefaultSchema(), reader);
+    }
+
+    /**
+     * Creates a new empty memory backend which will use the provided schema.
+     *
+     * @param schema
+     *            The schema to use for decoding filters, etc.
+     */
+    public MemoryBackend(final Schema schema) {
+        this.schema = schema;
+        this.decodeOptions = new DecodeOptions().setSchema(schema);
+    }
+
+    /**
+     * Creates a new memory backend which will use the provided schema, and will
+     * contain the entries read from the provided entry reader.
+     *
+     * @param schema
+     *            The schema to use for decoding filters, etc.
+     * @param reader
+     *            The entry reader.
+     * @throws IOException
+     *             If an unexpected IO error occurred while reading the entries.
+     */
+    public MemoryBackend(final Schema schema, final EntryReader reader) throws IOException {
+        this.schema = schema;
+        this.decodeOptions = new DecodeOptions().setSchema(schema);
+        if (reader != null) {
+            try {
+                while (reader.hasNext()) {
+                    final Entry entry = reader.readEntry();
+                    final DN dn = entry.getName();
+                    if (entries.containsKey(dn)) {
+                        throw new ErrorResultIOException(newErrorResult(
+                                ResultCode.ENTRY_ALREADY_EXISTS, "Attempted to add the entry '"
+                                        + dn.toString() + "' multiple times"));
+                    } else {
+                        entries.put(dn, entry);
+                    }
+                }
+            } finally {
+                reader.close();
+            }
+        }
+    }
+
+    @Override
+    public void handleAdd(final RequestContext requestContext, final AddRequest request,
+            final IntermediateResponseHandler intermediateResponseHandler,
+            final ResultHandler<? super Result> resultHandler) {
+        entryLock.writeLock().lock();
+        try {
+            final DN dn = request.getName();
+            final DN parent = dn.parent();
+            if (entries.containsKey(dn)) {
+                throw newErrorResult(ResultCode.ENTRY_ALREADY_EXISTS, "The entry '" + dn.toString()
+                        + "' already exists");
+            } else if (!entries.containsKey(parent)) {
+                noSuchObject(parent);
+            } else {
+                entries.put(dn, request);
+                resultHandler.handleResult(getResult(request, null, request));
+            }
+        } catch (final ErrorResultException e) {
+            resultHandler.handleErrorResult(e);
+        } finally {
+            entryLock.writeLock().unlock();
+        }
+    }
+
+    @Override
+    public void handleBind(final RequestContext requestContext, final int version,
+            final BindRequest request,
+            final IntermediateResponseHandler intermediateResponseHandler,
+            final ResultHandler<? super BindResult> resultHandler) {
+        entryLock.readLock().lock();
+        try {
+            final DN username = DN.valueOf(request.getName(), schema);
+            final byte[] password;
+            if (request instanceof SimpleBindRequest) {
+                password = ((SimpleBindRequest) request).getPassword();
+            } else if (request instanceof GenericBindRequest
+                    && request.getAuthenticationType() == ((byte) 0x80)) {
+                password = ((GenericBindRequest) request).getAuthenticationValue();
+            } else {
+                throw newErrorResult(ResultCode.PROTOCOL_ERROR,
+                        "non-SIMPLE authentication not supported: "
+                                + request.getAuthenticationType());
+            }
+            final Entry entry = getRequiredEntry(null, username);
+            if (entry.containsAttribute("userPassword", password)) {
+                resultHandler.handleResult(getBindResult(request, entry, entry));
+            } else {
+                throw newErrorResult(ResultCode.INVALID_CREDENTIALS, "Wrong password");
+            }
+        } catch (final LocalizedIllegalArgumentException e) {
+            resultHandler.handleErrorResult(newErrorResult(ResultCode.PROTOCOL_ERROR, e));
+        } catch (final EntryNotFoundException e) {
+            /*
+             * Usually you would not include a diagnostic message, but we'll add
+             * one here because the memory back-end is not intended for
+             * production use.
+             */
+            resultHandler.handleErrorResult(newErrorResult(ResultCode.INVALID_CREDENTIALS,
+                    "Unknown user"));
+        } catch (final ErrorResultException e) {
+            resultHandler.handleErrorResult(e);
+        } finally {
+            entryLock.readLock().unlock();
+        }
+    }
+
+    @Override
+    public void handleCompare(final RequestContext requestContext, final CompareRequest request,
+            final IntermediateResponseHandler intermediateResponseHandler,
+            final ResultHandler<? super CompareResult> resultHandler) {
+        entryLock.readLock().lock();
+        try {
+            final DN dn = request.getName();
+            final Entry entry = getRequiredEntry(request, dn);
+            final Attribute assertion =
+                    singletonAttribute(request.getAttributeDescription(), request
+                            .getAssertionValue());
+            resultHandler.handleResult(getCompareResult(request, entry, entry.containsAttribute(
+                    assertion, null)));
+        } catch (final ErrorResultException e) {
+            resultHandler.handleErrorResult(e);
+        } finally {
+            entryLock.readLock().unlock();
+        }
+    }
+
+    @Override
+    public void handleDelete(final RequestContext requestContext, final DeleteRequest request,
+            final IntermediateResponseHandler intermediateResponseHandler,
+            final ResultHandler<? super Result> resultHandler) {
+        entryLock.writeLock().lock();
+        try {
+            final DN dn = request.getName();
+            final Entry entry = getRequiredEntry(request, dn);
+            if (request.getControl(SubtreeDeleteRequestControl.DECODER, decodeOptions) != null) {
+                // Subtree delete.
+                entries.subMap(dn, dn.child(RDN.maxValue())).clear();
+            } else {
+                // Must be leaf.
+                final DN next = entries.higherKey(dn);
+                if (next == null || !next.isChildOf(dn)) {
+                    entries.remove(dn);
+                } else {
+                    throw newErrorResult(ResultCode.NOT_ALLOWED_ON_NONLEAF);
+                }
+            }
+            resultHandler.handleResult(getResult(request, entry, null));
+        } catch (final DecodeException e) {
+            resultHandler.handleErrorResult(newErrorResult(ResultCode.PROTOCOL_ERROR, e));
+        } catch (final ErrorResultException e) {
+            resultHandler.handleErrorResult(e);
+        } finally {
+            entryLock.writeLock().unlock();
+        }
+    }
+
+    @Override
+    public <R extends ExtendedResult> void handleExtendedRequest(
+            final RequestContext requestContext, final ExtendedRequest<R> request,
+            final IntermediateResponseHandler intermediateResponseHandler,
+            final ResultHandler<? super R> resultHandler) {
+        resultHandler.handleErrorResult(newErrorResult(ResultCode.UNWILLING_TO_PERFORM,
+                "Extended request operation not supported"));
+    }
+
+    @Override
+    public void handleModify(final RequestContext requestContext, final ModifyRequest request,
+            final IntermediateResponseHandler intermediateResponseHandler,
+            final ResultHandler<? super Result> resultHandler) {
+        entryLock.writeLock().lock();
+        try {
+            final DN dn = request.getName();
+            final Entry entry = getRequiredEntry(request, dn);
+            final Entry newEntry = new LinkedHashMapEntry(entry);
+            entries.put(dn, modifyEntry(newEntry, request));
+            resultHandler.handleResult(getResult(request, entry, newEntry));
+        } catch (final ErrorResultException e) {
+            resultHandler.handleErrorResult(e);
+        } finally {
+            entryLock.writeLock().unlock();
+        }
+    }
+
+    @Override
+    public void handleModifyDN(final RequestContext requestContext, final ModifyDNRequest request,
+            final IntermediateResponseHandler intermediateResponseHandler,
+            final ResultHandler<? super Result> resultHandler) {
+        resultHandler.handleErrorResult(newErrorResult(ResultCode.UNWILLING_TO_PERFORM,
+                "ModifyDN request operation not supported"));
+    }
+
+    @Override
+    public void handleSearch(final RequestContext requestContext, final SearchRequest request,
+            final IntermediateResponseHandler intermediateResponseHandler,
+            final SearchResultHandler resultHandler) {
+        entryLock.readLock().lock();
+        try {
+            final DN dn = request.getName();
+            final Entry baseEntry = getRequiredEntry(request, dn);
+            final SearchScope scope = request.getScope();
+            final Filter filter = request.getFilter();
+            final Matcher matcher = filter.matcher(schema);
+
+            if (scope.equals(SearchScope.BASE_OBJECT)) {
+                if (matcher.matches(baseEntry).toBoolean()) {
+                    sendEntry(request, resultHandler, baseEntry);
+                }
+            } else if (scope.equals(SearchScope.SINGLE_LEVEL)) {
+                final NavigableMap<DN, Entry> subtree =
+                        entries.subMap(dn, dn.child(RDN.maxValue()));
+                for (final Entry entry : subtree.values()) {
+                    // Check for cancellation.
+                    requestContext.checkIfCancelled(false);
+                    final DN childDN = entry.getName();
+                    if (childDN.isChildOf(dn)) {
+                        if (matcher.matches(entry).toBoolean()
+                                && !sendEntry(request, resultHandler, entry)) {
+                            // Caller has asked to stop sending results.
+                            break;
+                        }
+                    }
+                }
+            } else if (scope.equals(SearchScope.WHOLE_SUBTREE)) {
+                final NavigableMap<DN, Entry> subtree =
+                        entries.subMap(dn, dn.child(RDN.maxValue()));
+                for (final Entry entry : subtree.values()) {
+                    // Check for cancellation.
+                    requestContext.checkIfCancelled(false);
+                    if (matcher.matches(entry).toBoolean()
+                            && !sendEntry(request, resultHandler, entry)) {
+                        // Caller has asked to stop sending results.
+                        break;
+                    }
+                }
+            } else {
+                throw newErrorResult(ResultCode.PROTOCOL_ERROR,
+                        "Search request contains an unsupported search scope");
+            }
+            resultHandler.handleResult(newResult(ResultCode.SUCCESS));
+        } catch (final ErrorResultException e) {
+            resultHandler.handleErrorResult(e);
+        } finally {
+            entryLock.readLock().unlock();
+        }
+    }
+
+    private <R extends Result> R addResultControls(final Request request, final Entry before,
+            final Entry after, final R result) throws ErrorResultException {
+        try {
+            // Add pre-read response control if requested.
+            final PreReadRequestControl preRead =
+                    request.getControl(PreReadRequestControl.DECODER, decodeOptions);
+            if (preRead != null) {
+                if (preRead.isCritical() && before == null) {
+                    throw newErrorResult(ResultCode.UNAVAILABLE_CRITICAL_EXTENSION);
+                } else {
+                    result.addControl(PreReadResponseControl.newControl(filter(before, preRead
+                            .getAttributes())));
+                }
+            }
+
+            // Add post-read response control if requested.
+            final PostReadRequestControl postRead =
+                    request.getControl(PostReadRequestControl.DECODER, decodeOptions);
+            if (postRead != null) {
+                if (postRead.isCritical() && after == null) {
+                    throw newErrorResult(ResultCode.UNAVAILABLE_CRITICAL_EXTENSION);
+                } else {
+                    result.addControl(PostReadResponseControl.newControl(filter(after, postRead
+                            .getAttributes())));
+                }
+            }
+            return result;
+        } catch (final DecodeException e) {
+            throw newErrorResult(ResultCode.PROTOCOL_ERROR, e);
+        }
+    }
+
+    private Entry filter(final Entry entry, final Collection<String> attributes) {
+        // FIXME: attribute filtering not supported yet.
+        return entry;
+    }
+
+    private BindResult getBindResult(final BindRequest request, final Entry before,
+            final Entry after) throws ErrorResultException {
+        return addResultControls(request, before, after, newBindResult(ResultCode.SUCCESS));
+    }
+
+    private CompareResult getCompareResult(final CompareRequest request, final Entry entry,
+            final boolean compareResult) throws ErrorResultException {
+        return addResultControls(
+                request,
+                entry,
+                entry,
+                newCompareResult(compareResult ? ResultCode.COMPARE_TRUE : ResultCode.COMPARE_FALSE));
+    }
+
+    private Entry getRequiredEntry(final Request request, final DN dn) throws ErrorResultException {
+        final Entry entry = entries.get(dn);
+        if (entry == null) {
+            noSuchObject(dn);
+        } else if (request != null) {
+            AssertionRequestControl control;
+            try {
+                control = request.getControl(AssertionRequestControl.DECODER, decodeOptions);
+            } catch (final DecodeException e) {
+                throw newErrorResult(ResultCode.PROTOCOL_ERROR, e);
+            }
+            if (control != null) {
+                final Filter filter = control.getFilter();
+                final Matcher matcher = filter.matcher(schema);
+                if (!matcher.matches(entry).toBoolean()) {
+                    throw newErrorResult(ResultCode.ASSERTION_FAILED, "The filter '"
+                            + filter.toString() + "' did not match the entry '"
+                            + entry.getName().toString() + "'");
+                }
+            }
+        }
+        return entry;
+    }
+
+    private Result getResult(final Request request, final Entry before, final Entry after)
+            throws ErrorResultException {
+        return addResultControls(request, before, after, newResult(ResultCode.SUCCESS));
+    }
+
+    private void noSuchObject(final DN dn) throws ErrorResultException {
+        throw newErrorResult(ResultCode.NO_SUCH_OBJECT, "The entry '" + dn.toString()
+                + "' does not exist");
+    }
+
+    private boolean sendEntry(final SearchRequest request, final SearchResultHandler resultHandler,
+            final Entry entry) {
+        return resultHandler
+                .handleEntry(newSearchResultEntry(filter(entry, request.getAttributes())));
+    }
+}
diff --git a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Modification.java b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Modification.java
index 1e05070..45974ae 100644
--- a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Modification.java
+++ b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Modification.java
@@ -33,7 +33,6 @@
  */
 public final class Modification {
     private final ModificationType modificationType;
-
     private final Attribute attribute;
 
     /**
@@ -44,7 +43,8 @@
      * fully immutable:
      *
      * <pre>
-     * Modification change = new Modification(modificationType, Types.unmodifiableAttribute(attribute));
+     * Modification change = new Modification(modificationType, Attributes
+     *         .unmodifiableAttribute(attribute));
      * </pre>
      *
      * @param modificationType
@@ -54,7 +54,6 @@
      */
     public Modification(final ModificationType modificationType, final Attribute attribute) {
         Validator.ensureNotNull(modificationType, attribute);
-
         this.modificationType = modificationType;
         this.attribute = attribute;
     }
diff --git a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/controls/PostReadRequestControl.java b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/controls/PostReadRequestControl.java
index 23f6edd..2c65bee 100644
--- a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/controls/PostReadRequestControl.java
+++ b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/controls/PostReadRequestControl.java
@@ -22,21 +22,24 @@
  *
  *
  *      Copyright 2009 Sun Microsystems, Inc.
- *      Portions copyright 2012 ForgeRock AS.
+ *      Portions copyright 2012-2013 ForgeRock AS.
  */
 
 package org.forgerock.opendj.ldap.controls;
 
+import static java.util.Arrays.asList;
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singletonList;
+import static java.util.Collections.unmodifiableList;
 import static org.forgerock.opendj.ldap.CoreMessages.ERR_POSTREADREQ_CANNOT_DECODE_VALUE;
 import static org.forgerock.opendj.ldap.CoreMessages.ERR_POSTREADREQ_NO_CONTROL_VALUE;
 import static org.forgerock.opendj.ldap.CoreMessages.ERR_POSTREAD_CONTROL_BAD_OID;
 
 import java.io.IOException;
-import java.util.Arrays;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.LinkedHashSet;
-import java.util.Set;
+import java.util.List;
 
 import org.forgerock.i18n.LocalizableMessage;
 import org.forgerock.opendj.asn1.ASN1;
@@ -88,16 +91,16 @@
      */
     public static final String OID = "1.3.6.1.1.13.2";
 
-    // The set of raw attributes to return in the entry.
-    private final Set<String> attributes;
+    // The list of raw attributes to return in the entry.
+    private final List<String> attributes;
 
     private final boolean isCritical;
 
     private static final PostReadRequestControl CRITICAL_EMPTY_INSTANCE =
-            new PostReadRequestControl(true, Collections.<String> emptySet());
+            new PostReadRequestControl(true, Collections.<String> emptyList());
 
     private static final PostReadRequestControl NONCRITICAL_EMPTY_INSTANCE =
-            new PostReadRequestControl(false, Collections.<String> emptySet());
+            new PostReadRequestControl(false, Collections.<String> emptyList());
 
     /**
      * A decoder which can be used for decoding the post-read request control.
@@ -126,23 +129,23 @@
                     }
 
                     final ASN1Reader reader = ASN1.getReader(control.getValue());
-                    Set<String> attributes;
+                    List<String> attributes;
                     try {
                         reader.readStartSequence();
                         if (reader.hasNextElement()) {
                             final String firstAttribute = reader.readOctetStringAsString();
                             if (reader.hasNextElement()) {
-                                attributes = new LinkedHashSet<String>();
+                                attributes = new ArrayList<String>();
                                 attributes.add(firstAttribute);
                                 do {
                                     attributes.add(reader.readOctetStringAsString());
                                 } while (reader.hasNextElement());
-                                attributes = Collections.unmodifiableSet(attributes);
+                                attributes = unmodifiableList(attributes);
                             } else {
-                                attributes = Collections.singleton(firstAttribute);
+                                attributes = singletonList(firstAttribute);
                             }
                         } else {
-                            attributes = Collections.emptySet();
+                            attributes = emptyList();
                         }
                         reader.readEndSequence();
                     } catch (final Exception ae) {
@@ -190,11 +193,11 @@
         if (attributes.isEmpty()) {
             return isCritical ? CRITICAL_EMPTY_INSTANCE : NONCRITICAL_EMPTY_INSTANCE;
         } else if (attributes.size() == 1) {
-            return new PostReadRequestControl(isCritical, Collections.singleton(attributes
-                    .iterator().next()));
+            return new PostReadRequestControl(isCritical, singletonList(attributes.iterator()
+                    .next()));
         } else {
-            final Set<String> attributeSet = new LinkedHashSet<String>(attributes);
-            return new PostReadRequestControl(isCritical, Collections.unmodifiableSet(attributeSet));
+            return new PostReadRequestControl(isCritical, unmodifiableList(new ArrayList<String>(
+                    attributes)));
         }
     }
 
@@ -221,28 +224,28 @@
         if (attributes.length == 0) {
             return isCritical ? CRITICAL_EMPTY_INSTANCE : NONCRITICAL_EMPTY_INSTANCE;
         } else if (attributes.length == 1) {
-            return new PostReadRequestControl(isCritical, Collections.singleton(attributes[0]));
+            return new PostReadRequestControl(isCritical, singletonList(attributes[0]));
         } else {
-            final Set<String> attributeSet = new LinkedHashSet<String>(Arrays.asList(attributes));
-            return new PostReadRequestControl(isCritical, Collections.unmodifiableSet(attributeSet));
+            return new PostReadRequestControl(isCritical, unmodifiableList(new ArrayList<String>(
+                    asList(attributes))));
         }
     }
 
-    private PostReadRequestControl(final boolean isCritical, final Set<String> attributes) {
+    private PostReadRequestControl(final boolean isCritical, final List<String> attributes) {
         this.isCritical = isCritical;
         this.attributes = attributes;
     }
 
     /**
-     * Returns an unmodifiable set containing the names of attributes to be
+     * Returns an unmodifiable list containing the names of attributes to be
      * included with the response control. Attributes that are sub-types of
-     * listed attributes are implicitly included. The returned set may be empty,
-     * indicating that all user attributes should be returned.
+     * listed attributes are implicitly included. The returned list may be
+     * empty, indicating that all user attributes should be returned.
      *
-     * @return An unmodifiable set containing the names of attributes to be
+     * @return An unmodifiable list containing the names of attributes to be
      *         included with the response control.
      */
-    public Set<String> getAttributes() {
+    public List<String> getAttributes() {
         return attributes;
     }
 
diff --git a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/controls/PreReadRequestControl.java b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/controls/PreReadRequestControl.java
index bc00844..c2325e5 100644
--- a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/controls/PreReadRequestControl.java
+++ b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/controls/PreReadRequestControl.java
@@ -22,21 +22,24 @@
  *
  *
  *      Copyright 2009 Sun Microsystems, Inc.
- *      Portions copyright 2012 ForgeRock AS.
+ *      Portions copyright 2012-2013 ForgeRock AS.
  */
 
 package org.forgerock.opendj.ldap.controls;
 
+import static java.util.Arrays.asList;
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singletonList;
+import static java.util.Collections.unmodifiableList;
 import static org.forgerock.opendj.ldap.CoreMessages.ERR_PREREADREQ_CANNOT_DECODE_VALUE;
 import static org.forgerock.opendj.ldap.CoreMessages.ERR_PREREADREQ_NO_CONTROL_VALUE;
 import static org.forgerock.opendj.ldap.CoreMessages.ERR_PREREAD_CONTROL_BAD_OID;
 
 import java.io.IOException;
-import java.util.Arrays;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.LinkedHashSet;
-import java.util.Set;
+import java.util.List;
 
 import org.forgerock.i18n.LocalizableMessage;
 import org.forgerock.opendj.asn1.ASN1;
@@ -87,16 +90,16 @@
      */
     public static final String OID = "1.3.6.1.1.13.1";
 
-    // The set of raw attributes to return in the entry.
-    private final Set<String> attributes;
+    // The list of raw attributes to return in the entry.
+    private final List<String> attributes;
 
     private final boolean isCritical;
 
     private static final PreReadRequestControl CRITICAL_EMPTY_INSTANCE = new PreReadRequestControl(
-            true, Collections.<String> emptySet());
+            true, Collections.<String> emptyList());
 
     private static final PreReadRequestControl NONCRITICAL_EMPTY_INSTANCE =
-            new PreReadRequestControl(false, Collections.<String> emptySet());
+            new PreReadRequestControl(false, Collections.<String> emptyList());
 
     /**
      * A decoder which can be used for decoding the pre-read request control.
@@ -125,23 +128,23 @@
                     }
 
                     final ASN1Reader reader = ASN1.getReader(control.getValue());
-                    Set<String> attributes;
+                    List<String> attributes;
                     try {
                         reader.readStartSequence();
                         if (reader.hasNextElement()) {
                             final String firstAttribute = reader.readOctetStringAsString();
                             if (reader.hasNextElement()) {
-                                attributes = new LinkedHashSet<String>();
+                                attributes = new ArrayList<String>();
                                 attributes.add(firstAttribute);
                                 do {
                                     attributes.add(reader.readOctetStringAsString());
                                 } while (reader.hasNextElement());
-                                attributes = Collections.unmodifiableSet(attributes);
+                                attributes = unmodifiableList(attributes);
                             } else {
-                                attributes = Collections.singleton(firstAttribute);
+                                attributes = singletonList(firstAttribute);
                             }
                         } else {
-                            attributes = Collections.emptySet();
+                            attributes = emptyList();
                         }
                         reader.readEndSequence();
                     } catch (final Exception ae) {
@@ -189,11 +192,11 @@
         if (attributes.isEmpty()) {
             return isCritical ? CRITICAL_EMPTY_INSTANCE : NONCRITICAL_EMPTY_INSTANCE;
         } else if (attributes.size() == 1) {
-            return new PreReadRequestControl(isCritical, Collections.singleton(attributes
-                    .iterator().next()));
+            return new PreReadRequestControl(isCritical,
+                    singletonList(attributes.iterator().next()));
         } else {
-            final Set<String> attributeSet = new LinkedHashSet<String>(attributes);
-            return new PreReadRequestControl(isCritical, Collections.unmodifiableSet(attributeSet));
+            return new PreReadRequestControl(isCritical, unmodifiableList(new ArrayList<String>(
+                    attributes)));
         }
     }
 
@@ -220,28 +223,28 @@
         if (attributes.length == 0) {
             return isCritical ? CRITICAL_EMPTY_INSTANCE : NONCRITICAL_EMPTY_INSTANCE;
         } else if (attributes.length == 1) {
-            return new PreReadRequestControl(isCritical, Collections.singleton(attributes[0]));
+            return new PreReadRequestControl(isCritical, singletonList(attributes[0]));
         } else {
-            final Set<String> attributeSet = new LinkedHashSet<String>(Arrays.asList(attributes));
-            return new PreReadRequestControl(isCritical, Collections.unmodifiableSet(attributeSet));
+            return new PreReadRequestControl(isCritical, unmodifiableList(new ArrayList<String>(
+                    asList(attributes))));
         }
     }
 
-    private PreReadRequestControl(final boolean isCritical, final Set<String> attributes) {
+    private PreReadRequestControl(final boolean isCritical, final List<String> attributes) {
         this.isCritical = isCritical;
         this.attributes = attributes;
     }
 
     /**
-     * Returns an unmodifiable set containing the names of attributes to be
+     * Returns an unmodifiable list containing the names of attributes to be
      * included with the response control. Attributes that are sub-types of
-     * listed attributes are implicitly included. The returned set may be empty,
-     * indicating that all user attributes should be returned.
+     * listed attributes are implicitly included. The returned list may be
+     * empty, indicating that all user attributes should be returned.
      *
-     * @return An unmodifiable set containing the names of attributes to be
+     * @return An unmodifiable list containing the names of attributes to be
      *         included with the response control.
      */
-    public Set<String> getAttributes() {
+    public List<String> getAttributes() {
         return attributes;
     }
 
diff --git a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/requests/AbstractRequestImpl.java b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/requests/AbstractRequestImpl.java
index 90f8fd5..f13b118 100644
--- a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/requests/AbstractRequestImpl.java
+++ b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/requests/AbstractRequestImpl.java
@@ -22,7 +22,7 @@
  *
  *
  *      Copyright 2010 Sun Microsystems, Inc.
- *      Portions copyright 2012 ForgeRock AS.
+ *      Portions copyright 2012-2013 ForgeRock AS.
  */
 
 package org.forgerock.opendj.ldap.requests;
@@ -45,6 +45,20 @@
  *            The type of request.
  */
 abstract class AbstractRequestImpl<R extends Request> implements Request {
+
+    // Used by unmodifiable implementations as well.
+    static Control getControl(final List<Control> controls, final String oid) {
+        // Avoid creating an iterator if possible.
+        if (!controls.isEmpty()) {
+            for (final Control control : controls) {
+                if (control.getOID().equals(oid)) {
+                    return control;
+                }
+            }
+        }
+        return null;
+    }
+
     private final List<Control> controls = new LinkedList<Control>();
 
     /**
@@ -63,9 +77,9 @@
      * @throws NullPointerException
      *             If {@code request} was {@code null} .
      */
-    AbstractRequestImpl(Request request) {
+    AbstractRequestImpl(final Request request) {
         Validator.ensureNotNull(request);
-        for (Control control : request.getControls()) {
+        for (final Control control : request.getControls()) {
             // Create defensive copy.
             controls.add(GenericControl.newControl(control));
         }
@@ -74,6 +88,7 @@
     /**
      * {@inheritDoc}
      */
+    @Override
     public final R addControl(final Control control) {
         Validator.ensureNotNull(control);
         controls.add(control);
@@ -83,27 +98,26 @@
     /**
      * {@inheritDoc}
      */
-    public final <C extends Control> C getControl(final ControlDecoder<C> decoder,
-            final DecodeOptions options) throws DecodeException {
-        Validator.ensureNotNull(decoder, options);
-
-        // Avoid creating an iterator if possible.
-        if (controls.isEmpty()) {
-            return null;
-        }
-
-        for (final Control control : controls) {
-            if (control.getOID().equals(decoder.getOID())) {
-                return decoder.decodeControl(control, options);
-            }
-        }
-
-        return null;
+    @Override
+    public boolean containsControl(final String oid) {
+        return getControl(controls, oid) != null;
     }
 
     /**
      * {@inheritDoc}
      */
+    @Override
+    public final <C extends Control> C getControl(final ControlDecoder<C> decoder,
+            final DecodeOptions options) throws DecodeException {
+        Validator.ensureNotNull(decoder, options);
+        final Control control = getControl(controls, decoder.getOID());
+        return control != null ? decoder.decodeControl(control, options) : null;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
     public final List<Control> getControls() {
         return controls;
     }
diff --git a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/requests/AbstractUnmodifiableRequest.java b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/requests/AbstractUnmodifiableRequest.java
index f62e14a..4586e85 100644
--- a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/requests/AbstractUnmodifiableRequest.java
+++ b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/requests/AbstractUnmodifiableRequest.java
@@ -22,7 +22,7 @@
  *
  *
  *      Copyright 2010 Sun Microsystems, Inc.
- *      Portions copyright 2012 ForgeRock AS.
+ *      Portions copyright 2012-2013 ForgeRock AS.
  */
 
 package org.forgerock.opendj.ldap.requests;
@@ -73,38 +73,38 @@
      * {@inheritDoc}
      */
     @Override
+    public boolean containsControl(final String oid) {
+        return impl.containsControl(oid);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
     public final <C extends Control> C getControl(final ControlDecoder<C> decoder,
             final DecodeOptions options) throws DecodeException {
         Validator.ensureNotNull(decoder, options);
 
         final List<Control> controls = impl.getControls();
-
-        // Avoid creating an iterator if possible.
-        if (controls.isEmpty()) {
+        final Control control = AbstractRequestImpl.getControl(controls, decoder.getOID());
+        if (control != null) {
+            // Got a match. Return a defensive copy only if necessary.
+            final C decodedControl = decoder.decodeControl(control, options);
+            if (decodedControl != control) {
+                // This was not the original control so return it
+                // immediately.
+                return decodedControl;
+            } else if (decodedControl instanceof GenericControl) {
+                // Generic controls are immutable, so return it immediately.
+                return decodedControl;
+            } else {
+                // Re-decode to get defensive copy.
+                final GenericControl genericControl = GenericControl.newControl(control);
+                return decoder.decodeControl(genericControl, options);
+            }
+        } else {
             return null;
         }
-
-        for (final Control control : controls) {
-            if (control.getOID().equals(decoder.getOID())) {
-                // Got a match. Return a defensive copy only if necessary.
-                final C decodedControl = decoder.decodeControl(control, options);
-
-                if (decodedControl != control) {
-                    // This was not the original control so return it
-                    // immediately.
-                    return decodedControl;
-                } else if (decodedControl instanceof GenericControl) {
-                    // Generic controls are immutable, so return it immediately.
-                    return decodedControl;
-                } else {
-                    // Re-decode to get defensive copy.
-                    final GenericControl genericControl = GenericControl.newControl(control);
-                    return decoder.decodeControl(genericControl, options);
-                }
-            }
-        }
-
-        return null;
     }
 
     /**
@@ -113,8 +113,7 @@
     @Override
     public final List<Control> getControls() {
         // We need to make all controls unmodifiable as well, which implies
-        // making
-        // defensive copies where necessary.
+        // making defensive copies where necessary.
         final Function<Control, Control, Void> function = new Function<Control, Control, Void>() {
 
             @Override
diff --git a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/requests/Request.java b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/requests/Request.java
index 668f552..908720f 100644
--- a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/requests/Request.java
+++ b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/requests/Request.java
@@ -22,7 +22,7 @@
  *
  *
  *      Copyright 2009-2010 Sun Microsystems, Inc.
- *      Portions copyright 2012 ForgeRock AS.
+ *      Portions copyright 2012-2013 ForgeRock AS.
  */
 
 package org.forgerock.opendj.ldap.requests;
@@ -54,6 +54,17 @@
     Request addControl(Control control);
 
     /**
+     * Returns {@code true} if this request contains the specified request
+     * control.
+     *
+     * @param oid
+     *            The numeric OID of the request control.
+     * @return {@code true} if this request contains the specified request
+     *         control.
+     */
+    boolean containsControl(String oid);
+
+    /**
      * Decodes and returns the first control in this request having an OID
      * corresponding to the provided control decoder.
      *
diff --git a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/responses/AbstractResponseImpl.java b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/responses/AbstractResponseImpl.java
index 96195d0..04409f0 100644
--- a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/responses/AbstractResponseImpl.java
+++ b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/responses/AbstractResponseImpl.java
@@ -22,7 +22,7 @@
  *
  *
  *      Copyright 2010 Sun Microsystems, Inc.
- *      Portions copyright 2012 ForgeRock AS.
+ *      Portions copyright 2012-2013 ForgeRock AS.
  */
 
 package org.forgerock.opendj.ldap.responses;
@@ -45,6 +45,19 @@
  *            The type of response.
  */
 abstract class AbstractResponseImpl<S extends Response> implements Response {
+    // Used by unmodifiable implementations as well.
+    static Control getControl(final List<Control> controls, final String oid) {
+        // Avoid creating an iterator if possible.
+        if (!controls.isEmpty()) {
+            for (final Control control : controls) {
+                if (control.getOID().equals(oid)) {
+                    return control;
+                }
+            }
+        }
+        return null;
+    }
+
     private final List<Control> controls = new LinkedList<Control>();
 
     /**
@@ -63,9 +76,9 @@
      * @throws NullPointerException
      *             If {@code response} was {@code null} .
      */
-    AbstractResponseImpl(Response response) {
+    AbstractResponseImpl(final Response response) {
         Validator.ensureNotNull(response);
-        for (Control control : response.getControls()) {
+        for (final Control control : response.getControls()) {
             // Create defensive copy.
             controls.add(GenericControl.newControl(control));
         }
@@ -74,6 +87,7 @@
     /**
      * {@inheritDoc}
      */
+    @Override
     public final S addControl(final Control control) {
         Validator.ensureNotNull(control);
         controls.add(control);
@@ -83,27 +97,26 @@
     /**
      * {@inheritDoc}
      */
-    public final <C extends Control> C getControl(final ControlDecoder<C> decoder,
-            final DecodeOptions options) throws DecodeException {
-        Validator.ensureNotNull(decoder, options);
-
-        // Avoid creating an iterator if possible.
-        if (controls.isEmpty()) {
-            return null;
-        }
-
-        for (final Control control : controls) {
-            if (control.getOID().equals(decoder.getOID())) {
-                return decoder.decodeControl(control, options);
-            }
-        }
-
-        return null;
+    @Override
+    public boolean containsControl(final String oid) {
+        return getControl(controls, oid) != null;
     }
 
     /**
      * {@inheritDoc}
      */
+    @Override
+    public final <C extends Control> C getControl(final ControlDecoder<C> decoder,
+            final DecodeOptions options) throws DecodeException {
+        Validator.ensureNotNull(decoder, options);
+        final Control control = getControl(controls, decoder.getOID());
+        return control != null ? decoder.decodeControl(control, options) : null;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
     public final List<Control> getControls() {
         return controls;
     }
@@ -112,5 +125,4 @@
     public abstract String toString();
 
     abstract S getThis();
-
 }
diff --git a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/responses/AbstractUnmodifiableResponseImpl.java b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/responses/AbstractUnmodifiableResponseImpl.java
index ce9f554..28cd4ed 100644
--- a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/responses/AbstractUnmodifiableResponseImpl.java
+++ b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/responses/AbstractUnmodifiableResponseImpl.java
@@ -22,7 +22,7 @@
  *
  *
  *      Copyright 2010 Sun Microsystems, Inc.
- *      Portions copyright 2012 ForgeRock AS.
+ *      Portions copyright 2012-2013 ForgeRock AS.
  */
 
 package org.forgerock.opendj.ldap.responses;
@@ -75,38 +75,38 @@
      * {@inheritDoc}
      */
     @Override
+    public boolean containsControl(final String oid) {
+        return impl.containsControl(oid);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
     public final <C extends Control> C getControl(final ControlDecoder<C> decoder,
             final DecodeOptions options) throws DecodeException {
         Validator.ensureNotNull(decoder, options);
 
         final List<Control> controls = impl.getControls();
-
-        // Avoid creating an iterator if possible.
-        if (controls.isEmpty()) {
+        final Control control = AbstractResponseImpl.getControl(controls, decoder.getOID());
+        if (control != null) {
+            // Got a match. Return a defensive copy only if necessary.
+            final C decodedControl = decoder.decodeControl(control, options);
+            if (decodedControl != control) {
+                // This was not the original control so return it
+                // immediately.
+                return decodedControl;
+            } else if (decodedControl instanceof GenericControl) {
+                // Generic controls are immutable, so return it immediately.
+                return decodedControl;
+            } else {
+                // Re-decode to get defensive copy.
+                final GenericControl genericControl = GenericControl.newControl(control);
+                return decoder.decodeControl(genericControl, options);
+            }
+        } else {
             return null;
         }
-
-        for (final Control control : controls) {
-            if (control.getOID().equals(decoder.getOID())) {
-                // Got a match. Return a defensive copy only if necessary.
-                final C decodedControl = decoder.decodeControl(control, options);
-
-                if (decodedControl != control) {
-                    // This was not the original control so return it
-                    // immediately.
-                    return decodedControl;
-                } else if (decodedControl instanceof GenericControl) {
-                    // Generic controls are immutable, so return it immediately.
-                    return decodedControl;
-                } else {
-                    // Re-decode to get defensive copy.
-                    final GenericControl genericControl = GenericControl.newControl(control);
-                    return decoder.decodeControl(genericControl, options);
-                }
-            }
-        }
-
-        return null;
     }
 
     /**
diff --git a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/responses/Response.java b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/responses/Response.java
index 829ceba..e2632df 100644
--- a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/responses/Response.java
+++ b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/responses/Response.java
@@ -22,7 +22,7 @@
  *
  *
  *      Copyright 2009 Sun Microsystems, Inc.
- *      Portions copyright 2012 ForgeRock AS.
+ *      Portions copyright 2012-2013 ForgeRock AS.
  */
 
 package org.forgerock.opendj.ldap.responses;
@@ -54,6 +54,17 @@
     Response addControl(Control control);
 
     /**
+     * Returns {@code true} if this response contains the specified response
+     * control.
+     *
+     * @param oid
+     *            The numeric OID of the response control.
+     * @return {@code true} if this response contains the specified response
+     *         control.
+     */
+    boolean containsControl(String oid);
+
+    /**
      * Decodes and returns the first control in this response having an OID
      * corresponding to the provided control decoder.
      *
diff --git a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/resources/org/forgerock/opendj/ldap/core.properties b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/resources/org/forgerock/opendj/ldap/core.properties
index a051a85..abe82c2 100755
--- a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/resources/org/forgerock/opendj/ldap/core.properties
+++ b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/resources/org/forgerock/opendj/ldap/core.properties
@@ -22,7 +22,7 @@
 #
 #
 #      Copyright 2010 Sun Microsystems, Inc.
-#      Portions copyright 2011-2012 ForgeRock AS
+#      Portions copyright 2011-2013 ForgeRock AS
 #
 ERR_ATTR_SYNTAX_UNKNOWN_APPROXIMATE_MATCHING_RULE=Unable to retrieve \
  approximate matching rule %s used as the default for the %s attribute syntax. \
@@ -1405,3 +1405,16 @@
 ERR_LDIF_MALFORMED_CONTROL=Unable to parse LDIF change record starting at line %d \
  with distinguished name "%s" because it contained a malformed control \
  "%s"
+ERR_ENTRY_UNKNOWN_MODIFICATION_TYPE=Unsupported modification type '%s'
+ERR_ENTRY_DUPLICATE_VALUES=Unable to add one or more values to attribute \
+ '%s' because at least one of the values already exists
+ERR_ENTRY_NO_SUCH_VALUE=Unable to remove one or more values from attribute \
+ '%s' because at least one of the attributes does not exist in the entry
+ERR_ENTRY_INCREMENT_NO_SUCH_ATTRIBUTE=Unable to increment the value of attribute \
+ '%s' because that attribute does not exist in the entry
+ERR_ENTRY_INCREMENT_INVALID_VALUE_COUNT=Unable to increment the value of \
+ attribute '%s' because the provided modification did not have exactly \
+ one value to use as the increment
+ERR_ENTRY_INCREMENT_CANNOT_PARSE_AS_INT=Unable to increment the value of \
+ attribute '%s' because either the current value or the increment could \
+ not be parsed as an integer
diff --git a/opendj-sdk/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/LDAPServer.java b/opendj-sdk/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/LDAPServer.java
index c2f9743..dd851f4 100644
--- a/opendj-sdk/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/LDAPServer.java
+++ b/opendj-sdk/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/LDAPServer.java
@@ -22,7 +22,7 @@
  *
  *
  *      Copyright 2010 Sun Microsystems, Inc.
- *      Portions copyright 2011-2012 ForgeRock AS
+ *      Portions copyright 2011-2013 ForgeRock AS
  */
 
 package org.forgerock.opendj.ldap;
@@ -103,6 +103,10 @@
             return request.addControl(cntrl);
         }
 
+        public boolean containsControl(final String oid) {
+            return request.containsControl(oid);
+        }
+
         public <C extends Control> C getControl(final ControlDecoder<C> decoder,
                 final DecodeOptions options) throws DecodeException {
             return request.getControl(decoder, options);
diff --git a/opendj-sdk/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/MemoryBackendTestCase.java b/opendj-sdk/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/MemoryBackendTestCase.java
new file mode 100644
index 0000000..e008a3f
--- /dev/null
+++ b/opendj-sdk/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/MemoryBackendTestCase.java
@@ -0,0 +1,455 @@
+/*
+ * 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 2013 ForgeRock AS.
+ */
+package org.forgerock.opendj.ldap;
+
+import static org.fest.assertions.Assertions.assertThat;
+import static org.forgerock.opendj.ldap.Connections.newInternalConnectionFactory;
+import static org.forgerock.opendj.ldap.Connections.newServerConnectionFactory;
+import static org.forgerock.opendj.ldap.requests.Requests.newAddRequest;
+import static org.forgerock.opendj.ldap.requests.Requests.newDeleteRequest;
+import static org.forgerock.opendj.ldap.requests.Requests.newModifyRequest;
+import static org.forgerock.opendj.ldap.requests.Requests.newSimpleBindRequest;
+import static org.forgerock.opendj.ldif.LDIFEntryReader.valueOfLDIFEntry;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+import org.forgerock.opendj.ldap.controls.AssertionRequestControl;
+import org.forgerock.opendj.ldap.controls.PermissiveModifyRequestControl;
+import org.forgerock.opendj.ldap.controls.PostReadRequestControl;
+import org.forgerock.opendj.ldap.controls.PostReadResponseControl;
+import org.forgerock.opendj.ldap.controls.PreReadRequestControl;
+import org.forgerock.opendj.ldap.controls.PreReadResponseControl;
+import org.forgerock.opendj.ldif.ConnectionEntryReader;
+import org.forgerock.opendj.ldif.LDIFEntryReader;
+import org.testng.annotations.Test;
+
+/**
+ * Memory backend tests.
+ */
+@SuppressWarnings("javadoc")
+public class MemoryBackendTestCase extends SdkTestCase {
+
+    @Test
+    public void testAdd() throws Exception {
+        final Connection connection = getConnection();
+        final Entry newDomain =
+                valueOfLDIFEntry("dn: dc=new domain,dc=com", "objectClass: domain",
+                        "objectClass: top", "dc: new domain");
+        connection.add(newDomain);
+        assertThat(connection.readEntry("dc=new domain,dc=com")).isEqualTo(newDomain);
+    }
+
+    @Test(expectedExceptions = ConstraintViolationException.class)
+    public void testAddAlreadyExists() throws Exception {
+        final Connection connection = getConnection();
+        connection.add(valueOfLDIFEntry("dn: dc=example,dc=com", "objectClass: domain",
+                "objectClass: top", "dc: example"));
+    }
+
+    @Test(expectedExceptions = EntryNotFoundException.class)
+    public void testAddNoParent() throws Exception {
+        final Connection connection = getConnection();
+        connection.add(valueOfLDIFEntry("dn: dc=new domain,dc=missing,dc=com",
+                "objectClass: domain", "objectClass: top", "dc: new domain"));
+    }
+
+    @Test
+    public void testAddPostRead() throws Exception {
+        final Connection connection = getConnection();
+        final Entry newDomain =
+                valueOfLDIFEntry("dn: dc=new domain,dc=com", "objectClass: domain",
+                        "objectClass: top", "dc: new domain");
+        assertThat(
+                connection.add(
+                        newAddRequest(newDomain)
+                                .addControl(PostReadRequestControl.newControl(true))).getControl(
+                        PostReadResponseControl.DECODER, new DecodeOptions()).getEntry())
+                .isEqualTo(newDomain);
+    }
+
+    @Test(expectedExceptions = ErrorResultException.class)
+    public void testAddPreRead() throws Exception {
+        final Connection connection = getConnection();
+        final Entry newDomain =
+                valueOfLDIFEntry("dn: dc=new domain,dc=com", "objectClass: domain",
+                        "objectClass: top", "dc: new domain");
+        connection.add(newAddRequest(newDomain).addControl(PreReadRequestControl.newControl(true)));
+    }
+
+    @Test
+    public void testCompareFalse() throws Exception {
+        final Connection connection = getConnection();
+        assertThat(connection.compare("dc=example,dc=com", "objectclass", "person").matched())
+                .isFalse();
+    }
+
+    @Test(expectedExceptions = EntryNotFoundException.class)
+    public void testCompareNoSuchObject() throws Exception {
+        final Connection connection = getConnection();
+        connection.compare("uid=missing,ou=people,dc=example,dc=com", "uid", "missing");
+    }
+
+    @Test
+    public void testCompareTrue() throws Exception {
+        final Connection connection = getConnection();
+        assertThat(connection.compare("dc=example,dc=com", "objectclass", "domain").matched())
+                .isTrue();
+    }
+
+    @Test(expectedExceptions = AssertionFailureException.class)
+    public void testDeleteAssertionFalse() throws Exception {
+        final Connection connection = getConnection();
+        connection.delete(newDeleteRequest("dc=xxx,dc=com").addControl(
+                AssertionRequestControl.newControl(true, Filter.valueOf("(objectclass=person)"))));
+    }
+
+    @Test
+    public void testDeleteAssertionTrue() throws Exception {
+        final Connection connection = getConnection();
+        connection.delete(newDeleteRequest("dc=xxx,dc=com").addControl(
+                AssertionRequestControl.newControl(true, Filter.valueOf("(objectclass=domain)"))));
+    }
+
+    @Test(expectedExceptions = EntryNotFoundException.class)
+    public void testDeleteNoSuchObject() throws Exception {
+        final Connection connection = getConnection();
+        connection.delete("uid=missing,ou=people,dc=example,dc=com");
+    }
+
+    @Test
+    public void testDeleteOnLeaf() throws Exception {
+        final Connection connection = getConnection();
+        connection.delete("uid=test1,ou=people,dc=example,dc=com");
+        try {
+            connection.readEntry("dc=example,dc=com");
+        } catch (final EntryNotFoundException expected) {
+            // Do nothing.
+        }
+    }
+
+    @Test(expectedExceptions = ConstraintViolationException.class)
+    public void testDeleteOnNonLeaf() throws Exception {
+        final Connection connection = getConnection();
+        try {
+            connection.delete("dc=example,dc=com");
+        } finally {
+            assertThat(connection.readEntry("dc=example,dc=com")).isNotNull();
+        }
+    }
+
+    @Test(expectedExceptions = ErrorResultException.class)
+    public void testDeletePostRead() throws Exception {
+        final Connection connection = getConnection();
+        connection.delete(newDeleteRequest("dc=xxx,dc=com").addControl(
+                PostReadRequestControl.newControl(true)));
+    }
+
+    @Test
+    public void testDeletePreRead() throws Exception {
+        final Connection connection = getConnection();
+        assertThat(
+                connection.delete(
+                        newDeleteRequest("dc=xxx,dc=com").addControl(
+                                PreReadRequestControl.newControl(true))).getControl(
+                        PreReadResponseControl.DECODER, new DecodeOptions()).getEntry()).isEqualTo(
+                valueOfLDIFEntry("dn: dc=xxx,dc=com", "objectClass: domain", "objectClass: top",
+                        "dc: xxx"));
+    }
+
+    @Test
+    public void testDeleteSubtree() throws Exception {
+        final Connection connection = getConnection();
+        connection.deleteSubtree("dc=example,dc=com");
+        for (final String name : Arrays.asList("dc=example,dc=com", "ou=people,dc=example,dc=com",
+                "uid=test1,ou=people,dc=example,dc=com", "uid=test2,ou=people,dc=example,dc=com")) {
+            try {
+                connection.readEntry(name);
+            } catch (final EntryNotFoundException expected) {
+                // Do nothing.
+            }
+        }
+        assertThat(connection.readEntry("dc=xxx,dc=com")).isNotNull();
+    }
+
+    @Test
+    public void testModify() throws Exception {
+        final Connection connection = getConnection();
+        connection.modify("dn: dc=example,dc=com", "changetype: modify", "add: description",
+                "description: test description");
+        assertThat(connection.readEntry("dc=example,dc=com")).isEqualTo(
+                valueOfLDIFEntry("dn: dc=example,dc=com", "objectClass: domain",
+                        "objectClass: top", "dc: example", "description: test description"));
+    }
+
+    @Test(expectedExceptions = AssertionFailureException.class)
+    public void testModifyAssertionFalse() throws Exception {
+        final Connection connection = getConnection();
+        connection.modify(newModifyRequest("dn: dc=example,dc=com", "changetype: modify",
+                "add: description", "description: test description").addControl(
+                AssertionRequestControl.newControl(true, Filter.valueOf("(objectclass=person)"))));
+    }
+
+    @Test
+    public void testModifyAssertionTrue() throws Exception {
+        final Connection connection = getConnection();
+        connection.modify(newModifyRequest("dn: dc=example,dc=com", "changetype: modify",
+                "add: description", "description: test description").addControl(
+                AssertionRequestControl.newControl(true, Filter.valueOf("(objectclass=domain)"))));
+    }
+
+    @Test
+    public void testModifyBindPostRead() throws Exception {
+        final Connection connection = getConnection();
+        assertThat(
+                connection.modify(
+                        newModifyRequest("dn: dc=example,dc=com", "changetype: modify",
+                                "add: description", "description: test description").addControl(
+                                PostReadRequestControl.newControl(true))).getControl(
+                        PostReadResponseControl.DECODER, new DecodeOptions()).getEntry())
+                .isEqualTo(
+                        valueOfLDIFEntry("dn: dc=example,dc=com", "objectClass: domain",
+                                "objectClass: top", "dc: example", "description: test description"));
+    }
+
+    @Test
+    public void testModifyIncrement() throws Exception {
+        final Connection connection = getConnection();
+        connection.modify("dn: dc=example,dc=com", "changetype: modify", "add: integer",
+                "integer: 100", "-", "increment: integer", "integer: 10");
+        assertThat(connection.readEntry("dc=example,dc=com")).isEqualTo(
+                valueOfLDIFEntry("dn: dc=example,dc=com", "objectClass: domain",
+                        "objectClass: top", "dc: example", "integer: 110"));
+    }
+
+    @Test(expectedExceptions = ConstraintViolationException.class)
+    public void testModifyIncrementBadDelta() throws Exception {
+        final Connection connection = getConnection();
+        connection.modify("dn: dc=example,dc=com", "changetype: modify", "add: integer",
+                "integer: 100", "-", "increment: integer", "integer: nan");
+    }
+
+    @Test(expectedExceptions = ConstraintViolationException.class)
+    public void testModifyIncrementBadValue() throws Exception {
+        final Connection connection = getConnection();
+        connection.modify("dn: dc=example,dc=com", "changetype: modify", "add: integer",
+                "integer: nan", "-", "increment: integer", "integer: 10");
+    }
+
+    @Test(expectedExceptions = EntryNotFoundException.class)
+    public void testModifyNoSuchObject() throws Exception {
+        final Connection connection = getConnection();
+        connection.modify("dn: dc=missing,dc=com", "changetype: modify", "add: description",
+                "description: test description");
+    }
+
+    @Test
+    public void testModifyPermissiveWithDuplicateValues() throws Exception {
+        final Connection connection = getConnection();
+        connection.modify(newModifyRequest("dn: dc=example,dc=com", "changetype: modify",
+                "add: dc", "dc: example").addControl(
+                PermissiveModifyRequestControl.newControl(true)));
+        assertThat(connection.readEntry("dc=example,dc=com")).isEqualTo(
+                valueOfLDIFEntry("dn: dc=example,dc=com", "objectClass: domain",
+                        "objectClass: top", "dc: example"));
+    }
+
+    @Test
+    public void testModifyPermissiveWithMissingValues() throws Exception {
+        final Connection connection = getConnection();
+        connection.modify(newModifyRequest("dn: dc=example,dc=com", "changetype: modify",
+                "delete: dc", "dc: xxx")
+                .addControl(PermissiveModifyRequestControl.newControl(true)));
+        assertThat(connection.readEntry("dc=example,dc=com")).isEqualTo(
+                valueOfLDIFEntry("dn: dc=example,dc=com", "objectClass: domain",
+                        "objectClass: top", "dc: example"));
+    }
+
+    @Test
+    public void testModifyPreRead() throws Exception {
+        final Connection connection = getConnection();
+        assertThat(
+                connection.modify(
+                        newModifyRequest("dn: dc=example,dc=com", "changetype: modify",
+                                "add: description", "description: test description").addControl(
+                                PreReadRequestControl.newControl(true))).getControl(
+                        PreReadResponseControl.DECODER, new DecodeOptions()).getEntry()).isEqualTo(
+                valueOfLDIFEntry("dn: dc=example,dc=com", "objectClass: domain",
+                        "objectClass: top", "dc: example"));
+    }
+
+    @Test(expectedExceptions = ConstraintViolationException.class)
+    public void testModifyStrictWithDuplicateValues() throws Exception {
+        final Connection connection = getConnection();
+        connection.modify("dn: dc=example,dc=com", "changetype: modify", "add: dc", "dc: example");
+    }
+
+    @Test(expectedExceptions = ConstraintViolationException.class)
+    public void testModifyStrictWithMissingValues() throws Exception {
+        final Connection connection = getConnection();
+        connection.modify("dn: dc=example,dc=com", "changetype: modify", "delete: dc", "dc: xxx");
+    }
+
+    @Test
+    public void testSearchBase() throws Exception {
+        final Connection connection = getConnection();
+        assertThat(connection.readEntry("dc=example,dc=com")).isEqualTo(
+                valueOfLDIFEntry("dn: dc=example,dc=com", "objectClass: domain",
+                        "objectClass: top", "dc: example"));
+    }
+
+    @Test(expectedExceptions = EntryNotFoundException.class)
+    public void testSearchBaseNoSuchObject() throws Exception {
+        final Connection connection = getConnection();
+        connection.readEntry("dc=missing,dc=com");
+    }
+
+    @Test
+    public void testSearchOneLevel() throws Exception {
+        final Connection connection = getConnection();
+        final ConnectionEntryReader reader =
+                connection.search("dc=com", SearchScope.SINGLE_LEVEL, "(objectClass=*)");
+        assertThat(reader.readEntry()).isEqualTo(
+                valueOfLDIFEntry("dn: dc=example,dc=com", "objectClass: domain",
+                        "objectClass: top", "dc: example"));
+        assertThat(reader.readEntry()).isEqualTo(
+                valueOfLDIFEntry("dn: dc=xxx,dc=com", "objectClass: domain", "objectClass: top",
+                        "dc: xxx"));
+        assertThat(reader.hasNext()).isFalse();
+    }
+
+    @Test
+    public void testSearchSubtree() throws Exception {
+        final Connection connection = getConnection();
+        assertThat(
+                connection.searchSingleEntry("dc=example,dc=com", SearchScope.WHOLE_SUBTREE,
+                        "(uid=test1)")).isEqualTo(getUser1Entry());
+    }
+
+    @Test(expectedExceptions = EntryNotFoundException.class)
+    public void testSearchSubtreeNotFound() throws Exception {
+        final Connection connection = getConnection();
+        connection.searchSingleEntry("dc=example,dc=com", SearchScope.WHOLE_SUBTREE,
+                "(uid=missing)");
+    }
+
+    @Test
+    public void testSimpleBind() throws Exception {
+        final Connection connection = getConnection();
+        connection.bind("uid=test1,ou=people,dc=example,dc=com", "password".toCharArray());
+    }
+
+    @Test(expectedExceptions = AuthenticationException.class)
+    public void testSimpleBindBadPassword() throws Exception {
+        final Connection connection = getConnection();
+        connection.bind("uid=test1,ou=people,dc=example,dc=com", "bad".toCharArray());
+    }
+
+    @Test(expectedExceptions = AuthenticationException.class)
+    public void testSimpleBindNoSuchUser() throws Exception {
+        final Connection connection = getConnection();
+        connection.bind("uid=missing,ou=people,dc=example,dc=com", "password".toCharArray());
+    }
+
+    @Test
+    public void testSimpleBindPostRead() throws Exception {
+        final Connection connection = getConnection();
+        assertThat(
+                connection.bind(
+                        newSimpleBindRequest("uid=test1,ou=people,dc=example,dc=com",
+                                "password".toCharArray()).addControl(
+                                PostReadRequestControl.newControl(true))).getControl(
+                        PostReadResponseControl.DECODER, new DecodeOptions()).getEntry())
+                .isEqualTo(getUser1Entry());
+    }
+
+    @Test
+    public void testSimpleBindPreRead() throws Exception {
+        final Connection connection = getConnection();
+        assertThat(
+                connection.bind(
+                        newSimpleBindRequest("uid=test1,ou=people,dc=example,dc=com",
+                                "password".toCharArray()).addControl(
+                                PreReadRequestControl.newControl(true))).getControl(
+                        PreReadResponseControl.DECODER, new DecodeOptions()).getEntry()).isEqualTo(
+                getUser1Entry());
+    }
+
+    private Connection getConnection() throws IOException, ErrorResultException {
+        // @formatter:off
+        final MemoryBackend backend =
+                new MemoryBackend(new LDIFEntryReader(
+                        "dn: dc=com",
+                        "objectClass: domain",
+                        "objectClass: top",
+                        "dc: com",
+                        "",
+                        "dn: dc=example,dc=com",
+                        "objectClass: domain",
+                        "objectClass: top",
+                        "dc: example",
+                        "",
+                        "dn: ou=People,dc=example,dc=com",
+                        "objectClass: organizationalunit",
+                        "objectClass: top",
+                        "ou: People",
+                        "",
+                        "dn: uid=test1,ou=People,dc=example,dc=com",
+                        "objectClass: top",
+                        "objectClass: person",
+                        "uid: test1",
+                        "userpassword: password",
+                        "cn: test user 1",
+                        "sn: user 1",
+                        "",
+                        "dn: uid=test2,ou=People,dc=example,dc=com",
+                        "objectClass: top",
+                        "objectClass: person",
+                        "uid: test2",
+                        "userpassword: password",
+                        "cn: test user 2",
+                        "sn: user 2",
+                        "",
+                        "dn: dc=xxx,dc=com",
+                        "objectClass: domain",
+                        "objectClass: top",
+                        "dc: xxx"
+                ));
+        // @formatter:on
+
+        final Connection connection =
+                newInternalConnectionFactory(newServerConnectionFactory(backend), null)
+                        .getConnection();
+        return connection;
+    }
+
+    private Entry getUser1Entry() {
+        return valueOfLDIFEntry("dn: uid=test1,ou=People,dc=example,dc=com", "objectClass: top",
+                "objectClass: person", "uid: test1", "userpassword: password", "cn: test user 1",
+                "sn: user 1");
+    }
+
+}

--
Gitblit v1.10.0