From 4732c7ff40441ac16c8e69e38be58e20898ead82 Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Mon, 16 Dec 2024 00:31:56 +0100 Subject: [PATCH] Add better support for multiple top-level values --- .../com/google/gson/stream/JsonReader.java | 46 ++++++++-- .../com/google/gson/stream/JsonWriter.java | 54 ++++++++++-- .../test/java/com/google/gson/GsonTest.java | 6 +- .../internal/bind/JsonTreeReaderTest.java | 2 + .../internal/bind/JsonTreeWriterTest.java | 2 + .../google/gson/stream/JsonReaderTest.java | 83 ++++++++++++++++++- .../google/gson/stream/JsonWriterTest.java | 80 +++++++++++++++++- 7 files changed, 255 insertions(+), 18 deletions(-) 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 30876109a5..927c57662a 100644 --- a/gson/src/main/java/com/google/gson/stream/JsonReader.java +++ b/gson/src/main/java/com/google/gson/stream/JsonReader.java @@ -70,8 +70,9 @@ * The behavior of this reader can be customized with the following methods: * * * * The default configuration of {@code JsonReader} instances used internally by the {@link Gson} @@ -257,6 +258,8 @@ public class JsonReader implements Closeable { static final int DEFAULT_NESTING_LIMIT = 255; private int nestingLimit = DEFAULT_NESTING_LIMIT; + private boolean multiTopLevelValuesEnabled = false; + static final int BUFFER_SIZE = 1024; /** @@ -380,7 +383,8 @@ public final boolean isLenient() { *
  • Streams that start with the non-execute prefix, * {@code ")]}'\n"} *
  • Streams that include multiple top-level values. With legacy strict or strict - * parsing, each stream must contain exactly one top-level value. + * parsing, each stream must contain exactly one top-level value. Can be enabled + * independently using {@link #setMultiTopLevelValuesAllowed(boolean)}. *
  • Numbers may be {@link Double#isNaN() NaNs} or {@link Double#isInfinite() * infinities} represented by {@code NaN} and {@code (-)Infinity} respectively. *
  • End of line comments starting with {@code //} or {@code #} and ending with a @@ -402,8 +406,8 @@ public final boolean isLenient() { * @since 2.11.0 */ public final void setStrictness(Strictness strictness) { - Objects.requireNonNull(strictness); - this.strictness = strictness; + this.strictness = Objects.requireNonNull(strictness); + setMultiTopLevelValuesAllowed(strictness == Strictness.LENIENT); } /** @@ -416,6 +420,31 @@ public final Strictness getStrictness() { return strictness; } + /** + * Sets whether multiple top-level values are allowed. Only whitespace is supported as separator + * between top-level values. Values may also be concatenated without any whitespace in between, + * but for some values this causes ambiguities, for example for JSON numbers as top-level values. + * + *

    This setting overwrites and is overwritten by whether {@link #setStrictness(Strictness)} + * enabled support for multiple top-level values. + * + * @see #isMultiTopLevelValuesAllowed() + * @since $next-version$ + */ + public final void setMultiTopLevelValuesAllowed(boolean enabled) { + this.multiTopLevelValuesEnabled = enabled; + } + + /** + * Returns whether multiple top-level values are allowed. + * + * @see #setMultiTopLevelValuesAllowed(boolean) + * @since $next-version$ + */ + public final boolean isMultiTopLevelValuesAllowed() { + return multiTopLevelValuesEnabled; + } + /** * Sets the nesting limit of this reader. * @@ -661,10 +690,13 @@ int doPeek() throws IOException { int c = nextNonWhitespace(false); if (c == -1) { return peeked = PEEKED_EOF; - } else { - checkLenient(); - pos--; + } else if (!multiTopLevelValuesEnabled) { + throw new MalformedJsonException( + "Multiple top-level values support has not been enabled, use" + + " `JsonReader.setMultiTopLevelValuesAllowed(true)`," + + locationString()); } + pos--; } else if (peekStack == JsonScope.CLOSED) { throw new IllegalStateException("JsonReader is closed"); } diff --git a/gson/src/main/java/com/google/gson/stream/JsonWriter.java b/gson/src/main/java/com/google/gson/stream/JsonWriter.java index 70b0157b24..10f761b70f 100644 --- a/gson/src/main/java/com/google/gson/stream/JsonWriter.java +++ b/gson/src/main/java/com/google/gson/stream/JsonWriter.java @@ -72,6 +72,7 @@ * output *

  • {@link #setStrictness(Strictness)}, the default is {@link Strictness#LEGACY_STRICT} *
  • {@link #setSerializeNulls(boolean)}, by default {@code null} is serialized + *
  • {@link #setTopLevelSeparator(String)}, by default {@code null} (= disabled) * * * The default configuration of {@code JsonWriter} instances used internally by the {@link Gson} @@ -218,6 +219,11 @@ public class JsonWriter implements Closeable, Flushable { private Strictness strictness = Strictness.LEGACY_STRICT; + /** + * Separator between top-level values; {@code null} if multiple top-level values are not allowed + */ + private String topLevelSeparator = null; + private boolean htmlSafe; private String deferredName; @@ -334,9 +340,13 @@ public boolean isLenient() { *
    The behavior of these is currently identical. In these strictness modes, the writer only * writes JSON in accordance with RFC 8259. *
    {@link Strictness#LENIENT} - *
    This mode relaxes the behavior of the writer to allow the writing of {@link - * Double#isNaN() NaNs} and {@link Double#isInfinite() infinities}. It also allows writing - * multiple top level values. + *
    In lenient mode, the following departures from RFC 8259 are permitted: + *
      + *
    • Writing of {@link Double#isNaN() NaNs} and {@link Double#isInfinite() infinities}. + *
    • Writing multiple top level values. The values are concatenated without any + * separating whitespace. Can be enabled independently and can be further customized + * using {@link #setTopLevelSeparator(String)}. + *
    * * * @param strictness the new strictness of this writer. May not be {@code null}. @@ -345,6 +355,7 @@ public boolean isLenient() { */ public final void setStrictness(Strictness strictness) { this.strictness = Objects.requireNonNull(strictness); + setTopLevelSeparator(strictness == Strictness.LENIENT ? "" : null); } /** @@ -357,6 +368,33 @@ public final Strictness getStrictness() { return strictness; } + /** + * Sets the separator to use between multiple top-level values. When writing multiple top-level + * values the separator is written between the values, this can for example be useful for formats + * such as JSON Lines.
    + * Using {@code null} disables support for multiple top-level values (the default). + * + *

    This setting overwrites and is overwritten by whether {@link #setStrictness(Strictness)} + * enabled support for multiple top-level values. + * + * @param separator separator between top-level values, or {@code null} to disable. + * @see #getTopLevelSeparator() + * @since $next-version$ + */ + public final void setTopLevelSeparator(String separator) { + this.topLevelSeparator = separator; + } + + /** + * Returns the top-level separator, or {@code null} if disabled. + * + * @see #setTopLevelSeparator(String) + * @since $next-version$ + */ + public final String getTopLevelSeparator() { + return topLevelSeparator; + } + /** * Configures this writer to emit JSON that's safe for direct inclusion in HTML and XML documents. * This escapes the HTML characters {@code <}, {@code >}, {@code &}, {@code =} and {@code '} @@ -807,10 +845,14 @@ private void beforeName() throws IOException { private void beforeValue() throws IOException { switch (peek()) { case NONEMPTY_DOCUMENT: - if (strictness != Strictness.LENIENT) { - throw new IllegalStateException("JSON must have only one top-level value."); + if (topLevelSeparator == null) { + throw new IllegalStateException( + "Multiple top-level values support has not been enabled, use" + + " `JsonWriter.setTopLevelSeparator(String)`"); } - // fall-through + out.append(topLevelSeparator); + break; + case EMPTY_DOCUMENT: // first in document replaceTop(NONEMPTY_DOCUMENT); break; diff --git a/gson/src/test/java/com/google/gson/GsonTest.java b/gson/src/test/java/com/google/gson/GsonTest.java index 45cd1d4479..be005424f7 100644 --- a/gson/src/test/java/com/google/gson/GsonTest.java +++ b/gson/src/test/java/com/google/gson/GsonTest.java @@ -411,7 +411,11 @@ public void testNewJsonWriter_Default() throws IOException { // Additional top-level value IllegalStateException e = assertThrows(IllegalStateException.class, () -> jsonWriter.value(1)); - assertThat(e).hasMessageThat().isEqualTo("JSON must have only one top-level value."); + assertThat(e) + .hasMessageThat() + .isEqualTo( + "Multiple top-level values support has not been enabled, use" + + " `JsonWriter.setTopLevelSeparator(String)`"); jsonWriter.close(); assertThat(writer.toString()).isEqualTo("{\"\\u003ctest2\":true}"); 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 42d4649683..4d013da82b 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 @@ -195,6 +195,8 @@ public void testOverrides() { "isLenient()", "setStrictness(com.google.gson.Strictness)", "getStrictness()", + "setMultiTopLevelValuesAllowed(boolean)", + "isMultiTopLevelValuesAllowed()", "setNestingLimit(int)", "getNestingLimit()"); MoreAsserts.assertOverridesMethods(JsonReader.class, JsonTreeReader.class, ignoredMethods); diff --git a/gson/src/test/java/com/google/gson/internal/bind/JsonTreeWriterTest.java b/gson/src/test/java/com/google/gson/internal/bind/JsonTreeWriterTest.java index 97dc2e56c0..8d3006e8e0 100644 --- a/gson/src/test/java/com/google/gson/internal/bind/JsonTreeWriterTest.java +++ b/gson/src/test/java/com/google/gson/internal/bind/JsonTreeWriterTest.java @@ -272,6 +272,8 @@ public void testOverrides() { "isLenient()", "setStrictness(com.google.gson.Strictness)", "getStrictness()", + "setTopLevelSeparator(java.lang.String)", + "getTopLevelSeparator()", "setIndent(java.lang.String)", "setHtmlSafe(boolean)", "isHtmlSafe()", 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 f8a33be0df..8d055f1f03 100644 --- a/gson/src/test/java/com/google/gson/stream/JsonReaderTest.java +++ b/gson/src/test/java/com/google/gson/stream/JsonReaderTest.java @@ -1460,7 +1460,17 @@ public void testStrictMultipleTopLevelValues() throws IOException { reader.beginArray(); reader.endArray(); var e = assertThrows(MalformedJsonException.class, () -> reader.peek()); - assertStrictError(e, "line 1 column 5 path $"); + assertThat(e) + .hasMessageThat() + .isEqualTo( + "Multiple top-level values support has not been enabled, use" + + " `JsonReader.setMultiTopLevelValuesAllowed(true)`, at line 1 column 5 path $"); + + // But trailing whitespace is allowed + JsonReader reader2 = new JsonReader(reader("[] \n \t \r ")); + reader2.beginArray(); + reader2.endArray(); + assertThat(reader2.peek()).isEqualTo(JsonToken.END_DOCUMENT); } @Test @@ -1481,7 +1491,76 @@ public void testStrictMultipleTopLevelValuesWithSkipValue() throws IOException { reader.beginArray(); reader.endArray(); var e = assertThrows(MalformedJsonException.class, () -> reader.skipValue()); - assertStrictError(e, "line 1 column 5 path $"); + assertThat(e) + .hasMessageThat() + .isEqualTo( + "Multiple top-level values support has not been enabled, use" + + " `JsonReader.setMultiTopLevelValuesAllowed(true)`, at line 1 column 5 path $"); + } + + @Test + public void testMultipleTopLevelValuesStrictness() { + JsonReader reader = new JsonReader(reader("[]")); + assertThat(reader.isMultiTopLevelValuesAllowed()).isFalse(); + + reader.setStrictness(Strictness.STRICT); + assertThat(reader.isMultiTopLevelValuesAllowed()).isFalse(); + + reader.setStrictness(Strictness.LEGACY_STRICT); + assertThat(reader.isMultiTopLevelValuesAllowed()).isFalse(); + + reader.setStrictness(Strictness.LENIENT); + assertThat(reader.isMultiTopLevelValuesAllowed()).isTrue(); + + reader.setStrictness(Strictness.STRICT); + assertThat(reader.isMultiTopLevelValuesAllowed()).isFalse(); + // Verify that it can be enabled independently of Strictness + reader.setMultiTopLevelValuesAllowed(true); + assertThat(reader.getStrictness()).isEqualTo(Strictness.STRICT); + assertThat(reader.isMultiTopLevelValuesAllowed()).isTrue(); + } + + /** + * Tests multiple top-level values, enabled with {@link + * JsonReader#setMultiTopLevelValuesAllowed(boolean)}. + */ + @Test + public void testMultipleTopLevelValuesEnabled() throws IOException { + JsonReader reader = new JsonReader(reader("[]{}")); + reader.setStrictness(Strictness.STRICT); + reader.setMultiTopLevelValuesAllowed(true); + + reader.beginArray(); + reader.endArray(); + reader.beginObject(); + reader.endObject(); + assertThat(reader.peek()).isEqualTo(JsonToken.END_DOCUMENT); + + reader = new JsonReader(reader("true\n \n1")); + reader.setStrictness(Strictness.STRICT); + reader.setMultiTopLevelValuesAllowed(true); + + assertThat(reader.nextBoolean()).isTrue(); + assertThat(reader.nextInt()).isEqualTo(1); + assertThat(reader.peek()).isEqualTo(JsonToken.END_DOCUMENT); + } + + @Test + public void testMultipleTopLevelValuesDisabled() throws IOException { + JsonReader reader = new JsonReader(reader("[]{}")); + // Normally lenient mode allows multiple top-level values + reader.setStrictness(Strictness.LENIENT); + reader.setMultiTopLevelValuesAllowed(false); + + reader.beginArray(); + reader.endArray(); + + var e = assertThrows(MalformedJsonException.class, () -> reader.beginObject()); + assertThat(e) + .hasMessageThat() + .isEqualTo( + "Multiple top-level values support has not been enabled, use" + + " `JsonReader.setMultiTopLevelValuesAllowed(true)`, at line 1 column 4 path $"); } @Test diff --git a/gson/src/test/java/com/google/gson/stream/JsonWriterTest.java b/gson/src/test/java/com/google/gson/stream/JsonWriterTest.java index fd171e880f..2db07386d3 100644 --- a/gson/src/test/java/com/google/gson/stream/JsonWriterTest.java +++ b/gson/src/test/java/com/google/gson/stream/JsonWriterTest.java @@ -181,7 +181,11 @@ public void testMultipleTopLevelValues() throws IOException { IllegalStateException expected = assertThrows(IllegalStateException.class, jsonWriter::beginArray); - assertThat(expected).hasMessageThat().isEqualTo("JSON must have only one top-level value."); + assertThat(expected) + .hasMessageThat() + .isEqualTo( + "Multiple top-level values support has not been enabled, use" + + " `JsonWriter.setTopLevelSeparator(String)`"); } @Test @@ -193,7 +197,11 @@ public void testMultipleTopLevelValuesStrict() throws IOException { IllegalStateException expected = assertThrows(IllegalStateException.class, jsonWriter::beginArray); - assertThat(expected).hasMessageThat().isEqualTo("JSON must have only one top-level value."); + assertThat(expected) + .hasMessageThat() + .isEqualTo( + "Multiple top-level values support has not been enabled, use" + + " `JsonWriter.setTopLevelSeparator(String)`"); } @Test @@ -209,6 +217,74 @@ public void testMultipleTopLevelValuesLenient() throws IOException { assertThat(stringWriter.toString()).isEqualTo("[][]"); } + @Test + public void testMultipleTopLevelValuesStrictness() { + JsonWriter writer = new JsonWriter(new StringWriter()); + assertThat(writer.getTopLevelSeparator()).isNull(); + + writer.setStrictness(Strictness.STRICT); + assertThat(writer.getTopLevelSeparator()).isNull(); + + writer.setStrictness(Strictness.LEGACY_STRICT); + assertThat(writer.getTopLevelSeparator()).isNull(); + + writer.setStrictness(Strictness.LENIENT); + assertThat(writer.getTopLevelSeparator()).isEqualTo(""); + + writer.setStrictness(Strictness.STRICT); + assertThat(writer.getTopLevelSeparator()).isNull(); + // Verify that it can be enabled independently of Strictness + writer.setTopLevelSeparator("\n"); + assertThat(writer.getStrictness()).isEqualTo(Strictness.STRICT); + assertThat(writer.getTopLevelSeparator()).isEqualTo("\n"); + } + + /** + * Tests multiple top-level values, enabled with {@link JsonWriter#setTopLevelSeparator(String)}. + */ + @Test + public void testMultipleTopLevelValuesEnabled() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter writer = new JsonWriter(stringWriter); + writer.setStrictness(Strictness.STRICT); + writer.setTopLevelSeparator(""); + + writer.beginArray(); + writer.endArray(); + writer.beginObject(); + writer.endObject(); + writer.close(); + assertThat(stringWriter.toString()).isEqualTo("[]{}"); + + stringWriter = new StringWriter(); + writer = new JsonWriter(stringWriter); + writer.setStrictness(Strictness.STRICT); + writer.setTopLevelSeparator(" \n "); + + writer.value(1); + writer.value(2); + writer.close(); + assertThat(stringWriter.toString()).isEqualTo("1 \n 2"); + } + + @Test + public void testMultipleTopLevelValuesDisabled() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter writer = new JsonWriter(stringWriter); + // Normally lenient mode allows multiple top-level values + writer.setStrictness(Strictness.LENIENT); + writer.setTopLevelSeparator(null); + + writer.value(1); + + var e = assertThrows(IllegalStateException.class, () -> writer.value(2)); + assertThat(e) + .hasMessageThat() + .isEqualTo( + "Multiple top-level values support has not been enabled, use" + + " `JsonWriter.setTopLevelSeparator(String)`"); + } + @Test public void testBadNestingObject() throws IOException { StringWriter stringWriter = new StringWriter();