/* * 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.apollographql.apollo.internal.json; import com.apollographql.apollo.json.JsonDataException; import com.apollographql.apollo.json.JsonEncodingException; import java.io.EOFException; import java.io.IOException; import okio.Buffer; import okio.BufferedSource; import okio.ByteString; /** TODO add Modifications copyright **/ public final class BufferedSourceJsonReader extends JsonReader { private static final long MIN_INCOMPLETE_INTEGER = Long.MIN_VALUE / 10; private static final ByteString SINGLE_QUOTE_OR_SLASH = ByteString.encodeUtf8("'\\"); private static final ByteString DOUBLE_QUOTE_OR_SLASH = ByteString.encodeUtf8("\"\\"); private static final ByteString UNQUOTED_STRING_TERMINALS = ByteString.encodeUtf8("{}[]:, \n\t\r\f/\\;#="); private static final ByteString LINEFEED_OR_CARRIAGE_RETURN = ByteString.encodeUtf8("\n\r"); 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; /** True to accept non-spec compliant JSON */ private boolean lenient = false; /** True to throw a {@link JsonDataException} on any attempt to call {@link #skipValue()}. */ private boolean failOnUnknown = false; /** The input JSON. */ private final BufferedSource source; private final Buffer buffer; 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%. This stack permits // up to 32 levels of nesting including the top-level document. Deeper nesting is prone to trigger // StackOverflowErrors. private final int[] stack = new int[32]; private int stackSize = 0; { stack[stackSize++] = JsonScope.EMPTY_DOCUMENT; } private final String[] pathNames = new String[32]; private final int[] pathIndices = new int[32]; public BufferedSourceJsonReader(BufferedSource source) { if (source == null) { throw new NullPointerException("source == null"); } this.source = source; this.buffer = source.buffer(); } @Override public void setLenient(boolean lenient) { this.lenient = lenient; } @Override public boolean isLenient() { return lenient; } @Override public void setFailOnUnknown(boolean failOnUnknown) { this.failOnUnknown = failOnUnknown; } @Override public boolean failOnUnknown() { return failOnUnknown; } @Override public void beginArray() throws IOException { int p = peeked; if (p == PEEKED_NONE) { p = doPeek(); } if (p == PEEKED_BEGIN_ARRAY) { push(JsonScope.EMPTY_ARRAY); pathIndices[stackSize - 1] = 0; peeked = PEEKED_NONE; } else { throw new JsonDataException("Expected BEGIN_ARRAY but was " + peek() + " at path " + getPath()); } } @Override public void endArray() throws IOException { int p = peeked; if (p == PEEKED_NONE) { p = doPeek(); } if (p == PEEKED_END_ARRAY) { stackSize--; pathIndices[stackSize - 1]++; peeked = PEEKED_NONE; } else { throw new JsonDataException("Expected END_ARRAY but was " + peek() + " at path " + getPath()); } } @Override 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 JsonDataException("Expected BEGIN_OBJECT but was " + peek() + " at path " + getPath()); } } @Override public void endObject() throws IOException { int p = peeked; if (p == PEEKED_NONE) { p = doPeek(); } if (p == PEEKED_END_OBJECT) { stackSize--; pathNames[stackSize] = null; // Free the last path name so that it can be garbage collected! pathIndices[stackSize - 1]++; peeked = PEEKED_NONE; } else { throw new JsonDataException("Expected END_OBJECT but was " + peek() + " at path " + getPath()); } } @Override public boolean hasNext() throws IOException { int p = peeked; if (p == PEEKED_NONE) { p = doPeek(); } return p != PEEKED_END_OBJECT && p != PEEKED_END_ARRAY; } @Override public Token peek() throws IOException { int p = peeked; if (p == PEEKED_NONE) { p = doPeek(); } switch (p) { case PEEKED_BEGIN_OBJECT: return Token.BEGIN_OBJECT; case PEEKED_END_OBJECT: return Token.END_OBJECT; case PEEKED_BEGIN_ARRAY: return Token.BEGIN_ARRAY; case PEEKED_END_ARRAY: return Token.END_ARRAY; case PEEKED_SINGLE_QUOTED_NAME: case PEEKED_DOUBLE_QUOTED_NAME: case PEEKED_UNQUOTED_NAME: return Token.NAME; case PEEKED_TRUE: case PEEKED_FALSE: return Token.BOOLEAN; case PEEKED_NULL: return Token.NULL; case PEEKED_SINGLE_QUOTED: case PEEKED_DOUBLE_QUOTED: case PEEKED_UNQUOTED: case PEEKED_BUFFERED: return Token.STRING; case PEEKED_LONG: case PEEKED_NUMBER: return Token.NUMBER; case PEEKED_EOF: return Token.END_DOCUMENT; default: throw new AssertionError(); } } private int doPeek() throws IOException { 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); buffer.readByte(); // consume ']' or ','. 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); buffer.readByte(); // Consume '}' or ','. 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 '"': buffer.readByte(); // consume the '\"'. return peeked = PEEKED_DOUBLE_QUOTED_NAME; case '\'': buffer.readByte(); // consume the '\''. checkLenient(); return peeked = PEEKED_SINGLE_QUOTED_NAME; case '}': if (peekStack != JsonScope.NONEMPTY_OBJECT) { buffer.readByte(); // consume the '}'. return peeked = PEEKED_END_OBJECT; } else { throw syntaxError("Expected name"); } default: checkLenient(); 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); buffer.readByte(); // Consume ':'. switch (c) { case ':': break; case '=': checkLenient(); if (source.request(1) && buffer.getByte(0) == '>') { buffer.readByte(); // Consume '>'. } break; default: throw syntaxError("Expected ':'"); } } else if (peekStack == JsonScope.EMPTY_DOCUMENT) { 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(); } } else if (peekStack == JsonScope.CLOSED) { throw new IllegalStateException("JsonReader is closed"); } int c = nextNonWhitespace(true); switch (c) { case ']': if (peekStack == JsonScope.EMPTY_ARRAY) { buffer.readByte(); // Consume ']'. 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(); return peeked = PEEKED_NULL; } else { throw syntaxError("Unexpected value"); } case '\'': checkLenient(); buffer.readByte(); // Consume '\''. return peeked = PEEKED_SINGLE_QUOTED; case '"': buffer.readByte(); // Consume '\"'. return peeked = PEEKED_DOUBLE_QUOTED; case '[': buffer.readByte(); // Consume '['. return peeked = PEEKED_BEGIN_ARRAY; case '{': buffer.readByte(); // Consume '{'. return peeked = PEEKED_BEGIN_OBJECT; default: } int result = peekKeyword(); if (result != PEEKED_NONE) { return result; } result = peekNumber(); if (result != PEEKED_NONE) { return result; } if (!isLiteral(buffer.getByte(0))) { 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. byte c = buffer.getByte(0); 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 (!source.request(i + 1)) { return PEEKED_NONE; } c = buffer.getByte(i); if (c != keyword.charAt(i) && c != keywordUpper.charAt(i)) { return PEEKED_NONE; } } if (source.request(length + 1) && isLiteral(buffer.getByte(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. buffer.skip(length); return peeked = peeking; } private int peekNumber() throws IOException { 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 (!source.request(i + 1)) { break; } byte c = buffer.getByte(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; buffer.skip(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; } } private boolean isLiteral(int 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; } } @Override 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_DOUBLE_QUOTED_NAME) { result = nextQuotedValue(DOUBLE_QUOTE_OR_SLASH); } else if (p == PEEKED_SINGLE_QUOTED_NAME) { result = nextQuotedValue(SINGLE_QUOTE_OR_SLASH); } else { throw new JsonDataException("Expected a name but was " + peek() + " at path " + getPath()); } peeked = PEEKED_NONE; pathNames[stackSize - 1] = result; return result; } @Override 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_DOUBLE_QUOTED) { result = nextQuotedValue(DOUBLE_QUOTE_OR_SLASH); } else if (p == PEEKED_SINGLE_QUOTED) { result = nextQuotedValue(SINGLE_QUOTE_OR_SLASH); } else if (p == PEEKED_BUFFERED) { result = peekedString; peekedString = null; } else if (p == PEEKED_LONG) { result = Long.toString(peekedLong); } else if (p == PEEKED_NUMBER) { result = buffer.readUtf8(peekedNumberLength); } else { throw new JsonDataException("Expected a string but was " + peek() + " at path " + getPath()); } peeked = PEEKED_NONE; pathIndices[stackSize - 1]++; return result; } @Override public boolean nextBoolean() throws IOException { int p = peeked; if (p == PEEKED_NONE) { p = doPeek(); } if (p == PEEKED_TRUE) { peeked = PEEKED_NONE; pathIndices[stackSize - 1]++; return true; } else if (p == PEEKED_FALSE) { peeked = PEEKED_NONE; pathIndices[stackSize - 1]++; return false; } throw new JsonDataException("Expected a boolean but was " + peek() + " at path " + getPath()); } @Override public <T> T nextNull() throws IOException { int p = peeked; if (p == PEEKED_NONE) { p = doPeek(); } if (p == PEEKED_NULL) { peeked = PEEKED_NONE; pathIndices[stackSize - 1]++; return null; } else { throw new JsonDataException("Expected null but was " + peek() + " at path " + getPath()); } } @Override public double nextDouble() throws IOException { int p = peeked; if (p == PEEKED_NONE) { p = doPeek(); } if (p == PEEKED_LONG) { peeked = PEEKED_NONE; pathIndices[stackSize - 1]++; return (double) peekedLong; } if (p == PEEKED_NUMBER) { peekedString = buffer.readUtf8(peekedNumberLength); } else if (p == PEEKED_DOUBLE_QUOTED) { peekedString = nextQuotedValue(DOUBLE_QUOTE_OR_SLASH); } else if (p == PEEKED_SINGLE_QUOTED) { peekedString = nextQuotedValue(SINGLE_QUOTE_OR_SLASH); } else if (p == PEEKED_UNQUOTED) { peekedString = nextUnquotedValue(); } else if (p != PEEKED_BUFFERED) { throw new JsonDataException("Expected a double but was " + peek() + " at path " + getPath()); } peeked = PEEKED_BUFFERED; double result; try { result = Double.parseDouble(peekedString); } catch (NumberFormatException e) { throw new JsonDataException("Expected a double but was " + peekedString + " at path " + getPath()); } if (!lenient && (Double.isNaN(result) || Double.isInfinite(result))) { throw new JsonEncodingException("JSON forbids NaN and infinities: " + result + " at path " + getPath()); } peekedString = null; peeked = PEEKED_NONE; pathIndices[stackSize - 1]++; return result; } @Override public long nextLong() throws IOException { int p = peeked; if (p == PEEKED_NONE) { p = doPeek(); } if (p == PEEKED_LONG) { peeked = PEEKED_NONE; pathIndices[stackSize - 1]++; return peekedLong; } if (p == PEEKED_NUMBER) { peekedString = buffer.readUtf8(peekedNumberLength); } else if (p == PEEKED_DOUBLE_QUOTED || p == PEEKED_SINGLE_QUOTED) { peekedString = p == PEEKED_DOUBLE_QUOTED ? nextQuotedValue(DOUBLE_QUOTE_OR_SLASH) : nextQuotedValue(SINGLE_QUOTE_OR_SLASH); try { long result = Long.parseLong(peekedString); peeked = PEEKED_NONE; pathIndices[stackSize - 1]++; return result; } catch (NumberFormatException ignored) { // Fall back to parse as a double below. } } else if (p != PEEKED_BUFFERED) { throw new JsonDataException("Expected a long but was " + peek() + " at path " + getPath()); } peeked = PEEKED_BUFFERED; double asDouble; try { asDouble = Double.parseDouble(peekedString); } catch (NumberFormatException e) { throw new JsonDataException("Expected a long but was " + peekedString + " at path " + getPath()); } long result = (long) asDouble; if (result != asDouble) { // Make sure no precision was lost casting to 'long'. throw new JsonDataException("Expected a long but was " + peekedString + " at path " + getPath()); } peekedString = null; peeked = PEEKED_NONE; pathIndices[stackSize - 1]++; 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. * * @throws IOException if any unicode escape sequences are malformed. */ private String nextQuotedValue(ByteString runTerminator) throws IOException { StringBuilder builder = null; while (true) { long index = source.indexOfElement(runTerminator); if (index == -1L) throw syntaxError("Unterminated string"); // If we've got an escape character, we're going to need a string builder. if (buffer.getByte(index) == '\\') { if (builder == null) builder = new StringBuilder(); builder.append(buffer.readUtf8(index)); buffer.readByte(); // '\' builder.append(readEscapeCharacter()); continue; } // If it isn't the escape character, it's the quote. Return the string. if (builder == null) { String result = buffer.readUtf8(index); buffer.readByte(); // Consume the quote character. return result; } else { builder.append(buffer.readUtf8(index)); buffer.readByte(); // Consume the quote character. return builder.toString(); } } } /** Returns an unquoted value as a string. */ private String nextUnquotedValue() throws IOException { long i = source.indexOfElement(UNQUOTED_STRING_TERMINALS); return i != -1 ? buffer.readUtf8(i) : buffer.readUtf8(); } private void skipQuotedValue(ByteString runTerminator) throws IOException { while (true) { long index = source.indexOfElement(runTerminator); if (index == -1L) throw syntaxError("Unterminated string"); if (buffer.getByte(index) == '\\') { buffer.skip(index + 1); readEscapeCharacter(); } else { buffer.skip(index + 1); return; } } } private void skipUnquotedValue() throws IOException { long i = source.indexOfElement(UNQUOTED_STRING_TERMINALS); buffer.skip(i != -1L ? i : buffer.size()); } @Override public int nextInt() throws IOException { 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 JsonDataException("Expected an int but was " + peekedLong + " at path " + getPath()); } peeked = PEEKED_NONE; pathIndices[stackSize - 1]++; return result; } if (p == PEEKED_NUMBER) { peekedString = buffer.readUtf8(peekedNumberLength); } else if (p == PEEKED_DOUBLE_QUOTED || p == PEEKED_SINGLE_QUOTED) { peekedString = p == PEEKED_DOUBLE_QUOTED ? nextQuotedValue(DOUBLE_QUOTE_OR_SLASH) : nextQuotedValue(SINGLE_QUOTE_OR_SLASH); try { result = Integer.parseInt(peekedString); peeked = PEEKED_NONE; pathIndices[stackSize - 1]++; return result; } catch (NumberFormatException ignored) { // Fall back to parse as a double below. } } else if (p != PEEKED_BUFFERED) { throw new JsonDataException("Expected an int but was " + peek() + " at path " + getPath()); } peeked = PEEKED_BUFFERED; double asDouble; try { asDouble = Double.parseDouble(peekedString); } catch (NumberFormatException e) { throw new JsonDataException("Expected an int but was " + peekedString + " at path " + getPath()); } result = (int) asDouble; if (result != asDouble) { // Make sure no precision was lost casting to 'int'. throw new JsonDataException("Expected an int but was " + peekedString + " at path " + getPath()); } peekedString = null; peeked = PEEKED_NONE; pathIndices[stackSize - 1]++; return result; } @Override public void close() throws IOException { peeked = PEEKED_NONE; stack[0] = JsonScope.CLOSED; stackSize = 1; buffer.clear(); source.close(); } @Override public void skipValue() throws IOException { if (failOnUnknown) { throw new JsonDataException("Cannot skip unexpected " + peek() + " at " + getPath()); } 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_DOUBLE_QUOTED || p == PEEKED_DOUBLE_QUOTED_NAME) { skipQuotedValue(DOUBLE_QUOTE_OR_SLASH); } else if (p == PEEKED_SINGLE_QUOTED || p == PEEKED_SINGLE_QUOTED_NAME) { skipQuotedValue(SINGLE_QUOTE_OR_SLASH); } else if (p == PEEKED_NUMBER) { buffer.skip(peekedNumberLength); } peeked = PEEKED_NONE; } while (count != 0); pathIndices[stackSize - 1]++; pathNames[stackSize - 1] = "null"; } private void push(int newTop) { if (stackSize == stack.length) { throw new JsonDataException("Nesting too deep at " + getPath()); } stack[stackSize++] = newTop; } /** * 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. */ int p = 0; while (source.request(p + 1)) { int c = buffer.getByte(p++); if (c == '\n' || c == ' ' || c == '\r' || c == '\t') { continue; } buffer.skip(p - 1); if (c == '/') { if (!source.request(2)) { return c; } checkLenient(); byte peek = buffer.getByte(1); switch (peek) { case '*': // skip a /* c-style comment */ buffer.readByte(); // '/' buffer.readByte(); // '*' if (!skipTo("*/")) { throw syntaxError("Unterminated comment"); } buffer.readByte(); // '*' buffer.readByte(); // '/' p = 0; continue; case '/': // skip a // end-of-line comment buffer.readByte(); // '/' buffer.readByte(); // '/' skipToEndOfLine(); p = 0; continue; default: return c; } } else if (c == '#') { // Skip a # hash end-of-line comment. The JSON RFC doesn't specify this behaviour, but it's // required to parse existing documents. checkLenient(); skipToEndOfLine(); p = 0; } else { return c; } } if (throwOnEof) { throw new EOFException("End of input"); } 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 { long index = source.indexOfElement(LINEFEED_OR_CARRIAGE_RETURN); buffer.skip(index != -1 ? index + 1 : buffer.size()); } /** * @param toFind a string to search for. Must not contain a newline. */ private boolean skipTo(String toFind) throws IOException { outer: for (; source.request(toFind.length());) { for (int c = 0; c < toFind.length(); c++) { if (buffer.getByte(c) != toFind.charAt(c)) { buffer.readByte(); continue outer; } } return true; } return false; } @Override public String toString() { return "JsonReader(" + source + ")"; } @Override public String getPath() { return JsonScope.getPath(stackSize, stack, pathNames, pathIndices); } /** * 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 IOException if any unicode escape sequences are malformed. */ private char readEscapeCharacter() throws IOException { if (!source.request(1)) { throw syntaxError("Unterminated escape sequence"); } byte escaped = buffer.readByte(); switch (escaped) { case 'u': if (!source.request(4)) { throw new EOFException("Unterminated escape sequence at path " + getPath()); } // Equivalent to Integer.parseInt(stringPool.get(buffer, pos, 4), 16); char result = 0; for (int i = 0, end = i + 4; i < end; i++) { byte c = buffer.getByte(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 syntaxError("\\u" + buffer.readUtf8(4)); } } buffer.skip(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': case '\'': case '"': case '\\': case '/': return (char) escaped; default: if (!lenient) throw syntaxError("Invalid escape sequence: \\" + (char) escaped); return (char) escaped; } } /** * Throws a new IO exception with the given message and a context snippet * with this reader's content. */ private JsonEncodingException syntaxError(String message) throws JsonEncodingException { throw new JsonEncodingException(message + " at path " + getPath()); } @Override void promoteNameToValue() throws IOException { if (hasNext()) { peekedString = nextName(); peeked = PEEKED_BUFFERED; } } }