| Classes in this File | Line Coverage | Branch Coverage | Complexity | ||||
| HTTPBuilder |
|
| 0.0;0 | ||||
| HTTPBuilder$SendDelegate |
|
| 0.0;0 |
| 1 | /* | |
| 2 | * Copyright 2003-2008 the original author or authors. | |
| 3 | * | |
| 4 | * Licensed under the Apache License, Version 2.0 (the "License"); | |
| 5 | * you may not use this file except in compliance with the License. | |
| 6 | * You may obtain a copy of the License at | |
| 7 | * | |
| 8 | * http://www.apache.org/licenses/LICENSE-2.0 | |
| 9 | * | |
| 10 | * Unless required by applicable law or agreed to in writing, software | |
| 11 | * distributed under the License is distributed on an "AS IS" BASIS, | |
| 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| 13 | * See the License for the specific language governing permissions and | |
| 14 | * limitations under the License. | |
| 15 | * | |
| 16 | * You are receiving this code free of charge, which represents many hours of | |
| 17 | * effort from other individuals and corporations. As a responsible member | |
| 18 | * of the community, you are asked (but not required) to donate any | |
| 19 | * enhancements or improvements back to the community under a similar open | |
| 20 | * source license. Thank you. -TMN | |
| 21 | */ | |
| 22 | package groovyx.net.http; | |
| 23 | ||
| 24 | import groovy.lang.Closure; | |
| 25 | ||
| 26 | import java.io.IOException; | |
| 27 | import java.net.MalformedURLException; | |
| 28 | import java.net.URI; | |
| 29 | import java.net.URISyntaxException; | |
| 30 | import java.net.URL; | |
| 31 | import java.util.HashMap; | |
| 32 | import java.util.Map; | |
| 33 | ||
| 34 | import org.apache.http.HttpEntity; | |
| 35 | import org.apache.http.HttpEntityEnclosingRequest; | |
| 36 | import org.apache.http.HttpResponse; | |
| 37 | import org.apache.http.client.ClientProtocolException; | |
| 38 | import org.apache.http.client.HttpResponseException; | |
| 39 | import org.apache.http.client.methods.HttpGet; | |
| 40 | import org.apache.http.client.methods.HttpPost; | |
| 41 | import org.apache.http.client.methods.HttpRequestBase; | |
| 42 | import org.apache.http.impl.client.AbstractHttpClient; | |
| 43 | import org.apache.http.impl.client.DefaultHttpClient; | |
| 44 | import org.codehaus.groovy.runtime.DefaultGroovyMethods; | |
| 45 | import org.codehaus.groovy.runtime.MethodClosure; | |
| 46 | ||
| 47 | import static groovyx.net.http.URIBuilder.convertToURI; | |
| 48 | ||
| 49 | /** <p> | |
| 50 | * Groovy DSL for easily making HTTP requests, and handling request and response | |
| 51 | * data. This class adds a number of convenience mechanisms built on top of | |
| 52 | * Apache HTTPClient for things like URL-encoded POSTs and REST requests that | |
| 53 | * require building and parsing JSON or XML. Convenient access to a few common | |
| 54 | * authentication methods is also available.</p> | |
| 55 | * | |
| 56 | * <h3>Examples:</h3> | |
| 57 | * Perform an HTTP GET and print the response: | |
| 58 | * <pre> | |
| 59 | * def http = new HTTPBuilder('http://www.google.com') | |
| 60 | * | |
| 61 | * http.get( path:'/search', query:[q:'Groovy'] ) { resp, reader -> | |
| 62 | * println "response status: ${resp.statusLine}" | |
| 63 | * println 'Response data: -----' | |
| 64 | * System.out << reader | |
| 65 | * println '\n--------------------' | |
| 66 | * } | |
| 67 | * </pre> | |
| 68 | * | |
| 69 | * Long form for other HTTP methods, and response-code-specific handlers: | |
| 70 | * | |
| 71 | * <pre> | |
| 72 | * http.request(GET,TEXT) { req -> | |
| 73 | * response.success = { resp, stream -> | |
| 74 | * println 'my response handler!' | |
| 75 | * assert resp.statusLine.statusCode == 200 | |
| 76 | * println resp.statusLine | |
| 77 | * //System.out << stream // print response stream | |
| 78 | * } | |
| 79 | * | |
| 80 | * response.'401' = { resp -> | |
| 81 | * println 'access denied' | |
| 82 | * } | |
| 83 | * } | |
| 84 | * </pre> | |
| 85 | * | |
| 86 | * You can also set a default response handler called for any status | |
| 87 | * code > 399 that is not matched to a specific handler. Setting the value | |
| 88 | * outside a request closure means it will apply to all future requests with | |
| 89 | * this HTTPBuilder instance: | |
| 90 | * <pre> | |
| 91 | * http.handler.failure = { resp -> | |
| 92 | * println "Unexpected failure: ${resp.statusLine}" | |
| 93 | * } | |
| 94 | * </pre> | |
| 95 | * | |
| 96 | * | |
| 97 | * And... Automatic response parsing for registered content types! | |
| 98 | * | |
| 99 | * <pre> | |
| 100 | * http.request( 'http://ajax.googleapis.com', GET, JSON ) { | |
| 101 | * url.path = '/ajax/services/search/web' | |
| 102 | * url.query = [ v:'1.0', q: 'Calvin and Hobbes' ] | |
| 103 | * | |
| 104 | * response.success = { resp, json -> | |
| 105 | * assert json.size() == 3 | |
| 106 | * println "Query response: " | |
| 107 | * json.responseData.results.each { | |
| 108 | * println " ${it.titleNoFormatting} : ${it.visibleUrl}" | |
| 109 | * } | |
| 110 | * } | |
| 111 | * } | |
| 112 | * </pre> | |
| 113 | * | |
| 114 | * @author <a href='mailto:tnichols@enernoc.com'>Tom Nichols</a> | |
| 115 | */ | |
| 116 | public class HTTPBuilder { | |
| 117 | ||
| 118 | 5 | protected AbstractHttpClient client = new DefaultHttpClient(); |
| 119 | 5 | protected URI defaultURI = null; // TODO make this a URIBuilder? |
| 120 | 5 | protected AuthConfig auth = new AuthConfig( this ); |
| 121 | ||
| 122 | 5 | protected Object defaultContentType = ContentType.TEXT; |
| 123 | 5 | protected final Map<String,Closure> defaultResponseHandlers = buildDefaultResponseHandlers(); |
| 124 | 5 | protected ContentEncodingRegistry contentEncodingHandler = new ContentEncodingRegistry(); |
| 125 | ||
| 126 | 5 | protected final Map<String,String> defaultRequestHeaders = new HashMap<String,String>(); |
| 127 | ||
| 128 | 5 | protected EncoderRegistry encoders = new EncoderRegistry(); |
| 129 | 5 | protected ParserRegistry parsers = new ParserRegistry(); |
| 130 | ||
| 131 | public HTTPBuilder() { | |
| 132 | 5 | super(); |
| 133 | 5 | this.setContentEncoding( ContentEncoding.Type.GZIP, |
| 134 | ContentEncoding.Type.DEFLATE ); | |
| 135 | 5 | } |
| 136 | ||
| 137 | /** | |
| 138 | * Give a default URL to be used for all request methods that don't | |
| 139 | * explicitly take a URL parameter. | |
| 140 | * @param defaultURL either a {@link URL}, {@link URI} or String | |
| 141 | * @throws URISyntaxException if the URL was not parse-able | |
| 142 | */ | |
| 143 | public HTTPBuilder( Object defaultURL ) throws URISyntaxException { | |
| 144 | 2 | this(); |
| 145 | 2 | this.defaultURI = convertToURI( defaultURL ); |
| 146 | 2 | } |
| 147 | ||
| 148 | /** | |
| 149 | * Give a default URL to be used for all request methods that don't | |
| 150 | * explicitly take a URL parameter, and a default content-type to be used | |
| 151 | * for request encoding and response parsing. | |
| 152 | * @param defaultURL either a {@link URL}, {@link URI} or String | |
| 153 | * @param defaultContentType content-type string. See {@link ContentType} | |
| 154 | * for common types. | |
| 155 | * @throws URISyntaxException if the URL was not parse-able | |
| 156 | */ | |
| 157 | public HTTPBuilder( Object defaultURL, Object defaultContentType ) throws URISyntaxException { | |
| 158 | 0 | this(); |
| 159 | 0 | this.defaultURI = convertToURI( defaultURL ); |
| 160 | 0 | this.defaultContentType = defaultContentType; |
| 161 | 0 | } |
| 162 | ||
| 163 | /** | |
| 164 | * Convenience method to perform an HTTP GET. The response closure will be | |
| 165 | * called only on a successful response; a 'failed' response (i.e. any | |
| 166 | * HTTP status code > 399) will be handled by the registered 'failure' | |
| 167 | * handler. The {@link #defaultFailureHandler(HttpResponse) default | |
| 168 | * failure handler} throws an {@link HttpResponseException} | |
| 169 | * @param args see {@link SendDelegate#setPropertiesFromMap(Map)} | |
| 170 | * @param responseClosure code to handle a successful HTTP response | |
| 171 | * @return any value returned by the response closure. | |
| 172 | * @throws ClientProtocolException | |
| 173 | * @throws IOException | |
| 174 | * @throws URISyntaxException | |
| 175 | */ | |
| 176 | public Object get( Map<String,?> args, Closure responseClosure ) | |
| 177 | throws ClientProtocolException, IOException, URISyntaxException { | |
| 178 | 1 | SendDelegate delegate = new SendDelegate( new HttpGet(), |
| 179 | this.defaultContentType, | |
| 180 | this.defaultRequestHeaders, | |
| 181 | this.defaultResponseHandlers ); | |
| 182 | ||
| 183 | 1 | delegate.setPropertiesFromMap( args ); |
| 184 | 1 | delegate.getResponse().put( Status.SUCCESS.toString(), responseClosure ); |
| 185 | 1 | return this.doRequest( delegate ); |
| 186 | } | |
| 187 | ||
| 188 | /** <p> | |
| 189 | * Convenience method to perform an HTTP form POST. The response closure will be | |
| 190 | * called only on a successful response; a 'failed' response (i.e. any | |
| 191 | * HTTP status code > 399) will be handled by the registered 'failure' | |
| 192 | * handler. The {@link #defaultFailureHandler(HttpResponse) default | |
| 193 | * failure handler} throws an {@link HttpResponseException}.</p> | |
| 194 | * | |
| 195 | * <p>The request body (specified by a <code>body</code> named parameter) | |
| 196 | * will be converted to a url-encoded form string unless a different | |
| 197 | * <code>requestContentType</code> named parameter is passed to this method. | |
| 198 | * (See {@link EncoderRegistry#encodeForm(Map)}.) </p> | |
| 199 | * | |
| 200 | * @param args see {@link SendDelegate#setPropertiesFromMap(Map)} | |
| 201 | * @param responseClosure code to handle a successful HTTP response | |
| 202 | * @return any value returned by the response closure. | |
| 203 | * @throws ClientProtocolException | |
| 204 | * @throws IOException | |
| 205 | * @throws URISyntaxException | |
| 206 | */ | |
| 207 | public Object post( Map<String,?> args, Closure responseClosure ) | |
| 208 | throws URISyntaxException, ClientProtocolException, IOException { | |
| 209 | 0 | SendDelegate delegate = new SendDelegate( new HttpPost(), |
| 210 | this.defaultContentType, | |
| 211 | this.defaultRequestHeaders, | |
| 212 | this.defaultResponseHandlers ); | |
| 213 | ||
| 214 | /* by default assume the request body will be URLEncoded, but allow | |
| 215 | the 'requestContentType' named argument to override this if it is | |
| 216 | given */ | |
| 217 | 0 | delegate.setRequestContentType( ContentType.URLENC.toString() ); |
| 218 | 0 | delegate.setPropertiesFromMap( args ); |
| 219 | 0 | delegate.getResponse().put( Status.SUCCESS.toString(), responseClosure ); |
| 220 | ||
| 221 | 0 | return this.doRequest( delegate ); |
| 222 | } | |
| 223 | ||
| 224 | public Object request( Method m, Closure configClosure ) throws ClientProtocolException, IOException { | |
| 225 | 0 | return this.doRequest( this.defaultURI, m, this.defaultContentType, configClosure ); |
| 226 | } | |
| 227 | ||
| 228 | public Object request( Method m, Object contentType, Closure configClosure ) | |
| 229 | throws ClientProtocolException, IOException { | |
| 230 | 1 | return this.doRequest( this.defaultURI, m, contentType, configClosure ); |
| 231 | } | |
| 232 | ||
| 233 | /** | |
| 234 | * Make a request for the given HTTP method and content-type, with | |
| 235 | * additional options configured in the <code>configClosure</code>. See | |
| 236 | * {@link SendDelegate} for options. | |
| 237 | * @param uri either a URI, URL, or String | |
| 238 | * @param method {@link Method HTTP method} | |
| 239 | * @param contentType either a {@link ContentType} or valid content-type string. | |
| 240 | * @param configClosure closure from which to configure options like | |
| 241 | * {@link SendDelegate#getURL() url.path}, | |
| 242 | * {@link URIBuilder#setQuery(Map) request parameters}, | |
| 243 | * {@link SendDelegate#setHeaders(Map) headers}, | |
| 244 | * {@link SendDelegate#setBody(Object) request body} and | |
| 245 | * {@link SendDelegate#getResponse() response handlers}. | |
| 246 | * | |
| 247 | * @return whatever value was returned by the executed response handler. | |
| 248 | * @throws IllegalAccessException | |
| 249 | * @throws InstantiationException | |
| 250 | * @throws ClientProtocolException | |
| 251 | * @throws IOException | |
| 252 | * @throws URISyntaxException if a URI string or URL was invalid. | |
| 253 | */ | |
| 254 | public Object request( Object uri, Method method, Object contentType, Closure configClosure ) | |
| 255 | throws ClientProtocolException, IOException, URISyntaxException { | |
| 256 | 2 | return this.doRequest( convertToURI( uri ), method, contentType, configClosure ); |
| 257 | } | |
| 258 | ||
| 259 | protected Object doRequest( URI uri, Method method, Object contentType, Closure configClosure ) | |
| 260 | throws ClientProtocolException, IOException { | |
| 261 | ||
| 262 | HttpRequestBase reqMethod; | |
| 263 | 3 | try { reqMethod = method.getRequestType().newInstance(); |
| 264 | // this exception should reasonably never occur: | |
| 265 | 3 | } catch ( Exception e ) { throw new RuntimeException( e ); } |
| 266 | ||
| 267 | 3 | reqMethod.setURI( uri ); |
| 268 | 3 | SendDelegate delegate = new SendDelegate( reqMethod, contentType, |
| 269 | this.defaultRequestHeaders, | |
| 270 | this.defaultResponseHandlers ); | |
| 271 | 3 | configClosure.setDelegate( delegate ); |
| 272 | 3 | configClosure.call( client ); |
| 273 | ||
| 274 | 3 | return this.doRequest( delegate ); |
| 275 | } | |
| 276 | ||
| 277 | protected Object doRequest( SendDelegate delegate ) throws ClientProtocolException, IOException { | |
| 278 | ||
| 279 | 4 | HttpRequestBase reqMethod = delegate.getRequest(); |
| 280 | ||
| 281 | 4 | Object contentType = delegate.getContentType(); |
| 282 | 4 | String acceptContentTypes = contentType.toString(); |
| 283 | 4 | if ( contentType instanceof ContentType ) |
| 284 | 4 | acceptContentTypes = ((ContentType)contentType).getAcceptHeader(); |
| 285 | ||
| 286 | 4 | reqMethod.setHeader( "Accept", acceptContentTypes ); |
| 287 | 4 | reqMethod.setURI( delegate.getURL().toURI() ); |
| 288 | ||
| 289 | // set any request headers from the delegate | |
| 290 | 4 | Map<String,String> headers = delegate.getHeaders(); |
| 291 | 4 | for ( String key : headers.keySet() ) reqMethod.setHeader( key, headers.get( key ) ); |
| 292 | ||
| 293 | 4 | HttpResponse resp = client.execute( reqMethod ); |
| 294 | 4 | Closure responseClosure = delegate.findResponseHandler( |
| 295 | resp.getStatusLine().getStatusCode() ); | |
| 296 | ||
| 297 | 4 | Object[] closureArgs = null; |
| 298 | 4 | switch ( responseClosure.getMaximumNumberOfParameters() ) { |
| 299 | case 1 : | |
| 300 | 1 | closureArgs = new Object[] { resp }; |
| 301 | 1 | break; |
| 302 | case 2 : | |
| 303 | 3 | String responseContentType = parsers.getContentType( resp ); |
| 304 | 3 | Object parsedData = parsers.get( responseContentType ).call( resp ); |
| 305 | 3 | closureArgs = new Object[] { resp, parsedData }; |
| 306 | 3 | break; |
| 307 | default: | |
| 308 | 0 | throw new IllegalArgumentException( |
| 309 | "Response closure must accept one or two parameters" ); | |
| 310 | } | |
| 311 | ||
| 312 | 4 | Object returnVal = responseClosure.call( closureArgs ); |
| 313 | 4 | if ( resp.getEntity().isStreaming() ) resp.getEntity().consumeContent(); |
| 314 | 4 | return returnVal; |
| 315 | } | |
| 316 | ||
| 317 | protected Map<String,Closure> buildDefaultResponseHandlers() { | |
| 318 | 5 | Map<String,Closure> map = new HashMap<String, Closure>(); |
| 319 | 5 | map.put( Status.SUCCESS.toString(), |
| 320 | new MethodClosure(this,"defaultSuccessHandler")); | |
| 321 | 5 | map.put( Status.FAILURE.toString(), |
| 322 | new MethodClosure(this,"defaultFailureHandler")); | |
| 323 | ||
| 324 | 5 | return map; |
| 325 | } | |
| 326 | ||
| 327 | /** | |
| 328 | * This is the default <code>response.success</code> handler. It will be | |
| 329 | * executed if no status-code-specific handler is set (i.e. | |
| 330 | * <code>response.'200'= {..}</code>). This simply prints the status line | |
| 331 | * and the response stream to <code>System.out</code>. In most cases you | |
| 332 | * will want to define a <code>response.success = {...}</code> handler from | |
| 333 | * the request closure, which will replace this method. | |
| 334 | * | |
| 335 | * @param resp | |
| 336 | * @throws IllegalStateException | |
| 337 | * @throws IOException | |
| 338 | */ | |
| 339 | protected void defaultSuccessHandler( HttpResponse resp ) throws IllegalStateException, IOException { | |
| 340 | 0 | System.out.println( resp.getStatusLine() ); |
| 341 | 0 | System.out.println( DefaultGroovyMethods.getText( resp.getEntity().getContent() ) ); |
| 342 | 0 | } |
| 343 | ||
| 344 | /** | |
| 345 | * This is the default <code>response.failure</code> handler. It will be | |
| 346 | * executed if no status-code-specific handler is set (i.e. | |
| 347 | * <code>response.'404'= {..}</code>). This default handler will throw a | |
| 348 | * {@link HttpResponseException} when executed. In most cases you | |
| 349 | * will want to define your own <code>response.failure = {...}</code> | |
| 350 | * handler from the request closure, if you don't want an exception to be | |
| 351 | * thrown for a 4xx and 5xx status response. | |
| 352 | ||
| 353 | * @param resp | |
| 354 | * @throws HttpResponseException | |
| 355 | */ | |
| 356 | protected void defaultFailureHandler( HttpResponse resp ) throws HttpResponseException { | |
| 357 | 0 | throw new HttpResponseException( resp.getStatusLine().getStatusCode(), |
| 358 | resp.getStatusLine().getReasonPhrase() ); | |
| 359 | } | |
| 360 | ||
| 361 | /** | |
| 362 | * Retrieve the map of response code handlers. Each map key is a response | |
| 363 | * code as a string (i.e. '401') or either 'success' or 'failure'. Use this | |
| 364 | * to set default response handlers, e.g. | |
| 365 | * <pre>builder.handler.'401' = { resp -> println "${resp.statusLine}" }</pre> | |
| 366 | * @see Status | |
| 367 | * @return | |
| 368 | */ | |
| 369 | public Map<String,Closure> getHandler() { | |
| 370 | 2 | return this.defaultResponseHandlers; |
| 371 | } | |
| 372 | ||
| 373 | /** | |
| 374 | * Retrieve the map of registered response content-type parsers. Use | |
| 375 | * this to set default response parsers, e.g. | |
| 376 | * <pre> | |
| 377 | * builder.parser.'text/javascript' = { resp -> | |
| 378 | * return resp.entity.content // just returns an InputStream | |
| 379 | * }</pre> | |
| 380 | * @return | |
| 381 | */ | |
| 382 | public Map<String,Closure> getParser() { | |
| 383 | 0 | return this.parsers.registeredParsers; |
| 384 | } | |
| 385 | ||
| 386 | /** | |
| 387 | * Retrieve the map of registered request content-type encoders. Use this | |
| 388 | * to set a default request encoder, e.g. | |
| 389 | * <pre> | |
| 390 | * builder.encoder.'text/javascript' = { body -> | |
| 391 | * def json = body.call( new JsonGroovyBuilder() ) | |
| 392 | * return new StringEntity( json.toString() ) | |
| 393 | * } | |
| 394 | * @return | |
| 395 | */ | |
| 396 | public Map<String,Closure> getEncoder() { | |
| 397 | 0 | return this.encoders.registeredEncoders; |
| 398 | } | |
| 399 | ||
| 400 | /** | |
| 401 | * Set the default content type that will be used to select the appropriate | |
| 402 | * request encoder and response parser. The {@link ContentType} enum holds | |
| 403 | * some common content-types that may be used, i.e. <pre> | |
| 404 | * import static ContentType.* | |
| 405 | * builder.contentType = XML | |
| 406 | * </pre> | |
| 407 | * @see EncoderRegistry | |
| 408 | * @see ParserRegistry | |
| 409 | * @param ct either a {@link ContentType} or string value (i.e. <code>"text/xml"</code>.) | |
| 410 | */ | |
| 411 | public void setContentType( Object ct ) { | |
| 412 | 0 | this.defaultContentType = ct; |
| 413 | 0 | } |
| 414 | ||
| 415 | ||
| 416 | /** | |
| 417 | * Set acceptable request and response content-encodings. | |
| 418 | * @see ContentEncodingRegistry | |
| 419 | * @param encodings each Object should be either a | |
| 420 | * {@link ContentEncoding.Type} value, or a <code>content-encoding</code> | |
| 421 | * string that is known by the {@link ContentEncodingRegistry} | |
| 422 | */ | |
| 423 | public void setContentEncoding( Object... encodings ) { | |
| 424 | 5 | this.contentEncodingHandler.setInterceptors( client, encodings ); |
| 425 | 5 | } |
| 426 | ||
| 427 | /** | |
| 428 | * Set the default URL used for requests that do not explicitly take a | |
| 429 | * <code>url</code> param. | |
| 430 | * @param url a URL, URI, or String | |
| 431 | * @throws URISyntaxException | |
| 432 | */ | |
| 433 | public void setURL( Object url ) throws URISyntaxException { | |
| 434 | 1 | this.defaultURI = convertToURI( url ); |
| 435 | 1 | } |
| 436 | ||
| 437 | /** | |
| 438 | * Get the default URL used for requests that do not explicitly take a | |
| 439 | * <code>url</code> param. | |
| 440 | * @return url a {@link URL} instance. Note that the return type is Object | |
| 441 | * simply so that it matches with its JavaBean {@link #setURL(Object)} | |
| 442 | * counterpart. | |
| 443 | */ | |
| 444 | public Object getURL() { | |
| 445 | try { | |
| 446 | 1 | return defaultURI.toURL(); |
| 447 | 0 | } catch ( MalformedURLException e ) { |
| 448 | 0 | throw new RuntimeException( e ); |
| 449 | } | |
| 450 | } | |
| 451 | ||
| 452 | /** | |
| 453 | * Set the default headers to add to all requests made by this builder | |
| 454 | * instance. These values will replace any previously set default headers. | |
| 455 | * @param headers map of header names & values. | |
| 456 | */ | |
| 457 | public void setHeaders( Map<?,?> headers ) { | |
| 458 | 0 | this.defaultRequestHeaders.clear(); |
| 459 | 0 | if ( headers == null ) return; |
| 460 | 0 | for( Object key : headers.keySet() ) { |
| 461 | 0 | Object val = headers.get( key ); |
| 462 | 0 | if ( val == null ) continue; |
| 463 | 0 | this.defaultRequestHeaders.put( key.toString(), val.toString() ); |
| 464 | 0 | } |
| 465 | 0 | } |
| 466 | ||
| 467 | /** | |
| 468 | * Get the map of default headers that will be added to all requests. | |
| 469 | * This is a 'live' collection so it may be used to add or remove default | |
| 470 | * values. | |
| 471 | * @return the map of default header names and values. | |
| 472 | */ | |
| 473 | public Map<String,String> getHeaders() { | |
| 474 | 0 | return this.defaultRequestHeaders; |
| 475 | } | |
| 476 | ||
| 477 | /** | |
| 478 | * Return the underlying HTTPClient that is used to handle HTTP requests. | |
| 479 | * @return the client instance. | |
| 480 | */ | |
| 481 | 1 | public AbstractHttpClient getClient() { return this.client; } |
| 482 | ||
| 483 | /** | |
| 484 | * Used to access the {@link AuthConfig} handler used to configure common | |
| 485 | * authentication mechanism. Example: | |
| 486 | * <pre>builder.auth.basic( 'myUser', 'somePassword' )</pre> | |
| 487 | * @return | |
| 488 | */ | |
| 489 | 1 | public AuthConfig getAuth() { return this.auth; } |
| 490 | ||
| 491 | /** | |
| 492 | * Set an alternative {@link AuthConfig} implementation to handle | |
| 493 | * authorization. | |
| 494 | * @param ac instance to use. | |
| 495 | */ | |
| 496 | public void setAuthConfig( AuthConfig ac ) { | |
| 497 | 0 | this.auth = ac; |
| 498 | 0 | } |
| 499 | ||
| 500 | /** | |
| 501 | * Set a custom registry used to handle different request | |
| 502 | * <code>content-type</code>s. | |
| 503 | * @param er | |
| 504 | */ | |
| 505 | public void setEncoderRegistry( EncoderRegistry er ) { | |
| 506 | 0 | this.encoders = er; |
| 507 | 0 | } |
| 508 | ||
| 509 | /** | |
| 510 | * Set a custom registry used to handle different response | |
| 511 | * <code>content-type</code>s | |
| 512 | * @param pr | |
| 513 | */ | |
| 514 | public void setParserRegistry( ParserRegistry pr ) { | |
| 515 | 0 | this.parsers = pr; |
| 516 | 0 | } |
| 517 | ||
| 518 | /** | |
| 519 | * Set a custom registry used to handle different | |
| 520 | * <code>content-encoding</code> types in responses. | |
| 521 | * @param cer | |
| 522 | */ | |
| 523 | public void setContentEncodingRegistry( ContentEncodingRegistry cer ) { | |
| 524 | 0 | this.contentEncodingHandler = cer; |
| 525 | 0 | } |
| 526 | ||
| 527 | ||
| 528 | ||
| 529 | /** | |
| 530 | * Encloses all properties and method calls used within the | |
| 531 | * {@link HTTPBuilder#request(Object, Method, Object, Closure)} 'config' | |
| 532 | * closure argument. | |
| 533 | */ | |
| 534 | protected class SendDelegate { | |
| 535 | protected HttpRequestBase request; | |
| 536 | protected Object contentType; | |
| 537 | protected String requestContentType; | |
| 538 | 4 | protected Map<String,Closure> responseHandlers = new HashMap<String,Closure>(); |
| 539 | protected URIBuilder url; | |
| 540 | 4 | protected Map<String,String> headers = new HashMap<String,String>(); |
| 541 | ||
| 542 | public SendDelegate( HttpRequestBase request, Object contentType, | |
| 543 | Map<String,String> defaultRequestHeaders, | |
| 544 | 4 | Map<String,Closure> defaultResponseHandlers ) { |
| 545 | 4 | this.request = request; |
| 546 | 4 | this.headers.putAll( defaultRequestHeaders ); |
| 547 | 4 | this.contentType = contentType; |
| 548 | 4 | this.responseHandlers.putAll( defaultResponseHandlers ); |
| 549 | 4 | this.url = new URIBuilder(request.getURI()); |
| 550 | 4 | } |
| 551 | ||
| 552 | /** | |
| 553 | * Use this object to manipulate parts of the request URL, like | |
| 554 | * query params and request path. Example: | |
| 555 | * <pre> | |
| 556 | * builder.request(GET,XML) { | |
| 557 | * url.path = '../other/request.jsp' | |
| 558 | * url.params = [p1:1, p2:2] | |
| 559 | * ... | |
| 560 | * }</pre> | |
| 561 | * @return {@link URIBuilder} to manipulate the request URL | |
| 562 | */ | |
| 563 | 4 | public URIBuilder getURL() { return this.url; } |
| 564 | ||
| 565 | 4 | protected HttpRequestBase getRequest() { return this.request; } |
| 566 | ||
| 567 | /** | |
| 568 | * Get the content-type of any data sent in the request body and the | |
| 569 | * expected response content-type. | |
| 570 | * @return whatever value was assigned via {@link #setContentType(Object)} | |
| 571 | * or passed from the {@link HTTPBuilder#defaultContentType} when this | |
| 572 | * SendDelegateinstance was constructed. | |
| 573 | */ | |
| 574 | 4 | protected Object getContentType() { return this.contentType; } |
| 575 | ||
| 576 | /** | |
| 577 | * Set the content-type used for any data in the request body, as well | |
| 578 | * as the <code>Accept</code> content-type that will be used for parsing | |
| 579 | * the response. The value should be either a {@link ContentType} value | |
| 580 | * or a String, i.e. <code>"text/plain"</code> | |
| 581 | * @param ct content-type to send and recieve content | |
| 582 | */ | |
| 583 | protected void setContentType( Object ct ) { | |
| 584 | 0 | if ( ct == null ) this.contentType = defaultContentType; |
| 585 | 0 | this.contentType = ct; |
| 586 | 0 | } |
| 587 | ||
| 588 | /** | |
| 589 | * The request content-type, if different from the {@link #contentType}. | |
| 590 | * @return | |
| 591 | */ | |
| 592 | protected String getRequestContentType() { | |
| 593 | 0 | if ( this.requestContentType != null ) return this.requestContentType; |
| 594 | 0 | else return this.getContentType().toString(); |
| 595 | } | |
| 596 | ||
| 597 | /** | |
| 598 | * Assign a different content-type for the request than is expected for | |
| 599 | * the response. This is useful if i.e. you want to post URL-encoded | |
| 600 | * form data but expect the response to be XML or HTML. The | |
| 601 | * {@link #getContentType()} will always control the <code>Accept</code> | |
| 602 | * header, and will be used for the request content <i>unless</i> this | |
| 603 | * value is also explicitly set. | |
| 604 | * @param ct either a {@link ContentType} value or a valid content-type | |
| 605 | * String. | |
| 606 | */ | |
| 607 | protected void setRequestContentType( String ct ) { | |
| 608 | 0 | this.requestContentType = ct; |
| 609 | 0 | } |
| 610 | ||
| 611 | /** | |
| 612 | * Valid arguments: | |
| 613 | * <dl> | |
| 614 | * <dt>url</dt><dd>Either a URI, URL, or String. | |
| 615 | * If not supplied, the HTTPBuilder's default URL is used.</dd> | |
| 616 | * <dt>path</dt><dd>Request path that is merged with the URL</dd> | |
| 617 | * <dt>params</dt><dd>Map of request parameters</dd> | |
| 618 | * <dt>headers</dt><dd>Map of HTTP headers</dd> | |
| 619 | * <dt>contentType</dt><dd>Request content type and Accept header. | |
| 620 | * If not supplied, the HTTPBuilder's default content-type is used.</dd> | |
| 621 | * <dt>requestContentType</dt><dd>content type for the request, if it | |
| 622 | * is different from the expected response content-type</dd> | |
| 623 | * <dt>body</dt><dd>Request body that will be encoded based on the given contentType</dd> | |
| 624 | * </dl> | |
| 625 | * @param args named parameters to set properties on this delegate. | |
| 626 | * @throws MalformedURLException | |
| 627 | * @throws URISyntaxException | |
| 628 | */ | |
| 629 | @SuppressWarnings("unchecked") | |
| 630 | protected void setPropertiesFromMap( Map<String,?> args ) throws MalformedURLException, URISyntaxException { | |
| 631 | 1 | Object uri = args.get( "url" ); |
| 632 | 1 | if ( uri == null ) uri = defaultURI; |
| 633 | 1 | url = new URIBuilder( convertToURI( uri ) ); |
| 634 | ||
| 635 | 1 | Map params = (Map)args.get( "params" ); |
| 636 | 1 | if ( params != null ) this.url.setQuery( params ); |
| 637 | 1 | Map headers = (Map)args.get( "headers" ); |
| 638 | 1 | if ( headers != null ) this.setHeaders( headers ); |
| 639 | ||
| 640 | 1 | Object path = args.get( "path" ); |
| 641 | 1 | if ( path != null ) this.url.setPath( path.toString() ); |
| 642 | ||
| 643 | 1 | Object contentType = args.get( "contentType" ); |
| 644 | 1 | if ( contentType != null ) this.setContentType( contentType ); |
| 645 | ||
| 646 | 1 | contentType = args.get( "requestContentType" ); |
| 647 | 1 | if ( contentType != null ) this.setRequestContentType( contentType.toString() ); |
| 648 | ||
| 649 | 1 | Object body = args.get("body"); |
| 650 | 1 | if ( body != null ) this.setBody( body ); |
| 651 | 1 | } |
| 652 | ||
| 653 | /** | |
| 654 | * Set request headers. These values will be <strong>merged</strong> | |
| 655 | * with any {@link HTTPBuilder#getHeaders() default request headers.} | |
| 656 | * (The assumption is you'll probably want to add a bunch of headers to | |
| 657 | * whatever defaults you've already set). If you <i>only</i> want to | |
| 658 | * use values set here, simply call {@link #getHeaders() headers.clear()} | |
| 659 | * first. | |
| 660 | */ | |
| 661 | public void setHeaders( Map<?,?> newHeaders ) { | |
| 662 | 0 | for( Object key : newHeaders.keySet() ) { |
| 663 | 0 | Object val = newHeaders.get( key ); |
| 664 | 0 | if ( val == null ) this.headers.remove( key ); |
| 665 | 0 | else this.headers.put( key.toString(), val.toString() ); |
| 666 | 0 | } |
| 667 | 0 | } |
| 668 | ||
| 669 | /** | |
| 670 | * Get request headers (including any default headers). Note that this | |
| 671 | * will not include any <code>Accept</code>, <code>Content-Type</code>, | |
| 672 | * or <code>Content-Encoding</code> headers that are automatically | |
| 673 | * handled by any encoder or parsers in effect. Note that any values | |
| 674 | * set here <i>will</i> override any of those automatically assigned | |
| 675 | * values. | |
| 676 | * header that is a | |
| 677 | * @return | |
| 678 | */ | |
| 679 | public Map<String,String> getHeaders() { | |
| 680 | 5 | return this.headers; |
| 681 | } | |
| 682 | ||
| 683 | /** | |
| 684 | * Convenience method to set a request content-type at the same time | |
| 685 | * the request body is set. This is a variation of | |
| 686 | * {@link #setBody(Object)} that allows for a different content-type | |
| 687 | * than what is expected for the response. | |
| 688 | * | |
| 689 | * <p>Example: | |
| 690 | * <pre> | |
| 691 | * http.request(POST,HTML) { | |
| 692 | * | |
| 693 | * /* request data is interpreted as a JsonBuilder closure in the | |
| 694 | * default EncoderRegistry implementation * / | |
| 695 | * send( 'text/javascript' ) { | |
| 696 | * a : ['one','two','three'] | |
| 697 | * } | |
| 698 | * | |
| 699 | * // response content-type is what was specified in the outer request() argument: | |
| 700 | * response.success = { resp, html -> | |
| 701 | * | |
| 702 | * } | |
| 703 | * } | |
| 704 | * </pre> | |
| 705 | * @param contentType either a {@link ContentType} or content-type | |
| 706 | * string like <code>"text/xml"</code> | |
| 707 | * @param requestBody | |
| 708 | */ | |
| 709 | public void send( Object contentType, Object requestBody ) { | |
| 710 | 0 | this.setRequestContentType( contentType.toString() ); |
| 711 | 0 | this.setBody( requestBody ); |
| 712 | 0 | } |
| 713 | ||
| 714 | /** | |
| 715 | * Set the request body. This value may be of any type supported by | |
| 716 | * the associated {@link EncoderRegistry request encoder}. | |
| 717 | * @see #send(Object, Object) | |
| 718 | * @param body data or closure interpretes as the request body | |
| 719 | */ | |
| 720 | public void setBody( Object body ) { | |
| 721 | 0 | if ( ! (request instanceof HttpEntityEnclosingRequest ) ) |
| 722 | 0 | throw new UnsupportedOperationException( |
| 723 | "Cannot set a request body for a " + request.getMethod() + " method" ); | |
| 724 | 0 | Closure encoder = encoders.get( this.getRequestContentType() ); |
| 725 | 0 | HttpEntity entity = (HttpEntity)encoder.call( body ); |
| 726 | ||
| 727 | 0 | ((HttpEntityEnclosingRequest)request).setEntity( entity ); |
| 728 | 0 | } |
| 729 | ||
| 730 | /** | |
| 731 | * Get the proper response handler for the response code. This is called | |
| 732 | * by the {@link HTTPBuilder} class in order to find the proper handler | |
| 733 | * based on the response status code. | |
| 734 | * | |
| 735 | * @param statusCode HTTP response status code | |
| 736 | * @return the response handler | |
| 737 | */ | |
| 738 | protected Closure findResponseHandler( int statusCode ) { | |
| 739 | 4 | Closure handler = getResponse().get( Integer.toString( statusCode ) ); |
| 740 | 4 | if ( handler == null ) handler = |
| 741 | getResponse().get( Status.find( statusCode ).toString() ); | |
| 742 | 4 | return handler; |
| 743 | } | |
| 744 | ||
| 745 | /** | |
| 746 | * Access the response handler map to set response parsing logic. | |
| 747 | * i.e.<pre> | |
| 748 | * builder.request( GET, XML ) { | |
| 749 | * response.success = { xml -> | |
| 750 | * /* for XML content type, the default parser | |
| 751 | * will return an XmlSlurper * / | |
| 752 | * xml.root.children().each { println it } | |
| 753 | * } | |
| 754 | * }</pre> | |
| 755 | * @return | |
| 756 | */ | |
| 757 | 12 | public Map<String,Closure> getResponse() { return responseHandlers; } |
| 758 | } | |
| 759 | } |