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