org.pixmob.httpclient.HttpRequestBuilder.java Source code

Java tutorial

Introduction

Here is the source code for org.pixmob.httpclient.HttpRequestBuilder.java

Source

/*
 * Copyright (C) 2012 Pixmob (http://github.com/pixmob)
 *
 * 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.
 */
package org.pixmob.httpclient;

import static org.pixmob.httpclient.Constants.HTTP_DELETE;
import static org.pixmob.httpclient.Constants.HTTP_GET;
import static org.pixmob.httpclient.Constants.HTTP_HEAD;
import static org.pixmob.httpclient.Constants.HTTP_POST;
import static org.pixmob.httpclient.Constants.HTTP_PUT;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Field;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.net.URLEncoder;
import java.net.UnknownHostException;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.zip.GZIPInputStream;
import java.util.zip.Inflater;
import java.util.zip.InflaterInputStream;

import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;

import org.apache.http.conn.ssl.BrowserCompatHostnameVerifier;

import android.content.Context;
import android.os.Build;

/**
 * This class is used to prepare and execute an Http request.
 * 
 * @author Pixmob
 */
public final class HttpRequestBuilder {
    private static final SecureRandom SECURE_RANDOM = new SecureRandom();
    private static final String CONTENT_CHARSET = "UTF-8";
    private static final Map<String, List<String>> NO_HEADERS = new HashMap<String, List<String>>(0);
    private static TrustManager[] trustManagers;
    private final byte[] buffer = new byte[1024];
    private final HttpClient hc;
    private final List<HttpRequestHandler> reqHandlers = new ArrayList<HttpRequestHandler>(2);
    private String uri;
    private String method;
    private Set<Integer> expectedStatusCodes = new HashSet<Integer>(2);
    private Map<String, String> cookies;
    private Map<String, List<String>> headers;
    private Map<String, String> parameters;
    private byte[] content;
    private boolean contentSet;
    private String contentType;
    private HttpResponseHandler handler;

    HttpRequestBuilder(final HttpClient hc, final String uri, final String method) {
        this.hc = hc;
        this.uri = uri;
        this.method = method;
    }

    public HttpRequestBuilder with(HttpRequestHandler handler) {
        if (handler != null) {
            reqHandlers.add(handler);
        }
        return this;
    }

    public HttpRequestBuilder expect(int... statusCodes) {
        if (statusCodes != null) {
            for (final int statusCode : statusCodes) {
                if (statusCode < 1) {
                    throw new IllegalArgumentException("Invalid status code: " + statusCode);
                }
                expectedStatusCodes.add(statusCode);
            }
        }
        return this;
    }

    public HttpRequestBuilder content(byte[] content, String contentType) {
        this.content = content;
        this.contentType = contentType;
        if (content != null) {
            contentSet = true;
        }
        return this;
    }

    public HttpRequestBuilder noContent() {
        content = new byte[] {};
        contentType = "text/plain";
        if (content != null) {
            contentSet = true;
        }
        return this;
    }

    public HttpRequestBuilder cookies(Map<String, String> cookies) {
        this.cookies = cookies;
        return this;
    }

    public HttpRequestBuilder headers(Map<String, List<String>> headers) {
        this.headers = headers;
        return this;
    }

    public HttpRequestBuilder header(String name, String value) {
        if (name == null) {
            throw new IllegalArgumentException("Header name cannot be null");
        }
        if (value == null) {
            throw new IllegalArgumentException("Header value cannot be null");
        }
        if (headers == null) {
            headers = new HashMap<String, List<String>>(2);
        }
        List<String> values = headers.get(name);
        if (values == null) {
            values = new ArrayList<String>(1);
            headers.put(name, values);
        }
        values.add(value);
        return this;
    }

    public HttpRequestBuilder params(Map<String, String> parameters) {
        this.parameters = parameters;
        return this;
    }

    public HttpRequestBuilder param(String name, String value) {
        if (name == null) {
            throw new IllegalArgumentException("Parameter name cannot be null");
        }
        if (value == null) {
            throw new IllegalArgumentException("Parameter value cannot be null");
        }
        if (parameters == null) {
            parameters = new HashMap<String, String>(4);
        }
        parameters.put(name, value);
        return this;
    }

    public HttpRequestBuilder cookie(String name, String value) {
        if (name == null) {
            throw new IllegalArgumentException("Cookie name cannot be null");
        }
        if (value == null) {
            throw new IllegalArgumentException("Cookie value cannot be null");
        }
        if (cookies == null) {
            cookies = new HashMap<String, String>(2);
        }
        cookies.put(name, value);
        return this;
    }

    public HttpRequestBuilder to(HttpResponseHandler handler) {
        this.handler = handler;
        return this;
    }

    public HttpRequestBuilder to(File file) throws IOException {
        to(new WriteToOutputStreamHandler(new FileOutputStream(file)));
        return this;
    }

    public HttpRequestBuilder to(OutputStream output) {
        to(new WriteToOutputStreamHandler(output));
        return this;
    }

    public HttpResponse execute() throws HttpClientException {
        HttpURLConnection conn = null;
        UncloseableInputStream payloadStream = null;
        try {
            if (parameters != null && !parameters.isEmpty()) {
                final StringBuilder buf = new StringBuilder(256);
                if (HTTP_GET.equals(method) || HTTP_HEAD.equals(method)) {
                    buf.append('?');
                }

                int paramIdx = 0;
                for (final Map.Entry<String, String> e : parameters.entrySet()) {
                    if (paramIdx != 0) {
                        buf.append("&");
                    }
                    final String name = e.getKey();
                    final String value = e.getValue();
                    buf.append(URLEncoder.encode(name, CONTENT_CHARSET)).append("=")
                            .append(URLEncoder.encode(value, CONTENT_CHARSET));
                    ++paramIdx;
                }

                if (!contentSet
                        && (HTTP_POST.equals(method) || HTTP_DELETE.equals(method) || HTTP_PUT.equals(method))) {
                    try {
                        content = buf.toString().getBytes(CONTENT_CHARSET);
                    } catch (UnsupportedEncodingException e) {
                        // Unlikely to happen.
                        throw new HttpClientException("Encoding error", e);
                    }
                } else {
                    uri += buf;
                }
            }

            conn = (HttpURLConnection) new URL(uri).openConnection();
            conn.setConnectTimeout(hc.getConnectTimeout());
            conn.setReadTimeout(hc.getReadTimeout());
            conn.setAllowUserInteraction(false);
            conn.setInstanceFollowRedirects(false);
            conn.setRequestMethod(method);
            conn.setUseCaches(false);
            conn.setDoInput(true);

            if (headers != null && !headers.isEmpty()) {
                for (final Map.Entry<String, List<String>> e : headers.entrySet()) {
                    final List<String> values = e.getValue();
                    if (values != null) {
                        final String name = e.getKey();
                        for (final String value : values) {
                            conn.addRequestProperty(name, value);
                        }
                    }
                }
            }

            if (cookies != null && !cookies.isEmpty()
                    || hc.getInMemoryCookies() != null && !hc.getInMemoryCookies().isEmpty()) {
                final StringBuilder cookieHeaderValue = new StringBuilder(256);
                prepareCookieHeader(cookies, cookieHeaderValue);
                prepareCookieHeader(hc.getInMemoryCookies(), cookieHeaderValue);
                conn.setRequestProperty("Cookie", cookieHeaderValue.toString());
            }

            final String userAgent = hc.getUserAgent();
            if (userAgent != null) {
                conn.setRequestProperty("User-Agent", userAgent);
            }

            conn.setRequestProperty("Connection", "close");
            conn.setRequestProperty("Location", uri);
            conn.setRequestProperty("Referrer", uri);
            conn.setRequestProperty("Accept-Encoding", "gzip,deflate");
            conn.setRequestProperty("Accept-Charset", CONTENT_CHARSET);

            if (conn instanceof HttpsURLConnection) {
                setupSecureConnection(hc.getContext(), (HttpsURLConnection) conn);
            }

            for (final HttpRequestHandler connHandler : reqHandlers) {
                try {
                    connHandler.onRequest(conn);
                } catch (HttpClientException e) {
                    throw e;
                } catch (Exception e) {
                    throw new HttpClientException("Failed to prepare request to " + uri, e);
                }
            }

            // It seems that when writing content starts the connection
            if (HTTP_POST.equals(method) || HTTP_DELETE.equals(method) || HTTP_PUT.equals(method)) {

                if (content == null) {
                    noContent();
                }

                conn.setDoOutput(true);
                if (!contentSet) {
                    conn.setRequestProperty("Content-Type",
                            "application/x-www-form-urlencoded; charset=" + CONTENT_CHARSET);
                } else if (contentType != null) {
                    conn.setRequestProperty("Content-Type", contentType);
                }
                conn.setFixedLengthStreamingMode(content.length);

                final OutputStream out = conn.getOutputStream();
                out.write(content);
                out.flush();
            }

            conn.connect();

            final int statusCode = conn.getResponseCode();
            if (statusCode == -1) {
                throw new HttpClientException("Invalid response from " + uri);
            }
            if (!expectedStatusCodes.isEmpty() && !expectedStatusCodes.contains(statusCode)) {
                throw new HttpClientException(
                        "Expected status code " + expectedStatusCodes + ", got " + statusCode);
            } else if (expectedStatusCodes.isEmpty() && statusCode / 100 != 2) {
                throw new HttpClientException("Expected status code 2xx, got " + statusCode);
            }

            final Map<String, List<String>> headerFields = conn.getHeaderFields();
            final Map<String, String> inMemoryCookies = hc.getInMemoryCookies();
            if (headerFields != null) {
                final List<String> newCookies = headerFields.get("Set-Cookie");
                if (newCookies != null) {
                    for (final String newCookie : newCookies) {
                        final String rawCookie = newCookie.split(";", 2)[0];
                        final int i = rawCookie.indexOf('=');
                        final String name = rawCookie.substring(0, i);
                        final String value = rawCookie.substring(i + 1);
                        inMemoryCookies.put(name, value);
                    }
                }
            }

            if (isStatusCodeError(statusCode)) {
                // Got an error: cannot read input.
                payloadStream = new UncloseableInputStream(getErrorStream(conn));
            } else {
                payloadStream = new UncloseableInputStream(getInputStream(conn));
            }
            final HttpResponse resp = new HttpResponse(statusCode, payloadStream,
                    headerFields == null ? NO_HEADERS : headerFields, inMemoryCookies);
            if (handler != null) {
                try {
                    handler.onResponse(resp);
                } catch (HttpClientException e) {
                    throw e;
                } catch (Exception e) {
                    throw new HttpClientException("Error in response handler", e);
                }
            } else {
                final File temp = File.createTempFile("httpclient-req-", ".cache", hc.getContext().getCacheDir());
                resp.preload(temp);
                temp.delete();
            }
            return resp;
        } catch (SocketTimeoutException e) {
            if (handler != null) {
                try {
                    handler.onTimeout();
                    return null;
                } catch (HttpClientException e2) {
                    throw e2;
                } catch (Exception e2) {
                    throw new HttpClientException("Error in response handler", e2);
                }
            } else {
                throw new HttpClientException("Response timeout from " + uri, e);
            }
        } catch (IOException e) {
            throw new HttpClientException("Connection failed to " + uri, e);
        } finally {
            if (conn != null) {
                if (payloadStream != null) {
                    // Fully read Http response:
                    // http://docs.oracle.com/javase/6/docs/technotes/guides/net/http-keepalive.html
                    try {
                        while (payloadStream.read(buffer) != -1) {
                            ;
                        }
                    } catch (IOException ignore) {
                    }
                    payloadStream.forceClose();
                }
                conn.disconnect();
            }
        }
    }

    private static boolean isStatusCodeError(int sc) {
        final int i = sc / 100;
        return i == 4 || i == 5;
    }

    private static void prepareCookieHeader(Map<String, String> cookies, StringBuilder headerValue) {
        if (cookies != null) {
            for (final Map.Entry<String, String> e : cookies.entrySet()) {
                if (headerValue.length() != 0) {
                    headerValue.append("; ");
                }
                headerValue.append(e.getKey()).append("=").append(e.getValue());
            }
        }
    }

    /**
     * Open the {@link InputStream} of an Http response. This method supports
     * GZIP and DEFLATE responses.
     */
    private static InputStream getInputStream(HttpURLConnection conn) throws IOException {
        final List<String> contentEncodingValues = conn.getHeaderFields().get("Content-Encoding");
        if (contentEncodingValues != null) {
            for (final String contentEncoding : contentEncodingValues) {
                if (contentEncoding != null) {
                    if (contentEncoding.contains("gzip")) {
                        return new GZIPInputStream(conn.getInputStream());
                    }
                    if (contentEncoding.contains("deflate")) {
                        return new InflaterInputStream(conn.getInputStream(), new Inflater(true));
                    }
                }
            }
        }
        return conn.getInputStream();
    }

    /**
     * Open the error {@link InputStream} of an Http response. This method
     * supports GZIP and DEFLATE responses.
     */
    private static InputStream getErrorStream(HttpURLConnection conn) throws IOException {
        final List<String> contentEncodingValues = conn.getHeaderFields().get("Content-Encoding");
        if (contentEncodingValues != null) {
            for (final String contentEncoding : contentEncodingValues) {
                if (contentEncoding != null) {
                    if (contentEncoding.contains("gzip")) {
                        return new GZIPInputStream(conn.getErrorStream());
                    }
                    if (contentEncoding.contains("deflate")) {
                        return new InflaterInputStream(conn.getErrorStream(), new Inflater(true));
                    }
                }
            }
        }
        return conn.getErrorStream();
    }

    private static KeyStore loadCertificates(Context context) throws IOException {
        try {
            final KeyStore localTrustStore = KeyStore.getInstance("BKS");
            final InputStream in = context.getResources().openRawResource(R.raw.hc_keystore);
            try {
                localTrustStore.load(in, null);
            } finally {
                in.close();
            }

            return localTrustStore;
        } catch (Exception e) {
            final IOException ioe = new IOException("Failed to load SSL certificates");
            ioe.initCause(e);
            throw ioe;
        }
    }

    /**
     * Setup SSL connection.
     */
    private static void setupSecureConnection(Context context, HttpsURLConnection conn) throws IOException {
        final SSLContext sslContext;
        try {
            // SSL certificates are provided by the Guardian Project:
            // https://github.com/guardianproject/cacert
            if (trustManagers == null) {
                // Load SSL certificates:
                // http://nelenkov.blogspot.com/2011/12/using-custom-certificate-trust-store-on.html
                // Earlier Android versions do not have updated root CA
                // certificates, resulting in connection errors.
                final KeyStore keyStore = loadCertificates(context);

                final CustomTrustManager customTrustManager = new CustomTrustManager(keyStore);
                trustManagers = new TrustManager[] { customTrustManager };
            }

            // Init SSL connection with custom certificates.
            // The same SecureRandom instance is used for every connection to
            // speed up initialization.
            sslContext = SSLContext.getInstance("TLS");
            sslContext.init(null, trustManagers, SECURE_RANDOM);
        } catch (GeneralSecurityException e) {
            final IOException ioe = new IOException("Failed to initialize SSL engine");
            ioe.initCause(e);
            throw ioe;
        }

        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
            // Fix slow read:
            // http://code.google.com/p/android/issues/detail?id=13117
            // Prior to ICS, the host name is still resolved even if we already
            // know its IP address, for each connection.
            final SSLSocketFactory delegate = sslContext.getSocketFactory();
            final SSLSocketFactory socketFactory = new SSLSocketFactory() {
                @Override
                public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
                    InetAddress addr = InetAddress.getByName(host);
                    injectHostname(addr, host);
                    return delegate.createSocket(addr, port);
                }

                @Override
                public Socket createSocket(InetAddress host, int port) throws IOException {
                    return delegate.createSocket(host, port);
                }

                @Override
                public Socket createSocket(String host, int port, InetAddress localHost, int localPort)
                        throws IOException, UnknownHostException {
                    return delegate.createSocket(host, port, localHost, localPort);
                }

                @Override
                public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort)
                        throws IOException {
                    return delegate.createSocket(address, port, localAddress, localPort);
                }

                private void injectHostname(InetAddress address, String host) {
                    try {
                        Field field = InetAddress.class.getDeclaredField("hostName");
                        field.setAccessible(true);
                        field.set(address, host);
                    } catch (Exception ignored) {
                    }
                }

                @Override
                public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
                    injectHostname(s.getInetAddress(), host);
                    return delegate.createSocket(s, host, port, autoClose);
                }

                @Override
                public String[] getDefaultCipherSuites() {
                    return delegate.getDefaultCipherSuites();
                }

                @Override
                public String[] getSupportedCipherSuites() {
                    return delegate.getSupportedCipherSuites();
                }
            };
            conn.setSSLSocketFactory(socketFactory);
        } else {
            conn.setSSLSocketFactory(sslContext.getSocketFactory());
        }

        conn.setHostnameVerifier(new BrowserCompatHostnameVerifier());
    }
}