mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

Matthew Swift
08.58.2016 9cfc08902f5d1a22f4f5436b0facc8c047d45ed6
OPENDJ-3160 Factor out DN template support into separate class

Factored out DN template into DnTemplate class so that it can be reused
by the ReferencePropertyMapper. In addition, added supported for parent-
relative DNs using ".." notation.
3 files added
4 files modified
381 ■■■■ changed files
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/DnTemplate.java 160 ●●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2Ldap.java 59 ●●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LdapContext.java 35 ●●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResource.java 49 ●●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceCollection.java 4 ●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SubResourceSingleton.java 2 ●●● patch | view | raw | blame | history
opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/DnTemplateTest.java 72 ●●●●● patch | view | raw | blame | history
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/DnTemplate.java
New file
@@ -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);
        }
    }
}
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() {
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LdapContext.java
New file
@@ -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;
    }
}
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) {
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);
    }
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;
    }
opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/DnTemplateTest.java
New file
@@ -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));
    }
}