/*
 * Copyright (c) 2010-2011 Sonatype, Inc. All rights reserved.
 *
 * This program is licensed to you under the Apache License Version 2.0,
 * and you may not use this file except in compliance with the Apache License Version 2.0.
 * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the Apache License Version 2.0 is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
 */
package com.ning.http.client.providers.jdk;

import com.ning.http.client.AsyncHandler;
import com.ning.http.client.AsyncHttpClientConfig;
import com.ning.http.client.AsyncHttpProvider;
import com.ning.http.client.AsyncHttpProviderConfig;
import com.ning.http.client.Body;
import com.ning.http.client.FluentCaseInsensitiveStringsMap;
import com.ning.http.client.HttpResponseBodyPart;
import com.ning.http.client.HttpResponseHeaders;
import com.ning.http.client.HttpResponseStatus;
import com.ning.http.client.ListenableFuture;
import com.ning.http.client.MaxRedirectException;
import com.ning.http.client.PerRequestConfig;
import com.ning.http.client.ProgressAsyncHandler;
import com.ning.http.client.ProxyServer;
import com.ning.http.client.Realm;
import com.ning.http.client.Request;
import com.ning.http.client.RequestBuilder;
import com.ning.http.client.Response;
import com.ning.http.client.filter.FilterContext;
import com.ning.http.client.filter.FilterException;
import com.ning.http.client.filter.IOExceptionFilter;
import com.ning.http.client.filter.ResponseFilter;
import com.ning.http.client.listener.TransferCompletionHandler;
import com.ning.http.client.providers.netty.NettyAsyncHttpProvider;
import com.ning.http.multipart.MultipartRequestEntity;
import com.ning.http.util.AsyncHttpProviderUtils;
import com.ning.http.util.AuthenticatorUtils;
import com.ning.http.util.ProxyUtils;
import com.ning.http.util.SslUtils;
import com.ning.http.util.UTF8UrlEncoder;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.buffer.ChannelBuffers;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.naming.AuthenticationException;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLHandshakeException;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.net.Authenticator;
import java.net.ConnectException;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.PasswordAuthentication;
import java.net.Proxy;
import java.net.SocketAddress;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.security.GeneralSecurityException;
import java.security.NoSuchAlgorithmException;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.zip.GZIPInputStream;

import static com.ning.http.util.AsyncHttpProviderUtils.DEFAULT_CHARSET;

public class JDKAsyncHttpProvider implements AsyncHttpProvider {
    private final static Logger logger = LoggerFactory.getLogger(JDKAsyncHttpProvider.class);

    private final static String NTLM_DOMAIN = "http.auth.ntlm.domain";

    private final AsyncHttpClientConfig config;

    private final AtomicBoolean isClose = new AtomicBoolean(false);

    private final static int MAX_BUFFERED_BYTES = 8192;

    private final AtomicInteger maxConnections = new AtomicInteger();

    private String jdkNtlmDomain;

    private Authenticator jdkAuthenticator;

    private boolean bufferResponseInMemory = false;

    public JDKAsyncHttpProvider(AsyncHttpClientConfig config) {

        this.config = config;
        AsyncHttpProviderConfig<?, ?> providerConfig = config.getAsyncHttpProviderConfig();
        if (providerConfig != null && JDKAsyncHttpProviderConfig.class.isAssignableFrom(providerConfig.getClass())) {
            configure(JDKAsyncHttpProviderConfig.class.cast(providerConfig));
        }
    }

    private void configure(JDKAsyncHttpProviderConfig config) {
        for(Map.Entry<String,String> e: config.propertiesSet()) {
            System.setProperty(e.getKey(), e.getValue());
        }

        if (config.getProperty(JDKAsyncHttpProviderConfig.FORCE_RESPONSE_BUFFERING) != null) {
            bufferResponseInMemory = true;
        }
    }
    public <T> ListenableFuture<T> execute(Request request, AsyncHandler<T> handler) throws IOException {
        return execute(request, handler, null);
    }
    public <T> ListenableFuture<T> execute(Request request, AsyncHandler<T> handler, ListenableFuture<?> future) throws IOException {
        if (isClose.get()) {
            throw new IOException("Closed");
        }

        if (config.getMaxTotalConnections() > -1 && (maxConnections.get() + 1) > config.getMaxTotalConnections()) {
            throw new IOException(String.format("Too many connections %s", config.getMaxTotalConnections()));
        }

        ProxyServer proxyServer = request.getProxyServer() != null ? request.getProxyServer() : config.getProxyServer();
        Realm realm =  request.getRealm() != null ?  request.getRealm() : config.getRealm();
        boolean avoidProxy = ProxyUtils.avoidProxy( proxyServer, request );
        Proxy proxy = null;
        if (!avoidProxy && (proxyServer != null || realm != null)) {
            try {
                proxy = configureProxyAndAuth(proxyServer, realm);
            } catch (AuthenticationException e) {
                throw new IOException(e.getMessage());
            }
        }

        HttpURLConnection urlConnection = createUrlConnection(request);

        PerRequestConfig conf = request.getPerRequestConfig();
        int requestTimeout = (conf != null && conf.getRequestTimeoutInMs() != 0) ?
                conf.getRequestTimeoutInMs() : config.getRequestTimeoutInMs();

        JDKDelegateFuture delegate = null;
        if (future != null) {
            delegate = new JDKDelegateFuture(handler, requestTimeout, future, urlConnection);
        }

        JDKFuture f = delegate == null ? new JDKFuture<T>(handler, requestTimeout, urlConnection) : delegate;
        f.touch();

        f.setInnerFuture(config.executorService().submit(new AsyncHttpUrlConnection(urlConnection, request, handler, f)));
        maxConnections.incrementAndGet();

        return f;
    }

    private HttpURLConnection createUrlConnection(Request request) throws IOException {
        ProxyServer proxyServer = request.getProxyServer() != null ? request.getProxyServer() : config.getProxyServer();
        Realm realm =  request.getRealm() != null ?  request.getRealm() : config.getRealm();
        boolean avoidProxy = ProxyUtils.avoidProxy( proxyServer, request );
        Proxy proxy = null;
        if (!avoidProxy && proxyServer != null || realm != null) {
            try {
                proxy = configureProxyAndAuth(proxyServer, realm);
            } catch (AuthenticationException e) {
                throw new IOException(e.getMessage());
            }
        }

        HttpURLConnection urlConnection = null;
        if (proxy == null) {
            urlConnection =
                (HttpURLConnection) AsyncHttpProviderUtils.createUri( request.getUrl() ).toURL().openConnection( Proxy.NO_PROXY );
        } else {
            urlConnection = (HttpURLConnection) AsyncHttpProviderUtils.createUri(request.getUrl()).toURL().openConnection(proxy);
        }

        if (request.getUrl().startsWith("https")) {
            HttpsURLConnection secure = (HttpsURLConnection) urlConnection;
            SSLContext sslContext = config.getSSLContext();
            if (sslContext == null) {
                try {
                    sslContext = SslUtils.getSSLContext();
                } catch (NoSuchAlgorithmException e) {
                    throw new IOException(e.getMessage());
                } catch (GeneralSecurityException e) {
                    throw new IOException(e.getMessage());
                }
            }
            secure.setSSLSocketFactory(sslContext.getSocketFactory());
            secure.setHostnameVerifier(config.getHostnameVerifier());
        }
        return urlConnection;
    }

    public void close() {
        isClose.set(true);
    }

    public Response prepareResponse(HttpResponseStatus status, HttpResponseHeaders headers, Collection<HttpResponseBodyPart> bodyParts) {
        return new JDKResponse(status, headers, bodyParts);
    }

    private final class AsyncHttpUrlConnection<T> implements Callable<T> {

        private HttpURLConnection urlConnection;
        private Request request;
        private final AsyncHandler<T> asyncHandler;
        private final ListenableFuture<T> future;
        private int currentRedirectCount;
        private AtomicBoolean isAuth = new AtomicBoolean(false);
        private byte[] cachedBytes;
        private int cachedBytesLenght;
        private boolean terminate = true;

        public AsyncHttpUrlConnection(HttpURLConnection urlConnection, Request request, AsyncHandler<T> asyncHandler, ListenableFuture<T> future) {
            this.urlConnection = urlConnection;
            this.request = request;
            this.asyncHandler = asyncHandler;
            this.future = future;
            this.request = request;
        }

        public T call() throws Exception {
            AsyncHandler.STATE state = AsyncHandler.STATE.ABORT;
            try {
                URI uri = null;
                // Encoding with URLConnection is a bit bogus so we need to try both way before setting it
                try {
                    uri = AsyncHttpProviderUtils.createUri(request.getRawUrl());
                } catch (IllegalArgumentException u) {
                    uri = AsyncHttpProviderUtils.createUri(request.getUrl());
                }

                configure(uri, urlConnection, request);
                urlConnection.connect();

                if (TransferCompletionHandler.class.isAssignableFrom(asyncHandler.getClass())) {
                    throw new IllegalStateException(TransferCompletionHandler.class.getName() + "not supported by this provider");
                }

                int statusCode = urlConnection.getResponseCode();

                logger.debug("\n\nRequest {}\n\nResponse {}\n", request, statusCode);

                ResponseStatus status = new ResponseStatus(uri, urlConnection, JDKAsyncHttpProvider.this);
                FilterContext fc = new FilterContext.FilterContextBuilder().asyncHandler(asyncHandler).request(request).responseStatus(status).build();
                for (ResponseFilter asyncFilter : config.getResponseFilters()) {
                    fc = asyncFilter.filter(fc);
                    if (fc == null) {
                        throw new NullPointerException("FilterContext is null");
                    }
                }

                // The request has changed
                if (fc.replayRequest()) {
                    request = fc.getRequest();
                    urlConnection = createUrlConnection(request);
                    terminate = false;
                    return call();
                }

                boolean redirectEnabled = (request.isRedirectEnabled() || config.isRedirectEnabled());
                if (redirectEnabled && (statusCode == 302 || statusCode == 301)) {

                    if (currentRedirectCount++ < config.getMaxRedirects()) {
                        String location = urlConnection.getHeaderField("Location");
                        URI redirUri = AsyncHttpProviderUtils.getRedirectUri(uri, location);
                        String newUrl = redirUri.toString();

                        if (!newUrl.equals(uri.toString())) {
                            RequestBuilder builder = new RequestBuilder(request);

                            logger.debug("Redirecting to {}", newUrl);

                            request = builder.setUrl(newUrl).build();
                            urlConnection = createUrlConnection(request);
                            terminate = false;
                            return call();
                        }
                    } else {
                        throw new MaxRedirectException("Maximum redirect reached: " + config.getMaxRedirects());
                    }
                }

                Realm realm =  request.getRealm() != null ?  request.getRealm() : config.getRealm();
                if (statusCode == 401 && !isAuth.getAndSet(true) && realm != null ) {
                    String wwwAuth = urlConnection.getHeaderField("WWW-Authenticate");

                    logger.debug("Sending authentication to {}", request.getUrl());

                    Realm nr = new Realm.RealmBuilder().clone(realm)
                            .parseWWWAuthenticateHeader(wwwAuth)
                            .setUri(URI.create(request.getUrl()).getPath())
                            .setMethodName(request.getMethod())
                            .setUsePreemptiveAuth(true)
                            .build();
                    RequestBuilder builder = new RequestBuilder(request);
                    request = builder.setRealm(nr).build();
                    urlConnection = createUrlConnection(request);
                    terminate = false;
                    return call();
                }

                state = asyncHandler.onStatusReceived(status);
                if (state == AsyncHandler.STATE.CONTINUE) {
                    state = asyncHandler.onHeadersReceived(new ResponseHeaders(uri, urlConnection, JDKAsyncHttpProvider.this));
                }

                if (state == AsyncHandler.STATE.CONTINUE) {
                    InputStream is = getInputStream(urlConnection);
                    String contentEncoding = urlConnection.getHeaderField("Content-Encoding");
                    boolean isGZipped = contentEncoding == null ? false : "gzip".equalsIgnoreCase(contentEncoding);
                    if (isGZipped) {
                        is = new GZIPInputStream(is);
                    }

                    int byteToRead = urlConnection.getContentLength();
                    InputStream stream = is;
                    if (bufferResponseInMemory || byteToRead <= 0) {
                        int[] lengthWrapper = new int[1];
                        byte[] bytes = AsyncHttpProviderUtils.readFully(is, lengthWrapper);
                        stream = new ByteArrayInputStream(bytes, 0, lengthWrapper[0]);
                        byteToRead = lengthWrapper[0];
                    }

                    if (byteToRead > 0) {
                        int minBytes = Math.min(8192, byteToRead);
                        byte[] bytes = new byte[minBytes];
                        int leftBytes = minBytes < 8192 ? minBytes : byteToRead;
                        int read = 0;
                        while (leftBytes > -1) {

                            read = stream.read(bytes);
                            if (read == -1) {
                                break;
                            }

                            future.touch();

                            byte[] b = new byte[read];
                            System.arraycopy(bytes, 0, b, 0, read);
                            asyncHandler.onBodyPartReceived(new ResponseBodyPart(uri, b, JDKAsyncHttpProvider.this));

                            leftBytes -= read;
                        }
                    }

                    if (request.getMethod().equalsIgnoreCase("HEAD")) {
                        asyncHandler.onBodyPartReceived(new ResponseBodyPart(uri, "".getBytes(), JDKAsyncHttpProvider.this));
                    }
                }

                if (ProgressAsyncHandler.class.isAssignableFrom(asyncHandler.getClass())) {
                    ProgressAsyncHandler.class.cast(asyncHandler).onHeaderWriteCompleted();
                    ProgressAsyncHandler.class.cast(asyncHandler).onContentWriteCompleted();
                }
                try {
                    T t = asyncHandler.onCompleted();
                    future.content(t);
                    future.done(null);
                    return t;
                } catch (Throwable t) {
                    RuntimeException ex = new RuntimeException();
                    ex.initCause(t);
                    throw ex;
                }
            } catch (Throwable t) {
                logger.debug(t.getMessage(), t);

                if (IOException.class.isAssignableFrom(t.getClass()) && config.getIOExceptionFilters().size() > 0) {
                    FilterContext fc = new FilterContext.FilterContextBuilder().asyncHandler(asyncHandler)
                            .request(request).ioException(IOException.class.cast(t)).build();

                    try {
                        fc = handleIoException(fc);
                    } catch (FilterException e) {
                        if (config.getMaxTotalConnections() != -1) {
                            maxConnections.decrementAndGet();
                        }
                        future.done(null);
                    }

                    if (fc.replayRequest()) {
                        request = fc.getRequest();
                        urlConnection = createUrlConnection(request);
                        return call();
                    }
                }

                try {
                    future.abort(filterException(t));
                } catch (Throwable t2) {
                    logger.error(t2.getMessage(), t2);
                }
            } finally {
                if (terminate) {
                    if (config.getMaxTotalConnections() != -1) {
                        maxConnections.decrementAndGet();
                    }
                    urlConnection.disconnect();
                    if (jdkNtlmDomain != null) {
                        System.setProperty(NTLM_DOMAIN, jdkNtlmDomain);
                    }
                    Authenticator.setDefault(jdkAuthenticator);
                }
            }
            return null;
        }

        private FilterContext handleIoException(FilterContext fc) throws FilterException {
            for (IOExceptionFilter asyncFilter : config.getIOExceptionFilters()) {
                fc = asyncFilter.filter(fc);
                if (fc == null) {
                    throw new NullPointerException("FilterContext is null");
                }
            }
            return fc;
        }

        private Throwable filterException(Throwable t) {
            if (UnknownHostException.class.isAssignableFrom(t.getClass())) {
                t = new ConnectException(t.getMessage());
            }

            if (SocketTimeoutException.class.isAssignableFrom(t.getClass())) {
                int responseTimeoutInMs = config.getRequestTimeoutInMs();

                if (request.getPerRequestConfig() != null && request.getPerRequestConfig().getRequestTimeoutInMs() != -1) {
                    responseTimeoutInMs = request.getPerRequestConfig().getRequestTimeoutInMs();
                }
                t = new TimeoutException(String.format("No response received after %s", responseTimeoutInMs));
            }

            if (SSLHandshakeException.class.isAssignableFrom(t.getClass())) {
                Throwable t2 = new ConnectException();
                t2.initCause(t);
                t = t2;
            }

            return t;
        }

        private void configure(URI uri, HttpURLConnection urlConnection, Request request) throws IOException, AuthenticationException {

            PerRequestConfig conf = request.getPerRequestConfig();
            int requestTimeout = (conf != null && conf.getRequestTimeoutInMs() != 0) ?
                    conf.getRequestTimeoutInMs() : config.getRequestTimeoutInMs();

            urlConnection.setConnectTimeout(config.getConnectionTimeoutInMs());

            if (requestTimeout != -1)
                urlConnection.setReadTimeout(requestTimeout);

            urlConnection.setInstanceFollowRedirects(false);
            String host = uri.getHost();
            String method = request.getMethod();

            if (request.getVirtualHost() != null) {
                host = request.getVirtualHost();
            }

            if (uri.getPort() == -1 || request.getVirtualHost() != null) {
                urlConnection.setRequestProperty("Host", host);
            } else {
                urlConnection.setRequestProperty("Host", host + ":" + uri.getPort());
            }


            if (config.isCompressionEnabled()) {
                urlConnection.setRequestProperty("Accept-Encoding", "gzip");
            }

            if (!method.equalsIgnoreCase("CONNECT")) {
                FluentCaseInsensitiveStringsMap h = request.getHeaders();
                if (h != null) {
                    for (String name : h.keySet()) {
                        if (!"host".equalsIgnoreCase(name)) {
                            for (String value : h.get(name)) {
                                urlConnection.setRequestProperty(name, value);
                                if (name.equalsIgnoreCase("Expect")) {
                                    throw new IllegalStateException("Expect: 100-Continue not supported");
                                }
                            }
                        }
                    }
                }
            }

            String ka = config.getAllowPoolingConnection() ? "keep-alive" : "close";
            urlConnection.setRequestProperty("Connection", ka);
            ProxyServer proxyServer = request.getProxyServer() != null ? request.getProxyServer() : config.getProxyServer();
            boolean avoidProxy = ProxyUtils.avoidProxy( proxyServer, uri.getHost() );
            if (!avoidProxy) {
                urlConnection.setRequestProperty("Proxy-Connection", ka);
                if (proxyServer.getPrincipal() != null) {
                    urlConnection.setRequestProperty("Proxy-Authorization", AuthenticatorUtils.computeBasicAuthentication(proxyServer));
                }

                if (proxyServer.getProtocol().equals(ProxyServer.Protocol.NTLM)) {
                    jdkNtlmDomain = System.getProperty(NTLM_DOMAIN);
                    System.setProperty(NTLM_DOMAIN, proxyServer.getNtlmDomain());
                }
            }

            Realm realm =  request.getRealm() != null ?  request.getRealm() : config.getRealm();
            if (realm != null && realm.getUsePreemptiveAuth() ) {
                switch (realm.getAuthScheme()) {
                    case BASIC:
                        urlConnection.setRequestProperty("Authorization",
                                AuthenticatorUtils.computeBasicAuthentication(realm));
                        break;
                    case DIGEST:
                        if (realm.getNonce() != null && !realm.getNonce().equals("")) {
                            try {
                                urlConnection.setRequestProperty("Authorization",
                                        AuthenticatorUtils.computeDigestAuthentication(realm));
                            } catch (NoSuchAlgorithmException e) {
                                throw new SecurityException(e);
                            }
                        }
                        break;
                    case NTLM:
                        jdkNtlmDomain = System.getProperty(NTLM_DOMAIN);
                        System.setProperty(NTLM_DOMAIN, realm.getDomain());
                        break;
                    case NONE:
                        break;
                    default:
                        throw new IllegalStateException(String.format("Invalid Authentication %s", realm.toString()));
                }

            }

            // Add default accept headers.
            if (request.getHeaders().getFirstValue("Accept") == null) {
                urlConnection.setRequestProperty("Accept", "*/*");
            }

            if (request.getHeaders().getFirstValue("User-Agent") != null) {
                urlConnection.setRequestProperty("User-Agent", request.getHeaders().getFirstValue("User-Agent"));
            } else if (config.getUserAgent() != null) {
                urlConnection.setRequestProperty("User-Agent", config.getUserAgent());
            } else {
                urlConnection.setRequestProperty("User-Agent", AsyncHttpProviderUtils.constructUserAgent(JDKAsyncHttpProvider.class));
            }

            if (request.getCookies() != null && !request.getCookies().isEmpty()) {
                urlConnection.setRequestProperty("Cookie", AsyncHttpProviderUtils.encodeCookies(request.getCookies()));
            }

            String reqType = request.getMethod();
            urlConnection.setRequestMethod(reqType);

            if ("POST".equals(reqType) || "PUT".equals(reqType)) {
                urlConnection.setRequestProperty("Content-Length", "0");
                urlConnection.setDoOutput(true);
                String bodyCharset = request.getBodyEncoding() == null ? DEFAULT_CHARSET : request.getBodyEncoding();

                if (cachedBytes != null) {
                    urlConnection.setRequestProperty("Content-Length", String.valueOf(cachedBytesLenght));
                    urlConnection.setFixedLengthStreamingMode(cachedBytesLenght);
                    urlConnection.getOutputStream().write(cachedBytes, 0, cachedBytesLenght);
                } else if (request.getByteData() != null) {
                    urlConnection.setRequestProperty("Content-Length", String.valueOf(request.getByteData().length));
                    urlConnection.setFixedLengthStreamingMode(request.getByteData().length);

                    urlConnection.getOutputStream().write(request.getByteData());
                } else if (request.getStringData() != null) {
                    if (!request.getHeaders().containsKey("Content-Type")) {
                        urlConnection.setRequestProperty("Content-Type", "text/html;" + bodyCharset);
                    }
                    byte[] b = request.getStringData().getBytes(bodyCharset);
                    urlConnection.setRequestProperty("Content-Length", String.valueOf(b.length));
                    urlConnection.getOutputStream().write(b);
                } else if (request.getStreamData() != null) {
                    int[] lengthWrapper = new int[1];
                    cachedBytes = AsyncHttpProviderUtils.readFully(request.getStreamData(), lengthWrapper);
                    cachedBytesLenght = lengthWrapper[0];
                    urlConnection.setRequestProperty("Content-Length", String.valueOf(cachedBytesLenght));
                    urlConnection.setFixedLengthStreamingMode(cachedBytesLenght);

                    urlConnection.getOutputStream().write(cachedBytes, 0, cachedBytesLenght);
                } else if (request.getParams() != null) {
                    StringBuilder sb = new StringBuilder();
                    for (final Map.Entry<String, List<String>> paramEntry : request.getParams()) {
                        final String key = paramEntry.getKey();
                        for (final String value : paramEntry.getValue()) {
                            if (sb.length() > 0) {
                                sb.append("&");
                            }
                            UTF8UrlEncoder.appendEncoded(sb, key);
                            sb.append("=");
                            UTF8UrlEncoder.appendEncoded(sb, value);
                        }
                    }
                    urlConnection.setRequestProperty("Content-Length", String.valueOf(sb.length()));
                    urlConnection.setFixedLengthStreamingMode(sb.length());

                    if (!request.getHeaders().containsKey("Content-Type")) {
                        urlConnection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
                    }
                    urlConnection.getOutputStream().write(sb.toString().getBytes(bodyCharset));
                } else if (request.getParts() != null) {
                    int lenght = (int) request.getContentLength();
                    if (lenght != -1) {
                        urlConnection.setRequestProperty("Content-Length", String.valueOf(lenght));
                        urlConnection.setFixedLengthStreamingMode(lenght);
                    }

                    if (lenght == -1) {
                        lenght = MAX_BUFFERED_BYTES;
                    }

                    MultipartRequestEntity mre = AsyncHttpProviderUtils.createMultipartRequestEntity(request.getParts(), request.getParams());

                    urlConnection.setRequestProperty("Content-Type", mre.getContentType());
                    urlConnection.setRequestProperty("Content-Length", String.valueOf(mre.getContentLength()));

                    ChannelBuffer b = ChannelBuffers.dynamicBuffer(lenght);
                    mre.writeRequest(urlConnection.getOutputStream());
                } else if (request.getEntityWriter() != null) {
                    int lenght = (int) request.getContentLength();
                    if (lenght != -1) {
                        urlConnection.setRequestProperty("Content-Length", String.valueOf(lenght));
                        urlConnection.setFixedLengthStreamingMode(lenght);
                    }
                    request.getEntityWriter().writeEntity(urlConnection.getOutputStream());
                } else if (request.getFile() != null) {
                    File file = request.getFile();
                    if (!file.isFile()) {
                        throw new IOException(String.format(Thread.currentThread()
                                + "File %s is not a file or doesn't exist", file.getAbsolutePath()));
                    }
                    urlConnection.setRequestProperty("Content-Length", String.valueOf(file.length()));
                    urlConnection.setFixedLengthStreamingMode((int) file.length());

                    FileInputStream fis = new FileInputStream(file);
                    try {
                        OutputStream os = urlConnection.getOutputStream();
                        for (final byte[] buffer = new byte[1024 * 16];;) {
                            int read = fis.read(buffer);
                            if (read < 0) {
                                break;
                            }
                            os.write(buffer, 0, read);
                        }
                    } finally {
                        fis.close();
                    }
                } else if (request.getBodyGenerator() != null) {
                    Body body = request.getBodyGenerator().createBody();
                    try {
                        int length = (int) body.getContentLength();
                        if (length < 0) {
                            length = (int) request.getContentLength();
                        }
                        if (length >= 0) {
                            urlConnection.setRequestProperty("Content-Length", String.valueOf(length));
                            urlConnection.setFixedLengthStreamingMode(length);
                        }
                        OutputStream os = urlConnection.getOutputStream();
                        for (ByteBuffer buffer = ByteBuffer.allocate( 1024 * 8 );;) {
                            buffer.clear();
                            if (body.read(buffer) < 0) {
                                break;
                            }
                            os.write( buffer.array(), buffer.arrayOffset(), buffer.position() );
                        }
                    } finally {
                        try {
                            body.close();
                        } catch (IOException e) {
                            logger.warn("Failed to close request body: {}", e.getMessage(), e);
                        }
                    }
                }
            }
        }
    }

    private Proxy configureProxyAndAuth(final ProxyServer proxyServer, final Realm realm) throws AuthenticationException {

        Proxy proxy = null;
        if (proxyServer != null) {

            String proxyHost = proxyServer.getHost().startsWith("http://")
                    ? proxyServer.getHost().substring("http://".length()) : proxyServer.getHost();

            SocketAddress addr = new InetSocketAddress(proxyHost, proxyServer.getPort());
            proxy = new Proxy(Proxy.Type.HTTP, addr);
        }

        final boolean hasProxy = (proxyServer != null && proxyServer.getPrincipal() != null);
        final boolean hasAuthentication = (realm != null && realm.getPrincipal() != null);
        if (hasProxy || hasAuthentication) {

            Field f = null;
            try {
                f = Authenticator.class.getDeclaredField("theAuthenticator");

                f.setAccessible(true);
                jdkAuthenticator = (Authenticator) f.get(Authenticator.class);
            } catch (NoSuchFieldException e) {
            } catch (IllegalAccessException e) {
            }


            Authenticator.setDefault(new Authenticator() {
                protected PasswordAuthentication getPasswordAuthentication() {
                    if (hasProxy && getRequestingHost().equals(proxyServer.getHost())
                            && getRequestingPort() == proxyServer.getPort()) {
                        String password = "";
                        if (proxyServer.getPassword() != null) {
                            password = proxyServer.getPassword();
                        }
                        return new PasswordAuthentication(proxyServer.getPrincipal(), password.toCharArray());
                    }

                    if (hasAuthentication) {
                        return new PasswordAuthentication(realm.getPrincipal(), realm.getPassword().toCharArray());
                    }

                    return super.getPasswordAuthentication();
                }
            });
        } else {
            Authenticator.setDefault(null);
        }
        return proxy;
    }

    private InputStream getInputStream(HttpURLConnection urlConnection) throws IOException {
        if (urlConnection.getResponseCode() < 400) {
            return urlConnection.getInputStream();
        } else {
            InputStream ein = urlConnection.getErrorStream();
            return (ein != null)
                    ? ein : new ByteArrayInputStream(new byte[0]);
        }
    }

}
