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;
038
039import javax.activation.DataSource;
040import javax.mail.BodyPart;
041import javax.mail.MessagingException;
042import javax.mail.internet.ContentDisposition;
043import javax.mail.internet.ContentType;
044import javax.mail.internet.MimeBodyPart;
045import javax.mail.internet.MimeMultipart;
046import javax.mail.internet.ParseException;
047
048import org.forgerock.http.header.AcceptApiVersionHeader;
049import org.forgerock.http.header.ContentTypeHeader;
050import org.forgerock.http.header.MalformedHeaderException;
051import org.forgerock.http.io.PipeBufferedStream;
052import org.forgerock.http.protocol.Response;
053import org.forgerock.http.protocol.Status;
054import org.forgerock.http.routing.Version;
055import org.forgerock.http.util.Json;
056import org.forgerock.json.JsonValue;
057import org.forgerock.json.resource.ActionRequest;
058import org.forgerock.json.resource.BadRequestException;
059import org.forgerock.json.resource.InternalServerErrorException;
060import org.forgerock.json.resource.NotSupportedException;
061import org.forgerock.json.resource.PatchOperation;
062import org.forgerock.json.resource.PreconditionFailedException;
063import org.forgerock.json.resource.QueryRequest;
064import org.forgerock.json.resource.Request;
065import org.forgerock.json.resource.RequestType;
066import org.forgerock.json.resource.ResourceException;
067import org.forgerock.services.context.Context;
068import org.forgerock.util.encode.Base64url;
069import org.forgerock.util.promise.NeverThrowsException;
070import org.forgerock.util.promise.Promise;
071
072import com.fasterxml.jackson.core.JsonGenerator;
073import com.fasterxml.jackson.core.JsonParseException;
074import com.fasterxml.jackson.core.JsonParser;
075import com.fasterxml.jackson.databind.JsonMappingException;
076import com.fasterxml.jackson.databind.ObjectMapper;
077
078/**
079 * HTTP utility methods and constants.
080 */
081public final class HttpUtils {
082    static final String CACHE_CONTROL = "no-cache";
083    static final String CHARACTER_ENCODING = "UTF-8";
084    static final Pattern CONTENT_TYPE_REGEX = Pattern.compile(
085            "^application/json([ ]*;[ ]*charset=utf-8)?$", Pattern.CASE_INSENSITIVE);
086    static final String CRLF = "\r\n";
087    static final String ETAG_ANY = "*";
088
089    static final String MIME_TYPE_APPLICATION_JSON = "application/json";
090    static final String MIME_TYPE_MULTIPART_FORM_DATA = "multipart/form-data";
091    static final String MIME_TYPE_TEXT_PLAIN = "text/plain";
092
093    static final String HEADER_CACHE_CONTROL = "Cache-Control";
094    static final String HEADER_ETAG = "ETag";
095    static final String HEADER_IF_MATCH = "If-Match";
096    static final String HEADER_IF_NONE_MATCH = "If-None-Match";
097    static final String HEADER_IF_MODIFIED_SINCE = "If-Modified-Since";
098    static final String HEADER_IF_UNMODIFIED_SINCE = "If-Unmodified-Since";
099    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}