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