From 9cfc08902f5d1a22f4f5436b0facc8c047d45ed6 Mon Sep 17 00:00:00 2001
From: Matthew Swift <matthew.swift@forgerock.com>
Date: Thu, 25 Aug 2016 14:28:39 +0000
Subject: [PATCH] OPENDJ-3160 Factor out DN template support into separate class

---
 opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceCollection.java |    4 
 opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceSingleton.java  |    2 
 opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/DnTemplate.java            |  160 ++++++++++++++++++++++
 opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LdapContext.java      |   35 +++++
 opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/DnTemplateTest.java        |   72 ++++++++++
 opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResource.java           |   49 ------
 opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2Ldap.java             |   59 ++++++++
 7 files changed, 332 insertions(+), 49 deletions(-)

diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/DnTemplate.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/DnTemplate.java
new file mode 100644
index 0000000..b8e7ea2
--- /dev/null
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/DnTemplate.java
@@ -0,0 +1,160 @@
+/*
+ * 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 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.rest2ldap;
+
+import static org.forgerock.opendj.rest2ldap.Rest2Ldap.DECODE_OPTIONS;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.forgerock.http.routing.UriRouterContext;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.schema.Schema;
+import org.forgerock.services.context.Context;
+import org.forgerock.util.Options;
+
+/**
+ * Represents a DN template whose RDN values may be substituted with URL template parameters parsed during routing.
+ * Two types of DN template are supported: {@link #compile(String) absolute/relative} or {@link #compileRelative(String)
+ * relative}. The table below shows how DN templates will be resolved to DNs when the template parameter "subdomain"
+ * has the value "www" and the current routing state references the DN "dc=example,dc=com":
+ * <p>
+ * <table>
+ * <tr><th>DN Template</th><th>{@link #compile(String)}</th><th>{@link #compileRelative(String)}</th></tr>
+ * <tr><td>dc=www</td><td>dc=www</td><td>dc=www,dc=example,dc=com</td></tr>
+ * <tr><td>..</td><td>dc=com</td><td>dc=com</td></tr>
+ * <tr><td>dc={subdomain}</td><td>dc=www</td><td>dc=www,dc=example,dc=com</td></tr>
+ * <tr><td>dc={subdomain},..</td><td>dc=www,dc=com</td><td>dc=www,dc=com</td></tr>
+ * </table>
+ */
+final class DnTemplate {
+    private static final Pattern TEMPLATE_KEY_RE = Pattern.compile("\\{([^}]+)\\}");
+    private final String template;
+    private final String formatString;
+    private final List<String> variables;
+    private final int relativeOffset;
+
+    /**
+     * Compiles a DN template which will resolve LDAP entries relative to the current routing state. The DN template may
+     * contain trailing ".." RDNs in order to resolve entries which are relative to a parent of the current routing
+     * state.
+     *
+     * @param template
+     *         The string representation of the DN template.
+     * @return The compiled DN template.
+     */
+    static DnTemplate compileRelative(String template) {
+        return compile(template, true);
+    }
+
+    /**
+     * Compiles a DN template which will resolve LDAP entries relative to the root DSE by default, but MAY include
+     * relative RDNs indicating that that the DN template will be resolved against current routing state
+     * instead.
+     *
+     * @param template
+     *         The string representation of the DN template.
+     * @return The compiled DN template.
+     */
+    static DnTemplate compile(String template) {
+        return compile(template, false);
+    }
+
+    private static DnTemplate compile(String template, boolean isRelative) {
+        // Parse any trailing relative RDNs.
+        String trimmedTemplate;
+        int relativeOffset;
+
+        if (template.equals("..")) {
+            trimmedTemplate = "";
+            relativeOffset = 1;
+        } else if (template.endsWith(",..")) {
+            relativeOffset = 0;
+            for (trimmedTemplate = template;
+                 trimmedTemplate.endsWith(",..");
+                 trimmedTemplate = trimmedTemplate.substring(0, trimmedTemplate.length() - 3)) {
+                relativeOffset++;
+            }
+        } else if (isRelative) {
+            trimmedTemplate = template;
+            relativeOffset = 0;
+        } else {
+            trimmedTemplate = template;
+            relativeOffset = -1;
+        }
+
+        final List<String> templateVariables = new ArrayList<>();
+        final Matcher matcher = TEMPLATE_KEY_RE.matcher(trimmedTemplate);
+        final StringBuffer buffer = new StringBuffer(trimmedTemplate.length());
+        while (matcher.find()) {
+            matcher.appendReplacement(buffer, "%s");
+            templateVariables.add(matcher.group(1));
+        }
+        matcher.appendTail(buffer);
+        return new DnTemplate(trimmedTemplate, buffer.toString(), templateVariables, relativeOffset);
+    }
+
+    private DnTemplate(String template, String formatString, List<String> variables, int relativeOffset) {
+        this.template = template;
+        this.formatString = formatString;
+        this.variables = variables;
+        this.relativeOffset = relativeOffset;
+    }
+
+    DN format(final Context context) {
+        // First determine the base DN based on the context DN and the relative offset.
+        DN baseDn = null;
+        if (relativeOffset >= 0 && context.containsContext(RoutingContext.class)) {
+            baseDn = context.asContext(RoutingContext.class).getDn().parent(relativeOffset);
+        }
+        if (baseDn == null) {
+            baseDn = DN.rootDN();
+        }
+
+        // Construct a DN using any routing template parameters.
+        final Options options = context.asContext(Rest2LdapContext.class).getRest2ldap().getOptions();
+        final Schema schema = options.get(DECODE_OPTIONS).getSchemaResolver().resolveSchema(template);
+        if (variables.isEmpty()) {
+            final DN relativeDn = DN.valueOf(template, schema);
+            return baseDn.child(relativeDn);
+        } else {
+            final String[] values = new String[variables.size()];
+            for (int i = 0; i < values.length; i++) {
+                values[i] = getTemplateParameter(context, variables.get(i));
+            }
+            final DN relativeDn = DN.format(formatString, schema, (Object[]) values);
+            return baseDn.child(relativeDn);
+        }
+    }
+
+    private String getTemplateParameter(final Context context, final String parameter) {
+        UriRouterContext uriRouterContext = context.asContext(UriRouterContext.class);
+        for (;;) {
+            final Map<String, String> uriTemplateVariables = uriRouterContext.getUriTemplateVariables();
+            final String value = uriTemplateVariables.get(parameter);
+            if (value != null) {
+                return value;
+            }
+            if (!uriRouterContext.getParent().containsContext(UriRouterContext.class)) {
+                throw new IllegalStateException("DN template parameter " + parameter + " cannot be resolved");
+            }
+            uriRouterContext = uriRouterContext.getParent().asContext(UriRouterContext.class);
+        }
+    }
+}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2Ldap.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2Ldap.java
index e0f8920..1b7b2e5 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2Ldap.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2Ldap.java
@@ -26,17 +26,28 @@
 import java.util.LinkedHashMap;
 import java.util.Map;
 
+import org.forgerock.json.resource.ActionRequest;
+import org.forgerock.json.resource.ActionResponse;
 import org.forgerock.json.resource.BadRequestException;
+import org.forgerock.json.resource.CreateRequest;
+import org.forgerock.json.resource.DeleteRequest;
 import org.forgerock.json.resource.ForbiddenException;
 import org.forgerock.json.resource.InternalServerErrorException;
 import org.forgerock.json.resource.NotFoundException;
+import org.forgerock.json.resource.PatchRequest;
 import org.forgerock.json.resource.PermanentException;
 import org.forgerock.json.resource.PreconditionFailedException;
+import org.forgerock.json.resource.QueryRequest;
+import org.forgerock.json.resource.QueryResourceHandler;
+import org.forgerock.json.resource.QueryResponse;
+import org.forgerock.json.resource.ReadRequest;
 import org.forgerock.json.resource.RequestHandler;
 import org.forgerock.json.resource.ResourceException;
+import org.forgerock.json.resource.ResourceResponse;
 import org.forgerock.json.resource.RetryableException;
 import org.forgerock.json.resource.Router;
 import org.forgerock.json.resource.ServiceUnavailableException;
+import org.forgerock.json.resource.UpdateRequest;
 import org.forgerock.opendj.ldap.AssertionFailureException;
 import org.forgerock.opendj.ldap.AttributeDescription;
 import org.forgerock.opendj.ldap.AuthenticationException;
@@ -51,9 +62,11 @@
 import org.forgerock.opendj.ldap.ResultCode;
 import org.forgerock.opendj.ldap.TimeoutResultException;
 import org.forgerock.opendj.ldap.schema.Schema;
+import org.forgerock.services.context.Context;
 import org.forgerock.util.Option;
 import org.forgerock.util.Options;
 import org.forgerock.util.Reject;
+import org.forgerock.util.promise.Promise;
 
 /**
  * Provides methods for constructing Rest2Ldap protocol gateways. Applications construct a new Rest2Ldap
@@ -359,7 +372,51 @@
         Reject.ifTrue(!resources.containsKey(resourceId), "unrecognized resource '" + resourceId + "'");
         final SubResourceSingleton root = singletonOf(resourceId);
         root.build(this, null);
-        return root.addRoutes(new Router());
+        return rest2LdapContext(root.addRoutes(new Router()));
+    }
+
+    private RequestHandler rest2LdapContext(final RequestHandler delegate) {
+        return new RequestHandler() {
+            public Promise<ActionResponse, ResourceException> handleAction(final Context context,
+                                                                           final ActionRequest request) {
+                return delegate.handleAction(wrap(context), request);
+            }
+
+            public Promise<ResourceResponse, ResourceException> handleCreate(final Context context,
+                                                                             final CreateRequest request) {
+                return delegate.handleCreate(wrap(context), request);
+            }
+
+            public Promise<ResourceResponse, ResourceException> handleDelete(final Context context,
+                                                                             final DeleteRequest request) {
+                return delegate.handleDelete(wrap(context), request);
+            }
+
+            public Promise<ResourceResponse, ResourceException> handlePatch(final Context context,
+                                                                            final PatchRequest request) {
+                return delegate.handlePatch(wrap(context), request);
+            }
+
+            public Promise<QueryResponse, ResourceException> handleQuery(final Context context,
+                                                                         final QueryRequest request,
+                                                                         final QueryResourceHandler handler) {
+                return delegate.handleQuery(wrap(context), request, handler);
+            }
+
+            public Promise<ResourceResponse, ResourceException> handleRead(final Context context,
+                                                                           final ReadRequest request) {
+                return delegate.handleRead(wrap(context), request);
+            }
+
+            public Promise<ResourceResponse, ResourceException> handleUpdate(final Context context,
+                                                                             final UpdateRequest request) {
+                return delegate.handleUpdate(wrap(context), request);
+            }
+
+            private Context wrap(final Context context) {
+                return new Rest2LdapContext(context, Rest2Ldap.this);
+            }
+        };
     }
 
     Options getOptions() {
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LdapContext.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LdapContext.java
new file mode 100644
index 0000000..4e43342
--- /dev/null
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LdapContext.java
@@ -0,0 +1,35 @@
+/*
+ * 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 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.rest2ldap;
+
+import org.forgerock.services.context.AbstractContext;
+import org.forgerock.services.context.Context;
+
+/**
+ * A {@link Context} which communicates the {@link Rest2Ldap} instance to downstream handlers and property mappers.
+ */
+final class Rest2LdapContext extends AbstractContext {
+    private final Rest2Ldap rest2ldap;
+
+    Rest2LdapContext(final Context parent, final Rest2Ldap rest2ldap) {
+        super(parent, "rest2ldap context");
+        this.rest2ldap = rest2ldap;
+    }
+
+    Rest2Ldap getRest2ldap() {
+        return rest2ldap;
+    }
+}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResource.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResource.java
index 8d8acbc..1cb250a 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResource.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResource.java
@@ -16,16 +16,8 @@
  */
 package org.forgerock.opendj.rest2ldap;
 
-import static org.forgerock.opendj.rest2ldap.Rest2Ldap.DECODE_OPTIONS;
 import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_UNRECOGNIZED_SUB_RESOURCE_TYPE;
 
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-import org.forgerock.http.routing.UriRouterContext;
 import org.forgerock.i18n.LocalizableMessage;
 import org.forgerock.i18n.LocalizedIllegalArgumentException;
 import org.forgerock.json.resource.BadRequestException;
@@ -34,7 +26,6 @@
 import org.forgerock.json.resource.ResourceException;
 import org.forgerock.json.resource.Router;
 import org.forgerock.opendj.ldap.DN;
-import org.forgerock.opendj.ldap.schema.Schema;
 import org.forgerock.services.context.Context;
 import org.forgerock.util.Function;
 
@@ -49,14 +40,11 @@
  * </ul>
  */
 public abstract class SubResource {
-    private static final Pattern TEMPLATE_KEY_RE = Pattern.compile("\\{([^}]+)\\}");
-
     private final String resourceId;
-    private final List<String> dnTemplateVariables = new ArrayList<>();
-    private String dnTemplateFormatString;
+    private DnTemplate dnTemplate;
 
     String urlTemplate = "";
-    String dnTemplate = "";
+    String dnTemplateString = "";
     boolean isReadOnly = false;
     Rest2Ldap rest2Ldap;
     Resource resource;
@@ -90,19 +78,7 @@
         if (resource == null) {
             throw new LocalizedIllegalArgumentException(ERR_UNRECOGNIZED_SUB_RESOURCE_TYPE.get(parent, resourceId));
         }
-        this.dnTemplateFormatString = formatTemplate(dnTemplate, dnTemplateVariables);
-    }
-
-    // Parse the template keys and replace them with %s for formatting.
-    private String formatTemplate(final String template, final List<String> templateVariables) {
-        final Matcher matcher = TEMPLATE_KEY_RE.matcher(template);
-        final StringBuffer buffer = new StringBuffer(template.length());
-        while (matcher.find()) {
-            matcher.appendReplacement(buffer, "%s");
-            templateVariables.add(matcher.group(1));
-        }
-        matcher.appendTail(buffer);
-        return buffer.toString();
+        this.dnTemplate = DnTemplate.compileRelative(dnTemplateString);
     }
 
     abstract Router addRoutes(Router router);
@@ -125,24 +101,7 @@
     }
 
     final DN dnFrom(final Context context) {
-        final DN baseDn = context.containsContext(RoutingContext.class)
-                ? context.asContext(RoutingContext.class).getDn() : DN.rootDN();
-
-        final Schema schema = rest2Ldap.getOptions().get(DECODE_OPTIONS).getSchemaResolver().resolveSchema(dnTemplate);
-        if (dnTemplateVariables.isEmpty()) {
-            final DN relativeDn = DN.valueOf(dnTemplate, schema);
-            return baseDn.child(relativeDn);
-        } else {
-            final UriRouterContext uriRouterContext = context.asContext(UriRouterContext.class);
-            final Map<String, String> uriTemplateVariables = uriRouterContext.getUriTemplateVariables();
-            final String[] values = new String[dnTemplateVariables.size()];
-            for (int i = 0; i < values.length; i++) {
-                final String key = dnTemplateVariables.get(i);
-                values[i] = uriTemplateVariables.get(key);
-            }
-            final DN relativeDn = DN.format(dnTemplateFormatString, schema, (Object[]) values);
-            return baseDn.child(relativeDn);
-        }
+        return dnTemplate.format(context);
     }
 
     final RequestHandler subResourceRouterFrom(final RoutingContext context) {
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceCollection.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceCollection.java
index 0684f57..4dad784 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceCollection.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceCollection.java
@@ -177,7 +177,7 @@
      * @return A reference to this object.
      */
     public SubResourceCollection dnTemplate(final String dnTemplate) {
-        this.dnTemplate = dnTemplate;
+        this.dnTemplateString = dnTemplate;
         return this;
     }
 
@@ -257,7 +257,7 @@
     private SubResourceImpl collection(final Context context) {
         return new SubResourceImpl(rest2Ldap,
                                    dnFrom(context),
-                                   dnTemplate.isEmpty() ? null : glueObjectClasses,
+                                   dnTemplateString.isEmpty() ? null : glueObjectClasses,
                                    namingStrategy,
                                    resource);
     }
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceSingleton.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceSingleton.java
index a90ea8b..2e4076e 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceSingleton.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceSingleton.java
@@ -111,7 +111,7 @@
      * @return A reference to this object.
      */
     public SubResourceSingleton dnTemplate(final String dnTemplate) {
-        this.dnTemplate = dnTemplate;
+        this.dnTemplateString = dnTemplate;
         return this;
     }
 
diff --git a/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/DnTemplateTest.java b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/DnTemplateTest.java
new file mode 100644
index 0000000..7f7b3cf
--- /dev/null
+++ b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/DnTemplateTest.java
@@ -0,0 +1,72 @@
+/*
+ * 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 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.rest2ldap;
+
+import static java.util.Collections.singletonMap;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.forgerock.opendj.rest2ldap.Rest2Ldap.rest2Ldap;
+import static org.forgerock.util.Options.defaultOptions;
+
+import org.forgerock.http.routing.UriRouterContext;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.services.context.Context;
+import org.forgerock.services.context.RootContext;
+import org.forgerock.testng.ForgeRockTestCase;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+@Test
+@SuppressWarnings("javadoc")
+public final class DnTemplateTest extends ForgeRockTestCase {
+    private final Context context;
+    {
+        Context ctx = new RootContext();
+        ctx = new Rest2LdapContext(ctx, rest2Ldap(defaultOptions()));
+        ctx = new UriRouterContext(ctx, "", "", singletonMap("subdomain", "www"));
+        ctx = new RoutingContext(ctx, DN.valueOf("dc=example,dc=com"), null);
+        ctx = new UriRouterContext(ctx, "", "", singletonMap("tenant", "acme"));
+        context = ctx;
+    }
+
+    @DataProvider
+    Object[][] templateData() {
+        // @formatter:off
+        return new Object[][] {
+            { "dc=www", "dc=www", "dc=www,dc=example,dc=com" },
+            { "..", "dc=com", "dc=com" },
+            { "dc={subdomain}", "dc=www", "dc=www,dc=example,dc=com" },
+            { "dc={subdomain},..", "dc=www,dc=com", "dc=www,dc=com" },
+            { "dc={subdomain},dc={tenant},..", "dc=www,dc=acme,dc=com", "dc=www,dc=acme,dc=com" },
+        };
+        // @formatter:on
+    }
+
+    @SuppressWarnings("unused")
+    @Test(dataProvider = "templateData")
+    public void testCompile(String template, String expectedDn, String expectedRelativeDn) {
+        DnTemplate dnTemplate = DnTemplate.compile(template);
+        DN dn = dnTemplate.format(context);
+        assertThat((Object) dn).isEqualTo(DN.valueOf(expectedDn));
+    }
+
+    @SuppressWarnings("unused")
+    @Test(dataProvider = "templateData")
+    public void testCompileRelative(String template, String expectedDn, String expectedRelativeDn) {
+        DnTemplate dnTemplate = DnTemplate.compileRelative(template);
+        DN dn = dnTemplate.format(context);
+        assertThat((Object) dn).isEqualTo(DN.valueOf(expectedRelativeDn));
+    }
+}

--
Gitblit v1.10.0