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 | 15 | protected AbstractHttpClient client = new DefaultHttpClient(); |
119 | 15 | protected URI defaultURI = null; // TODO make this a URIBuilder? |
120 | 15 | protected AuthConfig auth = new AuthConfig( this ); |
121 | ||
122 | 15 | protected Object defaultContentType = ContentType.TEXT; |
123 | 15 | protected final Map<String,Closure> defaultResponseHandlers = buildDefaultResponseHandlers(); |
124 | 15 | protected ContentEncodingRegistry contentEncodingHandler = new ContentEncodingRegistry(); |
125 | ||
126 | 15 | protected final Map<String,String> defaultRequestHeaders = new HashMap<String,String>(); |
127 | ||
128 | 15 | protected EncoderRegistry encoders = new EncoderRegistry(); |
129 | 15 | protected ParserRegistry parsers = new ParserRegistry(); |
130 | ||
131 | public HTTPBuilder() { | |
132 | 15 | super(); |
133 | 15 | this.setContentEncoding( ContentEncoding.Type.GZIP, |
134 | ContentEncoding.Type.DEFLATE ); | |
135 | 15 | } |
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 | 6 | this(); |
145 | 6 | this.defaultURI = convertToURI( defaultURL ); |
146 | 6 | } |
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 | 3 | SendDelegate delegate = new SendDelegate( new HttpGet(), |
179 | this.defaultContentType, | |
180 | this.defaultRequestHeaders, | |
181 | this.defaultResponseHandlers ); | |
182 | ||
183 | 3 | delegate.setPropertiesFromMap( args ); |
184 | 3 | delegate.getResponse().put( Status.SUCCESS.toString(), responseClosure ); |
185 | 3 | 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 | 3 | 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#setPath(String) path}, | |
242 | * {@link SendDelegate#setParams(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 | 6 | 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 | 9 | try { reqMethod = method.getRequestType().newInstance(); |
264 | // this exception should reasonably never occur: | |
265 | 9 | } catch ( Exception e ) { throw new RuntimeException( e ); } |
266 | ||
267 | 9 | reqMethod.setURI( uri ); |
268 | 9 | SendDelegate delegate = new SendDelegate( reqMethod, contentType, |
269 | this.defaultRequestHeaders, | |
270 | this.defaultResponseHandlers ); | |
271 | 9 | configClosure.setDelegate( delegate ); |
272 | 9 | configClosure.call( client ); |
273 | ||
274 | 9 | return this.doRequest( delegate ); |
275 | } | |
276 | ||
277 | protected Object doRequest( SendDelegate delegate ) throws ClientProtocolException, IOException { | |
278 | ||
279 | 12 | HttpRequestBase reqMethod = delegate.getRequest(); |
280 | ||
281 | 12 | Object contentType = delegate.getContentType(); |
282 | 12 | String acceptContentTypes = contentType.toString(); |
283 | 12 | if ( contentType instanceof ContentType ) |
284 | 12 | acceptContentTypes = ((ContentType)contentType).getAcceptHeader(); |
285 | ||
286 | 12 | reqMethod.setHeader( "Accept", acceptContentTypes ); |
287 | 12 | reqMethod.setURI( delegate.getURL().toURI() ); |
288 | ||
289 | // set any request headers from the delegate | |
290 | 12 | Map<String,String> headers = delegate.getHeaders(); |
291 | 12 | for ( String key : headers.keySet() ) reqMethod.setHeader( key, headers.get( key ) ); |
292 | ||
293 | 12 | HttpResponse resp = client.execute( reqMethod ); |
294 | 12 | Closure responseClosure = delegate.findResponseHandler( |
295 | resp.getStatusLine().getStatusCode() ); | |
296 | ||
297 | 12 | Object[] closureArgs = null; |
298 | 12 | switch ( responseClosure.getMaximumNumberOfParameters() ) { |
299 | case 1 : | |
300 | 3 | closureArgs = new Object[] { resp }; |
301 | 3 | break; |
302 | case 2 : | |
303 | 9 | String responseContentType = parsers.getContentType( resp ); |
304 | 9 | Object parsedData = parsers.get( responseContentType ).call( resp ); |
305 | 9 | closureArgs = new Object[] { resp, parsedData }; |
306 | 9 | break; |
307 | default: | |
308 | 0 | throw new IllegalArgumentException( |
309 | "Response closure must accept one or two parameters" ); | |
310 | } | |
311 | ||
312 | 12 | Object returnVal = responseClosure.call( closureArgs ); |
313 | 12 | if ( resp.getEntity().isStreaming() ) resp.getEntity().consumeContent(); |
314 | 12 | return returnVal; |
315 | } | |
316 | ||
317 | protected Map<String,Closure> buildDefaultResponseHandlers() { | |
318 | 15 | Map<String,Closure> map = new HashMap<String, Closure>(); |
319 | 15 | map.put( Status.SUCCESS.toString(), |
320 | new MethodClosure(this,"defaultSuccessHandler")); | |
321 | 15 | map.put( Status.FAILURE.toString(), |
322 | new MethodClosure(this,"defaultFailureHandler")); | |
323 | ||
324 | 15 | 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 | 6 | 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 | 15 | this.contentEncodingHandler.setInterceptors( client, encodings ); |
425 | 15 | } |
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 | 3 | this.defaultURI = convertToURI( url ); |
435 | 3 | } |
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 | 3 | 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 | 3 | 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 | 3 | 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(URI, Method, ContentType, Closure)} 'config' | |
532 | * closure argument. | |
533 | */ | |
534 | protected class SendDelegate { | |
535 | protected HttpRequestBase request; | |
536 | protected Object contentType; | |
537 | protected String requestContentType; | |
538 | 12 | protected Map<String,Closure> responseHandlers = new HashMap<String,Closure>(); |
539 | protected URIBuilder url; | |
540 | 12 | protected Map<String,String> headers = new HashMap<String,String>(); |
541 | ||
542 | public SendDelegate( HttpRequestBase request, Object contentType, | |
543 | Map<String,String> defaultRequestHeaders, | |
544 | 12 | Map<String,Closure> defaultResponseHandlers ) { |
545 | 12 | this.request = request; |
546 | 12 | this.headers.putAll( defaultRequestHeaders ); |
547 | 12 | this.contentType = contentType; |
548 | 12 | this.responseHandlers.putAll( defaultResponseHandlers ); |
549 | 12 | this.url = new URIBuilder(request.getURI()); |
550 | 12 | } |
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 | 12 | public URIBuilder getURL() { return this.url; } |
564 | ||
565 | 12 | 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 | 12 | 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 | 3 | Object uri = args.get( "url" ); |
632 | 3 | if ( uri == null ) uri = defaultURI; |
633 | 3 | url = new URIBuilder( convertToURI( uri ) ); |
634 | ||
635 | 3 | Map params = (Map)args.get( "params" ); |
636 | 3 | if ( params != null ) this.url.setQuery( params ); |
637 | 3 | Map headers = (Map)args.get( "headers" ); |
638 | 3 | if ( headers != null ) this.setHeaders( headers ); |
639 | ||
640 | 3 | String path = (String)args.get( "path" ); |
641 | 3 | if ( path != null ) this.url.setPath( path ); |
642 | ||
643 | 3 | Object contentType = args.get( "contentType" ); |
644 | 3 | if ( contentType != null ) this.setContentType( contentType ); |
645 | ||
646 | 3 | contentType = args.get( "requestContentType" ); |
647 | 3 | if ( contentType != null ) this.setRequestContentType( contentType.toString() ); |
648 | ||
649 | 3 | Object body = args.get("body"); |
650 | 3 | if ( body != null ) this.setBody( body ); |
651 | 3 | } |
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 | 15 | 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 status | |
736 | * @param delegate | |
737 | * @return the response handler | |
738 | */ | |
739 | protected Closure findResponseHandler( int statusCode ) { | |
740 | 12 | Closure handler = getResponse().get( Integer.toString( statusCode ) ); |
741 | 12 | if ( handler == null ) handler = |
742 | getResponse().get( Status.find( statusCode ).toString() ); | |
743 | 12 | return handler; |
744 | } | |
745 | ||
746 | /** | |
747 | * Access the response handler map to set response parsing logic. | |
748 | * i.e.<pre> | |
749 | * builder.request( GET, XML ) { | |
750 | * response.success = { xml -> | |
751 | * /* for XML content type, the default parser | |
752 | * will return an XmlSlurper * / | |
753 | * xml.root.children().each { println it } | |
754 | * } | |
755 | * }</pre> | |
756 | * @return | |
757 | */ | |
758 | 36 | public Map<String,Closure> getResponse() { return responseHandlers; } |
759 | } | |
760 | } |