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