AbstractRouter.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 2015-2016 ForgeRock AS.
 */
package org.forgerock.services.routing;

import static org.forgerock.http.routing.RoutingMode.EQUALS;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;

import org.forgerock.http.ApiProducer;
import org.forgerock.http.routing.RoutingMode;
import org.forgerock.services.context.Context;
import org.forgerock.services.descriptor.Describable;
import org.forgerock.util.Pair;

/**
 * An abstract base class for implementing routers. Routers are common in applications which need to process incoming
 * requests based on varying criteria such as the target endpoint, API version expectations, client criteria (e.g.
 * source address), etc. This base class is designed to be protocol and framework independent. Frameworks should
 * sub-class an abstract router in order to provide framework specific behavior.
 * <p>
 * Generally speaking a router comprises of a series of routes, each of which is composed of a {@link RouteMatcher}
 * and a handler (H). When a request (R) is received the router invokes each {@code RouteMatcher} to see if it
 * matches and then invokes the associated handler if it is the best match.
 * <p>
 * Concrete implementations of {@code AbstractRouter} existing in both {@link org.forgerock.http.routing.Router CHF}
 * and CREST.
 *
 * @param <T> The type of the router.
 * @param <R> The type of the request.
 * @param <H> The type of the handler that will be used to handle routing requests.
 * @param <D> The type of descriptor object that the APIs supported by this router can be described using.
 */
public abstract class AbstractRouter<T extends AbstractRouter<T, R, H, D>, R, H, D>
        implements Describable<D, R>, Describable.Listener {

    private final Map<RouteMatcher<R>, H> routes = new ConcurrentHashMap<>();
    /** Matches the current route. */
    protected final RouteMatcher<R> thisRouterUriMatcher = uriMatcher(EQUALS, "");
    private volatile H defaultRoute;

    private final List<Describable.Listener> apiListeners = new CopyOnWriteArrayList<>();
    private ApiProducer<D> apiProducer;
    /** Api of the current router. */
    protected D api;
    private boolean apiNotificationsEnabled = true;

    /** Creates a new abstract router with no routes defined. */
    protected AbstractRouter() {
    }

    /**
     * Creates a new router containing the same routes and default route as the
     * provided router. Changes to the returned router's routing table will not
     * impact the provided router.
     *
     * @param router The router to be copied.
     */
    @SuppressWarnings("unchecked")
    protected AbstractRouter(AbstractRouter<T, R, H, D> router) {
        this.defaultRoute = router.defaultRoute;
        addAllRoutes((T) router);
    }

    /**
     * Returns this {@code AbstractRouter} instance, typed correctly.
     *
     * @return This {@code AbstractRouter} instance.
     */
    protected abstract T getThis();

    /**
     * Gets all registered routes on this router.
     *
     * @return All registered routes.
     */
    protected final Map<RouteMatcher<R>, H> getRoutes() {
        return Collections.unmodifiableMap(routes);
    }

    /**
     * Adds all of the routes defined in the provided router to this router.
     * New routes may be added while this router is processing requests.
     *
     * @param router The router whose routes are to be copied into this router.
     * @return This router instance.
     */
    public final T addAllRoutes(T router) {
        if (this != router) {
            boolean descriptorChanged = false;
            for (Map.Entry<RouteMatcher<R>, H> route : router.getRoutes().entrySet()) {
                H handler = route.getValue();
                descriptorChanged |= updateApiDescriptor(routes.put(route.getKey(), handler), handler);
            }
            if (descriptorChanged) {
                notifyDescriptorChange();
            }
        }
        return getThis();
    }

    /**
     * Adds a new route to this router for the provided handler. New routes may
     * be added while this router is processing requests.
     *
     * <p>The provided {@literal matcher} can be used to remove this route
     * later.</p>
     *
     * @param matcher The {@code RouteMatcher} that will evaluate whether
     *                  the incoming request matches this route.
     * @param handler The handler to which matching requests will be routed.
     * @return This router instance.
     */
    public final T addRoute(RouteMatcher<R> matcher, H handler) {
        return updateApiDescriptorAndNotify(routes.put(matcher, handler), handler);
    }

    private boolean updateApiDescriptor(H oldHandler, H newHandler) {
        boolean oldHandlerDescribable = oldHandler instanceof Describable;
        boolean newHandlerDescribable = newHandler instanceof Describable;
        if (oldHandlerDescribable) {
            ((Describable) oldHandler).removeDescriptorListener(this);
        }
        if (newHandlerDescribable) {
            ((Describable) newHandler).addDescriptorListener(this);
        }
        return oldHandlerDescribable || newHandlerDescribable;
    }

    private T updateApiDescriptorAndNotify(H oldHandler, H newHandler) {
        if (updateApiDescriptor(oldHandler, newHandler)) {
            notifyDescriptorChange();
        }
        return getThis();
    }

    /**
     * Sets the handler to be used as the default route for requests which do
     * not match any of the other defined routes.
     *
     * @param handler The handler to be used as the default route.
     * @return This router instance.
     */
    public final T setDefaultRoute(H handler) {
        H oldDefault = this.defaultRoute;
        this.defaultRoute = handler;
        return updateApiDescriptorAndNotify(oldDefault, handler);
    }

    /**
     * Returns the handler to be used as the default route for requests which
     * do not match any of the other defined routes.
     *
     * @return The handler to be used as the default route.
     */
    final H getDefaultRoute() {
        return defaultRoute;
    }

    /**
     * Removes all of the routes from this router. Routes may be removed while
     * this router is processing requests.
     *
     * @return This router instance.
     */
    public final T removeAllRoutes() {
        routes.clear();
        api = null;
        return getThis();
    }

    /**
     * Removes one or more routes from this router. Routes may be removed while
     * this router is processing requests.
     *
     * @param routes The {@code RouteMatcher}s of the routes to be removed.
     * @return {@code true} if at least one of the routes was found and removed.
     */
    @SafeVarargs
    public final boolean removeRoute(RouteMatcher<R>... routes) {
        boolean isModified = false;
        boolean apiDescriptorModified = false;
        for (RouteMatcher<R> route : routes) {
            H removed = this.routes.remove(route);
            isModified |= removed != null;
            apiDescriptorModified |= updateApiDescriptor(removed, null);
        }
        if (apiDescriptorModified) {
            notifyDescriptorChange();
        }
        return isModified;
    }

    /**
     * Finds the best route that matches the given request based on the route
     * matchers of the registered routes. If no registered route matches at
     * all then the default route is chosen, if present.
     *
     * @param context The request context.
     * @param request The request to be matched against the registered routes.
     * @return A {@code Pair} containing the decorated {@code Context} and the
     * handler which is the best match for the given request or {@code null} if
     * no route was found.
     * @throws IncomparableRouteMatchException If any of the registered
     * {@code RouteMatcher}s could not be compared to one another.
     */
    protected Pair<Context, H> getBestRoute(Context context, R request) throws IncomparableRouteMatchException {
        Pair<RouteMatch, H> bestMatch = getBestRouteMatch(context, request);
        if (bestMatch.getFirst() != null) {
            return Pair.of(bestMatch.getFirst().decorateContext(context), bestMatch.getSecond());
        }

        final H dftRoute = defaultRoute;
        return dftRoute != null ? Pair.of(context, dftRoute) : null;
    }

    /**
     * Get the best route for an API request. This is differs from {@link #getBestRoute(Context, Object)} in that it
     * also checks to see whether this router itself is being addressed, and returns the merged descriptor for all the
     * routes if so.
     *
     * @param context The request context.
     * @param request The request to be matched against the registered routes.
     * @return A {@code Pair} containing the decorated {@code Context} and the
     * handler which is the best match for the given request or {@code null} if
     * no route was found.
     * @throws IncomparableRouteMatchException If any of the registered
     * {@code RouteMatcher}s could not be compared to one another.
     */
    protected Pair<Context, H> getBestApiRoute(Context context, R request) throws IncomparableRouteMatchException {
        Pair<RouteMatch, H> bestMatch = getBestRouteMatch(context, request);

        Pair<RouteMatcher<R>, H> selfApiRoute = getSelfApiHandler();
        RouteMatch selfMatch = selfApiRoute.getFirst().evaluate(context, request);

        RouteMatch match = bestMatch.getFirst();
        if (selfMatch != null && selfMatch.isBetterMatchThan(match)) {
            return Pair.of(context, selfApiRoute.getSecond());
        } else if (match != null) {
            return Pair.of(match.decorateContext(context), bestMatch.getSecond());
        }

        final H dftRoute = defaultRoute;
        return dftRoute != null ? Pair.of(context, dftRoute) : null;
    }

    private Pair<RouteMatch, H> getBestRouteMatch(Context context, R request) throws IncomparableRouteMatchException {
        Pair<RouteMatch, H> bestMatch = Pair.of(null, null);
        for (Map.Entry<RouteMatcher<R>, H> route : routes.entrySet()) {
            RouteMatch result = route.getKey().evaluate(context, request);
            if (result != null) {
                if (result.isBetterMatchThan(bestMatch.getFirst())) {
                    bestMatch = Pair.of(result, route.getValue());
                }
            }
        }
        return bestMatch;
    }

    /**
     * Return a {@code Describable} handler that returns this {@code AbstractRouter}'s internal api description from the
     * {@link Describable#handleApiRequest(Context, Object)} method. All other methods should throw an
     * {@code UnsupportedOperationException}, as they should never be used.
     *
     * @return The describable handler.
     */
    protected abstract Pair<RouteMatcher<R>, H> getSelfApiHandler();

    @Override
    public synchronized D api(ApiProducer<D> producer) {
        if (apiProducer == null) {
            this.apiProducer = producer;
            notifyDescriptorChange();
        }
        return this.api;
    }

    /**
     * Create a URI matcher suitable for the request type {@code <R>}.
     * @param mode The routing mode.
     * @param pattern The pattern.
     * @return The matcher.
     */
    protected abstract RouteMatcher<R> uriMatcher(RoutingMode mode, String pattern);

    @Override
    @SuppressWarnings("unchecked")
    public D handleApiRequest(Context context, R request) {
        try {
            Pair<Context, H> bestRoute = getBestApiRoute(context, request);
            H handler = bestRoute != null ? bestRoute.getSecond() : null;
            if (handler instanceof Describable) {
                Context nextContext = bestRoute.getFirst();
                return ((Describable<D, R>) handler).handleApiRequest(nextContext, request);
            }
        } catch (IncomparableRouteMatchException e) {
            throw new UnsupportedOperationException(e);
        }
        if (thisRouterUriMatcher.evaluate(context, request) != null) {
            return this.api;
        }
        throw new IllegalStateException("No route matched the request " + request);
    }

    private void notifyListeners() {
        for (Describable.Listener listener : apiListeners) {
            listener.notifyDescriptorChange();
        }
    }

    @Override
    public void addDescriptorListener(Describable.Listener listener) {
        apiListeners.add(listener);
    }

    @Override
    public void removeDescriptorListener(Describable.Listener listener) {
        apiListeners.remove(listener);
    }

    @Override
    public void notifyDescriptorChange() {
        if (this.apiProducer != null && apiNotificationsEnabled) {
            apiNotificationsEnabled = false;
            this.api = buildApi(this.apiProducer);
            apiNotificationsEnabled = true;
            notifyListeners();
        }
    }

    /**
     * Build an api with a given {@link ApiProducer}.
     *
     * @param producer The given ApiProducer to use.
     * @return an api.
     */
    @SuppressWarnings("unchecked")
    protected D buildApi(ApiProducer<D> producer) {
        List<D> descriptors = new ArrayList<>(routes.size());
        for (Map.Entry<RouteMatcher<R>, H> route : routes.entrySet()) {
            H handler = route.getValue();
            if (handler instanceof Describable) {
                RouteMatcher<R> matcher = route.getKey();
                D descriptor = ((Describable<D, R>) handler).api(producer.newChildProducer(matcher.idFragment()));
                descriptors.add(matcher.transformApi(descriptor, producer));
            }
        }
        final H dftRoute = defaultRoute;
        if (dftRoute instanceof Describable) {
            descriptors.add(((Describable<D, R>) dftRoute).api(producer));
        }
        return descriptors.isEmpty() ? null : producer.merge(descriptors);
    }
}