View Javadoc

1   /*
2    * Copyright 2003-2008 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   *
16   * You are receiving this code free of charge, which represents many hours of
17   * effort from other individuals and corporations.  As a responsible member 
18   * of the community, you are asked (but not required) to donate any 
19   * enhancements or improvements back to the community under a similar open 
20   * source license.  Thank you. -TMN
21   */
22  package groovyx.net.http;
23  
24  import groovy.lang.Closure;
25  import groovy.lang.Writable;
26  import groovy.xml.StreamingMarkupBuilder;
27  import groovyx.net.http.HTTPBuilder.SendDelegate;
28  
29  import java.io.BufferedReader;
30  import java.io.ByteArrayInputStream;
31  import java.io.ByteArrayOutputStream;
32  import java.io.IOException;
33  import java.io.InputStream;
34  import java.io.PrintWriter;
35  import java.io.Reader;
36  import java.io.StringWriter;
37  import java.io.UnsupportedEncodingException;
38  import java.nio.charset.Charset;
39  import java.util.ArrayList;
40  import java.util.HashMap;
41  import java.util.List;
42  import java.util.Map;
43  
44  import net.sf.json.JSON;
45  import net.sf.json.JSONObject;
46  import net.sf.json.groovy.JsonGroovyBuilder;
47  
48  import org.apache.http.HttpEntity;
49  import org.apache.http.HttpEntityEnclosingRequest;
50  import org.apache.http.NameValuePair;
51  import org.apache.http.client.entity.UrlEncodedFormEntity;
52  import org.apache.http.entity.InputStreamEntity;
53  import org.apache.http.entity.StringEntity;
54  import org.apache.http.message.BasicNameValuePair;
55  import org.codehaus.groovy.runtime.MethodClosure;
56  
57  
58  /**
59   * <p>This factory (or registry) handles request body "encoding." This is not
60   * to be confused with HTTP content-encoding header.  When a 
61   * {@link SendDelegate#setBody(Object) body} is set from the builder, it is 
62   * processed based on the request content-type.  For instance, if the body
63   * is set to a map and the request content-type is JSON, the map will be 
64   * transformed to a JSON Object.  </p>
65   * 
66   * <p>Most default encoders can handle a closure as a request body.  In this 
67   * case, the closure is executed and a suitable 'builder' passed to the 
68   * closure that is  used for constructing the content.  In the case of 
69   * binary encoding this would be an OutputStream; for TEXT encoding it would
70   * be a PrintWriter, and for XML it would be an already-bound 
71   * {@link StreamingMarkupBuilder}. </p>
72   */
73  public class EncoderRegistry {
74  	
75  	Charset charset = Charset.defaultCharset(); // 1.5
76  	
77  	/**
78  	 * Set the charset used in the content-type header of all requests that send
79  	 * textual data.  This must be a chaset supported by the Java platform
80  	 * @see Charset#forName(String)
81  	 * @param charset 
82  	 */
83  	public void setCharset( String charset ) { 
84  		this.charset = Charset.forName(charset);
85  	}
86  	
87  	/**
88  	 * Default request encoder for a binary stream.  Acceptable argument 
89  	 * types are:
90  	 * <ul>
91  	 *   <li>InputStream</li>
92  	 *   <li>ByteArrayOutputStream</li>
93  	 *   <li>Closure</li>
94  	 * </ul>
95  	 * If a closure is given, it is executed with an OutputStream passed
96  	 * as the single closure argument.  Any data sent to the stream from the 
97  	 * body of the closure is used as the request content body.
98  	 * @param data
99  	 * @return an {@link HttpEntity} encapsulating this request data
100 	 * @throws UnsupportedEncodingException
101 	 */
102 	public InputStreamEntity encodeStream( Object data ) throws UnsupportedEncodingException {
103 		if ( data instanceof InputStream ) {
104 			return new InputStreamEntity( (InputStream)data, -1 );
105 		}
106 		else if ( data instanceof ByteArrayInputStream ) {
107 			ByteArrayInputStream in = ((ByteArrayInputStream)data);
108 			return new InputStreamEntity( in, in.available() );
109 		}
110 		else if ( data instanceof ByteArrayOutputStream ) {
111 			ByteArrayOutputStream out = ((ByteArrayOutputStream)data); 
112 			return new InputStreamEntity( new ByteArrayInputStream(
113 					out.toByteArray()), out.size() );
114 		}
115 		else if ( data instanceof Closure ) {
116 			ByteArrayOutputStream out = new ByteArrayOutputStream();
117 			((Closure)data).call( out ); // data is written to out
118 			return new InputStreamEntity( new ByteArrayInputStream(out.toByteArray()), out.size() );
119 		}
120 		throw new IllegalArgumentException( "Don't know how to encode " + data + " as a byte stream" );
121 	}
122 	
123 	/**
124 	 * Default handler used for a plain text content-type.  Acceptable argument
125 	 * types are:
126 	 * <ul>
127 	 *   <li>Closure</li>
128 	 *   <li>Writable</li>
129 	 *   <li>Reader</li>
130 	 * </ul>
131 	 * For Closure argument, a {@link PrintWriter} is passed as the single 
132 	 * argument to the closure.  Any data sent to the writer from the 
133 	 * closure will be sent to the request content body.
134 	 * @param data
135 	 * @return an {@link HttpEntity} encapsulating this request data
136 	 * @throws IOException
137 	 */
138 	public HttpEntity encodeText( Object data ) throws IOException {
139 		if ( data instanceof Closure ) {
140 			StringWriter out = new StringWriter();
141 			PrintWriter writer = new PrintWriter( out );
142 			((Closure)data).call( writer );
143 			writer.close();
144 			out.flush();
145 			data = out;
146 		}
147 		else if ( data instanceof Writable ) {
148 			StringWriter out = new StringWriter();
149 			((Writable)data).writeTo(out);
150 			out.flush();
151 			data = out;
152 		}
153 		else if ( data instanceof Reader && ! (data instanceof BufferedReader) )
154 			data = new BufferedReader( (Reader)data );
155 		if ( data instanceof BufferedReader ) {
156 			StringBuilder sb = new StringBuilder();
157 			BufferedReader reader = (BufferedReader)data;
158 			String line = null;
159 			while( (line = reader.readLine()) != null )
160 				sb.append( line );
161 			
162 			data = sb;
163 		}
164 		// if data is a String, we are already covered.
165 		return createEntity( ContentType.TEXT, data.toString() );
166 	}
167 	
168 	/**
169 	 * Set the request body as a url-encoded list of parameters.  This is 
170 	 * typically used to simulate a HTTP form POST. 
171 	 * @param params
172 	 * @return an {@link HttpEntity} encapsulating this request data
173 	 * @throws UnsupportedEncodingException
174 	 */
175 	public UrlEncodedFormEntity encodeForm( Map<String,Object> params ) 
176 			throws UnsupportedEncodingException {
177 		List<NameValuePair> paramList = new ArrayList<NameValuePair>();
178 
179 		for ( Map.Entry<String, Object> entry : params.entrySet() ) 
180 			paramList.add( new BasicNameValuePair(entry.getKey(), 
181 					entry.getValue().toString()) );
182 			
183 		return new UrlEncodedFormEntity( paramList, charset.name() );
184 	}
185 	
186 	/**
187 	 * Executes the given closure and passes a bound {@link StreamingMarkupBuilder}.
188 	 * @param xmlBuilder
189 	 * @return an {@link HttpEntity} encapsulating this request data
190 	 * @throws UnsupportedEncodingException
191 	 */
192 	public HttpEntity encodeXML( Closure xmlBuilder ) throws UnsupportedEncodingException {
193 		StreamingMarkupBuilder smb = new StreamingMarkupBuilder();
194 		String markup = smb.bind( xmlBuilder ).toString();
195 		return createEntity( ContentType.XML, markup);
196 	}
197 	
198 	/**
199 	 * Accepts a Map or a JavaBean object which is converted to JSON.  If
200 	 * a Closure is passed, it will be executed with a 
201 	 * {@link JsonGroovyBuilder} as the closure's delegate.  The closure 
202 	 * must return the result of the outermost builder method call. 
203 	 * @param model data to be converted to JSON, as specified above.
204 	 * @return an {@link HttpEntity} encapsulating this request data
205 	 * @throws UnsupportedEncodingException
206 	 */
207 	@SuppressWarnings("unchecked")
208 	public HttpEntity encodeJSON( Object model ) throws UnsupportedEncodingException {
209 		JSON json;
210 		
211 		if ( model instanceof Map ) {
212 			json = new JSONObject();
213 			((JSONObject)json).putAll( (Map)model );
214 		}
215 		else if ( model instanceof Closure ) {
216 			Closure closure = (Closure)model;
217 			closure.setDelegate( new JsonGroovyBuilder() );
218 			json = (JSONObject)closure.call();
219 		}
220 		else json = JSONObject.fromObject( model ); // Assume object is a JavaBean
221 		
222 		return this.createEntity( ContentType.JSON, json.toString() );
223 	}
224 	
225 	/**
226 	 * Helper method used by encoder methods to creates an {@link HttpEntity} 
227 	 * instance that encapsulates the request data.  This may be used by any 
228 	 * non-streaming encoder that needs to send textual data.  It also sets the 
229 	 * {@link #setCharset(String) charset} portion of the content-type header. 
230 	 * 
231 	 * @param ct content-type of the data
232 	 * @param data textual request data to be encoded 
233 	 * @return an instance to be used for the 
234 	 *  {@link HttpEntityEnclosingRequest#setEntity(HttpEntity) request content} 
235 	 * @throws UnsupportedEncodingException
236 	 */
237 	protected StringEntity createEntity( ContentType ct, String data ) 
238 			throws UnsupportedEncodingException {
239 		StringEntity entity = new StringEntity( data, charset.toString() );
240 		entity.setContentType( ct.toString() );
241 		return entity;
242 	}
243 	
244 	protected Map<String,Closure> registeredEncoders = buildDefaultEncoderMap();
245 
246 	/** 
247 	 * Used to set an additional encoder for the given content type.  The 
248 	 * Closure must return an {@link HttpEntity}.  It will also usually 
249 	 * accept a single argument, which will be the value given in  
250 	 * {@link SendDelegate#setBody(Object)}.
251 	 * @param contentType
252 	 * @param closure
253 	 */
254 	public void register( String contentType, Closure closure ) {
255 		registeredEncoders.put( contentType, closure );
256 	}
257 	
258 	/* Get the encoder for the given content-type.  Not usually called 
259 	 * by the end-user.  The HTTPBuilder will get the appropriate encoder 
260 	 * automatically in order to encode the request body data.
261 	 * @param contentType
262 	 * @return the encoder closure, or <code>null</code> if no encoder is
263 	 * registered.
264 	 */
265 	Closure get( String contentType ) { return registeredEncoders.get(contentType); }
266 	
267 	/**
268 	 * Returns a map of default encoders.  Override this method to change 
269 	 * what encoders are registered by default.  You can of course call
270 	 * <code>super.buildDefaultEncoderMap()</code> and then add or remove 
271 	 * from that result as well.
272 	 */
273 	protected Map<String,Closure> buildDefaultEncoderMap() {
274 		Map<String,Closure> encoders = new HashMap<String,Closure>();
275 		
276 		encoders.put( ContentType.BINARY.toString(), new MethodClosure(this,"encodeStream") );
277 		encoders.put( ContentType.TEXT.toString(), new MethodClosure( this, "encodeText" ) );
278 		encoders.put( ContentType.URLENC.toString(), new MethodClosure( this, "encodeForm" ) );
279 		
280 		Closure encClosure = new MethodClosure(this,"encodeXML");
281 		for ( String ct : ContentType.XML.getContentTypeStrings() )
282 			encoders.put( ct, encClosure );
283 		encoders.put( ContentType.HTML.toString(), encClosure );
284 		
285 		encClosure = new MethodClosure(this,"encodeJSON");
286 		for ( String ct : ContentType.JSON.getContentTypeStrings() )
287 			encoders.put( ct, encClosure );
288 		
289 		return encoders;
290 	}
291 }