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.http;
18  
19  import static org.forgerock.http.protocol.Responses.newInternalServerError;
20  import static org.forgerock.http.routing.Version.version;
21  import static org.forgerock.json.resource.ActionRequest.ACTION_ID_CREATE;
22  import static org.forgerock.util.Utils.closeSilently;
23  import static org.forgerock.util.promise.Promises.newResultPromise;
24  
25  import java.io.ByteArrayOutputStream;
26  import java.io.IOException;
27  import java.io.InputStream;
28  import java.io.OutputStream;
29  import java.util.ArrayDeque;
30  import java.util.Arrays;
31  import java.util.Collection;
32  import java.util.Iterator;
33  import java.util.LinkedHashMap;
34  import java.util.List;
35  import java.util.Map;
36  import java.util.regex.Matcher;
37  import java.util.regex.Pattern;
38  
39  import javax.activation.DataSource;
40  import javax.mail.BodyPart;
41  import javax.mail.MessagingException;
42  import javax.mail.internet.ContentDisposition;
43  import javax.mail.internet.ContentType;
44  import javax.mail.internet.MimeBodyPart;
45  import javax.mail.internet.MimeMultipart;
46  import javax.mail.internet.ParseException;
47  
48  import org.forgerock.http.header.AcceptApiVersionHeader;
49  import org.forgerock.http.header.ContentTypeHeader;
50  import org.forgerock.http.header.MalformedHeaderException;
51  import org.forgerock.http.io.PipeBufferedStream;
52  import org.forgerock.http.protocol.Response;
53  import org.forgerock.http.protocol.Status;
54  import org.forgerock.http.routing.Version;
55  import org.forgerock.http.util.Json;
56  import org.forgerock.json.JsonValue;
57  import org.forgerock.json.resource.ActionRequest;
58  import org.forgerock.json.resource.BadRequestException;
59  import org.forgerock.json.resource.InternalServerErrorException;
60  import org.forgerock.json.resource.NotSupportedException;
61  import org.forgerock.json.resource.PatchOperation;
62  import org.forgerock.json.resource.PreconditionFailedException;
63  import org.forgerock.json.resource.QueryRequest;
64  import org.forgerock.json.resource.Request;
65  import org.forgerock.json.resource.RequestType;
66  import org.forgerock.json.resource.ResourceException;
67  import org.forgerock.services.context.Context;
68  import org.forgerock.util.encode.Base64url;
69  import org.forgerock.util.promise.NeverThrowsException;
70  import org.forgerock.util.promise.Promise;
71  
72  import com.fasterxml.jackson.core.JsonGenerator;
73  import com.fasterxml.jackson.core.JsonParseException;
74  import com.fasterxml.jackson.core.JsonParser;
75  import com.fasterxml.jackson.databind.JsonMappingException;
76  import com.fasterxml.jackson.databind.ObjectMapper;
77  
78  /**
79   * HTTP utility methods and constants.
80   */
81  public final class HttpUtils {
82      static final String CACHE_CONTROL = "no-cache";
83      static final String CHARACTER_ENCODING = "UTF-8";
84      static final Pattern CONTENT_TYPE_REGEX = Pattern.compile(
85              "^application/json([ ]*;[ ]*charset=utf-8)?$", Pattern.CASE_INSENSITIVE);
86      static final String CRLF = "\r\n";
87      static final String ETAG_ANY = "*";
88  
89      static final String MIME_TYPE_APPLICATION_JSON = "application/json";
90      static final String MIME_TYPE_MULTIPART_FORM_DATA = "multipart/form-data";
91      static final String MIME_TYPE_TEXT_PLAIN = "text/plain";
92  
93      static final String HEADER_CACHE_CONTROL = "Cache-Control";
94      static final String HEADER_ETAG = "ETag";
95      static final String HEADER_IF_MATCH = "If-Match";
96      static final String HEADER_IF_NONE_MATCH = "If-None-Match";
97      static final String HEADER_IF_MODIFIED_SINCE = "If-Modified-Since";
98      static final String HEADER_IF_UNMODIFIED_SINCE = "If-Unmodified-Since";
99      static final String HEADER_LOCATION = "Location";
100     static final String HEADER_X_HTTP_METHOD_OVERRIDE = "X-HTTP-Method-Override";
101     /** the HTTP header for {@literal Content-Disposition}. */
102     public static final String CONTENT_DISPOSITION = "Content-Disposition";
103     static final Collection<String> RESTRICTED_HEADER_NAMES = Arrays.asList(
104             ContentTypeHeader.NAME,
105             AcceptApiVersionHeader.NAME,
106             HEADER_IF_MODIFIED_SINCE,
107             HEADER_IF_UNMODIFIED_SINCE,
108             HEADER_IF_MATCH,
109             HEADER_IF_NONE_MATCH,
110             HEADER_CACHE_CONTROL,
111             HEADER_ETAG,
112             HEADER_LOCATION,
113             HEADER_X_HTTP_METHOD_OVERRIDE,
114             CONTENT_DISPOSITION
115     );
116 
117     static final String METHOD_DELETE = "DELETE";
118     static final String METHOD_GET = "GET";
119     static final String METHOD_HEAD = "HEAD";
120     static final String METHOD_OPTIONS = "OPTIONS";
121     static final String METHOD_PATCH = "PATCH";
122     static final String METHOD_POST = "POST";
123     static final String METHOD_PUT = "PUT";
124     static final String METHOD_TRACE = "TRACE";
125 
126     /** the HTTP request parameter for an action. */
127     public static final String PARAM_ACTION = param(ActionRequest.FIELD_ACTION);
128     /** the HTTP request parameter to specify which fields to return. */
129     public static final String PARAM_FIELDS = param(Request.FIELD_FIELDS);
130     /** the HTTP request parameter to request a certain mimetype for a filed. */
131     public static final String PARAM_MIME_TYPE = param("mimeType");
132     /** the HTTP request parameter to request a certain page size. */
133     public static final String PARAM_PAGE_SIZE = param(QueryRequest.FIELD_PAGE_SIZE);
134     /** the HTTP request parameter to specify a paged results cookie. */
135     public static final String PARAM_PAGED_RESULTS_COOKIE =
136             param(QueryRequest.FIELD_PAGED_RESULTS_COOKIE);
137     /** the HTTP request parameter to specify a paged results offset. */
138     public static final String PARAM_PAGED_RESULTS_OFFSET =
139             param(QueryRequest.FIELD_PAGED_RESULTS_OFFSET);
140     /** the HTTP request parameter to request pretty printing. */
141     public static final String PARAM_PRETTY_PRINT = "_prettyPrint";
142     /** the HTTP request parameter to specify a query expression. */
143     public static final String PARAM_QUERY_EXPRESSION = param(QueryRequest.FIELD_QUERY_EXPRESSION);
144     /** the HTTP request parameter to specify a query filter. */
145     public static final String PARAM_QUERY_FILTER = param(QueryRequest.FIELD_QUERY_FILTER);
146     /** the HTTP request parameter to specify a query id. */
147     public static final String PARAM_QUERY_ID = param(QueryRequest.FIELD_QUERY_ID);
148     /** the HTTP request parameter to specify the sort keys. */
149     public static final String PARAM_SORT_KEYS = param(QueryRequest.FIELD_SORT_KEYS);
150     /** The policy used for counting total paged results. */
151     public static final String PARAM_TOTAL_PAGED_RESULTS_POLICY = param(QueryRequest.FIELD_TOTAL_PAGED_RESULTS_POLICY);
152     /** Request the CREST API Descriptor. */
153     public static final String PARAM_CREST_API = param("crestapi");
154 
155     /** Protocol Version 1. */
156     public static final Version PROTOCOL_VERSION_1 = version(1);
157     /** Protocol Version 2 - supports upsert on PUT. */
158     public static final Version PROTOCOL_VERSION_2 = version(2);
159     /**
160      * Protocol Version 2.1 - supports defacto standard for create requests when the ID of the created resource is
161      * to be allocated by the server, which are represented as a POST to the collection endpoint without an
162      * {@code _action} query parameter.
163      */
164     public static final Version PROTOCOL_VERSION_2_1 = version(2, 1);
165     /** The default version of the named protocol. */
166     public static final Version DEFAULT_PROTOCOL_VERSION = PROTOCOL_VERSION_2_1;
167     static final String FIELDS_DELIMITER = ",";
168     static final String SORT_KEYS_DELIMITER = ",";
169 
170     static final ObjectMapper JSON_MAPPER = new ObjectMapper()
171             .registerModules(new Json.JsonValueModule(), new Json.LocalizableStringModule());
172 
173     private static final String FILENAME = "filename";
174     private static final String MIME_TYPE = "mimetype";
175     private static final String CONTENT = "content";
176     private static final String NAME = "name";
177     private static final Pattern MULTIPART_FIELD_REGEX = Pattern.compile("^cid:(.*)#(" + FILENAME
178             + "|" + MIME_TYPE + "|" + CONTENT + ")$", Pattern.CASE_INSENSITIVE);
179     private static final int PART_NAME = 1;
180     private static final int PART_DATA_TYPE = 2;
181     private static final String REFERENCE_TAG = "$ref";
182 
183     private static final int BUFFER_SIZE = 1_024;
184     private static final int EOF = -1;
185 
186     /**
187      * Adapts an {@code Exception} to a {@code ResourceException}.
188      *
189      * @param t
190      *            The exception which caused the request to fail.
191      * @return The equivalent resource exception.
192      */
193     static ResourceException adapt(final Throwable t) {
194         if (t instanceof ResourceException) {
195             return (ResourceException) t;
196         } else {
197             return new InternalServerErrorException(t);
198         }
199     }
200 
201     /**
202      * Parses a header or request parameter as a boolean value.
203      *
204      * @param name
205      *            The name of the header or parameter.
206      * @param values
207      *            The header or parameter values.
208      * @return The boolean value.
209      * @throws ResourceException
210      *             If the value could not be parsed as a boolean.
211      */
212     static boolean asBooleanValue(final String name, final List<String> values)
213             throws ResourceException {
214         final String value = asSingleValue(name, values);
215         return Boolean.parseBoolean(value);
216     }
217 
218     /**
219      * Parses a header or request parameter as an integer value.
220      *
221      * @param name
222      *            The name of the header or parameter.
223      * @param values
224      *            The header or parameter values.
225      * @return The integer value.
226      * @throws ResourceException
227      *             If the value could not be parsed as a integer.
228      */
229     static int asIntValue(final String name, final List<String> values) throws ResourceException {
230         final String value = asSingleValue(name, values);
231         try {
232             return Integer.parseInt(value);
233         } catch (final NumberFormatException e) {
234             // FIXME: i18n.
235             throw new BadRequestException("The value \'" + value + "\' for parameter '" + name
236                     + "' could not be parsed as a valid integer");
237         }
238     }
239 
240     /**
241      * Parses a header or request parameter as a single string value.
242      *
243      * @param name
244      *            The name of the header or parameter.
245      * @param values
246      *            The header or parameter values.
247      * @return The single string value.
248      * @throws ResourceException
249      *             If the value could not be parsed as a single string.
250      */
251     static String asSingleValue(final String name, final List<String> values) throws ResourceException {
252         if (values == null || values.isEmpty()) {
253             // FIXME: i18n.
254             throw new BadRequestException("No values provided for the request parameter \'" + name
255                     + "\'");
256         } else if (values.size() > 1) {
257             // FIXME: i18n.
258             throw new BadRequestException(
259                     "Multiple values provided for the single-valued request parameter \'" + name
260                             + "\'");
261         }
262         return values.get(0);
263     }
264 
265     /**
266      * Safely fail an HTTP request using the provided {@code Exception}.
267      *
268      * @param req
269      *            The HTTP request.
270      * @param t
271      *            The resource exception indicating why the request failed.
272      */
273     static Promise<Response, NeverThrowsException> fail(org.forgerock.http.protocol.Request req, final Throwable t) {
274         return fail0(req, null, t);
275     }
276 
277     /**
278      * Safely fail an HTTP request using the provided {@code Exception}.
279      *
280      * @param req
281      *            The HTTP request.
282      * @param resp
283      *            The HTTP response.
284      * @param t
285      *            The resource exception indicating why the request failed.
286      */
287     static Promise<Response, NeverThrowsException> fail(org.forgerock.http.protocol.Request req,
288             org.forgerock.http.protocol.Response resp, final Throwable t) {
289         return fail0(req, resp, t);
290     }
291 
292     private static Promise<Response, NeverThrowsException> fail0(org.forgerock.http.protocol.Request req,
293             org.forgerock.http.protocol.Response resp, Throwable t) {
294         final ResourceException re = adapt(t);
295         try {
296             if (resp == null) {
297                 resp = prepareResponse(req);
298             } else {
299                 resp = prepareResponse(req, resp);
300             }
301             resp.setStatus(Status.valueOf(re.getCode()));
302             final JsonGenerator writer = getJsonGenerator(req, resp);
303             Json.makeLocalizingObjectWriter(JSON_MAPPER, req).writeValue(writer, re.toJsonValue().getObject());
304             closeSilently(writer);
305             return newResultPromise(resp);
306         } catch (final IOException ignored) {
307             // Ignore the error since this was probably the cause.
308             return newResultPromise(newInternalServerError());
309         } catch (MalformedHeaderException e) {
310             return newResultPromise(new Response(Status.BAD_REQUEST).setEntity("Malformed header"));
311         }
312     }
313 
314     /**
315      * Determines which CREST operation (CRUDPAQ) of the incoming request.
316      *
317      * @param request The request.
318      * @return The Operation.
319      * @throws ResourceException If the request operation could not be
320      * determined or is not supported.
321      */
322     public static RequestType determineRequestType(org.forgerock.http.protocol.Request request)
323             throws ResourceException {
324         // Dispatch the request based on method, taking into account
325         // method override header.
326         final String method = getMethod(request);
327         if (METHOD_DELETE.equals(method)) {
328             return RequestType.DELETE;
329         } else if (METHOD_GET.equals(method)) {
330             if (hasParameter(request, PARAM_QUERY_ID)
331                     || hasParameter(request, PARAM_QUERY_EXPRESSION)
332                     || hasParameter(request, PARAM_QUERY_FILTER)) {
333                 return RequestType.QUERY;
334             } else if (hasParameter(request, PARAM_CREST_API)) {
335                 return RequestType.API;
336             } else {
337                 return RequestType.READ;
338             }
339         } else if (METHOD_PATCH.equals(method)) {
340             return RequestType.PATCH;
341         } else if (METHOD_POST.equals(method)) {
342             return determinePostRequestType(request);
343         } else if (METHOD_PUT.equals(method)) {
344             return determinePutRequestType(request);
345         } else {
346             // TODO: i18n
347             throw new NotSupportedException("Method " + method + " not supported");
348         }
349     }
350 
351     private static RequestType determinePostRequestType(org.forgerock.http.protocol.Request request)
352             throws ResourceException {
353         List<String> parameter = getParameter(request, PARAM_ACTION);
354 
355         boolean defactoCreate = getRequestedProtocolVersion(request).compareTo(PROTOCOL_VERSION_2_1) >= 0
356                 && (parameter == null || parameter.isEmpty());
357 
358         return defactoCreate || asSingleValue(PARAM_ACTION, parameter).equalsIgnoreCase(ACTION_ID_CREATE)
359                 ? RequestType.CREATE
360                 : RequestType.ACTION;
361     }
362 
363     /**
364      * Determine whether the PUT request should be interpreted as a CREATE or an UPDATE depending on
365      * If-None-Match header, If-Match header, and protocol version.
366      *
367      * @param request The request.
368      * @return true if request is interpreted as a create; false if interpreted as an update
369      */
370     private static RequestType determinePutRequestType(org.forgerock.http.protocol.Request request)
371             throws BadRequestException {
372 
373         final Version protocolVersion = getRequestedProtocolVersion(request);
374         final String ifNoneMatch = getIfNoneMatch(request);
375         final String ifMatch = getIfMatch(request, protocolVersion);
376 
377         /* CREST-100
378          * For protocol version 1:
379          *
380          *  - "If-None-Match: x" is present, where 'x' is any non-* value: this is a bad request
381          *  - "If-None-Match: *" is present: this is a create which will fail if the object already exists.
382          *  - "If-None-Match: *" is not present:
383          *          This is an update which will fail if the object does not exist.  There are two ways to
384          *          perform the update, using the value of the If-Match header:
385          *           - "If-Match: <rev>" : update the object if its revision matches the header value
386          *           - "If-Match: * : update the object regardless of the object's revision
387          *           - "If-Match:" header is not present : same as "If-Match: *"; update regardless of object revision
388          *
389          * For protocol version 2 onward:
390          *
391          * Two methods of create are implied by PUT:
392          *
393          *  - "If-None-Match: x" is present, where 'x' is any non-* value: this is a bad request
394          *  - "If-None-Match: *" is present, this is a create which will fail if the object already exists.
395          *  - "If-Match" is present; this is an update only:
396          *           - "If-Match: <rev>" : update the object if its revision matches the header value
397          *           - "If-Match: * : update the object regardless of the object's revision
398          *  - Neither "If-None-Match" nor "If-Match" are present, this is either a create or an update ("upsert"):
399          *          Attempt a create; if it fails, attempt an update.  If the update fails, return an error
400          *          (the record could have been deleted between the create-failure and the update, for example).
401          */
402 
403         /* CREST-346 */
404         if (ifNoneMatch != null && !ETAG_ANY.equals(ifNoneMatch)) {
405             throw new BadRequestException("\"" + ifNoneMatch + "\" is not a supported value for If-None-Match on PUT");
406         }
407 
408         if (ETAG_ANY.equals(ifNoneMatch)) {
409             return RequestType.CREATE;
410         } else if (ifNoneMatch == null && ifMatch == null && protocolVersion.getMajor() >= 2) {
411             return RequestType.CREATE;
412         } else {
413             return RequestType.UPDATE;
414         }
415     }
416 
417     /**
418      * Attempts to parse the version header and return a corresponding resource {@link Version} representation.
419      * Further validates that the specified versions are valid. That being not in the future and no earlier
420      * that the current major version.
421      *
422      * @param req
423      *         The HTTP servlet request
424      *
425      * @return A non-null resource  {@link Version} instance
426      *
427      * @throws BadRequestException
428      *         If an invalid version is requested
429      */
430     static Version getRequestedResourceVersion(org.forgerock.http.protocol.Request req) throws BadRequestException {
431         return getAcceptApiVersionHeader(req).getResourceVersion();
432     }
433 
434     /**
435      * Attempts to parse the version header and return a corresponding protocol {@link Version} representation.
436      * Further validates that the specified versions are valid. That being not in the future and no earlier
437      * that the current major version.
438      *
439      * @param req
440      *         The HTTP servlet request
441      *
442      * @return A non-null resource  {@link Version} instance
443      *
444      * @throws BadRequestException
445      *         If an invalid version is requested
446      */
447     static Version getRequestedProtocolVersion(org.forgerock.http.protocol.Request req) throws BadRequestException {
448         Version protocolVersion = getAcceptApiVersionHeader(req).getProtocolVersion();
449         return protocolVersion != null ? protocolVersion : DEFAULT_PROTOCOL_VERSION;
450     }
451 
452     /**
453      * Validate and return the AcceptApiVersionHeader.
454      *
455      * @param req
456      *         The HTTP servlet request
457      *
458      * @return A non-null resource  {@link Version} instance
459      *
460      * @throws BadRequestException
461      *         If an invalid version is requested
462      */
463     private static AcceptApiVersionHeader getAcceptApiVersionHeader(org.forgerock.http.protocol.Request req)
464             throws BadRequestException {
465         AcceptApiVersionHeader apiVersionHeader;
466         try {
467             apiVersionHeader = AcceptApiVersionHeader.valueOf(req);
468         } catch (IllegalArgumentException e) {
469             throw new BadRequestException(e);
470         }
471         validateProtocolVersion(apiVersionHeader.getProtocolVersion());
472         return apiVersionHeader;
473     }
474 
475     /**
476      * Validate the Protocol version as not in the future.
477      *
478      * @param protocolVersion the protocol version from the request
479      * @throws BadRequestException if the request marks a protocol version greater than the current version
480      */
481     private static void validateProtocolVersion(Version protocolVersion) throws BadRequestException {
482         if (protocolVersion != null && protocolVersion.getMajor() > DEFAULT_PROTOCOL_VERSION.getMajor()) {
483             throw new BadRequestException("Unsupported major version: " + protocolVersion);
484         }
485         if (protocolVersion != null && protocolVersion.getMinor() > DEFAULT_PROTOCOL_VERSION.getMinor()) {
486             throw new BadRequestException("Unsupported minor version: " + protocolVersion);
487         }
488     }
489 
490     static String getIfMatch(org.forgerock.http.protocol.Request req, Version protocolVersion) {
491         final String etag = req.getHeaders().getFirst(HEADER_IF_MATCH);
492         if (etag != null) {
493             if (etag.length() >= 2) {
494                 // Remove quotes.
495                 if (etag.charAt(0) == '"') {
496                     return etag.substring(1, etag.length() - 1);
497                 }
498             } else if (etag.equals(ETAG_ANY) && protocolVersion.getMajor() < 2) {
499                 // If-Match * is implied prior to version 2
500                 return null;
501             }
502         }
503         return etag;
504     }
505 
506     static String getIfNoneMatch(org.forgerock.http.protocol.Request req) {
507         final String etag = req.getHeaders().getFirst(HEADER_IF_NONE_MATCH);
508         if (etag != null) {
509             if (etag.length() >= 2) {
510                 // Remove quotes.
511                 if (etag.charAt(0) == '"') {
512                     return etag.substring(1, etag.length() - 1);
513                 }
514             } else if (etag.equals(ETAG_ANY)) {
515                 // If-None-Match *.
516                 return ETAG_ANY;
517             }
518         }
519         return etag;
520     }
521 
522     /**
523      * Returns the content of the provided HTTP request decoded as a JSON
524      * object. The content is allowed to be empty, in which case an empty JSON
525      * object is returned.
526      *
527      * @param req
528      *            The HTTP request.
529      * @return The content of the provided HTTP request decoded as a JSON
530      *         object.
531      * @throws ResourceException
532      *             If the content could not be read or if the content was not
533      *             valid JSON.
534      */
535     static JsonValue getJsonContentIfPresent(org.forgerock.http.protocol.Request req) throws ResourceException {
536         return getJsonContent0(req, true);
537     }
538 
539     /**
540      * Returns the content of the provided HTTP request decoded as a JSON
541      * object. If there is no content then a {@link BadRequestException} will be
542      * thrown.
543      *
544      * @param req
545      *            The HTTP request.
546      * @return The content of the provided HTTP request decoded as a JSON
547      *         object.
548      * @throws ResourceException
549      *             If the content could not be read or if the content was not
550      *             valid JSON.
551      */
552     static JsonValue getJsonContent(org.forgerock.http.protocol.Request req) throws ResourceException {
553         return getJsonContent0(req, false);
554     }
555 
556     /**
557      * Creates a JSON generator which can be used for serializing JSON content
558      * in HTTP responses.
559      *
560      * @param req
561      *            The HTTP request.
562      * @param resp
563      *            The HTTP response.
564      * @return A JSON generator which can be used to write out a JSON response.
565      * @throws IOException
566      *             If an error occurred while obtaining an output stream.
567      */
568     static JsonGenerator getJsonGenerator(org.forgerock.http.protocol.Request req,
569             Response resp) throws IOException {
570 
571         PipeBufferedStream pipeStream = new PipeBufferedStream();
572         resp.setEntity(pipeStream.getOut());
573 
574         final JsonGenerator writer =
575                 JSON_MAPPER.getFactory().createGenerator(pipeStream.getIn());
576 
577         // Need to have the JsonGenerator close the stream so that it is
578         // properly released.
579         writer.configure(JsonGenerator.Feature.AUTO_CLOSE_TARGET, true);
580 
581         // Enable pretty printer if requested.
582         final List<String> values = getParameter(req, PARAM_PRETTY_PRINT);
583         if (values != null) {
584             try {
585                 if (asBooleanValue(PARAM_PRETTY_PRINT, values)) {
586                     writer.useDefaultPrettyPrinter();
587                 }
588             } catch (final ResourceException e) {
589                 // Ignore because we may be trying to obtain a generator in
590                 // order to output an error.
591             }
592         }
593         return writer;
594     }
595 
596     /**
597      * Returns the content of the provided HTTP request decoded as a JSON patch
598      * object.
599      *
600      * @param req
601      *            The HTTP request.
602      * @return The content of the provided HTTP request decoded as a JSON patch
603      *         object.
604      * @throws ResourceException
605      *             If the content could not be read or if the content was not a
606      *             valid JSON patch.
607      */
608     static List<PatchOperation> getJsonPatchContent(org.forgerock.http.protocol.Request req)
609             throws ResourceException {
610         return PatchOperation.valueOfList(new JsonValue(parseJsonBody(req, false)));
611     }
612 
613     /**
614      * Returns the content of the provided HTTP request decoded as a JSON action
615      * content.
616      *
617      * @param req
618      *            The HTTP request.
619      * @return The content of the provided HTTP request decoded as a JSON action
620      *         content.
621      * @throws ResourceException
622      *             If the content could not be read or if the content was not
623      *             valid JSON.
624      */
625     static JsonValue getJsonActionContent(org.forgerock.http.protocol.Request req) throws ResourceException {
626         return new JsonValue(parseJsonBody(req, true));
627     }
628 
629     /**
630      * Returns the effective method name for an HTTP request taking into account
631      * the "X-HTTP-Method-Override" header.
632      *
633      * @param req
634      *            The HTTP request.
635      * @return The effective method name.
636      */
637     static String getMethod(org.forgerock.http.protocol.Request req) {
638         String method = req.getMethod();
639         if (HttpUtils.METHOD_POST.equals(method)
640                 && req.getHeaders().getFirst(HttpUtils.HEADER_X_HTTP_METHOD_OVERRIDE) != null) {
641             method = req.getHeaders().getFirst(HttpUtils.HEADER_X_HTTP_METHOD_OVERRIDE);
642         }
643         return method;
644     }
645 
646     /**
647      * Returns the named parameter from the provided HTTP request using case
648      * insensitive matching.
649      *
650      * @param req
651      *            The HTTP request.
652      * @param parameter
653      *            The parameter to return.
654      * @return The parameter values or {@code null} if it wasn't present.
655      */
656     static List<String> getParameter(org.forgerock.http.protocol.Request req, String parameter) {
657         // Need to do case-insensitive matching.
658         for (final Map.Entry<String, List<String>> p : req.getForm().entrySet()) {
659             if (p.getKey().equalsIgnoreCase(parameter)) {
660                 return p.getValue();
661             }
662         }
663         return null;
664     }
665 
666     /**
667      * Returns {@code true} if the named parameter is present in the provided
668      * HTTP request using case insensitive matching.
669      *
670      * @param req
671      *            The HTTP request.
672      * @param parameter
673      *            The parameter to return.
674      * @return {@code true} if the named parameter is present.
675      */
676     static boolean hasParameter(org.forgerock.http.protocol.Request req, String parameter) {
677         return getParameter(req, parameter) != null;
678     }
679 
680     static Response prepareResponse(org.forgerock.http.protocol.Request req) throws ResourceException {
681         return prepareResponse(req, new Response(Status.OK));
682     }
683 
684     static Response prepareResponse(org.forgerock.http.protocol.Request req, org.forgerock.http.protocol.Response resp)
685             throws ResourceException {
686         //get content type from req path
687         try {
688             resp.setStatus(Status.OK);
689             String mimeType = req.getForm().getFirst(PARAM_MIME_TYPE);
690             if (METHOD_GET.equalsIgnoreCase(getMethod(req)) && mimeType != null && !mimeType.isEmpty()) {
691                 ContentType contentType = new ContentType(mimeType);
692                 resp.getHeaders().put(new ContentTypeHeader(contentType.toString(), CHARACTER_ENCODING, null));
693             } else {
694                 resp.getHeaders().put(new ContentTypeHeader(MIME_TYPE_APPLICATION_JSON, CHARACTER_ENCODING, null));
695             }
696 
697             resp.getHeaders().put(HEADER_CACHE_CONTROL, CACHE_CONTROL);
698             return resp;
699         } catch (ParseException e) {
700             throw new BadRequestException("The mime type parameter '" + req.getForm().getFirst(PARAM_MIME_TYPE)
701                     + "' can't be parsed", e);
702         }
703     }
704 
705     static void rejectIfMatch(org.forgerock.http.protocol.Request req) throws ResourceException {
706         if (req.getHeaders().getFirst(HEADER_IF_MATCH) != null) {
707             // FIXME: i18n
708             throw new PreconditionFailedException("If-Match not supported for " + getMethod(req) + " requests");
709         }
710     }
711 
712     static void rejectIfNoneMatch(org.forgerock.http.protocol.Request req) throws ResourceException,
713             PreconditionFailedException {
714         if (req.getHeaders().getFirst(HEADER_IF_NONE_MATCH) != null) {
715             // FIXME: i18n
716             throw new PreconditionFailedException("If-None-Match not supported for "
717                     + getMethod(req) + " requests");
718         }
719     }
720 
721     private static JsonValue getJsonContent0(org.forgerock.http.protocol.Request req, boolean allowEmpty)
722             throws ResourceException {
723         final Object body = parseJsonBody(req, allowEmpty);
724         if (body == null) {
725             return new JsonValue(new LinkedHashMap<>(0));
726         } else if (!(body instanceof Map)) {
727             throw new BadRequestException(
728                     "The request could not be processed because the provided "
729                             + "content is not a JSON object");
730         } else {
731             return new JsonValue(body);
732         }
733     }
734 
735     private static BodyPart getJsonRequestPart(final MimeMultipart mimeMultiparts)
736             throws BadRequestException, ResourceException {
737         try {
738             for (int i = 0; i < mimeMultiparts.getCount(); i++) {
739                 BodyPart part = mimeMultiparts.getBodyPart(i);
740                 ContentType contentType = new ContentType(part.getContentType());
741                 if (contentType.match(MIME_TYPE_APPLICATION_JSON)) {
742                     return part;
743                 }
744             }
745             throw new BadRequestException(
746                     "The request could not be processed because the multipart request "
747                     + "does not include Content-Type: " + MIME_TYPE_APPLICATION_JSON);
748         } catch (final MessagingException e) {
749             throw new BadRequestException(
750                     "The request could not be processed because the request cant be parsed", e);
751         } catch (final IOException e) {
752             throw adapt(e);
753         }
754 
755     }
756 
757     private static String getRequestPartData(final MimeMultipart mimeMultiparts,
758             final String partName, final String partDataType) throws IOException, MessagingException {
759         if (mimeMultiparts == null) {
760             throw new BadRequestException(
761                     "The request parameter is null when retrieving part data for part name: "
762                             + partName);
763         }
764 
765         if (partDataType == null || partDataType.isEmpty()) {
766             throw new BadRequestException("The request is requesting an unknown part field");
767         }
768         MimeBodyPart part = null;
769         for (int i = 0; i < mimeMultiparts.getCount(); i++) {
770             part = (MimeBodyPart) mimeMultiparts.getBodyPart(i);
771             ContentDisposition disposition =
772                     new ContentDisposition(part.getHeader(CONTENT_DISPOSITION, null));
773             if (disposition.getParameter(NAME).equalsIgnoreCase(partName)) {
774                 break;
775             }
776         }
777 
778         if (part == null) {
779             throw new BadRequestException(
780                     "The request is missing a referenced part for part name: " + partName);
781         }
782 
783         if (MIME_TYPE.equalsIgnoreCase(partDataType)) {
784             return new ContentType(part.getContentType()).toString();
785         } else if (FILENAME.equalsIgnoreCase(partDataType)) {
786             return part.getFileName();
787         } else if (CONTENT.equalsIgnoreCase(partDataType)) {
788             return Base64url.encode(toByteArray(part.getInputStream()));
789         } else {
790             throw new BadRequestException(
791                     "The request could not be processed because the multipart request "
792                             + "requests data from the part that isn't supported. Data requested: "
793                             + partDataType);
794         }
795     }
796 
797     private static boolean isAReferenceJsonObject(JsonValue node) {
798         return node.keys() != null && node.keys().size() == 1
799                 && REFERENCE_TAG.equalsIgnoreCase(node.keys().iterator().next());
800     }
801 
802     private static Object swapRequestPartsIntoContent(final MimeMultipart mimeMultiparts,
803             Object content) throws ResourceException {
804         try {
805             JsonValue root = new JsonValue(content);
806 
807             ArrayDeque<JsonValue> stack = new ArrayDeque<>();
808             stack.push(root);
809 
810             while (!stack.isEmpty()) {
811                 JsonValue node = stack.pop();
812                 if (isAReferenceJsonObject(node)) {
813                     Matcher matcher =
814                             MULTIPART_FIELD_REGEX.matcher(node.get(REFERENCE_TAG).asString());
815                     if (matcher.matches()) {
816                         String partName = matcher.group(PART_NAME);
817                         String requestPartData =
818                                 getRequestPartData(mimeMultiparts, partName, matcher
819                                         .group(PART_DATA_TYPE));
820                         root.put(node.getPointer(), requestPartData);
821                     } else {
822                         throw new BadRequestException("Invalid reference tag '" + node.toString()
823                                 + "'");
824                     }
825                 } else {
826                     Iterator<JsonValue> iter = node.iterator();
827                     while (iter.hasNext()) {
828                         stack.push(iter.next());
829                     }
830                 }
831             }
832             return root;
833         } catch (final IOException e) {
834             throw adapt(e);
835         } catch (final MessagingException e) {
836             throw new BadRequestException(
837                     "The request could not be processed because the request is not a valid multipart request");
838         }
839     }
840 
841     static boolean isMultiPartRequest(final String unknownContentType) throws BadRequestException {
842         try {
843             if (unknownContentType == null) {
844                 return false;
845             }
846             ContentType contentType = new ContentType(unknownContentType);
847             return contentType.match(MIME_TYPE_MULTIPART_FORM_DATA);
848         } catch (final ParseException e) {
849             throw new BadRequestException("The request content type can't be parsed.", e);
850         }
851     }
852 
853     private static Object parseJsonBody(org.forgerock.http.protocol.Request req, boolean allowEmpty)
854             throws ResourceException {
855         try {
856             String contentType = req.getHeaders().getFirst(ContentTypeHeader.class);
857             if (contentType == null && !allowEmpty) {
858                 throw new BadRequestException("The request could not be processed because the "
859                         + " content-type was not specified and is required");
860             }
861             boolean isMultiPartRequest = isMultiPartRequest(contentType);
862             MimeMultipart mimeMultiparts = null;
863             JsonParser jsonParser;
864             if (isMultiPartRequest) {
865                 mimeMultiparts = new MimeMultipart(new HttpServletRequestDataSource(req));
866                 BodyPart jsonPart = getJsonRequestPart(mimeMultiparts);
867                 jsonParser = JSON_MAPPER.getFactory().createParser(jsonPart.getInputStream());
868             } else {
869                 jsonParser = JSON_MAPPER.getFactory().createParser(req.getEntity().getRawContentInputStream());
870             }
871             try (JsonParser parser = jsonParser) {
872                 Object content = parser.readValueAs(Object.class);
873 
874                 // Ensure that there is no trailing data following the JSON resource.
875                 boolean hasTrailingGarbage;
876                 try {
877                     hasTrailingGarbage = parser.nextToken() != null;
878                 } catch (JsonParseException e) {
879                     hasTrailingGarbage = true;
880                 }
881                 if (hasTrailingGarbage) {
882                     throw new BadRequestException(
883                             "The request could not be processed because there is "
884                                     + "trailing data after the JSON content");
885                 }
886 
887                 if (isMultiPartRequest) {
888                     swapRequestPartsIntoContent(mimeMultiparts, content);
889                 }
890 
891                 return content;
892             }
893         } catch (final JsonParseException e) {
894             throw new BadRequestException(
895                     "The request could not be processed because the provided "
896                             + "content is not valid JSON", e)
897                 .setDetail(new JsonValue(e.getMessage()));
898         } catch (final JsonMappingException e) {
899             if (allowEmpty) {
900                 return null;
901             } else {
902                 throw new BadRequestException("The request could not be processed "
903                         + "because it did not contain any JSON content", e);
904             }
905         } catch (final IOException e) {
906             throw adapt(e);
907         } catch (final MessagingException e) {
908             throw new BadRequestException(
909                     "The request could not be processed because it can't be parsed", e);
910         }
911     }
912 
913     private static String param(final String field) {
914         return "_" + field;
915     }
916 
917     private HttpUtils() {
918         // Prevent instantiation.
919     }
920 
921     private static class HttpServletRequestDataSource implements DataSource {
922         private org.forgerock.http.protocol.Request request;
923 
924         HttpServletRequestDataSource(org.forgerock.http.protocol.Request request) {
925             this.request = request;
926         }
927 
928         public InputStream getInputStream() throws IOException {
929             return request.getEntity().getRawContentInputStream();
930         }
931 
932         public OutputStream getOutputStream() throws IOException {
933             return null;
934         }
935 
936         public String getContentType() {
937             return request.getHeaders().getFirst(ContentTypeHeader.class);
938         }
939 
940         public String getName() {
941             return "HttpServletRequestDataSource";
942         }
943     }
944 
945     private static byte[] toByteArray(final InputStream inputStream) throws IOException {
946         final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
947         final byte[] data = new byte[BUFFER_SIZE];
948         int size;
949         while ((size = inputStream.read(data)) != EOF) {
950             byteArrayOutputStream.write(data, 0, size);
951         }
952         byteArrayOutputStream.flush();
953         return byteArrayOutputStream.toByteArray();
954     }
955 
956     static HttpContextFactory staticContextFactory(final Context parentContext) {
957         return new HttpContextFactory() {
958             @Override
959             public Context createContext(Context parent, org.forgerock.http.protocol.Request request) {
960                 return parentContext;
961             }
962         };
963     }
964 }