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 groovy.lang.Closure;
025
026 import java.io.IOException;
027 import java.net.MalformedURLException;
028 import java.net.URI;
029 import java.net.URISyntaxException;
030 import java.net.URL;
031 import java.util.HashMap;
032 import java.util.Map;
033
034 import org.apache.commons.logging.Log;
035 import org.apache.commons.logging.LogFactory;
036 import org.apache.http.HttpEntity;
037 import org.apache.http.HttpEntityEnclosingRequest;
038 import org.apache.http.HttpResponse;
039 import org.apache.http.client.ClientProtocolException;
040 import org.apache.http.client.HttpResponseException;
041 import org.apache.http.client.methods.HttpGet;
042 import org.apache.http.client.methods.HttpPost;
043 import org.apache.http.client.methods.HttpRequestBase;
044 import org.apache.http.conn.ClientConnectionManager;
045 import org.apache.http.impl.client.AbstractHttpClient;
046 import org.apache.http.impl.client.DefaultHttpClient;
047 import org.codehaus.groovy.runtime.DefaultGroovyMethods;
048 import org.codehaus.groovy.runtime.MethodClosure;
049
050 import static groovyx.net.http.URIBuilder.convertToURI;
051
052 /** <p>
053 * Groovy DSL for easily making HTTP requests, and handling request and response
054 * data. This class adds a number of convenience mechanisms built on top of
055 * Apache HTTPClient for things like URL-encoded POSTs and REST requests that
056 * require building and parsing JSON or XML. Convenient access to a few common
057 * authentication methods is also available.</p>
058 *
059 *
060 * <h3>Conventions</h3>
061 * <p>HTTPBuilder has properties for default headers, URL, contentType, etc.
062 * All of these values are also assignable (and in many cases, in much finer
063 * detail) from the {@link SendDelegate} as well. In any cases where the value
064 * is not set on the delegate (from within a request closure,) the builder's
065 * default value is used. </p>
066 *
067 * <p>For instance, any methods that do not take a URL parameter assume you will
068 * set a URL value in the request closure or use the builder's assigned
069 * {@link #getURL() default URL}.</p>
070 *
071 *
072 * <h3>Response Parsing</h3>
073 * <p>By default, HTTPBuilder uses {@link ContentType#ANY} as the default
074 * content-type. This means the value of the request's <code>Accept</code>
075 * header is <code>*/*</code>, and the response parser is determined
076 * based on the response <code>content-type</code> header. </p>
077 *
078 * <p><strong>If</strong> any contentType is given (either in
079 * {@link #setContentType(Object)} or as a request method parameter), the
080 * builder will attempt to parse the response using that content-type,
081 * regardless of what the server actually responds with. </p>
082 *
083 *
084 * <h3>Examples:</h3>
085 * Perform an HTTP GET and print the response:
086 * <pre>
087 * def http = new HTTPBuilder('http://www.google.com')
088 *
089 * http.get( path : '/search',
090 * contentType : TEXT,
091 * query : [q:'Groovy'] ) { resp, reader ->
092 * println "response status: ${resp.statusLine}"
093 * println 'Response data: -----'
094 * System.out << reader
095 * println '\n--------------------'
096 * }
097 * </pre>
098 *
099 * 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 }