diff --git a/gson/src/main/java/com/google/gson/stream/JsonReader.java b/gson/src/main/java/com/google/gson/stream/JsonReader.java index 60d0c17f..68554f5e 100644 --- a/gson/src/main/java/com/google/gson/stream/JsonReader.java +++ b/gson/src/main/java/com/google/gson/stream/JsonReader.java @@ -272,6 +272,17 @@ public 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]; + /** * Creates a new instance that reads a JSON-encoded stream from {@code in}. */ @@ -333,10 +344,11 @@ public class JsonReader implements Closeable { } if (p == PEEKED_BEGIN_ARRAY) { push(JsonScope.EMPTY_ARRAY); + pathIndices[stackSize - 1] = 0; peeked = PEEKED_NONE; } else { throw new IllegalStateException("Expected BEGIN_ARRAY but was " + peek() - + " at line " + getLineNumber() + " column " + getColumnNumber()); + + " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath()); } } @@ -354,7 +366,7 @@ public class JsonReader implements Closeable { peeked = PEEKED_NONE; } else { throw new IllegalStateException("Expected END_ARRAY but was " + peek() - + " at line " + getLineNumber() + " column " + getColumnNumber()); + + " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath()); } } @@ -372,7 +384,7 @@ public class JsonReader implements Closeable { peeked = PEEKED_NONE; } else { throw new IllegalStateException("Expected BEGIN_OBJECT but was " + peek() - + " at line " + getLineNumber() + " column " + getColumnNumber()); + + " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath()); } } @@ -387,10 +399,11 @@ public class JsonReader implements Closeable { } if (p == PEEKED_END_OBJECT) { stackSize--; + pathNames[stackSize] = null; // Free the last path name so that it can be garbage collected! peeked = PEEKED_NONE; } else { throw new IllegalStateException("Expected END_OBJECT but was " + peek() - + " at line " + getLineNumber() + " column " + getColumnNumber()); + + " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath()); } } @@ -783,9 +796,10 @@ public class JsonReader implements Closeable { result = nextQuotedValue('"'); } else { throw new IllegalStateException("Expected a name but was " + peek() - + " at line " + getLineNumber() + " column " + getColumnNumber()); + + " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath()); } peeked = PEEKED_NONE; + pathNames[stackSize - 1] = result; return result; } @@ -819,9 +833,10 @@ public class JsonReader implements Closeable { pos += peekedNumberLength; } else { throw new IllegalStateException("Expected a string but was " + peek() - + " at line " + getLineNumber() + " column " + getColumnNumber()); + + " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath()); } peeked = PEEKED_NONE; + pathIndices[stackSize - 1]++; return result; } @@ -839,13 +854,15 @@ public class JsonReader implements Closeable { } if (p == PEEKED_TRUE) { peeked = PEEKED_NONE; + pathIndices[stackSize - 1]++; return true; } else if (p == PEEKED_FALSE) { peeked = PEEKED_NONE; + pathIndices[stackSize - 1]++; return false; } throw new IllegalStateException("Expected a boolean but was " + peek() - + " at line " + getLineNumber() + " column " + getColumnNumber()); + + " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath()); } /** @@ -862,9 +879,10 @@ public class JsonReader implements Closeable { } if (p == PEEKED_NULL) { peeked = PEEKED_NONE; + pathIndices[stackSize - 1]++; } else { throw new IllegalStateException("Expected null but was " + peek() - + " at line " + getLineNumber() + " column " + getColumnNumber()); + + " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath()); } } @@ -885,6 +903,7 @@ public class JsonReader implements Closeable { if (p == PEEKED_LONG) { peeked = PEEKED_NONE; + pathIndices[stackSize - 1]++; return (double) peekedLong; } @@ -897,17 +916,18 @@ public class JsonReader implements Closeable { peekedString = nextUnquotedValue(); } else if (p != PEEKED_BUFFERED) { throw new IllegalStateException("Expected a double but was " + peek() - + " at line " + getLineNumber() + " column " + getColumnNumber()); + + " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath()); } peeked = PEEKED_BUFFERED; double result = Double.parseDouble(peekedString); // don't catch this NumberFormatException. if (!lenient && (Double.isNaN(result) || Double.isInfinite(result))) { throw new MalformedJsonException("JSON forbids NaN and infinities: " + result - + " at line " + getLineNumber() + " column " + getColumnNumber()); + + " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath()); } peekedString = null; peeked = PEEKED_NONE; + pathIndices[stackSize - 1]++; return result; } @@ -929,6 +949,7 @@ public class JsonReader implements Closeable { if (p == PEEKED_LONG) { peeked = PEEKED_NONE; + pathIndices[stackSize - 1]++; return peekedLong; } @@ -940,13 +961,14 @@ public class JsonReader implements Closeable { try { long result = Long.parseLong(peekedString); peeked = PEEKED_NONE; + pathIndices[stackSize - 1]++; return result; } catch (NumberFormatException ignored) { // Fall back to parse as a double below. } } else { throw new IllegalStateException("Expected a long but was " + peek() - + " at line " + getLineNumber() + " column " + getColumnNumber()); + + " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath()); } peeked = PEEKED_BUFFERED; @@ -954,10 +976,11 @@ public class JsonReader implements Closeable { long result = (long) asDouble; if (result != asDouble) { // Make sure no precision was lost casting to 'long'. throw new NumberFormatException("Expected a long but was " + peekedString - + " at line " + getLineNumber() + " column " + getColumnNumber()); + + " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath()); } peekedString = null; peeked = PEEKED_NONE; + pathIndices[stackSize - 1]++; return result; } @@ -1151,9 +1174,10 @@ public class JsonReader implements Closeable { result = (int) peekedLong; if (peekedLong != result) { // Make sure no precision was lost casting to 'int'. throw new NumberFormatException("Expected an int but was " + peekedLong - + " at line " + getLineNumber() + " column " + getColumnNumber()); + + " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath()); } peeked = PEEKED_NONE; + pathIndices[stackSize - 1]++; return result; } @@ -1165,13 +1189,14 @@ public class JsonReader implements Closeable { try { result = Integer.parseInt(peekedString); peeked = PEEKED_NONE; + pathIndices[stackSize - 1]++; return result; } catch (NumberFormatException ignored) { // Fall back to parse as a double below. } } else { throw new IllegalStateException("Expected an int but was " + peek() - + " at line " + getLineNumber() + " column " + getColumnNumber()); + + " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath()); } peeked = PEEKED_BUFFERED; @@ -1179,10 +1204,11 @@ public class JsonReader implements Closeable { result = (int) asDouble; if (result != asDouble) { // Make sure no precision was lost casting to 'int'. throw new NumberFormatException("Expected an int but was " + peekedString - + " at line " + getLineNumber() + " column " + getColumnNumber()); + + " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath()); } peekedString = null; peeked = PEEKED_NONE; + pathIndices[stackSize - 1]++; return result; } @@ -1232,13 +1258,22 @@ public class JsonReader implements Closeable { } peeked = PEEKED_NONE; } while (count != 0); + + pathIndices[stackSize - 1]++; + pathNames[stackSize - 1] = "null"; } private void push(int newTop) { if (stackSize == stack.length) { int[] newStack = new int[stackSize * 2]; + int[] newPathIndices = new int[stackSize * 2]; + String[] newPathNames = new String[stackSize * 2]; System.arraycopy(stack, 0, newStack, 0, stackSize); + System.arraycopy(pathIndices, 0, newPathIndices, 0, stackSize); + System.arraycopy(pathNames, 0, newPathNames, 0, stackSize); stack = newStack; + pathIndices = newPathIndices; + pathNames = newPathNames; } stack[stackSize++] = newTop; } @@ -1431,6 +1466,37 @@ public class JsonReader implements Closeable { + " at line " + getLineNumber() + " column " + getColumnNumber(); } + /** + * Returns a JsonPath to + * 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(); + } + /** * Unescapes the character identified by the character or characters that * immediately follow a backslash. The backslash '\' should have already @@ -1503,7 +1569,7 @@ public class JsonReader implements Closeable { */ private IOException syntaxError(String message) throws IOException { throw new MalformedJsonException(message - + " at line " + getLineNumber() + " column " + getColumnNumber()); + + " at line " + getLineNumber() + " column " + getColumnNumber() + " path " + getPath()); } /** @@ -1547,7 +1613,8 @@ public class JsonReader implements Closeable { reader.peeked = PEEKED_UNQUOTED; } else { throw new IllegalStateException("Expected a name but was " + reader.peek() + " " - + " at line " + reader.getLineNumber() + " column " + reader.getColumnNumber()); + + " at line " + reader.getLineNumber() + " column " + reader.getColumnNumber() + + " path " + reader.getPath()); } } }; diff --git a/gson/src/test/java/com/google/gson/stream/JsonReaderPathTest.java b/gson/src/test/java/com/google/gson/stream/JsonReaderPathTest.java new file mode 100644 index 00000000..fe8c9ed0 --- /dev/null +++ b/gson/src/test/java/com/google/gson/stream/JsonReaderPathTest.java @@ -0,0 +1,176 @@ +/* + * 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.google.gson.stream; + +import java.io.IOException; +import java.io.StringReader; +import junit.framework.TestCase; + +public class JsonReaderPathTest extends TestCase { + public void testPath() throws IOException { + JsonReader reader = new JsonReader( + new StringReader("{\"a\":[2,true,false,null,\"b\",{\"c\":\"d\"},[3]]}")); + assertEquals("$", reader.getPath()); + reader.beginObject(); + assertEquals("$.", reader.getPath()); + reader.nextName(); + assertEquals("$.a", reader.getPath()); + reader.beginArray(); + assertEquals("$.a[0]", reader.getPath()); + reader.nextInt(); + assertEquals("$.a[1]", reader.getPath()); + reader.nextBoolean(); + assertEquals("$.a[2]", reader.getPath()); + reader.nextBoolean(); + assertEquals("$.a[3]", reader.getPath()); + reader.nextNull(); + assertEquals("$.a[4]", reader.getPath()); + reader.nextString(); + assertEquals("$.a[5]", reader.getPath()); + reader.beginObject(); + assertEquals("$.a[5].", reader.getPath()); + reader.nextName(); + assertEquals("$.a[5].c", reader.getPath()); + reader.nextString(); + assertEquals("$.a[5].c", reader.getPath()); + reader.endObject(); + assertEquals("$.a[5]", reader.getPath()); + reader.beginArray(); + assertEquals("$.a[5][0]", reader.getPath()); + reader.nextInt(); + assertEquals("$.a[5][1]", reader.getPath()); + reader.endArray(); + assertEquals("$.a[5]", reader.getPath()); + reader.endArray(); + assertEquals("$.a", reader.getPath()); + reader.endObject(); + assertEquals("$", reader.getPath()); + } + + public void testObjectPath() throws IOException { + JsonReader reader = new JsonReader(new StringReader("{\"a\":1,\"b\":2}")); + assertEquals("$", reader.getPath()); + + reader.peek(); + assertEquals("$", reader.getPath()); + reader.beginObject(); + assertEquals("$.", reader.getPath()); + + reader.peek(); + assertEquals("$.", reader.getPath()); + reader.nextName(); + assertEquals("$.a", reader.getPath()); + + reader.peek(); + assertEquals("$.a", reader.getPath()); + reader.nextInt(); + assertEquals("$.a", reader.getPath()); + + reader.peek(); + assertEquals("$.a", reader.getPath()); + reader.nextName(); + assertEquals("$.b", reader.getPath()); + + reader.peek(); + assertEquals("$.b", reader.getPath()); + reader.nextInt(); + assertEquals("$.b", reader.getPath()); + + reader.peek(); + assertEquals("$.b", reader.getPath()); + reader.endObject(); + assertEquals("$", reader.getPath()); + + reader.peek(); + assertEquals("$", reader.getPath()); + reader.close(); + assertEquals("$", reader.getPath()); + } + + public void testArrayPath() throws IOException { + JsonReader reader = new JsonReader(new StringReader("[1,2]")); + assertEquals("$", reader.getPath()); + + reader.peek(); + assertEquals("$", reader.getPath()); + reader.beginArray(); + assertEquals("$[0]", reader.getPath()); + + reader.peek(); + assertEquals("$[0]", reader.getPath()); + reader.nextInt(); + assertEquals("$[1]", reader.getPath()); + + reader.peek(); + assertEquals("$[1]", reader.getPath()); + reader.nextInt(); + assertEquals("$[2]", reader.getPath()); + + reader.peek(); + assertEquals("$[2]", reader.getPath()); + reader.endArray(); + assertEquals("$", reader.getPath()); + + reader.peek(); + assertEquals("$", reader.getPath()); + reader.close(); + assertEquals("$", reader.getPath()); + } + + public void testMultipleTopLevelValuesInOneDocument() throws IOException { + JsonReader reader = new JsonReader(new StringReader("[][]")); + reader.setLenient(true); + reader.beginArray(); + reader.endArray(); + assertEquals("$", reader.getPath()); + reader.beginArray(); + reader.endArray(); + assertEquals("$", reader.getPath()); + } + + public void testSkipArrayElements() throws IOException { + JsonReader reader = new JsonReader(new StringReader("[1,2,3]")); + reader.beginArray(); + reader.skipValue(); + reader.skipValue(); + assertEquals("$[2]", reader.getPath()); + } + + public void testSkipObjectNames() throws IOException { + JsonReader reader = new JsonReader(new StringReader("{\"a\":1}")); + reader.beginObject(); + reader.skipValue(); + assertEquals("$.null", reader.getPath()); + } + + public void testSkipObjectValues() throws IOException { + JsonReader reader = new JsonReader(new StringReader("{\"a\":1,\"b\":2}")); + reader.beginObject(); + reader.nextName(); + reader.skipValue(); + assertEquals("$.null", reader.getPath()); + reader.nextName(); + assertEquals("$.b", reader.getPath()); + } + + public void testSkipNestedStructures() throws IOException { + JsonReader reader = new JsonReader(new StringReader("[[1,2,3],4]")); + reader.beginArray(); + reader.skipValue(); + assertEquals("$[1]", reader.getPath()); + } +} diff --git a/gson/src/test/java/com/google/gson/stream/JsonReaderTest.java b/gson/src/test/java/com/google/gson/stream/JsonReaderTest.java index 6274cd43..ad35c067 100644 --- a/gson/src/test/java/com/google/gson/stream/JsonReaderTest.java +++ b/gson/src/test/java/com/google/gson/stream/JsonReaderTest.java @@ -1326,45 +1326,46 @@ public final class JsonReaderTest extends TestCase { } public void testFailWithPosition() throws IOException { - testFailWithPosition("Expected value at line 6 column 5", + testFailWithPosition("Expected value at line 6 column 5 path $[1]", "[\n\n\n\n\n\"a\",}]"); } public void testFailWithPositionGreaterThanBufferSize() throws IOException { String spaces = repeat(' ', 8192); - testFailWithPosition("Expected value at line 6 column 5", + testFailWithPosition("Expected value at line 6 column 5 path $[1]", "[\n\n" + spaces + "\n\n\n\"a\",}]"); } public void testFailWithPositionOverSlashSlashEndOfLineComment() throws IOException { - testFailWithPosition("Expected value at line 5 column 6", + testFailWithPosition("Expected value at line 5 column 6 path $[1]", "\n// foo\n\n//bar\r\n[\"a\",}"); } public void testFailWithPositionOverHashEndOfLineComment() throws IOException { - testFailWithPosition("Expected value at line 5 column 6", + testFailWithPosition("Expected value at line 5 column 6 path $[1]", "\n# foo\n\n#bar\r\n[\"a\",}"); } public void testFailWithPositionOverCStyleComment() throws IOException { - testFailWithPosition("Expected value at line 6 column 12", + testFailWithPosition("Expected value at line 6 column 12 path $[1]", "\n\n/* foo\n*\n*\r\nbar */[\"a\",}"); } public void testFailWithPositionOverQuotedString() throws IOException { - testFailWithPosition("Expected value at line 5 column 3", "[\"foo\nbar\r\nbaz\n\",\n }"); + testFailWithPosition("Expected value at line 5 column 3 path $[1]", + "[\"foo\nbar\r\nbaz\n\",\n }"); } public void testFailWithPositionOverUnquotedString() throws IOException { - testFailWithPosition("Expected value at line 5 column 2", "[\n\nabcd\n\n,}"); + testFailWithPosition("Expected value at line 5 column 2 path $[1]", "[\n\nabcd\n\n,}"); } public void testFailWithEscapedNewlineCharacter() throws IOException { - testFailWithPosition("Expected value at line 5 column 3", "[\n\n\"\\\n\n\",}"); + testFailWithPosition("Expected value at line 5 column 3 path $[1]", "[\n\n\"\\\n\n\",}"); } public void testFailWithPositionIsOffsetByBom() throws IOException { - testFailWithPosition("Expected value at line 1 column 6", + testFailWithPosition("Expected value at line 1 column 6 path $[1]", "\ufeff[\"a\",}]"); } @@ -1394,6 +1395,23 @@ public final class JsonReaderTest extends TestCase { } } + public void testFailWithPositionDeepPath() throws IOException { + JsonReader reader = new JsonReader(reader("[1,{\"a\":[2,3,}")); + reader.beginArray(); + reader.nextInt(); + reader.beginObject(); + reader.nextName(); + reader.beginArray(); + reader.nextInt(); + reader.nextInt(); + try { + reader.peek(); + fail(); + } catch (IOException expected) { + assertEquals("Expected value at line 1 column 14 path $[1].a[2]", expected.getMessage()); + } + } + public void testStrictVeryLongNumber() throws IOException { JsonReader reader = new JsonReader(reader("[0." + repeat('9', 8192) + "]")); reader.beginArray(); @@ -1430,6 +1448,8 @@ public final class JsonReaderTest extends TestCase { for (int i = 0; i < 40; i++) { reader.beginArray(); } + assertEquals("$[0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0]" + + "[0][0][0][0][0][0][0][0][0][0][0][0][0][0]", reader.getPath()); for (int i = 0; i < 40; i++) { reader.endArray(); } @@ -1449,6 +1469,8 @@ public final class JsonReaderTest extends TestCase { reader.beginObject(); assertEquals("a", reader.nextName()); } + assertEquals("$.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a" + + ".a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a", reader.getPath()); assertEquals(true, reader.nextBoolean()); for (int i = 0; i < 40; i++) { reader.endObject();