From ff499dddfe79aabd9b995f3f4d2a033ae7b2cfa7 Mon Sep 17 00:00:00 2001
From: Matthew Swift <matthew.swift@forgerock.com>
Date: Wed, 03 Apr 2013 23:12:04 +0000
Subject: [PATCH] OPENDJ-692: Implement delete support

---
 opendj3/opendj-rest2ldap-servlet/src/main/webapp/opendj-rest2ldap-servlet.json                            |   11 +-
 opendj3/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java              |    4 
 /dev/null                                                                                                 |   60 ---------------
 opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java                      |   37 +++-----
 opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java |   69 ++++++++++++-----
 opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Config.java                         |   17 +++
 6 files changed, 87 insertions(+), 111 deletions(-)

diff --git a/opendj3/opendj-rest2ldap-servlet/src/main/webapp/opendj-rest2ldap-servlet.json b/opendj3/opendj-rest2ldap-servlet/src/main/webapp/opendj-rest2ldap-servlet.json
index e3f3547..538842a 100644
--- a/opendj3/opendj-rest2ldap-servlet/src/main/webapp/opendj-rest2ldap-servlet.json
+++ b/opendj3/opendj-rest2ldap-servlet/src/main/webapp/opendj-rest2ldap-servlet.json
@@ -136,6 +136,12 @@
             "/users" : {
                 "baseDN" : "ou=people,dc=example,dc=com",
                 "readOnUpdatePolicy" : "controls",
+                "useSubtreeDelete" : true,
+                "etagAttribute" : "etag",
+                "namingStrategy" : {
+                    "strategy" : "clientDNNaming",
+                    "dnAttribute" : "uid"
+                },
                 "additionalLDAPAttributes" : [
                     {
                         "type" : "objectClass",
@@ -147,11 +153,6 @@
                         ]
                     }
                 ],
-                "namingStrategy" : {
-                    "strategy" : "clientDNNaming",
-                    "dnAttribute" : "uid"
-                },
-                "etagAttribute" : "etag",
                 "attributes" : {
                     "schemas"     : { "constant" : [ "urn:scim:schemas:core:1.0" ] },
                     "_id"         : { "simple"   : { "ldapAttribute" : "uid", "isSingleValued" : true, "isRequired" : true, "writability" : "createOnly" } },
diff --git a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Config.java b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Config.java
index cbabe03..cca57bd 100644
--- a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Config.java
+++ b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Config.java
@@ -23,20 +23,22 @@
  * Common configuration options.
  */
 final class Config {
+    private final AuthorizationPolicy authzPolicy;
     private final ConnectionFactory factory;
     private final DecodeOptions options;
-    private final AuthorizationPolicy authzPolicy;
     private final AuthzIdTemplate proxiedAuthzTemplate;
     private final ReadOnUpdatePolicy readOnUpdatePolicy;
     private final Schema schema;
+    private final boolean useSubtreeDelete;
 
     Config(final ConnectionFactory factory, final ReadOnUpdatePolicy readOnUpdatePolicy,
             final AuthorizationPolicy authzPolicy, final AuthzIdTemplate proxiedAuthzTemplate,
-            final Schema schema) {
+            final boolean useSubtreeDelete, final Schema schema) {
         this.factory = factory;
         this.readOnUpdatePolicy = readOnUpdatePolicy;
         this.authzPolicy = authzPolicy;
         this.proxiedAuthzTemplate = proxiedAuthzTemplate;
+        this.useSubtreeDelete = useSubtreeDelete;
         this.schema = schema;
         this.options = new DecodeOptions().setSchema(schema);
     }
@@ -86,6 +88,17 @@
     }
 
     /**
+     * Returns {@code true} if delete requests should include the subtree delete
+     * control.
+     *
+     * @return {@code true} if delete requests should include the subtree delete
+     *         control.
+     */
+    boolean useSubtreeDelete() {
+        return useSubtreeDelete;
+    }
+
+    /**
      * Returns the policy which should be used in order to read an entry before
      * it is deleted, or after it is added or modified.
      *
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 cc71506..a3c6824 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
@@ -54,6 +54,7 @@
 import org.forgerock.json.resource.UncategorizedException;
 import org.forgerock.json.resource.UpdateRequest;
 import org.forgerock.opendj.ldap.Attribute;
+import org.forgerock.opendj.ldap.AttributeDescription;
 import org.forgerock.opendj.ldap.DN;
 import org.forgerock.opendj.ldap.DecodeException;
 import org.forgerock.opendj.ldap.Entry;
@@ -62,10 +63,12 @@
 import org.forgerock.opendj.ldap.Function;
 import org.forgerock.opendj.ldap.SearchResultHandler;
 import org.forgerock.opendj.ldap.SearchScope;
+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.Requests;
 import org.forgerock.opendj.ldap.requests.SearchRequest;
@@ -86,17 +89,17 @@
     private final AttributeMapper attributeMapper;
     private final DN baseDN; // TODO: support template variables.
     private final Config config;
-    private final MVCCStrategy mvccStrategy;
+    private final AttributeDescription etagAttribute;
     private final NameStrategy nameStrategy;
 
     LDAPCollectionResourceProvider(final DN baseDN, final AttributeMapper mapper,
-            final NameStrategy nameStrategy, final MVCCStrategy mvccStrategy, final Config config,
-            final List<Attribute> additionalLDAPAttributes) {
+            final NameStrategy nameStrategy, final AttributeDescription etagAttribute,
+            final Config config, final List<Attribute> additionalLDAPAttributes) {
         this.baseDN = baseDN;
         this.attributeMapper = mapper;
         this.config = config;
         this.nameStrategy = nameStrategy;
-        this.mvccStrategy = mvccStrategy;
+        this.etagAttribute = etagAttribute;
         this.additionalLDAPAttributes = additionalLDAPAttributes;
     }
 
@@ -167,8 +170,6 @@
         final Context c = wrap(context);
         final ResultHandler<Resource> h = wrap(c, handler);
 
-        // FIXME: assertion and subtree delete controls.
-
         // Get connection then perform the search.
         c.run(h, new Runnable() {
             @Override
@@ -186,17 +187,26 @@
 
                             @Override
                             public void handleResult(final SearchResultEntry entry) {
-                                // Perform delete operation.
-                                final ChangeRecord deleteRequest =
-                                        Requests.newDeleteRequest(entry.getName());
-                                if (config.readOnUpdatePolicy() == CONTROLS) {
-                                    final String[] attributes =
-                                            getLDAPAttributes(c, request.getFields());
-                                    deleteRequest.addControl(PreReadRequestControl.newControl(
-                                            false, attributes));
+                                try {
+                                    // Perform delete operation.
+                                    final ChangeRecord deleteRequest =
+                                            Requests.newDeleteRequest(entry.getName());
+                                    if (config.readOnUpdatePolicy() == CONTROLS) {
+                                        final String[] attributes =
+                                                getLDAPAttributes(c, request.getFields());
+                                        deleteRequest.addControl(PreReadRequestControl.newControl(
+                                                false, attributes));
+                                    }
+                                    if (config.useSubtreeDelete()) {
+                                        deleteRequest.addControl(SubtreeDeleteRequestControl
+                                                .newControl(true));
+                                    }
+                                    addAssertionControl(deleteRequest, request.getRevision());
+                                    c.getConnection().applyChangeAsync(deleteRequest, null,
+                                            postUpdateHandler(c, h));
+                                } catch (final Exception e) {
+                                    h.handleError(asResourceException(e));
                                 }
-                                c.getConnection().applyChangeAsync(deleteRequest, null,
-                                        postUpdateHandler(c, h));
                             }
                         });
             }
@@ -260,8 +270,7 @@
                                     }
 
                                     final String id = nameStrategy.getResourceId(c, entry);
-                                    final String revision =
-                                            mvccStrategy.getRevisionFromEntry(c, entry);
+                                    final String revision = getRevisionFromEntry(entry);
                                     final ResultHandler<JsonValue> mapHandler =
                                             new ResultHandler<JsonValue>() {
                                                 @Override
@@ -370,7 +379,7 @@
     private void adaptEntry(final Context c, final Entry entry,
             final ResultHandler<Resource> handler) {
         final String actualResourceId = nameStrategy.getResourceId(c, entry);
-        final String revision = mvccStrategy.getRevisionFromEntry(c, entry);
+        final String revision = getRevisionFromEntry(entry);
         attributeMapper.toJSON(c, entry, transform(new Function<JsonValue, Resource, Void>() {
             @Override
             public Resource apply(final JsonValue value, final Void p) {
@@ -379,6 +388,20 @@
         }, handler));
     }
 
+    private void addAssertionControl(final ChangeRecord request, final String revision)
+            throws NotSupportedException {
+        if (revision != null) {
+            if (etagAttribute != null) {
+                request.addControl(AssertionRequestControl.newControl(true, Filter.equality(
+                        etagAttribute.toString(), revision)));
+            } else {
+                // FIXME: i18n
+                throw new NotSupportedException(
+                        "Multi-version concurrency control is not supported by this resource");
+            }
+        }
+    }
+
     private DN getBaseDN(final Context context) {
         return baseDN;
     }
@@ -410,7 +433,9 @@
 
         // Get the LDAP attributes required by the Etag and name stategies.
         nameStrategy.getLDAPAttributes(c, requestedLDAPAttributes);
-        mvccStrategy.getLDAPAttributes(c, requestedLDAPAttributes);
+        if (etagAttribute != null) {
+            requestedLDAPAttributes.add(etagAttribute.toString());
+        }
         return requestedLDAPAttributes.toArray(new String[requestedLDAPAttributes.size()]);
     }
 
@@ -590,6 +615,10 @@
         queryFilter.accept(visitor, h);
     }
 
+    private String getRevisionFromEntry(final Entry entry) {
+        return etagAttribute != null ? entry.parseAttribute(etagAttribute).asString() : null;
+    }
+
     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/MVCCStrategy.java b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/MVCCStrategy.java
deleted file mode 100644
index 5e33383..0000000
--- a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/MVCCStrategy.java
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * The contents of this file are subject to the terms of the Common Development and
- * Distribution License (the License). You may not use this file except in compliance with the
- * License.
- *
- * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
- * specific language governing permission and limitations under the License.
- *
- * When distributing Covered Software, include this CDDL Header Notice in each file and include
- * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
- * Header, with the fields enclosed by brackets [] replaced by your own identifying
- * information: "Portions copyright [year] [name of copyright owner]".
- *
- * Copyright 2013 ForgeRock AS.
- */
-
-package org.forgerock.opendj.rest2ldap;
-
-import java.util.Set;
-
-import org.forgerock.opendj.ldap.Entry;
-
-/**
- * A multi-version concurrency control strategy is responsible for ensuring that
- * clients can perform atomic updates to LDAP resources.
- */
-abstract class MVCCStrategy {
-    /*
-     * This interface is an abstract class so that methods can be made package
-     * private until API is finalized.
-     */
-
-    MVCCStrategy() {
-        // Nothing to do.
-    }
-
-    /**
-     * Adds the name of any LDAP attribute required by this MVCC strategy to the
-     * provided set.
-     *
-     * @param c
-     *            The context.
-     * @param ldapAttributes
-     *            The set into which any required LDAP attribute name should be
-     *            put.
-     */
-    abstract void getLDAPAttributes(Context c, Set<String> ldapAttributes);
-
-    /**
-     * Retrieves the revision value (etag) from the provided LDAP entry.
-     *
-     * @param c
-     *            The context.
-     * @param entry
-     *            The LDAP entry.
-     * @return The revision value.
-     */
-    abstract String getRevisionFromEntry(Context c, Entry entry);
-
-}
diff --git a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java
index c2837e3..7add0d5 100644
--- a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java
+++ b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java
@@ -79,12 +79,13 @@
         private AuthorizationPolicy authzPolicy = AuthorizationPolicy.NONE;
         private DN baseDN; // TODO: support template variables.
         private ConnectionFactory factory;
-        private MVCCStrategy mvccStrategy;
+        private AttributeDescription etagAttribute;
         private NameStrategy nameStrategy;
         private AuthzIdTemplate proxiedAuthzTemplate;
         private ReadOnUpdatePolicy readOnUpdatePolicy = CONTROLS;
         private AttributeMapper rootMapper;
         private Schema schema = Schema.getDefaultSchema();
+        private boolean useSubtreeDelete;
 
         Builder() {
             useEtagAttribute();
@@ -142,8 +143,9 @@
                 break;
             }
             return new LDAPCollectionResourceProvider(baseDN, rootMapper, nameStrategy,
-                    mvccStrategy, new Config(factory, readOnUpdatePolicy, authzPolicy,
-                            proxiedAuthzTemplate, schema), additionalLDAPAttributes);
+                    etagAttribute, new Config(factory, readOnUpdatePolicy, authzPolicy,
+                            proxiedAuthzTemplate, useSubtreeDelete, schema),
+                    additionalLDAPAttributes);
         }
 
         /**
@@ -194,6 +196,10 @@
                 useEtagAttribute(etagAttribute.asString());
             }
 
+            if (configuration.get("useSubtreeDelete").required().asBoolean()) {
+                useSubtreeDelete();
+            }
+
             mapper(configureObjectMapper(configuration.get("attributes").required()));
 
             return this;
@@ -267,7 +273,7 @@
         }
 
         public Builder useEtagAttribute(final AttributeDescription attribute) {
-            this.mvccStrategy = new AttributeMVCCStrategy(attribute);
+            this.etagAttribute = attribute;
             return this;
         }
 
@@ -294,6 +300,11 @@
             return useServerNaming(at(dnAttribute), ad(idAttribute));
         }
 
+        public Builder useSubtreeDelete() {
+            this.useSubtreeDelete = true;
+            return this;
+        }
+
         private AttributeDescription ad(final String attribute) {
             return AttributeDescription.valueOf(attribute, schema);
         }
@@ -386,24 +397,6 @@
         }
     }
 
-    private static final class AttributeMVCCStrategy extends MVCCStrategy {
-        private final AttributeDescription ldapAttribute;
-
-        private AttributeMVCCStrategy(final AttributeDescription ldapAttribute) {
-            this.ldapAttribute = ldapAttribute;
-        }
-
-        @Override
-        void getLDAPAttributes(final Context c, final Set<String> ldapAttributes) {
-            ldapAttributes.add(ldapAttribute.toString());
-        }
-
-        @Override
-        String getRevisionFromEntry(final Context c, final Entry entry) {
-            return entry.parseAttribute(ldapAttribute).asString();
-        }
-    }
-
     private static final class AttributeNameStrategy extends NameStrategy {
         private final AttributeDescription dnAttribute;
         private final AttributeDescription idAttribute;
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 68af5c4..28a8851 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
@@ -27,9 +27,9 @@
 
 import java.io.IOException;
 
-import org.forgerock.json.resource.ConflictException;
 import org.forgerock.json.resource.Connection;
 import org.forgerock.json.resource.NotFoundException;
+import org.forgerock.json.resource.PreconditionFailedException;
 import org.forgerock.json.resource.RequestHandler;
 import org.forgerock.json.resource.Resource;
 import org.forgerock.json.resource.RootContext;
@@ -79,7 +79,7 @@
         }
     }
 
-    @Test(expectedExceptions = ConflictException.class, enabled = false)
+    @Test(expectedExceptions = PreconditionFailedException.class)
     public void testDeleteMVCCNoMatch() throws Exception {
         final RequestHandler handler = newCollection(builder().build());
         final Connection connection = newInternalConnection(handler);

--
Gitblit v1.10.0