Promote stack management to the JsonReader/JsonWriter supertypes.

It turns out that we can reuse a lot of code with inheritance. Who knew?
This commit is contained in:
jwilson
2017-01-24 21:50:19 -05:00
parent a90b6c7740
commit 4b7ced70e4
7 changed files with 184 additions and 276 deletions

View File

@@ -57,35 +57,11 @@ final class BufferedSinkJsonWriter extends JsonWriter {
/** The output data, containing at most one top-level array or object. */ /** The output data, containing at most one top-level array or object. */
private final BufferedSink sink; private final BufferedSink sink;
// The nesting stack. Using a manual array rather than an ArrayList saves 20%. This stack permits /** The name/value separator; either ":" or ": ". */
// up to 32 levels of nesting including the top-level document. Deeper nesting is prone to trigger
// StackOverflowErrors.
private final int[] stack = new int[32];
private int stackSize = 0;
{
push(EMPTY_DOCUMENT);
}
private final String[] pathNames = new String[32];
private final 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 String separator = ":";
private boolean lenient;
private String deferredName; private String deferredName;
private boolean serializeNulls;
private boolean promoteNameToValue; private boolean promoteNameToValue;
BufferedSinkJsonWriter(BufferedSink sink) { BufferedSinkJsonWriter(BufferedSink sink) {
@@ -93,36 +69,12 @@ final class BufferedSinkJsonWriter extends JsonWriter {
throw new NullPointerException("sink == null"); throw new NullPointerException("sink == null");
} }
this.sink = sink; this.sink = sink;
pushScope(EMPTY_DOCUMENT);
} }
@Override public final void setIndent(String indent) { @Override public void setIndent(String indent) {
if (indent.length() == 0) { super.setIndent(indent);
this.indent = null; this.separator = !indent.isEmpty() ? ": " : ":";
this.separator = ":";
} else {
this.indent = indent;
this.separator = ": ";
}
}
@Override public final String getIndent() {
return indent != null ? indent : "";
}
@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 { @Override public JsonWriter beginArray() throws IOException {
@@ -150,7 +102,7 @@ final class BufferedSinkJsonWriter extends JsonWriter {
*/ */
private JsonWriter open(int empty, String openBracket) throws IOException { private JsonWriter open(int empty, String openBracket) throws IOException {
beforeValue(); beforeValue();
push(empty); pushScope(empty);
pathIndices[stackSize - 1] = 0; pathIndices[stackSize - 1] = 0;
sink.writeUtf8(openBracket); sink.writeUtf8(openBracket);
return this; return this;
@@ -160,9 +112,8 @@ final class BufferedSinkJsonWriter extends JsonWriter {
* Closes the current scope by appending any necessary whitespace and the * Closes the current scope by appending any necessary whitespace and the
* given bracket. * given bracket.
*/ */
private JsonWriter close(int empty, int nonempty, String closeBracket) private JsonWriter close(int empty, int nonempty, String closeBracket) throws IOException {
throws IOException { int context = peekScope();
int context = peek();
if (context != nonempty && context != empty) { if (context != nonempty && context != empty) {
throw new IllegalStateException("Nesting problem."); throw new IllegalStateException("Nesting problem.");
} }
@@ -180,30 +131,6 @@ final class BufferedSinkJsonWriter extends JsonWriter {
return this; return this;
} }
private void push(int newTop) {
if (stackSize == stack.length) {
throw new JsonDataException("Nesting too deep at " + getPath() + ": circular reference?");
}
stack[stackSize++] = newTop;
}
/**
* Returns the scope 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 { @Override public JsonWriter name(String name) throws IOException {
if (name == null) { if (name == null) {
throw new NullPointerException("name == null"); throw new NullPointerException("name == null");
@@ -337,7 +264,7 @@ final class BufferedSinkJsonWriter extends JsonWriter {
sink.close(); sink.close();
int size = stackSize; int size = stackSize;
if (size > 1 || size == 1 && stack[size - 1] != NONEMPTY_DOCUMENT) { if (size > 1 || size == 1 && scopes[size - 1] != NONEMPTY_DOCUMENT) {
throw new IOException("Incomplete document"); throw new IOException("Incomplete document");
} }
stackSize = 0; stackSize = 0;
@@ -395,7 +322,7 @@ final class BufferedSinkJsonWriter extends JsonWriter {
* adjusts the stack to expect the name's value. * adjusts the stack to expect the name's value.
*/ */
private void beforeName() throws IOException { private void beforeName() throws IOException {
int context = peek(); int context = peekScope();
if (context == NONEMPTY_OBJECT) { // first in object if (context == NONEMPTY_OBJECT) { // first in object
sink.writeByte(','); sink.writeByte(',');
} else if (context != EMPTY_OBJECT) { // not in an object! } else if (context != EMPTY_OBJECT) { // not in an object!
@@ -412,7 +339,7 @@ final class BufferedSinkJsonWriter extends JsonWriter {
*/ */
@SuppressWarnings("fallthrough") @SuppressWarnings("fallthrough")
private void beforeValue() throws IOException { private void beforeValue() throws IOException {
switch (peek()) { switch (peekScope()) {
case NONEMPTY_DOCUMENT: case NONEMPTY_DOCUMENT:
if (!lenient) { if (!lenient) {
throw new IllegalStateException( throw new IllegalStateException(
@@ -444,14 +371,10 @@ final class BufferedSinkJsonWriter extends JsonWriter {
} }
@Override void promoteNameToValue() throws IOException { @Override void promoteNameToValue() throws IOException {
int context = peek(); int context = peekScope();
if (context != NONEMPTY_OBJECT && context != EMPTY_OBJECT) { if (context != NONEMPTY_OBJECT && context != EMPTY_OBJECT) {
throw new IllegalStateException("Nesting problem."); throw new IllegalStateException("Nesting problem.");
} }
promoteNameToValue = true; promoteNameToValue = true;
} }
@Override public String getPath() {
return JsonScope.getPath(stackSize, stack, pathNames, pathIndices);
}
} }

View File

@@ -63,12 +63,6 @@ final class BufferedSourceJsonReader extends JsonReader {
private static final int NUMBER_CHAR_EXP_SIGN = 6; private static final int NUMBER_CHAR_EXP_SIGN = 6;
private static final int NUMBER_CHAR_EXP_DIGIT = 7; 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. */ /** The input JSON. */
private final BufferedSource source; private final BufferedSource source;
private final Buffer buffer; private final Buffer buffer;
@@ -94,41 +88,13 @@ final class BufferedSourceJsonReader extends JsonReader {
*/ */
private String peekedString; private String peekedString;
// The nesting stack. Using a manual array rather than an ArrayList saves 20%. This stack permits
// up to 32 levels of nesting including the top-level document. Deeper nesting is prone to trigger
// StackOverflowErrors.
private final int[] stack = new int[32];
private int stackSize = 0;
{
stack[stackSize++] = JsonScope.EMPTY_DOCUMENT;
}
private final String[] pathNames = new String[32];
private final int[] pathIndices = new int[32];
BufferedSourceJsonReader(BufferedSource source) { BufferedSourceJsonReader(BufferedSource source) {
if (source == null) { if (source == null) {
throw new NullPointerException("source == null"); throw new NullPointerException("source == null");
} }
this.source = source; this.source = source;
this.buffer = source.buffer(); this.buffer = source.buffer();
} pushScope(JsonScope.EMPTY_DOCUMENT);
@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 { @Override public void beginArray() throws IOException {
@@ -137,7 +103,7 @@ final class BufferedSourceJsonReader extends JsonReader {
p = doPeek(); p = doPeek();
} }
if (p == PEEKED_BEGIN_ARRAY) { if (p == PEEKED_BEGIN_ARRAY) {
push(JsonScope.EMPTY_ARRAY); pushScope(JsonScope.EMPTY_ARRAY);
pathIndices[stackSize - 1] = 0; pathIndices[stackSize - 1] = 0;
peeked = PEEKED_NONE; peeked = PEEKED_NONE;
} else { } else {
@@ -167,7 +133,7 @@ final class BufferedSourceJsonReader extends JsonReader {
p = doPeek(); p = doPeek();
} }
if (p == PEEKED_BEGIN_OBJECT) { if (p == PEEKED_BEGIN_OBJECT) {
push(JsonScope.EMPTY_OBJECT); pushScope(JsonScope.EMPTY_OBJECT);
peeked = PEEKED_NONE; peeked = PEEKED_NONE;
} else { } else {
throw new JsonDataException("Expected BEGIN_OBJECT but was " + peek() throw new JsonDataException("Expected BEGIN_OBJECT but was " + peek()
@@ -240,9 +206,9 @@ final class BufferedSourceJsonReader extends JsonReader {
} }
private int doPeek() throws IOException { private int doPeek() throws IOException {
int peekStack = stack[stackSize - 1]; int peekStack = scopes[stackSize - 1];
if (peekStack == JsonScope.EMPTY_ARRAY) { if (peekStack == JsonScope.EMPTY_ARRAY) {
stack[stackSize - 1] = JsonScope.NONEMPTY_ARRAY; scopes[stackSize - 1] = JsonScope.NONEMPTY_ARRAY;
} else if (peekStack == JsonScope.NONEMPTY_ARRAY) { } else if (peekStack == JsonScope.NONEMPTY_ARRAY) {
// Look for a comma before the next element. // Look for a comma before the next element.
int c = nextNonWhitespace(true); int c = nextNonWhitespace(true);
@@ -258,7 +224,7 @@ final class BufferedSourceJsonReader extends JsonReader {
throw syntaxError("Unterminated array"); throw syntaxError("Unterminated array");
} }
} else if (peekStack == JsonScope.EMPTY_OBJECT || peekStack == JsonScope.NONEMPTY_OBJECT) { } else if (peekStack == JsonScope.EMPTY_OBJECT || peekStack == JsonScope.NONEMPTY_OBJECT) {
stack[stackSize - 1] = JsonScope.DANGLING_NAME; scopes[stackSize - 1] = JsonScope.DANGLING_NAME;
// Look for a comma before the next element. // Look for a comma before the next element.
if (peekStack == JsonScope.NONEMPTY_OBJECT) { if (peekStack == JsonScope.NONEMPTY_OBJECT) {
int c = nextNonWhitespace(true); int c = nextNonWhitespace(true);
@@ -299,7 +265,7 @@ final class BufferedSourceJsonReader extends JsonReader {
} }
} }
} else if (peekStack == JsonScope.DANGLING_NAME) { } else if (peekStack == JsonScope.DANGLING_NAME) {
stack[stackSize - 1] = JsonScope.NONEMPTY_OBJECT; scopes[stackSize - 1] = JsonScope.NONEMPTY_OBJECT;
// Look for a colon before the value. // Look for a colon before the value.
int c = nextNonWhitespace(true); int c = nextNonWhitespace(true);
buffer.readByte(); // Consume ':'. buffer.readByte(); // Consume ':'.
@@ -316,7 +282,7 @@ final class BufferedSourceJsonReader extends JsonReader {
throw syntaxError("Expected ':'"); throw syntaxError("Expected ':'");
} }
} else if (peekStack == JsonScope.EMPTY_DOCUMENT) { } else if (peekStack == JsonScope.EMPTY_DOCUMENT) {
stack[stackSize - 1] = JsonScope.NONEMPTY_DOCUMENT; scopes[stackSize - 1] = JsonScope.NONEMPTY_DOCUMENT;
} else if (peekStack == JsonScope.NONEMPTY_DOCUMENT) { } else if (peekStack == JsonScope.NONEMPTY_DOCUMENT) {
int c = nextNonWhitespace(false); int c = nextNonWhitespace(false);
if (c == -1) { if (c == -1) {
@@ -923,7 +889,7 @@ final class BufferedSourceJsonReader extends JsonReader {
@Override public void close() throws IOException { @Override public void close() throws IOException {
peeked = PEEKED_NONE; peeked = PEEKED_NONE;
stack[0] = JsonScope.CLOSED; scopes[0] = JsonScope.CLOSED;
stackSize = 1; stackSize = 1;
buffer.clear(); buffer.clear();
source.close(); source.close();
@@ -941,10 +907,10 @@ final class BufferedSourceJsonReader extends JsonReader {
} }
if (p == PEEKED_BEGIN_ARRAY) { if (p == PEEKED_BEGIN_ARRAY) {
push(JsonScope.EMPTY_ARRAY); pushScope(JsonScope.EMPTY_ARRAY);
count++; count++;
} else if (p == PEEKED_BEGIN_OBJECT) { } else if (p == PEEKED_BEGIN_OBJECT) {
push(JsonScope.EMPTY_OBJECT); pushScope(JsonScope.EMPTY_OBJECT);
count++; count++;
} else if (p == PEEKED_END_ARRAY) { } else if (p == PEEKED_END_ARRAY) {
stackSize--; stackSize--;
@@ -968,13 +934,6 @@ final class BufferedSourceJsonReader extends JsonReader {
pathNames[stackSize - 1] = "null"; pathNames[stackSize - 1] = "null";
} }
private void push(int newTop) {
if (stackSize == stack.length) {
throw new JsonDataException("Nesting too deep at " + getPath());
}
stack[stackSize++] = newTop;
}
/** /**
* Returns the next character in the stream that is neither whitespace nor a * 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 * part of a comment. When this returns, the returned character is always at
@@ -1083,10 +1042,6 @@ final class BufferedSourceJsonReader extends JsonReader {
return "JsonReader(" + source + ")"; 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 * 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 * backslash. The backslash '\' should have already been read. This supports both unicode escapes
@@ -1151,14 +1106,6 @@ final class BufferedSourceJsonReader extends JsonReader {
} }
} }
/**
* Throws a new IO exception with the given message and a context snippet
* with this reader's content.
*/
private JsonEncodingException syntaxError(String message) throws JsonEncodingException {
throw new JsonEncodingException(message + " at path " + getPath());
}
@Override void promoteNameToValue() throws IOException { @Override void promoteNameToValue() throws IOException {
if (hasNext()) { if (hasNext()) {
peekedString = nextName(); peekedString = nextName();

View File

@@ -171,9 +171,21 @@ import okio.ByteString;
* of this class are not thread safe. * of this class are not thread safe.
*/ */
public abstract class JsonReader implements Closeable { public abstract class JsonReader implements Closeable {
/** // The nesting stack. Using a manual array rather than an ArrayList saves 20%. This stack permits
* Returns a new instance that reads a JSON-encoded stream from {@code source}. // up to 32 levels of nesting including the top-level document. Deeper nesting is prone to trigger
*/ // StackOverflowErrors.
int stackSize = 0;
final int[] scopes = new int[32];
final String[] pathNames = new String[32];
final int[] pathIndices = new int[32];
/** True to accept non-spec compliant JSON */
boolean lenient;
/** True to throw a {@link JsonDataException} on any attempt to call {@link #skipValue()}. */
boolean failOnUnknown;
/** Returns a new instance that reads a JSON-encoded stream from {@code source}. */
public static JsonReader of(BufferedSource source) { public static JsonReader of(BufferedSource source) {
return new BufferedSourceJsonReader(source); return new BufferedSourceJsonReader(source);
} }
@@ -182,6 +194,31 @@ public abstract class JsonReader implements Closeable {
// Package-private to control subclasses. // Package-private to control subclasses.
} }
final void pushScope(int newTop) {
if (stackSize == scopes.length) {
throw new JsonDataException("Nesting too deep at " + getPath());
}
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(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 * Configure this parser to be liberal in what it accepts. By default
* this parser is strict and only accepts JSON as specified by <a * this parser is strict and only accepts JSON as specified by <a
@@ -207,12 +244,16 @@ public abstract class JsonReader implements Closeable {
* <li>Name/value pairs separated by {@code ;} instead of {@code ,}. * <li>Name/value pairs separated by {@code ;} instead of {@code ,}.
* </ul> * </ul>
*/ */
public abstract void setLenient(boolean lenient); public final void setLenient(boolean lenient) {
this.lenient = lenient;
}
/** /**
* Returns true if this parser is liberal in what it accepts. * Returns true if this parser is liberal in what it accepts.
*/ */
public abstract boolean isLenient(); public final boolean isLenient() {
return lenient;
}
/** /**
* Configure whether this parser throws a {@link JsonDataException} when {@link #skipValue} is * Configure whether this parser throws a {@link JsonDataException} when {@link #skipValue} is
@@ -222,12 +263,16 @@ public abstract class JsonReader implements Closeable {
* useful in development and debugging because it means a typo like "locatiom" will be detected * 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. * early. It's potentially harmful in production because it complicates revising a JSON schema.
*/ */
public abstract void setFailOnUnknown(boolean failOnUnknown); public final void setFailOnUnknown(boolean failOnUnknown) {
this.failOnUnknown = failOnUnknown;
}
/** /**
* Returns true if this parser forbids skipping values. * Returns true if this parser forbids skipping values.
*/ */
public abstract boolean failOnUnknown(); public final boolean failOnUnknown() {
return failOnUnknown;
}
/** /**
* Consumes the next token from the JSON stream and asserts that it is the beginning of a new * Consumes the next token from the JSON stream and asserts that it is the beginning of a new
@@ -349,7 +394,9 @@ public abstract class JsonReader implements Closeable {
* 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.
*/ */
public abstract String getPath(); public final String getPath() {
return JsonScope.getPath(stackSize, scopes, pathNames, pathIndices);
}
/** /**
* Changes the reader to treat the next name as a string value. This is useful for map adapters so * Changes the reader to treat the next name as a string value. This is useful for map adapters so

View File

@@ -116,9 +116,23 @@ import okio.BufferedSink;
* malformed JSON string will fail with an {@link IllegalStateException}. * malformed JSON string will fail with an {@link IllegalStateException}.
*/ */
public abstract class JsonWriter implements Closeable, Flushable { public abstract class JsonWriter implements Closeable, Flushable {
// The nesting stack. Using a manual array rather than an ArrayList saves 20%. This stack permits
// up to 32 levels of nesting including the top-level document. Deeper nesting is prone to trigger
// StackOverflowErrors.
int stackSize = 0;
final int[] scopes = new int[32];
final String[] pathNames = new String[32];
final int[] pathIndices = new int[32];
/** /**
* Returns a new instance that writes a JSON-encoded stream to {@code sink}. * A string containing a full set of spaces for a single level of indentation, or null for no
* pretty printing.
*/ */
String indent;
boolean lenient;
boolean serializeNulls;
/** Returns a new instance that writes a JSON-encoded stream to {@code sink}. */
public static JsonWriter of(BufferedSink sink) { public static JsonWriter of(BufferedSink sink) {
return new BufferedSinkJsonWriter(sink); return new BufferedSinkJsonWriter(sink);
} }
@@ -127,6 +141,26 @@ public abstract class JsonWriter implements Closeable, Flushable {
// Package-private to control subclasses. // Package-private to control subclasses.
} }
/** Returns the scope on the top of the stack. */
final int peekScope() {
if (stackSize == 0) {
throw new IllegalStateException("JsonWriter is closed.");
}
return scopes[stackSize - 1];
}
final void pushScope(int newTop) {
if (stackSize == scopes.length) {
throw new JsonDataException("Nesting too deep at " + getPath() + ": circular reference?");
}
scopes[stackSize++] = newTop;
}
/** Replace the value on the top of the stack with the given value. */
final void replaceTop(int topOfStack) {
scopes[stackSize - 1] = topOfStack;
}
/** /**
* Sets the indentation string to be repeated for each level of indentation * Sets the indentation string to be repeated for each level of indentation
* in the encoded document. If {@code indent.isEmpty()} the encoded document * in the encoded document. If {@code indent.isEmpty()} the encoded document
@@ -135,13 +169,17 @@ public abstract class JsonWriter implements Closeable, Flushable {
* *
* @param indent a string containing only whitespace. * @param indent a string containing only whitespace.
*/ */
public abstract void setIndent(String indent); public void setIndent(String indent) {
this.indent = !indent.isEmpty() ? indent : null;
}
/** /**
* Returns a string containing only whitespace, used for each level of * Returns a string containing only whitespace, used for each level of
* indentation. If empty, the encoded document will be compact. * indentation. If empty, the encoded document will be compact.
*/ */
public abstract String getIndent(); public final String getIndent() {
return indent != null ? indent : "";
}
/** /**
* Configure this writer to relax its syntax rules. By default, this writer * Configure this writer to relax its syntax rules. By default, this writer
@@ -155,24 +193,32 @@ public abstract class JsonWriter implements Closeable, Flushable {
* Double#isInfinite() infinities}. * Double#isInfinite() infinities}.
* </ul> * </ul>
*/ */
public abstract void setLenient(boolean lenient); public final void setLenient(boolean lenient) {
this.lenient = lenient;
}
/** /**
* Returns true if this writer has relaxed syntax rules. * Returns true if this writer has relaxed syntax rules.
*/ */
public abstract boolean isLenient(); public final boolean isLenient() {
return lenient;
}
/** /**
* Sets whether object members are serialized when their value is null. * Sets whether object members are serialized when their value is null.
* This has no impact on array elements. The default is false. * This has no impact on array elements. The default is false.
*/ */
public abstract void setSerializeNulls(boolean serializeNulls); public final void setSerializeNulls(boolean serializeNulls) {
this.serializeNulls = serializeNulls;
}
/** /**
* Returns true if object members are serialized when their value is null. * Returns true if object members are serialized when their value is null.
* This has no impact on array elements. The default is false. * This has no impact on array elements. The default is false.
*/ */
public abstract boolean getSerializeNulls(); public final boolean getSerializeNulls() {
return serializeNulls;
}
/** /**
* Begins encoding a new array. Each call to this method must be paired with * Begins encoding a new array. Each call to this method must be paired with
@@ -276,5 +322,7 @@ public abstract class JsonWriter implements Closeable, Flushable {
* 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.
*/ */
public abstract String getPath(); public final String getPath() {
return JsonScope.getPath(stackSize, scopes, pathNames, pathIndices);
}
} }

View File

@@ -48,35 +48,13 @@ final class ObjectJsonReader 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 int stackSize = 0;
private final Object[] stack = new Object[32]; private final Object[] stack = new Object[32];
private final int[] scopes = new int[32];
private final String[] pathNames = new String[32];
private final int[] pathIndices = new int[32];
private boolean lenient;
private boolean failOnUnknown;
public ObjectJsonReader(Object root) { public ObjectJsonReader(Object root) {
scopes[stackSize] = JsonScope.NONEMPTY_DOCUMENT; scopes[stackSize] = JsonScope.NONEMPTY_DOCUMENT;
stack[stackSize++] = root; stack[stackSize++] = root;
} }
@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 { @Override public void beginArray() throws IOException {
List<?> peeked = require(List.class, Token.BEGIN_ARRAY); List<?> peeked = require(List.class, Token.BEGIN_ARRAY);
@@ -294,10 +272,6 @@ final class ObjectJsonReader extends JsonReader {
} }
} }
@Override public String getPath() {
return JsonScope.getPath(stackSize, scopes, pathNames, pathIndices);
}
@Override void promoteNameToValue() throws IOException { @Override void promoteNameToValue() throws IOException {
Map.Entry<?, ?> peeked = require(Map.Entry.class, Token.NAME); Map.Entry<?, ?> peeked = require(Map.Entry.class, Token.NAME);
@@ -344,16 +318,6 @@ final class ObjectJsonReader extends JsonReader {
throw typeMismatch(name, Token.NAME); throw typeMismatch(name, Token.NAME);
} }
private JsonDataException typeMismatch(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());
}
}
/** /**
* Removes a value and prepares for the next. If we're iterating a map or list this advances the * Removes a value and prepares for the next. If we're iterating a map or list this advances the
* iterator. * iterator.

View File

@@ -27,19 +27,11 @@ import static com.squareup.moshi.JsonScope.NONEMPTY_DOCUMENT;
/** Writes JSON by building a Java object comprising maps, lists, and JSON primitives. */ /** Writes JSON by building a Java object comprising maps, lists, and JSON primitives. */
final class ObjectJsonWriter extends JsonWriter { final class ObjectJsonWriter extends JsonWriter {
private String indent;
private boolean lenient;
private boolean serializeNulls;
private final Object[] stack = new Object[32]; private final Object[] stack = new Object[32];
private final int[] scopes = new int[32];
private final String[] pathNames = new String[32];
private final int[] pathIndices = new int[32];
private int stackSize = 0;
private String deferredName; private String deferredName;
ObjectJsonWriter() { ObjectJsonWriter() {
scopes[stackSize++] = EMPTY_DOCUMENT; pushScope(EMPTY_DOCUMENT);
} }
public Object root() { public Object root() {
@@ -50,30 +42,6 @@ final class ObjectJsonWriter extends JsonWriter {
return stack[0]; return stack[0];
} }
@Override public void setIndent(String indent) {
this.indent = indent;
}
@Override public String getIndent() {
return indent;
}
@Override public void setLenient(boolean lenient) {
this.lenient = lenient;
}
@Override public boolean isLenient() {
return lenient;
}
@Override public void setSerializeNulls(boolean serializeNulls) {
this.serializeNulls = serializeNulls;
}
@Override public boolean getSerializeNulls() {
return serializeNulls;
}
@Override public JsonWriter beginArray() throws IOException { @Override public JsonWriter beginArray() throws IOException {
if (stackSize == stack.length) { if (stackSize == stack.length) {
throw new JsonDataException("Nesting too deep at " + getPath() + ": circular reference?"); throw new JsonDataException("Nesting too deep at " + getPath() + ": circular reference?");
@@ -81,14 +49,13 @@ final class ObjectJsonWriter extends JsonWriter {
List<Object> list = new ArrayList<>(); List<Object> list = new ArrayList<>();
add(list); add(list);
stack[stackSize] = list; stack[stackSize] = list;
scopes[stackSize] = EMPTY_ARRAY;
pathIndices[stackSize] = 0; pathIndices[stackSize] = 0;
stackSize++; pushScope(EMPTY_ARRAY);
return this; return this;
} }
@Override public JsonWriter endArray() throws IOException { @Override public JsonWriter endArray() throws IOException {
if (peek() != EMPTY_ARRAY) { if (peekScope() != EMPTY_ARRAY) {
throw new IllegalStateException("Nesting problem."); throw new IllegalStateException("Nesting problem.");
} }
stackSize--; stackSize--;
@@ -104,13 +71,12 @@ final class ObjectJsonWriter extends JsonWriter {
Map<String, Object> map = new LinkedHashTreeMap<>(); Map<String, Object> map = new LinkedHashTreeMap<>();
add(map); add(map);
stack[stackSize] = map; stack[stackSize] = map;
scopes[stackSize] = EMPTY_OBJECT; pushScope(EMPTY_OBJECT);
stackSize++;
return this; return this;
} }
@Override public JsonWriter endObject() throws IOException { @Override public JsonWriter endObject() throws IOException {
if (peek() != EMPTY_OBJECT || deferredName != null) { if (peekScope() != EMPTY_OBJECT || deferredName != null) {
throw new IllegalStateException("Nesting problem."); throw new IllegalStateException("Nesting problem.");
} }
stackSize--; stackSize--;
@@ -127,7 +93,7 @@ final class ObjectJsonWriter extends JsonWriter {
if (stackSize == 0) { if (stackSize == 0) {
throw new IllegalStateException("JsonWriter is closed."); throw new IllegalStateException("JsonWriter is closed.");
} }
if (peek() != EMPTY_OBJECT || deferredName != null) { if (peekScope() != EMPTY_OBJECT || deferredName != null) {
throw new IllegalStateException("Nesting problem."); throw new IllegalStateException("Nesting problem.");
} }
pathNames[stackSize - 1] = name; pathNames[stackSize - 1] = name;
@@ -136,19 +102,27 @@ final class ObjectJsonWriter extends JsonWriter {
} }
@Override public JsonWriter value(String value) throws IOException { @Override public JsonWriter value(String value) throws IOException {
return add(value); add(value);
pathIndices[stackSize - 1]++;
return this;
} }
@Override public JsonWriter nullValue() throws IOException { @Override public JsonWriter nullValue() throws IOException {
return add(null); add(null);
pathIndices[stackSize - 1]++;
return this;
} }
@Override public JsonWriter value(boolean value) throws IOException { @Override public JsonWriter value(boolean value) throws IOException {
return add(value); add(value);
pathIndices[stackSize - 1]++;
return this;
} }
@Override public JsonWriter value(Boolean value) throws IOException { @Override public JsonWriter value(Boolean value) throws IOException {
return add(value); add(value);
pathIndices[stackSize - 1]++;
return this;
} }
@Override public JsonWriter value(double value) throws IOException { @Override public JsonWriter value(double value) throws IOException {
@@ -156,7 +130,9 @@ final class ObjectJsonWriter extends JsonWriter {
} }
@Override public JsonWriter value(long value) throws IOException { @Override public JsonWriter value(long value) throws IOException {
return add(value); add(value);
pathIndices[stackSize - 1]++;
return this;
} }
@Override public JsonWriter value(Number value) throws IOException { @Override public JsonWriter value(Number value) throws IOException {
@@ -166,17 +142,15 @@ final class ObjectJsonWriter extends JsonWriter {
throw new IllegalArgumentException("Numeric values must be finite, but was " + value); throw new IllegalArgumentException("Numeric values must be finite, but was " + value);
} }
} }
return add(value); add(value);
pathIndices[stackSize - 1]++;
return this;
} }
@Override void promoteNameToValue() throws IOException { @Override void promoteNameToValue() throws IOException {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
@Override public String getPath() {
return JsonScope.getPath(stackSize, scopes, pathNames, pathIndices);
}
@Override public void close() throws IOException { @Override public void close() throws IOException {
int size = stackSize; int size = stackSize;
if (size > 1 || size == 1 && scopes[size - 1] != NONEMPTY_DOCUMENT) { if (size > 1 || size == 1 && scopes[size - 1] != NONEMPTY_DOCUMENT) {
@@ -191,18 +165,8 @@ final class ObjectJsonWriter extends JsonWriter {
} }
} }
/**
* Returns the scope on the top of the stack.
*/
private int peek() {
if (stackSize == 0) {
throw new IllegalStateException("JsonWriter is closed.");
}
return scopes[stackSize - 1];
}
private ObjectJsonWriter add(Object newTop) { private ObjectJsonWriter add(Object newTop) {
int scope = peek(); int scope = peekScope();
if (stackSize == 1) { if (stackSize == 1) {
if (scope != EMPTY_DOCUMENT) { if (scope != EMPTY_DOCUMENT) {

View File

@@ -17,14 +17,27 @@ package com.squareup.moshi;
import java.io.IOException; import java.io.IOException;
import java.math.BigInteger; import java.math.BigInteger;
import okio.Buffer; import java.util.List;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assume.assumeTrue;
@RunWith(Parameterized.class)
public final class JsonWriterPathTest { public final class JsonWriterPathTest {
@Parameter public JsonWriterFactory factory;
@Parameters(name = "{0}")
public static List<Object[]> parameters() {
return JsonWriterFactory.factories();
}
@Test public void path() throws IOException { @Test public void path() throws IOException {
JsonWriter writer = JsonWriter.of(new Buffer()); JsonWriter writer = factory.newWriter();
assertThat(writer.getPath()).isEqualTo("$"); assertThat(writer.getPath()).isEqualTo("$");
writer.beginObject(); writer.beginObject();
assertThat(writer.getPath()).isEqualTo("$."); assertThat(writer.getPath()).isEqualTo("$.");
@@ -63,7 +76,7 @@ public final class JsonWriterPathTest {
} }
@Test public void arrayOfObjects() throws IOException { @Test public void arrayOfObjects() throws IOException {
JsonWriter writer = JsonWriter.of(new Buffer()); JsonWriter writer = factory.newWriter();
writer.beginArray(); writer.beginArray();
assertThat(writer.getPath()).isEqualTo("$[0]"); assertThat(writer.getPath()).isEqualTo("$[0]");
writer.beginObject(); writer.beginObject();
@@ -83,7 +96,7 @@ public final class JsonWriterPathTest {
} }
@Test public void arrayOfArrays() throws IOException { @Test public void arrayOfArrays() throws IOException {
JsonWriter writer = JsonWriter.of(new Buffer()); JsonWriter writer = factory.newWriter();
writer.beginArray(); writer.beginArray();
assertThat(writer.getPath()).isEqualTo("$[0]"); assertThat(writer.getPath()).isEqualTo("$[0]");
writer.beginArray(); writer.beginArray();
@@ -103,7 +116,7 @@ public final class JsonWriterPathTest {
} }
@Test public void objectPath() throws IOException { @Test public void objectPath() throws IOException {
JsonWriter writer = JsonWriter.of(new Buffer()); JsonWriter writer = factory.newWriter();
assertThat(writer.getPath()).isEqualTo("$"); assertThat(writer.getPath()).isEqualTo("$");
writer.beginObject(); writer.beginObject();
assertThat(writer.getPath()).isEqualTo("$."); assertThat(writer.getPath()).isEqualTo("$.");
@@ -122,7 +135,7 @@ public final class JsonWriterPathTest {
} }
@Test public void nestedObjects() throws IOException { @Test public void nestedObjects() throws IOException {
JsonWriter writer = JsonWriter.of(new Buffer()); JsonWriter writer = factory.newWriter();
assertThat(writer.getPath()).isEqualTo("$"); assertThat(writer.getPath()).isEqualTo("$");
writer.beginObject(); writer.beginObject();
assertThat(writer.getPath()).isEqualTo("$."); assertThat(writer.getPath()).isEqualTo("$.");
@@ -147,7 +160,7 @@ public final class JsonWriterPathTest {
} }
@Test public void arrayPath() throws IOException { @Test public void arrayPath() throws IOException {
JsonWriter writer = JsonWriter.of(new Buffer()); JsonWriter writer = factory.newWriter();
assertThat(writer.getPath()).isEqualTo("$"); assertThat(writer.getPath()).isEqualTo("$");
writer.beginArray(); writer.beginArray();
assertThat(writer.getPath()).isEqualTo("$[0]"); assertThat(writer.getPath()).isEqualTo("$[0]");
@@ -168,7 +181,7 @@ public final class JsonWriterPathTest {
} }
@Test public void nestedArrays() throws IOException { @Test public void nestedArrays() throws IOException {
JsonWriter writer = JsonWriter.of(new Buffer()); JsonWriter writer = factory.newWriter();
assertThat(writer.getPath()).isEqualTo("$"); assertThat(writer.getPath()).isEqualTo("$");
writer.beginArray(); writer.beginArray();
assertThat(writer.getPath()).isEqualTo("$[0]"); assertThat(writer.getPath()).isEqualTo("$[0]");
@@ -189,7 +202,9 @@ public final class JsonWriterPathTest {
} }
@Test public void multipleTopLevelValuesInOneDocument() throws IOException { @Test public void multipleTopLevelValuesInOneDocument() throws IOException {
JsonWriter writer = JsonWriter.of(new Buffer()); assumeTrue(factory.supportsMultipleTopLevelValuesInOneDocument());
JsonWriter writer = factory.newWriter();
writer.setLenient(true); writer.setLenient(true);
writer.beginArray(); writer.beginArray();
writer.endArray(); writer.endArray();
@@ -200,7 +215,7 @@ public final class JsonWriterPathTest {
} }
@Test public void skipNulls() throws IOException { @Test public void skipNulls() throws IOException {
JsonWriter writer = JsonWriter.of(new Buffer()); JsonWriter writer = factory.newWriter();
writer.setSerializeNulls(false); writer.setSerializeNulls(false);
assertThat(writer.getPath()).isEqualTo("$"); assertThat(writer.getPath()).isEqualTo("$");
writer.beginObject(); writer.beginObject();