View Javadoc
1   /*
2    * The contents of this file are subject to the terms of the Common Development and
3    * Distribution License (the License). You may not use this file except in compliance with the
4    * License.
5    *
6    * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
7    * specific language governing permission and limitations under the License.
8    *
9    * When distributing Covered Software, include this CDDL Header Notice in each file and include
10   * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
11   * Header, with the fields enclosed by brackets [] replaced by your own identifying
12   * information: "Portions copyright [year] [name of copyright owner]".
13   *
14   * Copyright 2015-2016 ForgeRock AS.
15   */
16  
17  package org.forgerock.json.resource;
18  
19  import static org.forgerock.http.routing.RoutingMode.EQUALS;
20  import static org.forgerock.http.routing.RoutingMode.STARTS_WITH;
21  import static org.forgerock.json.resource.Requests.copyOfActionRequest;
22  import static org.forgerock.json.resource.Requests.copyOfApiRequest;
23  import static org.forgerock.json.resource.Requests.copyOfCreateRequest;
24  import static org.forgerock.json.resource.Requests.copyOfDeleteRequest;
25  import static org.forgerock.json.resource.Requests.copyOfPatchRequest;
26  import static org.forgerock.json.resource.Requests.copyOfQueryRequest;
27  import static org.forgerock.json.resource.Requests.copyOfReadRequest;
28  import static org.forgerock.json.resource.Requests.copyOfUpdateRequest;
29  import static org.forgerock.json.resource.ResourceApiVersionRoutingFilter.setApiVersionInfo;
30  import static org.forgerock.json.resource.Resources.newHandler;
31  import static org.forgerock.json.resource.RouteMatchers.requestResourceApiVersionMatcher;
32  import static org.forgerock.json.resource.RouteMatchers.requestUriMatcher;
33  import static org.forgerock.json.resource.RouteMatchers.selfApiMatcher;
34  import static org.forgerock.util.promise.Promises.newExceptionPromise;
35  
36  import org.forgerock.api.models.ApiDescription;
37  import org.forgerock.http.ApiProducer;
38  import org.forgerock.http.routing.ApiVersionRouterContext;
39  import org.forgerock.http.routing.RoutingMode;
40  import org.forgerock.http.routing.UriRouterContext;
41  import org.forgerock.http.routing.Version;
42  import org.forgerock.services.context.Context;
43  import org.forgerock.services.descriptor.Describable;
44  import org.forgerock.services.routing.AbstractRouter;
45  import org.forgerock.services.routing.IncomparableRouteMatchException;
46  import org.forgerock.services.routing.RouteMatcher;
47  import org.forgerock.util.Pair;
48  import org.forgerock.util.promise.Promise;
49  
50  /**
51   * A router which routes requests based on route predicates. Each route is
52   * comprised of a {@link RouteMatcher route matcher} and a corresponding
53   * handler, when routing a request the router will call
54   * {@link RouteMatcher#evaluate} for each
55   * registered route and use the returned {@link RouteMatcher} to determine
56   * which route best matches the request.
57   *
58   * <p>Routes may be added and removed from a router as follows:
59   *
60   * <pre>
61   * Handler users = ...;
62   * Router router = new Router();
63   * RouteMatcher routeOne = RouteMatchers.requestUriMatcher(EQUALS, &quot;users&quot;);
64   * RouteMatcher routeTwo = RouteMatchers.requestUriMatcher(EQUALS, &quot;users/{userId}&quot;);
65   * router.addRoute(routeOne, users);
66   * router.addRoute(routeTwo, users);
67   *
68   * // Deregister a route.
69   * router.removeRoute(routeOne, routeTwo);
70   * </pre>
71   *
72   * @see AbstractRouter
73   * @see RouteMatchers
74   */
75  public class Router extends AbstractRouter<Router, Request, RequestHandler, ApiDescription>
76          implements RequestHandler {
77  
78      private RequestHandler selfApiHandler = new SelfApiHandler();
79  
80      /**
81       * Creates a new router with no routes defined.
82       */
83      public Router() {
84          super();
85      }
86  
87      /**
88       * Creates a new router containing the same routes and default route as the
89       * provided router. Changes to the returned router's routing table will not
90       * impact the provided router.
91       *
92       * @param router The router to be copied.
93       */
94      public Router(AbstractRouter<Router, Request, RequestHandler, ApiDescription> router) {
95          super(router);
96      }
97  
98      @Override
99      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 }