1818
1919package com .loopj .android .http ;
2020
21- import android .util .Base64 ;
22- import android .util .Base64OutputStream ;
2321import android .util .Log ;
2422
2523import org .apache .http .Header ;
2624import org .apache .http .HttpEntity ;
2725import org .apache .http .message .BasicHeader ;
2826
2927import java .io .BufferedOutputStream ;
30- import java .io .File ;
31- import java .io .FileInputStream ;
3228import java .io .IOException ;
3329import java .io .InputStream ;
3430import java .io .OutputStream ;
@@ -49,44 +45,50 @@ class JsonStreamerEntity implements HttpEntity {
4945 private static final UnsupportedOperationException ERR_UNSUPPORTED =
5046 new UnsupportedOperationException ("Unsupported operation in this implementation." );
5147
48+ // Size of the byte-array buffer used to read from streams.
49+ private static final int BUFFER_SIZE = 2048 ;
50+
51+ // Reusable StringBuilder used by escape() method.
52+ // Base64, at worst, will make a binary stream grow in size by approximately
53+ // (n + 2 - ((n + 2) % 3)) / 3 * 4, which is roughly 1.3333333% for a
54+ // large 'n'.
55+ private static final StringBuilder BUILDER =
56+ new StringBuilder ((int )(BUFFER_SIZE * 1.35f ));
57+
5258 private static final byte [] JSON_TRUE = "true" .getBytes ();
5359 private static final byte [] JSON_FALSE = "false" .getBytes ();
54- private static final byte [] STREAM_NAME = escape ("name" , true ).getBytes ();
55- private static final byte [] STREAM_TYPE = escape ("type" , true ).getBytes ();
56- private static final byte [] STREAM_CONTENTS = escape ("contents" , true ).getBytes ();
57- private static final byte [] STREAM_ELAPSED = escape ("_elapsed" , true ).getBytes ();
60+ private static final byte [] JSON_NULL = "null" .getBytes ();
61+ private static final byte [] STREAM_NAME = escape ("name" );
62+ private static final byte [] STREAM_TYPE = escape ("type" );
63+ private static final byte [] STREAM_CONTENTS = escape ("contents" );
64+ private static final byte [] STREAM_ELAPSED = escape ("_elapsed" );
5865
59- private static final Header HEADER_JSON =
66+ private static final Header HEADER_JSON_CONTENT =
6067 new BasicHeader ("Content-Type" , "application/json" );
68+ private static final Header HEADER_GZIP_ENCODING =
69+ new BasicHeader ("Content-Encoding" , "gzip" );
6170 private static final String APPLICATION_OCTET_STREAM =
6271 "application/octet-stream" ;
6372
64- // Size of the byte-array buffer used to read from files.
65- private static final int BUFFER_SIZE = 2048 ;
66-
6773 // K/V objects to be uploaded.
68- private final Map <String , Object > kvParams = new HashMap <String , Object >();
74+ private final Map <String , Object > kvParams =
75+ new HashMap <String , Object >();
6976
7077 // Streams and their associated meta-data to be uploaded.
71- private final Map <String , RequestParams .StreamWrapper > streamParams = new HashMap <String , RequestParams .StreamWrapper >();
78+ private final Map <String , RequestParams .StreamWrapper > streamParams =
79+ new HashMap <String , RequestParams .StreamWrapper >();
7280
7381 // Whether to use gzip compression while uploading
7482 private final Header contentEncoding ;
7583
7684 public JsonStreamerEntity (boolean contentEncoding ) {
77- this .contentEncoding = contentEncoding
78- ? new BasicHeader ("Content-Encoding" , "gzip" )
79- : null ;
85+ this .contentEncoding = contentEncoding ? HEADER_GZIP_ENCODING : null ;
8086 }
8187
8288 public void addPart (String key , Object value ) {
8389 kvParams .put (key , value );
8490 }
8591
86- public void addPart (String key , File file , String type ) throws IOException {
87- addPart (key , new FileInputStream (file ), file .getName (), type );
88- }
89-
9092 public void addPart (String key , InputStream inputStream , String name , String type ) {
9193 if (type == null ) {
9294 type = APPLICATION_OCTET_STREAM ;
@@ -121,7 +123,7 @@ public Header getContentEncoding() {
121123
122124 @ Override
123125 public Header getContentType () {
124- return HEADER_JSON ;
126+ return HEADER_JSON_CONTENT ;
125127 }
126128
127129 @ Override
@@ -139,13 +141,16 @@ public void writeTo(final OutputStream outstream) throws IOException {
139141 throw new IllegalStateException ("Output stream cannot be null." );
140142 }
141143
144+ // Record the time when uploading started.
142145 long now = System .currentTimeMillis ();
143- Log .i (LOG_TAG , "Started dumping at: " + now );
144146
145- OutputStream upload ;
147+ // Keys used by the HashMaps.
148+ Set <String > keys ;
146149
147- // GZIPOutputStream is available only from API level 8 and onward.
148- if (null != contentEncoding ) {
150+ // Use GZIP compression when sending streams, otherwise just use
151+ // a buffered output stream to speed things up a bit.
152+ OutputStream upload ;
153+ if (null != contentEncoding ) {
149154 upload = new GZIPOutputStream (new BufferedOutputStream (outstream ), BUFFER_SIZE );
150155 } else {
151156 upload = new BufferedOutputStream (outstream );
@@ -154,21 +159,18 @@ public void writeTo(final OutputStream outstream) throws IOException {
154159 // Always send a JSON object.
155160 upload .write ('{' );
156161
157- // Keys used by the HashMaps.
158- Set <String > keys ;
159-
160162 // Send the K/V values.
161163 keys = kvParams .keySet ();
162164 for (String key : keys ) {
163165 // Write the JSON object's key.
164- upload .write (escape (key , true ). getBytes ( ));
166+ upload .write (escape (key ));
165167 upload .write (':' );
166168
167169 // Evaluate the value (which cannot be null).
168170 Object value = kvParams .get (key );
169171
170172 if (value instanceof Boolean ) {
171- upload .write ((Boolean ) value ? JSON_TRUE : JSON_FALSE );
173+ upload .write ((Boolean )value ? JSON_TRUE : JSON_FALSE );
172174 } else if (value instanceof Long ) {
173175 upload .write ((((Number )value ).longValue () + "" ).getBytes ());
174176 } else if (value instanceof Double ) {
@@ -189,113 +191,129 @@ public void writeTo(final OutputStream outstream) throws IOException {
189191
190192 // Send the stream params.
191193 keys = streamParams .keySet ();
192- for (String key : keys ) {
194+ for (String key : keys ) {
193195 RequestParams .StreamWrapper entry = streamParams .get (key );
194196
195197 // Write the JSON object's key.
196- upload .write (escape (key , true ). getBytes ( ));
198+ upload .write (escape (key ));
197199
198200 // All uploads are sent as an object containing the file's details.
199- upload .write (":{" .getBytes ());
201+ upload .write (':' );
202+ upload .write ('{' );
200203
201204 // Send the streams's name.
202205 upload .write (STREAM_NAME );
203206 upload .write (':' );
204- upload .write (escape (entry .name , true ).getBytes ());
207+ upload .write (escape (entry .name ));
208+ upload .write (',' );
205209
206210 // Send the streams's content type.
207211 upload .write (STREAM_TYPE );
208212 upload .write (':' );
209- upload .write (escape (entry .contentType , true ).getBytes ());
213+ upload .write (escape (entry .contentType ));
214+ upload .write (',' );
210215
211216 // Prepare the file content's key.
212217 upload .write (STREAM_CONTENTS );
213218 upload .write (':' );
214219 upload .write ('"' );
215220
216- // Write the file's contents in Base64.
217- Base64OutputStream outputStream = new Base64OutputStream (upload , Base64 .NO_CLOSE | Base64 .NO_WRAP );
221+ // Upload the file's contents in Base64.
222+ Base64OutputStream outputStream =
223+ new Base64OutputStream (upload , Base64 .NO_CLOSE | Base64 .NO_WRAP );
224+
225+ // Read from input stream until no more data's left to read.
218226 int bytesRead ;
219- while (- 1 != ( bytesRead = entry .inputStream .read (buffer ))) {
227+ while (( bytesRead = entry .inputStream .read (buffer )) != - 1 ) {
220228 outputStream .write (buffer , 0 , bytesRead );
221229 }
222230
223- // Close the output stream.
231+ // Close the Base64 output stream.
224232 outputStream .close ();
225233
226- // Close the file's object.
234+ // End the file's object and prepare for next one .
227235 upload .write ('"' );
228236 upload .write ('}' );
229237 upload .write (',' );
230238 }
231239
232240 // Include the elapsed time taken to upload everything.
241+ // This might be useful for somebody, but it serves us well since
242+ // there will almost always be a ',' as the last sent character.
233243 upload .write (STREAM_ELAPSED );
234244 upload .write (':' );
235245 long elapsedTime = System .currentTimeMillis () - now ;
236246 upload .write ((elapsedTime + "}" ).getBytes ());
237247
238- Log .i (LOG_TAG , "JSON was uploaded in " + Math .floor (elapsedTime / 1000 ) + " seconds" );
248+ Log .i (LOG_TAG , "Uploaded JSON in " + Math .floor (elapsedTime / 1000 ) + " seconds" );
239249
240250 // Flush the contents up the stream.
241251 upload .flush ();
242252 upload .close ();
243253 }
244254
245- // Curtosy of Simple-JSON:
246- // http://goo.gl/XoW8RF
247- private static String escape (String string , boolean quotes ) {
248- StringBuilder sb = new StringBuilder ();
249- int length = string .length (), pos = -1 ;
250- if (quotes ) {
251- sb .append ('"' );
255+ // Curtosy of Simple-JSON: http://goo.gl/XoW8RF
256+ // Changed a bit to suit our needs in this class.
257+ static byte [] escape (String string ) {
258+ // If it's null, just return prematurely.
259+ if (string == null ) {
260+ return JSON_NULL ;
252261 }
262+
263+ // Surround with quotations.
264+ BUILDER .append ('"' );
265+
266+ int length = string .length (), pos = -1 ;
253267 while (++pos < length ) {
254268 char ch = string .charAt (pos );
255269 switch (ch ) {
256270 case '"' :
257- sb .append ("\\ \" " );
271+ BUILDER .append ("\\ \" " );
258272 break ;
259273 case '\\' :
260- sb .append ("\\ \\ " );
274+ BUILDER .append ("\\ \\ " );
261275 break ;
262276 case '\b' :
263- sb .append ("\\ b" );
277+ BUILDER .append ("\\ b" );
264278 break ;
265279 case '\f' :
266- sb .append ("\\ f" );
280+ BUILDER .append ("\\ f" );
267281 break ;
268282 case '\n' :
269- sb .append ("\\ n" );
283+ BUILDER .append ("\\ n" );
270284 break ;
271285 case '\r' :
272- sb .append ("\\ r" );
286+ BUILDER .append ("\\ r" );
273287 break ;
274288 case '\t' :
275- sb .append ("\\ t" );
276- break ;
277- case '/' :
278- sb .append ("\\ /" );
289+ BUILDER .append ("\\ t" );
279290 break ;
280291 default :
281292 // Reference: http://www.unicode.org/versions/Unicode5.1.0/
282293 if ((ch >= '\u0000' && ch <= '\u001F' ) || (ch >= '\u007F' && ch <= '\u009F' ) || (ch >= '\u2000' && ch <= '\u20FF' )) {
283294 String intString = Integer .toHexString (ch );
284- sb .append ("\\ u" );
295+ BUILDER .append ("\\ u" );
285296 int intLength = 4 - intString .length ();
286297 for (int zero = 0 ; zero < intLength ; zero ++) {
287- sb .append ('0' );
298+ BUILDER .append ('0' );
288299 }
289- sb .append (intString .toUpperCase ());
300+ BUILDER .append (intString .toUpperCase ());
290301 } else {
291- sb .append (ch );
302+ BUILDER .append (ch );
292303 }
293304 break ;
294305 }
295306 }
296- if (quotes ) {
297- sb .append ('"' );
307+
308+ // Surround with quotations.
309+ BUILDER .append ('"' );
310+
311+ try {
312+ return BUILDER .toString ().getBytes ();
313+ } finally {
314+ // Empty the String buffer.
315+ // This is 20-30% faster than instantiating a new object.
316+ BUILDER .setLength (0 );
298317 }
299- return sb .toString ();
300318 }
301319}
0 commit comments