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