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 2012-2016 ForgeRock AS.
15   */
16  
17  package org.forgerock.json.resource;
18  
19  import static org.forgerock.api.models.Parameter.*;
20  import static org.forgerock.api.models.Resource.AnnotatedTypeVariant.*;
21  import static org.forgerock.api.models.SubResources.*;
22  import static org.forgerock.http.routing.RoutingMode.*;
23  import static org.forgerock.json.resource.Responses.*;
24  import static org.forgerock.json.resource.RouteMatchers.*;
25  import static org.forgerock.util.promise.Promises.*;
26  
27  import java.lang.reflect.InvocationTargetException;
28  import java.lang.reflect.Method;
29  import java.util.Collection;
30  import java.util.LinkedHashMap;
31  import java.util.Map;
32  
33  import org.forgerock.api.annotations.CollectionProvider;
34  import org.forgerock.api.annotations.Path;
35  import org.forgerock.api.annotations.SingletonProvider;
36  import org.forgerock.api.enums.ParameterSource;
37  import org.forgerock.api.models.ApiDescription;
38  import org.forgerock.api.models.Items;
39  import org.forgerock.api.models.Parameter;
40  import org.forgerock.api.models.Resource;
41  import org.forgerock.api.models.SubResources;
42  import org.forgerock.http.ApiProducer;
43  import org.forgerock.http.routing.UriRouterContext;
44  import org.forgerock.json.JsonPointer;
45  import org.forgerock.json.JsonValue;
46  import org.forgerock.services.context.AbstractContext;
47  import org.forgerock.services.context.Context;
48  import org.forgerock.services.descriptor.Describable;
49  import org.forgerock.util.Reject;
50  import org.forgerock.util.promise.Promise;
51  
52  /**
53   * This class contains methods for creating and manipulating connection
54   * factories and connections.
55   */
56  public final class Resources {
57  
58      private static final class InternalConnectionFactory implements ConnectionFactory {
59          private final RequestHandler handler;
60  
61          private InternalConnectionFactory(final RequestHandler handler) {
62              this.handler = handler;
63          }
64  
65          @Override
66          public void close() {
67              // Do nothing.
68          }
69  
70          @Override
71          public Connection getConnection() {
72              return newInternalConnection(handler);
73          }
74  
75          public Promise<Connection, ResourceException> getConnectionAsync() {
76              return newSuccessfulPromise(getConnection());
77          }
78      }
79  
80      /**
81       * Adapts the provided {@link SynchronousRequestHandler} as a
82       * {@link RequestHandler}.
83       *
84       * @param syncHandler
85       *            The synchronous request handler to be adapted.
86       * @return The adapted synchronous request handler.
87       */
88      public static RequestHandler asRequestHandler(final SynchronousRequestHandler syncHandler) {
89          return syncHandler instanceof Describable
90                  ? new DescribedSyncRequestHandlerAdapter(syncHandler)
91                  : new SynchronousRequestHandlerAdapter(syncHandler);
92      }
93  
94      /**
95       * Returns a JSON object containing only the specified fields from the
96       * provided JSON value. If the list of fields is empty then the value is
97       * returned unchanged.
98       * <p>
99       * <b>NOTE:</b> this method only performs a shallow copy of extracted
100      * fields, so changes to the filtered JSON value may impact the original
101      * JSON value, and vice-versa.
102      *
103      * @param resource
104      *            The JSON value whose fields are to be filtered.
105      * @param fields
106      *            The list of fields to be extracted.
107      * @return The filtered JSON value.
108      */
109     public static JsonValue filterResource(final JsonValue resource,
110             final Collection<JsonPointer> fields) {
111         if (fields.isEmpty() || resource.isNull() || resource.size() == 0) {
112             return resource;
113         } else {
114             final Map<String, Object> filtered = new LinkedHashMap<>(fields.size());
115             for (final JsonPointer field : fields) {
116                 if (field.isEmpty()) {
117                     // Special case - copy resource fields (assumes Map).
118                     filtered.putAll(resource.asMap());
119                 } else {
120                     // FIXME: what should we do if the field refers to an array element?
121                     final JsonValue value = resource.get(field);
122                     if (value != null) {
123                         final String key = field.leaf();
124                         filtered.put(key, value.getObject());
125                     }
126                 }
127             }
128             return new JsonValue(filtered);
129         }
130     }
131 
132     /**
133      * Returns a JSON object containing only the specified fields from the
134      * provided resource. If the list of fields is empty then the resource is
135      * returned unchanged.
136      * <p>
137      * <b>NOTE:</b> this method only performs a shallow copy of extracted
138      * fields, so changes to the filtered resource may impact the original
139      * resource, and vice-versa.
140      *
141      * @param resource
142      *            The resource whose fields are to be filtered.
143      * @param fields
144      *            The list of fields to be extracted.
145      * @return The filtered resource.
146      */
147     public static ResourceResponse filterResource(final ResourceResponse resource,
148             final Collection<JsonPointer> fields) {
149         final JsonValue unfiltered = resource.getContent();
150         final Collection<JsonPointer> filterFields = resource.hasFields()
151                 ? resource.getFields()
152                 : fields;
153         final JsonValue filtered = filterResource(unfiltered, filterFields);
154         if (filtered == unfiltered) {
155             return resource; // Unchanged.
156         } else {
157             return newResourceResponse(resource.getId(), resource.getRevision(), filtered);
158         }
159     }
160 
161     /**
162      * Creates a new connection to a {@link RequestHandler}.
163      *
164      * @param handler
165      *            The request handler to which client requests should be
166      *            forwarded.
167      * @return The new internal connection.
168      * @throws NullPointerException
169      *             If {@code handler} was {@code null}.
170      */
171     public static Connection newInternalConnection(final RequestHandler handler) {
172         return new InternalConnection(handler);
173     }
174 
175     /**
176      * Creates a new connection factory which binds internal client connections
177      * to {@link RequestHandler}s.
178      *
179      * @param handler
180      *            The request handler to which client requests should be
181      *            forwarded.
182      * @return The new internal connection factory.
183      * @throws NullPointerException
184      *             If {@code handler} was {@code null}.
185      */
186     public static ConnectionFactory newInternalConnectionFactory(final RequestHandler handler) {
187         return new InternalConnectionFactory(handler);
188     }
189 
190     /**
191      * Creates a new {@link RequestHandler} backed by the supplied provider. The provider can be an instance of an
192      * interface resource handler, and annotated resource handler or an annotated request handler. The type of the
193      * provider will be determined from the {@code org.forgerock.api.annotations.RequestHandler#variant}
194      * annotation property, or from the type of interface implemented.
195      * <p>
196      * Sub-paths that are declared using the {@code org.forgerock.api.annotations.Path} annotation will also be routed
197      * through the returned handler.
198      * <p>
199      * This method uses the same logic as {@link #newCollection(Object)}, {@link #newSingleton(Object)} and
200      * {@link #newAnnotatedRequestHandler(Object)} to create the underlying {@link RequestHandler}s.
201      * @param provider The provider instance.
202      * @return The constructed handler.
203      */
204     public static RequestHandler newHandler(Object provider) {
205         Router router;
206         if (provider instanceof Describable) {
207             router = new Router();
208             addHandlers(provider, router, "", null);
209         } else {
210             final DescribableResourceHandler descriptorProvider = new DescribableResourceHandler();
211             router = new Router() {
212                 @Override
213                 protected ApiDescription buildApi(ApiProducer<ApiDescription> producer) {
214                     return descriptorProvider.api(producer);
215                 }
216             };
217             descriptorProvider.describes(addHandlers(provider, router, "",
218                     descriptorProvider.getDefinitionDescriptions()));
219         }
220         Path path = provider.getClass().getAnnotation(Path.class);
221         if (path != null) {
222             Router pathRouter = new Router();
223             pathRouter.addRoute(requestUriMatcher(STARTS_WITH, path.value()), router);
224             router = pathRouter;
225         }
226         return router;
227     }
228 
229     private static Resource addHandlers(Object provider, Router router, String basePath,
230             ApiDescription definitions, Parameter... pathParameters) {
231         HandlerVariant variant = deduceHandlerVariant(provider);
232         Parameter[] nextPathParameters = pathParameters;
233         switch (variant) {
234         case SINGLETON_RESOURCE:
235             addSingletonHandlerToRouter(provider, router, basePath);
236             break;
237         case COLLECTION_RESOURCE:
238             nextPathParameters = new Parameter[pathParameters.length + 1];
239             System.arraycopy(pathParameters, 0, nextPathParameters, 0, pathParameters.length);
240             nextPathParameters[pathParameters.length] = addCollectionHandlersToRouter(provider, router, basePath);
241             String pathParameter = nextPathParameters[pathParameters.length].getName();
242             basePath = (basePath.isEmpty() ? "" : basePath + "/") + "{" + pathParameter + "}";
243             break;
244         case REQUEST_HANDLER:
245             addRequestHandlerToRouter(provider, router, basePath);
246             break;
247         default:
248             return null;
249         }
250 
251         SubResources.Builder subResourcesBuilder = null;
252         for (Method m : provider.getClass().getMethods()) {
253             Path subpathAnnotation = m.getAnnotation(Path.class);
254             if (subpathAnnotation != null) {
255                 if (subResourcesBuilder == null) {
256                     subResourcesBuilder = subresources();
257                 }
258                 String subpath = subpathAnnotation.value().replaceAll("^/", "");
259                 try {
260                     Resource subResource = addHandlers(m.invoke(provider), router,
261                             basePath.isEmpty() ? subpath : basePath + "/" + subpath, definitions, nextPathParameters);
262                     if (subResource != null) {
263                         subResourcesBuilder.put(subpath, subResource);
264                     }
265                 } catch (IllegalAccessException | InvocationTargetException e) {
266                     throw new IllegalArgumentException("Could not construct handler tree", e);
267                 }
268             }
269         }
270         SubResources subResources = subResourcesBuilder == null ? null : subResourcesBuilder.build();
271         return makeDescriptor(provider, variant, subResources, definitions, pathParameters);
272     }
273 
274     private static HandlerVariant deduceHandlerVariant(Object provider) {
275         Class<?> type = provider.getClass();
276         org.forgerock.api.annotations.RequestHandler handler =
277                 provider.getClass().getAnnotation(org.forgerock.api.annotations.RequestHandler.class);
278         if (handler != null || provider instanceof RequestHandler) {
279             return HandlerVariant.REQUEST_HANDLER;
280         } else if (type.getAnnotation(SingletonProvider.class) != null
281                 || provider instanceof SingletonResourceProvider) {
282             return HandlerVariant.SINGLETON_RESOURCE;
283         } else if (type.getAnnotation(CollectionProvider.class) != null
284                 || provider instanceof CollectionResourceProvider) {
285             return HandlerVariant.COLLECTION_RESOURCE;
286         }
287         throw new IllegalArgumentException("Cannot deduce provider variant" + provider.getClass());
288     }
289 
290     private static void addRequestHandlerToRouter(Object provider, Router router, String basePath) {
291         router.addRoute(requestUriMatcher(STARTS_WITH, basePath), new AnnotatedRequestHandler(provider));
292     }
293 
294     private static Parameter addCollectionHandlersToRouter(Object provider, Router router, String basePath) {
295         boolean fromInterface = provider instanceof CollectionResourceProvider;
296         // Create a route for the collection.
297         final RequestHandler collectionHandler = fromInterface
298                 ? new InterfaceCollectionHandler((CollectionResourceProvider) provider)
299                 : new AnnotatedCollectionHandler(provider);
300         router.addRoute(requestUriMatcher(EQUALS, basePath), collectionHandler);
301 
302         // Create a route for the instances within the collection.
303         RequestHandler instanceHandler = fromInterface
304                 ? new InterfaceCollectionInstance((CollectionResourceProvider) provider)
305                 : new AnnotationCollectionInstance(provider);
306         Class<?> providerClass = provider.getClass();
307         CollectionProvider providerAnnotation = providerClass.getAnnotation(CollectionProvider.class);
308         Parameter pathParameter;
309         if (providerAnnotation != null) {
310             pathParameter = Parameter.fromAnnotation(providerClass, providerAnnotation.pathParam());
311         } else {
312             pathParameter = parameter().name("id").type("string").source(ParameterSource.PATH).required(true).build();
313         }
314         String pathParam = pathParameter.getName();
315         instanceHandler = new FilterChain(instanceHandler, new CollectionInstanceIdContextFilter(pathParam));
316         router.addRoute(requestUriMatcher(EQUALS, (basePath.isEmpty() ? "" : basePath + "/") + "{" + pathParam + "}"),
317                 instanceHandler);
318         return pathParameter;
319     }
320 
321     private static void addSingletonHandlerToRouter(Object provider, Router router, String basePath) {
322         router.addRoute(requestUriMatcher(EQUALS, basePath),
323                 provider instanceof SingletonResourceProvider
324                         ? new InterfaceSingletonHandler((SingletonResourceProvider) provider)
325                         : new AnnotatedSingletonHandler(provider));
326     }
327 
328     private static Resource makeDescriptor(Object provider, HandlerVariant variant, SubResources subResources,
329             ApiDescription definitions, Parameter[] pathParameters) {
330         if (provider instanceof Describable) {
331             return null;
332         }
333         Class<?> type = provider.getClass();
334         switch (variant) {
335         case SINGLETON_RESOURCE:
336             return Resource.fromAnnotatedType(type, SINGLETON_RESOURCE, subResources, definitions, pathParameters);
337         case COLLECTION_RESOURCE:
338             final Items items = Items.fromAnnotatedType(type, definitions, subResources);
339             return Resource.fromAnnotatedType(type, COLLECTION_RESOURCE_COLLECTION, items,
340                     definitions, pathParameters);
341         case REQUEST_HANDLER:
342             return Resource.fromAnnotatedType(type, REQUEST_HANDLER, subResources, definitions, pathParameters);
343         default:
344             return null;
345         }
346     }
347 
348     /**
349      * Returns a new request handler which will forward requests on to the
350      * provided collection resource provider. Incoming requests which are not
351      * appropriate for a resource collection or resource instance will result in
352      * a bad request error being returned to the client.
353      *
354      * @param provider
355      *            The collection resource provider. Either an implementation of {@link CollectionResourceProvider} or
356      *            a POJO annotated with annotations from {@link org.forgerock.json.resource.annotations}.
357      * @return A new request handler which will forward requests on to the
358      *         provided collection resource provider.
359      * @deprecated Use {@link #newHandler(Object)} instead.
360      */
361     @Deprecated
362     public static RequestHandler newCollection(final Object provider) {
363         return newHandler(provider);
364     }
365 
366     /**
367      * Returns a new request handler which will forward requests on to the
368      * provided singleton resource provider. Incoming requests which are not
369      * appropriate for a singleton resource (e.g. query) will result in a bad
370      * request error being returned to the client.
371      *
372      * @param provider
373      *            The singleton resource provider. Either an implementation of {@link SingletonResourceProvider} or
374      *            a POJO annotated with annotations from {@link org.forgerock.json.resource.annotations}.
375      * @return A new request handler which will forward requests on to the
376      *         provided singleton resource provider.
377      * @deprecated Use {@link #newHandler(Object)} instead.
378      */
379     @Deprecated
380     public static RequestHandler newSingleton(final Object provider) {
381         return newHandler(provider);
382     }
383 
384     /**
385      * Returns a new request handler which will forward requests on to the
386      * provided annotated request handler.
387      *
388      * @param provider
389      *            The POJO annotated with annotations from {@link org.forgerock.json.resource.annotations}.
390      * @return A new request handler which will forward requests on to the provided annotated POJO.
391      * @deprecated Use {@link #newHandler(Object)} instead.
392      */
393     @Deprecated
394     public static RequestHandler newAnnotatedRequestHandler(final Object provider) {
395         Reject.ifTrue(provider instanceof RequestHandler,
396                 "Refusing to create an annotated request handler using a provider that implements RequestHandler. "
397                         + "Use the RequestHandler implementation directly instead");
398         return newHandler(provider);
399     }
400 
401     /**
402      * Returns an uncloseable view of the provided connection. Attempts to call
403      * {@link Connection#close()} will be ignored.
404      *
405      * @param connection
406      *            The connection whose {@code close} method is to be disabled.
407      * @return An uncloseable view of the provided connection.
408      */
409     public static Connection uncloseable(final Connection connection) {
410         return new AbstractConnectionWrapper<Connection>(connection) {
411             @Override
412             public void close() {
413                 // Do nothing.
414             }
415         };
416     }
417 
418     /**
419      * Returns an uncloseable view of the provided connection factory. Attempts
420      * to call {@link ConnectionFactory#close()} will be ignored.
421      *
422      * @param factory
423      *            The connection factory whose {@code close} method is to be
424      *            disabled.
425      * @return An uncloseable view of the provided connection factory.
426      */
427     public static ConnectionFactory uncloseable(final ConnectionFactory factory) {
428         return new ConnectionFactory() {
429 
430             @Override
431             public Promise<Connection, ResourceException> getConnectionAsync() {
432                 return factory.getConnectionAsync();
433             }
434 
435             @Override
436             public Connection getConnection() throws ResourceException {
437                 return factory.getConnection();
438             }
439 
440             @Override
441             public void close() {
442                 // Do nothing.
443             }
444         };
445     }
446 
447     static String idOf(final Context context) {
448         String idFieldName = context.asContext(IdFieldContext.class).getIdFieldName();
449         return context.asContext(UriRouterContext.class).getUriTemplateVariables().get(idFieldName);
450     }
451 
452     static ResourceException newBadRequestException(final String fs, final Object... args) {
453         final String msg = String.format(fs, args);
454         return new BadRequestException(msg);
455     }
456 
457     private static <V> Promise<V, ResourceException> newSuccessfulPromise(V result) {
458         return newResultPromise(result);
459     }
460 
461     // Strips off the unwanted leaf routing context which was added when routing
462     // requests to a collection.
463     static Context parentOf(Context context) {
464         if (context instanceof IdFieldContext) {
465             context = context.getParent();
466         }
467         assert context instanceof UriRouterContext;
468         return context.getParent();
469     }
470 
471     private static class IdFieldContext extends AbstractContext {
472 
473         private static final String ID_FIELD_NAME = "IdFieldName";
474 
475         protected IdFieldContext(Context parent, String idFieldName) {
476             super(parent, "IdField");
477             this.data.put(ID_FIELD_NAME, idFieldName);
478         }
479 
480         protected IdFieldContext(JsonValue data, ClassLoader loader) {
481             super(data, loader);
482         }
483 
484         String getIdFieldName() {
485             return this.data.get(ID_FIELD_NAME).asString();
486         }
487     }
488 
489     private static final class CollectionInstanceIdContextFilter implements Filter {
490 
491         private final String idFieldName;
492 
493         private CollectionInstanceIdContextFilter(String idFieldName) {
494             this.idFieldName = idFieldName;
495         }
496 
497         @Override
498         public Promise<ActionResponse, ResourceException> filterAction(Context context, ActionRequest request,
499                 RequestHandler next) {
500             return next.handleAction(new IdFieldContext(context, idFieldName), request);
501         }
502 
503         @Override
504         public Promise<ResourceResponse, ResourceException> filterCreate(Context context, CreateRequest request,
505                 RequestHandler next) {
506             return next.handleCreate(new IdFieldContext(context, idFieldName), request);
507         }
508 
509         @Override
510         public Promise<ResourceResponse, ResourceException> filterDelete(Context context, DeleteRequest request,
511                 RequestHandler next) {
512             return next.handleDelete(new IdFieldContext(context, idFieldName), request);
513         }
514 
515         @Override
516         public Promise<ResourceResponse, ResourceException> filterPatch(Context context, PatchRequest request,
517                 RequestHandler next) {
518             return next.handlePatch(new IdFieldContext(context, idFieldName), request);
519         }
520 
521         @Override
522         public Promise<QueryResponse, ResourceException> filterQuery(Context context, QueryRequest request,
523                 QueryResourceHandler handler, RequestHandler next) {
524             return next.handleQuery(new IdFieldContext(context, idFieldName), request, handler);
525         }
526 
527         @Override
528         public Promise<ResourceResponse, ResourceException> filterRead(Context context, ReadRequest request,
529                 RequestHandler next) {
530             return next.handleRead(new IdFieldContext(context, idFieldName), request);
531         }
532 
533         @Override
534         public Promise<ResourceResponse, ResourceException> filterUpdate(Context context, UpdateRequest request,
535                 RequestHandler next) {
536             return next.handleUpdate(new IdFieldContext(context, idFieldName), request);
537         }
538     }
539 
540     /**
541      * Enumeration of the possible CREST handler variants.
542      */
543     enum HandlerVariant {
544         /** A singleton resource handler. */
545         SINGLETON_RESOURCE,
546         /** A collection resource handler. */
547         COLLECTION_RESOURCE,
548         /** A plain request handler. */
549         REQUEST_HANDLER
550     }
551 
552     // Prevent instantiation.
553     private Resources() {
554         // Nothing to do.
555     }
556 }