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.commons.logging.Log; | |
35 | import org.apache.commons.logging.LogFactory; | |
36 | import org.apache.http.HttpEntity; | |
37 | import org.apache.http.HttpEntityEnclosingRequest; | |
38 | import org.apache.http.HttpResponse; | |
39 | import org.apache.http.client.ClientProtocolException; | |
40 | import org.apache.http.client.HttpResponseException; | |
41 | import org.apache.http.client.methods.HttpGet; | |
42 | import org.apache.http.client.methods.HttpPost; | |
43 | import org.apache.http.client.methods.HttpRequestBase; | |
44 | import org.apache.http.conn.ClientConnectionManager; | |
45 | import org.apache.http.impl.client.AbstractHttpClient; | |
46 | import org.apache.http.impl.client.DefaultHttpClient; | |
47 | import org.codehaus.groovy.runtime.DefaultGroovyMethods; | |
48 | import org.codehaus.groovy.runtime.MethodClosure; | |
49 | ||
50 | import static groovyx.net.http.URIBuilder.convertToURI; | |
51 | ||
52 | /** <p> | |
53 | * Groovy DSL for easily making HTTP requests, and handling request and response | |
54 | * data. This class adds a number of convenience mechanisms built on top of | |
55 | * Apache HTTPClient for things like URL-encoded POSTs and REST requests that | |
56 | * require building and parsing JSON or XML. Convenient access to a few common | |
57 | * authentication methods is also available.</p> | |
58 | * | |
59 | * | |
60 | * <h3>Conventions</h3> | |
61 | * <p>HTTPBuilder has properties for default headers, URL, contentType, etc. | |
62 | * All of these values are also assignable (and in many cases, in much finer | |
63 | * detail) from the {@link SendDelegate} as well. In any cases where the value | |
64 | * is not set on the delegate (from within a request closure,) the builder's | |
65 | * default value is used. </p> | |
66 | * | |
67 | * <p>For instance, any methods that do not take a URL parameter assume you will | |
68 | * set a URL value in the request closure or use the builder's assigned | |
69 | * {@link #getURL() default URL}.</p> | |
70 | * | |
71 | * | |
72 | * <h3>Response Parsing</h3> | |
73 | * <p>By default, HTTPBuilder uses {@link ContentType#ANY} as the default | |
74 | * content-type. This means the value of the request's <code>Accept</code> | |
75 | * header is <code>*/*</code>, and the response parser is determined | |
76 | * based on the response <code>content-type</code> header. </p> | |
77 | * | |
78 | * <p><strong>If</strong> any contentType is given (either in | |
79 | * {@link #setContentType(Object)} or as a request method parameter), the | |
80 | * builder will attempt to parse the response using that content-type, | |
81 | * regardless of what the server actually responds with. </p> | |
82 | * | |
83 | * | |
84 | * <h3>Examples:</h3> | |
85 | * Perform an HTTP GET and print the response: | |
86 | * <pre> | |
87 | * def http = new HTTPBuilder('http://www.google.com') | |
88 | * | |
89 | * http.get( path : '/search', | |
90 | * contentType : TEXT, | |
91 | * query : [q:'Groovy'] ) { resp, reader -> | |
92 | * println "response status: ${resp.statusLine}" | |
93 | * println 'Response data: -----' | |
94 | * System.out << reader | |
95 | * println '\n--------------------' | |
96 | * } | |
97 | * </pre> | |
98 | * | |
99 | * 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 | 9 | * You can also set a default response handler called for any status |
123 | 9 | * 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 | 9 | * this HTTPBuilder instance: |
126 | * <pre> | |
127 | 9 | * http.handler.failure = { resp -> |
128 | 9 | * println "Unexpected failure: ${resp.statusLine}" |
129 | 9 | * } |
130 | * </pre> | |
131 | 9 | * |
132 | * | |
133 | 9 | * And... Automatic response parsing for registered content types! |
134 | 9 | * |
135 | * <pre> | |
136 | * http.request( 'http://ajax.googleapis.com', GET, JSON ) { | |
137 | 9 | * url.path = '/ajax/services/search/web' |
138 | 9 | * url.query = [ v:'1.0', q: 'Calvin and Hobbes' ] |
139 | 9 | * |
140 | * response.success = { resp, json -> | |
141 | 9 | * 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 | 5 | * |
151 | 5 | * @author <a href='mailto:tnichols@enernoc.com'>Tom Nichols</a> |
152 | 5 | */ |
153 | public class HTTPBuilder { | |
154 | ||
155 | protected AbstractHttpClient client; | |
156 | 27 | protected URI defaultURI = null; // TODO make this a URIBuilder? |
157 | 27 | protected AuthConfig auth = new AuthConfig( this ); |
158 | ||
159 | 27 | protected final Log log = LogFactory.getLog( getClass() ); |
160 | ||
161 | 27 | protected Object defaultContentType = ContentType.ANY; |
162 | 27 | protected final Map<String,Closure> defaultResponseHandlers = buildDefaultResponseHandlers(); |
163 | 27 | protected ContentEncodingRegistry contentEncodingHandler = new ContentEncodingRegistry(); |
164 | 0 | |
165 | 27 | protected final Map<String,String> defaultRequestHeaders = new HashMap<String,String>(); |
166 | 0 | |
167 | 27 | protected EncoderRegistry encoders = new EncoderRegistry(); |
168 | 27 | protected ParserRegistry parsers = new ParserRegistry(); |
169 | ||
170 | public HTTPBuilder() { | |
171 | 27 | super(); |
172 | 27 | this.client = new DefaultHttpClient(); |
173 | 27 | this.setContentEncoding( ContentEncoding.Type.GZIP, |
174 | ContentEncoding.Type.DEFLATE ); | |
175 | 27 | } |
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 | 21 | this(); |
185 | 15 | this.defaultURI = convertToURI( defaultURL ); |
186 | 15 | } |
187 | ||
188 | /** | |
189 | 6 | * Give a default URL to be used for all request methods that don't |
190 | 6 | * explicitly take a URL parameter, and a default content-type to be used |
191 | 6 | * 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 | 0 | this(); |
199 | 0 | this.defaultURI = convertToURI( defaultURL ); |
200 | 0 | this.defaultContentType = defaultContentType; |
201 | 0 | } |
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 | 1 | */ |
216 | public Object get( Map<String,?> args, Closure responseClosure ) | |
217 | throws ClientProtocolException, IOException, URISyntaxException { | |
218 | 18 | SendDelegate delegate = new SendDelegate( new HttpGet(), |
219 | this.defaultContentType, | |
220 | this.defaultRequestHeaders, | |
221 | this.defaultResponseHandlers ); | |
222 | ||
223 | 19 | delegate.setPropertiesFromMap( args ); |
224 | 19 | delegate.getResponse().put( Status.SUCCESS.toString(), responseClosure ); |
225 | 19 | return this.doRequest( delegate ); |
226 | } | |
227 | 1 | |
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 | 0 | * 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 | 6 | * 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 | 2 | 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 | 2 | delegate.setRequestContentType( ContentType.URLENC.toString() ); |
258 | 2 | delegate.setPropertiesFromMap( args ); |
259 | 2 | delegate.getResponse().put( Status.SUCCESS.toString(), responseClosure ); |
260 | ||
261 | 2 | return this.doRequest( delegate ); |
262 | 2 | } |
263 | ||
264 | public Object request( Method m, Closure configClosure ) throws ClientProtocolException, IOException { | |
265 | 0 | return this.doRequest( this.defaultURI, m, this.defaultContentType, configClosure ); |
266 | } | |
267 | ||
268 | public Object request( Method m, Object contentType, Closure configClosure ) | |
269 | 8 | throws ClientProtocolException, IOException { |
270 | 18 | return this.doRequest( this.defaultURI, m, contentType, configClosure ); |
271 | 8 | } |
272 | ||
273 | 8 | /** |
274 | 8 | * 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 | 8 | * @param uri either a URI, URL, or String |
278 | 8 | * @param method {@link Method HTTP method} |
279 | * @param contentType either a {@link ContentType} or valid content-type string. | |
280 | 8 | * @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 | 15 | * |
287 | * @return whatever value was returned by the executed response handler. | |
288 | 15 | * @throws IllegalAccessException |
289 | 15 | * @throws InstantiationException |
290 | 15 | * @throws ClientProtocolException |
291 | 15 | * @throws IOException |
292 | * @throws URISyntaxException if a URI string or URL was invalid. | |
293 | 15 | */ |
294 | 15 | public Object request( Object uri, Method method, Object contentType, Closure configClosure ) |
295 | throws ClientProtocolException, IOException, URISyntaxException { | |
296 | 6 | return this.doRequest( convertToURI( uri ), method, contentType, configClosure ); |
297 | 15 | } |
298 | 15 | |
299 | protected Object doRequest( URI uri, Method method, Object contentType, Closure configClosure ) | |
300 | 15 | throws ClientProtocolException, IOException { |
301 | 15 | |
302 | 15 | HttpRequestBase reqMethod; |
303 | 39 | try { reqMethod = method.getRequestType().newInstance(); |
304 | // this exception should reasonably never occur: | |
305 | 39 | } catch ( Exception e ) { throw new RuntimeException( e ); } |
306 | 15 | |
307 | 24 | reqMethod.setURI( uri ); |
308 | 30 | SendDelegate delegate = new SendDelegate( reqMethod, contentType, |
309 | 6 | this.defaultRequestHeaders, |
310 | this.defaultResponseHandlers ); | |
311 | 24 | configClosure.setDelegate( delegate ); |
312 | 33 | configClosure.call( client ); |
313 | ||
314 | 33 | return this.doRequest( delegate ); |
315 | 1 | } |
316 | ||
317 | 9 | protected Object doRequest( final SendDelegate delegate ) |
318 | 9 | throws ClientProtocolException, IOException { |
319 | 9 | |
320 | 44 | final HttpRequestBase reqMethod = delegate.getRequest(); |
321 | 9 | |
322 | 53 | Object contentType = delegate.getContentType(); |
323 | 44 | String acceptContentTypes = contentType.toString(); |
324 | 44 | if ( contentType instanceof ContentType ) |
325 | 44 | acceptContentTypes = ((ContentType)contentType).getAcceptHeader(); |
326 | ||
327 | 44 | reqMethod.setHeader( "Accept", acceptContentTypes ); |
328 | 59 | reqMethod.setURI( delegate.getURL().toURI() ); |
329 | 14 | |
330 | 14 | // set any request headers from the delegate |
331 | 58 | Map<String,String> headers = delegate.getHeaders(); |
332 | 44 | for ( String key : headers.keySet() ) { |
333 | 1 | String val = headers.get( key ); |
334 | 31 | if ( val == null ) reqMethod.removeHeaders( key ); |
335 | 40 | else reqMethod.setHeader( key, val ); |
336 | 40 | } |
337 | 30 | |
338 | 23 | HttpResponse resp = client.execute( reqMethod ); |
339 | 44 | int status = resp.getStatusLine().getStatusCode(); |
340 | 44 | Closure responseClosure = delegate.findResponseHandler( status ); |
341 | 23 | log.debug( "Response code: " + status + "; found handler: " + responseClosure ); |
342 | 14 | |
343 | 28 | Object[] closureArgs = null; |
344 | 14 | switch ( responseClosure.getMaximumNumberOfParameters() ) { |
345 | case 1 : | |
346 | 21 | closureArgs = new Object[] { resp }; |
347 | 5 | break; |
348 | 16 | case 2 : |
349 | 2 | // For HEAD or DELETE requests, there should be no response entity. |
350 | 9 | if ( resp.getEntity() == null ) { |
351 | 16 | log.warn( "Response contains no entity, but response closure " + |
352 | 16 | "expects parsed data. Passing null as second closure arg." ); |
353 | 16 | closureArgs = new Object[] { resp, null }; |
354 | 0 | break; |
355 | 16 | } |
356 | 16 | |
357 | 0 | // Otherwise, parse the response entity: |
358 | 0 | |
359 | 0 | // first, start with the _given_ content-type |
360 | 9 | String responseContentType = contentType.toString(); |
361 | // if the given content-type is ANY ("*/*") then use the response content-type | |
362 | 39 | if ( ContentType.ANY.toString().equals( responseContentType ) ) |
363 | 27 | responseContentType = ParserRegistry.getContentType( resp ); |
364 | 26 | |
365 | 35 | Object parsedData = parsers.get( responseContentType ).call( resp ); |
366 | 9 | if ( parsedData == null ) log.warn( "Parsed data is null!!!" ); |
367 | 9 | else log.debug( "Parsed data from content-type '" + responseContentType |
368 | + "' to object: " + parsedData.getClass() ); | |
369 | 27 | closureArgs = new Object[] { resp, parsedData }; |
370 | 27 | break; |
371 | default: | |
372 | 18 | throw new IllegalArgumentException( |
373 | "Response closure must accept one or two parameters" ); | |
374 | 0 | } |
375 | 18 | |
376 | 14 | Object returnVal = responseClosure.call( closureArgs ); |
377 | 14 | log.debug( "response handler result: " + returnVal ); |
378 | ||
379 | 14 | HttpEntity responseContent = resp.getEntity(); |
380 | 14 | if ( responseContent != null && responseContent.isStreaming() ) |
381 | 4 | responseContent.consumeContent(); |
382 | 14 | return returnVal; |
383 | } | |
384 | ||
385 | protected Map<String,Closure> buildDefaultResponseHandlers() { | |
386 | 9 | Map<String,Closure> map = new HashMap<String, Closure>(); |
387 | 12 | map.put( Status.SUCCESS.toString(), |
388 | new MethodClosure(this,"defaultSuccessHandler")); | |
389 | 9 | map.put( Status.FAILURE.toString(), |
390 | new MethodClosure(this,"defaultFailureHandler")); | |
391 | 0 | |
392 | 9 | return map; |
393 | 0 | } |
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 | 0 | * 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 | 2 | System.out.println( resp.getStatusLine() ); |
409 | 0 | System.out.println( DefaultGroovyMethods.getText( resp.getEntity().getContent() ) ); |
410 | 0 | } |
411 | ||
412 | /** | |
413 | * This is the default <code>response.failure</code> handler. It will be | |
414 | 0 | * 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 | 6 | * @param resp |
422 | * @throws HttpResponseException | |
423 | */ | |
424 | protected void defaultFailureHandler( HttpResponse resp ) throws HttpResponseException { | |
425 | 0 | throw new HttpResponseException( resp.getStatusLine().getStatusCode(), |
426 | resp.getStatusLine().getReasonPhrase() ); | |
427 | } | |
428 | ||
429 | 1 | /** |
430 | 1 | * 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 | 0 | * @see Status |
435 | * @return | |
436 | */ | |
437 | public Map<String,Closure> getHandler() { | |
438 | 3 | return this.defaultResponseHandlers; |
439 | } | |
440 | ||
441 | 9 | /** |
442 | 9 | * 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 | 0 | * @return |
449 | */ | |
450 | public Map<String,Closure> getParser() { | |
451 | 2 | return this.parsers.registeredParsers; |
452 | 2 | } |
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 | 5 | */ |
464 | 2 | public Map<String,Closure> getEncoder() { |
465 | 0 | 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 | 18 | * @see EncoderRegistry |
476 | 18 | * @see ParserRegistry |
477 | 0 | * @param ct either a {@link ContentType} or string value (i.e. <code>"text/xml"</code>.) |
478 | 0 | */ |
479 | 0 | public void setContentType( Object ct ) { |
480 | 1 | this.defaultContentType = ct; |
481 | 1 | } |
482 | 0 | |
483 | ||
484 | /** | |
485 | 4 | * Set acceptable request and response content-encodings. |
486 | 4 | * @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 | 0 | public void setContentEncoding( Object... encodings ) { |
492 | 9 | this.contentEncodingHandler.setInterceptors( client, encodings ); |
493 | 9 | } |
494 | ||
495 | /** | |
496 | * Set the default URL used for requests that do not explicitly take a | |
497 | 6 | * <code>url</code> param. |
498 | 3 | * @param url a URL, URI, or String |
499 | 0 | * @throws URISyntaxException |
500 | */ | |
501 | public void setURL( Object url ) throws URISyntaxException { | |
502 | 2 | this.defaultURI = convertToURI( url ); |
503 | 2 | } |
504 | ||
505 | /** | |
506 | 3 | * 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 | 0 | * simply so that it matches with its JavaBean {@link #setURL(Object)} |
510 | 0 | * counterpart. |
511 | 0 | */ |
512 | 0 | public Object getURL() { |
513 | 0 | try { |
514 | 3 | return defaultURI.toURL(); |
515 | 0 | } catch ( MalformedURLException e ) { |
516 | 0 | 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 | 0 | * @param headers map of header names & values. |
524 | 0 | */ |
525 | 0 | public void setHeaders( Map<?,?> headers ) { |
526 | 0 | this.defaultRequestHeaders.clear(); |
527 | 0 | if ( headers == null ) return; |
528 | 0 | for( Object key : headers.keySet() ) { |
529 | 0 | Object val = headers.get( key ); |
530 | 0 | if ( val == null ) continue; |
531 | 0 | this.defaultRequestHeaders.put( key.toString(), val.toString() ); |
532 | 6 | } |
533 | 0 | } |
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 | 6 | */ |
541 | 0 | public Map<String,String> getHeaders() { |
542 | 0 | return this.defaultRequestHeaders; |
543 | } | |
544 | ||
545 | /** | |
546 | * Return the underlying HTTPClient that is used to handle HTTP requests. | |
547 | * @return the client instance. | |
548 | 0 | */ |
549 | 5 | public AbstractHttpClient getClient() { return this.client; } |
550 | 2 | |
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 | 3 | public AuthConfig getAuth() { return this.auth; } |
558 | 0 | |
559 | /** | |
560 | * Set an alternative {@link AuthConfig} implementation to handle | |
561 | * authorization. | |
562 | * @param ac instance to use. | |
563 | 15 | */ |
564 | public void setAuthConfig( AuthConfig ac ) { | |
565 | 0 | this.auth = ac; |
566 | 0 | } |
567 | 0 | |
568 | /** | |
569 | 15 | * Set a custom registry used to handle different request |
570 | 15 | * <code>content-type</code>s. |
571 | 15 | * @param er |
572 | 15 | */ |
573 | 15 | public void setEncoderRegistry( EncoderRegistry er ) { |
574 | 0 | this.encoders = er; |
575 | 0 | } |
576 | 0 | |
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 | 4 | this.parsers = pr; |
584 | 4 | } |
585 | ||
586 | /** | |
587 | * Set a custom registry used to handle different | |
588 | 15 | * <code>content-encoding</code> types in responses. |
589 | * @param cer | |
590 | 15 | */ |
591 | public void setContentEncodingRegistry( ContentEncodingRegistry cer ) { | |
592 | 0 | this.contentEncodingHandler = cer; |
593 | 0 | } |
594 | ||
595 | /** | |
596 | * Release any system resources held by this instance. | |
597 | 30 | * @see ClientConnectionManager#shutdown() |
598 | */ | |
599 | 45 | public void shutdown() { |
600 | 1 | client.getConnectionManager().shutdown(); |
601 | 1 | } |
602 | ||
603 | 30 | |
604 | 30 | |
605 | 30 | /** |
606 | 30 | * Encloses all properties and method calls used within the |
607 | 30 | * {@link HTTPBuilder#request(Object, Method, Object, Closure)} 'config' |
608 | 30 | * closure argument. |
609 | 32 | */ |
610 | 2 | protected class SendDelegate { |
611 | 2 | protected HttpRequestBase request; |
612 | protected Object contentType; | |
613 | protected String requestContentType; | |
614 | 14 | protected Map<String,Closure> responseHandlers = new HashMap<String,Closure>(); |
615 | protected URIBuilder url; | |
616 | 14 | protected Map<String,String> headers = new HashMap<String,String>(); |
617 | ||
618 | 1 | public SendDelegate( HttpRequestBase request, Object contentType, |
619 | 0 | Map<String,String> defaultRequestHeaders, |
620 | 14 | Map<String,Closure> defaultResponseHandlers ) { |
621 | 14 | this.request = request; |
622 | 44 | this.headers.putAll( defaultRequestHeaders ); |
623 | 14 | this.contentType = contentType; |
624 | 44 | this.responseHandlers.putAll( defaultResponseHandlers ); |
625 | 14 | this.url = new URIBuilder(request.getURI()); |
626 | 14 | } |
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 | 31 | * url.path = '../other/request.jsp' |
634 | 1 | * url.params = [p1:1, p2:2] |
635 | * ... | |
636 | * }</pre> | |
637 | * @return {@link URIBuilder} to manipulate the request URL | |
638 | */ | |
639 | 14 | public URIBuilder getURL() { return this.url; } |
640 | ||
641 | 14 | protected HttpRequestBase getRequest() { return this.request; } |
642 | ||
643 | 4 | /** |
644 | 4 | * Get the content-type of any data sent in the request body and the |
645 | 4 | * 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 | 14 | protected Object getContentType() { return this.contentType; } |
651 | ||
652 | 2 | /** |
653 | 0 | * 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 | 7 | * or a String, i.e. <code>"text/plain"</code> |
657 | 7 | * @param ct content-type to send and recieve content |
658 | 7 | */ |
659 | protected void setContentType( Object ct ) { | |
660 | 8 | if ( ct == null ) this.contentType = defaultContentType; |
661 | 8 | this.contentType = ct; |
662 | 8 | } |
663 | 7 | |
664 | /** | |
665 | 7 | * The request content-type, if different from the {@link #contentType}. |
666 | 7 | * @return |
667 | 2 | */ |
668 | 9 | protected String getRequestContentType() { |
669 | 8 | if ( this.requestContentType != null ) return this.requestContentType; |
670 | 0 | else return this.getContentType().toString(); |
671 | 7 | } |
672 | 7 | |
673 | /** | |
674 | 7 | * Assign a different content-type for the request than is expected for |
675 | 7 | * the response. This is useful if i.e. you want to post URL-encoded |
676 | 7 | * 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 | 1 | this.requestContentType = ct; |
685 | 1 | } |
686 | ||
687 | 0 | /** |
688 | 0 | * Valid arguments: |
689 | 0 | * <dl> |
690 | 14 | * <dt>url</dt><dd>Either a URI, URL, or String. |
691 | 14 | * If not supplied, the HTTPBuilder's default URL is used.</dd> |
692 | 14 | * <dt>path</dt><dd>Request path that is merged with the URL</dd> |
693 | * <dt>params</dt><dd>Map of request parameters</dd> | |
694 | 14 | * <dt>headers</dt><dd>Map of HTTP headers</dd> |
695 | 14 | * <dt>contentType</dt><dd>Request content type and Accept header. |
696 | 14 | * If not supplied, the HTTPBuilder's default content-type is used.</dd> |
697 | 14 | * <dt>requestContentType</dt><dd>content type for the request, if it |
698 | * is different from the expected response content-type</dd> | |
699 | 14 | * <dt>body</dt><dd>Request body that will be encoded based on the given contentType</dd> |
700 | 14 | * </dl> |
701 | * @param args named parameters to set properties on this delegate. | |
702 | 14 | * @throws MalformedURLException |
703 | 14 | * @throws URISyntaxException |
704 | */ | |
705 | 30 | @SuppressWarnings("unchecked") |
706 | 14 | protected void setPropertiesFromMap( Map<String,?> args ) throws MalformedURLException, URISyntaxException { |
707 | 6 | Object uri = args.get( "url" ); |
708 | 20 | if ( uri == null ) uri = defaultURI; |
709 | 20 | url = new URIBuilder( convertToURI( uri ) ); |
710 | 14 | |
711 | 6 | Map params = (Map)args.get( "params" ); |
712 | 6 | if ( params != null ) this.url.setQuery( params ); |
713 | 6 | Map headers = (Map)args.get( "headers" ); |
714 | 6 | if ( headers != null ) this.getHeaders().putAll( headers ); |
715 | ||
716 | 6 | Object path = args.get( "path" ); |
717 | 6 | if ( path != null ) this.url.setPath( path.toString() ); |
718 | ||
719 | 6 | Object contentType = args.get( "contentType" ); |
720 | 6 | if ( contentType != null ) this.setContentType( contentType ); |
721 | 0 | |
722 | 6 | contentType = args.get( "requestContentType" ); |
723 | 6 | if ( contentType != null ) this.setRequestContentType( contentType.toString() ); |
724 | 0 | |
725 | 6 | Object body = args.get("body"); |
726 | 6 | if ( body != null ) this.setBody( body ); |
727 | 6 | } |
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 | 0 | * first. |
736 | 0 | */ |
737 | 0 | public void setHeaders( Map<?,?> newHeaders ) { |
738 | 0 | for( Object key : newHeaders.keySet() ) { |
739 | 32 | Object val = newHeaders.get( key ); |
740 | 0 | if ( val == null ) this.headers.remove( key ); |
741 | 0 | else this.headers.put( key.toString(), val.toString() ); |
742 | 0 | } |
743 | 0 | } |
744 | ||
745 | /** | |
746 | 1 | * Get request headers (including any default headers). Note that this |
747 | 0 | * will not include any <code>Accept</code>, <code>Content-Type</code>, |
748 | * or <code>Content-Encoding</code> headers that are automatically | |
749 | 1 | * handled by any encoder or parsers in effect. Note that any values |
750 | 1 | * set here <i>will</i> override any of those automatically assigned |
751 | * values. | |
752 | 1 | * header that is a |
753 | 1 | * @return |
754 | */ | |
755 | public Map<String,String> getHeaders() { | |
756 | 15 | 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 | 15 | * |
765 | 15 | * <p>Example: |
766 | * <pre> | |
767 | 15 | * http.request(POST,HTML) { |
768 | * | |
769 | 0 | * /* request data is interpreted as a JsonBuilder closure in the |
770 | 0 | * default EncoderRegistry implementation * / |
771 | 0 | * 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 | 2 | * </pre> |
781 | 0 | * @param contentType either a {@link ContentType} or content-type |
782 | 42 | * string like <code>"text/xml"</code> |
783 | 2 | * @param requestBody |
784 | 2 | */ |
785 | public void send( Object contentType, Object requestBody ) { | |
786 | 3 | this.setRequestContentType( contentType.toString() ); |
787 | 3 | this.setBody( requestBody ); |
788 | 1 | } |
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 | 1 | if ( ! (request instanceof HttpEntityEnclosingRequest ) ) |
798 | 30 | throw new UnsupportedOperationException( |
799 | 30 | "Cannot set a request body for a " + request.getMethod() + " method" ); |
800 | 1 | Closure encoder = encoders.get( this.getRequestContentType() ); |
801 | 31 | HttpEntity entity = (HttpEntity)encoder.call( body ); |
802 | ||
803 | 1 | ((HttpEntityEnclosingRequest)request).setEntity( entity ); |
804 | 1 | } |
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 | 14 | Closure handler = this.getResponse().get( Integer.toString( statusCode ) ); |
816 | 98 | if ( handler == null ) handler = |
817 | this.getResponse().get( Status.find( statusCode ).toString() ); | |
818 | 14 | 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 | 39 | public Map<String,Closure> getResponse() { return this.responseHandlers; } |
834 | } | |
835 | } |