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