From 6d12770d7fa9fc1c103d9e21614dac75a69abc78 Mon Sep 17 00:00:00 2001
From: Matthew Swift <matthew.swift@forgerock.com>
Date: Mon, 06 May 2013 22:26:14 +0000
Subject: [PATCH] CREST-3 - Add patch support

---
 opendj3/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java           |  151 +++++++++++++++++++++++++++++++++----
 opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimpleAttributeMapper.java       |    5 +
 opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectAttributeMapper.java       |   32 +++++--
 opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/JSONConstantAttributeMapper.java |    5 +
 opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractLDAPAttributeMapper.java |   15 ---
 opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferenceAttributeMapper.java    |    5 +
 6 files changed, 171 insertions(+), 42 deletions(-)

diff --git a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractLDAPAttributeMapper.java b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractLDAPAttributeMapper.java
index 7c1186e..995c57d 100644
--- a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractLDAPAttributeMapper.java
+++ b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AbstractLDAPAttributeMapper.java
@@ -142,13 +142,13 @@
                  * single-valued then the patch value must not be a list.
                  */
                 if (attributeIsSingleValued()) {
-                    if (v != null && v.isList()) {
+                    if (v.isList()) {
                         // Single-valued field violation.
                         throw new BadRequestException(i18n(
                                 "The request cannot be processed because an array of values was "
                                         + "provided for the single valued field '%s'", path));
                     }
-                } else if (v != null && !v.isList() && !operation.isIncrement()
+                } else if (!v.isList() && !operation.isIncrement()
                         && !(v.isNull() && (operation.isReplace() || operation.isRemove()))) {
                     // Multi-valued field violation.
                     throw new BadRequestException(i18n(
@@ -223,17 +223,6 @@
                 modType = ModificationType.REPLACE;
             } else if (operation.isIncrement()) {
                 modType = ModificationType.INCREMENT;
-                if (newValues.isEmpty()) {
-                    throw new BadRequestException(i18n(
-                            "The request cannot be processed because it included "
-                                    + "an increment patch operation but no value for field '%s'",
-                            path.child(field.get(0))));
-                } else if (newValues.size() > 1) {
-                    throw new BadRequestException(
-                            i18n("The request cannot be processed because it included "
-                                    + "an increment patch operation with multiple values for field '%s'",
-                                    path.child(field.get(0))));
-                }
             } else {
                 throw new NotSupportedException(i18n(
                         "The request cannot be processed because it included "
diff --git a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/JSONConstantAttributeMapper.java b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/JSONConstantAttributeMapper.java
index a1eee8d..29016d6 100644
--- a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/JSONConstantAttributeMapper.java
+++ b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/JSONConstantAttributeMapper.java
@@ -47,6 +47,11 @@
     }
 
     @Override
+    public String toString() {
+        return "constant(" + value.toString() + ")";
+    }
+
+    @Override
     void create(final Context c, final JsonPointer path, final JsonValue v,
             final ResultHandler<List<Attribute>> h) {
         if (!isNullOrEmpty(v) && !v.getObject().equals(value.getObject())) {
diff --git a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectAttributeMapper.java b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectAttributeMapper.java
index c05677f..ee22625 100644
--- a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectAttributeMapper.java
+++ b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ObjectAttributeMapper.java
@@ -85,6 +85,11 @@
     }
 
     @Override
+    public String toString() {
+        return "object(" + mappings.values().toString() + ")";
+    }
+
+    @Override
     void create(final Context c, final JsonPointer path, final JsonValue v,
             final ResultHandler<List<Attribute>> h) {
         try {
@@ -165,18 +170,21 @@
                  * by allowing the JSON value to be a partial object and
                  * add/remove/replace only the provided values.
                  */
-                checkMapping(path, operation.getValue());
+                final Map<String, Mapping> missingMappings = checkMapping(path, v);
 
                 // Accumulate the results of the subordinate mappings.
-                final ResultHandler<List<Modification>> handler = accumulator(h);
+                final ResultHandler<List<Modification>> handler =
+                        accumulator(mappings.size() - missingMappings.size(), h);
 
-                // Invoke the sub-mappers using a new patch operation targeted at each field.
-                for (final Map.Entry<String, Object> me : v.asMap().entrySet()) {
-                    final Mapping mapping = getMapping(me.getKey());
-                    final JsonValue subValue = new JsonValue(me.getValue());
-                    final PatchOperation subOperation =
-                            operation(operation.getOperation(), field /* empty */, subValue);
-                    mapping.mapper.patch(c, path.child(me.getKey()), subOperation, handler);
+                // Invoke mappings for which there are values provided.
+                if (!v.isNull()) {
+                    for (final Map.Entry<String, Object> me : v.asMap().entrySet()) {
+                        final Mapping mapping = getMapping(me.getKey());
+                        final JsonValue subValue = new JsonValue(me.getValue());
+                        final PatchOperation subOperation =
+                                operation(operation.getOperation(), field /* empty */, subValue);
+                        mapping.mapper.patch(c, path.child(me.getKey()), subOperation, handler);
+                    }
                 }
             } else {
                 /*
@@ -280,7 +288,11 @@
     }
 
     private <T> ResultHandler<List<T>> accumulator(final ResultHandler<List<T>> h) {
-        return accumulate(mappings.size(), transform(new Function<List<List<T>>, List<T>, Void>() {
+        return accumulator(mappings.size(), h);
+    }
+
+    private <T> ResultHandler<List<T>> accumulator(final int size, final ResultHandler<List<T>> h) {
+        return accumulate(size, transform(new Function<List<List<T>>, List<T>, Void>() {
             @Override
             public List<T> apply(final List<List<T>> value, final Void p) {
                 switch (value.size()) {
diff --git a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferenceAttributeMapper.java b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferenceAttributeMapper.java
index b84037d..230359a 100644
--- a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferenceAttributeMapper.java
+++ b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferenceAttributeMapper.java
@@ -122,6 +122,11 @@
     }
 
     @Override
+    public String toString() {
+        return "reference(" + ldapAttributeName.toString() + ")";
+    }
+
+    @Override
     void getLDAPFilter(final Context c, final JsonPointer path, final JsonPointer subPath,
             final FilterType type, final String operator, final Object valueAssertion,
             final ResultHandler<Filter> h) {
diff --git a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimpleAttributeMapper.java b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimpleAttributeMapper.java
index 8ed9a7b..a22bd61 100644
--- a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimpleAttributeMapper.java
+++ b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimpleAttributeMapper.java
@@ -112,6 +112,11 @@
     }
 
     @Override
+    public String toString() {
+        return "simple(" + ldapAttributeName.toString() + ")";
+    }
+
+    @Override
     void getLDAPFilter(final Context c, final JsonPointer path, final JsonPointer subPath,
             final FilterType type, final String operator, final Object valueAssertion,
             final ResultHandler<Filter> h) {
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 8802713..0bf227f 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
@@ -22,6 +22,8 @@
 import static org.forgerock.json.fluent.JsonValue.json;
 import static org.forgerock.json.fluent.JsonValue.object;
 import static org.forgerock.json.resource.PatchOperation.add;
+import static org.forgerock.json.resource.PatchOperation.increment;
+import static org.forgerock.json.resource.PatchOperation.operation;
 import static org.forgerock.json.resource.PatchOperation.remove;
 import static org.forgerock.json.resource.PatchOperation.replace;
 import static org.forgerock.json.resource.Requests.newDeleteRequest;
@@ -31,6 +33,7 @@
 import static org.forgerock.json.resource.Resources.newCollection;
 import static org.forgerock.json.resource.Resources.newInternalConnection;
 import static org.forgerock.opendj.ldap.Connections.newInternalConnectionFactory;
+import static org.forgerock.opendj.ldap.Functions.byteStringToInteger;
 import static org.forgerock.opendj.rest2ldap.Rest2LDAP.constant;
 import static org.forgerock.opendj.rest2ldap.Rest2LDAP.object;
 import static org.forgerock.opendj.rest2ldap.Rest2LDAP.simple;
@@ -44,6 +47,7 @@
 import org.forgerock.json.resource.BadRequestException;
 import org.forgerock.json.resource.Connection;
 import org.forgerock.json.resource.NotFoundException;
+import org.forgerock.json.resource.NotSupportedException;
 import org.forgerock.json.resource.PreconditionFailedException;
 import org.forgerock.json.resource.RequestHandler;
 import org.forgerock.json.resource.Resource;
@@ -120,22 +124,6 @@
     }
 
     @Test
-    public void testPatchReplaceWholeObject() throws Exception {
-        final RequestHandler handler = newCollection(builder().build());
-        final Connection connection = newInternalConnection(handler);
-        final JsonValue expected =
-                json(object(field("schemas", asList("urn:scim:schemas:core:1.0")), field("_id",
-                        "test1"), field("_rev", "12345"), field("name", object(field("displayName",
-                        "Humpty"), field("surname", "Dumpty")))));
-        final Resource resource1 =
-                connection.patch(ctx(), newPatchRequest("/test1", replace("/name", JsonValue
-                        .object(field("displayName", "Humpty"), field("surname", "Dumpty")))));
-        checkResourcesAreEqual(resource1, expected);
-        final Resource resource2 = connection.read(ctx(), newReadRequest("/test1"));
-        checkResourcesAreEqual(resource2, expected);
-    }
-
-    @Test
     public void testPatchAddOptionalAttribute() throws Exception {
         final RequestHandler handler = newCollection(builder().build());
         final Connection connection = newInternalConnection(handler);
@@ -149,6 +137,20 @@
         checkResourcesAreEqual(resource2, newContent);
     }
 
+    @Test
+    public void testPatchAddOptionalAttributeIndexAppend() throws Exception {
+        final RequestHandler handler = newCollection(builder().build());
+        final Connection connection = newInternalConnection(handler);
+        final JsonValue newContent = getTestUser1(12345);
+        newContent.put("description", asList("one", "two"));
+        final Resource resource1 =
+                connection.patch(ctx(), newPatchRequest("/test1", add("/description/-", "one"),
+                        add("/description/-", "two")));
+        checkResourcesAreEqual(resource1, newContent);
+        final Resource resource2 = connection.read(ctx(), newReadRequest("/test1"));
+        checkResourcesAreEqual(resource2, newContent);
+    }
+
     @Test(expectedExceptions = BadRequestException.class)
     public void testPatchConstantAttribute() throws Exception {
         final RequestHandler handler = newCollection(builder().build());
@@ -169,6 +171,23 @@
         checkResourcesAreEqual(resource2, getTestUser1(12345));
     }
 
+    @Test
+    public void testPatchIncrement() throws Exception {
+        final RequestHandler handler = newCollection(builder().build());
+        final Connection connection = newInternalConnection(handler);
+        final JsonValue newContent = getTestUser1(12345);
+        newContent.put("singleNumber", 100);
+        newContent.put("multiNumber", asList(200, 300));
+
+        final Resource resource1 =
+                connection.patch(ctx(), newPatchRequest("/test1", add("/singleNumber", 0), add(
+                        "/multiNumber", asList(100, 200)), increment("/singleNumber", 100),
+                        increment("/multiNumber", 100)));
+        checkResourcesAreEqual(resource1, newContent);
+        final Resource resource2 = connection.read(ctx(), newReadRequest("/test1"));
+        checkResourcesAreEqual(resource2, newContent);
+    }
+
     @Test(expectedExceptions = BadRequestException.class)
     public void testPatchMissingRequiredAttribute() throws Exception {
         final RequestHandler handler = newCollection(builder().build());
@@ -192,6 +211,28 @@
         checkResourcesAreEqual(resource2, newContent);
     }
 
+    @Test(expectedExceptions = NotSupportedException.class)
+    public void testPatchMultiValuedAttributeIndexAppend() throws Exception {
+        final RequestHandler handler = newCollection(builder().build());
+        final Connection connection = newInternalConnection(handler);
+        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);
+        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);
+        connection.patch(ctx(), newPatchRequest("/test1", add("/description", "one")));
+    }
+
     @Test
     public void testPatchMVCCMatch() throws Exception {
         final RequestHandler handler = newCollection(builder().build());
@@ -227,6 +268,54 @@
         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 JsonValue expected =
+                json(object(field("schemas", asList("urn:scim:schemas:core:1.0")), field("_id",
+                        "test1"), field("_rev", "12345"), field("name", object(field("displayName",
+                        "Humpty"), field("surname", "Dumpty")))));
+        final Resource resource1 =
+                connection.patch(ctx(), newPatchRequest("/test1", replace("/name", object(field(
+                        "displayName", "Humpty"), field("surname", "Dumpty")))));
+        checkResourcesAreEqual(resource1, expected);
+        final Resource resource2 = connection.read(ctx(), newReadRequest("/test1"));
+        checkResourcesAreEqual(resource2, expected);
+    }
+
+    @Test
+    public void testPatchReplaceWholeObject() throws Exception {
+        final RequestHandler handler = newCollection(builder().build());
+        final Connection connection = newInternalConnection(handler);
+        final JsonValue newContent =
+                json(object(field("name", object(field("displayName", "Humpty"), field("surname",
+                        "Dumpty")))));
+        final JsonValue expected =
+                json(object(field("schemas", asList("urn:scim:schemas:core:1.0")), field("_id",
+                        "test1"), field("_rev", "12345"), field("name", object(field("displayName",
+                        "Humpty"), field("surname", "Dumpty")))));
+        final Resource resource1 =
+                connection.patch(ctx(), newPatchRequest("/test1", replace("/", newContent)));
+        checkResourcesAreEqual(resource1, expected);
+        final Resource resource2 = connection.read(ctx(), newReadRequest("/test1"));
+        checkResourcesAreEqual(resource2, expected);
+    }
+
+    @Test(expectedExceptions = BadRequestException.class)
+    public void testPatchSingleValuedAttributeIndexAppend() throws Exception {
+        final RequestHandler handler = newCollection(builder().build());
+        final Connection connection = newInternalConnection(handler);
+        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);
+        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());
@@ -239,11 +328,31 @@
     public void testPatchUnknownAttribute() throws Exception {
         final RequestHandler handler = newCollection(builder().build());
         final Connection connection = newInternalConnection(handler);
-        final JsonValue newContent = getTestUser1Updated(12345);
-        newContent.add("dummy", "junk");
         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);
+        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);
+        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);
+        connection.patch(ctx(), newPatchRequest("/test1", add("/description/dummy/dummy", "junk")));
+    }
+
     @Test
     public void testRead() throws Exception {
         final RequestHandler handler = newCollection(builder().build());
@@ -436,7 +545,11 @@
                                 "_rev",
                                 simple("etag").isSingleValued().isRequired().writability(
                                         WritabilityPolicy.READ_ONLY)).attribute("description",
-                                simple("description")));
+                                simple("description")).attribute(
+                                "singleNumber",
+                                simple("singleNumber").decoder(byteStringToInteger())
+                                        .isSingleValued()).attribute("multiNumber",
+                                simple("multiNumber").decoder(byteStringToInteger())));
     }
 
     private void checkResourcesAreEqual(final Resource actual, final JsonValue expected) {

--
Gitblit v1.10.0