mirror of
https://github.com/fankes/moshi.git
synced 2025-10-19 16:09:21 +08:00
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:
@@ -57,35 +57,11 @@ final class BufferedSinkJsonWriter extends JsonWriter {
|
||||
/** The output data, containing at most one top-level array or object. */
|
||||
private final BufferedSink sink;
|
||||
|
||||
// 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;
|
||||
{
|
||||
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 ": ".
|
||||
*/
|
||||
/** The name/value separator; either ":" or ": ". */
|
||||
private String separator = ":";
|
||||
|
||||
private boolean lenient;
|
||||
|
||||
private String deferredName;
|
||||
|
||||
private boolean serializeNulls;
|
||||
|
||||
private boolean promoteNameToValue;
|
||||
|
||||
BufferedSinkJsonWriter(BufferedSink sink) {
|
||||
@@ -93,36 +69,12 @@ final class BufferedSinkJsonWriter extends JsonWriter {
|
||||
throw new NullPointerException("sink == null");
|
||||
}
|
||||
this.sink = sink;
|
||||
pushScope(EMPTY_DOCUMENT);
|
||||
}
|
||||
|
||||
@Override public final void setIndent(String indent) {
|
||||
if (indent.length() == 0) {
|
||||
this.indent = null;
|
||||
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 void setIndent(String indent) {
|
||||
super.setIndent(indent);
|
||||
this.separator = !indent.isEmpty() ? ": " : ":";
|
||||
}
|
||||
|
||||
@Override public JsonWriter beginArray() throws IOException {
|
||||
@@ -150,7 +102,7 @@ final class BufferedSinkJsonWriter extends JsonWriter {
|
||||
*/
|
||||
private JsonWriter open(int empty, String openBracket) throws IOException {
|
||||
beforeValue();
|
||||
push(empty);
|
||||
pushScope(empty);
|
||||
pathIndices[stackSize - 1] = 0;
|
||||
sink.writeUtf8(openBracket);
|
||||
return this;
|
||||
@@ -160,9 +112,8 @@ final class BufferedSinkJsonWriter extends JsonWriter {
|
||||
* Closes the current scope by appending any necessary whitespace and the
|
||||
* given bracket.
|
||||
*/
|
||||
private JsonWriter close(int empty, int nonempty, String closeBracket)
|
||||
throws IOException {
|
||||
int context = peek();
|
||||
private JsonWriter close(int empty, int nonempty, String closeBracket) throws IOException {
|
||||
int context = peekScope();
|
||||
if (context != nonempty && context != empty) {
|
||||
throw new IllegalStateException("Nesting problem.");
|
||||
}
|
||||
@@ -180,30 +131,6 @@ final class BufferedSinkJsonWriter extends JsonWriter {
|
||||
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 {
|
||||
if (name == null) {
|
||||
throw new NullPointerException("name == null");
|
||||
@@ -337,7 +264,7 @@ final class BufferedSinkJsonWriter extends JsonWriter {
|
||||
sink.close();
|
||||
|
||||
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");
|
||||
}
|
||||
stackSize = 0;
|
||||
@@ -395,7 +322,7 @@ final class BufferedSinkJsonWriter extends JsonWriter {
|
||||
* adjusts the stack to expect the name's value.
|
||||
*/
|
||||
private void beforeName() throws IOException {
|
||||
int context = peek();
|
||||
int context = peekScope();
|
||||
if (context == NONEMPTY_OBJECT) { // first in object
|
||||
sink.writeByte(',');
|
||||
} else if (context != EMPTY_OBJECT) { // not in an object!
|
||||
@@ -412,7 +339,7 @@ final class BufferedSinkJsonWriter extends JsonWriter {
|
||||
*/
|
||||
@SuppressWarnings("fallthrough")
|
||||
private void beforeValue() throws IOException {
|
||||
switch (peek()) {
|
||||
switch (peekScope()) {
|
||||
case NONEMPTY_DOCUMENT:
|
||||
if (!lenient) {
|
||||
throw new IllegalStateException(
|
||||
@@ -444,14 +371,10 @@ final class BufferedSinkJsonWriter extends JsonWriter {
|
||||
}
|
||||
|
||||
@Override void promoteNameToValue() throws IOException {
|
||||
int context = peek();
|
||||
int context = peekScope();
|
||||
if (context != NONEMPTY_OBJECT && context != EMPTY_OBJECT) {
|
||||
throw new IllegalStateException("Nesting problem.");
|
||||
}
|
||||
promoteNameToValue = true;
|
||||
}
|
||||
|
||||
@Override public String getPath() {
|
||||
return JsonScope.getPath(stackSize, stack, pathNames, pathIndices);
|
||||
}
|
||||
}
|
||||
|
@@ -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_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. */
|
||||
private final BufferedSource source;
|
||||
private final Buffer buffer;
|
||||
@@ -94,41 +88,13 @@ final class BufferedSourceJsonReader extends JsonReader {
|
||||
*/
|
||||
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) {
|
||||
if (source == null) {
|
||||
throw new NullPointerException("source == null");
|
||||
}
|
||||
this.source = source;
|
||||
this.buffer = source.buffer();
|
||||
}
|
||||
|
||||
@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;
|
||||
pushScope(JsonScope.EMPTY_DOCUMENT);
|
||||
}
|
||||
|
||||
@Override public void beginArray() throws IOException {
|
||||
@@ -137,7 +103,7 @@ final class BufferedSourceJsonReader extends JsonReader {
|
||||
p = doPeek();
|
||||
}
|
||||
if (p == PEEKED_BEGIN_ARRAY) {
|
||||
push(JsonScope.EMPTY_ARRAY);
|
||||
pushScope(JsonScope.EMPTY_ARRAY);
|
||||
pathIndices[stackSize - 1] = 0;
|
||||
peeked = PEEKED_NONE;
|
||||
} else {
|
||||
@@ -167,7 +133,7 @@ final class BufferedSourceJsonReader extends JsonReader {
|
||||
p = doPeek();
|
||||
}
|
||||
if (p == PEEKED_BEGIN_OBJECT) {
|
||||
push(JsonScope.EMPTY_OBJECT);
|
||||
pushScope(JsonScope.EMPTY_OBJECT);
|
||||
peeked = PEEKED_NONE;
|
||||
} else {
|
||||
throw new JsonDataException("Expected BEGIN_OBJECT but was " + peek()
|
||||
@@ -240,9 +206,9 @@ final class BufferedSourceJsonReader extends JsonReader {
|
||||
}
|
||||
|
||||
private int doPeek() throws IOException {
|
||||
int peekStack = stack[stackSize - 1];
|
||||
int peekStack = scopes[stackSize - 1];
|
||||
if (peekStack == JsonScope.EMPTY_ARRAY) {
|
||||
stack[stackSize - 1] = JsonScope.NONEMPTY_ARRAY;
|
||||
scopes[stackSize - 1] = JsonScope.NONEMPTY_ARRAY;
|
||||
} else if (peekStack == JsonScope.NONEMPTY_ARRAY) {
|
||||
// Look for a comma before the next element.
|
||||
int c = nextNonWhitespace(true);
|
||||
@@ -258,7 +224,7 @@ final class BufferedSourceJsonReader extends JsonReader {
|
||||
throw syntaxError("Unterminated array");
|
||||
}
|
||||
} 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.
|
||||
if (peekStack == JsonScope.NONEMPTY_OBJECT) {
|
||||
int c = nextNonWhitespace(true);
|
||||
@@ -299,7 +265,7 @@ final class BufferedSourceJsonReader extends JsonReader {
|
||||
}
|
||||
}
|
||||
} 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.
|
||||
int c = nextNonWhitespace(true);
|
||||
buffer.readByte(); // Consume ':'.
|
||||
@@ -316,7 +282,7 @@ final class BufferedSourceJsonReader extends JsonReader {
|
||||
throw syntaxError("Expected ':'");
|
||||
}
|
||||
} else if (peekStack == JsonScope.EMPTY_DOCUMENT) {
|
||||
stack[stackSize - 1] = JsonScope.NONEMPTY_DOCUMENT;
|
||||
scopes[stackSize - 1] = JsonScope.NONEMPTY_DOCUMENT;
|
||||
} else if (peekStack == JsonScope.NONEMPTY_DOCUMENT) {
|
||||
int c = nextNonWhitespace(false);
|
||||
if (c == -1) {
|
||||
@@ -923,7 +889,7 @@ final class BufferedSourceJsonReader extends JsonReader {
|
||||
|
||||
@Override public void close() throws IOException {
|
||||
peeked = PEEKED_NONE;
|
||||
stack[0] = JsonScope.CLOSED;
|
||||
scopes[0] = JsonScope.CLOSED;
|
||||
stackSize = 1;
|
||||
buffer.clear();
|
||||
source.close();
|
||||
@@ -941,10 +907,10 @@ final class BufferedSourceJsonReader extends JsonReader {
|
||||
}
|
||||
|
||||
if (p == PEEKED_BEGIN_ARRAY) {
|
||||
push(JsonScope.EMPTY_ARRAY);
|
||||
pushScope(JsonScope.EMPTY_ARRAY);
|
||||
count++;
|
||||
} else if (p == PEEKED_BEGIN_OBJECT) {
|
||||
push(JsonScope.EMPTY_OBJECT);
|
||||
pushScope(JsonScope.EMPTY_OBJECT);
|
||||
count++;
|
||||
} else if (p == PEEKED_END_ARRAY) {
|
||||
stackSize--;
|
||||
@@ -968,13 +934,6 @@ final class BufferedSourceJsonReader extends JsonReader {
|
||||
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
|
||||
* 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 + ")";
|
||||
}
|
||||
|
||||
@Override public String getPath() {
|
||||
return JsonScope.getPath(stackSize, stack, pathNames, pathIndices);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -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 {
|
||||
if (hasNext()) {
|
||||
peekedString = nextName();
|
||||
|
@@ -171,9 +171,21 @@ import okio.ByteString;
|
||||
* of this class are not thread safe.
|
||||
*/
|
||||
public abstract class JsonReader implements Closeable {
|
||||
/**
|
||||
* Returns a new instance that reads a JSON-encoded stream from {@code source}.
|
||||
*/
|
||||
// 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];
|
||||
|
||||
/** 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) {
|
||||
return new BufferedSourceJsonReader(source);
|
||||
}
|
||||
@@ -182,6 +194,31 @@ public abstract class JsonReader implements Closeable {
|
||||
// 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
|
||||
* 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 ,}.
|
||||
* </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.
|
||||
*/
|
||||
public abstract boolean isLenient();
|
||||
public final boolean isLenient() {
|
||||
return lenient;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* 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.
|
||||
*/
|
||||
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
|
||||
@@ -349,7 +394,9 @@ public abstract class JsonReader implements Closeable {
|
||||
* Returns a <a href="http://goessner.net/articles/JsonPath/">JsonPath</a> to
|
||||
* 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
|
||||
|
@@ -116,9 +116,23 @@ import okio.BufferedSink;
|
||||
* malformed JSON string will fail with an {@link IllegalStateException}.
|
||||
*/
|
||||
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) {
|
||||
return new BufferedSinkJsonWriter(sink);
|
||||
}
|
||||
@@ -127,6 +141,26 @@ public abstract class JsonWriter implements Closeable, Flushable {
|
||||
// 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
|
||||
* 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.
|
||||
*/
|
||||
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
|
||||
* 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
|
||||
@@ -155,24 +193,32 @@ public abstract class JsonWriter implements Closeable, Flushable {
|
||||
* Double#isInfinite() infinities}.
|
||||
* </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.
|
||||
*/
|
||||
public abstract boolean isLenient();
|
||||
public final boolean isLenient() {
|
||||
return lenient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether object members are serialized when their value is null.
|
||||
* 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.
|
||||
* 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
|
||||
@@ -276,5 +322,7 @@ public abstract class JsonWriter implements Closeable, Flushable {
|
||||
* Returns a <a href="http://goessner.net/articles/JsonPath/">JsonPath</a> to
|
||||
* the current location in the JSON value.
|
||||
*/
|
||||
public abstract String getPath();
|
||||
public final String getPath() {
|
||||
return JsonScope.getPath(stackSize, scopes, pathNames, pathIndices);
|
||||
}
|
||||
}
|
||||
|
@@ -48,35 +48,13 @@ final class ObjectJsonReader extends JsonReader {
|
||||
/** Sentinel object pushed on {@link #stack} when the reader is closed. */
|
||||
private static final Object JSON_READER_CLOSED = new Object();
|
||||
|
||||
private int stackSize = 0;
|
||||
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) {
|
||||
scopes[stackSize] = JsonScope.NONEMPTY_DOCUMENT;
|
||||
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 {
|
||||
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 {
|
||||
Map.Entry<?, ?> peeked = require(Map.Entry.class, Token.NAME);
|
||||
|
||||
@@ -344,16 +318,6 @@ final class ObjectJsonReader extends JsonReader {
|
||||
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
|
||||
* iterator.
|
||||
|
@@ -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. */
|
||||
final class ObjectJsonWriter extends JsonWriter {
|
||||
private String indent;
|
||||
private boolean lenient;
|
||||
private boolean serializeNulls;
|
||||
|
||||
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;
|
||||
|
||||
ObjectJsonWriter() {
|
||||
scopes[stackSize++] = EMPTY_DOCUMENT;
|
||||
pushScope(EMPTY_DOCUMENT);
|
||||
}
|
||||
|
||||
public Object root() {
|
||||
@@ -50,30 +42,6 @@ final class ObjectJsonWriter extends JsonWriter {
|
||||
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 {
|
||||
if (stackSize == stack.length) {
|
||||
throw new JsonDataException("Nesting too deep at " + getPath() + ": circular reference?");
|
||||
@@ -81,14 +49,13 @@ final class ObjectJsonWriter extends JsonWriter {
|
||||
List<Object> list = new ArrayList<>();
|
||||
add(list);
|
||||
stack[stackSize] = list;
|
||||
scopes[stackSize] = EMPTY_ARRAY;
|
||||
pathIndices[stackSize] = 0;
|
||||
stackSize++;
|
||||
pushScope(EMPTY_ARRAY);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override public JsonWriter endArray() throws IOException {
|
||||
if (peek() != EMPTY_ARRAY) {
|
||||
if (peekScope() != EMPTY_ARRAY) {
|
||||
throw new IllegalStateException("Nesting problem.");
|
||||
}
|
||||
stackSize--;
|
||||
@@ -104,13 +71,12 @@ final class ObjectJsonWriter extends JsonWriter {
|
||||
Map<String, Object> map = new LinkedHashTreeMap<>();
|
||||
add(map);
|
||||
stack[stackSize] = map;
|
||||
scopes[stackSize] = EMPTY_OBJECT;
|
||||
stackSize++;
|
||||
pushScope(EMPTY_OBJECT);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override public JsonWriter endObject() throws IOException {
|
||||
if (peek() != EMPTY_OBJECT || deferredName != null) {
|
||||
if (peekScope() != EMPTY_OBJECT || deferredName != null) {
|
||||
throw new IllegalStateException("Nesting problem.");
|
||||
}
|
||||
stackSize--;
|
||||
@@ -127,7 +93,7 @@ final class ObjectJsonWriter extends JsonWriter {
|
||||
if (stackSize == 0) {
|
||||
throw new IllegalStateException("JsonWriter is closed.");
|
||||
}
|
||||
if (peek() != EMPTY_OBJECT || deferredName != null) {
|
||||
if (peekScope() != EMPTY_OBJECT || deferredName != null) {
|
||||
throw new IllegalStateException("Nesting problem.");
|
||||
}
|
||||
pathNames[stackSize - 1] = name;
|
||||
@@ -136,19 +102,27 @@ final class ObjectJsonWriter extends JsonWriter {
|
||||
}
|
||||
|
||||
@Override public JsonWriter value(String value) throws IOException {
|
||||
return add(value);
|
||||
add(value);
|
||||
pathIndices[stackSize - 1]++;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override public JsonWriter nullValue() throws IOException {
|
||||
return add(null);
|
||||
add(null);
|
||||
pathIndices[stackSize - 1]++;
|
||||
return this;
|
||||
}
|
||||
|
||||
@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 {
|
||||
return add(value);
|
||||
add(value);
|
||||
pathIndices[stackSize - 1]++;
|
||||
return this;
|
||||
}
|
||||
|
||||
@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 {
|
||||
return add(value);
|
||||
add(value);
|
||||
pathIndices[stackSize - 1]++;
|
||||
return this;
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
return add(value);
|
||||
add(value);
|
||||
pathIndices[stackSize - 1]++;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override void promoteNameToValue() throws IOException {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override public String getPath() {
|
||||
return JsonScope.getPath(stackSize, scopes, pathNames, pathIndices);
|
||||
}
|
||||
|
||||
@Override public void close() throws IOException {
|
||||
int size = stackSize;
|
||||
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) {
|
||||
int scope = peek();
|
||||
int scope = peekScope();
|
||||
|
||||
if (stackSize == 1) {
|
||||
if (scope != EMPTY_DOCUMENT) {
|
||||
|
@@ -17,14 +17,27 @@ package com.squareup.moshi;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import okio.Buffer;
|
||||
import java.util.List;
|
||||
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.junit.Assume.assumeTrue;
|
||||
|
||||
@RunWith(Parameterized.class)
|
||||
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 {
|
||||
JsonWriter writer = JsonWriter.of(new Buffer());
|
||||
JsonWriter writer = factory.newWriter();
|
||||
assertThat(writer.getPath()).isEqualTo("$");
|
||||
writer.beginObject();
|
||||
assertThat(writer.getPath()).isEqualTo("$.");
|
||||
@@ -63,7 +76,7 @@ public final class JsonWriterPathTest {
|
||||
}
|
||||
|
||||
@Test public void arrayOfObjects() throws IOException {
|
||||
JsonWriter writer = JsonWriter.of(new Buffer());
|
||||
JsonWriter writer = factory.newWriter();
|
||||
writer.beginArray();
|
||||
assertThat(writer.getPath()).isEqualTo("$[0]");
|
||||
writer.beginObject();
|
||||
@@ -83,7 +96,7 @@ public final class JsonWriterPathTest {
|
||||
}
|
||||
|
||||
@Test public void arrayOfArrays() throws IOException {
|
||||
JsonWriter writer = JsonWriter.of(new Buffer());
|
||||
JsonWriter writer = factory.newWriter();
|
||||
writer.beginArray();
|
||||
assertThat(writer.getPath()).isEqualTo("$[0]");
|
||||
writer.beginArray();
|
||||
@@ -103,7 +116,7 @@ public final class JsonWriterPathTest {
|
||||
}
|
||||
|
||||
@Test public void objectPath() throws IOException {
|
||||
JsonWriter writer = JsonWriter.of(new Buffer());
|
||||
JsonWriter writer = factory.newWriter();
|
||||
assertThat(writer.getPath()).isEqualTo("$");
|
||||
writer.beginObject();
|
||||
assertThat(writer.getPath()).isEqualTo("$.");
|
||||
@@ -122,7 +135,7 @@ public final class JsonWriterPathTest {
|
||||
}
|
||||
|
||||
@Test public void nestedObjects() throws IOException {
|
||||
JsonWriter writer = JsonWriter.of(new Buffer());
|
||||
JsonWriter writer = factory.newWriter();
|
||||
assertThat(writer.getPath()).isEqualTo("$");
|
||||
writer.beginObject();
|
||||
assertThat(writer.getPath()).isEqualTo("$.");
|
||||
@@ -147,7 +160,7 @@ public final class JsonWriterPathTest {
|
||||
}
|
||||
|
||||
@Test public void arrayPath() throws IOException {
|
||||
JsonWriter writer = JsonWriter.of(new Buffer());
|
||||
JsonWriter writer = factory.newWriter();
|
||||
assertThat(writer.getPath()).isEqualTo("$");
|
||||
writer.beginArray();
|
||||
assertThat(writer.getPath()).isEqualTo("$[0]");
|
||||
@@ -168,7 +181,7 @@ public final class JsonWriterPathTest {
|
||||
}
|
||||
|
||||
@Test public void nestedArrays() throws IOException {
|
||||
JsonWriter writer = JsonWriter.of(new Buffer());
|
||||
JsonWriter writer = factory.newWriter();
|
||||
assertThat(writer.getPath()).isEqualTo("$");
|
||||
writer.beginArray();
|
||||
assertThat(writer.getPath()).isEqualTo("$[0]");
|
||||
@@ -189,7 +202,9 @@ public final class JsonWriterPathTest {
|
||||
}
|
||||
|
||||
@Test public void multipleTopLevelValuesInOneDocument() throws IOException {
|
||||
JsonWriter writer = JsonWriter.of(new Buffer());
|
||||
assumeTrue(factory.supportsMultipleTopLevelValuesInOneDocument());
|
||||
|
||||
JsonWriter writer = factory.newWriter();
|
||||
writer.setLenient(true);
|
||||
writer.beginArray();
|
||||
writer.endArray();
|
||||
@@ -200,7 +215,7 @@ public final class JsonWriterPathTest {
|
||||
}
|
||||
|
||||
@Test public void skipNulls() throws IOException {
|
||||
JsonWriter writer = JsonWriter.of(new Buffer());
|
||||
JsonWriter writer = factory.newWriter();
|
||||
writer.setSerializeNulls(false);
|
||||
assertThat(writer.getPath()).isEqualTo("$");
|
||||
writer.beginObject();
|
||||
|
Reference in New Issue
Block a user