From d3c584bd5b000e15ebcf8f6715a91888cf229a15 Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Sun, 23 Jan 2022 02:18:16 +0100 Subject: [PATCH 1/4] Fix JsonReader.skipValue() not behaving properly at end of document JsonReader implementation erroneously reset `peeked` to PEEKED_NONE; JsonTreeReader threw ArrayIndexOutOfBoundsException. --- .../google/gson/internal/bind/JsonTreeReader.java | 5 ++++- .../java/com/google/gson/stream/JsonReader.java | 5 +++++ .../gson/internal/bind/JsonTreeReaderTest.java | 14 ++++++++++++++ .../com/google/gson/stream/JsonReaderTest.java | 14 ++++++++++++++ 4 files changed, 37 insertions(+), 1 deletion(-) 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 f8238bc28b..ea2fd962bb 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 @@ -268,9 +268,12 @@ JsonElement nextJsonElement() throws IOException { } @Override public void skipValue() throws IOException { - if (peek() == JsonToken.NAME) { + JsonToken peeked = peek(); + if (peeked == JsonToken.NAME) { nextName(); pathNames[stackSize - 2] = "null"; + } else if (peeked == JsonToken.END_DOCUMENT) { + throw new IllegalStateException("Cannot skip at end of document"); } else { popStack(); if (stackSize > 0) { 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 a8cb22aa32..6cbc9a39c6 100644 --- a/gson/src/main/java/com/google/gson/stream/JsonReader.java +++ b/gson/src/main/java/com/google/gson/stream/JsonReader.java @@ -1223,6 +1223,9 @@ public void close() 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. + * + * @throws IllegalStateException when called at the end of the document, that is, + * after the last value. */ public void skipValue() throws IOException { int count = 0; @@ -1252,6 +1255,8 @@ public void skipValue() throws IOException { skipQuotedValue('"'); } else if (p == PEEKED_NUMBER) { pos += peekedNumberLength; + } else if (p == PEEKED_EOF) { + throw new IllegalStateException("Cannot skip at end of document"); } peeked = PEEKED_NONE; } while (count != 0); 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 f6a5bf33e8..7e5d5454c4 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 @@ -47,4 +47,18 @@ public void testSkipValue_filledJsonObject() throws IOException { in.skipValue(); assertEquals(JsonToken.END_DOCUMENT, in.peek()); } + + public void testSkipValue_afterEndOfDocument() throws IOException { + JsonTreeReader reader = new JsonTreeReader(new JsonObject()); + reader.beginObject(); + reader.endObject(); + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + + try { + reader.skipValue(); + fail(); + } catch (IllegalStateException e) { + assertEquals("Cannot skip at end of document", e.getMessage()); + } + } } 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 65cbd075f0..246e689e32 100644 --- a/gson/src/test/java/com/google/gson/stream/JsonReaderTest.java +++ b/gson/src/test/java/com/google/gson/stream/JsonReaderTest.java @@ -157,6 +157,20 @@ 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()); + + try { + reader.skipValue(); + fail(); + } catch (IllegalStateException e) { + assertEquals("Cannot skip at end of document", e.getMessage()); + } + } + public void testHelloWorld() throws IOException { String json = "{\n" + " \"hello\": true,\n" + From 08e614daf607f5fae437a593fff7e2930a6f5c37 Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Sun, 23 Jan 2022 02:29:57 +0100 Subject: [PATCH 2/4] Fix JsonReader.skipValue() not behaving properly at end of array and object For JsonReader this caused an IllegalStateException (in the past it caused JsonReader to get stuck in an infinite loop); for JsonTreeReader it only popped the empty iterator but not the JsonArray or JsonObject, which caused peek() to again report END_ARRAY or END_OBJECT. --- .../google/gson/internal/bind/JsonTreeReader.java | 4 ++++ .../java/com/google/gson/stream/JsonReader.java | 12 ++++++++---- .../gson/internal/bind/JsonTreeReaderTest.java | 14 ++++++++++++++ .../com/google/gson/stream/JsonReaderTest.java | 14 ++++++++++++++ 4 files changed, 40 insertions(+), 4 deletions(-) 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 ea2fd962bb..8c69e5cdd5 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 @@ -272,6 +272,10 @@ JsonElement nextJsonElement() throws IOException { if (peeked == JsonToken.NAME) { nextName(); pathNames[stackSize - 2] = "null"; + } else if (peeked == JsonToken.END_ARRAY) { + endArray(); + } else if (peeked == JsonToken.END_OBJECT) { + endObject(); } else if (peeked == JsonToken.END_DOCUMENT) { throw new IllegalStateException("Cannot skip at end of document"); } else { 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 6cbc9a39c6..3551e5ddc9 100644 --- a/gson/src/main/java/com/google/gson/stream/JsonReader.java +++ b/gson/src/main/java/com/google/gson/stream/JsonReader.java @@ -1220,9 +1220,13 @@ public void close() 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. + * + *

If the next value is a JSON object or array, all nested elements are + * skipped. For JSON objects when called before the name of a property has been + * consumed, only the name is skipped. When called at the end of a JSON array + * or object, the end of the array or object is skipped. * * @throws IllegalStateException when called at the end of the document, that is, * after the last value. @@ -1259,7 +1263,7 @@ public void skipValue() throws IOException { throw new IllegalStateException("Cannot skip at end of document"); } peeked = PEEKED_NONE; - } while (count != 0); + } while (count > 0); pathIndices[stackSize - 1]++; pathNames[stackSize - 1] = "null"; 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 7e5d5454c4..c9bb999dcd 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 @@ -61,4 +61,18 @@ public void testSkipValue_afterEndOfDocument() throws IOException { assertEquals("Cannot skip at end of document", e.getMessage()); } } + + public void testSkipValue_atArrayEnd() throws IOException { + JsonTreeReader reader = new JsonTreeReader(new JsonArray()); + reader.beginArray(); + reader.skipValue(); + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + } + + public void testSkipValue_atObjectEnd() throws IOException { + JsonTreeReader reader = new JsonTreeReader(new JsonObject()); + reader.beginObject(); + reader.skipValue(); + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + } } 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 246e689e32..e51962371c 100644 --- a/gson/src/test/java/com/google/gson/stream/JsonReaderTest.java +++ b/gson/src/test/java/com/google/gson/stream/JsonReaderTest.java @@ -171,6 +171,20 @@ public void testSkipValueAfterEndOfDocument() throws IOException { } } + public void testSkipValueAtArrayEnd() throws IOException { + JsonReader reader = new JsonReader(reader("[]")); + reader.beginArray(); + reader.skipValue(); + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + } + + public void testSkipValueAtObjectEnd() throws IOException { + JsonReader reader = new JsonReader(reader("{}")); + reader.beginObject(); + reader.skipValue(); + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + } + public void testHelloWorld() throws IOException { String json = "{\n" + " \"hello\": true,\n" + From 98f62466f5496cf10ae9610ea87054308ae401d0 Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Sun, 23 Jan 2022 02:51:23 +0100 Subject: [PATCH 3/4] Only have JsonReader.skipValue() overwrite path name when name was skipped This improves the JSON path when the value for a property was skipped and before the subsequent property (or the end of the object) getPath() is called. --- .../gson/internal/bind/JsonTreeReader.java | 6 ++-- .../com/google/gson/stream/JsonReader.java | 29 ++++++++++++++++--- .../gson/stream/JsonReaderPathTest.java | 8 ++--- 3 files changed, 31 insertions(+), 12 deletions(-) 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 8c69e5cdd5..32c4e353db 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 @@ -95,6 +95,7 @@ public JsonTreeReader(JsonElement element) { popStack(); // empty iterator popStack(); // object if (stackSize > 0) { + pathNames[stackSize - 1] = null; // Free the last path name so that it can be garbage collected pathIndices[stackSize - 1]++; } } @@ -271,7 +272,7 @@ JsonElement nextJsonElement() throws IOException { JsonToken peeked = peek(); if (peeked == JsonToken.NAME) { nextName(); - pathNames[stackSize - 2] = "null"; + pathNames[stackSize - 2] = ""; } else if (peeked == JsonToken.END_ARRAY) { endArray(); } else if (peeked == JsonToken.END_OBJECT) { @@ -280,9 +281,6 @@ JsonElement nextJsonElement() throws IOException { throw new IllegalStateException("Cannot skip at end of document"); } else { popStack(); - if (stackSize > 0) { - pathNames[stackSize - 1] = "null"; - } } if (stackSize > 0) { pathIndices[stackSize - 1]++; 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 3551e5ddc9..c89b13d77e 100644 --- a/gson/src/main/java/com/google/gson/stream/JsonReader.java +++ b/gson/src/main/java/com/google/gson/stream/JsonReader.java @@ -1249,14 +1249,36 @@ public void skipValue() throws IOException { stackSize--; count--; } else if (p == 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--; - } else if (p == PEEKED_UNQUOTED_NAME || p == PEEKED_UNQUOTED) { + } else if (p == PEEKED_UNQUOTED) { skipUnquotedValue(); - } else if (p == PEEKED_SINGLE_QUOTED || p == PEEKED_SINGLE_QUOTED_NAME) { + } else if (p == PEEKED_SINGLE_QUOTED) { skipQuotedValue('\''); - } else if (p == PEEKED_DOUBLE_QUOTED || p == PEEKED_DOUBLE_QUOTED_NAME) { + } else if (p == PEEKED_DOUBLE_QUOTED) { skipQuotedValue('"'); + } else if (p == PEEKED_UNQUOTED_NAME) { + skipUnquotedValue(); + // Only update when name is explicitly skipped, otherwise stack is not updated anyways + if (count == 0) { + pathNames[stackSize - 1] = ""; + } + } else if (p == PEEKED_SINGLE_QUOTED_NAME) { + skipQuotedValue('\''); + // Only update when name is explicitly skipped, otherwise stack is not updated anyways + if (count == 0) { + pathNames[stackSize - 1] = ""; + } + } else if (p == PEEKED_DOUBLE_QUOTED_NAME) { + skipQuotedValue('"'); + // Only update when name is explicitly skipped, otherwise stack is not updated anyways + if (count == 0) { + pathNames[stackSize - 1] = ""; + } } else if (p == PEEKED_NUMBER) { pos += peekedNumberLength; } else if (p == PEEKED_EOF) { @@ -1266,7 +1288,6 @@ public void skipValue() throws IOException { } while (count > 0); pathIndices[stackSize - 1]++; - pathNames[stackSize - 1] = "null"; } private void push(int newTop) { 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..f58e1808b7 100644 --- a/gson/src/test/java/com/google/gson/stream/JsonReaderPathTest.java +++ b/gson/src/test/java/com/google/gson/stream/JsonReaderPathTest.java @@ -225,8 +225,8 @@ public static List parameters() { JsonReader reader = factory.create("{\"a\":1}"); reader.beginObject(); reader.skipValue(); - assertEquals("$.null", reader.getPreviousPath()); - assertEquals("$.null", reader.getPath()); + assertEquals("$.", reader.getPreviousPath()); + assertEquals("$.", reader.getPath()); } @Test public void skipObjectValues() throws IOException { @@ -236,8 +236,8 @@ 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()); From 9291d3ccb9d64ad535d8fd80bf8f8c42147eb7b1 Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Wed, 14 Sep 2022 01:36:39 +0200 Subject: [PATCH 4/4] Address feedback; improve test coverage --- .../gson/internal/bind/JsonTreeReader.java | 43 +++--- .../com/google/gson/stream/JsonReader.java | 140 ++++++++++-------- .../internal/bind/JsonTreeReaderTest.java | 27 +++- .../gson/stream/JsonReaderPathTest.java | 95 +++++++++++- .../google/gson/stream/JsonReaderTest.java | 55 +++++-- 5 files changed, 257 insertions(+), 103 deletions(-) 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 3fec6027d1..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,10 +93,10 @@ 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) { - pathNames[stackSize - 1] = null; // Free the last path name so that it can be garbage collected pathIndices[stackSize - 1]++; } } @@ -166,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) { @@ -271,20 +275,25 @@ JsonElement nextJsonElement() throws IOException { @Override public void skipValue() throws IOException { JsonToken peeked = peek(); - if (peeked == JsonToken.NAME) { - nextName(); - pathNames[stackSize - 2] = ""; - } else if (peeked == JsonToken.END_ARRAY) { - endArray(); - } else if (peeked == JsonToken.END_OBJECT) { - endObject(); - } else if (peeked == JsonToken.END_DOCUMENT) { - throw new IllegalStateException("Cannot skip at end of document"); - } else { - popStack(); - } - if (stackSize > 0) { - pathIndices[stackSize - 1]++; + 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 e708f6c469..61b84a93b8 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; @@ -1236,13 +1235,16 @@ public int nextInt() throws IOException { * Skips the next value recursively. This method is intended for use when * the JSON token stream contains unrecognized or unhandled values. * - *

If the next value is a JSON object or array, all nested elements are - * skipped. For JSON objects when called before the name of a property has been - * consumed, only the name is skipped. When called at the end of a JSON array - * or object, the end of the array or object is skipped. - * - * @throws IllegalStateException when called at the end of the document, that is, - * after the last value. + *

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

*/ public void skipValue() throws IOException { int count = 0; @@ -1252,50 +1254,64 @@ 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) { - // 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--; - } else if (p == PEEKED_UNQUOTED) { - skipUnquotedValue(); - } else if (p == PEEKED_SINGLE_QUOTED) { - skipQuotedValue('\''); - } else if (p == PEEKED_DOUBLE_QUOTED) { - skipQuotedValue('"'); - } else if (p == PEEKED_UNQUOTED_NAME) { - skipUnquotedValue(); - // Only update when name is explicitly skipped, otherwise stack is not updated anyways - if (count == 0) { - pathNames[stackSize - 1] = ""; - } - } else if (p == PEEKED_SINGLE_QUOTED_NAME) { - skipQuotedValue('\''); - // Only update when name is explicitly skipped, otherwise stack is not updated anyways - if (count == 0) { - pathNames[stackSize - 1] = ""; - } - } else if (p == PEEKED_DOUBLE_QUOTED_NAME) { - skipQuotedValue('"'); - // Only update when name is explicitly skipped, otherwise stack is not updated anyways - if (count == 0) { - pathNames[stackSize - 1] = ""; - } - } else if (p == PEEKED_NUMBER) { - pos += peekedNumberLength; - } else if (p == PEEKED_EOF) { - throw new IllegalStateException("Cannot skip at end of document"); + 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); @@ -1535,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 @@ -1552,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 90835d9b2e..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,18 @@ 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 { @@ -61,12 +74,10 @@ public void testSkipValue_afterEndOfDocument() throws IOException { reader.endObject(); assertEquals(JsonToken.END_DOCUMENT, reader.peek()); - try { - reader.skipValue(); - fail(); - } catch (IllegalStateException e) { - assertEquals("Cannot skip at end of document", e.getMessage()); - } + assertEquals("$", reader.getPath()); + reader.skipValue(); + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + assertEquals("$", reader.getPath()); } public void testSkipValue_atArrayEnd() throws IOException { @@ -74,6 +85,7 @@ public void testSkipValue_atArrayEnd() throws IOException { reader.beginArray(); reader.skipValue(); assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + assertEquals("$", reader.getPath()); } public void testSkipValue_atObjectEnd() throws IOException { @@ -81,8 +93,9 @@ public void testSkipValue_atObjectEnd() throws IOException { reader.beginObject(); reader.skipValue(); assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + assertEquals("$", reader.getPath()); } - + public void testHasNext_endOfDocument() throws IOException { JsonTreeReader reader = new JsonTreeReader(new JsonObject()); reader.beginObject(); 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 f58e1808b7..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("$.", reader.getPreviousPath()); assertEquals("$.", reader.getPath()); + + reader.beginArray(); + assertEquals("$.[0]", reader.getPreviousPath()); + assertEquals("$.[0]", reader.getPath()); } @Test public void skipObjectValues() throws IOException { @@ -243,6 +258,18 @@ public static List parameters() { 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 dd3be58b03..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}")); @@ -170,12 +199,10 @@ public void testSkipValueAfterEndOfDocument() throws IOException { reader.endObject(); assertEquals(JsonToken.END_DOCUMENT, reader.peek()); - try { - reader.skipValue(); - fail(); - } catch (IllegalStateException e) { - assertEquals("Cannot skip at end of document", e.getMessage()); - } + assertEquals("$", reader.getPath()); + reader.skipValue(); + assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + assertEquals("$", reader.getPath()); } public void testSkipValueAtArrayEnd() throws IOException { @@ -183,6 +210,7 @@ public void testSkipValueAtArrayEnd() throws IOException { reader.beginArray(); reader.skipValue(); assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + assertEquals("$", reader.getPath()); } public void testSkipValueAtObjectEnd() throws IOException { @@ -190,6 +218,7 @@ public void testSkipValueAtObjectEnd() throws IOException { reader.beginObject(); reader.skipValue(); assertEquals(JsonToken.END_DOCUMENT, reader.peek()); + assertEquals("$", reader.getPath()); } public void testHelloWorld() throws IOException {