001 /*
002 * Copyright 2003-2008 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * You are receiving this code free of charge, which represents many hours of
017 * effort from other individuals and corporations. As a responsible member
018 * of the community, you are asked (but not required) to donate any
019 * enhancements or improvements back to the community under a similar open
020 * source license. Thank you. -TMN
021 */
022 package groovyx.net.http;
023
024 import static groovyx.net.http.URIBuilder.convertToURI;
025 import groovy.lang.Closure;
026
027 import java.io.ByteArrayInputStream;
028 import java.io.ByteArrayOutputStream;
029 import java.io.Closeable;
030 import java.io.IOException;
031 import java.io.InputStream;
032 import java.io.Reader;
033 import java.io.StringReader;
034 import java.io.StringWriter;
035 import java.net.MalformedURLException;
036 import java.net.URI;
037 import java.net.URISyntaxException;
038 import java.net.URL;
039 import java.util.HashMap;
040 import java.util.Map;
041
042 import org.apache.commons.logging.Log;
043 import org.apache.commons.logging.LogFactory;
044 import org.apache.http.HttpEntity;
045 import org.apache.http.HttpEntityEnclosingRequest;
046 import org.apache.http.HttpResponse;
047 import org.apache.http.client.ClientProtocolException;
048 import org.apache.http.client.HttpResponseException;
049 import org.apache.http.client.methods.HttpGet;
050 import org.apache.http.client.methods.HttpPost;
051 import org.apache.http.client.methods.HttpRequestBase;
052 import org.apache.http.conn.ClientConnectionManager;
053 import org.apache.http.impl.client.AbstractHttpClient;
054 import org.apache.http.impl.client.DefaultHttpClient;
055 import org.codehaus.groovy.runtime.DefaultGroovyMethods;
056 import org.codehaus.groovy.runtime.MethodClosure;
057
058 /** <p>
059 * Groovy DSL for easily making HTTP requests, and handling request and response
060 * data. This class adds a number of convenience mechanisms built on top of
061 * Apache HTTPClient for things like URL-encoded POSTs and REST requests that
062 * require building and parsing JSON or XML. Convenient access to a few common
063 * authentication methods is also available.</p>
064 *
065 *
066 * <h3>Conventions</h3>
067 * <p>HTTPBuilder has properties for default headers, URL, contentType, etc.
068 * All of these values are also assignable (and in many cases, in much finer
069 * detail) from the {@link SendDelegate} as well. In any cases where the value
070 * is not set on the delegate (from within a request closure,) the builder's
071 * default value is used. </p>
072 *
073 * <p>For instance, any methods that do not take a URL parameter assume you will
074 * set a URL value in the request closure or use the builder's assigned
075 * {@link #getURL() default URL}.</p>
076 *
077 *
078 * <h3>Response Parsing</h3>
079 * <p>By default, HTTPBuilder uses {@link ContentType#ANY} as the default
080 * content-type. This means the value of the request's <code>Accept</code>
081 * header is <code>*/*</code>, and the response parser is determined
082 * based on the response <code>content-type</code> header. </p>
083 *
084 * <p><strong>If</strong> any contentType is given (either in
085 * {@link #setContentType(Object)} or as a request method parameter), the
086 * builder will attempt to parse the response using that content-type,
087 * regardless of what the server actually responds with. </p>
088 *
089 *
090 * <h3>Examples:</h3>
091 * Perform an HTTP GET and print the response:
092 * <pre>
093 * def http = new HTTPBuilder('http://www.google.com')
094 *
095 * http.get( path : '/search',
096 * contentType : TEXT,
097 * query : [q:'Groovy'] ) { resp, reader ->
098 * println "response status: ${resp.statusLine}"
099 * println 'Response data: -----'
100 * System.out << reader
101 * println '\n--------------------'
102 * }
103 * </pre>
104 *
105 * Long form for other HTTP methods, and response-code-specific handlers.
106 * This is roughly equivalent to the above example.
107 *
108 * <pre>
109 * def http = new HTTPBuilder('http://www.google.com/search?q=groovy')
110 *
111 * http.request( GET, TEXT ) { req ->
112 *
113 * // executed for all successful responses:
114 * response.success = { resp, reader ->
115 * println 'my response handler!'
116 * assert resp.statusLine.statusCode == 200
117 * println resp.statusLine
118 * System.out << reader // print response stream
119 * }
120 *
121 * // executed only if the response status code is 401:
122 * response.'404' = { resp ->
123 * println 'not found!'
124 * }
125 * }
126 * </pre>
127 *
128 * You can also set a default response handler called for any status
129 * code > 399 that is not matched to a specific handler. Setting the value
130 * outside a request closure means it will apply to all future requests with
131 * this HTTPBuilder instance:
132 * <pre>
133 * http.handler.failure = { resp ->
134 * println "Unexpected failure: ${resp.statusLine}"
135 * }
136 * </pre>
137 *
138 *
139 * And... Automatic response parsing for registered content types!
140 *
141 * <pre>
142 * http.request( 'http://ajax.googleapis.com', GET, JSON ) {
143 * url.path = '/ajax/services/search/web'
144 * url.query = [ v:'1.0', q: 'Calvin and Hobbes' ]
145 *
146 * response.success = { resp, json ->
147 * assert json.size() == 3
148 * println "Query response: "
149 * json.responseData.results.each {
150 * println " ${it.titleNoFormatting} : ${it.visibleUrl}"
151 * }
152 * }
153 * }
154 * </pre>
155 *
156 *
157 * @author <a href='mailto:tnichols@enernoc.com'>Tom Nichols</a>
158 */
159 public class HTTPBuilder {
160
161 protected AbstractHttpClient client;
162 protected URI defaultURI = null; // TODO make this a URIBuilder?
163 protected AuthConfig auth = new AuthConfig( this );
164
165 protected final Log log = LogFactory.getLog( getClass() );
166
167 protected Object defaultContentType = ContentType.ANY;
168 protected final Map<String,Closure> defaultResponseHandlers = buildDefaultResponseHandlers();
169 protected ContentEncodingRegistry contentEncodingHandler = new ContentEncodingRegistry();
170
171 protected final Map<String,String> defaultRequestHeaders = new HashMap<String,String>();
172
173 protected EncoderRegistry encoders = new EncoderRegistry();
174 protected ParserRegistry parsers = new ParserRegistry();
175
176 public HTTPBuilder() {
177 super();
178 this.client = new DefaultHttpClient();
179 this.setContentEncoding( ContentEncoding.Type.GZIP,
180 ContentEncoding.Type.DEFLATE );
181 }
182
183 /**
184 * Give a default URL to be used for all request methods that don't
185 * explicitly take a URL parameter.
186 * @param defaultURL either a {@link URL}, {@link URI} or String
187 * @throws URISyntaxException if the URL was not parse-able
188 */
189 public HTTPBuilder( Object defaultURL ) throws URISyntaxException {
190 this();
191 this.defaultURI = convertToURI( defaultURL );
192 }
193
194 /**
195 * Give a default URL to be used for all request methods that don't
196 * explicitly take a URL parameter, and a default content-type to be used
197 * for request encoding and response parsing.
198 * @param defaultURL either a {@link URL}, {@link URI} or String
199 * @param defaultContentType content-type string. See {@link ContentType}
200 * for common types.
201 * @throws URISyntaxException if the URL was not parse-able
202 */
203 public HTTPBuilder( Object defaultURL, Object defaultContentType ) throws URISyntaxException {
204 this();
205 this.defaultURI = convertToURI( defaultURL );
206 this.defaultContentType = defaultContentType;
207 }
208
209 /**
210 * <p>Convenience method to perform an HTTP GET. It will use the HTTPBuilder's
211 * {@link #getHandler() registered response handlers} to handle success or
212 * failure status codes. By default, the <code>success</code> response
213 * handler will attempt to parse the data and simply return the parsed
214 * object.</p>
215 *
216 * <p><strong>Note:</strong> If using the {@link #defaultSuccessHandler(HttpResponse, Object)
217 * default <code>success</code> response handler}, be sure to read the
218 * caveat regarding streaming response data.</p>
219 *
220 * @see #getHandler()
221 * @see #defaultSuccessHandler(HttpResponse, Object)
222 * @see #defaultFailureHandler(HttpResponse)
223 * @param args see {@link SendDelegate#setPropertiesFromMap(Map)}
224 * @return whatever was returned from the response closure.
225 * @throws URISyntaxException
226 * @throws IOException
227 * @throws ClientProtocolException
228 */
229 public Object get( Map<String,?> args )
230 throws ClientProtocolException, IOException, URISyntaxException {
231 return this.get( args, null );
232 }
233
234 /**
235 * <p>Convenience method to perform an HTTP GET. The response closure will
236 * be called only on a successful response. </p>
237 *
238 * <p>A 'failed' response (i.e. any HTTP status code > 399) will be handled
239 * by the registered 'failure' handler. The
240 * {@link #defaultFailureHandler(HttpResponse) default failure handler}
241 * throws an {@link HttpResponseException}.</p>
242 *
243 * @param args see {@link SendDelegate#setPropertiesFromMap(Map)}
244 * @param responseClosure code to handle a successful HTTP response
245 * @return any value returned by the response closure.
246 * @throws ClientProtocolException
247 * @throws IOException
248 * @throws URISyntaxException
249 */
250 public Object get( Map<String,?> args, Closure responseClosure )
251 throws ClientProtocolException, IOException, URISyntaxException {
252 SendDelegate delegate = new SendDelegate( new HttpGet(),
253 this.defaultContentType,
254 this.defaultRequestHeaders,
255 this.defaultResponseHandlers );
256
257 delegate.setPropertiesFromMap( args );
258 if ( responseClosure != null ) delegate.getResponse().put(
259 Status.SUCCESS.toString(), responseClosure );
260 return this.doRequest( delegate );
261 }
262
263 /**
264 * <p>Convenience method to perform an HTTP POST. It will use the HTTPBuilder's
265 * {@link #getHandler() registered response handlers} to handle success or
266 * failure status codes. By default, the <code>success</code> response
267 * handler will attempt to parse the data and simply return the parsed
268 * object. </p>
269 *
270 * <p><strong>Note:</strong> If using the {@link #defaultSuccessHandler(HttpResponse, Object)
271 * default <code>success</code> response handler}, be sure to read the
272 * caveat regarding streaming response data.</p>
273 *
274 * @see #getHandler()
275 * @see #defaultSuccessHandler(HttpResponse, Object)
276 * @see #defaultFailureHandler(HttpResponse)
277 * @param args see {@link SendDelegate#setPropertiesFromMap(Map)}
278 * @return whatever was returned from the response closure.
279 * @throws IOException
280 * @throws URISyntaxException
281 * @throws ClientProtocolException
282 * @throws URISyntaxException
283 * @throws IOException
284 * @throws ClientProtocolException
285 */
286 public Object post( Map<String,?> args )
287 throws ClientProtocolException, URISyntaxException, IOException {
288 return this.post( args, null );
289 }
290
291 /** <p>
292 * Convenience method to perform an HTTP form POST. The response closure will be
293 * called only on a successful response.</p>
294 *
295 * <p>A 'failed' response (i.e. any
296 * HTTP status code > 399) will be handled by the registered 'failure'
297 * handler. The {@link #defaultFailureHandler(HttpResponse) default
298 * failure handler} throws an {@link HttpResponseException}.</p>
299 *
300 * <p>The request body (specified by a <code>body</code> named parameter)
301 * will be converted to a url-encoded form string unless a different
302 * <code>requestContentType</code> named parameter is passed to this method.
303 * (See {@link EncoderRegistry#encodeForm(Map)}.) </p>
304 *
305 * @param args see {@link SendDelegate#setPropertiesFromMap(Map)}
306 * @param responseClosure code to handle a successful HTTP response
307 * @return any value returned by the response closure.
308 * @throws ClientProtocolException
309 * @throws IOException
310 * @throws URISyntaxException
311 */
312 public Object post( Map<String,?> args, Closure responseClosure )
313 throws URISyntaxException, ClientProtocolException, IOException {
314 SendDelegate delegate = new SendDelegate( new HttpPost(),
315 this.defaultContentType,
316 this.defaultRequestHeaders,
317 this.defaultResponseHandlers );
318
319 /* by default assume the request body will be URLEncoded, but allow
320 the 'requestContentType' named argument to override this if it is
321 given */
322 delegate.setRequestContentType( ContentType.URLENC.toString() );
323 delegate.setPropertiesFromMap( args );
324
325 if ( responseClosure != null ) delegate.getResponse().put(
326 Status.SUCCESS.toString(), responseClosure );
327
328 return this.doRequest( delegate );
329 }
330
331 /**
332 * Make an HTTP request to the default URL and content-type.
333 * @see #request(Object, Method, Object, Closure)
334 * @param method {@link Method HTTP method}
335 * @param contentType either a {@link ContentType} or valid content-type string.
336 * @param configClosure request configuration options
337 * @return whatever value was returned by the executed response handler.
338 * @throws ClientProtocolException
339 * @throws IOException
340 */
341 public Object request( Method m, Closure configClosure ) throws ClientProtocolException, IOException {
342 return this.doRequest( this.defaultURI, m, this.defaultContentType, configClosure );
343 }
344
345 /**
346 * Make an HTTP request using the default URL, with the given method,
347 * content-type, and configuration.
348 * @see #request(Object, Method, Object, Closure)
349 * @param method {@link Method HTTP method}
350 * @param contentType either a {@link ContentType} or valid content-type string.
351 * @param configClosure request configuration options
352 * @return whatever value was returned by the executed response handler.
353 * @throws ClientProtocolException
354 * @throws IOException
355 */
356 public Object request( Method m, Object contentType, Closure configClosure )
357 throws ClientProtocolException, IOException {
358 return this.doRequest( this.defaultURI, m, contentType, configClosure );
359 }
360
361 /**
362 * Make a request for the given HTTP method and content-type, with
363 * additional options configured in the <code>configClosure</code>. See
364 * {@link SendDelegate} for options.
365 * @param uri either a URI, URL, or String
366 * @param method {@link Method HTTP method}
367 * @param contentType either a {@link ContentType} or valid content-type string.
368 * @param configClosure closure from which to configure options like
369 * {@link SendDelegate#getURL() url.path},
370 * {@link URIBuilder#setQuery(Map) request parameters},
371 * {@link SendDelegate#setHeaders(Map) headers},
372 * {@link SendDelegate#setBody(Object) request body} and
373 * {@link SendDelegate#getResponse() response handlers}.
374 *
375 * @return whatever value was returned by the executed response handler.
376 * @throws ClientProtocolException
377 * @throws IOException
378 * @throws URISyntaxException if a URI string or URL was invalid.
379 */
380 public Object request( Object uri, Method method, Object contentType, Closure configClosure )
381 throws ClientProtocolException, IOException, URISyntaxException {
382 return this.doRequest( convertToURI( uri ), method, contentType, configClosure );
383 }
384
385 /**
386 * Create a {@link SendDelegate} from the given arguments, execute the
387 * config closure, then pass the delegate to {@link #doRequest(SendDelegate)},
388 * which actually executes the request.
389 */
390 protected Object doRequest( URI uri, Method method, Object contentType, Closure configClosure )
391 throws ClientProtocolException, IOException {
392
393 HttpRequestBase reqMethod;
394 try { reqMethod = method.getRequestType().newInstance();
395 // this exception should reasonably never occur:
396 } catch ( Exception e ) { throw new RuntimeException( e ); }
397
398 reqMethod.setURI( uri );
399 SendDelegate delegate = new SendDelegate( reqMethod, contentType,
400 this.defaultRequestHeaders,
401 this.defaultResponseHandlers );
402 configClosure.setDelegate( delegate );
403 configClosure.call( client );
404
405 return this.doRequest( delegate );
406 }
407
408 /**
409 * All <code>request</code> methods delegate to this method.
410 */
411 protected Object doRequest( final SendDelegate delegate )
412 throws ClientProtocolException, IOException {
413
414 final HttpRequestBase reqMethod = delegate.getRequest();
415
416 Object contentType = delegate.getContentType();
417 String acceptContentTypes = contentType.toString();
418 if ( contentType instanceof ContentType )
419 acceptContentTypes = ((ContentType)contentType).getAcceptHeader();
420
421 reqMethod.setHeader( "Accept", acceptContentTypes );
422 reqMethod.setURI( delegate.getURL().toURI() );
423
424 // set any request headers from the delegate
425 Map<String,String> headers = delegate.getHeaders();
426 for ( String key : headers.keySet() ) {
427 String val = headers.get( key );
428 if ( val == null ) reqMethod.removeHeaders( key );
429 else reqMethod.setHeader( key, val );
430 }
431
432 HttpResponse resp = client.execute( reqMethod );
433 int status = resp.getStatusLine().getStatusCode();
434 Closure responseClosure = delegate.findResponseHandler( status );
435 log.debug( "Response code: " + status + "; found handler: " + responseClosure );
436
437 Object[] closureArgs = null;
438 switch ( responseClosure.getMaximumNumberOfParameters() ) {
439 case 1 :
440 closureArgs = new Object[] { resp };
441 break;
442 case 2 :
443 // For HEAD or DELETE requests, there should be no response entity.
444 if ( resp.getEntity() == null ) {
445 log.warn( "Response contains no entity, but response closure " +
446 "expects parsed data. Passing null as second closure arg." );
447 closureArgs = new Object[] { resp, null };
448 break;
449 }
450
451 // Otherwise, parse the response entity:
452
453 // first, start with the _given_ content-type
454 String responseContentType = contentType.toString();
455 // if the given content-type is ANY ("*/*") then use the response content-type
456 if ( ContentType.ANY.toString().equals( responseContentType ) )
457 responseContentType = ParserRegistry.getContentType( resp );
458
459 Object parsedData = parsers.get( responseContentType ).call( resp );
460 if ( parsedData == null ) log.warn( "Parsed data is null!!!" );
461 else log.debug( "Parsed data from content-type '" + responseContentType
462 + "' to object: " + parsedData.getClass() );
463 closureArgs = new Object[] { resp, parsedData };
464 break;
465 default:
466 throw new IllegalArgumentException(
467 "Response closure must accept one or two parameters" );
468 }
469
470 Object returnVal = responseClosure.call( closureArgs );
471 log.debug( "response handler result: " + returnVal );
472
473 HttpEntity responseContent = resp.getEntity();
474 if ( responseContent != null && responseContent.isStreaming() )
475 responseContent.consumeContent();
476 return returnVal;
477 }
478
479 protected Map<String,Closure> buildDefaultResponseHandlers() {
480 Map<String,Closure> map = new HashMap<String, Closure>();
481 map.put( Status.SUCCESS.toString(),
482 new MethodClosure(this,"defaultSuccessHandler"));
483 map.put( Status.FAILURE.toString(),
484 new MethodClosure(this,"defaultFailureHandler"));
485
486 return map;
487 }
488
489 /**
490 * <p>This is the default <code>response.success</code> handler. It will be
491 * executed if the response is not handled by a status-code-specific handler
492 * (i.e. <code>response.'200'= {..}</code>) and no generic 'success' handler
493 * is given (i.e. <code>response.success = {..}</code>.) This handler simply
494 * returns the parsed data from the response body. In most cases you will
495 * probably want to define a <code>response.success = {...}</code> handler
496 * from the request closure, which will replace the response handler defined
497 * by this method. </p>
498 *
499 * <h4>Note for parsers that return streaming content:</h4>
500 * <p>For responses parsed as {@link ParserRegistry#parseStream(HttpResponse)
501 * BINARY} or {@link ParserRegistry#parseText(HttpResponse) TEXT}, the
502 * parser will return streaming content -- an <code>InputStream</code> or
503 * <code>Reader</code>. In these cases, this handler will buffer the the
504 * response content before the network connection is closed. </p>
505 *
506 * <p>In practice, a user-supplied response handler closure is
507 * <i>designed</i> to handle streaming content so it can be read directly from
508 * the response stream without buffering, which will be much more efficient.
509 * Therefore, it is recommended that request method variants be used which
510 * explicitly accept a response handler closure in these cases.</p>
511 *
512 * @param resp HTTP response
513 * @param parsedData parsed data as resolved from this instance's {@link ParserRegistry}
514 * @return the parsed data object (whatever the parser returns).
515 */
516 protected Object defaultSuccessHandler( HttpResponse resp, Object parsedData ) throws IOException {
517 //If response is streaming, buffer it in a byte array:
518 if ( parsedData instanceof InputStream ) {
519 ByteArrayOutputStream buffer = new ByteArrayOutputStream();
520 DefaultGroovyMethods.leftShift( buffer, (InputStream)parsedData );
521 parsedData = new ByteArrayInputStream( buffer.toByteArray() );
522 }
523 else if ( parsedData instanceof Reader ) {
524 StringWriter buffer = new StringWriter();
525 DefaultGroovyMethods.leftShift( buffer, (Reader)parsedData );
526 parsedData = new StringReader( buffer.toString() );
527 }
528 else if ( parsedData instanceof Closeable )
529 log.warn( "Parsed data is streaming, but will be accessible after " +
530 "the network connection is closed. Use at your own risk!" );
531 return parsedData;
532 }
533
534 /**
535 * This is the default <code>response.failure</code> handler. It will be
536 * executed if no status-code-specific handler is set (i.e.
537 * <code>response.'404'= {..}</code>). This default handler will throw a
538 * {@link HttpResponseException} when executed. In most cases you
539 * will want to define your own <code>response.failure = {...}</code>
540 * handler from the request closure, if you don't want an exception to be
541 * thrown for a 4xx and 5xx status response.
542
543 * @param resp
544 * @throws HttpResponseException
545 */
546 protected void defaultFailureHandler( HttpResponse resp ) throws HttpResponseException {
547 throw new HttpResponseException( resp.getStatusLine().getStatusCode(),
548 resp.getStatusLine().getReasonPhrase() );
549 }
550
551 /**
552 * Retrieve the map of response code handlers. Each map key is a response
553 * code as a string (i.e. '401') or either 'success' or 'failure'. Use this
554 * to set default response handlers, e.g.
555 * <pre>builder.handler.'401' = { resp -> println "${resp.statusLine}" }</pre>
556 * @see Status
557 * @return
558 */
559 public Map<String,Closure> getHandler() {
560 return this.defaultResponseHandlers;
561 }
562
563 /**
564 * Retrieve the map of registered response content-type parsers. Use
565 * this to set default response parsers, e.g.
566 * <pre>
567 * builder.parser.'text/javascript' = { resp ->
568 * return resp.entity.content // just returns an InputStream
569 * }</pre>
570 * @return
571 */
572 public Map<String,Closure> getParser() {
573 return this.parsers.registeredParsers;
574 }
575
576 /**
577 * Retrieve the map of registered request content-type encoders. Use this
578 * to set a default request encoder, e.g.
579 * <pre>
580 * builder.encoder.'text/javascript' = { body ->
581 * def json = body.call( new JsonGroovyBuilder() )
582 * return new StringEntity( json.toString() )
583 * }
584 * @return
585 */
586 public Map<String,Closure> getEncoder() {
587 return this.encoders.registeredEncoders;
588 }
589
590 /**
591 * Set the default content type that will be used to select the appropriate
592 * request encoder and response parser. The {@link ContentType} enum holds
593 * some common content-types that may be used, i.e. <pre>
594 * import static ContentType.*
595 * builder.contentType = XML
596 * </pre>
597 * @see EncoderRegistry
598 * @see ParserRegistry
599 * @param ct either a {@link ContentType} or string value (i.e. <code>"text/xml"</code>.)
600 */
601 public void setContentType( Object ct ) {
602 this.defaultContentType = ct;
603 }
604
605
606 /**
607 * Set acceptable request and response content-encodings.
608 * @see ContentEncodingRegistry
609 * @param encodings each Object should be either a
610 * {@link ContentEncoding.Type} value, or a <code>content-encoding</code>
611 * string that is known by the {@link ContentEncodingRegistry}
612 */
613 public void setContentEncoding( Object... encodings ) {
614 this.contentEncodingHandler.setInterceptors( client, encodings );
615 }
616
617 /**
618 * Set the default URL used for requests that do not explicitly take a
619 * <code>url</code> param.
620 * @param url a URL, URI, or String
621 * @throws URISyntaxException
622 */
623 public void setURL( Object url ) throws URISyntaxException {
624 this.defaultURI = convertToURI( url );
625 }
626
627 /**
628 * Get the default URL used for requests that do not explicitly take a
629 * <code>url</code> param.
630 * @return url a {@link URL} instance. Note that the return type is Object
631 * simply so that it matches with its JavaBean {@link #setURL(Object)}
632 * counterpart.
633 */
634 public Object getURL() {
635 try {
636 return defaultURI.toURL();
637 } catch ( MalformedURLException e ) {
638 throw new RuntimeException( e );
639 }
640 }
641
642 /**
643 * Set the default headers to add to all requests made by this builder
644 * instance. These values will replace any previously set default headers.
645 * @param headers map of header names & values.
646 */
647 public void setHeaders( Map<?,?> headers ) {
648 this.defaultRequestHeaders.clear();
649 if ( headers == null ) return;
650 for( Object key : headers.keySet() ) {
651 Object val = headers.get( key );
652 if ( val == null ) continue;
653 this.defaultRequestHeaders.put( key.toString(), val.toString() );
654 }
655 }
656
657 /**
658 * Get the map of default headers that will be added to all requests.
659 * This is a 'live' collection so it may be used to add or remove default
660 * values.
661 * @return the map of default header names and values.
662 */
663 public Map<String,String> getHeaders() {
664 return this.defaultRequestHeaders;
665 }
666
667 /**
668 * Return the underlying HTTPClient that is used to handle HTTP requests.
669 * @return the client instance.
670 */
671 public AbstractHttpClient getClient() { return this.client; }
672
673 /**
674 * Used to access the {@link AuthConfig} handler used to configure common
675 * authentication mechanism. Example:
676 * <pre>builder.auth.basic( 'myUser', 'somePassword' )</pre>
677 * @return
678 */
679 public AuthConfig getAuth() { return this.auth; }
680
681 /**
682 * Set an alternative {@link AuthConfig} implementation to handle
683 * authorization.
684 * @param ac instance to use.
685 */
686 public void setAuthConfig( AuthConfig ac ) {
687 this.auth = ac;
688 }
689
690 /**
691 * Set a custom registry used to handle different request
692 * <code>content-type</code>s.
693 * @param er
694 */
695 public void setEncoderRegistry( EncoderRegistry er ) {
696 this.encoders = er;
697 }
698
699 /**
700 * Set a custom registry used to handle different response
701 * <code>content-type</code>s
702 * @param pr
703 */
704 public void setParserRegistry( ParserRegistry pr ) {
705 this.parsers = pr;
706 }
707
708 /**
709 * Set a custom registry used to handle different
710 * <code>content-encoding</code> types in responses.
711 * @param cer
712 */
713 public void setContentEncodingRegistry( ContentEncodingRegistry cer ) {
714 this.contentEncodingHandler = cer;
715 }
716
717 /**
718 * Release any system resources held by this instance.
719 * @see ClientConnectionManager#shutdown()
720 */
721 public void shutdown() {
722 client.getConnectionManager().shutdown();
723 }
724
725
726
727 /**
728 * Encloses all properties and method calls used within the
729 * {@link HTTPBuilder#request(Object, Method, Object, Closure)} 'config'
730 * closure argument.
731 */
732 protected class SendDelegate {
733 protected HttpRequestBase request;
734 protected Object contentType;
735 protected String requestContentType;
736 protected Map<String,Closure> responseHandlers = new HashMap<String,Closure>();
737 protected URIBuilder url;
738 protected Map<String,String> headers = new HashMap<String,String>();
739
740 public SendDelegate( HttpRequestBase request, Object contentType,
741 Map<String,String> defaultRequestHeaders,
742 Map<String,Closure> defaultResponseHandlers ) {
743 this.request = request;
744 this.headers.putAll( defaultRequestHeaders );
745 this.contentType = contentType;
746 this.responseHandlers.putAll( defaultResponseHandlers );
747 this.url = new URIBuilder(request.getURI());
748 }
749
750 /**
751 * Use this object to manipulate parts of the request URL, like
752 * query params and request path. Example:
753 * <pre>
754 * builder.request(GET,XML) {
755 * url.path = '../other/request.jsp'
756 * url.params = [p1:1, p2:2]
757 * ...
758 * }</pre>
759 * @return {@link URIBuilder} to manipulate the request URL
760 */
761 public URIBuilder getURL() { return this.url; }
762
763 protected HttpRequestBase getRequest() { return this.request; }
764
765 /**
766 * Get the content-type of any data sent in the request body and the
767 * expected response content-type.
768 * @return whatever value was assigned via {@link #setContentType(Object)}
769 * or passed from the {@link HTTPBuilder#defaultContentType} when this
770 * SendDelegateinstance was constructed.
771 */
772 protected Object getContentType() { return this.contentType; }
773
774 /**
775 * Set the content-type used for any data in the request body, as well
776 * as the <code>Accept</code> content-type that will be used for parsing
777 * the response. The value should be either a {@link ContentType} value
778 * or a String, i.e. <code>"text/plain"</code>
779 * @param ct content-type to send and recieve content
780 */
781 protected void setContentType( Object ct ) {
782 if ( ct == null ) this.contentType = defaultContentType;
783 this.contentType = ct;
784 }
785
786 /**
787 * The request content-type, if different from the {@link #contentType}.
788 * @return
789 */
790 protected String getRequestContentType() {
791 if ( this.requestContentType != null ) return this.requestContentType;
792 else return this.getContentType().toString();
793 }
794
795 /**
796 * Assign a different content-type for the request than is expected for
797 * the response. This is useful if i.e. you want to post URL-encoded
798 * form data but expect the response to be XML or HTML. The
799 * {@link #getContentType()} will always control the <code>Accept</code>
800 * header, and will be used for the request content <i>unless</i> this
801 * value is also explicitly set.
802 * @param ct either a {@link ContentType} value or a valid content-type
803 * String.
804 */
805 protected void setRequestContentType( String ct ) {
806 this.requestContentType = ct;
807 }
808
809 /**
810 * Valid arguments:
811 * <dl>
812 * <dt>url</dt><dd>Either a URI, URL, or String.
813 * If not supplied, the HTTPBuilder's default URL is used.</dd>
814 * <dt>path</dt><dd>Request path that is merged with the URL</dd>
815 * <dt>params</dt><dd>Map of request parameters</dd>
816 * <dt>headers</dt><dd>Map of HTTP headers</dd>
817 * <dt>contentType</dt><dd>Request content type and Accept header.
818 * If not supplied, the HTTPBuilder's default content-type is used.</dd>
819 * <dt>requestContentType</dt><dd>content type for the request, if it
820 * is different from the expected response content-type</dd>
821 * <dt>body</dt><dd>Request body that will be encoded based on the given contentType</dd>
822 * </dl>
823 * @param args named parameters to set properties on this delegate.
824 * @throws MalformedURLException
825 * @throws URISyntaxException
826 */
827 @SuppressWarnings("unchecked")
828 protected void setPropertiesFromMap( Map<String,?> args ) throws MalformedURLException, URISyntaxException {
829 Object uri = args.get( "url" );
830 if ( uri == null ) uri = defaultURI;
831 url = new URIBuilder( convertToURI( uri ) );
832
833 Map params = (Map)args.get( "params" );
834 if ( params != null ) this.url.setQuery( params );
835 Map headers = (Map)args.get( "headers" );
836 if ( headers != null ) this.getHeaders().putAll( headers );
837
838 Object path = args.get( "path" );
839 if ( path != null ) this.url.setPath( path.toString() );
840
841 Object contentType = args.get( "contentType" );
842 if ( contentType != null ) this.setContentType( contentType );
843
844 contentType = args.get( "requestContentType" );
845 if ( contentType != null ) this.setRequestContentType( contentType.toString() );
846
847 Object body = args.get("body");
848 if ( body != null ) this.setBody( body );
849 }
850
851 /**
852 * Set request headers. These values will be <strong>merged</strong>
853 * with any {@link HTTPBuilder#getHeaders() default request headers.}
854 * (The assumption is you'll probably want to add a bunch of headers to
855 * whatever defaults you've already set). If you <i>only</i> want to
856 * use values set here, simply call {@link #getHeaders() headers.clear()}
857 * first.
858 */
859 public void setHeaders( Map<?,?> newHeaders ) {
860 for( Object key : newHeaders.keySet() ) {
861 Object val = newHeaders.get( key );
862 if ( val == null ) this.headers.remove( key );
863 else this.headers.put( key.toString(), val.toString() );
864 }
865 }
866
867 /**
868 * Get request headers (including any default headers). Note that this
869 * will not include any <code>Accept</code>, <code>Content-Type</code>,
870 * or <code>Content-Encoding</code> headers that are automatically
871 * handled by any encoder or parsers in effect. Note that any values
872 * set here <i>will</i> override any of those automatically assigned
873 * values.
874 * header that is a
875 * @return
876 */
877 public Map<String,String> getHeaders() {
878 return this.headers;
879 }
880
881 /**
882 * Convenience method to set a request content-type at the same time
883 * the request body is set. This is a variation of
884 * {@link #setBody(Object)} that allows for a different content-type
885 * than what is expected for the response.
886 *
887 * <p>Example:
888 * <pre>
889 * http.request(POST,HTML) {
890 *
891 * /* request data is interpreted as a JsonBuilder closure in the
892 * default EncoderRegistry implementation * /
893 * send( 'text/javascript' ) {
894 * a : ['one','two','three']
895 * }
896 *
897 * // response content-type is what was specified in the outer request() argument:
898 * response.success = { resp, html ->
899 *
900 * }
901 * }
902 * </pre>
903 * @param contentType either a {@link ContentType} or content-type
904 * string like <code>"text/xml"</code>
905 * @param requestBody
906 */
907 public void send( Object contentType, Object requestBody ) {
908 this.setRequestContentType( contentType.toString() );
909 this.setBody( requestBody );
910 }
911
912 /**
913 * Set the request body. This value may be of any type supported by
914 * the associated {@link EncoderRegistry request encoder}.
915 * @see #send(Object, Object)
916 * @param body data or closure interpretes as the request body
917 */
918 public void setBody( Object body ) {
919 if ( ! (request instanceof HttpEntityEnclosingRequest ) )
920 throw new UnsupportedOperationException(
921 "Cannot set a request body for a " + request.getMethod() + " method" );
922 Closure encoder = encoders.get( this.getRequestContentType() );
923 HttpEntity entity = (HttpEntity)encoder.call( body );
924
925 ((HttpEntityEnclosingRequest)request).setEntity( entity );
926 }
927
928 /**
929 * Get the proper response handler for the response code. This is called
930 * by the {@link HTTPBuilder} class in order to find the proper handler
931 * based on the response status code.
932 *
933 * @param statusCode HTTP response status code
934 * @return the response handler
935 */
936 protected Closure findResponseHandler( int statusCode ) {
937 Closure handler = this.getResponse().get( Integer.toString( statusCode ) );
938 if ( handler == null ) handler =
939 this.getResponse().get( Status.find( statusCode ).toString() );
940 return handler;
941 }
942
943 /**
944 * Access the response handler map to set response parsing logic.
945 * i.e.<pre>
946 * builder.request( GET, XML ) {
947 * response.success = { xml ->
948 * /* for XML content type, the default parser
949 * will return an XmlSlurper * /
950 * xml.root.children().each { println it }
951 * }
952 * }</pre>
953 * @return
954 */
955 public Map<String,Closure> getResponse() { return this.responseHandlers; }
956 }
957 }