UriRouterContext.java

/*
 * 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 2012-2017 ForgeRock AS.
 */

package org.forgerock.http.routing;

import static java.lang.String.format;
import static org.forgerock.util.Reject.checkNotNull;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;

import org.forgerock.json.JsonValue;
import org.forgerock.services.context.AbstractContext;
import org.forgerock.services.context.Context;

/**
 * A {@link Context} which is created when a request has been routed. The
 * context includes:
 * <ul>
 * <li>the portion of the request URI which matched the URI template
 * <li>the portion of the request URI which is remaining to be matched</li>
 * <li>a method for obtaining the base URI, which represents the portion of the
 * request URI which has been routed so far. This is obtained dynamically by
 * concatenating the matched URI with matched URIs in parent router contexts
 * <li>a map which contains the parsed URI template variables, keyed on the URI
 * template variable name.
 * </ul>
 *
 * The {@literal originalUri} represents the incoming request URI, as sent
 * by the client. So it can be used to generate redirects or links that will
 * be usable in the context of the caller (user-agent).
 *
 * This is considered as the starting point of the current routing process.
 * Most of the time, that will be the URI received by web server, but it may
 * happen that the URI received by the web server is not the same one that the
 * one sent by the user-agent (e.g. : behind a load balancer, in a cloud
 * environment, ... ). The real URI can be found by other means, it is possible to
 * “overwrite” it in other UriRouterContexts that can be accessed in the contexts
 * chain. Then in that case, we need to start a new routing process by considering
 * that given URI as the new originalUri.
 *
 * Otherwise child {@code UriRouterContext} may just redefine only the
 * {code matchedUri}, {@code remainingUri}, {@code uriTemplateVariables} as
 * part of their routing process.
 *
 */
public final class UriRouterContext extends AbstractContext {

    // Persisted attribute names
    private static final String ATTR_MATCHED_URI = "matchedUri";
    private static final String ATTR_REMAINIG_URI = "remainingUri";
    private static final String ATTR_URI_TEMPLATE_VARIABLES = "uriTemplateVariables";
    private static final String ATTR_ORIGINAL_URI = "originalUri";

    private final Map<String, String> uriTemplateVariables;

    /**
     * The original message's URI, considered as the starting of the current process.
     * Most of the time, that will the URI received by web server, but it may
     * happen that the URI received by the web server is not the real one that
     * we want to process.
     */
    private URI originalUri;

    /**
     * Creates a new routing context having the provided parent, URI template
     * variables, and an ID automatically generated using
     * {@code UUID.randomUUID()}.
     *
     * The parameters provided in this {@link UriRouterContext} will override any
     * parameters inherited from parent {@link UriRouterContext}s.
     *
     * @param parent
     *            The parent server context.
     * @param matchedUri
     *            The matched URI
     * @param remainingUri
     *            The remaining URI to be matched.
     * @param uriTemplateVariables
     *            A {@code Map} containing the parsed URI template variables,
     *            keyed on the URI template variable name.
     */
    public UriRouterContext(final Context parent, final String matchedUri, final String remainingUri,
            final Map<String, String> uriTemplateVariables) {
        this(parent, matchedUri, remainingUri, uriTemplateVariables, null);
    }

    /**
     * Creates a new routing context having the provided parent, URI template
     * variables, and an ID automatically generated using
     * {@code UUID.randomUUID()}.
     *
     * The parameters provided in this {@link UriRouterContext} will override any
     * parameters inherited from parent {@link UriRouterContext}s.
     *
     * @param parent
     *            The parent server context. (not null)
     * @param matchedUri
     *            The matched URI
     * @param remainingUri
     *            The remaining URI to be matched.
     * @param uriTemplateVariables
     *            A {@code Map} containing the parsed URI template variables,
     *            keyed on the URI template variable name. (not null)
     * @param originalUri
     *            The original URI. If not {@literal null} it will override the
     *            {@code originalUri} defined in the closest {@code UriRouterContext}
     *            referenced in the context's chain.
     */
    public UriRouterContext(final Context parent, final String matchedUri, final String remainingUri,
            final Map<String, String> uriTemplateVariables, URI originalUri) {
        super(checkNotNull(parent, "Cannot instantiate UriRouterContext with null parent Context"), "router");
        data.put(ATTR_MATCHED_URI, matchedUri);
        data.put(ATTR_REMAINIG_URI, remainingUri);
        this.uriTemplateVariables = Collections.unmodifiableMap(uriTemplateVariables);
        data.put(ATTR_URI_TEMPLATE_VARIABLES, this.uriTemplateVariables);

        if (originalUri != null) {
            this.originalUri = originalUri;
            data.put(ATTR_ORIGINAL_URI, originalUri.toASCIIString());
        }
    }

    /**
     * Restore from JSON representation.
     *
     * @param savedContext
     *            The JSON representation from which this context's attributes
     *            should be parsed.
     * @param classLoader
     *            The ClassLoader which can properly resolve the persisted class-name.
     */
    public UriRouterContext(final JsonValue savedContext, final ClassLoader classLoader) {
        super(savedContext, classLoader);
        this.uriTemplateVariables =
                Collections.unmodifiableMap(data.get(ATTR_URI_TEMPLATE_VARIABLES).required().asMap(String.class));

        final String savedUri = data.get(ATTR_ORIGINAL_URI).asString();
        try {
            this.originalUri = new URI(savedUri);
        } catch (URISyntaxException ex) {
            throw new IllegalArgumentException(format("The URI %s is not valid", savedUri));
        }
    }

    /**
     * Returns the portion of the request URI which has been routed so far. This
     * is obtained dynamically by concatenating the matched URI with the base
     * URI of the parent router context if present. The base URI is never
     * {@code null} but may be "" (empty string).
     *
     * @return The non-{@code null} portion of the request URI which has been
     *         routed so far.
     */
    public String getBaseUri() {
        final StringBuilder builder = new StringBuilder();
        final Context parent = getParent();
        if (parent.containsContext(UriRouterContext.class)) {
            final String baseUri = parent.asContext(UriRouterContext.class).getBaseUri();
            if (!baseUri.isEmpty()) {
                builder.append(baseUri);
            }
        }
        final String matchedUri = getMatchedUri();
        if (matchedUri.length() > 0) {
            if (builder.length() > 0) {
                builder.append('/');
            }
            builder.append(matchedUri);
        }
        return builder.toString();
    }

    /**
     * Returns the portion of the request URI which matched the URI template.
     * The matched URI is never {@code null} but may be "" (empty string).
     *
     * @return The non-{@code null} portion of the request URI which matched the
     *         URI template.
     */
    public String getMatchedUri() {
        return data.get(ATTR_MATCHED_URI).asString();
    }

    /**
     * Returns the portion of the request URI which is remaining to be matched
     * be the next router. The remaining URI is never {@code null} but may be
     * "" (empty string).
     *
     * @return The non-{@code null} portion of the request URI which is
     * remaining to be matched.
     */
    public String getRemainingUri() {
        return data.get(ATTR_REMAINIG_URI).asString();
    }

    /**
     * Returns an unmodifiable {@code Map} containing the parsed URI template
     * variables, keyed on the URI template variable name.
     *
     * @return The unmodifiable {@code Map} containing the parsed URI template
     *         variables, keyed on the URI template variable name.
     */
    public Map<String, String> getUriTemplateVariables() {
        return uriTemplateVariables;
    }

    /**
     * Get the original URI.
     *
     * @return The original URI
     */
    public URI getOriginalUri() {
        final Context parent = getParent();

        if (this.originalUri != null) {
            return this.originalUri;
        } else if (parent.containsContext(UriRouterContext.class)) {
            return parent.asContext(UriRouterContext.class).getOriginalUri();
        } else {
            return null;
        }
    }

    /**
     * Return a builder for a new {@link UriRouterContext}.
     * @param parent parent context
     * @return a builder for a new {@link UriRouterContext}.
     */
    public static Builder uriRouterContext(Context parent) {
        return new Builder(parent);
    }

    /**
     * Ease {@link UriRouterContext} construction.
     */
    public static class Builder {

        private final Context parent;
        private String matchedUri;
        private String remainingUri;
        private URI originalUri;
        private Map<String, String> variables = new LinkedHashMap<>();

        Builder(final Context parent) {
            this.parent = parent;
        }

        /**
         * Set the {@code matchedUri} value.
         * @param matchedUri matched uri
         * @return this builder
         */
        public Builder matchedUri(String matchedUri) {
            this.matchedUri = matchedUri;
            return this;
        }

        /**
         * Set the {@code remainingUri} value.
         * @param remainingUri remaining uri
         * @return this builder
         */
        public Builder remainingUri(String remainingUri) {
            this.remainingUri = remainingUri;
            return this;
        }

        /**
         * Set the {@code originalUri} value (only first UriRouterContext is expected to have that value set).
         * @param originalUri original uri
         * @return this builder
         */
        public Builder originalUri(URI originalUri) {
            this.originalUri = originalUri;
            return this;
        }

        /**
         * Set the {@code variables} value.
         * @param variables matched variables
         * @return this builder
         */
        public Builder templateVariables(Map<String, String> variables) {
            this.variables = checkNotNull(variables);
            return this;
        }

        /**
         * Add the given {@code name}:{@code value} pair in the {@code variables} map.
         * @param name matched variable name
         * @param value matched variable value
         * @return this builder
         */
        public Builder templateVariable(String name, String value) {
            this.variables.put(name, value);
            return this;
        }

        /**
         * Returns a new {@link UriRouterContext} build from provided values.
         * @return a new {@link UriRouterContext}.
         */
        public UriRouterContext build() {
            return new UriRouterContext(parent, matchedUri, remainingUri, variables, originalUri);
        }
    }
}