blob: 7dad7854ca0d4ce885b0e3fe5540f54ea2a72470 [file] [log] [blame]
package org.jetbrains.io;
import com.google.gson.JsonParseException;
import com.google.gson.stream.JsonToken;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.Closeable;
public final class JsonReaderEx implements Closeable {
/** The only non-execute prefix this parser permits */
private static final char[] NON_EXECUTE_PREFIX = ")]}'\n".toCharArray();
private static final long MIN_INCOMPLETE_INTEGER = Long.MIN_VALUE / 10;
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;
/** When this is returned, the string value is stored in peekedString. */
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;
/** When this is returned, the integer value is stored in peekedLong. */
private static final int PEEKED_LONG = 15;
private static final int PEEKED_NUMBER = 16;
private static final int PEEKED_EOF = 17;
/* 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;
private final CharSequence in;
/** True to accept non-spec compliant JSON */
private boolean lenient = false;
private int position;
private final int limit;
private int peeked = PEEKED_NONE;
/**
* A peeked value that was composed entirely of digits with an optional
* leading dash. Positive values may not have a leading 0.
*/
private long peekedLong;
/**
* The number of characters in a peeked number literal. Increment 'pos' by
* this after reading a number.
*/
private int peekedNumberLength;
/**
* 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.
*/
private String peekedString;
/*
* The nesting stack. Using a manual array rather than an ArrayList saves 20%.
*/
private int[] stack = new int[32];
private int stackSize = 0;
private StringBuilder builder;
/**
* Creates a new instance that reads a JSON-encoded stream from {@code in}.
*/
public JsonReaderEx(@NotNull CharSequence in) {
this(in, 0);
}
public JsonReaderEx(@NotNull CharSequence in, int start) {
this(in, start, JsonScope.EMPTY_DOCUMENT);
}
private JsonReaderEx(@NotNull CharSequence in, int start, int scope) {
this.in = in;
position = start;
limit = in.length();
stack[stackSize++] = scope;
}
private final static class JsonScope {
/**
* An array with no elements requires no separators or newlines before
* it is closed.
*/
static final int EMPTY_ARRAY = 1;
/**
* A array with at least one value requires a comma and newline before
* the next element.
*/
static final int NONEMPTY_ARRAY = 2;
/**
* An object with no name/value pairs requires no separators or newlines
* before it is closed.
*/
static final int EMPTY_OBJECT = 3;
/**
* An object whose most recent element is a key. The next element must
* be a value.
*/
static final int DANGLING_NAME = 4;
/**
* An object with at least one name/value pair requires a comma and
* newline before the next element.
*/
static final int NONEMPTY_OBJECT = 5;
/**
* No object or array has been started.
*/
static final int EMPTY_DOCUMENT = 6;
/**
* A document with at an array or object.
*/
static final int NONEMPTY_DOCUMENT = 7;
/**
* A document that's been closed and cannot be accessed.
*/
static final int CLOSED = 8;
}
@Nullable
public JsonReaderEx subReader() {
JsonToken nextToken = peek();
switch (nextToken) {
case BEGIN_ARRAY:
case BEGIN_OBJECT:
case STRING:
case NUMBER:
case BOOLEAN:
break;
case NULL:
// just return null
return null;
default:
throw createParseError("Cannot create sub reader, next token " + nextToken + " is not value");
}
JsonReaderEx subReader = new JsonReaderEx(in, position, stack[stackSize - 1]);
subReader.peeked = peeked;
subReader.peekedLong = peekedLong;
subReader.peekedNumberLength = peekedNumberLength;
subReader.peekedString = peekedString;
return subReader;
}
public final void setLenient(boolean lenient) {
this.lenient = lenient;
}
public final JsonReaderEx lenient(boolean lenient) {
this.lenient = lenient;
return this;
}
@SuppressWarnings("UnusedDeclaration")
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() {
int p = peeked;
if (p == PEEKED_NONE) {
p = doPeek();
}
if (p == PEEKED_BEGIN_ARRAY) {
push(JsonScope.EMPTY_ARRAY);
peeked = PEEKED_NONE;
}
else {
throw createParseError("Expected BEGIN_ARRAY but was " + peek());
}
}
/**
* Consumes the next token from the JSON stream and asserts that it is the
* end of the current array.
*/
public void endArray() {
int p = peeked;
if (p == PEEKED_NONE) {
p = doPeek();
}
if (p == PEEKED_END_ARRAY) {
stackSize--;
peeked = PEEKED_NONE;
}
else {
throw createParseError("Expected END_ARRAY but was " + peek());
}
}
/**
* Consumes the next token from the JSON stream and asserts that it is the
* beginning of a new object.
*/
public void beginObject() {
int p = peeked;
if (p == PEEKED_NONE) {
p = doPeek();
}
if (p == PEEKED_BEGIN_OBJECT) {
push(JsonScope.EMPTY_OBJECT);
peeked = PEEKED_NONE;
}
else {
throw createParseError("Expected BEGIN_OBJECT but was " + peek());
}
}
/**
* Consumes the next token from the JSON stream and asserts that it is the
* end of the current object.
*/
public void endObject() {
int p = peeked;
if (p == PEEKED_NONE) {
p = doPeek();
}
if (p == PEEKED_END_OBJECT) {
stackSize--;
peeked = PEEKED_NONE;
}
else {
throw new IllegalStateException("Expected END_OBJECT but was " + peek());
}
}
/**
* Returns true if the current array or object has another element.
*/
public boolean hasNext() {
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() {
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:
case PEEKED_BUFFERED:
return JsonToken.STRING;
case PEEKED_LONG:
case PEEKED_NUMBER:
return JsonToken.NUMBER;
case PEEKED_EOF:
return JsonToken.END_DOCUMENT;
default:
throw new AssertionError();
}
}
private int doPeek() {
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 createParseError("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 createParseError("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 createParseError("Expected name");
}
default:
checkLenient();
position--; // Don't consume the first character in an unquoted string.
if (isLiteral((char)c)) {
return peeked = PEEKED_UNQUOTED_NAME;
}
else {
throw createParseError("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 (position < limit && in.charAt(position) == '>') {
position++;
}
break;
default:
throw createParseError("Expected ':'");
}
}
else if (peekStack == JsonScope.EMPTY_DOCUMENT) {
if (lenient) {
consumeNonExecutePrefix();
}
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();
position--;
}
}
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();
position--;
return peeked = PEEKED_NULL;
}
else {
throw createParseError("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:
position--; // Don't consume the first character in a literal value.
}
if (stackSize == 1) {
checkLenient(); // Top-level value isn't an array or an object.
}
int result = peekKeyword();
if (result != PEEKED_NONE) {
return result;
}
result = peekNumber();
if (result != PEEKED_NONE) {
return result;
}
if (!isLiteral(in.charAt(position))) {
throw createParseError("Expected value");
}
checkLenient();
return peeked = PEEKED_UNQUOTED;
}
private int peekKeyword() {
// Figure out which keyword we're matching against by its first character.
char c = in.charAt(position);
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 (position + i >= limit) {
return PEEKED_NONE;
}
c = in.charAt(position + i);
if (c != keyword.charAt(i) && c != keywordUpper.charAt(i)) {
return PEEKED_NONE;
}
}
if ((position + length < limit) && isLiteral(in.charAt(position + 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.
position += length;
return peeked = peeking;
}
@SuppressWarnings("ConstantConditions")
private int peekNumber() {
// Like nextNonWhitespace, this uses locals 'p' and 'l' to save inner-loop field access.
CharSequence in = this.in;
int p = position;
int l = limit;
long value = 0; // Negative to accommodate Long.MIN_VALUE more easily.
boolean negative = false;
boolean fitsInLong = true;
int last = NUMBER_CHAR_NONE;
int i = 0;
charactersOfNumber:
for (; true; i++) {
if (p + i == l) {
if (i == limit) {
// 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;
}
p = position;
l = limit;
}
char c = in.charAt(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;
case '+':
if (last == NUMBER_CHAR_EXP_E) {
last = NUMBER_CHAR_EXP_SIGN;
continue;
}
return PEEKED_NONE;
case 'e':
case 'E':
if (last == NUMBER_CHAR_DIGIT || last == NUMBER_CHAR_FRACTION_DIGIT) {
last = NUMBER_CHAR_EXP_E;
continue;
}
return PEEKED_NONE;
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;
}
}
}
// 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;
position += i;
return peeked = PEEKED_LONG;
}
else if (last == NUMBER_CHAR_DIGIT || last == NUMBER_CHAR_FRACTION_DIGIT || last == NUMBER_CHAR_EXP_DIGIT) {
peekedNumberLength = i;
return peeked = PEEKED_NUMBER;
}
else {
return PEEKED_NONE;
}
}
/**
* Consumes the non-execute prefix if it exists.
*/
private void consumeNonExecutePrefix() {
// fast forward through the leading whitespace
nextNonWhitespace(true);
position--;
if (position + NON_EXECUTE_PREFIX.length > limit) {
return;
}
for (int i = 0; i < NON_EXECUTE_PREFIX.length; i++) {
if (in.charAt(position + i) != NON_EXECUTE_PREFIX[i]) {
return; // not a security token!
}
}
// we consumed a security token!
position += NON_EXECUTE_PREFIX.length;
}
private boolean isLiteral(char c) {
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 JsonToken#NAME property name}, and consumes it
*/
public String nextName() {
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 createParseError("Expected a name but was " + peek());
}
peeked = PEEKED_NONE;
return result;
}
public CharSequence nextNameAsCharSequence() {
// todo
return nextName();
}
@SuppressWarnings("UnusedDeclaration")
private static final class MyCharSequence implements CharSequence {
private final CharSequence in;
private final int offset;
private final int length;
public MyCharSequence(CharSequence in, int offset, int length) {
this.in = in;
this.offset = offset;
this.length = length;
}
@Override
public int length() {
return length;
}
@Override
public char charAt(int index) {
return in.charAt(offset + index);
}
@Override
public CharSequence subSequence(int start, int end) {
if ((end - start) > length) {
throw new StringIndexOutOfBoundsException(end);
}
return in.subSequence(offset + start, offset + end);
}
@NotNull
@Override
public String toString() {
return in.subSequence(offset, in.length()).toString();
}
@Override
public boolean equals(Object object) {
if (!(object instanceof CharSequence)) {
return false;
}
CharSequence o = (CharSequence)object;
if (o.length() != length) {
return false;
}
for (int i = 0; i < length; i++) {
if (o.charAt(i) != charAt(i)) {
return false;
}
}
return true;
}
}
public String nextAsString() {
return peek() == JsonToken.STRING ? nextString() : null;
}
public String nextString() {
return nextString(false);
}
/**
* 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(boolean anyPrimitiveAsString) {
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('"');
}
else if (p == PEEKED_BUFFERED) {
result = peekedString;
peekedString = null;
}
else if (p == PEEKED_LONG) {
result = Long.toString(peekedLong);
}
else if (p == PEEKED_NUMBER) {
int end = position + peekedNumberLength;
result = in.subSequence(position, end).toString();
position = end;
}
else if (anyPrimitiveAsString && p == PEEKED_TRUE) {
result = "true";
}
else if (anyPrimitiveAsString && p == PEEKED_FALSE) {
result = "false";
}
else if (anyPrimitiveAsString && p == PEEKED_NULL) {
result = "null";
}
else {
throw createParseError("Expected a string but was " + peek());
}
peeked = PEEKED_NONE;
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() {
int p = peeked;
if (p == PEEKED_NONE) {
p = doPeek();
}
if (p == PEEKED_TRUE) {
peeked = PEEKED_NONE;
return true;
}
else if (p == PEEKED_FALSE) {
peeked = PEEKED_NONE;
return false;
}
throw createParseError("Expected a boolean but was " + peek());
}
/**
* 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() {
int p = peeked;
if (p == PEEKED_NONE) {
p = doPeek();
}
if (p == PEEKED_NULL) {
peeked = PEEKED_NONE;
}
else {
throw createParseError("Expected null but was " + peek());
}
}
/**
* 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.
* @throws NumberFormatException if the next literal value cannot be parsed
* as a double, or is non-finite.
*/
public double nextDouble() {
int p = peeked;
if (p == PEEKED_NONE) {
p = doPeek();
}
if (p == PEEKED_LONG) {
peeked = PEEKED_NONE;
return (double)peekedLong;
}
if (p == PEEKED_NUMBER) {
int end = position + peekedNumberLength;
peekedString = in.subSequence(position, end).toString();
position = end;
}
else if (p == PEEKED_SINGLE_QUOTED || p == PEEKED_DOUBLE_QUOTED) {
peekedString = nextQuotedValue(p == PEEKED_SINGLE_QUOTED ? '\'' : '"');
}
else if (p == PEEKED_UNQUOTED) {
peekedString = nextUnquotedValue();
}
else if (p != PEEKED_BUFFERED) {
throw createParseError("Expected a double but was " + peek());
}
peeked = PEEKED_BUFFERED;
double result = Double.parseDouble(peekedString); // don't catch this NumberFormatException.
if (!lenient && (Double.isNaN(result) || Double.isInfinite(result))) {
throw createParseError("JSON forbids NaN and infinities: " + result);
}
peekedString = null;
peeked = PEEKED_NONE;
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() {
int p = peeked;
if (p == PEEKED_NONE) {
p = doPeek();
}
if (p == PEEKED_LONG) {
peeked = PEEKED_NONE;
return peekedLong;
}
if (p == PEEKED_NUMBER) {
int end = position + peekedNumberLength;
peekedString = in.subSequence(position, end).toString();
position = end;
}
else if (p == PEEKED_SINGLE_QUOTED || p == PEEKED_DOUBLE_QUOTED) {
peekedString = nextQuotedValue(p == PEEKED_SINGLE_QUOTED ? '\'' : '"');
try {
long result = Long.parseLong(peekedString);
peeked = PEEKED_NONE;
return result;
}
catch (NumberFormatException ignored) {
// Fall back to parse as a double below.
}
}
else {
throw createParseError("Expected a long but was " + peek());
}
peeked = PEEKED_BUFFERED;
double asDouble = Double.parseDouble(peekedString); // don't catch this NumberFormatException.
long result = (long)asDouble;
if (result != asDouble) { // Make sure no precision was lost casting to 'long'.
throw new NumberFormatException("Expected a long but was " + peekedString
+ " at line " + getLineNumber() + " column " + getColumnNumber());
}
peekedString = null;
peeked = PEEKED_NONE;
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) {
// Like nextNonWhitespace, this uses locals 'p' and 'l' to save inner-loop field access.
CharSequence in = this.in;
int p = position;
int l = limit;
// the index of the first character not yet appended to the builder
int start = p;
StringBuilder builder = null;
while (p < l) {
char c = in.charAt(p++);
if (c == quote) {
position = p;
if (builder == null) {
return in.subSequence(start, p - 1).toString();
}
else {
return builder.append(in, start, p - 1).toString();
}
}
else if (c == '\\') {
position = p;
if (builder == null) {
if (this.builder == null) {
this.builder = new StringBuilder((p - start) + 16);
}
else {
this.builder.setLength(0);
}
builder = this.builder;
}
builder.append(in, start, p - 1);
builder.append(readEscapeCharacter());
p = position;
l = limit;
start = p;
}
//else if (c == '\n') {
// //lineNumber++;
// //lineStart = p;
//}
}
position = p;
throw createParseError("Unterminated string");
}
/**
* Returns an unquoted value as a string.
*/
private String nextUnquotedValue() {
int i = position;
findNonLiteralCharacter:
for (; i < limit; i++) {
switch (in.charAt(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;
}
}
String result = in.subSequence(position, i).toString();
position = i;
return result;
}
private void skipQuotedValue(char quote) {
// Like nextNonWhitespace, this uses locals 'p' and 'l' to save inner-loop field access.
CharSequence in = this.in;
int p = position;
int l = limit;
// the index of the first character not yet appended to the builder
while (p < l) {
int c = in.charAt(p++);
if (c == quote) {
position = p;
return;
}
else if (c == '\\') {
position = p;
readEscapeCharacter();
p = position;
l = limit;
}
//else if (c == '\n') {
// //lineNumber++;
// //lineStart = p;
//}
}
position = p;
throw createParseError("Unterminated string");
}
private void skipUnquotedValue() {
int i = position;
for (; i < limit; i++) {
switch (in.charAt(i)) {
case '/':
case '\\':
case ';':
case '#':
case '=':
checkLenient(); // fall-through
case '{':
case '}':
case '[':
case ']':
case ':':
case ',':
case ' ':
case '\t':
case '\f':
case '\r':
case '\n':
position = i;
return;
}
}
position = i;
}
/**
* 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() {
int p = peeked;
if (p == PEEKED_NONE) {
p = doPeek();
}
int result;
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
+ " at line " + getLineNumber() + " column " + getColumnNumber());
}
peeked = PEEKED_NONE;
return result;
}
if (p == PEEKED_NUMBER) {
int end = position + peekedNumberLength;
peekedString = in.subSequence(position, end).toString();
position = end;
}
else if (p == PEEKED_SINGLE_QUOTED || p == PEEKED_DOUBLE_QUOTED) {
peekedString = nextQuotedValue(p == PEEKED_SINGLE_QUOTED ? '\'' : '"');
try {
result = Integer.parseInt(peekedString);
peeked = PEEKED_NONE;
return result;
}
catch (NumberFormatException ignored) {
// Fall back to parse as a double below.
}
}
else {
throw createParseError("Expected an int but was " + peek());
}
peeked = PEEKED_BUFFERED;
double asDouble = Double.parseDouble(peekedString); // don't catch this NumberFormatException.
result = (int)asDouble;
if (result != asDouble) { // Make sure no precision was lost casting to 'int'.
throw new NumberFormatException("Expected an int but was " + peekedString
+ " at line " + getLineNumber() + " column " + getColumnNumber());
}
peekedString = null;
peeked = PEEKED_NONE;
return result;
}
/**
* Closes this JSON reader and the underlying {@link java.io.Reader}.
*/
public void close() {
peeked = PEEKED_NONE;
stack[0] = JsonScope.CLOSED;
stackSize = 1;
}
/**
* 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() {
int count = 0;
do {
int p = peeked;
if (p == PEEKED_NONE) {
p = doPeek();
}
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) {
skipUnquotedValue();
}
else if (p == PEEKED_SINGLE_QUOTED || p == PEEKED_SINGLE_QUOTED_NAME) {
skipQuotedValue('\'');
}
else if (p == PEEKED_DOUBLE_QUOTED || p == PEEKED_DOUBLE_QUOTED_NAME) {
skipQuotedValue('"');
}
else if (p == PEEKED_NUMBER) {
position += peekedNumberLength;
}
peeked = PEEKED_NONE;
}
while (count != 0);
}
public void skipValues() {
while (hasNext()) {
skipValue();
}
}
private void push(int newTop) {
if (stackSize == stack.length) {
int[] newStack = new int[stackSize * 2];
System.arraycopy(stack, 0, newStack, 0, stackSize);
stack = newStack;
}
stack[stackSize++] = newTop;
}
private int getLineNumber() {
int result = 1;
for (int i = 0; i < position; i++) {
if (in.charAt(i) == '\n') {
result++;
}
}
return result;
}
private int getColumnNumber() {
int result = 1;
for (int i = 0; i < position; i++) {
if (in.charAt(i) == '\n') {
result = 1;
}
else {
result++;
}
}
return result;
}
/**
* 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 position}.
*/
private int nextNonWhitespace(boolean throwOnEof) {
/*
* 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.
*/
CharSequence in = this.in;
int p = position;
int l = limit;
while (true) {
if (p == l) {
position = p;
break;
}
int c = in.charAt(p++);
if (c == '\n') {
//lineNumber++;
//lineStart = p;
continue;
}
else if (c == ' ' || c == '\r' || c == '\t') {
continue;
}
if (c == '/') {
position = p;
if (p == l) {
position--; // push back '/' so it's still in the buffer when this method returns
boolean charsLoaded = (position + 3) < limit;
position++; // consume the '/' again
if (!charsLoaded) {
return c;
}
}
checkLenient();
char peek = in.charAt(position);
switch (peek) {
case '*':
// skip a /* c-style comment */
position++;
if (!skipTo("*/")) {
throw createParseError("Unterminated comment");
}
p = position + 2;
l = limit;
continue;
case '/':
// skip a // end-of-line comment
position++;
skipToEndOfLine();
p = position;
l = limit;
continue;
default:
return c;
}
}
else if (c == '#') {
position = 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 = position;
l = limit;
}
else {
position = p;
return c;
}
}
if (throwOnEof) {
throw createParseError("End of input");
}
else {
return -1;
}
}
private void checkLenient() {
if (!lenient) {
throw createParseError("Use JsonReaderEx.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() {
while (position < limit) {
char c = in.charAt(position++);
if (c == '\n' || c == '\r') {
break;
}
}
}
private boolean skipTo(String toFind) {
outer:
for (; position + toFind.length() <= limit; position++) {
if (in.charAt(position) == '\n') {
//lineNumber++;
//lineStart = pos + 1;
continue;
}
for (int c = 0; c < toFind.length(); c++) {
if (in.charAt(position + c) != toFind.charAt(c)) {
continue outer;
}
}
return true;
}
return false;
}
@Override
public String toString() {
return getClass().getSimpleName() + " at line " + getLineNumber() + " column " + getColumnNumber();
}
/**
* 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() {
if (position == limit) {
throw createParseError("Unterminated escape sequence");
}
char escaped = in.charAt(position++);
switch (escaped) {
case 'u':
if (position + 4 > limit) {
throw createParseError("Unterminated escape sequence");
}
// Equivalent to Integer.parseInt(stringPool.get(buffer, pos, 4), 16);
char result = 0;
for (int i = position, end = i + 4; i < end; i++) {
char c = in.charAt(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" + in.subSequence(position, position + 4));
}
}
position += 4;
return result;
case 't':
return '\t';
case 'b':
return '\b';
case 'n':
return '\n';
case 'r':
return '\r';
case 'f':
return '\f';
case '\n':
//lineNumber++;
//lineStart = pos;
// fall-through
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 JsonParseException createParseError(String message) {
throw new JsonParseException(message + " at line " + getLineNumber() + " column " + getColumnNumber());
}
}