All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.osgl.http.H Maven / Gradle / Ivy

package org.osgl.http;

/*-
 * #%L
 * OSGL HTTP
 * %%
 * Copyright (C) 2017 OSGL (Open Source General Library)
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */

import static org.osgl.http.H.Header.Names.*;

import org.osgl.$;
import org.osgl.cache.CacheService;
import org.osgl.exception.NotAppliedException;
import org.osgl.exception.UnexpectedIOException;
import org.osgl.http.util.DefaultCurrentStateStore;
import org.osgl.http.util.Path;
import org.osgl.logging.LogManager;
import org.osgl.logging.Logger;
import org.osgl.storage.ISObject;
import org.osgl.util.*;
import org.osgl.util.converter.TypeConverterRegistry;
import org.osgl.web.util.UserAgent;
import osgl.version.Version;
import osgl.version.Versioned;

import java.io.*;
import java.net.URLEncoder;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * The namespace to access Http features.
 * Alias of {@link org.osgl.http.Http}
 */
@Versioned
public class H {

    static {
        registerTypeConverters();
    }


    public static final Version VERSION = Version.get();

    protected static final Logger logger = LogManager.get(Http.class);

    private static CurrentStateStore current = new DefaultCurrentStateStore();

    static void setCurrentStateStore(CurrentStateStore store) {
        current = $.requireNotNull(store);
    }

    public enum Method {
        GET, HEAD, POST, DELETE, PUT, PATCH, TRACE, OPTIONS, CONNECT, _UNKNOWN_;

        static {
            registerTypeConverters();
        }

        private static EnumSet unsafeMethods = EnumSet.of(POST, DELETE, PUT, PATCH);
        private static EnumSet actionMethods = EnumSet.of(GET, POST, PUT, PATCH, DELETE);
        private static final int HC_GET = GET.name().hashCode();
        private static final int HC_HEAD = HEAD.name().hashCode();
        private static final int HC_POST = POST.name().hashCode();
        private static final int HC_DELETE = DELETE.name().hashCode();
        private static final int HC_PUT = PUT.name().hashCode();
        private static final int HC_PATCH = PATCH.name().hashCode();
        private static final int HC_OPTIONS = OPTIONS.name().hashCode();

        /**
         * Returns if this http method is safe, meaning it
         * won't change the state of the server
         *
         * @return true if the method is safe or false otherwise
         * @see #unsafe()
         */
        public boolean safe() {
            return !unsafe();
        }

        /**
         * Returns if this http method is unsafe, meaning
         * it will change the state of the server
         *
         * @return true if the method is unsafe or false otherwise
         * @see #safe()
         */
        public boolean unsafe() {
            return unsafeMethods.contains(this);
        }

        /**
         * Is this known method?
         * @return `true` if this method is unknown
         */
        public boolean isUnknown() {
            return _UNKNOWN_ == this;
        }

        private static volatile Map methods;

        /**
         * Returns an HTTP Method enum corresponding to the method string.
         *
         * If no HTTP method enum found then {@link #_UNKNOWN_} will be returned
         *
         * @param method the method string
         * @return the HTTP Method enum as described above
         */
        public static Method valueOfIgnoreCase(String method) {
            if (null == methods) {
                synchronized (Method.class) {
                    if (null == methods) {
                        methods = new HashMap<>();
                        for (Method m : values()) {
                            methods.put(m.name(), m);
                        }
                    }
                }
            }
            int hc = method.hashCode();
            if (HC_GET == hc) {
                return GET;
            } else if (HC_POST == hc) {
                return POST;
            } else if (HC_OPTIONS == hc) {
                return OPTIONS;
            } else if (HC_PUT == hc) {
                return PUT;
            } else if (HC_DELETE == hc) {
                return DELETE;
            } else if (HC_HEAD == hc) {
                return HEAD;
            }
            // performance tune, most of the case we don't need the
            // toUpperCase() call
            Method m = methods.get(method);
            if (null == m) {
                m = methods.get(method.toUpperCase());
            }
            return null != m ? m : _UNKNOWN_;
        }

        public static EnumSet actionMethods() {
            return actionMethods.clone();
        }
    } // eof Method

    public static final class Status implements Serializable, Comparable {

        static {
            registerTypeConverters();
        }

        public enum Code {
            ;
            public static final int CONTINUE = 100;
            public static final int SWITCHING_PROTOCOLS = 101;
            public static final int PROCESSING = 102;
            public static final int CHECKPOINT = 103;

            public static final int OK = 200;
            public static final int CREATED = 201;
            public static final int ACCEPTED = 202;
            public static final int NON_AUTHORITATIVE_INFORMATION = 203;
            public static final int NO_CONTENT = 204;
            public static final int RESET_CONTENT = 205;
            public static final int PARTIAL_CONTENT = 206;
            public static final int MULTI_STATUS = 207;
            public static final int ALREADY_REPORTED = 208;
            public static final int IM_USED = 226;
            // see http://stackoverflow.com/questions/199099/how-to-manage-a-redirect-request-after-a-jquery-ajax-call
            public static final int FOUND_AJAX = 278;

            public static final int MULTIPLE_CHOICES = 300;
            public static final int MOVED_PERMANENTLY = 301;
            public static final int FOUND = 302;
            /**
             * this is deprecated, one should use {@link #FOUND} instead
             */
            @Deprecated
            public static final int MOVED_TEMPORARILY = FOUND;
            public static final int SEE_OTHER = 303;
            public static final int NOT_MODIFIED = 304;
            public static final int USE_PROXY = 305;
            public static final int SWITCH_PROXY = 306;
            public static final int TEMPORARY_REDIRECT = 307;
            public static final int PERMANENT_REDIRECT = 308;

            public static final int BAD_REQUEST = 400;
            public static final int UNAUTHORIZED = 401;
            public static final int PAYMENT_REQUIRED = 402;
            public static final int FORBIDDEN = 403;
            public static final int NOT_FOUND = 404;
            public static final int METHOD_NOT_ALLOWED = 405;
            public static final int NOT_ACCEPTABLE = 406;
            public static final int PROXY_AUTHENTICATION_REQUIRED = 407;
            public static final int REQUEST_TIMEOUT = 408;
            public static final int CONFLICT = 409;
            public static final int GONE = 410;
            public static final int LENGTH_REQUIRED = 411;
            public static final int PRECONDITION_FAILED = 412;
            public static final int PAYLOAD_TOO_LARGE = 413;
            public static final int URI_TOO_LONG = 414;
            public static final int UNSUPPORTED_MEDIA_TYPE = 415;
            public static final int RANGE_NOT_SATISFIABLE = 416;
            public static final int EXPECTATION_FAILED = 417;
            public static final int I_AM_A_TEAPOT = 418;
            public static final int AUTHENTICATION_TIMEOUT = 419;
            public static final int METHOD_FAILURE = 420;
            public static final int MISDIRECTED_REQUEST = 421;
            public static final int UNPROCESSABLE_ENTITY = 422;
            public static final int LOCKED = 423;
            public static final int FAILED_DEPENDENCY = 424;
            public static final int UPGRADE_REQUIRED = 426;
            public static final int PRECONDITION_REQUIRED = 428;
            public static final int TOO_MANY_REQUESTS = 429;
            public static final int REQUEST_HEADER_FIELDS_TOO_LARGE = 431;
            /**
             * One should use {@link #UNAVAILABLE_FOR_LEGAL_REASONS} instead
             */
            @Deprecated
            public static final int UNAVAILABLE_FOR_LEGAL_REASON = 451;
            public static final int UNAVAILABLE_FOR_LEGAL_REASONS = 451;

            public static final int INTERNAL_SERVER_ERROR = 500;
            public static final int NOT_IMPLEMENTED = 501;
            public static final int BAD_GATEWAY = 502;
            public static final int SERVICE_UNAVAILABLE = 503;
            public static final int GATEWAY_TIMEOUT = 504;
            public static final int HTTP_VERSION_NOT_SUPPORTED = 505;
            public static final int VARIANT_ALSO_NEGOTIATES = 506;
            public static final int INSUFFICIENT_STORAGE = 507;
            public static final int LOOP_DETECTED = 508;
            public static final int NOT_EXTENDED = 510;
            public static final int NETWORK_AUTHENTICATION_REQUIRED = 511;
        }

        private static final Map predefinedStatus = new LinkedHashMap<>();
        private static final long serialVersionUID = -286619406116817809L;

        private int code;

        private Status(int code) {
            this(code, true);
        }

        private Status(int code, boolean predefined) {
            this.code = code;
            if (predefined) {
                predefinedStatus.put(code, this);
            }
        }

        /**
         * Returns the int value of the status
         * @return status code
         */
        public final int code() {
            return code;
        }

        /**
         * Returns {@code true} if the status is either a {@link #isClientError() client error}
         * or {@link #isServerError() server error}
         * @return if this status error
         */
        public boolean isError() {
            return isClientError() || isServerError();
        }

        /**
         * Returns true if the status is server error (5xx)
         * @return if this status server error
         */
        public boolean isServerError() {
            return code / 100 == 5;
        }

        /**
         * Returns true if the status is client error (4xx)
         * @return if this status client error
         */
        public boolean isClientError() {
            return code / 100 == 4;
        }

        /**
         * Returns true if the status is success series (2xx)
         * @return if this status success series
         */
        public boolean isSuccess() {
            return code / 100 == 2;
        }

        /**
         * Returns true if the status is redirect series (3xx)
         * @return if this status redirect series
         */
        public boolean isRedirect() {
            return code / 100 == 3;
        }

        /**
         * Returns true if the status is informational series (1xx)
         * @return is this status informational series
         */
        public boolean isInformational() {
            return code / 100 == 1;
        }

        /**
         * Return a string representation of this status code.
         */
        @Override
        public String toString() {
            return Integer.toString(code);
        }

        @Override
        public int hashCode() {
            return code;
        }

        @Override
        public boolean equals(Object obj) {
            if (obj == this) {
                return true;
            }

            if (obj instanceof Status) {
                Status that = (Status) obj;
                return that.code() == code;
            }

            return false;
        }

        @Override
        public int compareTo(Status o) {
            return code - o.code;
        }

        protected final Object clone() throws CloneNotSupportedException {
            throw new CloneNotSupportedException();
        }

        private Object readResolve() {
            Status predefined = predefinedStatus.get(code);
            return null != predefined ? predefined : this;
        }

        /**
         * Alias of {@link #valueOf(int)}
         * @param n the status number
         * @return the status instance
         */
        public static Status of(int n) {
            return valueOf(n);
        }

        public static Status valueOf(int n) {
            E.illegalArgumentIf(n < 100 || n > 599, "invalid http status code: %s", n);
            Status retVal = predefinedStatus.get(n);
            if (null == retVal) {
                retVal = new Status(n, false);
            }
            return retVal;
        }

        public static List predefined() {
            return C.list(predefinedStatus.values());
        }

        // 1xx Informational

        /**
         * {@code 100 Continue}.
         *
         * @see HTTP/1.1
         */
        public static final Status CONTINUE = new Status(Code.CONTINUE);
        /**
         * {@code 101 Switching Protocols}.
         *
         * @see HTTP/1.1
         */
        public static final Status SWITCHING_PROTOCOLS = new Status(Code.SWITCHING_PROTOCOLS);
        /**
         * {@code 102 Processing}.
         *
         * @see WebDAV
         */
        public static final Status PROCESSING = new Status(Code.PROCESSING);
        /**
         * {@code 103 Checkpoint}.
         *
         * @see A proposal for supporting
         * resumable POST/PUT HTTP requests in HTTP/1.0
         */
        public static final Status CHECKPOINT = new Status(Code.CHECKPOINT);

        // 2xx Success

        /**
         * {@code 200 OK}.
         *
         * @see HTTP/1.1
         */
        public static final Status OK = new Status(Code.OK);
        /**
         * {@code 201 Created}.
         *
         * @see HTTP/1.1
         */
        public static final Status CREATED = new Status(Code.CREATED);
        /**
         * {@code 202 Accepted}.
         *
         * @see HTTP/1.1
         */
        public static final Status ACCEPTED = new Status(Code.ACCEPTED);
        /**
         * {@code 203 Non-Authoritative Information}.
         *
         * @see HTTP/1.1
         */
        public static final Status NON_AUTHORITATIVE_INFORMATION = new Status(Code.NON_AUTHORITATIVE_INFORMATION);
        /**
         * {@code 204 No Content}.
         *
         * @see HTTP/1.1
         */
        public static final Status NO_CONTENT = new Status(Code.NO_CONTENT);
        /**
         * {@code 205 Reset Content}.
         *
         * @see HTTP/1.1
         */
        public static final Status RESET_CONTENT = new Status(Code.RESET_CONTENT);
        /**
         * {@code 206 Partial Content}.
         *
         * @see HTTP/1.1
         */
        public static final Status PARTIAL_CONTENT = new Status(Code.PARTIAL_CONTENT);
        /**
         * {@code 207 Multi-Status}.
         *
         * @see WebDAV
         */
        public static final Status MULTI_STATUS = new Status(Code.MULTI_STATUS);
        /**
         * {@code 208 Already Reported}.
         *
         * @see WebDAV Binding Extensions
         */
        public static final Status ALREADY_REPORTED = new Status(Code.ALREADY_REPORTED);
        /**
         * {@code 226 IM Used}.
         *
         * @see Delta encoding in HTTP
         */
        public static final Status IM_USED = new Status(Code.IM_USED);

        /**
         * {@code 278} - Faked http status to handle redirection on ajax case.
         *
         * @see this stackoverflow
         */
        public static final Status FOUND_AJAX = new Status(Code.FOUND_AJAX);

        // 3xx Redirection

        /**
         * {@code 300 Multiple Choices}.
         *
         * @see HTTP/1.1
         */
        public static final Status MULTIPLE_CHOICES = new Status(Code.MULTIPLE_CHOICES);
        /**
         * {@code 301 Moved Permanently}.
         *
         * @see HTTP/1.1
         */
        public static final Status MOVED_PERMANENTLY = new Status(Code.MOVED_PERMANENTLY);
        /**
         * {@code 302 Found}.
         *
         * @see HTTP/1.1
         */
        public static final Status FOUND = new Status(Code.FOUND);
        /**
         * {@code 302 Moved Temporarily}.
         *
         * @see HTTP/1.0
         * @deprecated In favor of {@link #FOUND} which will be returned from {@code Status.valueOf(302)}
         */
        @Deprecated
        public static final Status MOVED_TEMPORARILY = FOUND;
        /**
         * {@code 303 See Other}.
         *
         * @see HTTP/1.1
         */
        public static final Status SEE_OTHER = new Status(Code.SEE_OTHER);
        /**
         * {@code 304 Not Modified}.
         *
         * @see HTTP/1.1
         */
        public static final Status NOT_MODIFIED = new Status(Code.NOT_MODIFIED);
        /**
         * {@code 305 Use Proxy}.
         *
         * @see HTTP/1.1
         */
        public static final Status USE_PROXY = new Status(Code.USE_PROXY);

        /**
         * {@code 306 Switch Proxy}
         *
         * @see http://getstatuscode.com/306
         */
        public static final Status SWITCH_PROXY = new Status(Code.SWITCH_PROXY);
        /**
         * {@code 307 Temporary Redirect}.
         *
         * @see HTTP/1.1
         */
        public static final Status TEMPORARY_REDIRECT = new Status(Code.TEMPORARY_REDIRECT);
        /**
         * {@code 308 Permanent Redirect}.
         *
         * @see http://getstatuscode.com/308
         */
        public static final Status PERMANENT_REDIRECT = new Status(Code.PERMANENT_REDIRECT);


        // --- 4xx Client Error ---

        /**
         * {@code 400 Bad Request}.
         *
         * @see HTTP/1.1
         */
        public static final Status BAD_REQUEST = new Status(Code.BAD_REQUEST);
        /**
         * {@code 401 Unauthorized}.
         *
         * @see HTTP/1.1
         */
        public static final Status UNAUTHORIZED = new Status(Code.UNAUTHORIZED);
        /**
         * {@code 402 Payment Required}.
         *
         * @see HTTP/1.1
         */
        public static final Status PAYMENT_REQUIRED = new Status(Code.PAYMENT_REQUIRED);
        /**
         * {@code 403 Forbidden}.
         *
         * @see HTTP/1.1
         */
        public static final Status FORBIDDEN = new Status(Code.FORBIDDEN);
        /**
         * {@code 404 Not Found}.
         *
         * @see HTTP/1.1
         */
        public static final Status NOT_FOUND = new Status(Code.NOT_FOUND);
        /**
         * {@code 405 Method Not Allowed}.
         *
         * @see HTTP/1.1
         */
        public static final Status METHOD_NOT_ALLOWED = new Status(Code.METHOD_NOT_ALLOWED);
        /**
         * {@code 406 Not Acceptable}.
         *
         * @see HTTP/1.1
         */
        public static final Status NOT_ACCEPTABLE = new Status(Code.NOT_ACCEPTABLE);
        /**
         * {@code 407 Proxy Authentication Required}.
         *
         * @see HTTP/1.1
         */
        public static final Status PROXY_AUTHENTICATION_REQUIRED = new Status(Code.PROXY_AUTHENTICATION_REQUIRED);
        /**
         * {@code 408 Request Timeout}.
         *
         * @see HTTP/1.1
         */
        public static final Status REQUEST_TIMEOUT = new Status(Code.REQUEST_TIMEOUT);
        /**
         * {@code 409 Conflict}.
         *
         * @see HTTP/1.1
         */
        public static final Status CONFLICT = new Status(Code.CONFLICT);
        /**
         * {@code 410 Gone}.
         *
         * @see HTTP/1.1
         */
        public static final Status GONE = new Status(Code.GONE);
        /**
         * {@code 411 Length Required}.
         *
         * @see HTTP/1.1
         */
        public static final Status LENGTH_REQUIRED = new Status(Code.LENGTH_REQUIRED);
        /**
         * {@code 412 Precondition failed}.
         *
         * @see HTTP/1.1
         */
        public static final Status PRECONDITION_FAILED = new Status(Code.PRECONDITION_FAILED);
        /**
         * {@code 413 Request Entity Too Large}.
         *
         * @see HTTP/1.1
         */
        public static final Status REQUEST_ENTITY_TOO_LARGE = new Status(Code.PAYLOAD_TOO_LARGE);
        /**
         * Alias of {@link #REQUEST_ENTITY_TOO_LARGE}
         */
        public static final Status PAYLOAD_TOO_LARGE = REQUEST_ENTITY_TOO_LARGE;
        /**
         * {@code 414 Request-URI Too Long}.
         *
         * @see HTTP/1.1
         */
        public static final Status REQUEST_URI_TOO_LONG = new Status(Code.URI_TOO_LONG);
        /**
         * Alias of {@link #REQUEST_URI_TOO_LONG}
         */
        public static final Status URI_TOO_LONG = REQUEST_URI_TOO_LONG;
        /**
         * {@code 415 Unsupported Media Type}.
         *
         * @see HTTP/1.1
         */
        public static final Status UNSUPPORTED_MEDIA_TYPE = new Status(Code.UNSUPPORTED_MEDIA_TYPE);
        /**
         * {@code 416 Requested Range Not Satisfiable}.
         *
         * @see HTTP/1.1
         */
        public static final Status REQUESTED_RANGE_NOT_SATISFIABLE = new Status(Code.RANGE_NOT_SATISFIABLE);
        /**
         * Alias of {@link #REQUESTED_RANGE_NOT_SATISFIABLE}
         */
        public static final Status RANGE_NOT_SATISFIABLE = REQUESTED_RANGE_NOT_SATISFIABLE;
        /**
         * {@code 417 Expectation Failed}.
         *
         * @see HTTP/1.1
         */
        public static final Status EXPECTATION_FAILED = new Status(Code.EXPECTATION_FAILED);
        /**
         * {@code 418 I'm a teapot}.
         *
         * @see HTCPCP/1.0
         */
        public static final Status I_AM_A_TEAPOT = new Status(Code.I_AM_A_TEAPOT);
        /**
         * {@code 419 Authentication Timeout}
         * See http://getstatuscode.com/419
         */
        public static final Status AUTHENTICATION_TIMEOUT = new Status(Code.AUTHENTICATION_TIMEOUT);

        /**
         * {@code 420 Method Failure}
         * See http://getstatuscode.com/420
         */
        public static final Status METHOD_FAILURE = new Status(Code.METHOD_FAILURE);
        /**
         * {@code 421 Misdirected request}
         * See Hypertext Transfer Protocol Version 2 (HTTP/2)
         */
        public static final Status MISDIRECTED_REQUEST = new Status(Code.MISDIRECTED_REQUEST);
        /**
         * {@code 422 Unprocessable Entity}.
         *
         * @see WebDAV
         */
        public static final Status UNPROCESSABLE_ENTITY = new Status(Code.UNPROCESSABLE_ENTITY);
        /**
         * {@code 423 Locked}.
         *
         * @see WebDAV
         */
        public static final Status LOCKED = new Status(Code.LOCKED);
        /**
         * {@code 424 Failed Dependency}.
         *
         * @see WebDAV
         */
        public static final Status FAILED_DEPENDENCY = new Status(Code.FAILED_DEPENDENCY);
        /**
         * {@code 426 Upgrade Required}.
         *
         * @see Upgrading to TLS Within HTTP/1.1
         */
        public static final Status UPGRADE_REQUIRED = new Status(Code.UPGRADE_REQUIRED);
        /**
         * {@code 428 Precondition Required}.
         *
         * @see Additional HTTP Status Codes
         */
        public static final Status PRECONDITION_REQUIRED = new Status(Code.PRECONDITION_REQUIRED);
        /**
         * {@code 429 Too Many Requests}.
         *
         * @see Additional HTTP Status Codes
         */
        public static final Status TOO_MANY_REQUESTS = new Status(Code.TOO_MANY_REQUESTS);
        /**
         * {@code 431 Request Header Fields Too Large}.
         *
         * @see Additional HTTP Status Codes
         */
        public static final Status REQUEST_HEADER_FIELDS_TOO_LARGE = new Status(Code.REQUEST_HEADER_FIELDS_TOO_LARGE);

        /**
         * {@code 451 Unavailable for legal reasons}
         *
         * This constant is deprecated. One should use {@link #UNAVAILABLE_FOR_LEGAL_REASONS} instead
         *
         * @see http://getstatuscode.com/451
         */
        @Deprecated
        public static final Status UNAVAILABLE_FOR_LEGAL_REASON = new Status(Code.UNAVAILABLE_FOR_LEGAL_REASON);

        /**
         * {@code 451 Unavailable for legal reasons}
         * @see http://getstatuscode.com/451
         */
        public static final Status UNAVAILABLE_FOR_LEGAL_REASONS = new Status(Code.UNAVAILABLE_FOR_LEGAL_REASONS);
        // --- 5xx Server Error ---

        /**
         * {@code 500 Internal Server Error}.
         *
         * @see HTTP/1.1
         */
        public static final Status INTERNAL_SERVER_ERROR = new Status(Code.INTERNAL_SERVER_ERROR);
        /**
         * {@code 501 Not Implemented}.
         *
         * @see HTTP/1.1
         */
        public static final Status NOT_IMPLEMENTED = new Status(Code.NOT_IMPLEMENTED);
        /**
         * {@code 502 Bad Gateway}.
         *
         * @see HTTP/1.1
         */
        public static final Status BAD_GATEWAY = new Status(Code.BAD_GATEWAY);
        /**
         * {@code 503 Service Unavailable}.
         *
         * @see HTTP/1.1
         */
        public static final Status SERVICE_UNAVAILABLE = new Status(Code.SERVICE_UNAVAILABLE);
        /**
         * {@code 504 Gateway Timeout}.
         *
         * @see HTTP/1.1
         */
        public static final Status GATEWAY_TIMEOUT = new Status(Code.GATEWAY_TIMEOUT);
        /**
         * {@code 505 HTTP Version Not Supported}.
         *
         * @see HTTP/1.1
         */
        public static final Status HTTP_VERSION_NOT_SUPPORTED = new Status(Code.HTTP_VERSION_NOT_SUPPORTED);
        /**
         * {@code 506 Variant Also Negotiates}
         *
         * @see Transparent Content Negotiation
         */
        public static final Status VARIANT_ALSO_NEGOTIATES = new Status(Code.VARIANT_ALSO_NEGOTIATES);
        /**
         * {@code 507 Insufficient Storage}
         *
         * @see WebDAV
         */
        public static final Status INSUFFICIENT_STORAGE = new Status(Code.INSUFFICIENT_STORAGE);
        /**
         * {@code 508 Loop Detected}
         *
         * @see WebDAV Binding Extensions
         */
        public static final Status LOOP_DETECTED = new Status(Code.LOOP_DETECTED);
        /**
         * {@code 510 Not Extended}
         *
         * @see HTTP Extension Framework
         */
        public static final Status NOT_EXTENDED = new Status(Code.NOT_EXTENDED);
        /**
         * {@code 511 Network Authentication Required}.
         *
         * @see Additional HTTP Status Codes
         */
        public static final Status NETWORK_AUTHENTICATION_REQUIRED = new Status(Code.NETWORK_AUTHENTICATION_REQUIRED);

    }

    public static Status status(int n) {
        return Status.valueOf(n);
    }

    public static final class Header implements java.io.Serializable {

        private static final long serialVersionUID = -3987421318751857114L;

        public static final class Names {
            /**
             * {@code "Accept"}
             */
            public static final String ACCEPT = "Accept";
            /**
             * {@code "Accept-Charset"}
             */
            public static final String ACCEPT_CHARSET = "Accept-Charset";
            /**
             * {@code "Accept-Encoding"}
             */
            public static final String ACCEPT_ENCODING = "Accept-Encoding";
            /**
             * {@code "Accept-Language"}
             */
            public static final String ACCEPT_LANGUAGE = "Accept-Language";
            /**
             * {@code "Accept-Ranges"}
             */
            public static final String ACCEPT_RANGES = "Accept-Ranges";
            /**
             * {@code "Accept-Patch"}
             */
            public static final String ACCEPT_PATCH = "Accept-Patch";
            /**
             * {@code "Access-Control-Allow-Origin"}
             */
            public static final String ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin";
            /**
             * {@code "Access-Control-Allow-Methods"}
             */
            public static final String ACCESS_CONTROL_ALLOW_METHODS = "Access-Control-Allow-Methods";
            /**
             * {@code "Access-Control-Allow-Headers"}
             */
            public static final String ACCESS_CONTROL_ALLOW_HEADERS = "Access-Control-Allow-Headers";
            /**
             * {@code "Access-Control-Allow-Credentials"}
             */
            public static final String ACCESS_CONTROL_ALLOW_CREDENTIALS = "Access-Control-Allow-Credentials";
            /**
             * {@code "Access-Control-Expose-Headers"}
             */
            public static final String ACCESS_CONTROL_EXPOSE_HEADERS = "Access-Control-Expose-Headers";
            /**
             * {@code "Access-Control-Max-Age"}
             */
            public static final String ACCESS_CONTROL_MAX_AGE = "Access-Control-Max-Age";
            /**
             * {@code "Access-Control-Request-Method"}
             */
            public static final String ACCESS_CONTROL_REQUEST_METHOD = "Access-Control-Request-Method";
            /**
             * {@code "Access-Control-Request-Headers"}
             */
            public static final String ACCESS_CONTROL_REQUEST_HEADERS = "Access-Control-Request-Headers";

            /**
             * {@code "Age"}
             */
            public static final String AGE = "Age";
            /**
             * {@code "Allow"}
             */
            public static final String ALLOW = "Allow";
            /**
             * {@code "Authorization"}
             */
            public static final String AUTHORIZATION = "Authorization";
            /**
             * {@code "Cache-Control"}
             */
            public static final String CACHE_CONTROL = "Cache-Control";
            /**
             * {@code "Connection"}
             */
            public static final String CONNECTION = "Connection";
            /**
             * {@code "Content-Base"}
             */
            public static final String CONTENT_BASE = "Content-Base";
            /**
             * {@code "Content-Disposition"}
             */
            public static final String CONTENT_DISPOSITION = "Content-Disposition";
            /**
             * {@code "Content-Encoding"}
             */
            public static final String CONTENT_ENCODING = "Content-Encoding";
            /**
             * {@code "Content-MD5"}
             */
            public static final String CONTENT_MD5 = "Content-Md5";
            /**
             * {@code "Content-Language"}
             */
            public static final String CONTENT_LANGUAGE = "Content-Language";
            /**
             * {@code "Content-Length"}
             */
            public static final String CONTENT_LENGTH = "Content-Length";
            /**
             * {@code "Content-Location"}
             */
            public static final String CONTENT_LOCATION = "Content-Location";
            /**
             * {@code "Content-Security-Policy"}
             */
            public static final String CONTENT_SECURITY_POLICY = "Content-Security-Policy";
            /**
             * {@code "Content-Transfer-Encoding"}
             */
            public static final String CONTENT_TRANSFER_ENCODING = "Content-Transfer-Encoding";
            /**
             * {@code "Content-Range"}
             */
            public static final String CONTENT_RANGE = "Content-Range";
            /**
             * {@code "Content-Type"}
             */
            public static final String CONTENT_TYPE = "Content-Type";
            /**
             * {@code "Cookie"}
             */
            public static final String COOKIE = "Cookie";
            /**
             * {@code "Date"}
             */
            public static final String DATE = "Date";
            /**
             * {@code "ETag"}
             */
            public static final String ETAG = "Etag";
            /**
             * {@code "Expect"}
             */
            public static final String EXPECT = "Expect";
            /**
             * {@code "Expires"}
             */
            public static final String EXPIRES = "Expires";
            /**
             * {@code "From"}
             */
            public static final String FROM = "From";
            /**
             * {@code "Front-End-Https"}
             */
            public static final String FRONT_END_HTTPS = "Front-End-Https";
            /**
             * {@code "Host"}
             */
            public static final String HOST = "Host";
            /**
             * {@code "HTTP_CLIENT_IP"}
             */
            public static final String HTTP_CLIENT_IP = "HTTP_CLIENT_IP";
            /**
             * {@code "HTTP_X_FORWARDED_FOR"}
             */
            public static final String HTTP_X_FORWARDED_FOR = "HTTP_X_FORWARDED_FOR";
            /**
             * {@code "If-Match"}
             */
            public static final String IF_MATCH = "If-Match";
            /**
             * {@code "If-Modified-Since"}
             */
            public static final String IF_MODIFIED_SINCE = "If-Modified-Since";
            /**
             * {@code "If-None-Match"}
             */
            public static final String IF_NONE_MATCH = "If-None-Match";
            /**
             * {@code "If-Range"}
             */
            public static final String IF_RANGE = "If-Range";
            /**
             * {@code "If-Unmodified-Since"}
             */
            public static final String IF_UNMODIFIED_SINCE = "If-Unmodified-Since";
            /**
             * {@code "Last-Modified"}
             */
            public static final String LAST_MODIFIED = "Last-Modified";
            /**
             * {@code "Location"}
             */
            public static final String LOCATION = "Location";
            /**
             * {@code "Max-Forwards"}
             */
            public static final String MAX_FORWARDS = "Max-Forwards";
            /**
             * {@code "Origin"}
             */
            public static final String ORIGIN = "Origin";
            /**
             * {@code "Pragma"}
             */
            public static final String PRAGMA = "Pragma";
            /**
             * {@code "Proxy-Authenticate"}
             */
            public static final String PROXY_AUTHENTICATE = "Proxy-Authenticate";
            /**
             * {@code "Proxy-Authorization"}
             */
            public static final String PROXY_AUTHORIZATION = "Proxy-Authorization";
            /**
             * {@code "Proxy-Client-IP"}
             */
            public static final String PROXY_CLIENT_IP = "Proxy-Client-Ip";
            /**
             * {@code "Proxy-Connection"}
             */
            public static final String PROXY_CONNECTION = "Proxy_Connection";
            /**
             * {@code "Range"}
             */
            public static final String RANGE = "Range";
            /**
             * {@code "Referer"}
             */
            public static final String REFERER = "Referer";
            /**
             * {@code "Retry-After"}
             */
            public static final String RETRY_AFTER = "Retry-After";
            /**
             * the header used to put the real ip by load balancers like F5
             * {@code "rlnclientipaddr"}
             */
            public static final String RLNCLIENTIPADDR = "rlnclientipaddr";
            /**
             * {@code "Sec-Websocket-Key1"}
             */
            public static final String SEC_WEBSOCKET_KEY1 = "Sec-Websocket-Key1";
            /**
             * {@code "Sec-Websocket-Key2"}
             */
            public static final String SEC_WEBSOCKET_KEY2 = "Sec-Websocket-Key2";
            /**
             * {@code "Sec-Websocket-Location"}
             */
            public static final String SEC_WEBSOCKET_LOCATION = "Sec-Websocket-Location";
            /**
             * {@code "Sec-Websocket-Origin"}
             */
            public static final String SEC_WEBSOCKET_ORIGIN = "Sec-Websocket-Rrigin";
            /**
             * {@code "Sec-Websocket-Protocol"}
             */
            public static final String SEC_WEBSOCKET_PROTOCOL = "Sec-Websocket-Protocol";
            /**
             * {@code "Sec-Websocket-Version"}
             */
            public static final String SEC_WEBSOCKET_VERSION = "Sec-Websocket-Version";
            /**
             * {@code "Sec-Websocket-Key"}
             */
            public static final String SEC_WEBSOCKET_KEY = "Sec-Websocket-Key";
            /**
             * {@code "Sec-Websocket-Accept"}
             */
            public static final String SEC_WEBSOCKET_ACCEPT = "Sec-Websocket-Accept";
            /**
             * {@code "Server"}
             */
            public static final String SERVER = "Server";
            /**
             * {@code "Set-Cookie"}
             */
            public static final String SET_COOKIE = "Set-Cookie";
            /**
             * {@code "Set-Cookie2"}
             */
            public static final String SET_COOKIE2 = "Set-Cookie2";
            /**
             * {@code "TE"}
             */
            public static final String TE = "TE";
            /**
             * {@code "Trailer"}
             */
            public static final String TRAILER = "Trailer";
            /**
             * {@code "Transfer-Encoding"}
             */
            public static final String TRANSFER_ENCODING = "Transfer-Encoding";
            /**
             * {@code "Upgrade"}
             */
            public static final String UPGRADE = "Upgrade";
            /**
             * {@code "User-Agent"}
             */
            public static final String USER_AGENT = "User-Agent";
            /**
             * {@code "Vary"}
             */
            public static final String VARY = "Vary";
            /**
             * {@code "Via"}
             */
            public static final String VIA = "Via";
            /**
             * {@code "Warning"}
             */
            public static final String WARNING = "Warning";
            /**
             * {@code "WebSocket-Location"}
             */
            public static final String WEBSOCKET_LOCATION = "Websocket-Location";
            /**
             * {@code "WebSocket-Origin"}
             */
            public static final String WEBSOCKET_ORIGIN = "Webwocket-Origin";
            /**
             * {@code "WebSocket-Protocol"}
             */
            public static final String WEBSOCKET_PROTOCOL = "Websocket-Protocol";
            /**
             * {@code "WL-Proxy-Client-IP"}
             */
            public static final String WL_PROXY_CLIENT_IP = "Wl-Proxy-Client-Ip";
            /**
             * {@code "WWW-Authenticate"}
             */
            public static final String WWW_AUTHENTICATE = "WWW-Authenticate";

            /**
             * {@code "X_Requested_With"}
             */
            public static final String X_REQUESTED_WITH = "X-Requested-With";

            /**
             * {@code "X-Forwarded-Host"}
             */
            public static final String X_FORWARDED_HOST = "X-Forwarded-Host";

            /**
             * {@code "X_Forwared_For"}
             */
            public static final String X_FORWARDED_FOR = "X-Forwarded-For";

            /**
             * {@code "X_Forwared_Proto"}
             */
            public static final String X_FORWARDED_PROTO = "X-Forwarded-Proto";
            /**
             * {@code "X-Forwarded-Ssl"}
             */
            public static final String X_FORWARDED_SSL = "X-Forwarded-Ssl";

            /**
             * {@code "X-Http-Method-Override"}
             */
            public static final String X_HTTP_METHOD_OVERRIDE = "X-Http-Method-Override";
            /**
             * {@code "X-Url-Scheme"}
             */
            public static final String X_URL_SCHEME = "X-Url-Scheme";
            /**
             * {@code "X-Xsrf-Token"}
             */
            public static final String X_XSRF_TOKEN = "X-Xsrf-Token";

            private Names() {
                super();
            }
        }

        private String name;
        private C.List values;

        public Header(String name, String value) {
            E.NPE(name);
            this.name = name;
            this.values = C.list(value);
        }

        public Header(String name, String... values) {
            E.NPE(name);
            this.name = name;
            this.values = C.listOf(values);
        }

        public Header(String name, Iterable values) {
            E.NPE(name);
            this.name = name;
            this.values = C.list(values);
        }

        public String name() {
            return name;
        }

        public String value() {
            return values.get(0);
        }

        public C.List values() {
            return values;
        }

        @Override
        public String toString() {
            return values.toString();
        }
    } // eof Header

    /**
     * Specify the format of the requested content type
     */
    public static class Format implements Serializable {

        static {
            registerTypeConverters();
        }

        private static final Map predefined = new LinkedHashMap();

        private int ordinal;
        private String name;
        private String contentType;

        private Format(String name, String contentType) {
            this(name, contentType, true);
        }

        private Format(String name, String contentType, boolean predefined) {
            this.name = name.toLowerCase();
            this.contentType = contentType;
            if (predefined) {
                Format.predefined.put(name, this);
                this.ordinal = ordinal(name);
            } else {
                this.ordinal = -1;
            }
        }

        public final String name() {
            return name;
        }

        public final int ordinal() {
            return ordinal;
        }

        /**
         * Returns the content type string
         *
         * @return the content type string of this format
         */
        public String contentType() {
            return contentType;
        }

        /**
         * Deprecated. Please use {@link #contentType()}
         * @return the content type string of the format
         */
        @Deprecated
        public final String toContentType() {
            return contentType();
        }

        public final String getName() {
            return name();
        }

        public final String getContentType() {
            return contentType();
        }

        public boolean isText() {
            return JSON == this || HTML == this || CSV == this
                    || JAVASCRIPT == this
                    || TXT == this
                    || XML == this
                    || contentType.startsWith("text/")
                    || S.eq("application/json", contentType);
        }

        /**
         * Returns the error message
         *
         * @param message the message
         * @return the message directly
         */
        public String errorMessage(String message) {
            return message;
        }

        @Override
        public int hashCode() {
            if (ordinal != -1) {
                return ordinal;
            }
            return $.hc(name, contentType);
        }

        @Override
        public String toString() {
            return name();
        }

        @Override
        public boolean equals(Object obj) {
            if (obj == this) {
                return true;
            }
            if (obj instanceof Format) {
                Format that = (Format) obj;
                return $.eq(that.name, this.name) && $.eq(that.contentType, this.contentType);
            }
            return false;
        }

        private Object readResolve() {
            if (ordinal == -1) {
                return this;
            }
            return predefined.get(name);
        }

        /**
         * Deprecated. please Use {@link #predefined()}
         * @return an array of predefined Formats
         */
        public static Format[] values() {
            Format[] retVal = new Format[predefined.size()];
            return predefined.values().toArray(retVal);
        }

        public static List predefined() {
            return C.list(predefined.values());
        }

        public static Format of(String name) {
            return valueOf(name);
        }

        public static Format of(String name, String contentType) {
            return valueOf(name, contentType);
        }

        public static Format valueOf(String name) {
            name = name.toLowerCase();
            if (name.startsWith(".")) {
                name = S.afterLast(name, ".");
            }
            return predefined.get(name.toLowerCase());
        }

        public static Format valueOf(String name, String contentType) {
            Format retVal = valueOf(name);
            if (null != retVal) {
                return retVal;
            }
            E.illegalArgumentIf(S.blank(name), "name cannot be blank string");
            E.illegalArgumentIf(S.blank(contentType), "content type cannot be blank string");
            name = name.toLowerCase();
            if (name.startsWith(".")) {
                name = S.afterLast(name, ".");
            }
            return new Format(name, contentType, false);
        }

        public static Format resolve(Format def, String accept) {
            E.NPE(def);
            return resolve_(def, accept);
        }

        public static Format resolve(Iterable accepts) {
            return resolve(Format.HTML, accepts);
        }

        public static Format resolve(Format def, Iterable accepts) {
            Format retVal;
            for (String s : accepts) {
                retVal = resolve_(null, s);
                if (null != retVal) {
                    return retVal;
                }
            }
            return $.ifNullThen(def, Format.HTML);
        }

        public static Format resolve(String... accepts) {
            return resolve(Format.HTML, accepts);
        }

        public static Format resolve(Format def, String... accepts) {
            Format retVal;
            for (String s : accepts) {
                retVal = resolve_(null, s);
                if (null != retVal) {
                    return retVal;
                }
            }
            return $.ifNullThen(def, Format.HTML);
        }

        /**
         * Resolve {@code Format} instance out of an http "Accept" header.
         *
         * @param accept the value of http "Accept" header
         * @return an {@code Format} instance
         */
        public static Format resolve(String accept) {
            return resolve_(Format.UNKNOWN, accept);
        }


        public static String toContentType(String fmt) {
            Format f = predefined.get(fmt.toLowerCase());
            if (null == f) {
                f = HTML;
            }
            return f.contentType();
        }

        private static int ordinal(String s) {
            int l = s.length(), h = 0;
            for (int i = 0; i < l; ++i) {
                char c = s.charAt(i);
                h = 31 * h + c;
            }
            return h;
        }

        private static Format resolve_(Format def, String contentType) {
            Format fmt = def;
            if (S.blank(contentType)) {
                fmt = HTML;
            } else if (contentType.contains("application/xhtml") || contentType.contains("text/html") || contentType.startsWith("*/*")) {
                fmt = HTML;
            } else if (contentType.contains("text/css")) {
                fmt = CSS;
            } else if (contentType.contains("application/json") || contentType.contains("text/javascript")) {
                fmt = JSON;
            } else if (contentType.contains("application/x-www-form-urlencoded")) {
                fmt = FORM_URL_ENCODED;
            } else if (contentType.contains("multipart/form-data") || contentType.contains("multipart/mixed")) {
                fmt = FORM_MULTIPART_DATA;
            } else if (contentType.contains("image")) {
                if (contentType.contains("png")) {
                    fmt = PNG;
                } else if (contentType.contains("jpg") || contentType.contains("jpeg")) {
                    fmt = JPG;
                } else if (contentType.contains("gif")) {
                    fmt = GIF;
                } else if (contentType.contains("svg")) {
                    fmt = SVG;
                } else if (contentType.contains("ico")) {
                    fmt = ICO;
                } else if (contentType.contains("bmp")) {
                    fmt = BMP;
                } else {
                    // just specify an arbitrary sub type
                    // see https://superuser.com/questions/979135/is-there-a-generic-mime-type-for-all-image-files
                    fmt = PNG;
                }
            } else if (contentType.contains("application/xml") || contentType.contains("text/xml")) {
                fmt = XML;
            } else if (contentType.contains("text/plain")) {
                fmt = TXT;
            } else if (contentType.contains("csv") || contentType.contains("comma-separated-values")) {
                fmt = CSV;
            } else if (contentType.contains("ms-excel")) {
                fmt = XLS;
            } else if (contentType.contains("spreadsheetml")) {
                fmt = XLSX;
            } else if (contentType.contains("pdf")) {
                fmt = PDF;
            } else if (contentType.contains("msword")) {
                fmt = DOC;
            } else if (contentType.contains("wordprocessingml")) {
                fmt = DOCX;
            } else if (contentType.contains("rtf")) {
                fmt = RTF;
            } else if (contentType.contains("audio")) {
                if (contentType.contains("mpeg3")) {
                    fmt = MP3;
                } else if (contentType.contains("mp")) {
                    fmt = MPA;
                } else if (contentType.contains("mod")) {
                    fmt = MOD;
                } else if (contentType.contains("wav")) {
                    fmt = WAV;
                } else if (contentType.contains("ogg")) {
                    fmt = OGA;
                } else {
                    // just specify an arbitrary sub type
                    // see https://superuser.com/questions/979135/is-there-a-generic-mime-type-for-all-image-files
                    fmt = WAV;
                }
            } else if (contentType.contains("video")) {
                if (contentType.contains("mp4")) {
                    fmt = MP4;
                } else if (contentType.contains("webm")) {
                    fmt = WEBM;
                } else if (contentType.contains("ogg")) {
                    fmt = OGV;
                } else if (contentType.contains("mov")) {
                    fmt = MOV;
                } else if (contentType.contains("mpeg")) {
                    fmt = MPG;
                } else if (contentType.contains("x-flv")) {
                    fmt = FLV;
                } else {
                    // just specify an arbitrary sub type
                    // see https://superuser.com/questions/979135/is-there-a-generic-mime-type-for-all-image-files
                    fmt = MP4;
                }
            }

            return fmt;
        }

        static {
            try {
                InputStream is = H.class.getResourceAsStream("mime-types.properties");
                Properties types = new Properties();
                types.load(is);
                for (Object k : types.keySet()) {
                    String fmt = k.toString();
                    String contentType = types.getProperty(fmt);
                    new Format(fmt, contentType);
                }
            } catch (IOException e) {
                throw E.ioException(e);
            }
        }

        /**
         * The "text/html" content format
         */
        public static final Format HTML = valueOf("html");
        /**
         * Deprecated, please use {@link #HTML}
         */
        @Deprecated
        public static final Format html = HTML;

        /**
         * The "text/xml" content format
         */
        public static final Format XML = valueOf("xml");
        /**
         * Deprecated, please use {@link #XML}
         */
        @Deprecated
        public static final Format xml = XML;

        /**
         * The "application/json" content format
         */
        public static final Format JSON = new Format("json", "application/json") {
            @Override
            public String errorMessage(String message) {
                return S.fmt("{\"error\": \"%s\"}", message);
            }
        };
        /**
         * Deprecated. Please use {@link #JSON}
         */
        @Deprecated
        public static final Format json = JSON;

        /**
         * The "text/css" content format
         */
        public static final Format CSS = new Format("css", "text/css");

        /**
         * The "application/javascript" content format
         */
        public static final Format JAVASCRIPT = new Format("javascript", "application/javascript") {
            @Override
            public String errorMessage(String message) {
                return "alert(" + message + ");";
            }
        };

        /**
         * The "application/vnd.ms-excel" content format
         */
        public static final Format XLS = valueOf("xls");
        /**
         * Deprecated. Please use {@link #XLS}
         */
        public static final Format xls = XLS;

        /**
         * The "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" content format
         */
        public static final Format XLSX = valueOf("xlsx");
        /**
         * Deprecated. Please use {@link #XLSX}
         */
        public static final Format xlsx = XLSX;

        /**
         * The "application/vnd.ms-word" content format
         */
        public static final Format DOC = valueOf("doc");

        /**
         * Deprecated. Please use {@link #DOC}
         */
        public static final Format doc = DOC;

        /**
         * The "application/vnd.openxmlformats-officedocument.wordprocessingml.document" content format
         */
        public static final Format DOCX = valueOf("docx");

        /**
         * Deprecated. Please use {@link #DOCX}
         */
        public static final Format docx = DOCX;

        /**
         * The "text/csv" content format
         */
        public static final Format CSV = valueOf("csv");

        /**
         * Deprecated, please use {@link #CSV}
         */
        @Deprecated
        public static final Format csv = CSV;

        /**
         * The "text/plain" content format
         */
        public static final Format TXT = valueOf("txt");

        /**
         * Deprecated, please use {@link #TXT}
         */
        @Deprecated
        public static final Format txt = TXT;

        /**
         * The "application/pdf" content format
         */
        public static final Format PDF = valueOf("pdf");
        /**
         * Deprecated, please use {@link #PDF}
         */
        @Deprecated
        public static final Format pdf = PDF;

        /**
         * The "application/rtf" content format
         */
        public static final Format RTF = valueOf("rtf");
        /**
         * Deprecated, please use {@link #RTF}
         */
        @Deprecated
        public static final Format rtf = RTF;

        // --- common images
        public static final Format GIF = valueOf("gif");
        public static final Format ICO = valueOf("ico");
        public static final Format JPG = valueOf("jpg");
        public static final Format BMP = valueOf("bmp");
        public static final Format PNG = valueOf("png");
        public static final Format SVG = valueOf("svg");
        public static final Format TIF = valueOf("tif");

        // -- common videos
        public static final Format MOV = valueOf("mov");
        public static final Format MP4 = valueOf("mp4");
        public static final Format MPG = valueOf("mpg");
        public static final Format AVI = valueOf("avi");
        public static final Format FLV = valueOf("flv");
        public static final Format OGV = valueOf("ogv");
        public static final Format WEBM = valueOf("webm");

        // -- common audios
        public static final Format MP3 = valueOf("mp3");
        public static final Format MPA = valueOf("mpa");
        public static final Format WAV = valueOf("wav");
        public static final Format MOD = valueOf("mod");
        public static final Format OGA = valueOf("oga");


        /**
         * The "application/x-www-form-urlencoded" content format
         */
        public static final Format FORM_URL_ENCODED = new Format("form_url_encoded", "application/x-www-form-urlencoded");
        /**
         * Deprecated, please use {@link #FORM_URL_ENCODED}
         */
        @Deprecated
        public static final Format form_url_encoded = FORM_URL_ENCODED;

        /**
         * The "multipart/form-data" content format
         */
        public static final Format FORM_MULTIPART_DATA = new Format("form_multipart_data", "multipart/form-data");
        /**
         * Deprecated, please use {@link #FORM_MULTIPART_DATA}
         */
        @Deprecated
        public static final Format form_multipart_data = FORM_MULTIPART_DATA;

        public static final Format BINARY = new Format("binary", "application/octet-stream");

        /**
         * The "unknown" content format. Use default content type: "text/html"
         */
        public static final Format UNKNOWN = new Format("unknown", "text/html") {
            @Override
            public String contentType() {
                return "text/html";
            }

            @Override
            public String toString() {
                return name();
            }
        };
        /**
         * Deprecated, please use {@link #UNKNOWN}
         */
        @Deprecated
        public static final Format unknown = UNKNOWN;

        public static final class Ordinal {
            public static final int HTML = Format.HTML.ordinal;
            public static final int XML = Format.XML.ordinal;
            public static final int JSON = Format.JSON.ordinal;
            public static final int XLS = Format.XLS.ordinal;
            public static final int XLSX = Format.XLSX.ordinal;
            public static final int DOC = Format.DOC.ordinal;
            public static final int DOCX = Format.DOCX.ordinal;
            public static final int CSV = Format.CSV.ordinal;
            public static final int TXT = Format.TXT.ordinal;
            public static final int PDF = Format.PDF.ordinal;
            public static final int RTF = Format.RTF.ordinal;
            public static final int GIF = Format.GIF.ordinal;
            public static final int ICO = Format.ICO.ordinal;
            public static final int JPG = Format.JPG.ordinal;
            public static final int BMP = Format.BMP.ordinal;
            public static final int PNG = Format.PNG.ordinal;
            public static final int SVG = Format.SVG.ordinal;
            public static final int TIF = Format.TIF.ordinal;
            public static final int MOV = Format.MOV.ordinal;
            public static final int MP4 = Format.MP4.ordinal;
            public static final int MPG = Format.MPG.ordinal;
            public static final int AVI = Format.AVI.ordinal;
            public static final int FLV = Format.FLV.ordinal;
            public static final int MP3 = Format.MP3.ordinal;
            public static final int MPA = Format.MPA.ordinal;
            public static final int WAV = Format.WAV.ordinal;
            public static final int FORM_URL_ENCODED = Format.FORM_URL_ENCODED.ordinal;
            public static final int FORM_MULTIPART_DATA = Format.FORM_MULTIPART_DATA.ordinal;
        }
    }

    public enum MediaType {
        CSS, CSV, DOC, DOCX, HTML, JAVASCRIPT, JSON, PDF, TXT, XLS, XLSX, XML;
        private Format fmt;
        private MediaType() {
            fmt = Format.valueOf(name());
        }

        public Format format() {
            return fmt;
        }

        @Override
        public String toString() {
            return fmt.contentType();
        }
    }

    public static Format format(String name) {
        return Format.valueOf(name);
    }

    public static Format format(String name, String contentType) {
        return Format.valueOf(name, contentType);
    }

    /**
     * The HTTP cookie
     */
    public static class Cookie implements Serializable {

        private static final long serialVersionUID = 5325872881041347558L;

        private String name;

        // default is non-persistent cookie
        private int maxAge = -1;

        private boolean secure = HttpConfig.secure();

        private String path = "/";

        private String domain;

        private String value;

        private boolean httpOnly = true;

        private int version;

        private Date expires;

        private String comment;

        public Cookie(String name) {
            this(name, "");
        }

        public Cookie(String name, String value) {
            E.NPE(name);
            this.name = name;
            this.value = null == value ? "" : value;
        }

        public Cookie(String name, String value, String path) {
            E.NPE(name);
            this.name = name;
            this.value = null == value ? "" : value;
            this.path = path;
        }

        public Cookie(String name, String value, int maxAge, boolean secure, String path, String domain, boolean httpOnly) {
            this(name, value);
            this.maxAge = maxAge;
            this.secure = secure;
            this.path = path;
            this.domain = domain;
            this.httpOnly = httpOnly;
        }

        /**
         * Returns the name of the cookie. Cookie name
         * cannot be changed after created
         *
         * @return the name
         */
        public String name() {
            return name;
        }

        /**
         * Returns the value of the cookie
         * @return the value
         */
        public String value() {
            return value;
        }

        /**
         * Set a value to a cookie and the return {@code this} cookie
         *
         * @param value the value to be set to the cookie
         * @return this cookie
         */
        public Cookie value(String value) {
            this.value = value;
            return this;
        }

        /**
         * Returns the domain of the cookie
         *
         * @return domain
         */
        public String domain() {
            return domain;
        }

        /**
         * Set the domain of the cookie
         *
         * @param domain the  domain string
         * @return this cookie
         */
        public Cookie domain(String domain) {
            this.domain = domain;
            return this;
        }

        /**
         * Returns the path on the server
         * to which the browser returns this cookie. The
         * cookie is visible to all subpaths on the server.
         *
         * @return the path
         * @see #path(String)
         */
        public String path() {
            return path;
        }

        /**
         * Specifies a path for the cookie
         * to which the client should return the cookie.
         * 
         * 

The cookie is visible to all the pages in the directory * you specify, and all the pages in that directory's subdirectories. * *

Consult RFC 2109 (available on the Internet) for more * information on setting path names for cookies. * * @param uri a String specifying a path * @return this cookie after path is set * @see #path */ public Cookie path(String uri) { this.path = uri; return this; } /** * Returns the maximum age of cookie specified in seconds. If * maxAge is set to {@code -1} then the cookie will persist until * browser shutdown * * @return the max age of the cookie */ public int maxAge() { return maxAge; } /** * Set the max age of the cookie in seconds. *

A positive value indicates that the cookie will expire * after that many seconds have passed. Note that the value is * the maximum age when the cookie will expire, not the cookie's * current age. * *

A negative value means * that the cookie is not stored persistently and will be deleted * when the Web browser exits. A zero value causes the cookie * to be deleted. * * @param maxAge the max age to be set * @return this instance * @see #maxAge() */ public Cookie maxAge(int maxAge) { this.maxAge = maxAge; return this; } public Date expires() { if (null != expires) { return expires; } if (maxAge < 0) { return null; } return new Date($.ms() + maxAge * 1000); } public Cookie expires(Date expires) { this.expires = expires; if (null != expires && -1 == maxAge) { maxAge = (int) ((expires.getTime() - $.ms()) / 1000); } return this; } /** * Returns true if the browser is sending cookies * only over a secure protocol, or false if the * browser can send cookies using any protocol. * * @return if the cookie is secure * @see #secure(boolean) */ public boolean secure() { return secure; } /** * Indicates to the browser whether the cookie should only be sent * using a secure protocol, such as HTTPS or SSL. * *

The default value is false. * * @param secure the cookie secure requirement * @return this cookie instance */ public Cookie secure(boolean secure) { this.secure = secure; return this; } /** * Returns the version of the protocol this cookie complies * with. Version 1 complies with RFC 2109, * and version 0 complies with the original * cookie specification drafted by Netscape. Cookies provided * by a browser use and identify the browser's cookie version. * * @return 0 if the cookie complies with the * original Netscape specification; 1 * if the cookie complies with RFC 2109 * @see #version(int) */ public int version() { return version; } /** * Sets the version of the cookie protocol that this Cookie complies * with. * *

Version 0 complies with the original Netscape cookie * specification. Version 1 complies with RFC 2109. * *

Since RFC 2109 is still somewhat new, consider * version 1 as experimental; do not use it yet on production sites. * * @param v 0 if the cookie should comply with the original Netscape * specification; 1 if the cookie should comply with RFC 2109 * @return this cookie instance * @see #version() */ public Cookie version(int v) { this.version = v; return this; } public boolean httpOnly() { return httpOnly; } public Cookie httpOnly(boolean httpOnly) { this.httpOnly = httpOnly; return this; } public String comment() { return comment; } public Cookie comment(String comment) { this.comment = comment; return this; } public Cookie decr() { E.illegalStateIfNot(S.isInt(value), "cannot call decr() on cookie which value is not an integer"); int n = Integer.parseInt(value) - 1; value = S.string(n); return this; } public Cookie decr(int n) { E.illegalStateIfNot(S.isInt(value), "cannot call decr(int) on cookie which value is not an integer"); int n0 = Integer.parseInt(value) - n; value = S.string(n0); return this; } public Cookie incr() { E.illegalStateIfNot(S.isInt(value), "cannot call incr() on cookie which value is not an integer"); int n = Integer.parseInt(value) + 1; value = S.string(n); return this; } public Cookie incr(int n) { E.illegalStateIfNot(S.isInt(value), "cannot call incr(int) on cookie which value is not an integer"); int n0 = Integer.parseInt(value) + n; value = S.string(n0); return this; } public Cookie addToResponse() { H.Response resp = H.Response.current(); E.illegalStateIf(null == resp, "No current response."); resp.addCookie(this); return this; } /** * The function object namespace */ public static enum F { ; public static final $.F2 ADD_TO_RESPONSE = new $.F2() { @Override public Void apply(Cookie cookie, Response response) throws NotAppliedException, $.Break { response.addCookie(cookie); return null; } }; } } // eof Cookie public static class KV implements Serializable { private static final long serialVersionUID = 891504755320699989L; protected Map data = new HashMap<>(); private boolean dirty = false; private KV() {} private KV(Map data) { E.NPE(data); this.data = data; } /** * Associate a string value with the key specified during * initialization. The difference between calling {@code load} * and {@link #put(String, String)} is the former will not change * the dirty tag * @param key the key * @param val the value * @return this instance */ public T load(String key, String val) { E.illegalArgumentIf(key.contains(":")); data.put(key, val); return me(); } /** * Associate a string value with the key specified. * @param key the key * @param val the value * @return this instance */ public T put(String key, String val) { E.illegalArgumentIf(key.contains(":")); dirty = true; return load(key, val); } /** * Associate an Object value's String representation with the * key specified. If the object is {@code null} then {@code null} * is associated with the key specified * @param key the key * @param val the value * @return this instance */ public T put(String key, Object val) { String valStr = null == val ? null : val.toString(); return put(key, valStr); } /** * Returns the string value associated with the key specified * @param key the key * @return the value string */ public String get(String key) { return data.get(key); } /** * Returns the key set of internal data map * @return key set */ public Set keySet() { return data.keySet(); } /** * Returns the entry set of internal data map * * @return entry set */ public Set> entrySet() { return data.entrySet(); } /** * Returns {@code true} if internal data map is empty * @return if data is empty */ public boolean isEmpty() { return data.isEmpty(); } /** * Indicate if the KV has been changed * * @return {@code true} if this instance has been changed */ public boolean dirty() { return dirty; } /** * Alias of {@link #dirty()} * @return true if data has been changed */ public boolean changed() { return dirty; } /** * Returns true if the internal data map is empty * @return true if data is empty or false otherwise */ public boolean empty() { return data.isEmpty(); } /** * Returns true if an association with key specified exists in * the internal map * @param key the key * @return true if data contains key or false otherwise */ public boolean containsKey(String key) { return data.containsKey(key); } /** * Alias of {@link #containsKey(String)} * @param key the key * @return true if data contains the key or false otherwise */ public boolean contains(String key) { return containsKey(key); } /** * Returns the number of assoications stored in the internal map * @return size */ public int size() { return data.size(); } /** * Release an association with key specified * @param key specify the k-v pair that should be removed from internal map * @return this instance */ public T remove(String key) { data.remove(key); return me(); } /** * Clear the internal data map. In other words, all * Key/Value association stored in this instance has been * release * * @return this instance */ public T clear() { data.clear(); return me(); } @Override public int hashCode() { return data.hashCode(); } @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (obj instanceof KV) { KV that = (KV) obj; return that.data.equals(this.data); } return false; } @Override public String toString() { return data.toString(); } protected T me() { return (T) this; } } /** * Defines a data structure to encapsulate a stateless session which * accept only {@code String} type value, and will be persisted at * client side as a cookie. This means the entire size of the * information stored in session including names and values shall * not exceed 4096 bytes. * *

To store typed value or big value, use the cache methods * of the session class. However it is subject to the implementation * to decide whether cache methods are provided and how it is * implemented

*/ public static final class Session extends KV { /** * Session identifier */ public static final String KEY_ID = "___ID"; /** * Stores the expiration date in the session */ public static final String KEY_EXPIRATION = "___TS"; /** * Stores the authenticity token in the session */ public static final String KEY_AUTHENTICITY_TOKEN = "___AT"; /** * Used to mark if a session has just expired */ public static final String KEY_EXPIRE_INDICATOR = "___expired"; /** * Stores the fingerprint to the session */ public static final String KEY_FINGER_PRINT = "__FP"; private static final long serialVersionUID = -423716328552054481L; private String id; public Session() { } /** * Returns the session identifier * @return id of the session */ public String id() { if (null == id) { id = data.get(KEY_ID); if (null == id) { id = UUID.randomUUID().toString(); put(KEY_ID, id()); } } return id; } /** * Decrement the value associated with `key` by one. * * If there is no value associated with `key` then put number `-1` for `key` in * the session. * * If the existing value associated with `key` is not an integer then raise `IllegalStateExeption`. * * @param key * the key to get the existing value and associate the new value * @return this session instance */ public Session decr(String key) { return decr(key, 1); } /** * Decrement the value associated with `key` by `n`. * * If there is no value associated with `key` then put number `-n` for `key` in * the session. * * If the existing value associated with `key` is not an integer then raise `IllegalStateExeption`. * * @param key * the key to get the existing value and associate the new value * @return this session instance */ public Session decr(String key, int n) { String v = get(key); if (null == v) { put(key, -n); } else { E.illegalStateIfNot(S.isInt(v), "Cannot decr on value that is no an integer"); int n0 = Integer.parseInt(v); put(key, n0 - n); } return this; } /** * Increment the value associated with `key` by one. * * If there is no value associated with `key` then put number `1` for `key` in * the session. * * If the existing value associated with `key` is not an integer then raise `IllegalStateExeption`. * * @param key * the key to get the existing value and associate the new value * @return this session instance */ public Session incr(String key) { return incr(key, 1); } /** * Increment the value associated with `key` by `n`. * * If there is no value associated with `key` then put number `n` for `key` in * the session. * * If the existing value associated with `key` is not an integer then raise `IllegalStateExeption`. * * @param key * the key to get the existing value and associate the new value * @return this session instance */ public Session incr(String key, int n) { String v = get(key); if (null == v) { put(key, n); } else { E.illegalStateIfNot(S.isInt(v), "Cannot incr on value that is no an integer"); int n0 = Integer.parseInt(v); put(key, n0 + n); } return this; } // ------- regular session attribute operations --- /** * Returns {@code true} if the session is empty. e.g. * does not contain anything else than the timestamp */ public boolean empty() { return super.empty() || (containsKey(KEY_EXPIRATION) && size() == 1); } public boolean isEmpty() { return super.empty() || (containsKey(KEY_EXPIRATION) && size() == 1); } /** * Check if the session is expired. A session is considered * to be expired if it has a timestamp and the timestamp is * non negative number and is less than {@link System#currentTimeMillis()} * * @return {@code true} if the session is expired */ public boolean expired() { long expiry = expiry(); if (expiry < 0) return false; return (expiry < System.currentTimeMillis()); } /** * Returns the expiration time in milliseconds of this session. If * there is no expiration set up, then this method return {@code -1} * * @return the difference, measured in milliseconds, between * the expiry of the session and midnight, January 1, * 1970 UTC, or {@code -1} if the session has no * expiry */ public long expiry() { String s = get(KEY_EXPIRATION); if (S.blank(s)) return -1; return Long.parseLong(s); } /** * Set session expiry in milliseconds * * @param expiry the difference, measured in milliseconds, between * the expiry and midnight, January 1, 1970 UTC. * @return the session instance */ public Session expireOn(long expiry) { put(KEY_EXPIRATION, S.string(expiry)); return this; } // ------- eof regular session attribute operations --- @Override public boolean equals(Object obj) { boolean superEq = super.equals(obj); return superEq && (obj instanceof H.Session) && $.eq(((Session) obj).id, id); } // ------- cache operations ------ /* * Attach session id to a cache key */ private String k(String key) { return S.builder(id()).append(key).toString(); } private static volatile CacheService cs; private static CacheService cs() { if (null != cs) return cs; synchronized (H.class) { if (null == cs) { cs = HttpConfig.sessionCache(); } return cs; } } /** * Store an object into cache using key specified. The key will be * appended with session id, so that it distinct between caching * using the same key but in different user sessions. * *

The object is cached for {@link org.osgl.cache.CacheService#setDefaultTTL(int) default} ttl

* * @param key the key to cache the object * @param obj the object to be cached * @return this session instance */ public Session cache(String key, Object obj) { cs().put(k(key), obj); return this; } /** * Store an object into cache with expiration specified * * @param key the key to cache the object * @param obj the object to be cached * @param expiration specify the cache expiration in seconds * @return this session instance * @see #cache(String, Object) */ public Session cache(String key, Object obj, int expiration) { cs().put(k(key), obj, expiration); return this; } /** * Store an object into cache for 1 hour * * @param key the key to cache the object * @param obj the object to be cached * @return the session instance */ public Session cacheFor1Hr(String key, Object obj) { return cache(key, obj, 60 * 60); } /** * Store an object into cache for 30 minutes * * @param key the key to cache the object * @param obj the object to be cached * @return the session instance */ public Session cacheFor30Min(String key, Object obj) { return cache(key, obj, 30 * 60); } /** * Store an object into cache for 10 minutes * * @param key the key to cache the object * @param obj the object to be cached * @return the session instance */ public Session cacheFor10Min(String key, Object obj) { return cache(key, obj, 10 * 60); } /** * Store an object into cache for 1 minutes * * @param key the key to cache the object * @param obj the object to be cached * @return the session instance */ public Session cacheFor1Min(String key, Object obj) { return cache(key, obj, 60); } /** * Evict an object from cache * * @param key the key to cache the object * @return this session instance */ public Session evict(String key) { cs().evict(k(key)); return this; } /** * Retrieve an object from cache by key. The key * will be attached with session id * * @param key the key to get the cached object * @param the object type * @return the object in the cache, or {@code null} * if it cannot find the object by key * specified * @see #cache(String, Object) */ public T cached(String key) { return cs().get(k(key)); } /** * Retrieve an object from cache by key. The key * will be attached with session id * * @param key the key to get the cached object * @param clz the class to specify the return type * @param the object type * @return the object in the cache, or {@code null} * if it cannot find the object by key * specified * @see #cache(String, Object) */ public T cached(String key, Class clz) { return cs().get(k(key)); } // ------- eof cache operations ------ /** * Return a session instance of the current execution context, * For example from a {@link java.lang.ThreadLocal} * * @return the current session instance */ public static Session current() { return current.session(); } /** * Set a session instance into the current execution context, * for example into a {@link java.lang.ThreadLocal} * * @param session the session to be set to current execution context */ public static void current(Session session) { current.session(session); } // used to parse session data persisted in the cookie value private static final Pattern _PARSER = Pattern.compile(S.HSEP + "([^:]*):([^" + S.HSEP + "]*)" + S.HSEP); /** * Resolve a Session instance from a session cookie * * @param sessionCookie the cookie corresponding to a session * @param ttl session time to live in seconds * @return a Session instance * @see #serialize(String) */ public static Session resolve(Cookie sessionCookie, int ttl) { Session session = new Session(); long expiration = System.currentTimeMillis() + ttl * 1000; boolean hasTtl = ttl > -1; String value = null == sessionCookie ? null : sessionCookie.value(); if (S.blank(value)) { if (hasTtl) { session.expireOn(expiration); } } else { int firstDashIndex = value.indexOf("-"); if (firstDashIndex > -1) { String signature = value.substring(0, firstDashIndex); String data = value.substring(firstDashIndex + 1); if (S.eq(signature, sign(data))) { String sessionData = Codec.decodeUrl(data, Charsets.UTF_8); Matcher matcher = _PARSER.matcher(sessionData); while (matcher.find()) { session.put(matcher.group(1), matcher.group(2)); } } } if (hasTtl && session.expired()) { session = new Session().expireOn(expiration); } } return session; } /** * Serialize this session into a cookie. Note the cookie * returned has only name, value maxAge been set. It's up * to the caller to set the secure, httpOnly and path * attributes * * @param sessionKey the cookie name for the session cookie * @return a cookie captures this session's information or {@code null} if * this session is empty or this session hasn't been changed and * there is no expiry * @see #resolve(org.osgl.http.H.Cookie, int) */ public Cookie serialize(String sessionKey) { long expiry = expiry(); boolean hasTtl = expiry > -1; boolean expired = !hasTtl && expiry < System.currentTimeMillis(); if (!changed() && !hasTtl) return null; if (empty() || expired) { // empty session, delete the session cookie return new H.Cookie(sessionKey).maxAge(0); } StringBuilder sb = S.builder(); for (String k : keySet()) { sb.append(S.HSEP); sb.append(k); sb.append(":"); sb.append(get(k)); sb.append(S.HSEP); } String data = Codec.encodeUrl(sb.toString(), Charsets.UTF_8); String sign = sign(data); String value = S.builder(sign).append("-").append(data).toString(); Cookie cookie = new Cookie(sessionKey).value(value); if (expiry > -1L) { int ttl = (int) ((expiry - System.currentTimeMillis()) / 1000); cookie.maxAge(ttl); } return cookie; } private static String sign(String s) { return Crypto.sign(s, s.getBytes(Charsets.UTF_8)); } } // eof Session /** * A Flash represent a storage scope that attributes inside is valid only * for one session interaction. This feature of flash makes it very good * for server to pass one time information to client, e.g. form submission * error message etc. * *

Like {@link org.osgl.http.H.Session}, you can store only String type * information to flash, and the total number of information stored * including keys and values shall not exceed 4096 bytes as flash is * persisted as cookie in browser

*/ public static final class Flash extends KV { // used to parse flash data persisted in the cookie value private static final Pattern _PARSER = Session._PARSER; private static final long serialVersionUID = 5609789840171619780L; /** * Stores the data that will be output to cookie so next time the user's request income * they will be available for the application to access */ private Map out = new HashMap<>(); /** * Add an attribute to the flash scope. The data is * added to both data buffer and the out buffer * * @param key the key to index the attribute * @param value the value of the attribute * @return the flash instance */ public Flash put(String key, String value) { out.put(key, value); return super.put(key, value); } /** * Add an attribute to the flash scope. The value is in Object * type, however it will be convert to its {@link Object#toString() string * representation} before put into the flash * * @param key the key to index the attribute * @param value the value to be put into the flash * @return this flash instance */ public Flash put(String key, Object value) { return put(key, null == value ? null : value.toString()); } /** * Add an attribute to the flash's current scope. Meaning when next time * the user request to the server, the attribute will not be there anymore. * * @param key the attribute key * @param value the attribute value * @return the flash instance */ public Flash now(String key, String value) { return super.put(key, value); } /** * Add an "error" message to the flash scope * * @param message the error message * @return the flash instance * @see #put(String, Object) */ public Flash error(String message) { return put("error", message); } /** * Add an "error" message to the flash scope, with * optional format arguments * * @param message the message template * @param args the format arguments * @return this flash instance */ public Flash error(String message, Object... args) { return put("error", S.fmt(message, args)); } /** * Get the "error" message that has been added to * the flash scope. * * @return the "error" message or {@code null} if * no error message has been added to the flash */ public String error() { return get("error"); } /** * Add a "success" message to the flash scope * * @param message the error message * @return the flash instance * @see #put(String, Object) */ public Flash success(String message) { return put("success", message); } /** * Add a "success" message to the flash scope, with * optional format arguments * * @param message the message template * @param args the format arguments * @return this flash instance */ public Flash success(String message, Object... args) { return put("success", S.fmt(message, args)); } /** * Get the "success" message that has been added to * the flash scope. * * @return the "success" message or {@code null} if * no success message has been added to the flash */ public String success() { return get("success"); } /** * Discard a data from the output buffer of the flash but * the data buffer is remain untouched. Meaning * the app can still get the data {@link #put(String, Object)} * into the flash scope, however they will NOT * be write to the client cookie, thus the next * time client request the server, the app will * not be able to get the info anymore * * @param key the key to the data to be discarded * @return the flash instance */ public Flash discard(String key) { out.remove(key); return this; } /** * Discard the whole output buffer of the flash but * the data buffer is remain untouched. Meaning * the app can still get the data {@link #put(String, Object)} * into the flash scope, however they will NOT * be write to the client cookie, thus the next * time client request the server, the app will * not be able to get those info anymore * * @return the flash instance */ public Flash discard() { out.clear(); return this; } /** * Keep a data that has been {@link #put(String, Object) put} * into the flash for one time. The data that has been kept * will be persistent to client cookie for one time, thus * the next time when user request the server, the app * can still get the data, but only for one time unless * the app call {@code keep} method again * * @param key the key to identify the data to be kept * @return this flash * @see #keep() */ public Flash keep(String key) { if (super.containsKey(key)) { out.put(key, get(key)); } return this; } /** * Keep all data that has been {@link #put(String, Object) put} * into the flash for one time. The data that has been kept * will be persistent to client cookie for one time, thus * the next time when user request the server, the app * can still get the data, but only for one time unless * the app call {@code keep} method again * * @return the flash instance */ public Flash keep() { out.putAll(data); return this; } public KV out() { return new KV(out); } /** * Return a flash instance of the current execution context, * For example from a {@link java.lang.ThreadLocal} * * @return the current flash instance */ public static Flash current() { return current.flash(); } /** * Set a flash instance into the current execution context, * for example into a {@link java.lang.ThreadLocal} * * @param flash the flash to be set to current execution context */ public static void current(Flash flash) { current.flash(flash); } /** * Resolve a Flash instance from a cookie. If the cookie supplied * is {@code null} then an empty Flash instance is returned * * @param flashCookie the flash cookie * @return a Flash instance * @see #serialize(String) */ public static Flash resolve(Cookie flashCookie) { Flash flash = new Flash(); if (null != flashCookie) { String value = flashCookie.value(); if (S.notBlank(value)) { String s = Codec.decodeUrl(value, Charsets.UTF_8); Matcher m = _PARSER.matcher(s); while (m.find()) { flash.data.put(m.group(1), m.group(2)); } } } return flash; } /** * Serialize this Flash instance into a Cookie. Note * the cookie returned has only name, value and max Age * been set. It's up to the caller to set secure, path * and httpOnly attributes. * * @param flashKey the cookie name * @return a Cookie represent this flash instance * @see #resolve(org.osgl.http.H.Cookie) */ public Cookie serialize(String flashKey) { if (out.isEmpty()) { return new Cookie(flashKey).maxAge(0); } StringBuilder sb = S.builder(); for (String key : out.keySet()) { sb.append(S.HSEP); sb.append(key); sb.append(":"); sb.append(out.get(key)); sb.append(S.HSEP); } String value = Codec.encodeUrl(sb.toString(), Charsets.UTF_8); return new Cookie(flashKey).value(value); } @Override public boolean equals(Object obj) { boolean superEq = super.equals(obj); return superEq && (obj instanceof H.Flash); } } // eof Flash /** * Defines the HTTP request trait * * @param the type of the implementation class */ public static abstract class Request { private static SimpleDateFormat dateFormat; static { dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US); dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); } /** * Returns the class of the implementation. Not to be used * by application * @return the class */ protected abstract Class _impl(); private Format accept; private Format contentType; private String ip; private int port = -1; private State state = State.NONE; private Object context; private String etag; private String referer; protected volatile InputStream inputStream; protected volatile Reader reader; private Map cookies = new HashMap<>(); /** * Attach a context object to the request instance * @param context the context object * @return the request instance itself */ public T context(Object context) { this.context = $.notNull(context); return me(); } /** * Get the context object from the request instance * @param the generic type of the context object * @return the context object */ public CONTEXT context() { return (CONTEXT) context; } /** * Returns the HTTP method of the request * @return method */ public abstract Method method(); /** * Set the Http method on this request. Used by framework to "override" * a HTTP method * @param method the method to set * @return this request instance */ public abstract T method(Method method); /** * Returns the header content by name. If there are * multiple headers with the same name, then the first * one is returned. If there is no header has the name * then {@code null} is returned * *

Note header name is case insensitive

* * @param name the name of the header * @return the header content */ public abstract String header(String name); /** * Returns all header content by name. This method returns * content of all header with the same name specified in * an {@link java.lang.Iterable} of {@code String}. If there * is no header has the name specified, then an empty iterable * is returned. * *

Note header name is case insensitive

* * @param name the name of the header * @return all values of the header */ public abstract Iterable headers(String name); /** * Return the request {@link org.osgl.http.H.Format accept} * * @return the request accept */ public Format accept() { if (null == accept) { resolveAcceptFormat(); } return accept; } /** * Set {@link org.osgl.http.H.Format accept} to the request * @param fmt the format to be set * @return this request */ public T accept(Format fmt) { this.accept = $.notNull(fmt); return me(); } public T accept(MediaType mediaType) { this.accept = mediaType.format(); return me(); } /** * Return the "referer(sic)" header value * @return the http referrer */ public String referrer() { if (null == referer) { referer = header(REFERER); if (null == referer) { referer = ""; } } return referer; } /** * This method is an alias of {@link #referer} which follows the * HTTP misspelling header name `referer` * @return the http referer */ public String referer() { return referrer(); } public String etag() { if (null == etag) { etag = method().safe() ? header(IF_NONE_MATCH) : header(IF_MATCH); } return etag; } public boolean etagMatches(String etag) { String etag0 = this.etag(); return null != etag0 && S.eq(etag0, etag); } /** * Check if the request is an ajax call * * @return {@code true} if it is an ajax call */ public boolean isAjax() { return S.eq(header(X_REQUESTED_WITH), "XMLHttpRequest"); } /** * Returns the path of the request. This does not include the * context path. The path is a composite of * {@link javax.servlet.http.HttpServletRequest#getServletPath()} * and {@link javax.servlet.http.HttpServletRequest#getPathInfo()} * *

* The path starts with "/" but not end with "/" *

* * @return URL path */ public abstract String path(); /** * Returns the context path of the request. * The context path starts with "/" but not end * with "/". If there is no context path * then and empty "" is returned * @return context path */ public abstract String contextPath(); /** * Returns the full URI path. It's composed of * {@link #contextPath()} and {@link #path()} * The full path starts with "/" * @return full path */ public String fullPath() { return Path.url(path(), this); } /** * Alias of {@link #fullPath()} * * @return the full URL path of the request */ public String url() { return fullPath(); } /** * Returns the full URL including scheme, domain, port and * full request path plus query string * * @return the absolute URL */ public String fullUrl() { return Path.fullUrl(path(), this); } /** * Returns query string or an empty String if the request * doesn't contains a query string * @return query string */ public abstract String query(); /** * Check if the request was made on a secure channel * * @return {@code true} if this is a secure request */ public abstract boolean secure(); /** * Returns the scheme of the request, specifically one of the * "http" and "https" * * @return the scheme of the request */ public String scheme() { return secure() ? "https" : "http"; } protected void _setCookie(String name, Cookie cookie) { cookies.put(name, cookie); } private String domain; /** * Alias of {@link #host()}. * @return host of this request */ public String domain() { if (null == domain) resolveHostPort(); return domain; } /** * Returns host of this request. * * It will first check the `X-Forwarded-Host` header, if no value then check * the `Host` header, if still no value then return empty string; otherwise * * 1. host will be the part before `:` of the value * 2. port will be the part after `:` of the value * * @return host of this request */ public String host() { return domain(); } /** * Returns the port of this request. * * If port cannot be resolved, then it will return the default port: * * 80 for no secure connection * * 443 for secure connection * * @return port the port of this request * @see #port() */ public int port() { if (-1 == port) resolveHostPort(); return port; } /** * Returns remote ip address. * @return remote ip of this request */ protected abstract String _ip(); private static boolean ipOk(String s) { return S.notEmpty(s) && S.neq("unknown", s); } private void resolveIp() { String rmt = _ip(); if (!HttpConfig.isXForwardedAllowed(rmt)) { ip = rmt; return; } String s = header(X_FORWARDED_FOR); if (!ipOk(s)) { if (HttpConfig.allowExtensiveRemoteAddrResolving()) { s = header(PROXY_CLIENT_IP); if (!ipOk(s)) { s = header(WL_PROXY_CLIENT_IP); if (!ipOk(s)) { s = header(HTTP_CLIENT_IP); if (!ipOk(s)) { s = header(HTTP_X_FORWARDED_FOR); if (!ipOk(s)) { ip = rmt; return; } } } } } else { ip = rmt; return; } } // in case there are multiple ip addresses (due to cascade proxies), then use the first one. if (s.length() > 15) { int pos = s.indexOf(","); if (pos > 0) { s = s.substring(0, pos); } } ip = s; } private void resolveHostPort() { String host = header(X_FORWARDED_HOST); if (S.empty(host)) { host = header(HOST); } if (null != host) { FastStr fs = FastStr.unsafeOf(host); if (fs.contains(':')) { domain = fs.beforeFirst(':').toString(); try { port = Integer.parseInt(fs.afterFirst(':').toString()); } catch (NumberFormatException e) { port = defPort(); } } else { domain = host; port = defPort(); } } else { domain = ""; port = defPort(); } } private int defPort() { return secure() ? 443 : 80; } /** * Returns the remote ip address of this request. * * The resolving process of remote ip address: * 1. Check `X-Forwarded-For` header, if no value or value is `unknown` then * 2. Check `Proxy-Client-ip`, if no value or value is `unknown` then * 3. Check `Wl-Proxy-Client-Ip`, if no value or value is `unknown` then * 4. Check `HTTP_CLIENT_IP`, if no value or value is `unknown` then * 5. Check `HTTP_X_FORWARDED_FOR`, if no value or value is `unknown` then * 6. return the ip address passed by underline network stack, e.g. netty or undertow * * @return remote ip of this request */ public String ip() { if (null == ip) { resolveIp(); } return ip; } /** * Returns useragent string of this request * @return useragent string */ public String userAgentStr() { return header(USER_AGENT); } public UserAgent userAgent() { return UserAgent.parse(userAgentStr()); } protected abstract void _initCookieMap(); /** * Returns cookie by it's name * * @param name the cookie name * @return the cookie or {@code null} if not found */ public H.Cookie cookie(String name) { if (cookies.isEmpty()) { _initCookieMap(); } return cookies.get(name); } /** * Returns all cookies of the request in Iterable * @return cookies */ public List cookies() { if (cookies.isEmpty()) { _initCookieMap(); } return C.list(cookies.values()); } /** * resolve the request accept * * @return this request instance */ private T resolveAcceptFormat() { String accept = header(ACCEPT); this.accept = Format.resolve(accept); return (T) this; } /** * Check if the requested resource is modified with etag and * last timestamp (usually the timestamp of a static file e.g.) * * @param etag the etag to compare with "If_None_Match" * header in browser * @param since the last timestamp to compare with * "If_Modified_Since" header in browser * @return {@code true} if the resource has changed * or {@code false} otherwise */ public boolean isModified(String etag, long since) { String browserEtag = header(IF_NONE_MATCH); if (null == browserEtag) return true; if (!S.eq(browserEtag, etag)) { return true; } String s = header(IF_MODIFIED_SINCE); if (null == s) return true; try { Date browserDate = dateFormat.parse(s); if (browserDate.getTime() >= since) { return false; } } catch (ParseException ex) { logger.error(ex, "Can't parse date: %s", s); } return true; } private void parseContentTypeAndEncoding() { String type = header(CONTENT_TYPE); if (null == type) { contentType = Format.HTML; encoding = "utf-8"; } else { String[] contentTypeParts = type.split(";"); String _contentType = contentTypeParts[0].trim().toLowerCase(); String _encoding = "utf-8"; // check for encoding-info if (contentTypeParts.length >= 2) { String[] encodingInfoParts = contentTypeParts[1].split(("=")); if (encodingInfoParts.length == 2 && encodingInfoParts[0].trim().equalsIgnoreCase("charset")) { // encoding-info was found in request _encoding = encodingInfoParts[1].trim(); if (S.notBlank(_encoding) && ((_encoding.startsWith("\"") && _encoding.endsWith("\"")) || (_encoding.startsWith("'") && _encoding.endsWith("'"))) ) { _encoding = _encoding.substring(1, _encoding.length() - 1).trim(); } } } contentType = Format.resolve(_contentType); encoding = _encoding; } } /** * Return content type of the request * @return content type */ public Format contentType() { if (null == contentType) { parseContentTypeAndEncoding(); } return contentType; } private String encoding; /** * Returns encoding of the request * @return character encoding */ public String characterEncoding() { if (null == encoding) { parseContentTypeAndEncoding(); } return encoding; } private C.List locales; private void parseLocales() { String s = header(ACCEPT_LANGUAGE); if (S.blank(s)) { locales = C.list(HttpConfig.defaultLocale()); return; } // preprocess to remove all blanks s = S.str(s).remove(new $.F1() { @Override public Boolean apply(Character character) { char c = character; return c == ' ' || c == '\t'; } }).toString(); ListBuilder lb = ListBuilder.create(); // parse things like "da,en-gb;q=0.8,en;q=0.7" String[] sa = s.split(","); for (String s0 : sa) { String[] arr = s0.trim().split(";"); //Parse the locale Locale locale; String[] l = arr[0].split("-"); switch(l.length){ case 2: locale = new Locale(l[0], l[1]); break; case 3: locale = new Locale(l[0], l[1], l[2]); break; default: locale = new Locale(l[0]); break; } lb.add(locale); } if (lb.isEmpty()) lb.add(HttpConfig.defaultLocale()); locales = lb.toList(); } /** * Returns locale of the request * @return locale */ public Locale locale() { if (null == locales) parseLocales(); return locales.get(0); } /** * Returns all locales of the request * @return all locales of the request */ public C.List locales() { if (null == locales) parseLocales(); return locales; } private long len = -2; /** * Returns the content length of the request * @return content length */ public long contentLength() { if (len > -2) return len; String s = header(CONTENT_LENGTH); if (S.blank(s)) { len = -1; } else { try { len = Long.parseLong(s); } catch (NumberFormatException e) { len = -1; logger.error("Error parsing content-length: %s", s); } } return len; } public boolean readerCreated() { return state == State.READER; } protected abstract InputStream createInputStream(); /** * Returns body of the request as binary data using {@link java.io.InputStream} * * @return input stream of the request body * @throws IllegalStateException if {@link #reader()} has already * been called on this request instance */ public InputStream inputStream() throws IllegalStateException { return state.inputStream(this); } private void createReader() { if (null != reader) { return; } synchronized (this) { if (null != reader) { return; } createInputStream(); String charset = characterEncoding(); Charset cs = null == charset ? Charsets.UTF_8 : Charset.forName(charset); reader = new InputStreamReader(inputStream(), cs); } } /** * Returns body of the request as binary data using {@link java.io.Reader} * * @return the reader of the request body * @throws IllegalStateException if {@link #inputStream()} has already * been called on this request instance */ public Reader reader() throws IllegalStateException { return state.reader(this); } /** * Return a request parameter value by name. If there is no parameter * found with the name specified, then {@code null} is returned. If * there are multiple values associated with the name, then the * first one is returned * * @param name the parameter name * @return the parameter value of {@code null} if not found */ public abstract String paramVal(String name); /** * Returns all values associated with the name specified in the * http request. If there is no parameter found with the name, * then {@code new String[0]} shall be returned * * @param name the parameter name * @return all values of the parameter */ public abstract String[] paramVals(String name); /** * Return all parameter names * * @return an {@link java.lang.Iterable} of parameter names */ public abstract Iterable paramNames(); private void parseAuthorization() { if (null != user) return; user = ""; password = ""; String s = header(AUTHORIZATION); if (null != s && s.startsWith("Basic")) { String data = s.substring(6); String[] decodedData = new String(Codec.decodeBASE64(data)).split(":"); user = decodedData.length > 0 ? decodedData[0] : null; password = decodedData.length > 1 ? decodedData[1] : null; } } private String user; /** * The Http Basic user * @return the user */ public String user() { if (null == user) parseAuthorization(); return user; } private String password; /** * the Http Basic password * @return the password */ public String password() { if (null == password) parseAuthorization(); return password; } protected final T me() { return (T) this; } /** * Return a request instance of the current execution context, * For example from a {@link java.lang.ThreadLocal} * * @param the requestion type * @return the current request instance */ @SuppressWarnings("unchecked") public static T current() { return (T) current.request(); } /** * Set a request instance into the current execution context, * for example into a {@link java.lang.ThreadLocal} * * @param the response type * @param request the request to be set to current execution context */ public static void current(T request) { current.request(request); } private enum State { NONE, STREAM() { @Override Reader reader(Request req) { throw new IllegalStateException("reader() already called"); } }, READER() { @Override InputStream inputStream(Request req) { throw new IllegalStateException("inputStream() already called"); } }; InputStream inputStream(Request req) { req.inputStream = req.createInputStream(); req.state = STREAM; return req.inputStream; } Reader reader(Request req) { req.createReader(); req.state = READER; return req.reader; } } } // eof Request /** * Defines the HTTP response trait */ public static abstract class Response { private State state = State.NONE; protected volatile OutputStream outputStream; protected volatile Writer writer; protected volatile Output output; private Object context; /** * Attach a context object to the response instance * @param context the context object * @return the response instance itself */ public T context(Object context) { this.context = $.requireNotNull(context); return me(); } /** * Get the context object from the response instance * @param the generic type of the context object * @return the context object */ public CONTEXT context() { return (CONTEXT) context; } /** * Returns the class of the implementation. Not to be used * by application * @return implementation class */ protected abstract Class _impl(); public boolean writerCreated() { return state == State.WRITER; } public boolean outputCreated() { return state == State.OUTPUT; } public boolean outputStreamCreated() { return state == State.STREAM; } protected abstract OutputStream createOutputStream(); protected Writer createWriter() { outputStream = createOutputStream(); String charset = characterEncoding(); Charset cs = null == charset ? Charsets.UTF_8 : Charset.forName(charset); return new OutputStreamWriter(outputStream, cs); } protected abstract Output createOutput(); private void ensureWriter() { if (null != writer) { return; } synchronized (this) { if (null != writer) { return; } writer = createWriter(); } } /** * Returns the output to write to the response. * @return output of the response. * @throws IllegalStateException if {@link #writer()} or {@link #outputStream()} is called already * @throws UnexpectedIOException if there are IO exception */ public Output output() { return state.output(this); } /** * Returns the output stream to write to the response. * * @return output stream to the response * @throws java.lang.IllegalStateException if * {@link #writer()} is called already * @throws org.osgl.exception.UnexpectedIOException if * there are output exception */ public OutputStream outputStream() throws IllegalStateException, UnexpectedIOException { return state.outputStream(this); } /** * Returns the writer to write to the response * * @return writer to the response * @throws java.lang.IllegalStateException if {@link #outputStream()} is called already * @throws org.osgl.exception.UnexpectedIOException if there are output exception */ public Writer writer() throws IllegalStateException, UnexpectedIOException { return state.writer(this); } /** * Returns a print writer to write to the response * * @return the print writer for the response * @throws IllegalStateException if {@link #outputStream()} is called already * @throws UnexpectedIOException if there are output exception */ public PrintWriter printWriter() { Writer w = writer(); if (w instanceof PrintWriter) { return (PrintWriter) w; } else { return new PrintWriter(w); } } /** * Returns the name of the character encoding (MIME charset) * used for the body sent in this response. * The character encoding may have been specified explicitly * using the {@link #characterEncoding(String)} or * {@link #contentType(String)} methods, or implicitly using the * {@link #locale(java.util.Locale)} method. Explicit specifications take * precedence over implicit specifications. Calls made * to these methods after getWriter has been * called or after the response has been committed have no * effect on the character encoding. If no character encoding * has been specified, ISO-8859-1 is returned. *

See RFC 2047 (http://www.ietf.org/rfc/rfc2047.txt) * for more information about character encoding and MIME. * * @return a String specifying the * name of the character encoding, for * example, UTF-8 */ public abstract String characterEncoding(); /** * Returns the content type used for the MIME body * sent in this response. The content type proper must * have been specified using {@link #contentType(String)} * before the response is committed. If no content type * has been specified, this method returns null. * If a content type has been specified, and a * character encoding has been explicitly or implicitly * specified as described in {@link #characterEncoding()} * or {@link #writer()} has been called, * the charset parameter is included in the string returned. * If no character encoding has been specified, the * charset parameter is omitted. * * @param encoding the encoding * @return a String specifying the * content type, for example, * text/html; charset=UTF-8, * or null */ public abstract T characterEncoding(String encoding); /** * Set the length of the content to be write to the response * * @param len an long value specifying the length of the * content being returned to the client; sets * the Content-Length header * @return the response it self * @see #outputStream * @see #writer */ public abstract T contentLength(long len); /** * Sub class to overwrite this method to set content type to * the response * * @param type a String specifying the MIME * type of the content */ protected abstract void _setContentType(String type); private String contentType; /** * Sets the content type of the response being sent to * the client. The content type may include the type of character * encoding used, for example, text/html; charset=ISO-8859-4. * If content type has already been set to the response, this method * will update the content type with the new value * *

this method must be called before calling {@link #writer()} * or {@link #outputStream()}

* * @param type a String specifying the MIME * type of the content * @return the response it self * @see #outputStream * @see #writer * @see #initContentType(String) */ public T contentType(String type) { _setContentType(type); contentType = type; return me(); } /** * This method set the content type to the response if there * is no content type been set already. * * @param type a String specifying the MIME * type of the content * @return the response it self * @see #contentType(String) */ public T initContentType(String type) { return (null == contentType) ? contentType(type) : (T) this; } public T contentDisposition(String filename, boolean inline) { final String type = inline ? "inline" : "attachment"; if (S.blank(filename)) { header(CONTENT_DISPOSITION, type); } else { if(canAsciiEncode(filename)) { String contentDisposition = "%s; filename=\"%s\""; header(CONTENT_DISPOSITION, S.fmt(contentDisposition, type, filename)); } else { final String encoding = characterEncoding(); String contentDisposition = "%1$s; filename*="+encoding+"''%2$s; filename=\"%2$s\""; try { header(CONTENT_DISPOSITION, S.fmt(contentDisposition, type, URLEncoder.encode(filename, encoding))); } catch (UnsupportedEncodingException e) { throw E.encodingException(e); } } } return me(); } /** * This method will prepare response header for file download. * @param filename the filename * @return this response instance */ public T prepareDownload(String filename) { return contentDisposition(filename, false); } /** * Set the etag header * @param etag the etag content * @return this response */ public T etag(String etag) { header(ETAG, etag); return me(); } /** * Sets the locale of the response, setting the headers (including the * Content-Type's charset) as appropriate. This method should be called * before a call to {@link #writer()}. By default, the response locale * is the default locale for the server. * * @param loc the locale of the response * @see #locale() */ protected abstract void _setLocale(Locale loc); public T locale(Locale locale) { _setLocale(locale); return me(); } /** * Returns the locale assigned to the response. * * @return locale * @see #locale(java.util.Locale) */ public abstract Locale locale(); /** * Adds the specified cookie to the response. This method can be called * multiple times to add more than one cookie. * * @param cookie the Cookie to return to the client */ public abstract void addCookie(H.Cookie cookie); /** * Returns a boolean indicating whether the named response header * has already been set. * * @param name the header name * @return true if the named response header * has already been set; * false otherwise */ public abstract boolean containsHeader(String name); /** * Sends an error response to the client using the specified * status. The server defaults to creating the * response to look like an HTML-formatted server error page * containing the specified message, setting the content type * to "text/html", leaving cookies and other headers unmodified. * * If an error-page declaration has been made for the web application * corresponding to the status code passed in, it will be served back in * preference to the suggested msg parameter. * *

If the response has already been committed, this method throws * an IllegalStateException. * After using this method, the response should be considered * to be committed and should not be written to. * * @param statusCode the error status code * @param msg the descriptive message * @return the response itself * @throws org.osgl.exception.UnexpectedIOException If an input or output exception occurs * @throws IllegalStateException If the response was committed */ public abstract T sendError(int statusCode, String msg); /** * Sames as {@link #sendError(int, String)} but accept message format * arguments * * @param statusCode the error status code * @param msg the descriptive message template * @param args the descriptive message arguments * @return the response itself * @throws org.osgl.exception.UnexpectedIOException If an input or output exception occurs * @throws IllegalStateException If the response was committed */ public T sendError(int statusCode, String msg, Object... args) { return sendError(statusCode, S.fmt(msg, args)); } /** * Sends an error response to the client using the specified status * code and clearing the buffer. *

If the response has already been committed, this method throws * an IllegalStateException. * After using this method, the response should be considered * to be committed and should not be written to. * * @param statusCode the error status code * @return the response itself * @throws org.osgl.exception.UnexpectedIOException If the response was committed before this method call */ public abstract T sendError(int statusCode); /** * Sends a temporary redirect response to the client using the * specified redirect location URL. This method can accept relative URLs; * the servlet container must convert the relative URL to an absolute URL * before sending the response to the client. If the location is relative * without a leading '/' the container interprets it as relative to * the current request URI. If the location is relative with a leading * '/' the container interprets it as relative to the servlet container root. * *

If the response has already been committed, this method throws * an IllegalStateException. * After using this method, the response should be considered * to be committed and should not be written to. * * @param location the redirect location URL * @return the response itself * @throws org.osgl.exception.UnexpectedIOException If the response was committed before this method call * @throws IllegalStateException If the response was committed or * if a partial URL is given and cannot be converted into a valid URL */ public abstract T sendRedirect(String location); /** * Sets a response header with the given name and value. * If the header had already been set, the new value overwrites the * previous one. The containsHeader method can be * used to test for the presence of a header before setting its * value. * * @param name the name of the header * @param value the header value If it contains octet string, * it should be encoded according to RFC 2047 * (http://www.ietf.org/rfc/rfc2047.txt) * @return the response itself * @see #containsHeader * @see #addHeader */ public abstract T header(String name, String value); /** * Sets the status code for this response. This method is used to * set the return status code when there is no error (for example, * for the status codes SC_OK or SC_MOVED_TEMPORARILY). If there * is an error, and the caller wishes to invoke an error-page defined * in the web application, the sendError method should be used * instead. *

The container clears the buffer and sets the Location header, preserving * cookies and other headers. * * @param statusCode the status code * @return the response itself * @see #sendError * @see #status(int) */ public abstract T status(int statusCode); /** * Sets the status for this response. This method is used to * set the return status code when there is no error (for example, * for the status OK or MOVED_TEMPORARILY). If there * is an error, and the caller wishes to invoke an error-page defined * in the web application, the sendError method should be used * instead. *

The container clears the buffer and sets the Location header, preserving * cookies and other headers. * * @param status the status * @return the response itself * @see #sendError */ public T status(Status status) { status(status.code()); return me(); } /** * Get the status code of this response. * * If the status code has not been set to this response, * `-1` should be returned. * * @return the status code of this response */ public abstract int statusCode(); /** * Adds a response header with the given name and value. * This method allows response headers to have multiple values. * * @param name the name of the header * @param value the additional header value If it contains * octet string, it should be encoded * according to RFC 2047 * (http://www.ietf.org/rfc/rfc2047.txt) * @return this response itself * @see #header(String, String) */ public abstract T addHeader(String name, String value); /** * Adds a response header with given name and value if the header * with the same name has not been added yet * @param name the name of the header * @param value the header value * @return this response itself * @see #addHeader(String, String) */ public T addHeaderIfNotAdded(String name, String value) { if (!containsHeader(name)) { addHeader(name, value); } return me(); } public T writeBinary(ISObject binary) { IO.copy(binary.asInputStream(), outputStream(), true); return me(); } /** * Write a string to the response * * @param s the string to write to the response * @return this response itself */ public T writeContent(String s) { try { IO.write(s.getBytes(characterEncoding()), outputStream()); } catch (UnsupportedEncodingException e) { throw E.encodingException(e); } return me(); } public abstract T writeContent(ByteBuffer buffer); /** * Write content to the response * * @param content the content to write * @return the response itself */ public T writeText(String content) { initContentType(Format.TXT.contentType()); return writeContent(content); } /** * Write content to the response * * @param content the content to write * @return the response itself */ public T writeHtml(String content) { initContentType(Format.HTML.contentType()); return writeContent(content); } /** * Write content to the response * * @param content the content to write * @return the response itself */ public T writeJSON(String content) { initContentType(Format.JSON.contentType()); return writeContent(content); } /** * Calling this method commits the response, meaning the status * code and headers will be written to the client */ public abstract void commit(); /** * Close output or outputStream or writer opened on this response */ public void close() { state.close(this); } /** * Return a request instance of the current execution context, * For example from a {@link java.lang.ThreadLocal} * * @param the response type * @return the current request instance */ @SuppressWarnings("unchecked") public static T current() { return (T) current.response(); } /** * Set a request instance into the current execution context, * for example into a {@link java.lang.ThreadLocal} * * @param response the request to be set to current execution context * @param the sub type of response */ public static void current(T response) { current.response(response); } protected T me() { return (T) this; } private static boolean canAsciiEncode(String string) { CharsetEncoder asciiEncoder = Charset.forName("US-ASCII").newEncoder(); return asciiEncoder.canEncode(string); } private enum State { NONE, OUTPUT() { @Override OutputStream outputStream(Response resp) { return output(resp).asOutputStream(); } @Override Writer writer(Response resp) { return output(resp).asWriter(); } @Override void close(Response resp) { IO.close(resp.output); } }, STREAM() { @Override Writer writer(Response resp) { return new OutputStreamWriter(outputStream(resp)); } @Override Output output(Response resp) { return new OutputStreamOutput(outputStream(resp)); } @Override void close(Response resp) { IO.close(resp.outputStream); } }, WRITER() { @Override OutputStream outputStream(Response resp) { return new WriterOutputStream(writer(resp)); } @Override Output output(Response resp) { return new WriterOutput(writer(resp)); } @Override void close(Response resp) { IO.close(resp.writer); } }; Output output(Response resp) { if (null == resp.output) { resp.output = resp.createOutput(); resp.state = OUTPUT; } return resp.output; } OutputStream outputStream(Response resp) { if (null == resp.outputStream) { resp.outputStream = resp.createOutputStream(); resp.state = STREAM; } return resp.outputStream; } Writer writer(Response resp) { if (null == resp.writer) { resp.ensureWriter(); resp.state = WRITER; } return resp.writer; } void close(Response resp) {} } } // eof Response H() { } /** * Clear all current context */ public static void cleanUp() { current.clear(); } private static boolean registered; public static void registerTypeConverters() { if (!registered) { TypeConverterRegistry.INSTANCE.register(new $.TypeConverter() { @Override public H.Status convert(Integer o) { return null == o ? null : H.Status.of(o); } }).register(new $.TypeConverter() { @Override public H.Format convert(String o) { return null == o ? null : H.Format.of(o); } }).register(new $.TypeConverter() { @Override public Method convert(String s) { return Method.valueOfIgnoreCase(s); } }); registered = true; } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy