View Javadoc

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