diff --git a/moshi/src/main/java/com/squareup/moshi/AdapterMethodsFactory.java b/moshi/src/main/java/com/squareup/moshi/AdapterMethodsFactory.java
index aadc866..e33e1f3 100644
--- a/moshi/src/main/java/com/squareup/moshi/AdapterMethodsFactory.java
+++ b/moshi/src/main/java/com/squareup/moshi/AdapterMethodsFactory.java
@@ -59,7 +59,7 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
throw new AssertionError();
} catch (InvocationTargetException e) {
if (e.getCause() instanceof IOException) throw (IOException) e.getCause();
- throw new JsonDataException(e.getCause()); // TODO: more context?
+ throw new JsonDataException(e.getCause() + " at " + writer.getPath());
}
}
}
@@ -77,7 +77,7 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
throw new AssertionError();
} catch (InvocationTargetException e) {
if (e.getCause() instanceof IOException) throw (IOException) e.getCause();
- throw new JsonDataException(e.getCause()); // TODO: more context?
+ throw new JsonDataException(e.getCause() + " at " + reader.getPath());
}
}
}
diff --git a/moshi/src/main/java/com/squareup/moshi/JsonReader.java b/moshi/src/main/java/com/squareup/moshi/JsonReader.java
index 720b198..76b1a33 100644
--- a/moshi/src/main/java/com/squareup/moshi/JsonReader.java
+++ b/moshi/src/main/java/com/squareup/moshi/JsonReader.java
@@ -248,14 +248,6 @@ public final class JsonReader implements Closeable {
stack[stackSize++] = JsonScope.EMPTY_DOCUMENT;
}
- /*
- * The path members. It corresponds directly to stack: At indices where the
- * stack contains an object (EMPTY_OBJECT, DANGLING_NAME or NONEMPTY_OBJECT),
- * pathNames contains the name at this scope. Where it contains an array
- * (EMPTY_ARRAY, NONEMPTY_ARRAY) pathIndices contains the current index in
- * that array. Otherwise the value is undefined, and we take advantage of that
- * by incrementing pathIndices when doing so isn't useful.
- */
private String[] pathNames = new String[32];
private int[] pathIndices = new int[32];
@@ -1269,30 +1261,7 @@ public final class JsonReader implements Closeable {
* the current location in the JSON value.
*/
public String getPath() {
- StringBuilder result = new StringBuilder().append('$');
- for (int i = 0, size = stackSize; i < size; i++) {
- switch (stack[i]) {
- case JsonScope.EMPTY_ARRAY:
- case JsonScope.NONEMPTY_ARRAY:
- result.append('[').append(pathIndices[i]).append(']');
- break;
-
- case JsonScope.EMPTY_OBJECT:
- case JsonScope.DANGLING_NAME:
- case JsonScope.NONEMPTY_OBJECT:
- result.append('.');
- if (pathNames[i] != null) {
- result.append(pathNames[i]);
- }
- break;
-
- case JsonScope.NONEMPTY_DOCUMENT:
- case JsonScope.EMPTY_DOCUMENT:
- case JsonScope.CLOSED:
- break;
- }
- }
- return result.toString();
+ return JsonScope.getPath(stackSize, stack, pathNames, pathIndices);
}
/**
diff --git a/moshi/src/main/java/com/squareup/moshi/JsonScope.java b/moshi/src/main/java/com/squareup/moshi/JsonScope.java
index 776d725..180839a 100644
--- a/moshi/src/main/java/com/squareup/moshi/JsonScope.java
+++ b/moshi/src/main/java/com/squareup/moshi/JsonScope.java
@@ -15,53 +15,65 @@
*/
package com.squareup.moshi;
-/**
- * Lexical scoping elements within a JSON reader or writer.
- */
+/** Lexical scoping elements within a JSON reader or writer. */
final class JsonScope {
- /**
- * An array with no elements requires no separators or newlines before
- * it is closed.
- */
+ /** An array with no elements requires no separators or newlines before it is closed. */
static final int EMPTY_ARRAY = 1;
- /**
- * A array with at least one value requires a comma and newline before
- * the next element.
- */
+ /** A array with at least one value requires a comma and newline before the next element. */
static final int NONEMPTY_ARRAY = 2;
- /**
- * An object with no name/value pairs requires no separators or newlines
- * before it is closed.
- */
+ /** An object with no name/value pairs requires no separators or newlines before it is closed. */
static final int EMPTY_OBJECT = 3;
- /**
- * An object whose most recent element is a key. The next element must
- * be a value.
- */
+ /** An object whose most recent element is a key. The next element must be a value. */
static final int DANGLING_NAME = 4;
- /**
- * An object with at least one name/value pair requires a comma and
- * newline before the next element.
- */
+ /** An object with at least one name/value pair requires a separator before the next element. */
static final int NONEMPTY_OBJECT = 5;
- /**
- * No object or array has been started.
- */
+ /** No object or array has been started. */
static final int EMPTY_DOCUMENT = 6;
- /**
- * A document with at an array or object.
- */
+ /** A document with at an array or object. */
static final int NONEMPTY_DOCUMENT = 7;
- /**
- * A document that's been closed and cannot be accessed.
- */
+ /** A document that's been closed and cannot be accessed. */
static final int CLOSED = 8;
+
+ /**
+ * Renders the path in a JSON document to a string. The {@code pathNames} and {@code pathIndices}
+ * parameters corresponds directly to stack: At indices where the stack contains an object
+ * (EMPTY_OBJECT, DANGLING_NAME or NONEMPTY_OBJECT), pathNames contains the name at this scope.
+ * Where it contains an array (EMPTY_ARRAY, NONEMPTY_ARRAY) pathIndices contains the current index
+ * in that array. Otherwise the value is undefined, and we take advantage of that by incrementing
+ * pathIndices when doing so isn't useful.
+ */
+ static String getPath(int stackSize, int[] stack, String[] pathNames, int[] pathIndices) {
+ StringBuilder result = new StringBuilder().append('$');
+ for (int i = 0, size = stackSize; i < size; i++) {
+ switch (stack[i]) {
+ case EMPTY_ARRAY:
+ case NONEMPTY_ARRAY:
+ result.append('[').append(pathIndices[i]).append(']');
+ break;
+
+ case EMPTY_OBJECT:
+ case DANGLING_NAME:
+ case NONEMPTY_OBJECT:
+ result.append('.');
+ if (pathNames[i] != null) {
+ result.append(pathNames[i]);
+ }
+ break;
+
+ case NONEMPTY_DOCUMENT:
+ case EMPTY_DOCUMENT:
+ case CLOSED:
+ break;
+ }
+ }
+ return result.toString();
+ }
}
diff --git a/moshi/src/main/java/com/squareup/moshi/JsonWriter.java b/moshi/src/main/java/com/squareup/moshi/JsonWriter.java
index a8483c8..e22bc55 100644
--- a/moshi/src/main/java/com/squareup/moshi/JsonWriter.java
+++ b/moshi/src/main/java/com/squareup/moshi/JsonWriter.java
@@ -160,6 +160,9 @@ public final class JsonWriter implements Closeable, Flushable {
push(EMPTY_DOCUMENT);
}
+ private String[] pathNames = new String[32];
+ private 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.
@@ -290,6 +293,7 @@ public final class JsonWriter implements Closeable, Flushable {
*/
private JsonWriter open(int empty, String openBracket) throws IOException {
beforeValue(true);
+ pathIndices[stackSize] = 0;
push(empty);
sink.writeUtf8(openBracket);
return this;
@@ -310,6 +314,8 @@ public final class JsonWriter implements Closeable, Flushable {
}
stackSize--;
+ pathNames[stackSize] = null; // Free the last path name so that it can be garbage collected!
+ pathIndices[stackSize - 1]++;
if (context == nonempty) {
newline();
}
@@ -360,6 +366,7 @@ public final class JsonWriter implements Closeable, Flushable {
throw new IllegalStateException("JsonWriter is closed.");
}
deferredName = name;
+ pathNames[stackSize - 1] = name;
return this;
}
@@ -384,6 +391,7 @@ public final class JsonWriter implements Closeable, Flushable {
writeDeferredName();
beforeValue(false);
string(value);
+ pathIndices[stackSize - 1]++;
return this;
}
@@ -403,6 +411,7 @@ public final class JsonWriter implements Closeable, Flushable {
}
beforeValue(false);
sink.writeUtf8("null");
+ pathIndices[stackSize - 1]++;
return this;
}
@@ -415,6 +424,7 @@ public final class JsonWriter implements Closeable, Flushable {
writeDeferredName();
beforeValue(false);
sink.writeUtf8(value ? "true" : "false");
+ pathIndices[stackSize - 1]++;
return this;
}
@@ -432,6 +442,7 @@ public final class JsonWriter implements Closeable, Flushable {
writeDeferredName();
beforeValue(false);
sink.writeUtf8(Double.toString(value));
+ pathIndices[stackSize - 1]++;
return this;
}
@@ -444,6 +455,7 @@ public final class JsonWriter implements Closeable, Flushable {
writeDeferredName();
beforeValue(false);
sink.writeUtf8(Long.toString(value));
+ pathIndices[stackSize - 1]++;
return this;
}
@@ -467,6 +479,7 @@ public final class JsonWriter implements Closeable, Flushable {
}
beforeValue(false);
sink.writeUtf8(string);
+ pathIndices[stackSize - 1]++;
return this;
}
@@ -598,4 +611,12 @@ public final class JsonWriter implements Closeable, Flushable {
throw new IllegalStateException("Nesting problem.");
}
}
+
+ /**
+ * Returns a JsonPath to
+ * the current location in the JSON value.
+ */
+ public String getPath() {
+ return JsonScope.getPath(stackSize, stack, pathNames, pathIndices);
+ }
}
diff --git a/moshi/src/test/java/com/squareup/moshi/AdapterMethodsTest.java b/moshi/src/test/java/com/squareup/moshi/AdapterMethodsTest.java
index f864f57..2de853b 100644
--- a/moshi/src/test/java/com/squareup/moshi/AdapterMethodsTest.java
+++ b/moshi/src/test/java/com/squareup/moshi/AdapterMethodsTest.java
@@ -233,6 +233,37 @@ public final class AdapterMethodsTest {
@interface Nullable {
}
+ @Test public void adapterThrows() throws Exception {
+ Moshi moshi = new Moshi.Builder()
+ .add(new ExceptionThrowingPointJsonAdapter())
+ .build();
+ JsonAdapter arrayOfPointAdapter = moshi.adapter(Point[].class).lenient();
+ try {
+ arrayOfPointAdapter.toJson(new Point[] { null, null, new Point(0, 0) });
+ fail();
+ } catch (JsonDataException expected) {
+ assertThat(expected.getMessage())
+ .isEqualTo("java.lang.Exception: pointToJson fail! at $[2]");
+ }
+ try {
+ arrayOfPointAdapter.fromJson("[null,null,[0,0]]");
+ fail();
+ } catch (JsonDataException expected) {
+ assertThat(expected.getMessage())
+ .isEqualTo("java.lang.Exception: pointFromJson fail! at $[2]");
+ }
+ }
+
+ static class ExceptionThrowingPointJsonAdapter {
+ @ToJson void pointToJson(JsonWriter writer, Point point) throws Exception {
+ throw new Exception("pointToJson fail!");
+ }
+
+ @FromJson Point pointFromJson(JsonReader reader) throws Exception {
+ throw new Exception("pointFromJson fail!");
+ }
+ }
+
static class Point {
final int x;
final int y;
diff --git a/moshi/src/test/java/com/squareup/moshi/JsonWriterPathTest.java b/moshi/src/test/java/com/squareup/moshi/JsonWriterPathTest.java
new file mode 100644
index 0000000..43badd3
--- /dev/null
+++ b/moshi/src/test/java/com/squareup/moshi/JsonWriterPathTest.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2014 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.moshi;
+
+import java.io.IOException;
+import java.math.BigInteger;
+import okio.Buffer;
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public final class JsonWriterPathTest {
+ @Test public void path() throws IOException {
+ JsonWriter writer = new JsonWriter(new Buffer());
+ assertThat(writer.getPath()).isEqualTo("$");
+ writer.beginObject();
+ assertThat(writer.getPath()).isEqualTo("$.");
+ writer.name("a");
+ assertThat(writer.getPath()).isEqualTo("$.a");
+ writer.beginArray();
+ assertThat(writer.getPath()).isEqualTo("$.a[0]");
+ writer.value(2);
+ assertThat(writer.getPath()).isEqualTo("$.a[1]");
+ writer.value(true);
+ assertThat(writer.getPath()).isEqualTo("$.a[2]");
+ writer.value(false);
+ assertThat(writer.getPath()).isEqualTo("$.a[3]");
+ writer.nullValue();
+ assertThat(writer.getPath()).isEqualTo("$.a[4]");
+ writer.value("b");
+ assertThat(writer.getPath()).isEqualTo("$.a[5]");
+ writer.beginObject();
+ assertThat(writer.getPath()).isEqualTo("$.a[5].");
+ writer.name("c");
+ assertThat(writer.getPath()).isEqualTo("$.a[5].c");
+ writer.value("d");
+ assertThat(writer.getPath()).isEqualTo("$.a[5].c");
+ writer.endObject();
+ assertThat(writer.getPath()).isEqualTo("$.a[6]");
+ writer.beginArray();
+ assertThat(writer.getPath()).isEqualTo("$.a[6][0]");
+ writer.value(3);
+ assertThat(writer.getPath()).isEqualTo("$.a[6][1]");
+ writer.endArray();
+ assertThat(writer.getPath()).isEqualTo("$.a[7]");
+ writer.endArray();
+ assertThat(writer.getPath()).isEqualTo("$.a");
+ writer.endObject();
+ assertThat(writer.getPath()).isEqualTo("$");
+ }
+
+ @Test public void arrayOfObjects() throws IOException {
+ JsonWriter writer = new JsonWriter(new Buffer());
+ writer.beginArray();
+ assertThat(writer.getPath()).isEqualTo("$[0]");
+ writer.beginObject();
+ assertThat(writer.getPath()).isEqualTo("$[0].");
+ writer.endObject();
+ assertThat(writer.getPath()).isEqualTo("$[1]");
+ writer.beginObject();
+ assertThat(writer.getPath()).isEqualTo("$[1].");
+ writer.endObject();
+ assertThat(writer.getPath()).isEqualTo("$[2]");
+ writer.beginObject();
+ assertThat(writer.getPath()).isEqualTo("$[2].");
+ writer.endObject();
+ assertThat(writer.getPath()).isEqualTo("$[3]");
+ writer.endArray();
+ assertThat(writer.getPath()).isEqualTo("$");
+ }
+
+ @Test public void arrayOfArrays() throws IOException {
+ JsonWriter writer = new JsonWriter(new Buffer());
+ writer.beginArray();
+ assertThat(writer.getPath()).isEqualTo("$[0]");
+ writer.beginArray();
+ assertThat(writer.getPath()).isEqualTo("$[0][0]");
+ writer.endArray();
+ assertThat(writer.getPath()).isEqualTo("$[1]");
+ writer.beginArray();
+ assertThat(writer.getPath()).isEqualTo("$[1][0]");
+ writer.endArray();
+ assertThat(writer.getPath()).isEqualTo("$[2]");
+ writer.beginArray();
+ assertThat(writer.getPath()).isEqualTo("$[2][0]");
+ writer.endArray();
+ assertThat(writer.getPath()).isEqualTo("$[3]");
+ writer.endArray();
+ assertThat(writer.getPath()).isEqualTo("$");
+ }
+
+ @Test public void objectPath() throws IOException {
+ JsonWriter writer = new JsonWriter(new Buffer());
+ assertThat(writer.getPath()).isEqualTo("$");
+ writer.beginObject();
+ assertThat(writer.getPath()).isEqualTo("$.");
+ writer.name("a");
+ assertThat(writer.getPath()).isEqualTo("$.a");
+ writer.value(1);
+ assertThat(writer.getPath()).isEqualTo("$.a");
+ writer.name("b");
+ assertThat(writer.getPath()).isEqualTo("$.b");
+ writer.value(2);
+ assertThat(writer.getPath()).isEqualTo("$.b");
+ writer.endObject();
+ assertThat(writer.getPath()).isEqualTo("$");
+ writer.close();
+ assertThat(writer.getPath()).isEqualTo("$");
+ }
+
+ @Test public void nestedObjects() throws IOException {
+ JsonWriter writer = new JsonWriter(new Buffer());
+ assertThat(writer.getPath()).isEqualTo("$");
+ writer.beginObject();
+ assertThat(writer.getPath()).isEqualTo("$.");
+ writer.name("a");
+ assertThat(writer.getPath()).isEqualTo("$.a");
+ writer.beginObject();
+ assertThat(writer.getPath()).isEqualTo("$.a.");
+ writer.name("b");
+ assertThat(writer.getPath()).isEqualTo("$.a.b");
+ writer.beginObject();
+ assertThat(writer.getPath()).isEqualTo("$.a.b.");
+ writer.name("c");
+ assertThat(writer.getPath()).isEqualTo("$.a.b.c");
+ writer.nullValue();
+ assertThat(writer.getPath()).isEqualTo("$.a.b.c");
+ writer.endObject();
+ assertThat(writer.getPath()).isEqualTo("$.a.b");
+ writer.endObject();
+ assertThat(writer.getPath()).isEqualTo("$.a");
+ writer.endObject();
+ assertThat(writer.getPath()).isEqualTo("$");
+ }
+
+ @Test public void arrayPath() throws IOException {
+ JsonWriter writer = new JsonWriter(new Buffer());
+ assertThat(writer.getPath()).isEqualTo("$");
+ writer.beginArray();
+ assertThat(writer.getPath()).isEqualTo("$[0]");
+ writer.value(1);
+ assertThat(writer.getPath()).isEqualTo("$[1]");
+ writer.value(true);
+ assertThat(writer.getPath()).isEqualTo("$[2]");
+ writer.value("a");
+ assertThat(writer.getPath()).isEqualTo("$[3]");
+ writer.value(5.5d);
+ assertThat(writer.getPath()).isEqualTo("$[4]");
+ writer.value(BigInteger.ONE);
+ assertThat(writer.getPath()).isEqualTo("$[5]");
+ writer.endArray();
+ assertThat(writer.getPath()).isEqualTo("$");
+ writer.close();
+ assertThat(writer.getPath()).isEqualTo("$");
+ }
+
+ @Test public void nestedArrays() throws IOException {
+ JsonWriter writer = new JsonWriter(new Buffer());
+ assertThat(writer.getPath()).isEqualTo("$");
+ writer.beginArray();
+ assertThat(writer.getPath()).isEqualTo("$[0]");
+ writer.beginArray();
+ assertThat(writer.getPath()).isEqualTo("$[0][0]");
+ writer.beginArray();
+ assertThat(writer.getPath()).isEqualTo("$[0][0][0]");
+ writer.nullValue();
+ assertThat(writer.getPath()).isEqualTo("$[0][0][1]");
+ writer.endArray();
+ assertThat(writer.getPath()).isEqualTo("$[0][1]");
+ writer.endArray();
+ assertThat(writer.getPath()).isEqualTo("$[1]");
+ writer.endArray();
+ assertThat(writer.getPath()).isEqualTo("$");
+ writer.close();
+ assertThat(writer.getPath()).isEqualTo("$");
+ }
+
+ @Test public void multipleTopLevelValuesInOneDocument() throws IOException {
+ JsonWriter writer = new JsonWriter(new Buffer());
+ writer.setLenient(true);
+ writer.beginArray();
+ writer.endArray();
+ assertThat(writer.getPath()).isEqualTo("$");
+ writer.beginArray();
+ writer.endArray();
+ assertThat(writer.getPath()).isEqualTo("$");
+ }
+
+ @Test public void skipNulls() throws IOException {
+ JsonWriter writer = new JsonWriter(new Buffer());
+ writer.setSerializeNulls(false);
+ assertThat(writer.getPath()).isEqualTo("$");
+ writer.beginObject();
+ assertThat(writer.getPath()).isEqualTo("$.");
+ writer.name("a");
+ assertThat(writer.getPath()).isEqualTo("$.a");
+ writer.nullValue();
+ assertThat(writer.getPath()).isEqualTo("$.a");
+ writer.name("b");
+ assertThat(writer.getPath()).isEqualTo("$.b");
+ writer.nullValue();
+ assertThat(writer.getPath()).isEqualTo("$.b");
+ writer.endObject();
+ assertThat(writer.getPath()).isEqualTo("$");
+ }
+}