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