All Downloads are FREE. Search and download functionalities are using the official Maven repository.

groovyx.net.http.HttpURLClient Maven / Gradle / Ivy

Go to download

A builder-style HTTP client API, including authentication, and extensible handling of common content-types such as JSON and XML. It is built on top of Apache's HttpClient.

There is a newer version: 0.7.1
Show newest version
/*
 * 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;

/**
 * 

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: *

    *
  • Parser and Encoder support
  • *
  • Easy request and response header manipulation
  • *
  • Basic authentication
  • *
* Notably absent are status-code based response handling and the more complex * authentication mechanisms.

* * TODO request encoding support (if anyone asks for it) * * @see GAE URLFetch * @author Tom Nichols * @since 0.5.0 */ public class HttpURLClient { private Map defaultHeaders = new HashMap(); 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: *
*
url
the entire request URL
*
path
the path portion of the request URL, if a default * URL is set on this instance.
*
query
URL query parameters for this request.
*
timeout
see {@link HttpURLConnection#setReadTimeout(int)}
*
method
This defaults to GET, or POST if a body * parameter is also specified.
*
contentType
Explicitly specify how to parse the response. * If this value is ContentType.ANY, the response Content-Type * header is used to determine how to parse the response.
*
requestContentType
used in a PUT or POST request to * transform the request body and set the proper * Content-Type header. This defaults to the * contentType if unset.
*
auth
Basic authorization; pass the value as a list in the * form [user, pass]
*
headers
additional request headers, as a map
*
body
request content body, for a PUT or POST request. * This will be encoded using the requestContentType
*
* @param args named parameters * @return the parsed response * @throws URISyntaxException * @throws MalformedURLException * @throws IOException */ public HttpResponseDecorator request( Map args ) throws URISyntaxException, MalformedURLException, IOException { // copy so we don't modify the original collection when removing items: args = new HashMap(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 null 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 * Signpost documentation * 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 toString() 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
headers = new ArrayList
(); // 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
headers = new ArrayList
(); 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 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. client.encoders.'application/xml' = {....}. * @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 null is passed, * {@link ContentType#ANY} will be used. If this value is * {@link ContentType#ANY}, the response Content-Type 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 contentType is {@link ContentType#ANY}, * a requestContentType must 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; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy