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.json.resource; 018 019import static org.forgerock.http.routing.RoutingMode.EQUALS; 020import static org.forgerock.http.routing.RoutingMode.STARTS_WITH; 021import static org.forgerock.json.resource.Requests.*; 022import static org.forgerock.json.resource.ResourceApiVersionRoutingFilter.setApiVersionInfo; 023import static org.forgerock.json.resource.Resources.newCollection; 024import static org.forgerock.json.resource.Resources.newSingleton; 025import static org.forgerock.json.resource.RouteMatchers.requestResourceApiVersionMatcher; 026import static org.forgerock.json.resource.RouteMatchers.requestUriMatcher; 027import static org.forgerock.util.promise.Promises.newExceptionPromise; 028 029import org.forgerock.http.routing.ApiVersionRouterContext; 030import org.forgerock.http.routing.RoutingMode; 031import org.forgerock.http.routing.UriRouterContext; 032import org.forgerock.http.routing.Version; 033import org.forgerock.services.context.Context; 034import org.forgerock.services.routing.AbstractRouter; 035import org.forgerock.services.routing.IncomparableRouteMatchException; 036import org.forgerock.services.routing.RouteMatcher; 037import org.forgerock.util.Pair; 038import org.forgerock.util.promise.Promise; 039 040/** 041 * A router which routes requests based on route predicates. Each route is 042 * comprised of a {@link RouteMatcher route matcher} and a corresponding 043 * handler, when routing a request the router will call 044 * {@link RouteMatcher#evaluate} for each 045 * registered route and use the returned {@link RouteMatcher} to determine 046 * which route best matches the request. 047 * 048 * <p>Routes may be added and removed from a router as follows: 049 * 050 * <pre> 051 * Handler users = ...; 052 * Router router = new Router(); 053 * RouteMatcher routeOne = RouteMatchers.requestUriMatcher(EQUALS, "users"); 054 * RouteMatcher routeTwo = RouteMatchers.requestUriMatcher(EQUALS, "users/{userId}"); 055 * router.addRoute(routeOne, users); 056 * router.addRoute(routeTwo, users); 057 * 058 * // Deregister a route. 059 * router.removeRoute(routeOne, routeTwo); 060 * </pre> 061 * 062 * @see AbstractRouter 063 * @see RouteMatchers 064 */ 065public class Router extends AbstractRouter<Router, Request, RequestHandler> implements RequestHandler { 066 067 /** 068 * Creates a new router with no routes defined. 069 */ 070 public Router() { 071 super(); 072 } 073 074 /** 075 * Creates a new router containing the same routes and default route as the 076 * provided router. Changes to the returned router's routing table will not 077 * impact the provided router. 078 * 079 * @param router The router to be copied. 080 */ 081 public Router(AbstractRouter<Router, Request, RequestHandler> router) { 082 super(router); 083 } 084 085 @Override 086 protected Router getThis() { 087 return this; 088 } 089 090 /** 091 * Adds a new route to this router for the provided collection resource 092 * provider. New routes may be added while this router is processing 093 * requests. 094 * <p> 095 * The provided URI template must match the resource collection itself, not 096 * resource instances. For example: 097 * 098 * <pre> 099 * CollectionResourceProvider users = ...; 100 * Router router = new Router(); 101 * 102 * // This is valid usage: the template matches the resource collection. 103 * router.addRoute(Router.uriTemplate("users"), users); 104 * 105 * // This is invalid usage: the template matches resource instances. 106 * router.addRoute(Router.uriTemplate("users/{userId}"), users); 107 * </pre> 108 * 109 * @param uriTemplate 110 * The URI template which request resource names must match. 111 * @param provider 112 * The collection resource provider to which matching requests 113 * will be routed. 114 * @return The {@link RouteMatcher} for the route that can be used to 115 * remove the route at a later point. 116 */ 117 public RouteMatcher<Request> addRoute(UriTemplate uriTemplate, CollectionResourceProvider provider) { 118 RouteMatcher<Request> routeMatcher = requestUriMatcher(STARTS_WITH, uriTemplate.template); 119 addRoute(routeMatcher, newCollection(provider)); 120 return routeMatcher; 121 } 122 123 /** 124 * Adds a new route to this router for the provided singleton resource 125 * provider. New routes may be added while this router is processing 126 * requests. 127 * 128 * @param uriTemplate 129 * The URI template which request resource names must match. 130 * @param provider 131 * The singleton resource provider to which matching requests 132 * will be routed. 133 * @return The {@link RouteMatcher} for the route that can be used to 134 * remove the route at a later point. 135 */ 136 public RouteMatcher<Request> addRoute(UriTemplate uriTemplate, SingletonResourceProvider provider) { 137 RouteMatcher<Request> routeMatcher = requestUriMatcher(EQUALS, uriTemplate.template); 138 addRoute(routeMatcher, newSingleton(provider)); 139 return routeMatcher; 140 } 141 142 /** 143 * Adds a new route to this router for the provided request handler. New 144 * routes may be added while this router is processing requests. 145 * 146 * @param mode 147 * Indicates how the URI template should be matched against 148 * resource names. 149 * @param uriTemplate 150 * The URI template which request resource names must match. 151 * @param handler 152 * The request handler to which matching requests will be routed. 153 * @return The {@link RouteMatcher} for the route that can be used to 154 * remove the route at a later point. 155 */ 156 public RouteMatcher<Request> addRoute(RoutingMode mode, UriTemplate uriTemplate, RequestHandler handler) { 157 RouteMatcher<Request> routeMatcher = requestUriMatcher(mode, uriTemplate.template); 158 addRoute(routeMatcher, handler); 159 return routeMatcher; 160 } 161 162 /** 163 * Creates a {@link UriTemplate} from a URI template string that will be 164 * used to match and route incoming requests. 165 * 166 * @param template The URI template. 167 * @return A {@code UriTemplate} instance. 168 */ 169 public static UriTemplate uriTemplate(String template) { 170 return new UriTemplate(template); 171 } 172 173 /** 174 * Adds a new route to this router for the provided collection resource 175 * provider. New routes may be added while this router is processing 176 * requests. 177 * 178 * @param version The resource API version the the request must match. 179 * @param provider The collection resource provider to which matching 180 * requests will be routed. 181 * @return The {@link RouteMatcher} for the route that can be used to 182 * remove the route at a later point. 183 */ 184 public RouteMatcher<Request> addRoute(Version version, CollectionResourceProvider provider) { 185 return addRoute(version, newCollection(provider)); 186 } 187 188 /** 189 * Adds a new route to this router for the provided singleton resource 190 * provider. New routes may be added while this router is processing 191 * requests. 192 * 193 * @param version The resource API version the the request must match. 194 * @param provider The singleton resource provider to which matching 195 * requests will be routed. 196 * @return The {@link RouteMatcher} for the route that can be used to 197 * remove the route at a later point. 198 */ 199 public RouteMatcher<Request> addRoute(Version version, SingletonResourceProvider provider) { 200 return addRoute(version, newSingleton(provider)); 201 } 202 203 /** 204 * Adds a new route to this router for the provided request handler. New 205 * routes may be added while this router is processing requests. 206 * 207 * @param version The resource API version the the request must match. 208 * @param handler 209 * The request handler to which matching requests will be routed. 210 * @return The {@link RouteMatcher} for the route that can be used to 211 * remove the route at a later point. 212 */ 213 public RouteMatcher<Request> addRoute(Version version, RequestHandler handler) { 214 RouteMatcher<Request> routeMatcher = requestResourceApiVersionMatcher(version); 215 addRoute(routeMatcher, handler); 216 return routeMatcher; 217 } 218 219 private Pair<Context, RequestHandler> getBestMatch(Context context, Request request) 220 throws ResourceException { 221 try { 222 Pair<Context, RequestHandler> bestMatch = getBestRoute(context, request); 223 if (bestMatch == null) { 224 throw new NotFoundException(String.format("Resource '%s' not found", request.getResourcePath())); 225 } else { 226 return bestMatch; 227 } 228 } catch (IncomparableRouteMatchException e) { 229 throw new InternalServerErrorException(e.getMessage(), e); 230 } 231 } 232 233 @Override 234 public Promise<ActionResponse, ResourceException> handleAction(Context context, ActionRequest request) { 235 try { 236 Pair<Context, RequestHandler> bestMatch = getBestMatch(context, request); 237 UriRouterContext routerContext = getRouterContext(bestMatch.getFirst()); 238 ActionRequest routedRequest = wasRouted(context, routerContext) 239 ? copyOfActionRequest(request).setResourcePath(getResourcePath(routerContext)) 240 : request; 241 return bestMatch.getSecond().handleAction(bestMatch.getFirst(), routedRequest); 242 } catch (ResourceException e) { 243 return newExceptionPromise(e); 244 } 245 } 246 247 @Override 248 public Promise<ResourceResponse, ResourceException> handleCreate(Context context, CreateRequest request) { 249 try { 250 Pair<Context, RequestHandler> bestMatch = getBestMatch(context, request); 251 UriRouterContext routerContext = getRouterContext(bestMatch.getFirst()); 252 CreateRequest routedRequest = wasRouted(context, routerContext) 253 ? copyOfCreateRequest(request).setResourcePath(getResourcePath(routerContext)) 254 : request; 255 return bestMatch.getSecond().handleCreate(bestMatch.getFirst(), routedRequest); 256 } catch (ResourceException e) { 257 return newExceptionPromise(e); 258 } 259 } 260 261 @Override 262 public Promise<ResourceResponse, ResourceException> handleDelete(Context context, DeleteRequest request) { 263 try { 264 Pair<Context, RequestHandler> bestMatch = getBestMatch(context, request); 265 UriRouterContext routerContext = getRouterContext(bestMatch.getFirst()); 266 DeleteRequest routedRequest = wasRouted(context, routerContext) 267 ? copyOfDeleteRequest(request).setResourcePath(getResourcePath(routerContext)) 268 : request; 269 return bestMatch.getSecond().handleDelete(bestMatch.getFirst(), routedRequest); 270 } catch (ResourceException e) { 271 return newExceptionPromise(e); 272 } 273 } 274 275 @Override 276 public Promise<ResourceResponse, ResourceException> handlePatch(Context context, PatchRequest request) { 277 try { 278 Pair<Context, RequestHandler> bestMatch = getBestMatch(context, request); 279 UriRouterContext routerContext = getRouterContext(bestMatch.getFirst()); 280 PatchRequest routedRequest = wasRouted(context, routerContext) 281 ? copyOfPatchRequest(request).setResourcePath(getResourcePath(routerContext)) 282 : request; 283 return bestMatch.getSecond().handlePatch(bestMatch.getFirst(), routedRequest); 284 } catch (ResourceException e) { 285 return newExceptionPromise(e); 286 } 287 } 288 289 @Override 290 public Promise<QueryResponse, ResourceException> handleQuery(final Context context, 291 final QueryRequest request, final QueryResourceHandler handler) { 292 try { 293 Pair<Context, RequestHandler> bestMatch = getBestMatch(context, request); 294 final Context decoratedContext = bestMatch.getFirst(); 295 UriRouterContext routerContext = getRouterContext(decoratedContext); 296 QueryRequest routedRequest = wasRouted(context, routerContext) 297 ? copyOfQueryRequest(request).setResourcePath(getResourcePath(routerContext)) 298 : request; 299 QueryResourceHandler resourceHandler = new QueryResourceHandler() { 300 @Override 301 public boolean handleResource(ResourceResponse resource) { 302 if (decoratedContext.containsContext(ApiVersionRouterContext.class)) { 303 ApiVersionRouterContext apiVersionRouterContext = 304 decoratedContext.asContext(ApiVersionRouterContext.class); 305 setApiVersionInfo(apiVersionRouterContext, request, resource); 306 } 307 return handler.handleResource(resource); 308 } 309 }; 310 return bestMatch.getSecond().handleQuery(decoratedContext, routedRequest, resourceHandler); 311 } catch (ResourceException e) { 312 return newExceptionPromise(e); 313 } 314 } 315 316 @Override 317 public Promise<ResourceResponse, ResourceException> handleRead(Context context, ReadRequest request) { 318 try { 319 Pair<Context, RequestHandler> bestMatch = getBestMatch(context, request); 320 UriRouterContext routerContext = getRouterContext(bestMatch.getFirst()); 321 ReadRequest routedRequest = wasRouted(context, routerContext) 322 ? copyOfReadRequest(request).setResourcePath(getResourcePath(routerContext)) 323 : request; 324 return bestMatch.getSecond().handleRead(bestMatch.getFirst(), routedRequest); 325 } catch (ResourceException e) { 326 return newExceptionPromise(e); 327 } 328 } 329 330 @Override 331 public Promise<ResourceResponse, ResourceException> handleUpdate(Context context, UpdateRequest request) { 332 try { 333 Pair<Context, RequestHandler> bestMatch = getBestMatch(context, request); 334 UriRouterContext routerContext = getRouterContext(bestMatch.getFirst()); 335 UpdateRequest routedRequest = wasRouted(context, routerContext) 336 ? copyOfUpdateRequest(request).setResourcePath(getResourcePath(routerContext)) 337 : request; 338 return bestMatch.getSecond().handleUpdate(bestMatch.getFirst(), routedRequest); 339 } catch (ResourceException e) { 340 return newExceptionPromise(e); 341 } 342 } 343 344 private UriRouterContext getRouterContext(Context context) { 345 if (context.containsContext(UriRouterContext.class)) { 346 return context.asContext(UriRouterContext.class); 347 } else { 348 return null; 349 } 350 } 351 352 private boolean wasRouted(Context originalContext, UriRouterContext routerContext) { 353 return routerContext != null 354 && (!originalContext.containsContext(UriRouterContext.class) 355 || routerContext != originalContext.asContext(UriRouterContext.class)); 356 } 357 358 private String getResourcePath(UriRouterContext routerContext) { 359 return routerContext.getRemainingUri(); 360 } 361 362 /** 363 * Represents a URI template string that will be used to match and route 364 * incoming requests. 365 */ 366 public static final class UriTemplate { 367 private final String template; 368 369 private UriTemplate(String template) { 370 this.template = template; 371 } 372 373 /** 374 * Return the string representation of the UriTemplate. 375 * 376 * @return the string representation of the UriTemplate. 377 */ 378 @Override 379 public String toString() { 380 return template; 381 } 382 } 383}