diff --git a/moshi/src/main/java/com/squareup/moshi/JsonReader.java b/moshi/src/main/java/com/squareup/moshi/JsonReader.java
index eabc01a..e04acf9 100644
--- a/moshi/src/main/java/com/squareup/moshi/JsonReader.java
+++ b/moshi/src/main/java/com/squareup/moshi/JsonReader.java
@@ -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
// grow itself up to 256 levels of nesting including the top-level document. Deeper nesting is
// prone to trigger StackOverflowErrors.
- int stackSize = 0;
- int[] scopes = new int[32];
- String[] pathNames = new String[32];
- int[] pathIndices = new int[32];
+ int stackSize;
+ int[] scopes;
+ String[] pathNames;
+ int[] pathIndices;
/** True to accept non-spec compliant JSON. */
boolean lenient;
@@ -196,8 +196,21 @@ public abstract class JsonReader implements Closeable {
return new JsonUtf8Reader(source);
}
+ // Package-private to control subclasses.
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) {
@@ -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.
+ *
+ *
+ */
+ // TODO(jwilson): make this public once it's supported in JsonUtf8Reader.
+ abstract JsonReader peekJson();
+
/**
* Returns a JsonPath to
* the current location in the JSON value.
diff --git a/moshi/src/main/java/com/squareup/moshi/JsonUtf8Reader.java b/moshi/src/main/java/com/squareup/moshi/JsonUtf8Reader.java
index 14e12a5..16c478a 100644
--- a/moshi/src/main/java/com/squareup/moshi/JsonUtf8Reader.java
+++ b/moshi/src/main/java/com/squareup/moshi/JsonUtf8Reader.java
@@ -1059,6 +1059,10 @@ final class JsonUtf8Reader extends JsonReader {
return false;
}
+ @Override JsonReader peekJson() {
+ throw new UnsupportedOperationException("TODO");
+ }
+
@Override public String toString() {
return "JsonReader(" + source + ")";
}
diff --git a/moshi/src/main/java/com/squareup/moshi/JsonValueReader.java b/moshi/src/main/java/com/squareup/moshi/JsonValueReader.java
index fb3991b..4246d3d 100644
--- a/moshi/src/main/java/com/squareup/moshi/JsonValueReader.java
+++ b/moshi/src/main/java/com/squareup/moshi/JsonValueReader.java
@@ -20,10 +20,11 @@ import java.math.BigDecimal;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
-import java.util.ListIterator;
import java.util.Map;
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
* primitives. It does depth-first traversal keeping a stack starting with the root object. During
@@ -32,11 +33,11 @@ import javax.annotation.Nullable;
*
*
The next element to act upon is on the top of the stack.
*
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.
*
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
- * is pushed on top of the iterator.
+ * replaces the map with an {@link JsonIterator} of its entries. The first element of the
+ * iterator is pushed on top of the iterator.
*
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.
*
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. */
private static final Object JSON_READER_CLOSED = new Object();
- private Object[] stack = new Object[32];
+ private Object[] stack;
JsonValueReader(Object root) {
scopes[stackSize] = JsonScope.NONEMPTY_DOCUMENT;
+ stack = new Object[32];
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 {
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;
scopes[stackSize - 1] = JsonScope.EMPTY_ARRAY;
pathIndices[stackSize - 1] = 0;
@@ -71,8 +86,8 @@ final class JsonValueReader extends JsonReader {
}
@Override public void endArray() throws IOException {
- ListIterator> peeked = require(ListIterator.class, Token.END_ARRAY);
- if (peeked.hasNext()) {
+ JsonIterator peeked = require(JsonIterator.class, Token.END_ARRAY);
+ if (peeked.endToken != Token.END_ARRAY || peeked.hasNext()) {
throw typeMismatch(peeked, Token.END_ARRAY);
}
remove();
@@ -81,7 +96,8 @@ final class JsonValueReader extends JsonReader {
@Override public void beginObject() throws IOException {
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;
scopes[stackSize - 1] = JsonScope.EMPTY_OBJECT;
@@ -92,8 +108,8 @@ final class JsonValueReader extends JsonReader {
}
@Override public void endObject() throws IOException {
- Iterator> peeked = require(Iterator.class, Token.END_OBJECT);
- if (peeked instanceof ListIterator || peeked.hasNext()) {
+ JsonIterator peeked = require(JsonIterator.class, Token.END_OBJECT);
+ if (peeked.endToken != Token.END_OBJECT || peeked.hasNext()) {
throw typeMismatch(peeked, Token.END_OBJECT);
}
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.
Object peeked = stack[stackSize - 1];
- if (peeked instanceof ListIterator) return Token.END_ARRAY;
- if (peeked instanceof Iterator) return Token.END_OBJECT;
+ if (peeked instanceof JsonIterator) return ((JsonIterator) peeked).endToken;
if (peeked instanceof List) return Token.BEGIN_ARRAY;
if (peeked instanceof Map) return Token.BEGIN_OBJECT;
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 {
if (hasNext()) {
String name = nextName();
@@ -313,7 +332,7 @@ final class JsonValueReader extends JsonReader {
@Override public void close() throws IOException {
Arrays.fill(stack, 0, stackSize, null);
stack[0] = JSON_READER_CLOSED;
- scopes[0] = JsonScope.CLOSED;
+ scopes[0] = CLOSED;
stackSize = 1;
}
@@ -374,4 +393,33 @@ final class JsonValueReader extends JsonReader {
}
}
}
+
+ static final class JsonIterator implements Iterator