001/*
002 * Copyright 2008-2011 Thomas Nichols.  http://blog.thomnichols.org
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * You are receiving this code free of charge, which represents many hours of
017 * effort from other individuals and corporations.  As a responsible member
018 * of the community, you are encouraged (but not required) to donate any
019 * enhancements or improvements back to the community under a similar open
020 * source license.  Thank you. -TMN
021 */
022package groovyx.net.http;
023
024import static groovyx.net.http.URIBuilder.convertToURI;
025import groovy.lang.Closure;
026
027import java.io.ByteArrayInputStream;
028import java.io.ByteArrayOutputStream;
029import java.io.Closeable;
030import java.io.IOException;
031import java.io.InputStream;
032import java.io.Reader;
033import java.io.StringReader;
034import java.io.StringWriter;
035import java.net.URI;
036import java.net.URISyntaxException;
037import java.net.URL;
038import java.util.Arrays;
039import java.util.Map;
040
041import org.apache.commons.logging.Log;
042import org.apache.commons.logging.LogFactory;
043import org.apache.http.Header;
044import org.apache.http.HttpEntity;
045import org.apache.http.HttpEntityEnclosingRequest;
046import org.apache.http.HttpHost;
047import org.apache.http.HttpResponse;
048import org.apache.http.client.ClientProtocolException;
049import org.apache.http.client.ResponseHandler;
050import org.apache.http.client.methods.HttpGet;
051import org.apache.http.client.methods.HttpPost;
052import org.apache.http.client.methods.HttpRequestBase;
053import org.apache.http.client.protocol.ClientContext;
054import org.apache.http.conn.ClientConnectionManager;
055import org.apache.http.conn.params.ConnRoutePNames;
056import org.apache.http.cookie.params.CookieSpecPNames;
057import org.apache.http.client.HttpClient;
058import org.apache.http.impl.client.AbstractHttpClient;
059import org.apache.http.impl.client.DefaultHttpClient;
060import org.apache.http.params.BasicHttpParams;
061import org.apache.http.params.HttpParams;
062import org.apache.http.protocol.HttpContext;
063import org.codehaus.groovy.runtime.DefaultGroovyMethods;
064import org.codehaus.groovy.runtime.MethodClosure;
065
066/** <p>
067 * Groovy DSL for easily making HTTP requests, and handling request and response
068 * data.  This class adds a number of convenience mechanisms built on top of
069 * Apache HTTPClient for things like URL-encoded POSTs and REST requests that
070 * require building and parsing JSON or XML.  Convenient access to a few common
071 * authentication methods is also available.</p>
072 *
073 *
074 * <h3>Conventions</h3>
075 * <p>HTTPBuilder has properties for default headers, URI, contentType, etc.
076 * All of these values are also assignable (and in many cases, in much finer
077 * detail) from the {@link RequestConfigDelegate} as well.  In any cases where the value
078 * is not set on the delegate (from within a request closure,) the builder's
079 * default value is used.  </p>
080 *
081 * <p>For instance, any methods that do not take a <code>uri</code> parameter
082 * assume you will set the <code>uri</code> property in the request closure or
083 * use HTTPBuilder's assigned {@link #getUri() default URI}.</p>
084 *
085 *
086 * <h3>Response Parsing</h3>
087 * <p>By default, HTTPBuilder uses {@link ContentType#ANY} as the default
088 * content-type.  This means the value of the request's <code>Accept</code>
089 * header is <code>&#42;/*</code>, and the response parser is determined
090 * based on the response <code>content-type</code> header. </p>
091 *
092 * <p><strong>If</strong> any contentType is given (either in
093 * {@link #setContentType(Object)} or as a request method parameter), the
094 * builder will attempt to parse the response using that content-type,
095 * regardless of what the server actually responds with.  </p>
096 *
097 *
098 * <h3>Examples:</h3>
099 * Perform an HTTP GET and print the response:
100 * <pre>
101 *   def http = new HTTPBuilder('http://www.google.com')
102 *
103 *   http.get( path : '/search',
104 *             contentType : TEXT,
105 *             query : [q:'Groovy'] ) { resp, reader ->
106 *     println "response status: ${resp.statusLine}"
107 *     println 'Response data: -----'
108 *     System.out << reader
109 *     println '\n--------------------'
110 *   }
111 * </pre>
112 *
113 * Long form for other HTTP methods, and response-code-specific handlers.
114 * This is roughly equivalent to the above example.
115 *
116 * <pre>
117 *   def http = new HTTPBuilder('http://www.google.com/search?q=groovy')
118 *
119 *   http.request( GET, TEXT ) { req ->
120 *
121 *     // executed for all successful responses:
122 *     response.success = { resp, reader ->
123 *       println 'my response handler!'
124 *       assert resp.statusLine.statusCode == 200
125 *       println resp.statusLine
126 *       System.out << reader // print response stream
127 *     }
128 *
129 *     // executed only if the response status code is 401:
130 *     response.'404' = { resp ->
131 *       println 'not found!'
132 *     }
133 *   }
134 * </pre>
135 *
136 * You can also set a default response handler called for any status
137 * code > 399 that is not matched to a specific handler. Setting the value
138 * outside a request closure means it will apply to all future requests with
139 * this HTTPBuilder instance:
140 * <pre>
141 *   http.handler.failure = { resp ->
142 *     println "Unexpected failure: ${resp.statusLine}"
143 *   }
144 * </pre>
145 *
146 *
147 * And...  Automatic response parsing for registered content types!
148 *
149 * <pre>
150 *   http.request( 'http://ajax.googleapis.com', GET, JSON ) {
151 *     uri.path = '/ajax/services/search/web'
152 *     uri.query = [ v:'1.0', q: 'Calvin and Hobbes' ]
153 *
154 *     response.success = { resp, json ->
155 *       assert json.size() == 3
156 *       println "Query response: "
157 *       json.responseData.results.each {
158 *         println "  ${it.titleNoFormatting} : ${it.visibleUrl}"
159 *       }
160 *     }
161 *   }
162 * </pre>
163 *
164 *
165 * @author <a href='mailto:[email protected]'>Tom Nichols</a>
166 */
167public class HTTPBuilder {
168
169    private HttpClient client;
170    protected URIBuilder defaultURI = null;
171    protected AuthConfig auth = new AuthConfig( this );
172
173    protected final Log log = LogFactory.getLog( getClass() );
174
175    protected Object defaultContentType = ContentType.ANY;
176    protected Object defaultRequestContentType = null;
177    protected boolean autoAcceptHeader = true;
178    protected final Map<Object,Closure> defaultResponseHandlers =
179        new StringHashMap<Closure>( buildDefaultResponseHandlers() );
180    protected ContentEncodingRegistry contentEncodingHandler = new ContentEncodingRegistry();
181
182    protected final Map<Object,Object> defaultRequestHeaders = new StringHashMap<Object>();
183
184    protected EncoderRegistry encoders = new EncoderRegistry();
185    protected ParserRegistry parsers = new ParserRegistry();
186
187    /**
188     * Creates a new instance with a <code>null</code> default URI.
189     */
190    public HTTPBuilder() {
191        setContentEncoding( ContentEncoding.Type.GZIP,
192                ContentEncoding.Type.DEFLATE );
193    }
194
195    /**
196     * Give a default URI to be used for all request methods that don't
197     * explicitly take a URI parameter.
198     * @param defaultURI either a {@link URL}, {@link URI} or object whose
199     *  <code>toString()</code> produces a valid URI string.  See
200     *  {@link URIBuilder#convertToURI(Object)}.
201     * @throws URISyntaxException if the given argument does not represent a valid URI
202     */
203    public HTTPBuilder( Object defaultURI ) throws URISyntaxException {
204        setUri( defaultURI );
205    }
206
207    /**
208     * Give a default URI to be used for all request methods that don't
209     * explicitly take a URI parameter, and a default content-type to be used
210     * for request encoding and response parsing.
211     * @param defaultURI either a {@link URL}, {@link URI} or object whose
212     *  <code>toString()</code> produces a valid URI string.  See
213     *  {@link URIBuilder#convertToURI(Object)}.
214     * @param defaultContentType content-type string.  See {@link ContentType}
215     *   for common types.
216     * @throws URISyntaxException if the uri argument does not represent a valid URI
217     */
218    public HTTPBuilder( Object defaultURI, Object defaultContentType ) throws URISyntaxException {
219        setUri( defaultURI );
220        this.defaultContentType = defaultContentType;
221    }
222
223    /**
224     * <p>Convenience method to perform an HTTP GET.  It will use the HTTPBuilder's
225     * {@link #getHandler() registered response handlers} to handle success or
226     * failure status codes.  By default, the <code>success</code> response
227     * handler will attempt to parse the data and simply return the parsed
228     * object.</p>
229     *
230     * <p><strong>Note:</strong> If using the {@link #defaultSuccessHandler(HttpResponseDecorator, Object)
231     * default <code>success</code> response handler}, be sure to read the
232     * caveat regarding streaming response data.</p>
233     *
234     * @see #getHandler()
235     * @see #defaultSuccessHandler(HttpResponseDecorator, Object)
236     * @see #defaultFailureHandler(HttpResponseDecorator)
237     * @param args see {@link RequestConfigDelegate#setPropertiesFromMap(Map)}
238     * @return whatever was returned from the response closure.
239     * @throws URISyntaxException if a uri argument is given which does not
240     *      represent a valid URI
241     * @throws IOException
242     * @throws ClientProtocolException
243     */
244    public Object get( Map<String,?> args )
245            throws ClientProtocolException, IOException, URISyntaxException {
246        return this.get( args, null );
247    }
248
249    /**
250     * <p>Convenience method to perform an HTTP GET.  The response closure will
251     * be called only on a successful response.  </p>
252     *
253     * <p>A 'failed' response (i.e. any HTTP status code > 399) will be handled
254     * by the registered 'failure' handler.  The
255     * {@link #defaultFailureHandler(HttpResponseDecorator) default failure handler}
256     * throws an {@link HttpResponseException}.</p>
257     *
258     * @param args see {@link RequestConfigDelegate#setPropertiesFromMap(Map)}
259     * @param responseClosure code to handle a successful HTTP response
260     * @return any value returned by the response closure.
261     * @throws ClientProtocolException
262     * @throws IOException
263     * @throws URISyntaxException if a uri argument is given which does not
264     *      represent a valid URI
265     */
266    public Object get( Map<String,?> args, Closure responseClosure )
267            throws ClientProtocolException, IOException, URISyntaxException {
268        RequestConfigDelegate delegate = new RequestConfigDelegate( new HttpGet(),
269                this.defaultContentType,
270                this.defaultRequestHeaders,
271                this.defaultResponseHandlers );
272
273        delegate.setPropertiesFromMap( args );
274        if ( responseClosure != null ) delegate.getResponse().put(
275                Status.SUCCESS, responseClosure );
276        return this.doRequest( delegate );
277    }
278
279    /**
280     * <p>Convenience method to perform an HTTP POST.  It will use the HTTPBuilder's
281     * {@link #getHandler() registered response handlers} to handle success or
282     * failure status codes.  By default, the <code>success</code> response
283     * handler will attempt to parse the data and simply return the parsed
284     * object. </p>
285     *
286     * <p><strong>Note:</strong> If using the {@link #defaultSuccessHandler(HttpResponseDecorator, Object)
287     * default <code>success</code> response handler}, be sure to read the
288     * caveat regarding streaming response data.</p>
289     *
290     * @see #getHandler()
291     * @see #defaultSuccessHandler(HttpResponseDecorator, Object)
292     * @see #defaultFailureHandler(HttpResponseDecorator)
293     * @param args see {@link RequestConfigDelegate#setPropertiesFromMap(Map)}
294     * @return whatever was returned from the response closure.
295     * @throws IOException
296     * @throws URISyntaxException if a uri argument is given which does not
297     *      represent a valid URI
298     * @throws ClientProtocolException
299     */
300    public Object post( Map<String,?> args )
301            throws ClientProtocolException, URISyntaxException, IOException {
302        return this.post( args, null );
303    }
304
305    /** <p>
306     * Convenience method to perform an HTTP form POST.  The response closure will be
307     * called only on a successful response.</p>
308     *
309     * <p>A 'failed' response (i.e. any
310     * HTTP status code > 399) will be handled by the registered 'failure'
311     * handler.  The {@link #defaultFailureHandler(HttpResponseDecorator) default
312     * failure handler} throws an {@link HttpResponseException}.</p>
313     *
314     * <p>The request body (specified by a <code>body</code> named parameter)
315     * will be converted to a url-encoded form string unless a different
316     * <code>requestContentType</code> named parameter is passed to this method.
317     *  (See {@link EncoderRegistry#encodeForm(Map)}.) </p>
318     *
319     * @param args see {@link RequestConfigDelegate#setPropertiesFromMap(Map)}
320     * @param responseClosure code to handle a successful HTTP response
321     * @return any value returned by the response closure.
322     * @throws ClientProtocolException
323     * @throws IOException
324     * @throws URISyntaxException if a uri argument is given which does not
325     *      represent a valid URI
326     */
327    public Object post( Map<String,?> args, Closure responseClosure )
328            throws URISyntaxException, ClientProtocolException, IOException {
329        RequestConfigDelegate delegate = new RequestConfigDelegate( new HttpPost(),
330                this.defaultContentType,
331                this.defaultRequestHeaders,
332                this.defaultResponseHandlers );
333
334        /* by default assume the request body will be URLEncoded, but allow
335           the 'requestContentType' named argument to override this if it is
336           given */
337        delegate.setRequestContentType( ContentType.URLENC.toString() );
338        delegate.setPropertiesFromMap( args );
339
340        if ( responseClosure != null ) delegate.getResponse().put(
341                Status.SUCCESS.toString(), responseClosure );
342
343        return this.doRequest( delegate );
344    }
345
346    /**
347     * Make an HTTP request to the default URI, and parse using the default
348     * content-type.
349     * @see #request(Object, Method, Object, Closure)
350     * @param method {@link Method HTTP method}
351     * @param configClosure request configuration options
352     * @return whatever value was returned by the executed response handler.
353     * @throws ClientProtocolException
354     * @throws IOException
355     */
356    public Object request( Method method, Closure configClosure ) throws ClientProtocolException, IOException {
357        return this.doRequest( this.defaultURI.toURI(), method,
358                this.defaultContentType, configClosure );
359    }
360
361    /**
362     * Make an HTTP request using the default URI, with the given method,
363     * content-type, and configuration.
364     * @see #request(Object, Method, Object, Closure)
365     * @param method {@link Method HTTP method}
366     * @param contentType either a {@link ContentType} or valid content-type string.
367     * @param configClosure request configuration options
368     * @return whatever value was returned by the executed response handler.
369     * @throws ClientProtocolException
370     * @throws IOException
371     */
372    public Object request( Method method, Object contentType, Closure configClosure )
373            throws ClientProtocolException, IOException {
374        return this.doRequest( this.defaultURI.toURI(), method,
375                contentType, configClosure );
376    }
377
378    /**
379     * Make a request for the given HTTP method and content-type, with
380     * additional options configured in the <code>configClosure</code>.  See
381     * {@link RequestConfigDelegate} for options.
382     * @param uri either a {@link URL}, {@link URI} or object whose
383     *  <code>toString()</code> produces a valid URI string.  See
384     *  {@link URIBuilder#convertToURI(Object)}.
385     * @param method {@link Method HTTP method}
386     * @param contentType either a {@link ContentType} or valid content-type string.
387     * @param configClosure closure from which to configure options like
388     *   {@link RequestConfigDelegate#getUri() uri.path},
389     *   {@link URIBuilder#setQuery(Map) request parameters},
390     *   {@link RequestConfigDelegate#setHeaders(Map) headers},
391     *   {@link RequestConfigDelegate#setBody(Object) request body} and
392     *   {@link RequestConfigDelegate#getResponse() response handlers}.
393     *
394     * @return whatever value was returned by the executed response handler.
395     * @throws ClientProtocolException
396     * @throws IOException
397     * @throws URISyntaxException if the uri argument does not represent a valid URI
398     */
399    public Object request( Object uri, Method method, Object contentType, Closure configClosure )
400            throws ClientProtocolException, IOException, URISyntaxException {
401        return this.doRequest( convertToURI( uri ), method, contentType, configClosure );
402    }
403
404    /**
405     * Create a {@link RequestConfigDelegate} from the given arguments, execute the
406     * config closure, then pass the delegate to {@link #doRequest(RequestConfigDelegate)},
407     * which actually executes the request.
408     */
409    protected Object doRequest( URI uri, Method method, Object contentType, Closure configClosure )
410            throws ClientProtocolException, IOException {
411
412        HttpRequestBase reqMethod;
413        try { reqMethod = method.getRequestType().newInstance();
414        // this exception should reasonably never occur:
415        } catch ( Exception e ) { throw new RuntimeException( e ); }
416
417        reqMethod.setURI( uri );
418        RequestConfigDelegate delegate = new RequestConfigDelegate( reqMethod, contentType,
419                this.defaultRequestHeaders,
420                this.defaultResponseHandlers );
421        configClosure.setDelegate( delegate );
422        configClosure.setResolveStrategy( Closure.DELEGATE_FIRST );
423        configClosure.call( reqMethod );
424
425        return this.doRequest( delegate );
426    }
427
428    /**
429     * All <code>request</code> methods delegate to this method.
430     */
431    protected Object doRequest( final RequestConfigDelegate delegate )
432            throws ClientProtocolException, IOException {
433        delegate.encodeBody();
434        final HttpRequestBase reqMethod = delegate.getRequest();
435
436        final Object contentType = delegate.getContentType();
437
438        if ( this.autoAcceptHeader ) {
439            String acceptContentTypes = contentType.toString();
440            if ( contentType instanceof ContentType )
441                acceptContentTypes = ((ContentType)contentType).getAcceptHeader();
442            reqMethod.setHeader( "Accept", acceptContentTypes );
443        }
444
445        reqMethod.setURI( delegate.getUri().toURI() );
446        if ( reqMethod.getURI() == null)
447            throw new IllegalStateException( "Request URI cannot be null" );
448
449        log.debug( reqMethod.getMethod() + " " + reqMethod.getURI() );
450
451        // set any request headers from the delegate
452        Map<?,?> headers = delegate.getHeaders();
453        for ( Object key : headers.keySet() ) {
454            Object val = headers.get( key );
455            if ( key == null ) continue;
456            if ( val == null ) reqMethod.removeHeaders( key.toString() );
457            else reqMethod.setHeader( key.toString(), val.toString() );
458        }
459
460        ResponseHandler<Object> responseHandler = new ResponseHandler<Object>() {
461            public Object handleResponse(HttpResponse response)
462                throws ClientProtocolException, IOException {
463                HttpResponseDecorator resp = new HttpResponseDecorator(
464                        response, delegate.getContext(), null );
465                try {
466                    int status = resp.getStatusLine().getStatusCode();
467                    Closure responseClosure = delegate.findResponseHandler( status );
468                    log.debug( "Response code: " + status + "; found handler: " + responseClosure );
469
470                    Object[] closureArgs = null;
471                    switch ( responseClosure.getMaximumNumberOfParameters() ) {
472                    case 1 :
473                        closureArgs = new Object[] { resp };
474                        break;
475                    case 2 : // parse the response entity if the response handler expects it:
476                        HttpEntity entity = resp.getEntity();
477                        try {
478                            if ( entity == null || entity.getContentLength() == 0 )
479                                closureArgs = new Object[] { resp, null };
480                            else closureArgs = new Object[] { resp, parseResponse( resp, contentType ) };
481                        }
482                        catch ( Exception ex ) {
483                            Header h = entity.getContentType();
484                            String respContentType = h != null ? h.getValue() : null;
485                            log.warn( "Error parsing '" + respContentType + "' response", ex );
486                            throw new ResponseParseException( resp, ex );
487                        }
488                        break;
489                    default:
490                        throw new IllegalArgumentException(
491                                "Response closure must accept one or two parameters" );
492                    }
493
494                    Object returnVal = responseClosure.call( closureArgs );
495                    log.trace( "response handler result: " + returnVal );
496
497                    return returnVal;
498                }
499                finally {
500                    HttpEntity entity = resp.getEntity();
501                    if ( entity != null ) entity.consumeContent();
502                }
503            }
504        };
505
506        return getClient().execute(reqMethod, responseHandler, delegate.getContext());
507    }
508
509    /**
510     * Parse the response data based on the given content-type.
511     * If the given content-type is {@link ContentType#ANY}, the
512     * <code>content-type</code> header from the response will be used to
513     * determine how to parse the response.
514     * @param resp
515     * @param contentType
516     * @return whatever was returned from the parser retrieved for the given
517     *  content-type, or <code>null</code> if no parser could be found for this
518     *  content-type.  The parser will also return <code>null</code> if the
519     *  response does not contain any content (e.g. in response to a HEAD request).
520     * @throws HttpResponseException if there is a error parsing the response
521     */
522    protected Object parseResponse( HttpResponse resp, Object contentType )
523            throws HttpResponseException {
524        // For HEAD or OPTIONS requests, there should be no response entity.
525        if ( resp.getEntity() == null ) {
526            log.debug( "Response contains no entity.  Parsed data is null." );
527            return null;
528        }
529        // first, start with the _given_ content-type
530        String responseContentType = contentType.toString();
531        // if the given content-type is ANY ("*/*") then use the response content-type
532        try {
533            if ( ContentType.ANY.toString().equals( responseContentType ) )
534                responseContentType = ParserRegistry.getContentType( resp );
535        }
536        catch ( RuntimeException ex ) {
537            log.warn( "Could not parse content-type: " + ex.getMessage() );
538            /* if for whatever reason we can't determine the content-type, but
539             * still want to attempt to parse the data, use the BINARY
540             * content-type so that the response will be buffered into a
541             * ByteArrayInputStream. */
542            responseContentType = ContentType.BINARY.toString();
543        }
544
545        Object parsedData = null;
546        Closure parser = parsers.getAt( responseContentType );
547        if ( parser == null ) log.warn( "No parser found for content-type: "
548            + responseContentType );
549        else {
550            log.debug( "Parsing response as: " + responseContentType );
551            parsedData = parser.call( resp );
552            if ( parsedData == null ) log.warn( "Parser returned null!" );
553            else log.debug( "Parsed data to instance of: " + parsedData.getClass() );
554        }
555        return parsedData;
556    }
557
558    /**
559     * Creates default response handlers for {@link Status#SUCCESS success} and
560     * {@link Status#FAILURE failure} status codes.  This is used to populate
561     * the handler map when a new HTTPBuilder instance is created.
562     * @see #defaultSuccessHandler(HttpResponseDecorator, Object)
563     * @see #defaultFailureHandler(HttpResponseDecorator)
564     * @return the default response handler map.
565     */
566    protected Map<Object,Closure> buildDefaultResponseHandlers() {
567        Map<Object,Closure> map = new StringHashMap<Closure>();
568        map.put( Status.SUCCESS,
569                new MethodClosure(this,"defaultSuccessHandler"));
570        map.put(  Status.FAILURE,
571                new MethodClosure(this,"defaultFailureHandler"));
572
573        return map;
574    }
575
576    /**
577     * <p>This is the default <code>response.success</code> handler.  It will be
578     * executed if the response is not handled by a status-code-specific handler
579     * (i.e. <code>response.'200'= {..}</code>) and no generic 'success' handler
580     * is given (i.e. <code>response.success = {..}</code>.)  This handler simply
581     * returns the parsed data from the response body.  In most cases you will
582     * probably want to define a <code>response.success = {...}</code> handler
583     * from the request closure, which will replace the response handler defined
584     * by this method.  </p>
585     *
586     * <h4>Note for parsers that return streaming content:</h4>
587     * <p>For responses parsed as {@link ParserRegistry#parseStream(HttpResponse)
588     * BINARY} or {@link ParserRegistry#parseText(HttpResponse) TEXT}, the
589     * parser will return streaming content -- an <code>InputStream</code> or
590     * <code>Reader</code>.  In these cases, this handler will buffer the the
591     * response content before the network connection is closed.  </p>
592     *
593     * <p>In practice, a user-supplied response handler closure is
594     * <i>designed</i> to handle streaming content so it can be read directly from
595     * the response stream without buffering, which will be much more efficient.
596     * Therefore, it is recommended that request method variants be used which
597     * explicitly accept a response handler closure in these cases.</p>
598     *
599     * @param resp HTTP response
600     * @param parsedData parsed data as resolved from this instance's {@link ParserRegistry}
601     * @return the parsed data object (whatever the parser returns).
602     * @throws ResponseParseException if there is an error buffering a streaming
603     *   response.
604     */
605    protected Object defaultSuccessHandler( HttpResponseDecorator resp, Object parsedData )
606            throws ResponseParseException {
607        try {
608            //If response is streaming, buffer it in a byte array:
609            if ( parsedData instanceof InputStream ) {
610                ByteArrayOutputStream buffer = new ByteArrayOutputStream();
611                DefaultGroovyMethods.leftShift( buffer, (InputStream)parsedData );
612                parsedData = new ByteArrayInputStream( buffer.toByteArray() );
613            }
614            else if ( parsedData instanceof Reader ) {
615                StringWriter buffer = new StringWriter();
616                DefaultGroovyMethods.leftShift( buffer, (Reader)parsedData );
617                parsedData = new StringReader( buffer.toString() );
618            }
619            else if ( parsedData instanceof Closeable )
620                log.warn( "Parsed data is streaming, but will be accessible after " +
621                        "the network connection is closed.  Use at your own risk!" );
622            return parsedData;
623        }
624        catch ( IOException ex ) {
625            throw new ResponseParseException( resp, ex );
626        }
627    }
628
629    /**
630     * This is the default <code>response.failure</code> handler.  It will be
631     * executed if no status-code-specific handler is set (i.e.
632     * <code>response.'404'= {..}</code>).  This default handler will throw a
633     * {@link HttpResponseException} when executed.  In most cases you
634     * will want to define your own <code>response.failure = {...}</code>
635     * handler from the request closure, if you don't want an exception to be
636     * thrown for 4xx and 5xx status responses.
637
638     * @param resp
639     * @throws HttpResponseException
640     */
641    protected void defaultFailureHandler( HttpResponseDecorator resp ) throws HttpResponseException {
642        throw new HttpResponseException( resp );
643    }
644
645    /**
646     * Retrieve the map of response code handlers.  Each map key is a response
647     * code as a string (i.e. '401') or either 'success' or 'failure'.  Use this
648     * to set default response handlers, e.g.
649     * <pre>builder.handler.'401' = { resp -> println "${resp.statusLine}" }</pre>
650     * @see Status
651     * @return
652     */
653    public Map<?,Closure> getHandler() {
654        return this.defaultResponseHandlers;
655    }
656
657    /**
658     * Retrieve the map of registered response content-type parsers.  Use
659     * this to set default response parsers, e.g.
660     * <pre>
661     * builder.parser.'text/javascript' = { resp ->
662     *    return resp.entity.content // just returns an InputStream
663     * }</pre>
664     * @return
665     */
666    public ParserRegistry getParser() {
667        return this.parsers;
668    }
669
670    /**
671     * Retrieve the map of registered request content-type encoders.  Use this
672     * to customize a request encoder for specific content-types, e.g.
673     * <pre>
674     * builder.encoder.'text/javascript' = { body ->
675     *   def json = body.call( new JsonGroovyBuilder() )
676     *   return new StringEntity( json.toString() )
677     * }</pre>
678     * By default this map is populated by calling
679     * {@link EncoderRegistry#buildDefaultEncoderMap()}.  This method is also
680     * used by {@link RequestConfigDelegate} to retrieve the proper encoder for building
681     * the request content body.
682     *
683     * @return a map of 'encoder' closures, keyed by content-type string.
684     */
685    public EncoderRegistry getEncoder() {
686        return this.encoders;
687    }
688
689    /**
690     * Set the default content type that will be used to select the appropriate
691     * request encoder and response parser.  The {@link ContentType} enum holds
692     * some common content-types that may be used, i.e. <pre>
693     * import static ContentType.*
694     * builder.contentType = XML
695     * </pre>
696     * Setting the default content-type does three things:
697     * <ol>
698     *   <li>It tells the builder to encode any {@link RequestConfigDelegate#setBody(Object)
699     *   request body} as this content-type.  Calling {@link
700     *   RequestConfigDelegate#setRequestContentType(String)} can override this
701     *   on a per-request basis.</li>
702     *   <li>Tells the builder to parse any response as this content-type,
703     *   regardless of any <code>content-type</code> header that is sent in the
704     *   response.</li>
705     *   <li>Sets the <code>Accept</code> header to this content-type for all
706     *   requests (see {@link ContentType#getAcceptHeader()}).  Note
707     *   that any <code>Accept</code> header explicitly set either in
708     *   {@link #setHeaders(Map)} or {@link RequestConfigDelegate#setHeaders(Map)}
709     *   will override this value.</li>
710     * </ol>
711     * <p>Additionally, if the content-type is set to {@link ContentType#ANY},
712     * HTTPBuilder <i>will</i> rely on the <code>content-type</code> response
713     * header to determine how to parse the response data.  This allows the user
714     * to rely on response headers if they are accurate, or ignore them and
715     * forcibly use a certain response parser if so desired.</p>
716     *
717     * <p>This value is a default and may always be overridden on a per-request
718     * basis by using the {@link #request(Method, Object, Closure)
719     * builder.request( Method, ContentType, Closure )} method or passing a
720     * <code>contentType</code> named parameter.
721     * @see EncoderRegistry
722     * @see ParserRegistry
723     * @param ct either a {@link ContentType} or string value (i.e. <code>"text/xml"</code>.)
724     */
725    public void setContentType( Object ct ) {
726        this.defaultContentType = ct;
727    }
728
729    /**
730     * @return default content type used for request and response.
731     */
732    public Object getContentType() {
733        return this.defaultContentType;
734    }
735
736    /**
737     * Indicate whether or not this cliernt should send an <code>Accept</code>
738     * header automatically based on the {@link #getContentType() contentType}
739     * property.
740     * @param shouldSendAcceptHeader <code>true</code> if the client should
741     * automatically insert an <code>Accept</code> header, otherwise <code>false</code>.
742     */
743    public void setAutoAcceptHeader( boolean shouldSendAcceptHeader ) {
744        this.autoAcceptHeader = shouldSendAcceptHeader;
745    }
746
747    /**
748     * Indicates whether or not this client should automatically send an
749     * <code>Accept</code> header based on the {@link #getContentType() contentType}
750     * property.  Default is <code>true</code>.
751     * @return <code>true</code> if the client should automatically add an
752     * <code>Accept</code> header to the request; if <code>false</code>, no
753     * header is added.
754     */
755    public boolean isAutoAcceptHeader() {
756        return this.autoAcceptHeader;
757    }
758
759    /**
760     * Set acceptable request and response content-encodings.
761     * @see ContentEncodingRegistry
762     * @param encodings each Object should be either a
763     * {@link ContentEncoding.Type} value, or a <code>content-encoding</code>
764     * string that is known by the {@link ContentEncodingRegistry}
765     */
766    public void setContentEncoding( Object... encodings ) {
767                HttpClient client = getClient();
768                if ( client instanceof AbstractHttpClient ) {
769          this.contentEncodingHandler.setInterceptors( (AbstractHttpClient)client, encodings );
770                } else {
771                  throw new IllegalStateException("The HttpClient is not an AbstractHttpClient!");
772                }
773
774    }
775
776    /**
777     * Set the default URI used for requests that do not explicitly take a
778     * <code>uri</code> param.
779     * @param uri either a {@link URL}, {@link URI} or object whose
780     *  <code>toString()</code> produces a valid URI string.  See
781     *  {@link URIBuilder#convertToURI(Object)}.
782     * @throws URISyntaxException if the uri argument does not represent a valid URI
783     */
784    public void setUri( Object uri ) throws URISyntaxException {
785        this.defaultURI = uri != null ? new URIBuilder( convertToURI( uri ) ) : null;
786    }
787
788    /**
789     * Get the default URI used for requests that do not explicitly take a
790     * <code>uri</code> param.
791     * @return a {@link URIBuilder} instance.  Note that the return type is Object
792     * simply so that it matches with its JavaBean {@link #setUri(Object)}
793     * counterpart.
794     */
795    public Object getUri() {
796        return defaultURI;
797    }
798
799    /**
800     * Set the default headers to add to all requests made by this builder
801     * instance.  These values will replace any previously set default headers.
802     * @param headers map of header names & values.
803     */
804    public void setHeaders( Map<?,?> headers ) {
805        this.defaultRequestHeaders.clear();
806        if ( headers == null ) return;
807        for( Object key : headers.keySet() ) {
808            Object val = headers.get( key );
809            if ( val == null ) continue;
810            this.defaultRequestHeaders.put( key.toString(), val.toString() );
811        }
812    }
813
814    /**
815     * Get the map of default headers that will be added to all requests.
816     * This is a 'live' collection so it may be used to add or remove default
817     * values.
818     * @return the map of default header names and values.
819     */
820    public Map<?,?> getHeaders() {
821        return this.defaultRequestHeaders;
822    }
823
824    /**
825     * Return the underlying HTTPClient that is used to handle HTTP requests.
826     * @return the client instance.
827     */
828    public HttpClient getClient() {
829        if (client == null) {
830            HttpParams defaultParams = new BasicHttpParams();
831            defaultParams.setParameter( CookieSpecPNames.DATE_PATTERNS,
832                    Arrays.asList("EEE, dd-MMM-yyyy HH:mm:ss z", "EEE, dd MMM yyyy HH:mm:ss z") );
833            client = createClient(defaultParams);
834        }
835        return client;
836    }
837
838    public void setClient(HttpClient client) {
839        this.client = client;
840    }
841
842    /**
843     * Override this method in a subclass to customize creation of the
844     * HttpClient instance.
845     * @param params
846     * @return
847     */
848    protected HttpClient createClient( HttpParams params ) {
849        return new DefaultHttpClient(params);
850    }
851
852    /**
853     * Used to access the {@link AuthConfig} handler used to configure common
854     * authentication mechanism.  Example:
855     * <pre>builder.auth.basic( 'myUser', 'somePassword' )</pre>
856     * @return
857     */
858    public AuthConfig getAuth() { return this.auth; }
859
860    /**
861     * Set an alternative {@link AuthConfig} implementation to handle
862     * authorization.
863     * @param ac instance to use.
864     */
865    public void setAuthConfig( AuthConfig ac ) {
866        this.auth = ac;
867    }
868
869    /**
870     * Set a custom registry used to handle different request
871     * <code>content-type</code>s.
872     * @param er
873     */
874    public void setEncoderRegistry( EncoderRegistry er ) {
875        this.encoders = er;
876    }
877
878    /**
879     * Set a custom registry used to handle different response
880     * <code>content-type</code>s
881     * @param pr
882     */
883    public void setParserRegistry( ParserRegistry pr ) {
884        this.parsers = pr;
885    }
886
887    /**
888     * Set a custom registry used to handle different
889     * <code>content-encoding</code> types in responses.
890     * @param cer
891     */
892    public void setContentEncodingRegistry( ContentEncodingRegistry cer ) {
893        this.contentEncodingHandler = cer;
894    }
895
896    /**
897     * Set the default HTTP proxy to be used for all requests.
898     * @see HttpHost#HttpHost(String, int, String)
899     * @param host host name or IP
900     * @param port port, or -1 for the default port
901     * @param scheme usually "http" or "https," or <code>null</code> for the default
902     */
903    public void setProxy( String host, int port, String scheme ) {
904        getClient().getParams().setParameter(
905                ConnRoutePNames.DEFAULT_PROXY,
906                new HttpHost(host,port,scheme) );
907    }
908
909    /**
910     * Release any system resources held by this instance.
911     * @see ClientConnectionManager#shutdown()
912     */
913    public void shutdown() {
914        getClient().getConnectionManager().shutdown();
915    }
916
917
918
919    /**
920     * <p>Encloses all properties and method calls used within the
921     * {@link HTTPBuilder#request(Object, Method, Object, Closure)} 'config'
922     * closure argument.  That is, an instance of this class is set as the
923     * closure's delegate.  This allows the user to configure various parameters
924     * within the scope of a single request.  </p>
925     *
926     * <p>All properties of this class are available from within the closure.
927     * For example, you can manipulate various aspects of the
928     * {@link HTTPBuilder#setUri(Object) default request URI} for this request
929     * by calling <code>uri.path = '/api/location'</code>.  This allows for the
930     * ability to modify parameters per-request while leaving any values set
931     * directly on the HTTPBuilder instance unchanged for subsequent requests.
932     * </p>
933     *
934     */
935    protected class RequestConfigDelegate {
936        private HttpRequestBase request;
937        private Object contentType;
938        private Object requestContentType;
939        private Map<Object,Closure> responseHandlers = new StringHashMap<Closure>();
940        private URIBuilder uri;
941        private Map<Object,Object> headers = new StringHashMap<Object>();
942        private HttpContextDecorator context = new HttpContextDecorator();
943        private Object body;
944
945        public RequestConfigDelegate( HttpRequestBase request, Object contentType,
946                Map<?,?> defaultRequestHeaders,
947                Map<?,Closure> defaultResponseHandlers ) {
948            if ( request == null ) throw new IllegalArgumentException(
949                    "Internal error - HttpRequest instance cannot be null" );
950            this.request = request;
951            this.headers.putAll( defaultRequestHeaders );
952            this.contentType = contentType;
953            if ( defaultRequestContentType != null )
954                this.requestContentType = defaultRequestContentType.toString();
955            this.responseHandlers.putAll( defaultResponseHandlers );
956            URI uri = request.getURI();
957            if ( uri != null ) this.uri = new URIBuilder(uri);
958        }
959
960        public RequestConfigDelegate( Map<String,?> args, HttpRequestBase request, Closure successHandler )
961                throws URISyntaxException {
962            this( request, defaultContentType, defaultRequestHeaders, defaultResponseHandlers );
963            if ( successHandler != null )
964                this.responseHandlers.put( Status.SUCCESS.toString(), successHandler );
965            setPropertiesFromMap( args );
966        }
967
968        /**
969         * Use this object to manipulate parts of the request URI, like
970         * query params and request path.  Example:
971         * <pre>
972         * builder.request(GET,XML) {
973         *   uri.path = '../other/request.jsp'
974         *   uri.query = [p1:1, p2:2]
975         *   ...
976         * }</pre>
977         *
978         * <p>This method signature returns <code>Object</code> so that the
979         * complementary {@link #setUri(Object)} method can accept various
980         * types. </p>
981         * @return {@link URIBuilder} to manipulate the request URI
982         */
983        public URIBuilder getUri() { return this.uri; }
984
985        /**
986         * <p>Set the entire URI to be used for this request.  Acceptable
987         * parameter types are:
988         * <ul>
989         *   <li><code>URL</code></li>
990         *   <li><code>URI</code></li>
991         *   <li><code>URIBuilder</code></li>
992         * </ul>
993         * Any other parameter type will be assumed that its
994         * <code>toString()</code> method produces a valid URI.</p>
995         *
996         * <p>Note that if you want to change just a portion of the request URI,
997         * (e.g. the host, port, path, etc.) you can call {@link #getUri()}
998         * which will return a {@link URIBuilder} which can manipulate portions
999         * of the request URI.</p>
1000         *
1001         * @see URIBuilder#convertToURI(Object)
1002         * @throws URISyntaxException if an argument is given that is not a valid URI
1003         * @param uri the URI to use for this request.
1004         */
1005        public void setUri( Object uri ) throws URISyntaxException {
1006            if ( uri instanceof URIBuilder ) this.uri = (URIBuilder)uri;
1007            this.uri = new URIBuilder( convertToURI( uri ) );
1008        }
1009
1010        /**
1011         * Directly access the Apache HttpClient instance that will
1012         * be used to execute this request.
1013         * @see HttpRequestBase
1014         */
1015        protected HttpRequestBase getRequest() { return this.request; }
1016
1017        /**
1018         * Get the content-type of any data sent in the request body and the
1019         * expected response content-type.  If the request content-type is
1020         * expected to differ from the response content-type (i.e. a URL-encoded
1021         * POST that should return an HTML page) then this value will be used
1022         * for the <i>response</i> content-type, while
1023         * {@link #setRequestContentType(String)} should be used for the request.
1024         *
1025         * @return whatever value was assigned via {@link #setContentType(Object)}
1026         * or passed from the {@link HTTPBuilder#defaultContentType} when this
1027         * RequestConfigDelegate instance was constructed.
1028         */
1029        protected Object getContentType() { return this.contentType; }
1030
1031        /**
1032         * Set the content-type used for any data in the request body, as well
1033         * as the <code>Accept</code> content-type that will be used for parsing
1034         * the response. The value should be either a {@link ContentType} value
1035         * or a String, i.e. <code>"text/plain"</code>.  This will default to
1036         * {@link HTTPBuilder#getContentType()} for requests that do not
1037         * explicitly pass a <code>contentType</code> parameter (such as
1038         * {@link HTTPBuilder#request(Method, Object, Closure)}).
1039         * @param ct the value that will be used for the <code>Content-Type</code>
1040         * and <code>Accept</code> request headers.
1041         */
1042        protected void setContentType( Object ct ) {
1043            if ( ct == null ) this.contentType = defaultContentType;
1044            else this.contentType = ct;
1045        }
1046
1047        /**
1048         * The request content-type, if different from the {@link #contentType}.
1049         * @return either a {@link ContentType} value or String like <code>text/plain</code>
1050         */
1051        protected Object getRequestContentType() {
1052            if ( this.requestContentType != null ) return this.requestContentType;
1053            else return this.getContentType();
1054        }
1055
1056        /**
1057         * <p>Assign a different content-type for the request than is expected for
1058         * the response.  This is useful if i.e. you want to post URL-encoded
1059         * form data but expect the response to be XML or HTML.  The
1060         * {@link #getContentType()} will always control the <code>Accept</code>
1061         * header, and will be used for the request content <i>unless</i> this
1062         * value is also explicitly set.</p>
1063         * <p>Note that this method is used internally; calls within a request
1064         * configuration closure should call {@link #send(Object, Object)}
1065         * to set the request body and content-type at the same time.</p>
1066         * @param ct either a {@link ContentType} value or a valid content-type
1067         * String.
1068         */
1069        protected void setRequestContentType( Object ct ) {
1070            this.requestContentType = ct;
1071        }
1072
1073        /**
1074         * Valid arguments:
1075         * <dl>
1076         *   <dt>uri</dt><dd>Either a URI, URL, or object whose
1077         *      <code>toString()</code> method produces a valid URI string.
1078         *      If this parameter is not supplied, the HTTPBuilder's default
1079         *      URI is used.</dd>
1080         *   <dt>path</dt><dd>Request path that is merged with the URI</dd>
1081         *   <dt>queryString</dt><dd>an escaped query string</dd>
1082         *   <dt>query</dt><dd>Map of URL query parameters</dd>
1083         *   <dt>headers</dt><dd>Map of HTTP headers</dd>
1084         *   <dt>contentType</dt><dd>Request content type and Accept header.
1085         *      If not supplied, the HTTPBuilder's default content-type is used.</dd>
1086         *   <dt>requestContentType</dt><dd>content type for the request, if it
1087         *      is different from the expected response content-type</dd>
1088         *   <dt>body</dt><dd>Request body that will be encoded based on the given contentType</dd>
1089         * </dl>
1090         * Note that if both <code>queryString</code> and <code>query</code> are given,
1091         * <code>query</code> will be merged with (and potentially override)
1092         * the parameters given as part of <code>queryString</code>.
1093         * @param args named parameters to set properties on this delegate.
1094         * @throws URISyntaxException if the uri argument does not represent a valid URI
1095         */
1096        @SuppressWarnings("unchecked")
1097        protected void setPropertiesFromMap( Map<String,?> args ) throws URISyntaxException {
1098            if ( args == null ) return;
1099            if ( args.containsKey( "url" ) ) throw new IllegalArgumentException(
1100                    "The 'url' parameter is deprecated; use 'uri' instead" );
1101            Object uri = args.remove( "uri" );
1102            if ( uri == null ) uri = defaultURI;
1103            if ( uri == null ) throw new IllegalStateException(
1104                    "Default URI is null, and no 'uri' parameter was given" );
1105            this.uri = new URIBuilder( convertToURI( uri ) );
1106
1107            Map query = (Map)args.remove( "params" );
1108            if ( query != null ) {
1109                log.warn( "'params' argument is deprecated; use 'query' instead." );
1110                this.uri.setQuery( query );
1111            }
1112            String queryString = (String)args.remove("queryString");
1113            if ( queryString != null ) this.uri.setRawQuery(queryString);
1114
1115            query = (Map)args.remove( "query" );
1116            if ( query != null ) this.uri.addQueryParams( query );
1117            Map headers = (Map)args.remove( "headers" );
1118            if ( headers != null ) this.getHeaders().putAll( headers );
1119
1120            Object path = args.remove( "path" );
1121            if ( path != null ) this.uri.setPath( path.toString() );
1122
1123            Object contentType = args.remove( "contentType" );
1124            if ( contentType != null ) this.setContentType( contentType );
1125
1126            contentType = args.remove( "requestContentType" );
1127            if ( contentType != null ) this.setRequestContentType( contentType );
1128
1129            Object body = args.remove("body");
1130            if ( body != null ) this.setBody( body );
1131
1132            if ( args.size() > 0 ) {
1133                String invalidArgs = "";
1134                for ( String k : args.keySet() ) invalidArgs += k + ",";
1135                throw new IllegalArgumentException("Unexpected keyword args: " + invalidArgs);
1136            }
1137        }
1138
1139        /**
1140         * Set request headers.  These values will be <strong>merged</strong>
1141         * with any {@link HTTPBuilder#getHeaders() default request headers.}
1142         * (The assumption is you'll probably want to add a bunch of headers to
1143         * whatever defaults you've already set).  If you <i>only</i> want to
1144         * use values set here, simply call {@link #getHeaders() headers.clear()}
1145         * first.
1146         */
1147        public void setHeaders( Map<?,?> newHeaders ) {
1148            this.headers.putAll( newHeaders );
1149        }
1150
1151        /**
1152         * <p>Get request headers (including any default headers set on this
1153         * {@link HTTPBuilder#setHeaders(Map) HTTPBuilder instance}).  Note that
1154         * this will not include any <code>Accept</code>, <code>Content-Type</code>,
1155         * or <code>Content-Encoding</code> headers that are automatically
1156         * handled by any encoder or parsers in effect.  Note that any values
1157         * set here <i>will</i> override any of those automatically assigned
1158         * values.</p>
1159         *
1160         * <p>Example: <code>headers.'Accept-Language' = 'en, en-gb;q=0.8'</code></p>
1161         * @return a map of HTTP headers that will be sent in the request.
1162         */
1163        public Map<?,?> getHeaders() {
1164            return this.headers;
1165        }
1166
1167        /**
1168         * Convenience method to set a request content-type at the same time
1169         * the request body is set.  This is a variation of
1170         * {@link #setBody(Object)} that allows for a different content-type
1171         * than what is expected for the response.
1172         *
1173         * <p>Example:
1174         * <pre>
1175         * http.request(POST,HTML) {
1176         *
1177         *   /* request data is interpreted as a JsonBuilder closure by
1178         *      HTTPBuilder's default EncoderRegistry implementation * /
1179         *   send( 'text/javascript' ) {
1180         *     a : ['one','two','three']
1181         *   }
1182         *
1183         *   // response content-type is what was specified in the outer request() argument:
1184         *   response.success = { resp, html ->
1185         *
1186         *   }
1187         * }
1188         * </pre>
1189         * The <code>send</code> call is equivalent to the following:
1190         * <pre>
1191         *   requestContentType = 'text/javascript'
1192         *   body = { a : ['one','two','three'] }
1193         * </pre>
1194         *
1195         * @param contentType either a {@link ContentType} or equivalent
1196         *   content-type string like <code>"text/xml"</code>
1197         * @param requestBody
1198         */
1199        public void send( Object contentType, Object requestBody ) {
1200            this.setRequestContentType( contentType );
1201            this.setBody( requestBody );
1202        }
1203
1204        /**
1205         * Set the request body.  This value may be of any type supported by
1206         * the associated {@link EncoderRegistry request encoder}.  That is,
1207         * the value of <code>body</code> will be interpreted by the encoder
1208         * associated with the current {@link #getRequestContentType() request
1209         * content-type}.
1210         * @see #send(Object, Object)
1211         * @param body data or closure interpreted as the request body
1212         */
1213        public void setBody( Object body ) {
1214            this.body = body;
1215        }
1216
1217        public void encodeBody() {
1218            if (body == null) {
1219                return;
1220            }
1221            if ( ! (request instanceof HttpEntityEnclosingRequest ) )
1222                throw new IllegalArgumentException(
1223                        "Cannot set a request body for a " + request.getMethod() + " method" );
1224
1225            Closure encoder = encoders.getAt( this.getRequestContentType() );
1226
1227            // Either content type or encoder is empty.
1228            if ( encoder == null )
1229                throw new IllegalArgumentException(
1230                        "No encoder found for request content type " + getRequestContentType() );
1231
1232            HttpEntity entity = encoder.getMaximumNumberOfParameters() == 2
1233                    ? (HttpEntity)encoder.call( new Object[] { body, this.getRequestContentType() } )
1234                    : (HttpEntity)encoder.call( body );
1235
1236            ((HttpEntityEnclosingRequest)this.request).setEntity( entity );
1237        }
1238
1239        /**
1240         * Get the proper response handler for the response code.  This is called
1241         * by the {@link HTTPBuilder} class in order to find the proper handler
1242         * based on the response status code.
1243         *
1244         * @param statusCode HTTP response status code
1245         * @return the response handler
1246         */
1247        protected Closure findResponseHandler( int statusCode ) {
1248            Closure handler = this.getResponse().get( Integer.toString( statusCode ) );
1249            if ( handler == null ) handler =
1250                this.getResponse().get( Status.find( statusCode ).toString() );
1251            return handler;
1252        }
1253
1254        /**
1255         * Access the response handler map to set response parsing logic.
1256         * i.e.<pre>
1257         * builder.request( GET, XML ) {
1258         *   response.success = { xml ->
1259         *      /* for XML content type, the default parser
1260         *         will return an XmlSlurper * /
1261         *      xml.root.children().each { println it }
1262         *   }
1263         * }</pre>
1264         * @return
1265         */
1266        public Map<Object,Closure> getResponse() { return this.responseHandlers; }
1267
1268        /**
1269         * Get the {@link HttpContext} that will be used for this request.  By
1270         * default, a new context is created for each request.
1271         * @see ClientContext
1272         * @return
1273         */
1274        public HttpContextDecorator getContext() { return this.context; }
1275
1276        /**
1277         * Set the {@link HttpContext} that will be used for this request.
1278         * @param ctx
1279         */
1280        public void setContext( HttpContext ctx ) { this.context = new HttpContextDecorator(ctx); }
1281    }
1282}