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