mirror of
https://github.com/fankes/moshi.git
synced 2025-10-20 00:19:21 +08:00
Merge pull request #680 from square/jwilson.0923.peekvalue
Implement peekJson() for JsonValueReader
This commit is contained in:
@@ -180,10 +180,10 @@ public abstract class JsonReader implements Closeable {
|
|||||||
// The nesting stack. Using a manual array rather than an ArrayList saves 20%. This stack will
|
// 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
|
// grow itself up to 256 levels of nesting including the top-level document. Deeper nesting is
|
||||||
// prone to trigger StackOverflowErrors.
|
// prone to trigger StackOverflowErrors.
|
||||||
int stackSize = 0;
|
int stackSize;
|
||||||
int[] scopes = new int[32];
|
int[] scopes;
|
||||||
String[] pathNames = new String[32];
|
String[] pathNames;
|
||||||
int[] pathIndices = new int[32];
|
int[] pathIndices;
|
||||||
|
|
||||||
/** True to accept non-spec compliant JSON. */
|
/** True to accept non-spec compliant JSON. */
|
||||||
boolean lenient;
|
boolean lenient;
|
||||||
@@ -196,8 +196,21 @@ public abstract class JsonReader implements Closeable {
|
|||||||
return new JsonUtf8Reader(source);
|
return new JsonUtf8Reader(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Package-private to control subclasses.
|
||||||
JsonReader() {
|
JsonReader() {
|
||||||
// Package-private to control subclasses.
|
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) {
|
final void pushScope(int newTop) {
|
||||||
@@ -461,6 +474,32 @@ public abstract class JsonReader implements Closeable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* For example, we can use `peek()` 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.peekReader();
|
||||||
|
* peek.nextInt() // Returns 456.
|
||||||
|
* peek.nextInt() // Returns 789.
|
||||||
|
* peek.endArray()
|
||||||
|
*
|
||||||
|
* jsonReader.nextInt() // Returns 456, reader contains 789 and ].
|
||||||
|
* }</pre>
|
||||||
|
*/
|
||||||
|
// TODO(jwilson): make this public once it's supported in JsonUtf8Reader.
|
||||||
|
abstract JsonReader peekJson();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a <a href="http://goessner.net/articles/JsonPath/">JsonPath</a> to
|
* Returns a <a href="http://goessner.net/articles/JsonPath/">JsonPath</a> to
|
||||||
* the current location in the JSON value.
|
* the current location in the JSON value.
|
||||||
|
@@ -1051,6 +1051,10 @@ final class JsonUtf8Reader extends JsonReader {
|
|||||||
return found;
|
return found;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override JsonReader peekJson() {
|
||||||
|
throw new UnsupportedOperationException("TODO");
|
||||||
|
}
|
||||||
|
|
||||||
@Override public String toString() {
|
@Override public String toString() {
|
||||||
return "JsonReader(" + source + ")";
|
return "JsonReader(" + source + ")";
|
||||||
}
|
}
|
||||||
|
@@ -20,10 +20,11 @@ import java.math.BigDecimal;
|
|||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.ListIterator;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
import static com.squareup.moshi.JsonScope.CLOSED;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class reads a JSON document by traversing a Java object comprising maps, lists, and JSON
|
* This class reads a JSON document by traversing a Java object comprising maps, lists, and JSON
|
||||||
* primitives. It does depth-first traversal keeping a stack starting with the root object. During
|
* primitives. It does depth-first traversal keeping a stack starting with the root object. During
|
||||||
@@ -32,11 +33,11 @@ import javax.annotation.Nullable;
|
|||||||
* <ul>
|
* <ul>
|
||||||
* <li>The next element to act upon is on the top of the stack.
|
* <li>The next element to act upon is on the top of the stack.
|
||||||
* <li>When the top of the stack is a {@link List}, calling {@link #beginArray()} replaces the
|
* <li>When the top of the stack is a {@link List}, calling {@link #beginArray()} replaces the
|
||||||
* list with a {@link ListIterator}. The first element of the iterator is pushed on top of the
|
* list with a {@link JsonIterator}. The first element of the iterator is pushed on top of the
|
||||||
* iterator.
|
* iterator.
|
||||||
* <li>Similarly, when the top of the stack is a {@link Map}, calling {@link #beginObject()}
|
* <li>Similarly, when the top of the stack is a {@link Map}, calling {@link #beginObject()}
|
||||||
* replaces the map with an {@link Iterator} of its entries. The first element of the iterator
|
* replaces the map with an {@link JsonIterator} of its entries. The first element of the
|
||||||
* is pushed on top of the iterator.
|
* iterator is pushed on top of the iterator.
|
||||||
* <li>When the top of the stack is a {@link Map.Entry}, calling {@link #nextName()} returns the
|
* <li>When the top of the stack is a {@link Map.Entry}, calling {@link #nextName()} returns the
|
||||||
* entry's key and replaces the entry with its value on the stack.
|
* entry's key and replaces the entry with its value on the stack.
|
||||||
* <li>When an element is consumed it is popped. If the new top of the stack has a non-exhausted
|
* <li>When an element is consumed it is popped. If the new top of the stack has a non-exhausted
|
||||||
@@ -49,17 +50,31 @@ final class JsonValueReader extends JsonReader {
|
|||||||
/** Sentinel object pushed on {@link #stack} when the reader is closed. */
|
/** Sentinel object pushed on {@link #stack} when the reader is closed. */
|
||||||
private static final Object JSON_READER_CLOSED = new Object();
|
private static final Object JSON_READER_CLOSED = new Object();
|
||||||
|
|
||||||
private Object[] stack = new Object[32];
|
private Object[] stack;
|
||||||
|
|
||||||
JsonValueReader(Object root) {
|
JsonValueReader(Object root) {
|
||||||
scopes[stackSize] = JsonScope.NONEMPTY_DOCUMENT;
|
scopes[stackSize] = JsonScope.NONEMPTY_DOCUMENT;
|
||||||
|
stack = new Object[32];
|
||||||
stack[stackSize++] = root;
|
stack[stackSize++] = root;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Copy-constructor makes a deep copy for peeking. */
|
||||||
|
JsonValueReader(JsonValueReader copyFrom) {
|
||||||
|
super(copyFrom);
|
||||||
|
|
||||||
|
stack = copyFrom.stack.clone();
|
||||||
|
for (int i = 0; i < stackSize; i++) {
|
||||||
|
if (stack[i] instanceof JsonIterator) {
|
||||||
|
stack[i] = ((JsonIterator) stack[i]).clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override public void beginArray() throws IOException {
|
@Override public void beginArray() throws IOException {
|
||||||
List<?> peeked = require(List.class, Token.BEGIN_ARRAY);
|
List<?> peeked = require(List.class, Token.BEGIN_ARRAY);
|
||||||
|
|
||||||
ListIterator<?> iterator = peeked.listIterator();
|
JsonIterator iterator = new JsonIterator(
|
||||||
|
Token.END_ARRAY, peeked.toArray(new Object[peeked.size()]), 0);
|
||||||
stack[stackSize - 1] = iterator;
|
stack[stackSize - 1] = iterator;
|
||||||
scopes[stackSize - 1] = JsonScope.EMPTY_ARRAY;
|
scopes[stackSize - 1] = JsonScope.EMPTY_ARRAY;
|
||||||
pathIndices[stackSize - 1] = 0;
|
pathIndices[stackSize - 1] = 0;
|
||||||
@@ -71,8 +86,8 @@ final class JsonValueReader extends JsonReader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override public void endArray() throws IOException {
|
@Override public void endArray() throws IOException {
|
||||||
ListIterator<?> peeked = require(ListIterator.class, Token.END_ARRAY);
|
JsonIterator peeked = require(JsonIterator.class, Token.END_ARRAY);
|
||||||
if (peeked.hasNext()) {
|
if (peeked.endToken != Token.END_ARRAY || peeked.hasNext()) {
|
||||||
throw typeMismatch(peeked, Token.END_ARRAY);
|
throw typeMismatch(peeked, Token.END_ARRAY);
|
||||||
}
|
}
|
||||||
remove();
|
remove();
|
||||||
@@ -81,7 +96,8 @@ final class JsonValueReader extends JsonReader {
|
|||||||
@Override public void beginObject() throws IOException {
|
@Override public void beginObject() throws IOException {
|
||||||
Map<?, ?> peeked = require(Map.class, Token.BEGIN_OBJECT);
|
Map<?, ?> peeked = require(Map.class, Token.BEGIN_OBJECT);
|
||||||
|
|
||||||
Iterator<?> iterator = peeked.entrySet().iterator();
|
JsonIterator iterator = new JsonIterator(
|
||||||
|
Token.END_OBJECT, peeked.entrySet().toArray(new Object[peeked.size()]), 0);
|
||||||
stack[stackSize - 1] = iterator;
|
stack[stackSize - 1] = iterator;
|
||||||
scopes[stackSize - 1] = JsonScope.EMPTY_OBJECT;
|
scopes[stackSize - 1] = JsonScope.EMPTY_OBJECT;
|
||||||
|
|
||||||
@@ -92,8 +108,8 @@ final class JsonValueReader extends JsonReader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override public void endObject() throws IOException {
|
@Override public void endObject() throws IOException {
|
||||||
Iterator<?> peeked = require(Iterator.class, Token.END_OBJECT);
|
JsonIterator peeked = require(JsonIterator.class, Token.END_OBJECT);
|
||||||
if (peeked instanceof ListIterator || peeked.hasNext()) {
|
if (peeked.endToken != Token.END_OBJECT || peeked.hasNext()) {
|
||||||
throw typeMismatch(peeked, Token.END_OBJECT);
|
throw typeMismatch(peeked, Token.END_OBJECT);
|
||||||
}
|
}
|
||||||
pathNames[stackSize - 1] = null;
|
pathNames[stackSize - 1] = null;
|
||||||
@@ -112,8 +128,7 @@ final class JsonValueReader extends JsonReader {
|
|||||||
|
|
||||||
// If the top of the stack is an iterator, take its first element and push it on the stack.
|
// If the top of the stack is an iterator, take its first element and push it on the stack.
|
||||||
Object peeked = stack[stackSize - 1];
|
Object peeked = stack[stackSize - 1];
|
||||||
if (peeked instanceof ListIterator) return Token.END_ARRAY;
|
if (peeked instanceof JsonIterator) return ((JsonIterator) peeked).endToken;
|
||||||
if (peeked instanceof Iterator) return Token.END_OBJECT;
|
|
||||||
if (peeked instanceof List) return Token.BEGIN_ARRAY;
|
if (peeked instanceof List) return Token.BEGIN_ARRAY;
|
||||||
if (peeked instanceof Map) return Token.BEGIN_OBJECT;
|
if (peeked instanceof Map) return Token.BEGIN_OBJECT;
|
||||||
if (peeked instanceof Map.Entry) return Token.NAME;
|
if (peeked instanceof Map.Entry) return Token.NAME;
|
||||||
@@ -303,6 +318,10 @@ final class JsonValueReader extends JsonReader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override JsonReader peekJson() {
|
||||||
|
return new JsonValueReader(this);
|
||||||
|
}
|
||||||
|
|
||||||
@Override void promoteNameToValue() throws IOException {
|
@Override void promoteNameToValue() throws IOException {
|
||||||
if (hasNext()) {
|
if (hasNext()) {
|
||||||
String name = nextName();
|
String name = nextName();
|
||||||
@@ -313,7 +332,7 @@ final class JsonValueReader extends JsonReader {
|
|||||||
@Override public void close() throws IOException {
|
@Override public void close() throws IOException {
|
||||||
Arrays.fill(stack, 0, stackSize, null);
|
Arrays.fill(stack, 0, stackSize, null);
|
||||||
stack[0] = JSON_READER_CLOSED;
|
stack[0] = JSON_READER_CLOSED;
|
||||||
scopes[0] = JsonScope.CLOSED;
|
scopes[0] = CLOSED;
|
||||||
stackSize = 1;
|
stackSize = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -374,4 +393,33 @@ final class JsonValueReader extends JsonReader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static final class JsonIterator implements Iterator<Object>, Cloneable {
|
||||||
|
final Token endToken;
|
||||||
|
final Object[] array;
|
||||||
|
int next;
|
||||||
|
|
||||||
|
JsonIterator(Token endToken, Object[] array, int next) {
|
||||||
|
this.endToken = endToken;
|
||||||
|
this.array = array;
|
||||||
|
this.next = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public boolean hasNext() {
|
||||||
|
return next < array.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public Object next() {
|
||||||
|
return array[next++];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public void remove() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override protected JsonIterator clone() {
|
||||||
|
// No need to copy the array; it's read-only.
|
||||||
|
return new JsonIterator(endToken, array, next);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -96,9 +96,38 @@ abstract class JsonCodecFactory {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
final JsonCodecFactory valuePeek = new JsonCodecFactory() {
|
||||||
|
@Override public JsonReader newReader(String json) throws IOException {
|
||||||
|
return value.newReader(json).peekJson();
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(jwilson): fix precision checks and delete his method.
|
||||||
|
@Override boolean implementsStrictPrecision() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override JsonWriter newWriter() {
|
||||||
|
return value.newWriter();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override String json() {
|
||||||
|
return value.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(jwilson): support BigDecimal and BigInteger and delete his method.
|
||||||
|
@Override boolean supportsBigNumbers() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public String toString() {
|
||||||
|
return "ValuePeek";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return Arrays.asList(
|
return Arrays.asList(
|
||||||
new Object[] { utf8 },
|
new Object[] { utf8 },
|
||||||
new Object[] { value });
|
new Object[] { value },
|
||||||
|
new Object[] { valuePeek });
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract JsonReader newReader(String json) throws IOException;
|
abstract JsonReader newReader(String json) throws IOException;
|
||||||
|
@@ -985,4 +985,103 @@ public final class JsonReaderTest {
|
|||||||
reader.readJsonValue();
|
reader.readJsonValue();
|
||||||
assertThat(reader.hasNext()).isFalse();
|
assertThat(reader.hasNext()).isFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test public void basicPeekJson() throws IOException {
|
||||||
|
JsonReader reader = newReader("{\"a\":12,\"b\":[34,56],\"c\":78}");
|
||||||
|
assumeTrue(reader instanceof JsonValueReader); // Not implemented for JsonUtf8Reader yet!
|
||||||
|
reader.beginObject();
|
||||||
|
assertThat(reader.nextName()).isEqualTo("a");
|
||||||
|
assertThat(reader.nextInt()).isEqualTo(12);
|
||||||
|
assertThat(reader.nextName()).isEqualTo("b");
|
||||||
|
reader.beginArray();
|
||||||
|
assertThat(reader.nextInt()).isEqualTo(34);
|
||||||
|
|
||||||
|
// Peek.
|
||||||
|
JsonReader peekReader = reader.peekJson();
|
||||||
|
assertThat(peekReader.nextInt()).isEqualTo(56);
|
||||||
|
peekReader.endArray();
|
||||||
|
assertThat(peekReader.nextName()).isEqualTo("c");
|
||||||
|
assertThat(peekReader.nextInt()).isEqualTo(78);
|
||||||
|
peekReader.endObject();
|
||||||
|
assertThat(peekReader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT);
|
||||||
|
|
||||||
|
// Read again.
|
||||||
|
assertThat(reader.nextInt()).isEqualTo(56);
|
||||||
|
reader.endArray();
|
||||||
|
assertThat(reader.nextName()).isEqualTo("c");
|
||||||
|
assertThat(reader.nextInt()).isEqualTo(78);
|
||||||
|
reader.endObject();
|
||||||
|
assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We have a document that requires 12 operations to read. We read it step-by-step with one real
|
||||||
|
* reader. Before each of the real reader’s operations we create a peeking reader and let it read
|
||||||
|
* the rest of the document.
|
||||||
|
*/
|
||||||
|
@Test public void peekJsonReader() throws IOException {
|
||||||
|
JsonReader reader = newReader("[12,34,{\"a\":56,\"b\":78},90]");
|
||||||
|
assumeTrue(reader instanceof JsonValueReader); // Not implemented for JsonUtf8Reader yet!
|
||||||
|
for (int i = 0; i < 12; i++) {
|
||||||
|
readPeek12Steps(reader.peekJson(), i, 12);
|
||||||
|
readPeek12Steps(reader, i, i + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read a fragment of {@code reader}. This assumes the fixed document defined in {@link
|
||||||
|
* #peekJsonReader} and reads a range of it on each call.
|
||||||
|
*/
|
||||||
|
private void readPeek12Steps(JsonReader reader, int from, int until) throws IOException {
|
||||||
|
switch (from) {
|
||||||
|
case 0:
|
||||||
|
if (until == 0) break;
|
||||||
|
reader.beginArray();
|
||||||
|
assertThat(reader.getPath()).isEqualTo("$[0]");
|
||||||
|
case 1:
|
||||||
|
if (until == 1) break;
|
||||||
|
assertThat(reader.nextInt()).isEqualTo(12);
|
||||||
|
assertThat(reader.getPath()).isEqualTo("$[1]");
|
||||||
|
case 2:
|
||||||
|
if (until == 2) break;
|
||||||
|
assertThat(reader.nextInt()).isEqualTo(34);
|
||||||
|
assertThat(reader.getPath()).isEqualTo("$[2]");
|
||||||
|
case 3:
|
||||||
|
if (until == 3) break;
|
||||||
|
reader.beginObject();
|
||||||
|
assertThat(reader.getPath()).isEqualTo("$[2].");
|
||||||
|
case 4:
|
||||||
|
if (until == 4) break;
|
||||||
|
assertThat(reader.nextName()).isEqualTo("a");
|
||||||
|
assertThat(reader.getPath()).isEqualTo("$[2].a");
|
||||||
|
case 5:
|
||||||
|
if (until == 5) break;
|
||||||
|
assertThat(reader.nextInt()).isEqualTo(56);
|
||||||
|
assertThat(reader.getPath()).isEqualTo("$[2].a");
|
||||||
|
case 6:
|
||||||
|
if (until == 6) break;
|
||||||
|
assertThat(reader.nextName()).isEqualTo("b");
|
||||||
|
assertThat(reader.getPath()).isEqualTo("$[2].b");
|
||||||
|
case 7:
|
||||||
|
if (until == 7) break;
|
||||||
|
assertThat(reader.nextInt()).isEqualTo(78);
|
||||||
|
assertThat(reader.getPath()).isEqualTo("$[2].b");
|
||||||
|
case 8:
|
||||||
|
if (until == 8) break;
|
||||||
|
reader.endObject();
|
||||||
|
assertThat(reader.getPath()).isEqualTo("$[3]");
|
||||||
|
case 9:
|
||||||
|
if (until == 9) break;
|
||||||
|
assertThat(reader.nextInt()).isEqualTo(90);
|
||||||
|
assertThat(reader.getPath()).isEqualTo("$[4]");
|
||||||
|
case 10:
|
||||||
|
if (until == 10) break;
|
||||||
|
reader.endArray();
|
||||||
|
assertThat(reader.getPath()).isEqualTo("$");
|
||||||
|
case 11:
|
||||||
|
if (until == 11) break;
|
||||||
|
assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT);
|
||||||
|
assertThat(reader.getPath()).isEqualTo("$");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user