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