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, &quot;users&quot;);
064 * RouteMatcher routeTwo = RouteMatchers.requestUriMatcher(EQUALS, &quot;users/{userId}&quot;);
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}