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