Skip to content

Commit

Permalink
Add better support for multiple top-level values
Browse files Browse the repository at this point in the history
  • Loading branch information
Marcono1234 committed Dec 26, 2024
1 parent 84e5f16 commit 4732c7f
Show file tree
Hide file tree
Showing 7 changed files with 255 additions and 18 deletions.
46 changes: 39 additions & 7 deletions gson/src/main/java/com/google/gson/stream/JsonReader.java
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,9 @@
* The behavior of this reader can be customized with the following methods:
*
* <ul>
* <li>{@link #setStrictness(Strictness)}, the default is {@link Strictness#LEGACY_STRICT}
* <li>{@link #setMultiTopLevelValuesAllowed(boolean)}, the default is {@code false}
* <li>{@link #setNestingLimit(int)}, the default is {@value #DEFAULT_NESTING_LIMIT}
* <li>{@link #setStrictness(Strictness)}, the default is {@link Strictness#LEGACY_STRICT}
* </ul>
*
* The default configuration of {@code JsonReader} instances used internally by the {@link Gson}
Expand Down Expand Up @@ -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;

/**
Expand Down Expand Up @@ -380,7 +383,8 @@ public final boolean isLenient() {
* <li>Streams that start with the <a href="#nonexecuteprefix">non-execute prefix</a>,
* {@code ")]}'\n"}
* <li>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)}.
* <li>Numbers may be {@link Double#isNaN() NaNs} or {@link Double#isInfinite()
* infinities} represented by {@code NaN} and {@code (-)Infinity} respectively.
* <li>End of line comments starting with {@code //} or {@code #} and ending with a
Expand All @@ -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);
}

/**
Expand All @@ -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.
*
* <p>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.
*
Expand Down Expand Up @@ -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");
}
Expand Down
54 changes: 48 additions & 6 deletions gson/src/main/java/com/google/gson/stream/JsonWriter.java
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
* output
* <li>{@link #setStrictness(Strictness)}, the default is {@link Strictness#LEGACY_STRICT}
* <li>{@link #setSerializeNulls(boolean)}, by default {@code null} is serialized
* <li>{@link #setTopLevelSeparator(String)}, by default {@code null} (= disabled)
* </ul>
*
* The default configuration of {@code JsonWriter} instances used internally by the {@link Gson}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -334,9 +340,13 @@ public boolean isLenient() {
* <dd>The behavior of these is currently identical. In these strictness modes, the writer only
* writes JSON in accordance with RFC 8259.
* <dt>{@link Strictness#LENIENT}
* <dd>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.
* <dd>In lenient mode, the following departures from RFC 8259 are permitted:
* <ul>
* <li>Writing of {@link Double#isNaN() NaNs} and {@link Double#isInfinite() infinities}.
* <li>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)}.
* </ul>
* </dl>
*
* @param strictness the new strictness of this writer. May not be {@code null}.
Expand All @@ -345,6 +355,7 @@ public boolean isLenient() {
*/
public final void setStrictness(Strictness strictness) {
this.strictness = Objects.requireNonNull(strictness);
setTopLevelSeparator(strictness == Strictness.LENIENT ? "" : null);
}

/**
Expand All @@ -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 <a href="https://jsonlines.org/">JSON Lines</a>.<br>
* Using {@code null} disables support for multiple top-level values (the default).
*
* <p>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 '}
Expand Down Expand Up @@ -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;
Expand Down
6 changes: 5 additions & 1 deletion gson/src/test/java/com/google/gson/GsonTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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}");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()",
Expand Down
83 changes: 81 additions & 2 deletions gson/src/test/java/com/google/gson/stream/JsonReaderTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Loading

0 comments on commit 4732c7f

Please sign in to comment.