Implement JsonPath in JsonReader.

This commit is contained in:
Jesse Wilson 2014-07-31 05:17:54 +00:00
parent 80bbf4a85a
commit fbc7e69c81
3 changed files with 291 additions and 26 deletions

View File

@ -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 <a href="http://goessner.net/articles/JsonPath/">JsonPath</a> 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());
}
}
};

View File

@ -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());
}
}

View File

@ -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();