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 }