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