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