2012-08-26 06:02:09 +02:00
|
|
|
/*
|
|
|
|
* Copyright (C) 2010 Google Inc.
|
|
|
|
*
|
|
|
|
* 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 com.google.gson.stream;
|
|
|
|
|
2012-08-26 21:31:06 +02:00
|
|
|
import com.google.gson.internal.JsonReaderInternalAccess;
|
|
|
|
import com.google.gson.internal.bind.JsonTreeReader;
|
2012-08-26 06:02:09 +02:00
|
|
|
import java.io.Closeable;
|
|
|
|
import java.io.EOFException;
|
|
|
|
import java.io.IOException;
|
|
|
|
import java.io.Reader;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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 a JSON stream in Java?",
|
|
|
|
* "geo": null,
|
|
|
|
* "user": {
|
|
|
|
* "name": "json_newb",
|
|
|
|
* "followers_count": 41
|
|
|
|
* }
|
|
|
|
* },
|
|
|
|
* {
|
|
|
|
* "id": 912345678902,
|
|
|
|
* "text": "@json_newb just use 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.
|
|
|
|
*
|
|
|
|
* <a name="nonexecuteprefix"/><h3>Non-Execute Prefix</h3>
|
|
|
|
* Web servers that serve private data using JSON may be vulnerable to <a
|
|
|
|
* href="http://en.wikipedia.org/wiki/JSON#Cross-site_request_forgery">Cross-site
|
|
|
|
* request forgery</a> attacks. In such an attack, a malicious site gains access
|
|
|
|
* to a private JSON file by executing it with an HTML {@code <script>} tag.
|
|
|
|
*
|
|
|
|
* <p>Prefixing JSON files with <code>")]}'\n"</code> makes them non-executable
|
|
|
|
* by {@code <script>} tags, disarming the attack. Since the prefix is malformed
|
|
|
|
* JSON, strict parsing fails when it is encountered. This class permits the
|
|
|
|
* non-execute prefix when {@link #setLenient(boolean) lenient parsing} is
|
|
|
|
* enabled.
|
|
|
|
*
|
|
|
|
* <p>Each {@code JsonReader} may be used to read a single JSON stream. Instances
|
|
|
|
* of this class are not thread safe.
|
|
|
|
*
|
|
|
|
* @author Jesse Wilson
|
|
|
|
* @since 1.6
|
|
|
|
*/
|
2012-08-26 21:34:46 +02:00
|
|
|
public class JsonReader implements Closeable {
|
2012-08-26 21:31:06 +02:00
|
|
|
/** The only non-execute prefix this parser permits */
|
|
|
|
private static final char[] NON_EXECUTE_PREFIX = ")]}'\n".toCharArray();
|
2012-08-27 04:34:52 +02:00
|
|
|
private static final long MIN_INCOMPLETE_INTEGER = Long.MIN_VALUE / 10;
|
2012-08-26 21:31:06 +02:00
|
|
|
|
2012-08-26 06:02:09 +02:00
|
|
|
private static final int PEEKED_NONE = 0;
|
|
|
|
private static final int PEEKED_BEGIN_OBJECT = 1;
|
|
|
|
private static final int PEEKED_END_OBJECT = 2;
|
|
|
|
private static final int PEEKED_BEGIN_ARRAY = 3;
|
|
|
|
private static final int PEEKED_END_ARRAY = 4;
|
|
|
|
private static final int PEEKED_TRUE = 5;
|
|
|
|
private static final int PEEKED_FALSE = 6;
|
|
|
|
private static final int PEEKED_NULL = 7;
|
|
|
|
private static final int PEEKED_SINGLE_QUOTED = 8;
|
|
|
|
private static final int PEEKED_DOUBLE_QUOTED = 9;
|
|
|
|
private static final int PEEKED_UNQUOTED = 10;
|
2012-08-27 03:17:50 +02:00
|
|
|
/** When this is returned, the string value is stored in peekedString. */
|
2012-08-27 00:06:57 +02:00
|
|
|
private static final int PEEKED_BUFFERED = 11;
|
|
|
|
private static final int PEEKED_SINGLE_QUOTED_NAME = 12;
|
|
|
|
private static final int PEEKED_DOUBLE_QUOTED_NAME = 13;
|
|
|
|
private static final int PEEKED_UNQUOTED_NAME = 14;
|
2012-09-02 19:46:02 +02:00
|
|
|
/** When this is returned, the integer value is stored in peekedLong. */
|
|
|
|
private static final int PEEKED_LONG = 15;
|
2012-08-27 00:06:57 +02:00
|
|
|
private static final int PEEKED_NUMBER = 16;
|
|
|
|
private static final int PEEKED_EOF = 17;
|
2012-08-26 06:02:09 +02:00
|
|
|
|
2012-09-10 18:13:33 +02:00
|
|
|
/* State machine when parsing numbers */
|
|
|
|
private static final int NUMBER_CHAR_NONE = 0;
|
|
|
|
private static final int NUMBER_CHAR_SIGN = 1;
|
|
|
|
private static final int NUMBER_CHAR_DIGIT = 2;
|
|
|
|
private static final int NUMBER_CHAR_DECIMAL = 3;
|
|
|
|
private static final int NUMBER_CHAR_FRACTION_DIGIT = 4;
|
|
|
|
private static final int NUMBER_CHAR_EXP_E = 5;
|
|
|
|
private static final int NUMBER_CHAR_EXP_SIGN = 6;
|
|
|
|
private static final int NUMBER_CHAR_EXP_DIGIT = 7;
|
|
|
|
|
2012-08-26 06:02:09 +02:00
|
|
|
/** The input JSON. */
|
|
|
|
private final Reader in;
|
|
|
|
|
|
|
|
/** 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.
|
|
|
|
*/
|
|
|
|
private final char[] buffer = new char[1024];
|
|
|
|
private int pos = 0;
|
|
|
|
private int limit = 0;
|
|
|
|
|
2012-08-28 05:52:18 +02:00
|
|
|
private int lineNumber = 0;
|
|
|
|
private int lineStart = 0;
|
2012-08-26 06:02:09 +02:00
|
|
|
|
2015-12-27 07:39:19 +01:00
|
|
|
int peeked = PEEKED_NONE;
|
2012-08-26 06:02:09 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* A peeked value that was composed entirely of digits with an optional
|
|
|
|
* leading dash. Positive values may not have a leading 0.
|
|
|
|
*/
|
2012-09-02 19:46:02 +02:00
|
|
|
private long peekedLong;
|
2012-08-26 06:02:09 +02:00
|
|
|
|
|
|
|
/**
|
2012-08-27 03:17:50 +02:00
|
|
|
* The number of characters in a peeked number literal. Increment 'pos' by
|
|
|
|
* this after reading a number.
|
2012-08-26 06:02:09 +02:00
|
|
|
*/
|
|
|
|
private int peekedNumberLength;
|
|
|
|
|
2012-08-27 03:17:50 +02:00
|
|
|
/**
|
|
|
|
* A peeked string that should be parsed on the next double, long or string.
|
|
|
|
* This is populated before a numeric value is parsed and used if that parsing
|
|
|
|
* fails.
|
|
|
|
*/
|
2012-08-27 00:06:57 +02:00
|
|
|
private String peekedString;
|
|
|
|
|
2012-08-26 06:02:09 +02:00
|
|
|
/*
|
|
|
|
* The nesting stack. Using a manual array rather than an ArrayList saves 20%.
|
|
|
|
*/
|
|
|
|
private int[] stack = new int[32];
|
|
|
|
private int stackSize = 0;
|
|
|
|
{
|
|
|
|
stack[stackSize++] = JsonScope.EMPTY_DOCUMENT;
|
|
|
|
}
|
|
|
|
|
2014-07-31 07:17:54 +02:00
|
|
|
/*
|
|
|
|
* The path members. It corresponds directly to stack: At indices where the
|
|
|
|
* stack contains an object (EMPTY_OBJECT, DANGLING_NAME or NONEMPTY_OBJECT),
|
|
|
|
* pathNames contains the name at this scope. Where it contains an array
|
|
|
|
* (EMPTY_ARRAY, NONEMPTY_ARRAY) pathIndices contains the current index in
|
|
|
|
* that array. Otherwise the value is undefined, and we take advantage of that
|
|
|
|
* by incrementing pathIndices when doing so isn't useful.
|
|
|
|
*/
|
|
|
|
private String[] pathNames = new String[32];
|
|
|
|
private int[] pathIndices = new int[32];
|
|
|
|
|
2012-08-26 06:02:09 +02:00
|
|
|
/**
|
|
|
|
* Creates a new instance that reads a JSON-encoded stream from {@code in}.
|
|
|
|
*/
|
2012-08-26 21:34:46 +02:00
|
|
|
public JsonReader(Reader in) {
|
2012-08-26 06:02:09 +02:00
|
|
|
if (in == null) {
|
|
|
|
throw new NullPointerException("in == null");
|
|
|
|
}
|
|
|
|
this.in = in;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2016-01-17 09:03:04 +01:00
|
|
|
* Configure this parser to be liberal in what it accepts. By default,
|
2012-08-26 06:02:09 +02:00
|
|
|
* 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>Streams that start with the <a href="#nonexecuteprefix">non-execute
|
|
|
|
* prefix</a>, <code>")]}'\n"</code>.
|
|
|
|
* <li>Streams that include multiple top-level values. With strict parsing,
|
|
|
|
* each stream must contain exactly one top-level value.
|
|
|
|
* <li>Top-level values of any type. With strict parsing, the top-level
|
|
|
|
* value must be an object or an array.
|
|
|
|
* <li>Numbers may be {@link Double#isNaN() NaNs} or {@link
|
|
|
|
* Double#isInfinite() infinities}.
|
|
|
|
* <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 final void setLenient(boolean lenient) {
|
|
|
|
this.lenient = lenient;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns true if this parser is liberal in what it accepts.
|
|
|
|
*/
|
|
|
|
public final boolean isLenient() {
|
|
|
|
return lenient;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Consumes the next token from the JSON stream and asserts that it is the
|
|
|
|
* beginning of a new array.
|
|
|
|
*/
|
|
|
|
public void beginArray() throws IOException {
|
|
|
|
int p = peeked;
|
|
|
|
if (p == PEEKED_NONE) {
|
|
|
|
p = doPeek();
|
|
|
|
}
|
|
|
|
if (p == PEEKED_BEGIN_ARRAY) {
|
|
|
|
push(JsonScope.EMPTY_ARRAY);
|
2014-07-31 07:17:54 +02:00
|
|
|
pathIndices[stackSize - 1] = 0;
|
2012-08-26 06:02:09 +02:00
|
|
|
peeked = PEEKED_NONE;
|
|
|
|
} else {
|
|
|
|
throw new IllegalStateException("Expected BEGIN_ARRAY but was " + peek()
|
2014-07-31 07:17:54 +02:00
|
|
|
+ " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath());
|
2012-08-26 06:02:09 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Consumes the next token from the JSON stream and asserts that it is the
|
|
|
|
* end of the current array.
|
|
|
|
*/
|
|
|
|
public void endArray() throws IOException {
|
|
|
|
int p = peeked;
|
|
|
|
if (p == PEEKED_NONE) {
|
|
|
|
p = doPeek();
|
|
|
|
}
|
|
|
|
if (p == PEEKED_END_ARRAY) {
|
|
|
|
stackSize--;
|
2014-11-11 02:59:48 +01:00
|
|
|
pathIndices[stackSize - 1]++;
|
2012-08-26 06:02:09 +02:00
|
|
|
peeked = PEEKED_NONE;
|
|
|
|
} else {
|
|
|
|
throw new IllegalStateException("Expected END_ARRAY but was " + peek()
|
2014-07-31 07:17:54 +02:00
|
|
|
+ " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath());
|
2012-08-26 06:02:09 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Consumes the next token from the JSON stream and asserts that it is the
|
|
|
|
* beginning of a new object.
|
|
|
|
*/
|
|
|
|
public void beginObject() throws IOException {
|
|
|
|
int p = peeked;
|
|
|
|
if (p == PEEKED_NONE) {
|
|
|
|
p = doPeek();
|
|
|
|
}
|
|
|
|
if (p == PEEKED_BEGIN_OBJECT) {
|
|
|
|
push(JsonScope.EMPTY_OBJECT);
|
|
|
|
peeked = PEEKED_NONE;
|
|
|
|
} else {
|
|
|
|
throw new IllegalStateException("Expected BEGIN_OBJECT but was " + peek()
|
2014-07-31 07:17:54 +02:00
|
|
|
+ " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath());
|
2012-08-26 06:02:09 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Consumes the next token from the JSON stream and asserts that it is the
|
|
|
|
* end of the current object.
|
|
|
|
*/
|
|
|
|
public void endObject() throws IOException {
|
|
|
|
int p = peeked;
|
|
|
|
if (p == PEEKED_NONE) {
|
|
|
|
p = doPeek();
|
|
|
|
}
|
|
|
|
if (p == PEEKED_END_OBJECT) {
|
|
|
|
stackSize--;
|
2014-07-31 07:17:54 +02:00
|
|
|
pathNames[stackSize] = null; // Free the last path name so that it can be garbage collected!
|
2014-11-11 02:59:48 +01:00
|
|
|
pathIndices[stackSize - 1]++;
|
2012-08-26 06:02:09 +02:00
|
|
|
peeked = PEEKED_NONE;
|
|
|
|
} else {
|
|
|
|
throw new IllegalStateException("Expected END_OBJECT but was " + peek()
|
2014-07-31 07:17:54 +02:00
|
|
|
+ " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath());
|
2012-08-26 06:02:09 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns true if the current array or object has another element.
|
|
|
|
*/
|
|
|
|
public boolean hasNext() throws IOException {
|
|
|
|
int p = peeked;
|
|
|
|
if (p == PEEKED_NONE) {
|
|
|
|
p = doPeek();
|
|
|
|
}
|
|
|
|
return p != PEEKED_END_OBJECT && p != PEEKED_END_ARRAY;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the type of the next token without consuming it.
|
|
|
|
*/
|
|
|
|
public JsonToken peek() throws IOException {
|
|
|
|
int p = peeked;
|
|
|
|
if (p == PEEKED_NONE) {
|
|
|
|
p = doPeek();
|
|
|
|
}
|
|
|
|
|
|
|
|
switch (p) {
|
|
|
|
case PEEKED_BEGIN_OBJECT:
|
|
|
|
return JsonToken.BEGIN_OBJECT;
|
|
|
|
case PEEKED_END_OBJECT:
|
|
|
|
return JsonToken.END_OBJECT;
|
|
|
|
case PEEKED_BEGIN_ARRAY:
|
|
|
|
return JsonToken.BEGIN_ARRAY;
|
|
|
|
case PEEKED_END_ARRAY:
|
|
|
|
return JsonToken.END_ARRAY;
|
|
|
|
case PEEKED_SINGLE_QUOTED_NAME:
|
|
|
|
case PEEKED_DOUBLE_QUOTED_NAME:
|
|
|
|
case PEEKED_UNQUOTED_NAME:
|
|
|
|
return JsonToken.NAME;
|
|
|
|
case PEEKED_TRUE:
|
|
|
|
case PEEKED_FALSE:
|
|
|
|
return JsonToken.BOOLEAN;
|
|
|
|
case PEEKED_NULL:
|
|
|
|
return JsonToken.NULL;
|
|
|
|
case PEEKED_SINGLE_QUOTED:
|
|
|
|
case PEEKED_DOUBLE_QUOTED:
|
|
|
|
case PEEKED_UNQUOTED:
|
2012-08-27 00:06:57 +02:00
|
|
|
case PEEKED_BUFFERED:
|
2012-08-26 06:02:09 +02:00
|
|
|
return JsonToken.STRING;
|
2012-09-02 19:46:02 +02:00
|
|
|
case PEEKED_LONG:
|
2012-08-26 06:02:09 +02:00
|
|
|
case PEEKED_NUMBER:
|
|
|
|
return JsonToken.NUMBER;
|
|
|
|
case PEEKED_EOF:
|
|
|
|
return JsonToken.END_DOCUMENT;
|
|
|
|
default:
|
|
|
|
throw new AssertionError();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-12-27 07:39:19 +01:00
|
|
|
int doPeek() throws IOException {
|
2012-08-26 06:02:09 +02:00
|
|
|
int peekStack = stack[stackSize - 1];
|
|
|
|
if (peekStack == JsonScope.EMPTY_ARRAY) {
|
|
|
|
stack[stackSize - 1] = JsonScope.NONEMPTY_ARRAY;
|
|
|
|
} else if (peekStack == JsonScope.NONEMPTY_ARRAY) {
|
|
|
|
// Look for a comma before the next element.
|
|
|
|
int c = nextNonWhitespace(true);
|
|
|
|
switch (c) {
|
|
|
|
case ']':
|
|
|
|
return peeked = PEEKED_END_ARRAY;
|
|
|
|
case ';':
|
|
|
|
checkLenient(); // fall-through
|
|
|
|
case ',':
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
throw syntaxError("Unterminated array");
|
|
|
|
}
|
|
|
|
} else if (peekStack == JsonScope.EMPTY_OBJECT || peekStack == JsonScope.NONEMPTY_OBJECT) {
|
|
|
|
stack[stackSize - 1] = JsonScope.DANGLING_NAME;
|
|
|
|
// Look for a comma before the next element.
|
|
|
|
if (peekStack == JsonScope.NONEMPTY_OBJECT) {
|
|
|
|
int c = nextNonWhitespace(true);
|
|
|
|
switch (c) {
|
|
|
|
case '}':
|
|
|
|
return peeked = PEEKED_END_OBJECT;
|
|
|
|
case ';':
|
|
|
|
checkLenient(); // fall-through
|
|
|
|
case ',':
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
throw syntaxError("Unterminated object");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
int c = nextNonWhitespace(true);
|
|
|
|
switch (c) {
|
|
|
|
case '"':
|
|
|
|
return peeked = PEEKED_DOUBLE_QUOTED_NAME;
|
|
|
|
case '\'':
|
|
|
|
checkLenient();
|
|
|
|
return peeked = PEEKED_SINGLE_QUOTED_NAME;
|
|
|
|
case '}':
|
|
|
|
if (peekStack != JsonScope.NONEMPTY_OBJECT) {
|
|
|
|
return peeked = PEEKED_END_OBJECT;
|
|
|
|
} else {
|
|
|
|
throw syntaxError("Expected name");
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
checkLenient();
|
|
|
|
pos--; // Don't consume the first character in an unquoted string.
|
|
|
|
if (isLiteral((char) c)) {
|
|
|
|
return peeked = PEEKED_UNQUOTED_NAME;
|
|
|
|
} else {
|
|
|
|
throw syntaxError("Expected name");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else if (peekStack == JsonScope.DANGLING_NAME) {
|
|
|
|
stack[stackSize - 1] = JsonScope.NONEMPTY_OBJECT;
|
|
|
|
// Look for a colon before the value.
|
|
|
|
int c = nextNonWhitespace(true);
|
|
|
|
switch (c) {
|
|
|
|
case ':':
|
|
|
|
break;
|
|
|
|
case '=':
|
|
|
|
checkLenient();
|
|
|
|
if ((pos < limit || fillBuffer(1)) && buffer[pos] == '>') {
|
|
|
|
pos++;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
throw syntaxError("Expected ':'");
|
|
|
|
}
|
|
|
|
} else if (peekStack == JsonScope.EMPTY_DOCUMENT) {
|
2012-08-26 21:31:06 +02:00
|
|
|
if (lenient) {
|
|
|
|
consumeNonExecutePrefix();
|
|
|
|
}
|
2012-08-26 06:02:09 +02:00
|
|
|
stack[stackSize - 1] = JsonScope.NONEMPTY_DOCUMENT;
|
|
|
|
} else if (peekStack == JsonScope.NONEMPTY_DOCUMENT) {
|
|
|
|
int c = nextNonWhitespace(false);
|
|
|
|
if (c == -1) {
|
|
|
|
return peeked = PEEKED_EOF;
|
|
|
|
} else {
|
|
|
|
checkLenient();
|
|
|
|
pos--;
|
|
|
|
}
|
|
|
|
} else if (peekStack == JsonScope.CLOSED) {
|
|
|
|
throw new IllegalStateException("JsonReader is closed");
|
|
|
|
}
|
|
|
|
|
|
|
|
int c = nextNonWhitespace(true);
|
|
|
|
switch (c) {
|
|
|
|
case ']':
|
|
|
|
if (peekStack == JsonScope.EMPTY_ARRAY) {
|
|
|
|
return peeked = PEEKED_END_ARRAY;
|
|
|
|
}
|
|
|
|
// fall-through to handle ",]"
|
|
|
|
case ';':
|
|
|
|
case ',':
|
|
|
|
// In lenient mode, a 0-length literal in an array means 'null'.
|
|
|
|
if (peekStack == JsonScope.EMPTY_ARRAY || peekStack == JsonScope.NONEMPTY_ARRAY) {
|
|
|
|
checkLenient();
|
|
|
|
pos--;
|
|
|
|
return peeked = PEEKED_NULL;
|
|
|
|
} else {
|
|
|
|
throw syntaxError("Unexpected value");
|
|
|
|
}
|
|
|
|
case '\'':
|
|
|
|
checkLenient();
|
|
|
|
return peeked = PEEKED_SINGLE_QUOTED;
|
|
|
|
case '"':
|
|
|
|
if (stackSize == 1) {
|
|
|
|
checkLenient();
|
|
|
|
}
|
|
|
|
return peeked = PEEKED_DOUBLE_QUOTED;
|
|
|
|
case '[':
|
|
|
|
return peeked = PEEKED_BEGIN_ARRAY;
|
|
|
|
case '{':
|
|
|
|
return peeked = PEEKED_BEGIN_OBJECT;
|
|
|
|
default:
|
|
|
|
pos--; // Don't consume the first character in a literal value.
|
|
|
|
}
|
|
|
|
|
|
|
|
if (stackSize == 1) {
|
2012-08-27 00:06:57 +02:00
|
|
|
checkLenient(); // Top-level value isn't an array or an object.
|
2012-08-26 06:02:09 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
int result = peekKeyword();
|
|
|
|
if (result != PEEKED_NONE) {
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
result = peekNumber();
|
|
|
|
if (result != PEEKED_NONE) {
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!isLiteral(buffer[pos])) {
|
|
|
|
throw syntaxError("Expected value");
|
|
|
|
}
|
|
|
|
|
|
|
|
checkLenient();
|
|
|
|
return peeked = PEEKED_UNQUOTED;
|
|
|
|
}
|
|
|
|
|
|
|
|
private int peekKeyword() throws IOException {
|
|
|
|
// Figure out which keyword we're matching against by its first character.
|
|
|
|
char c = buffer[pos];
|
|
|
|
String keyword;
|
|
|
|
String keywordUpper;
|
|
|
|
int peeking;
|
|
|
|
if (c == 't' || c == 'T') {
|
|
|
|
keyword = "true";
|
|
|
|
keywordUpper = "TRUE";
|
|
|
|
peeking = PEEKED_TRUE;
|
|
|
|
} else if (c == 'f' || c == 'F') {
|
|
|
|
keyword = "false";
|
|
|
|
keywordUpper = "FALSE";
|
|
|
|
peeking = PEEKED_FALSE;
|
|
|
|
} else if (c == 'n' || c == 'N') {
|
|
|
|
keyword = "null";
|
|
|
|
keywordUpper = "NULL";
|
|
|
|
peeking = PEEKED_NULL;
|
|
|
|
} else {
|
|
|
|
return PEEKED_NONE;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Confirm that chars [1..length) match the keyword.
|
|
|
|
int length = keyword.length();
|
|
|
|
for (int i = 1; i < length; i++) {
|
|
|
|
if (pos + i >= limit && !fillBuffer(i + 1)) {
|
|
|
|
return PEEKED_NONE;
|
|
|
|
}
|
|
|
|
c = buffer[pos + i];
|
|
|
|
if (c != keyword.charAt(i) && c != keywordUpper.charAt(i)) {
|
|
|
|
return PEEKED_NONE;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ((pos + length < limit || fillBuffer(length + 1))
|
|
|
|
&& isLiteral(buffer[pos + length])) {
|
|
|
|
return PEEKED_NONE; // Don't match trues, falsey or nullsoft!
|
|
|
|
}
|
|
|
|
|
|
|
|
// We've found the keyword followed either by EOF or by a non-literal character.
|
|
|
|
pos += length;
|
|
|
|
return peeked = peeking;
|
|
|
|
}
|
|
|
|
|
|
|
|
private int peekNumber() throws IOException {
|
2012-09-02 22:12:19 +02:00
|
|
|
// Like nextNonWhitespace, this uses locals 'p' and 'l' to save inner-loop field access.
|
|
|
|
char[] buffer = this.buffer;
|
|
|
|
int p = pos;
|
|
|
|
int l = limit;
|
|
|
|
|
2012-09-02 19:46:02 +02:00
|
|
|
long value = 0; // Negative to accommodate Long.MIN_VALUE more easily.
|
2012-08-26 06:02:09 +02:00
|
|
|
boolean negative = false;
|
|
|
|
boolean fitsInLong = true;
|
2012-09-02 22:12:19 +02:00
|
|
|
int last = NUMBER_CHAR_NONE;
|
2012-08-26 06:02:09 +02:00
|
|
|
|
2012-09-02 22:12:19 +02:00
|
|
|
int i = 0;
|
2012-08-26 06:02:09 +02:00
|
|
|
|
2012-09-02 22:12:19 +02:00
|
|
|
charactersOfNumber:
|
|
|
|
for (; true; i++) {
|
|
|
|
if (p + i == l) {
|
|
|
|
if (i == buffer.length) {
|
|
|
|
// Though this looks like a well-formed number, it's too long to continue reading. Give up
|
|
|
|
// and let the application handle this as an unquoted literal.
|
|
|
|
return PEEKED_NONE;
|
|
|
|
}
|
|
|
|
if (!fillBuffer(i + 1)) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
p = pos;
|
|
|
|
l = limit;
|
2012-08-26 06:02:09 +02:00
|
|
|
}
|
|
|
|
|
2012-09-02 22:12:19 +02:00
|
|
|
char c = buffer[p + i];
|
|
|
|
switch (c) {
|
|
|
|
case '-':
|
|
|
|
if (last == NUMBER_CHAR_NONE) {
|
|
|
|
negative = true;
|
|
|
|
last = NUMBER_CHAR_SIGN;
|
|
|
|
continue;
|
|
|
|
} else if (last == NUMBER_CHAR_EXP_E) {
|
|
|
|
last = NUMBER_CHAR_EXP_SIGN;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
return PEEKED_NONE;
|
2012-08-26 06:02:09 +02:00
|
|
|
|
2012-09-02 22:12:19 +02:00
|
|
|
case '+':
|
|
|
|
if (last == NUMBER_CHAR_EXP_E) {
|
|
|
|
last = NUMBER_CHAR_EXP_SIGN;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
return PEEKED_NONE;
|
2012-08-26 06:02:09 +02:00
|
|
|
|
2012-09-02 22:12:19 +02:00
|
|
|
case 'e':
|
|
|
|
case 'E':
|
|
|
|
if (last == NUMBER_CHAR_DIGIT || last == NUMBER_CHAR_FRACTION_DIGIT) {
|
|
|
|
last = NUMBER_CHAR_EXP_E;
|
|
|
|
continue;
|
2012-08-26 06:02:09 +02:00
|
|
|
}
|
|
|
|
return PEEKED_NONE;
|
2012-09-02 22:12:19 +02:00
|
|
|
|
|
|
|
case '.':
|
|
|
|
if (last == NUMBER_CHAR_DIGIT) {
|
|
|
|
last = NUMBER_CHAR_DECIMAL;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
return PEEKED_NONE;
|
|
|
|
|
|
|
|
default:
|
|
|
|
if (c < '0' || c > '9') {
|
|
|
|
if (!isLiteral(c)) {
|
|
|
|
break charactersOfNumber;
|
|
|
|
}
|
|
|
|
return PEEKED_NONE;
|
|
|
|
}
|
|
|
|
if (last == NUMBER_CHAR_SIGN || last == NUMBER_CHAR_NONE) {
|
|
|
|
value = -(c - '0');
|
|
|
|
last = NUMBER_CHAR_DIGIT;
|
|
|
|
} else if (last == NUMBER_CHAR_DIGIT) {
|
|
|
|
if (value == 0) {
|
|
|
|
return PEEKED_NONE; // Leading '0' prefix is not allowed (since it could be octal).
|
|
|
|
}
|
|
|
|
long newValue = value * 10 - (c - '0');
|
|
|
|
fitsInLong &= value > MIN_INCOMPLETE_INTEGER
|
|
|
|
|| (value == MIN_INCOMPLETE_INTEGER && newValue < value);
|
|
|
|
value = newValue;
|
|
|
|
} else if (last == NUMBER_CHAR_DECIMAL) {
|
|
|
|
last = NUMBER_CHAR_FRACTION_DIGIT;
|
|
|
|
} else if (last == NUMBER_CHAR_EXP_E || last == NUMBER_CHAR_EXP_SIGN) {
|
|
|
|
last = NUMBER_CHAR_EXP_DIGIT;
|
|
|
|
}
|
2012-08-26 06:02:09 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2012-09-02 22:12:19 +02:00
|
|
|
// We've read a complete number. Decide if it's a PEEKED_LONG or a PEEKED_NUMBER.
|
|
|
|
if (last == NUMBER_CHAR_DIGIT && fitsInLong && (value != Long.MIN_VALUE || negative)) {
|
|
|
|
peekedLong = negative ? value : -value;
|
|
|
|
pos += i;
|
|
|
|
return peeked = PEEKED_LONG;
|
|
|
|
} else if (last == NUMBER_CHAR_DIGIT || last == NUMBER_CHAR_FRACTION_DIGIT
|
|
|
|
|| last == NUMBER_CHAR_EXP_DIGIT) {
|
2012-08-26 06:02:09 +02:00
|
|
|
peekedNumberLength = i;
|
|
|
|
return peeked = PEEKED_NUMBER;
|
2012-09-02 22:12:19 +02:00
|
|
|
} else {
|
|
|
|
return PEEKED_NONE;
|
2012-08-26 06:02:09 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private boolean isLiteral(char c) throws IOException {
|
|
|
|
switch (c) {
|
|
|
|
case '/':
|
|
|
|
case '\\':
|
|
|
|
case ';':
|
|
|
|
case '#':
|
|
|
|
case '=':
|
|
|
|
checkLenient(); // fall-through
|
|
|
|
case '{':
|
|
|
|
case '}':
|
|
|
|
case '[':
|
|
|
|
case ']':
|
|
|
|
case ':':
|
|
|
|
case ',':
|
|
|
|
case ' ':
|
|
|
|
case '\t':
|
|
|
|
case '\f':
|
|
|
|
case '\r':
|
|
|
|
case '\n':
|
|
|
|
return false;
|
|
|
|
default:
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the next token, a {@link com.google.gson.stream.JsonToken#NAME property name}, and
|
|
|
|
* consumes it.
|
|
|
|
*
|
|
|
|
* @throws java.io.IOException if the next token in the stream is not a property
|
|
|
|
* name.
|
|
|
|
*/
|
|
|
|
public String nextName() throws IOException {
|
|
|
|
int p = peeked;
|
|
|
|
if (p == PEEKED_NONE) {
|
|
|
|
p = doPeek();
|
|
|
|
}
|
|
|
|
String result;
|
|
|
|
if (p == PEEKED_UNQUOTED_NAME) {
|
|
|
|
result = nextUnquotedValue();
|
|
|
|
} else if (p == PEEKED_SINGLE_QUOTED_NAME) {
|
|
|
|
result = nextQuotedValue('\'');
|
|
|
|
} else if (p == PEEKED_DOUBLE_QUOTED_NAME) {
|
|
|
|
result = nextQuotedValue('"');
|
|
|
|
} else {
|
|
|
|
throw new IllegalStateException("Expected a name but was " + peek()
|
2014-07-31 07:17:54 +02:00
|
|
|
+ " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath());
|
2012-08-26 06:02:09 +02:00
|
|
|
}
|
|
|
|
peeked = PEEKED_NONE;
|
2014-07-31 07:17:54 +02:00
|
|
|
pathNames[stackSize - 1] = result;
|
2012-08-26 06:02:09 +02:00
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the {@link com.google.gson.stream.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 {
|
|
|
|
int p = peeked;
|
|
|
|
if (p == PEEKED_NONE) {
|
|
|
|
p = doPeek();
|
|
|
|
}
|
|
|
|
String result;
|
|
|
|
if (p == PEEKED_UNQUOTED) {
|
|
|
|
result = nextUnquotedValue();
|
|
|
|
} else if (p == PEEKED_SINGLE_QUOTED) {
|
|
|
|
result = nextQuotedValue('\'');
|
|
|
|
} else if (p == PEEKED_DOUBLE_QUOTED) {
|
|
|
|
result = nextQuotedValue('"');
|
2012-08-27 00:06:57 +02:00
|
|
|
} else if (p == PEEKED_BUFFERED) {
|
|
|
|
result = peekedString;
|
|
|
|
peekedString = null;
|
2012-09-02 19:46:02 +02:00
|
|
|
} else if (p == PEEKED_LONG) {
|
|
|
|
result = Long.toString(peekedLong);
|
2012-08-26 06:02:09 +02:00
|
|
|
} else if (p == PEEKED_NUMBER) {
|
|
|
|
result = new String(buffer, pos, peekedNumberLength);
|
|
|
|
pos += peekedNumberLength;
|
|
|
|
} else {
|
|
|
|
throw new IllegalStateException("Expected a string but was " + peek()
|
2014-07-31 07:17:54 +02:00
|
|
|
+ " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath());
|
2012-08-26 06:02:09 +02:00
|
|
|
}
|
|
|
|
peeked = PEEKED_NONE;
|
2014-07-31 07:17:54 +02:00
|
|
|
pathIndices[stackSize - 1]++;
|
2012-08-26 06:02:09 +02:00
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the {@link com.google.gson.stream.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 {
|
|
|
|
int p = peeked;
|
|
|
|
if (p == PEEKED_NONE) {
|
|
|
|
p = doPeek();
|
|
|
|
}
|
|
|
|
if (p == PEEKED_TRUE) {
|
|
|
|
peeked = PEEKED_NONE;
|
2014-07-31 07:17:54 +02:00
|
|
|
pathIndices[stackSize - 1]++;
|
2012-08-26 06:02:09 +02:00
|
|
|
return true;
|
|
|
|
} else if (p == PEEKED_FALSE) {
|
|
|
|
peeked = PEEKED_NONE;
|
2014-07-31 07:17:54 +02:00
|
|
|
pathIndices[stackSize - 1]++;
|
2012-08-26 06:02:09 +02:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
throw new IllegalStateException("Expected a boolean but was " + peek()
|
2014-07-31 07:17:54 +02:00
|
|
|
+ " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath());
|
2012-08-26 06:02:09 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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 {
|
|
|
|
int p = peeked;
|
|
|
|
if (p == PEEKED_NONE) {
|
|
|
|
p = doPeek();
|
|
|
|
}
|
|
|
|
if (p == PEEKED_NULL) {
|
|
|
|
peeked = PEEKED_NONE;
|
2014-07-31 07:17:54 +02:00
|
|
|
pathIndices[stackSize - 1]++;
|
2012-08-26 06:02:09 +02:00
|
|
|
} else {
|
|
|
|
throw new IllegalStateException("Expected null but was " + peek()
|
2014-07-31 07:17:54 +02:00
|
|
|
+ " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath());
|
2012-08-26 06:02:09 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the {@link com.google.gson.stream.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.
|
|
|
|
* @throws NumberFormatException if the next literal value cannot be parsed
|
|
|
|
* as a double, or is non-finite.
|
|
|
|
*/
|
|
|
|
public double nextDouble() throws IOException {
|
|
|
|
int p = peeked;
|
|
|
|
if (p == PEEKED_NONE) {
|
|
|
|
p = doPeek();
|
|
|
|
}
|
|
|
|
|
2012-09-02 19:46:02 +02:00
|
|
|
if (p == PEEKED_LONG) {
|
2012-08-26 06:02:09 +02:00
|
|
|
peeked = PEEKED_NONE;
|
2014-07-31 07:17:54 +02:00
|
|
|
pathIndices[stackSize - 1]++;
|
2012-09-02 19:46:02 +02:00
|
|
|
return (double) peekedLong;
|
2012-08-26 06:02:09 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (p == PEEKED_NUMBER) {
|
2012-08-27 00:06:57 +02:00
|
|
|
peekedString = new String(buffer, pos, peekedNumberLength);
|
2012-08-26 06:02:09 +02:00
|
|
|
pos += peekedNumberLength;
|
|
|
|
} else if (p == PEEKED_SINGLE_QUOTED || p == PEEKED_DOUBLE_QUOTED) {
|
2012-08-27 00:06:57 +02:00
|
|
|
peekedString = nextQuotedValue(p == PEEKED_SINGLE_QUOTED ? '\'' : '"');
|
2012-08-26 06:02:09 +02:00
|
|
|
} else if (p == PEEKED_UNQUOTED) {
|
2012-08-27 00:06:57 +02:00
|
|
|
peekedString = nextUnquotedValue();
|
|
|
|
} else if (p != PEEKED_BUFFERED) {
|
2012-08-26 06:02:09 +02:00
|
|
|
throw new IllegalStateException("Expected a double but was " + peek()
|
2014-07-31 07:17:54 +02:00
|
|
|
+ " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath());
|
2012-08-26 06:02:09 +02:00
|
|
|
}
|
|
|
|
|
2012-08-27 00:06:57 +02:00
|
|
|
peeked = PEEKED_BUFFERED;
|
|
|
|
double result = Double.parseDouble(peekedString); // don't catch this NumberFormatException.
|
2012-08-26 06:02:09 +02:00
|
|
|
if (!lenient && (Double.isNaN(result) || Double.isInfinite(result))) {
|
|
|
|
throw new MalformedJsonException("JSON forbids NaN and infinities: " + result
|
2014-07-31 07:17:54 +02:00
|
|
|
+ " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath());
|
2012-08-26 06:02:09 +02:00
|
|
|
}
|
2012-08-27 00:06:57 +02:00
|
|
|
peekedString = null;
|
2012-08-26 06:02:09 +02:00
|
|
|
peeked = PEEKED_NONE;
|
2014-07-31 07:17:54 +02:00
|
|
|
pathIndices[stackSize - 1]++;
|
2012-08-26 06:02:09 +02:00
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the {@link com.google.gson.stream.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 {
|
|
|
|
int p = peeked;
|
|
|
|
if (p == PEEKED_NONE) {
|
|
|
|
p = doPeek();
|
|
|
|
}
|
|
|
|
|
2012-09-02 19:46:02 +02:00
|
|
|
if (p == PEEKED_LONG) {
|
2012-08-26 06:02:09 +02:00
|
|
|
peeked = PEEKED_NONE;
|
2014-07-31 07:17:54 +02:00
|
|
|
pathIndices[stackSize - 1]++;
|
2012-09-02 19:46:02 +02:00
|
|
|
return peekedLong;
|
2012-08-26 06:02:09 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (p == PEEKED_NUMBER) {
|
2012-08-27 00:06:57 +02:00
|
|
|
peekedString = new String(buffer, pos, peekedNumberLength);
|
2012-08-26 06:02:09 +02:00
|
|
|
pos += peekedNumberLength;
|
|
|
|
} else if (p == PEEKED_SINGLE_QUOTED || p == PEEKED_DOUBLE_QUOTED) {
|
2012-08-27 00:06:57 +02:00
|
|
|
peekedString = nextQuotedValue(p == PEEKED_SINGLE_QUOTED ? '\'' : '"');
|
2012-08-26 06:02:09 +02:00
|
|
|
try {
|
2012-08-27 00:06:57 +02:00
|
|
|
long result = Long.parseLong(peekedString);
|
2012-08-26 06:02:09 +02:00
|
|
|
peeked = PEEKED_NONE;
|
2014-07-31 07:17:54 +02:00
|
|
|
pathIndices[stackSize - 1]++;
|
2012-08-26 06:02:09 +02:00
|
|
|
return result;
|
|
|
|
} catch (NumberFormatException ignored) {
|
|
|
|
// Fall back to parse as a double below.
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
throw new IllegalStateException("Expected a long but was " + peek()
|
2014-07-31 07:17:54 +02:00
|
|
|
+ " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath());
|
2012-08-26 06:02:09 +02:00
|
|
|
}
|
|
|
|
|
2012-08-27 00:06:57 +02:00
|
|
|
peeked = PEEKED_BUFFERED;
|
|
|
|
double asDouble = Double.parseDouble(peekedString); // don't catch this NumberFormatException.
|
2012-08-26 06:02:09 +02:00
|
|
|
long result = (long) asDouble;
|
|
|
|
if (result != asDouble) { // Make sure no precision was lost casting to 'long'.
|
2012-08-27 00:06:57 +02:00
|
|
|
throw new NumberFormatException("Expected a long but was " + peekedString
|
2014-07-31 07:17:54 +02:00
|
|
|
+ " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath());
|
2012-08-26 06:02:09 +02:00
|
|
|
}
|
2012-08-27 00:06:57 +02:00
|
|
|
peekedString = null;
|
2012-08-26 06:02:09 +02:00
|
|
|
peeked = PEEKED_NONE;
|
2014-07-31 07:17:54 +02:00
|
|
|
pathIndices[stackSize - 1]++;
|
2012-08-26 06:02:09 +02:00
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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 nextQuotedValue(char quote) throws IOException {
|
|
|
|
// Like nextNonWhitespace, this uses locals 'p' and 'l' to save inner-loop field access.
|
|
|
|
char[] buffer = this.buffer;
|
2013-04-08 20:36:23 +02:00
|
|
|
StringBuilder builder = new StringBuilder();
|
2012-08-26 06:02:09 +02:00
|
|
|
while (true) {
|
|
|
|
int p = pos;
|
|
|
|
int l = limit;
|
|
|
|
/* the index of the first character not yet appended to the builder. */
|
|
|
|
int start = p;
|
2012-08-28 05:52:18 +02:00
|
|
|
while (p < l) {
|
|
|
|
int c = buffer[p++];
|
2012-08-26 06:02:09 +02:00
|
|
|
|
|
|
|
if (c == quote) {
|
2012-08-28 05:52:18 +02:00
|
|
|
pos = p;
|
2013-04-08 20:36:23 +02:00
|
|
|
builder.append(buffer, start, p - start - 1);
|
|
|
|
return builder.toString();
|
2012-08-26 06:02:09 +02:00
|
|
|
} else if (c == '\\') {
|
2012-08-28 05:52:18 +02:00
|
|
|
pos = p;
|
|
|
|
builder.append(buffer, start, p - start - 1);
|
2012-08-26 06:02:09 +02:00
|
|
|
builder.append(readEscapeCharacter());
|
|
|
|
p = pos;
|
|
|
|
l = limit;
|
|
|
|
start = p;
|
2012-08-28 05:52:18 +02:00
|
|
|
} else if (c == '\n') {
|
|
|
|
lineNumber++;
|
|
|
|
lineStart = p;
|
2012-08-26 06:02:09 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
builder.append(buffer, start, p - start);
|
|
|
|
pos = p;
|
|
|
|
if (!fillBuffer(1)) {
|
|
|
|
throw syntaxError("Unterminated string");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns an unquoted value as a string.
|
|
|
|
*/
|
|
|
|
@SuppressWarnings("fallthrough")
|
|
|
|
private String nextUnquotedValue() throws IOException {
|
|
|
|
StringBuilder builder = null;
|
|
|
|
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 (i < buffer.length) {
|
|
|
|
if (fillBuffer(i + 1)) {
|
|
|
|
continue;
|
|
|
|
} else {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// use a StringBuilder when the value is too long. This is too long to be a number!
|
|
|
|
if (builder == null) {
|
|
|
|
builder = new StringBuilder();
|
|
|
|
}
|
|
|
|
builder.append(buffer, pos, i);
|
|
|
|
pos += i;
|
|
|
|
i = 0;
|
|
|
|
if (!fillBuffer(1)) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
String result;
|
2012-08-27 03:17:50 +02:00
|
|
|
if (builder == null) {
|
2012-08-26 06:02:09 +02:00
|
|
|
result = new String(buffer, pos, i);
|
|
|
|
} else {
|
|
|
|
builder.append(buffer, pos, i);
|
|
|
|
result = builder.toString();
|
|
|
|
}
|
|
|
|
pos += i;
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2012-08-27 03:17:50 +02:00
|
|
|
private void skipQuotedValue(char quote) throws IOException {
|
|
|
|
// Like nextNonWhitespace, this uses locals 'p' and 'l' to save inner-loop field access.
|
|
|
|
char[] buffer = this.buffer;
|
|
|
|
do {
|
|
|
|
int p = pos;
|
|
|
|
int l = limit;
|
|
|
|
/* the index of the first character not yet appended to the builder. */
|
|
|
|
while (p < l) {
|
|
|
|
int c = buffer[p++];
|
|
|
|
if (c == quote) {
|
|
|
|
pos = p;
|
|
|
|
return;
|
|
|
|
} else if (c == '\\') {
|
|
|
|
pos = p;
|
|
|
|
readEscapeCharacter();
|
|
|
|
p = pos;
|
|
|
|
l = limit;
|
2012-08-28 05:52:18 +02:00
|
|
|
} else if (c == '\n') {
|
|
|
|
lineNumber++;
|
|
|
|
lineStart = p;
|
2012-08-27 03:17:50 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
pos = p;
|
|
|
|
} while (fillBuffer(1));
|
|
|
|
throw syntaxError("Unterminated string");
|
|
|
|
}
|
|
|
|
|
|
|
|
private void skipUnquotedValue() throws IOException {
|
|
|
|
do {
|
|
|
|
int i = 0;
|
|
|
|
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':
|
|
|
|
pos += i;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
pos += i;
|
|
|
|
} while (fillBuffer(1));
|
|
|
|
}
|
|
|
|
|
2012-08-26 06:02:09 +02:00
|
|
|
/**
|
|
|
|
* Returns the {@link com.google.gson.stream.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 {
|
|
|
|
int p = peeked;
|
|
|
|
if (p == PEEKED_NONE) {
|
|
|
|
p = doPeek();
|
|
|
|
}
|
|
|
|
|
|
|
|
int result;
|
2012-09-02 19:46:02 +02:00
|
|
|
if (p == PEEKED_LONG) {
|
|
|
|
result = (int) peekedLong;
|
|
|
|
if (peekedLong != result) { // Make sure no precision was lost casting to 'int'.
|
|
|
|
throw new NumberFormatException("Expected an int but was " + peekedLong
|
2014-07-31 07:17:54 +02:00
|
|
|
+ " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath());
|
2012-08-26 06:02:09 +02:00
|
|
|
}
|
|
|
|
peeked = PEEKED_NONE;
|
2014-07-31 07:17:54 +02:00
|
|
|
pathIndices[stackSize - 1]++;
|
2012-08-26 06:02:09 +02:00
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (p == PEEKED_NUMBER) {
|
2012-08-27 00:06:57 +02:00
|
|
|
peekedString = new String(buffer, pos, peekedNumberLength);
|
2012-08-26 06:02:09 +02:00
|
|
|
pos += peekedNumberLength;
|
|
|
|
} else if (p == PEEKED_SINGLE_QUOTED || p == PEEKED_DOUBLE_QUOTED) {
|
2012-08-27 00:06:57 +02:00
|
|
|
peekedString = nextQuotedValue(p == PEEKED_SINGLE_QUOTED ? '\'' : '"');
|
2012-08-26 06:02:09 +02:00
|
|
|
try {
|
2012-08-27 00:06:57 +02:00
|
|
|
result = Integer.parseInt(peekedString);
|
2012-08-26 06:02:09 +02:00
|
|
|
peeked = PEEKED_NONE;
|
2014-07-31 07:17:54 +02:00
|
|
|
pathIndices[stackSize - 1]++;
|
2012-08-26 06:02:09 +02:00
|
|
|
return result;
|
|
|
|
} catch (NumberFormatException ignored) {
|
|
|
|
// Fall back to parse as a double below.
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
throw new IllegalStateException("Expected an int but was " + peek()
|
2014-07-31 07:17:54 +02:00
|
|
|
+ " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath());
|
2012-08-26 06:02:09 +02:00
|
|
|
}
|
|
|
|
|
2012-08-27 00:06:57 +02:00
|
|
|
peeked = PEEKED_BUFFERED;
|
|
|
|
double asDouble = Double.parseDouble(peekedString); // don't catch this NumberFormatException.
|
2012-08-26 06:02:09 +02:00
|
|
|
result = (int) asDouble;
|
|
|
|
if (result != asDouble) { // Make sure no precision was lost casting to 'int'.
|
2012-08-27 00:06:57 +02:00
|
|
|
throw new NumberFormatException("Expected an int but was " + peekedString
|
2014-07-31 07:17:54 +02:00
|
|
|
+ " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath());
|
2012-08-26 06:02:09 +02:00
|
|
|
}
|
2012-08-27 00:06:57 +02:00
|
|
|
peekedString = null;
|
2012-08-26 06:02:09 +02:00
|
|
|
peeked = PEEKED_NONE;
|
2014-07-31 07:17:54 +02:00
|
|
|
pathIndices[stackSize - 1]++;
|
2012-08-26 06:02:09 +02:00
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Closes this JSON reader and the underlying {@link java.io.Reader}.
|
|
|
|
*/
|
|
|
|
public void close() throws IOException {
|
|
|
|
peeked = PEEKED_NONE;
|
|
|
|
stack[0] = JsonScope.CLOSED;
|
|
|
|
stackSize = 1;
|
|
|
|
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.
|
|
|
|
*/
|
|
|
|
public void skipValue() throws IOException {
|
|
|
|
int count = 0;
|
|
|
|
do {
|
2013-01-14 19:38:46 +01:00
|
|
|
int p = peeked;
|
|
|
|
if (p == PEEKED_NONE) {
|
|
|
|
p = doPeek();
|
|
|
|
}
|
|
|
|
|
2012-08-26 06:02:09 +02:00
|
|
|
if (p == PEEKED_BEGIN_ARRAY) {
|
|
|
|
push(JsonScope.EMPTY_ARRAY);
|
|
|
|
count++;
|
|
|
|
} else if (p == PEEKED_BEGIN_OBJECT) {
|
|
|
|
push(JsonScope.EMPTY_OBJECT);
|
|
|
|
count++;
|
|
|
|
} else if (p == PEEKED_END_ARRAY) {
|
|
|
|
stackSize--;
|
|
|
|
count--;
|
|
|
|
} else if (p == PEEKED_END_OBJECT) {
|
|
|
|
stackSize--;
|
|
|
|
count--;
|
|
|
|
} else if (p == PEEKED_UNQUOTED_NAME || p == PEEKED_UNQUOTED) {
|
2012-08-27 03:17:50 +02:00
|
|
|
skipUnquotedValue();
|
2012-08-26 06:02:09 +02:00
|
|
|
} else if (p == PEEKED_SINGLE_QUOTED || p == PEEKED_SINGLE_QUOTED_NAME) {
|
2012-08-27 03:17:50 +02:00
|
|
|
skipQuotedValue('\'');
|
2012-08-26 06:02:09 +02:00
|
|
|
} else if (p == PEEKED_DOUBLE_QUOTED || p == PEEKED_DOUBLE_QUOTED_NAME) {
|
2012-08-27 03:17:50 +02:00
|
|
|
skipQuotedValue('"');
|
2012-08-26 06:02:09 +02:00
|
|
|
} else if (p == PEEKED_NUMBER) {
|
|
|
|
pos += peekedNumberLength;
|
|
|
|
}
|
|
|
|
peeked = PEEKED_NONE;
|
|
|
|
} while (count != 0);
|
2014-07-31 07:17:54 +02:00
|
|
|
|
|
|
|
pathIndices[stackSize - 1]++;
|
|
|
|
pathNames[stackSize - 1] = "null";
|
2012-08-26 06:02:09 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
private void push(int newTop) {
|
|
|
|
if (stackSize == stack.length) {
|
|
|
|
int[] newStack = new int[stackSize * 2];
|
2014-07-31 07:17:54 +02:00
|
|
|
int[] newPathIndices = new int[stackSize * 2];
|
|
|
|
String[] newPathNames = new String[stackSize * 2];
|
2012-08-26 06:02:09 +02:00
|
|
|
System.arraycopy(stack, 0, newStack, 0, stackSize);
|
2014-07-31 07:17:54 +02:00
|
|
|
System.arraycopy(pathIndices, 0, newPathIndices, 0, stackSize);
|
|
|
|
System.arraycopy(pathNames, 0, newPathNames, 0, stackSize);
|
2012-08-26 06:02:09 +02:00
|
|
|
stack = newStack;
|
2014-07-31 07:17:54 +02:00
|
|
|
pathIndices = newPathIndices;
|
|
|
|
pathNames = newPathNames;
|
2012-08-26 06:02:09 +02:00
|
|
|
}
|
|
|
|
stack[stackSize++] = newTop;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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 {
|
|
|
|
char[] buffer = this.buffer;
|
2012-08-28 05:52:18 +02:00
|
|
|
lineStart -= pos;
|
2012-08-26 06:02:09 +02:00
|
|
|
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
|
2012-08-28 05:52:18 +02:00
|
|
|
if (lineNumber == 0 && lineStart == 0 && limit > 0 && buffer[0] == '\ufeff') {
|
2012-08-26 06:02:09 +02:00
|
|
|
pos++;
|
2012-08-28 05:52:18 +02:00
|
|
|
lineStart++;
|
2012-08-28 03:48:25 +02:00
|
|
|
minimum++;
|
2012-08-26 06:02:09 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (limit >= minimum) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2015-12-27 07:39:19 +01:00
|
|
|
int getLineNumber() {
|
2012-08-28 05:52:18 +02:00
|
|
|
return lineNumber + 1;
|
2012-08-26 06:02:09 +02:00
|
|
|
}
|
|
|
|
|
2015-12-27 07:39:19 +01:00
|
|
|
int getColumnNumber() {
|
2012-08-28 05:52:18 +02:00
|
|
|
return pos - lineStart + 1;
|
2012-08-26 06:02:09 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the next character in the stream that is neither whitespace nor a
|
|
|
|
* part of a comment. When this returns, the returned character is always at
|
|
|
|
* {@code buffer[pos-1]}; this means the caller can always push back the
|
|
|
|
* returned character by decrementing {@code pos}.
|
|
|
|
*/
|
|
|
|
private int nextNonWhitespace(boolean throwOnEof) throws IOException {
|
|
|
|
/*
|
|
|
|
* This code uses ugly local variables 'p' and 'l' representing the 'pos'
|
|
|
|
* and 'limit' fields respectively. Using locals rather than fields saves
|
|
|
|
* a few field reads for each whitespace character in a pretty-printed
|
|
|
|
* document, resulting in a 5% speedup. We need to flush 'p' to its field
|
|
|
|
* before any (potentially indirect) call to fillBuffer() and reread both
|
|
|
|
* 'p' and 'l' after any (potentially indirect) call to the same method.
|
|
|
|
*/
|
|
|
|
char[] buffer = this.buffer;
|
|
|
|
int p = pos;
|
|
|
|
int l = limit;
|
|
|
|
while (true) {
|
|
|
|
if (p == l) {
|
|
|
|
pos = p;
|
|
|
|
if (!fillBuffer(1)) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
p = pos;
|
|
|
|
l = limit;
|
|
|
|
}
|
|
|
|
|
|
|
|
int c = buffer[p++];
|
2012-08-28 05:52:18 +02:00
|
|
|
if (c == '\n') {
|
|
|
|
lineNumber++;
|
|
|
|
lineStart = p;
|
|
|
|
continue;
|
|
|
|
} else if (c == ' ' || c == '\r' || c == '\t') {
|
2012-08-26 06:02:09 +02:00
|
|
|
continue;
|
2012-08-27 06:17:29 +02:00
|
|
|
}
|
2012-08-26 06:02:09 +02:00
|
|
|
|
2012-08-27 06:17:29 +02:00
|
|
|
if (c == '/') {
|
2012-08-26 06:02:09 +02:00
|
|
|
pos = p;
|
|
|
|
if (p == l) {
|
|
|
|
pos--; // push back '/' so it's still in the buffer when this method returns
|
|
|
|
boolean charsLoaded = fillBuffer(2);
|
|
|
|
pos++; // consume the '/' again
|
|
|
|
if (!charsLoaded) {
|
|
|
|
return c;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
checkLenient();
|
|
|
|
char peek = buffer[pos];
|
|
|
|
switch (peek) {
|
|
|
|
case '*':
|
|
|
|
// skip a /* c-style comment */
|
|
|
|
pos++;
|
|
|
|
if (!skipTo("*/")) {
|
|
|
|
throw syntaxError("Unterminated comment");
|
|
|
|
}
|
|
|
|
p = pos + 2;
|
|
|
|
l = limit;
|
|
|
|
continue;
|
|
|
|
|
|
|
|
case '/':
|
|
|
|
// skip a // end-of-line comment
|
|
|
|
pos++;
|
|
|
|
skipToEndOfLine();
|
|
|
|
p = pos;
|
|
|
|
l = limit;
|
|
|
|
continue;
|
|
|
|
|
|
|
|
default:
|
|
|
|
return c;
|
|
|
|
}
|
2012-08-27 06:17:29 +02:00
|
|
|
} else if (c == '#') {
|
2012-08-26 06:02:09 +02:00
|
|
|
pos = p;
|
|
|
|
/*
|
|
|
|
* 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();
|
|
|
|
p = pos;
|
|
|
|
l = limit;
|
2012-08-27 06:17:29 +02:00
|
|
|
} else {
|
2012-08-26 06:02:09 +02:00
|
|
|
pos = p;
|
|
|
|
return c;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (throwOnEof) {
|
|
|
|
throw new EOFException("End of input"
|
|
|
|
+ " at line " + getLineNumber() + " column " + getColumnNumber());
|
|
|
|
} else {
|
|
|
|
return -1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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.
|
|
|
|
*/
|
|
|
|
private void skipToEndOfLine() throws IOException {
|
|
|
|
while (pos < limit || fillBuffer(1)) {
|
|
|
|
char c = buffer[pos++];
|
2012-08-28 05:52:18 +02:00
|
|
|
if (c == '\n') {
|
|
|
|
lineNumber++;
|
|
|
|
lineStart = pos;
|
|
|
|
break;
|
|
|
|
} else if (c == '\r') {
|
2012-08-26 06:02:09 +02:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2012-08-28 05:52:18 +02:00
|
|
|
/**
|
|
|
|
* @param toFind a string to search for. Must not contain a newline.
|
|
|
|
*/
|
2012-08-26 06:02:09 +02:00
|
|
|
private boolean skipTo(String toFind) throws IOException {
|
|
|
|
outer:
|
|
|
|
for (; pos + toFind.length() <= limit || fillBuffer(toFind.length()); pos++) {
|
2012-08-28 05:52:18 +02:00
|
|
|
if (buffer[pos] == '\n') {
|
|
|
|
lineNumber++;
|
|
|
|
lineStart = pos + 1;
|
|
|
|
continue;
|
|
|
|
}
|
2012-08-26 06:02:09 +02:00
|
|
|
for (int c = 0; c < toFind.length(); c++) {
|
|
|
|
if (buffer[pos + c] != toFind.charAt(c)) {
|
|
|
|
continue outer;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override public String toString() {
|
|
|
|
return getClass().getSimpleName()
|
|
|
|
+ " at line " + getLineNumber() + " column " + getColumnNumber();
|
|
|
|
}
|
|
|
|
|
2014-07-31 07:17:54 +02:00
|
|
|
/**
|
|
|
|
* Returns a <a href="http://goessner.net/articles/JsonPath/">JsonPath</a> to
|
|
|
|
* the current location in the JSON value.
|
|
|
|
*/
|
|
|
|
public String getPath() {
|
|
|
|
StringBuilder result = new StringBuilder().append('$');
|
|
|
|
for (int i = 0, size = stackSize; i < size; i++) {
|
|
|
|
switch (stack[i]) {
|
|
|
|
case JsonScope.EMPTY_ARRAY:
|
|
|
|
case JsonScope.NONEMPTY_ARRAY:
|
|
|
|
result.append('[').append(pathIndices[i]).append(']');
|
|
|
|
break;
|
|
|
|
|
|
|
|
case JsonScope.EMPTY_OBJECT:
|
|
|
|
case JsonScope.DANGLING_NAME:
|
|
|
|
case JsonScope.NONEMPTY_OBJECT:
|
|
|
|
result.append('.');
|
|
|
|
if (pathNames[i] != null) {
|
|
|
|
result.append(pathNames[i]);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case JsonScope.NONEMPTY_DOCUMENT:
|
|
|
|
case JsonScope.EMPTY_DOCUMENT:
|
|
|
|
case JsonScope.CLOSED:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return result.toString();
|
|
|
|
}
|
|
|
|
|
2012-08-26 06:02:09 +02:00
|
|
|
/**
|
|
|
|
* 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");
|
|
|
|
}
|
|
|
|
// Equivalent to Integer.parseInt(stringPool.get(buffer, pos, 4), 16);
|
|
|
|
char result = 0;
|
|
|
|
for (int i = pos, end = i + 4; i < end; i++) {
|
|
|
|
char c = buffer[i];
|
|
|
|
result <<= 4;
|
|
|
|
if (c >= '0' && c <= '9') {
|
|
|
|
result += (c - '0');
|
|
|
|
} else if (c >= 'a' && c <= 'f') {
|
|
|
|
result += (c - 'a' + 10);
|
|
|
|
} else if (c >= 'A' && c <= 'F') {
|
|
|
|
result += (c - 'A' + 10);
|
|
|
|
} else {
|
|
|
|
throw new NumberFormatException("\\u" + new String(buffer, pos, 4));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
pos += 4;
|
|
|
|
return result;
|
|
|
|
|
|
|
|
case 't':
|
|
|
|
return '\t';
|
|
|
|
|
|
|
|
case 'b':
|
|
|
|
return '\b';
|
|
|
|
|
|
|
|
case 'n':
|
|
|
|
return '\n';
|
|
|
|
|
|
|
|
case 'r':
|
|
|
|
return '\r';
|
|
|
|
|
|
|
|
case 'f':
|
|
|
|
return '\f';
|
|
|
|
|
2012-08-28 05:52:18 +02:00
|
|
|
case '\n':
|
|
|
|
lineNumber++;
|
|
|
|
lineStart = pos;
|
|
|
|
// fall-through
|
|
|
|
|
2012-08-26 06:02:09 +02:00
|
|
|
case '\'':
|
|
|
|
case '"':
|
|
|
|
case '\\':
|
|
|
|
default:
|
|
|
|
return escaped;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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
|
2014-07-31 07:17:54 +02:00
|
|
|
+ " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath());
|
2012-08-26 06:02:09 +02:00
|
|
|
}
|
|
|
|
|
2012-08-26 21:31:06 +02:00
|
|
|
/**
|
|
|
|
* Consumes the non-execute prefix if it exists.
|
|
|
|
*/
|
|
|
|
private void consumeNonExecutePrefix() throws IOException {
|
|
|
|
// fast forward through the leading whitespace
|
|
|
|
nextNonWhitespace(true);
|
|
|
|
pos--;
|
|
|
|
|
|
|
|
if (pos + NON_EXECUTE_PREFIX.length > limit && !fillBuffer(NON_EXECUTE_PREFIX.length)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (int i = 0; i < NON_EXECUTE_PREFIX.length; i++) {
|
|
|
|
if (buffer[pos + i] != NON_EXECUTE_PREFIX[i]) {
|
|
|
|
return; // not a security token!
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// we consumed a security token!
|
|
|
|
pos += NON_EXECUTE_PREFIX.length;
|
|
|
|
}
|
|
|
|
|
2012-08-26 21:34:46 +02:00
|
|
|
static {
|
2012-08-26 06:02:09 +02:00
|
|
|
JsonReaderInternalAccess.INSTANCE = new JsonReaderInternalAccess() {
|
|
|
|
@Override public void promoteNameToValue(JsonReader reader) throws IOException {
|
|
|
|
if (reader instanceof JsonTreeReader) {
|
|
|
|
((JsonTreeReader)reader).promoteNameToValue();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
int p = reader.peeked;
|
|
|
|
if (p == PEEKED_NONE) {
|
|
|
|
p = reader.doPeek();
|
|
|
|
}
|
|
|
|
if (p == PEEKED_DOUBLE_QUOTED_NAME) {
|
|
|
|
reader.peeked = PEEKED_DOUBLE_QUOTED;
|
|
|
|
} else if (p == PEEKED_SINGLE_QUOTED_NAME) {
|
|
|
|
reader.peeked = PEEKED_SINGLE_QUOTED;
|
|
|
|
} else if (p == PEEKED_UNQUOTED_NAME) {
|
|
|
|
reader.peeked = PEEKED_UNQUOTED;
|
|
|
|
} else {
|
|
|
|
throw new IllegalStateException("Expected a name but was " + reader.peek() + " "
|
2014-07-31 07:17:54 +02:00
|
|
|
+ " at line " + reader.getLineNumber() + " column " + reader.getColumnNumber()
|
|
|
|
+ " path " + reader.getPath());
|
2012-08-26 06:02:09 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
2012-08-26 21:34:46 +02:00
|
|
|
}
|
2012-08-26 06:02:09 +02:00
|
|
|
}
|