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  package org.forgerock.json.resource.http;
17  
18  import static java.util.concurrent.TimeUnit.MINUTES;
19  import static org.forgerock.api.commons.CommonsApi.COMMONS_API_DESCRIPTION;
20  import static org.wrensecurity.guava.common.base.Optional.absent;
21  import static org.wrensecurity.guava.common.base.Strings.isNullOrEmpty;
22  import static org.forgerock.http.util.Paths.addLeadingSlash;
23  import static org.forgerock.http.util.Paths.removeTrailingSlash;
24  import static org.forgerock.json.resource.Applications.simpleCrestApplication;
25  import static org.forgerock.json.resource.Requests.newApiRequest;
26  import static org.forgerock.json.resource.ResourcePath.resourcePath;
27  import static org.forgerock.json.resource.http.HttpUtils.CONTENT_TYPE_REGEX;
28  import static org.forgerock.json.resource.http.HttpUtils.ETAG_ANY;
29  import static org.forgerock.json.resource.http.HttpUtils.FIELDS_DELIMITER;
30  import static org.forgerock.json.resource.http.HttpUtils.HEADER_IF_MATCH;
31  import static org.forgerock.json.resource.http.HttpUtils.HEADER_IF_MODIFIED_SINCE;
32  import static org.forgerock.json.resource.http.HttpUtils.HEADER_IF_NONE_MATCH;
33  import static org.forgerock.json.resource.http.HttpUtils.HEADER_IF_UNMODIFIED_SINCE;
34  import static org.forgerock.json.resource.http.HttpUtils.MIME_TYPE_APPLICATION_JSON;
35  import static org.forgerock.json.resource.http.HttpUtils.MIME_TYPE_MULTIPART_FORM_DATA;
36  import static org.forgerock.json.resource.http.HttpUtils.PARAM_ACTION;
37  import static org.forgerock.json.resource.http.HttpUtils.PARAM_FIELDS;
38  import static org.forgerock.json.resource.http.HttpUtils.PARAM_MIME_TYPE;
39  import static org.forgerock.json.resource.http.HttpUtils.PARAM_PAGED_RESULTS_COOKIE;
40  import static org.forgerock.json.resource.http.HttpUtils.PARAM_PAGED_RESULTS_OFFSET;
41  import static org.forgerock.json.resource.http.HttpUtils.PARAM_PAGE_SIZE;
42  import static org.forgerock.json.resource.http.HttpUtils.PARAM_PRETTY_PRINT;
43  import static org.forgerock.json.resource.http.HttpUtils.PARAM_QUERY_EXPRESSION;
44  import static org.forgerock.json.resource.http.HttpUtils.PARAM_QUERY_FILTER;
45  import static org.forgerock.json.resource.http.HttpUtils.PARAM_QUERY_ID;
46  import static org.forgerock.json.resource.http.HttpUtils.PARAM_SORT_KEYS;
47  import static org.forgerock.json.resource.http.HttpUtils.PARAM_TOTAL_PAGED_RESULTS_POLICY;
48  import static org.forgerock.json.resource.http.HttpUtils.PROTOCOL_VERSION_1;
49  import static org.forgerock.json.resource.http.HttpUtils.RESTRICTED_HEADER_NAMES;
50  import static org.forgerock.json.resource.http.HttpUtils.SORT_KEYS_DELIMITER;
51  import static org.forgerock.json.resource.http.HttpUtils.asBooleanValue;
52  import static org.forgerock.json.resource.http.HttpUtils.asIntValue;
53  import static org.forgerock.json.resource.http.HttpUtils.asSingleValue;
54  import static org.forgerock.json.resource.http.HttpUtils.determineRequestType;
55  import static org.forgerock.json.resource.http.HttpUtils.fail;
56  import static org.forgerock.json.resource.http.HttpUtils.getIfMatch;
57  import static org.forgerock.json.resource.http.HttpUtils.getIfNoneMatch;
58  import static org.forgerock.json.resource.http.HttpUtils.getJsonActionContent;
59  import static org.forgerock.json.resource.http.HttpUtils.getJsonContent;
60  import static org.forgerock.json.resource.http.HttpUtils.getJsonPatchContent;
61  import static org.forgerock.json.resource.http.HttpUtils.getMethod;
62  import static org.forgerock.json.resource.http.HttpUtils.getParameter;
63  import static org.forgerock.json.resource.http.HttpUtils.getRequestedResourceVersion;
64  import static org.forgerock.json.resource.http.HttpUtils.prepareResponse;
65  import static org.forgerock.json.resource.http.HttpUtils.rejectIfMatch;
66  import static org.forgerock.json.resource.http.HttpUtils.rejectIfNoneMatch;
67  import static org.forgerock.json.resource.http.HttpUtils.staticContextFactory;
68  import static org.forgerock.util.Reject.checkNotNull;
69  import static org.forgerock.util.promise.Promises.newResultPromise;
70  
71  import java.util.ArrayList;
72  import java.util.Collections;
73  import java.util.List;
74  import java.util.Map;
75  import java.util.TreeMap;
76  import java.util.concurrent.CopyOnWriteArrayList;
77  import java.util.concurrent.ExecutionException;
78  
79  import org.forgerock.api.CrestApiProducer;
80  import org.forgerock.api.jackson.PathsModule;
81  import org.forgerock.api.models.ApiDescription;
82  import org.forgerock.api.transform.OpenApiTransformer;
83  import org.wrensecurity.guava.common.base.Optional;
84  import org.wrensecurity.guava.common.cache.CacheBuilder;
85  import org.wrensecurity.guava.common.cache.CacheLoader;
86  import org.wrensecurity.guava.common.cache.LoadingCache;
87  import org.wrensecurity.guava.common.util.concurrent.UncheckedExecutionException;
88  import org.forgerock.http.ApiProducer;
89  import org.forgerock.http.Handler;
90  import org.forgerock.http.header.AcceptLanguageHeader;
91  import org.forgerock.http.header.ContentTypeHeader;
92  import org.forgerock.http.protocol.Form;
93  import org.forgerock.http.protocol.Response;
94  import org.forgerock.http.protocol.Status;
95  import org.forgerock.http.routing.UriRouterContext;
96  import org.forgerock.http.routing.Version;
97  import org.forgerock.http.swagger.SwaggerUtils;
98  import org.forgerock.http.util.Json;
99  import org.forgerock.http.util.Uris;
100 import org.forgerock.json.JsonValue;
101 import org.forgerock.json.resource.ActionRequest;
102 import org.forgerock.json.resource.AdviceContext;
103 import org.forgerock.json.resource.BadRequestException;
104 import org.forgerock.json.resource.ConflictException;
105 import org.forgerock.json.resource.Connection;
106 import org.forgerock.json.resource.ConnectionFactory;
107 import org.forgerock.json.resource.CountPolicy;
108 import org.forgerock.json.resource.CreateRequest;
109 import org.forgerock.json.resource.CrestApplication;
110 import org.forgerock.json.resource.DeleteRequest;
111 import org.forgerock.json.resource.NotSupportedException;
112 import org.forgerock.json.resource.PatchRequest;
113 import org.forgerock.json.resource.PreconditionFailedException;
114 import org.forgerock.json.resource.QueryFilters;
115 import org.forgerock.json.resource.QueryRequest;
116 import org.forgerock.json.resource.ReadRequest;
117 import org.forgerock.json.resource.Request;
118 import org.forgerock.json.resource.RequestType;
119 import org.forgerock.json.resource.Requests;
120 import org.forgerock.json.resource.ResourceException;
121 import org.forgerock.json.resource.ResourcePath;
122 import org.forgerock.json.resource.UpdateRequest;
123 import org.forgerock.services.context.ClientContext;
124 import org.forgerock.services.context.Context;
125 import org.forgerock.services.context.RootContext;
126 import org.forgerock.services.descriptor.Describable;
127 import org.forgerock.util.AsyncFunction;
128 import org.forgerock.util.i18n.PreferredLocales;
129 import org.forgerock.util.promise.NeverThrowsException;
130 import org.forgerock.util.promise.Promise;
131 import org.slf4j.Logger;
132 import org.slf4j.LoggerFactory;
133 
134 import com.fasterxml.jackson.databind.ObjectMapper;
135 import com.fasterxml.jackson.databind.ObjectWriter;
136 
137 import io.swagger.models.Path;
138 import io.swagger.models.Swagger;
139 
140 /**
141  * HTTP adapter from HTTP calls to JSON resource calls. This class can be
142  * used in any {@link org.forgerock.http.Handler}, just create a new instance and override the handle(Context, Request)
143  * method in your HTTP Handler to delegate all those calls to this class's handle(Context, Request)
144  * method.
145  * <p>
146  * For example:
147  *
148  * <pre>
149  * public class TestHandler extends org.forgerock.http.Handler {
150  *     private final  HttpAdapter adapter;
151  *
152  *     public TestHandler() {
153  *         RequestHandler handler = xxx;
154  *         ConnectionFactory connectionFactory =
155  *                 Resources.newInternalConnectionFactory(handler);
156  *         adapter = new HttpAdapter(connectionFactory);
157  *     }
158  *
159  *     protected Promise<Response, ResponseException> handler(Context context,
160  *                 org.forgerock.http.Request req)
161  *             throws ResponseException {
162  *         return adapter.handle(context, req);
163  *     }
164  * }
165  * </pre>
166  *
167  * Note that this adapter does not provide implementations for the HTTP HEAD,
168  * OPTIONS, or TRACE methods. A simpler approach is to use the
169  * {@link CrestHttp} class contained within this package to build HTTP
170  * Handlers since it provides support for these HTTP methods.
171  */
172 final class HttpAdapter implements Handler, Describable<Swagger, org.forgerock.http.protocol.Request>,
173         Describable.Listener {
174 
175     private static final Logger logger = LoggerFactory.getLogger(HttpAdapter.class);
176     private static final ObjectMapper API_OBJECT_MAPPER = new ObjectMapper().registerModules(
177             new Json.LocalizableStringModule(),
178             new Json.JsonValueModule(),
179             new PathsModule());
180 
181     private final ConnectionFactory connectionFactory;
182     private final HttpContextFactory contextFactory;
183     private final String apiId;
184     private final String apiVersion;
185     private final List<Describable.Listener> apiListeners = new CopyOnWriteArrayList<>();
186     private ApiProducer<Swagger> apiProducer;
187     private LoadingCache<String, Optional<Swagger>> descriptorCache;
188 
189     /**
190      * Creates a new HTTP adapter with the provided connection factory and a
191      * context factory the {@link SecurityContextFactory}.
192      *
193      * @param connectionFactory
194      *            The connection factory.
195      * @deprecated Use {@link CrestHttp#newHttpHandler(CrestApplication)} instead.
196      */
197     @Deprecated
198     public HttpAdapter(ConnectionFactory connectionFactory) {
199         this(connectionFactory, (HttpContextFactory) null);
200     }
201 
202     /**
203      * Creates a new HTTP adapter with the provided connection factory and
204      * parent request context.
205      *
206      * @param connectionFactory
207      *            The connection factory.
208      * @param parentContext
209      *            The parent request context which should be used as the parent
210      *            context of each request context.
211      * @deprecated Use {@link CrestHttp#newHttpHandler(CrestApplication, Context)} instead.
212      */
213     @SuppressWarnings("deprecation")
214     @Deprecated
215     public HttpAdapter(ConnectionFactory connectionFactory, final Context parentContext) {
216         this(connectionFactory, staticContextFactory(parentContext));
217     }
218 
219     /**
220      * Creates a new HTTP adapter with the provided connection factory and
221      * context factory.
222      *
223      * @param connectionFactory
224      *            The connection factory.
225      * @param contextFactory
226      *            The context factory which will be used to obtain the parent
227      *            context of each request context, or {@code null} if the
228      *            {@link SecurityContextFactory} should be used.
229      * @deprecated Use {@link #HttpAdapter(CrestApplication, HttpContextFactory)} instead
230      */
231     @SuppressWarnings("deprecation")
232     @Deprecated
233     public HttpAdapter(ConnectionFactory connectionFactory, HttpContextFactory contextFactory) {
234         this(simpleCrestApplication(connectionFactory, null, null), contextFactory);
235     }
236 
237     /**
238      * Creates a new HTTP adapter with the provided connection factory and
239      * context factory.
240      *
241      * @param application
242      *            The CREST application.
243      * @param contextFactory
244      *            The context factory which will be used to obtain the parent
245      *            context of each request context, or {@code null} if the
246      *            {@link SecurityContextFactory} should be used.
247      */
248     @SuppressWarnings("deprecation")
249     public HttpAdapter(CrestApplication application, HttpContextFactory contextFactory) {
250         this.contextFactory = contextFactory != null ? contextFactory : SecurityContextFactory
251                 .getHttpServletContextFactory();
252         this.connectionFactory = checkNotNull(application.getConnectionFactory());
253         this.apiId = application.getApiId();
254         this.apiVersion = application.getApiVersion();
255 
256         try {
257             Optional<Describable<ApiDescription, Request>> describable = getDescribableConnection();
258             if (describable.isPresent()) {
259                 describable.get().addDescriptorListener(this);
260             }
261         } catch (ResourceException e) {
262             logger.warn("Could not create connection", e);
263         }
264 
265     }
266 
267     /**
268      * Handles the incoming HTTP request and converts it to a CREST request.
269      *
270      * @param context {@inheritDoc}
271      * @param request {@inheritDoc}
272      * @return Promise containing a {@code Response} or {@code ResponseException}.
273      */
274     @Override
275     public Promise<Response, NeverThrowsException> handle(Context context,
276             org.forgerock.http.protocol.Request request) {
277         try {
278             RequestType requestType = determineRequestType(request);
279             switch (requestType) {
280             case CREATE:
281                 return doCreate(context, request);
282             case READ:
283                 return doRead(context, request);
284             case UPDATE:
285                 return doUpdate(context, request);
286             case DELETE:
287                 return doDelete(context, request);
288             case PATCH:
289                 return doPatch(context, request);
290             case ACTION:
291                 return doAction(context, request);
292             case QUERY:
293                 return doQuery(context, request);
294             case API:
295                 return doApiRequest(context, request);
296             default:
297                 throw new NotSupportedException("Operation " + requestType + " not supported");
298             }
299         } catch (ResourceException e) {
300             return fail(request, e);
301         }
302     }
303 
304     Promise<Response, NeverThrowsException> doDelete(Context context, org.forgerock.http.protocol.Request req) {
305         try {
306             Version requestedResourceVersion = getRequestedResourceVersion(req);
307 
308             // Prepare response.
309             Response resp = prepareResponse(req);
310 
311             // Validate request.
312             preprocessRequest(req);
313             rejectIfNoneMatch(req);
314 
315             // use the version-1 meaning of getIfMatch; i.e., treat * as null
316             final String ifMatchRevision = getIfMatch(req, PROTOCOL_VERSION_1);
317             final Form parameters = req.getForm();
318             final DeleteRequest request =
319                     Requests.newDeleteRequest(getResourcePath(context, req))
320                             .setRevision(ifMatchRevision)
321                             .setResourceVersion(requestedResourceVersion);
322             for (final Map.Entry<String, List<String>> p : parameters.entrySet()) {
323                 final String name = p.getKey();
324                 final List<String> values = p.getValue();
325                 if (parseCommonParameter(name, values, request)) {
326                     continue;
327                 } else {
328                     request.setAdditionalParameter(name, asSingleValue(name, values));
329                 }
330             }
331             return doRequest(context, req, resp, request);
332         } catch (final Exception e) {
333             return fail(req, e);
334         }
335     }
336 
337     Promise<Response, NeverThrowsException> doRead(Context context, org.forgerock.http.protocol.Request req) {
338         try {
339             Version requestedResourceVersion = getRequestedResourceVersion(req);
340 
341             // Prepare response.
342             Response resp = prepareResponse(req);
343 
344             // Validate request.
345             preprocessRequest(req);
346             rejectIfMatch(req);
347 
348             final Form parameters = req.getForm();
349             // Read of instance within collection or singleton.
350             final String rev = getIfNoneMatch(req);
351             if (ETAG_ANY.equals(rev)) {
352                 // FIXME: i18n
353                 throw new PreconditionFailedException("If-None-Match * not appropriate for "
354                         + getMethod(req) + " requests");
355             }
356 
357             final ReadRequest request = Requests.newReadRequest(getResourcePath(context, req))
358                     .setResourceVersion(requestedResourceVersion);
359             for (final Map.Entry<String, List<String>> p : parameters.entrySet()) {
360                 final String name = p.getKey();
361                 final List<String> values = p.getValue();
362                 if (parseCommonParameter(name, values, request)) {
363                     continue;
364                 } else if (PARAM_MIME_TYPE.equalsIgnoreCase(name)) {
365                     if (values.size() != 1 || values.get(0).split(FIELDS_DELIMITER).length > 1) {
366                         // FIXME: i18n.
367                         throw new BadRequestException("Only one mime type value allowed");
368                     }
369                     if (parameters.get(PARAM_FIELDS).size() != 1) {
370                         // FIXME: i18n.
371                         throw new BadRequestException("The mime type parameter requires only "
372                                 + "1 field to be specified");
373                     }
374                 } else {
375                     request.setAdditionalParameter(name, asSingleValue(name, values));
376                 }
377             }
378             return doRequest(context, req, resp, request);
379         } catch (final Exception e) {
380             return fail(req, e);
381         }
382     }
383 
384     Promise<Response, NeverThrowsException> doQuery(Context context, org.forgerock.http.protocol.Request req) {
385         try {
386             Version requestedResourceVersion = getRequestedResourceVersion(req);
387 
388             // Prepare response.
389             Response resp = prepareResponse(req);
390 
391             // Validate request.
392             preprocessRequest(req);
393             rejectIfMatch(req);
394 
395             final Form parameters = req.getForm();
396             // Additional pre-validation for queries.
397             rejectIfNoneMatch(req);
398 
399             // Query against collection.
400             final QueryRequest request = Requests.newQueryRequest(getResourcePath(context, req))
401                     .setResourceVersion(requestedResourceVersion);
402 
403             for (final Map.Entry<String, List<String>> p : parameters.entrySet()) {
404                 final String name = p.getKey();
405                 final List<String> values = p.getValue();
406 
407                 if (parseCommonParameter(name, values, request)) {
408                     continue;
409                 } else if (name.equalsIgnoreCase(PARAM_SORT_KEYS)) {
410                     for (final String s : values) {
411                         try {
412                             request.addSortKey(s.split(SORT_KEYS_DELIMITER));
413                         } catch (final IllegalArgumentException e) {
414                             // FIXME: i18n.
415                             throw new BadRequestException("The value '" + s
416                                     + "' for parameter '" + name
417                                     + "' could not be parsed as a comma "
418                                     + "separated list of sort keys");
419                         }
420                     }
421                 } else if (name.equalsIgnoreCase(PARAM_QUERY_ID)) {
422                     request.setQueryId(asSingleValue(name, values));
423                 } else if (name.equalsIgnoreCase(PARAM_QUERY_EXPRESSION)) {
424                     request.setQueryExpression(asSingleValue(name, values));
425                 } else if (name.equalsIgnoreCase(PARAM_PAGED_RESULTS_COOKIE)) {
426                     request.setPagedResultsCookie(asSingleValue(name, values));
427                 } else if (name.equalsIgnoreCase(PARAM_PAGED_RESULTS_OFFSET)) {
428                     request.setPagedResultsOffset(asIntValue(name, values));
429                 } else if (name.equalsIgnoreCase(PARAM_PAGE_SIZE)) {
430                     request.setPageSize(asIntValue(name, values));
431                 } else if (name.equalsIgnoreCase(PARAM_QUERY_FILTER)) {
432                     final String s = asSingleValue(name, values);
433                     try {
434                         request.setQueryFilter(QueryFilters.parse(s));
435                     } catch (final IllegalArgumentException e) {
436                         // FIXME: i18n.
437                         throw new BadRequestException("The value '" + s + "' for parameter '"
438                                 + name + "' could not be parsed as a valid query filter");
439                     }
440                 } else if (name.equalsIgnoreCase(PARAM_TOTAL_PAGED_RESULTS_POLICY)) {
441                     final String policy = asSingleValue(name, values);
442 
443                     try {
444                         request.setTotalPagedResultsPolicy(CountPolicy.valueOf(policy.toUpperCase()));
445                     } catch (IllegalArgumentException e) {
446                         // FIXME: i18n.
447                         throw new BadRequestException("The value '" + policy + "' for parameter '"
448                                 + name + "' could not be parsed as a valid count policy");
449                     }
450                 } else {
451                     request.setAdditionalParameter(name, asSingleValue(name, values));
452                 }
453             }
454 
455             // Check for incompatible arguments.
456             if (request.getQueryId() != null && request.getQueryFilter() != null) {
457                 // FIXME: i18n.
458                 throw new BadRequestException("The parameters " + PARAM_QUERY_ID + " and "
459                         + PARAM_QUERY_FILTER + " are mutually exclusive");
460             }
461 
462             if (request.getQueryId() != null && request.getQueryExpression() != null) {
463                 // FIXME: i18n.
464                 throw new BadRequestException("The parameters " + PARAM_QUERY_ID + " and "
465                         + PARAM_QUERY_EXPRESSION + " are mutually exclusive");
466             }
467 
468             if (request.getQueryFilter() != null && request.getQueryExpression() != null) {
469                 // FIXME: i18n.
470                 throw new BadRequestException("The parameters " + PARAM_QUERY_FILTER + " and "
471                         + PARAM_QUERY_EXPRESSION + " are mutually exclusive");
472             }
473 
474             if (request.getPagedResultsOffset() > 0 && request.getPagedResultsCookie() != null) {
475                 // FIXME: i18n.
476                 throw new BadRequestException("The parameters " + PARAM_PAGED_RESULTS_OFFSET + " and "
477                         + PARAM_PAGED_RESULTS_COOKIE + " are mutually exclusive");
478             }
479 
480             return doRequest(context, req, resp, request);
481         } catch (final Exception e) {
482             return fail(req, e);
483         }
484     }
485 
486     Promise<Response, NeverThrowsException> doPatch(Context context, org.forgerock.http.protocol.Request req) {
487         try {
488             Version requestedResourceVersion = getRequestedResourceVersion(req);
489 
490             // Prepare response.
491             Response resp = prepareResponse(req);
492 
493             // Validate request.
494             preprocessRequest(req);
495             if (req.getHeaders().getFirst(HEADER_IF_NONE_MATCH) != null) {
496                 // FIXME: i18n
497                 throw new PreconditionFailedException(
498                         "Use of If-None-Match not supported for PATCH requests");
499             }
500 
501             // use the version 1 meaning of getIfMatch; i.e., treat * as null
502             final String ifMatchRevision = getIfMatch(req, PROTOCOL_VERSION_1);
503             final Form parameters = req.getForm();
504             final PatchRequest request =
505                     Requests.newPatchRequest(getResourcePath(context, req))
506                             .setRevision(ifMatchRevision)
507                             .setResourceVersion(requestedResourceVersion);
508             request.getPatchOperations().addAll(getJsonPatchContent(req));
509             for (final Map.Entry<String, List<String>> p : parameters.entrySet()) {
510                 final String name = p.getKey();
511                 final List<String> values = p.getValue();
512                 if (parseCommonParameter(name, values, request)) {
513                     continue;
514                 } else {
515                     request.setAdditionalParameter(name, asSingleValue(name, values));
516                 }
517             }
518             return doRequest(context, req, resp, request);
519         } catch (final Exception e) {
520             return fail(req, e);
521         }
522     }
523 
524     Promise<Response, NeverThrowsException> doCreate(Context context, org.forgerock.http.protocol.Request req) {
525         try {
526             Version requestedResourceVersion = getRequestedResourceVersion(req);
527             // Prepare response.
528             Response resp = prepareResponse(req);
529 
530             // Validate request.
531             preprocessRequest(req);
532 
533             if ("POST".equals(getMethod(req))) {
534 
535                 rejectIfNoneMatch(req);
536                 rejectIfMatch(req);
537 
538                 final Form parameters = req.getForm();
539                 final JsonValue content = getJsonContent(req);
540                 final CreateRequest request =
541                         Requests.newCreateRequest(getResourcePath(context, req), content)
542                                 .setResourceVersion(requestedResourceVersion);
543                 for (final Map.Entry<String, List<String>> p : parameters.entrySet()) {
544                     final String name = p.getKey();
545                     final List<String> values = p.getValue();
546                     if (parseCommonParameter(name, values, request)) {
547                         continue;
548                     } else if (name.equalsIgnoreCase(PARAM_ACTION)) {
549                         // Ignore - already handled.
550                     } else {
551                         request.setAdditionalParameter(name, asSingleValue(name, values));
552                     }
553                 }
554                 return doRequest(context, req, resp, request);
555             } else {
556 
557                 if (req.getHeaders().getFirst(HEADER_IF_MATCH) != null
558                         && req.getHeaders().getFirst(HEADER_IF_NONE_MATCH) != null) {
559                     // FIXME: i18n
560                     throw new PreconditionFailedException(
561                             "Simultaneous use of If-Match and If-None-Match not supported for PUT requests");
562                 }
563 
564                 final Form parameters = req.getForm();
565                 final JsonValue content = getJsonContent(req);
566 
567                 // This is a create with a user provided resource ID: split the
568                 // path into the parent resource name and resource ID.
569                 final ResourcePath resourcePath = getResourcePath(context, req);
570                 if (resourcePath.isEmpty()) {
571                     // FIXME: i18n.
572                     throw new BadRequestException("No new resource ID in HTTP PUT request");
573                 }
574 
575                 // We have a pathInfo of the form "{container}/{id}"
576                 final CreateRequest request =
577                         Requests.newCreateRequest(resourcePath.parent(), content)
578                                 .setNewResourceId(resourcePath.leaf())
579                                 .setResourceVersion(requestedResourceVersion);
580                 for (final Map.Entry<String, List<String>> p : parameters.entrySet()) {
581                     final String name = p.getKey();
582                     final List<String> values = p.getValue();
583                     if (parseCommonParameter(name, values, request)) {
584                         continue;
585                     } else {
586                         request.setAdditionalParameter(name, asSingleValue(name, values));
587                     }
588                 }
589                 return doRequest(context, req, resp, request);
590             }
591         } catch (final Exception e) {
592             return fail(req, e);
593         }
594     }
595 
596     Promise<Response, NeverThrowsException> doAction(Context context, org.forgerock.http.protocol.Request req) {
597         try {
598             Version requestedResourceVersion = getRequestedResourceVersion(req);
599 
600             // Prepare response.
601             Response resp = prepareResponse(req);
602 
603             // Validate request.
604             preprocessRequest(req);
605             rejectIfNoneMatch(req);
606             rejectIfMatch(req);
607 
608             final Form parameters = req.getForm();
609             final String action = asSingleValue(PARAM_ACTION, getParameter(req, PARAM_ACTION));
610             // Action request.
611             final JsonValue content = getJsonActionContent(req);
612             final ActionRequest request =
613                     Requests.newActionRequest(getResourcePath(context, req), action)
614                             .setContent(content)
615                             .setResourceVersion(requestedResourceVersion);
616             for (final Map.Entry<String, List<String>> p : parameters.entrySet()) {
617                 final String name = p.getKey();
618                 final List<String> values = p.getValue();
619                 if (parseCommonParameter(name, values, request)) {
620                     continue;
621                 } else if (name.equalsIgnoreCase(PARAM_ACTION)) {
622                     // Ignore - already handled.
623                 } else {
624                     request.setAdditionalParameter(name, asSingleValue(name, values));
625                 }
626             }
627             return doRequest(context, req, resp, request);
628         } catch (final Exception e) {
629             return fail(req, e);
630         }
631     }
632 
633     Promise<Response, NeverThrowsException> doUpdate(Context context, org.forgerock.http.protocol.Request req) {
634         try {
635             Version requestedResourceVersion = getRequestedResourceVersion(req);
636 
637             // Prepare response.
638             Response resp = prepareResponse(req);
639 
640             // Validate request.
641             preprocessRequest(req);
642 
643             if (req.getHeaders().getFirst(HEADER_IF_MATCH) != null
644                     && req.getHeaders().getFirst(HEADER_IF_NONE_MATCH) != null) {
645                 // FIXME: i18n
646                 throw new PreconditionFailedException(
647                         "Simultaneous use of If-Match and If-None-Match not supported for PUT requests");
648             }
649 
650             // use the version 1 meaning of getIfMatch; i.e., treat * as null
651             final String ifMatchRevision = getIfMatch(req, PROTOCOL_VERSION_1);
652             final Form parameters = req.getForm();
653             final JsonValue content = getJsonContent(req);
654 
655             final UpdateRequest request =
656                     Requests.newUpdateRequest(getResourcePath(context, req), content)
657                             .setRevision(ifMatchRevision)
658                             .setResourceVersion(requestedResourceVersion);
659             for (final Map.Entry<String, List<String>> p : parameters.entrySet()) {
660                 final String name = p.getKey();
661                 final List<String> values = p.getValue();
662                 if (parseCommonParameter(name, values, request)) {
663                     continue;
664                 } else {
665                     request.setAdditionalParameter(name, asSingleValue(name, values));
666                 }
667             }
668             return doRequest(context, req, resp, request);
669         } catch (final Exception e) {
670             return fail(req, e);
671         }
672     }
673 
674     @SuppressWarnings("unchecked")
675     private Promise<Response, NeverThrowsException> doApiRequest(Context context,
676             final org.forgerock.http.protocol.Request req) {
677         try {
678             Optional<Describable<ApiDescription, Request>> describable = getDescribableConnection();
679             if (!describable.isPresent()) {
680                 throw new NotSupportedException();
681             }
682             Request request = newApiRequest(getResourcePath(context, req));
683             context = prepareRequest(context, req, request);
684             ApiDescription api = describable.get().handleApiRequest(context, request);
685 
686             ObjectWriter writer = Json.makeLocalizingObjectWriter(API_OBJECT_MAPPER, request.getPreferredLocales());
687 
688             // Enable pretty printer if requested.
689             final List<String> values = getParameter(req, PARAM_PRETTY_PRINT);
690             if (values != null) {
691                 if (asBooleanValue(PARAM_PRETTY_PRINT, values)) {
692                     writer = writer.withDefaultPrettyPrinter();
693                 }
694             }
695 
696             return newResultPromise(new Response(Status.OK).setEntity(writer.writeValueAsBytes(api)));
697         } catch (Exception e) {
698             return fail(req, e);
699         }
700     }
701 
702     @SuppressWarnings("unchecked")
703     private Optional<Describable<ApiDescription, Request>> getDescribableConnection()
704             throws ResourceException {
705         if (apiId == null || apiVersion == null) {
706             logger.info("CREST API Descriptor API ID and Version are not set. Not describing.");
707             return absent();
708         }
709         Connection connection = connectionFactory.getConnection();
710         if (connection instanceof Describable) {
711             return Optional.of((Describable<ApiDescription, Request>) connection);
712         } else {
713             return absent();
714         }
715     }
716 
717     private Promise<Response, NeverThrowsException> doRequest(Context context, org.forgerock.http.protocol.Request req,
718             Response resp, Request request) throws Exception {
719         Context ctx = prepareRequest(context, req, request);
720         final RequestRunner runner = new RequestRunner(ctx, request, req, resp);
721         return connectionFactory.getConnectionAsync()
722                 .thenAsync(new AsyncFunction<Connection, Response, NeverThrowsException>() {
723                     @Override
724                     public Promise<Response, NeverThrowsException> apply(Connection connection) {
725                         return runner.handleResult(connection);
726                     }
727                 }, new AsyncFunction<ResourceException, Response, NeverThrowsException>() {
728                     @Override
729                     public Promise<Response, NeverThrowsException> apply(ResourceException error) {
730                         return runner.handleError(error);
731                     }
732                 });
733     }
734 
735     private Context prepareRequest(Context context, org.forgerock.http.protocol.Request req, Request request)
736             throws ResourceException, org.forgerock.http.header.MalformedHeaderException {
737         Context ctx = newRequestContext(context, req);
738         final AcceptLanguageHeader acceptLanguageHeader = req.getHeaders().get(AcceptLanguageHeader.class);
739         request.setPreferredLocales(acceptLanguageHeader != null
740                 ? acceptLanguageHeader.getLocales()
741                 : new PreferredLocales(null));
742         return ctx;
743     }
744 
745     /**
746      * Gets the raw (still url-encoded) resource name from the request. Removes leading and trailing forward slashes.
747      */
748     private ResourcePath getResourcePath(Context context, org.forgerock.http.protocol.Request req)
749             throws ResourceException {
750         try {
751             if (context.containsContext(UriRouterContext.class)) {
752                 ResourcePath reqPath = ResourcePath.valueOf(req.getUri().getRawPath());
753                 return reqPath.subSequence(getMatchedUri(context).size(), reqPath.size());
754             } else {
755                 return ResourcePath.valueOf(req.getUri().getRawPath()); //TODO is this a valid assumption?
756             }
757         } catch (IllegalArgumentException e) {
758             throw new BadRequestException(e.getMessage());
759         }
760     }
761 
762     private ResourcePath getMatchedUri(Context context) {
763         List<ResourcePath> matched = new ArrayList<>();
764         Context ctx = context;
765         while (ctx.containsContext(UriRouterContext.class)) {
766             UriRouterContext uriRouterContext = ctx.asContext(UriRouterContext.class);
767             matched.add(ResourcePath.valueOf(uriRouterContext.getMatchedUri()));
768             ctx = uriRouterContext.getParent();
769         }
770         Collections.reverse(matched);
771         ResourcePath matchedUri = new ResourcePath();
772         for (ResourcePath resourcePath : matched) {
773             matchedUri = matchedUri.concat(resourcePath);
774         }
775         return matchedUri;
776     }
777 
778     private Context newRequestContext(Context context, org.forgerock.http.protocol.Request req)
779             throws ResourceException {
780         final Context parent = contextFactory.createContext(context, req);
781         return new AdviceContext(new HttpContext(parent, req), RESTRICTED_HEADER_NAMES);
782     }
783 
784     private boolean parseCommonParameter(final String name, final List<String> values,
785             final Request request) throws ResourceException {
786         if (name.equalsIgnoreCase(PARAM_FIELDS)) {
787             for (final String s : values) {
788                 try {
789                     request.addField(s.split(","));
790                 } catch (final IllegalArgumentException e) {
791                     // FIXME: i18n.
792                     throw new BadRequestException("The value '" + s + "' for parameter '" + name
793                             + "' could not be parsed as a comma separated list of JSON pointers");
794                 }
795             }
796             return true;
797         } else if (name.equalsIgnoreCase(PARAM_PRETTY_PRINT)) {
798             // This will be handled by the completionHandlerFactory, so just validate.
799             asBooleanValue(name, values);
800             return true;
801         } else {
802             // Unrecognized - must be request specific.
803             return false;
804         }
805     }
806 
807     private void preprocessRequest(org.forgerock.http.protocol.Request req) throws ResourceException {
808         // TODO: check Accept (including charset parameter) and Accept-Charset headers
809 
810         // Check content-type.
811         final String contentType = ContentTypeHeader.valueOf(req).getType();
812         if (!req.getMethod().equalsIgnoreCase(HttpUtils.METHOD_GET)
813                 && contentType != null
814                 && !CONTENT_TYPE_REGEX.matcher(contentType).matches()
815                 && !HttpUtils.isMultiPartRequest(contentType)) {
816             // TODO: i18n
817             throw new BadRequestException(
818                     "The request could not be processed because it specified the content-type '"
819                             + contentType + "' when only the content-type '"
820                             + MIME_TYPE_APPLICATION_JSON + "' and '"
821                             + MIME_TYPE_MULTIPART_FORM_DATA + "' are supported");
822         }
823 
824         if (req.getHeaders().getFirst(HEADER_IF_MODIFIED_SINCE) != null) {
825             // TODO: i18n
826             throw new ConflictException("Header If-Modified-Since not supported");
827         }
828 
829         if (req.getHeaders().getFirst(HEADER_IF_UNMODIFIED_SINCE) != null) {
830             // TODO: i18n
831             throw new ConflictException("Header If-Unmodified-Since not supported");
832         }
833     }
834 
835     @Override
836     public Swagger api(ApiProducer<Swagger> producer) {
837         this.apiProducer = producer;
838         return updateDescriptor();
839     }
840 
841     private Swagger updateDescriptor() {
842         if (apiProducer == null) {
843             // Not yet attached to CHF
844             return null;
845         }
846         try {
847             Optional<Describable<ApiDescription, Request>> describable = getDescribableConnection();
848             if (describable.isPresent()) {
849                 ApiDescription api = describable.get().api(new CrestApiProducer(apiId, apiVersion));
850                 if (api != null) {
851                     this.descriptorCache = CacheBuilder.newBuilder().expireAfterAccess(30, MINUTES)
852                             .build(new CacheLoader<String, Optional<Swagger>>() {
853                                 @Override
854                                 public Optional<Swagger> load(String uri) throws ResourceException {
855                                     UriRouterContext context = new UriRouterContext(new RootContext(), "", uri,
856                                             Collections.<String, String>emptyMap());
857                                     ApiDescription api = getDescribableConnection().get()
858                                             .handleApiRequest(context, newApiRequest(resourcePath(uri)));
859                                     // Avoid NPE later during transformation
860                                     if (api == null) {
861                                         return absent();
862                                     }
863                                     Swagger swagger = OpenApiTransformer.execute(api, COMMONS_API_DESCRIPTION);
864                                     uri = removeTrailingSlash(uri);
865                                     if (!isNullOrEmpty(uri)) {
866                                         uri = addLeadingSlash(Uris.urlDecodePathElement(uri));
867                                     }
868                                     Map<String, Path> paths = new TreeMap<>();
869                                     for (Map.Entry<String, Path> path : swagger.getPaths().entrySet()) {
870                                         String pathString = path.getKey();
871                                         // A path from Swagger will always start with a slash.
872                                         // Remove leading slash from only if it is also the end of the path
873                                         if ((pathString.startsWith("/#") || pathString.equals("/")) && !uri.isEmpty()) {
874                                             pathString = pathString.substring(1);
875                                         }
876                                         paths.put(uri + pathString, path.getValue());
877                                     }
878                                     swagger.setPaths(paths);
879                                     return Optional.of(apiProducer.addApiInfo(swagger));
880                                 }
881                             });
882                     try {
883                         return descriptorCache.get("").orNull();
884                     } catch (ExecutionException e) {
885                         throw (ResourceException) e.getCause();
886                     }
887                 }
888             }
889         } catch (ResourceException e) {
890             throw new IllegalStateException("Cannot get connection", e);
891         }
892         return null;
893     }
894 
895     @Override
896     public Swagger handleApiRequest(Context context, org.forgerock.http.protocol.Request request) {
897         if (descriptorCache == null) {
898             return null;
899         }
900         Optional<Swagger> result;
901         try {
902             if (context.containsContext(UriRouterContext.class)) {
903                 result = descriptorCache.get(context.asContext(UriRouterContext.class).getRemainingUri());
904             } else {
905                 result = descriptorCache.get("");
906             }
907         } catch (ExecutionException e) {
908             throw new UnsupportedOperationException("Cannot get connection", e);
909         } catch (UncheckedExecutionException e) {
910             throw (RuntimeException) e.getCause();
911         }
912         Swagger descriptor = result.orNull();
913         if (descriptor != null && descriptor.getHost() == null) {
914             return SwaggerUtils.clone(descriptor).host(context.asContext(ClientContext.class).getLocalAddress());
915         }
916         return descriptor;
917     }
918 
919     @Override
920     public void addDescriptorListener(Listener listener) {
921         apiListeners.add(listener);
922     }
923 
924     @Override
925     public void removeDescriptorListener(Listener listener) {
926         apiListeners.remove(listener);
927     }
928 
929     @Override
930     public void notifyDescriptorChange() {
931         updateDescriptor();
932         for (Listener listener : apiListeners) {
933             listener.notifyDescriptorChange();
934         }
935     }
936 }