From df38eb2ff437f173ad2adf208da699393e2bee8c Mon Sep 17 00:00:00 2001
From: Matthew Swift <matthew.swift@forgerock.com>
Date: Fri, 05 Jul 2013 21:41:32 +0000
Subject: [PATCH] Fix OPENDJ-1044: Doing a PUT with JSON data identical to what is on the server results in 400 status code

---
 opendj3/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java              |  295 +++++++++++++++++++++-----------
 opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Utils.java                          |    6 
 opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java |  199 ++++++++++++++-------
 3 files changed, 333 insertions(+), 167 deletions(-)

diff --git a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java
index ad470ef..80f0ef2 100644
--- a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java
+++ b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java
@@ -15,6 +15,7 @@
  */
 package org.forgerock.opendj.rest2ldap;
 
+import static java.util.Arrays.asList;
 import static org.forgerock.opendj.ldap.Filter.alwaysFalse;
 import static org.forgerock.opendj.ldap.Filter.alwaysTrue;
 import static org.forgerock.opendj.ldap.requests.Requests.newAddRequest;
@@ -211,69 +212,103 @@
         final Context c = wrap(context);
         final ResultHandler<Resource> h = wrap(c, handler);
 
-        /*
-         * Get the connection, search if needed, then determine modifications,
-         * then perform modify.
-         */
-        c.run(h, doUpdate(c, resourceId, request.getRevision(), new ResultHandler<DN>() {
-            @Override
-            public void handleError(final ResourceException error) {
-                h.handleError(error);
-            }
-
-            @Override
-            public void handleResult(final DN dn) {
-                //  Convert the patch operations to LDAP modifications.
-                final ResultHandler<List<Modification>> handler =
-                        accumulate(request.getPatchOperations().size(),
-                                new ResultHandler<List<List<Modification>>>() {
-                                    @Override
-                                    public void handleError(final ResourceException error) {
-                                        h.handleError(error);
-                                    }
-
-                                    @Override
-                                    public void handleResult(final List<List<Modification>> result) {
-                                        //  The patch operations have been converted successfully.
-                                        try {
-                                            final ModifyRequest modifyRequest =
-                                                    newModifyRequest(dn);
-                                            if (config.readOnUpdatePolicy() == CONTROLS) {
-                                                final String[] attributes =
-                                                        getLDAPAttributes(c, request.getFields());
-                                                modifyRequest.addControl(PostReadRequestControl
-                                                        .newControl(false, attributes));
-                                            }
-                                            if (config.usePermissiveModify()) {
-                                                modifyRequest
-                                                        .addControl(PermissiveModifyRequestControl
-                                                                .newControl(true));
-                                            }
-                                            addAssertionControl(modifyRequest, request
-                                                    .getRevision());
-
-                                            // Add the modifications.
-                                            for (final List<Modification> modifications : result) {
-                                                if (modifications != null) {
-                                                    modifyRequest.getModifications().addAll(
-                                                            modifications);
-                                                }
-                                            }
-
-                                            // Perform the modify request.
-                                            c.getConnection().applyChangeAsync(modifyRequest, null,
-                                                    postUpdateHandler(c, h));
-                                        } catch (final Exception e) {
-                                            h.handleError(asResourceException(e));
-                                        }
-                                    }
-                                });
-
-                for (final PatchOperation operation : request.getPatchOperations()) {
-                    attributeMapper.patch(c, new JsonPointer(), operation, handler);
+        if (request.getPatchOperations().isEmpty()) {
+            /*
+             * This patch is a no-op so just read the entry and check its
+             * version.
+             */
+            c.run(h, new Runnable() {
+                @Override
+                public void run() {
+                    final String[] attributes = getLDAPAttributes(c, request.getFields());
+                    final SearchRequest searchRequest =
+                            nameStrategy.createSearchRequest(c, getBaseDN(c), resourceId)
+                                    .addAttribute(attributes);
+                    c.getConnection().searchSingleEntryAsync(searchRequest,
+                            postEmptyPatchHandler(c, request, h));
                 }
-            }
-        }));
+            });
+        } else {
+            /*
+             * Get the connection, search if needed, then determine
+             * modifications, then perform modify.
+             */
+            c.run(h, doUpdate(c, resourceId, request.getRevision(), new ResultHandler<DN>() {
+                @Override
+                public void handleError(final ResourceException error) {
+                    h.handleError(error);
+                }
+
+                @Override
+                public void handleResult(final DN dn) {
+                    //  Convert the patch operations to LDAP modifications.
+                    final ResultHandler<List<Modification>> handler =
+                            accumulate(request.getPatchOperations().size(),
+                                    new ResultHandler<List<List<Modification>>>() {
+                                        @Override
+                                        public void handleError(final ResourceException error) {
+                                            h.handleError(error);
+                                        }
+
+                                        @Override
+                                        public void handleResult(
+                                                final List<List<Modification>> result) {
+                                            //  The patch operations have been converted successfully.
+                                            try {
+                                                final ModifyRequest modifyRequest =
+                                                        newModifyRequest(dn);
+
+                                                // Add the modifications.
+                                                for (final List<Modification> modifications : result) {
+                                                    if (modifications != null) {
+                                                        modifyRequest.getModifications().addAll(
+                                                                modifications);
+                                                    }
+                                                }
+
+                                                final List<String> attributes =
+                                                        asList(getLDAPAttributes(c, request
+                                                                .getFields()));
+                                                if (modifyRequest.getModifications().isEmpty()) {
+                                                    /*
+                                                     * This patch is a no-op so
+                                                     * just read the entry and
+                                                     * check its version.
+                                                     */
+                                                    c.getConnection().readEntryAsync(dn,
+                                                            attributes,
+                                                            postEmptyPatchHandler(c, request, h));
+                                                } else {
+                                                    // Add controls and perform the modify request.
+                                                    if (config.readOnUpdatePolicy() == CONTROLS) {
+                                                        modifyRequest
+                                                                .addControl(PostReadRequestControl
+                                                                        .newControl(false,
+                                                                                attributes));
+                                                    }
+                                                    if (config.usePermissiveModify()) {
+                                                        modifyRequest
+                                                                .addControl(PermissiveModifyRequestControl
+                                                                        .newControl(true));
+                                                    }
+                                                    addAssertionControl(modifyRequest, request
+                                                            .getRevision());
+                                                    c.getConnection().applyChangeAsync(
+                                                            modifyRequest, null,
+                                                            postUpdateHandler(c, h));
+                                                }
+                                            } catch (final Exception e) {
+                                                h.handleError(asResourceException(e));
+                                            }
+                                        }
+                                    });
+
+                    for (final PatchOperation operation : request.getPatchOperations()) {
+                        attributeMapper.patch(c, new JsonPointer(), operation, handler);
+                    }
+                }
+            }));
+        }
     }
 
     @Override
@@ -545,10 +580,21 @@
                                                 public void handleResult(
                                                         final List<Modification> result) {
                                                     // Perform the modify operation.
-                                                    modifyRequest.getModifications().addAll(result);
-                                                    c.getConnection().applyChangeAsync(
-                                                            modifyRequest, null,
-                                                            postUpdateHandler(c, h));
+                                                    if (result.isEmpty()) {
+                                                        /*
+                                                         * No changes to be
+                                                         * performed, so just
+                                                         * return the entry that
+                                                         * we read.
+                                                         */
+                                                        adaptEntry(c, entry, h);
+                                                    } else {
+                                                        modifyRequest.getModifications().addAll(
+                                                                result);
+                                                        c.getConnection().applyChangeAsync(
+                                                                modifyRequest, null,
+                                                                postUpdateHandler(c, h));
+                                                    }
                                                 }
                                             });
                                 } catch (final Exception e) {
@@ -871,6 +917,27 @@
         return etagAttribute != null ? entry.parseAttribute(etagAttribute).asString() : null;
     }
 
+    private org.forgerock.opendj.ldap.ResultHandler<SearchResultEntry> postEmptyPatchHandler(
+            final Context c, final PatchRequest request, final ResultHandler<Resource> h) {
+        return new org.forgerock.opendj.ldap.ResultHandler<SearchResultEntry>() {
+            @Override
+            public void handleErrorResult(final ErrorResultException error) {
+                h.handleError(asResourceException(error));
+            }
+
+            @Override
+            public void handleResult(final SearchResultEntry entry) {
+                try {
+                    // Fail if there is a version mismatch.
+                    ensureMVCCVersionMatches(entry, request.getRevision());
+                    adaptEntry(c, entry, h);
+                } catch (final Exception e) {
+                    h.handleError(asResourceException(e));
+                }
+            }
+        };
+    }
+
     private org.forgerock.opendj.ldap.ResultHandler<Result> postUpdateHandler(final Context c,
             final ResultHandler<Resource> handler) {
         // The handler which will be invoked for the LDAP add result.
diff --git a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Utils.java b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Utils.java
index 3558ac3..987edf2 100644
--- a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Utils.java
+++ b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Utils.java
@@ -64,12 +64,16 @@
         private final List<V> results;
 
         private AccumulatingResultHandler(final int size, final ResultHandler<List<V>> handler) {
-            if (size <= 0) {
+            if (size < 0) {
                 throw new IllegalStateException();
             }
             this.latch = new AtomicInteger(size);
             this.results = new ArrayList<V>(size);
             this.handler = handler;
+            if (size == 0) {
+                // Invoke immediately.
+                handler.handleResult(results);
+            }
         }
 
         @Override
diff --git a/opendj3/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java b/opendj3/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java
index 0bf227f..c195206 100644
--- a/opendj3/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java
+++ b/opendj3/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java
@@ -42,6 +42,8 @@
 import static org.forgerock.opendj.rest2ldap.TestUtils.ctx;
 
 import java.io.IOException;
+import java.util.LinkedList;
+import java.util.List;
 
 import org.forgerock.json.fluent.JsonValue;
 import org.forgerock.json.resource.BadRequestException;
@@ -49,10 +51,27 @@
 import org.forgerock.json.resource.NotFoundException;
 import org.forgerock.json.resource.NotSupportedException;
 import org.forgerock.json.resource.PreconditionFailedException;
-import org.forgerock.json.resource.RequestHandler;
 import org.forgerock.json.resource.Resource;
 import org.forgerock.opendj.ldap.ConnectionFactory;
+import org.forgerock.opendj.ldap.IntermediateResponseHandler;
 import org.forgerock.opendj.ldap.MemoryBackend;
+import org.forgerock.opendj.ldap.RequestContext;
+import org.forgerock.opendj.ldap.RequestHandler;
+import org.forgerock.opendj.ldap.ResultHandler;
+import org.forgerock.opendj.ldap.SearchResultHandler;
+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.Request;
+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.Result;
 import org.forgerock.opendj.ldif.LDIFEntryReader;
 import org.forgerock.opendj.rest2ldap.Rest2LDAP.Builder;
 import org.forgerock.testng.ForgeRockTestCase;
@@ -70,8 +89,7 @@
 
     @Test
     public void testDelete() throws Exception {
-        final RequestHandler handler = newCollection(builder().build());
-        final Connection connection = newInternalConnection(handler);
+        final Connection connection = newConnection();
         final Resource resource = connection.delete(ctx(), newDeleteRequest("/test1"));
         checkResourcesAreEqual(resource, getTestUser1(12345));
         try {
@@ -84,8 +102,7 @@
 
     @Test
     public void testDeleteMVCCMatch() throws Exception {
-        final RequestHandler handler = newCollection(builder().build());
-        final Connection connection = newInternalConnection(handler);
+        final Connection connection = newConnection();
         final Resource resource =
                 connection.delete(ctx(), newDeleteRequest("/test1").setRevision("12345"));
         checkResourcesAreEqual(resource, getTestUser1(12345));
@@ -99,22 +116,19 @@
 
     @Test(expectedExceptions = PreconditionFailedException.class)
     public void testDeleteMVCCNoMatch() throws Exception {
-        final RequestHandler handler = newCollection(builder().build());
-        final Connection connection = newInternalConnection(handler);
+        final Connection connection = newConnection();
         connection.delete(ctx(), newDeleteRequest("/test1").setRevision("12346"));
     }
 
     @Test(expectedExceptions = NotFoundException.class)
     public void testDeleteNotFound() throws Exception {
-        final RequestHandler handler = newCollection(builder().build());
-        final Connection connection = newInternalConnection(handler);
+        final Connection connection = newConnection();
         connection.delete(ctx(), newDeleteRequest("/missing"));
     }
 
     @Test
     public void testPatch() throws Exception {
-        final RequestHandler handler = newCollection(builder().build());
-        final Connection connection = newInternalConnection(handler);
+        final Connection connection = newConnection();
         final Resource resource1 =
                 connection.patch(ctx(), newPatchRequest("/test1", add("/name/displayName",
                         "changed")));
@@ -124,9 +138,26 @@
     }
 
     @Test
+    public void testPatchEmpty() throws Exception {
+        final List<Request> requests = new LinkedList<Request>();
+        final Connection connection = newConnection(requests);
+        final Resource resource1 = connection.patch(ctx(), newPatchRequest("/test1"));
+        checkResourcesAreEqual(resource1, getTestUser1(12345));
+
+        /*
+         * Check that no modify operation was sent (only a single search should
+         * be sent in order to get the current resource).
+         */
+        assertThat(requests).hasSize(1);
+        assertThat(requests.get(0)).isInstanceOf(SearchRequest.class);
+
+        final Resource resource2 = connection.read(ctx(), newReadRequest("/test1"));
+        checkResourcesAreEqual(resource2, getTestUser1(12345));
+    }
+
+    @Test
     public void testPatchAddOptionalAttribute() throws Exception {
-        final RequestHandler handler = newCollection(builder().build());
-        final Connection connection = newInternalConnection(handler);
+        final Connection connection = newConnection();
         final JsonValue newContent = getTestUser1(12345);
         newContent.put("description", asList("one", "two"));
         final Resource resource1 =
@@ -139,8 +170,7 @@
 
     @Test
     public void testPatchAddOptionalAttributeIndexAppend() throws Exception {
-        final RequestHandler handler = newCollection(builder().build());
-        final Connection connection = newInternalConnection(handler);
+        final Connection connection = newConnection();
         final JsonValue newContent = getTestUser1(12345);
         newContent.put("description", asList("one", "two"));
         final Resource resource1 =
@@ -153,15 +183,13 @@
 
     @Test(expectedExceptions = BadRequestException.class)
     public void testPatchConstantAttribute() throws Exception {
-        final RequestHandler handler = newCollection(builder().build());
-        final Connection connection = newInternalConnection(handler);
+        final Connection connection = newConnection();
         connection.patch(ctx(), newPatchRequest("/test1", add("/schemas", asList("junk"))));
     }
 
     @Test
     public void testPatchDeleteOptionalAttribute() throws Exception {
-        final RequestHandler handler = newCollection(builder().build());
-        final Connection connection = newInternalConnection(handler);
+        final Connection connection = newConnection();
         connection.patch(ctx(),
                 newPatchRequest("/test1", add("/description", asList("one", "two"))));
         final Resource resource1 =
@@ -173,8 +201,7 @@
 
     @Test
     public void testPatchIncrement() throws Exception {
-        final RequestHandler handler = newCollection(builder().build());
-        final Connection connection = newInternalConnection(handler);
+        final Connection connection = newConnection();
         final JsonValue newContent = getTestUser1(12345);
         newContent.put("singleNumber", 100);
         newContent.put("multiNumber", asList(200, 300));
@@ -190,15 +217,13 @@
 
     @Test(expectedExceptions = BadRequestException.class)
     public void testPatchMissingRequiredAttribute() throws Exception {
-        final RequestHandler handler = newCollection(builder().build());
-        final Connection connection = newInternalConnection(handler);
+        final Connection connection = newConnection();
         connection.patch(ctx(), newPatchRequest("/test1", remove("/name/surname")));
     }
 
     @Test
     public void testPatchModifyOptionalAttribute() throws Exception {
-        final RequestHandler handler = newCollection(builder().build());
-        final Connection connection = newInternalConnection(handler);
+        final Connection connection = newConnection();
         connection.patch(ctx(),
                 newPatchRequest("/test1", add("/description", asList("one", "two"))));
         final Resource resource1 =
@@ -213,30 +238,26 @@
 
     @Test(expectedExceptions = NotSupportedException.class)
     public void testPatchMultiValuedAttributeIndexAppend() throws Exception {
-        final RequestHandler handler = newCollection(builder().build());
-        final Connection connection = newInternalConnection(handler);
+        final Connection connection = newConnection();
         connection.patch(ctx(), newPatchRequest("/test1", add("/description/0", "junk")));
     }
 
     @Test(expectedExceptions = BadRequestException.class)
     public void testPatchMultiValuedAttributeIndexAppendWithList() throws Exception {
-        final RequestHandler handler = newCollection(builder().build());
-        final Connection connection = newInternalConnection(handler);
+        final Connection connection = newConnection();
         connection.patch(ctx(), newPatchRequest("/test1", add("/description/-",
                 asList("one", "two"))));
     }
 
     @Test(expectedExceptions = BadRequestException.class)
     public void testPatchMultiValuedAttributeWithSingleValue() throws Exception {
-        final RequestHandler handler = newCollection(builder().build());
-        final Connection connection = newInternalConnection(handler);
+        final Connection connection = newConnection();
         connection.patch(ctx(), newPatchRequest("/test1", add("/description", "one")));
     }
 
     @Test
     public void testPatchMVCCMatch() throws Exception {
-        final RequestHandler handler = newCollection(builder().build());
-        final Connection connection = newInternalConnection(handler);
+        final Connection connection = newConnection();
         final Resource resource1 =
                 connection.patch(ctx(), newPatchRequest("/test1",
                         add("/name/displayName", "changed")).setRevision("12345"));
@@ -247,31 +268,27 @@
 
     @Test(expectedExceptions = PreconditionFailedException.class)
     public void testPatchMVCCNoMatch() throws Exception {
-        final RequestHandler handler = newCollection(builder().build());
-        final Connection connection = newInternalConnection(handler);
+        final Connection connection = newConnection();
         connection.patch(ctx(), newPatchRequest("/test1", add("/name/displayName", "changed"))
                 .setRevision("12346"));
     }
 
     @Test(expectedExceptions = NotFoundException.class)
     public void testPatchNotFound() throws Exception {
-        final RequestHandler handler = newCollection(builder().build());
-        final Connection connection = newInternalConnection(handler);
+        final Connection connection = newConnection();
         connection.patch(ctx(), newPatchRequest("/missing", add("/name/displayName", "changed")));
     }
 
     @Test(expectedExceptions = BadRequestException.class)
     public void testPatchReadOnlyAttribute() throws Exception {
-        final RequestHandler handler = newCollection(builder().build());
-        final Connection connection = newInternalConnection(handler);
+        final Connection connection = newConnection();
         // Etag is read-only.
         connection.patch(ctx(), newPatchRequest("/test1", add("_rev", "99999")));
     }
 
     @Test
     public void testPatchReplacePartialObject() throws Exception {
-        final RequestHandler handler = newCollection(builder().build());
-        final Connection connection = newInternalConnection(handler);
+        final Connection connection = newConnection();
         final JsonValue expected =
                 json(object(field("schemas", asList("urn:scim:schemas:core:1.0")), field("_id",
                         "test1"), field("_rev", "12345"), field("name", object(field("displayName",
@@ -286,8 +303,7 @@
 
     @Test
     public void testPatchReplaceWholeObject() throws Exception {
-        final RequestHandler handler = newCollection(builder().build());
-        final Connection connection = newInternalConnection(handler);
+        final Connection connection = newConnection();
         final JsonValue newContent =
                 json(object(field("name", object(field("displayName", "Humpty"), field("surname",
                         "Dumpty")))));
@@ -304,83 +320,70 @@
 
     @Test(expectedExceptions = BadRequestException.class)
     public void testPatchSingleValuedAttributeIndexAppend() throws Exception {
-        final RequestHandler handler = newCollection(builder().build());
-        final Connection connection = newInternalConnection(handler);
+        final Connection connection = newConnection();
         connection.patch(ctx(), newPatchRequest("/test1", add("/name/surname/-", "junk")));
     }
 
     @Test(expectedExceptions = NotSupportedException.class)
     public void testPatchSingleValuedAttributeIndexNumber() throws Exception {
-        final RequestHandler handler = newCollection(builder().build());
-        final Connection connection = newInternalConnection(handler);
+        final Connection connection = newConnection();
         connection.patch(ctx(), newPatchRequest("/test1", add("/name/surname/0", "junk")));
     }
 
     @Test(expectedExceptions = BadRequestException.class)
     public void testPatchSingleValuedAttributeWithMultipleValues() throws Exception {
-        final RequestHandler handler = newCollection(builder().build());
-        final Connection connection = newInternalConnection(handler);
+        final Connection connection = newConnection();
         connection.patch(ctx(), newPatchRequest("/test1", add("/name/surname", asList("black",
                 "white"))));
     }
 
     @Test(expectedExceptions = BadRequestException.class)
     public void testPatchUnknownAttribute() throws Exception {
-        final RequestHandler handler = newCollection(builder().build());
-        final Connection connection = newInternalConnection(handler);
+        final Connection connection = newConnection();
         connection.patch(ctx(), newPatchRequest("/test1", add("/dummy", "junk")));
     }
 
     @Test(expectedExceptions = NotSupportedException.class)
     public void testPatchUnknownOperation() throws Exception {
-        final RequestHandler handler = newCollection(builder().build());
-        final Connection connection = newInternalConnection(handler);
+        final Connection connection = newConnection();
         connection.patch(ctx(), newPatchRequest("/test1", operation("dummy", "/description",
                 asList("one", "two"))));
     }
 
     @Test(expectedExceptions = BadRequestException.class)
     public void testPatchUnknownSubAttribute() throws Exception {
-        final RequestHandler handler = newCollection(builder().build());
-        final Connection connection = newInternalConnection(handler);
+        final Connection connection = newConnection();
         connection.patch(ctx(), newPatchRequest("/test1", add("/description/dummy", "junk")));
     }
 
     @Test(expectedExceptions = BadRequestException.class)
     public void testPatchUnknownSubSubAttribute() throws Exception {
-        final RequestHandler handler = newCollection(builder().build());
-        final Connection connection = newInternalConnection(handler);
+        final Connection connection = newConnection();
         connection.patch(ctx(), newPatchRequest("/test1", add("/description/dummy/dummy", "junk")));
     }
 
     @Test
     public void testRead() throws Exception {
-        final RequestHandler handler = newCollection(builder().build());
-        final Resource resource =
-                newInternalConnection(handler).read(ctx(), newReadRequest("/test1"));
+        final Resource resource = newConnection().read(ctx(), newReadRequest("/test1"));
         checkResourcesAreEqual(resource, getTestUser1(12345));
     }
 
     @Test(expectedExceptions = NotFoundException.class)
     public void testReadNotFound() throws Exception {
-        final RequestHandler handler = newCollection(builder().build());
-        newInternalConnection(handler).read(ctx(), newReadRequest("/missing"));
+        newConnection().read(ctx(), newReadRequest("/missing"));
     }
 
     @Test
     public void testReadSelectAllFields() throws Exception {
-        final RequestHandler handler = newCollection(builder().build());
         final Resource resource =
-                newInternalConnection(handler).read(ctx(), newReadRequest("/test1").addField("/"));
+                newConnection().read(ctx(), newReadRequest("/test1").addField("/"));
         checkResourcesAreEqual(resource, getTestUser1(12345));
     }
 
     @Test
     public void testReadSelectPartial() throws Exception {
-        final RequestHandler handler = newCollection(builder().build());
         final Resource resource =
-                newInternalConnection(handler).read(ctx(),
-                        newReadRequest("/test1").addField("/name/surname"));
+                newConnection().read(ctx(), newReadRequest("/test1").addField("/name/surname"));
         assertThat(resource.getId()).isEqualTo("test1");
         assertThat(resource.getRevision()).isEqualTo("12345");
         assertThat(resource.getContent().get("_id").asString()).isNull();
@@ -392,10 +395,8 @@
     // Disabled - see CREST-86 (Should JSON resource fields be case insensitive?)
     @Test(enabled = false)
     public void testReadSelectPartialInsensitive() throws Exception {
-        final RequestHandler handler = newCollection(builder().build());
         final Resource resource =
-                newInternalConnection(handler).read(ctx(),
-                        newReadRequest("/test1").addField("/name/SURNAME"));
+                newConnection().read(ctx(), newReadRequest("/test1").addField("/name/SURNAME"));
         assertThat(resource.getId()).isEqualTo("test1");
         assertThat(resource.getRevision()).isEqualTo("12345");
         assertThat(resource.getContent().get("_id").asString()).isNull();
@@ -406,8 +407,7 @@
 
     @Test
     public void testUpdate() throws Exception {
-        final RequestHandler handler = newCollection(builder().build());
-        final Connection connection = newInternalConnection(handler);
+        final Connection connection = newConnection();
         final Resource resource1 =
                 connection.update(ctx(), newUpdateRequest("/test1", getTestUser1Updated(12345)));
         checkResourcesAreEqual(resource1, getTestUser1Updated(12345));
@@ -416,9 +416,27 @@
     }
 
     @Test
+    public void testUpdateNoChange() throws Exception {
+        final List<Request> requests = new LinkedList<Request>();
+        final Connection connection = newConnection(requests);
+        final Resource resource1 =
+                connection.update(ctx(), newUpdateRequest("/test1", getTestUser1(12345)));
+
+        /*
+         * Check that no modify operation was sent (only a single search should
+         * be sent in order to get the current resource).
+         */
+        assertThat(requests).hasSize(1);
+        assertThat(requests.get(0)).isInstanceOf(SearchRequest.class);
+
+        checkResourcesAreEqual(resource1, getTestUser1(12345));
+        final Resource resource2 = connection.read(ctx(), newReadRequest("/test1"));
+        checkResourcesAreEqual(resource2, getTestUser1(12345));
+    }
+
+    @Test
     public void testUpdateAddOptionalAttribute() throws Exception {
-        final RequestHandler handler = newCollection(builder().build());
-        final Connection connection = newInternalConnection(handler);
+        final Connection connection = newConnection();
         final JsonValue newContent = getTestUser1Updated(12345);
         newContent.put("description", asList("one", "two"));
         final Resource resource1 = connection.update(ctx(), newUpdateRequest("/test1", newContent));
@@ -429,8 +447,7 @@
 
     @Test(expectedExceptions = BadRequestException.class)
     public void testUpdateConstantAttribute() throws Exception {
-        final RequestHandler handler = newCollection(builder().build());
-        final Connection connection = newInternalConnection(handler);
+        final Connection connection = newConnection();
         final JsonValue newContent = getTestUser1Updated(12345);
         newContent.put("schemas", asList("junk"));
         connection.update(ctx(), newUpdateRequest("/test1", newContent));
@@ -438,8 +455,7 @@
 
     @Test
     public void testUpdateDeleteOptionalAttribute() throws Exception {
-        final RequestHandler handler = newCollection(builder().build());
-        final Connection connection = newInternalConnection(handler);
+        final Connection connection = newConnection();
         final JsonValue newContent = getTestUser1Updated(12345);
         newContent.put("description", asList("one", "two"));
         connection.update(ctx(), newUpdateRequest("/test1", newContent));
@@ -452,8 +468,7 @@
 
     @Test(expectedExceptions = BadRequestException.class)
     public void testUpdateMissingRequiredAttribute() throws Exception {
-        final RequestHandler handler = newCollection(builder().build());
-        final Connection connection = newInternalConnection(handler);
+        final Connection connection = newConnection();
         final JsonValue newContent = getTestUser1Updated(12345);
         newContent.get("name").remove("surname");
         connection.update(ctx(), newUpdateRequest("/test1", newContent));
@@ -461,8 +476,7 @@
 
     @Test
     public void testUpdateModifyOptionalAttribute() throws Exception {
-        final RequestHandler handler = newCollection(builder().build());
-        final Connection connection = newInternalConnection(handler);
+        final Connection connection = newConnection();
         final JsonValue newContent = getTestUser1Updated(12345);
         newContent.put("description", asList("one", "two"));
         connection.update(ctx(), newUpdateRequest("/test1", newContent));
@@ -475,8 +489,7 @@
 
     @Test
     public void testUpdateMVCCMatch() throws Exception {
-        final RequestHandler handler = newCollection(builder().build());
-        final Connection connection = newInternalConnection(handler);
+        final Connection connection = newConnection();
         final Resource resource1 =
                 connection.update(ctx(), newUpdateRequest("/test1", getTestUser1Updated(12345))
                         .setRevision("12345"));
@@ -487,31 +500,27 @@
 
     @Test(expectedExceptions = PreconditionFailedException.class)
     public void testUpdateMVCCNoMatch() throws Exception {
-        final RequestHandler handler = newCollection(builder().build());
-        final Connection connection = newInternalConnection(handler);
+        final Connection connection = newConnection();
         connection.update(ctx(), newUpdateRequest("/test1", getTestUser1Updated(12345))
                 .setRevision("12346"));
     }
 
     @Test(expectedExceptions = NotFoundException.class)
     public void testUpdateNotFound() throws Exception {
-        final RequestHandler handler = newCollection(builder().build());
-        final Connection connection = newInternalConnection(handler);
+        final Connection connection = newConnection();
         connection.update(ctx(), newUpdateRequest("/missing", getTestUser1Updated(12345)));
     }
 
     @Test(expectedExceptions = BadRequestException.class)
     public void testUpdateReadOnlyAttribute() throws Exception {
-        final RequestHandler handler = newCollection(builder().build());
-        final Connection connection = newInternalConnection(handler);
+        final Connection connection = newConnection();
         // Etag is read-only.
         connection.update(ctx(), newUpdateRequest("/test1", getTestUser1Updated(99999)));
     }
 
     @Test(expectedExceptions = BadRequestException.class)
     public void testUpdateSingleValuedAttributeWithMultipleValues() throws Exception {
-        final RequestHandler handler = newCollection(builder().build());
-        final Connection connection = newInternalConnection(handler);
+        final Connection connection = newConnection();
         final JsonValue newContent = getTestUser1Updated(12345);
         newContent.put("surname", asList("black", "white"));
         connection.update(ctx(), newUpdateRequest("/test1", newContent));
@@ -519,17 +528,24 @@
 
     @Test(expectedExceptions = BadRequestException.class)
     public void testUpdateUnknownAttribute() throws Exception {
-        final RequestHandler handler = newCollection(builder().build());
-        final Connection connection = newInternalConnection(handler);
+        final Connection connection = newConnection();
         final JsonValue newContent = getTestUser1Updated(12345);
         newContent.add("dummy", "junk");
         connection.update(ctx(), newUpdateRequest("/test1", newContent));
     }
 
-    private Builder builder() throws IOException {
-        return Rest2LDAP.builder().ldapConnectionFactory(getConnectionFactory()).baseDN("dc=test")
-                .useEtagAttribute().useClientDNNaming("uid").readOnUpdatePolicy(
-                        ReadOnUpdatePolicy.CONTROLS).authorizationPolicy(AuthorizationPolicy.NONE)
+    private Connection newConnection() throws IOException {
+        return newConnection(new LinkedList<Request>());
+    }
+
+    private Connection newConnection(final List<Request> requests) throws IOException {
+        return newInternalConnection(newCollection(builder(requests).build()));
+    }
+
+    private Builder builder(final List<Request> requests) throws IOException {
+        return Rest2LDAP.builder().ldapConnectionFactory(getConnectionFactory(requests)).baseDN(
+                "dc=test").useEtagAttribute().useClientDNNaming("uid").readOnUpdatePolicy(
+                ReadOnUpdatePolicy.CONTROLS).authorizationPolicy(AuthorizationPolicy.NONE)
                 .additionalLDAPAttribute("objectClass", "top", "person")
                 .mapper(object()
                         .attribute("schemas", constant(asList("urn:scim:schemas:core:1.0")))
@@ -560,7 +576,7 @@
                 expectedResource.getContent().getObject());
     }
 
-    private ConnectionFactory getConnectionFactory() throws IOException {
+    private ConnectionFactory getConnectionFactory(final List<Request> requests) throws IOException {
         // @formatter:off
         final MemoryBackend backend =
                 new MemoryBackend(new LDIFEntryReader(
@@ -589,7 +605,86 @@
                 ));
         // @formatter:on
 
-        return newInternalConnectionFactory(backend);
+        return newInternalConnectionFactory(recordRequests(backend, requests));
+    }
+
+    private RequestHandler<RequestContext> recordRequests(
+            final RequestHandler<RequestContext> handler, final List<Request> requests) {
+        return new RequestHandler<RequestContext>() {
+            @Override
+            public void handleAdd(RequestContext requestContext, AddRequest request,
+                    IntermediateResponseHandler intermediateResponseHandler,
+                    ResultHandler<Result> resultHandler) {
+                requests.add(request);
+                handler.handleAdd(requestContext, request, intermediateResponseHandler,
+                        resultHandler);
+            }
+
+            @Override
+            public void handleBind(RequestContext requestContext, int version, BindRequest request,
+                    IntermediateResponseHandler intermediateResponseHandler,
+                    ResultHandler<BindResult> resultHandler) {
+                requests.add(request);
+                handler.handleBind(requestContext, version, request, intermediateResponseHandler,
+                        resultHandler);
+            }
+
+            @Override
+            public void handleCompare(RequestContext requestContext, CompareRequest request,
+                    IntermediateResponseHandler intermediateResponseHandler,
+                    ResultHandler<CompareResult> resultHandler) {
+                requests.add(request);
+                handler.handleCompare(requestContext, request, intermediateResponseHandler,
+                        resultHandler);
+            }
+
+            @Override
+            public void handleDelete(RequestContext requestContext, DeleteRequest request,
+                    IntermediateResponseHandler intermediateResponseHandler,
+                    ResultHandler<Result> resultHandler) {
+                requests.add(request);
+                handler.handleDelete(requestContext, request, intermediateResponseHandler,
+                        resultHandler);
+            }
+
+            @Override
+            public <R extends ExtendedResult> void handleExtendedRequest(
+                    RequestContext requestContext, ExtendedRequest<R> request,
+                    IntermediateResponseHandler intermediateResponseHandler,
+                    ResultHandler<R> resultHandler) {
+                requests.add(request);
+                handler.handleExtendedRequest(requestContext, request, intermediateResponseHandler,
+                        resultHandler);
+            }
+
+            @Override
+            public void handleModify(RequestContext requestContext, ModifyRequest request,
+                    IntermediateResponseHandler intermediateResponseHandler,
+                    ResultHandler<Result> resultHandler) {
+                requests.add(request);
+                handler.handleModify(requestContext, request, intermediateResponseHandler,
+                        resultHandler);
+            }
+
+            @Override
+            public void handleModifyDN(RequestContext requestContext, ModifyDNRequest request,
+                    IntermediateResponseHandler intermediateResponseHandler,
+                    ResultHandler<Result> resultHandler) {
+                requests.add(request);
+                handler.handleModifyDN(requestContext, request, intermediateResponseHandler,
+                        resultHandler);
+            }
+
+            @Override
+            public void handleSearch(RequestContext requestContext, SearchRequest request,
+                    IntermediateResponseHandler intermediateResponseHandler,
+                    SearchResultHandler resultHandler) {
+                requests.add(request);
+                handler.handleSearch(requestContext, request, intermediateResponseHandler,
+                        resultHandler);
+            }
+
+        };
     }
 
     private JsonValue getTestUser1(final int rev) {

--
Gitblit v1.10.0