/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.util;

import libcore.internal.StringPool;

import java.io.Closeable;
import java.io.EOFException;
import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.List;


Reads a JSON (RFC 4627) encoded value as a stream of tokens. This stream includes both literal values (strings, numbers, booleans, and nulls) as well as the begin and end delimiters of objects and arrays. The tokens are traversed in depth-first order, the same order that they appear in the JSON document. Within JSON objects, name/value pairs are represented by a single token.

Parsing JSON

To create a recursive descent parser for your own JSON streams, first create an entry point method that creates a JsonReader.

Next, create handler methods for each structure in your JSON text. You'll need a method for each object type and for each array type.

  • Within array handling methods, first call beginArray to consume the array's opening bracket. Then create a while loop that accumulates values, terminating when hasNext is false. Finally, read the array's closing bracket by calling endArray.
  • Within object handling methods, first call beginObject to consume the object's opening brace. Then create a while loop that assigns values to local variables based on their name. This loop should terminate when hasNext is false. Finally, read the object's closing brace by calling endObject.

When a nested object or array is encountered, delegate to the corresponding handler method.

When an unknown name is encountered, strict parsers should fail with an exception. Lenient parsers should call skipValue() to recursively skip the value's nested tokens, which may otherwise conflict.

If a value may be null, you should first check using peek(). Null literals can be consumed using either nextNull() or skipValue().

Example

Suppose we'd like to parse a stream of messages such as the following:
 
[
  {
    "id": 912345678901,
    "text": "How do I read JSON on Android?",
    "geo": null,
    "user": {
      "name": "android_newb",
      "followers_count": 41
     }
  },
  {
    "id": 912345678902,
    "text": "@android_newb just use android.util.JsonReader!",
    "geo": [50.454722, -104.606667],
    "user": {
      "name": "jesse",
      "followers_count": 2
    }
  }
]
This code implements the parser for the above structure:
 
  public List<Message> readJsonStream(InputStream in) throws IOException {
    JsonReader reader = new JsonReader(new InputStreamReader(in, "UTF-8"));
    try {
      return readMessagesArray(reader);
    } finally {
      reader.close();
    }
  }
  public List<Message> readMessagesArray(JsonReader reader) throws IOException {
    List<Message> messages = new ArrayList<Message>();
    reader.beginArray();
    while (reader.hasNext()) {
      messages.add(readMessage(reader));
    }
    reader.endArray();
    return messages;
  }
  public Message readMessage(JsonReader reader) throws IOException {
    long id = -1;
    String text = null;
    User user = null;
    List<Double> geo = null;
    reader.beginObject();
    while (reader.hasNext()) {
      String name = reader.nextName();
      if (name.equals("id")) {
        id = reader.nextLong();
      } else if (name.equals("text")) {
        text = reader.nextString();
      } else if (name.equals("geo") && reader.peek() != JsonToken.NULL) {
        geo = readDoublesArray(reader);
      } else if (name.equals("user")) {
        user = readUser(reader);
      } else {
        reader.skipValue();
      }
    }
    reader.endObject();
    return new Message(id, text, user, geo);
  }
  public List<Double> readDoublesArray(JsonReader reader) throws IOException {
    List<Double> doubles = new ArrayList<Double>();
    reader.beginArray();
    while (reader.hasNext()) {
      doubles.add(reader.nextDouble());
    }
    reader.endArray();
    return doubles;
  }
  public User readUser(JsonReader reader) throws IOException {
    String username = null;
    int followersCount = -1;
    reader.beginObject();
    while (reader.hasNext()) {
      String name = reader.nextName();
      if (name.equals("name")) {
        username = reader.nextString();
      } else if (name.equals("followers_count")) {
        followersCount = reader.nextInt();
      } else {
        reader.skipValue();
      }
    }
    reader.endObject();
    return new User(username, followersCount);
  }

Number Handling

This reader permits numeric values to be read as strings and string values to be read as numbers. For example, both elements of the JSON array [1, "1"] may be read using either nextInt or nextString. This behavior is intended to prevent lossy numeric conversions: double is JavaScript's only numeric type and very large values like 9007199254740993 cannot be represented exactly on that platform. To minimize precision loss, extremely large values should be written and read as strings in JSON.

Each JsonReader may be used to read a single JSON stream. Instances of this class are not thread safe.

/** * Reads a JSON (<a href="http://www.ietf.org/rfc/rfc4627.txt">RFC 4627</a>) * encoded value as a stream of tokens. This stream includes both literal * values (strings, numbers, booleans, and nulls) as well as the begin and * end delimiters of objects and arrays. The tokens are traversed in * depth-first order, the same order that they appear in the JSON document. * Within JSON objects, name/value pairs are represented by a single token. * * <h3>Parsing JSON</h3> * To create a recursive descent parser for your own JSON streams, first create * an entry point method that creates a {@code JsonReader}. * * <p>Next, create handler methods for each structure in your JSON text. You'll * need a method for each object type and for each array type. * <ul> * <li>Within <strong>array handling</strong> methods, first call {@link * #beginArray} to consume the array's opening bracket. Then create a * while loop that accumulates values, terminating when {@link #hasNext} * is false. Finally, read the array's closing bracket by calling {@link * #endArray}. * <li>Within <strong>object handling</strong> methods, first call {@link * #beginObject} to consume the object's opening brace. Then create a * while loop that assigns values to local variables based on their name. * This loop should terminate when {@link #hasNext} is false. Finally, * read the object's closing brace by calling {@link #endObject}. * </ul> * <p>When a nested object or array is encountered, delegate to the * corresponding handler method. * * <p>When an unknown name is encountered, strict parsers should fail with an * exception. Lenient parsers should call {@link #skipValue()} to recursively * skip the value's nested tokens, which may otherwise conflict. * * <p>If a value may be null, you should first check using {@link #peek()}. * Null literals can be consumed using either {@link #nextNull()} or {@link * #skipValue()}. * * <h3>Example</h3> * Suppose we'd like to parse a stream of messages such as the following: <pre> {@code * [ * { * "id": 912345678901, * "text": "How do I read JSON on Android?", * "geo": null, * "user": { * "name": "android_newb", * "followers_count": 41 * } * }, * { * "id": 912345678902, * "text": "@android_newb just use android.util.JsonReader!", * "geo": [50.454722, -104.606667], * "user": { * "name": "jesse", * "followers_count": 2 * } * } * ]}</pre> * This code implements the parser for the above structure: <pre> {@code * * public List<Message> readJsonStream(InputStream in) throws IOException { * JsonReader reader = new JsonReader(new InputStreamReader(in, "UTF-8")); * try { * return readMessagesArray(reader); * } finally { * reader.close(); * } * } * * public List<Message> readMessagesArray(JsonReader reader) throws IOException { * List<Message> messages = new ArrayList<Message>(); * * reader.beginArray(); * while (reader.hasNext()) { * messages.add(readMessage(reader)); * } * reader.endArray(); * return messages; * } * * public Message readMessage(JsonReader reader) throws IOException { * long id = -1; * String text = null; * User user = null; * List<Double> geo = null; * * reader.beginObject(); * while (reader.hasNext()) { * String name = reader.nextName(); * if (name.equals("id")) { * id = reader.nextLong(); * } else if (name.equals("text")) { * text = reader.nextString(); * } else if (name.equals("geo") && reader.peek() != JsonToken.NULL) { * geo = readDoublesArray(reader); * } else if (name.equals("user")) { * user = readUser(reader); * } else { * reader.skipValue(); * } * } * reader.endObject(); * return new Message(id, text, user, geo); * } * * public List<Double> readDoublesArray(JsonReader reader) throws IOException { * List<Double> doubles = new ArrayList<Double>(); * * reader.beginArray(); * while (reader.hasNext()) { * doubles.add(reader.nextDouble()); * } * reader.endArray(); * return doubles; * } * * public User readUser(JsonReader reader) throws IOException { * String username = null; * int followersCount = -1; * * reader.beginObject(); * while (reader.hasNext()) { * String name = reader.nextName(); * if (name.equals("name")) { * username = reader.nextString(); * } else if (name.equals("followers_count")) { * followersCount = reader.nextInt(); * } else { * reader.skipValue(); * } * } * reader.endObject(); * return new User(username, followersCount); * }}</pre> * * <h3>Number Handling</h3> * This reader permits numeric values to be read as strings and string values to * be read as numbers. For example, both elements of the JSON array {@code * [1, "1"]} may be read using either {@link #nextInt} or {@link #nextString}. * This behavior is intended to prevent lossy numeric conversions: double is * JavaScript's only numeric type and very large values like {@code * 9007199254740993} cannot be represented exactly on that platform. To minimize * precision loss, extremely large values should be written and read as strings * in JSON. * * <p>Each {@code JsonReader} may be used to read a single JSON stream. Instances * of this class are not thread safe. */
public final class JsonReader implements Closeable { private static final String TRUE = "true"; private static final String FALSE = "false"; private final StringPool stringPool = new StringPool();
The input JSON.
/** The input JSON. */
private final Reader in;
True to accept non-spec compliant JSON
/** True to accept non-spec compliant JSON */
private boolean lenient = false;
Use a manual buffer to easily read and unread upcoming characters, and also so we can create strings without an intermediate StringBuilder. We decode literals directly out of this buffer, so it must be at least as long as the longest token that can be reported as a number.
/** * Use a manual buffer to easily read and unread upcoming characters, and * also so we can create strings without an intermediate StringBuilder. * We decode literals directly out of this buffer, so it must be at least as * long as the longest token that can be reported as a number. */
private final char[] buffer = new char[1024]; private int pos = 0; private int limit = 0; /* * The offset of the first character in the buffer. */ private int bufferStartLine = 1; private int bufferStartColumn = 1; private final List<JsonScope> stack = new ArrayList<JsonScope>(); { push(JsonScope.EMPTY_DOCUMENT); }
The type of the next token to be returned by peek and advance. If null, peek() will assign a value.
/** * The type of the next token to be returned by {@link #peek} and {@link * #advance}. If null, peek() will assign a value. */
private JsonToken token;
The text of the next name.
/** The text of the next name. */
private String name; /* * For the next literal value, we may have the text value, or the position * and length in the buffer. */ private String value; private int valuePos; private int valueLength;
True if we're currently handling a skipValue() call.
/** True if we're currently handling a skipValue() call. */
private boolean skipping = false;
Creates a new instance that reads a JSON-encoded stream from in.
/** * Creates a new instance that reads a JSON-encoded stream from {@code in}. */
public JsonReader(Reader in) { if (in == null) { throw new NullPointerException("in == null"); } this.in = in; }
Configure this parser to be be liberal in what it accepts. By default, this parser is strict and only accepts JSON as specified by RFC 4627. Setting the parser to lenient causes it to ignore the following syntax errors:
  • End of line comments starting with // or # and ending with a newline character.
  • C-style comments starting with /* and ending with */. Such comments may not be nested.
  • Names that are unquoted or 'single quoted'.
  • Strings that are unquoted or 'single quoted'.
  • Array elements separated by ; instead of ,.
  • Unnecessary array separators. These are interpreted as if null was the omitted value.
  • Names and values separated by = or => instead of :.
  • Name/value pairs separated by ; instead of ,.
/** * Configure this parser to be be liberal in what it accepts. By default, * this parser is strict and only accepts JSON as specified by <a * href="http://www.ietf.org/rfc/rfc4627.txt">RFC 4627</a>. Setting the * parser to lenient causes it to ignore the following syntax errors: * * <ul> * <li>End of line comments starting with {@code //} or {@code #} and * ending with a newline character. * <li>C-style comments starting with {@code /*} and ending with * {@code *}{@code /}. Such comments may not be nested. * <li>Names that are unquoted or {@code 'single quoted'}. * <li>Strings that are unquoted or {@code 'single quoted'}. * <li>Array elements separated by {@code ;} instead of {@code ,}. * <li>Unnecessary array separators. These are interpreted as if null * was the omitted value. * <li>Names and values separated by {@code =} or {@code =>} instead of * {@code :}. * <li>Name/value pairs separated by {@code ;} instead of {@code ,}. * </ul> */
public void setLenient(boolean lenient) { this.lenient = lenient; }
Returns true if this parser is liberal in what it accepts.
/** * Returns true if this parser is liberal in what it accepts. */
public boolean isLenient() { return lenient; }
Consumes the next token from the JSON stream and asserts that it is the beginning of a new array.
/** * Consumes the next token from the JSON stream and asserts that it is the * beginning of a new array. */
public void beginArray() throws IOException { expect(JsonToken.BEGIN_ARRAY); }
Consumes the next token from the JSON stream and asserts that it is the end of the current array.
/** * Consumes the next token from the JSON stream and asserts that it is the * end of the current array. */
public void endArray() throws IOException { expect(JsonToken.END_ARRAY); }
Consumes the next token from the JSON stream and asserts that it is the beginning of a new object.
/** * Consumes the next token from the JSON stream and asserts that it is the * beginning of a new object. */
public void beginObject() throws IOException { expect(JsonToken.BEGIN_OBJECT); }
Consumes the next token from the JSON stream and asserts that it is the end of the current object.
/** * Consumes the next token from the JSON stream and asserts that it is the * end of the current object. */
public void endObject() throws IOException { expect(JsonToken.END_OBJECT); }
Consumes expected.
/** * Consumes {@code expected}. */
private void expect(JsonToken expected) throws IOException { peek(); if (token != expected) { throw new IllegalStateException("Expected " + expected + " but was " + peek()); } advance(); }
Returns true if the current array or object has another element.
/** * Returns true if the current array or object has another element. */
public boolean hasNext() throws IOException { peek(); return token != JsonToken.END_OBJECT && token != JsonToken.END_ARRAY; }
Returns the type of the next token without consuming it.
/** * Returns the type of the next token without consuming it. */
public JsonToken peek() throws IOException { if (token != null) { return token; } switch (peekStack()) { case EMPTY_DOCUMENT: replaceTop(JsonScope.NONEMPTY_DOCUMENT); JsonToken firstToken = nextValue(); if (!lenient && token != JsonToken.BEGIN_ARRAY && token != JsonToken.BEGIN_OBJECT) { throw new IOException( "Expected JSON document to start with '[' or '{' but was " + token); } return firstToken; case EMPTY_ARRAY: return nextInArray(true); case NONEMPTY_ARRAY: return nextInArray(false); case EMPTY_OBJECT: return nextInObject(true); case DANGLING_NAME: return objectValue(); case NONEMPTY_OBJECT: return nextInObject(false); case NONEMPTY_DOCUMENT: try { JsonToken token = nextValue(); if (lenient) { return token; } throw syntaxError("Expected EOF"); } catch (EOFException e) { return token = JsonToken.END_DOCUMENT; // TODO: avoid throwing here? } case CLOSED: throw new IllegalStateException("JsonReader is closed"); default: throw new AssertionError(); } }
Advances the cursor in the JSON stream to the next token.
/** * Advances the cursor in the JSON stream to the next token. */
private JsonToken advance() throws IOException { peek(); JsonToken result = token; token = null; value = null; name = null; return result; }
Returns the next token, a property name, and consumes it.
Throws:
  • IOException – if the next token in the stream is not a property name.
/** * Returns the next token, a {@link JsonToken#NAME property name}, and * consumes it. * * @throws IOException if the next token in the stream is not a property * name. */
public String nextName() throws IOException { peek(); if (token != JsonToken.NAME) { throw new IllegalStateException("Expected a name but was " + peek()); } String result = name; advance(); return result; }
Returns the string value of the next token, consuming it. If the next token is a number, this method will return its string form.
Throws:
/** * Returns the {@link JsonToken#STRING string} value of the next token, * consuming it. If the next token is a number, this method will return its * string form. * * @throws IllegalStateException if the next token is not a string or if * this reader is closed. */
public String nextString() throws IOException { peek(); if (token != JsonToken.STRING && token != JsonToken.NUMBER) { throw new IllegalStateException("Expected a string but was " + peek()); } String result = value; advance(); return result; }
Returns the boolean value of the next token, consuming it.
Throws:
/** * Returns the {@link JsonToken#BOOLEAN boolean} value of the next token, * consuming it. * * @throws IllegalStateException if the next token is not a boolean or if * this reader is closed. */
public boolean nextBoolean() throws IOException { peek(); if (token != JsonToken.BOOLEAN) { throw new IllegalStateException("Expected a boolean but was " + token); } boolean result = (value == TRUE); advance(); return result; }
Consumes the next token from the JSON stream and asserts that it is a literal null.
Throws:
  • IllegalStateException – if the next token is not null or if this reader is closed.
/** * Consumes the next token from the JSON stream and asserts that it is a * literal null. * * @throws IllegalStateException if the next token is not null or if this * reader is closed. */
public void nextNull() throws IOException { peek(); if (token != JsonToken.NULL) { throw new IllegalStateException("Expected null but was " + token); } advance(); }
Returns the double value of the next token, consuming it. If the next token is a string, this method will attempt to parse it as a double using Double.parseDouble(String).
Throws:
/** * Returns the {@link JsonToken#NUMBER double} value of the next token, * consuming it. If the next token is a string, this method will attempt to * parse it as a double using {@link Double#parseDouble(String)}. * * @throws IllegalStateException if the next token is not a literal value. */
public double nextDouble() throws IOException { peek(); if (token != JsonToken.STRING && token != JsonToken.NUMBER) { throw new IllegalStateException("Expected a double but was " + token); } double result = Double.parseDouble(value); advance(); return result; }
Returns the long value of the next token, consuming it. If the next token is a string, this method will attempt to parse it as a long. If the next token's numeric value cannot be exactly represented by a Java long, this method throws.
Throws:
/** * Returns the {@link JsonToken#NUMBER long} value of the next token, * consuming it. If the next token is a string, this method will attempt to * parse it as a long. If the next token's numeric value cannot be exactly * represented by a Java {@code long}, this method throws. * * @throws IllegalStateException if the next token is not a literal value. * @throws NumberFormatException if the next literal value cannot be parsed * as a number, or exactly represented as a long. */
public long nextLong() throws IOException { peek(); if (token != JsonToken.STRING && token != JsonToken.NUMBER) { throw new IllegalStateException("Expected a long but was " + token); } long result; try { result = Long.parseLong(value); } catch (NumberFormatException ignored) { double asDouble = Double.parseDouble(value); // don't catch this NumberFormatException result = (long) asDouble; if ((double) result != asDouble) { throw new NumberFormatException(value); } } advance(); return result; }
Returns the int value of the next token, consuming it. If the next token is a string, this method will attempt to parse it as an int. If the next token's numeric value cannot be exactly represented by a Java int, this method throws.
Throws:
/** * Returns the {@link JsonToken#NUMBER int} value of the next token, * consuming it. If the next token is a string, this method will attempt to * parse it as an int. If the next token's numeric value cannot be exactly * represented by a Java {@code int}, this method throws. * * @throws IllegalStateException if the next token is not a literal value. * @throws NumberFormatException if the next literal value cannot be parsed * as a number, or exactly represented as an int. */
public int nextInt() throws IOException { peek(); if (token != JsonToken.STRING && token != JsonToken.NUMBER) { throw new IllegalStateException("Expected an int but was " + token); } int result; try { result = Integer.parseInt(value); } catch (NumberFormatException ignored) { double asDouble = Double.parseDouble(value); // don't catch this NumberFormatException result = (int) asDouble; if ((double) result != asDouble) { throw new NumberFormatException(value); } } advance(); return result; }
Closes this JSON reader and the underlying Reader.
/** * Closes this JSON reader and the underlying {@link Reader}. */
public void close() throws IOException { value = null; token = null; stack.clear(); stack.add(JsonScope.CLOSED); in.close(); }
Skips the next value recursively. If it is an object or array, all nested elements are skipped. This method is intended for use when the JSON token stream contains unrecognized or unhandled values.
/** * Skips the next value recursively. If it is an object or array, all nested * elements are skipped. This method is intended for use when the JSON token * stream contains unrecognized or unhandled values. */
public void skipValue() throws IOException { skipping = true; try { if (!hasNext() || peek() == JsonToken.END_DOCUMENT) { throw new IllegalStateException("No element left to skip"); } int count = 0; do { JsonToken token = advance(); if (token == JsonToken.BEGIN_ARRAY || token == JsonToken.BEGIN_OBJECT) { count++; } else if (token == JsonToken.END_ARRAY || token == JsonToken.END_OBJECT) { count--; } } while (count != 0); } finally { skipping = false; } } private JsonScope peekStack() { return stack.get(stack.size() - 1); } private JsonScope pop() { return stack.remove(stack.size() - 1); } private void push(JsonScope newTop) { stack.add(newTop); }
Replace the value on the top of the stack with the given value.
/** * Replace the value on the top of the stack with the given value. */
private void replaceTop(JsonScope newTop) { stack.set(stack.size() - 1, newTop); } private JsonToken nextInArray(boolean firstElement) throws IOException { if (firstElement) { replaceTop(JsonScope.NONEMPTY_ARRAY); } else { /* Look for a comma before each element after the first element. */ switch (nextNonWhitespace()) { case ']': pop(); return token = JsonToken.END_ARRAY; case ';': checkLenient(); // fall-through case ',': break; default: throw syntaxError("Unterminated array"); } } switch (nextNonWhitespace()) { case ']': if (firstElement) { pop(); return token = JsonToken.END_ARRAY; } // fall-through to handle ",]" case ';': case ',': /* In lenient mode, a 0-length literal means 'null' */ checkLenient(); pos--; value = "null"; return token = JsonToken.NULL; default: pos--; return nextValue(); } } private JsonToken nextInObject(boolean firstElement) throws IOException { /* * Read delimiters. Either a comma/semicolon separating this and the * previous name-value pair, or a close brace to denote the end of the * object. */ if (firstElement) { /* Peek to see if this is the empty object. */ switch (nextNonWhitespace()) { case '}': pop(); return token = JsonToken.END_OBJECT; default: pos--; } } else { switch (nextNonWhitespace()) { case '}': pop(); return token = JsonToken.END_OBJECT; case ';': case ',': break; default: throw syntaxError("Unterminated object"); } } /* Read the name. */ int quote = nextNonWhitespace(); switch (quote) { case '\'': checkLenient(); // fall-through case '"': name = nextString((char) quote); break; default: checkLenient(); pos--; name = nextLiteral(false); if (name.isEmpty()) { throw syntaxError("Expected name"); } } replaceTop(JsonScope.DANGLING_NAME); return token = JsonToken.NAME; } private JsonToken objectValue() throws IOException { /* * Read the name/value separator. Usually a colon ':'. In lenient mode * we also accept an equals sign '=', or an arrow "=>". */ switch (nextNonWhitespace()) { case ':': break; case '=': checkLenient(); if ((pos < limit || fillBuffer(1)) && buffer[pos] == '>') { pos++; } break; default: throw syntaxError("Expected ':'"); } replaceTop(JsonScope.NONEMPTY_OBJECT); return nextValue(); } private JsonToken nextValue() throws IOException { int c = nextNonWhitespace(); switch (c) { case '{': push(JsonScope.EMPTY_OBJECT); return token = JsonToken.BEGIN_OBJECT; case '[': push(JsonScope.EMPTY_ARRAY); return token = JsonToken.BEGIN_ARRAY; case '\'': checkLenient(); // fall-through case '"': value = nextString((char) c); return token = JsonToken.STRING; default: pos--; return readLiteral(); } }
Returns true once limit - pos >= minimum. If the data is exhausted before that many characters are available, this returns false.
/** * Returns true once {@code limit - pos >= minimum}. If the data is * exhausted before that many characters are available, this returns * false. */
private boolean fillBuffer(int minimum) throws IOException { // Before clobbering the old characters, update where buffer starts for (int i = 0; i < pos; i++) { if (buffer[i] == '\n') { bufferStartLine++; bufferStartColumn = 1; } else { bufferStartColumn++; } } if (limit != pos) { limit -= pos; System.arraycopy(buffer, pos, buffer, 0, limit); } else { limit = 0; } pos = 0; int total; while ((total = in.read(buffer, limit, buffer.length - limit)) != -1) { limit += total; // if this is the first read, consume an optional byte order mark (BOM) if it exists if (bufferStartLine == 1 && bufferStartColumn == 1 && limit > 0 && buffer[0] == '\ufeff') { pos++; bufferStartColumn--; } if (limit >= minimum) { return true; } } return false; } private int getLineNumber() { int result = bufferStartLine; for (int i = 0; i < pos; i++) { if (buffer[i] == '\n') { result++; } } return result; } private int getColumnNumber() { int result = bufferStartColumn; for (int i = 0; i < pos; i++) { if (buffer[i] == '\n') { result = 1; } else { result++; } } return result; } private int nextNonWhitespace() throws IOException { while (pos < limit || fillBuffer(1)) { int c = buffer[pos++]; switch (c) { case '\t': case ' ': case '\n': case '\r': continue; case '/': if (pos == limit && !fillBuffer(1)) { return c; } checkLenient(); char peek = buffer[pos]; switch (peek) { case '*': // skip a /* c-style comment */ pos++; if (!skipTo("*/")) { throw syntaxError("Unterminated comment"); } pos += 2; continue; case '/': // skip a // end-of-line comment pos++; skipToEndOfLine(); continue; default: return c; } case '#': /* * Skip a # hash end-of-line comment. The JSON RFC doesn't * specify this behaviour, but it's required to parse * existing documents. See http://b/2571423. */ checkLenient(); skipToEndOfLine(); continue; default: return c; } } throw new EOFException("End of input"); } private void checkLenient() throws IOException { if (!lenient) { throw syntaxError("Use JsonReader.setLenient(true) to accept malformed JSON"); } }
Advances the position until after the next newline character. If the line is terminated by "\r\n", the '\n' must be consumed as whitespace by the caller.
/** * Advances the position until after the next newline character. If the line * is terminated by "\r\n", the '\n' must be consumed as whitespace by the * caller. */
private void skipToEndOfLine() throws IOException { while (pos < limit || fillBuffer(1)) { char c = buffer[pos++]; if (c == '\r' || c == '\n') { break; } } } private boolean skipTo(String toFind) throws IOException { outer: for (; pos + toFind.length() <= limit || fillBuffer(toFind.length()); pos++) { for (int c = 0; c < toFind.length(); c++) { if (buffer[pos + c] != toFind.charAt(c)) { continue outer; } } return true; } return false; }
Returns the string up to but not including quote, unescaping any character escape sequences encountered along the way. The opening quote should have already been read. This consumes the closing quote, but does not include it in the returned string.
Params:
  • quote – either ' or ".
Throws:
/** * Returns the string up to but not including {@code quote}, unescaping any * character escape sequences encountered along the way. The opening quote * should have already been read. This consumes the closing quote, but does * not include it in the returned string. * * @param quote either ' or ". * @throws NumberFormatException if any unicode escape sequences are * malformed. */
private String nextString(char quote) throws IOException { StringBuilder builder = null; do { /* the index of the first character not yet appended to the builder. */ int start = pos; while (pos < limit) { int c = buffer[pos++]; if (c == quote) { if (skipping) { return "skipped!"; } else if (builder == null) { return stringPool.get(buffer, start, pos - start - 1); } else { builder.append(buffer, start, pos - start - 1); return builder.toString(); } } else if (c == '\\') { if (builder == null) { builder = new StringBuilder(); } builder.append(buffer, start, pos - start - 1); builder.append(readEscapeCharacter()); start = pos; } } if (builder == null) { builder = new StringBuilder(); } builder.append(buffer, start, pos - start); } while (fillBuffer(1)); throw syntaxError("Unterminated string"); }
Reads the value up to but not including any delimiter characters. This does not consume the delimiter character.
Params:
  • assignOffsetsOnly – true for this method to only set the valuePos and valueLength fields and return a null result. This only works if the literal is short; a string is returned otherwise.
/** * Reads the value up to but not including any delimiter characters. This * does not consume the delimiter character. * * @param assignOffsetsOnly true for this method to only set the valuePos * and valueLength fields and return a null result. This only works if * the literal is short; a string is returned otherwise. */
private String nextLiteral(boolean assignOffsetsOnly) throws IOException { StringBuilder builder = null; valuePos = -1; valueLength = 0; int i = 0; findNonLiteralCharacter: while (true) { for (; pos + i < limit; i++) { switch (buffer[pos + i]) { case '/': case '\\': case ';': case '#': case '=': checkLenient(); // fall-through case '{': case '}': case '[': case ']': case ':': case ',': case ' ': case '\t': case '\f': case '\r': case '\n': break findNonLiteralCharacter; } } /* * Attempt to load the entire literal into the buffer at once. If * we run out of input, add a non-literal character at the end so * that decoding doesn't need to do bounds checks. */ if (i < buffer.length) { if (fillBuffer(i + 1)) { continue; } else { buffer[limit] = '\0'; break; } } // use a StringBuilder when the value is too long. It must be an unquoted string. if (builder == null) { builder = new StringBuilder(); } builder.append(buffer, pos, i); valueLength += i; pos += i; i = 0; if (!fillBuffer(1)) { break; } } String result; if (assignOffsetsOnly && builder == null) { valuePos = pos; result = null; } else if (skipping) { result = "skipped!"; } else if (builder == null) { result = stringPool.get(buffer, pos, i); } else { builder.append(buffer, pos, i); result = builder.toString(); } valueLength += i; pos += i; return result; } @Override public String toString() { return getClass().getSimpleName() + " near " + getSnippet(); }
Unescapes the character identified by the character or characters that immediately follow a backslash. The backslash '\' should have already been read. This supports both unicode escapes "u000A" and two-character escapes "\n".
Throws:
  • NumberFormatException – if any unicode escape sequences are malformed.
/** * Unescapes the character identified by the character or characters that * immediately follow a backslash. The backslash '\' should have already * been read. This supports both unicode escapes "u000A" and two-character * escapes "\n". * * @throws NumberFormatException if any unicode escape sequences are * malformed. */
private char readEscapeCharacter() throws IOException { if (pos == limit && !fillBuffer(1)) { throw syntaxError("Unterminated escape sequence"); } char escaped = buffer[pos++]; switch (escaped) { case 'u': if (pos + 4 > limit && !fillBuffer(4)) { throw syntaxError("Unterminated escape sequence"); } String hex = stringPool.get(buffer, pos, 4); pos += 4; return (char) Integer.parseInt(hex, 16); case 't': return '\t'; case 'b': return '\b'; case 'n': return '\n'; case 'r': return '\r'; case 'f': return '\f'; case '\'': case '"': case '\\': default: return escaped; } }
Reads a null, boolean, numeric or unquoted string literal value.
/** * Reads a null, boolean, numeric or unquoted string literal value. */
private JsonToken readLiteral() throws IOException { value = nextLiteral(true); if (valueLength == 0) { throw syntaxError("Expected literal value"); } token = decodeLiteral(); if (token == JsonToken.STRING) { checkLenient(); } return token; }
Assigns nextToken based on the value of nextValue.
/** * Assigns {@code nextToken} based on the value of {@code nextValue}. */
private JsonToken decodeLiteral() throws IOException { if (valuePos == -1) { // it was too long to fit in the buffer so it can only be a string return JsonToken.STRING; } else if (valueLength == 4 && ('n' == buffer[valuePos ] || 'N' == buffer[valuePos ]) && ('u' == buffer[valuePos + 1] || 'U' == buffer[valuePos + 1]) && ('l' == buffer[valuePos + 2] || 'L' == buffer[valuePos + 2]) && ('l' == buffer[valuePos + 3] || 'L' == buffer[valuePos + 3])) { value = "null"; return JsonToken.NULL; } else if (valueLength == 4 && ('t' == buffer[valuePos ] || 'T' == buffer[valuePos ]) && ('r' == buffer[valuePos + 1] || 'R' == buffer[valuePos + 1]) && ('u' == buffer[valuePos + 2] || 'U' == buffer[valuePos + 2]) && ('e' == buffer[valuePos + 3] || 'E' == buffer[valuePos + 3])) { value = TRUE; return JsonToken.BOOLEAN; } else if (valueLength == 5 && ('f' == buffer[valuePos ] || 'F' == buffer[valuePos ]) && ('a' == buffer[valuePos + 1] || 'A' == buffer[valuePos + 1]) && ('l' == buffer[valuePos + 2] || 'L' == buffer[valuePos + 2]) && ('s' == buffer[valuePos + 3] || 'S' == buffer[valuePos + 3]) && ('e' == buffer[valuePos + 4] || 'E' == buffer[valuePos + 4])) { value = FALSE; return JsonToken.BOOLEAN; } else { value = stringPool.get(buffer, valuePos, valueLength); return decodeNumber(buffer, valuePos, valueLength); } }
Determine whether the characters is a JSON number. Numbers are of the form -12.34e+56. Fractional and exponential parts are optional. Leading zeroes are not allowed in the value or exponential part, but are allowed in the fraction.
/** * Determine whether the characters is a JSON number. Numbers are of the * form -12.34e+56. Fractional and exponential parts are optional. Leading * zeroes are not allowed in the value or exponential part, but are allowed * in the fraction. */
private JsonToken decodeNumber(char[] chars, int offset, int length) { int i = offset; int c = chars[i]; if (c == '-') { c = chars[++i]; } if (c == '0') { c = chars[++i]; } else if (c >= '1' && c <= '9') { c = chars[++i]; while (c >= '0' && c <= '9') { c = chars[++i]; } } else { return JsonToken.STRING; } if (c == '.') { c = chars[++i]; while (c >= '0' && c <= '9') { c = chars[++i]; } } if (c == 'e' || c == 'E') { c = chars[++i]; if (c == '+' || c == '-') { c = chars[++i]; } if (c >= '0' && c <= '9') { c = chars[++i]; while (c >= '0' && c <= '9') { c = chars[++i]; } } else { return JsonToken.STRING; } } if (i == offset + length) { return JsonToken.NUMBER; } else { return JsonToken.STRING; } }
Throws a new IO exception with the given message and a context snippet with this reader's content.
/** * Throws a new IO exception with the given message and a context snippet * with this reader's content. */
private IOException syntaxError(String message) throws IOException { throw new MalformedJsonException(message + " at line " + getLineNumber() + " column " + getColumnNumber()); } private CharSequence getSnippet() { StringBuilder snippet = new StringBuilder(); int beforePos = Math.min(pos, 20); snippet.append(buffer, pos - beforePos, beforePos); int afterPos = Math.min(limit - pos, 20); snippet.append(buffer, pos, afterPos); return snippet; } }