/*
 * @(#)HttpRequest.java
 *
 * Copyright (c) 2001-2002 JScape
 * 1147 S. 53rd Pl., Mesa, Arizona, 85206, U.S.A.
 * All rights reserved.
 *
 * This software is the confidential and proprietary information of
 * JScape. ("Confidential Information").  You shall not disclose such
 * Confidential Information and shall use it only in accordance with
 * the terms of the license agreement you entered into with JScape.
 */
package com.jscape.inet.http;

import com.jscape.inet.http.protocol.marshaling.BasicAuthorizationFormat;
import com.jscape.inet.mime.MimeException;
import com.jscape.inet.mime.MimeHeader;
import com.jscape.inet.mime.MimeMessage;
import com.jscape.inet.util.URLParser;
import com.jscape.util.ByteBuffer;

import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Locale;
import java.util.Vector;


/**
 * Creates the properties of an HTTP request.<P>
 * <p/>
 * Example Usage:
 * <P>
 * <pre>
 * HttpRequest request = new HttpRequest("http://www.myserver.com","GET");
 * request.addParameter(new HttpParameter("username","jsmith"));
 * request.addParameter(new HttpParameter("password","secret"));
 * </pre>
 * </p>
 */
public class HttpRequest
        extends MimeMessage
        implements Serializable {


    /**
     * Represents a carriage return
     */
    private static final byte CR = 13;

    /**
     * Represents a line feed
     */
    private static final byte LF = 10;
    private Hashtable cookies = new Hashtable();
    private Vector parameters = new Vector();
    private String method = "GET";
    private String urlStr;
    private int proxyPort;
    private String proxyHost;
    private String proxyUsername;
    private String proxyPassword;
    private String httpVersion = "1.0";
    private boolean utf8 = true;

    /**
     * codes for performing UTF-8 url encoding
     */
    private static final String[] HEX = {
            "%00", "%01", "%02", "%03", "%04", "%05", "%06", "%07",
            "%08", "%09", "%0a", "%0b", "%0c", "%0d", "%0e", "%0f",
            "%10", "%11", "%12", "%13", "%14", "%15", "%16", "%17",
            "%18", "%19", "%1a", "%1b", "%1c", "%1d", "%1e", "%1f",
            "%20", "%21", "%22", "%23", "%24", "%25", "%26", "%27",
            "%28", "%29", "%2a", "%2b", "%2c", "%2d", "%2e", "%2f",
            "%30", "%31", "%32", "%33", "%34", "%35", "%36", "%37",
            "%38", "%39", "%3a", "%3b", "%3c", "%3d", "%3e", "%3f",
            "%40", "%41", "%42", "%43", "%44", "%45", "%46", "%47",
            "%48", "%49", "%4a", "%4b", "%4c", "%4d", "%4e", "%4f",
            "%50", "%51", "%52", "%53", "%54", "%55", "%56", "%57",
            "%58", "%59", "%5a", "%5b", "%5c", "%5d", "%5e", "%5f",
            "%60", "%61", "%62", "%63", "%64", "%65", "%66", "%67",
            "%68", "%69", "%6a", "%6b", "%6c", "%6d", "%6e", "%6f",
            "%70", "%71", "%72", "%73", "%74", "%75", "%76", "%77",
            "%78", "%79", "%7a", "%7b", "%7c", "%7d", "%7e", "%7f",
            "%80", "%81", "%82", "%83", "%84", "%85", "%86", "%87",
            "%88", "%89", "%8a", "%8b", "%8c", "%8d", "%8e", "%8f",
            "%90", "%91", "%92", "%93", "%94", "%95", "%96", "%97",
            "%98", "%99", "%9a", "%9b", "%9c", "%9d", "%9e", "%9f",
            "%a0", "%a1", "%a2", "%a3", "%a4", "%a5", "%a6", "%a7",
            "%a8", "%a9", "%aa", "%ab", "%ac", "%ad", "%ae", "%af",
            "%b0", "%b1", "%b2", "%b3", "%b4", "%b5", "%b6", "%b7",
            "%b8", "%b9", "%ba", "%bb", "%bc", "%bd", "%be", "%bf",
            "%c0", "%c1", "%c2", "%c3", "%c4", "%c5", "%c6", "%c7",
            "%c8", "%c9", "%ca", "%cb", "%cc", "%cd", "%ce", "%cf",
            "%d0", "%d1", "%d2", "%d3", "%d4", "%d5", "%d6", "%d7",
            "%d8", "%d9", "%da", "%db", "%dc", "%dd", "%de", "%df",
            "%e0", "%e1", "%e2", "%e3", "%e4", "%e5", "%e6", "%e7",
            "%e8", "%e9", "%ea", "%eb", "%ec", "%ed", "%ee", "%ef",
            "%f0", "%f1", "%f2", "%f3", "%f4", "%f5", "%f6", "%f7",
            "%f8", "%f9", "%fa", "%fb", "%fc", "%fd", "%fe", "%ff"
    };


    /**
     * Creates a new <code>HttpRequest</code> instance using default GET request method.
     * Any parameters provided in the URL must be URL encoded
     * Additional parameters added using addParameter methods will be appended to any
     * existing parameters in the URL.
     *
     * @param url the URL to retrieve
     *
     * @throws HttpException if a MIME related error occurs
     */
    public HttpRequest(String url)
            throws HttpException {
        this(url, "GET");
    }

    /**
     * Creates a new <code>HttpRequest</code> instance.
     * Any parameters provided in the URL for a GET request must be URL encoded.
     * Additional parameters added using addParameter methods will be appended to any
     * existing parameters in the URL.
     *
     * @param url    the URL to retrieve
     * @param method the request method to use GET or POST
     *
     * @throws HttpException if a MIME related parsing error occurs
     */
    public HttpRequest(String url, String method)
            throws HttpException {
        super();
        this.method = method.toUpperCase(Locale.ENGLISH);

        try {
            setURL(url);

            // write Accept header
            addReplaceHeader("Accept", "*/*");

            URLParser urlParser = new URLParser(url);
            addHostHeader();
        } catch (MalformedURLException mfe) {
            throw new HttpException(mfe.getMessage(), mfe);
        } catch (MimeException me) {
            throw new HttpException(me.getMessage(), me);
        }
    }

    /**
     * Sets the hostname of the proxy server to be used in HTTP communications.
     * Hostname must be a fully qualified domain name or dotted ipaddress. For
     * example proxy.domain.com or 10.0.0.1
     *
     * @param proxyHost the hostname or ipaddress of the proxy server
     */
    public void setProxyHost(String proxyHost) {
        synchronized (this) {
            this.proxyHost = proxyHost;
            try {
                addHostHeader();
            } catch (Exception e) {
                // todo: ignore?
            }
        }
    }

    /**
     * Gets the hostname/ipaddress of the proxy server responsible for handling
     * HTTP requests.
     *
     * @return the hostname or ipaddress of the proxy server
     */
    public String getProxyHost() {
        return proxyHost;
    }

    /**
     * Sets the port to be used when communicating with an HTTP proxy server.
     *
     * @param proxyPort port of listening HTTP proxy server
     */
    public void setProxyPort(int proxyPort) {
        this.proxyPort = proxyPort;
    }

    /**
     * Gets the port of HTTP proxy server.
     *
     * @return port of listening HTTP proxy server
     */
    public int getProxyPort() {
        return proxyPort;
    }

    /**
     * Gets the username used in authentication to HTTP proxy server.
     *
     * @return username used in proxy authentication
     */
    public String getProxyUsername() {
        return proxyUsername;
    }

    /**
     * Sets the username and password used in authentication to HTTP proxy server.
     *
     * @param proxyUsername username used in proxy authentication
     * @param proxyPassword password used in proxy authentication
     */
    public void setProxyLogin(String proxyUsername, String proxyPassword) {
        synchronized (this) {
            this.proxyUsername = proxyUsername;
            this.proxyPassword = proxyPassword;
        }
    }

    /**
     * Adds cookie name/value pair to this <code>HttpRequest</code>
     *
     * @param name  the cookie name
     * @param value the cookie value
     */
    @SuppressWarnings("unchecked")
    public void addCookie(String name, String value) {
        cookies.put(name, value);
        try {
            MimeHeader cookie = new MimeHeader();
            cookie.setName("Cookie");
            Enumeration keys = cookies.keys();
            StringBuffer buffer = new StringBuffer();
            while (keys.hasMoreElements()) {
                String key = (String) keys.nextElement();
                String val = (String) cookies.get(key);
                buffer.append(key);
                buffer.append("=");
                buffer.append(val);
                if (keys.hasMoreElements()) {
                    buffer.append("; ");
                }
            }
            cookie.setValue(buffer.toString());
            addReplaceHeader(cookie);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * Adds MIME header to this <code>HttpRequest</code>.
     *
     * @param header a <code>MimeHeader</code>
     */
    public void addHeader(MimeHeader header) {
        header.setFolding(false);
        super.addHeader(header);

    }

    /**
     * Adds MIME header to this <code>HttpRequest</code>.
     *
     * @param name  the header name
     * @param value the header value
     *
     * @throws MimeException if a MIME related parsing error occurs
     */
    public void addHeader(String name, String value)
            throws MimeException {
        MimeHeader header = new MimeHeader(name, value);
        header.setFolding(false);
        super.addHeader(header);
    }

    /**
     * Adds MIME header to this <code>HttpRequest</code>, replacing existing header if found.
     *
     * @param header a <code>MimeHeader</code>
     */
    public void addReplaceHeader(MimeHeader header) {
        header.setFolding(false);
        super.addReplaceHeader(header);
    }

    /**
     * Adds MIME header to this <code>HttpRequest</code>, replacing existing header if found.
     *
     * @param name  the header name
     * @param value the header value
     *
     * @throws MimeException if a MIME related parsing error occurs
     */
    public void addReplaceHeader(String name, String value)
            throws MimeException {
        MimeHeader header = new MimeHeader(name, value);
        header.setFolding(false);
        super.addReplaceHeader(header);
    }

    /**
     * Adds a query parameter to HTTP request. HTTP query parameters are
     * typically used in submitting form data for processing by a CGI or
     * servlet resident on the HTTP server. Names of query parameters need not
     * be unique.
     *
     * @param parameter the <code>HttpParameter</code> to add
     *
     * @see HttpParameter
     */
    @SuppressWarnings("unchecked")
    public void addParameter(HttpParameter parameter) {
        parameters.add(parameter);
    }

    /**
     * Adds a query parameter to HTTP request replacing any other parameters with same name.
     * HTTP query parameters are typically used in submitting form data for processing by a CGI or
     * servlet resident on the HTTP server. Names of query parameters need not be unique.
     *
     * @param parameter the HttpParameter to add or replace
     */
    @SuppressWarnings("unchecked")
    public void addReplaceParameter(HttpParameter parameter) {
        String name = parameter.getName();
        boolean replaced = false;

        for (int i = 0; i < parameters.size(); ++i) {
            HttpParameter tmp = (HttpParameter) parameters.get(i);
            if (tmp.getName().equals(name)) {
                parameters.set(i, parameter);
                replaced = true;
            }
        }

        if (!replaced) {
            addParameter(parameter);
        }
    }

    /**
     * Adds a query parameter to HTTP request replacing any other parameters with same name.
     * HTTP query parameters are typically used in submitting form data for processing by a CGI or
     * servlet resident on the HTTP server. Names of query parameters need not be unique.
     *
     * @param name  the name of the parameter to add.
     * @param value the value of the parameter to add.
     */
    public void addReplaceParameter(String name, String value) {
        addReplaceParameter(new HttpParameter(name, value));
    }

    /**
     * Adds a query parameter to HTTP request. HTTP query parameters are
     * typically used in submitting form data for processing by a CGI or
     * servlet resident on the HTTP server. Names of query parameters need not
     * be unique.
     *
     * @param name  the name of the parameter to add.
     * @param value the value of the parameter to add.
     */
    public void addParameter(String name, String value) {
        addParameter(new HttpParameter(name, value));
    }

    /**
     * Clears all parameters from HTTP request
     */
    public void clearParameters() {
        parameters.clear();
    }

    /**
     * Clears all cookies from HTTP request
     */
    public void clearCookies() {
        cookies.clear();
    }

    /**
     * Gets all HTTP parameters to be sent with HTTP request. HTTP query
     * parameters are typically used in submitting form data for processing by
     * a CGI or servlet resident on the HTTP server. Names of query parameters
     * need not be unique.
     *
     * @return an <code>Enumeration</code> of <code>HttpParameter</code>
     *
     * @see HttpParameter
     */
    public Enumeration getParameters() {
        return parameters.elements();
    }

    /**
     * Sets username and password to be used in HTTP Basic Authentication. HTTP
     * Basic Authentication is used when attempting to access a
     * username/password protected URL.
     *
     * @param username the username used in HTTP Basic Authentication
     * @param password the password used in HTTP Basic Authentication
     *
     * @throws HttpException if a MIME related parsing error occurs
     */
    public void setBasicAuthentication(String username, String password)
            throws HttpException {
        synchronized (this) {
            try {
                MimeHeader authHeader = new MimeHeader("Authorization", BasicAuthorizationFormat.format(username, password));
                addReplaceHeader(authHeader);
            } catch (MimeException me) {
                throw new HttpException(me.getMessage(), me);
            }
        }
    }

    /**
     * Sets the User-Agent header of a HTTP request. The User-Agent header is
     * used in identifying the type of client making the HTTP request. The
     * User-Agent header typically contains the name and version of the client
     * software.
     *
     * @param userAgent the user agent
     *
     * @throws HttpException if a MIME related parsing error occurs
     */
    public void setUserAgent(String userAgent)
            throws HttpException {
        try {
            addReplaceHeader(new MimeHeader("User-Agent", userAgent));
        } catch (MimeException me) {
            throw new HttpException(me.getMessage(), me);
        }
    }

    /**
     * Gets User-Agent header of HTTP request.
     *
     * @return the user agent
     */
    public String getUserAgent() {
        MimeHeader header = getHeader("User-Agent");

        if (header == null) {
            return null;
        } else {
            return header.getValue();
        }
    }

    /**
     * Sets the request method used in HTTP request. Valid request methods are
     * GET, POST and PUT, default is GET.  The GET method appends all HTTP
     * parameters to the end of the URL when making a HTTP request and has a
     * 4K limit on the amount of data which may be passed.  The POST method
     * effectively hides this data in the HTTP request headers and has no data
     * size limit.  The PUT method is used in HTTP 1.1 compliant server which
     * support the PUT request.
     *
     * @param method request method used in HTTP request
     */
    public void setMethod(String method) {
        if (method != null) {
            synchronized (this) {
                this.method = method.toUpperCase(Locale.ENGLISH);
            }
        }
    }

    /**
     * Gets the request method used in HTTP request. Valid request methods are
     * GET, POST and PUT, default is GET.  The GET method appends all HTTP
     * parameters to the end of the URL when making a HTTP request and has a
     * 4K limit on the amount of data which may be passed.  The POST method
     * effectively hides this data in the HTTP request headers and has no data
     * size limit.  The PUT method is used in HTTP 1.1 compliant server which
     * support the PUT request.
     *
     * @return the request method used in HTTP request
     */
    public String getMethod() {
        return method;
    }

    /**
     * Sets URL of HTTP request. Example of a valid URL is
     * http://www.myserver.com/file.html<br>
     * Any parameters provided in the URL for a GET request must be URL encoded.
     * Additional parameters added using addParameter methods will be appended to any
     * existing parameters in the URL.
     *
     * @param urlStr the URL of HTTP request
     */
    public void setURL(String urlStr) {
        synchronized (this) {
            try {
                // If the query has been submitted as part of urlStr
                // add the parameters and remove from urlStr
                URLParser url = new URLParser(urlStr);

                String query = url.getQuery();
            } catch (Exception e) {
                // todo: ignore?
            }

            this.urlStr = urlStr;
            try {
                addHostHeader();
            } catch (Exception e) {
                // todo: ignore?
            }

        }
    }

    /**
     * Gets URL of HTTP request.
     *
     * @return the URL of HTTP request
     */
    public String getURL() {
        return urlStr;
    }

    private String getRequestLineURL() {
        if (getProxyHost() != null) {
            try {
                URLParser url = new URLParser(urlStr);
                String protocol = url.getProtocol();
                if (protocol.equalsIgnoreCase("https")) {
                    return "http://" + url.getURLString().substring(8);
                } else {
                    return urlStr;
                }
            } catch (Exception e) {
                e.printStackTrace();
                return urlStr;
            }
        } else {
            return urlStr;
        }
    }

    private void addHostHeader()
            throws MalformedURLException, MimeException {
        URLParser urlParser = new URLParser(getRequestLineURL());

        // write Host header
        if (urlParser.getPort() != 80) {
            addReplaceHeader(
                    "Host",
                    urlParser.getHost() + ":" + urlParser.getPort());
        } else {
            addReplaceHeader("Host", urlParser.getHost());
        }
    }

    /**
     * Gets request line used when connecting to web server. If request method
     * is get then request line is in the format of: <br>
     * <code>GET path HTTP/1.0</code>
     * <p/>
     * <p>
     * If request method is POST then request line is in the format of: <br>
     * <code>POST path HTTP/1.0</code>
     * </p>
     * <p/>
     * <p>
     * Where path represents the relative path of the URL to retrieve of
     * absolute path of the URL to retrieve if connecting through a proxy
     * server.
     * </p>
     *
     * @return the reguest line sent to the HTTP server
     *
     * @throws MalformedURLException if an error occurs while trying to build
     *                               request line due to invalid URL
     */
    protected String getRequestLine(boolean parametersRequired)
            throws MalformedURLException {
        String requestLineUrl = getRequestLineURL();
        StringBuilder url = new StringBuilder(requestLineUrl);

        if (parametersRequired) {
            Enumeration params = parameters.elements();

            // check if more parameters need to be added
            if (params.hasMoreElements()) {
                if (!this.urlStr.contains("?")) {
                    url.append("?");
                } else {
                    url.append("&");
                }
            }

            // add additional parameters to GET query string
            while (params.hasMoreElements()) {
                HttpParameter param = (HttpParameter) params.nextElement();
                url.append(
                        param.isEncoded()
                        ? param.getName()
                        : encode(param.getName()));
                url.append("=");
                String value = param.getValue();
                if (value != null) {
                    url.append(
                            param.isEncoded()
                            ? param.getValue()
                            : encode(param.getValue()));
                }

                if (params.hasMoreElements()) {
                    url.append("&");
                }
            }
        }

        URLParser urlParser = new URLParser(url.toString());
        StringBuilder requestLine = new StringBuilder().append(getMethod()).append(" ");

        if (getProxyHost() != null) {
            requestLine.append(urlParser.getURLString());
        } else {
            if (!urlParser.getPath().startsWith("/")) {
                requestLine.append("/");
            }

            requestLine.append(urlParser.getPath());

            String query = urlParser.getQuery();

            if (query != null) {
                requestLine.append("?");
                requestLine.append(query);
            }
        }

        requestLine.append(" HTTP/").append(httpVersion);

        return requestLine.toString();
    }

    /**
     * Gets the URL encoded name / value pairs used in request.
     *
     * @return request data
     *
     * @throws HttpException if an error occurs while building up request data
     */
    protected String getRequestData()
            throws HttpException {
        StringBuffer formData = new StringBuffer();

        synchronized (this) {
            try {
                Enumeration p = parameters.elements();

                if (p.hasMoreElements()) {
                    if (method.equals("GET")) {
                        formData.append("?");
                    }

                    while (p.hasMoreElements()) {
                        HttpParameter params = (HttpParameter) p.nextElement();

                        formData.append(encode(params.getName()));

                        // this fixes a bug not allowing non-value parameters
                        String val = params.getValue();

                        if (val != null) {
                            formData.append("=");
                            formData.append(encode(val));
                        }

                        if (p.hasMoreElements()) {
                            formData.append("&");
                        }
                    }
                }
            } catch (Exception e) {
                throw new HttpException(e.getMessage(), e);
            }
        }

        return formData.toString();
    }

    /**
     * Adds Proxy-Authorization header to Http request.
     *
     * @throws MimeException if a MIME related error occurs
     */
    private void addProxyAuthorizationHeader()
            throws MimeException {
        if (proxyUsername != null) {
            addReplaceHeader("Proxy-Authorization", BasicAuthorizationFormat.format(proxyUsername, proxyPassword));
        }
    }

    /**
     * Gets contents of this <code>HttpRequest</code>. Contents include all
     * MIME message headers, body and any parts. If request method is POST
     * then required Content-Type and Content-Length headers are added
     * automatically unless already defined using
     * <code>MimeMessage#addHeader</code> method.
     *
     * @return message contents
     *
     * @throws MimeException if a MIME related error occurs while building message contents
     * @see MimeMessage#addHeader
     */
    public byte[] getMessage()
            throws MimeException {
        if ("POST".equals(this.method) || "PUT".equals(this.method)) {
            int contentLength = 0;

            try {
                // get request data
                String requestData = getRequestData();

                // set message body if needed
                if ((requestData != null) && (requestData.length() > 0)) {
                    setBody(requestData);
                }

                // split message into body and header portions
                ByteBuffer buffer = new ByteBuffer(super.getMessage());
                byte[] headersEnd = { CR, LF, CR, LF };
                int bodyStart = buffer.indexOf(headersEnd);
                ByteBuffer body = buffer.getBuffer(bodyStart + headersEnd.length);
                byte[] bytes = body.toByteArray();
                long length = bytes.length;

                for (int i = 0; i < this.parts.size(); i++) {
                    MimeMessage msg = (MimeMessage) this.parts.get(i);
                    if (msg.getBodyAsFile() != null) {
                        length += msg.getBodyAsFile().length();
                        length -= HttpFileUpload.PLACEHOLDER.getBytes(StandardCharsets.US_ASCII).length;
                        break;
                    }
                }

                // set content type header if needed
                if (getHeader("Content-Type") == null) {
                    addReplaceHeader(
                            new MimeHeader(
                                    "Content-Type",
                                    "application/x-www-form-urlencoded"));
                }

                // set content length header if needed
                if (getHeader("Content-Length") == null) {
                    addReplaceHeader(
                            new MimeHeader(
                                    "Content-Length",
                                    Long.toString(length)));
                }
            } catch (HttpException he) {
                throw new MimeException(he.getMessage(), he);
            }
        }

        // write connection header
        if (proxyHost != null) {
            addProxyAuthorizationHeader();
            addReplaceHeader("Proxy-Connection", "Close");
        } else {
            addReplaceHeader("Connection", "Close");
        }
        return super.getMessage();
    }

    /**
     * Gets the HTTP version number sent in this <code>HttpRequest</code>.
     * Default value is 1.0
     *
     * @return the HTTP version number
     */
    public String getHttpVersion() {
        return httpVersion;
    }

    /**
     * Sets the HTTP version number sent in this <code>HttpRequest</code>.
     * Default value is 1.0
     *
     * @param httpVersion the HTTP version number
     */
    public void setHttpVersion(String httpVersion) {
        this.httpVersion = httpVersion;
    }

    /**
     * Encode a string to the "x-www-form-urlencoded" form, enhanced
     * with the UTF-8-in-URL proposal. This is what happens:
     * <p/>
     * <ul>
     * <li><p>The ASCII characters 'a' through 'z', 'A' through 'Z',
     * and '0' through '9' remain the same.
     * <p/>
     * <li><p>The unreserved characters - _ . ! ~ * ' ( ) remain the same.
     * <p/>
     * <li><p>The space character ' ' is converted into a plus sign '+'.
     * <p/>
     * <li><p>All other ASCII characters are converted into the
     * 3-character string "%xy", where xy is
     * the two-digit hexadecimal representation of the character
     * code
     * <p/>
     * <li><p>All non-ASCII characters are encoded in two steps: first
     * to a sequence of 2 or 3 bytes, using the UTF-8 algorithm;
     * secondly each of these bytes is encoded as "%xx".
     * </ul>
     *
     * @param s The string to be encoded
     *
     * @return The encoded string
     */
    private String encode(String s) {
        if (!utf8) {
            try {
                return URLEncoder.encode(s, Charset.defaultCharset().name());
            } catch (UnsupportedEncodingException e) {
                throw new RuntimeException(e);
            }
        } else {
            StringBuilder buffer = new StringBuilder();
            int len = s.length();
            for (int i = 0; i < len; i++) {
                int ch = s.charAt(i);
                if ('A' <= ch && ch <= 'Z') { // 'A'..'Z'
                    buffer.append((char) ch);
                } else if ('a' <= ch && ch <= 'z') { // 'a'..'z'
                    buffer.append((char) ch);
                } else if ('0' <= ch && ch <= '9') { // '0'..'9'
                    buffer.append((char) ch);
                } else if (ch == ' ') { // space
                    buffer.append('+');
                } else if (
                        ch == '-'
                                || ch == '_' // unreserved
                                || ch == '.'
                                || ch == '!'
                                || ch == '~'
                                || ch == '*'
                                || ch == '\''
                                || ch == '('
                                || ch == ')') {
                    buffer.append((char) ch);
                } else if (ch <= 0x007f) { // other ASCII
                    buffer.append(HEX[ch]);
                } else if (ch <= 0x07FF) { // non-ASCII <= 0x7FF
                    buffer.append(HEX[0xc0 | (ch >> 6)]);
                    buffer.append(HEX[0x80 | (ch & 0x3F)]);
                } else { // 0x7FF < ch <= 0xFFFF
                    buffer.append(HEX[0xe0 | (ch >> 12)]);
                    buffer.append(HEX[0x80 | ((ch >> 6) & 0x3F)]);
                    buffer.append(HEX[0x80 | (ch & 0x3F)]);
                }
            }
            return buffer.toString();

        }
    }

    /**
     * Gets whether UTF-8 encoding is used when encoding parameters, default is true.
     *
     * @return true if UTF-8 encoding is enabled, false otherwise
     */
    public boolean isUTF8() {
        return utf8;
    }

    /**
     * Sets whether UTF-8 encoding is used when encoding parameters, default is true.
     *
     * @param b true if UTF-8 encoding is enabled, false otherwise
     */
    public void setUTF8(boolean b) {
        utf8 = b;
    }
}
