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    }