Coverage Report - groovyx.net.http.HTTPBuilder
 
Classes in this File Line Coverage Branch Coverage Complexity
HTTPBuilder
62%
62/100
47%
7/15
0
HTTPBuilder$SendDelegate
60%
33/55
35%
9/26
0
 
 1  
 /*
 2  
  * Copyright 2003-2008 the original author or authors.
 3  
  *
 4  
  * Licensed under the Apache License, Version 2.0 (the "License");
 5  
  * you may not use this file except in compliance with the License.
 6  
  * You may obtain a copy of the License at
 7  
  *
 8  
  *     http://www.apache.org/licenses/LICENSE-2.0
 9  
  *
 10  
  * Unless required by applicable law or agreed to in writing, software
 11  
  * distributed under the License is distributed on an "AS IS" BASIS,
 12  
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 13  
  * See the License for the specific language governing permissions and
 14  
  * limitations under the License.
 15  
  *
 16  
  * You are receiving this code free of charge, which represents many hours of
 17  
  * effort from other individuals and corporations.  As a responsible member 
 18  
  * of the community, you are asked (but not required) to donate any 
 19  
  * enhancements or improvements back to the community under a similar open 
 20  
  * source license.  Thank you. -TMN
 21  
  */
 22  
 package groovyx.net.http;
 23  
 
 24  
 import groovy.lang.Closure;
 25  
 
 26  
 import java.io.IOException;
 27  
 import java.net.MalformedURLException;
 28  
 import java.net.URI;
 29  
 import java.net.URISyntaxException;
 30  
 import java.net.URL;
 31  
 import java.util.HashMap;
 32  
 import java.util.Map;
 33  
 
 34  
 import org.apache.http.HttpEntity;
 35  
 import org.apache.http.HttpEntityEnclosingRequest;
 36  
 import org.apache.http.HttpResponse;
 37  
 import org.apache.http.client.ClientProtocolException;
 38  
 import org.apache.http.client.HttpResponseException;
 39  
 import org.apache.http.client.methods.HttpGet;
 40  
 import org.apache.http.client.methods.HttpPost;
 41  
 import org.apache.http.client.methods.HttpRequestBase;
 42  
 import org.apache.http.impl.client.AbstractHttpClient;
 43  
 import org.apache.http.impl.client.DefaultHttpClient;
 44  
 import org.codehaus.groovy.runtime.DefaultGroovyMethods;
 45  
 import org.codehaus.groovy.runtime.MethodClosure;
 46  
 
 47  
 import static groovyx.net.http.URIBuilder.convertToURI;
 48  
 
 49  
 /** <p>
 50  
  * Groovy DSL for easily making HTTP requests, and handling request and response
 51  
  * data.  This class adds a number of convenience mechanisms built on top of 
 52  
  * Apache HTTPClient for things like URL-encoded POSTs and REST requests that 
 53  
  * require building and parsing JSON or XML.  Convenient access to a few common
 54  
  * authentication methods is also available.</p>
 55  
  * 
 56  
  * <h3>Examples:</h3>
 57  
  * Perform an HTTP GET and print the response:
 58  
  * <pre>
 59  
  *   def http = new HTTPBuilder('http://www.google.com')
 60  
  *   
 61  
  *   http.get( path:'/search', query:[q:'Groovy'] ) { resp, reader ->
 62  
  *     println "response status: ${resp.statusLine}"
 63  
  *     println 'Response data: -----'
 64  
  *     System.out << reader
 65  
  *     println '\n--------------------'
 66  
  *   }
 67  
  * </pre>
 68  
  *   
 69  
  * Long form for other HTTP methods, and response-code-specific handlers:
 70  
  *   
 71  
  * <pre>
 72  
  *   http.request(GET,TEXT) { req ->
 73  
  *     response.success = { resp, stream ->
 74  
  *       println 'my response handler!'
 75  
  *       assert resp.statusLine.statusCode == 200
 76  
  *       println resp.statusLine
 77  
  *       //System.out << stream // print response stream
 78  
  *     }
 79  
  *     
 80  
  *     response.'401' = { resp ->
 81  
  *       println 'access denied'
 82  
  *     }
 83  
  *   }
 84  
  * </pre>
 85  
  *   
 86  
  * You can also set a default response handler called for any status
 87  
  * code > 399 that is not matched to a specific handler. Setting the value
 88  
  * outside a request closure means it will apply to all future requests with
 89  
  * this HTTPBuilder instance:
 90  
  * <pre>
 91  
  *   http.handler.failure = { resp ->
 92  
  *     println "Unexpected failure: ${resp.statusLine}"
 93  
  *   }
 94  
  * </pre>
 95  
  *   
 96  
  *   
 97  
  * And...  Automatic response parsing for registered content types!
 98  
  *   
 99  
  * <pre>
 100  
  *   http.request( 'http://ajax.googleapis.com', GET, JSON ) {
 101  
  *     url.path = '/ajax/services/search/web'
 102  
  *     url.query = [ v:'1.0', q: 'Calvin and Hobbes' ]
 103  
  *     
 104  
  *     response.success = { resp, json ->
 105  
  *       assert json.size() == 3
 106  
  *       println "Query response: "
 107  
  *       json.responseData.results.each {
 108  
  *         println "  ${it.titleNoFormatting} : ${it.visibleUrl}"
 109  
  *       }
 110  
  *     }
 111  
  *   }
 112  
  * </pre>
 113  
  * 
 114  
  * @author <a href='mailto:tnichols@enernoc.com'>Tom Nichols</a>
 115  
  */
 116  
 public class HTTPBuilder {
 117  
         
 118  5
         protected AbstractHttpClient client = new DefaultHttpClient();        
 119  5
         protected URI defaultURI = null; // TODO make this a URIBuilder?
 120  5
         protected AuthConfig auth = new AuthConfig( this );
 121  
         
 122  5
         protected Object defaultContentType = ContentType.TEXT;
 123  5
         protected final Map<String,Closure> defaultResponseHandlers = buildDefaultResponseHandlers();
 124  5
         protected ContentEncodingRegistry contentEncodingHandler = new ContentEncodingRegistry();
 125  
         
 126  5
         protected final Map<String,String> defaultRequestHeaders = new HashMap<String,String>();
 127  
         
 128  5
         protected EncoderRegistry encoders = new EncoderRegistry();
 129  5
         protected ParserRegistry parsers = new ParserRegistry();
 130  
         
 131  
         public HTTPBuilder() { 
 132  5
                 super();
 133  5
                 this.setContentEncoding( ContentEncoding.Type.GZIP, 
 134  
                                 ContentEncoding.Type.DEFLATE );
 135  5
         }
 136  
         
 137  
         /**
 138  
          * Give a default URL to be used for all request methods that don't 
 139  
          * explicitly take a URL parameter.
 140  
          * @param defaultURL either a {@link URL}, {@link URI} or String
 141  
          * @throws URISyntaxException if the URL was not parse-able
 142  
          */
 143  
         public HTTPBuilder( Object defaultURL ) throws URISyntaxException {
 144  2
                 this();
 145  2
                 this.defaultURI = convertToURI( defaultURL );
 146  2
         }
 147  
         
 148  
         /**
 149  
          * Give a default URL to be used for all request methods that don't 
 150  
          * explicitly take a URL parameter, and a default content-type to be used
 151  
          * for request encoding and response parsing.
 152  
          * @param defaultURL either a {@link URL}, {@link URI} or String
 153  
          * @param defaultContentType content-type string.  See {@link ContentType}
 154  
          *   for common types.
 155  
          * @throws URISyntaxException if the URL was not parse-able
 156  
          */
 157  
         public HTTPBuilder( Object defaultURL, Object defaultContentType ) throws URISyntaxException {
 158  0
                 this();
 159  0
                 this.defaultURI = convertToURI( defaultURL );
 160  0
                 this.defaultContentType = defaultContentType; 
 161  0
         }
 162  
         
 163  
         /**
 164  
          * Convenience method to perform an HTTP GET.  The response closure will be 
 165  
          * called only on a successful response; a 'failed' response (i.e. any 
 166  
          * HTTP status code > 399) will be handled by the registered 'failure' 
 167  
          * handler.  The {@link #defaultFailureHandler(HttpResponse) default 
 168  
          * failure handler} throws an {@link HttpResponseException}
 169  
          * @param args see {@link SendDelegate#setPropertiesFromMap(Map)}
 170  
          * @param responseClosure code to handle a successful HTTP response
 171  
          * @return any value returned by the response closure.
 172  
          * @throws ClientProtocolException
 173  
          * @throws IOException
 174  
          * @throws URISyntaxException
 175  
          */
 176  
         public Object get( Map<String,?> args, Closure responseClosure ) 
 177  
                         throws ClientProtocolException, IOException, URISyntaxException {
 178  1
                 SendDelegate delegate = new SendDelegate( new HttpGet(),
 179  
                                 this.defaultContentType,
 180  
                                 this.defaultRequestHeaders,
 181  
                                 this.defaultResponseHandlers );
 182  
                 
 183  1
                 delegate.setPropertiesFromMap( args );                
 184  1
                 delegate.getResponse().put( Status.SUCCESS.toString(), responseClosure );
 185  1
                 return this.doRequest( delegate );
 186  
         }
 187  
         
 188  
         /** <p>
 189  
          * Convenience method to perform an HTTP form POST.  The response closure will be 
 190  
          * called only on a successful response; a 'failed' response (i.e. any 
 191  
          * HTTP status code > 399) will be handled by the registered 'failure' 
 192  
          * handler.  The {@link #defaultFailureHandler(HttpResponse) default 
 193  
          * failure handler} throws an {@link HttpResponseException}.</p>  
 194  
          * 
 195  
          * <p>The request body (specified by a <code>body</code> named parameter) 
 196  
          * will be converted to a url-encoded form string unless a different 
 197  
          * <code>requestContentType</code> named parameter is passed to this method.
 198  
          *  (See {@link EncoderRegistry#encodeForm(Map)}.) </p>
 199  
          * 
 200  
          * @param args see {@link SendDelegate#setPropertiesFromMap(Map)}
 201  
          * @param responseClosure code to handle a successful HTTP response
 202  
          * @return any value returned by the response closure.
 203  
          * @throws ClientProtocolException
 204  
          * @throws IOException
 205  
          * @throws URISyntaxException
 206  
          */
 207  
         public Object post( Map<String,?> args, Closure responseClosure ) 
 208  
                         throws URISyntaxException, ClientProtocolException, IOException {
 209  0
                 SendDelegate delegate = new SendDelegate( new HttpPost(),
 210  
                                 this.defaultContentType, 
 211  
                                 this.defaultRequestHeaders,
 212  
                                 this.defaultResponseHandlers );
 213  
                 
 214  
                 /* by default assume the request body will be URLEncoded, but allow
 215  
                    the 'requestContentType' named argument to override this if it is 
 216  
                    given */ 
 217  0
                 delegate.setRequestContentType( ContentType.URLENC.toString() );
 218  0
                 delegate.setPropertiesFromMap( args );                
 219  0
                 delegate.getResponse().put( Status.SUCCESS.toString(), responseClosure );
 220  
 
 221  0
                 return this.doRequest( delegate );
 222  
         }
 223  
         
 224  
         public Object request( Method m, Closure configClosure ) throws ClientProtocolException, IOException {
 225  0
                 return this.doRequest( this.defaultURI, m, this.defaultContentType, configClosure );
 226  
         }
 227  
 
 228  
         public Object request( Method m, Object contentType, Closure configClosure ) 
 229  
                         throws ClientProtocolException, IOException {
 230  1
                 return this.doRequest( this.defaultURI, m, contentType, configClosure );
 231  
         }
 232  
 
 233  
         /**
 234  
          * Make a request for the given HTTP method and content-type, with 
 235  
          * additional options configured in the <code>configClosure</code>.  See
 236  
          * {@link SendDelegate} for options.
 237  
          * @param uri either a URI, URL, or String
 238  
          * @param method {@link Method HTTP method}
 239  
          * @param contentType either a {@link ContentType} or valid content-type string.
 240  
          * @param configClosure closure from which to configure options like 
 241  
          *   {@link SendDelegate#getURL() url.path}, 
 242  
          *   {@link URIBuilder#setQuery(Map) request parameters}, 
 243  
          *   {@link SendDelegate#setHeaders(Map) headers},
 244  
          *   {@link SendDelegate#setBody(Object) request body} and
 245  
          *   {@link SendDelegate#getResponse() response handlers}. 
 246  
          *   
 247  
          * @return whatever value was returned by the executed response handler.
 248  
          * @throws IllegalAccessException
 249  
          * @throws InstantiationException
 250  
          * @throws ClientProtocolException
 251  
          * @throws IOException
 252  
          * @throws URISyntaxException if a URI string or URL was invalid.
 253  
          */
 254  
         public Object request( Object uri, Method method, Object contentType, Closure configClosure ) 
 255  
                         throws ClientProtocolException, IOException, URISyntaxException {
 256  2
                 return this.doRequest( convertToURI( uri ), method, contentType, configClosure );
 257  
         }
 258  
 
 259  
         protected Object doRequest( URI uri, Method method, Object contentType, Closure configClosure ) 
 260  
                         throws ClientProtocolException, IOException {
 261  
 
 262  
                 HttpRequestBase reqMethod;
 263  3
                 try { reqMethod = method.getRequestType().newInstance();
 264  
                 // this exception should reasonably never occur:
 265  3
                 } catch ( Exception e ) { throw new RuntimeException( e ); }
 266  
 
 267  3
                 reqMethod.setURI( uri );
 268  3
                 SendDelegate delegate = new SendDelegate( reqMethod, contentType, 
 269  
                                 this.defaultRequestHeaders,
 270  
                                 this.defaultResponseHandlers );                
 271  3
                 configClosure.setDelegate( delegate );
 272  3
                 configClosure.call( client );                
 273  
 
 274  3
                 return this.doRequest( delegate );
 275  
         }
 276  
         
 277  
         protected Object doRequest( SendDelegate delegate ) throws ClientProtocolException, IOException {
 278  
                 
 279  4
                 HttpRequestBase reqMethod = delegate.getRequest();
 280  
                 
 281  4
                 Object contentType = delegate.getContentType();
 282  4
                 String acceptContentTypes = contentType.toString();
 283  4
                 if ( contentType instanceof ContentType ) 
 284  4
                         acceptContentTypes = ((ContentType)contentType).getAcceptHeader();
 285  
                 
 286  4
                 reqMethod.setHeader( "Accept", acceptContentTypes );
 287  4
                 reqMethod.setURI( delegate.getURL().toURI() );
 288  
 
 289  
                 // set any request headers from the delegate
 290  4
                 Map<String,String> headers = delegate.getHeaders(); 
 291  4
                 for ( String key : headers.keySet() ) reqMethod.setHeader( key, headers.get( key ) );
 292  
                 
 293  4
                 HttpResponse resp = client.execute( reqMethod );
 294  4
                 Closure responseClosure = delegate.findResponseHandler( 
 295  
                                 resp.getStatusLine().getStatusCode() );
 296  
                 
 297  4
                 Object[] closureArgs = null;
 298  4
                 switch ( responseClosure.getMaximumNumberOfParameters() ) {
 299  
                 case 1 :
 300  1
                         closureArgs = new Object[] { resp };
 301  1
                         break;
 302  
                 case 2 :
 303  3
                         String responseContentType = parsers.getContentType( resp );
 304  3
                         Object parsedData = parsers.get( responseContentType ).call( resp );                        
 305  3
                         closureArgs = new Object[] { resp, parsedData };
 306  3
                         break;
 307  
                 default:
 308  0
                         throw new IllegalArgumentException( 
 309  
                                         "Response closure must accept one or two parameters" );
 310  
                 }
 311  
                 
 312  4
                 Object returnVal = responseClosure.call( closureArgs );
 313  4
                 if ( resp.getEntity().isStreaming() ) resp.getEntity().consumeContent();
 314  4
                 return returnVal;
 315  
         }
 316  
         
 317  
         protected Map<String,Closure> buildDefaultResponseHandlers() {
 318  5
                 Map<String,Closure> map = new HashMap<String, Closure>();
 319  5
                 map.put( Status.SUCCESS.toString(), 
 320  
                                 new MethodClosure(this,"defaultSuccessHandler"));
 321  5
                 map.put(  Status.FAILURE.toString(),
 322  
                                 new MethodClosure(this,"defaultFailureHandler"));
 323  
                 
 324  5
                 return map;
 325  
         }
 326  
 
 327  
         /**
 328  
          * This is the default <code>response.success</code> handler.  It will be 
 329  
          * executed if no status-code-specific handler is set (i.e. 
 330  
          * <code>response.'200'= {..}</code>).  This simply prints the status line 
 331  
          * and the response stream to <code>System.out</code>.  In most cases you
 332  
          * will want to define a <code>response.success = {...}</code> handler from 
 333  
          * the request closure, which will replace this method.   
 334  
          *  
 335  
          * @param resp
 336  
          * @throws IllegalStateException
 337  
          * @throws IOException
 338  
          */
 339  
         protected void defaultSuccessHandler( HttpResponse resp ) throws IllegalStateException, IOException {
 340  0
                 System.out.println( resp.getStatusLine() );
 341  0
                 System.out.println( DefaultGroovyMethods.getText( resp.getEntity().getContent() ) ); 
 342  0
         }
 343  
         
 344  
         /**
 345  
          * This is the default <code>response.failure</code> handler.  It will be 
 346  
          * executed if no status-code-specific handler is set (i.e. 
 347  
          * <code>response.'404'= {..}</code>).  This default handler will throw a 
 348  
          * {@link HttpResponseException} when executed.  In most cases you
 349  
          * will want to define your own <code>response.failure = {...}</code> 
 350  
          * handler from the request closure, if you don't want an exception to be 
 351  
          * thrown for a 4xx and 5xx status response.   
 352  
 
 353  
          * @param resp
 354  
          * @throws HttpResponseException
 355  
          */
 356  
         protected void defaultFailureHandler( HttpResponse resp ) throws HttpResponseException {
 357  0
                 throw new HttpResponseException( resp.getStatusLine().getStatusCode(), 
 358  
                                 resp.getStatusLine().getReasonPhrase() );
 359  
         }
 360  
         
 361  
         /**
 362  
          * Retrieve the map of response code handlers.  Each map key is a response 
 363  
          * code as a string (i.e. '401') or either 'success' or 'failure'.  Use this
 364  
          * to set default response handlers, e.g.
 365  
          * <pre>builder.handler.'401' = { resp -> println "${resp.statusLine}" }</pre>
 366  
          * @see Status 
 367  
          * @return
 368  
          */
 369  
         public Map<String,Closure> getHandler() {
 370  2
                 return this.defaultResponseHandlers;
 371  
         }
 372  
         
 373  
         /**
 374  
          * Retrieve the map of registered response content-type parsers.  Use 
 375  
          * this to set default response parsers, e.g.
 376  
          * <pre>
 377  
          * builder.parser.'text/javascript' = { resp -> 
 378  
          *           return resp.entity.content // just returns an InputStream
 379  
          * }</pre>  
 380  
          * @return
 381  
          */
 382  
         public Map<String,Closure> getParser() {
 383  0
                 return this.parsers.registeredParsers;
 384  
         }
 385  
         
 386  
         /**
 387  
          * Retrieve the map of registered request content-type encoders.  Use this
 388  
          * to set a default request encoder, e.g.
 389  
          * <pre>
 390  
          * builder.encoder.'text/javascript' = { body ->
 391  
          *   def json = body.call( new JsonGroovyBuilder() )
 392  
          *   return new StringEntity( json.toString() )
 393  
          * } 
 394  
          * @return
 395  
          */
 396  
         public Map<String,Closure> getEncoder() {
 397  0
                 return this.encoders.registeredEncoders;
 398  
         }
 399  
         
 400  
         /**
 401  
          * Set the default content type that will be used to select the appropriate
 402  
          * request encoder and response parser.  The {@link ContentType} enum holds
 403  
          * some common content-types that may be used, i.e. <pre>
 404  
          * import static ContentType.*
 405  
          * builder.contentType = XML
 406  
          * </pre> 
 407  
          * @see EncoderRegistry
 408  
          * @see ParserRegistry
 409  
          * @param ct either a {@link ContentType} or string value (i.e. <code>"text/xml"</code>.)
 410  
          */
 411  
         public void setContentType( Object ct ) {
 412  0
                 this.defaultContentType = ct;
 413  0
         }
 414  
         
 415  
         
 416  
         /**
 417  
          * Set acceptable request and response content-encodings. 
 418  
          * @see ContentEncodingRegistry
 419  
          * @param encodings each Object should be either a 
 420  
          * {@link ContentEncoding.Type} value, or a <code>content-encoding</code> 
 421  
          * string that is known by the {@link ContentEncodingRegistry}
 422  
          */
 423  
         public void setContentEncoding( Object... encodings ) {
 424  5
                 this.contentEncodingHandler.setInterceptors( client, encodings );
 425  5
         }
 426  
         
 427  
         /**
 428  
          * Set the default URL used for requests that do not explicitly take a 
 429  
          * <code>url</code> param.  
 430  
          * @param url a URL, URI, or String
 431  
          * @throws URISyntaxException
 432  
          */
 433  
         public void setURL( Object url ) throws URISyntaxException {
 434  1
                 this.defaultURI = convertToURI( url );
 435  1
         }
 436  
         
 437  
         /**
 438  
          * Get the default URL used for requests that do not explicitly take a 
 439  
          * <code>url</code> param.
 440  
          * @return url a {@link URL} instance.  Note that the return type is Object
 441  
          * simply so that it matches with its JavaBean {@link #setURL(Object)} 
 442  
          * counterpart.
 443  
          */
 444  
         public Object getURL() {
 445  
                 try {
 446  1
                         return defaultURI.toURL();
 447  0
                 } catch ( MalformedURLException e ) {
 448  0
                         throw new RuntimeException( e );
 449  
                 }
 450  
         }
 451  
 
 452  
         /**
 453  
          * Set the default headers to add to all requests made by this builder 
 454  
          * instance.  These values will replace any previously set default headers.
 455  
          * @param headers map of header names & values.
 456  
          */
 457  
         public void setHeaders( Map<?,?> headers ) {
 458  0
                 this.defaultRequestHeaders.clear();
 459  0
                 if ( headers == null ) return;
 460  0
                 for( Object key : headers.keySet() ) {
 461  0
                         Object val = headers.get( key );
 462  0
                         if ( val == null ) continue;
 463  0
                         this.defaultRequestHeaders.put( key.toString(), val.toString() );
 464  0
                 }
 465  0
         }
 466  
         
 467  
         /**
 468  
          * Get the map of default headers that will be added to all requests.
 469  
          * This is a 'live' collection so it may be used to add or remove default 
 470  
          * values. 
 471  
          * @return the map of default header names and values.
 472  
          */
 473  
         public Map<String,String> getHeaders() {
 474  0
                 return this.defaultRequestHeaders;
 475  
         }
 476  
 
 477  
         /**
 478  
          * Return the underlying HTTPClient that is used to handle HTTP requests.
 479  
          * @return the client instance.
 480  
          */
 481  1
         public AbstractHttpClient getClient() { return this.client; }
 482  
         
 483  
         /**
 484  
          * Used to access the {@link AuthConfig} handler used to configure common 
 485  
          * authentication mechanism.  Example:
 486  
          * <pre>builder.auth.basic( 'myUser', 'somePassword' )</pre>
 487  
          * @return
 488  
          */
 489  1
         public AuthConfig getAuth() { return this.auth; }
 490  
         
 491  
         /**
 492  
          * Set an alternative {@link AuthConfig} implementation to handle 
 493  
          * authorization.
 494  
          * @param ac instance to use. 
 495  
          */
 496  
         public void setAuthConfig( AuthConfig ac ) {
 497  0
                 this.auth = ac;
 498  0
         }
 499  
         
 500  
         /**
 501  
          * Set a custom registry used to handle different request 
 502  
          * <code>content-type</code>s.
 503  
          * @param er
 504  
          */
 505  
         public void setEncoderRegistry( EncoderRegistry er ) {
 506  0
                 this.encoders = er;
 507  0
         }
 508  
         
 509  
         /**
 510  
          * Set a custom registry used to handle different response 
 511  
          * <code>content-type</code>s
 512  
          * @param pr
 513  
          */
 514  
         public void setParserRegistry( ParserRegistry pr ) {
 515  0
                 this.parsers = pr;
 516  0
         }
 517  
         
 518  
         /**
 519  
          * Set a custom registry used to handle different 
 520  
          * <code>content-encoding</code> types in responses.  
 521  
          * @param cer
 522  
          */
 523  
         public void setContentEncodingRegistry( ContentEncodingRegistry cer ) {
 524  0
                 this.contentEncodingHandler = cer;
 525  0
         }
 526  
         
 527  
         
 528  
         
 529  
         /**
 530  
          * Encloses all properties and method calls used within the 
 531  
          * {@link HTTPBuilder#request(Object, Method, Object, Closure)} 'config' 
 532  
          * closure argument. 
 533  
          */
 534  
         protected class SendDelegate {
 535  
                 protected HttpRequestBase request;
 536  
                 protected Object contentType;
 537  
                 protected String requestContentType;
 538  4
                 protected Map<String,Closure> responseHandlers = new HashMap<String,Closure>();
 539  
                 protected URIBuilder url;
 540  4
                 protected Map<String,String> headers = new HashMap<String,String>();
 541  
                 
 542  
                 public SendDelegate( HttpRequestBase request, Object contentType, 
 543  
                                 Map<String,String> defaultRequestHeaders,
 544  4
                                 Map<String,Closure> defaultResponseHandlers ) {
 545  4
                         this.request = request;
 546  4
                         this.headers.putAll( defaultRequestHeaders );
 547  4
                         this.contentType = contentType;
 548  4
                         this.responseHandlers.putAll( defaultResponseHandlers );
 549  4
                         this.url = new URIBuilder(request.getURI());
 550  4
                 }
 551  
                 
 552  
                 /** 
 553  
                  * Use this object to manipulate parts of the request URL, like 
 554  
                  * query params and request path.  Example:
 555  
                  * <pre>
 556  
                  * builder.request(GET,XML) {
 557  
                  *   url.path = '../other/request.jsp'
 558  
                  *   url.params = [p1:1, p2:2]
 559  
                  *   ...
 560  
                  * }</pre>
 561  
                  * @return {@link URIBuilder} to manipulate the request URL 
 562  
                  */
 563  4
                 public URIBuilder getURL() { return this.url; }
 564  
 
 565  4
                 protected HttpRequestBase getRequest() { return this.request; }
 566  
                 
 567  
                 /**
 568  
                  * Get the content-type of any data sent in the request body and the 
 569  
                  * expected response content-type.
 570  
                  * @return whatever value was assigned via {@link #setContentType(Object)}
 571  
                  * or passed from the {@link HTTPBuilder#defaultContentType} when this
 572  
                  * SendDelegateinstance was constructed.
 573  
                  */
 574  4
                 protected Object getContentType() { return this.contentType; }
 575  
                 
 576  
                 /**
 577  
                  * Set the content-type used for any data in the request body, as well
 578  
                  * as the <code>Accept</code> content-type that will be used for parsing
 579  
                  * the response. The value should be either a {@link ContentType} value 
 580  
                  * or a String, i.e. <code>"text/plain"</code>
 581  
                  * @param ct content-type to send and recieve content
 582  
                  */
 583  
                 protected void setContentType( Object ct ) {
 584  0
                         if ( ct == null ) this.contentType = defaultContentType;
 585  0
                         this.contentType = ct; 
 586  0
                 }
 587  
                 
 588  
                 /**
 589  
                  * The request content-type, if different from the {@link #contentType}.
 590  
                  * @return
 591  
                  */
 592  
                 protected String getRequestContentType() {
 593  0
                         if ( this.requestContentType != null ) return this.requestContentType;
 594  0
                         else return this.getContentType().toString();
 595  
                 }
 596  
                 
 597  
                 /**
 598  
                  * Assign a different content-type for the request than is expected for 
 599  
                  * the response.  This is useful if i.e. you want to post URL-encoded
 600  
                  * form data but expect the response to be XML or HTML.  The 
 601  
                  * {@link #getContentType()} will always control the <code>Accept</code>
 602  
                  * header, and will be used for the request content <i>unless</i> this 
 603  
                  * value is also explicitly set.
 604  
                  * @param ct either a {@link ContentType} value or a valid content-type
 605  
                  * String.
 606  
                  */
 607  
                 protected void setRequestContentType( String ct ) { 
 608  0
                         this.requestContentType = ct; 
 609  0
                 }
 610  
                 
 611  
                 /**
 612  
                  * Valid arguments:
 613  
                  * <dl>
 614  
                  *   <dt>url</dt><dd>Either a URI, URL, or String. 
 615  
                  *           If not supplied, the HTTPBuilder's default URL is used.</dd>
 616  
                  *   <dt>path</dt><dd>Request path that is merged with the URL</dd>
 617  
                  *   <dt>params</dt><dd>Map of request parameters</dd>
 618  
                  *   <dt>headers</dt><dd>Map of HTTP headers</dd>
 619  
                  *   <dt>contentType</dt><dd>Request content type and Accept header.  
 620  
                  *           If not supplied, the HTTPBuilder's default content-type is used.</dd>
 621  
                  *   <dt>requestContentType</dt><dd>content type for the request, if it
 622  
                  *      is different from the expected response content-type</dd>
 623  
                  *   <dt>body</dt><dd>Request body that will be encoded based on the given contentType</dd>
 624  
                  * </dl>
 625  
                  * @param args named parameters to set properties on this delegate.
 626  
                  * @throws MalformedURLException
 627  
                  * @throws URISyntaxException
 628  
                  */
 629  
                 @SuppressWarnings("unchecked")
 630  
                 protected void setPropertiesFromMap( Map<String,?> args ) throws MalformedURLException, URISyntaxException {
 631  1
                         Object uri = args.get( "url" );
 632  1
                         if ( uri == null ) uri = defaultURI;
 633  1
                         url = new URIBuilder( convertToURI( uri ) );
 634  
                         
 635  1
                         Map params = (Map)args.get( "params" );
 636  1
                         if ( params != null ) this.url.setQuery( params );
 637  1
                         Map headers = (Map)args.get( "headers" );
 638  1
                         if ( headers != null ) this.setHeaders( headers );
 639  
                         
 640  1
                         Object path = args.get( "path" );
 641  1
                         if ( path != null ) this.url.setPath( path.toString() );
 642  
                         
 643  1
                         Object contentType = args.get( "contentType" );
 644  1
                         if ( contentType != null ) this.setContentType( contentType );
 645  
                         
 646  1
                         contentType = args.get( "requestContentType" );
 647  1
                         if ( contentType != null ) this.setRequestContentType( contentType.toString() );
 648  
                         
 649  1
                         Object body = args.get("body");
 650  1
                         if ( body != null ) this.setBody( body );
 651  1
                 }
 652  
 
 653  
                 /**
 654  
                  * Set request headers.  These values will be <strong>merged</strong>
 655  
                  * with any {@link HTTPBuilder#getHeaders() default request headers.} 
 656  
                  * (The assumption is you'll probably want to add a bunch of headers to 
 657  
                  * whatever defaults you've already set).  If you <i>only</i> want to 
 658  
                  * use values set here, simply call {@link #getHeaders() headers.clear()}
 659  
                  * first.
 660  
                  */
 661  
                 public void setHeaders( Map<?,?> newHeaders ) {
 662  0
                         for( Object key : newHeaders.keySet() ) {
 663  0
                                 Object val = newHeaders.get( key );
 664  0
                                 if ( val == null ) this.headers.remove( key );
 665  0
                                 else this.headers.put( key.toString(), val.toString() );
 666  0
                         }
 667  0
                 }
 668  
                 
 669  
                 /**
 670  
                  * Get request headers (including any default headers).  Note that this
 671  
                  * will not include any <code>Accept</code>, <code>Content-Type</code>,
 672  
                  * or <code>Content-Encoding</code> headers that are automatically
 673  
                  * handled by any encoder or parsers in effect.  Note that any values 
 674  
                  * set here <i>will</i> override any of those automatically assigned 
 675  
                  * values.
 676  
                  * header that is a
 677  
                  * @return
 678  
                  */
 679  
                 public Map<String,String> getHeaders() {
 680  5
                         return this.headers;
 681  
                 }
 682  
                 
 683  
                 /**
 684  
                  * Convenience method to set a request content-type at the same time
 685  
                  * the request body is set.  This is a variation of 
 686  
                  * {@link #setBody(Object)} that allows for a different content-type
 687  
                  * than what is expected for the response.  
 688  
                  * 
 689  
                  * <p>Example:        
 690  
                  * <pre>
 691  
                  * http.request(POST,HTML) {
 692  
                  *   
 693  
                  *   /* request data is interpreted as a JsonBuilder closure in the 
 694  
                  *      default EncoderRegistry implementation * /
 695  
                  *   send( 'text/javascript' ) {  
 696  
                  *     a : ['one','two','three']
 697  
                  *   }
 698  
                  *   
 699  
                  *   // response content-type is what was specified in the outer request() argument:
 700  
                  *   response.success = { resp, html -> 
 701  
                  *   
 702  
                  *   }
 703  
                  * }
 704  
                  * </pre>
 705  
                  * @param contentType either a {@link ContentType} or content-type 
 706  
                  *         string like <code>"text/xml"</code>
 707  
                  * @param requestBody
 708  
                  */
 709  
                 public void send( Object contentType, Object requestBody ) {
 710  0
                         this.setRequestContentType( contentType.toString() );
 711  0
                         this.setBody( requestBody );
 712  0
                 }
 713  
 
 714  
                 /**
 715  
                  * Set the request body.  This value may be of any type supported by 
 716  
                  * the associated {@link EncoderRegistry request encoder}.  
 717  
                  * @see #send(Object, Object)
 718  
                  * @param body data or closure interpretes as the request body
 719  
                  */
 720  
                 public void setBody( Object body ) {
 721  0
                         if ( ! (request instanceof HttpEntityEnclosingRequest ) )
 722  0
                                 throw new UnsupportedOperationException( 
 723  
                                                 "Cannot set a request body for a " + request.getMethod() + " method" );
 724  0
                         Closure encoder = encoders.get( this.getRequestContentType() );
 725  0
                         HttpEntity entity = (HttpEntity)encoder.call( body );
 726  
                         
 727  0
                         ((HttpEntityEnclosingRequest)request).setEntity( entity );
 728  0
                 }
 729  
                 
 730  
                 /**
 731  
                  * Get the proper response handler for the response code.  This is called
 732  
                  * by the {@link HTTPBuilder} class in order to find the proper handler
 733  
                  * based on the response status code.
 734  
                  *  
 735  
                  * @param statusCode HTTP response status code
 736  
                  * @return the response handler
 737  
                  */
 738  
                 protected Closure findResponseHandler( int statusCode ) {
 739  4
                         Closure handler = getResponse().get( Integer.toString( statusCode ) );
 740  4
                         if ( handler == null ) handler = 
 741  
                                 getResponse().get( Status.find( statusCode ).toString() );
 742  4
                         return handler;
 743  
                 }
 744  
                 
 745  
                 /**
 746  
                  * Access the response handler map to set response parsing logic.  
 747  
                  * i.e.<pre>
 748  
                  * builder.request( GET, XML ) {
 749  
                  *   response.success = { xml ->
 750  
                  *      /* for XML content type, the default parser 
 751  
                  *         will return an XmlSlurper * /
 752  
                  *           xml.root.children().each { println it } 
 753  
                  *   }
 754  
                  * }</pre>
 755  
                  * @return
 756  
                  */
 757  12
                 public Map<String,Closure> getResponse() { return responseHandlers; }
 758  
         }        
 759  
 }