001    /*
002     * Copyright 2003-2008 the original author or authors.
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 asked (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.MalformedURLException;
036    import java.net.URI;
037    import java.net.URISyntaxException;
038    import java.net.URL;
039    import java.util.HashMap;
040    import java.util.Map;
041    
042    import org.apache.commons.logging.Log;
043    import org.apache.commons.logging.LogFactory;
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.HttpResponseException;
050    import org.apache.http.client.methods.HttpGet;
051    import org.apache.http.client.methods.HttpPost;
052    import org.apache.http.client.methods.HttpRequestBase;
053    import org.apache.http.conn.ClientConnectionManager;
054    import org.apache.http.conn.params.ConnRoutePNames;
055    import org.apache.http.impl.client.AbstractHttpClient;
056    import org.apache.http.impl.client.DefaultHttpClient;
057    import org.codehaus.groovy.runtime.DefaultGroovyMethods;
058    import org.codehaus.groovy.runtime.MethodClosure;
059    
060    /** <p>
061     * Groovy DSL for easily making HTTP requests, and handling request and response
062     * data.  This class adds a number of convenience mechanisms built on top of 
063     * Apache HTTPClient for things like URL-encoded POSTs and REST requests that 
064     * require building and parsing JSON or XML.  Convenient access to a few common
065     * authentication methods is also available.</p>
066     * 
067     * 
068     * <h3>Conventions</h3>
069     * <p>HTTPBuilder has properties for default headers, URL, contentType, etc.  
070     * All of these values are also assignable (and in many cases, in much finer 
071     * detail) from the {@link SendDelegate} as well.  In any cases where the value
072     * is not set on the delegate (from within a request closure,) the builder's 
073     * default value is used.  </p>
074     * 
075     * <p>For instance, any methods that do not take a URL parameter assume you will
076     * set a URL value in the request closure or use the builder's assigned 
077     * {@link #getURL() default URL}.</p>
078     * 
079     * 
080     * <h3>Response Parsing</h3>
081     * <p>By default, HTTPBuilder uses {@link ContentType#ANY} as the default 
082     * content-type.  This means the value of the request's <code>Accept</code> 
083     * header is <code>&#42;/*</code>, and the response parser is determined 
084     * based on the response <code>content-type</code> header. </p>
085     * 
086     * <p><strong>If</strong> any contentType is given (either in 
087     * {@link #setContentType(Object)} or as a request method parameter), the 
088     * builder will attempt to parse the response using that content-type, 
089     * regardless of what the server actually responds with.  </p>
090     * 
091     *  
092     * <h3>Examples:</h3>
093     * Perform an HTTP GET and print the response:
094     * <pre>
095     *   def http = new HTTPBuilder('http://www.google.com')
096     *   
097     *   http.get( path : '/search', 
098     *             contentType : TEXT,
099     *             query : [q:'Groovy'] ) { resp, reader ->
100     *     println "response status: ${resp.statusLine}"
101     *     println 'Response data: -----'
102     *     System.out << reader
103     *     println '\n--------------------'
104     *   }
105     * </pre>
106     *   
107     * Long form for other HTTP methods, and response-code-specific handlers.  
108     * This is roughly equivalent to the above example.
109     *   
110     * <pre>
111     *   def http = new HTTPBuilder('http://www.google.com/search?q=groovy')
112     *   
113     *   http.request( GET, TEXT ) { req ->
114     *   
115     *     // executed for all successful responses:
116     *     response.success = { resp, reader ->
117     *       println 'my response handler!'
118     *       assert resp.statusLine.statusCode == 200
119     *       println resp.statusLine
120     *       System.out << reader // print response stream
121     *     }
122     *     
123     *     // executed only if the response status code is 401:
124     *     response.'404' = { resp -> 
125     *       println 'not found!'
126     *     }
127     *   }
128     * </pre>
129     *   
130     * You can also set a default response handler called for any status
131     * code > 399 that is not matched to a specific handler. Setting the value
132     * outside a request closure means it will apply to all future requests with
133     * this HTTPBuilder instance:
134     * <pre>
135     *   http.handler.failure = { resp ->
136     *     println "Unexpected failure: ${resp.statusLine}"
137     *   }
138     * </pre>
139     *   
140     *   
141     * And...  Automatic response parsing for registered content types!
142     *   
143     * <pre>
144     *   http.request( 'http://ajax.googleapis.com', GET, JSON ) {
145     *     url.path = '/ajax/services/search/web'
146     *     url.query = [ v:'1.0', q: 'Calvin and Hobbes' ]
147     *     
148     *     response.success = { resp, json ->
149     *       assert json.size() == 3
150     *       println "Query response: "
151     *       json.responseData.results.each {
152     *         println "  ${it.titleNoFormatting} : ${it.visibleUrl}"
153     *       }
154     *     }
155     *   }
156     * </pre>
157     * 
158     * 
159     * @author <a href='mailto:tnichols@enernoc.com'>Tom Nichols</a>
160     */
161    public class HTTPBuilder {
162            
163            protected AbstractHttpClient client;
164            protected URI defaultURI = null; // TODO make this a URIBuilder?
165            protected AuthConfig auth = new AuthConfig( this );
166            
167            protected final Log log = LogFactory.getLog( getClass() );
168            
169            protected Object defaultContentType = ContentType.ANY;
170            protected final Map<String,Closure> defaultResponseHandlers = buildDefaultResponseHandlers();
171            protected ContentEncodingRegistry contentEncodingHandler = new ContentEncodingRegistry();
172            
173            protected final Map<String,String> defaultRequestHeaders = new HashMap<String,String>();
174            
175            protected EncoderRegistry encoders = new EncoderRegistry();
176            protected ParserRegistry parsers = new ParserRegistry();
177            
178            public HTTPBuilder() { 
179                    super();
180                    this.client = new DefaultHttpClient();
181                    this.setContentEncoding( ContentEncoding.Type.GZIP, 
182                                    ContentEncoding.Type.DEFLATE );
183            }
184            
185            /**
186             * Give a default URL to be used for all request methods that don't 
187             * explicitly take a URL parameter.
188             * @param defaultURL either a {@link URL}, {@link URI} or String
189             * @throws URISyntaxException if the URL was not parse-able
190             */
191            public HTTPBuilder( Object defaultURL ) throws URISyntaxException {
192                    this();
193                    this.defaultURI = convertToURI( defaultURL );
194            }
195            
196            /**
197             * Give a default URL to be used for all request methods that don't 
198             * explicitly take a URL parameter, and a default content-type to be used
199             * for request encoding and response parsing.
200             * @param defaultURL either a {@link URL}, {@link URI} or String
201             * @param defaultContentType content-type string.  See {@link ContentType}
202             *   for common types.
203             * @throws URISyntaxException if the URL was not parse-able
204             */
205            public HTTPBuilder( Object defaultURL, Object defaultContentType ) throws URISyntaxException {
206                    this();
207                    this.defaultURI = convertToURI( defaultURL );
208                    this.defaultContentType = defaultContentType; 
209            }
210            
211            /**
212             * <p>Convenience method to perform an HTTP GET.  It will use the HTTPBuilder's
213             * {@link #getHandler() registered response handlers} to handle success or 
214             * failure status codes.  By default, the <code>success</code> response 
215             * handler will attempt to parse the data and simply return the parsed 
216             * object.</p>
217             * 
218             * <p><strong>Note:</strong> If using the {@link #defaultSuccessHandler(HttpResponse, Object)
219             * default <code>success</code> response handler}, be sure to read the 
220             * caveat regarding streaming response data.</p>
221             * 
222             * @see #getHandler()
223             * @see #defaultSuccessHandler(HttpResponse, Object)
224             * @see #defaultFailureHandler(HttpResponse)
225             * @param args see {@link SendDelegate#setPropertiesFromMap(Map)}
226             * @return whatever was returned from the response closure.  
227             * @throws URISyntaxException 
228             * @throws IOException 
229             * @throws ClientProtocolException 
230             */
231            public Object get( Map<String,?> args ) 
232                            throws ClientProtocolException, IOException, URISyntaxException {
233                    return this.get( args, null );
234            }
235            
236            /**
237             * <p>Convenience method to perform an HTTP GET.  The response closure will 
238             * be called only on a successful response.  </p>
239             * 
240             * <p>A 'failed' response (i.e. any HTTP status code > 399) will be handled 
241             * by the registered 'failure' handler.  The 
242             * {@link #defaultFailureHandler(HttpResponse) default failure handler} 
243             * throws an {@link HttpResponseException}.</p>
244             * 
245             * @param args see {@link SendDelegate#setPropertiesFromMap(Map)}
246             * @param responseClosure code to handle a successful HTTP response
247             * @return any value returned by the response closure.
248             * @throws ClientProtocolException
249             * @throws IOException
250             * @throws URISyntaxException
251             */
252            public Object get( Map<String,?> args, Closure responseClosure ) 
253                            throws ClientProtocolException, IOException, URISyntaxException {
254                    SendDelegate delegate = new SendDelegate( new HttpGet(),
255                                    this.defaultContentType,
256                                    this.defaultRequestHeaders,
257                                    this.defaultResponseHandlers );
258                    
259                    delegate.setPropertiesFromMap( args );
260                    if ( responseClosure != null ) delegate.getResponse().put( 
261                                    Status.SUCCESS.toString(), responseClosure );
262                    return this.doRequest( delegate );
263            }
264            
265            /**
266             * <p>Convenience method to perform an HTTP POST.  It will use the HTTPBuilder's
267             * {@link #getHandler() registered response handlers} to handle success or 
268             * failure status codes.  By default, the <code>success</code> response 
269             * handler will attempt to parse the data and simply return the parsed 
270             * object. </p>
271             * 
272             * <p><strong>Note:</strong> If using the {@link #defaultSuccessHandler(HttpResponse, Object)
273             * default <code>success</code> response handler}, be sure to read the 
274             * caveat regarding streaming response data.</p>
275             * 
276             * @see #getHandler()
277             * @see #defaultSuccessHandler(HttpResponse, Object)
278             * @see #defaultFailureHandler(HttpResponse)
279             * @param args see {@link SendDelegate#setPropertiesFromMap(Map)}
280             * @return whatever was returned from the response closure.  
281             * @throws IOException 
282             * @throws URISyntaxException 
283             * @throws ClientProtocolException 
284             * @throws URISyntaxException 
285             * @throws IOException 
286             * @throws ClientProtocolException 
287             */
288            public Object post( Map<String,?> args ) 
289                            throws ClientProtocolException, URISyntaxException, IOException {
290                    return this.post( args, null );
291            }
292            
293            /** <p>
294             * Convenience method to perform an HTTP form POST.  The response closure will be 
295             * called only on a successful response.</p>   
296             * 
297             * <p>A 'failed' response (i.e. any 
298             * HTTP status code > 399) will be handled by the registered 'failure' 
299             * handler.  The {@link #defaultFailureHandler(HttpResponse) default 
300             * failure handler} throws an {@link HttpResponseException}.</p>  
301             * 
302             * <p>The request body (specified by a <code>body</code> named parameter) 
303             * will be converted to a url-encoded form string unless a different 
304             * <code>requestContentType</code> named parameter is passed to this method.
305             *  (See {@link EncoderRegistry#encodeForm(Map)}.) </p>
306             * 
307             * @param args see {@link SendDelegate#setPropertiesFromMap(Map)}
308             * @param responseClosure code to handle a successful HTTP response
309             * @return any value returned by the response closure.
310             * @throws ClientProtocolException
311             * @throws IOException
312             * @throws URISyntaxException
313             */
314            public Object post( Map<String,?> args, Closure responseClosure ) 
315                            throws URISyntaxException, ClientProtocolException, IOException {
316                    SendDelegate delegate = new SendDelegate( new HttpPost(),
317                                    this.defaultContentType, 
318                                    this.defaultRequestHeaders,
319                                    this.defaultResponseHandlers );
320                    
321                    /* by default assume the request body will be URLEncoded, but allow
322                       the 'requestContentType' named argument to override this if it is 
323                       given */ 
324                    delegate.setRequestContentType( ContentType.URLENC.toString() );
325                    delegate.setPropertiesFromMap( args );
326                    
327                    if ( responseClosure != null ) delegate.getResponse().put( 
328                                    Status.SUCCESS.toString(), responseClosure );
329    
330                    return this.doRequest( delegate );
331            }
332            
333            /**
334             * Make an HTTP request to the default URL and content-type.
335             * @see #request(Object, Method, Object, Closure)
336             * @param method {@link Method HTTP method}
337             * @param contentType either a {@link ContentType} or valid content-type string.
338             * @param configClosure request configuration options
339             * @return whatever value was returned by the executed response handler.
340             * @throws ClientProtocolException
341             * @throws IOException
342             */
343            public Object request( Method m, Closure configClosure ) throws ClientProtocolException, IOException {
344                    return this.doRequest( this.defaultURI, m, this.defaultContentType, configClosure );
345            }
346    
347            /**
348             * Make an HTTP request using the default URL, with the given method, 
349             * content-type, and configuration.
350             * @see #request(Object, Method, Object, Closure)
351             * @param method {@link Method HTTP method}
352             * @param contentType either a {@link ContentType} or valid content-type string.
353             * @param configClosure request configuration options
354             * @return whatever value was returned by the executed response handler.
355             * @throws ClientProtocolException
356             * @throws IOException
357             */
358            public Object request( Method m, Object contentType, Closure configClosure ) 
359                            throws ClientProtocolException, IOException {
360                    return this.doRequest( this.defaultURI, m, contentType, configClosure );
361            }
362    
363            /**
364             * Make a request for the given HTTP method and content-type, with 
365             * additional options configured in the <code>configClosure</code>.  See
366             * {@link SendDelegate} for options.
367             * @param uri either a URI, URL, or String
368             * @param method {@link Method HTTP method}
369             * @param contentType either a {@link ContentType} or valid content-type string.
370             * @param configClosure closure from which to configure options like 
371             *   {@link SendDelegate#getURL() url.path}, 
372             *   {@link URIBuilder#setQuery(Map) request parameters}, 
373             *   {@link SendDelegate#setHeaders(Map) headers},
374             *   {@link SendDelegate#setBody(Object) request body} and
375             *   {@link SendDelegate#getResponse() response handlers}. 
376             *   
377             * @return whatever value was returned by the executed response handler.
378             * @throws ClientProtocolException
379             * @throws IOException
380             * @throws URISyntaxException if a URI string or URL was invalid.
381             */
382            public Object request( Object uri, Method method, Object contentType, Closure configClosure ) 
383                            throws ClientProtocolException, IOException, URISyntaxException {
384                    return this.doRequest( convertToURI( uri ), method, contentType, configClosure );
385            }
386    
387            /**
388             * Create a {@link SendDelegate} from the given arguments, execute the 
389             * config closure, then pass the delegate to {@link #doRequest(SendDelegate)},
390             * which actually executes the request.
391             */
392            protected Object doRequest( URI uri, Method method, Object contentType, Closure configClosure ) 
393                            throws ClientProtocolException, IOException {
394    
395                    HttpRequestBase reqMethod;
396                    try { reqMethod = method.getRequestType().newInstance();
397                    // this exception should reasonably never occur:
398                    } catch ( Exception e ) { throw new RuntimeException( e ); }
399    
400                    reqMethod.setURI( uri );
401                    SendDelegate delegate = new SendDelegate( reqMethod, contentType, 
402                                    this.defaultRequestHeaders,
403                                    this.defaultResponseHandlers );         
404                    configClosure.setDelegate( delegate );
405                    configClosure.call( client );           
406    
407                    return this.doRequest( delegate );
408            }
409            
410            /**
411             * All <code>request</code> methods delegate to this method.
412             */
413            protected Object doRequest( final SendDelegate delegate ) 
414                            throws ClientProtocolException, IOException {
415                    
416                    final HttpRequestBase reqMethod = delegate.getRequest();
417                    
418                    Object contentType = delegate.getContentType();
419                    String acceptContentTypes = contentType.toString();
420                    if ( contentType instanceof ContentType ) 
421                            acceptContentTypes = ((ContentType)contentType).getAcceptHeader();
422                    
423                    reqMethod.setHeader( "Accept", acceptContentTypes );
424                    reqMethod.setURI( delegate.getURL().toURI() );
425    
426                    // set any request headers from the delegate
427                    Map<String,String> headers = delegate.getHeaders(); 
428                    for ( String key : headers.keySet() ) {
429                            String val = headers.get( key );
430                            if ( val == null ) reqMethod.removeHeaders( key ); 
431                            else reqMethod.setHeader( key, val );
432                    }
433                    
434                    HttpResponse resp = client.execute( reqMethod );
435                    int status = resp.getStatusLine().getStatusCode();
436                    Closure responseClosure = delegate.findResponseHandler( status );
437                    log.debug( "Response code: " + status + "; found handler: " + responseClosure );
438                    
439                    Object[] closureArgs = null;
440                    switch ( responseClosure.getMaximumNumberOfParameters() ) {
441                    case 1 :
442                            closureArgs = new Object[] { resp };
443                            break;
444                    case 2 :
445                            // For HEAD or DELETE requests, there should be no response entity.
446                            if ( resp.getEntity() == null ) {
447                                    log.warn( "Response contains no entity, but response closure " +
448                                                    "expects parsed data.  Passing null as second closure arg." );
449                                    closureArgs = new Object[] { resp, null };
450                                    break;
451                            }
452                            
453                            // Otherwise, parse the response entity:
454                            
455                            // first, start with the _given_ content-type
456                            String responseContentType = contentType.toString();
457                            // if the given content-type is ANY ("*/*") then use the response content-type
458                            if ( ContentType.ANY.toString().equals( responseContentType ) )
459                                    responseContentType = ParserRegistry.getContentType( resp );
460                            
461                            Object parsedData = parsers.get( responseContentType ).call( resp );
462                            if ( parsedData == null ) log.warn( "Parsed data is null!!!" );
463                            else log.debug( "Parsed data from content-type '" + responseContentType 
464                                            + "' to object: " + parsedData.getClass() );
465                            closureArgs = new Object[] { resp, parsedData };
466                            break;
467                    default:
468                            throw new IllegalArgumentException( 
469                                            "Response closure must accept one or two parameters" );
470                    }
471                    
472                    Object returnVal = responseClosure.call( closureArgs );
473                    log.debug( "response handler result: " + returnVal );
474                    
475                    HttpEntity responseContent = resp.getEntity(); 
476                    if ( responseContent != null && responseContent.isStreaming() ) 
477                            responseContent.consumeContent();
478                    return returnVal;
479            }
480            
481            protected Map<String,Closure> buildDefaultResponseHandlers() {
482                    Map<String,Closure> map = new HashMap<String, Closure>();
483                    map.put( Status.SUCCESS.toString(), 
484                                    new MethodClosure(this,"defaultSuccessHandler"));
485                    map.put(  Status.FAILURE.toString(),
486                                    new MethodClosure(this,"defaultFailureHandler"));
487                    
488                    return map;
489            }
490    
491            /**
492             * <p>This is the default <code>response.success</code> handler.  It will be 
493             * executed if the response is not handled by a status-code-specific handler 
494             * (i.e. <code>response.'200'= {..}</code>) and no generic 'success' handler 
495             * is given (i.e. <code>response.success = {..}</code>.)  This handler simply 
496             * returns the parsed data from the response body.  In most cases you will 
497             * probably want to define a <code>response.success = {...}</code> handler 
498             * from the request closure, which will replace the response handler defined 
499             * by this method.  </p>
500             * 
501             * <h4>Note for parsers that return streaming content:</h4>
502             * <p>For responses parsed as {@link ParserRegistry#parseStream(HttpResponse) 
503             * BINARY} or {@link ParserRegistry#parseText(HttpResponse) TEXT}, the 
504             * parser will return streaming content -- an <code>InputStream</code> or 
505             * <code>Reader</code>.  In these cases, this handler will buffer the the 
506             * response content before the network connection is closed.  </p>
507             * 
508             * <p>In practice, a user-supplied response handler closure is 
509             * <i>designed</i> to handle streaming content so it can be read directly from 
510             * the response stream without buffering, which will be much more efficient.
511             * Therefore, it is recommended that request method variants be used which 
512             * explicitly accept a response handler closure in these cases.</p>
513             *  
514             * @param resp HTTP response
515             * @param parsedData parsed data as resolved from this instance's {@link ParserRegistry}
516             * @return the parsed data object (whatever the parser returns).
517             */
518            protected Object defaultSuccessHandler( HttpResponse resp, Object parsedData ) throws IOException {
519                    //If response is streaming, buffer it in a byte array:
520                    if ( parsedData instanceof InputStream ) {
521                            ByteArrayOutputStream buffer = new ByteArrayOutputStream();
522                            DefaultGroovyMethods.leftShift( buffer, (InputStream)parsedData );
523                            parsedData = new ByteArrayInputStream( buffer.toByteArray() );
524                    }
525                    else if ( parsedData instanceof Reader ) {
526                            StringWriter buffer = new StringWriter();
527                            DefaultGroovyMethods.leftShift( buffer, (Reader)parsedData );
528                            parsedData = new StringReader( buffer.toString() );
529                    }
530                    else if ( parsedData instanceof Closeable )
531                            log.warn( "Parsed data is streaming, but will be accessible after " +
532                                            "the network connection is closed.  Use at your own risk!" );
533                    return parsedData;
534            }
535            
536            /**
537             * This is the default <code>response.failure</code> handler.  It will be 
538             * executed if no status-code-specific handler is set (i.e. 
539             * <code>response.'404'= {..}</code>).  This default handler will throw a 
540             * {@link HttpResponseException} when executed.  In most cases you
541             * will want to define your own <code>response.failure = {...}</code> 
542             * handler from the request closure, if you don't want an exception to be 
543             * thrown for a 4xx and 5xx status response.   
544    
545             * @param resp
546             * @throws HttpResponseException
547             */
548            protected void defaultFailureHandler( HttpResponse resp ) throws HttpResponseException {
549                    throw new HttpResponseException( resp.getStatusLine().getStatusCode(), 
550                                    resp.getStatusLine().getReasonPhrase() );
551            }
552            
553            /**
554             * Retrieve the map of response code handlers.  Each map key is a response 
555             * code as a string (i.e. '401') or either 'success' or 'failure'.  Use this
556             * to set default response handlers, e.g.
557             * <pre>builder.handler.'401' = { resp -> println "${resp.statusLine}" }</pre>
558             * @see Status 
559             * @return
560             */
561            public Map<String,Closure> getHandler() {
562                    return this.defaultResponseHandlers;
563            }
564            
565            /**
566             * Retrieve the map of registered response content-type parsers.  Use 
567             * this to set default response parsers, e.g.
568             * <pre>
569             * builder.parser.'text/javascript' = { resp -> 
570             *        return resp.entity.content // just returns an InputStream
571             * }</pre>  
572             * @return
573             */
574            public Map<String,Closure> getParser() {
575                    return this.parsers.registeredParsers;
576            }
577            
578            /**
579             * Retrieve the map of registered request content-type encoders.  Use this
580             * to set a default request encoder, e.g.
581             * <pre>
582             * builder.encoder.'text/javascript' = { body ->
583             *   def json = body.call( new JsonGroovyBuilder() )
584             *   return new StringEntity( json.toString() )
585             * } 
586             * @return
587             */
588            public Map<String,Closure> getEncoder() {
589                    return this.encoders.registeredEncoders;
590            }
591            
592            /**
593             * Set the default content type that will be used to select the appropriate
594             * request encoder and response parser.  The {@link ContentType} enum holds
595             * some common content-types that may be used, i.e. <pre>
596             * import static ContentType.*
597             * builder.contentType = XML
598             * </pre> 
599             * @see EncoderRegistry
600             * @see ParserRegistry
601             * @param ct either a {@link ContentType} or string value (i.e. <code>"text/xml"</code>.)
602             */
603            public void setContentType( Object ct ) {
604                    this.defaultContentType = ct;
605            }
606            
607            
608            /**
609             * Set acceptable request and response content-encodings. 
610             * @see ContentEncodingRegistry
611             * @param encodings each Object should be either a 
612             * {@link ContentEncoding.Type} value, or a <code>content-encoding</code> 
613             * string that is known by the {@link ContentEncodingRegistry}
614             */
615            public void setContentEncoding( Object... encodings ) {
616                    this.contentEncodingHandler.setInterceptors( client, encodings );
617            }
618            
619            /**
620             * Set the default URL used for requests that do not explicitly take a 
621             * <code>url</code> param.  
622             * @param url a URL, URI, or String
623             * @throws URISyntaxException
624             */
625            public void setURL( Object url ) throws URISyntaxException {
626                    this.defaultURI = convertToURI( url );
627            }
628            
629            /**
630             * Get the default URL used for requests that do not explicitly take a 
631             * <code>url</code> param.
632             * @return url a {@link URL} instance.  Note that the return type is Object
633             * simply so that it matches with its JavaBean {@link #setURL(Object)} 
634             * counterpart.
635             */
636            public Object getURL() {
637                    try {
638                            return defaultURI.toURL();
639                    } catch ( MalformedURLException e ) {
640                            throw new RuntimeException( e );
641                    }
642            }
643    
644            /**
645             * Set the default headers to add to all requests made by this builder 
646             * instance.  These values will replace any previously set default headers.
647             * @param headers map of header names & values.
648             */
649            public void setHeaders( Map<?,?> headers ) {
650                    this.defaultRequestHeaders.clear();
651                    if ( headers == null ) return;
652                    for( Object key : headers.keySet() ) {
653                            Object val = headers.get( key );
654                            if ( val == null ) continue;
655                            this.defaultRequestHeaders.put( key.toString(), val.toString() );
656                    }
657            }
658            
659            /**
660             * Get the map of default headers that will be added to all requests.
661             * This is a 'live' collection so it may be used to add or remove default 
662             * values. 
663             * @return the map of default header names and values.
664             */
665            public Map<String,String> getHeaders() {
666                    return this.defaultRequestHeaders;
667            }
668    
669            /**
670             * Return the underlying HTTPClient that is used to handle HTTP requests.
671             * @return the client instance.
672             */
673            public AbstractHttpClient getClient() { return this.client; }
674            
675            /**
676             * Used to access the {@link AuthConfig} handler used to configure common 
677             * authentication mechanism.  Example:
678             * <pre>builder.auth.basic( 'myUser', 'somePassword' )</pre>
679             * @return
680             */
681            public AuthConfig getAuth() { return this.auth; }
682            
683            /**
684             * Set an alternative {@link AuthConfig} implementation to handle 
685             * authorization.
686             * @param ac instance to use. 
687             */
688            public void setAuthConfig( AuthConfig ac ) {
689                    this.auth = ac;
690            }
691            
692            /**
693             * Set a custom registry used to handle different request 
694             * <code>content-type</code>s.
695             * @param er
696             */
697            public void setEncoderRegistry( EncoderRegistry er ) {
698                    this.encoders = er;
699            }
700            
701            /**
702             * Set a custom registry used to handle different response 
703             * <code>content-type</code>s
704             * @param pr
705             */
706            public void setParserRegistry( ParserRegistry pr ) {
707                    this.parsers = pr;
708            }
709            
710            /**
711             * Set a custom registry used to handle different 
712             * <code>content-encoding</code> types in responses.  
713             * @param cer
714             */
715            public void setContentEncodingRegistry( ContentEncodingRegistry cer ) {
716                    this.contentEncodingHandler = cer;
717            }
718            
719            /**
720             * Set the default HTTP proxy to be used for all requests.
721             * @see HttpHost#HttpHost(String, int, String)
722             * @param host host name or IP
723             * @param port port, or -1 for the default port
724             * @param scheme usually "http" or "https," or <code>null</code> for the default
725             */
726            public void setProxy( String host, int port, String scheme ) {
727                    getClient().getParams().setParameter( 
728                                    ConnRoutePNames.DEFAULT_PROXY, 
729                                    new HttpHost(host,port,scheme) );
730            }
731            
732            /**
733             * Release any system resources held by this instance.
734             * @see ClientConnectionManager#shutdown()
735             */
736            public void shutdown() {
737                    client.getConnectionManager().shutdown();
738            }       
739    
740            
741            
742            /**
743             * Encloses all properties and method calls used within the 
744             * {@link HTTPBuilder#request(Object, Method, Object, Closure)} 'config' 
745             * closure argument. 
746             */
747            protected class SendDelegate {
748                    protected HttpRequestBase request;
749                    protected Object contentType;
750                    protected String requestContentType;
751                    protected Map<String,Closure> responseHandlers = new HashMap<String,Closure>();
752                    protected URIBuilder url;
753                    protected Map<String,String> headers = new HashMap<String,String>();
754                    
755                    public SendDelegate( HttpRequestBase request, Object contentType, 
756                                    Map<String,String> defaultRequestHeaders,
757                                    Map<String,Closure> defaultResponseHandlers ) {
758                            this.request = request;
759                            this.headers.putAll( defaultRequestHeaders );
760                            this.contentType = contentType;
761                            this.responseHandlers.putAll( defaultResponseHandlers );
762                            this.url = new URIBuilder(request.getURI());
763                    }
764                    
765                    /** 
766                     * Use this object to manipulate parts of the request URL, like 
767                     * query params and request path.  Example:
768                     * <pre>
769                     * builder.request(GET,XML) {
770                     *   url.path = '../other/request.jsp'
771                     *   url.params = [p1:1, p2:2]
772                     *   ...
773                     * }</pre>
774                     * @return {@link URIBuilder} to manipulate the request URL 
775                     */
776                    public URIBuilder getURL() { return this.url; }
777    
778                    protected HttpRequestBase getRequest() { return this.request; }
779                    
780                    /**
781                     * Get the content-type of any data sent in the request body and the 
782                     * expected response content-type.
783                     * @return whatever value was assigned via {@link #setContentType(Object)}
784                     * or passed from the {@link HTTPBuilder#defaultContentType} when this
785                     * SendDelegateinstance was constructed.
786                     */
787                    protected Object getContentType() { return this.contentType; }
788                    
789                    /**
790                     * Set the content-type used for any data in the request body, as well
791                     * as the <code>Accept</code> content-type that will be used for parsing
792                     * the response. The value should be either a {@link ContentType} value 
793                     * or a String, i.e. <code>"text/plain"</code>
794                     * @param ct content-type to send and recieve content
795                     */
796                    protected void setContentType( Object ct ) {
797                            if ( ct == null ) this.contentType = defaultContentType;
798                            this.contentType = ct; 
799                    }
800                    
801                    /**
802                     * The request content-type, if different from the {@link #contentType}.
803                     * @return
804                     */
805                    protected String getRequestContentType() {
806                            if ( this.requestContentType != null ) return this.requestContentType;
807                            else return this.getContentType().toString();
808                    }
809                    
810                    /**
811                     * Assign a different content-type for the request than is expected for 
812                     * the response.  This is useful if i.e. you want to post URL-encoded
813                     * form data but expect the response to be XML or HTML.  The 
814                     * {@link #getContentType()} will always control the <code>Accept</code>
815                     * header, and will be used for the request content <i>unless</i> this 
816                     * value is also explicitly set.
817                     * @param ct either a {@link ContentType} value or a valid content-type
818                     * String.
819                     */
820                    protected void setRequestContentType( String ct ) { 
821                            this.requestContentType = ct; 
822                    }
823                    
824                    /**
825                     * Valid arguments:
826                     * <dl>
827                     *   <dt>url</dt><dd>Either a URI, URL, or String. 
828                     *      If not supplied, the HTTPBuilder's default URL is used.</dd>
829                     *   <dt>path</dt><dd>Request path that is merged with the URL</dd>
830                     *   <dt>params</dt><dd>Map of request parameters</dd>
831                     *   <dt>headers</dt><dd>Map of HTTP headers</dd>
832                     *   <dt>contentType</dt><dd>Request content type and Accept header.  
833                     *      If not supplied, the HTTPBuilder's default content-type is used.</dd>
834                     *   <dt>requestContentType</dt><dd>content type for the request, if it
835                     *      is different from the expected response content-type</dd>
836                     *   <dt>body</dt><dd>Request body that will be encoded based on the given contentType</dd>
837                     * </dl>
838                     * @param args named parameters to set properties on this delegate.
839                     * @throws MalformedURLException
840                     * @throws URISyntaxException
841                     */
842                    @SuppressWarnings("unchecked")
843                    protected void setPropertiesFromMap( Map<String,?> args ) throws MalformedURLException, URISyntaxException {
844                            Object uri = args.get( "url" );
845                            if ( uri == null ) uri = defaultURI;
846                            url = new URIBuilder( convertToURI( uri ) );
847                            
848                            Map params = (Map)args.get( "params" );
849                            if ( params != null ) this.url.setQuery( params );
850                            Map headers = (Map)args.get( "headers" );
851                            if ( headers != null ) this.getHeaders().putAll( headers );
852                            
853                            Object path = args.get( "path" );
854                            if ( path != null ) this.url.setPath( path.toString() );
855                            
856                            Object contentType = args.get( "contentType" );
857                            if ( contentType != null ) this.setContentType( contentType );
858                            
859                            contentType = args.get( "requestContentType" );
860                            if ( contentType != null ) this.setRequestContentType( contentType.toString() );
861                            
862                            Object body = args.get("body");
863                            if ( body != null ) this.setBody( body );
864                    }
865    
866                    /**
867                     * Set request headers.  These values will be <strong>merged</strong>
868                     * with any {@link HTTPBuilder#getHeaders() default request headers.} 
869                     * (The assumption is you'll probably want to add a bunch of headers to 
870                     * whatever defaults you've already set).  If you <i>only</i> want to 
871                     * use values set here, simply call {@link #getHeaders() headers.clear()}
872                     * first.
873                     */
874                    public void setHeaders( Map<?,?> newHeaders ) {
875                            for( Object key : newHeaders.keySet() ) {
876                                    Object val = newHeaders.get( key );
877                                    if ( val == null ) this.headers.remove( key );
878                                    else this.headers.put( key.toString(), val.toString() );
879                            }
880                    }
881                    
882                    /**
883                     * Get request headers (including any default headers).  Note that this
884                     * will not include any <code>Accept</code>, <code>Content-Type</code>,
885                     * or <code>Content-Encoding</code> headers that are automatically
886                     * handled by any encoder or parsers in effect.  Note that any values 
887                     * set here <i>will</i> override any of those automatically assigned 
888                     * values.
889                     * header that is a
890                     * @return
891                     */
892                    public Map<String,String> getHeaders() {
893                            return this.headers;
894                    }
895                    
896                    /**
897                     * Convenience method to set a request content-type at the same time
898                     * the request body is set.  This is a variation of 
899                     * {@link #setBody(Object)} that allows for a different content-type
900                     * than what is expected for the response.  
901                     * 
902                     * <p>Example:    
903                     * <pre>
904                     * http.request(POST,HTML) {
905                     *   
906                     *   /* request data is interpreted as a JsonBuilder closure in the 
907                     *      default EncoderRegistry implementation * /
908                     *   send( 'text/javascript' ) {  
909                     *     a : ['one','two','three']
910                     *   }
911                     *   
912                     *   // response content-type is what was specified in the outer request() argument:
913                     *   response.success = { resp, html -> 
914                     *   
915                     *   }
916                     * }
917                     * </pre>
918                     * @param contentType either a {@link ContentType} or content-type 
919                     *      string like <code>"text/xml"</code>
920                     * @param requestBody
921                     */
922                    public void send( Object contentType, Object requestBody ) {
923                            this.setRequestContentType( contentType.toString() );
924                            this.setBody( requestBody );
925                    }
926    
927                    /**
928                     * Set the request body.  This value may be of any type supported by 
929                     * the associated {@link EncoderRegistry request encoder}.  
930                     * @see #send(Object, Object)
931                     * @param body data or closure interpretes as the request body
932                     */
933                    public void setBody( Object body ) {
934                            if ( ! (request instanceof HttpEntityEnclosingRequest ) )
935                                    throw new UnsupportedOperationException( 
936                                                    "Cannot set a request body for a " + request.getMethod() + " method" );
937                            Closure encoder = encoders.get( this.getRequestContentType() );
938                            HttpEntity entity = (HttpEntity)encoder.call( body );
939                            
940                            ((HttpEntityEnclosingRequest)request).setEntity( entity );
941                    }
942                    
943                    /**
944                     * Get the proper response handler for the response code.  This is called
945                     * by the {@link HTTPBuilder} class in order to find the proper handler
946                     * based on the response status code.
947                     *  
948                     * @param statusCode HTTP response status code
949                     * @return the response handler
950                     */
951                    protected Closure findResponseHandler( int statusCode ) {
952                            Closure handler = this.getResponse().get( Integer.toString( statusCode ) );
953                            if ( handler == null ) handler = 
954                                    this.getResponse().get( Status.find( statusCode ).toString() );
955                            return handler;
956                    }
957                    
958                    /**
959                     * Access the response handler map to set response parsing logic.  
960                     * i.e.<pre>
961                     * builder.request( GET, XML ) {
962                     *   response.success = { xml ->
963                     *      /* for XML content type, the default parser 
964                     *         will return an XmlSlurper * /
965                     *      xml.root.children().each { println it } 
966                     *   }
967                     * }</pre>
968                     * @return
969                     */
970                    public Map<String,Closure> getResponse() { return this.responseHandlers; }
971            }
972    }