1 /*
2  * Copyright (C) 2010 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.json.stream;
18 
19 import java.io.Closeable;
20 import java.io.IOException;
21 import java.io.Writer;
22 import java.util.ArrayList;
23 import java.util.List;
24 
25 /**
26  * Writes a JSON (<a href="http://www.ietf.org/rfc/rfc4627.txt">RFC 4627</a>) encoded value to a
27  * stream, one token at a time. The stream includes both literal values (strings, numbers, booleans
28  * and nulls) as well as the begin and end delimiters of objects and arrays.
29  *
30  * <h3>Encoding JSON</h3>
31  *
32  * To encode your data as JSON, create a new {@code JsonWriter}. Each JSON document must contain one
33  * top-level array or object. Call methods on the writer as you walk the structure's contents,
34  * nesting arrays and objects as necessary:
35  *
36  * <ul>
37  *   <li>To write <strong>arrays</strong>, first call {@link #beginArray()}. Write each of the
38  *       array's elements with the appropriate {@link #value} methods or by nesting other arrays and
39  *       objects. Finally close the array using {@link #endArray()}.
40  *   <li>To write <strong>objects</strong>, first call {@link #beginObject()}. Write each of the
41  *       object's properties by alternating calls to {@link #name} with the property's value. Write
42  *       property values with the appropriate {@link #value} method or by nesting other objects or
43  *       arrays. Finally close the object using {@link #endObject()}.
44  * </ul>
45  *
46  * <h3>Example</h3>
47  *
48  * Suppose we'd like to encode a stream of messages such as the following:
49  *
50  * <pre>{@code
51  * [
52  *   {
53  *     "id": 912345678901,
54  *     "text": "How do I write JSON on Android?",
55  *     "geo": null,
56  *     "user": {
57  *       "name": "android_newb",
58  *       "followers_count": 41
59  *      }
60  *   },
61  *   {
62  *     "id": 912345678902,
63  *     "text": "@android_newb just use android.util.JsonWriter!",
64  *     "geo": [50.454722, -104.606667],
65  *     "user": {
66  *       "name": "jesse",
67  *       "followers_count": 2
68  *     }
69  *   }
70  * ]
71  * }</pre>
72  *
73  * This code encodes the above structure:
74  *
75  * <pre>{@code
76  * public void writeJsonStream(OutputStream out, List<Message> messages) throws IOException {
77  *   JsonWriter writer = new JsonWriter(new OutputStreamWriter(out, "UTF-8"));
78  *   writer.setIndent("  ");
79  *   writeMessagesArray(writer, messages);
80  *   writer.close();
81  * }
82  *
83  * public void writeMessagesArray(JsonWriter writer, List<Message> messages) throws IOException {
84  *   writer.beginArray();
85  *   for (Message message : messages) {
86  *     writeMessage(writer, message);
87  *   }
88  *   writer.endArray();
89  * }
90  *
91  * public void writeMessage(JsonWriter writer, Message message) throws IOException {
92  *   writer.beginObject();
93  *   writer.name("id").value(message.getId());
94  *   writer.name("text").value(message.getText());
95  *   if (message.getGeo() != null) {
96  *     writer.name("geo");
97  *     writeDoublesArray(writer, message.getGeo());
98  *   } else {
99  *     writer.name("geo").nullValue();
100  *   }
101  *   writer.name("user");
102  *   writeUser(writer, message.getUser());
103  *   writer.endObject();
104  * }
105  *
106  * public void writeUser(JsonWriter writer, User user) throws IOException {
107  *   writer.beginObject();
108  *   writer.name("name").value(user.getName());
109  *   writer.name("followers_count").value(user.getFollowersCount());
110  *   writer.endObject();
111  * }
112  *
113  * public void writeDoublesArray(JsonWriter writer, List<Double> doubles) throws IOException {
114  *   writer.beginArray();
115  *   for (Double value : doubles) {
116  *     writer.value(value);
117  *   }
118  *   writer.endArray();
119  * }
120  * }</pre>
121  *
122  * <p>Each {@code JsonWriter} may be used to write a single JSON stream. Instances of this class are
123  * not thread safe. Calls that would result in a malformed JSON string will fail with an {@link
124  * IllegalStateException}.
125  */
126 public class JsonWriter implements Closeable {
127 
128     /** The output data, containing at most one top-level array or object. */
129     protected final Writer mOut;
130 
131     protected final List<JsonScope> mStack = new ArrayList<JsonScope>();
132 
133     {
134         mStack.add(JsonScope.EMPTY_DOCUMENT);
135     }
136 
137     /**
138      * A string containing a full set of spaces for a single level of indentation, or null for no
139      * pretty printing.
140      */
141     private String mIndent;
142 
143     /** The name/value separator; either ":" or ": ". */
144     protected String mSeparator = ":";
145 
146     /**
147      * Creates a new instance that writes a JSON-encoded stream to {@code out}.
148      * For best performance, ensure {@link Writer} is buffered; wrapping in
149      * {@link java.io.BufferedWriter BufferedWriter} if necessary.
150      */
JsonWriter(Writer out)151     public JsonWriter(Writer out) {
152         if (out == null) {
153             throw new NullPointerException("out == null");
154         }
155         this.mOut = out;
156     }
157 
158     /**
159      * Sets the indentation string to be repeated for each level of indentation
160      * in the encoded document. If {@code indent.isEmpty()} the encoded document
161      * will be compact. Otherwise the encoded document will be more
162      * human-readable.
163      *
164      * @param indent a string containing only whitespace.
165      */
setIndent(String indent)166     public void setIndent(String indent) {
167         if (indent.isEmpty()) {
168             this.mIndent = null;
169             this.mSeparator = ":";
170         } else {
171             this.mIndent = indent;
172             this.mSeparator = ": ";
173         }
174     }
175 
176     /**
177      * Begins encoding a new array. Each call to this method must be paired with
178      * a call to {@link #endArray}.
179      *
180      * @return this writer.
181      */
beginArray()182     public JsonWriter beginArray() throws IOException {
183         return open(JsonScope.EMPTY_ARRAY, "[");
184     }
185 
186     /**
187      * Ends encoding the current array.
188      *
189      * @return this writer.
190      */
endArray()191     public JsonWriter endArray() throws IOException {
192         return close(JsonScope.EMPTY_ARRAY, JsonScope.NONEMPTY_ARRAY, "]");
193     }
194 
195     /**
196      * Begins encoding a new object. Each call to this method must be paired
197      * with a call to {@link #endObject}.
198      *
199      * @return this writer.
200      */
beginObject()201     public JsonWriter beginObject() throws IOException {
202         return open(JsonScope.EMPTY_OBJECT, "{");
203     }
204 
205     /**
206      * Ends encoding the current object.
207      *
208      * @return this writer.
209      */
endObject()210     public JsonWriter endObject() throws IOException {
211         return close(JsonScope.EMPTY_OBJECT, JsonScope.NONEMPTY_OBJECT, "}");
212     }
213 
214     /**
215      * Enters a new scope by appending any necessary whitespace and the given
216      * bracket.
217      */
open(JsonScope empty, String openBracket)218     private JsonWriter open(JsonScope empty, String openBracket) throws IOException {
219         beforeValue(true);
220         mStack.add(empty);
221         mOut.write(openBracket);
222         return this;
223     }
224 
225     /**
226      * Closes the current scope by appending any necessary whitespace and the
227      * given bracket.
228      */
close(JsonScope empty, JsonScope nonempty, String closeBracket)229     private JsonWriter close(JsonScope empty, JsonScope nonempty, String closeBracket)
230             throws IOException {
231         JsonScope context = peek();
232         if (context != nonempty && context != empty) {
233             throw new IllegalStateException("Nesting problem: " + mStack);
234         }
235 
236         mStack.remove(mStack.size() - 1);
237         if (context == nonempty) {
238             newline();
239         }
240         mOut.write(closeBracket);
241         return this;
242     }
243 
244     /** Returns the value on the top of the stack. */
peek()245     protected JsonScope peek() {
246         return mStack.get(mStack.size() - 1);
247     }
248 
249     /** Replace the value on the top of the stack with the given value. */
replaceTop(JsonScope topOfStack)250     protected void replaceTop(JsonScope topOfStack) {
251         mStack.set(mStack.size() - 1, topOfStack);
252     }
253 
254     /**
255      * Encodes the property name.
256      *
257      * @param name the name of the forthcoming value. May not be null.
258      * @return this writer.
259      */
name(String name)260     public JsonWriter name(String name) throws IOException {
261         if (name == null) {
262             throw new NullPointerException("name == null");
263         }
264         beforeName();
265         string(name);
266         return this;
267     }
268 
269     /**
270      * Encodes {@code value}.
271      *
272      * @param value the literal string value, or null to encode a null literal.
273      * @return this writer.
274      */
value(String value)275     public JsonWriter value(String value) throws IOException {
276         if (value == null) {
277             return nullValue();
278         }
279         beforeValue(false);
280         string(value);
281         return this;
282     }
283 
284     /**
285      * Encodes {@code null}.
286      *
287      * @return this writer.
288      */
nullValue()289     public JsonWriter nullValue() throws IOException {
290         beforeValue(false);
291         mOut.write("null");
292         return this;
293     }
294 
295     /**
296      * Encodes {@code value}.
297      *
298      * @return this writer.
299      */
value(boolean value)300     public JsonWriter value(boolean value) throws IOException {
301         beforeValue(false);
302         mOut.write(value ? "true" : "false");
303         return this;
304     }
305 
306     /**
307      * Encodes {@code value}.
308      *
309      * @param value a finite value. May not be {@link Double#isNaN() NaNs} or
310      *     {@link Double#isInfinite() infinities}.
311      * @return this writer.
312      */
value(double value)313     public JsonWriter value(double value) throws IOException {
314         if (Double.isNaN(value) || Double.isInfinite(value)) {
315             throw new IllegalArgumentException("Numeric values must be finite, but was " + value);
316         }
317         beforeValue(false);
318         mOut.append(Double.toString(value));
319         return this;
320     }
321 
322     /**
323      * Encodes {@code value}.
324      *
325      * @return this writer.
326      */
value(long value)327     public JsonWriter value(long value) throws IOException {
328         beforeValue(false);
329         mOut.write(Long.toString(value));
330         return this;
331     }
332 
333     /**
334      * Ensures all buffered data is written to the underlying {@link Writer}
335      * and flushes that writer.
336      */
flush()337     public void flush() throws IOException {
338         mOut.flush();
339     }
340 
341     /**
342      * Flushes and closes this writer and the underlying {@link Writer}.
343      *
344      * @throws IOException if the JSON document is incomplete.
345      */
close()346     public void close() throws IOException {
347         mOut.close();
348 
349         if (peek() != JsonScope.NONEMPTY_DOCUMENT) {
350             throw new IOException("Incomplete document");
351         }
352     }
353 
string(String value)354     private void string(String value) throws IOException {
355         mOut.write("\"");
356         for (int i = 0, length = value.length(); i < length; i++) {
357             char c = value.charAt(i);
358 
359             /*
360              * From RFC 4627, "All Unicode characters may be placed within the
361              * quotation marks except for the characters that must be escaped:
362              * quotation mark, reverse solidus, and the control characters
363              * (U+0000 through U+001F)."
364              */
365             switch (c) {
366                 case '"':
367                 case '\\':
368                 case '/':
369                     mOut.write('\\');
370                     mOut.write(c);
371                     break;
372 
373                 case '\t':
374                     mOut.write("\\t");
375                     break;
376 
377                 case '\b':
378                     mOut.write("\\b");
379                     break;
380 
381                 case '\n':
382                     mOut.write("\\n");
383                     break;
384 
385                 case '\r':
386                     mOut.write("\\r");
387                     break;
388 
389                 case '\f':
390                     mOut.write("\\f");
391                     break;
392 
393                 default:
394                     if (c <= 0x1F) {
395                         mOut.write(String.format("\\u%04x", (int) c));
396                     } else {
397                         mOut.write(c);
398                     }
399                     break;
400             }
401 
402         }
403         mOut.write("\"");
404     }
405 
newline()406     protected void newline() throws IOException {
407         if (mIndent == null) {
408             return;
409         }
410 
411         mOut.write("\n");
412         for (int i = 1; i < mStack.size(); i++) {
413             mOut.write(mIndent);
414         }
415     }
416 
417     /**
418      * Inserts any necessary separators and whitespace before a name. Also adjusts the stack to
419      * expect the name's value.
420      */
beforeName()421     protected void beforeName() throws IOException {
422         JsonScope context = peek();
423         if (context == JsonScope.NONEMPTY_OBJECT) { // first in object
424             mOut.write(',');
425         } else if (context != JsonScope.EMPTY_OBJECT) { // not in an object!
426             throw new IllegalStateException("Nesting problem: " + mStack);
427         }
428         newline();
429         replaceTop(JsonScope.DANGLING_NAME);
430     }
431 
432     /**
433      * Inserts any necessary separators and whitespace before a literal value, inline array, or
434      * inline object. Also adjusts the stack to expect either a closing bracket or another element.
435      *
436      * @param root true if the value is a new array or object, the two values permitted as top-level
437      *     elements.
438      */
beforeValue(boolean root)439     protected void beforeValue(boolean root) throws IOException {
440         switch (peek()) {
441             case EMPTY_DOCUMENT: // first in document
442                 if (!root) {
443                     throw new IllegalStateException(
444                             "JSON must start with an array or an object.");
445                 }
446                 replaceTop(JsonScope.NONEMPTY_DOCUMENT);
447                 break;
448 
449             case EMPTY_ARRAY: // first in array
450                 replaceTop(JsonScope.NONEMPTY_ARRAY);
451                 newline();
452                 break;
453 
454             case NONEMPTY_ARRAY: // another in array
455                 mOut.append(',');
456                 newline();
457                 break;
458 
459             case DANGLING_NAME: // value for name
460                 mOut.append(mSeparator);
461                 replaceTop(JsonScope.NONEMPTY_OBJECT);
462                 break;
463 
464             case NONEMPTY_DOCUMENT:
465                 throw new IllegalStateException(
466                         "JSON must have only one top-level value.");
467 
468             default:
469                 throw new IllegalStateException("Nesting problem: " + mStack);
470         }
471     }
472 }
473