001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2015 ForgeRock AS.
015 */
016
017package org.forgerock.services.routing;
018
019import java.util.Collections;
020import java.util.Map;
021import java.util.concurrent.ConcurrentHashMap;
022
023import org.forgerock.services.context.Context;
024import org.forgerock.util.Pair;
025
026/**
027 * An abstract base class for implementing routers. Routers are common in applications which need to process incoming
028 * requests based on varying criteria such as the target endpoint, API version expectations, client criteria (e.g.
029 * source address), etc. This base class is designed to be protocol and framework independent. Frameworks should
030 * sub-class an abstract router in order to provide framework specific behavior.
031 * <p>
032 * Generally speaking a router comprises of a series of routes, each of which is composed of a {@link RouteMatcher}
033 * and a handler (H). When a request (R) is received the router invokes each {@code RouteMatcher} to see if it
034 * matches and then invokes the associated handler if it is the best match.
035 * <p>
036 * Concrete implementations of {@code AbstractRouter} existing in both {@link org.forgerock.http.routing.Router CHF}
037 * and CREST.
038 *
039 * @param <T> The type of the router.
040 * @param <R> The type of the request.
041 * @param <H> The type of the handler that will be used to handle routing requests.
042 */
043public abstract class AbstractRouter<T extends AbstractRouter<T, R, H>, R, H> {
044
045    private final Map<RouteMatcher<R>, H> routes = new ConcurrentHashMap<>();
046    private volatile H defaultRoute;
047
048    /**
049     * Creates a new abstract router with no routes defined.
050     */
051    protected AbstractRouter() {
052    }
053
054    /**
055     * Creates a new router containing the same routes and default route as the
056     * provided router. Changes to the returned router's routing table will not
057     * impact the provided router.
058     *
059     * @param router The router to be copied.
060     */
061    @SuppressWarnings("unchecked")
062    protected AbstractRouter(AbstractRouter<T, R, H> router) {
063        this.defaultRoute = router.defaultRoute;
064        addAllRoutes((T) router);
065    }
066
067    /**
068     * Returns this {@code AbstractRouter} instance, typed correctly.
069     *
070     * @return This {@code AbstractRouter} instance.
071     */
072    protected abstract T getThis();
073
074    /**
075     * Gets all registered routes on this router.
076     *
077     * @return All registered routes.
078     */
079    final Map<RouteMatcher<R>, H> getRoutes() {
080        return Collections.unmodifiableMap(routes);
081    }
082
083    /**
084     * Adds all of the routes defined in the provided router to this router.
085     * New routes may be added while this router is processing requests.
086     *
087     * @param router The router whose routes are to be copied into this router.
088     * @return This router instance.
089     */
090    public final T addAllRoutes(T router) {
091        if (this != router) {
092            for (Map.Entry<RouteMatcher<R>, H> route : router.getRoutes().entrySet()) {
093                addRoute(route.getKey(), route.getValue());
094            }
095        }
096        return getThis();
097    }
098
099    /**
100     * Adds a new route to this router for the provided handler. New routes may
101     * be added while this router is processing requests.
102     *
103     * <p>The provided {@literal matcher} can be used to remove this route
104     * later.</p>
105     *
106     * @param matcher The {@code RouteMatcher} that will evaluate whether
107     *                  the incoming request matches this route.
108     * @param handler The handler to which matching requests will be routed.
109     * @return This router instance.
110     */
111    public final T addRoute(RouteMatcher<R> matcher, H handler) {
112        routes.put(matcher, handler);
113        return getThis();
114    }
115
116    /**
117     * Sets the handler to be used as the default route for requests which do
118     * not match any of the other defined routes.
119     *
120     * @param handler The handler to be used as the default route.
121     * @return This router instance.
122     */
123    public final T setDefaultRoute(H handler) {
124        this.defaultRoute = handler;
125        return getThis();
126    }
127
128    /**
129     * Returns the handler to be used as the default route for requests which
130     * do not match any of the other defined routes.
131     *
132     * @return The handler to be used as the default route.
133     */
134    final H getDefaultRoute() {
135        return defaultRoute;
136    }
137
138    /**
139     * Removes all of the routes from this router. Routes may be removed while
140     * this router is processing requests.
141     *
142     * @return This router instance.
143     */
144    public final T removeAllRoutes() {
145        routes.clear();
146        return getThis();
147    }
148
149    /**
150     * Removes one or more routes from this router. Routes may be removed while
151     * this router is processing requests.
152     *
153     * @param routes The {@code RouteMatcher}s of the routes to be removed.
154     * @return {@code true} if at least one of the routes was found and removed.
155     */
156    @SafeVarargs
157    public final boolean removeRoute(RouteMatcher<R>... routes) {
158        boolean isModified = false;
159        for (RouteMatcher<R> route : routes) {
160            isModified |= this.routes.remove(route) != null;
161        }
162        return isModified;
163    }
164
165    /**
166     * Finds the best route that matches the given request based on the route
167     * matchers of the registered routes. If no registered route matches at
168     * all then the default route is chosen, if present.
169     *
170     * @param context The request context.
171     * @param request The request to be matched against the registered routes.
172     * @return A {@code Pair} containing the decorated {@code Context} and the
173     * handler which is the best match for the given request or {@code null} if
174     * no route was found.
175     * @throws IncomparableRouteMatchException If any of the registered
176     * {@code RouteMatcher}s could not be compared to one another.
177     */
178    protected Pair<Context, H> getBestRoute(Context context, R request) throws IncomparableRouteMatchException {
179        H handler = null;
180        RouteMatch bestMatch = null;
181        for (Map.Entry<RouteMatcher<R>, H> route : routes.entrySet()) {
182            RouteMatch result = route.getKey().evaluate(context, request);
183            if (result != null) {
184                if (result.isBetterMatchThan(bestMatch)) {
185                    handler = route.getValue();
186                    bestMatch = result;
187                }
188            }
189        }
190        if (bestMatch != null) {
191            return Pair.of(bestMatch.decorateContext(context), handler);
192        }
193
194        handler = defaultRoute;
195        if (handler != null) {
196            return Pair.of(context, handler);
197        }
198        return null;
199    }
200}