groovyx.net.http.HttpURLClient.java Source code

Java tutorial

Introduction

Here is the source code for groovyx.net.http.HttpURLClient.java

Source

/*
 * Copyright 2008-2011 Thomas Nichols.  http://blog.thomnichols.org
 *
 * 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.
 *
 * You are receiving this code free of charge, which represents many hours of
 * effort from other individuals and corporations.  As a responsible member
 * of the community, you are encouraged (but not required) to donate any
 * enhancements or improvements back to the community under a similar open
 * source license.  Thank you. -TMN
 */

package groovyx.net.http;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import oauth.signpost.OAuthConsumer;
import oauth.signpost.basic.DefaultOAuthConsumer;
import oauth.signpost.basic.HttpURLConnectionRequestAdapter;
import oauth.signpost.exception.OAuthException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.Header;
import org.apache.http.HeaderIterator;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.ProtocolVersion;
import org.apache.http.StatusLine;
import org.apache.http.message.BasicHeader;
import org.apache.http.message.BasicHeaderIterator;
import org.apache.http.message.BasicStatusLine;
import org.apache.http.params.HttpParams;
import org.codehaus.groovy.runtime.DefaultGroovyMethods;
import org.codehaus.groovy.runtime.EncodingGroovyMethods;

/**
 * <p>This class provides a simplified API similar to {@link HTTPBuilder}, but
 * uses {@link java.net.HttpURLConnection} for I/O so that it is compatible
 * with Google App Engine.  Features:
 * <ul>
 *  <li>Parser and Encoder support</li>
 *  <li>Easy request and response header manipulation</li>
 *  <li>Basic authentication</li>
 * </ul>
 * Notably absent are status-code based response handling and the more complex
 * authentication mechanisms.</p>
 *
 * TODO request encoding support (if anyone asks for it)
 *
 * @see <a href='http://code.google.com/appengine/docs/java/urlfetch/overview.html'>GAE URLFetch</a>
 * @author <a href='mailto:tomstrummer+httpbuilder@gmail.com'>Tom Nichols</a>
 * @since 0.5.0
 */
public class HttpURLClient {

    private Map<String, String> defaultHeaders = new HashMap<String, String>();
    private EncoderRegistry encoderRegistry = new EncoderRegistry();
    private ParserRegistry parserRegistry = new ParserRegistry();
    private Object contentType = ContentType.ANY;
    private Object requestContentType = null;
    private URIBuilder defaultURL = null;
    private boolean followRedirects = true;
    protected OAuthWrapper oauth;

    /** Logger instance defined for use by sub-classes */
    protected Log log = LogFactory.getLog(getClass());

    /**
     * Perform a request.  Parameters are:
     * <dl>
     *   <dt>url</dt><dd>the entire request URL</dd>
     *   <dt>path</dt><dd>the path portion of the request URL, if a default
     *     URL is set on this instance.</dd>
     *   <dt>query</dt><dd>URL query parameters for this request.</dd>
     *   <dt>timeout</dt><dd>see {@link HttpURLConnection#setReadTimeout(int)}</dd>
     *   <dt>method</dt><dd>This defaults to GET, or POST if a <code>body</code>
     *   parameter is also specified.</dd>
     *   <dt>contentType</dt><dd>Explicitly specify how to parse the response.
     *     If this value is ContentType.ANY, the response <code>Content-Type</code>
     *     header is used to determine how to parse the response.</dd>
     *   <dt>requestContentType</dt><dd>used in a PUT or POST request to
     *     transform the request body and set the proper
     *     <code>Content-Type</code> header.  This defaults to the
     *     <code>contentType</code> if unset.</dd>
     *   <dt>auth</dt><dd>Basic authorization; pass the value as a list in the
     *   form [user, pass]</dd>
     *   <dt>headers</dt><dd>additional request headers, as a map</dd>
     *   <dt>body</dt><dd>request content body, for a PUT or POST request.
     *     This will be encoded using the requestContentType</dd>
     * </dl>
     * @param args named parameters
     * @return the parsed response
     * @throws URISyntaxException
     * @throws MalformedURLException
     * @throws IOException
     */
    public HttpResponseDecorator request(Map<String, ?> args)
            throws URISyntaxException, MalformedURLException, IOException {

        // copy so we don't modify the original collection when removing items:
        args = new HashMap<String, Object>(args);

        Object arg = args.remove("url");
        if (arg == null && this.defaultURL == null)
            throw new IllegalStateException("Either the 'defaultURL' property"
                    + " must be set or a 'url' parameter must be passed to the " + "request method.");
        URIBuilder url = arg != null ? new URIBuilder(arg.toString()) : defaultURL.clone();

        arg = null;
        arg = args.remove("path");
        if (arg != null)
            url.setPath(arg.toString());
        arg = null;
        arg = args.remove("query");
        if (arg != null) {
            if (!(arg instanceof Map<?, ?>))
                throw new IllegalArgumentException("'query' must be a map");
            url.setQuery((Map<?, ?>) arg);
        }

        HttpURLConnection conn = (HttpURLConnection) url.toURL().openConnection();
        conn.setInstanceFollowRedirects(this.followRedirects);

        arg = null;
        arg = args.remove("timeout");
        if (arg != null)
            conn.setConnectTimeout(Integer.parseInt(arg.toString()));

        arg = null;
        arg = args.remove("method");
        if (arg != null)
            conn.setRequestMethod(arg.toString());

        arg = null;
        arg = args.remove("contentType");
        Object contentType = arg != null ? arg : this.contentType;
        if (contentType instanceof ContentType)
            conn.addRequestProperty("Accept", ((ContentType) contentType).getAcceptHeader());

        arg = null;
        arg = args.remove("requestContentType");
        String requestContentType = arg != null ? arg.toString()
                : this.requestContentType != null ? this.requestContentType.toString()
                        : contentType != null ? contentType.toString() : null;

        // must add default headers before setting auth:
        for (String key : defaultHeaders.keySet())
            conn.addRequestProperty(key, defaultHeaders.get(key));

        arg = null;
        arg = args.remove("auth");
        if (arg != null) {
            if (oauth != null)
                log.warn("You are trying to use both OAuth and basic authentication!");
            try {
                List<?> vals = (List<?>) arg;
                conn.addRequestProperty("Authorization",
                        getBasicAuthHeader(vals.get(0).toString(), vals.get(1).toString()));
            } catch (Exception ex) {
                throw new IllegalArgumentException("Auth argument must be a list in the form [user,pass]");
            }
        }

        arg = null;
        arg = args.remove("headers");
        if (arg != null) {
            if (!(arg instanceof Map<?, ?>))
                throw new IllegalArgumentException("'headers' must be a map");
            Map<?, ?> headers = (Map<?, ?>) arg;
            for (Object key : headers.keySet())
                conn.addRequestProperty(key.toString(), headers.get(key).toString());
        }

        arg = null;
        arg = args.remove("body");
        if (arg != null) { // if there is a request POST or PUT body
            conn.setDoOutput(true);
            final HttpEntity body = (HttpEntity) encoderRegistry.getAt(requestContentType).call(arg);
            // TODO configurable request charset

            //TODO don't override if there is a 'content-type' in the headers list
            conn.addRequestProperty("Content-Type", requestContentType);
            try {
                // OAuth Sign if necessary.
                if (oauth != null)
                    conn = oauth.sign(conn, body);
                // send request data
                DefaultGroovyMethods.leftShift(conn.getOutputStream(), body.getContent());
            } finally {
                conn.getOutputStream().close();
            }
        }
        // sign the request if we're using OAuth
        else if (oauth != null)
            conn = oauth.sign(conn, null);

        if (args.size() > 0) {
            String illegalArgs = "";
            for (String k : args.keySet())
                illegalArgs += k + ",";
            throw new IllegalArgumentException("Unknown named parameters: " + illegalArgs);
        }

        String method = conn.getRequestMethod();
        log.debug(method + " " + url);

        HttpResponse response = new HttpURLResponseAdapter(conn);
        if (ContentType.ANY.equals(contentType))
            contentType = conn.getContentType();

        Object result = this.getparsedResult(method, contentType, response);

        log.debug(response.getStatusLine());
        HttpResponseDecorator decoratedResponse = new HttpResponseDecorator(response, result);

        if (log.isTraceEnabled()) {
            for (Header h : decoratedResponse.getHeaders())
                log.trace(" << " + h.getName() + " : " + h.getValue());
        }

        if (conn.getResponseCode() > 399)
            throw new HttpResponseException(decoratedResponse);

        return decoratedResponse;
    }

    private Object getparsedResult(String method, Object contentType, HttpResponse response)
            throws ResponseParseException {

        Object parsedData = method.equals("HEAD") || method.equals("OPTIONS") ? null
                : parserRegistry.getAt(contentType).call(response);
        try {
            //If response is streaming, buffer it in a byte array:
            if (parsedData instanceof InputStream) {
                ByteArrayOutputStream buffer = new ByteArrayOutputStream();
                DefaultGroovyMethods.leftShift(buffer, (InputStream) parsedData);
                parsedData = new ByteArrayInputStream(buffer.toByteArray());
            } else if (parsedData instanceof Reader) {
                StringWriter buffer = new StringWriter();
                DefaultGroovyMethods.leftShift(buffer, (Reader) parsedData);
                parsedData = new StringReader(buffer.toString());
            } else if (parsedData instanceof Closeable)
                log.warn("Parsed data is streaming, but cannot be buffered: " + parsedData.getClass());
            return parsedData;
        } catch (IOException ex) {
            throw new ResponseParseException(new HttpResponseDecorator(response, null), ex);
        }
    }

    private String getBasicAuthHeader(String user, String pass) throws UnsupportedEncodingException {
        return "Basic " + EncodingGroovyMethods.encodeBase64((user + ":" + pass).getBytes("ISO-8859-1")).toString();
    }

    /**
     * Set basic user and password authorization to be used for every request.
     * Pass <code>null</code> to un-set authorization for this instance.
     * @param user
     * @param pass
     * @throws UnsupportedEncodingException
     */
    public void setBasicAuth(Object user, Object pass) throws UnsupportedEncodingException {
        if (user == null)
            this.defaultHeaders.remove("Authorization");
        else
            this.defaultHeaders.put("Authorization", getBasicAuthHeader(user.toString(), pass.toString()));
    }

    /**
     * Sign all outbound requests with the given OAuth keys and tokens.  It
     * is assumed you have already generated a consumer keypair and retrieved
     * a proper access token pair from your target service (see
     * <a href='http://code.google.com/p/oauth-signpost/wiki/TwitterAndSignpost'>Signpost documentation</a>
     * for more details.)  Once this has been done all requests will be signed.
     * @param consumerKey null if you want to _stop_ signing requests.
     * @param consumerSecret
     * @param accessToken
     * @param accessSecret
     */
    public void setOAuth(Object consumerKey, Object consumerSecret, Object accessToken, Object accessSecret) {
        if (consumerKey == null) {
            oauth = null;
            return;
        }
        this.oauth = new OAuthWrapper(consumerKey, consumerSecret, accessToken, accessSecret);
    }

    /**
     * This class basically wraps Signpost classes so they are not loaded
     * until {@link HttpURLClient#setOAuth(Object, Object, Object, Object)}
     * is called.  This allows Signpost to act as an optional
     * dependency.  If you are not using Signpost, you don't need the JAR
     * on your classpath.
     * @since 0.5.1
     */
    private static class OAuthWrapper {
        protected OAuthConsumer oauth;

        OAuthWrapper(Object consumerKey, Object consumerSecret, Object accessToken, Object accessSecret) {
            oauth = new DefaultOAuthConsumer(consumerKey.toString(), consumerSecret.toString());
            oauth.setTokenWithSecret(accessToken.toString(), accessSecret.toString());
        }

        HttpURLConnection sign(HttpURLConnection request, final HttpEntity body) throws IOException {
            try { // OAuth Sign.
                  // Note that the request body must be repeatable even though it is an input stream.
                if (body == null)
                    return (HttpURLConnection) oauth.sign(request).unwrap();
                else
                    return (HttpURLConnection) oauth.sign(new HttpURLConnectionRequestAdapter(request) {
                        /* @Override */
                        public InputStream getMessagePayload() throws IOException {
                            return body.getContent();
                        }
                    }).unwrap();
            } catch (final OAuthException ex) {
                //              throw new IOException( "OAuth signing error", ex ); // 1.6 only!
                throw new IOException("OAuth signing error: " + ex.getMessage()) {
                    private static final long serialVersionUID = -13848840190384656L;

                    /* @Override */ public Throwable getCause() {
                        return ex;
                    }
                };
            }
        }
    }

    /**
     * Control whether this instance should automatically follow redirect
     * responses. See {@link HttpURLConnection#setInstanceFollowRedirects(boolean)}
     * @param follow true if the connection should automatically follow
     * redirect responses from the server.
     */
    public void setFollowRedirects(boolean follow) {
        this.followRedirects = follow;
    }

    /**
     * See {@link #setFollowRedirects(boolean)}
     * @return
     */
    public boolean isFollowRedirects() {
        return this.followRedirects;
    }

    /**
     * The default URL for this request.  This is a {@link URIBuilder} which can
     * be used to easily manipulate portions of the request URL.
     * @return
     */
    public Object getUrl() {
        return this.defaultURL;
    }

    /**
     * Set the default request URL.
     * @see URIBuilder#convertToURI(Object)
     * @param url any object whose <code>toString()</code> produces a valid URI.
     * @throws URISyntaxException
     */
    public void setUrl(Object url) throws URISyntaxException {
        this.defaultURL = new URIBuilder(URIBuilder.convertToURI(url));
    }

    /**
     * This class makes a HttpURLConnection look like an HttpResponse for use
     * by {@link ParserRegistry} and {@link HttpResponseDecorator}.
     */
    private final class HttpURLResponseAdapter implements HttpResponse {

        HttpURLConnection conn;
        Header[] headers;

        HttpURLResponseAdapter(HttpURLConnection conn) {
            this.conn = conn;
        }

        public HttpEntity getEntity() {
            return new HttpEntity() {

                public void consumeContent() throws IOException {
                    conn.getInputStream().close();
                }

                public InputStream getContent() throws IOException, IllegalStateException {
                    if (Status.find(conn.getResponseCode()) == Status.FAILURE)
                        return conn.getErrorStream();
                    return conn.getInputStream();
                }

                public Header getContentEncoding() {
                    return new BasicHeader("Content-Encoding", conn.getContentEncoding());
                }

                public long getContentLength() {
                    return conn.getContentLength();
                }

                public Header getContentType() {
                    return new BasicHeader("Content-Type", conn.getContentType());
                }

                public boolean isChunked() {
                    String enc = conn.getHeaderField("Transfer-Encoding");
                    return enc != null && enc.contains("chunked");
                }

                public boolean isRepeatable() {
                    return false;
                }

                public boolean isStreaming() {
                    return true;
                }

                public void writeTo(OutputStream out) throws IOException {
                    DefaultGroovyMethods.leftShift(out, conn.getInputStream());
                }

            };
        }

        public Locale getLocale() { //TODO test me
            String val = conn.getHeaderField("Locale");
            return val != null ? new Locale(val) : Locale.getDefault();
        }

        public StatusLine getStatusLine() {
            try {
                return new BasicStatusLine(this.getProtocolVersion(), conn.getResponseCode(),
                        conn.getResponseMessage());
            } catch (IOException ex) {
                throw new RuntimeException("Error reading status line", ex);
            }
        }

        public boolean containsHeader(String key) {
            return conn.getHeaderField(key) != null;
        }

        public Header[] getAllHeaders() {
            if (this.headers != null)
                return this.headers;
            List<Header> headers = new ArrayList<Header>();

            // see http://java.sun.com/j2se/1.5.0/docs/api/java/net/HttpURLConnection.html#getHeaderFieldKey(int)
            int i = conn.getHeaderFieldKey(0) != null ? 0 : 1;
            String key;
            while ((key = conn.getHeaderFieldKey(i)) != null) {
                headers.add(new BasicHeader(key, conn.getHeaderField(i++)));
            }

            this.headers = headers.toArray(new Header[headers.size()]);
            return this.headers;
        }

        public Header getFirstHeader(String key) {
            for (Header h : getAllHeaders())
                if (h.getName().equals(key))
                    return h;
            return null;
        }

        /**
         * Note that HttpURLConnection does not support multiple headers of
         * the same name.
         */
        public Header[] getHeaders(String key) {
            List<Header> headers = new ArrayList<Header>();
            for (Header h : getAllHeaders())
                if (h.getName().equals(key))
                    headers.add(h);
            return headers.toArray(new Header[headers.size()]);
        }

        /**
         * @see URLConnection#getHeaderField(String)
         */
        public Header getLastHeader(String key) {
            String val = conn.getHeaderField(key);
            return val != null ? new BasicHeader(key, val) : null;
        }

        public HttpParams getParams() {
            return null;
        }

        public ProtocolVersion getProtocolVersion() {
            /* TODO this could potentially cause problems if the server is
               using HTTP 1.0 */
            return new ProtocolVersion("HTTP", 1, 1);
        }

        public HeaderIterator headerIterator() {
            return new BasicHeaderIterator(this.getAllHeaders(), null);
        }

        public HeaderIterator headerIterator(String key) {
            return new BasicHeaderIterator(this.getHeaders(key), key);
        }

        /* Setters are part of the interface, but aren't applicable for this
         * adapter */
        public void setEntity(HttpEntity entity) {
        }

        public void setLocale(Locale l) {
        }

        public void setReasonPhrase(String phrase) {
        }

        public void setStatusCode(int code) {
        }

        public void setStatusLine(StatusLine line) {
        }

        public void setStatusLine(ProtocolVersion v, int code) {
        }

        public void setStatusLine(ProtocolVersion arg0, int arg1, String arg2) {
        }

        public void addHeader(Header arg0) {
        }

        public void addHeader(String arg0, String arg1) {
        }

        public void removeHeader(Header arg0) {
        }

        public void removeHeaders(String arg0) {
        }

        public void setHeader(Header arg0) {
        }

        public void setHeader(String arg0, String arg1) {
        }

        public void setHeaders(Header[] arg0) {
        }

        public void setParams(HttpParams arg0) {
        }
    }

    /**
     * Retrieve the default headers that will be sent in each request.  Note
     * that this is a 'live' map that can be directly manipulated to add or
     * remove the default request headers.
     * @return
     */
    public Map<String, String> getHeaders() {
        return defaultHeaders;
    }

    /**
     * Set default headers to be sent with every request.
     * @param headers
     */
    public void setHeaders(Map<?, ?> headers) {
        this.defaultHeaders.clear();
        for (Object key : headers.keySet()) {
            Object val = headers.get(key);
            if (val != null)
                this.defaultHeaders.put(key.toString(), val.toString());
        }
    }

    /**
     * Get the encoder registry used by this instance, which can be used
     * to directly modify the request serialization behavior.
     * i.e. <code>client.encoders.'application/xml' = {....}</code>.
     * @return
     */
    public EncoderRegistry getEncoders() {
        return encoderRegistry;
    }

    public void setEncoders(EncoderRegistry encoderRegistry) {
        this.encoderRegistry = encoderRegistry;
    }

    /**
     * Retrieve the parser registry used by this instance, which can be used to
     * directly modify the parsing behavior.
     * @return
     */
    public ParserRegistry getParsers() {
        return parserRegistry;
    }

    public void setParsers(ParserRegistry parserRegistry) {
        this.parserRegistry = parserRegistry;
    }

    /**
     * Get the default content-type used for parsing response data.
     * @return a String or {@link ContentType} object.  Defaults to
     * {@link ContentType#ANY}
     */
    public Object getContentType() {
        return contentType;
    }

    /**
     * Set the default content-type used to control response parsing and request
     * serialization behavior.  If <code>null</code> is passed,
     * {@link ContentType#ANY} will be used.  If this value is
     * {@link ContentType#ANY}, the response <code>Content-Type</code> header is
     * used to parse the response.
     * @param ct a String or {@link ContentType} value.
     */
    public void setContentType(Object ct) {
        this.contentType = (ct == null) ? ContentType.ANY : ct;
    }

    /**
     * Get the default content-type used to serialize the request data.
     * @return
     */
    public Object getRequestContentType() {
        return requestContentType;
    }

    /**
     * Set the default content-type used to control request body serialization.
     * If null, the {@link #getContentType() contentType property} is used.
     * Additionally, if the <code>contentType</code> is {@link ContentType#ANY},
     * a <code>requestContentType</code> <i>must</i> be specified when
     * performing a POST or PUT request that sends request data.
     * @param requestContentType String or {@link ContentType} value.
     */
    public void setRequestContentType(Object requestContentType) {
        this.requestContentType = requestContentType;
    }
}