Home | History | Annotate | Download | only in stream
      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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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. */
    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. */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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 
    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 
    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      */
    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      */
    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