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