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}