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