/*
* 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 java.io.Closeable;
import java.io.EOFException;
import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.List;
import libcore.internal.StringPool;
/**
* 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. */
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;
/*
* 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 {@link #peek} and {@link
* #advance}. If null, peek() will assign a value.
*/
private JsonToken token;
/** 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. */
private boolean skipping = false;
/**
* 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 <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.
*/
public 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 {
expect(JsonToken.BEGIN_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.
*/
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 array.
*/
public void endObject() throws IOException {
expect(JsonToken.END_OBJECT);
}
/**
* 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.
*/
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.
*/
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.
*/
private JsonToken advance() throws IOException {
peek();
JsonToken result = token;
token = null;
value = null;
name = null;
return result;
}
/**
* 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 {@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 {@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.
*/
public void nextNull() throws IOException {
peek();
if (token != JsonToken.NULL) {
throw new IllegalStateException("Expected null but was " + token);
}
advance();
}
/**
* 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 {@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 {@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 {@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.
*/
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.
*/
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 {@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.
*/
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 {@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.
*
* @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.
*/
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.
*/
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 {@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.
*/
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.
*/
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;
}
}