/*
 * Copyright 2014 Red Hat, Inc. and/or its affiliates.
 *
 * 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.
 */
/*
 * Copyright (c) 2014 Kevin Sawicki <kevinsawicki@gmail.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to
 * deal in the Software without restriction, including without limitation the
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
 * sell copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
 * IN THE SOFTWARE.
 */
package org.kie.remote.common.rest;

import static java.net.HttpURLConnection.HTTP_BAD_REQUEST;
import static java.net.Proxy.Type.HTTP;
import static javax.ws.rs.HttpMethod.DELETE;
import static javax.ws.rs.HttpMethod.GET;
import static javax.ws.rs.HttpMethod.POST;
import static javax.ws.rs.HttpMethod.PUT;
import static javax.ws.rs.core.HttpHeaders.ACCEPT;
import static javax.ws.rs.core.HttpHeaders.ACCEPT_CHARSET;
import static javax.ws.rs.core.HttpHeaders.ACCEPT_ENCODING;
import static javax.ws.rs.core.HttpHeaders.AUTHORIZATION;
import static javax.ws.rs.core.HttpHeaders.CONTENT_ENCODING;
import static javax.ws.rs.core.HttpHeaders.CONTENT_LENGTH;
import static javax.ws.rs.core.HttpHeaders.CONTENT_TYPE;
import static javax.ws.rs.core.MediaType.APPLICATION_FORM_URLENCODED;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.Flushable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.Proxy;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.security.AccessController;
import java.security.GeneralSecurityException;
import java.security.PrivilegedAction;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.Callable;
import java.util.zip.GZIPInputStream;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import javax.ws.rs.core.MediaType;

/**
 * This class is only meant to be used internally by the kie-remote-client code! For interacting with the
 * REST API, please use a proper REST framework such as RestEasy or Apache CXF.
 * </p>
 * <b>Using this class to interact with the REST API w<i>ill not be supported</i> and any issues or problems
 * that arise from such use will be dismissed with a referral to this exact text!</b>
 * </p>
 * <rr>
 * A fluid interface for making HTTP requests using an underlying {@link HttpURLConnection} (or sub-class).
 * <p>
 * Each instance supports making a single request and cannot be reused for further requests.
 *
 * This code was originally copied from Kevin Sawicki's
 * <a href="https://github.com/kevinsawicki/http-request">HttpRequest project</a> * project.
 * </p>
 * However, it has been extensively modified and rewritten to fit the use case in this code.
 */
public class KieRemoteHttpRequest {

    public static final String CHARSET_UTF8 = "UTF-8";
    public static final String HEADER_PROXY_AUTHORIZATION = "Proxy-Authorization";
    public static final String HEADER_REFERER = "Referer";
    public static final String HEADER_SERVER = "Server";
    public static final String PARAM_CHARSET = "charset";
    private static final String[] EMPTY_STRINGS = new String[0];

    // Request information

    private static final int DEFAULT_TIMEOUT_SECS = 5;

    private RequestInfo requestInfo;

    private int bufferSize = 8192;
    private boolean ignoreCloseExceptions = true;
    boolean uncompress = false;

    private HttpURLConnection connection = null;
    private RequestOutputStream output;

    boolean followRedirects = false;
    String httpProxyHost;
    int httpProxyPort;


    private KieRemoteHttpResponse response = null;

    private static class RequestInfo {
        URL baseUrl;
        URL requestUrl;

        String user;
        String password;

        Integer timeoutInMilliSecs = DEFAULT_TIMEOUT_SECS * 1000;

        String requestMethod;

        Map<String, List<String>> headers;
        Map<String, List<String>> queryParameters;
        Map<String, List<String>> formParameters;
        boolean form = false;
        String charset;

        StringBuilder body;
        MediaType bodyContentType;

        public URL getRequestUrl() {
            if( requestUrl == null ) {
                requestUrl = baseUrl;
            }
            return requestUrl;
        }

        public void setRequestUrl( String urlString ) {
            requestUrl = convertStringToUrl(urlString);
        }

        public List<String> getHeader( String name ) {
            if( headers == null ) {
                headers = new LinkedHashMap<String, List<String>>();
            }
            if( headers.get(name) == null ) {
                return Collections.EMPTY_LIST;
            } else {
                return headers.get(name);
            }
        }

        public void setHeader( String name, Object value ) {
            if( this.headers == null ) {
                this.headers = new LinkedHashMap<String, List<String>>();
            }
            if( this.headers.get(name) == null ) {
                this.headers.put(name, new ArrayList<String>());
            }
            this.headers.get(name).add(value == null ? null : value.toString());
        }

        public void setQueryParameter( String name, Object value ) {
            if( this.queryParameters == null ) {
                this.queryParameters = new LinkedHashMap<String, List<String>>();
            }
            if( this.queryParameters.get(name) == null ) {
                this.queryParameters.put(name, new ArrayList<String>());
            }
            this.queryParameters.get(name).add(value == null ? null : value.toString());
        }

        public void setFormParameter( String name, Object value ) {
            if( this.formParameters == null ) {
                this.formParameters = new LinkedHashMap<String, List<String>>();
            }
            if( this.formParameters.get(name) == null ) {
                this.formParameters.put(name, new ArrayList<String>());
            }
            this.formParameters.get(name).add(value == null ? null : value.toString());
        }

        public void addToBody( CharSequence addToBody ) {
            if( this.body == null ) {
                this.body = new StringBuilder();
            }
            this.body.append(addToBody);
        }

        @Override
        public RequestInfo clone() {
            RequestInfo clone = new RequestInfo();
            clone.baseUrl = baseUrl;
            clone.body = body;
            clone.bodyContentType = bodyContentType;
            clone.charset = charset;
            clone.form = form;
            clone.formParameters = formParameters;
            clone.headers = headers;
            clone.password = password;
            clone.queryParameters = queryParameters;
            clone.requestMethod = requestMethod;
            clone.requestUrl = requestUrl;
            clone.timeoutInMilliSecs = timeoutInMilliSecs;
            clone.user = user;
            return clone;
        }
    }

    private RequestInfo getRequestInfo() {
        if( requestInfo == null ) {
            requestInfo = new RequestInfo();
        }
        return requestInfo;
    }

    /**
     * Creates {@link HttpURLConnection HTTP connections} for {@link URL urls}.
     */
    public interface ConnectionFactory {
        /**
         * Open an {@link HttpURLConnection} for the specified {@link URL}.
         *
         * @throws IOException
         */
        HttpURLConnection create( URL url ) throws IOException;

        /**
         * Open an {@link HttpURLConnection} for the specified {@link URL} and {@link Proxy}.
         *
         * @throws IOException
         */
        HttpURLConnection create( URL url, Proxy proxy ) throws IOException;

        /**
         * A {@link ConnectionFactory} which uses the built-in {@link URL#openConnection()}
         */
        ConnectionFactory DEFAULT = new ConnectionFactory() {
            public HttpURLConnection create( URL url ) throws IOException {
                return (HttpURLConnection) url.openConnection();
            }

            public HttpURLConnection create( URL url, Proxy proxy ) throws IOException {
                return (HttpURLConnection) url.openConnection(proxy);
            }
        };
    }

    private static ConnectionFactory CONNECTION_FACTORY = ConnectionFactory.DEFAULT;

    /**
     * Operation that handles executing a callback once complete and handling
     * nested exceptions
     *
     * @param <V>
     */
    private static abstract class Operation<V> implements Callable<V> {

        /**
         * Run operation
         *
         * @return result
         * @throws KieRemoteHttpRequestException
         * @throws IOException
         */
        protected abstract V run() throws KieRemoteHttpRequestException, IOException;

        /**
         * Operation complete callback
         *
         * @throws IOException
         */
        protected abstract void done() throws IOException;

        public V call() throws KieRemoteHttpRequestException {
            boolean thrown = false;
            try {
                return run();
            } catch( KieRemoteHttpRequestException e ) {
                thrown = true;
                throw e;
            } catch( IOException ioe ) {
                thrown = true;
                throw new KieRemoteHttpRequestException("Unable to do " + this.getClass().getSimpleName(), ioe);
            } finally {
                try {
                    done();
                } catch( IOException ioe ) {
                    if( !thrown )
                        throw new KieRemoteHttpRequestException("Exception thrown when finishing "
                                + this.getClass().getSimpleName(), ioe);
                }
            }
        }
    }

    /**
     * Class that ensures a {@link Closeable} gets closed with proper exception
     * handling.
     *
     * @param <V>
     */
    private static abstract class CloseOperation<V> extends Operation<V> {

        private final Closeable closeable;

        private final boolean ignoreCloseExceptions;

        /**
         * Create closer for operation
         *
         * @param closeable
         * @param ignoreCloseExceptions
         */
        protected CloseOperation(final Closeable closeable, final boolean ignoreCloseExceptions) {
            this.closeable = closeable;
            this.ignoreCloseExceptions = ignoreCloseExceptions;
        }

        @Override
        protected void done() throws IOException {
            if( closeable instanceof Flushable )
                ((Flushable) closeable).flush();
            if( ignoreCloseExceptions )
                try {
                    closeable.close();
                } catch( IOException ioe ) {
                    // Ignored
                }
            else
                closeable.close();
        }
    }

    /**
     * Class that and ensures a {@link Flushable} gets flushed with proper
     * exception handling.
     *
     * @param <V>
     */
    private static abstract class FlushOperation<V> extends Operation<V> {

        private final Flushable flushable;

        /**
         * Create flush operation
         *
         * @param flushable
         */
        protected FlushOperation(final Flushable flushable) {
            this.flushable = flushable;
        }

        @Override
        protected void done() throws IOException {
            flushable.flush();
        }
    }

    /**
     * Request output stream
     */
    public static class RequestOutputStream extends BufferedOutputStream {

        private final CharsetEncoder encoder;

        /**
         * Create request output stream
         *
         * @param stream
         * @param charset
         * @param bufferSize
         */
        public RequestOutputStream(final OutputStream stream, final String charset, final int bufferSize) {
            super(stream, bufferSize);

            encoder = Charset.forName(getValidCharset(charset)).newEncoder();
        }

        /**
         * Write string to stream
         *
         * @param value
         * @return this stream
         * @throws IOException
         */
        public RequestOutputStream write( final String value ) throws IOException {
            final ByteBuffer bytes = encoder.encode(CharBuffer.wrap(value));

            super.write(bytes.array(), 0, bytes.limit());

            return this;
        }
    }

    private static String getValidCharset( final String charset ) {
        if( charset != null && charset.length() > 0 )
            return charset;
        else
            return CHARSET_UTF8;
    }

    private static StringBuilder addPathSeparator( final String baseUrl, final StringBuilder result ) {
        // Add trailing slash if the base URL doesn't have any path segments.
        //
        // The following test is checking for the last slash not being part of
        // the protocol to host separator: '://'.
        if( baseUrl.indexOf(':') + 2 == baseUrl.lastIndexOf('/') )
            result.append('/');
        return result;
    }

    private static StringBuilder addParamPrefix( final String baseUrl, final StringBuilder result ) {
        // Add '?' if missing and add '&' if params already exist in base url
        final int queryStart = baseUrl.indexOf('?');
        final int lastChar = result.length() - 1;
        if( queryStart == -1 )
            result.append('?');
        else if( queryStart < lastChar && baseUrl.charAt(lastChar) != '&' )
            result.append('&');
        return result;
    }

    /**
     * Encode the given URL as an ASCII {@link String}
     * <p>
     * This method ensures the path and query segments of the URL are properly encoded such as ' ' characters being encoded to '%20'
     * or any UTF-8 characters that are non-ASCII. No encoding of URLs is done by default by the {@link KieRemoteHttpRequest}
     * constructors and so if URL encoding is needed this method should be called before calling the {@link KieRemoteHttpRequest}
     * constructor.
     *
     * @param url
     * @return encoded URL
     * @throws KieRemoteHttpRequestException
     */
    static String encodeUrlToUTF8( final CharSequence url ) throws KieRemoteHttpRequestException {
        URL parsed;
        try {
            parsed = new URL(url.toString());
        } catch( IOException ioe ) {
            throw new KieRemoteHttpRequestException("Unable to encode url '" + url.toString() + "'", ioe);
        }

        String host = parsed.getHost();
        int port = parsed.getPort();
        if( port != -1 )
            host = host + ':' + Integer.toString(port);

        try {
            String encoded = new URI(parsed.getProtocol(), host, parsed.getPath(), parsed.getQuery(), null).toASCIIString();
            int paramsStart = encoded.indexOf('?');
            if( paramsStart > 0 && paramsStart + 1 < encoded.length() )
                encoded = encoded.substring(0, paramsStart + 1) + encoded.substring(paramsStart + 1).replace("+", "%2B");
            return encoded;
        } catch( URISyntaxException e ) {
            KieRemoteHttpRequestException krhre = new KieRemoteHttpRequestException("Unable to parse parse URI", e);
            throw krhre;
        }
    }

    /**
     * Append given map as query parameters to the base URL
     * <p>
     * Each map entry's key will be a parameter name and the value's {@link Object#toString()} will be the parameter value.
     *
     * @param url
     * @param params
     * @return URL with appended query params
     */
    static String appendQueryParameters( final CharSequence url, final Map<?, ?> params ) {
        final String baseUrl = url.toString();
        if( params == null || params.isEmpty() )
            return baseUrl;

        final StringBuilder result = new StringBuilder(baseUrl);

        addPathSeparator(baseUrl, result);
        addParamPrefix(baseUrl, result);

        Entry<?, ?> entry;
        Object value;
        Iterator<?> iterator = params.entrySet().iterator();
        entry = (Entry<?, ?>) iterator.next();
        result.append(entry.getKey().toString());
        result.append('=');
        value = entry.getValue();
        if( value != null )
            result.append(value);

        while( iterator.hasNext() ) {
            result.append('&');
            entry = (Entry<?, ?>) iterator.next();
            result.append(entry.getKey().toString());
            result.append('=');
            value = entry.getValue();
            if( value != null )
                result.append(value);
        }

        return result.toString();
    }

    /**
     * Append given name/value pairs as query parameters to the base URL
     * <p>
     * The params argument is interpreted as a sequence of name/value pairs so the given number of params must be divisible by 2.
     *
     * @param url
     * @param params
     * name/value pairs
     * @return URL with appended query params
     */
    static String appendQueryParameters( final CharSequence url, final Object... params ) {
        final String baseUrl = url.toString();
        if( params == null || params.length == 0 ) {
            return baseUrl;
        }

        if( params.length % 2 != 0 ) {
            throw new IllegalArgumentException("Must specify an even number of parameter names/values");
        }

        final StringBuilder result = new StringBuilder(baseUrl);

        addPathSeparator(baseUrl, result);
        addParamPrefix(baseUrl, result);

        Object value;
        result.append(params[0]);
        result.append('=');
        value = params[1];
        if( value != null )
            result.append(value);

        for( int i = 2; i < params.length; i += 2 ) {
            result.append('&');
            result.append(params[i]);
            result.append('=');
            value = params[i + 1];
            if( value != null ) {
                result.append(value);
            }
        }

        return result.toString();
    }

    public static void setKeepAlive( final boolean keepAlive ) {
        setProperty("http.keepAlive", Boolean.toString(keepAlive));
    }

    public static void setMaxConnections( final int maxConnections ) {
        setProperty("http.maxConnections", Integer.toString(maxConnections));
    }

    private static String setProperty( final String name, final String value ) {
        final PrivilegedAction<String> action;
        if( value != null )
            action = new PrivilegedAction<String>() {

                public String run() {
                    return System.setProperty(name, value);
                }
            };
        else
            action = new PrivilegedAction<String>() {

                public String run() {
                    return System.clearProperty(name);
                }
            };
        return AccessController.doPrivileged(action);
    }

    // Factory methods ------------------------------------------------------------------------------------------------------------

    public static KieRemoteHttpRequest deleteRequest( final URL url ) throws KieRemoteHttpRequestException {
        KieRemoteHttpRequest request = new KieRemoteHttpRequest(url);
        request.getRequestInfo().requestMethod = DELETE;
        return request;
    }

    public static KieRemoteHttpRequest putRequest( final URL url ) throws KieRemoteHttpRequestException {
        KieRemoteHttpRequest request = new KieRemoteHttpRequest(url);
        request.getRequestInfo().requestMethod = PUT;
        return request;
    }

    public static KieRemoteHttpRequest getRequest( final String urlString ) throws KieRemoteHttpRequestException {
        KieRemoteHttpRequest request = new KieRemoteHttpRequest(urlString);
        request.getRequestInfo().requestMethod = GET;
        return request;
    }

    public static KieRemoteHttpRequest getRequest( final URL url ) throws KieRemoteHttpRequestException {
        KieRemoteHttpRequest request = new KieRemoteHttpRequest(url);
        request.getRequestInfo().requestMethod = GET;
        return request;
    }

    public static KieRemoteHttpRequest postRequest( final URL url ) throws KieRemoteHttpRequestException {
        KieRemoteHttpRequest request = new KieRemoteHttpRequest(url);
        request.getRequestInfo().requestMethod = POST;
        return request;
    }

    public static KieRemoteHttpRequest newRequest( final String url ) throws KieRemoteHttpRequestException {
        return new KieRemoteHttpRequest(url);
    }

    public static KieRemoteHttpRequest newRequest( final URL url ) throws KieRemoteHttpRequestException {
        return new KieRemoteHttpRequest(url);
    }

    public static KieRemoteHttpRequest newRequest( final String url, String username, String password )
            throws KieRemoteHttpRequestException {
        return new KieRemoteHttpRequest(url, username, password);
    }

    public static KieRemoteHttpRequest newRequest( final URL url, String username, String password )
            throws KieRemoteHttpRequestException {
        return new KieRemoteHttpRequest(url, username, password);
    }

    private KieRemoteHttpRequest(final URL url) throws KieRemoteHttpRequestException {
        getRequestInfo().baseUrl = url;
    }

    private KieRemoteHttpRequest(final String urlString) throws KieRemoteHttpRequestException {
        getRequestInfo().baseUrl = convertStringToUrl(urlString);
    }

    private static URL convertStringToUrl( final String urlString ) throws KieRemoteHttpRequestException {
        try {
            return new URL(urlString);
        } catch( MalformedURLException e ) {
            throw new KieRemoteHttpRequestException("Unable to create request with url '" + urlString + "'", e);
        }

    }

    // Constructors --------------------------------------------------------------------------------------------------------------

    private KieRemoteHttpRequest(RequestInfo requestInfo) {
        this.requestInfo = requestInfo;
    }

    private KieRemoteHttpRequest(URL stringUrl, String username, String password) {
        RequestInfo requestInfo = getRequestInfo();
        requestInfo.baseUrl = stringUrl;
        requestInfo.user = username;
        requestInfo.password = password;
    }

    private KieRemoteHttpRequest(String stringUrl, String username, String password) {
        this(stringUrl);
        RequestInfo requestInfo = getRequestInfo();
        requestInfo.user = username;
        requestInfo.password = password;
    }

    // HTTP methods --------------------------------------------------------------------------------------------------------------

    public KieRemoteHttpRequest get( final String relativeUrl ) throws KieRemoteHttpRequestException {
        relativeRequest(relativeUrl, GET);
        responseCode();
        return this;
    }

    public KieRemoteHttpRequest get() throws KieRemoteHttpRequestException {
        getRequestInfo().requestMethod = GET;
        responseCode();
        return this;
    }

    public KieRemoteHttpRequest post( final String relativeUrl ) throws KieRemoteHttpRequestException {
        relativeRequest(relativeUrl, POST);
        responseCode();
        return this;
    }

    public KieRemoteHttpRequest post() throws KieRemoteHttpRequestException {
        getRequestInfo().requestMethod = POST;
        responseCode();
        return this;
    }

    public KieRemoteHttpRequest put( final String relativeUrl ) throws KieRemoteHttpRequestException {
        relativeRequest(relativeUrl, PUT);
        responseCode();
        return this;
    }

    public KieRemoteHttpRequest put() throws KieRemoteHttpRequestException {
        getRequestInfo().requestMethod = PUT;
        responseCode();
        return this;
    }

    public KieRemoteHttpRequest delete( final String relativeUrl ) throws KieRemoteHttpRequestException {
        relativeRequest(relativeUrl, DELETE);
        responseCode();
        return this;
    }

    public KieRemoteHttpRequest delete() throws KieRemoteHttpRequestException {
        getRequestInfo().requestMethod = DELETE;
        responseCode();
        return this;
    }

    // General Input/Output helper methods ----------------------------------------------------------------------------------------

    /**
     * Copy from input stream to output stream
     *
     * @param input
     * @param output
     * @return this request
     * @throws IOException
     */
    private KieRemoteHttpRequest copy( final InputStream input, final OutputStream output ) throws IOException {
        return new CloseOperation<KieRemoteHttpRequest>(input, ignoreCloseExceptions) {

            @Override
            public KieRemoteHttpRequest run() throws IOException {
                final byte[] buffer = new byte[bufferSize];
                int read;
                while( (read = input.read(buffer)) != -1 ) {
                    output.write(buffer, 0, read);
                }
                return KieRemoteHttpRequest.this;
            }
        }.call();
    }

    // Fluent setter's/ property getter's -----------------------------------------------------------------------------------------

    public KieRemoteHttpRequest ignoreCloseExceptions( final boolean ignore ) {
        ignoreCloseExceptions = ignore;
        return this;
    }

    public boolean ignoreCloseExceptions() {
        return ignoreCloseExceptions;
    }

    public KieRemoteHttpRequest bufferSize( final int size ) {
        if( size < 1 )
            throw new IllegalArgumentException("Size must be greater than zero");
        bufferSize = size;
        return this;
    }

    public int bufferSize() {
        return bufferSize;
    }

    /**
     * Set whether or not the response body should be automatically uncompressed when read from.
     * <p>
     * This will only affect requests that have the 'Content-Encoding' response header set to 'gzip'.
     * <p>
     * This causes all receive methods to use a {@link GZIPInputStream} when applicable so that higher level streams and readers can
     * read the data uncompressed.
     * <p>
     * Setting this option does not cause any request headers to be set automatically so {@link #acceptGzipEncoding()} should be
     * used in conjunction with this setting to tell the server to gzip the response.
     *
     * @param uncompress
     * @return this request
     */
    public KieRemoteHttpRequest setUncompress( final boolean uncompress ) {
        this.uncompress = uncompress;
        return this;
    }

    public KieRemoteHttpRequest followRedirects( final boolean followRedirects ) {
        this.followRedirects = followRedirects;
        return this;
    }

    public URI getUri() {
        try {
            return getRequestInfo().getRequestUrl().toURI();
        } catch( URISyntaxException urise ) {
            throw new KieRemoteHttpRequestException("Invalid request URL", urise);
        }
    }

    public KieRemoteHttpRequest timeout( final long timeoutInMilliseconds ) {
        if( connection != null ) {
            connection.setReadTimeout((int) timeoutInMilliseconds);
        } else {
            getRequestInfo().timeoutInMilliSecs = (int) timeoutInMilliseconds;
        }
        return this;
    }

    private void setRequestUrl( String urlString ) {
        getRequestInfo().setRequestUrl(urlString);
    }

    // Connection methods --------------------------------------------------------------------------------------------------------

    private HttpURLConnection createConnection() {
        String urlString = getRequestInfo().getRequestUrl().toString();
        if( getRequestInfo().requestMethod == null ) {
            throw new KieRemoteHttpRequestException("Please specify (and execute?) a HTTP method first.");
        }
        try {
            final HttpURLConnection connection;
            if( this.httpProxyHost != null ) {
                Proxy proxy = new Proxy(HTTP, new InetSocketAddress(this.httpProxyHost, this.httpProxyPort));
                connection = CONNECTION_FACTORY.create(getRequestInfo().getRequestUrl(), proxy);
            } else {
                connection = CONNECTION_FACTORY.create(getRequestInfo().getRequestUrl());
            }
            // support for localhost and https
            if (getRequestInfo().getRequestUrl().getProtocol().equalsIgnoreCase("https") && getRequestInfo().getRequestUrl().getHost().equalsIgnoreCase("localhost")) {
                ((HttpsURLConnection) connection).setHostnameVerifier(new HostnameVerifier(){

                    public boolean verify(String hostname, javax.net.ssl.SSLSession sslSession) {
                        if (hostname.equalsIgnoreCase("localhost")) {
                            return true;
                        }
                        return false;
                    }
                });
            }

            connection.setRequestMethod(getRequestInfo().requestMethod);
            return connection;
        } catch( IOException ioe ) {
            throw new KieRemoteHttpRequestException("Unable to create (" + getRequestInfo().requestMethod + ") connection to '"
                    + urlString + "'", ioe);
        }
    }

    HttpURLConnection getConnection() {
        if( getRequestInfo().requestMethod == null ) {
            throw new KieRemoteHttpRequestException("Please set HTTP request method before opening a connection.");
        }
        initializeConnection();
        return connection;
    }

    private void initializeConnection() {
        if( connection == null ) {
            addQueryParametersToUrl();
            connection = createConnection();
            // timeout
            connection.setReadTimeout(getRequestInfo().timeoutInMilliSecs);
            connection.setConnectTimeout(getRequestInfo().timeoutInMilliSecs);

            // various
            RequestInfo requestInfo = getRequestInfo();
            int contentLength = 0;
            if( requestInfo.body != null ) {
                contentLength = requestInfo.body.toString().getBytes(Charset.forName("UTF-8")).length;
                connection.setFixedLengthStreamingMode(contentLength);
                List<String> contentTypeList = requestInfo.getHeader(ACCEPT);
                if( contentTypeList != null && ! contentTypeList.isEmpty() ) {
                   requestInfo.setHeader(CONTENT_TYPE, contentTypeList.get(0));
                }
            }
            requestInfo.setHeader(CONTENT_LENGTH, contentLength);
            connection.setInstanceFollowRedirects(followRedirects);

            // auth
            if( requestInfo.user != null && requestInfo.password != null ) {
                basicAuthorization(requestInfo.user, requestInfo.password);
            }

            // headers
            if( requestInfo.headers != null ) {
                for( Entry<String, List<String>> entry : requestInfo.headers.entrySet() ) {
                    List<String> headerVals = entry.getValue();
                    for( String val : headerVals ) {
                        connection.setRequestProperty(entry.getKey(), val);
                    }
                }
            }

            // output: form parameters, body
            addFormParametersToConnection();
            if( requestInfo.body != null ) {
                try {
                    openOutput();
                    output.write(requestInfo.body.toString());
                } catch( IOException ioe ) {
                    throw new KieRemoteHttpRequestException("Unable to add char sequence to request body", ioe);
                }
            }
        }
    }

    // relative request methods ---------------------------------------------------------------------------------------------------

    public KieRemoteHttpRequest relativeRequest( String relativeUrlString, String httpMethod ) {
        relativeRequest(relativeUrlString);
        getRequestInfo().requestMethod = httpMethod;
        return this;
    }

    public KieRemoteHttpRequest relativeRequest( String relativeUrlString ) {
        String baseUrlString = getRequestInfo().baseUrl.toExternalForm();
        boolean urlSlash = baseUrlString.endsWith("/");
        boolean postfixSlash = relativeUrlString.startsWith("/");
        String separator = "";
        if( !urlSlash && !postfixSlash ) {
            separator = "/";
        } else if( urlSlash && postfixSlash ) {
            relativeUrlString = relativeUrlString.substring(1);
        }

        setRequestUrl(baseUrlString + separator + relativeUrlString);
        return this;
    }

    // Fluent connection manipulation methods -------------------------------------------------------------------------------------

    public KieRemoteHttpRequest disconnect() {
        getConnection().disconnect();
        return this;
    }

    public KieRemoteHttpRequest resetStream() throws IOException {
        getConnection().getInputStream().reset();
        return this;
    }

    // Connection related getter methods -----------------------------------------------------------------------------------------

    public KieRemoteHttpRequest followRedirets( final boolean followRedirects ) {
        this.followRedirects = followRedirects;
        return this;
    }

    public URL getUrl() {
        return getRequestInfo().getRequestUrl();
    }

    public String getMethod() {
        return getRequestInfo().requestMethod;
    }

    // Request header related methods -------------------------------------------------------------------------------------------

    public String getHeader( final String name ) {
        List<String> headerVals = getRequestInfo().getHeader(name);
        if( headerVals != null && !headerVals.isEmpty() ) {
            return headerVals.get(0);
        }
        return null;
    }

    public KieRemoteHttpRequest header( final String name, final Object value ) {
        getRequestInfo().setHeader(name, value);
        return this;
    }

    public KieRemoteHttpRequest headers( final Map<String, String> headers ) {
        if( !headers.isEmpty() ) {
            for( Entry<String, String> header : headers.entrySet() ) {
                header(header.getKey(), header.getValue());
            }
        }
        return this;
    }

    public List<String> getRequestHeader( String headerName ) {
        return getRequestInfo().getHeader(headerName);
        // return getConnection().getRequestProperties().get(headerName);
    }

    /**
     * Set the 'Accept-Encoding' header to given value
     *
     * @param acceptEncoding
     * @return this request
     */
    public KieRemoteHttpRequest acceptEncoding( final String acceptEncoding ) {
        return header(ACCEPT_ENCODING, acceptEncoding);
    }

    /**
     * Set the 'Accept-Charset' header to given value
     *
     * @param acceptCharset
     * @return this request
     */
    public KieRemoteHttpRequest acceptCharset( final String acceptCharset ) {
        return header(ACCEPT_CHARSET, acceptCharset);
    }

    /**
     * Set the 'Authorization' header to given values in Basic authentication
     * format
     *
     * @param name
     * @param password
     * @return this request
     */
    public KieRemoteHttpRequest basicAuthorization( final String name, final String password ) {
        return header(AUTHORIZATION, "Basic " + Base64Util.encode(name + ':' + password));
    }

    /**
     * Set the 'Authorization' header to given values in Bearer/Token authentication
     * format
     *
     * @param token
     * @return this request
     */
    public KieRemoteHttpRequest tokenAuthorization( final String token ) {
        return header(AUTHORIZATION, "Bearer " + token);
    }

    /**
     * Set the 'Content-Type' request header to the given value
     *
     * @param contentType
     * @return this request
     */
    public KieRemoteHttpRequest contentType( final String contentType ) {
        return contentType(contentType, null);
    }

    /**
     * Set the 'Content-Type' request header to the given value and charset
     *
     * @param contentType
     * @param charset
     * @return this request
     */
    public KieRemoteHttpRequest contentType( final String contentType, final String charset ) {
        if( charset != null && charset.length() > 0 ) {
            final String separator = "; " + PARAM_CHARSET + '=';
            return header(CONTENT_TYPE, contentType + separator + charset);
        } else
            return header(CONTENT_TYPE, contentType);
    }

    /**
     * Set the 'Accept' header to given value
     *
     * @param accept
     * @return this request
     */
    public KieRemoteHttpRequest accept( final String accept ) {
        RequestInfo requestInfo = getRequestInfo();
        if( requestInfo.getHeader(ACCEPT).isEmpty() ) {
           requestInfo.setHeader(ACCEPT, new ArrayList<String>());
        }
        requestInfo.headers.get(ACCEPT).set(0, accept);
        return this;
    }

    // Request/Output management methods
    // --------------------------------------------------------------------------------------------------

    /**
     * Open output stream
     *
     * @return this request
     * @throws IOException
     */
    private KieRemoteHttpRequest openOutput() throws IOException {
        if( output != null ) {
            return this;
        }
        getConnection().setDoOutput(true);
        final String charset = getHeaderParam(getConnection().getRequestProperty(CONTENT_TYPE), PARAM_CHARSET);
        output = new RequestOutputStream(getConnection().getOutputStream(), charset, bufferSize);
        return this;
    }

    /**
     * Close output stream
     *
     * @return this request
     * @throws KieRemoteHttpRequestException
     * @throws IOException
     */
    private KieRemoteHttpRequest closeOutput() throws IOException {
        if( connection == null ) {
            throw new KieRemoteHttpRequestException("Please execute a HTTP method first on the request.");
        }
        if( output == null ) {
            return this;
        }
        if( ignoreCloseExceptions ) {
            try {
                output.close();
            } catch( IOException ignored ) {
                // Ignored
            }
        } else {
            output.close();
        }
        output = null;
        return this;
    }

    /**
     * Call {@link #closeOutput()} and re-throw a caught {@link IOException}s as
     * an {@link KieRemoteHttpRequestException}
     *
     * @return this request
     * @throws KieRemoteHttpRequestException
     */
    private KieRemoteHttpRequest closeOutputQuietly() throws KieRemoteHttpRequestException {
        try {
            return closeOutput();
        } catch( IOException ioe ) {
            throw new KieRemoteHttpRequestException("Unable to close output from response", ioe);
        }
    }

    // Request/Input helper methods -----------------------------------------------------------------------------------------------

    public KieRemoteHttpRequest body( final CharSequence value ) throws KieRemoteHttpRequestException {
        getRequestInfo().addToBody(value);
        return this;
    }

    public OutputStreamWriter writer() throws KieRemoteHttpRequestException {
        try {
            openOutput();
            return new OutputStreamWriter(output, output.encoder.charset());
        } catch( IOException ioe ) {
            throw new KieRemoteHttpRequestException("Unable to create writer to request output stream", ioe);
        }
    }

    // query parameter methods ----------------------------------------------------------------------------------------------------

    public KieRemoteHttpRequest query( final Object name, final Object value ) throws KieRemoteHttpRequestException {
        getRequestInfo().setQueryParameter(name.toString(), value != null ? value.toString() : null);
        return this;
    }

    public KieRemoteHttpRequest query( final Map<?, ?> values ) throws KieRemoteHttpRequestException {
        if( !values.isEmpty() ) {
            for( Entry<?, ?> entry : values.entrySet() ) {
                query(entry.getKey(), entry.getValue());
            }
        }
        return this;
    }

    private void addQueryParametersToUrl() {
        RequestInfo requestInfo = getRequestInfo();
        Object[] paramList = null;
        if( requestInfo.queryParameters != null ) {
            List<String> queryParamList = new ArrayList<String>();
            for( Entry<String, List<String>> paramListEntry : requestInfo.queryParameters.entrySet() ) {
                String name = paramListEntry.getKey();
                for( String val : paramListEntry.getValue() ) {
                    queryParamList.add(name);
                    queryParamList.add(val);
                }
            }
            paramList = queryParamList.toArray();
        }
        String unencodedUrlString = appendQueryParameters(requestInfo.getRequestUrl().toString(), paramList);
        String urlString = encodeUrlToUTF8(unencodedUrlString);
        requestInfo.setRequestUrl(urlString);
    }

    // Form parameter methods -----------------------------------------------------------------------------------------------------

    public KieRemoteHttpRequest form( final Object name, final Object value, String charset ) throws KieRemoteHttpRequestException {
        if( !getRequestInfo().form ) {
            contentType(APPLICATION_FORM_URLENCODED, charset);
            getRequestInfo().form = true;
        }
        charset = getValidCharset(charset);

        RequestInfo requestInfo = getRequestInfo();
        requestInfo.form = true;
        requestInfo.charset = charset;
        requestInfo.setFormParameter(name.toString(), value);

        return this;
    }

    public KieRemoteHttpRequest form( final Object name, final Object value ) throws KieRemoteHttpRequestException {
        return form(name, value, CHARSET_UTF8);
    }

    public KieRemoteHttpRequest form( final Map<?, ?> values, final String charset ) throws KieRemoteHttpRequestException {
        if( !values.isEmpty() ) {
            for( Entry<?, ?> entry : values.entrySet() ) {
                form(entry.getKey(), entry.getValue(), charset);
            }
        }
        return this;
    }

    public KieRemoteHttpRequest form( final Map<?, ?> values ) throws KieRemoteHttpRequestException {
        return form(values, CHARSET_UTF8);
    }

    private void addFormParametersToConnection() {
        RequestInfo requestInfo = getRequestInfo();
        if( requestInfo.form && requestInfo.formParameters != null ) {

            String name = null;
            String value = null;
            try {
                openOutput();
                boolean first = true;

                for( Entry<String, List<String>> entry : requestInfo.formParameters.entrySet() ) {
                    name = entry.getKey();
                    for( String formValue : entry.getValue() ) {
                        value = formValue;
                        if( !first ) {
                            output.write('&');
                        }
                        first = false;
                        output.write(URLEncoder.encode(name.toString(), requestInfo.charset));
                        output.write('=');
                        if( value != null ) {
                            output.write(URLEncoder.encode(value.toString(), requestInfo.charset));
                        }
                    }
                }
            } catch( IOException ioe ) {
                throw new KieRemoteHttpRequestException(
                        "Unable to add form parameter (" + name + "/" + value + ") to request body", ioe);
            }
        }
    }

    // Response related methods --------------------------------------------------------------------------------------------------

    public KieRemoteHttpResponse response() {
        if( this.response == null ) {
            this.response = new KieRemoteHttpResponse() {

                private String body = null;

                // @formatter:off
                @Override
                public InputStream stream() throws KieRemoteHttpRequestException { return responseStream(); }
                @Override
                public String message() throws KieRemoteHttpRequestException { return responseMessage(); }
                @Override
                public int intHeader( String name ) throws KieRemoteHttpRequestException { return intResponseHeader(name); }
                @Override
                public String[] headers( String name ) { return responseHeaders(name); }
                @Override
                public Map<String, List<String>> headers() throws KieRemoteHttpRequestException { return responseHeaders(); }
                @Override
                public Map<String, String> headerParameters( String headerName ) { return responseHeaderParameters(headerName); }
                @Override
                public String headerParameter( String headerName, String paramName ) { return responseHeaderParameter(headerName, paramName); }
                @Override
                public String header( String name ) throws KieRemoteHttpRequestException { return responseHeader(name); }
                @Override
                public String contentType() { return responseContentType(); }
                @Override
                public int contentLength() { return responseContentLength(); }
                @Override
                public String contentEncoding() { return responseContentEncoding(); }
                @Override
                public int code() throws KieRemoteHttpRequestException { return responseCode(); }
                @Override
                public String charset() { return responseCharset(); }
                @Override
                public byte[] bytes() throws KieRemoteHttpRequestException { return responseBytes(); }
                @Override
                public BufferedInputStream buffer() throws KieRemoteHttpRequestException { return responseBuffer(); }
                @Override
                public String body() throws KieRemoteHttpRequestException {
                    if( body == null ) {
                        body = responseBody();
                    }
                    return body;
                }
                // @formatter:on
            };
        }
        return this.response;
    }

    private int responseCode() throws KieRemoteHttpRequestException {
        initializeConnection();
        try {
            closeOutput();
            return getConnection().getResponseCode();
        } catch( IOException ioe ) {
            throw new KieRemoteHttpRequestException("Error occurred when trying to retrieve response code", ioe);
        }
    }

    private String responseMessage() throws KieRemoteHttpRequestException {
        initializeConnection();
        try {
            closeOutput();
            return getConnection().getResponseMessage();
        } catch( IOException ioe ) {
            throw new KieRemoteHttpRequestException("Error occurred when trying to retrieve response message", ioe);
        }
    }

    private String responseBody() throws KieRemoteHttpRequestException {
        String charset = responseCharset();
        final ByteArrayOutputStream output = byteStream();
        try {
            copy(responseBuffer(), output);
            return output.toString(getValidCharset(charset));
        } catch( IOException ioe ) {
            throw new KieRemoteHttpRequestException("Error occurred when retrieving response body", ioe);
        }
    }

    private byte[] responseBytes() throws KieRemoteHttpRequestException {
        final ByteArrayOutputStream output = byteStream();
        try {
            copy(responseBuffer(), output);
        } catch( IOException ioe ) {
            throw new KieRemoteHttpRequestException("Error occurred when retrieving byte content of response", ioe);
        }
        return output.toByteArray();
    }

    private ByteArrayOutputStream byteStream() {
        final int size = responseContentLength();
        if( size > 0 )
            return new ByteArrayOutputStream(size);
        else
            return new ByteArrayOutputStream();
    }

    private InputStream responseStream() throws KieRemoteHttpRequestException {
        InputStream stream;
        if( responseCode() < HTTP_BAD_REQUEST ) {
            try {
                stream = getConnection().getInputStream();
            } catch( IOException ioe ) {
                throw new KieRemoteHttpRequestException("Unable to retrieve input stream of response", ioe);
            }
        } else {
            stream = getConnection().getErrorStream();
            if( stream == null )
                try {
                    stream = getConnection().getInputStream();
                } catch( IOException ioe ) {
                    if( responseContentLength() > 0 )
                        throw new KieRemoteHttpRequestException("Unable to retrieve input stream of response", ioe);
                    else
                        stream = new ByteArrayInputStream(new byte[0]);
                }
        }

        if( !uncompress || !"gzip".equals(responseContentEncoding()) ) {
            return stream;
        } else {
            try {
                return new GZIPInputStream(stream);
            } catch( IOException e ) {
                throw new KieRemoteHttpRequestException("Unable to decompress gzipped stream", e);
            }
        }
    }

    private BufferedInputStream responseBuffer() throws KieRemoteHttpRequestException {
        return new BufferedInputStream(responseStream(), bufferSize);
    }

    // Response header related methods -------------------------------------------------------------------------------------------

    private String responseHeader( final String name ) throws KieRemoteHttpRequestException {
        closeOutputQuietly();
        return getConnection().getHeaderField(name);
    }

    private int intResponseHeader( final String name ) throws KieRemoteHttpRequestException {
        closeOutputQuietly();
        return getConnection().getHeaderFieldInt(name, -1);
    }

    private Map<String, List<String>> responseHeaders() throws KieRemoteHttpRequestException {
        closeOutputQuietly();
        return getConnection().getHeaderFields();
    }

    /**
     * Get all values of the given header from the response
     *
     * @param name
     * @return non-null but possibly empty array of {@link String} header values
     */
    private String[] responseHeaders( final String name ) {
        final Map<String, List<String>> headers = responseHeaders();
        if( headers == null || headers.isEmpty() )
            return EMPTY_STRINGS;

        final List<String> values = headers.get(name);
        if( values != null && !values.isEmpty() )
            return values.toArray(new String[values.size()]);
        else
            return EMPTY_STRINGS;
    }

    /**
     * Get parameter with given name from header value in response
     *
     * @param headerName
     * @param paramName
     * @return parameter value or null if missing
     */
    private String responseHeaderParameter( final String headerName, final String paramName ) {
        return getHeaderParam(responseHeader(headerName), paramName);
    }

    /**
     * Get all parameters from header value in response
     * <p>
     * This will be all key=value pairs after the first ';' that are separated by a ';'
     *
     * @param headerName
     * @return non-null but possibly empty map of parameter headers
     */
    private Map<String, String> responseHeaderParameters( final String headerName ) {
        return getHeaderParams(responseHeader(headerName));
    }

    /**
     * Get parameter values from header value
     *
     * @param header
     * @return parameter value or null if none
     */
    private static Map<String, String> getHeaderParams( final String header ) {
        if( header == null || header.length() == 0 )
            return Collections.emptyMap();

        final int headerLength = header.length();
        int start = header.indexOf(';') + 1;
        if( start == 0 || start == headerLength )
            return Collections.emptyMap();

        int end = header.indexOf(';', start);
        if( end == -1 )
            end = headerLength;

        Map<String, String> params = new LinkedHashMap<String, String>();
        while( start < end ) {
            int nameEnd = header.indexOf('=', start);
            if( nameEnd != -1 && nameEnd < end ) {
                String name = header.substring(start, nameEnd).trim();
                if( name.length() > 0 ) {
                    String value = header.substring(nameEnd + 1, end).trim();
                    int length = value.length();
                    if( length != 0 )
                        if( length > 2 && '"' == value.charAt(0) && '"' == value.charAt(length - 1) )
                            params.put(name, value.substring(1, length - 1));
                        else
                            params.put(name, value);
                }
            }

            start = end + 1;
            end = header.indexOf(';', start);
            if( end == -1 )
                end = headerLength;
        }

        return params;
    }

    /**
     * Get parameter value from header value
     *
     * @param value
     * @param paramName
     * @return parameter value or null if none
     */
    private static String getHeaderParam( final String value, final String paramName ) {
        if( value == null || value.length() == 0 )
            return null;

        final int length = value.length();
        int start = value.indexOf(';') + 1;
        if( start == 0 || start == length )
            return null;

        int end = value.indexOf(';', start);
        if( end == -1 )
            end = length;

        while( start < end ) {
            int nameEnd = value.indexOf('=', start);
            if( nameEnd != -1 && nameEnd < end && paramName.equals(value.substring(start, nameEnd).trim()) ) {
                String paramValue = value.substring(nameEnd + 1, end).trim();
                int valueLength = paramValue.length();
                if( valueLength != 0 )
                    if( valueLength > 2 && '"' == paramValue.charAt(0) && '"' == paramValue.charAt(valueLength - 1) )
                        return paramValue.substring(1, valueLength - 1);
                    else
                        return paramValue;
            }

            start = end + 1;
            end = value.indexOf(';', start);
            if( end == -1 )
                end = length;
        }

        return null;
    }

    private String responseContentEncoding() {
        return responseHeader(CONTENT_ENCODING);
    }

    private String responseContentType() {
        return responseHeader(CONTENT_TYPE);
    }

    private int responseContentLength() {
        closeOutputQuietly();
        return getConnection().getHeaderFieldInt(CONTENT_LENGTH, -1);
    }

    private String responseCharset() {
        return responseHeaderParameter(CONTENT_TYPE, PARAM_CHARSET);
    }

    // SSL / Certification methods -----------------------------------------------------------------------------------------------

    private static SSLSocketFactory TRUSTED_FACTORY;
    private static HostnameVerifier TRUSTED_VERIFIER;

    private static SSLSocketFactory getTrustedFactory() throws KieRemoteHttpRequestException {
        if( TRUSTED_FACTORY == null ) {
            final TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() {

                public X509Certificate[] getAcceptedIssuers() {
                    return new X509Certificate[0];
                }

                public void checkClientTrusted( X509Certificate[] chain, String authType ) {
                    // Intentionally left blank
                }

                public void checkServerTrusted( X509Certificate[] chain, String authType ) {
                    // Intentionally left blank
                }
            } };
            try {
                SSLContext context = SSLContext.getInstance("TLS");
                context.init(null, trustAllCerts, new SecureRandom());
                TRUSTED_FACTORY = context.getSocketFactory();
            } catch( GeneralSecurityException e ) {
                throw new KieRemoteHttpRequestException("Security exception configuring SSL context", e);
            }
        }

        return TRUSTED_FACTORY;
    }

    private static HostnameVerifier getTrustedVerifier() {
        if( TRUSTED_VERIFIER == null )
            TRUSTED_VERIFIER = new HostnameVerifier() {

                public boolean verify( String hostname, SSLSession session ) {
                    return true;
                }
            };

        return TRUSTED_VERIFIER;
    }

    /**
     * Configure HTTPS connection to trust all certificates
     * <p>
     * This method does nothing if the current request is not a HTTPS request
     *
     * @return this request
     * @throws KieRemoteHttpRequestException
     */
    public KieRemoteHttpRequest trustAllCerts() throws KieRemoteHttpRequestException {
        final HttpURLConnection connection = getConnection();
        if( connection instanceof HttpsURLConnection )
            ((HttpsURLConnection) connection).setSSLSocketFactory(getTrustedFactory());
        return this;
    }

    /**
     * Configure HTTPS connection to trust all hosts using a custom {@link HostnameVerifier} that always returns <code>true</code>
     * for each
     * host verified
     * <p>
     * This method does nothing if the current request is not a HTTPS request
     *
     * @return this request
     */
    public KieRemoteHttpRequest trustAllHosts() {
        final HttpURLConnection connection = getConnection();
        if( connection instanceof HttpsURLConnection )
            ((HttpsURLConnection) connection).setHostnameVerifier(getTrustedVerifier());
        return this;
    }

    // Proxy methods --------------------------------------------------------------------------------------------------------------

    public static void setProxyHost( final String host ) {
        setProperty("http.proxyHost", host);
        setProperty("https.proxyHost", host);
    }

    public static void setProxyPort( final int port ) {
        final String portValue = Integer.toString(port);
        setProperty("http.proxyPort", portValue);
        setProperty("https.proxyPort", portValue);
    }

    public static void setNonProxyHosts( final String... hosts ) {
        if( hosts != null && hosts.length > 0 ) {
            StringBuilder separated = new StringBuilder();
            int last = hosts.length - 1;
            for( int i = 0; i < last; i++ )
                separated.append(hosts[i]).append('|');
            separated.append(hosts[last]);
            setProperty("http.nonProxyHosts", separated.toString());
        } else
            setProperty("http.nonProxyHosts", null);
    }

    /**
     * Configure an HTTP proxy on this connection. Use {{@link #proxyBasic(String, String)} if
     * this proxy requires basic authentication.
     *
     * @param proxyHost
     * @param proxyPort
     * @return this request
     */
    public KieRemoteHttpRequest useProxy( final String proxyHost, final int proxyPort ) {
        if( connection != null ) {
            throw new IllegalStateException(
                    "The connection has already been created. This method must be called before reading or writing to the request.");
        }

        this.httpProxyHost = proxyHost;
        this.httpProxyPort = proxyPort;
        return this;
    }

    /**
     * Set the 'Proxy-Authorization' header to given value
     *
     * @param proxyAuthorization
     * @return this request
     */
    public KieRemoteHttpRequest proxyAuthorization( final String proxyAuthorization ) {
        return header(HEADER_PROXY_AUTHORIZATION, proxyAuthorization);
    }

    /**
     * Set the 'Proxy-Authorization' header to given values in Basic authentication
     * format
     *
     * @param name
     * @param password
     * @return this request
     */
    public KieRemoteHttpRequest proxyBasic( final String name, final String password ) {
        return proxyAuthorization("Basic " + Base64Util.encode(name + ':' + password));
    }

    // OTHER ----------------------------------------------------------------------------------------------------------------------

    @Override
    public String toString() {
        return getMethod() + ' ' + getUrl();
    }

    @Override
    public KieRemoteHttpRequest clone() {
        if( connection != null ) {
            throw new KieRemoteHttpRequestException("Unable to clone request with open or completed connection.");
        }
        return new KieRemoteHttpRequest(getRequestInfo().clone());
    }

}
