Package groovyx.net.http

Source Code of groovyx.net.http.HttpURLClient$OAuthWrapper

/*
* 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;
    }
}
TOP

Related Classes of groovyx.net.http.HttpURLClient$OAuthWrapper

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.