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