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