View Javadoc

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>&#42;/*</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 }