diff --git a/gson/src/main/java/com/google/gson/internal/bind/JsonTreeReader.java b/gson/src/main/java/com/google/gson/internal/bind/JsonTreeReader.java index 4cd24e2d8c..e47c57c73c 100644 --- a/gson/src/main/java/com/google/gson/internal/bind/JsonTreeReader.java +++ b/gson/src/main/java/com/google/gson/internal/bind/JsonTreeReader.java @@ -93,6 +93,7 @@ public JsonTreeReader(JsonElement element) { @Override public void endObject() throws IOException { expect(JsonToken.END_OBJECT); + pathNames[stackSize - 1] = null; // Free the last path name so that it can be garbage collected popStack(); // empty iterator popStack(); // object if (stackSize > 0) { @@ -165,16 +166,20 @@ private void expect(JsonToken expected) throws IOException { } } - @Override public String nextName() throws IOException { + private String nextName(boolean skipName) throws IOException { expect(JsonToken.NAME); Iterator i = (Iterator) peekStack(); Map.Entry entry = (Map.Entry) i.next(); String result = (String) entry.getKey(); - pathNames[stackSize - 1] = result; + pathNames[stackSize - 1] = skipName ? "" : result; push(entry.getValue()); return result; } + @Override public String nextName() throws IOException { + return nextName(false); + } + @Override public String nextString() throws IOException { JsonToken token = peek(); if (token != JsonToken.STRING && token != JsonToken.NUMBER) { @@ -269,17 +274,26 @@ JsonElement nextJsonElement() throws IOException { } @Override public void skipValue() throws IOException { - if (peek() == JsonToken.NAME) { - nextName(); - pathNames[stackSize - 2] = "null"; - } else { - popStack(); - if (stackSize > 0) { - pathNames[stackSize - 1] = "null"; - } - } - if (stackSize > 0) { - pathIndices[stackSize - 1]++; + JsonToken peeked = peek(); + switch (peeked) { + case NAME: + nextName(true); + break; + case END_ARRAY: + endArray(); + break; + case END_OBJECT: + endObject(); + break; + case END_DOCUMENT: + // Do nothing + break; + default: + popStack(); + if (stackSize > 0) { + pathIndices[stackSize - 1]++; + } + break; } } 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 925c50f780..ed6bab977a 100644 --- a/gson/src/main/java/com/google/gson/stream/JsonReader.java +++ b/gson/src/main/java/com/google/gson/stream/JsonReader.java @@ -777,10 +777,9 @@ private boolean isLiteral(char c) throws IOException { } /** - * Returns the next token, a {@link com.google.gson.stream.JsonToken#NAME property name}, and - * consumes it. + * Returns the next token, a {@link JsonToken#NAME property name}, and consumes it. * - * @throws java.io.IOException if the next token in the stream is not a property + * @throws IOException if the next token in the stream is not a property * name. */ public String nextName() throws IOException { @@ -804,7 +803,7 @@ public String nextName() throws IOException { } /** - * Returns the {@link com.google.gson.stream.JsonToken#STRING string} value of the next token, + * Returns the {@link JsonToken#STRING string} value of the next token, * consuming it. If the next token is a number, this method will return its * string form. * @@ -840,7 +839,7 @@ public String nextString() throws IOException { } /** - * Returns the {@link com.google.gson.stream.JsonToken#BOOLEAN boolean} value of the next token, + * Returns the {@link JsonToken#BOOLEAN boolean} value of the next token, * consuming it. * * @throws IllegalStateException if the next token is not a boolean or if @@ -884,7 +883,7 @@ public void nextNull() throws IOException { } /** - * Returns the {@link com.google.gson.stream.JsonToken#NUMBER double} value of the next token, + * Returns the {@link JsonToken#NUMBER double} value of the next token, * consuming it. If the next token is a string, this method will attempt to * parse it as a double using {@link Double#parseDouble(String)}. * @@ -930,7 +929,7 @@ public double nextDouble() throws IOException { } /** - * Returns the {@link com.google.gson.stream.JsonToken#NUMBER long} value of the next token, + * Returns the {@link JsonToken#NUMBER long} value of the next token, * consuming it. If the next token is a string, this method will attempt to * parse it as a long. If the next token's numeric value cannot be exactly * represented by a Java {@code long}, this method throws. @@ -1163,7 +1162,7 @@ private void skipUnquotedValue() throws IOException { } /** - * Returns the {@link com.google.gson.stream.JsonToken#NUMBER int} value of the next token, + * Returns the {@link JsonToken#NUMBER int} value of the next token, * consuming it. If the next token is a string, this method will attempt to * parse it as an int. If the next token's numeric value cannot be exactly * represented by a Java {@code int}, this method throws. @@ -1223,7 +1222,7 @@ public int nextInt() throws IOException { } /** - * Closes this JSON reader and the underlying {@link java.io.Reader}. + * Closes this JSON reader and the underlying {@link Reader}. */ @Override public void close() throws IOException { peeked = PEEKED_NONE; @@ -1233,9 +1232,19 @@ public int nextInt() throws IOException { } /** - * Skips the next value recursively. If it is an object or array, all nested - * elements are skipped. This method is intended for use when the JSON token - * stream contains unrecognized or unhandled values. + * Skips the next value recursively. This method is intended for use when + * the JSON token stream contains unrecognized or unhandled values. + * + *

The behavior depends on the type of the next JSON token: + *

*/ public void skipValue() throws IOException { int count = 0; @@ -1245,32 +1254,69 @@ public void skipValue() throws IOException { p = doPeek(); } - if (p == PEEKED_BEGIN_ARRAY) { - push(JsonScope.EMPTY_ARRAY); - count++; - } else if (p == PEEKED_BEGIN_OBJECT) { - push(JsonScope.EMPTY_OBJECT); - count++; - } else if (p == PEEKED_END_ARRAY) { - stackSize--; - count--; - } else if (p == PEEKED_END_OBJECT) { - stackSize--; - count--; - } else if (p == PEEKED_UNQUOTED_NAME || p == PEEKED_UNQUOTED) { - skipUnquotedValue(); - } else if (p == PEEKED_SINGLE_QUOTED || p == PEEKED_SINGLE_QUOTED_NAME) { - skipQuotedValue('\''); - } else if (p == PEEKED_DOUBLE_QUOTED || p == PEEKED_DOUBLE_QUOTED_NAME) { - skipQuotedValue('"'); - } else if (p == PEEKED_NUMBER) { - pos += peekedNumberLength; + switch (p) { + case PEEKED_BEGIN_ARRAY: + push(JsonScope.EMPTY_ARRAY); + count++; + break; + case PEEKED_BEGIN_OBJECT: + push(JsonScope.EMPTY_OBJECT); + count++; + break; + case PEEKED_END_ARRAY: + stackSize--; + count--; + break; + case PEEKED_END_OBJECT: + // Only update when object end is explicitly skipped, otherwise stack is not updated anyways + if (count == 0) { + pathNames[stackSize - 1] = null; // Free the last path name so that it can be garbage collected + } + stackSize--; + count--; + break; + case PEEKED_UNQUOTED: + skipUnquotedValue(); + break; + case PEEKED_SINGLE_QUOTED: + skipQuotedValue('\''); + break; + case PEEKED_DOUBLE_QUOTED: + skipQuotedValue('"'); + break; + case PEEKED_UNQUOTED_NAME: + skipUnquotedValue(); + // Only update when name is explicitly skipped, otherwise stack is not updated anyways + if (count == 0) { + pathNames[stackSize - 1] = ""; + } + break; + case PEEKED_SINGLE_QUOTED_NAME: + skipQuotedValue('\''); + // Only update when name is explicitly skipped, otherwise stack is not updated anyways + if (count == 0) { + pathNames[stackSize - 1] = ""; + } + break; + case PEEKED_DOUBLE_QUOTED_NAME: + skipQuotedValue('"'); + // Only update when name is explicitly skipped, otherwise stack is not updated anyways + if (count == 0) { + pathNames[stackSize - 1] = ""; + } + break; + case PEEKED_NUMBER: + pos += peekedNumberLength; + break; + case PEEKED_EOF: + // Do nothing + return; + // For all other tokens there is nothing to do; token has already been consumed from underlying reader } peeked = PEEKED_NONE; - } while (count != 0); + } while (count > 0); pathIndices[stackSize - 1]++; - pathNames[stackSize - 1] = "null"; } private void push(int newTop) { @@ -1505,7 +1551,7 @@ private String getPath(boolean usePreviousPath) { *
  • For JSON arrays the path points to the index of the previous element.
    * If no element has been consumed yet it uses the index 0 (even if there are no elements).
  • *
  • For JSON objects the path points to the last property, or to the current - * property if its value has not been consumed yet.
  • + * property if its name has already been consumed. * * *

    This method can be useful to add additional context to exception messages @@ -1522,7 +1568,7 @@ public String getPreviousPath() { *

  • For JSON arrays the path points to the index of the next element (even * if there are no further elements).
  • *
  • For JSON objects the path points to the last property, or to the current - * property if its value has not been consumed yet.
  • + * property if its name has already been consumed. * * *

    This method can be useful to add additional context to exception messages diff --git a/gson/src/test/java/com/google/gson/internal/bind/JsonTreeReaderTest.java b/gson/src/test/java/com/google/gson/internal/bind/JsonTreeReaderTest.java index 4e6a218efb..767d63bda3 100644 --- a/gson/src/test/java/com/google/gson/internal/bind/JsonTreeReaderTest.java +++ b/gson/src/test/java/com/google/gson/internal/bind/JsonTreeReaderTest.java @@ -35,6 +35,7 @@ public void testSkipValue_emptyJsonObject() throws IOException { JsonTreeReader in = new JsonTreeReader(new JsonObject()); in.skipValue(); assertEquals(JsonToken.END_DOCUMENT, in.peek()); + assertEquals("$", in.getPath()); } public void testSkipValue_filledJsonObject() throws IOException { @@ -53,6 +54,46 @@ public void testSkipValue_filledJsonObject() throws IOException { JsonTreeReader in = new JsonTreeReader(jsonObject); in.skipValue(); assertEquals(JsonToken.END_DOCUMENT, in.peek()); + assertEquals("$", in.getPath()); + } + + public void testSkipValue_name() throws IOException { + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("a", "value"); + JsonTreeReader in = new JsonTreeReader(jsonObject); + in.beginObject(); + in.skipValue(); + assertEquals(JsonToken.STRING, in.peek()); + assertEquals("$.", in.getPath()); + assertEquals("value", in.nextString()); + } + + public void testSkipValue_afterEndOfDocument() throws IOException { + JsonTreeReader reader = new JsonTreeReader(new JsonObject()); + reader.beginObject(); + reader.endObject(); + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + + assertEquals("$", reader.getPath()); + reader.skipValue(); + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + assertEquals("$", reader.getPath()); + } + + public void testSkipValue_atArrayEnd() throws IOException { + JsonTreeReader reader = new JsonTreeReader(new JsonArray()); + reader.beginArray(); + reader.skipValue(); + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + assertEquals("$", reader.getPath()); + } + + public void testSkipValue_atObjectEnd() throws IOException { + JsonTreeReader reader = new JsonTreeReader(new JsonObject()); + reader.beginObject(); + reader.skipValue(); + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + assertEquals("$", reader.getPath()); } public void testHasNext_endOfDocument() throws IOException { diff --git a/gson/src/test/java/com/google/gson/stream/JsonReaderPathTest.java b/gson/src/test/java/com/google/gson/stream/JsonReaderPathTest.java index ab802be1d7..a755bd8349 100644 --- a/gson/src/test/java/com/google/gson/stream/JsonReaderPathTest.java +++ b/gson/src/test/java/com/google/gson/stream/JsonReaderPathTest.java @@ -16,6 +16,9 @@ package com.google.gson.stream; +import static org.junit.Assert.assertEquals; +import static org.junit.Assume.assumeTrue; + import com.google.gson.JsonElement; import com.google.gson.internal.Streams; import com.google.gson.internal.bind.JsonTreeReader; @@ -27,9 +30,6 @@ import org.junit.runner.RunWith; import org.junit.runners.Parameterized; -import static org.junit.Assert.assertEquals; -import static org.junit.Assume.assumeTrue; - @SuppressWarnings("resource") @RunWith(Parameterized.class) public class JsonReaderPathTest { @@ -221,12 +221,27 @@ public static List parameters() { assertEquals("$[2]", reader.getPath()); } + @Test public void skipArrayEnd() throws IOException { + JsonReader reader = factory.create("[[],1]"); + reader.beginArray(); + reader.beginArray(); + assertEquals("$[0][0]", reader.getPreviousPath()); + assertEquals("$[0][0]", reader.getPath()); + reader.skipValue(); // skip end of array + assertEquals("$[0]", reader.getPreviousPath()); + assertEquals("$[1]", reader.getPath()); + } + @Test public void skipObjectNames() throws IOException { - JsonReader reader = factory.create("{\"a\":1}"); + JsonReader reader = factory.create("{\"a\":[]}"); reader.beginObject(); reader.skipValue(); - assertEquals("$.null", reader.getPreviousPath()); - assertEquals("$.null", reader.getPath()); + assertEquals("$.", reader.getPreviousPath()); + assertEquals("$.", reader.getPath()); + + reader.beginArray(); + assertEquals("$.[0]", reader.getPreviousPath()); + assertEquals("$.[0]", reader.getPath()); } @Test public void skipObjectValues() throws IOException { @@ -236,13 +251,25 @@ public static List parameters() { assertEquals("$.", reader.getPath()); reader.nextName(); reader.skipValue(); - assertEquals("$.null", reader.getPreviousPath()); - assertEquals("$.null", reader.getPath()); + assertEquals("$.a", reader.getPreviousPath()); + assertEquals("$.a", reader.getPath()); reader.nextName(); assertEquals("$.b", reader.getPreviousPath()); assertEquals("$.b", reader.getPath()); } + @Test public void skipObjectEnd() throws IOException { + JsonReader reader = factory.create("{\"a\":{},\"b\":2}"); + reader.beginObject(); + reader.nextName(); + reader.beginObject(); + assertEquals("$.a.", reader.getPreviousPath()); + assertEquals("$.a.", reader.getPath()); + reader.skipValue(); // skip end of object + assertEquals("$.a", reader.getPreviousPath()); + assertEquals("$.a", reader.getPath()); + } + @Test public void skipNestedStructures() throws IOException { JsonReader reader = factory.create("[[1,2,3],4]"); reader.beginArray(); @@ -251,6 +278,20 @@ public static List parameters() { assertEquals("$[1]", reader.getPath()); } + @Test public void skipEndOfDocument() throws IOException { + JsonReader reader = factory.create("[]"); + reader.beginArray(); + reader.endArray(); + assertEquals("$", reader.getPreviousPath()); + assertEquals("$", reader.getPath()); + reader.skipValue(); + assertEquals("$", reader.getPreviousPath()); + assertEquals("$", reader.getPath()); + reader.skipValue(); + assertEquals("$", reader.getPreviousPath()); + assertEquals("$", reader.getPath()); + } + @Test public void arrayOfObjects() throws IOException { JsonReader reader = factory.create("[{},{},{}]"); reader.beginArray(); @@ -307,6 +348,52 @@ public static List parameters() { assertEquals("$", reader.getPath()); } + @Test public void objectOfObjects() throws IOException { + JsonReader reader = factory.create("{\"a\":{\"a1\":1,\"a2\":2},\"b\":{\"b1\":1}}"); + reader.beginObject(); + assertEquals("$.", reader.getPreviousPath()); + assertEquals("$.", reader.getPath()); + reader.nextName(); + assertEquals("$.a", reader.getPreviousPath()); + assertEquals("$.a", reader.getPath()); + reader.beginObject(); + assertEquals("$.a.", reader.getPreviousPath()); + assertEquals("$.a.", reader.getPath()); + reader.nextName(); + assertEquals("$.a.a1", reader.getPreviousPath()); + assertEquals("$.a.a1", reader.getPath()); + reader.nextInt(); + assertEquals("$.a.a1", reader.getPreviousPath()); + assertEquals("$.a.a1", reader.getPath()); + reader.nextName(); + assertEquals("$.a.a2", reader.getPreviousPath()); + assertEquals("$.a.a2", reader.getPath()); + reader.nextInt(); + assertEquals("$.a.a2", reader.getPreviousPath()); + assertEquals("$.a.a2", reader.getPath()); + reader.endObject(); + assertEquals("$.a", reader.getPreviousPath()); + assertEquals("$.a", reader.getPath()); + reader.nextName(); + assertEquals("$.b", reader.getPreviousPath()); + assertEquals("$.b", reader.getPath()); + reader.beginObject(); + assertEquals("$.b.", reader.getPreviousPath()); + assertEquals("$.b.", reader.getPath()); + reader.nextName(); + assertEquals("$.b.b1", reader.getPreviousPath()); + assertEquals("$.b.b1", reader.getPath()); + reader.nextInt(); + assertEquals("$.b.b1", reader.getPreviousPath()); + assertEquals("$.b.b1", reader.getPath()); + reader.endObject(); + assertEquals("$.b", reader.getPreviousPath()); + assertEquals("$.b", reader.getPath()); + reader.endObject(); + assertEquals("$", reader.getPreviousPath()); + assertEquals("$", reader.getPath()); + } + public enum Factory { STRING_READER { @Override public JsonReader create(String data) { 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 7ec5e46202..faaa87a243 100644 --- a/gson/src/test/java/com/google/gson/stream/JsonReaderTest.java +++ b/gson/src/test/java/com/google/gson/stream/JsonReaderTest.java @@ -16,13 +16,6 @@ package com.google.gson.stream; -import java.io.EOFException; -import java.io.IOException; -import java.io.Reader; -import java.io.StringReader; -import java.util.Arrays; -import junit.framework.TestCase; - import static com.google.gson.stream.JsonToken.BEGIN_ARRAY; import static com.google.gson.stream.JsonToken.BEGIN_OBJECT; import static com.google.gson.stream.JsonToken.BOOLEAN; @@ -33,6 +26,13 @@ import static com.google.gson.stream.JsonToken.NUMBER; import static com.google.gson.stream.JsonToken.STRING; +import java.io.EOFException; +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; +import java.util.Arrays; +import junit.framework.TestCase; + @SuppressWarnings("resource") public final class JsonReaderTest extends TestCase { public void testReadArray() throws IOException { @@ -140,6 +140,35 @@ public void testSkipObjectAfterPeek() throws Exception { assertEquals(JsonToken.END_DOCUMENT, reader.peek()); } + public void testSkipObjectName() throws IOException { + JsonReader reader = new JsonReader(reader("{\"a\": 1}")); + reader.beginObject(); + reader.skipValue(); + assertEquals(JsonToken.NUMBER, reader.peek()); + assertEquals("$.", reader.getPath()); + assertEquals(1, reader.nextInt()); + } + + public void testSkipObjectNameSingleQuoted() throws IOException { + JsonReader reader = new JsonReader(reader("{'a': 1}")); + reader.setLenient(true); + reader.beginObject(); + reader.skipValue(); + assertEquals(JsonToken.NUMBER, reader.peek()); + assertEquals("$.", reader.getPath()); + assertEquals(1, reader.nextInt()); + } + + public void testSkipObjectNameUnquoted() throws IOException { + JsonReader reader = new JsonReader(reader("{a: 1}")); + reader.setLenient(true); + reader.beginObject(); + reader.skipValue(); + assertEquals(JsonToken.NUMBER, reader.peek()); + assertEquals("$.", reader.getPath()); + assertEquals(1, reader.nextInt()); + } + public void testSkipInteger() throws IOException { JsonReader reader = new JsonReader(reader( "{\"a\":123456789,\"b\":-123456789}")); @@ -164,6 +193,34 @@ public void testSkipDouble() throws IOException { assertEquals(JsonToken.END_DOCUMENT, reader.peek()); } + public void testSkipValueAfterEndOfDocument() throws IOException { + JsonReader reader = new JsonReader(reader("{}")); + reader.beginObject(); + reader.endObject(); + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + + assertEquals("$", reader.getPath()); + reader.skipValue(); + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + assertEquals("$", reader.getPath()); + } + + public void testSkipValueAtArrayEnd() throws IOException { + JsonReader reader = new JsonReader(reader("[]")); + reader.beginArray(); + reader.skipValue(); + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + assertEquals("$", reader.getPath()); + } + + public void testSkipValueAtObjectEnd() throws IOException { + JsonReader reader = new JsonReader(reader("{}")); + reader.beginObject(); + reader.skipValue(); + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + assertEquals("$", reader.getPath()); + } + public void testHelloWorld() throws IOException { String json = "{\n" + " \"hello\": true,\n" +