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("$"); + } +}