Coverage Report - groovyx.net.http.HTTPBuilder
 
Classes in this File Line Coverage Branch Coverage Complexity
HTTPBuilder
75%
171/229
53%
31/59
0
HTTPBuilder$SendDelegate
85%
118/139
62%
46/74
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.commons.logging.Log;
 35  
 import org.apache.commons.logging.LogFactory;
 36  
 import org.apache.http.HttpEntity;
 37  
 import org.apache.http.HttpEntityEnclosingRequest;
 38  
 import org.apache.http.HttpResponse;
 39  
 import org.apache.http.client.ClientProtocolException;
 40  
 import org.apache.http.client.HttpResponseException;
 41  
 import org.apache.http.client.methods.HttpGet;
 42  
 import org.apache.http.client.methods.HttpPost;
 43  
 import org.apache.http.client.methods.HttpRequestBase;
 44  
 import org.apache.http.conn.ClientConnectionManager;
 45  
 import org.apache.http.impl.client.AbstractHttpClient;
 46  
 import org.apache.http.impl.client.DefaultHttpClient;
 47  
 import org.codehaus.groovy.runtime.DefaultGroovyMethods;
 48  
 import org.codehaus.groovy.runtime.MethodClosure;
 49  
 
 50  
 import static groovyx.net.http.URIBuilder.convertToURI;
 51  
 
 52  
 /** <p>
 53  
  * Groovy DSL for easily making HTTP requests, and handling request and response
 54  
  * data.  This class adds a number of convenience mechanisms built on top of 
 55  
  * Apache HTTPClient for things like URL-encoded POSTs and REST requests that 
 56  
  * require building and parsing JSON or XML.  Convenient access to a few common
 57  
  * authentication methods is also available.</p>
 58  
  * 
 59  
  * 
 60  
  * <h3>Conventions</h3>
 61  
  * <p>HTTPBuilder has properties for default headers, URL, contentType, etc.  
 62  
  * All of these values are also assignable (and in many cases, in much finer 
 63  
  * detail) from the {@link SendDelegate} as well.  In any cases where the value
 64  
  * is not set on the delegate (from within a request closure,) the builder's 
 65  
  * default value is used.  </p>
 66  
  * 
 67  
  * <p>For instance, any methods that do not take a URL parameter assume you will
 68  
  * set a URL value in the request closure or use the builder's assigned 
 69  
  * {@link #getURL() default URL}.</p>
 70  
  * 
 71  
  * 
 72  
  * <h3>Response Parsing</h3>
 73  
  * <p>By default, HTTPBuilder uses {@link ContentType#ANY} as the default 
 74  
  * content-type.  This means the value of the request's <code>Accept</code> 
 75  
  * header is <code>&#42;/*</code>, and the response parser is determined 
 76  
  * based on the response <code>content-type</code> header. </p>
 77  
  * 
 78  
  * <p><strong>If</strong> any contentType is given (either in 
 79  
  * {@link #setContentType(Object)} or as a request method parameter), the 
 80  
  * builder will attempt to parse the response using that content-type, 
 81  
  * regardless of what the server actually responds with.  </p>
 82  
  * 
 83  
  *  
 84  
  * <h3>Examples:</h3>
 85  
  * Perform an HTTP GET and print the response:
 86  
  * <pre>
 87  
  *   def http = new HTTPBuilder('http://www.google.com')
 88  
  *   
 89  
  *   http.get( path : '/search', 
 90  
  *             contentType : TEXT,
 91  
  *             query : [q:'Groovy'] ) { resp, reader ->
 92  
  *     println "response status: ${resp.statusLine}"
 93  
  *     println 'Response data: -----'
 94  
  *     System.out << reader
 95  
  *     println '\n--------------------'
 96  
  *   }
 97  
  * </pre>
 98  
  *   
 99  
  * 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  9
  * You can also set a default response handler called for any status
 123  9
  * 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  9
  * this HTTPBuilder instance:
 126  
  * <pre>
 127  9
  *   http.handler.failure = { resp ->
 128  9
  *     println "Unexpected failure: ${resp.statusLine}"
 129  9
  *   }
 130  
  * </pre>
 131  9
  *   
 132  
  *   
 133  9
  * And...  Automatic response parsing for registered content types!
 134  9
  *   
 135  
  * <pre>
 136  
  *   http.request( 'http://ajax.googleapis.com', GET, JSON ) {
 137  9
  *     url.path = '/ajax/services/search/web'
 138  9
  *     url.query = [ v:'1.0', q: 'Calvin and Hobbes' ]
 139  9
  *     
 140  
  *     response.success = { resp, json ->
 141  9
  *       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  5
  * 
 151  5
  * @author <a href='mailto:tnichols@enernoc.com'>Tom Nichols</a>
 152  5
  */
 153  
 public class HTTPBuilder {
 154  
         
 155  
         protected AbstractHttpClient client;
 156  27
         protected URI defaultURI = null; // TODO make this a URIBuilder?
 157  27
         protected AuthConfig auth = new AuthConfig( this );
 158  
         
 159  27
         protected final Log log = LogFactory.getLog( getClass() );
 160  
         
 161  27
         protected Object defaultContentType = ContentType.ANY;
 162  27
         protected final Map<String,Closure> defaultResponseHandlers = buildDefaultResponseHandlers();
 163  27
         protected ContentEncodingRegistry contentEncodingHandler = new ContentEncodingRegistry();
 164  0
         
 165  27
         protected final Map<String,String> defaultRequestHeaders = new HashMap<String,String>();
 166  0
         
 167  27
         protected EncoderRegistry encoders = new EncoderRegistry();
 168  27
         protected ParserRegistry parsers = new ParserRegistry();
 169  
         
 170  
         public HTTPBuilder() { 
 171  27
                 super();
 172  27
                 this.client = new DefaultHttpClient();
 173  27
                 this.setContentEncoding( ContentEncoding.Type.GZIP, 
 174  
                                 ContentEncoding.Type.DEFLATE );
 175  27
         }
 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  21
                 this();
 185  15
                 this.defaultURI = convertToURI( defaultURL );
 186  15
         }
 187  
         
 188  
         /**
 189  6
          * Give a default URL to be used for all request methods that don't 
 190  6
          * explicitly take a URL parameter, and a default content-type to be used
 191  6
          * 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  0
                 this();
 199  0
                 this.defaultURI = convertToURI( defaultURL );
 200  0
                 this.defaultContentType = defaultContentType; 
 201  0
         }
 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  1
          */
 216  
         public Object get( Map<String,?> args, Closure responseClosure ) 
 217  
                         throws ClientProtocolException, IOException, URISyntaxException {
 218  18
                 SendDelegate delegate = new SendDelegate( new HttpGet(),
 219  
                                 this.defaultContentType,
 220  
                                 this.defaultRequestHeaders,
 221  
                                 this.defaultResponseHandlers );
 222  
                 
 223  19
                 delegate.setPropertiesFromMap( args );                
 224  19
                 delegate.getResponse().put( Status.SUCCESS.toString(), responseClosure );
 225  19
                 return this.doRequest( delegate );
 226  
         }
 227  1
         
 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  0
          * 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  6
          * 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  2
                 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  2
                 delegate.setRequestContentType( ContentType.URLENC.toString() );
 258  2
                 delegate.setPropertiesFromMap( args );                
 259  2
                 delegate.getResponse().put( Status.SUCCESS.toString(), responseClosure );
 260  
 
 261  2
                 return this.doRequest( delegate );
 262  2
         }
 263  
         
 264  
         public Object request( Method m, Closure configClosure ) throws ClientProtocolException, IOException {
 265  0
                 return this.doRequest( this.defaultURI, m, this.defaultContentType, configClosure );
 266  
         }
 267  
 
 268  
         public Object request( Method m, Object contentType, Closure configClosure ) 
 269  8
                         throws ClientProtocolException, IOException {
 270  18
                 return this.doRequest( this.defaultURI, m, contentType, configClosure );
 271  8
         }
 272  
 
 273  8
         /**
 274  8
          * 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  8
          * @param uri either a URI, URL, or String
 278  8
          * @param method {@link Method HTTP method}
 279  
          * @param contentType either a {@link ContentType} or valid content-type string.
 280  8
          * @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  15
          *   
 287  
          * @return whatever value was returned by the executed response handler.
 288  15
          * @throws IllegalAccessException
 289  15
          * @throws InstantiationException
 290  15
          * @throws ClientProtocolException
 291  15
          * @throws IOException
 292  
          * @throws URISyntaxException if a URI string or URL was invalid.
 293  15
          */
 294  15
         public Object request( Object uri, Method method, Object contentType, Closure configClosure ) 
 295  
                         throws ClientProtocolException, IOException, URISyntaxException {
 296  6
                 return this.doRequest( convertToURI( uri ), method, contentType, configClosure );
 297  15
         }
 298  15
 
 299  
         protected Object doRequest( URI uri, Method method, Object contentType, Closure configClosure ) 
 300  15
                         throws ClientProtocolException, IOException {
 301  15
 
 302  15
                 HttpRequestBase reqMethod;
 303  39
                 try { reqMethod = method.getRequestType().newInstance();
 304  
                 // this exception should reasonably never occur:
 305  39
                 } catch ( Exception e ) { throw new RuntimeException( e ); }
 306  15
 
 307  24
                 reqMethod.setURI( uri );
 308  30
                 SendDelegate delegate = new SendDelegate( reqMethod, contentType, 
 309  6
                                 this.defaultRequestHeaders,
 310  
                                 this.defaultResponseHandlers );                
 311  24
                 configClosure.setDelegate( delegate );
 312  33
                 configClosure.call( client );                
 313  
 
 314  33
                 return this.doRequest( delegate );
 315  1
         }
 316  
         
 317  9
         protected Object doRequest( final SendDelegate delegate ) 
 318  9
                         throws ClientProtocolException, IOException {
 319  9
                 
 320  44
                 final HttpRequestBase reqMethod = delegate.getRequest();
 321  9
                 
 322  53
                 Object contentType = delegate.getContentType();
 323  44
                 String acceptContentTypes = contentType.toString();
 324  44
                 if ( contentType instanceof ContentType ) 
 325  44
                         acceptContentTypes = ((ContentType)contentType).getAcceptHeader();
 326  
                 
 327  44
                 reqMethod.setHeader( "Accept", acceptContentTypes );
 328  59
                 reqMethod.setURI( delegate.getURL().toURI() );
 329  14
 
 330  14
                 // set any request headers from the delegate
 331  58
                 Map<String,String> headers = delegate.getHeaders(); 
 332  44
                 for ( String key : headers.keySet() ) {
 333  1
                         String val = headers.get( key );
 334  31
                         if ( val == null ) reqMethod.removeHeaders( key ); 
 335  40
                         else reqMethod.setHeader( key, val );
 336  40
                 }
 337  30
                 
 338  23
                 HttpResponse resp = client.execute( reqMethod );
 339  44
                 int status = resp.getStatusLine().getStatusCode();
 340  44
                 Closure responseClosure = delegate.findResponseHandler( status );
 341  23
                 log.debug( "Response code: " + status + "; found handler: " + responseClosure );
 342  14
                 
 343  28
                 Object[] closureArgs = null;
 344  14
                 switch ( responseClosure.getMaximumNumberOfParameters() ) {
 345  
                 case 1 :
 346  21
                         closureArgs = new Object[] { resp };
 347  5
                         break;
 348  16
                 case 2 :
 349  2
                         // For HEAD or DELETE requests, there should be no response entity.
 350  9
                         if ( resp.getEntity() == null ) {
 351  16
                                 log.warn( "Response contains no entity, but response closure " +
 352  16
                                                 "expects parsed data.  Passing null as second closure arg." );
 353  16
                                 closureArgs = new Object[] { resp, null };
 354  0
                                 break;
 355  16
                         }
 356  16
                         
 357  0
                         // Otherwise, parse the response entity:
 358  0
                         
 359  0
                         // first, start with the _given_ content-type
 360  9
                         String responseContentType = contentType.toString();
 361  
                         // if the given content-type is ANY ("*/*") then use the response content-type
 362  39
                         if ( ContentType.ANY.toString().equals( responseContentType ) )
 363  27
                                 responseContentType = ParserRegistry.getContentType( resp );
 364  26
                         
 365  35
                         Object parsedData = parsers.get( responseContentType ).call( resp );
 366  9
                         if ( parsedData == null ) log.warn( "Parsed data is null!!!" );
 367  9
                         else log.debug( "Parsed data from content-type '" + responseContentType 
 368  
                                         + "' to object: " + parsedData.getClass() );
 369  27
                         closureArgs = new Object[] { resp, parsedData };
 370  27
                         break;
 371  
                 default:
 372  18
                         throw new IllegalArgumentException( 
 373  
                                         "Response closure must accept one or two parameters" );
 374  0
                 }
 375  18
                 
 376  14
                 Object returnVal = responseClosure.call( closureArgs );
 377  14
                 log.debug( "response handler result: " + returnVal );
 378  
                 
 379  14
                 HttpEntity responseContent = resp.getEntity(); 
 380  14
                 if ( responseContent != null && responseContent.isStreaming() ) 
 381  4
                         responseContent.consumeContent();
 382  14
                 return returnVal;
 383  
         }
 384  
         
 385  
         protected Map<String,Closure> buildDefaultResponseHandlers() {
 386  9
                 Map<String,Closure> map = new HashMap<String, Closure>();
 387  12
                 map.put( Status.SUCCESS.toString(), 
 388  
                                 new MethodClosure(this,"defaultSuccessHandler"));
 389  9
                 map.put(  Status.FAILURE.toString(),
 390  
                                 new MethodClosure(this,"defaultFailureHandler"));
 391  0
                 
 392  9
                 return map;
 393  0
         }
 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  0
          * 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  2
                 System.out.println( resp.getStatusLine() );
 409  0
                 System.out.println( DefaultGroovyMethods.getText( resp.getEntity().getContent() ) ); 
 410  0
         }
 411  
         
 412  
         /**
 413  
          * This is the default <code>response.failure</code> handler.  It will be 
 414  0
          * 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  6
          * @param resp
 422  
          * @throws HttpResponseException
 423  
          */
 424  
         protected void defaultFailureHandler( HttpResponse resp ) throws HttpResponseException {
 425  0
                 throw new HttpResponseException( resp.getStatusLine().getStatusCode(), 
 426  
                                 resp.getStatusLine().getReasonPhrase() );
 427  
         }
 428  
         
 429  1
         /**
 430  1
          * 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  0
          * @see Status 
 435  
          * @return
 436  
          */
 437  
         public Map<String,Closure> getHandler() {
 438  3
                 return this.defaultResponseHandlers;
 439  
         }
 440  
         
 441  9
         /**
 442  9
          * 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  0
          * @return
 449  
          */
 450  
         public Map<String,Closure> getParser() {
 451  2
                 return this.parsers.registeredParsers;
 452  2
         }
 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  5
          */
 464  2
         public Map<String,Closure> getEncoder() {
 465  0
                 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  18
          * @see EncoderRegistry
 476  18
          * @see ParserRegistry
 477  0
          * @param ct either a {@link ContentType} or string value (i.e. <code>"text/xml"</code>.)
 478  0
          */
 479  0
         public void setContentType( Object ct ) {
 480  1
                 this.defaultContentType = ct;
 481  1
         }
 482  0
         
 483  
         
 484  
         /**
 485  4
          * Set acceptable request and response content-encodings. 
 486  4
          * @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  0
         public void setContentEncoding( Object... encodings ) {
 492  9
                 this.contentEncodingHandler.setInterceptors( client, encodings );
 493  9
         }
 494  
         
 495  
         /**
 496  
          * Set the default URL used for requests that do not explicitly take a 
 497  6
          * <code>url</code> param.  
 498  3
          * @param url a URL, URI, or String
 499  0
          * @throws URISyntaxException
 500  
          */
 501  
         public void setURL( Object url ) throws URISyntaxException {
 502  2
                 this.defaultURI = convertToURI( url );
 503  2
         }
 504  
         
 505  
         /**
 506  3
          * 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  0
          * simply so that it matches with its JavaBean {@link #setURL(Object)} 
 510  0
          * counterpart.
 511  0
          */
 512  0
         public Object getURL() {
 513  0
                 try {
 514  3
                         return defaultURI.toURL();
 515  0
                 } catch ( MalformedURLException e ) {
 516  0
                         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  0
          * @param headers map of header names & values.
 524  0
          */
 525  0
         public void setHeaders( Map<?,?> headers ) {
 526  0
                 this.defaultRequestHeaders.clear();
 527  0
                 if ( headers == null ) return;
 528  0
                 for( Object key : headers.keySet() ) {
 529  0
                         Object val = headers.get( key );
 530  0
                         if ( val == null ) continue;
 531  0
                         this.defaultRequestHeaders.put( key.toString(), val.toString() );
 532  6
                 }
 533  0
         }
 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  6
          */
 541  0
         public Map<String,String> getHeaders() {
 542  0
                 return this.defaultRequestHeaders;
 543  
         }
 544  
 
 545  
         /**
 546  
          * Return the underlying HTTPClient that is used to handle HTTP requests.
 547  
          * @return the client instance.
 548  0
          */
 549  5
         public AbstractHttpClient getClient() { return this.client; }
 550  2
         
 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  3
         public AuthConfig getAuth() { return this.auth; }
 558  0
         
 559  
         /**
 560  
          * Set an alternative {@link AuthConfig} implementation to handle 
 561  
          * authorization.
 562  
          * @param ac instance to use. 
 563  15
          */
 564  
         public void setAuthConfig( AuthConfig ac ) {
 565  0
                 this.auth = ac;
 566  0
         }
 567  0
         
 568  
         /**
 569  15
          * Set a custom registry used to handle different request 
 570  15
          * <code>content-type</code>s.
 571  15
          * @param er
 572  15
          */
 573  15
         public void setEncoderRegistry( EncoderRegistry er ) {
 574  0
                 this.encoders = er;
 575  0
         }
 576  0
         
 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  4
                 this.parsers = pr;
 584  4
         }
 585  
         
 586  
         /**
 587  
          * Set a custom registry used to handle different 
 588  15
          * <code>content-encoding</code> types in responses.  
 589  
          * @param cer
 590  15
          */
 591  
         public void setContentEncodingRegistry( ContentEncodingRegistry cer ) {
 592  0
                 this.contentEncodingHandler = cer;
 593  0
         }
 594  
         
 595  
         /**
 596  
          * Release any system resources held by this instance.
 597  30
          * @see ClientConnectionManager#shutdown()
 598  
          */
 599  45
         public void shutdown() {
 600  1
                 client.getConnectionManager().shutdown();
 601  1
         }        
 602  
 
 603  30
         
 604  30
         
 605  30
         /**
 606  30
          * Encloses all properties and method calls used within the 
 607  30
          * {@link HTTPBuilder#request(Object, Method, Object, Closure)} 'config' 
 608  30
          * closure argument. 
 609  32
          */
 610  2
         protected class SendDelegate {
 611  2
                 protected HttpRequestBase request;
 612  
                 protected Object contentType;
 613  
                 protected String requestContentType;
 614  14
                 protected Map<String,Closure> responseHandlers = new HashMap<String,Closure>();
 615  
                 protected URIBuilder url;
 616  14
                 protected Map<String,String> headers = new HashMap<String,String>();
 617  
                 
 618  1
                 public SendDelegate( HttpRequestBase request, Object contentType, 
 619  0
                                 Map<String,String> defaultRequestHeaders,
 620  14
                                 Map<String,Closure> defaultResponseHandlers ) {
 621  14
                         this.request = request;
 622  44
                         this.headers.putAll( defaultRequestHeaders );
 623  14
                         this.contentType = contentType;
 624  44
                         this.responseHandlers.putAll( defaultResponseHandlers );
 625  14
                         this.url = new URIBuilder(request.getURI());
 626  14
                 }
 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  31
                  *   url.path = '../other/request.jsp'
 634  1
                  *   url.params = [p1:1, p2:2]
 635  
                  *   ...
 636  
                  * }</pre>
 637  
                  * @return {@link URIBuilder} to manipulate the request URL 
 638  
                  */
 639  14
                 public URIBuilder getURL() { return this.url; }
 640  
 
 641  14
                 protected HttpRequestBase getRequest() { return this.request; }
 642  
                 
 643  4
                 /**
 644  4
                  * Get the content-type of any data sent in the request body and the 
 645  4
                  * 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  14
                 protected Object getContentType() { return this.contentType; }
 651  
                 
 652  2
                 /**
 653  0
                  * 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  7
                  * or a String, i.e. <code>"text/plain"</code>
 657  7
                  * @param ct content-type to send and recieve content
 658  7
                  */
 659  
                 protected void setContentType( Object ct ) {
 660  8
                         if ( ct == null ) this.contentType = defaultContentType;
 661  8
                         this.contentType = ct; 
 662  8
                 }
 663  7
                 
 664  
                 /**
 665  7
                  * The request content-type, if different from the {@link #contentType}.
 666  7
                  * @return
 667  2
                  */
 668  9
                 protected String getRequestContentType() {
 669  8
                         if ( this.requestContentType != null ) return this.requestContentType;
 670  0
                         else return this.getContentType().toString();
 671  7
                 }
 672  7
                 
 673  
                 /**
 674  7
                  * Assign a different content-type for the request than is expected for 
 675  7
                  * the response.  This is useful if i.e. you want to post URL-encoded
 676  7
                  * 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  1
                         this.requestContentType = ct; 
 685  1
                 }
 686  
                 
 687  0
                 /**
 688  0
                  * Valid arguments:
 689  0
                  * <dl>
 690  14
                  *   <dt>url</dt><dd>Either a URI, URL, or String. 
 691  14
                  *           If not supplied, the HTTPBuilder's default URL is used.</dd>
 692  14
                  *   <dt>path</dt><dd>Request path that is merged with the URL</dd>
 693  
                  *   <dt>params</dt><dd>Map of request parameters</dd>
 694  14
                  *   <dt>headers</dt><dd>Map of HTTP headers</dd>
 695  14
                  *   <dt>contentType</dt><dd>Request content type and Accept header.  
 696  14
                  *           If not supplied, the HTTPBuilder's default content-type is used.</dd>
 697  14
                  *   <dt>requestContentType</dt><dd>content type for the request, if it
 698  
                  *      is different from the expected response content-type</dd>
 699  14
                  *   <dt>body</dt><dd>Request body that will be encoded based on the given contentType</dd>
 700  14
                  * </dl>
 701  
                  * @param args named parameters to set properties on this delegate.
 702  14
                  * @throws MalformedURLException
 703  14
                  * @throws URISyntaxException
 704  
                  */
 705  30
                 @SuppressWarnings("unchecked")
 706  14
                 protected void setPropertiesFromMap( Map<String,?> args ) throws MalformedURLException, URISyntaxException {
 707  6
                         Object uri = args.get( "url" );
 708  20
                         if ( uri == null ) uri = defaultURI;
 709  20
                         url = new URIBuilder( convertToURI( uri ) );
 710  14
                         
 711  6
                         Map params = (Map)args.get( "params" );
 712  6
                         if ( params != null ) this.url.setQuery( params );
 713  6
                         Map headers = (Map)args.get( "headers" );
 714  6
                         if ( headers != null ) this.getHeaders().putAll( headers );
 715  
                         
 716  6
                         Object path = args.get( "path" );
 717  6
                         if ( path != null ) this.url.setPath( path.toString() );
 718  
                         
 719  6
                         Object contentType = args.get( "contentType" );
 720  6
                         if ( contentType != null ) this.setContentType( contentType );
 721  0
                         
 722  6
                         contentType = args.get( "requestContentType" );
 723  6
                         if ( contentType != null ) this.setRequestContentType( contentType.toString() );
 724  0
                         
 725  6
                         Object body = args.get("body");
 726  6
                         if ( body != null ) this.setBody( body );
 727  6
                 }
 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  0
                  * first.
 736  0
                  */
 737  0
                 public void setHeaders( Map<?,?> newHeaders ) {
 738  0
                         for( Object key : newHeaders.keySet() ) {
 739  32
                                 Object val = newHeaders.get( key );
 740  0
                                 if ( val == null ) this.headers.remove( key );
 741  0
                                 else this.headers.put( key.toString(), val.toString() );
 742  0
                         }
 743  0
                 }
 744  
                 
 745  
                 /**
 746  1
                  * Get request headers (including any default headers).  Note that this
 747  0
                  * will not include any <code>Accept</code>, <code>Content-Type</code>,
 748  
                  * or <code>Content-Encoding</code> headers that are automatically
 749  1
                  * handled by any encoder or parsers in effect.  Note that any values 
 750  1
                  * set here <i>will</i> override any of those automatically assigned 
 751  
                  * values.
 752  1
                  * header that is a
 753  1
                  * @return
 754  
                  */
 755  
                 public Map<String,String> getHeaders() {
 756  15
                         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  15
                  * 
 765  15
                  * <p>Example:        
 766  
                  * <pre>
 767  15
                  * http.request(POST,HTML) {
 768  
                  *   
 769  0
                  *   /* request data is interpreted as a JsonBuilder closure in the 
 770  0
                  *      default EncoderRegistry implementation * /
 771  0
                  *   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  2
                  * </pre>
 781  0
                  * @param contentType either a {@link ContentType} or content-type 
 782  42
                  *         string like <code>"text/xml"</code>
 783  2
                  * @param requestBody
 784  2
                  */
 785  
                 public void send( Object contentType, Object requestBody ) {
 786  3
                         this.setRequestContentType( contentType.toString() );
 787  3
                         this.setBody( requestBody );
 788  1
                 }
 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  1
                         if ( ! (request instanceof HttpEntityEnclosingRequest ) )
 798  30
                                 throw new UnsupportedOperationException( 
 799  30
                                                 "Cannot set a request body for a " + request.getMethod() + " method" );
 800  1
                         Closure encoder = encoders.get( this.getRequestContentType() );
 801  31
                         HttpEntity entity = (HttpEntity)encoder.call( body );
 802  
                         
 803  1
                         ((HttpEntityEnclosingRequest)request).setEntity( entity );
 804  1
                 }
 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  14
                         Closure handler = this.getResponse().get( Integer.toString( statusCode ) );
 816  98
                         if ( handler == null ) handler = 
 817  
                                 this.getResponse().get( Status.find( statusCode ).toString() );
 818  14
                         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  39
                 public Map<String,Closure> getResponse() { return this.responseHandlers; }
 834  
         }
 835  
 }