Skip to content

Commit 6857a93

Browse files
committed
Merge pull request android-async-http#780 from fineswap/MultiThreadingBug
Couple of fixes, one is very important:
2 parents 7da8ed5 + 2b4c33b commit 6857a93

File tree

2 files changed

+122
-90
lines changed

2 files changed

+122
-90
lines changed

library/src/main/java/com/loopj/android/http/JsonStreamerEntity.java

Lines changed: 100 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
package com.loopj.android.http;
2020

21+
import android.text.TextUtils;
2122
import android.util.Log;
2223

2324
import org.apache.http.Header;
@@ -51,18 +52,12 @@ public class JsonStreamerEntity implements HttpEntity {
5152
// Buffer used for reading from input streams.
5253
private final byte[] buffer = new byte[BUFFER_SIZE];
5354

54-
// Reusable StringBuilder used by escape() method.
55-
// Its size is just initial, if more space is needed, the system will
56-
// automatically enlarge the buffer.
57-
private static final StringBuilder BUILDER = new StringBuilder(128);
58-
5955
private static final byte[] JSON_TRUE = "true".getBytes();
6056
private static final byte[] JSON_FALSE = "false".getBytes();
6157
private static final byte[] JSON_NULL = "null".getBytes();
6258
private static final byte[] STREAM_NAME = escape("name");
6359
private static final byte[] STREAM_TYPE = escape("type");
6460
private static final byte[] STREAM_CONTENTS = escape("contents");
65-
private static final byte[] STREAM_ELAPSED = escape("_elapsed");
6661

6762
private static final Header HEADER_JSON_CONTENT =
6863
new BasicHeader(
@@ -80,11 +75,16 @@ public class JsonStreamerEntity implements HttpEntity {
8075
// Whether to use gzip compression while uploading
8176
private final Header contentEncoding;
8277

78+
private final byte[] elapsedField;
79+
8380
private final ResponseHandlerInterface progressHandler;
8481

85-
public JsonStreamerEntity(ResponseHandlerInterface progressHandler, boolean useGZipCompression) {
82+
public JsonStreamerEntity(ResponseHandlerInterface progressHandler, boolean useGZipCompression, String elapsedField) {
8683
this.progressHandler = progressHandler;
8784
this.contentEncoding = useGZipCompression ? HEADER_GZIP_ENCODING : null;
85+
this.elapsedField = TextUtils.isEmpty(elapsedField)
86+
? null
87+
: escape(elapsedField);
8888
}
8989

9090
/**
@@ -147,7 +147,7 @@ public void writeTo(final OutputStream out) throws IOException {
147147

148148
// Use GZIP compression when sending streams, otherwise just use
149149
// a buffered output stream to speed things up a bit.
150-
OutputStream os = null != contentEncoding
150+
OutputStream os = contentEncoding != null
151151
? new GZIPOutputStream(out, BUFFER_SIZE)
152152
: out;
153153

@@ -157,71 +157,90 @@ public void writeTo(final OutputStream out) throws IOException {
157157
// Keys used by the HashMaps.
158158
Set<String> keys = jsonParams.keySet();
159159

160-
boolean isFileWrapper;
161-
162-
// Go over all keys and handle each's value.
163-
for (String key : keys) {
164-
// Evaluate the value (which cannot be null).
165-
Object value = jsonParams.get(key);
166-
167-
// Bail out prematurely if value's null.
168-
if (value == null) {
169-
continue;
170-
}
160+
int keysCount = keys.size();
161+
if (0 < keysCount) {
162+
int keysProcessed = 0;
163+
boolean isFileWrapper;
171164

172-
// Write the JSON object's key.
173-
os.write(escape(key));
174-
os.write(':');
165+
// Go over all keys and handle each's value.
166+
for (String key : keys) {
167+
// Indicate that this key has been processed.
168+
keysProcessed++;
175169

176-
// Check if this is a FileWrapper.
177-
isFileWrapper = value instanceof RequestParams.FileWrapper;
170+
try {
171+
// Evaluate the value (which cannot be null).
172+
Object value = jsonParams.get(key);
178173

179-
// If a file should be uploaded.
180-
if (isFileWrapper || value instanceof RequestParams.StreamWrapper) {
181-
// All uploads are sent as an object containing the file's details.
182-
os.write('{');
174+
// Write the JSON object's key.
175+
os.write(escape(key));
176+
os.write(':');
183177

184-
// Determine how to handle this entry.
185-
if (isFileWrapper) {
186-
writeToFromFile(os, (RequestParams.FileWrapper) value);
187-
} else {
188-
writeToFromStream(os, (RequestParams.StreamWrapper) value);
178+
// Bail out prematurely if value's null.
179+
if (value == null) {
180+
os.write(JSON_NULL);
181+
} else {
182+
// Check if this is a FileWrapper.
183+
isFileWrapper = value instanceof RequestParams.FileWrapper;
184+
185+
// If a file should be uploaded.
186+
if (isFileWrapper || value instanceof RequestParams.StreamWrapper) {
187+
// All uploads are sent as an object containing the file's details.
188+
os.write('{');
189+
190+
// Determine how to handle this entry.
191+
if (isFileWrapper) {
192+
writeToFromFile(os, (RequestParams.FileWrapper) value);
193+
} else {
194+
writeToFromStream(os, (RequestParams.StreamWrapper) value);
195+
}
196+
197+
// End the file's object and prepare for next one.
198+
os.write('}');
199+
} else if (value instanceof JsonValueInterface) {
200+
os.write(((JsonValueInterface) value).getEscapedJsonValue());
201+
} else if (value instanceof org.json.JSONObject) {
202+
os.write(((org.json.JSONObject) value).toString().getBytes());
203+
} else if (value instanceof org.json.JSONArray) {
204+
os.write(((org.json.JSONArray) value).toString().getBytes());
205+
} else if (value instanceof Boolean) {
206+
os.write((Boolean) value ? JSON_TRUE : JSON_FALSE);
207+
} else if (value instanceof Long) {
208+
os.write((((Number) value).longValue() + "").getBytes());
209+
} else if (value instanceof Double) {
210+
os.write((((Number) value).doubleValue() + "").getBytes());
211+
} else if (value instanceof Float) {
212+
os.write((((Number) value).floatValue() + "").getBytes());
213+
} else if (value instanceof Integer) {
214+
os.write((((Number) value).intValue() + "").getBytes());
215+
} else {
216+
os.write(escape(value.toString()));
217+
}
218+
}
219+
} finally {
220+
// Separate each K:V with a comma, except the last one.
221+
if (elapsedField != null || keysProcessed < keysCount) {
222+
os.write(',');
223+
}
189224
}
225+
}
190226

191-
// End the file's object and prepare for next one.
192-
os.write('}');
193-
} else if (value instanceof JsonValueInterface) {
194-
os.write(((JsonValueInterface) value).getEscapedJsonValue());
195-
} else if (value instanceof org.json.JSONObject) {
196-
os.write(((org.json.JSONObject) value).toString().getBytes());
197-
} else if (value instanceof org.json.JSONArray) {
198-
os.write(((org.json.JSONArray) value).toString().getBytes());
199-
} else if (value instanceof Boolean) {
200-
os.write((Boolean) value ? JSON_TRUE : JSON_FALSE);
201-
} else if (value instanceof Long) {
202-
os.write((((Number) value).longValue() + "").getBytes());
203-
} else if (value instanceof Double) {
204-
os.write((((Number) value).doubleValue() + "").getBytes());
205-
} else if (value instanceof Float) {
206-
os.write((((Number) value).floatValue() + "").getBytes());
207-
} else if (value instanceof Integer) {
208-
os.write((((Number) value).intValue() + "").getBytes());
209-
} else {
210-
os.write(escape(value.toString()));
227+
// Calculate how many milliseconds it took to upload the contents.
228+
long elapsedTime = System.currentTimeMillis() - now;
229+
230+
// Include the elapsed time taken to upload everything.
231+
// This might be useful for somebody, but it serves us well since
232+
// there will almost always be a ',' as the last sent character.
233+
if (elapsedField != null) {
234+
os.write(elapsedField);
235+
os.write(':');
236+
os.write((elapsedTime + "").getBytes());
211237
}
212238

213-
os.write(',');
239+
Log.i(LOG_TAG, "Uploaded JSON in " + Math.floor(elapsedTime / 1000) + " seconds");
214240
}
215241

216-
// Include the elapsed time taken to upload everything.
217-
// This might be useful for somebody, but it serves us well since
218-
// there will almost always be a ',' as the last sent character.
219-
os.write(STREAM_ELAPSED);
220-
os.write(':');
221-
long elapsedTime = System.currentTimeMillis() - now;
222-
os.write((elapsedTime + "}").getBytes());
223-
224-
Log.i(LOG_TAG, "Uploaded JSON in " + Math.floor(elapsedTime / 1000) + " seconds");
242+
// Close the JSON object.
243+
os.write('}');
225244

226245
// Flush the contents up the stream.
227246
os.flush();
@@ -321,60 +340,57 @@ static byte[] escape(String string) {
321340
return JSON_NULL;
322341
}
323342

343+
// Create a string builder to generate the escaped string.
344+
StringBuilder sb = new StringBuilder(128);
345+
324346
// Surround with quotations.
325-
BUILDER.append('"');
347+
sb.append('"');
326348

327349
int length = string.length(), pos = -1;
328350
while (++pos < length) {
329351
char ch = string.charAt(pos);
330352
switch (ch) {
331353
case '"':
332-
BUILDER.append("\\\"");
354+
sb.append("\\\"");
333355
break;
334356
case '\\':
335-
BUILDER.append("\\\\");
357+
sb.append("\\\\");
336358
break;
337359
case '\b':
338-
BUILDER.append("\\b");
360+
sb.append("\\b");
339361
break;
340362
case '\f':
341-
BUILDER.append("\\f");
363+
sb.append("\\f");
342364
break;
343365
case '\n':
344-
BUILDER.append("\\n");
366+
sb.append("\\n");
345367
break;
346368
case '\r':
347-
BUILDER.append("\\r");
369+
sb.append("\\r");
348370
break;
349371
case '\t':
350-
BUILDER.append("\\t");
372+
sb.append("\\t");
351373
break;
352374
default:
353375
// Reference: http://www.unicode.org/versions/Unicode5.1.0/
354376
if ((ch >= '\u0000' && ch <= '\u001F') || (ch >= '\u007F' && ch <= '\u009F') || (ch >= '\u2000' && ch <= '\u20FF')) {
355377
String intString = Integer.toHexString(ch);
356-
BUILDER.append("\\u");
378+
sb.append("\\u");
357379
int intLength = 4 - intString.length();
358380
for (int zero = 0; zero < intLength; zero++) {
359-
BUILDER.append('0');
381+
sb.append('0');
360382
}
361-
BUILDER.append(intString.toUpperCase(Locale.US));
383+
sb.append(intString.toUpperCase(Locale.US));
362384
} else {
363-
BUILDER.append(ch);
385+
sb.append(ch);
364386
}
365387
break;
366388
}
367389
}
368390

369391
// Surround with quotations.
370-
BUILDER.append('"');
371-
372-
try {
373-
return BUILDER.toString().getBytes();
374-
} finally {
375-
// Empty the String buffer.
376-
// This is 20-30% faster than instantiating a new object.
377-
BUILDER.setLength(0);
378-
}
392+
sb.append('"');
393+
394+
return sb.toString().getBytes();
379395
}
380396
}

library/src/main/java/com/loopj/android/http/RequestParams.java

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ public class RequestParams implements Serializable {
9999
protected final static String LOG_TAG = "RequestParams";
100100
protected boolean isRepeatable;
101101
protected boolean useJsonStreamer;
102+
protected String elapsedFieldInJsonStreamer = "_elapsed";
102103
protected boolean autoCloseInputStreams;
103104
protected final ConcurrentHashMap<String, String> urlParams = new ConcurrentHashMap<String, String>();
104105
protected final ConcurrentHashMap<String, StreamWrapper> streamParams = new ConcurrentHashMap<String, StreamWrapper>();
@@ -412,12 +413,25 @@ public String toString() {
412413
return result.toString();
413414
}
414415

415-
public void setHttpEntityIsRepeatable(boolean isRepeatable) {
416-
this.isRepeatable = isRepeatable;
416+
public void setHttpEntityIsRepeatable(boolean flag) {
417+
this.isRepeatable = flag;
417418
}
418419

419-
public void setUseJsonStreamer(boolean useJsonStreamer) {
420-
this.useJsonStreamer = useJsonStreamer;
420+
public void setUseJsonStreamer(boolean flag) {
421+
this.useJsonStreamer = flag;
422+
}
423+
424+
/**
425+
* Sets an additional field when upload a JSON object through the streamer
426+
* to hold the time, in milliseconds, it took to upload the payload. By
427+
* default, this field is set to "_elapsed".
428+
*
429+
* To disable this feature, call this method with null as the field value.
430+
*
431+
* @param value field name to add elapsed time, or null to disable
432+
*/
433+
public void setElapsedFieldInJsonStreamer(String value) {
434+
this.elapsedFieldInJsonStreamer = value;
421435
}
422436

423437
/**
@@ -449,8 +463,10 @@ public HttpEntity getEntity(ResponseHandlerInterface progressHandler) throws IOE
449463
}
450464

451465
private HttpEntity createJsonStreamerEntity(ResponseHandlerInterface progressHandler) throws IOException {
452-
JsonStreamerEntity entity = new JsonStreamerEntity(progressHandler,
453-
!fileParams.isEmpty() || !streamParams.isEmpty());
466+
JsonStreamerEntity entity = new JsonStreamerEntity(
467+
progressHandler,
468+
!fileParams.isEmpty() || !streamParams.isEmpty(),
469+
elapsedFieldInJsonStreamer);
454470

455471
// Add string params
456472
for (ConcurrentHashMap.Entry<String, String> entry : urlParams.entrySet()) {

0 commit comments

Comments
 (0)