001/* 002 * Copyright 2008-2011 Thomas Nichols. http://blog.thomnichols.org 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 encouraged (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 */ 022package groovyx.net.http; 023 024import static groovyx.net.http.URIBuilder.convertToURI; 025import groovy.lang.Closure; 026 027import java.io.ByteArrayInputStream; 028import java.io.ByteArrayOutputStream; 029import java.io.Closeable; 030import java.io.IOException; 031import java.io.InputStream; 032import java.io.Reader; 033import java.io.StringReader; 034import java.io.StringWriter; 035import java.net.URI; 036import java.net.URISyntaxException; 037import java.net.URL; 038import java.util.Arrays; 039import java.util.Map; 040 041import org.apache.commons.logging.Log; 042import org.apache.commons.logging.LogFactory; 043import org.apache.http.Header; 044import org.apache.http.HttpEntity; 045import org.apache.http.HttpEntityEnclosingRequest; 046import org.apache.http.HttpHost; 047import org.apache.http.HttpResponse; 048import org.apache.http.client.ClientProtocolException; 049import org.apache.http.client.ResponseHandler; 050import org.apache.http.client.methods.HttpGet; 051import org.apache.http.client.methods.HttpPost; 052import org.apache.http.client.methods.HttpRequestBase; 053import org.apache.http.client.protocol.ClientContext; 054import org.apache.http.conn.ClientConnectionManager; 055import org.apache.http.conn.params.ConnRoutePNames; 056import org.apache.http.cookie.params.CookieSpecPNames; 057import org.apache.http.client.HttpClient; 058import org.apache.http.impl.client.AbstractHttpClient; 059import org.apache.http.impl.client.DefaultHttpClient; 060import org.apache.http.params.BasicHttpParams; 061import org.apache.http.params.HttpParams; 062import org.apache.http.protocol.HttpContext; 063import org.codehaus.groovy.runtime.DefaultGroovyMethods; 064import org.codehaus.groovy.runtime.MethodClosure; 065 066/** <p> 067 * Groovy DSL for easily making HTTP requests, and handling request and response 068 * data. This class adds a number of convenience mechanisms built on top of 069 * Apache HTTPClient for things like URL-encoded POSTs and REST requests that 070 * require building and parsing JSON or XML. Convenient access to a few common 071 * authentication methods is also available.</p> 072 * 073 * 074 * <h3>Conventions</h3> 075 * <p>HTTPBuilder has properties for default headers, URI, contentType, etc. 076 * All of these values are also assignable (and in many cases, in much finer 077 * detail) from the {@link RequestConfigDelegate} as well. In any cases where the value 078 * is not set on the delegate (from within a request closure,) the builder's 079 * default value is used. </p> 080 * 081 * <p>For instance, any methods that do not take a <code>uri</code> parameter 082 * assume you will set the <code>uri</code> property in the request closure or 083 * use HTTPBuilder's assigned {@link #getUri() default URI}.</p> 084 * 085 * 086 * <h3>Response Parsing</h3> 087 * <p>By default, HTTPBuilder uses {@link ContentType#ANY} as the default 088 * content-type. This means the value of the request's <code>Accept</code> 089 * header is <code>*/*</code>, and the response parser is determined 090 * based on the response <code>content-type</code> header. </p> 091 * 092 * <p><strong>If</strong> any contentType is given (either in 093 * {@link #setContentType(Object)} or as a request method parameter), the 094 * builder will attempt to parse the response using that content-type, 095 * regardless of what the server actually responds with. </p> 096 * 097 * 098 * <h3>Examples:</h3> 099 * Perform an HTTP GET and print the response: 100 * <pre> 101 * def http = new HTTPBuilder('http://www.google.com') 102 * 103 * http.get( path : '/search', 104 * contentType : TEXT, 105 * query : [q:'Groovy'] ) { resp, reader -> 106 * println "response status: ${resp.statusLine}" 107 * println 'Response data: -----' 108 * System.out << reader 109 * println '\n--------------------' 110 * } 111 * </pre> 112 * 113 * Long form for other HTTP methods, and response-code-specific handlers. 114 * This is roughly equivalent to the above example. 115 * 116 * <pre> 117 * def http = new HTTPBuilder('http://www.google.com/search?q=groovy') 118 * 119 * http.request( GET, TEXT ) { req -> 120 * 121 * // executed for all successful responses: 122 * response.success = { resp, reader -> 123 * println 'my response handler!' 124 * assert resp.statusLine.statusCode == 200 125 * println resp.statusLine 126 * System.out << reader // print response stream 127 * } 128 * 129 * // executed only if the response status code is 401: 130 * response.'404' = { resp -> 131 * println 'not found!' 132 * } 133 * } 134 * </pre> 135 * 136 * You can also set a default response handler called for any status 137 * code > 399 that is not matched to a specific handler. Setting the value 138 * outside a request closure means it will apply to all future requests with 139 * this HTTPBuilder instance: 140 * <pre> 141 * http.handler.failure = { resp -> 142 * println "Unexpected failure: ${resp.statusLine}" 143 * } 144 * </pre> 145 * 146 * 147 * And... Automatic response parsing for registered content types! 148 * 149 * <pre> 150 * http.request( 'http://ajax.googleapis.com', GET, JSON ) { 151 * uri.path = '/ajax/services/search/web' 152 * uri.query = [ v:'1.0', q: 'Calvin and Hobbes' ] 153 * 154 * response.success = { resp, json -> 155 * assert json.size() == 3 156 * println "Query response: " 157 * json.responseData.results.each { 158 * println " ${it.titleNoFormatting} : ${it.visibleUrl}" 159 * } 160 * } 161 * } 162 * </pre> 163 * 164 * 165 * @author <a href='mailto:[email protected]'>Tom Nichols</a> 166 */ 167public class HTTPBuilder { 168 169 private HttpClient client; 170 protected URIBuilder defaultURI = null; 171 protected AuthConfig auth = new AuthConfig( this ); 172 173 protected final Log log = LogFactory.getLog( getClass() ); 174 175 protected Object defaultContentType = ContentType.ANY; 176 protected Object defaultRequestContentType = null; 177 protected boolean autoAcceptHeader = true; 178 protected final Map<Object,Closure> defaultResponseHandlers = 179 new StringHashMap<Closure>( buildDefaultResponseHandlers() ); 180 protected ContentEncodingRegistry contentEncodingHandler = new ContentEncodingRegistry(); 181 182 protected final Map<Object,Object> defaultRequestHeaders = new StringHashMap<Object>(); 183 184 protected EncoderRegistry encoders = new EncoderRegistry(); 185 protected ParserRegistry parsers = new ParserRegistry(); 186 187 /** 188 * Creates a new instance with a <code>null</code> default URI. 189 */ 190 public HTTPBuilder() { 191 setContentEncoding( ContentEncoding.Type.GZIP, 192 ContentEncoding.Type.DEFLATE ); 193 } 194 195 /** 196 * Give a default URI to be used for all request methods that don't 197 * explicitly take a URI parameter. 198 * @param defaultURI either a {@link URL}, {@link URI} or object whose 199 * <code>toString()</code> produces a valid URI string. See 200 * {@link URIBuilder#convertToURI(Object)}. 201 * @throws URISyntaxException if the given argument does not represent a valid URI 202 */ 203 public HTTPBuilder( Object defaultURI ) throws URISyntaxException { 204 setUri( defaultURI ); 205 } 206 207 /** 208 * Give a default URI to be used for all request methods that don't 209 * explicitly take a URI parameter, and a default content-type to be used 210 * for request encoding and response parsing. 211 * @param defaultURI either a {@link URL}, {@link URI} or object whose 212 * <code>toString()</code> produces a valid URI string. See 213 * {@link URIBuilder#convertToURI(Object)}. 214 * @param defaultContentType content-type string. See {@link ContentType} 215 * for common types. 216 * @throws URISyntaxException if the uri argument does not represent a valid URI 217 */ 218 public HTTPBuilder( Object defaultURI, Object defaultContentType ) throws URISyntaxException { 219 setUri( defaultURI ); 220 this.defaultContentType = defaultContentType; 221 } 222 223 /** 224 * <p>Convenience method to perform an HTTP GET. It will use the HTTPBuilder's 225 * {@link #getHandler() registered response handlers} to handle success or 226 * failure status codes. By default, the <code>success</code> response 227 * handler will attempt to parse the data and simply return the parsed 228 * object.</p> 229 * 230 * <p><strong>Note:</strong> If using the {@link #defaultSuccessHandler(HttpResponseDecorator, Object) 231 * default <code>success</code> response handler}, be sure to read the 232 * caveat regarding streaming response data.</p> 233 * 234 * @see #getHandler() 235 * @see #defaultSuccessHandler(HttpResponseDecorator, Object) 236 * @see #defaultFailureHandler(HttpResponseDecorator) 237 * @param args see {@link RequestConfigDelegate#setPropertiesFromMap(Map)} 238 * @return whatever was returned from the response closure. 239 * @throws URISyntaxException if a uri argument is given which does not 240 * represent a valid URI 241 * @throws IOException 242 * @throws ClientProtocolException 243 */ 244 public Object get( Map<String,?> args ) 245 throws ClientProtocolException, IOException, URISyntaxException { 246 return this.get( args, null ); 247 } 248 249 /** 250 * <p>Convenience method to perform an HTTP GET. The response closure will 251 * be called only on a successful response. </p> 252 * 253 * <p>A 'failed' response (i.e. any HTTP status code > 399) will be handled 254 * by the registered 'failure' handler. The 255 * {@link #defaultFailureHandler(HttpResponseDecorator) default failure handler} 256 * throws an {@link HttpResponseException}.</p> 257 * 258 * @param args see {@link RequestConfigDelegate#setPropertiesFromMap(Map)} 259 * @param responseClosure code to handle a successful HTTP response 260 * @return any value returned by the response closure. 261 * @throws ClientProtocolException 262 * @throws IOException 263 * @throws URISyntaxException if a uri argument is given which does not 264 * represent a valid URI 265 */ 266 public Object get( Map<String,?> args, Closure responseClosure ) 267 throws ClientProtocolException, IOException, URISyntaxException { 268 RequestConfigDelegate delegate = new RequestConfigDelegate( new HttpGet(), 269 this.defaultContentType, 270 this.defaultRequestHeaders, 271 this.defaultResponseHandlers ); 272 273 delegate.setPropertiesFromMap( args ); 274 if ( responseClosure != null ) delegate.getResponse().put( 275 Status.SUCCESS, responseClosure ); 276 return this.doRequest( delegate ); 277 } 278 279 /** 280 * <p>Convenience method to perform an HTTP POST. It will use the HTTPBuilder's 281 * {@link #getHandler() registered response handlers} to handle success or 282 * failure status codes. By default, the <code>success</code> response 283 * handler will attempt to parse the data and simply return the parsed 284 * object. </p> 285 * 286 * <p><strong>Note:</strong> If using the {@link #defaultSuccessHandler(HttpResponseDecorator, Object) 287 * default <code>success</code> response handler}, be sure to read the 288 * caveat regarding streaming response data.</p> 289 * 290 * @see #getHandler() 291 * @see #defaultSuccessHandler(HttpResponseDecorator, Object) 292 * @see #defaultFailureHandler(HttpResponseDecorator) 293 * @param args see {@link RequestConfigDelegate#setPropertiesFromMap(Map)} 294 * @return whatever was returned from the response closure. 295 * @throws IOException 296 * @throws URISyntaxException if a uri argument is given which does not 297 * represent a valid URI 298 * @throws ClientProtocolException 299 */ 300 public Object post( Map<String,?> args ) 301 throws ClientProtocolException, URISyntaxException, IOException { 302 return this.post( args, null ); 303 } 304 305 /** <p> 306 * Convenience method to perform an HTTP form POST. The response closure will be 307 * called only on a successful response.</p> 308 * 309 * <p>A 'failed' response (i.e. any 310 * HTTP status code > 399) will be handled by the registered 'failure' 311 * handler. The {@link #defaultFailureHandler(HttpResponseDecorator) default 312 * failure handler} throws an {@link HttpResponseException}.</p> 313 * 314 * <p>The request body (specified by a <code>body</code> named parameter) 315 * will be converted to a url-encoded form string unless a different 316 * <code>requestContentType</code> named parameter is passed to this method. 317 * (See {@link EncoderRegistry#encodeForm(Map)}.) </p> 318 * 319 * @param args see {@link RequestConfigDelegate#setPropertiesFromMap(Map)} 320 * @param responseClosure code to handle a successful HTTP response 321 * @return any value returned by the response closure. 322 * @throws ClientProtocolException 323 * @throws IOException 324 * @throws URISyntaxException if a uri argument is given which does not 325 * represent a valid URI 326 */ 327 public Object post( Map<String,?> args, Closure responseClosure ) 328 throws URISyntaxException, ClientProtocolException, IOException { 329 RequestConfigDelegate delegate = new RequestConfigDelegate( new HttpPost(), 330 this.defaultContentType, 331 this.defaultRequestHeaders, 332 this.defaultResponseHandlers ); 333 334 /* by default assume the request body will be URLEncoded, but allow 335 the 'requestContentType' named argument to override this if it is 336 given */ 337 delegate.setRequestContentType( ContentType.URLENC.toString() ); 338 delegate.setPropertiesFromMap( args ); 339 340 if ( responseClosure != null ) delegate.getResponse().put( 341 Status.SUCCESS.toString(), responseClosure ); 342 343 return this.doRequest( delegate ); 344 } 345 346 /** 347 * Make an HTTP request to the default URI, and parse using the default 348 * content-type. 349 * @see #request(Object, Method, Object, Closure) 350 * @param method {@link Method HTTP method} 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 method, Closure configClosure ) throws ClientProtocolException, IOException { 357 return this.doRequest( this.defaultURI.toURI(), method, 358 this.defaultContentType, configClosure ); 359 } 360 361 /** 362 * Make an HTTP request using the default URI, with the given method, 363 * content-type, and configuration. 364 * @see #request(Object, Method, Object, Closure) 365 * @param method {@link Method HTTP method} 366 * @param contentType either a {@link ContentType} or valid content-type string. 367 * @param configClosure request configuration options 368 * @return whatever value was returned by the executed response handler. 369 * @throws ClientProtocolException 370 * @throws IOException 371 */ 372 public Object request( Method method, Object contentType, Closure configClosure ) 373 throws ClientProtocolException, IOException { 374 return this.doRequest( this.defaultURI.toURI(), method, 375 contentType, configClosure ); 376 } 377 378 /** 379 * Make a request for the given HTTP method and content-type, with 380 * additional options configured in the <code>configClosure</code>. See 381 * {@link RequestConfigDelegate} for options. 382 * @param uri either a {@link URL}, {@link URI} or object whose 383 * <code>toString()</code> produces a valid URI string. See 384 * {@link URIBuilder#convertToURI(Object)}. 385 * @param method {@link Method HTTP method} 386 * @param contentType either a {@link ContentType} or valid content-type string. 387 * @param configClosure closure from which to configure options like 388 * {@link RequestConfigDelegate#getUri() uri.path}, 389 * {@link URIBuilder#setQuery(Map) request parameters}, 390 * {@link RequestConfigDelegate#setHeaders(Map) headers}, 391 * {@link RequestConfigDelegate#setBody(Object) request body} and 392 * {@link RequestConfigDelegate#getResponse() response handlers}. 393 * 394 * @return whatever value was returned by the executed response handler. 395 * @throws ClientProtocolException 396 * @throws IOException 397 * @throws URISyntaxException if the uri argument does not represent a valid URI 398 */ 399 public Object request( Object uri, Method method, Object contentType, Closure configClosure ) 400 throws ClientProtocolException, IOException, URISyntaxException { 401 return this.doRequest( convertToURI( uri ), method, contentType, configClosure ); 402 } 403 404 /** 405 * Create a {@link RequestConfigDelegate} from the given arguments, execute the 406 * config closure, then pass the delegate to {@link #doRequest(RequestConfigDelegate)}, 407 * which actually executes the request. 408 */ 409 protected Object doRequest( URI uri, Method method, Object contentType, Closure configClosure ) 410 throws ClientProtocolException, IOException { 411 412 HttpRequestBase reqMethod; 413 try { reqMethod = method.getRequestType().newInstance(); 414 // this exception should reasonably never occur: 415 } catch ( Exception e ) { throw new RuntimeException( e ); } 416 417 reqMethod.setURI( uri ); 418 RequestConfigDelegate delegate = new RequestConfigDelegate( reqMethod, contentType, 419 this.defaultRequestHeaders, 420 this.defaultResponseHandlers ); 421 configClosure.setDelegate( delegate ); 422 configClosure.setResolveStrategy( Closure.DELEGATE_FIRST ); 423 configClosure.call( reqMethod ); 424 425 return this.doRequest( delegate ); 426 } 427 428 /** 429 * All <code>request</code> methods delegate to this method. 430 */ 431 protected Object doRequest( final RequestConfigDelegate delegate ) 432 throws ClientProtocolException, IOException { 433 delegate.encodeBody(); 434 final HttpRequestBase reqMethod = delegate.getRequest(); 435 436 final Object contentType = delegate.getContentType(); 437 438 if ( this.autoAcceptHeader ) { 439 String acceptContentTypes = contentType.toString(); 440 if ( contentType instanceof ContentType ) 441 acceptContentTypes = ((ContentType)contentType).getAcceptHeader(); 442 reqMethod.setHeader( "Accept", acceptContentTypes ); 443 } 444 445 reqMethod.setURI( delegate.getUri().toURI() ); 446 if ( reqMethod.getURI() == null) 447 throw new IllegalStateException( "Request URI cannot be null" ); 448 449 log.debug( reqMethod.getMethod() + " " + reqMethod.getURI() ); 450 451 // set any request headers from the delegate 452 Map<?,?> headers = delegate.getHeaders(); 453 for ( Object key : headers.keySet() ) { 454 Object val = headers.get( key ); 455 if ( key == null ) continue; 456 if ( val == null ) reqMethod.removeHeaders( key.toString() ); 457 else reqMethod.setHeader( key.toString(), val.toString() ); 458 } 459 460 ResponseHandler<Object> responseHandler = new ResponseHandler<Object>() { 461 public Object handleResponse(HttpResponse response) 462 throws ClientProtocolException, IOException { 463 HttpResponseDecorator resp = new HttpResponseDecorator( 464 response, delegate.getContext(), null ); 465 try { 466 int status = resp.getStatusLine().getStatusCode(); 467 Closure responseClosure = delegate.findResponseHandler( status ); 468 log.debug( "Response code: " + status + "; found handler: " + responseClosure ); 469 470 Object[] closureArgs = null; 471 switch ( responseClosure.getMaximumNumberOfParameters() ) { 472 case 1 : 473 closureArgs = new Object[] { resp }; 474 break; 475 case 2 : // parse the response entity if the response handler expects it: 476 HttpEntity entity = resp.getEntity(); 477 try { 478 if ( entity == null || entity.getContentLength() == 0 ) 479 closureArgs = new Object[] { resp, null }; 480 else closureArgs = new Object[] { resp, parseResponse( resp, contentType ) }; 481 } 482 catch ( Exception ex ) { 483 Header h = entity.getContentType(); 484 String respContentType = h != null ? h.getValue() : null; 485 log.warn( "Error parsing '" + respContentType + "' response", ex ); 486 throw new ResponseParseException( resp, ex ); 487 } 488 break; 489 default: 490 throw new IllegalArgumentException( 491 "Response closure must accept one or two parameters" ); 492 } 493 494 Object returnVal = responseClosure.call( closureArgs ); 495 log.trace( "response handler result: " + returnVal ); 496 497 return returnVal; 498 } 499 finally { 500 HttpEntity entity = resp.getEntity(); 501 if ( entity != null ) entity.consumeContent(); 502 } 503 } 504 }; 505 506 return getClient().execute(reqMethod, responseHandler, delegate.getContext()); 507 } 508 509 /** 510 * Parse the response data based on the given content-type. 511 * If the given content-type is {@link ContentType#ANY}, the 512 * <code>content-type</code> header from the response will be used to 513 * determine how to parse the response. 514 * @param resp 515 * @param contentType 516 * @return whatever was returned from the parser retrieved for the given 517 * content-type, or <code>null</code> if no parser could be found for this 518 * content-type. The parser will also return <code>null</code> if the 519 * response does not contain any content (e.g. in response to a HEAD request). 520 * @throws HttpResponseException if there is a error parsing the response 521 */ 522 protected Object parseResponse( HttpResponse resp, Object contentType ) 523 throws HttpResponseException { 524 // For HEAD or OPTIONS requests, there should be no response entity. 525 if ( resp.getEntity() == null ) { 526 log.debug( "Response contains no entity. Parsed data is null." ); 527 return null; 528 } 529 // first, start with the _given_ content-type 530 String responseContentType = contentType.toString(); 531 // if the given content-type is ANY ("*/*") then use the response content-type 532 try { 533 if ( ContentType.ANY.toString().equals( responseContentType ) ) 534 responseContentType = ParserRegistry.getContentType( resp ); 535 } 536 catch ( RuntimeException ex ) { 537 log.warn( "Could not parse content-type: " + ex.getMessage() ); 538 /* if for whatever reason we can't determine the content-type, but 539 * still want to attempt to parse the data, use the BINARY 540 * content-type so that the response will be buffered into a 541 * ByteArrayInputStream. */ 542 responseContentType = ContentType.BINARY.toString(); 543 } 544 545 Object parsedData = null; 546 Closure parser = parsers.getAt( responseContentType ); 547 if ( parser == null ) log.warn( "No parser found for content-type: " 548 + responseContentType ); 549 else { 550 log.debug( "Parsing response as: " + responseContentType ); 551 parsedData = parser.call( resp ); 552 if ( parsedData == null ) log.warn( "Parser returned null!" ); 553 else log.debug( "Parsed data to instance of: " + parsedData.getClass() ); 554 } 555 return parsedData; 556 } 557 558 /** 559 * Creates default response handlers for {@link Status#SUCCESS success} and 560 * {@link Status#FAILURE failure} status codes. This is used to populate 561 * the handler map when a new HTTPBuilder instance is created. 562 * @see #defaultSuccessHandler(HttpResponseDecorator, Object) 563 * @see #defaultFailureHandler(HttpResponseDecorator) 564 * @return the default response handler map. 565 */ 566 protected Map<Object,Closure> buildDefaultResponseHandlers() { 567 Map<Object,Closure> map = new StringHashMap<Closure>(); 568 map.put( Status.SUCCESS, 569 new MethodClosure(this,"defaultSuccessHandler")); 570 map.put( Status.FAILURE, 571 new MethodClosure(this,"defaultFailureHandler")); 572 573 return map; 574 } 575 576 /** 577 * <p>This is the default <code>response.success</code> handler. It will be 578 * executed if the response is not handled by a status-code-specific handler 579 * (i.e. <code>response.'200'= {..}</code>) and no generic 'success' handler 580 * is given (i.e. <code>response.success = {..}</code>.) This handler simply 581 * returns the parsed data from the response body. In most cases you will 582 * probably want to define a <code>response.success = {...}</code> handler 583 * from the request closure, which will replace the response handler defined 584 * by this method. </p> 585 * 586 * <h4>Note for parsers that return streaming content:</h4> 587 * <p>For responses parsed as {@link ParserRegistry#parseStream(HttpResponse) 588 * BINARY} or {@link ParserRegistry#parseText(HttpResponse) TEXT}, the 589 * parser will return streaming content -- an <code>InputStream</code> or 590 * <code>Reader</code>. In these cases, this handler will buffer the the 591 * response content before the network connection is closed. </p> 592 * 593 * <p>In practice, a user-supplied response handler closure is 594 * <i>designed</i> to handle streaming content so it can be read directly from 595 * the response stream without buffering, which will be much more efficient. 596 * Therefore, it is recommended that request method variants be used which 597 * explicitly accept a response handler closure in these cases.</p> 598 * 599 * @param resp HTTP response 600 * @param parsedData parsed data as resolved from this instance's {@link ParserRegistry} 601 * @return the parsed data object (whatever the parser returns). 602 * @throws ResponseParseException if there is an error buffering a streaming 603 * response. 604 */ 605 protected Object defaultSuccessHandler( HttpResponseDecorator resp, Object parsedData ) 606 throws ResponseParseException { 607 try { 608 //If response is streaming, buffer it in a byte array: 609 if ( parsedData instanceof InputStream ) { 610 ByteArrayOutputStream buffer = new ByteArrayOutputStream(); 611 DefaultGroovyMethods.leftShift( buffer, (InputStream)parsedData ); 612 parsedData = new ByteArrayInputStream( buffer.toByteArray() ); 613 } 614 else if ( parsedData instanceof Reader ) { 615 StringWriter buffer = new StringWriter(); 616 DefaultGroovyMethods.leftShift( buffer, (Reader)parsedData ); 617 parsedData = new StringReader( buffer.toString() ); 618 } 619 else if ( parsedData instanceof Closeable ) 620 log.warn( "Parsed data is streaming, but will be accessible after " + 621 "the network connection is closed. Use at your own risk!" ); 622 return parsedData; 623 } 624 catch ( IOException ex ) { 625 throw new ResponseParseException( resp, ex ); 626 } 627 } 628 629 /** 630 * This is the default <code>response.failure</code> handler. It will be 631 * executed if no status-code-specific handler is set (i.e. 632 * <code>response.'404'= {..}</code>). This default handler will throw a 633 * {@link HttpResponseException} when executed. In most cases you 634 * will want to define your own <code>response.failure = {...}</code> 635 * handler from the request closure, if you don't want an exception to be 636 * thrown for 4xx and 5xx status responses. 637 638 * @param resp 639 * @throws HttpResponseException 640 */ 641 protected void defaultFailureHandler( HttpResponseDecorator resp ) throws HttpResponseException { 642 throw new HttpResponseException( resp ); 643 } 644 645 /** 646 * Retrieve the map of response code handlers. Each map key is a response 647 * code as a string (i.e. '401') or either 'success' or 'failure'. Use this 648 * to set default response handlers, e.g. 649 * <pre>builder.handler.'401' = { resp -> println "${resp.statusLine}" }</pre> 650 * @see Status 651 * @return 652 */ 653 public Map<?,Closure> getHandler() { 654 return this.defaultResponseHandlers; 655 } 656 657 /** 658 * Retrieve the map of registered response content-type parsers. Use 659 * this to set default response parsers, e.g. 660 * <pre> 661 * builder.parser.'text/javascript' = { resp -> 662 * return resp.entity.content // just returns an InputStream 663 * }</pre> 664 * @return 665 */ 666 public ParserRegistry getParser() { 667 return this.parsers; 668 } 669 670 /** 671 * Retrieve the map of registered request content-type encoders. Use this 672 * to customize a request encoder for specific content-types, e.g. 673 * <pre> 674 * builder.encoder.'text/javascript' = { body -> 675 * def json = body.call( new JsonGroovyBuilder() ) 676 * return new StringEntity( json.toString() ) 677 * }</pre> 678 * By default this map is populated by calling 679 * {@link EncoderRegistry#buildDefaultEncoderMap()}. This method is also 680 * used by {@link RequestConfigDelegate} to retrieve the proper encoder for building 681 * the request content body. 682 * 683 * @return a map of 'encoder' closures, keyed by content-type string. 684 */ 685 public EncoderRegistry getEncoder() { 686 return this.encoders; 687 } 688 689 /** 690 * Set the default content type that will be used to select the appropriate 691 * request encoder and response parser. The {@link ContentType} enum holds 692 * some common content-types that may be used, i.e. <pre> 693 * import static ContentType.* 694 * builder.contentType = XML 695 * </pre> 696 * Setting the default content-type does three things: 697 * <ol> 698 * <li>It tells the builder to encode any {@link RequestConfigDelegate#setBody(Object) 699 * request body} as this content-type. Calling {@link 700 * RequestConfigDelegate#setRequestContentType(String)} can override this 701 * on a per-request basis.</li> 702 * <li>Tells the builder to parse any response as this content-type, 703 * regardless of any <code>content-type</code> header that is sent in the 704 * response.</li> 705 * <li>Sets the <code>Accept</code> header to this content-type for all 706 * requests (see {@link ContentType#getAcceptHeader()}). Note 707 * that any <code>Accept</code> header explicitly set either in 708 * {@link #setHeaders(Map)} or {@link RequestConfigDelegate#setHeaders(Map)} 709 * will override this value.</li> 710 * </ol> 711 * <p>Additionally, if the content-type is set to {@link ContentType#ANY}, 712 * HTTPBuilder <i>will</i> rely on the <code>content-type</code> response 713 * header to determine how to parse the response data. This allows the user 714 * to rely on response headers if they are accurate, or ignore them and 715 * forcibly use a certain response parser if so desired.</p> 716 * 717 * <p>This value is a default and may always be overridden on a per-request 718 * basis by using the {@link #request(Method, Object, Closure) 719 * builder.request( Method, ContentType, Closure )} method or passing a 720 * <code>contentType</code> named parameter. 721 * @see EncoderRegistry 722 * @see ParserRegistry 723 * @param ct either a {@link ContentType} or string value (i.e. <code>"text/xml"</code>.) 724 */ 725 public void setContentType( Object ct ) { 726 this.defaultContentType = ct; 727 } 728 729 /** 730 * @return default content type used for request and response. 731 */ 732 public Object getContentType() { 733 return this.defaultContentType; 734 } 735 736 /** 737 * Indicate whether or not this cliernt should send an <code>Accept</code> 738 * header automatically based on the {@link #getContentType() contentType} 739 * property. 740 * @param shouldSendAcceptHeader <code>true</code> if the client should 741 * automatically insert an <code>Accept</code> header, otherwise <code>false</code>. 742 */ 743 public void setAutoAcceptHeader( boolean shouldSendAcceptHeader ) { 744 this.autoAcceptHeader = shouldSendAcceptHeader; 745 } 746 747 /** 748 * Indicates whether or not this client should automatically send an 749 * <code>Accept</code> header based on the {@link #getContentType() contentType} 750 * property. Default is <code>true</code>. 751 * @return <code>true</code> if the client should automatically add an 752 * <code>Accept</code> header to the request; if <code>false</code>, no 753 * header is added. 754 */ 755 public boolean isAutoAcceptHeader() { 756 return this.autoAcceptHeader; 757 } 758 759 /** 760 * Set acceptable request and response content-encodings. 761 * @see ContentEncodingRegistry 762 * @param encodings each Object should be either a 763 * {@link ContentEncoding.Type} value, or a <code>content-encoding</code> 764 * string that is known by the {@link ContentEncodingRegistry} 765 */ 766 public void setContentEncoding( Object... encodings ) { 767 HttpClient client = getClient(); 768 if ( client instanceof AbstractHttpClient ) { 769 this.contentEncodingHandler.setInterceptors( (AbstractHttpClient)client, encodings ); 770 } else { 771 throw new IllegalStateException("The HttpClient is not an AbstractHttpClient!"); 772 } 773 774 } 775 776 /** 777 * Set the default URI used for requests that do not explicitly take a 778 * <code>uri</code> param. 779 * @param uri either a {@link URL}, {@link URI} or object whose 780 * <code>toString()</code> produces a valid URI string. See 781 * {@link URIBuilder#convertToURI(Object)}. 782 * @throws URISyntaxException if the uri argument does not represent a valid URI 783 */ 784 public void setUri( Object uri ) throws URISyntaxException { 785 this.defaultURI = uri != null ? new URIBuilder( convertToURI( uri ) ) : null; 786 } 787 788 /** 789 * Get the default URI used for requests that do not explicitly take a 790 * <code>uri</code> param. 791 * @return a {@link URIBuilder} instance. Note that the return type is Object 792 * simply so that it matches with its JavaBean {@link #setUri(Object)} 793 * counterpart. 794 */ 795 public Object getUri() { 796 return defaultURI; 797 } 798 799 /** 800 * Set the default headers to add to all requests made by this builder 801 * instance. These values will replace any previously set default headers. 802 * @param headers map of header names & values. 803 */ 804 public void setHeaders( Map<?,?> headers ) { 805 this.defaultRequestHeaders.clear(); 806 if ( headers == null ) return; 807 for( Object key : headers.keySet() ) { 808 Object val = headers.get( key ); 809 if ( val == null ) continue; 810 this.defaultRequestHeaders.put( key.toString(), val.toString() ); 811 } 812 } 813 814 /** 815 * Get the map of default headers that will be added to all requests. 816 * This is a 'live' collection so it may be used to add or remove default 817 * values. 818 * @return the map of default header names and values. 819 */ 820 public Map<?,?> getHeaders() { 821 return this.defaultRequestHeaders; 822 } 823 824 /** 825 * Return the underlying HTTPClient that is used to handle HTTP requests. 826 * @return the client instance. 827 */ 828 public HttpClient getClient() { 829 if (client == null) { 830 HttpParams defaultParams = new BasicHttpParams(); 831 defaultParams.setParameter( CookieSpecPNames.DATE_PATTERNS, 832 Arrays.asList("EEE, dd-MMM-yyyy HH:mm:ss z", "EEE, dd MMM yyyy HH:mm:ss z") ); 833 client = createClient(defaultParams); 834 } 835 return client; 836 } 837 838 public void setClient(HttpClient client) { 839 this.client = client; 840 } 841 842 /** 843 * Override this method in a subclass to customize creation of the 844 * HttpClient instance. 845 * @param params 846 * @return 847 */ 848 protected HttpClient createClient( HttpParams params ) { 849 return new DefaultHttpClient(params); 850 } 851 852 /** 853 * Used to access the {@link AuthConfig} handler used to configure common 854 * authentication mechanism. Example: 855 * <pre>builder.auth.basic( 'myUser', 'somePassword' )</pre> 856 * @return 857 */ 858 public AuthConfig getAuth() { return this.auth; } 859 860 /** 861 * Set an alternative {@link AuthConfig} implementation to handle 862 * authorization. 863 * @param ac instance to use. 864 */ 865 public void setAuthConfig( AuthConfig ac ) { 866 this.auth = ac; 867 } 868 869 /** 870 * Set a custom registry used to handle different request 871 * <code>content-type</code>s. 872 * @param er 873 */ 874 public void setEncoderRegistry( EncoderRegistry er ) { 875 this.encoders = er; 876 } 877 878 /** 879 * Set a custom registry used to handle different response 880 * <code>content-type</code>s 881 * @param pr 882 */ 883 public void setParserRegistry( ParserRegistry pr ) { 884 this.parsers = pr; 885 } 886 887 /** 888 * Set a custom registry used to handle different 889 * <code>content-encoding</code> types in responses. 890 * @param cer 891 */ 892 public void setContentEncodingRegistry( ContentEncodingRegistry cer ) { 893 this.contentEncodingHandler = cer; 894 } 895 896 /** 897 * Set the default HTTP proxy to be used for all requests. 898 * @see HttpHost#HttpHost(String, int, String) 899 * @param host host name or IP 900 * @param port port, or -1 for the default port 901 * @param scheme usually "http" or "https," or <code>null</code> for the default 902 */ 903 public void setProxy( String host, int port, String scheme ) { 904 getClient().getParams().setParameter( 905 ConnRoutePNames.DEFAULT_PROXY, 906 new HttpHost(host,port,scheme) ); 907 } 908 909 /** 910 * Release any system resources held by this instance. 911 * @see ClientConnectionManager#shutdown() 912 */ 913 public void shutdown() { 914 getClient().getConnectionManager().shutdown(); 915 } 916 917 918 919 /** 920 * <p>Encloses all properties and method calls used within the 921 * {@link HTTPBuilder#request(Object, Method, Object, Closure)} 'config' 922 * closure argument. That is, an instance of this class is set as the 923 * closure's delegate. This allows the user to configure various parameters 924 * within the scope of a single request. </p> 925 * 926 * <p>All properties of this class are available from within the closure. 927 * For example, you can manipulate various aspects of the 928 * {@link HTTPBuilder#setUri(Object) default request URI} for this request 929 * by calling <code>uri.path = '/api/location'</code>. This allows for the 930 * ability to modify parameters per-request while leaving any values set 931 * directly on the HTTPBuilder instance unchanged for subsequent requests. 932 * </p> 933 * 934 */ 935 protected class RequestConfigDelegate { 936 private HttpRequestBase request; 937 private Object contentType; 938 private Object requestContentType; 939 private Map<Object,Closure> responseHandlers = new StringHashMap<Closure>(); 940 private URIBuilder uri; 941 private Map<Object,Object> headers = new StringHashMap<Object>(); 942 private HttpContextDecorator context = new HttpContextDecorator(); 943 private Object body; 944 945 public RequestConfigDelegate( HttpRequestBase request, Object contentType, 946 Map<?,?> defaultRequestHeaders, 947 Map<?,Closure> defaultResponseHandlers ) { 948 if ( request == null ) throw new IllegalArgumentException( 949 "Internal error - HttpRequest instance cannot be null" ); 950 this.request = request; 951 this.headers.putAll( defaultRequestHeaders ); 952 this.contentType = contentType; 953 if ( defaultRequestContentType != null ) 954 this.requestContentType = defaultRequestContentType.toString(); 955 this.responseHandlers.putAll( defaultResponseHandlers ); 956 URI uri = request.getURI(); 957 if ( uri != null ) this.uri = new URIBuilder(uri); 958 } 959 960 public RequestConfigDelegate( Map<String,?> args, HttpRequestBase request, Closure successHandler ) 961 throws URISyntaxException { 962 this( request, defaultContentType, defaultRequestHeaders, defaultResponseHandlers ); 963 if ( successHandler != null ) 964 this.responseHandlers.put( Status.SUCCESS.toString(), successHandler ); 965 setPropertiesFromMap( args ); 966 } 967 968 /** 969 * Use this object to manipulate parts of the request URI, like 970 * query params and request path. Example: 971 * <pre> 972 * builder.request(GET,XML) { 973 * uri.path = '../other/request.jsp' 974 * uri.query = [p1:1, p2:2] 975 * ... 976 * }</pre> 977 * 978 * <p>This method signature returns <code>Object</code> so that the 979 * complementary {@link #setUri(Object)} method can accept various 980 * types. </p> 981 * @return {@link URIBuilder} to manipulate the request URI 982 */ 983 public URIBuilder getUri() { return this.uri; } 984 985 /** 986 * <p>Set the entire URI to be used for this request. Acceptable 987 * parameter types are: 988 * <ul> 989 * <li><code>URL</code></li> 990 * <li><code>URI</code></li> 991 * <li><code>URIBuilder</code></li> 992 * </ul> 993 * Any other parameter type will be assumed that its 994 * <code>toString()</code> method produces a valid URI.</p> 995 * 996 * <p>Note that if you want to change just a portion of the request URI, 997 * (e.g. the host, port, path, etc.) you can call {@link #getUri()} 998 * which will return a {@link URIBuilder} which can manipulate portions 999 * of the request URI.</p> 1000 * 1001 * @see URIBuilder#convertToURI(Object) 1002 * @throws URISyntaxException if an argument is given that is not a valid URI 1003 * @param uri the URI to use for this request. 1004 */ 1005 public void setUri( Object uri ) throws URISyntaxException { 1006 if ( uri instanceof URIBuilder ) this.uri = (URIBuilder)uri; 1007 this.uri = new URIBuilder( convertToURI( uri ) ); 1008 } 1009 1010 /** 1011 * Directly access the Apache HttpClient instance that will 1012 * be used to execute this request. 1013 * @see HttpRequestBase 1014 */ 1015 protected HttpRequestBase getRequest() { return this.request; } 1016 1017 /** 1018 * Get the content-type of any data sent in the request body and the 1019 * expected response content-type. If the request content-type is 1020 * expected to differ from the response content-type (i.e. a URL-encoded 1021 * POST that should return an HTML page) then this value will be used 1022 * for the <i>response</i> content-type, while 1023 * {@link #setRequestContentType(String)} should be used for the request. 1024 * 1025 * @return whatever value was assigned via {@link #setContentType(Object)} 1026 * or passed from the {@link HTTPBuilder#defaultContentType} when this 1027 * RequestConfigDelegate instance was constructed. 1028 */ 1029 protected Object getContentType() { return this.contentType; } 1030 1031 /** 1032 * Set the content-type used for any data in the request body, as well 1033 * as the <code>Accept</code> content-type that will be used for parsing 1034 * the response. The value should be either a {@link ContentType} value 1035 * or a String, i.e. <code>"text/plain"</code>. This will default to 1036 * {@link HTTPBuilder#getContentType()} for requests that do not 1037 * explicitly pass a <code>contentType</code> parameter (such as 1038 * {@link HTTPBuilder#request(Method, Object, Closure)}). 1039 * @param ct the value that will be used for the <code>Content-Type</code> 1040 * and <code>Accept</code> request headers. 1041 */ 1042 protected void setContentType( Object ct ) { 1043 if ( ct == null ) this.contentType = defaultContentType; 1044 else this.contentType = ct; 1045 } 1046 1047 /** 1048 * The request content-type, if different from the {@link #contentType}. 1049 * @return either a {@link ContentType} value or String like <code>text/plain</code> 1050 */ 1051 protected Object getRequestContentType() { 1052 if ( this.requestContentType != null ) return this.requestContentType; 1053 else return this.getContentType(); 1054 } 1055 1056 /** 1057 * <p>Assign a different content-type for the request than is expected for 1058 * the response. This is useful if i.e. you want to post URL-encoded 1059 * form data but expect the response to be XML or HTML. The 1060 * {@link #getContentType()} will always control the <code>Accept</code> 1061 * header, and will be used for the request content <i>unless</i> this 1062 * value is also explicitly set.</p> 1063 * <p>Note that this method is used internally; calls within a request 1064 * configuration closure should call {@link #send(Object, Object)} 1065 * to set the request body and content-type at the same time.</p> 1066 * @param ct either a {@link ContentType} value or a valid content-type 1067 * String. 1068 */ 1069 protected void setRequestContentType( Object ct ) { 1070 this.requestContentType = ct; 1071 } 1072 1073 /** 1074 * Valid arguments: 1075 * <dl> 1076 * <dt>uri</dt><dd>Either a URI, URL, or object whose 1077 * <code>toString()</code> method produces a valid URI string. 1078 * If this parameter is not supplied, the HTTPBuilder's default 1079 * URI is used.</dd> 1080 * <dt>path</dt><dd>Request path that is merged with the URI</dd> 1081 * <dt>queryString</dt><dd>an escaped query string</dd> 1082 * <dt>query</dt><dd>Map of URL query parameters</dd> 1083 * <dt>headers</dt><dd>Map of HTTP headers</dd> 1084 * <dt>contentType</dt><dd>Request content type and Accept header. 1085 * If not supplied, the HTTPBuilder's default content-type is used.</dd> 1086 * <dt>requestContentType</dt><dd>content type for the request, if it 1087 * is different from the expected response content-type</dd> 1088 * <dt>body</dt><dd>Request body that will be encoded based on the given contentType</dd> 1089 * </dl> 1090 * Note that if both <code>queryString</code> and <code>query</code> are given, 1091 * <code>query</code> will be merged with (and potentially override) 1092 * the parameters given as part of <code>queryString</code>. 1093 * @param args named parameters to set properties on this delegate. 1094 * @throws URISyntaxException if the uri argument does not represent a valid URI 1095 */ 1096 @SuppressWarnings("unchecked") 1097 protected void setPropertiesFromMap( Map<String,?> args ) throws URISyntaxException { 1098 if ( args == null ) return; 1099 if ( args.containsKey( "url" ) ) throw new IllegalArgumentException( 1100 "The 'url' parameter is deprecated; use 'uri' instead" ); 1101 Object uri = args.remove( "uri" ); 1102 if ( uri == null ) uri = defaultURI; 1103 if ( uri == null ) throw new IllegalStateException( 1104 "Default URI is null, and no 'uri' parameter was given" ); 1105 this.uri = new URIBuilder( convertToURI( uri ) ); 1106 1107 Map query = (Map)args.remove( "params" ); 1108 if ( query != null ) { 1109 log.warn( "'params' argument is deprecated; use 'query' instead." ); 1110 this.uri.setQuery( query ); 1111 } 1112 String queryString = (String)args.remove("queryString"); 1113 if ( queryString != null ) this.uri.setRawQuery(queryString); 1114 1115 query = (Map)args.remove( "query" ); 1116 if ( query != null ) this.uri.addQueryParams( query ); 1117 Map headers = (Map)args.remove( "headers" ); 1118 if ( headers != null ) this.getHeaders().putAll( headers ); 1119 1120 Object path = args.remove( "path" ); 1121 if ( path != null ) this.uri.setPath( path.toString() ); 1122 1123 Object contentType = args.remove( "contentType" ); 1124 if ( contentType != null ) this.setContentType( contentType ); 1125 1126 contentType = args.remove( "requestContentType" ); 1127 if ( contentType != null ) this.setRequestContentType( contentType ); 1128 1129 Object body = args.remove("body"); 1130 if ( body != null ) this.setBody( body ); 1131 1132 if ( args.size() > 0 ) { 1133 String invalidArgs = ""; 1134 for ( String k : args.keySet() ) invalidArgs += k + ","; 1135 throw new IllegalArgumentException("Unexpected keyword args: " + invalidArgs); 1136 } 1137 } 1138 1139 /** 1140 * Set request headers. These values will be <strong>merged</strong> 1141 * with any {@link HTTPBuilder#getHeaders() default request headers.} 1142 * (The assumption is you'll probably want to add a bunch of headers to 1143 * whatever defaults you've already set). If you <i>only</i> want to 1144 * use values set here, simply call {@link #getHeaders() headers.clear()} 1145 * first. 1146 */ 1147 public void setHeaders( Map<?,?> newHeaders ) { 1148 this.headers.putAll( newHeaders ); 1149 } 1150 1151 /** 1152 * <p>Get request headers (including any default headers set on this 1153 * {@link HTTPBuilder#setHeaders(Map) HTTPBuilder instance}). Note that 1154 * this will not include any <code>Accept</code>, <code>Content-Type</code>, 1155 * or <code>Content-Encoding</code> headers that are automatically 1156 * handled by any encoder or parsers in effect. Note that any values 1157 * set here <i>will</i> override any of those automatically assigned 1158 * values.</p> 1159 * 1160 * <p>Example: <code>headers.'Accept-Language' = 'en, en-gb;q=0.8'</code></p> 1161 * @return a map of HTTP headers that will be sent in the request. 1162 */ 1163 public Map<?,?> getHeaders() { 1164 return this.headers; 1165 } 1166 1167 /** 1168 * Convenience method to set a request content-type at the same time 1169 * the request body is set. This is a variation of 1170 * {@link #setBody(Object)} that allows for a different content-type 1171 * than what is expected for the response. 1172 * 1173 * <p>Example: 1174 * <pre> 1175 * http.request(POST,HTML) { 1176 * 1177 * /* request data is interpreted as a JsonBuilder closure by 1178 * HTTPBuilder's default EncoderRegistry implementation * / 1179 * send( 'text/javascript' ) { 1180 * a : ['one','two','three'] 1181 * } 1182 * 1183 * // response content-type is what was specified in the outer request() argument: 1184 * response.success = { resp, html -> 1185 * 1186 * } 1187 * } 1188 * </pre> 1189 * The <code>send</code> call is equivalent to the following: 1190 * <pre> 1191 * requestContentType = 'text/javascript' 1192 * body = { a : ['one','two','three'] } 1193 * </pre> 1194 * 1195 * @param contentType either a {@link ContentType} or equivalent 1196 * content-type string like <code>"text/xml"</code> 1197 * @param requestBody 1198 */ 1199 public void send( Object contentType, Object requestBody ) { 1200 this.setRequestContentType( contentType ); 1201 this.setBody( requestBody ); 1202 } 1203 1204 /** 1205 * Set the request body. This value may be of any type supported by 1206 * the associated {@link EncoderRegistry request encoder}. That is, 1207 * the value of <code>body</code> will be interpreted by the encoder 1208 * associated with the current {@link #getRequestContentType() request 1209 * content-type}. 1210 * @see #send(Object, Object) 1211 * @param body data or closure interpreted as the request body 1212 */ 1213 public void setBody( Object body ) { 1214 this.body = body; 1215 } 1216 1217 public void encodeBody() { 1218 if (body == null) { 1219 return; 1220 } 1221 if ( ! (request instanceof HttpEntityEnclosingRequest ) ) 1222 throw new IllegalArgumentException( 1223 "Cannot set a request body for a " + request.getMethod() + " method" ); 1224 1225 Closure encoder = encoders.getAt( this.getRequestContentType() ); 1226 1227 // Either content type or encoder is empty. 1228 if ( encoder == null ) 1229 throw new IllegalArgumentException( 1230 "No encoder found for request content type " + getRequestContentType() ); 1231 1232 HttpEntity entity = encoder.getMaximumNumberOfParameters() == 2 1233 ? (HttpEntity)encoder.call( new Object[] { body, this.getRequestContentType() } ) 1234 : (HttpEntity)encoder.call( body ); 1235 1236 ((HttpEntityEnclosingRequest)this.request).setEntity( entity ); 1237 } 1238 1239 /** 1240 * Get the proper response handler for the response code. This is called 1241 * by the {@link HTTPBuilder} class in order to find the proper handler 1242 * based on the response status code. 1243 * 1244 * @param statusCode HTTP response status code 1245 * @return the response handler 1246 */ 1247 protected Closure findResponseHandler( int statusCode ) { 1248 Closure handler = this.getResponse().get( Integer.toString( statusCode ) ); 1249 if ( handler == null ) handler = 1250 this.getResponse().get( Status.find( statusCode ).toString() ); 1251 return handler; 1252 } 1253 1254 /** 1255 * Access the response handler map to set response parsing logic. 1256 * i.e.<pre> 1257 * builder.request( GET, XML ) { 1258 * response.success = { xml -> 1259 * /* for XML content type, the default parser 1260 * will return an XmlSlurper * / 1261 * xml.root.children().each { println it } 1262 * } 1263 * }</pre> 1264 * @return 1265 */ 1266 public Map<Object,Closure> getResponse() { return this.responseHandlers; } 1267 1268 /** 1269 * Get the {@link HttpContext} that will be used for this request. By 1270 * default, a new context is created for each request. 1271 * @see ClientContext 1272 * @return 1273 */ 1274 public HttpContextDecorator getContext() { return this.context; } 1275 1276 /** 1277 * Set the {@link HttpContext} that will be used for this request. 1278 * @param ctx 1279 */ 1280 public void setContext( HttpContext ctx ) { this.context = new HttpContextDecorator(ctx); } 1281 } 1282}