Convert JsonReader to Kotlin (#1505)

* Rename .java to .kt

* Convert JsonReader to Kotlin

Made it sealed along the way like JsonWriter

Some of the properties had different docs for setting and getting, which dokka doesn't seem to have a nice way to differentiate. Went with a little marked up form but open to suggestions.

* Restore isLenient name to lenient

This plays nice for both java and kotlin consumers, as it's still isLenient to java consumers but not a source breaking change for kotlin

* Fix peek nullability and a kotlin source change

* Fix another nullability

* Use collection builders

* Ok this didn't work quite as I expected

* Update moshi/src/main/java/com/squareup/moshi/JsonReader.kt

Co-authored-by: Parth Padgaonkar <1294660+JvmName@users.noreply.github.com>

Co-authored-by: Parth Padgaonkar <1294660+JvmName@users.noreply.github.com>
This commit is contained in:
Zac Sweers
2022-01-17 02:52:02 -05:00
committed by GitHub
parent e6081dc90e
commit 47697c2601
5 changed files with 679 additions and 709 deletions

View File

@@ -191,7 +191,7 @@ public class PolymorphicJsonAdapterFactory<T> internal constructor(
override fun fromJson(reader: JsonReader): Any? { override fun fromJson(reader: JsonReader): Any? {
val peeked = reader.peekJson() val peeked = reader.peekJson()
peeked.setFailOnUnknown(false) peeked.failOnUnknown = false
val labelIndex = peeked.use(::labelIndex) val labelIndex = peeked.use(::labelIndex)
return if (labelIndex == -1) { return if (labelIndex == -1) {
fallbackJsonAdapter?.fromJson(reader) fallbackJsonAdapter?.fromJson(reader)

View File

@@ -188,12 +188,12 @@ public abstract class JsonAdapter<T> {
val delegate: JsonAdapter<T> = this val delegate: JsonAdapter<T> = this
return object : JsonAdapter<T>() { return object : JsonAdapter<T>() {
override fun fromJson(reader: JsonReader): T? { override fun fromJson(reader: JsonReader): T? {
val lenient = reader.isLenient val lenient = reader.lenient
reader.isLenient = true reader.lenient = true
return try { return try {
delegate.fromJson(reader) delegate.fromJson(reader)
} finally { } finally {
reader.isLenient = lenient reader.lenient = lenient
} }
} }
@@ -225,12 +225,12 @@ public abstract class JsonAdapter<T> {
val delegate: JsonAdapter<T> = this val delegate: JsonAdapter<T> = this
return object : JsonAdapter<T>() { return object : JsonAdapter<T>() {
override fun fromJson(reader: JsonReader): T? { override fun fromJson(reader: JsonReader): T? {
val skipForbidden = reader.failOnUnknown() val skipForbidden = reader.failOnUnknown
reader.setFailOnUnknown(true) reader.failOnUnknown = true
return try { return try {
delegate.fromJson(reader) delegate.fromJson(reader)
} finally { } finally {
reader.setFailOnUnknown(skipForbidden) reader.failOnUnknown = skipForbidden
} }
} }

View File

@@ -1,701 +0,0 @@
/*
* 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 static java.util.Collections.unmodifiableList;
import java.io.Closeable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.CheckReturnValue;
import javax.annotation.Nullable;
import okio.Buffer;
import okio.BufferedSource;
import okio.ByteString;
/**
* Reads a JSON (<a href="http://www.ietf.org/rfc/rfc7159.txt">RFC 7159</a>) encoded value as a
* stream of tokens. This stream includes both literal values (strings, numbers, booleans, and
* nulls) as well as the begin and end delimiters of objects and arrays. The tokens are traversed in
* depth-first order, the same order that they appear in the JSON document. Within JSON objects,
* name/value pairs are represented by a single token.
*
* <h2>Parsing JSON</h2>
*
* To create a recursive descent parser for your own JSON streams, first create an entry point
* method that creates a {@code JsonReader}.
*
* <p>Next, create handler methods for each structure in your JSON text. You'll need a method for
* each object type and for each array type.
*
* <ul>
* <li>Within <strong>array handling</strong> methods, first call {@link #beginArray} to consume
* the array's opening bracket. Then create a while loop that accumulates values, terminating
* when {@link #hasNext} is false. Finally, read the array's closing bracket by calling {@link
* #endArray}.
* <li>Within <strong>object handling</strong> methods, first call {@link #beginObject} to consume
* the object's opening brace. Then create a while loop that assigns values to local variables
* based on their name. This loop should terminate when {@link #hasNext} is false. Finally,
* read the object's closing brace by calling {@link #endObject}.
* </ul>
*
* <p>When a nested object or array is encountered, delegate to the corresponding handler method.
*
* <p>When an unknown name is encountered, strict parsers should fail with an exception. Lenient
* parsers should call {@link #skipValue()} to recursively skip the value's nested tokens, which may
* otherwise conflict.
*
* <p>If a value may be null, you should first check using {@link #peek()}. Null literals can be
* consumed using either {@link #nextNull()} or {@link #skipValue()}.
*
* <h2>Example</h2>
*
* Suppose we'd like to parse a stream of messages such as the following:
*
* <pre>{@code
* [
* {
* "id": 912345678901,
* "text": "How do I read a JSON stream in Java?",
* "geo": null,
* "user": {
* "name": "json_newb",
* "followers_count": 41
* }
* },
* {
* "id": 912345678902,
* "text": "@json_newb just use JsonReader!",
* "geo": [50.454722, -104.606667],
* "user": {
* "name": "jesse",
* "followers_count": 2
* }
* }
* ]
* }</pre>
*
* This code implements the parser for the above structure:
*
* <pre>{@code
* public List<Message> readJsonStream(BufferedSource source) throws IOException {
* JsonReader reader = JsonReader.of(source);
* try {
* return readMessagesArray(reader);
* } finally {
* reader.close();
* }
* }
*
* public List<Message> readMessagesArray(JsonReader reader) throws IOException {
* List<Message> messages = new ArrayList<Message>();
*
* reader.beginArray();
* while (reader.hasNext()) {
* messages.add(readMessage(reader));
* }
* reader.endArray();
* return messages;
* }
*
* public Message readMessage(JsonReader reader) throws IOException {
* long id = -1;
* String text = null;
* User user = null;
* List<Double> geo = null;
*
* reader.beginObject();
* while (reader.hasNext()) {
* String name = reader.nextName();
* if (name.equals("id")) {
* id = reader.nextLong();
* } else if (name.equals("text")) {
* text = reader.nextString();
* } else if (name.equals("geo") && reader.peek() != Token.NULL) {
* geo = readDoublesArray(reader);
* } else if (name.equals("user")) {
* user = readUser(reader);
* } else {
* reader.skipValue();
* }
* }
* reader.endObject();
* return new Message(id, text, user, geo);
* }
*
* public List<Double> readDoublesArray(JsonReader reader) throws IOException {
* List<Double> doubles = new ArrayList<Double>();
*
* reader.beginArray();
* while (reader.hasNext()) {
* doubles.add(reader.nextDouble());
* }
* reader.endArray();
* return doubles;
* }
*
* public User readUser(JsonReader reader) throws IOException {
* String username = null;
* int followersCount = -1;
*
* reader.beginObject();
* while (reader.hasNext()) {
* String name = reader.nextName();
* if (name.equals("name")) {
* username = reader.nextString();
* } else if (name.equals("followers_count")) {
* followersCount = reader.nextInt();
* } else {
* reader.skipValue();
* }
* }
* reader.endObject();
* return new User(username, followersCount);
* }
* }</pre>
*
* <h2>Number Handling</h2>
*
* This reader permits numeric values to be read as strings and string values to be read as numbers.
* For example, both elements of the JSON array {@code [1, "1"]} may be read using either {@link
* #nextInt} or {@link #nextString}. This behavior is intended to prevent lossy numeric conversions:
* double is JavaScript's only numeric type and very large values like {@code 9007199254740993}
* cannot be represented exactly on that platform. To minimize precision loss, extremely large
* values should be written and read as strings in JSON.
*
* <p>Each {@code JsonReader} may be used to read a single JSON stream. Instances of this class are
* not thread safe.
*/
public abstract class JsonReader implements Closeable {
// The nesting stack. Using a manual array rather than an ArrayList saves 20%. This stack will
// grow itself up to 256 levels of nesting including the top-level document. Deeper nesting is
// prone to trigger StackOverflowErrors.
int stackSize;
int[] scopes;
String[] pathNames;
int[] pathIndices;
/** True to accept non-spec compliant JSON. */
boolean lenient;
/** True to throw a {@link JsonDataException} on any attempt to call {@link #skipValue()}. */
boolean failOnUnknown;
private Map<Class<?>, Object> tags;
/** Returns a new instance that reads UTF-8 encoded JSON from {@code source}. */
@CheckReturnValue
public static JsonReader of(BufferedSource source) {
return new JsonUtf8Reader(source);
}
// Package-private to control subclasses.
JsonReader() {
scopes = new int[32];
pathNames = new String[32];
pathIndices = new int[32];
}
// Package-private to control subclasses.
JsonReader(JsonReader copyFrom) {
this.stackSize = copyFrom.stackSize;
this.scopes = copyFrom.scopes.clone();
this.pathNames = copyFrom.pathNames.clone();
this.pathIndices = copyFrom.pathIndices.clone();
this.lenient = copyFrom.lenient;
this.failOnUnknown = copyFrom.failOnUnknown;
}
final void pushScope(int newTop) {
if (stackSize == scopes.length) {
if (stackSize == 256) {
throw new JsonDataException("Nesting too deep at " + getPath());
}
scopes = Arrays.copyOf(scopes, scopes.length * 2);
pathNames = Arrays.copyOf(pathNames, pathNames.length * 2);
pathIndices = Arrays.copyOf(pathIndices, pathIndices.length * 2);
}
scopes[stackSize++] = newTop;
}
/**
* Throws a new IO exception with the given message and a context snippet with this reader's
* content.
*/
final JsonEncodingException syntaxError(String message) throws JsonEncodingException {
throw new JsonEncodingException(message + " at path " + getPath());
}
final JsonDataException typeMismatch(@Nullable Object value, Object expected) {
if (value == null) {
return new JsonDataException("Expected " + expected + " but was null at path " + getPath());
} else {
return new JsonDataException(
"Expected "
+ expected
+ " but was "
+ value
+ ", a "
+ value.getClass().getName()
+ ", at path "
+ getPath());
}
}
/**
* Configure this parser to be liberal in what it accepts. By default this parser is strict and
* only accepts JSON as specified by <a href="http://www.ietf.org/rfc/rfc7159.txt">RFC 7159</a>.
* Setting the parser to lenient causes it to ignore the following syntax errors:
*
* <ul>
* <li>Streams that include multiple top-level values. With strict parsing, each stream must
* contain exactly one top-level value.
* <li>Numbers may be {@linkplain Double#isNaN() NaNs} or {@link Double#isInfinite()
* infinities}.
* <li>End of line comments starting with {@code //} or {@code #} and ending with a newline
* character.
* <li>C-style comments starting with {@code /*} and ending with {@code *}{@code /}. Such
* comments may not be nested.
* <li>Names that are unquoted or {@code 'single quoted'}.
* <li>Strings that are unquoted or {@code 'single quoted'}.
* <li>Array elements separated by {@code ;} instead of {@code ,}.
* <li>Unnecessary array separators. These are interpreted as if null was the omitted value.
* <li>Names and values separated by {@code =} or {@code =>} instead of {@code :}.
* <li>Name/value pairs separated by {@code ;} instead of {@code ,}.
* </ul>
*/
public final void setLenient(boolean lenient) {
this.lenient = lenient;
}
/** Returns true if this parser is liberal in what it accepts. */
@CheckReturnValue
public final boolean isLenient() {
return lenient;
}
/**
* Configure whether this parser throws a {@link JsonDataException} when {@link #skipValue} is
* called. By default this parser permits values to be skipped.
*
* <p>Forbid skipping to prevent unrecognized values from being silently ignored. This option is
* 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 final void setFailOnUnknown(boolean failOnUnknown) {
this.failOnUnknown = failOnUnknown;
}
/** Returns true if this parser forbids skipping names and values. */
@CheckReturnValue
public final boolean failOnUnknown() {
return failOnUnknown;
}
/**
* Consumes the next token from the JSON stream and asserts that it is the beginning of a new
* array.
*/
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 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 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 abstract void endObject() throws IOException;
/** Returns true if the current array or object has another element. */
@CheckReturnValue
public abstract boolean hasNext() throws IOException;
/** Returns the type of the next token without consuming it. */
@CheckReturnValue
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.
*/
@CheckReturnValue
public abstract String nextName() throws IOException;
/**
* If the next token is a {@linkplain Token#NAME property name} that's in {@code options}, this
* consumes it and returns its index. Otherwise this returns -1 and no name is consumed.
*/
@CheckReturnValue
public abstract int selectName(Options options) throws IOException;
/**
* Skips the next token, consuming it. This method is intended for use when the JSON token stream
* contains unrecognized or unhandled names.
*
* <p>This throws a {@link JsonDataException} if this parser has been configured to {@linkplain
* #failOnUnknown fail on unknown} names.
*/
public abstract void skipName() throws IOException;
/**
* Returns the {@linkplain Token#STRING string} value of the next token, consuming it. If the next
* token is a number, this method will return its string form.
*
* @throws JsonDataException if the next token is not a string or if this reader is closed.
*/
public abstract String nextString() throws IOException;
/**
* If the next token is a {@linkplain Token#STRING string} that's in {@code options}, this
* consumes it and returns its index. Otherwise this returns -1 and no string is consumed.
*/
@CheckReturnValue
public abstract int selectString(Options options) 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 abstract boolean nextBoolean() throws IOException;
/**
* Consumes the next token from the JSON stream and asserts that it is a literal null. Returns
* null.
*
* @throws JsonDataException if the next token is not null or if this reader is closed.
*/
public abstract @Nullable <T> T nextNull() throws IOException;
/**
* Returns the {@linkplain Token#NUMBER double} value of the next token, consuming it. If the next
* token is a string, this method will attempt to parse it as a double using {@link
* Double#parseDouble(String)}.
*
* @throws 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 abstract double nextDouble() throws IOException;
/**
* Returns the {@linkplain Token#NUMBER long} value of the next token, consuming it. If the next
* token is a string, this method will attempt to parse it as a long. If the next token's numeric
* value cannot be exactly represented by a Java {@code long}, this method throws.
*
* @throws 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 abstract long nextLong() throws IOException;
/**
* Returns the {@linkplain Token#NUMBER int} value of the next token, consuming it. If the next
* token is a string, this method will attempt to parse it as an int. If the next token's numeric
* value cannot be exactly represented by a Java {@code int}, this method throws.
*
* @throws 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 abstract int nextInt() throws IOException;
/**
* Returns the next value as a stream of UTF-8 bytes and consumes it.
*
* <p>The following program demonstrates how JSON bytes are returned from an enclosing stream as
* their original bytes, including their original whitespace:
*
* <pre>{@code
* String json = "{\"a\": [4, 5 ,6.0, {\"x\":7}, 8], \"b\": 9}";
* JsonReader reader = JsonReader.of(new Buffer().writeUtf8(json));
* reader.beginObject();
* assertThat(reader.nextName()).isEqualTo("a");
* try (BufferedSource bufferedSource = reader.nextSource()) {
* assertThat(bufferedSource.readUtf8()).isEqualTo("[4, 5 ,6.0, {\"x\":7}, 8]");
* }
* assertThat(reader.nextName()).isEqualTo("b");
* assertThat(reader.nextInt()).isEqualTo(9);
* reader.endObject();
* }</pre>
*
* <p>This reads an entire value: composite objects like arrays and objects are returned in their
* entirety. The stream starts with the first character of the value (typically {@code [}, <code>{
* </code>, or {@code "}) and ends with the last character of the object (typically {@code ]},
* <code>}</code>, or {@code "}).
*
* <p>The returned source may not be used after any other method on this {@code JsonReader} is
* called. For example, the following code crashes with an exception:
*
* <pre>{@code
* JsonReader reader = ...
* reader.beginArray();
* BufferedSource source = reader.nextSource();
* reader.endArray();
* source.readUtf8(); // Crash!
* }</pre>
*
* <p>The returned bytes are not validated. This method assumes the stream is well-formed JSON and
* only attempts to find the value's boundary in the byte stream. It is the caller's
* responsibility to check that the returned byte stream is a valid JSON value.
*
* <p>Closing the returned source <strong>does not</strong> close this reader.
*/
public abstract BufferedSource nextSource() throws IOException;
/**
* Skips the next value recursively. If it is an object or array, all nested elements are skipped.
* This method is intended for use when the JSON token stream contains unrecognized or unhandled
* values.
*
* <p>This throws a {@link JsonDataException} if this parser has been configured to {@linkplain
* #failOnUnknown fail on unknown} values.
*/
public abstract void skipValue() throws IOException;
/**
* Returns the value of the next token, consuming it. The result may be a string, number, boolean,
* null, map, or list, according to the JSON structure.
*
* @throws JsonDataException if the next token is not a literal value, if a JSON object has a
* duplicate key.
* @see JsonWriter#jsonValue(Object)
*/
public final @Nullable Object readJsonValue() throws IOException {
switch (peek()) {
case BEGIN_ARRAY:
List<Object> list = new ArrayList<>();
beginArray();
while (hasNext()) {
list.add(readJsonValue());
}
endArray();
return list;
case BEGIN_OBJECT:
Map<String, Object> map = new LinkedHashTreeMap<>();
beginObject();
while (hasNext()) {
String name = nextName();
Object value = readJsonValue();
Object replaced = map.put(name, value);
if (replaced != null) {
throw new JsonDataException(
"Map key '"
+ name
+ "' has multiple values at path "
+ getPath()
+ ": "
+ replaced
+ " and "
+ value);
}
}
endObject();
return map;
case STRING:
return nextString();
case NUMBER:
return nextDouble();
case BOOLEAN:
return nextBoolean();
case NULL:
return nextNull();
default:
throw new IllegalStateException(
"Expected a value but was " + peek() + " at path " + getPath());
}
}
/**
* Returns a new {@code JsonReader} that can read data from this {@code JsonReader} without
* consuming it. The returned reader becomes invalid once this one is next read or closed.
*
* <p>For example, we can use {@code peekJson()} to lookahead and read the same data multiple
* times.
*
* <pre>{@code
* Buffer buffer = new Buffer();
* buffer.writeUtf8("[123, 456, 789]")
*
* JsonReader jsonReader = JsonReader.of(buffer);
* jsonReader.beginArray();
* jsonReader.nextInt(); // Returns 123, reader contains 456, 789 and ].
*
* JsonReader peek = reader.peekJson();
* peek.nextInt() // Returns 456.
* peek.nextInt() // Returns 789.
* peek.endArray()
*
* jsonReader.nextInt() // Returns 456, reader contains 789 and ].
* }</pre>
*/
@CheckReturnValue
public abstract JsonReader peekJson();
/**
* Returns a <a href="http://goessner.net/articles/JsonPath/">JsonPath</a> to the current location
* in the JSON value.
*/
@CheckReturnValue
public final String getPath() {
return JsonScope.getPath(stackSize, scopes, pathNames, pathIndices);
}
/** Returns the tag value for the given class key. */
@SuppressWarnings("unchecked")
@CheckReturnValue
public final @Nullable <T> T tag(Class<T> clazz) {
if (tags == null) {
return null;
}
return (T) tags.get(clazz);
}
/** Assigns the tag value using the given class key and value. */
public final <T> void setTag(Class<T> clazz, T value) {
if (!clazz.isAssignableFrom(value.getClass())) {
throw new IllegalArgumentException("Tag value must be of type " + clazz.getName());
}
if (tags == null) {
tags = new LinkedHashMap<>();
}
tags.put(clazz, value);
}
/**
* 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.
*
* <p>In this example, calling this method allows two sequential calls to {@link #nextString()}:
*
* <pre>{@code
* JsonReader reader = JsonReader.of(new Buffer().writeUtf8("{\"a\":\"b\"}"));
* reader.beginObject();
* reader.promoteNameToValue();
* assertEquals("a", reader.nextString());
* assertEquals("b", reader.nextString());
* reader.endObject();
* }</pre>
*/
public abstract void promoteNameToValue() throws IOException;
/**
* A set of strings to be chosen with {@link #selectName} or {@link #selectString}. This prepares
* the encoded values of the strings so they can be read directly from the input source.
*/
public static final class Options {
final String[] strings;
final okio.Options doubleQuoteSuffix;
private Options(String[] strings, okio.Options doubleQuoteSuffix) {
this.strings = strings;
this.doubleQuoteSuffix = doubleQuoteSuffix;
}
/** Returns a copy of this {@link Options Option's} strings. */
public List<String> strings() {
return unmodifiableList(Arrays.asList(strings));
}
@CheckReturnValue
public static Options of(String... strings) {
try {
ByteString[] result = new ByteString[strings.length];
Buffer buffer = new Buffer();
for (int i = 0; i < strings.length; i++) {
JsonUtf8Writer.string(buffer, strings[i]);
buffer.readByte(); // Skip the leading double quote (but leave the trailing one).
result[i] = buffer.readByteString();
}
return new Options(strings.clone(), okio.Options.of(result));
} catch (IOException e) {
throw new AssertionError(e);
}
}
}
/** A structure, name, or value type in a JSON-encoded string. */
public enum Token {
/**
* The opening of a JSON array. Written using {@link JsonWriter#beginArray} and read using
* {@link JsonReader#beginArray}.
*/
BEGIN_ARRAY,
/**
* The closing of a JSON array. Written using {@link JsonWriter#endArray} and read using {@link
* JsonReader#endArray}.
*/
END_ARRAY,
/**
* The opening of a JSON object. Written using {@link JsonWriter#beginObject} and read using
* {@link JsonReader#beginObject}.
*/
BEGIN_OBJECT,
/**
* The closing of a JSON object. Written using {@link JsonWriter#endObject} and read using
* {@link JsonReader#endObject}.
*/
END_OBJECT,
/**
* A JSON property name. Within objects, tokens alternate between names and their values.
* Written using {@link JsonWriter#name} and read using {@link JsonReader#nextName}
*/
NAME,
/** A JSON string. */
STRING,
/**
* A JSON number represented in this API by a Java {@code double}, {@code long}, or {@code int}.
*/
NUMBER,
/** A JSON {@code true} or {@code false}. */
BOOLEAN,
/** A JSON {@code null}. */
NULL,
/**
* The end of the JSON stream. This sentinel value is returned by {@link JsonReader#peek()} to
* signal that the JSON-encoded value has no more tokens.
*/
END_DOCUMENT
}
}

View File

@@ -0,0 +1,671 @@
/*
* 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 com.squareup.moshi.JsonScope.getPath
import com.squareup.moshi.JsonUtf8Writer.Companion.string
import okio.Buffer
import okio.BufferedSource
import okio.Closeable
import okio.IOException
import javax.annotation.CheckReturnValue
import okio.Options as OkioOptions
/**
* Reads a JSON ([RFC 7159](http://www.ietf.org/rfc/rfc7159.txt)) encoded value as a
* stream of tokens. This stream includes both literal values (strings, numbers, booleans, and
* nulls) as well as the begin and end delimiters of objects and arrays. The tokens are traversed in
* depth-first order, the same order that they appear in the JSON document. Within JSON objects,
* name/value pairs are represented by a single token.
*
* ## Parsing JSON
*
* To create a recursive descent parser for your own JSON streams, first create an entry point
* method that creates a `JsonReader`.
*
* Next, create handler methods for each structure in your JSON text. You'll need a method for
* each object type and for each array type.
* * Within **array handling** methods, first call [beginArray] to consume
* the array's opening bracket. Then create a `while` loop that accumulates values, terminating
* when [hasNext] is false. Finally, read the array's closing bracket by calling [endArray].
* * Within **object handling** methods, first call [beginObject] to consume
* the object's opening brace. Then create a `while` loop that assigns values to local variables
* based on their name. This loop should terminate when [hasNext] is false. Finally,
* read the object's closing brace by calling [endObject].
*
* When a nested object or array is encountered, delegate to the corresponding handler method.
*
* When an unknown name is encountered, strict parsers should fail with an exception. Lenient
* parsers should call [skipValue] to recursively skip the value's nested tokens, which may
* otherwise conflict.
*
* If a value may be null, you should first check using [peek]. Null literals can be
* consumed using either [nextNull] or [skipValue].
*
* ## Example
*
* Suppose we'd like to parse a stream of messages such as the following:
*
* ```json
* [
* {
* "id": 912345678901,
* "text": "How do I read a JSON stream in Java?",
* "geo": null,
* "user": {
* "name": "json_newb",
* "followers_count": 41
* }
* },
* {
* "id": 912345678902,
* "text": "@json_newb just use JsonReader!",
* "geo": [50.454722, -104.606667],
* "user": {
* "name": "jesse",
* "followers_count": 2
* }
* }
* ]
* ```
*
* This code implements the parser for the above structure:
*
* ```java
* public List<Message> readJsonStream(BufferedSource source) throws IOException {
* JsonReader reader = JsonReader.of(source);
* try {
* return readMessagesArray(reader);
* } finally {
* reader.close();
* }
* }
*
* public List<Message> readMessagesArray(JsonReader reader) throws IOException {
* List<Message> messages = new ArrayList<Message>();
*
* reader.beginArray();
* while (reader.hasNext()) {
* messages.add(readMessage(reader));
* }
* reader.endArray();
* return messages;
* }
*
* public Message readMessage(JsonReader reader) throws IOException {
* long id = -1;
* String text = null;
* User user = null;
* List<Double> geo = null;
*
* reader.beginObject();
* while (reader.hasNext()) {
* String name = reader.nextName();
* if (name.equals("id")) {
* id = reader.nextLong();
* } else if (name.equals("text")) {
* text = reader.nextString();
* } else if (name.equals("geo") && reader.peek() != Token.NULL) {
* geo = readDoublesArray(reader);
* } else if (name.equals("user")) {
* user = readUser(reader);
* } else {
* reader.skipValue();
* }
* }
* reader.endObject();
* return new Message(id, text, user, geo);
* }
*
* public List<Double> readDoublesArray(JsonReader reader) throws IOException {
* List<Double> doubles = new ArrayList<Double>();
*
* reader.beginArray();
* while (reader.hasNext()) {
* doubles.add(reader.nextDouble());
* }
* reader.endArray();
* return doubles;
* }
*
* public User readUser(JsonReader reader) throws IOException {
* String username = null;
* int followersCount = -1;
*
* reader.beginObject();
* while (reader.hasNext()) {
* String name = reader.nextName();
* if (name.equals("name")) {
* username = reader.nextString();
* } else if (name.equals("followers_count")) {
* followersCount = reader.nextInt();
* } else {
* reader.skipValue();
* }
* }
* reader.endObject();
* return new User(username, followersCount);
* }
* ```
*
* ## Number Handling
*
* This reader permits numeric values to be read as strings and string values to be read as numbers.
* For example, both elements of the JSON array `[1, "1"]` may be read using either [nextInt] or [nextString]. This behavior is intended to prevent lossy numeric conversions:
* double is JavaScript's only numeric type and very large values like `9007199254740993`
* cannot be represented exactly on that platform. To minimize precision loss, extremely large
* values should be written and read as strings in JSON.
*
* Each `JsonReader` may be used to read a single JSON stream. Instances of this class are
* not thread safe.
*/
public sealed class JsonReader : Closeable {
// The nesting stack. Using a manual array rather than an ArrayList saves 20%. This stack will
// grow itself up to 256 levels of nesting including the top-level document. Deeper nesting is
// prone to trigger StackOverflowErrors.
@JvmField
protected var stackSize: Int = 0
@JvmField
protected var scopes: IntArray
@JvmField
protected var pathNames: Array<String?>
@JvmField
protected var pathIndices: IntArray
/**
* Returns true if this parser is liberal in what it accepts.
*
* ## Getting
* True to accept non-spec compliant JSON.
*
* ## Setting
* Configure this parser to be liberal in what it accepts. By default this parser is strict and
* only accepts JSON as specified by [RFC 7159](http://www.ietf.org/rfc/rfc7159.txt).
* Setting the parser to lenient causes it to ignore the following syntax errors:
* * Streams that include multiple top-level values. With strict parsing, each stream must
* contain exactly one top-level value.
* * Numbers may be [NaNs][Double.isNaN] or [infinities][Double.isInfinite].
* * End of line comments starting with `//` or `#` and ending with a newline
* character.
* * C-style comments starting with `/ *` and ending with `*``/`. Such
* comments may not be nested.
* * Names that are unquoted or `'single quoted'`.
* * Strings that are unquoted or `'single quoted'`.
* * Array elements separated by `;` instead of `,`.
* * Unnecessary array separators. These are interpreted as if null was the omitted value.
* * Names and values separated by `=` or `=>` instead of `:`.
* * Name/value pairs separated by `;` instead of `,`.
*/
@get:CheckReturnValue
@get:JvmName("isLenient")
public var lenient: Boolean = false
/**
* True to throw a [JsonDataException] on any attempt to call [skipValue].
*
* ## Getting
* Returns true if this parser forbids skipping names and values.
*
* ## Setting
* Configure whether this parser throws a [JsonDataException] when [skipValue] is
* called. By default this parser permits values to be skipped.
*
* Forbid skipping to prevent unrecognized values from being silently ignored. This option is
* 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.
*/
/** Returns true if this parser forbids skipping names and values. */
@get:JvmName("failOnUnknown")
public var failOnUnknown: Boolean = false
private var tags: MutableMap<Class<*>, Any>? = null
protected constructor() {
scopes = IntArray(32)
pathNames = arrayOfNulls(32)
pathIndices = IntArray(32)
}
protected constructor(copyFrom: JsonReader) {
stackSize = copyFrom.stackSize
scopes = copyFrom.scopes.clone()
pathNames = copyFrom.pathNames.clone()
pathIndices = copyFrom.pathIndices.clone()
lenient = copyFrom.lenient
failOnUnknown = copyFrom.failOnUnknown
}
protected fun pushScope(newTop: Int) {
if (stackSize == scopes.size) {
if (stackSize == 256) {
throw JsonDataException("Nesting too deep at $path")
}
scopes = scopes.copyOf(scopes.size * 2)
pathNames = pathNames.copyOf(pathNames.size * 2)
pathIndices = pathIndices.copyOf(pathIndices.size * 2)
}
scopes[stackSize++] = newTop
}
/**
* Throws a new IO exception with the given message and a context snippet with this reader's
* content.
*/
@Suppress("NOTHING_TO_INLINE")
@Throws(JsonEncodingException::class)
protected inline fun syntaxError(message: String): JsonEncodingException {
throw JsonEncodingException("$message at path $path")
}
protected fun typeMismatch(value: Any?, expected: Any): JsonDataException {
return if (value == null) {
JsonDataException("Expected $expected but was null at path $path")
} else {
JsonDataException("Expected $expected but was $value, a ${value.javaClass.name}, at path $path")
}
}
/**
* Consumes the next token from the JSON stream and asserts that it is the beginning of a new
* array.
*/
@Throws(IOException::class)
public abstract fun beginArray()
/**
* Consumes the next token from the JSON stream and asserts that it is the end of the current
* array.
*/
@Throws(IOException::class)
public abstract fun endArray()
/**
* Consumes the next token from the JSON stream and asserts that it is the beginning of a new
* object.
*/
@Throws(IOException::class)
public abstract fun beginObject()
/**
* Consumes the next token from the JSON stream and asserts that it is the end of the current
* object.
*/
@Throws(IOException::class)
public abstract fun endObject()
/** Returns true if the current array or object has another element. */
@CheckReturnValue
@Throws(IOException::class)
public abstract operator fun hasNext(): Boolean
/** Returns the type of the next token without consuming it. */
@CheckReturnValue
@Throws(IOException::class)
public abstract fun peek(): Token
/**
* Returns the next token, a [property name][Token.NAME], and consumes it.
*
* @throws JsonDataException if the next token in the stream is not a property name.
*/
@CheckReturnValue
@Throws(IOException::class)
public abstract fun nextName(): String
/**
* If the next token is a [property name][Token.NAME] that's in [options], this
* consumes it and returns its index. Otherwise, this returns -1 and no name is consumed.
*/
@CheckReturnValue
@Throws(IOException::class)
public abstract fun selectName(options: Options): Int
/**
* Skips the next token, consuming it. This method is intended for use when the JSON token stream
* contains unrecognized or unhandled names.
*
* This throws a [JsonDataException] if this parser has been configured to [failOnUnknown] names.
*/
@Throws(IOException::class)
public abstract fun skipName()
/**
* Returns the [string][Token.STRING] value of the next token, consuming it. If the next
* token is a number, this method will return its string form.
*
* @throws JsonDataException if the next token is not a string or if this reader is closed.
*/
@Throws(IOException::class)
public abstract fun nextString(): String
/**
* If the next token is a [string][Token.STRING] that's in [options], this
* consumes it and returns its index. Otherwise, this returns -1 and no string is consumed.
*/
@CheckReturnValue
@Throws(IOException::class)
public abstract fun selectString(options: Options): Int
/**
* Returns the [boolean][Token.BOOLEAN] value of the next token, consuming it.
*
* @throws JsonDataException if the next token is not a boolean or if this reader is closed.
*/
@Throws(IOException::class)
public abstract fun nextBoolean(): Boolean
/**
* Consumes the next token from the JSON stream and asserts that it is a literal null. Returns
* null.
*
* @throws JsonDataException if the next token is not null or if this reader is closed.
*/
@Throws(IOException::class)
public abstract fun <T> nextNull(): T?
/**
* Returns the [double][Token.NUMBER] value of the next token, consuming it. If the next
* token is a string, this method will attempt to parse it as a double using [java.lang.Double.parseDouble].
*
* @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.
*/
@Throws(IOException::class)
public abstract fun nextDouble(): Double
/**
* Returns the [long][Token.NUMBER] value of the next token, consuming it. If the next
* token is a string, this method will attempt to parse it as a long. If the next token's numeric
* value cannot be exactly represented by a Java `long`, this method throws.
*
* @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.
*/
@Throws(IOException::class)
public abstract fun nextLong(): Long
/**
* Returns the [int][Token.NUMBER] value of the next token, consuming it. If the next
* token is a string, this method will attempt to parse it as an int. If the next token's numeric
* value cannot be exactly represented by a Java `int`, this method throws.
*
* @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.
*/
@Throws(IOException::class)
public abstract fun nextInt(): Int
/**
* Returns the next value as a stream of UTF-8 bytes and consumes it.
*
* The following program demonstrates how JSON bytes are returned from an enclosing stream as
* their original bytes, including their original whitespace:
*
* ```java
* String json = "{\"a\": [4, 5 ,6.0, {\"x\":7}, 8], \"b\": 9}";
* JsonReader reader = JsonReader.of(new Buffer().writeUtf8(json));
* reader.beginObject();
* assertThat(reader.nextName()).isEqualTo("a");
* try (BufferedSource bufferedSource = reader.nextSource()) {
* assertThat(bufferedSource.readUtf8()).isEqualTo("[4, 5 ,6.0, {\"x\":7}, 8]");
* }
* assertThat(reader.nextName()).isEqualTo("b");
* assertThat(reader.nextInt()).isEqualTo(9);
* reader.endObject();
* ```
*
* This reads an entire value: composite objects like arrays and objects are returned in their
* entirety. The stream starts with the first character of the value (typically `[`, `{` * , or `"`)
* and ends with the last character of the object (typically `]`, `}`, or `"`).
*
* The returned source may not be used after any other method on this `JsonReader` is
* called. For example, the following code crashes with an exception:
*
* ```
* JsonReader reader = ...
* reader.beginArray();
* BufferedSource source = reader.nextSource();
* reader.endArray();
* source.readUtf8(); // Crash!
* ```
*
* The returned bytes are not validated. This method assumes the stream is well-formed JSON and
* only attempts to find the value's boundary in the byte stream. It is the caller's
* responsibility to check that the returned byte stream is a valid JSON value.
*
* Closing the returned source **does not** close this reader.
*/
@Throws(IOException::class)
public abstract fun nextSource(): BufferedSource
/**
* Skips the next value recursively. If it is an object or array, all nested elements are skipped.
* This method is intended for use when the JSON token stream contains unrecognized or unhandled
* values.
*
* This throws a [JsonDataException] if this parser has been configured to [failOnUnknown] values.
*/
@Throws(IOException::class)
public abstract fun skipValue()
/**
* Returns the value of the next token, consuming it. The result may be a string, number, boolean,
* null, map, or list, according to the JSON structure.
*
* @throws JsonDataException if the next token is not a literal value, if a JSON object has a
* duplicate key.
* @see JsonWriter.jsonValue
*/
@Throws(IOException::class)
public fun readJsonValue(): Any? {
return when (peek()) {
Token.BEGIN_ARRAY -> {
return buildList {
beginArray()
while (hasNext()) {
add(readJsonValue())
}
endArray()
}
}
Token.BEGIN_OBJECT -> {
return buildMap {
beginObject()
while (hasNext()) {
val name = nextName()
val value = readJsonValue()
val replaced = put(name, value)
if (replaced != null) {
throw JsonDataException("Map key '$name' has multiple values at path $path: $replaced and $value")
}
}
endObject()
}
}
Token.STRING -> nextString()
Token.NUMBER -> nextDouble()
Token.BOOLEAN -> nextBoolean()
Token.NULL -> nextNull<Any>()
else -> throw IllegalStateException("Expected a value but was ${peek()} at path $path")
}
}
/**
* Returns a new `JsonReader` that can read data from this `JsonReader` without
* consuming it. The returned reader becomes invalid once this one is next read or closed.
*
* For example, we can use `peekJson()` to lookahead and read the same data multiple
* times.
*
* ```java
* Buffer buffer = new Buffer();
* buffer.writeUtf8("[123, 456, 789]")
*
* JsonReader jsonReader = JsonReader.of(buffer);
* jsonReader.beginArray();
* jsonReader.nextInt(); // Returns 123, reader contains 456, 789 and ].
*
* JsonReader peek = reader.peekJson();
* peek.nextInt() // Returns 456.
* peek.nextInt() // Returns 789.
* peek.endArray()
*
* jsonReader.nextInt() // Returns 456, reader contains 789 and ].
* ```
*/
@CheckReturnValue
public abstract fun peekJson(): JsonReader
/**
* Returns a [JsonPath](http://goessner.net/articles/JsonPath/) to the current location
* in the JSON value.
*/
@get:CheckReturnValue
public val path: String
get() = getPath(stackSize, scopes, pathNames, pathIndices)
/** Returns the tag value for the given class key. */
@CheckReturnValue
public fun <T : Any> tag(clazz: Class<T>): T? {
@Suppress("UNCHECKED_CAST")
return tags?.let { it[clazz] as T? }
}
/** Assigns the tag value using the given class key and value. */
public fun <T : Any> setTag(clazz: Class<T>, value: T) {
require(clazz.isAssignableFrom(value.javaClass)) { "Tag value must be of type ${clazz.name}" }
val tagsToUse = tags ?: LinkedHashMap<Class<*>, Any>().also { tags = it }
tagsToUse[clazz] = value
}
/**
* 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 [nextString] to read a name value.
*
* In this example, calling this method allows two sequential calls to [nextString]:
*
* ```java
* JsonReader reader = JsonReader.of(new Buffer().writeUtf8("{\"a\":\"b\"}"));
* reader.beginObject();
* reader.promoteNameToValue();
* assertEquals("a", reader.nextString());
* assertEquals("b", reader.nextString());
* reader.endObject();
* ```
*/
@Throws(IOException::class)
public abstract fun promoteNameToValue()
/**
* A set of strings to be chosen with [selectName] or [selectString]. This prepares
* the encoded values of the strings so they can be read directly from the input source.
*/
public class Options private constructor(
internal val strings: Array<out String>,
internal val doubleQuoteSuffix: OkioOptions
) {
/** Returns a copy of this [Option's][Options] strings. */
public fun strings(): List<String> {
return buildList(strings.size) {
for (string in strings) {
add(string)
}
}
}
public companion object {
@CheckReturnValue
@JvmStatic
public fun of(vararg strings: String): Options {
return try {
val buffer = Buffer()
val result = Array(strings.size) { i ->
buffer.string(strings[i])
buffer.readByte() // Skip the leading double quote (but leave the trailing one).
buffer.readByteString()
}
Options(strings.clone(), OkioOptions.of(*result))
} catch (e: IOException) {
throw AssertionError(e)
}
}
}
}
/** A structure, name, or value type in a JSON-encoded string. */
public enum class Token {
/**
* The opening of a JSON array. Written using [JsonWriter.beginArray] and read using
* [JsonReader.beginArray].
*/
BEGIN_ARRAY,
/**
* The closing of a JSON array. Written using [JsonWriter.endArray] and read using [JsonReader.endArray].
*/
END_ARRAY,
/**
* The opening of a JSON object. Written using [JsonWriter.beginObject] and read using
* [JsonReader.beginObject].
*/
BEGIN_OBJECT,
/**
* The closing of a JSON object. Written using [JsonWriter.endObject] and read using
* [JsonReader.endObject].
*/
END_OBJECT,
/**
* A JSON property name. Within objects, tokens alternate between names and their values.
* Written using [JsonWriter.name] and read using [JsonReader.nextName]
*/
NAME,
/** A JSON string. */
STRING,
/**
* A JSON number represented in this API by a Java `double`, `long`, or `int`.
*/
NUMBER,
/** A JSON `true` or `false`. */
BOOLEAN,
/** A JSON `null`. */
NULL,
/**
* The end of the JSON stream. This sentinel value is returned by [JsonReader.peek] to
* signal that the JSON-encoded value has no more tokens.
*/
END_DOCUMENT
}
public companion object {
/** Returns a new instance that reads UTF-8 encoded JSON from `source`. */
@CheckReturnValue
@JvmStatic
public fun of(source: BufferedSource): JsonReader {
return JsonUtf8Reader(source)
}
}
}

View File

@@ -118,7 +118,7 @@ internal object StandardJsonAdapters : JsonAdapter.Factory {
override fun fromJson(reader: JsonReader): Float { override fun fromJson(reader: JsonReader): Float {
val value = reader.nextDouble().toFloat() val value = reader.nextDouble().toFloat()
// Double check for infinity after float conversion; many doubles > Float.MAX // Double check for infinity after float conversion; many doubles > Float.MAX
if (!reader.isLenient && value.isInfinite()) { if (!reader.lenient && value.isInfinite()) {
throw JsonDataException( throw JsonDataException(
"JSON forbids NaN and infinities: $value at path ${reader.path}" "JSON forbids NaN and infinities: $value at path ${reader.path}"
) )