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 import groovy.lang.Writable;
026 import groovy.xml.StreamingMarkupBuilder;
027 import groovyx.net.http.HTTPBuilder.SendDelegate;
028
029 import java.io.BufferedReader;
030 import java.io.ByteArrayInputStream;
031 import java.io.ByteArrayOutputStream;
032 import java.io.IOException;
033 import java.io.InputStream;
034 import java.io.PrintWriter;
035 import java.io.Reader;
036 import java.io.StringWriter;
037 import java.io.UnsupportedEncodingException;
038 import java.nio.charset.Charset;
039 import java.util.ArrayList;
040 import java.util.HashMap;
041 import java.util.List;
042 import java.util.Map;
043
044 import net.sf.json.JSON;
045 import net.sf.json.JSONObject;
046 import net.sf.json.groovy.JsonGroovyBuilder;
047
048 import org.apache.http.HttpEntity;
049 import org.apache.http.HttpEntityEnclosingRequest;
050 import org.apache.http.NameValuePair;
051 import org.apache.http.client.entity.UrlEncodedFormEntity;
052 import org.apache.http.entity.InputStreamEntity;
053 import org.apache.http.entity.StringEntity;
054 import org.apache.http.message.BasicNameValuePair;
055 import org.codehaus.groovy.runtime.MethodClosure;
056
057
058 /**
059 * <p>This factory (or registry) handles request body "encoding." This is not
060 * to be confused with HTTP content-encoding header. When a
061 * {@link SendDelegate#setBody(Object) body} is set from the builder, it is
062 * processed based on the request content-type. For instance, if the body
063 * is set to a map and the request content-type is JSON, the map will be
064 * transformed to a JSON Object. </p>
065 *
066 * <p>Most default encoders can handle a closure as a request body. In this
067 * case, the closure is executed and a suitable 'builder' passed to the
068 * closure that is used for constructing the content. In the case of
069 * binary encoding this would be an OutputStream; for TEXT encoding it would
070 * be a PrintWriter, and for XML it would be an already-bound
071 * {@link StreamingMarkupBuilder}. </p>
072 */
073 public class EncoderRegistry {
074
075 Charset charset = Charset.defaultCharset(); // 1.5
076
077 /**
078 * Set the charset used in the content-type header of all requests that send
079 * textual data. This must be a chaset supported by the Java platform
080 * @see Charset#forName(String)
081 * @param charset
082 */
083 public void setCharset( String charset ) {
084 this.charset = Charset.forName(charset);
085 }
086
087 /**
088 * Default request encoder for a binary stream. Acceptable argument
089 * types are:
090 * <ul>
091 * <li>InputStream</li>
092 * <li>ByteArrayOutputStream</li>
093 * <li>Closure</li>
094 * </ul>
095 * If a closure is given, it is executed with an OutputStream passed
096 * as the single closure argument. Any data sent to the stream from the
097 * body of the closure is used as the request content body.
098 * @param data
099 * @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 }