From 1071cec7d1499f01e3d3e4878c849436132022db Mon Sep 17 00:00:00 2001 From: Jake Wharton Date: Wed, 20 Jan 2016 23:19:37 -0500 Subject: [PATCH] Break apart Okio-based JSON reader and writer. --- .../moshi/BufferedSinkJsonWriter.java | 441 +++++++ .../moshi/BufferedSourceJsonReader.java | 1078 +++++++++++++++++ .../java/com/squareup/moshi/JsonReader.java | 1068 +--------------- .../java/com/squareup/moshi/JsonWriter.java | 428 +------ ...t.java => BufferedSinkJsonWriterTest.java} | 2 +- ...java => BufferedSourceJsonReaderTest.java} | 2 +- 6 files changed, 1571 insertions(+), 1448 deletions(-) create mode 100644 moshi/src/main/java/com/squareup/moshi/BufferedSinkJsonWriter.java create mode 100644 moshi/src/main/java/com/squareup/moshi/BufferedSourceJsonReader.java rename moshi/src/test/java/com/squareup/moshi/{JsonWriterTest.java => BufferedSinkJsonWriterTest.java} (99%) rename moshi/src/test/java/com/squareup/moshi/{JsonReaderTest.java => BufferedSourceJsonReaderTest.java} (99%) diff --git a/moshi/src/main/java/com/squareup/moshi/BufferedSinkJsonWriter.java b/moshi/src/main/java/com/squareup/moshi/BufferedSinkJsonWriter.java new file mode 100644 index 0000000..133f7fc --- /dev/null +++ b/moshi/src/main/java/com/squareup/moshi/BufferedSinkJsonWriter.java @@ -0,0 +1,441 @@ +/* + * 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.squareup.moshi; + +import java.io.IOException; +import okio.BufferedSink; +import okio.Sink; + +import static com.squareup.moshi.JsonScope.DANGLING_NAME; +import static com.squareup.moshi.JsonScope.EMPTY_ARRAY; +import static com.squareup.moshi.JsonScope.EMPTY_DOCUMENT; +import static com.squareup.moshi.JsonScope.EMPTY_OBJECT; +import static com.squareup.moshi.JsonScope.NONEMPTY_ARRAY; +import static com.squareup.moshi.JsonScope.NONEMPTY_DOCUMENT; +import static com.squareup.moshi.JsonScope.NONEMPTY_OBJECT; + +final class BufferedSinkJsonWriter extends JsonWriter { + + /* + * From RFC 7159, "All Unicode characters may be placed within the + * quotation marks except for the characters that must be escaped: + * quotation mark, reverse solidus, and the control characters + * (U+0000 through U+001F)." + * + * We also escape '\u2028' and '\u2029', which JavaScript interprets as + * newline characters. This prevents eval() from failing with a syntax + * error. http://code.google.com/p/google-gson/issues/detail?id=341 + */ + private static final String[] REPLACEMENT_CHARS; + static { + REPLACEMENT_CHARS = new String[128]; + for (int i = 0; i <= 0x1f; i++) { + REPLACEMENT_CHARS[i] = String.format("\\u%04x", (int) i); + } + REPLACEMENT_CHARS['"'] = "\\\""; + REPLACEMENT_CHARS['\\'] = "\\\\"; + REPLACEMENT_CHARS['\t'] = "\\t"; + REPLACEMENT_CHARS['\b'] = "\\b"; + REPLACEMENT_CHARS['\n'] = "\\n"; + REPLACEMENT_CHARS['\r'] = "\\r"; + REPLACEMENT_CHARS['\f'] = "\\f"; + } + + /** The output data, containing at most one top-level array or object. */ + private final BufferedSink sink; + + private int[] stack = new int[32]; + private int stackSize = 0; + { + push(EMPTY_DOCUMENT); + } + + private String[] pathNames = new String[32]; + private int[] pathIndices = new int[32]; + + /** + * A string containing a full set of spaces for a single level of + * indentation, or null for no pretty printing. + */ + private String indent; + + /** + * The name/value separator; either ":" or ": ". + */ + private String separator = ":"; + + private boolean lenient; + + private String deferredName; + + private boolean serializeNulls; + + private boolean promoteNameToValue; + + BufferedSinkJsonWriter(BufferedSink sink) { + if (sink == null) { + throw new NullPointerException("sink == null"); + } + this.sink = sink; + } + + @Override public final void setIndent(String indent) { + if (indent.length() == 0) { + this.indent = null; + this.separator = ":"; + } else { + this.indent = indent; + this.separator = ": "; + } + } + + @Override public final void setLenient(boolean lenient) { + this.lenient = lenient; + } + + @Override public boolean isLenient() { + return lenient; + } + + @Override public final void setSerializeNulls(boolean serializeNulls) { + this.serializeNulls = serializeNulls; + } + + @Override public final boolean getSerializeNulls() { + return serializeNulls; + } + + @Override public JsonWriter beginArray() throws IOException { + writeDeferredName(); + return open(EMPTY_ARRAY, "["); + } + + @Override public JsonWriter endArray() throws IOException { + return close(EMPTY_ARRAY, NONEMPTY_ARRAY, "]"); + } + + @Override public JsonWriter beginObject() throws IOException { + writeDeferredName(); + return open(EMPTY_OBJECT, "{"); + } + + @Override public JsonWriter endObject() throws IOException { + promoteNameToValue = false; + return close(EMPTY_OBJECT, NONEMPTY_OBJECT, "}"); + } + + /** + * Enters a new scope by appending any necessary whitespace and the given + * bracket. + */ + private JsonWriter open(int empty, String openBracket) throws IOException { + beforeValue(); + pathIndices[stackSize] = 0; + push(empty); + sink.writeUtf8(openBracket); + return this; + } + + /** + * Closes the current scope by appending any necessary whitespace and the + * given bracket. + */ + private JsonWriter close(int empty, int nonempty, String closeBracket) + throws IOException { + int context = peek(); + if (context != nonempty && context != empty) { + throw new IllegalStateException("Nesting problem."); + } + if (deferredName != null) { + throw new IllegalStateException("Dangling name: " + deferredName); + } + + stackSize--; + pathNames[stackSize] = null; // Free the last path name so that it can be garbage collected! + pathIndices[stackSize - 1]++; + if (context == nonempty) { + newline(); + } + sink.writeUtf8(closeBracket); + return this; + } + + 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; + } + + /** + * Returns the value on the top of the stack. + */ + private int peek() { + if (stackSize == 0) { + throw new IllegalStateException("JsonWriter is closed."); + } + return stack[stackSize - 1]; + } + + /** + * Replace the value on the top of the stack with the given value. + */ + private void replaceTop(int topOfStack) { + stack[stackSize - 1] = topOfStack; + } + + @Override public JsonWriter name(String name) throws IOException { + if (name == null) { + throw new NullPointerException("name == null"); + } + if (stackSize == 0) { + throw new IllegalStateException("JsonWriter is closed."); + } + if (deferredName != null) { + throw new IllegalStateException(); + } + deferredName = name; + pathNames[stackSize - 1] = name; + promoteNameToValue = false; + return this; + } + + private void writeDeferredName() throws IOException { + if (deferredName != null) { + beforeName(); + string(deferredName); + deferredName = null; + } + } + + @Override public JsonWriter value(String value) throws IOException { + if (value == null) { + return nullValue(); + } + if (promoteNameToValue) { + return name(value); + } + writeDeferredName(); + beforeValue(); + string(value); + pathIndices[stackSize - 1]++; + return this; + } + + @Override public JsonWriter nullValue() throws IOException { + if (deferredName != null) { + if (serializeNulls) { + writeDeferredName(); + } else { + deferredName = null; + return this; // skip the name and the value + } + } + beforeValue(); + sink.writeUtf8("null"); + pathIndices[stackSize - 1]++; + return this; + } + + @Override public JsonWriter value(boolean value) throws IOException { + writeDeferredName(); + beforeValue(); + sink.writeUtf8(value ? "true" : "false"); + pathIndices[stackSize - 1]++; + return this; + } + + @Override public JsonWriter value(double value) throws IOException { + if (Double.isNaN(value) || Double.isInfinite(value)) { + throw new IllegalArgumentException("Numeric values must be finite, but was " + value); + } + if (promoteNameToValue) { + return name(Double.toString(value)); + } + writeDeferredName(); + beforeValue(); + sink.writeUtf8(Double.toString(value)); + pathIndices[stackSize - 1]++; + return this; + } + + @Override public JsonWriter value(long value) throws IOException { + if (promoteNameToValue) { + return name(Long.toString(value)); + } + writeDeferredName(); + beforeValue(); + sink.writeUtf8(Long.toString(value)); + pathIndices[stackSize - 1]++; + return this; + } + + @Override public JsonWriter value(Number value) throws IOException { + if (value == null) { + return nullValue(); + } + + String string = value.toString(); + if (!lenient + && (string.equals("-Infinity") || string.equals("Infinity") || string.equals("NaN"))) { + throw new IllegalArgumentException("Numeric values must be finite, but was " + value); + } + if (promoteNameToValue) { + return name(string); + } + writeDeferredName(); + beforeValue(); + sink.writeUtf8(string); + pathIndices[stackSize - 1]++; + return this; + } + + /** + * Ensures all buffered data is written to the underlying {@link Sink} + * and flushes that writer. + */ + public void flush() throws IOException { + if (stackSize == 0) { + throw new IllegalStateException("JsonWriter is closed."); + } + sink.flush(); + } + + /** + * Flushes and closes this writer and the underlying {@link Sink}. + * + * @throws JsonDataException if the JSON document is incomplete. + */ + public void close() throws IOException { + sink.close(); + + int size = stackSize; + if (size > 1 || size == 1 && stack[size - 1] != NONEMPTY_DOCUMENT) { + throw new IOException("Incomplete document"); + } + stackSize = 0; + } + + private void string(String value) throws IOException { + String[] replacements = REPLACEMENT_CHARS; + sink.writeByte('"'); + int last = 0; + int length = value.length(); + for (int i = 0; i < length; i++) { + char c = value.charAt(i); + String replacement; + if (c < 128) { + replacement = replacements[c]; + if (replacement == null) { + continue; + } + } else if (c == '\u2028') { + replacement = "\\u2028"; + } else if (c == '\u2029') { + replacement = "\\u2029"; + } else { + continue; + } + if (last < i) { + sink.writeUtf8(value, last, i); + } + sink.writeUtf8(replacement); + last = i + 1; + } + if (last < length) { + sink.writeUtf8(value, last, length); + } + sink.writeByte('"'); + } + + private void newline() throws IOException { + if (indent == null) { + return; + } + + sink.writeByte('\n'); + for (int i = 1, size = stackSize; i < size; i++) { + sink.writeUtf8(indent); + } + } + + /** + * Inserts any necessary separators and whitespace before a name. Also + * adjusts the stack to expect the name's value. + */ + private void beforeName() throws IOException { + int context = peek(); + if (context == NONEMPTY_OBJECT) { // first in object + sink.writeByte(','); + } else if (context != EMPTY_OBJECT) { // not in an object! + throw new IllegalStateException("Nesting problem."); + } + newline(); + replaceTop(DANGLING_NAME); + } + + /** + * Inserts any necessary separators and whitespace before a literal value, + * inline array, or inline object. Also adjusts the stack to expect either a + * closing bracket or another element. + */ + @SuppressWarnings("fallthrough") + private void beforeValue() throws IOException { + switch (peek()) { + case NONEMPTY_DOCUMENT: + if (!lenient) { + throw new IllegalStateException( + "JSON must have only one top-level value."); + } + // fall-through + case EMPTY_DOCUMENT: // first in document + replaceTop(NONEMPTY_DOCUMENT); + break; + + case EMPTY_ARRAY: // first in array + replaceTop(NONEMPTY_ARRAY); + newline(); + break; + + case NONEMPTY_ARRAY: // another in array + sink.writeByte(','); + newline(); + break; + + case DANGLING_NAME: // value for name + sink.writeUtf8(separator); + replaceTop(NONEMPTY_OBJECT); + break; + + default: + throw new IllegalStateException("Nesting problem."); + } + } + + @Override void promoteNameToValue() throws IOException { + int context = peek(); + if (context != NONEMPTY_OBJECT && context != EMPTY_OBJECT) { + throw new IllegalStateException("Nesting problem."); + } + promoteNameToValue = true; + } + + @Override public String getPath() { + return JsonScope.getPath(stackSize, stack, pathNames, pathIndices); + } +} diff --git a/moshi/src/main/java/com/squareup/moshi/BufferedSourceJsonReader.java b/moshi/src/main/java/com/squareup/moshi/BufferedSourceJsonReader.java new file mode 100644 index 0000000..8f7d5a9 --- /dev/null +++ b/moshi/src/main/java/com/squareup/moshi/BufferedSourceJsonReader.java @@ -0,0 +1,1078 @@ +/* + * 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.squareup.moshi; + +import java.io.EOFException; +import java.io.IOException; +import okio.Buffer; +import okio.BufferedSource; +import okio.ByteString; + +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%. + */ + private int[] stack = new int[32]; + private int stackSize = 0; + { + stack[stackSize++] = JsonScope.EMPTY_DOCUMENT; + } + + private String[] pathNames = new String[32]; + private int[] pathIndices = new int[32]; + + 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 (fillBuffer(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 (!fillBuffer(i + 1)) { + return PEEKED_NONE; + } + c = buffer.getByte(i); + if (c != keyword.charAt(i) && c != keywordUpper.charAt(i)) { + return PEEKED_NONE; + } + } + + if (fillBuffer(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 (!fillBuffer(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 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 IOException("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) { + int[] newStack = new int[stackSize * 2]; + int[] newPathIndices = new int[stackSize * 2]; + String[] newPathNames = new String[stackSize * 2]; + System.arraycopy(stack, 0, newStack, 0, stackSize); + System.arraycopy(pathIndices, 0, newPathIndices, 0, stackSize); + System.arraycopy(pathNames, 0, newPathNames, 0, stackSize); + stack = newStack; + pathIndices = newPathIndices; + pathNames = newPathNames; + } + stack[stackSize++] = newTop; + } + + /** + * Returns true once {@code limit - pos >= minimum}. If the data is + * exhausted before that many characters are available, this returns + * false. + */ + private boolean fillBuffer(int minimum) throws IOException { + return source.request(minimum); + } + + /** + * 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 (fillBuffer(p + 1)) { + int c = buffer.getByte(p++); + if (c == '\n' || c == ' ' || c == '\r' || c == '\t') { + continue; + } + + buffer.skip(p - 1); + if (c == '/') { + if (!fillBuffer(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. See http://b/2571423. + 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 (; fillBuffer(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 (!fillBuffer(1)) { + throw syntaxError("Unterminated escape sequence"); + } + + byte escaped = buffer.readByte(); + switch (escaped) { + case 'u': + if (!fillBuffer(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 '\\': + default: + return (char) escaped; + } + } + + /** + * Throws a new IO exception with the given message and a context snippet + * with this reader's content. + */ + private IOException syntaxError(String message) throws IOException { + throw new IOException(message + " at path " + getPath()); + } + + @Override void promoteNameToValue() throws IOException { + if (hasNext()) { + peekedString = nextName(); + peeked = PEEKED_BUFFERED; + } + } +} diff --git a/moshi/src/main/java/com/squareup/moshi/JsonReader.java b/moshi/src/main/java/com/squareup/moshi/JsonReader.java index d26d563..5cabc78 100644 --- a/moshi/src/main/java/com/squareup/moshi/JsonReader.java +++ b/moshi/src/main/java/com/squareup/moshi/JsonReader.java @@ -16,11 +16,8 @@ package com.squareup.moshi; import java.io.Closeable; -import java.io.EOFException; import java.io.IOException; -import okio.Buffer; import okio.BufferedSource; -import okio.ByteString; /** * Reads a JSON (RFC 7159) @@ -171,102 +168,16 @@ import okio.ByteString; *

Each {@code JsonReader} may be used to read a single JSON stream. Instances * of this class are not thread safe. */ -public class JsonReader implements Closeable { - 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%. - */ - private int[] stack = new int[32]; - private int stackSize = 0; - { - stack[stackSize++] = JsonScope.EMPTY_DOCUMENT; - } - - private String[] pathNames = new String[32]; - private int[] pathIndices = new int[32]; - - private JsonReader(BufferedSource source) { - if (source == null) { - throw new NullPointerException("source == null"); - } - this.source = source; - this.buffer = source.buffer(); - } - +public abstract class JsonReader implements Closeable { /** * Returns a new instance that reads a JSON-encoded stream from {@code source}. */ public static JsonReader of(BufferedSource source) { - return new JsonReader(source); + return new BufferedSourceJsonReader(source); + } + + JsonReader() { + // Package-private to control subclasses. } /** @@ -298,16 +209,12 @@ public class JsonReader implements Closeable { *

  • Name/value pairs separated by {@code ;} instead of {@code ,}. * */ - public final void setLenient(boolean lenient) { - this.lenient = lenient; - } + public abstract void setLenient(boolean lenient); /** * Returns true if this parser is liberal in what it accepts. */ - public final boolean isLenient() { - return lenient; - } + public abstract boolean isLenient(); /** * Configure whether this parser throws a {@link JsonDataException} when {@link #skipValue} is @@ -317,467 +224,53 @@ public class JsonReader implements Closeable { * useful in development and debugging because it means a typo like "locatiom" will be detected * early. It's potentially harmful in production because it complicates revising a JSON schema. */ - public void setFailOnUnknown(boolean failOnUnknown) { - this.failOnUnknown = failOnUnknown; - } + public abstract void setFailOnUnknown(boolean failOnUnknown); /** * Returns true if this parser forbids skipping values. */ - public boolean failOnUnknown() { - return failOnUnknown; - } + public abstract boolean failOnUnknown(); /** * Consumes the next token from the JSON stream and asserts that it is the beginning of a new * array. */ - public void beginArray() throws IOException { - int p = peeked; - if (p == PEEKED_NONE) { - p = doPeek(); - } - if (p == PEEKED_BEGIN_ARRAY) { - push(JsonScope.EMPTY_ARRAY); - pathIndices[stackSize - 1] = 0; - peeked = PEEKED_NONE; - } else { - throw new JsonDataException("Expected BEGIN_ARRAY but was " + peek() - + " at path " + getPath()); - } - } + public abstract void beginArray() throws IOException; /** * Consumes the next token from the JSON stream and asserts that it is the * end of the current array. */ - public void endArray() throws IOException { - int p = peeked; - if (p == PEEKED_NONE) { - p = doPeek(); - } - if (p == PEEKED_END_ARRAY) { - stackSize--; - pathIndices[stackSize - 1]++; - peeked = PEEKED_NONE; - } else { - throw new JsonDataException("Expected END_ARRAY but was " + peek() - + " at path " + getPath()); - } - } + public abstract void endArray() throws IOException; /** * Consumes the next token from the JSON stream and asserts that it is the beginning of a new * object. */ - public void beginObject() throws IOException { - int p = peeked; - if (p == PEEKED_NONE) { - p = doPeek(); - } - if (p == PEEKED_BEGIN_OBJECT) { - push(JsonScope.EMPTY_OBJECT); - peeked = PEEKED_NONE; - } else { - throw new JsonDataException("Expected BEGIN_OBJECT but was " + peek() - + " at path " + getPath()); - } - } + public abstract void beginObject() throws IOException; /** * Consumes the next token from the JSON stream and asserts that it is the end of the current * object. */ - public void endObject() throws IOException { - int p = peeked; - if (p == PEEKED_NONE) { - p = doPeek(); - } - if (p == PEEKED_END_OBJECT) { - stackSize--; - 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()); - } - } + public abstract void endObject() throws IOException; /** * Returns true if the current array or object has another element. */ - public boolean hasNext() throws IOException { - int p = peeked; - if (p == PEEKED_NONE) { - p = doPeek(); - } - return p != PEEKED_END_OBJECT && p != PEEKED_END_ARRAY; - } + public abstract boolean hasNext() throws IOException; /** * Returns the type of the next token without consuming it. */ - 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 (fillBuffer(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 (!fillBuffer(i + 1)) { - return PEEKED_NONE; - } - c = buffer.getByte(i); - if (c != keyword.charAt(i) && c != keywordUpper.charAt(i)) { - return PEEKED_NONE; - } - } - - if (fillBuffer(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 (!fillBuffer(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; - } - } + public abstract Token peek() throws IOException; /** * Returns the next token, a {@linkplain Token#NAME property name}, and consumes it. * * @throws JsonDataException if the next token in the stream is not a property name. */ - public String nextName() throws IOException { - int p = peeked; - if (p == PEEKED_NONE) { - p = doPeek(); - } - String result; - if (p == PEEKED_UNQUOTED_NAME) { - result = nextUnquotedValue(); - } else if (p == PEEKED_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; - } + public abstract String nextName() throws IOException; /** * Returns the {@linkplain Token#STRING string} value of the next token, consuming it. If the next @@ -785,54 +278,14 @@ public class JsonReader implements Closeable { * * @throws JsonDataException if the next token is not a string or if this reader is closed. */ - public String nextString() throws IOException { - int p = peeked; - if (p == PEEKED_NONE) { - p = doPeek(); - } - String result; - if (p == PEEKED_UNQUOTED) { - result = nextUnquotedValue(); - } else if (p == PEEKED_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; - } + public abstract String nextString() throws IOException; /** * Returns the {@linkplain Token#BOOLEAN boolean} value of the next token, consuming it. * * @throws JsonDataException if the next token is not a boolean or if this reader is closed. */ - public boolean nextBoolean() throws IOException { - int p = peeked; - if (p == PEEKED_NONE) { - p = doPeek(); - } - if (p == PEEKED_TRUE) { - peeked = PEEKED_NONE; - 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()); - } + public abstract boolean nextBoolean() throws IOException; /** * Consumes the next token from the JSON stream and asserts that it is a literal null. Returns @@ -840,19 +293,7 @@ public class JsonReader implements Closeable { * * @throws JsonDataException if the next token is not null or if this reader is closed. */ - public 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()); - } - } + public abstract T nextNull() throws IOException; /** * Returns the {@linkplain Token#NUMBER double} value of the next token, consuming it. If the next @@ -862,47 +303,7 @@ public class JsonReader implements Closeable { * @throws JsonDataException if the next token is not a literal value, or if the next literal * value cannot be parsed as a double, or is non-finite. */ - public double nextDouble() throws IOException { - int p = peeked; - if (p == PEEKED_NONE) { - p = doPeek(); - } - - 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 IOException("JSON forbids NaN and infinities: " + result - + " at path " + getPath()); - } - peekedString = null; - peeked = PEEKED_NONE; - pathIndices[stackSize - 1]++; - return result; - } + public abstract double nextDouble() throws IOException; /** * Returns the {@linkplain Token#NUMBER long} value of the next token, consuming it. If the next @@ -912,116 +313,7 @@ public class JsonReader implements Closeable { * @throws JsonDataException if the next token is not a literal value, if the next literal value * cannot be parsed as a number, or exactly represented as a long. */ - public long nextLong() throws IOException { - int p = peeked; - if (p == PEEKED_NONE) { - p = doPeek(); - } - - 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()); - } + public abstract long nextLong() throws IOException; /** * Returns the {@linkplain Token#NUMBER int} value of the next token, consuming it. If the next @@ -1031,71 +323,7 @@ public class JsonReader implements Closeable { * @throws JsonDataException if the next token is not a literal value, if the next literal value * cannot be parsed as a number, or exactly represented as an int. */ - public int nextInt() throws IOException { - int p = peeked; - if (p == PEEKED_NONE) { - p = doPeek(); - } - - int result; - 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; - } - - /** - * Closes this JSON reader and the underlying {@link java.io.Reader}. - */ - public void close() throws IOException { - peeked = PEEKED_NONE; - stack[0] = JsonScope.CLOSED; - stackSize = 1; - buffer.clear(); - source.close(); - } + public abstract int nextInt() throws IOException; /** * Skips the next value recursively. If it is an object or array, all nested elements are skipped. @@ -1105,263 +333,19 @@ public class JsonReader implements Closeable { *

    This throws a {@link JsonDataException} if this parser has been configured to {@linkplain * #failOnUnknown fail on unknown} values. */ - 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) { - int[] newStack = new int[stackSize * 2]; - int[] newPathIndices = new int[stackSize * 2]; - String[] newPathNames = new String[stackSize * 2]; - System.arraycopy(stack, 0, newStack, 0, stackSize); - System.arraycopy(pathIndices, 0, newPathIndices, 0, stackSize); - System.arraycopy(pathNames, 0, newPathNames, 0, stackSize); - stack = newStack; - pathIndices = newPathIndices; - pathNames = newPathNames; - } - stack[stackSize++] = newTop; - } - - /** - * Returns true once {@code limit - pos >= minimum}. If the data is - * exhausted before that many characters are available, this returns - * false. - */ - private boolean fillBuffer(int minimum) throws IOException { - return source.request(minimum); - } - - /** - * 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 (fillBuffer(p + 1)) { - int c = buffer.getByte(p++); - if (c == '\n' || c == ' ' || c == '\r' || c == '\t') { - continue; - } - - buffer.skip(p - 1); - if (c == '/') { - if (!fillBuffer(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. See http://b/2571423. - 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 (; fillBuffer(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 getClass().getSimpleName(); - } + public abstract void skipValue() throws IOException; /** * Returns a JsonPath to * the current location in the JSON value. */ - 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 (!fillBuffer(1)) { - throw syntaxError("Unterminated escape sequence"); - } - - byte escaped = buffer.readByte(); - switch (escaped) { - case 'u': - if (!fillBuffer(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 '\\': - default: - return (char) escaped; - } - } - - /** - * Throws a new IO exception with the given message and a context snippet - * with this reader's content. - */ - private IOException syntaxError(String message) throws IOException { - throw new IOException(message + " at path " + getPath()); - } + public abstract String getPath(); /** * Changes the reader to treat the next name as a string value. This is useful for map adapters so * that arbitrary type adapters can use {@link #nextString} to read a name value. */ - void promoteNameToValue() throws IOException { - if (hasNext()) { - peekedString = nextName(); - peeked = PEEKED_BUFFERED; - } - } + abstract void promoteNameToValue() throws IOException; /** * A structure, name, or value type in a JSON-encoded string. diff --git a/moshi/src/main/java/com/squareup/moshi/JsonWriter.java b/moshi/src/main/java/com/squareup/moshi/JsonWriter.java index 5eba247..e8f7be3 100644 --- a/moshi/src/main/java/com/squareup/moshi/JsonWriter.java +++ b/moshi/src/main/java/com/squareup/moshi/JsonWriter.java @@ -19,15 +19,6 @@ import java.io.Closeable; import java.io.Flushable; import java.io.IOException; import okio.BufferedSink; -import okio.Sink; - -import static com.squareup.moshi.JsonScope.DANGLING_NAME; -import static com.squareup.moshi.JsonScope.EMPTY_ARRAY; -import static com.squareup.moshi.JsonScope.EMPTY_DOCUMENT; -import static com.squareup.moshi.JsonScope.EMPTY_OBJECT; -import static com.squareup.moshi.JsonScope.NONEMPTY_ARRAY; -import static com.squareup.moshi.JsonScope.NONEMPTY_DOCUMENT; -import static com.squareup.moshi.JsonScope.NONEMPTY_OBJECT; /** * Writes a JSON (RFC 7159) @@ -124,76 +115,16 @@ import static com.squareup.moshi.JsonScope.NONEMPTY_OBJECT; * Instances of this class are not thread safe. Calls that would result in a * malformed JSON string will fail with an {@link IllegalStateException}. */ -public class JsonWriter implements Closeable, Flushable { - - /* - * From RFC 7159, "All Unicode characters may be placed within the - * quotation marks except for the characters that must be escaped: - * quotation mark, reverse solidus, and the control characters - * (U+0000 through U+001F)." - * - * We also escape '\u2028' and '\u2029', which JavaScript interprets as - * newline characters. This prevents eval() from failing with a syntax - * error. http://code.google.com/p/google-gson/issues/detail?id=341 - */ - private static final String[] REPLACEMENT_CHARS; - static { - REPLACEMENT_CHARS = new String[128]; - for (int i = 0; i <= 0x1f; i++) { - REPLACEMENT_CHARS[i] = String.format("\\u%04x", (int) i); - } - REPLACEMENT_CHARS['"'] = "\\\""; - REPLACEMENT_CHARS['\\'] = "\\\\"; - REPLACEMENT_CHARS['\t'] = "\\t"; - REPLACEMENT_CHARS['\b'] = "\\b"; - REPLACEMENT_CHARS['\n'] = "\\n"; - REPLACEMENT_CHARS['\r'] = "\\r"; - REPLACEMENT_CHARS['\f'] = "\\f"; - } - - /** The output data, containing at most one top-level array or object. */ - private final BufferedSink sink; - - private int[] stack = new int[32]; - private int stackSize = 0; - { - push(EMPTY_DOCUMENT); - } - - private String[] pathNames = new String[32]; - private int[] pathIndices = new int[32]; - - /** - * A string containing a full set of spaces for a single level of - * indentation, or null for no pretty printing. - */ - private String indent; - - /** - * The name/value separator; either ":" or ": ". - */ - private String separator = ":"; - - private boolean lenient; - - private String deferredName; - - private boolean serializeNulls; - - private boolean promoteNameToValue; - - private JsonWriter(BufferedSink sink) { - if (sink == null) { - throw new NullPointerException("sink == null"); - } - this.sink = sink; - } - +public abstract class JsonWriter implements Closeable, Flushable { /** * Returns a new instance that writes a JSON-encoded stream to {@code sink}. */ public static JsonWriter of(BufferedSink sink) { - return new JsonWriter(sink); + return new BufferedSinkJsonWriter(sink); + } + + JsonWriter() { + // Package-private to control subclasses. } /** @@ -204,15 +135,7 @@ public class JsonWriter implements Closeable, Flushable { * * @param indent a string containing only whitespace. */ - public final void setIndent(String indent) { - if (indent.length() == 0) { - this.indent = null; - this.separator = ":"; - } else { - this.indent = indent; - this.separator = ": "; - } - } + public abstract void setIndent(String indent); /** * Configure this writer to relax its syntax rules. By default, this writer @@ -226,32 +149,24 @@ public class JsonWriter implements Closeable, Flushable { * Double#isInfinite() infinities}. * */ - public final void setLenient(boolean lenient) { - this.lenient = lenient; - } + public abstract void setLenient(boolean lenient); /** * Returns true if this writer has relaxed syntax rules. */ - public boolean isLenient() { - return lenient; - } + public abstract boolean isLenient(); /** * Sets whether object members are serialized when their value is null. * This has no impact on array elements. The default is false. */ - public final void setSerializeNulls(boolean serializeNulls) { - this.serializeNulls = serializeNulls; - } + public abstract void setSerializeNulls(boolean serializeNulls); /** * Returns true if object members are serialized when their value is null. * This has no impact on array elements. The default is false. */ - public final boolean getSerializeNulls() { - return serializeNulls; - } + public abstract boolean getSerializeNulls(); /** * Begins encoding a new array. Each call to this method must be paired with @@ -259,19 +174,14 @@ public class JsonWriter implements Closeable, Flushable { * * @return this writer. */ - public JsonWriter beginArray() throws IOException { - writeDeferredName(); - return open(EMPTY_ARRAY, "["); - } + public abstract JsonWriter beginArray() throws IOException; /** * Ends encoding the current array. * * @return this writer. */ - public JsonWriter endArray() throws IOException { - return close(EMPTY_ARRAY, NONEMPTY_ARRAY, "]"); - } + public abstract JsonWriter endArray() throws IOException; /** * Begins encoding a new object. Each call to this method must be paired @@ -279,82 +189,14 @@ public class JsonWriter implements Closeable, Flushable { * * @return this writer. */ - public JsonWriter beginObject() throws IOException { - writeDeferredName(); - return open(EMPTY_OBJECT, "{"); - } + public abstract JsonWriter beginObject() throws IOException; /** * Ends encoding the current object. * * @return this writer. */ - public JsonWriter endObject() throws IOException { - promoteNameToValue = false; - return close(EMPTY_OBJECT, NONEMPTY_OBJECT, "}"); - } - - /** - * Enters a new scope by appending any necessary whitespace and the given - * bracket. - */ - private JsonWriter open(int empty, String openBracket) throws IOException { - beforeValue(); - pathIndices[stackSize] = 0; - push(empty); - sink.writeUtf8(openBracket); - return this; - } - - /** - * Closes the current scope by appending any necessary whitespace and the - * given bracket. - */ - private JsonWriter close(int empty, int nonempty, String closeBracket) - throws IOException { - int context = peek(); - if (context != nonempty && context != empty) { - throw new IllegalStateException("Nesting problem."); - } - if (deferredName != null) { - throw new IllegalStateException("Dangling name: " + deferredName); - } - - stackSize--; - pathNames[stackSize] = null; // Free the last path name so that it can be garbage collected! - pathIndices[stackSize - 1]++; - if (context == nonempty) { - newline(); - } - sink.writeUtf8(closeBracket); - return this; - } - - 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; - } - - /** - * Returns the value on the top of the stack. - */ - private int peek() { - if (stackSize == 0) { - throw new IllegalStateException("JsonWriter is closed."); - } - return stack[stackSize - 1]; - } - - /** - * Replace the value on the top of the stack with the given value. - */ - private void replaceTop(int topOfStack) { - stack[stackSize - 1] = topOfStack; - } + public abstract JsonWriter endObject() throws IOException; /** * Encodes the property name. @@ -362,29 +204,7 @@ public class JsonWriter implements Closeable, Flushable { * @param name the name of the forthcoming value. May not be null. * @return this writer. */ - public JsonWriter name(String name) throws IOException { - if (name == null) { - throw new NullPointerException("name == null"); - } - if (stackSize == 0) { - throw new IllegalStateException("JsonWriter is closed."); - } - if (deferredName != null) { - throw new IllegalStateException(); - } - deferredName = name; - pathNames[stackSize - 1] = name; - promoteNameToValue = false; - return this; - } - - private void writeDeferredName() throws IOException { - if (deferredName != null) { - beforeName(); - string(deferredName); - deferredName = null; - } - } + public abstract JsonWriter name(String name) throws IOException; /** * Encodes {@code value}. @@ -392,52 +212,21 @@ public class JsonWriter implements Closeable, Flushable { * @param value the literal string value, or null to encode a null literal. * @return this writer. */ - public JsonWriter value(String value) throws IOException { - if (value == null) { - return nullValue(); - } - if (promoteNameToValue) { - return name(value); - } - writeDeferredName(); - beforeValue(); - string(value); - pathIndices[stackSize - 1]++; - return this; - } + public abstract JsonWriter value(String value) throws IOException; /** * Encodes {@code null}. * * @return this writer. */ - public JsonWriter nullValue() throws IOException { - if (deferredName != null) { - if (serializeNulls) { - writeDeferredName(); - } else { - deferredName = null; - return this; // skip the name and the value - } - } - beforeValue(); - sink.writeUtf8("null"); - pathIndices[stackSize - 1]++; - return this; - } + public abstract JsonWriter nullValue() throws IOException; /** * Encodes {@code value}. * * @return this writer. */ - public JsonWriter value(boolean value) throws IOException { - writeDeferredName(); - beforeValue(); - sink.writeUtf8(value ? "true" : "false"); - pathIndices[stackSize - 1]++; - return this; - } + public abstract JsonWriter value(boolean value) throws IOException; /** * Encodes {@code value}. @@ -446,35 +235,14 @@ public class JsonWriter implements Closeable, Flushable { * {@linkplain Double#isInfinite() infinities}. * @return this writer. */ - public JsonWriter value(double value) throws IOException { - if (Double.isNaN(value) || Double.isInfinite(value)) { - throw new IllegalArgumentException("Numeric values must be finite, but was " + value); - } - if (promoteNameToValue) { - return name(Double.toString(value)); - } - writeDeferredName(); - beforeValue(); - sink.writeUtf8(Double.toString(value)); - pathIndices[stackSize - 1]++; - return this; - } + public abstract JsonWriter value(double value) throws IOException; /** * Encodes {@code value}. * * @return this writer. */ - public JsonWriter value(long value) throws IOException { - if (promoteNameToValue) { - return name(Long.toString(value)); - } - writeDeferredName(); - beforeValue(); - sink.writeUtf8(Long.toString(value)); - pathIndices[stackSize - 1]++; - return this; - } + public abstract JsonWriter value(long value) throws IOException; /** * Encodes {@code value}. @@ -483,165 +251,17 @@ public class JsonWriter implements Closeable, Flushable { * {@linkplain Double#isInfinite() infinities}. * @return this writer. */ - public JsonWriter value(Number value) throws IOException { - if (value == null) { - return nullValue(); - } - - String string = value.toString(); - if (!lenient - && (string.equals("-Infinity") || string.equals("Infinity") || string.equals("NaN"))) { - throw new IllegalArgumentException("Numeric values must be finite, but was " + value); - } - if (promoteNameToValue) { - return name(string); - } - writeDeferredName(); - beforeValue(); - sink.writeUtf8(string); - pathIndices[stackSize - 1]++; - return this; - } - - /** - * Ensures all buffered data is written to the underlying {@link Sink} - * and flushes that writer. - */ - public void flush() throws IOException { - if (stackSize == 0) { - throw new IllegalStateException("JsonWriter is closed."); - } - sink.flush(); - } - - /** - * Flushes and closes this writer and the underlying {@link Sink}. - * - * @throws JsonDataException if the JSON document is incomplete. - */ - public void close() throws IOException { - sink.close(); - - int size = stackSize; - if (size > 1 || size == 1 && stack[size - 1] != NONEMPTY_DOCUMENT) { - throw new IOException("Incomplete document"); - } - stackSize = 0; - } - - private void string(String value) throws IOException { - String[] replacements = REPLACEMENT_CHARS; - sink.writeByte('"'); - int last = 0; - int length = value.length(); - for (int i = 0; i < length; i++) { - char c = value.charAt(i); - String replacement; - if (c < 128) { - replacement = replacements[c]; - if (replacement == null) { - continue; - } - } else if (c == '\u2028') { - replacement = "\\u2028"; - } else if (c == '\u2029') { - replacement = "\\u2029"; - } else { - continue; - } - if (last < i) { - sink.writeUtf8(value, last, i); - } - sink.writeUtf8(replacement); - last = i + 1; - } - if (last < length) { - sink.writeUtf8(value, last, length); - } - sink.writeByte('"'); - } - - private void newline() throws IOException { - if (indent == null) { - return; - } - - sink.writeByte('\n'); - for (int i = 1, size = stackSize; i < size; i++) { - sink.writeUtf8(indent); - } - } - - /** - * Inserts any necessary separators and whitespace before a name. Also - * adjusts the stack to expect the name's value. - */ - private void beforeName() throws IOException { - int context = peek(); - if (context == NONEMPTY_OBJECT) { // first in object - sink.writeByte(','); - } else if (context != EMPTY_OBJECT) { // not in an object! - throw new IllegalStateException("Nesting problem."); - } - newline(); - replaceTop(DANGLING_NAME); - } - - /** - * Inserts any necessary separators and whitespace before a literal value, - * inline array, or inline object. Also adjusts the stack to expect either a - * closing bracket or another element. - */ - @SuppressWarnings("fallthrough") - private void beforeValue() throws IOException { - switch (peek()) { - case NONEMPTY_DOCUMENT: - if (!lenient) { - throw new IllegalStateException( - "JSON must have only one top-level value."); - } - // fall-through - case EMPTY_DOCUMENT: // first in document - replaceTop(NONEMPTY_DOCUMENT); - break; - - case EMPTY_ARRAY: // first in array - replaceTop(NONEMPTY_ARRAY); - newline(); - break; - - case NONEMPTY_ARRAY: // another in array - sink.writeByte(','); - newline(); - break; - - case DANGLING_NAME: // value for name - sink.writeUtf8(separator); - replaceTop(NONEMPTY_OBJECT); - break; - - default: - throw new IllegalStateException("Nesting problem."); - } - } + public abstract JsonWriter value(Number value) throws IOException; /** * Changes the reader to treat the next string value as a name. This is useful for map adapters so * that arbitrary type adapters can use {@link #value(String)} to write a name value. */ - void promoteNameToValue() throws IOException { - int context = peek(); - if (context != NONEMPTY_OBJECT && context != EMPTY_OBJECT) { - throw new IllegalStateException("Nesting problem."); - } - promoteNameToValue = true; - } + abstract void promoteNameToValue() throws IOException; /** * Returns a JsonPath to * the current location in the JSON value. */ - public String getPath() { - return JsonScope.getPath(stackSize, stack, pathNames, pathIndices); - } + public abstract String getPath(); } diff --git a/moshi/src/test/java/com/squareup/moshi/JsonWriterTest.java b/moshi/src/test/java/com/squareup/moshi/BufferedSinkJsonWriterTest.java similarity index 99% rename from moshi/src/test/java/com/squareup/moshi/JsonWriterTest.java rename to moshi/src/test/java/com/squareup/moshi/BufferedSinkJsonWriterTest.java index ba4a8fe..9a09e3b 100644 --- a/moshi/src/test/java/com/squareup/moshi/JsonWriterTest.java +++ b/moshi/src/test/java/com/squareup/moshi/BufferedSinkJsonWriterTest.java @@ -24,7 +24,7 @@ import org.junit.Test; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.fail; -public final class JsonWriterTest { +public final class BufferedSinkJsonWriterTest { @Test public void nullsValuesNotSerializedByDefault() throws IOException { Buffer buffer = new Buffer(); JsonWriter jsonWriter = JsonWriter.of(buffer); diff --git a/moshi/src/test/java/com/squareup/moshi/JsonReaderTest.java b/moshi/src/test/java/com/squareup/moshi/BufferedSourceJsonReaderTest.java similarity index 99% rename from moshi/src/test/java/com/squareup/moshi/JsonReaderTest.java rename to moshi/src/test/java/com/squareup/moshi/BufferedSourceJsonReaderTest.java index 2ca3b3a..913d4c0 100644 --- a/moshi/src/test/java/com/squareup/moshi/JsonReaderTest.java +++ b/moshi/src/test/java/com/squareup/moshi/BufferedSourceJsonReaderTest.java @@ -35,7 +35,7 @@ import static com.squareup.moshi.TestUtil.newReader; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.fail; -public final class JsonReaderTest { +public final class BufferedSourceJsonReaderTest { @Test public void readingDoesNotBuffer() throws IOException { Buffer buffer = new Buffer().writeUtf8("{}{}");