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