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