/* * Copyright 2013-2015 Sergey Ignatov, Alexander Zolotov, Florin Patan * * 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.goide.dlv; import com.google.gson.JsonParseException; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonToken; import com.intellij.util.text.CharSequenceReader; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.Closeable; import java.util.Arrays; 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; @NotNull 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. */ @Nullable private String peekedString; /* * The nesting stack. Using a manual array rather than an ArrayList saves 20%. */ private int[] stack; 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, new int[32]); stack[stackSize++] = JsonScope.EMPTY_DOCUMENT; } private JsonReaderEx(@NotNull CharSequence in, int start, @NotNull int[] stack) { this.in = in; position = start; limit = in.length(); this.stack = stack; } 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, Arrays.copyOf(stack, stack.length)); subReader.stackSize = stackSize; subReader.peeked = peeked; subReader.peekedLong = peekedLong; subReader.peekedNumberLength = peekedNumberLength; subReader.peekedString = peekedString; return subReader; } @NotNull public JsonReader asGson() { JsonToken nextToken = peek(); switch (nextToken) { case BEGIN_ARRAY: case BEGIN_OBJECT: case STRING: case NUMBER: case BOOLEAN: break; default: throw createParseError("Cannot create sub reader, next token " + nextToken + " is not value"); } CharSequence sequence = position > 0 ? in.subSequence(position - 1, in.length()) : in; return new JsonReader(new CharSequenceReader(sequence)); } /** * Consumes the next token from the JSON stream and asserts that it is the * beginning of a new object. */ @NotNull public JsonReaderEx 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()); } return this; } /** * 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. */ @NotNull 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 */ @Nullable public String nextName() { String result = nextNameOrNull(); if (result == null) { throw createParseError("Expected a name but was " + peek()); } return result; } @Nullable public String nextNameOrNull() { 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 { if (p != PEEKED_END_OBJECT && p != PEEKED_END_ARRAY) { throw createParseError("Expected a name but was " + peek()); } return null; } peeked = PEEKED_NONE; return result; } @Nullable 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. */ @Nullable 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; } /** * 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 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. */ @NotNull 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. */ @NotNull 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}. */ @Override 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 || 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); } 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' || 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(@NotNull 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; } @NotNull @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. */ @NotNull private JsonParseException createParseError(String message) { throw new JsonParseException(message + " at line " + getLineNumber() + " column " + getColumnNumber()); } }