Skip to content

Commit f620e90

Browse files
committed
Merge pull request android-async-http#386 from orientalsensation/JsonStreamerEntity
Added ability to upload data as JSON object using streams.
2 parents 1bbb5ff + 8393ea7 commit f620e90

File tree

2 files changed

+348
-2
lines changed

2 files changed

+348
-2
lines changed
Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
/*
2+
Android Asynchronous Http Client
3+
Copyright (c) 2011 James Smith <[email protected]>
4+
http://loopj.com
5+
6+
Licensed under the Apache License, Version 2.0 (the "License");
7+
you may not use this file except in compliance with the License.
8+
You may obtain a copy of the License at
9+
10+
http://www.apache.org/licenses/LICENSE-2.0
11+
12+
Unless required by applicable law or agreed to in writing, software
13+
distributed under the License is distributed on an "AS IS" BASIS,
14+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
See the License for the specific language governing permissions and
16+
limitations under the License.
17+
*/
18+
19+
package com.loopj.android.http;
20+
21+
import org.apache.http.Header;
22+
import org.apache.http.HttpEntity;
23+
import org.apache.http.message.BasicHeader;
24+
25+
import android.util.Log;
26+
import android.util.Base64;
27+
import android.util.Base64OutputStream;
28+
29+
import java.io.File;
30+
import java.io.FileInputStream;
31+
import java.io.InputStream;
32+
import java.io.OutputStream;
33+
import java.io.BufferedOutputStream;
34+
import java.io.IOException;
35+
36+
import java.util.Set;
37+
import java.util.Map;
38+
import java.util.HashMap;
39+
import java.util.zip.GZIPOutputStream;
40+
41+
/**
42+
* HTTP entity to upload JSON data using streams.
43+
* This has very low memory footprint; suitable for uploading large
44+
* files using base64 encoding.
45+
*/
46+
class JsonStreamerEntity implements HttpEntity {
47+
48+
private static final String LOG_TAG = "JsonStreamerEntity";
49+
50+
private static final UnsupportedOperationException ERR_UNSUPPORTED =
51+
new UnsupportedOperationException("Unsupported operation in this implementation.");
52+
53+
private static final byte[] JSON_TRUE = "true".getBytes();
54+
private static final byte[] JSON_FALSE = "false".getBytes();
55+
private static final byte[] STREAM_NAME = escape("name", true).getBytes();
56+
private static final byte[] STREAM_TYPE = escape("type", true).getBytes();
57+
private static final byte[] STREAM_CONTENTS = escape("contents", true).getBytes();
58+
private static final byte[] STREAM_ELAPSED = escape("_elapsed", true).getBytes();
59+
60+
private static final Header HEADER_JSON =
61+
new BasicHeader("Content-Type", "application/json");
62+
private static final String APPLICATION_OCTET_STREAM =
63+
"application/octet-stream";
64+
65+
// Size of the byte-array buffer used to read from files.
66+
private static int BUFFER_SIZE = 2048;
67+
68+
// K/V objects to be uploaded.
69+
private final Map<String, Object> kvParams = new HashMap();
70+
71+
// Streams and their associated meta-data to be uploaded.
72+
private final Map<String, RequestParams.StreamWrapper> streamParams = new HashMap();
73+
74+
// Whether to use gzip compression while uploading
75+
private final Header contentEncoding;
76+
77+
public JsonStreamerEntity(boolean contentEncoding) {
78+
this.contentEncoding = contentEncoding
79+
? new BasicHeader("Content-Encoding", "gzip")
80+
: null;
81+
}
82+
83+
public void addPart(String key, Object value) {
84+
kvParams.put(key, value);
85+
}
86+
87+
public void addPart(String key, File file, String type) throws IOException {
88+
addPart(key, new FileInputStream(file), file.getName(), type);
89+
}
90+
91+
public void addPart(String key, InputStream inputStream, String name, String type) {
92+
if (type == null) {
93+
type = APPLICATION_OCTET_STREAM;
94+
}
95+
streamParams.put(key, new RequestParams.StreamWrapper(inputStream, name, type));
96+
}
97+
98+
@Override
99+
public boolean isRepeatable() {
100+
return false;
101+
}
102+
103+
@Override
104+
public boolean isChunked() {
105+
return false;
106+
}
107+
108+
@Override
109+
public boolean isStreaming() {
110+
return false;
111+
}
112+
113+
@Override
114+
public long getContentLength() {
115+
return -1;
116+
}
117+
118+
@Override
119+
public Header getContentEncoding() {
120+
return contentEncoding;
121+
}
122+
123+
@Override
124+
public Header getContentType() {
125+
return HEADER_JSON;
126+
}
127+
128+
@Override
129+
public void consumeContent() throws IOException, UnsupportedOperationException {
130+
}
131+
132+
@Override
133+
public InputStream getContent() throws IOException, UnsupportedOperationException {
134+
throw ERR_UNSUPPORTED;
135+
}
136+
137+
@Override
138+
public void writeTo(final OutputStream outstream) throws IOException {
139+
if (outstream == null) {
140+
throw new IllegalStateException("Output stream cannot be null.");
141+
}
142+
143+
long now = System.currentTimeMillis();
144+
Log.i(LOG_TAG, "Started dumping at: " + now);
145+
146+
OutputStream upload;
147+
148+
// GZIPOutputStream is available only from API level 8 and onward.
149+
if(null != contentEncoding) {
150+
upload = new GZIPOutputStream(new BufferedOutputStream(outstream), BUFFER_SIZE);
151+
} else {
152+
upload = new BufferedOutputStream(outstream);
153+
}
154+
155+
// Always send a JSON object.
156+
upload.write('{');
157+
158+
// Keys used by the HashMaps.
159+
Set<String> keys;
160+
161+
// Send the K/V values.
162+
keys = kvParams.keySet();
163+
for (String key : keys) {
164+
// Write the JSON object's key.
165+
upload.write(escape(key, true).getBytes());
166+
upload.write(':');
167+
168+
// Evaluate the value (which cannot be null).
169+
Object value = kvParams.get(key);
170+
171+
if (value instanceof Boolean) {
172+
upload.write(((Boolean)value).booleanValue() ? JSON_TRUE : JSON_FALSE);
173+
} else if (value instanceof Long) {
174+
upload.write((((Number)value).longValue() + "").getBytes());
175+
} else if (value instanceof Double) {
176+
upload.write((((Number)value).doubleValue() + "").getBytes());
177+
} else if (value instanceof Float) {
178+
upload.write((((Number)value).floatValue() + "").getBytes());
179+
} else if (value instanceof Integer) {
180+
upload.write((((Number)value).intValue() + "").getBytes());
181+
} else {
182+
upload.write(value.toString().getBytes());
183+
}
184+
185+
upload.write(',');
186+
}
187+
188+
// Buffer used for reading from input streams.
189+
byte[] buffer = new byte[BUFFER_SIZE];
190+
191+
// Send the stream params.
192+
keys = streamParams.keySet();
193+
for(String key : keys) {
194+
RequestParams.StreamWrapper entry = streamParams.get(key);
195+
196+
// Write the JSON object's key.
197+
upload.write(escape(key, true).getBytes());
198+
199+
// All uploads are sent as an object containing the file's details.
200+
upload.write(":{".getBytes());
201+
202+
// Send the streams's name.
203+
upload.write(STREAM_NAME);
204+
upload.write(':');
205+
upload.write(escape(entry.name, true).getBytes());
206+
207+
// Send the streams's content type.
208+
upload.write(STREAM_TYPE);
209+
upload.write(':');
210+
upload.write(escape(entry.contentType, true).getBytes());
211+
212+
// Prepare the file content's key.
213+
upload.write(STREAM_CONTENTS);
214+
upload.write(':');
215+
upload.write('"');
216+
217+
// Write the file's contents in Base64.
218+
Base64OutputStream outputStream = new Base64OutputStream(upload, Base64.NO_CLOSE | Base64.NO_WRAP);
219+
int bytesRead;
220+
while(-1 != (bytesRead = entry.inputStream.read(buffer))) {
221+
outputStream.write(buffer, 0, bytesRead);
222+
}
223+
224+
// Close the output stream.
225+
outputStream.close();
226+
227+
// Close the file's object.
228+
upload.write('"');
229+
upload.write('}');
230+
upload.write(',');
231+
}
232+
233+
// GC.
234+
keys = null;
235+
buffer = null;
236+
237+
// Include the elapsed time taken to upload everything.
238+
upload.write(STREAM_ELAPSED);
239+
upload.write(':');
240+
long elapsedTime = System.currentTimeMillis() - now;
241+
upload.write((elapsedTime + "}").getBytes());
242+
243+
Log.i(LOG_TAG, "JSON was uploaded in " + Math.floor(elapsedTime / 1000) + " seconds");
244+
245+
// Flush the contents up the stream.
246+
upload.flush();
247+
upload.close();
248+
}
249+
250+
// Curtosy of Simple-JSON:
251+
// http://goo.gl/XoW8RF
252+
private static String escape(String string, boolean quotes) {
253+
StringBuilder sb = new StringBuilder();
254+
int length = string.length(), pos = -1;
255+
if (quotes) {
256+
sb.append('"');
257+
}
258+
while (++pos < length) {
259+
char ch = string.charAt(pos);
260+
switch (ch) {
261+
case '"':
262+
sb.append("\\\"");
263+
break;
264+
case '\\':
265+
sb.append("\\\\");
266+
break;
267+
case '\b':
268+
sb.append("\\b");
269+
break;
270+
case '\f':
271+
sb.append("\\f");
272+
break;
273+
case '\n':
274+
sb.append("\\n");
275+
break;
276+
case '\r':
277+
sb.append("\\r");
278+
break;
279+
case '\t':
280+
sb.append("\\t");
281+
break;
282+
case '/':
283+
sb.append("\\/");
284+
break;
285+
default:
286+
// Reference: http://www.unicode.org/versions/Unicode5.1.0/
287+
if((ch >= '\u0000' && ch <= '\u001F') || (ch >= '\u007F' && ch <= '\u009F') || (ch >= '\u2000' && ch <= '\u20FF')) {
288+
String intString = Integer.toHexString(ch);
289+
sb.append("\\u");
290+
int intLength = 4 - intString.length();
291+
for (int zero = 0; zero < intLength; zero++) {
292+
sb.append('0');
293+
}
294+
sb.append(intString.toUpperCase());
295+
} else {
296+
sb.append(ch);
297+
}
298+
break;
299+
}
300+
}
301+
if (quotes) {
302+
sb.append('"');
303+
}
304+
return sb.toString();
305+
}
306+
}

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

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,8 @@
8686
*/
8787
public class RequestParams {
8888

89-
protected boolean isRepeatable = false;
89+
protected boolean isRepeatable;
90+
protected boolean useJsonStreamer;
9091
protected ConcurrentHashMap<String, String> urlParams;
9192
protected ConcurrentHashMap<String, StreamWrapper> streamParams;
9293
protected ConcurrentHashMap<String, FileWrapper> fileParams;
@@ -312,6 +313,13 @@ public void setHttpEntityIsRepeatable(boolean isRepeatable) {
312313
this.isRepeatable = isRepeatable;
313314
}
314315

316+
public void setUseJsonStreamer(boolean useJsonStreamer) {
317+
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.FROYO) {
318+
throw new IllegalStateException("Use of JSON streamer is available for API level 8 and later.");
319+
}
320+
this.useJsonStreamer = useJsonStreamer;
321+
}
322+
315323
/**
316324
* Returns an HttpEntity containing all request parameters
317325
*
@@ -321,13 +329,45 @@ public void setHttpEntityIsRepeatable(boolean isRepeatable) {
321329
* @throws IOException if one of the streams cannot be read
322330
*/
323331
public HttpEntity getEntity(ResponseHandlerInterface progressHandler) throws IOException {
324-
if (streamParams.isEmpty() && fileParams.isEmpty()) {
332+
if (useJsonStreamer) {
333+
return createJsonStreamerEntity();
334+
} else if (streamParams.isEmpty() && fileParams.isEmpty()) {
325335
return createFormEntity();
326336
} else {
327337
return createMultipartEntity(progressHandler);
328338
}
329339
}
330340

341+
private HttpEntity createJsonStreamerEntity() throws IOException {
342+
JsonStreamerEntity entity = new JsonStreamerEntity(!fileParams.isEmpty() || !streamParams.isEmpty());
343+
344+
// Add string params
345+
for (ConcurrentHashMap.Entry<String, String> entry : urlParams.entrySet()) {
346+
entity.addPart(entry.getKey(), entry.getValue());
347+
}
348+
349+
// Add non-string params
350+
for (ConcurrentHashMap.Entry<String, Object> entry : urlParamsWithObjects.entrySet()) {
351+
entity.addPart(entry.getKey(), entry.getValue());
352+
}
353+
354+
// Add file params
355+
for (ConcurrentHashMap.Entry<String, FileWrapper> entry : fileParams.entrySet()) {
356+
FileWrapper fileWrapper = entry.getValue();
357+
entity.addPart(entry.getKey(), fileWrapper.file, fileWrapper.contentType);
358+
}
359+
360+
// Add stream params
361+
for (ConcurrentHashMap.Entry<String, StreamWrapper> entry : streamParams.entrySet()) {
362+
StreamWrapper stream = entry.getValue();
363+
if (stream.inputStream != null) {
364+
entity.addPart(entry.getKey(), stream.inputStream, stream.name, stream.contentType);
365+
}
366+
}
367+
368+
return entity;
369+
}
370+
331371
private HttpEntity createFormEntity() {
332372
try {
333373
return new UrlEncodedFormEntity(getParamsList(), HTTP.UTF_8);

0 commit comments

Comments
 (0)