Implement peekJson() for JsonValueReader
The JsonUtf8Reader variant relies on an update to Okio that we're not quite ready for. This one became straightforward after I changed out the iterators to be cloneable. That's necessary to split one iterator into two. It's too bad the platform's built-in iterators are not cloneable; that would have been convenient and potentially more efficient. Related to https://github.com/square/moshi/issues/672
This commit is contained in:
parent
d6ad1b8bad
commit
00dcac60d4
5 changed files with 239 additions and 20 deletions
|
@ -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.
|
||||
*
|
||||
* <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
|
||||
* the current location in the JSON value.
|
||||
|
|
|
@ -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 + ")";
|
||||
}
|
||||
|
|
|
@ -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;
|
|||
* <ul>
|
||||
* <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
|
||||
* 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.
|
||||
* <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
|
||||
* 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.
|
||||
* <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.
|
||||
* <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. */
|
||||
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<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(
|
||||
new Object[] { utf8 },
|
||||
new Object[] { value });
|
||||
new Object[] { value },
|
||||
new Object[] { valuePeek });
|
||||
}
|
||||
|
||||
abstract JsonReader newReader(String json) throws IOException;
|
||||
|
|
|
@ -985,4 +985,103 @@ public final class JsonReaderTest {
|
|||
reader.readJsonValue();
|
||||
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("$");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue