From ef35a34a6033914644410845c4b90f52247c763f Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Sun, 5 Mar 2023 00:40:49 +0100 Subject: [PATCH 01/36] Enforce build using JDK >= 11 (#2333) Building already requires JDK >= 11, but previously using a lower JDK version such as JDK 8 would lead to cryptic build errors because some compiler flags are unsupported. Using the Maven Enforcer Plugin makes sure that the build fails early with a somewhat better to understand error message. --- pom.xml | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index 067eee22ed..e030ff26a3 100644 --- a/pom.xml +++ b/pom.xml @@ -86,6 +86,31 @@ + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.2.1 + + + + enforce-jdk-version + + enforce + + + + + + [11,) + + + + + + + + @@ -121,9 +146,6 @@ 2.18.0 - - [11,) - @@ -131,9 +153,6 @@ maven-javadoc-plugin 3.5.0 - - [11,) - 11 From 0adcdc80d5ef3a40086a8abd6e2f55164a7c2597 Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Mon, 6 Mar 2023 16:33:50 +0100 Subject: [PATCH 02/36] Fail Maven build on warnings (#2335) * Fail Maven build on warnings * Fix configuration parameter name For the Javadoc plugin it is "warnings" (plural) instead of "warning" --- gson/pom.xml | 6 ------ pom.xml | 14 ++++++++++++++ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/gson/pom.xml b/gson/pom.xml index d80226c0a4..deed98dddf 100644 --- a/gson/pom.xml +++ b/gson/pom.xml @@ -55,12 +55,6 @@ 31.1-jre test - - com.google.errorprone - error_prone_annotations - 2.18.0 - true - diff --git a/pom.xml b/pom.xml index e030ff26a3..752fc3871e 100644 --- a/pom.xml +++ b/pom.xml @@ -85,6 +85,18 @@ + + + + com.google.errorprone + error_prone_annotations + 2.18.0 + true + + + @@ -120,6 +132,7 @@ true true + true true @@ -173,6 +186,7 @@ false true + true From 85ebaf7c3553a5d5c058fd6067824a7c5258d07f Mon Sep 17 00:00:00 2001 From: Maicol <79454487+MaicolAntali@users.noreply.github.com> Date: Mon, 6 Mar 2023 17:24:09 +0100 Subject: [PATCH 03/36] Fix #2334 (#2337) * Fix #2334 This commit replaces the `NumberFormatException` with `MalformedJsonException` in the `JsonReader#readEscapeCharacter()` and also fixes the tests. * Removes white-space --- gson/src/main/java/com/google/gson/stream/JsonReader.java | 4 ++-- gson/src/test/java/com/google/gson/stream/JsonReaderTest.java | 2 +- 2 files changed, 3 insertions(+), 3 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 559ab2db81..995353797a 100644 --- a/gson/src/main/java/com/google/gson/stream/JsonReader.java +++ b/gson/src/main/java/com/google/gson/stream/JsonReader.java @@ -1587,7 +1587,7 @@ public String getPath() { * been read. This supports both unicode escapes "u000A" and two-character * escapes "\n". * - * @throws NumberFormatException if any unicode escape sequences are + * @throws MalformedJsonException if any unicode escape sequences are * malformed. */ @SuppressWarnings("fallthrough") @@ -1614,7 +1614,7 @@ private char readEscapeCharacter() throws IOException { } else if (c >= 'A' && c <= 'F') { result += (c - 'A' + 10); } else { - throw new NumberFormatException("\\u" + new String(buffer, pos, 4)); + throw new MalformedJsonException("\\u" + new String(buffer, pos, 4)); } } pos += 4; 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 8ebe20b571..c3940eff46 100644 --- a/gson/src/test/java/com/google/gson/stream/JsonReaderTest.java +++ b/gson/src/test/java/com/google/gson/stream/JsonReaderTest.java @@ -355,7 +355,7 @@ public void testUnescapingInvalidCharacters() throws IOException { try { reader.nextString(); fail(); - } catch (NumberFormatException expected) { + } catch (MalformedJsonException expected) { } } From f467ec20c31099e39263d4451972130befd0953c Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Mon, 6 Mar 2023 21:29:09 +0100 Subject: [PATCH 04/36] Fix incorrect comment in `pom.xml` (#2338) Accidentally referred to wrong JDK bug report --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 752fc3871e..bf69db0f1a 100644 --- a/pom.xml +++ b/pom.xml @@ -88,7 +88,7 @@ + compiler warnings, see also comments on https://bugs.openjdk.org/browse/JDK-6365854 --> com.google.errorprone error_prone_annotations From 2f7be29a30802b09e49b78287a1574aaa79b875a Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Tue, 7 Mar 2023 22:39:05 +0100 Subject: [PATCH 05/36] Specify Error Prone JVM args with `.mvn/jvm.config` (#2339) * Specify Error Prone JVM args with `.mvn/jvm.config` This allows avoiding fork mode for compilation, which: - might slightly reduce compilation time - guarantees that all compiler diagnostics are properly reported * Combine enforcer executions --- .mvn/jvm.config | 10 ++++++++++ pom.xml | 20 +++++++------------- 2 files changed, 17 insertions(+), 13 deletions(-) create mode 100644 .mvn/jvm.config diff --git a/.mvn/jvm.config b/.mvn/jvm.config new file mode 100644 index 0000000000..3ed5dea48c --- /dev/null +++ b/.mvn/jvm.config @@ -0,0 +1,10 @@ +--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED +--add-opens jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED +--add-opens jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED diff --git a/pom.xml b/pom.xml index bf69db0f1a..a8cd071608 100644 --- a/pom.xml +++ b/pom.xml @@ -104,14 +104,19 @@ maven-enforcer-plugin 3.2.1 - - enforce-jdk-version + enforce-versions enforce + + + [3.3.1,) + + + [11,) @@ -133,21 +138,10 @@ true true true - true -XDcompilePolicy=simple -Xplugin:ErrorProne -XepExcludedPaths:.*/generated-test-sources/protobuf/.* - -J--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED - -J--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED - -J--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED - -J--add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED - -J--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED - -J--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED - -J--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED - -J--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED - -J--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED - -J--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED -Xlint:all,-options From 1da826dc6c8eb85ad9ed97e120823428fe1236f4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Mar 2023 06:45:07 -0800 Subject: [PATCH 06/36] Bump moditect-maven-plugin from 1.0.0.RC2 to 1.0.0.RC3 (#2340) Bumps [moditect-maven-plugin](https://github.com/moditect/moditect) from 1.0.0.RC2 to 1.0.0.RC3. - [Release notes](https://github.com/moditect/moditect/releases) - [Commits](https://github.com/moditect/moditect/compare/1.0.0.CR2...1.0.0.RC3) --- updated-dependencies: - dependency-name: org.moditect:moditect-maven-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- gson/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gson/pom.xml b/gson/pom.xml index deed98dddf..7ecd2c980f 100644 --- a/gson/pom.xml +++ b/gson/pom.xml @@ -235,7 +235,7 @@ org.moditect moditect-maven-plugin - 1.0.0.RC2 + 1.0.0.RC3 add-module-info From 6eddbf30310913d599631eb156fad32f7ee8ddf8 Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Sun, 19 Mar 2023 16:54:31 +0100 Subject: [PATCH 07/36] Make dependency on Error Prone Annotations non-optional (#2346) --- gson/pom.xml | 11 +++++++++++ pom.xml | 12 ------------ 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/gson/pom.xml b/gson/pom.xml index 7ecd2c980f..f36db3363c 100644 --- a/gson/pom.xml +++ b/gson/pom.xml @@ -38,6 +38,17 @@ + + + com.google.errorprone + error_prone_annotations + 2.18.0 + + junit junit diff --git a/pom.xml b/pom.xml index a8cd071608..c65ed56f45 100644 --- a/pom.xml +++ b/pom.xml @@ -85,18 +85,6 @@ - - - - com.google.errorprone - error_prone_annotations - 2.18.0 - true - - - From 26229d33d81f0d60f508411eb3c541f3e87322ac Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Sun, 19 Mar 2023 17:15:35 +0100 Subject: [PATCH 08/36] FormattingStyle follow-up (#2327) * FormattingStyle follow-up * Add links to FormattingStyle * Use Truth for FormattingStyleTest * Reduce pull request scope to Javadoc and minor code changes --- .../java/com/google/gson/FormattingStyle.java | 15 +++++++--- .../java/com/google/gson/GsonBuilder.java | 8 ++++-- .../com/google/gson/stream/JsonWriter.java | 8 +++--- .../gson/functional/FormattingStyleTest.java | 28 +++++++++++-------- 4 files changed, 38 insertions(+), 21 deletions(-) diff --git a/gson/src/main/java/com/google/gson/FormattingStyle.java b/gson/src/main/java/com/google/gson/FormattingStyle.java index 19b307aa74..ed9f86dd1f 100644 --- a/gson/src/main/java/com/google/gson/FormattingStyle.java +++ b/gson/src/main/java/com/google/gson/FormattingStyle.java @@ -16,14 +16,17 @@ package com.google.gson; +import com.google.gson.stream.JsonWriter; import java.util.Objects; /** - * A class used to control what the serialization looks like. + * A class used to control what the serialization output looks like. * *

It currently defines the kind of newline to use, and the indent, but * might add more in the future.

* + * @see GsonBuilder#setPrettyPrinting(FormattingStyle) + * @see JsonWriter#setFormattingStyle(FormattingStyle) * @see Wikipedia Newline article * * @since $next-version$ @@ -32,7 +35,11 @@ public class FormattingStyle { private final String newline; private final String indent; - static public final FormattingStyle DEFAULT = + /** + * The default pretty printing formatting style using {@code "\n"} as + * newline and two spaces as indent. + */ + public static final FormattingStyle DEFAULT = new FormattingStyle("\n", " "); private FormattingStyle(String newline, String indent) { @@ -44,7 +51,7 @@ private FormattingStyle(String newline, String indent) { } if (!indent.matches("[ \t]*")) { throw new IllegalArgumentException( - "Only combinations of spaces and tabs allowed in indent."); + "Only combinations of spaces and tabs are allowed in indent."); } this.newline = newline; this.indent = indent; @@ -54,7 +61,7 @@ private FormattingStyle(String newline, String indent) { * Creates a {@link FormattingStyle} with the specified newline setting. * *

It can be used to accommodate certain OS convention, for example - * hardcode {@code "\r"} for Linux and macos, {@code "\r\n"} for Windows, or + * hardcode {@code "\n"} for Linux and macOS, {@code "\r\n"} for Windows, or * call {@link java.lang.System#lineSeparator()} to match the current OS.

* *

Only combinations of {@code \n} and {@code \r} are allowed.

diff --git a/gson/src/main/java/com/google/gson/GsonBuilder.java b/gson/src/main/java/com/google/gson/GsonBuilder.java index 0afc2337c1..f8f1b27f80 100644 --- a/gson/src/main/java/com/google/gson/GsonBuilder.java +++ b/gson/src/main/java/com/google/gson/GsonBuilder.java @@ -481,6 +481,9 @@ public GsonBuilder addDeserializationExclusionStrategy(ExclusionStrategy strateg * Configures Gson to output JSON that fits in a page for pretty printing. This option only * affects JSON serialization. * + *

This is a convenience method which simply calls {@link #setPrettyPrinting(FormattingStyle)} + * with {@link FormattingStyle#DEFAULT}. + * * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern */ public GsonBuilder setPrettyPrinting() { @@ -488,11 +491,12 @@ public GsonBuilder setPrettyPrinting() { } /** - * Configures Gson to output JSON that uses a certain kind of formatting stile (for example newline and indent). - * This option only affects JSON serialization. + * Configures Gson to output JSON that uses a certain kind of formatting style (for example newline and indent). + * This option only affects JSON serialization. By default Gson produces compact JSON output without any formatting. * *

Has no effect if the serialized format is a single line.

* + * @param formattingStyle the formatting style to use. * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern * @since $next-version$ */ 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 460dcce200..2244fc859d 100644 --- a/gson/src/main/java/com/google/gson/stream/JsonWriter.java +++ b/gson/src/main/java/com/google/gson/stream/JsonWriter.java @@ -217,7 +217,7 @@ public JsonWriter(Writer out) { * @param indent a string containing only whitespace. */ public final void setIndent(String indent) { - if (indent.length() == 0) { + if (indent.isEmpty()) { setFormattingStyle(null); } else { setFormattingStyle(FormattingStyle.DEFAULT.withIndent(indent)); @@ -226,7 +226,7 @@ public final void setIndent(String indent) { /** * Sets the pretty printing style to be used in the encoded document. - * No pretty printing if null. + * No pretty printing is done if the given style is {@code null}. * *

Sets the various attributes to be used in the encoded document. * For example the indentation string to be repeated for each level of indentation. @@ -234,7 +234,7 @@ public final void setIndent(String indent) { * *

Has no effect if the serialized format is a single line.

* - * @param formattingStyle the style used for pretty printing, no pretty printing if null. + * @param formattingStyle the style used for pretty printing, no pretty printing if {@code null}. * @since $next-version$ */ public final void setFormattingStyle(FormattingStyle formattingStyle) { @@ -249,7 +249,7 @@ public final void setFormattingStyle(FormattingStyle formattingStyle) { /** * Returns the pretty printing style used by this writer. * - * @return the FormattingStyle that will be used. + * @return the {@code FormattingStyle} that will be used. * @since $next-version$ */ public final FormattingStyle getFormattingStyle() { diff --git a/gson/src/test/java/com/google/gson/functional/FormattingStyleTest.java b/gson/src/test/java/com/google/gson/functional/FormattingStyleTest.java index 4bdbb95f86..170e0ff29d 100644 --- a/gson/src/test/java/com/google/gson/functional/FormattingStyleTest.java +++ b/gson/src/test/java/com/google/gson/functional/FormattingStyleTest.java @@ -15,8 +15,7 @@ */ package com.google.gson.functional; -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; +import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; import com.google.gson.FormattingStyle; @@ -54,7 +53,7 @@ public void testDefault() { Gson gson = new GsonBuilder().setPrettyPrinting().create(); String json = gson.toJson(INPUT); // Make sure the default uses LF, like before. - assertEquals(EXPECTED_LF, json); + assertThat(json).isEqualTo(EXPECTED_LF); } @Test @@ -62,7 +61,7 @@ public void testNewlineCrLf() { FormattingStyle style = FormattingStyle.DEFAULT.withNewline("\r\n"); Gson gson = new GsonBuilder().setPrettyPrinting(style).create(); String json = gson.toJson(INPUT); - assertEquals(EXPECTED_CRLF, json); + assertThat(json).isEqualTo(EXPECTED_CRLF); } @Test @@ -70,7 +69,7 @@ public void testNewlineLf() { FormattingStyle style = FormattingStyle.DEFAULT.withNewline("\n"); Gson gson = new GsonBuilder().setPrettyPrinting(style).create(); String json = gson.toJson(INPUT); - assertEquals(EXPECTED_LF, json); + assertThat(json).isEqualTo(EXPECTED_LF); } @Test @@ -78,7 +77,7 @@ public void testNewlineCr() { FormattingStyle style = FormattingStyle.DEFAULT.withNewline("\r"); Gson gson = new GsonBuilder().setPrettyPrinting(style).create(); String json = gson.toJson(INPUT); - assertEquals(EXPECTED_CR, json); + assertThat(json).isEqualTo(EXPECTED_CR); } @Test @@ -86,7 +85,7 @@ public void testNewlineOs() { FormattingStyle style = FormattingStyle.DEFAULT.withNewline(System.lineSeparator()); Gson gson = new GsonBuilder().setPrettyPrinting(style).create(); String json = gson.toJson(INPUT); - assertEquals(EXPECTED_OS, json); + assertThat(json).isEqualTo(EXPECTED_OS); } @Test @@ -96,7 +95,7 @@ public void testVariousCombinationsToString() { FormattingStyle style = FormattingStyle.DEFAULT.withNewline(newline).withIndent(indent); Gson gson = new GsonBuilder().setPrettyPrinting(style).create(); String json = gson.toJson(INPUT); - assertEquals(buildExpected(newline, indent), json); + assertThat(json).isEqualTo(buildExpected(newline, indent)); } } } @@ -115,11 +114,11 @@ public void testVariousCombinationsParse() { String toParse = buildExpected(newline, indent); actualParsed = gson.fromJson(toParse, INPUT.getClass()); - assertArrayEquals(INPUT, actualParsed); + assertThat(actualParsed).isEqualTo(INPUT); // Parse the mixed string with the gson parsers configured with various newline / indents. actualParsed = gson.fromJson(jsonStringMix, INPUT.getClass()); - assertArrayEquals(INPUT, actualParsed); + assertThat(actualParsed).isEqualTo(INPUT); } } } @@ -127,22 +126,29 @@ public void testVariousCombinationsParse() { @Test public void testStyleValidations() { try { - // TBD if we want to accept \u2028 and \u2029. For now we don't. + // TBD if we want to accept \u2028 and \u2029. For now we don't because JSON specification + // does not consider them to be newlines FormattingStyle.DEFAULT.withNewline("\u2028"); fail("Gson should not accept anything but \\r and \\n for newline"); } catch (IllegalArgumentException expected) { + assertThat(expected).hasMessageThat() + .isEqualTo("Only combinations of \\n and \\r are allowed in newline."); } try { FormattingStyle.DEFAULT.withNewline("NL"); fail("Gson should not accept anything but \\r and \\n for newline"); } catch (IllegalArgumentException expected) { + assertThat(expected).hasMessageThat() + .isEqualTo("Only combinations of \\n and \\r are allowed in newline."); } try { FormattingStyle.DEFAULT.withIndent("\f"); fail("Gson should not accept anything but space and tab for indent"); } catch (IllegalArgumentException expected) { + assertThat(expected).hasMessageThat() + .isEqualTo("Only combinations of spaces and tabs are allowed in indent."); } } From 75bf3b41574421f081e93052ce1aca5e8dbe3d9a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Mar 2023 07:57:58 -0700 Subject: [PATCH 09/36] Bump maven-release-plugin from 3.0.0-M7 to 3.0.0 (#2347) Bumps [maven-release-plugin](https://github.com/apache/maven-release) from 3.0.0-M7 to 3.0.0. - [Release notes](https://github.com/apache/maven-release/releases) - [Commits](https://github.com/apache/maven-release/compare/maven-release-3.0.0-M7...maven-release-3.0.0) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-release-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index c65ed56f45..d4f93ccb92 100644 --- a/pom.xml +++ b/pom.xml @@ -189,7 +189,7 @@ org.apache.maven.plugins maven-release-plugin - 3.0.0-M7 + 3.0.0 true From 7f303b5db4b08955175a6d35be2a557632ec32e5 Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Tue, 21 Mar 2023 23:26:57 +0100 Subject: [PATCH 10/36] Mention creation of GitHub release in `ReleaseProcess.md` (#2349) * Mention creation of GitHub release in `ReleaseProcess.md` * Link to GitHub releases from README --- README.md | 2 +- ReleaseProcess.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3dec58f984..04eb1c94e8 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ see [`GsonBuilder.disableJdkUnsafe()`](https://javadoc.io/doc/com.google.code.gs * [API Javadoc](https://www.javadoc.io/doc/com.google.code.gson/gson): Documentation for the current release * [User guide](UserGuide.md): This guide contains examples on how to use Gson in your code * [Troubleshooting guide](Troubleshooting.md): Describes how to solve common issues when using Gson - * [Change log](CHANGELOG.md): Changes in the recent versions + * [Releases and change log](https://github.com/google/gson/releases): Latest releases and changes in these versions; for older releases see [`CHANGELOG.md`](CHANGELOG.md) * [Design document](GsonDesignDocument.md): This document discusses issues we faced while designing Gson. It also includes a comparison of Gson with other Java libraries that can be used for Json conversion Please use the ['gson' tag on StackOverflow](https://stackoverflow.com/questions/tagged/gson) or the [google-gson Google group](https://groups.google.com/group/google-gson) to discuss Gson or to post questions. diff --git a/ReleaseProcess.md b/ReleaseProcess.md index eaa0e7c542..35103557ea 100644 --- a/ReleaseProcess.md +++ b/ReleaseProcess.md @@ -13,7 +13,7 @@ The following is a step-by-step procedure for releasing a new version of Google- 1. [Log in to Nexus repository manager](https://oss.sonatype.org/index.html#welcome) at Sonatype and close the staging repository for Gson. 1. Download and sanity check all downloads. Do not skip this step! Once you release the staging repository, there is no going back. It will get synced with Maven Central and you will not be able to update or delete anything. Your only recourse will be to release a new version of Gson and hope that no one uses the old one. 1. Release the staging repository for Gson. Gson will now get synced to Maven Central with-in the next hour. For issues consult [Sonatype Guide](https://central.sonatype.org/publish/release/). -1. Update [Gson Changelog](CHANGELOG.md). Also, look at all bugs that were fixed and add a few lines describing what changed in the release. +1. Create a [GitHub release](https://github.com/google/gson/releases) for the new version. You can let GitHub [automatically generate the description for the release](https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes), but you should edit it manually to point out the most important changes and potentially incompatible changes. 1. Update version references in (version might be referenced multiple times): - [`README.md`](README.md) - [`UserGuide.md`](UserGuide.md) From c34b9ff79a534803c4fb5eb10e0b35972282ccf1 Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Thu, 23 Mar 2023 21:36:57 +0100 Subject: [PATCH 11/36] Fix incorrect inherited URLs in `pom.xml` (#2351) Previously Maven appended the artifact ID of the modules which lead for example for the `gson` module to the incorrect URL https://github.com/google/gson/gson This can be checked with `mvn help:effective-pom` --- pom.xml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index d4f93ccb92..93abb76ed5 100644 --- a/pom.xml +++ b/pom.xml @@ -14,7 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. --> - + 4.0.0 com.google.code.gson @@ -39,7 +40,9 @@ 11 - + + https://github.com/google/gson/ scm:git:https://github.com/google/gson.git scm:git:git@github.com:google/gson.git From 1d9d9774be408d1f24cb7c7cb4f2050cdb265e2f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 Mar 2023 08:28:24 -0700 Subject: [PATCH 12/36] Bump maven-resources-plugin from 3.3.0 to 3.3.1 (#2360) Bumps [maven-resources-plugin](https://github.com/apache/maven-resources-plugin) from 3.3.0 to 3.3.1. - [Release notes](https://github.com/apache/maven-resources-plugin/releases) - [Commits](https://github.com/apache/maven-resources-plugin/compare/maven-resources-plugin-3.3.0...maven-resources-plugin-3.3.1) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-resources-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- gson/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gson/pom.xml b/gson/pom.xml index f36db3363c..8af1ecdee3 100644 --- a/gson/pom.xml +++ b/gson/pom.xml @@ -201,7 +201,7 @@ maven-resources-plugin - 3.3.0 + 3.3.1 post-obfuscate-class From 3adead6ab4f240ed53322eff34210ed4a69dd490 Mon Sep 17 00:00:00 2001 From: TuZhiQiang <2623036785@qq.com> Date: Fri, 31 Mar 2023 01:32:09 +0800 Subject: [PATCH 13/36] Fix incorrect sample code in User Guide (#2362) --- UserGuide.md | 1 - 1 file changed, 1 deletion(-) diff --git a/UserGuide.md b/UserGuide.md index 0538067114..dfa76b043c 100644 --- a/UserGuide.md +++ b/UserGuide.md @@ -651,7 +651,6 @@ public class SampleObjectForTest { @Foo private final int annotatedField; private final String stringField; private final long longField; - private final Class clazzField; public SampleObjectForTest() { annotatedField = 5; From 051cb43fd9040a432626ef53523f1e7db7ab52c1 Mon Sep 17 00:00:00 2001 From: Maicol <79454487+MaicolAntali@users.noreply.github.com> Date: Sat, 1 Apr 2023 22:59:51 +0200 Subject: [PATCH 14/36] Fixes outdated URL (`ISO8601Utils.java`) (#2363) * Fixes comments Fixes the javadoc and the link to Jackson databind ISO8601Utils.java * Updates outdated URL in UtcDateTypeAdapter --- .../com/google/gson/typeadapters/UtcDateTypeAdapter.java | 2 +- .../com/google/gson/internal/bind/util/ISO8601Utils.java | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/extras/src/main/java/com/google/gson/typeadapters/UtcDateTypeAdapter.java b/extras/src/main/java/com/google/gson/typeadapters/UtcDateTypeAdapter.java index 2278f842bf..fb4196f409 100644 --- a/extras/src/main/java/com/google/gson/typeadapters/UtcDateTypeAdapter.java +++ b/extras/src/main/java/com/google/gson/typeadapters/UtcDateTypeAdapter.java @@ -62,7 +62,7 @@ public Date read(JsonReader in) throws IOException { } // Date parsing code from Jackson databind ISO8601Utils.java - // https://github.com/FasterXML/jackson-databind/blob/master/src/main/java/com/fasterxml/jackson/databind/util/ISO8601Utils.java + // https://github.com/FasterXML/jackson-databind/blob/2.8/src/main/java/com/fasterxml/jackson/databind/util/ISO8601Utils.java private static final String GMT_ID = "GMT"; /** diff --git a/gson/src/main/java/com/google/gson/internal/bind/util/ISO8601Utils.java b/gson/src/main/java/com/google/gson/internal/bind/util/ISO8601Utils.java index a120690d38..3fd22d7407 100644 --- a/gson/src/main/java/com/google/gson/internal/bind/util/ISO8601Utils.java +++ b/gson/src/main/java/com/google/gson/internal/bind/util/ISO8601Utils.java @@ -25,15 +25,15 @@ import java.util.TimeZone; /** - * Utilities methods for manipulating dates in iso8601 format. This is much much faster and GC friendly than using SimpleDateFormat so + * Utilities methods for manipulating dates in iso8601 format. This is much faster and GC friendly than using SimpleDateFormat so * highly suitable if you (un)serialize lots of date objects. * * Supported parse format: [yyyy-MM-dd|yyyyMMdd][T(hh:mm[:ss[.sss]]|hhmm[ss[.sss]])]?[Z|[+-]hh[:]mm]] * * @see this specification */ -//Date parsing code from Jackson databind ISO8601Utils.java -// https://github.com/FasterXML/jackson-databind/blob/master/src/main/java/com/fasterxml/jackson/databind/util/ISO8601Utils.java +// Date parsing code from Jackson databind ISO8601Utils.java +// https://github.com/FasterXML/jackson-databind/blob/2.8/src/main/java/com/fasterxml/jackson/databind/util/ISO8601Utils.java public class ISO8601Utils { /** From f1b4a71de56ce9a8008cba897924e70d52509077 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 5 Apr 2023 09:06:16 -0700 Subject: [PATCH 15/36] Bump maven-enforcer-plugin from 3.2.1 to 3.3.0 (#2366) Bumps [maven-enforcer-plugin](https://github.com/apache/maven-enforcer) from 3.2.1 to 3.3.0. - [Release notes](https://github.com/apache/maven-enforcer/releases) - [Commits](https://github.com/apache/maven-enforcer/compare/enforcer-3.2.1...enforcer-3.3.0) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-enforcer-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 93abb76ed5..a57ca468d1 100644 --- a/pom.xml +++ b/pom.xml @@ -93,7 +93,7 @@ org.apache.maven.plugins maven-enforcer-plugin - 3.2.1 + 3.3.0 enforce-versions From b43ccee88927fa65d5e39a8ad4d0bebca7bf9994 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 8 Apr 2023 10:05:20 -0700 Subject: [PATCH 16/36] Bump maven-surefire-plugin from 3.0.0-M9 to 3.0.0 (#2342) Bumps [maven-surefire-plugin](https://github.com/apache/maven-surefire) from 3.0.0-M9 to 3.0.0. - [Release notes](https://github.com/apache/maven-surefire/releases) - [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.0.0-M9...surefire-3.0.0) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-surefire-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- gson/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gson/pom.xml b/gson/pom.xml index 8af1ecdee3..afd6ac7183 100644 --- a/gson/pom.xml +++ b/gson/pom.xml @@ -136,7 +136,7 @@ org.apache.maven.plugins maven-surefire-plugin - 3.0.0-M9 + 3.0.0 + + +## `ClassCastException` when using deserialized object **Symptom:** `ClassCastException` is thrown when accessing an object deserialized by Gson @@ -16,7 +19,7 @@ This guide describes how to troubleshoot common issues when using Gson. The overloads with `Type` parameter do not provide any type-safety guarantees. - When using `TypeToken` make sure you don't capture a type variable. For example avoid something like `new TypeToken>()` (where `T` is a type variable). Due to Java type erasure the actual type of `T` is not available at runtime. Refactor your code to pass around `TypeToken` instances or use [`TypeToken.getParameterized(...)`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/reflect/TypeToken.html#getParameterized(java.lang.reflect.Type,java.lang.reflect.Type...)), for example `TypeToken.getParameterized(List.class, elementClass)`. -## `InaccessibleObjectException`: 'module ... does not "opens ..." to unnamed module' +## `InaccessibleObjectException`: 'module ... does not "opens ..." to unnamed module' **Symptom:** An exception with a message in the form 'module ... does not "opens ..." to unnamed module' is thrown @@ -30,7 +33,7 @@ When no built-in adapter for a type exists and no custom adapter has been regist If you want to prevent using reflection on third-party classes in the future you can write your own [`ReflectionAccessFilter`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/ReflectionAccessFilter.html) or use one of the predefined ones, such as `ReflectionAccessFilter.BLOCK_ALL_PLATFORM`. -## `InaccessibleObjectException`: 'module ... does not "opens ..." to module com.google.gson' +## `InaccessibleObjectException`: 'module ... does not "opens ..." to module com.google.gson' **Symptom:** An exception with a message in the form 'module ... does not "opens ..." to module com.google.gson' is thrown @@ -51,7 +54,7 @@ module mymodule { Or in case this occurs for a field in one of your classes which you did not actually want to serialize or deserialize in the first place, you can exclude that field, see the [user guide](UserGuide.md#excluding-fields-from-serialization-and-deserialization). -## Android app not working in Release mode; random property names +## Android app not working in Release mode; random property names **Symptom:** Your Android app is working fine in Debug mode but fails in Release mode and the JSON properties have seemingly random names such as `a`, `b`, ... @@ -59,7 +62,7 @@ Or in case this occurs for a field in one of your classes which you did not actu **Solution:** Make sure you have configured ProGuard / R8 correctly to preserve the names of your fields. See the [Android example](examples/android-proguard-example/README.md) for more information. -## Android app unable to parse JSON after app update +## Android app unable to parse JSON after app update **Symptom:** You released a new version of your Android app and it fails to parse JSON data created by the previous version of your app @@ -71,7 +74,7 @@ If you want to preserve backward compatibility for you app you can use [`@Serial Normally ProGuard and R8 produce a mapping file, this makes it easier to find out the obfuscated field names instead of having to find them out through trial and error or other means. See the [Android Studio user guide](https://developer.android.com/studio/build/shrink-code.html#retracing) for more information. -## Default field values not present after deserialization +## Default field values not present after deserialization **Symptom:** You have assign default values to fields but after deserialization the fields have their standard value (such as `null` or `0`) @@ -84,7 +87,7 @@ Normally ProGuard and R8 produce a mapping file, this makes it easier to find ou Otherwise Gson will by default try to use JDK `Unsafe` or similar means to create an instance of your class without invoking the constructor and without running any initializers. You can also disable that behavior through [`GsonBuilder.disableJdkUnsafe()`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/GsonBuilder.html#disableJdkUnsafe()) to notice such issues early on. -## `null` values for anonymous and local classes +## `null` values for anonymous and local classes **Symptom:** Objects of a class are always serialized as JSON `null` / always deserialized as Java `null` @@ -97,7 +100,7 @@ Notes: - "double brace-initialization" also creates anonymous classes - Local record classes (feature added in Java 16) are supported by Gson and are not affected by this -## Map keys having unexpected format in JSON +## Map keys having unexpected format in JSON **Symptom:** JSON output for `Map` keys is unexpected / cannot be deserialized again @@ -105,15 +108,32 @@ Notes: **Solution:** Use [`GsonBuilder.enableComplexMapKeySerialization()`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/GsonBuilder.html#enableComplexMapKeySerialization()). See also the [user guide](UserGuide.md#maps-examples) for more information. -## Parsing JSON fails with `MalformedJsonException` +## Parsing JSON fails with `MalformedJsonException` **Symptom:** JSON parsing fails with `MalformedJsonException` **Reason:** The JSON data is actually malformed -**Solution:** During debugging log the JSON data right before calling Gson methods or set a breakpoint to inspect the data and make sure it has the expected format. Sometimes APIs might return HTML error pages (instead of JSON data) when reaching rate limits or when other errors occur. Also read the location information of the `MalformedJsonException` exception message, it indicates where exactly in the document the malformed data was detected, including the [JSONPath](https://goessner.net/articles/JsonPath/). +**Solution:** During debugging, log the JSON data right before calling Gson methods or set a breakpoint to inspect the data and make sure it has the expected format. Sometimes APIs might return HTML error pages (instead of JSON data) when reaching rate limits or when other errors occur. Also read the location information of the `MalformedJsonException` exception message, it indicates where exactly in the document the malformed data was detected, including the [JSONPath](https://goessner.net/articles/JsonPath/). + +For example, let's assume you want to deserialize the following JSON data: + +```json +{ + "languages": [ + "English", + "French", + ] +} +``` -## Integral JSON number is parsed as `double` +This will fail with an exception similar to this one: `MalformedJsonException: Use JsonReader.setLenient(true) to accept malformed JSON at line 5 column 4 path $.languages[2]` +The problem here is the trailing comma (`,`) after `"French"`, trailing commas are not allowed by the JSON specification. The location information "line 5 column 4" points to the `]` in the JSON data (with some slight inaccuracies) because Gson expected another value after `,` instead of the closing `]`. The JSONPath `$.languages[2]` in the exception message also points there: `$.` refers to the root object, `languages` refers to its member of that name and `[2]` refers to the (missing) third value in the JSON array value of that member (numbering starts at 0, so it is `[2]` instead of `[3]`). +The proper solution here is to fix the malformed JSON data. + +To spot syntax errors in the JSON data easily you can open it in an editor with support for JSON, for example Visual Studio Code. It will highlight within the JSON data the error location and show why the JSON data is considered invalid. + +## Integral JSON number is parsed as `double` **Symptom:** JSON data contains an integral number such as `45` but Gson returns it as `double` @@ -121,17 +141,17 @@ Notes: **Solution:** Use [`GsonBuilder.setObjectToNumberStrategy`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/GsonBuilder.html#setObjectToNumberStrategy(com.google.gson.ToNumberStrategy)) to specify what type of number should be returned -## Malformed JSON not rejected +## Malformed JSON not rejected **Symptom:** Gson parses malformed JSON without throwing any exceptions **Reason:** Due to legacy reasons Gson performs parsing by default in lenient mode -**Solution:** See [`Gson` class documentation](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/Gson.html) section "Lenient JSON handling" +**Solution:** See [`Gson` class documentation](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/Gson.html#default-lenient) section "Lenient JSON handling" Note: Even in non-lenient mode Gson deviates slightly from the JSON specification, see [`JsonReader.setLenient`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/stream/JsonReader.html#setLenient(boolean)) for more details. -## `IllegalStateException`: "Expected ... but was ..." +## `IllegalStateException`: "Expected ... but was ..." **Symptom:** An `IllegalStateException` with a message in the form "Expected ... but was ..." is thrown @@ -139,13 +159,36 @@ Note: Even in non-lenient mode Gson deviates slightly from the JSON specificatio **Solution:** Make sure that your classes correctly model the JSON data. Also during debugging log the JSON data right before calling Gson methods or set a breakpoint to inspect the data and make sure it has the expected format. Read the location information of the exception message, it indicates where exactly in the document the error occurred, including the [JSONPath](https://goessner.net/articles/JsonPath/). -## `IllegalStateException`: "Expected ... but was NULL" +For example, let's assume you have the following Java class: + +```java +class WebPage { + String languages; +} +``` + +And you want to deserialize the following JSON data: + +```json +{ + "languages": ["English", "French"] +} +``` + +This will fail with an exception similar to this one: `IllegalStateException: Expected a string but was BEGIN_ARRAY at line 2 column 17 path $.languages` +This means Gson expected a JSON string value but found the beginning of a JSON array (`[`). The location information "line 2 column 17" points to the `[` in the JSON data (with some slight inaccuracies), so does the JSONPath `$.languages` in the exception message. It refers to the `languages` member of the root object (`$.`). +The solution here is to change in the `WebPage` class the field `String languages` to `List languages`. + +## `IllegalStateException`: "Expected ... but was NULL" **Symptom:** An `IllegalStateException` with a message in the form "Expected ... but was NULL" is thrown -**Reason:** You have written a custom `TypeAdapter` which does not properly handle a JSON null value +**Reason:** -**Solution:** Add code similar to the following at the beginning of the `read` method of your adapter: +- A built-in adapter does not support JSON null values +- You have written a custom `TypeAdapter` which does not properly handle JSON null values + +**Solution:** If this occurs for a custom adapter you wrote, add code similar to the following at the beginning of its `read` method: ```java @Override @@ -154,14 +197,14 @@ public MyClass read(JsonReader in) throws IOException { in.nextNull(); return null; } - + ... } ``` Alternatively you can call [`nullSafe()`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/TypeAdapter.html#nullSafe()) on the adapter instance you created. -## Properties missing in JSON +## Properties missing in JSON **Symptom:** Properties are missing in the JSON output @@ -171,7 +214,7 @@ Alternatively you can call [`nullSafe()`](https://www.javadoc.io/doc/com.google. Note: Gson does not support anonymous and local classes and will serialize them as JSON null, see the [related troubleshooting point](#null-values-for-anonymous-and-local-classes). -## JSON output changes for newer Android versions +## JSON output changes for newer Android versions **Symptom:** The JSON output differs when running on newer Android versions @@ -185,7 +228,7 @@ When no built-in adapter for a type exists and no custom adapter has been regist If you want to prevent using reflection on third-party classes in the future you can write your own [`ReflectionAccessFilter`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/ReflectionAccessFilter.html) or use one of the predefined ones, such as `ReflectionAccessFilter.BLOCK_ALL_PLATFORM`. -## JSON output contains values of `static` fields +## JSON output contains values of `static` fields **Symptom:** The JSON output contains values of `static` fields @@ -193,7 +236,7 @@ If you want to prevent using reflection on third-party classes in the future you **Solution:** When calling `GsonBuilder.excludeFieldsWithModifiers` you overwrite the default excluded modifiers. Therefore, you have to explicitly exclude `static` fields if desired. This can be done by adding `Modifier.STATIC` as additional argument. -## `NoSuchMethodError` when calling Gson methods +## `NoSuchMethodError` when calling Gson methods **Symptom:** A `java.lang.NoSuchMethodError` is thrown when trying to call certain Gson methods @@ -210,3 +253,31 @@ System.out.println(Gson.class.getProtectionDomain().getCodeSource().getLocation( ``` If that fails with a `NullPointerException` you have to try one of the other ways to find out where a class is loaded from. + +## `IllegalArgumentException`: 'Class ... declares multiple JSON fields named '...'' + +**Symptom:** An exception with the message 'Class ... declares multiple JSON fields named '...'' is thrown + +**Reason:** + +- The name you have specified with a [`@SerializedName`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/annotations/SerializedName.html) annotation for a field collides with the name of another field +- The [`FieldNamingStrategy`](https://javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/FieldNamingStrategy.html) you have specified produces conflicting field names +- A field of your class has the same name as the field of a superclass + +Gson prevents multiple fields with the same name because during deserialization it would be ambiguous for which field the JSON data should be deserialized. For serialization it would cause the same field to appear multiple times in JSON. While the JSON specification permits this, it is likely that the application parsing the JSON data will not handle it correctly. + +**Solution:** First identify the fields with conflicting names based on the exception message. Then decide if you want to rename one of them using the [`@SerializedName`](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/annotations/SerializedName.html) annotation, or if you want to [exclude](UserGuide.md#excluding-fields-from-serialization-and-deserialization) one of them. When excluding one of the fields you have to include it for both serialization and deserialization (even if your application only performs one of these actions) because the duplicate field check cannot differentiate between these actions. + +## `UnsupportedOperationException` when serializing or deserializing `java.lang.Class` + +**Symptom:** An `UnsupportedOperationException` is thrown when trying to serialize or deserialize `java.lang.Class` + +**Reason:** Gson intentionally does not permit serializing and deserializing `java.lang.Class` for security reasons. Otherwise a malicious user could make your application load an arbitrary class from the classpath and, depending on what your application does with the `Class`, in the worst case perform a remote code execution attack. + +**Solution:** First check if you really need to serialize or deserialize a `Class`. Often it is possible to use string aliases and then map them to the known `Class`; you could write a custom [`TypeAdapter`](https://javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/TypeAdapter.html) to do this. If the `Class` values are not known in advance, try to introduce a common base class or interface for all these classes and then verify that the deserialized class is a subclass. For example assuming the base class is called `MyBaseClass`, your custom `TypeAdapter` should load the class like this: + +```java +Class.forName(jsonString, false, getClass().getClassLoader()).asSubclass(MyBaseClass.cla‌​ss) +``` + +This will not initialize arbitrary classes, and it will throw a `ClassCastException` if the loaded class is not the same as or a subclass of `MyBaseClass`. diff --git a/gson/src/main/java/com/google/gson/Gson.java b/gson/src/main/java/com/google/gson/Gson.java index e90831046f..ec5f69f63e 100644 --- a/gson/src/main/java/com/google/gson/Gson.java +++ b/gson/src/main/java/com/google/gson/Gson.java @@ -105,7 +105,7 @@ *

See the Gson User Guide * for a more complete set of examples.

* - *

Lenient JSON handling

+ *

Lenient JSON handling

* For legacy reasons most of the {@code Gson} methods allow JSON data which does not * comply with the JSON specification, regardless of whether {@link GsonBuilder#setLenient()} * is used or not. If this behavior is not desired, the following workarounds can be used: diff --git a/gson/src/main/java/com/google/gson/internal/TroubleshootingGuide.java b/gson/src/main/java/com/google/gson/internal/TroubleshootingGuide.java new file mode 100644 index 0000000000..69c1b06d90 --- /dev/null +++ b/gson/src/main/java/com/google/gson/internal/TroubleshootingGuide.java @@ -0,0 +1,12 @@ +package com.google.gson.internal; + +public class TroubleshootingGuide { + private TroubleshootingGuide() {} + + /** + * Creates a URL referring to the specified troubleshooting section. + */ + public static String createUrl(String id) { + return "https://github.com/google/gson/blob/master/Troubleshooting.md#" + id; + } +} diff --git a/gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java b/gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java index 9194fc33bf..6a43b817c9 100644 --- a/gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java +++ b/gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java @@ -33,6 +33,7 @@ import com.google.gson.internal.ObjectConstructor; import com.google.gson.internal.Primitives; import com.google.gson.internal.ReflectionAccessFilterHelper; +import com.google.gson.internal.TroubleshootingGuide; import com.google.gson.internal.reflect.ReflectionHelper; import com.google.gson.reflect.TypeToken; import com.google.gson.stream.JsonReader; @@ -114,7 +115,7 @@ public TypeAdapter create(Gson gson, final TypeToken type) { if (filterResult == FilterResult.BLOCK_ALL) { throw new JsonIOException( "ReflectionAccessFilter does not permit using reflection for " + raw - + ". Register a TypeAdapter for this type or adjust the access filter."); + + ". Register a TypeAdapter for this type or adjust the access filter."); } boolean blockInaccessible = filterResult == FilterResult.BLOCK_INACCESSIBLE; @@ -306,7 +307,8 @@ private Map getBoundFields(Gson context, TypeToken type, if (previous != null) { throw new IllegalArgumentException("Class " + originalRaw.getName() + " declares multiple JSON fields named '" + previous.name + "'; conflict is caused" - + " by fields " + ReflectionHelper.fieldToString(previous.field) + " and " + ReflectionHelper.fieldToString(field)); + + " by fields " + ReflectionHelper.fieldToString(previous.field) + " and " + ReflectionHelper.fieldToString(field) + + "\nSee " + TroubleshootingGuide.createUrl("duplicate-fields")); } } type = TypeToken.get($Gson$Types.resolve(type.getType(), raw, raw.getGenericSuperclass())); diff --git a/gson/src/main/java/com/google/gson/internal/bind/TypeAdapters.java b/gson/src/main/java/com/google/gson/internal/bind/TypeAdapters.java index b76a0f0e3c..0f414e81e9 100644 --- a/gson/src/main/java/com/google/gson/internal/bind/TypeAdapters.java +++ b/gson/src/main/java/com/google/gson/internal/bind/TypeAdapters.java @@ -28,6 +28,7 @@ import com.google.gson.TypeAdapterFactory; import com.google.gson.annotations.SerializedName; import com.google.gson.internal.LazilyParsedNumber; +import com.google.gson.internal.TroubleshootingGuide; import com.google.gson.reflect.TypeToken; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonToken; @@ -73,12 +74,14 @@ private TypeAdapters() { @Override public void write(JsonWriter out, Class value) throws IOException { throw new UnsupportedOperationException("Attempted to serialize java.lang.Class: " - + value.getName() + ". Forgot to register a type adapter?"); + + value.getName() + ". Forgot to register a type adapter?" + + "\nSee " + TroubleshootingGuide.createUrl("java-lang-class-unsupported")); } @Override public Class read(JsonReader in) throws IOException { throw new UnsupportedOperationException( - "Attempted to deserialize a java.lang.Class. Forgot to register a type adapter?"); + "Attempted to deserialize a java.lang.Class. Forgot to register a type adapter?" + + "\nSee " + TroubleshootingGuide.createUrl("java-lang-class-unsupported")); } }.nullSafe(); diff --git a/gson/src/main/java/com/google/gson/internal/reflect/ReflectionHelper.java b/gson/src/main/java/com/google/gson/internal/reflect/ReflectionHelper.java index 41dd4cf372..de1df3552d 100644 --- a/gson/src/main/java/com/google/gson/internal/reflect/ReflectionHelper.java +++ b/gson/src/main/java/com/google/gson/internal/reflect/ReflectionHelper.java @@ -18,6 +18,7 @@ import com.google.gson.JsonIOException; import com.google.gson.internal.GsonBuildConfig; +import com.google.gson.internal.TroubleshootingGuide; import java.lang.reflect.AccessibleObject; import java.lang.reflect.Constructor; import java.lang.reflect.Field; @@ -40,6 +41,17 @@ public class ReflectionHelper { private ReflectionHelper() {} + private static String getInaccessibleTroubleshootingSuffix(Exception e) { + // Class was added in Java 9, therefore cannot use instanceof + if (e.getClass().getName().equals("java.lang.reflect.InaccessibleObjectException")) { + String message = e.getMessage(); + String troubleshootingId = message != null && message.contains("to module com.google.gson") + ? "reflection-inaccessible-to-module-gson" : "reflection-inaccessible"; + return "\nSee " + TroubleshootingGuide.createUrl(troubleshootingId); + } + return ""; + } + /** * Internal implementation of making an {@link AccessibleObject} accessible. * @@ -52,7 +64,8 @@ public static void makeAccessible(AccessibleObject object) throws JsonIOExceptio } catch (Exception exception) { String description = getAccessibleObjectDescription(object, false); throw new JsonIOException("Failed making " + description + " accessible; either increase its visibility" - + " or write a custom TypeAdapter for its declaring type.", exception); + + " or write a custom TypeAdapter for its declaring type." + getInaccessibleTroubleshootingSuffix(exception), + exception); } } @@ -142,7 +155,7 @@ public static String tryMakeAccessible(Constructor constructor) { return "Failed making constructor '" + constructorToString(constructor) + "' accessible;" + " either increase its visibility or write a custom InstanceCreator or TypeAdapter for" // Include the message since it might contain more detailed information - + " its declaring type: " + exception.getMessage(); + + " its declaring type: " + exception.getMessage() + getInaccessibleTroubleshootingSuffix(exception); } } 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 e0931320d1..de7aef5ff5 100644 --- a/gson/src/main/java/com/google/gson/stream/JsonReader.java +++ b/gson/src/main/java/com/google/gson/stream/JsonReader.java @@ -17,6 +17,7 @@ package com.google.gson.stream; import com.google.gson.internal.JsonReaderInternalAccess; +import com.google.gson.internal.TroubleshootingGuide; import com.google.gson.internal.bind.JsonTreeReader; import java.io.Closeable; import java.io.EOFException; @@ -355,7 +356,7 @@ public void beginArray() throws IOException { pathIndices[stackSize - 1] = 0; peeked = PEEKED_NONE; } else { - throw new IllegalStateException("Expected BEGIN_ARRAY but was " + peek() + locationString()); + throw unexpectedTokenError("BEGIN_ARRAY"); } } @@ -373,7 +374,7 @@ public void endArray() throws IOException { pathIndices[stackSize - 1]++; peeked = PEEKED_NONE; } else { - throw new IllegalStateException("Expected END_ARRAY but was " + peek() + locationString()); + throw unexpectedTokenError("END_ARRAY"); } } @@ -390,7 +391,7 @@ public void beginObject() throws IOException { push(JsonScope.EMPTY_OBJECT); peeked = PEEKED_NONE; } else { - throw new IllegalStateException("Expected BEGIN_OBJECT but was " + peek() + locationString()); + throw unexpectedTokenError("BEGIN_OBJECT"); } } @@ -409,7 +410,7 @@ public void endObject() throws IOException { pathIndices[stackSize - 1]++; peeked = PEEKED_NONE; } else { - throw new IllegalStateException("Expected END_OBJECT but was " + peek() + locationString()); + throw unexpectedTokenError("END_OBJECT"); } } @@ -797,7 +798,7 @@ public String nextName() throws IOException { } else if (p == PEEKED_DOUBLE_QUOTED_NAME) { result = nextQuotedValue('"'); } else { - throw new IllegalStateException("Expected a name but was " + peek() + locationString()); + throw unexpectedTokenError("a name"); } peeked = PEEKED_NONE; pathNames[stackSize - 1] = result; @@ -833,7 +834,7 @@ public String nextString() throws IOException { result = new String(buffer, pos, peekedNumberLength); pos += peekedNumberLength; } else { - throw new IllegalStateException("Expected a string but was " + peek() + locationString()); + throw unexpectedTokenError("a string"); } peeked = PEEKED_NONE; pathIndices[stackSize - 1]++; @@ -861,7 +862,7 @@ public boolean nextBoolean() throws IOException { pathIndices[stackSize - 1]++; return false; } - throw new IllegalStateException("Expected a boolean but was " + peek() + locationString()); + throw unexpectedTokenError("a boolean"); } /** @@ -880,7 +881,7 @@ public void nextNull() throws IOException { peeked = PEEKED_NONE; pathIndices[stackSize - 1]++; } else { - throw new IllegalStateException("Expected null but was " + peek() + locationString()); + throw unexpectedTokenError("null"); } } @@ -915,14 +916,13 @@ public double nextDouble() throws IOException { } else if (p == PEEKED_UNQUOTED) { peekedString = nextUnquotedValue(); } else if (p != PEEKED_BUFFERED) { - throw new IllegalStateException("Expected a double but was " + peek() + locationString()); + throw unexpectedTokenError("a double"); } 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 + locationString()); + throw syntaxError("JSON forbids NaN and infinities: " + result); } peekedString = null; peeked = PEEKED_NONE; @@ -970,7 +970,7 @@ public long nextLong() throws IOException { // Fall back to parse as a double below. } } else { - throw new IllegalStateException("Expected a long but was " + peek() + locationString()); + throw unexpectedTokenError("a long"); } peeked = PEEKED_BUFFERED; @@ -1208,7 +1208,7 @@ public int nextInt() throws IOException { // Fall back to parse as a double below. } } else { - throw new IllegalStateException("Expected an int but was " + peek() + locationString()); + throw unexpectedTokenError("an int"); } peeked = PEEKED_BUFFERED; @@ -1584,10 +1584,10 @@ public String getPath() { /** * Unescapes the character identified by the character or characters that * immediately follow a backslash. The backslash '\' should have already - * been read. This supports both unicode escapes "u000A" and two-character + * been read. This supports both Unicode escapes "u000A" and two-character * escapes "\n". * - * @throws MalformedJsonException if any unicode escape sequences are + * @throws MalformedJsonException if any Unicode escape sequences are * malformed. */ @SuppressWarnings("fallthrough") @@ -1614,7 +1614,7 @@ private char readEscapeCharacter() throws IOException { } else if (c >= 'A' && c <= 'F') { result += (c - 'A' + 10); } else { - throw new MalformedJsonException("\\u" + new String(buffer, pos, 4)); + throw syntaxError("Malformed Unicode escape \\u" + new String(buffer, pos, 4)); } } pos += 4; @@ -1656,7 +1656,16 @@ private char readEscapeCharacter() throws IOException { * with this reader's content. */ private IOException syntaxError(String message) throws IOException { - throw new MalformedJsonException(message + locationString()); + throw new MalformedJsonException(message + locationString() + + "\nSee " + TroubleshootingGuide.createUrl("malformed-json")); + } + + private IllegalStateException unexpectedTokenError(String expected) throws IOException { + JsonToken peeked = peek(); + String troubleshootingId = peeked == JsonToken.NULL + ? "adapter-not-null-safe" : "unexpected-json-structure"; + return new IllegalStateException("Expected " + expected + " but was " + peek() + locationString() + + "\nSee " + TroubleshootingGuide.createUrl(troubleshootingId)); } /** @@ -1699,8 +1708,7 @@ private void consumeNonExecutePrefix() throws IOException { } else if (p == PEEKED_UNQUOTED_NAME) { reader.peeked = PEEKED_UNQUOTED; } else { - throw new IllegalStateException( - "Expected a name but was " + reader.peek() + reader.locationString()); + throw reader.unexpectedTokenError("a name"); } } }; diff --git a/gson/src/test/java/com/google/gson/JsonArrayTest.java b/gson/src/test/java/com/google/gson/JsonArrayTest.java index c3c0184fd6..21a5d58401 100644 --- a/gson/src/test/java/com/google/gson/JsonArrayTest.java +++ b/gson/src/test/java/com/google/gson/JsonArrayTest.java @@ -17,7 +17,6 @@ package com.google.gson; import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth.assertWithMessage; import static org.junit.Assert.fail; import com.google.common.testing.EqualsTester; @@ -140,22 +139,19 @@ public void testFailedGetArrayValues() { jsonArray.getAsBoolean(); fail("expected getBoolean to fail"); } catch (UnsupportedOperationException e) { - assertWithMessage("Expected an exception message") - .that(e).hasMessageThat().isEqualTo("JsonObject"); + assertThat(e).hasMessageThat().isEqualTo("JsonObject"); } try { jsonArray.get(-1); fail("expected get to fail"); } catch (IndexOutOfBoundsException e) { - assertWithMessage("Expected an exception message") - .that(e).hasMessageThat().isEqualTo("Index -1 out of bounds for length 1"); + assertThat(e).hasMessageThat().isEqualTo("Index -1 out of bounds for length 1"); } try { jsonArray.getAsString(); fail("expected getString to fail"); } catch (UnsupportedOperationException e) { - assertWithMessage("Expected an exception message") - .that(e).hasMessageThat().isEqualTo("JsonObject"); + assertThat(e).hasMessageThat().isEqualTo("JsonObject"); } jsonArray.remove(0); @@ -164,36 +160,31 @@ public void testFailedGetArrayValues() { jsonArray.getAsDouble(); fail("expected getDouble to fail"); } catch (NumberFormatException e) { - assertWithMessage("Expected an exception message") - .that(e).hasMessageThat().isEqualTo("For input string: \"hello\""); + assertThat(e).hasMessageThat().isEqualTo("For input string: \"hello\""); } try { jsonArray.getAsInt(); fail("expected getInt to fail"); } catch (NumberFormatException e) { - assertWithMessage("Expected an exception message") - .that(e).hasMessageThat().isEqualTo("For input string: \"hello\""); + assertThat(e).hasMessageThat().isEqualTo("For input string: \"hello\""); } try { jsonArray.get(0).getAsJsonArray(); fail("expected getJSONArray to fail"); } catch (IllegalStateException e) { - assertWithMessage("Expected an exception message") - .that(e).hasMessageThat().isEqualTo("Not a JSON Array: \"hello\""); + assertThat(e).hasMessageThat().isEqualTo("Not a JSON Array: \"hello\""); } try { jsonArray.getAsJsonObject(); fail("expected getJSONObject to fail"); } catch (IllegalStateException e) { - assertWithMessage("Expected an exception message") - .that(e).hasMessageThat().isEqualTo( "Not a JSON Object: [\"hello\"]"); + assertThat(e).hasMessageThat().isEqualTo("Not a JSON Object: [\"hello\"]"); } try { jsonArray.getAsLong(); fail("expected getLong to fail"); } catch (NumberFormatException e) { - assertWithMessage("Expected an exception message") - .that(e).hasMessageThat().isEqualTo("For input string: \"hello\""); + assertThat(e).hasMessageThat().isEqualTo("For input string: \"hello\""); } } diff --git a/gson/src/test/java/com/google/gson/ToNumberPolicyTest.java b/gson/src/test/java/com/google/gson/ToNumberPolicyTest.java index 4750d24fae..2e40867f8f 100644 --- a/gson/src/test/java/com/google/gson/ToNumberPolicyTest.java +++ b/gson/src/test/java/com/google/gson/ToNumberPolicyTest.java @@ -37,7 +37,9 @@ public void testDouble() throws IOException { strategy.readNumber(fromString("1e400")); fail(); } catch (MalformedJsonException expected) { - assertThat(expected).hasMessageThat().isEqualTo("JSON forbids NaN and infinities: Infinity at line 1 column 6 path $"); + assertThat(expected).hasMessageThat().isEqualTo( + "JSON forbids NaN and infinities: Infinity at line 1 column 6 path $" + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#malformed-json"); } try { strategy.readNumber(fromString("\"not-a-number\"")); @@ -80,19 +82,25 @@ public void testLongOrDouble() throws IOException { strategy.readNumber(fromString("NaN")); fail(); } catch (MalformedJsonException expected) { - assertThat(expected).hasMessageThat().isEqualTo("Use JsonReader.setLenient(true) to accept malformed JSON at line 1 column 1 path $"); + assertThat(expected).hasMessageThat().isEqualTo( + "Use JsonReader.setLenient(true) to accept malformed JSON at line 1 column 1 path $" + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#malformed-json"); } try { strategy.readNumber(fromString("Infinity")); fail(); } catch (MalformedJsonException expected) { - assertThat(expected).hasMessageThat().isEqualTo("Use JsonReader.setLenient(true) to accept malformed JSON at line 1 column 1 path $"); + assertThat(expected).hasMessageThat().isEqualTo( + "Use JsonReader.setLenient(true) to accept malformed JSON at line 1 column 1 path $" + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#malformed-json"); } try { strategy.readNumber(fromString("-Infinity")); fail(); } catch (MalformedJsonException expected) { - assertThat(expected).hasMessageThat().isEqualTo("Use JsonReader.setLenient(true) to accept malformed JSON at line 1 column 1 path $"); + assertThat(expected).hasMessageThat().isEqualTo( + "Use JsonReader.setLenient(true) to accept malformed JSON at line 1 column 1 path $" + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#malformed-json"); } } @@ -117,21 +125,29 @@ public void testNullsAreNeverExpected() throws IOException { ToNumberPolicy.DOUBLE.readNumber(fromString("null")); fail(); } catch (IllegalStateException expected) { + assertThat(expected).hasMessageThat().isEqualTo("Expected a double but was NULL at line 1 column 5 path $" + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#adapter-not-null-safe"); } try { ToNumberPolicy.LAZILY_PARSED_NUMBER.readNumber(fromString("null")); fail(); } catch (IllegalStateException expected) { + assertThat(expected).hasMessageThat().isEqualTo("Expected a string but was NULL at line 1 column 5 path $" + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#adapter-not-null-safe"); } try { ToNumberPolicy.LONG_OR_DOUBLE.readNumber(fromString("null")); fail(); } catch (IllegalStateException expected) { + assertThat(expected).hasMessageThat().isEqualTo("Expected a string but was NULL at line 1 column 5 path $" + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#adapter-not-null-safe"); } try { ToNumberPolicy.BIG_DECIMAL.readNumber(fromString("null")); fail(); } catch (IllegalStateException expected) { + assertThat(expected).hasMessageThat().isEqualTo("Expected a string but was NULL at line 1 column 5 path $" + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#adapter-not-null-safe"); } } diff --git a/gson/src/test/java/com/google/gson/functional/DefaultTypeAdaptersTest.java b/gson/src/test/java/com/google/gson/functional/DefaultTypeAdaptersTest.java index d0d2eff78c..841b01251e 100644 --- a/gson/src/test/java/com/google/gson/functional/DefaultTypeAdaptersTest.java +++ b/gson/src/test/java/com/google/gson/functional/DefaultTypeAdaptersTest.java @@ -95,6 +95,8 @@ public void testClassSerialization() { gson.toJson(String.class); fail(); } catch (UnsupportedOperationException expected) { + assertThat(expected).hasMessageThat().isEqualTo("Attempted to serialize java.lang.Class: java.lang.String. Forgot to register a type adapter?" + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#java-lang-class-unsupported"); } // Override with a custom type adapter for class. gson = new GsonBuilder().registerTypeAdapter(Class.class, new MyClassTypeAdapter()).create(); @@ -107,6 +109,8 @@ public void testClassDeserialization() { gson.fromJson("String.class", Class.class); fail(); } catch (UnsupportedOperationException expected) { + assertThat(expected).hasMessageThat().isEqualTo("Attempted to deserialize a java.lang.Class. Forgot to register a type adapter?" + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#java-lang-class-unsupported"); } // Override with a custom type adapter for class. gson = new GsonBuilder().registerTypeAdapter(Class.class, new MyClassTypeAdapter()).create(); @@ -365,7 +369,7 @@ public void testBitSetDeserialization() { gson.fromJson("[1, []]", BitSet.class); fail(); } catch (JsonSyntaxException e) { - assertThat(e.getMessage()).isEqualTo("Invalid bitset value type: BEGIN_ARRAY; at path $[1]"); + assertThat(e).hasMessageThat().isEqualTo("Invalid bitset value type: BEGIN_ARRAY; at path $[1]"); } try { @@ -631,7 +635,7 @@ public void testJsonElementTypeMismatch() { gson.fromJson("\"abc\"", JsonObject.class); fail(); } catch (JsonSyntaxException expected) { - assertThat(expected.getMessage()).isEqualTo("Expected a com.google.gson.JsonObject but was com.google.gson.JsonPrimitive; at path $"); + assertThat(expected).hasMessageThat().isEqualTo("Expected a com.google.gson.JsonObject but was com.google.gson.JsonPrimitive; at path $"); } } diff --git a/gson/src/test/java/com/google/gson/functional/GsonVersionDiagnosticsTest.java b/gson/src/test/java/com/google/gson/functional/GsonVersionDiagnosticsTest.java index b7029487e8..a1a048fcd7 100644 --- a/gson/src/test/java/com/google/gson/functional/GsonVersionDiagnosticsTest.java +++ b/gson/src/test/java/com/google/gson/functional/GsonVersionDiagnosticsTest.java @@ -53,8 +53,8 @@ public void setUp() { @Test public void testVersionPattern() { - assertThat(GSON_VERSION_PATTERN.matcher("(GSON 2.8.5)").matches()).isTrue(); - assertThat(GSON_VERSION_PATTERN.matcher("(GSON 2.8.5-SNAPSHOT)").matches()).isTrue(); + assertThat("(GSON 2.8.5)").matches(GSON_VERSION_PATTERN); + assertThat("(GSON 2.8.5-SNAPSHOT)").matches(GSON_VERSION_PATTERN); } @Test @@ -80,7 +80,7 @@ private void ensureAssertionErrorPrintsGsonVersion(AssertionError expected) { assertThat(end > 0 && end > start + 6).isTrue(); String version = msg.substring(start, end); // System.err.println(version); - assertThat(GSON_VERSION_PATTERN.matcher(version).matches()).isTrue(); + assertThat(version).matches(GSON_VERSION_PATTERN); } private static final class TestType { diff --git a/gson/src/test/java/com/google/gson/functional/MapAsArrayTypeAdapterTest.java b/gson/src/test/java/com/google/gson/functional/MapAsArrayTypeAdapterTest.java index 38a45644ba..5d863f1538 100644 --- a/gson/src/test/java/com/google/gson/functional/MapAsArrayTypeAdapterTest.java +++ b/gson/src/test/java/com/google/gson/functional/MapAsArrayTypeAdapterTest.java @@ -59,7 +59,7 @@ public void testSerializeComplexMapWithTypeAdapter() { @Test @Ignore - public void disabled_testTwoTypesCollapseToOneSerialize() { + public void testTwoTypesCollapseToOneSerialize() { Gson gson = new GsonBuilder() .enableComplexMapKeySerialization() .create(); diff --git a/gson/src/test/java/com/google/gson/functional/MoreSpecificTypeSerializationTest.java b/gson/src/test/java/com/google/gson/functional/MoreSpecificTypeSerializationTest.java index 25a5b5ff84..f360e84d97 100644 --- a/gson/src/test/java/com/google/gson/functional/MoreSpecificTypeSerializationTest.java +++ b/gson/src/test/java/com/google/gson/functional/MoreSpecificTypeSerializationTest.java @@ -17,7 +17,6 @@ package com.google.gson.functional; import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth.assertWithMessage; import com.google.gson.Gson; import com.google.gson.JsonObject; @@ -57,8 +56,8 @@ public void testListOfSubclassFields() { list.add(new Sub(2, 3)); ClassWithContainersOfBaseFields target = new ClassWithContainersOfBaseFields(list, null); String json = gson.toJson(target); - assertWithMessage(json).that(json).contains("{\"b\":1}"); - assertWithMessage(json).that(json).contains("{\"s\":3,\"b\":2}"); + assertThat(json).contains("{\"b\":1}"); + assertThat(json).contains("{\"s\":3,\"b\":2}"); } @Test @@ -98,8 +97,8 @@ public void testListOfParameterizedSubclassFields() { ClassWithContainersOfParameterizedBaseFields target = new ClassWithContainersOfParameterizedBaseFields(list, null); String json = gson.toJson(target); - assertWithMessage(json).that(json).contains("{\"t\":\"one\"}"); - assertWithMessage(json).that(json).doesNotContain("\"s\":"); + assertThat(json).contains("{\"t\":\"one\"}"); + assertThat(json).doesNotContain("\"s\":"); } /** diff --git a/gson/src/test/java/com/google/gson/functional/NamingPolicyTest.java b/gson/src/test/java/com/google/gson/functional/NamingPolicyTest.java index 5527d36381..8234df8ecc 100644 --- a/gson/src/test/java/com/google/gson/functional/NamingPolicyTest.java +++ b/gson/src/test/java/com/google/gson/functional/NamingPolicyTest.java @@ -26,6 +26,7 @@ import com.google.gson.common.TestTypes.ClassWithSerializedNameFields; import com.google.gson.common.TestTypes.StringWrapper; import java.lang.reflect.Field; +import java.util.Locale; import org.junit.Before; import org.junit.Test; @@ -137,7 +138,29 @@ public void testGsonDuplicateNameUsingSerializedNameFieldNamingPolicySerializati assertThat(expected).hasMessageThat() .isEqualTo("Class com.google.gson.functional.NamingPolicyTest$ClassWithDuplicateFields declares multiple JSON fields named 'a';" + " conflict is caused by fields com.google.gson.functional.NamingPolicyTest$ClassWithDuplicateFields#a and" - + " com.google.gson.functional.NamingPolicyTest$ClassWithDuplicateFields#b"); + + " com.google.gson.functional.NamingPolicyTest$ClassWithDuplicateFields#b" + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#duplicate-fields"); + } + } + + @Test + public void testGsonDuplicateNameDueToBadNamingPolicy() { + Gson gson = builder.setFieldNamingStrategy(new FieldNamingStrategy() { + @Override + public String translateName(Field f) { + return "x"; + } + }).create(); + + try { + gson.toJson(new ClassWithTwoFields()); + fail(); + } catch (IllegalArgumentException expected) { + assertThat(expected).hasMessageThat() + .isEqualTo("Class com.google.gson.functional.NamingPolicyTest$ClassWithTwoFields declares multiple JSON fields named 'x';" + + " conflict is caused by fields com.google.gson.functional.NamingPolicyTest$ClassWithTwoFields#a and" + + " com.google.gson.functional.NamingPolicyTest$ClassWithTwoFields#b" + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#duplicate-fields"); } } @@ -209,7 +232,7 @@ static final class AtName { private static final class UpperCaseNamingStrategy implements FieldNamingStrategy { @Override public String translateName(Field f) { - return f.getName().toUpperCase(); + return f.getName().toUpperCase(Locale.ROOT); } } @@ -239,4 +262,12 @@ private static class ClassWithComplexFieldName { this.value = value; } } + + @SuppressWarnings("unused") + private static class ClassWithTwoFields { + public int a; + public int b; + + public ClassWithTwoFields() {} + } } diff --git a/gson/src/test/java/com/google/gson/functional/ObjectTest.java b/gson/src/test/java/com/google/gson/functional/ObjectTest.java index 208db3a01e..46324e98d8 100644 --- a/gson/src/test/java/com/google/gson/functional/ObjectTest.java +++ b/gson/src/test/java/com/google/gson/functional/ObjectTest.java @@ -177,7 +177,8 @@ public void testClassWithDuplicateFields() { } catch (IllegalArgumentException e) { assertThat(e).hasMessageThat().isEqualTo("Class com.google.gson.functional.ObjectTest$Subclass declares multiple JSON fields named 's';" + " conflict is caused by fields com.google.gson.functional.ObjectTest$Superclass1#s and" - + " com.google.gson.functional.ObjectTest$Superclass2#s"); + + " com.google.gson.functional.ObjectTest$Superclass2#s" + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#duplicate-fields"); } } @@ -196,6 +197,7 @@ public void testNestedDeserialization() { Nested target = gson.fromJson(json, Nested.class); assertThat(target.getExpectedJson()).isEqualTo(json); } + @Test public void testNullSerialization() { assertThat(gson.toJson(null)).isEqualTo("null"); @@ -632,7 +634,7 @@ public void testStaticFieldDeserialization() { gson.fromJson("{\"s\":\"custom\"}", ClassWithStaticFinalField.class); fail(); } catch (JsonIOException e) { - assertThat( e.getMessage()).isEqualTo("Cannot set value of 'static final' field 'com.google.gson.functional.ObjectTest$ClassWithStaticFinalField#s'"); + assertThat(e).hasMessageThat().isEqualTo("Cannot set value of 'static final' field 'com.google.gson.functional.ObjectTest$ClassWithStaticFinalField#s'"); } } @@ -652,7 +654,7 @@ public void testThrowingDefaultConstructor() { } // TODO: Adjust this once Gson throws more specific exception type catch (RuntimeException e) { - assertThat( e.getMessage()).isEqualTo("Failed to invoke constructor 'com.google.gson.functional.ObjectTest$ClassWithThrowingConstructor()' with no args"); + assertThat(e).hasMessageThat().isEqualTo("Failed to invoke constructor 'com.google.gson.functional.ObjectTest$ClassWithThrowingConstructor()' with no args"); assertThat(e).hasCauseThat().isSameInstanceAs(ClassWithThrowingConstructor.thrownException); } } diff --git a/gson/src/test/java/com/google/gson/functional/PrimitiveTest.java b/gson/src/test/java/com/google/gson/functional/PrimitiveTest.java index f7bccb46e3..244d288371 100644 --- a/gson/src/test/java/com/google/gson/functional/PrimitiveTest.java +++ b/gson/src/test/java/com/google/gson/functional/PrimitiveTest.java @@ -92,21 +92,21 @@ public void testByteDeserializationLossy() { gson.fromJson("-129", byte.class); fail(); } catch (JsonSyntaxException e) { - assertThat(e.getMessage()).isEqualTo("Lossy conversion from -129 to byte; at path $"); + assertThat(e).hasMessageThat().isEqualTo("Lossy conversion from -129 to byte; at path $"); } try { gson.fromJson("256", byte.class); fail(); } catch (JsonSyntaxException e) { - assertThat(e.getMessage()).isEqualTo("Lossy conversion from 256 to byte; at path $"); + assertThat(e).hasMessageThat().isEqualTo("Lossy conversion from 256 to byte; at path $"); } try { gson.fromJson("2147483648", byte.class); fail(); } catch (JsonSyntaxException e) { - assertThat(e.getMessage()).isEqualTo("java.lang.NumberFormatException: Expected an int but was 2147483648 at line 1 column 11 path $"); + assertThat(e).hasMessageThat().isEqualTo("java.lang.NumberFormatException: Expected an int but was 2147483648 at line 1 column 11 path $"); } } @@ -140,21 +140,21 @@ public void testShortDeserializationLossy() { gson.fromJson("-32769", short.class); fail(); } catch (JsonSyntaxException e) { - assertThat(e.getMessage()).isEqualTo("Lossy conversion from -32769 to short; at path $"); + assertThat(e).hasMessageThat().isEqualTo("Lossy conversion from -32769 to short; at path $"); } try { gson.fromJson("65536", short.class); fail(); } catch (JsonSyntaxException e) { - assertThat(e.getMessage()).isEqualTo("Lossy conversion from 65536 to short; at path $"); + assertThat(e).hasMessageThat().isEqualTo("Lossy conversion from 65536 to short; at path $"); } try { gson.fromJson("2147483648", short.class); fail(); } catch (JsonSyntaxException e) { - assertThat(e.getMessage()).isEqualTo("java.lang.NumberFormatException: Expected an int but was 2147483648 at line 1 column 11 path $"); + assertThat(e).hasMessageThat().isEqualTo("java.lang.NumberFormatException: Expected an int but was 2147483648 at line 1 column 11 path $"); } } @@ -1064,6 +1064,7 @@ public void testDeserializingBigIntegerAsBigDecimal() { @Test public void testStringsAsBooleans() { String json = "['true', 'false', 'TRUE', 'yes', '1']"; - assertThat( gson.>fromJson(json, new TypeToken>() {}.getType())).isEqualTo(Arrays.asList(true, false, true, false, false)); + List deserialized = gson.fromJson(json, new TypeToken>() {}); + assertThat(deserialized).isEqualTo(Arrays.asList(true, false, true, false, false)); } } diff --git a/gson/src/test/java/com/google/gson/functional/ReflectionAccessTest.java b/gson/src/test/java/com/google/gson/functional/ReflectionAccessTest.java index 756603607b..bd0c71983c 100644 --- a/gson/src/test/java/com/google/gson/functional/ReflectionAccessTest.java +++ b/gson/src/test/java/com/google/gson/functional/ReflectionAccessTest.java @@ -79,7 +79,7 @@ public void checkPermission(Permission perm) { gson.getAdapter(clazz); fail(); } catch (SecurityException e) { - assertThat(e.getMessage()).isEqualTo("Gson: no-member-access"); + assertThat(e).hasMessageThat().isEqualTo("Gson: no-member-access"); } final AtomicBoolean wasReadCalled = new AtomicBoolean(false); @@ -107,6 +107,20 @@ public Object read(JsonReader in) throws IOException { } } + private static JsonIOException assertInaccessibleException(String json, Class toDeserialize) { + Gson gson = new Gson(); + try { + gson.fromJson(json, toDeserialize); + throw new AssertionError("Missing exception; test has to be run with `--illegal-access=deny`"); + } catch (JsonSyntaxException e) { + throw new AssertionError("Unexpected exception; test has to be run with `--illegal-access=deny`", e); + } catch (JsonIOException expected) { + assertThat(expected).hasMessageThat().endsWith("\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#reflection-inaccessible"); + // Return exception for further assertions + return expected; + } + } + /** * Test serializing an instance of a non-accessible internal class, but where * Gson supports serializing one of its superinterfaces. @@ -126,14 +140,19 @@ public void testSerializeInternalImplementationObject() { // But deserialization should fail Class internalClass = Collections.emptyList().getClass(); - try { - gson.fromJson("[]", internalClass); - fail("Missing exception; test has to be run with `--illegal-access=deny`"); - } catch (JsonSyntaxException e) { - throw new AssertionError("Unexpected exception; test has to be run with `--illegal-access=deny`", e); - } catch (JsonIOException expected) { - assertThat(expected).hasMessageThat().startsWith("Failed making constructor 'java.util.Collections$EmptyList()' accessible;" - + " either increase its visibility or write a custom InstanceCreator or TypeAdapter for its declaring type: "); - } + JsonIOException exception = assertInaccessibleException("[]", internalClass); + // Don't check exact class name because it is a JDK implementation detail + assertThat(exception).hasMessageThat().startsWith("Failed making constructor '"); + assertThat(exception).hasMessageThat().contains("' accessible; either increase its visibility or" + + " write a custom InstanceCreator or TypeAdapter for its declaring type: "); + } + + @Test + public void testInaccessibleField() { + JsonIOException exception = assertInaccessibleException("{}", Throwable.class); + // Don't check exact field name because it is a JDK implementation detail + assertThat(exception).hasMessageThat().startsWith("Failed making field 'java.lang.Throwable#"); + assertThat(exception).hasMessageThat().contains("' accessible; either increase its visibility or" + + " write a custom TypeAdapter for its declaring type."); } } diff --git a/gson/src/test/java/com/google/gson/functional/StreamingTypeAdaptersTest.java b/gson/src/test/java/com/google/gson/functional/StreamingTypeAdaptersTest.java index fecc828f47..d259564eb3 100644 --- a/gson/src/test/java/com/google/gson/functional/StreamingTypeAdaptersTest.java +++ b/gson/src/test/java/com/google/gson/functional/StreamingTypeAdaptersTest.java @@ -17,7 +17,6 @@ package com.google.gson.functional; import static com.google.common.truth.Truth.assertThat; -import static com.google.common.truth.Truth.assertWithMessage; import static org.junit.Assert.fail; import com.google.common.base.Splitter; @@ -151,7 +150,7 @@ public void testSerialize1dArray() { public void testDeserialize1dArray() throws IOException { TypeAdapter arrayAdapter = miniGson.getAdapter(new TypeToken() {}); double[] array = arrayAdapter.fromJson("[1.0,2.0,3.0]"); - assertWithMessage(Arrays.toString(array)).that(Arrays.equals(new double[]{1.0, 2.0, 3.0}, array)).isTrue(); + assertThat(array).isEqualTo(new double[]{1.0, 2.0, 3.0}); } @Test @@ -166,7 +165,7 @@ public void testDeserialize2dArray() throws IOException { TypeAdapter arrayAdapter = miniGson.getAdapter(new TypeToken() {}); double[][] array = arrayAdapter.fromJson("[[1.0,2.0],[3.0]]"); double[][] expected = { {1.0, 2.0 }, { 3.0 } }; - assertWithMessage(Arrays.toString(array)).that(Arrays.deepEquals(expected, array)).isTrue(); + assertThat(array).isEqualTo(expected); } @Test @@ -195,7 +194,10 @@ public void testNullSafe() { try { gson.fromJson(json, Truck.class); fail(); - } catch (JsonSyntaxException expected) {} + } catch (JsonSyntaxException expected) { + assertThat(expected).hasMessageThat().isEqualTo("java.lang.IllegalStateException: Expected a string but was NULL at line 1 column 33 path $.passengers[0]" + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#adapter-not-null-safe"); + } gson = new GsonBuilder().registerTypeAdapter(Person.class, typeAdapter.nullSafe()).create(); assertThat(gson.toJson(truck, Truck.class)) .isEqualTo("{\"horsePower\":1.0,\"passengers\":[null,\"jesse,30\"]}"); @@ -216,7 +218,7 @@ public void testSerializeRecursive() { + "'left':{'label':'left','left':null,'right':null}," + "'right':{'label':'right','left':null,'right':null}}"); } - + @Test public void testFromJsonTree() { JsonObject truckObject = new JsonObject(); diff --git a/gson/src/test/java/com/google/gson/functional/TypeAdapterPrecedenceTest.java b/gson/src/test/java/com/google/gson/functional/TypeAdapterPrecedenceTest.java index e96175a3c9..328b6ede1e 100644 --- a/gson/src/test/java/com/google/gson/functional/TypeAdapterPrecedenceTest.java +++ b/gson/src/test/java/com/google/gson/functional/TypeAdapterPrecedenceTest.java @@ -75,7 +75,7 @@ public void testStreamingFollowedByNonstreaming() { .registerTypeAdapter(Foo.class, newDeserializer("deserializer")) .create(); assertThat(gson.toJson(new Foo("foo"))).isEqualTo("\"foo via serializer\""); - assertThat( gson.fromJson("foo", Foo.class).name).isEqualTo("foo via deserializer"); + assertThat(gson.fromJson("foo", Foo.class).name).isEqualTo("foo via deserializer"); } @Test diff --git a/gson/src/test/java/com/google/gson/internal/LinkedTreeMapTest.java b/gson/src/test/java/com/google/gson/internal/LinkedTreeMapTest.java index f79c76b7cc..b3b7ff53d0 100644 --- a/gson/src/test/java/com/google/gson/internal/LinkedTreeMapTest.java +++ b/gson/src/test/java/com/google/gson/internal/LinkedTreeMapTest.java @@ -99,7 +99,7 @@ public void testPutNullValue_Forbidden() { map.put("a", null); fail(); } catch (NullPointerException e) { - assertThat(e.getMessage()).isEqualTo("value == null"); + assertThat(e).hasMessageThat().isEqualTo("value == null"); } assertThat(map).hasSize(0); assertThat(map).doesNotContainKey("a"); @@ -132,7 +132,7 @@ public void testEntrySetValueNull_Forbidden() { entry.setValue(null); fail(); } catch (NullPointerException e) { - assertThat(e.getMessage()).isEqualTo("value == null"); + assertThat(e).hasMessageThat().isEqualTo("value == null"); } assertThat(entry.getValue()).isEqualTo("1"); assertThat(map.get("a")).isEqualTo("1"); diff --git a/gson/src/test/java/com/google/gson/internal/bind/JsonElementReaderTest.java b/gson/src/test/java/com/google/gson/internal/bind/JsonElementReaderTest.java index 9974da13ce..4ee7656147 100644 --- a/gson/src/test/java/com/google/gson/internal/bind/JsonElementReaderTest.java +++ b/gson/src/test/java/com/google/gson/internal/bind/JsonElementReaderTest.java @@ -63,21 +63,21 @@ public void testStrictNansAndInfinities() throws IOException { reader.nextDouble(); fail(); } catch (MalformedJsonException e) { - assertThat(e.getMessage()).isEqualTo("JSON forbids NaN and infinities: NaN"); + assertThat(e).hasMessageThat().isEqualTo("JSON forbids NaN and infinities: NaN"); } assertThat(reader.nextString()).isEqualTo("NaN"); try { reader.nextDouble(); fail(); } catch (MalformedJsonException e) { - assertThat(e.getMessage()).isEqualTo("JSON forbids NaN and infinities: -Infinity"); + assertThat(e).hasMessageThat().isEqualTo("JSON forbids NaN and infinities: -Infinity"); } assertThat(reader.nextString()).isEqualTo("-Infinity"); try { reader.nextDouble(); fail(); } catch (MalformedJsonException e) { - assertThat(e.getMessage()).isEqualTo("JSON forbids NaN and infinities: Infinity"); + assertThat(e).hasMessageThat().isEqualTo("JSON forbids NaN and infinities: Infinity"); } assertThat(reader.nextString()).isEqualTo("Infinity"); reader.endArray(); 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 5faa718015..cea6023c61 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 @@ -133,7 +133,7 @@ public JsonElement deepCopy() { reader.peek(); fail(); } catch (MalformedJsonException expected) { - assertThat(expected.getMessage()).isEqualTo("Custom JsonElement subclass " + CustomSubclass.class.getName() + " is not supported"); + assertThat(expected).hasMessageThat().isEqualTo("Custom JsonElement subclass " + CustomSubclass.class.getName() + " is not supported"); } } diff --git a/gson/src/test/java/com/google/gson/metrics/PerformanceTest.java b/gson/src/test/java/com/google/gson/metrics/PerformanceTest.java index a9b5ca1039..e864000950 100644 --- a/gson/src/test/java/com/google/gson/metrics/PerformanceTest.java +++ b/gson/src/test/java/com/google/gson/metrics/PerformanceTest.java @@ -34,7 +34,7 @@ /** * Tests to measure performance for Gson. All tests in this file will be disabled in code. To run - * them remove disabled_ prefix from the tests and run them. + * them remove the {@code @Ignore} annotation from the tests. * * @author Inderjeet Singh * @author Joel Leitch @@ -58,7 +58,7 @@ public void testDummy() { @Test @Ignore - public void disabled_testStringDeserialization() { + public void testStringDeserialization() { StringBuilder sb = new StringBuilder(8096); sb.append("Error Yippie"); @@ -117,7 +117,7 @@ private CollectionEntry() { */ @Test @Ignore - public void disabled_testLargeCollectionSerialization() { + public void testLargeCollectionSerialization() { int count = 1400000; List list = new ArrayList<>(count); for (int i = 0; i < count; ++i) { @@ -131,7 +131,7 @@ public void disabled_testLargeCollectionSerialization() { */ @Test @Ignore - public void disabled_testLargeCollectionDeserialization() { + public void testLargeCollectionDeserialization() { StringBuilder sb = new StringBuilder(); int count = 87000; boolean first = true; @@ -157,7 +157,7 @@ public void disabled_testLargeCollectionDeserialization() { // Last I tested, Gson was able to serialize upto 14MB byte array @Test @Ignore - public void disabled_testByteArraySerialization() { + public void testByteArraySerialization() { for (int size = 4145152; true; size += 1036288) { byte[] ba = new byte[size]; for (int i = 0; i < size; ++i) { @@ -174,7 +174,7 @@ public void disabled_testByteArraySerialization() { // Last I tested, Gson was able to deserialize a byte array of 11MB @Test @Ignore - public void disabled_testByteArrayDeserialization() { + public void testByteArrayDeserialization() { for (int numElements = 10639296; true; numElements += 16384) { StringBuilder sb = new StringBuilder(numElements*2); sb.append("["); @@ -205,7 +205,7 @@ public void disabled_testByteArrayDeserialization() { @Test @Ignore - public void disabled_testSerializeClasses() { + public void testSerializeClasses() { ClassWithList c = new ClassWithList("str"); for (int i = 0; i < COLLECTION_SIZE; ++i) { c.list.add(new ClassWithField("element-" + i)); @@ -222,7 +222,7 @@ public void disabled_testSerializeClasses() { @Test @Ignore - public void disabled_testDeserializeClasses() { + public void testDeserializeClasses() { String json = buildJsonForClassWithList(); ClassWithList[] target = new ClassWithList[NUM_ITERATIONS]; long t1 = System.currentTimeMillis(); @@ -236,7 +236,7 @@ public void disabled_testDeserializeClasses() { @Test @Ignore - public void disabled_testLargeObjectSerializationAndDeserialization() { + public void testLargeObjectSerializationAndDeserialization() { Map largeObject = new HashMap<>(); for (long l = 0; l < 100000; l++) { largeObject.put("field" + l, l); @@ -256,7 +256,7 @@ public void disabled_testLargeObjectSerializationAndDeserialization() { @Test @Ignore - public void disabled_testSerializeExposedClasses() { + public void testSerializeExposedClasses() { ClassWithListOfObjects c1 = new ClassWithListOfObjects("str"); for (int i1 = 0; i1 < COLLECTION_SIZE; ++i1) { c1.list.add(new ClassWithExposedField("element-" + i1)); @@ -274,7 +274,7 @@ public void disabled_testSerializeExposedClasses() { @Test @Ignore - public void disabled_testDeserializeExposedClasses() { + public void testDeserializeExposedClasses() { String json = buildJsonForClassWithList(); ClassWithListOfObjects[] target = new ClassWithListOfObjects[NUM_ITERATIONS]; long t1 = System.currentTimeMillis(); @@ -288,7 +288,7 @@ public void disabled_testDeserializeExposedClasses() { @Test @Ignore - public void disabled_testLargeGsonMapRoundTrip() throws Exception { + public void testLargeGsonMapRoundTrip() throws Exception { Map original = new HashMap<>(); for (long i = 0; i < 1000000; i++) { original.put(i, i + 1); diff --git a/gson/src/test/java/com/google/gson/reflect/TypeTokenTest.java b/gson/src/test/java/com/google/gson/reflect/TypeTokenTest.java index 5df759f00b..61c5dfc2b9 100644 --- a/gson/src/test/java/com/google/gson/reflect/TypeTokenTest.java +++ b/gson/src/test/java/com/google/gson/reflect/TypeTokenTest.java @@ -235,21 +235,21 @@ class SubSubTypeToken2 extends SubTypeToken {} new SubTypeToken() {}; fail(); } catch (IllegalStateException expected) { - assertThat(expected.getMessage()).isEqualTo("Must only create direct subclasses of TypeToken"); + assertThat(expected).hasMessageThat().isEqualTo("Must only create direct subclasses of TypeToken"); } try { new SubSubTypeToken1(); fail(); } catch (IllegalStateException expected) { - assertThat(expected.getMessage()).isEqualTo("Must only create direct subclasses of TypeToken"); + assertThat(expected).hasMessageThat().isEqualTo("Must only create direct subclasses of TypeToken"); } try { new SubSubTypeToken2(); fail(); } catch (IllegalStateException expected) { - assertThat(expected.getMessage()).isEqualTo("Must only create direct subclasses of TypeToken"); + assertThat(expected).hasMessageThat().isEqualTo("Must only create direct subclasses of TypeToken"); } } 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 2009f16bd6..90406b6df0 100644 --- a/gson/src/test/java/com/google/gson/stream/JsonReaderTest.java +++ b/gson/src/test/java/com/google/gson/stream/JsonReaderTest.java @@ -16,6 +16,7 @@ package com.google.gson.stream; +import static com.google.common.truth.Truth.assertThat; 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; @@ -25,7 +26,6 @@ import static com.google.gson.stream.JsonToken.NULL; import static com.google.gson.stream.JsonToken.NUMBER; import static com.google.gson.stream.JsonToken.STRING; -import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; import java.io.EOFException; @@ -273,7 +273,9 @@ public void testInvalidJsonInput() throws IOException { try { reader.nextName(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertThat(expected).hasMessageThat().isEqualTo("Invalid escape sequence at line 2 column 8 path $." + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#malformed-json"); } } @@ -288,16 +290,16 @@ public void testNulls() { } @Test - public void testEmptyString() { + public void testEmptyString() throws IOException { try { new JsonReader(reader("")).beginArray(); fail(); - } catch (IOException expected) { + } catch (EOFException expected) { } try { new JsonReader(reader("")).beginObject(); fail(); - } catch (IOException expected) { + } catch (EOFException expected) { } } @@ -357,6 +359,8 @@ public void testUnescapingInvalidCharacters() throws IOException { reader.nextString(); fail(); } catch (MalformedJsonException expected) { + assertThat(expected).hasMessageThat().isEqualTo("Malformed Unicode escape \\u000g at line 1 column 5 path $[0]" + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#malformed-json"); } } @@ -368,7 +372,9 @@ public void testUnescapingTruncatedCharacters() throws IOException { try { reader.nextString(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertThat(expected).hasMessageThat().isEqualTo("Unterminated escape sequence at line 1 column 5 path $[0]" + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#malformed-json"); } } @@ -380,7 +386,9 @@ public void testUnescapingTruncatedSequence() throws IOException { try { reader.nextString(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertThat(expected).hasMessageThat().isEqualTo("Unterminated escape sequence at line 1 column 4 path $[0]" + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#malformed-json"); } } @@ -446,6 +454,7 @@ public void testStrictNonFiniteDoubles() throws IOException { reader.nextDouble(); fail(); } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 2 path $[0]"); } } @@ -458,6 +467,8 @@ public void testStrictQuotedNonFiniteDoubles() throws IOException { reader.nextDouble(); fail(); } catch (MalformedJsonException expected) { + assertThat(expected).hasMessageThat().isEqualTo("JSON forbids NaN and infinities: NaN at line 1 column 7 path $[0]" + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#malformed-json"); } } @@ -494,6 +505,7 @@ public void testStrictNonFiniteDoublesWithSkipValue() throws IOException { reader.skipValue(); fail(); } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 2 path $[0]"); } } @@ -532,8 +544,8 @@ public void testLongs() throws IOException { } @Test - @Ignore - public void disabled_testNumberWithOctalPrefix() throws IOException { + @Ignore("JsonReader advances after exception for invalid number was thrown; to be decided if that is acceptable") + public void testNumberWithOctalPrefix() throws IOException { String json = "[01]"; JsonReader reader = new JsonReader(reader(json)); reader.beginArray(); @@ -541,21 +553,25 @@ public void disabled_testNumberWithOctalPrefix() throws IOException { reader.peek(); fail(); } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 2 path $[0]"); } try { reader.nextInt(); fail(); } catch (MalformedJsonException expected) { + assertStrictError(expected, "TODO"); } try { reader.nextLong(); fail(); } catch (MalformedJsonException expected) { + assertStrictError(expected, "TODO"); } try { reader.nextDouble(); fail(); } catch (MalformedJsonException expected) { + assertStrictError(expected, "TODO"); } assertThat(reader.nextString()).isEqualTo("01"); reader.endArray(); @@ -582,6 +598,7 @@ public void testPeekingUnquotedStringsPrefixedWithBooleans() throws IOException reader.nextBoolean(); fail(); } catch (IllegalStateException expected) { + assertUnexpectedStructureError(expected, "a boolean", "STRING", "line 1 column 2 path $[0]"); } assertThat(reader.nextString()).isEqualTo("truey"); reader.endArray(); @@ -723,7 +740,7 @@ public void testNegativeZero() throws Exception { */ @Test @Ignore - public void disabled_testPeekLargerThanLongMaxValue() throws IOException { + public void testPeekLargerThanLongMaxValue() throws IOException { JsonReader reader = new JsonReader(reader("[9223372036854775808]")); reader.setLenient(true); reader.beginArray(); @@ -741,7 +758,7 @@ public void disabled_testPeekLargerThanLongMaxValue() throws IOException { */ @Test @Ignore - public void disabled_testPeekLargerThanLongMinValue() throws IOException { + public void testPeekLargerThanLongMinValue() throws IOException { @SuppressWarnings("FloatingPointLiteralPrecision") double d = -9223372036854775809d; JsonReader reader = new JsonReader(reader("[-9223372036854775809]")); @@ -762,7 +779,7 @@ public void disabled_testPeekLargerThanLongMinValue() throws IOException { */ @Test @Ignore - public void disabled_testHighPrecisionLong() throws IOException { + public void testHighPrecisionLong() throws IOException { String json = "[9223372036854775806.000]"; JsonReader reader = new JsonReader(reader(json)); reader.beginArray(); @@ -817,7 +834,9 @@ public void testMissingValue() throws IOException { try { reader.nextString(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertThat(expected).hasMessageThat().isEqualTo("Expected value at line 1 column 6 path $.a" + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#malformed-json"); } } @@ -830,38 +849,41 @@ public void testPrematureEndOfInput() throws IOException { try { reader.nextName(); fail(); - } catch (IOException expected) { + } catch (EOFException expected) { } } @Test public void testPrematurelyClosed() throws IOException { + JsonReader reader = new JsonReader(reader("{\"a\":[]}")); + reader.beginObject(); + reader.close(); try { - JsonReader reader = new JsonReader(reader("{\"a\":[]}")); - reader.beginObject(); - reader.close(); reader.nextName(); fail(); } catch (IllegalStateException expected) { + assertThat(expected).hasMessageThat().isEqualTo("JsonReader is closed"); } + reader = new JsonReader(reader("{\"a\":[]}")); + reader.close(); try { - JsonReader reader = new JsonReader(reader("{\"a\":[]}")); - reader.close(); reader.beginObject(); fail(); } catch (IllegalStateException expected) { + assertThat(expected).hasMessageThat().isEqualTo("JsonReader is closed"); } + reader = new JsonReader(reader("{\"a\":true}")); + reader.beginObject(); + String unused1 = reader.nextName(); + JsonToken unused2 = reader.peek(); + reader.close(); try { - JsonReader reader = new JsonReader(reader("{\"a\":true}")); - reader.beginObject(); - String unused1 = reader.nextName(); - JsonToken unused2 = reader.peek(); - reader.close(); reader.nextBoolean(); fail(); } catch (IllegalStateException expected) { + assertThat(expected).hasMessageThat().isEqualTo("JsonReader is closed"); } } @@ -873,53 +895,63 @@ public void testNextFailuresDoNotAdvance() throws IOException { String unused = reader.nextString(); fail(); } catch (IllegalStateException expected) { + assertUnexpectedStructureError(expected, "a string", "NAME", "line 1 column 3 path $."); } assertThat(reader.nextName()).isEqualTo("a"); try { String unused = reader.nextName(); fail(); } catch (IllegalStateException expected) { + assertUnexpectedStructureError(expected, "a name", "BOOLEAN", "line 1 column 10 path $.a"); } try { reader.beginArray(); fail(); } catch (IllegalStateException expected) { + assertUnexpectedStructureError(expected, "BEGIN_ARRAY", "BOOLEAN", "line 1 column 10 path $.a"); } try { reader.endArray(); fail(); } catch (IllegalStateException expected) { + assertUnexpectedStructureError(expected, "END_ARRAY", "BOOLEAN", "line 1 column 10 path $.a"); } try { reader.beginObject(); fail(); } catch (IllegalStateException expected) { + assertUnexpectedStructureError(expected, "BEGIN_OBJECT", "BOOLEAN", "line 1 column 10 path $.a"); } try { reader.endObject(); fail(); } catch (IllegalStateException expected) { + assertUnexpectedStructureError(expected, "END_OBJECT", "BOOLEAN", "line 1 column 10 path $.a"); } assertThat(reader.nextBoolean()).isTrue(); try { reader.nextString(); fail(); } catch (IllegalStateException expected) { + assertUnexpectedStructureError(expected, "a string", "END_OBJECT", "line 1 column 11 path $.a"); } try { reader.nextName(); fail(); } catch (IllegalStateException expected) { + assertUnexpectedStructureError(expected, "a name", "END_OBJECT", "line 1 column 11 path $.a"); } try { reader.beginArray(); fail(); } catch (IllegalStateException expected) { + assertUnexpectedStructureError(expected, "BEGIN_ARRAY", "END_OBJECT", "line 1 column 11 path $.a"); } try { reader.endArray(); fail(); } catch (IllegalStateException expected) { + assertUnexpectedStructureError(expected, "END_ARRAY", "END_OBJECT", "line 1 column 11 path $.a"); } reader.endObject(); assertThat(reader.peek()).isEqualTo(JsonToken.END_DOCUMENT); @@ -947,6 +979,7 @@ public void testStringNullIsNotNull() throws IOException { reader.nextNull(); fail(); } catch (IllegalStateException expected) { + assertUnexpectedStructureError(expected, "null", "STRING", "line 1 column 3 path $[0]"); } } @@ -958,6 +991,7 @@ public void testNullLiteralIsNotAString() throws IOException { reader.nextString(); fail(); } catch (IllegalStateException expected) { + assertUnexpectedStructureError(expected, "a string", "NULL", "line 1 column 6 path $[0]"); } } @@ -969,7 +1003,8 @@ public void testStrictNameValueSeparator() throws IOException { try { reader.nextBoolean(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 6 path $.a"); } reader = new JsonReader(reader("{\"a\"=>true}")); @@ -978,7 +1013,8 @@ public void testStrictNameValueSeparator() throws IOException { try { reader.nextBoolean(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 6 path $.a"); } } @@ -1005,7 +1041,8 @@ public void testStrictNameValueSeparatorWithSkipValue() throws IOException { try { reader.skipValue(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 6 path $.a"); } reader = new JsonReader(reader("{\"a\"=>true}")); @@ -1014,7 +1051,8 @@ public void testStrictNameValueSeparatorWithSkipValue() throws IOException { try { reader.skipValue(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 6 path $.a"); } } @@ -1045,7 +1083,8 @@ public void testStrictComments() throws IOException { try { reader.nextBoolean(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 3 path $[0]"); } reader = new JsonReader(reader("[# comment \n true]")); @@ -1053,7 +1092,8 @@ public void testStrictComments() throws IOException { try { reader.nextBoolean(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 3 path $[0]"); } reader = new JsonReader(reader("[/* comment */ true]")); @@ -1061,7 +1101,8 @@ public void testStrictComments() throws IOException { try { reader.nextBoolean(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 3 path $[0]"); } } @@ -1090,7 +1131,8 @@ public void testStrictCommentsWithSkipValue() throws IOException { try { reader.skipValue(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 3 path $[0]"); } reader = new JsonReader(reader("[# comment \n true]")); @@ -1098,7 +1140,8 @@ public void testStrictCommentsWithSkipValue() throws IOException { try { reader.skipValue(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 3 path $[0]"); } reader = new JsonReader(reader("[/* comment */ true]")); @@ -1106,7 +1149,8 @@ public void testStrictCommentsWithSkipValue() throws IOException { try { reader.skipValue(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 3 path $[0]"); } } @@ -1117,7 +1161,8 @@ public void testStrictUnquotedNames() throws IOException { try { reader.nextName(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 3 path $."); } } @@ -1136,7 +1181,8 @@ public void testStrictUnquotedNamesWithSkipValue() throws IOException { try { reader.skipValue(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 3 path $."); } } @@ -1147,7 +1193,8 @@ public void testStrictSingleQuotedNames() throws IOException { try { reader.nextName(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 3 path $."); } } @@ -1166,7 +1213,8 @@ public void testStrictSingleQuotedNamesWithSkipValue() throws IOException { try { reader.skipValue(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 3 path $."); } } @@ -1178,6 +1226,7 @@ public void testStrictUnquotedStrings() throws IOException { reader.nextString(); fail(); } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 2 path $[0]"); } } @@ -1189,6 +1238,7 @@ public void testStrictUnquotedStringsWithSkipValue() throws IOException { reader.skipValue(); fail(); } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 2 path $[0]"); } } @@ -1207,7 +1257,8 @@ public void testStrictSingleQuotedStrings() throws IOException { try { reader.nextString(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 3 path $[0]"); } } @@ -1226,7 +1277,8 @@ public void testStrictSingleQuotedStringsWithSkipValue() throws IOException { try { reader.skipValue(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 3 path $[0]"); } } @@ -1235,10 +1287,10 @@ public void testStrictSemicolonDelimitedArray() throws IOException { JsonReader reader = new JsonReader(reader("[true;true]")); reader.beginArray(); try { - boolean unused1 = reader.nextBoolean(); - boolean unused2 = reader.nextBoolean(); + boolean unused = reader.nextBoolean(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 2 path $[0]"); } } @@ -1256,10 +1308,10 @@ public void testStrictSemicolonDelimitedArrayWithSkipValue() throws IOException JsonReader reader = new JsonReader(reader("[true;true]")); reader.beginArray(); try { - reader.skipValue(); reader.skipValue(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 2 path $[0]"); } } @@ -1269,10 +1321,10 @@ public void testStrictSemicolonDelimitedNameValuePair() throws IOException { reader.beginObject(); assertThat(reader.nextName()).isEqualTo("a"); try { - boolean unused1 = reader.nextBoolean(); - String unused2 = reader.nextName(); + boolean unused = reader.nextBoolean(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 6 path $.a"); } } @@ -1292,10 +1344,10 @@ public void testStrictSemicolonDelimitedNameValuePairWithSkipValue() throws IOEx reader.beginObject(); assertThat(reader.nextName()).isEqualTo("a"); try { - reader.skipValue(); reader.skipValue(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 6 path $.a"); } } @@ -1307,7 +1359,8 @@ public void testStrictUnnecessaryArraySeparators() throws IOException { try { reader.nextNull(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 8 path $[1]"); } reader = new JsonReader(reader("[,true]")); @@ -1315,7 +1368,8 @@ public void testStrictUnnecessaryArraySeparators() throws IOException { try { reader.nextNull(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 3 path $[0]"); } reader = new JsonReader(reader("[true,]")); @@ -1324,7 +1378,8 @@ public void testStrictUnnecessaryArraySeparators() throws IOException { try { reader.nextNull(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 8 path $[1]"); } reader = new JsonReader(reader("[,]")); @@ -1332,7 +1387,8 @@ public void testStrictUnnecessaryArraySeparators() throws IOException { try { reader.nextNull(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 3 path $[0]"); } } @@ -1376,7 +1432,8 @@ public void testStrictUnnecessaryArraySeparatorsWithSkipValue() throws IOExcepti try { reader.skipValue(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 8 path $[1]"); } reader = new JsonReader(reader("[,true]")); @@ -1384,7 +1441,8 @@ public void testStrictUnnecessaryArraySeparatorsWithSkipValue() throws IOExcepti try { reader.skipValue(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 3 path $[0]"); } reader = new JsonReader(reader("[true,]")); @@ -1393,7 +1451,8 @@ public void testStrictUnnecessaryArraySeparatorsWithSkipValue() throws IOExcepti try { reader.skipValue(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 8 path $[1]"); } reader = new JsonReader(reader("[,]")); @@ -1401,7 +1460,8 @@ public void testStrictUnnecessaryArraySeparatorsWithSkipValue() throws IOExcepti try { reader.skipValue(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 3 path $[0]"); } } @@ -1413,7 +1473,8 @@ public void testStrictMultipleTopLevelValues() throws IOException { try { reader.peek(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 5 path $"); } } @@ -1437,7 +1498,8 @@ public void testStrictMultipleTopLevelValuesWithSkipValue() throws IOException { try { reader.skipValue(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 5 path $"); } } @@ -1477,22 +1539,24 @@ public void testTopLevelValueTypeWithSkipValue() throws IOException { } @Test - public void testStrictNonExecutePrefix() { + public void testStrictNonExecutePrefix() throws IOException { JsonReader reader = new JsonReader(reader(")]}'\n []")); try { reader.beginArray(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 1 path $"); } } @Test - public void testStrictNonExecutePrefixWithSkipValue() { + public void testStrictNonExecutePrefixWithSkipValue() throws IOException { JsonReader reader = new JsonReader(reader(")]}'\n []")); try { reader.skipValue(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 1 path $"); } } @@ -1515,14 +1579,16 @@ public void testLenientNonExecutePrefixWithLeadingWhitespace() throws IOExceptio } @Test - public void testLenientPartialNonExecutePrefix() { + public void testLenientPartialNonExecutePrefix() throws IOException { JsonReader reader = new JsonReader(reader(")]}' []")); reader.setLenient(true); + assertThat(reader.nextString()).isEqualTo(")"); try { - assertThat(reader.nextString()).isEqualTo(")"); reader.nextString(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertThat(expected).hasMessageThat().isEqualTo("Unexpected value at line 1 column 3 path $" + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#malformed-json"); } } @@ -1540,7 +1606,8 @@ public void testBomForbiddenAsOtherCharacterInDocument() throws IOException { try { reader.endArray(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 2 path $[0]"); } } @@ -1606,8 +1673,8 @@ private void testFailWithPosition(String message, String json) throws IOExceptio try { JsonToken unused2 = reader1.peek(); fail(); - } catch (IOException expected) { - assertThat(expected.getMessage()).isEqualTo(message); + } catch (MalformedJsonException expected) { + assertThat(expected).hasMessageThat().isEqualTo(message + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#malformed-json"); } // Also validate that it works when skipping. @@ -1618,8 +1685,8 @@ private void testFailWithPosition(String message, String json) throws IOExceptio try { JsonToken unused3 = reader2.peek(); fail(); - } catch (IOException expected) { - assertThat(expected.getMessage()).isEqualTo(message); + } catch (MalformedJsonException expected) { + assertThat(expected).hasMessageThat().isEqualTo(message + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#malformed-json"); } } @@ -1636,8 +1703,10 @@ public void testFailWithPositionDeepPath() throws IOException { try { JsonToken unused5 = reader.peek(); fail(); - } catch (IOException expected) { - assertThat(expected.getMessage()).isEqualTo("Expected value at line 1 column 14 path $[1].a[2]"); + } catch (MalformedJsonException expected) { + assertThat(expected).hasMessageThat().isEqualTo( + "Expected value at line 1 column 14 path $[1].a[2]" + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#malformed-json"); } } @@ -1646,9 +1715,10 @@ public void testStrictVeryLongNumber() throws IOException { JsonReader reader = new JsonReader(reader("[0." + repeat('9', 8192) + "]")); reader.beginArray(); try { - assertThat(reader.nextDouble()).isEqualTo(1d); + reader.nextDouble(); fail(); } catch (MalformedJsonException expected) { + assertStrictError(expected, "line 1 column 2 path $[0]"); } } @@ -1721,6 +1791,8 @@ public void testStringEndingInSlash() throws IOException { reader.peek(); fail(); } catch (MalformedJsonException expected) { + assertThat(expected).hasMessageThat().isEqualTo("Expected value at line 1 column 1 path $" + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#malformed-json"); } } @@ -1732,6 +1804,8 @@ public void testDocumentWithCommentEndingInSlash() throws IOException { reader.peek(); fail(); } catch (MalformedJsonException expected) { + assertThat(expected).hasMessageThat().isEqualTo("Expected value at line 1 column 10 path $" + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#malformed-json"); } } @@ -1743,6 +1817,8 @@ public void testStringWithLeadingSlash() throws IOException { reader.peek(); fail(); } catch (MalformedJsonException expected) { + assertThat(expected).hasMessageThat().isEqualTo("Expected value at line 1 column 1 path $" + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#malformed-json"); } } @@ -1757,6 +1833,8 @@ public void testUnterminatedObject() throws IOException { reader.peek(); fail(); } catch (MalformedJsonException expected) { + assertThat(expected).hasMessageThat().isEqualTo("Unterminated object at line 1 column 16 path $.a" + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#malformed-json"); } } @@ -1883,7 +1961,9 @@ public void testStrictExtraCommasInMaps() throws IOException { try { reader.peek(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertThat(expected).hasMessageThat().isEqualTo("Expected name at line 1 column 11 path $.a" + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#malformed-json"); } } @@ -1897,7 +1977,9 @@ public void testLenientExtraCommasInMaps() throws IOException { try { reader.peek(); fail(); - } catch (IOException expected) { + } catch (MalformedJsonException expected) { + assertThat(expected).hasMessageThat().isEqualTo("Expected name at line 1 column 11 path $.a" + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#malformed-json"); } } @@ -1909,45 +1991,45 @@ private String repeat(char c, int count) { @Test public void testMalformedDocuments() throws IOException { - assertDocument("{]", BEGIN_OBJECT, IOException.class); - assertDocument("{,", BEGIN_OBJECT, IOException.class); - assertDocument("{{", BEGIN_OBJECT, IOException.class); - assertDocument("{[", BEGIN_OBJECT, IOException.class); - assertDocument("{:", BEGIN_OBJECT, IOException.class); - assertDocument("{\"name\",", BEGIN_OBJECT, NAME, IOException.class); - assertDocument("{\"name\",", BEGIN_OBJECT, NAME, IOException.class); - assertDocument("{\"name\":}", BEGIN_OBJECT, NAME, IOException.class); - assertDocument("{\"name\"::", BEGIN_OBJECT, NAME, IOException.class); - assertDocument("{\"name\":,", BEGIN_OBJECT, NAME, IOException.class); - assertDocument("{\"name\"=}", BEGIN_OBJECT, NAME, IOException.class); - assertDocument("{\"name\"=>}", BEGIN_OBJECT, NAME, IOException.class); - assertDocument("{\"name\"=>\"string\":", BEGIN_OBJECT, NAME, STRING, IOException.class); - assertDocument("{\"name\"=>\"string\"=", BEGIN_OBJECT, NAME, STRING, IOException.class); - assertDocument("{\"name\"=>\"string\"=>", BEGIN_OBJECT, NAME, STRING, IOException.class); - assertDocument("{\"name\"=>\"string\",", BEGIN_OBJECT, NAME, STRING, IOException.class); + assertDocument("{]", BEGIN_OBJECT, MalformedJsonException.class); + assertDocument("{,", BEGIN_OBJECT, MalformedJsonException.class); + assertDocument("{{", BEGIN_OBJECT, MalformedJsonException.class); + assertDocument("{[", BEGIN_OBJECT, MalformedJsonException.class); + assertDocument("{:", BEGIN_OBJECT, MalformedJsonException.class); + assertDocument("{\"name\",", BEGIN_OBJECT, NAME, MalformedJsonException.class); + assertDocument("{\"name\",", BEGIN_OBJECT, NAME, MalformedJsonException.class); + assertDocument("{\"name\":}", BEGIN_OBJECT, NAME, MalformedJsonException.class); + assertDocument("{\"name\"::", BEGIN_OBJECT, NAME, MalformedJsonException.class); + assertDocument("{\"name\":,", BEGIN_OBJECT, NAME, MalformedJsonException.class); + assertDocument("{\"name\"=}", BEGIN_OBJECT, NAME, MalformedJsonException.class); + assertDocument("{\"name\"=>}", BEGIN_OBJECT, NAME, MalformedJsonException.class); + assertDocument("{\"name\"=>\"string\":", BEGIN_OBJECT, NAME, STRING, MalformedJsonException.class); + assertDocument("{\"name\"=>\"string\"=", BEGIN_OBJECT, NAME, STRING, MalformedJsonException.class); + assertDocument("{\"name\"=>\"string\"=>", BEGIN_OBJECT, NAME, STRING, MalformedJsonException.class); + assertDocument("{\"name\"=>\"string\",", BEGIN_OBJECT, NAME, STRING, EOFException.class); assertDocument("{\"name\"=>\"string\",\"name\"", BEGIN_OBJECT, NAME, STRING, NAME); - assertDocument("[}", BEGIN_ARRAY, IOException.class); + assertDocument("[}", BEGIN_ARRAY, MalformedJsonException.class); assertDocument("[,]", BEGIN_ARRAY, NULL, NULL, END_ARRAY); - assertDocument("{", BEGIN_OBJECT, IOException.class); - assertDocument("{\"name\"", BEGIN_OBJECT, NAME, IOException.class); - assertDocument("{\"name\",", BEGIN_OBJECT, NAME, IOException.class); - assertDocument("{'name'", BEGIN_OBJECT, NAME, IOException.class); - assertDocument("{'name',", BEGIN_OBJECT, NAME, IOException.class); - assertDocument("{name", BEGIN_OBJECT, NAME, IOException.class); - assertDocument("[", BEGIN_ARRAY, IOException.class); - assertDocument("[string", BEGIN_ARRAY, STRING, IOException.class); - assertDocument("[\"string\"", BEGIN_ARRAY, STRING, IOException.class); - assertDocument("['string'", BEGIN_ARRAY, STRING, IOException.class); - assertDocument("[123", BEGIN_ARRAY, NUMBER, IOException.class); - assertDocument("[123,", BEGIN_ARRAY, NUMBER, IOException.class); - assertDocument("{\"name\":123", BEGIN_OBJECT, NAME, NUMBER, IOException.class); - assertDocument("{\"name\":123,", BEGIN_OBJECT, NAME, NUMBER, IOException.class); - assertDocument("{\"name\":\"string\"", BEGIN_OBJECT, NAME, STRING, IOException.class); - assertDocument("{\"name\":\"string\",", BEGIN_OBJECT, NAME, STRING, IOException.class); - assertDocument("{\"name\":'string'", BEGIN_OBJECT, NAME, STRING, IOException.class); - assertDocument("{\"name\":'string',", BEGIN_OBJECT, NAME, STRING, IOException.class); - assertDocument("{\"name\":false", BEGIN_OBJECT, NAME, BOOLEAN, IOException.class); - assertDocument("{\"name\":false,,", BEGIN_OBJECT, NAME, BOOLEAN, IOException.class); + assertDocument("{", BEGIN_OBJECT, EOFException.class); + assertDocument("{\"name\"", BEGIN_OBJECT, NAME, EOFException.class); + assertDocument("{\"name\",", BEGIN_OBJECT, NAME, MalformedJsonException.class); + assertDocument("{'name'", BEGIN_OBJECT, NAME, EOFException.class); + assertDocument("{'name',", BEGIN_OBJECT, NAME, MalformedJsonException.class); + assertDocument("{name", BEGIN_OBJECT, NAME, EOFException.class); + assertDocument("[", BEGIN_ARRAY, EOFException.class); + assertDocument("[string", BEGIN_ARRAY, STRING, EOFException.class); + assertDocument("[\"string\"", BEGIN_ARRAY, STRING, EOFException.class); + assertDocument("['string'", BEGIN_ARRAY, STRING, EOFException.class); + assertDocument("[123", BEGIN_ARRAY, NUMBER, EOFException.class); + assertDocument("[123,", BEGIN_ARRAY, NUMBER, EOFException.class); + assertDocument("{\"name\":123", BEGIN_OBJECT, NAME, NUMBER, EOFException.class); + assertDocument("{\"name\":123,", BEGIN_OBJECT, NAME, NUMBER, EOFException.class); + assertDocument("{\"name\":\"string\"", BEGIN_OBJECT, NAME, STRING, EOFException.class); + assertDocument("{\"name\":\"string\",", BEGIN_OBJECT, NAME, STRING, EOFException.class); + assertDocument("{\"name\":'string'", BEGIN_OBJECT, NAME, STRING, EOFException.class); + assertDocument("{\"name\":'string',", BEGIN_OBJECT, NAME, STRING, EOFException.class); + assertDocument("{\"name\":false", BEGIN_OBJECT, NAME, BOOLEAN, EOFException.class); + assertDocument("{\"name\":false,,", BEGIN_OBJECT, NAME, BOOLEAN, MalformedJsonException.class); } /** @@ -1964,6 +2046,8 @@ public void testUnterminatedStringFailure() throws IOException { reader.nextString(); fail(); } catch (MalformedJsonException expected) { + assertThat(expected).hasMessageThat().isEqualTo("Unterminated string at line 1 column 9 path $[0]" + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#malformed-json"); } } @@ -1983,6 +2067,17 @@ public void testReadAcrossBuffers() throws IOException { assertThat(token).isEqualTo(JsonToken.NUMBER); } + private static void assertStrictError(MalformedJsonException exception, String expectedLocation) { + assertThat(exception).hasMessageThat().isEqualTo("Use JsonReader.setLenient(true) to accept malformed JSON at " + expectedLocation + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#malformed-json"); + } + + private static void assertUnexpectedStructureError(IllegalStateException exception, String expectedToken, String actualToken, String expectedLocation) { + String troubleshootingId = actualToken.equals("NULL") ? "adapter-not-null-safe" : "unexpected-json-structure"; + assertThat(exception).hasMessageThat().isEqualTo("Expected " + expectedToken + " but was " + actualToken + " at " + expectedLocation + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#" + troubleshootingId); + } + private void assertDocument(String document, Object... expectations) throws IOException { JsonReader reader = new JsonReader(reader(document)); reader.setLenient(true); @@ -2005,15 +2100,15 @@ private void assertDocument(String document, Object... expectations) throws IOEx assertThat(reader.nextInt()).isEqualTo(123); } else if (expectation == NULL) { reader.nextNull(); - } else if (expectation == IOException.class) { + } else if (expectation instanceof Class && Exception.class.isAssignableFrom((Class) expectation)) { try { reader.peek(); fail(); - } catch (IOException expected) { - // OK: Should fail + } catch (Exception expected) { + assertThat(expected.getClass()).isEqualTo((Class) expectation); } } else { - throw new AssertionError(); + throw new AssertionError("Unsupported expectation value: " + expectation); } } } 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 70470a166b..2ee120f38e 100644 --- a/gson/src/test/java/com/google/gson/stream/JsonWriterTest.java +++ b/gson/src/test/java/com/google/gson/stream/JsonWriterTest.java @@ -472,7 +472,7 @@ public void testMalformedNumbers() throws IOException { jsonWriter.value(new LazilyParsedNumber(malformedNumber)); fail("Should have failed writing malformed number: " + malformedNumber); } catch (IllegalArgumentException e) { - assertThat(e.getMessage()).isEqualTo("String created by class com.google.gson.internal.LazilyParsedNumber is not a valid JSON number: " + malformedNumber); + assertThat(e).hasMessageThat().isEqualTo("String created by class com.google.gson.internal.LazilyParsedNumber is not a valid JSON number: " + malformedNumber); } } } From 55cf9fa941e93030244ecc9092f65183c16fc745 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Apr 2023 07:27:14 -0700 Subject: [PATCH 21/36] Bump jackson-databind from 2.14.2 to 2.15.0 (#2377) Bumps [jackson-databind](https://github.com/FasterXML/jackson) from 2.14.2 to 2.15.0. - [Release notes](https://github.com/FasterXML/jackson/releases) - [Commits](https://github.com/FasterXML/jackson/commits) --- updated-dependencies: - dependency-name: com.fasterxml.jackson.core:jackson-databind dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- metrics/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metrics/pom.xml b/metrics/pom.xml index b5b708c636..b2fc7b5e6d 100644 --- a/metrics/pom.xml +++ b/metrics/pom.xml @@ -48,7 +48,7 @@ com.fasterxml.jackson.core jackson-databind - 2.14.2 + 2.15.0 com.google.caliper From 8333ccac62eb586f437305d66f4b6438a3529c52 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 May 2023 06:23:28 -0700 Subject: [PATCH 22/36] Bump moditect-maven-plugin from 1.0.0.RC3 to 1.0.0.Final (#2381) Bumps [moditect-maven-plugin](https://github.com/moditect/moditect) from 1.0.0.RC3 to 1.0.0.Final. - [Release notes](https://github.com/moditect/moditect/releases) - [Commits](https://github.com/moditect/moditect/compare/1.0.0.RC3...1.0.0.Final) --- updated-dependencies: - dependency-name: org.moditect:moditect-maven-plugin dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- gson/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gson/pom.xml b/gson/pom.xml index afd6ac7183..581e19d561 100644 --- a/gson/pom.xml +++ b/gson/pom.xml @@ -246,7 +246,7 @@ org.moditect moditect-maven-plugin - 1.0.0.RC3 + 1.0.0.Final add-module-info From 5fffd5aca1094d045e978bd143032500934395f4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 May 2023 12:11:43 -0700 Subject: [PATCH 23/36] Bump maven-gpg-plugin from 3.0.1 to 3.1.0 (#2384) Bumps [maven-gpg-plugin](https://github.com/apache/maven-gpg-plugin) from 3.0.1 to 3.1.0. - [Commits](https://github.com/apache/maven-gpg-plugin/compare/maven-gpg-plugin-3.0.1...maven-gpg-plugin-3.1.0) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-gpg-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index a57ca468d1..5c5a789402 100644 --- a/pom.xml +++ b/pom.xml @@ -187,7 +187,7 @@ org.apache.maven.plugins maven-gpg-plugin - 3.0.1 + 3.1.0 org.apache.maven.plugins From 1339b506298cb854877f5d39bea1affc8162146c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 May 2023 12:17:29 -0700 Subject: [PATCH 24/36] Bump maven-surefire-plugin from 3.0.0 to 3.1.0 (#2383) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [maven-surefire-plugin](https://github.com/apache/maven-surefire) from 3.0.0 to 3.1.0. - [Release notes](https://github.com/apache/maven-surefire/releases) - [Commits](https://github.com/apache/maven-surefire/compare/surefire-3.0.0...surefire-3.1.0) --- updated-dependencies: - dependency-name: org.apache.maven.plugins:maven-surefire-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Éamonn McManus --- gson/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gson/pom.xml b/gson/pom.xml index 581e19d561..2930ac46ef 100644 --- a/gson/pom.xml +++ b/gson/pom.xml @@ -136,7 +136,7 @@ org.apache.maven.plugins maven-surefire-plugin - 3.0.0 + 3.1.0 -XDcompilePolicy=simple - -Xplugin:ErrorProne -XepExcludedPaths:.*/generated-test-sources/protobuf/.* + -Xplugin:ErrorProne + -XepExcludedPaths:.*/generated-test-sources/protobuf/.* + -Xep:HidingField:OFF + -Xep:NotJavadoc:OFF + -Xlint:all,-options @@ -141,7 +145,7 @@ com.google.errorprone error_prone_core - 2.18.0 + 2.19.0 From eae63f92fb42e603bddd22f238a2a82a61140ce2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 May 2023 09:43:22 -0700 Subject: [PATCH 27/36] Bump error_prone_annotations from 2.19.0 to 2.19.1 (#2389) Bumps [error_prone_annotations](https://github.com/google/error-prone) from 2.19.0 to 2.19.1. - [Release notes](https://github.com/google/error-prone/releases) - [Commits](https://github.com/google/error-prone/compare/v2.19.0...v2.19.1) --- updated-dependencies: - dependency-name: com.google.errorprone:error_prone_annotations dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- gson/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gson/pom.xml b/gson/pom.xml index c21bfeb80c..abbe573dfd 100644 --- a/gson/pom.xml +++ b/gson/pom.xml @@ -46,7 +46,7 @@ com.google.errorprone error_prone_annotations - 2.19.0 + 2.19.1 From 3c4b0721005dbc5e0d7515b95a321715d48b3291 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 May 2023 09:43:50 -0700 Subject: [PATCH 28/36] Bump error_prone_core from 2.19.0 to 2.19.1 (#2388) Bumps [error_prone_core](https://github.com/google/error-prone) from 2.19.0 to 2.19.1. - [Release notes](https://github.com/google/error-prone/releases) - [Commits](https://github.com/google/error-prone/compare/v2.19.0...v2.19.1) --- updated-dependencies: - dependency-name: com.google.errorprone:error_prone_core dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 0607d2dc1c..c236153da7 100644 --- a/pom.xml +++ b/pom.xml @@ -145,7 +145,7 @@ com.google.errorprone error_prone_core - 2.19.0 + 2.19.1
From a9bd30b57796b4490d9f3eb4b59edea49fb327a3 Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Thu, 11 May 2023 22:18:09 +0200 Subject: [PATCH 29/36] Enable Error Prone HidingField bug pattern again (#2390) Bug which required disabling this bug pattern was fixed in 2.19.1 and Error Prone dependency was updated to that version in commit 3c4b0721005dbc5e0d7515b95a321715d48b3291 --- pom.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/pom.xml b/pom.xml index c236153da7..a3ca02886a 100644 --- a/pom.xml +++ b/pom.xml @@ -134,7 +134,6 @@ -XDcompilePolicy=simple -Xplugin:ErrorProne -XepExcludedPaths:.*/generated-test-sources/protobuf/.* - -Xep:HidingField:OFF -Xep:NotJavadoc:OFF true diff --git a/pom.xml b/pom.xml index 26e8022abe..55b17e8068 100644 --- a/pom.xml +++ b/pom.xml @@ -29,6 +29,7 @@ gson + shrinker-test extras metrics proto @@ -83,7 +84,12 @@ junit junit 4.13.2 - test +
+ + + com.google.truth + truth + 1.1.3
diff --git a/proto/pom.xml b/proto/pom.xml index 9225ff2239..313bb10613 100644 --- a/proto/pom.xml +++ b/proto/pom.xml @@ -62,7 +62,6 @@ com.google.truth truth - 1.1.3 test diff --git a/shrinker-test/README.md b/shrinker-test/README.md new file mode 100644 index 0000000000..f9b674d143 --- /dev/null +++ b/shrinker-test/README.md @@ -0,0 +1,9 @@ +# shrinker-test + +This Maven module contains integration tests which check the behavior of Gson when used in combination with code shrinking and obfuscation tools, such as ProGuard or R8. + +The code which is shrunken is under `src/main/java`; it should not contain any important assertions in case the code shrinking tools affect these assertions in any way. The test code under `src/test/java` executes the shrunken and obfuscated JAR and verifies that it behaves as expected. + +The tests might be a bit brittle, especially the R8 test setup. Future ProGuard and R8 versions might cause the tests to behave differently. In case tests fail the ProGuard and R8 mapping files created in the `target` directory can help with debugging. If necessary rewrite tests or even remove them if they cannot be implemented anymore for newer ProGuard or R8 versions. + +**Important:** Because execution of the code shrinking tools is performed during the Maven build, trying to directly run the integration tests from the IDE might not work, or might use stale results if you changed the configuration in between. Run `mvn clean verify` before trying to run the integration tests from the IDE. diff --git a/shrinker-test/pom.xml b/shrinker-test/pom.xml new file mode 100644 index 0000000000..bd6f789ac6 --- /dev/null +++ b/shrinker-test/pom.xml @@ -0,0 +1,218 @@ + + + + 4.0.0 + + + com.google.code.gson + gson-parent + 2.10.2-SNAPSHOT + + + shrinker-test + + + 8 + + + + + + google + https://maven.google.com + + + + + + com.google.code.gson + gson + ${project.parent.version} + + + + junit + junit + test + + + com.google.truth + truth + test + + + + + + + + com.github.siom79.japicmp + japicmp-maven-plugin + + + true + + + + org.apache.maven.plugins + maven-deploy-plugin + + + true + + + + + + + + + com.github.wvengen + proguard-maven-plugin + 2.6.0 + + + package + + proguard + + + + + true + ${project.basedir}/proguard.pro + + + + + + ${java.home}/jmods/java.base.jmod + + ${java.home}/jmods/java.sql.jmod + + ${java.home}/jmods/java.compiler.jmod + + + true + proguard-output.jar + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.4.1 + + + package + + shade + + + false + + false + + + *:* + + + META-INF/MANIFEST.MF + + + + + + + + + + + org.codehaus.mojo + exec-maven-plugin + 3.1.0 + + + r8 + package + + java + + + + false + false + + true + + + com.android.tools + r8 + + + + com.android.tools.r8.R8 + + --release + + --classfile + --lib${java.home} + --pg-conf${project.basedir}/r8.pro + + --pg-map-output${project.build.directory}/r8_map.txt + --output${project.build.directory}/r8-output.jar + ${project.build.directory}/${project.build.finalName}.jar + + + + + + + + com.android.tools + r8 + 8.0.40 + + + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + 3.1.0 + + + + integration-test + verify + + + + + + + diff --git a/shrinker-test/proguard.pro b/shrinker-test/proguard.pro new file mode 100644 index 0000000000..0cb35048c9 --- /dev/null +++ b/shrinker-test/proguard.pro @@ -0,0 +1,29 @@ +### Common rules for ProGuard and R8 +### Should only contains rules needed specifically for the integration test; +### any general rules which are relevant for all users should not be here but in `META-INF/proguard` of Gson + +-allowaccessmodification + +# On Windows mixed case class names might cause problems +-dontusemixedcaseclassnames + +# Ignore notes about duplicate JDK classes +-dontnote module-info,jdk.internal.** + + +# Keep test entrypoints +-keep class com.example.Main { + public static void runTests(...); +} +-keep class com.example.DefaultConstructorMain { + public static java.lang.String runTest(); + public static java.lang.String runTestNoJdkUnsafe(); +} + + +### Test data setup + +# Keep fields without annotations which should be preserved +-keepclassmembers class com.example.ClassWithNamedFields { + !transient ; +} diff --git a/shrinker-test/r8.pro b/shrinker-test/r8.pro new file mode 100644 index 0000000000..a415aa1614 --- /dev/null +++ b/shrinker-test/r8.pro @@ -0,0 +1,36 @@ +# Extend the ProGuard rules +-include proguard.pro + +### The following rules are needed for R8 in "full mode", which performs more aggressive optimizations than ProGuard +### See https://r8.googlesource.com/r8/+/refs/heads/master/compatibility-faq.md#r8-full-mode + +# Keep the no-args constructor of deserialized classes +-keepclassmembers class com.example.ClassWithDefaultConstructor { + (); +} +-keepclassmembers class com.example.GenericClasses$GenericClass { + (); +} +-keepclassmembers class com.example.GenericClasses$UsingGenericClass { + (); +} +-keepclassmembers class com.example.GenericClasses$GenericUsingGenericClass { + (); +} + +# For classes with generic type parameter R8 in "full mode" requires to have a keep rule to +# preserve the generic signature +-keep,allowshrinking,allowoptimization,allowobfuscation,allowaccessmodification class com.example.GenericClasses$GenericClass +-keep,allowshrinking,allowoptimization,allowobfuscation,allowaccessmodification class com.example.GenericClasses$GenericUsingGenericClass + +# Don't obfuscate class name, to check it in exception message +-keep,allowshrinking,allowoptimization class com.example.DefaultConstructorMain$TestClass +# This rule has the side-effect that R8 still removes the no-args constructor, but does not make the class abstract +-keep class com.example.DefaultConstructorMain$TestClassNotAbstract { + @com.google.gson.annotations.SerializedName ; +} + +# Keep enum constants which are not explicitly used in code +-keep class com.example.EnumClass { + ** SECOND; +} diff --git a/shrinker-test/src/main/java/com/example/ClassWithAdapter.java b/shrinker-test/src/main/java/com/example/ClassWithAdapter.java new file mode 100644 index 0000000000..aa7f08da6b --- /dev/null +++ b/shrinker-test/src/main/java/com/example/ClassWithAdapter.java @@ -0,0 +1,44 @@ +package com.example; + +import com.google.gson.TypeAdapter; +import com.google.gson.annotations.JsonAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; + +@JsonAdapter(ClassWithAdapter.Adapter.class) +public class ClassWithAdapter { + static class Adapter extends TypeAdapter { + @Override + public ClassWithAdapter read(JsonReader in) throws IOException { + in.beginObject(); + String name = in.nextName(); + if (!name.equals("custom")) { + throw new IllegalArgumentException("Unexpected name: " + name); + } + int i = in.nextInt(); + in.endObject(); + + return new ClassWithAdapter(i); + } + + @Override + public void write(JsonWriter out, ClassWithAdapter value) throws IOException { + out.beginObject(); + out.name("custom"); + out.value(value.i); + out.endObject(); + } + } + + public Integer i; + + public ClassWithAdapter(int i) { + this.i = i; + } + + @Override + public String toString() { + return "ClassWithAdapter[" + i + "]"; + } +} diff --git a/shrinker-test/src/main/java/com/example/ClassWithDefaultConstructor.java b/shrinker-test/src/main/java/com/example/ClassWithDefaultConstructor.java new file mode 100644 index 0000000000..6296237f66 --- /dev/null +++ b/shrinker-test/src/main/java/com/example/ClassWithDefaultConstructor.java @@ -0,0 +1,12 @@ +package com.example; + +import com.google.gson.annotations.SerializedName; + +public class ClassWithDefaultConstructor { + @SerializedName("myField") + public int i; + + public ClassWithDefaultConstructor() { + i = -3; + } +} diff --git a/shrinker-test/src/main/java/com/example/ClassWithExposeAnnotation.java b/shrinker-test/src/main/java/com/example/ClassWithExposeAnnotation.java new file mode 100644 index 0000000000..30a61fa921 --- /dev/null +++ b/shrinker-test/src/main/java/com/example/ClassWithExposeAnnotation.java @@ -0,0 +1,13 @@ +package com.example; + +import com.google.gson.annotations.Expose; + +/** + * Uses {@link Expose} annotation. + */ +public class ClassWithExposeAnnotation { + @Expose + int i; + + int i2; +} diff --git a/shrinker-test/src/main/java/com/example/ClassWithJsonAdapterAnnotation.java b/shrinker-test/src/main/java/com/example/ClassWithJsonAdapterAnnotation.java new file mode 100644 index 0000000000..238ee1818e --- /dev/null +++ b/shrinker-test/src/main/java/com/example/ClassWithJsonAdapterAnnotation.java @@ -0,0 +1,126 @@ +package com.example; + +import com.google.gson.Gson; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.annotations.JsonAdapter; +import com.google.gson.annotations.SerializedName; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import java.lang.reflect.Type; + +/** + * Uses {@link JsonAdapter} annotation on fields. + */ +public class ClassWithJsonAdapterAnnotation { + // For this field don't use @SerializedName and ignore it for deserialization + @JsonAdapter(value = Adapter.class, nullSafe = false) + DummyClass f; + + @SerializedName("f1") + @JsonAdapter(Adapter.class) + DummyClass f1; + + @SerializedName("f2") + @JsonAdapter(Factory.class) + DummyClass f2; + + @SerializedName("f3") + @JsonAdapter(Serializer.class) + DummyClass f3; + + @SerializedName("f4") + @JsonAdapter(Deserializer.class) + DummyClass f4; + + public ClassWithJsonAdapterAnnotation() { + } + + // Note: R8 seems to make this constructor the no-args constructor and initialize fields + // by default; currently this is not visible in the deserialization test because the JSON data + // contains values for all fields; but it is noticeable once the JSON data is missing fields + public ClassWithJsonAdapterAnnotation(int i1, int i2, int i3, int i4) { + f1 = new DummyClass(Integer.toString(i1)); + f2 = new DummyClass(Integer.toString(i2)); + f3 = new DummyClass(Integer.toString(i3)); + f4 = new DummyClass(Integer.toString(i4)); + + // Note: Deliberately don't initialize field `f` here to not refer to it anywhere in code + } + + @Override + public String toString() { + return "ClassWithJsonAdapterAnnotation[f1=" + f1 + ", f2=" + f2 + ", f3=" + f3 + ", f4=" + f4 + "]"; + } + + static class Adapter extends TypeAdapter { + @Override + public DummyClass read(JsonReader in) throws IOException { + return new DummyClass("adapter-" + in.nextInt()); + } + + @Override + public void write(JsonWriter out, DummyClass value) throws IOException { + out.value("adapter-" + value); + } + } + + static class Factory implements TypeAdapterFactory { + @Override + public TypeAdapter create(Gson gson, TypeToken type) { + @SuppressWarnings("unchecked") // the code below is not type safe, but does not matter for this test + TypeAdapter r = (TypeAdapter) new TypeAdapter() { + @Override + public DummyClass read(JsonReader in) throws IOException { + return new DummyClass("factory-" + in.nextInt()); + } + + @Override + public void write(JsonWriter out, DummyClass value) throws IOException { + out.value("factory-" + value.s); + } + }; + + return r; + } + } + + static class Serializer implements JsonSerializer { + @Override + public JsonElement serialize(DummyClass src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive("serializer-" + src.s); + } + } + + static class Deserializer implements JsonDeserializer { + @Override + public DummyClass deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + return new DummyClass("deserializer-" + json.getAsInt()); + } + } + + // Use this separate class mainly to work around incorrect delegation behavior for JsonSerializer + // and JsonDeserializer used with @JsonAdapter, see https://github.com/google/gson/issues/1783 + static class DummyClass { + @SerializedName("s") + String s; + + DummyClass(String s) { + this.s = s; + } + + @Override + public String toString() { + return s; + } + } +} diff --git a/shrinker-test/src/main/java/com/example/ClassWithNamedFields.java b/shrinker-test/src/main/java/com/example/ClassWithNamedFields.java new file mode 100644 index 0000000000..0a68da9c25 --- /dev/null +++ b/shrinker-test/src/main/java/com/example/ClassWithNamedFields.java @@ -0,0 +1,10 @@ +package com.example; + +public class ClassWithNamedFields { + public int myField; + public short notAccessedField = -1; + + public ClassWithNamedFields(int i) { + myField = i; + } +} diff --git a/shrinker-test/src/main/java/com/example/ClassWithSerializedName.java b/shrinker-test/src/main/java/com/example/ClassWithSerializedName.java new file mode 100644 index 0000000000..ce982215ca --- /dev/null +++ b/shrinker-test/src/main/java/com/example/ClassWithSerializedName.java @@ -0,0 +1,15 @@ +package com.example; + +import com.google.gson.annotations.SerializedName; + +public class ClassWithSerializedName { + @SerializedName("myField") + public int i; + + @SerializedName("notAccessed") + public short notAccessedField = -1; + + public ClassWithSerializedName(int i) { + this.i = i; + } +} diff --git a/shrinker-test/src/main/java/com/example/ClassWithVersionAnnotations.java b/shrinker-test/src/main/java/com/example/ClassWithVersionAnnotations.java new file mode 100644 index 0000000000..9c554f7e9b --- /dev/null +++ b/shrinker-test/src/main/java/com/example/ClassWithVersionAnnotations.java @@ -0,0 +1,21 @@ +package com.example; + +import com.google.gson.annotations.Since; +import com.google.gson.annotations.Until; + +/** + * Uses {@link Since} and {@link Until} annotations. + */ +public class ClassWithVersionAnnotations { + @Since(1) + int i1; + + @Until(1) // will be ignored with GsonBuilder.setVersion(1) + int i2; + + @Since(2) // will be ignored with GsonBuilder.setVersion(1) + int i3; + + @Until(2) + int i4; +} diff --git a/shrinker-test/src/main/java/com/example/DefaultConstructorMain.java b/shrinker-test/src/main/java/com/example/DefaultConstructorMain.java new file mode 100644 index 0000000000..e570866bec --- /dev/null +++ b/shrinker-test/src/main/java/com/example/DefaultConstructorMain.java @@ -0,0 +1,37 @@ +package com.example; + +import static com.example.TestExecutor.same; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.annotations.SerializedName; + +public class DefaultConstructorMain { + static class TestClass { + @SerializedName("s") + public String s; + } + + // R8 rule for this class still removes no-args constructor, but doesn't make class abstract + static class TestClassNotAbstract { + @SerializedName("s") + public String s; + } + + /** + * Main entrypoint, called by {@code ShrinkingIT.testDefaultConstructor()}. + */ + public static String runTest() { + TestClass deserialized = new Gson().fromJson("{\"s\":\"value\"}", same(TestClass.class)); + return deserialized.s; + } + + /** + * Main entrypoint, called by {@code ShrinkingIT.testDefaultConstructorNoJdkUnsafe()}. + */ + public static String runTestNoJdkUnsafe() { + Gson gson = new GsonBuilder().disableJdkUnsafe().create(); + TestClassNotAbstract deserialized = gson.fromJson("{\"s\": \"value\"}", same(TestClassNotAbstract.class)); + return deserialized.s; + } +} diff --git a/shrinker-test/src/main/java/com/example/EnumClass.java b/shrinker-test/src/main/java/com/example/EnumClass.java new file mode 100644 index 0000000000..36688887bb --- /dev/null +++ b/shrinker-test/src/main/java/com/example/EnumClass.java @@ -0,0 +1,6 @@ +package com.example; + +public enum EnumClass { + FIRST, + SECOND +} diff --git a/shrinker-test/src/main/java/com/example/EnumClassWithSerializedName.java b/shrinker-test/src/main/java/com/example/EnumClassWithSerializedName.java new file mode 100644 index 0000000000..a127a8be13 --- /dev/null +++ b/shrinker-test/src/main/java/com/example/EnumClassWithSerializedName.java @@ -0,0 +1,10 @@ +package com.example; + +import com.google.gson.annotations.SerializedName; + +public enum EnumClassWithSerializedName { + @SerializedName("one") + FIRST, + @SerializedName("two") + SECOND +} diff --git a/shrinker-test/src/main/java/com/example/GenericClasses.java b/shrinker-test/src/main/java/com/example/GenericClasses.java new file mode 100644 index 0000000000..cd91149be4 --- /dev/null +++ b/shrinker-test/src/main/java/com/example/GenericClasses.java @@ -0,0 +1,66 @@ +package com.example; + +import com.google.gson.TypeAdapter; +import com.google.gson.annotations.JsonAdapter; +import com.google.gson.annotations.SerializedName; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; + +public class GenericClasses { + static class GenericClass { + @SerializedName("t") + T t; + + @Override + public String toString() { + return "{t=" + t + "}"; + } + } + + static class UsingGenericClass { + @SerializedName("g") + GenericClass g; + + @Override + public String toString() { + return "{g=" + g + "}"; + } + } + + static class GenericUsingGenericClass { + @SerializedName("g") + GenericClass g; + + @Override + public String toString() { + return "{g=" + g + "}"; + } + } + + @JsonAdapter(DummyClass.Adapter.class) + static class DummyClass { + String s; + + DummyClass(String s) { + this.s = s; + } + + @Override + public String toString() { + return s; + } + + static class Adapter extends TypeAdapter { + @Override + public DummyClass read(JsonReader in) throws IOException { + return new DummyClass("read-" + in.nextInt()); + } + + @Override + public void write(JsonWriter out, DummyClass value) throws IOException { + throw new UnsupportedOperationException(); + } + } + } +} diff --git a/shrinker-test/src/main/java/com/example/Main.java b/shrinker-test/src/main/java/com/example/Main.java new file mode 100644 index 0000000000..55bbb6377d --- /dev/null +++ b/shrinker-test/src/main/java/com/example/Main.java @@ -0,0 +1,142 @@ +package com.example; + +import static com.example.TestExecutor.same; + +import com.example.GenericClasses.DummyClass; +import com.example.GenericClasses.GenericClass; +import com.example.GenericClasses.GenericUsingGenericClass; +import com.example.GenericClasses.UsingGenericClass; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import java.util.Arrays; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Supplier; + +public class Main { + /** + * Main entrypoint, called by {@code ShrinkingIT.test()}. + * + *

To be safe let all tests put their output to the consumer and let integration test verify it; + * don't perform any relevant assertions in this code because code shrinkers could affect it. + * + * @param outputConsumer consumes the test output: {@code name, content} pairs + */ + public static void runTests(BiConsumer outputConsumer) { + // Create the TypeToken instances on demand because creation of them can fail when + // generic signatures were erased + testTypeTokenWriteRead(outputConsumer, "anonymous", () -> new TypeToken>() {}); + testTypeTokenWriteRead(outputConsumer, "manual", () -> TypeToken.getParameterized(List.class, ClassWithAdapter.class)); + + testNamedFields(outputConsumer); + testSerializedName(outputConsumer); + + testNoJdkUnsafe(outputConsumer); + + testEnum(outputConsumer); + testEnumSerializedName(outputConsumer); + + testExposeAnnotation(outputConsumer); + testVersionAnnotations(outputConsumer); + testJsonAdapterAnnotation(outputConsumer); + + testGenericClasses(outputConsumer); + } + + private static void testTypeTokenWriteRead(BiConsumer outputConsumer, String description, Supplier> typeTokenSupplier) { + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + + TestExecutor.run(outputConsumer, "Write: TypeToken " + description, + () -> gson.toJson(Arrays.asList(new ClassWithAdapter(1)), typeTokenSupplier.get().getType())); + TestExecutor.run(outputConsumer, "Read: TypeToken " + description, () -> { + Object deserialized = gson.fromJson("[{\"custom\": 3}]", typeTokenSupplier.get()); + return deserialized.toString(); + }); + } + + /** + * Calls {@link Gson#toJson}, but (hopefully) in a way which prevents code shrinkers + * from understanding that reflection is used for {@code obj}. + */ + private static String toJson(Gson gson, Object obj) { + return gson.toJson(same(obj)); + } + + /** + * Calls {@link Gson#fromJson}, but (hopefully) in a way which prevents code shrinkers + * from understanding that reflection is used for {@code c}. + */ + private static T fromJson(Gson gson, String json, Class c) { + return gson.fromJson(json, same(c)); + } + + private static void testNamedFields(BiConsumer outputConsumer) { + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + TestExecutor.run(outputConsumer, "Write: Named fields", () -> toJson(gson, new ClassWithNamedFields(2))); + TestExecutor.run(outputConsumer, "Read: Named fields", () -> { + ClassWithNamedFields deserialized = fromJson(gson, "{\"myField\": 3}", ClassWithNamedFields.class); + return Integer.toString(deserialized.myField); + }); + } + + private static void testSerializedName(BiConsumer outputConsumer) { + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + TestExecutor.run(outputConsumer, "Write: SerializedName", () -> toJson(gson, new ClassWithSerializedName(2))); + TestExecutor.run(outputConsumer, "Read: SerializedName", () -> { + ClassWithSerializedName deserialized = fromJson(gson, "{\"myField\": 3}", ClassWithSerializedName.class); + return Integer.toString(deserialized.i); + }); + } + + private static void testNoJdkUnsafe(BiConsumer outputConsumer) { + Gson gson = new GsonBuilder().disableJdkUnsafe().create(); + TestExecutor.run(outputConsumer, "Read: No JDK Unsafe; initial constructor value", () -> { + ClassWithDefaultConstructor deserialized = fromJson(gson, "{}", ClassWithDefaultConstructor.class); + return Integer.toString(deserialized.i); + }); + TestExecutor.run(outputConsumer, "Read: No JDK Unsafe; custom value", () -> { + ClassWithDefaultConstructor deserialized = fromJson(gson, "{\"myField\": 3}", ClassWithDefaultConstructor.class); + return Integer.toString(deserialized.i); + }); + } + + private static void testEnum(BiConsumer outputConsumer) { + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + TestExecutor.run(outputConsumer, "Write: Enum", () -> toJson(gson, EnumClass.FIRST)); + TestExecutor.run(outputConsumer, "Read: Enum", () -> fromJson(gson, "\"SECOND\"", EnumClass.class).toString()); + } + + private static void testEnumSerializedName(BiConsumer outputConsumer) { + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + TestExecutor.run(outputConsumer, "Write: Enum SerializedName", + () -> toJson(gson, EnumClassWithSerializedName.FIRST)); + TestExecutor.run(outputConsumer, "Read: Enum SerializedName", + () -> fromJson(gson, "\"two\"", EnumClassWithSerializedName.class).toString()); + } + + private static void testExposeAnnotation(BiConsumer outputConsumer) { + Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create(); + TestExecutor.run(outputConsumer, "Write: @Expose", () -> toJson(gson, new ClassWithExposeAnnotation())); + } + + private static void testVersionAnnotations(BiConsumer outputConsumer) { + Gson gson = new GsonBuilder().setVersion(1).create(); + TestExecutor.run(outputConsumer, "Write: Version annotations", () -> toJson(gson, new ClassWithVersionAnnotations())); + } + + private static void testJsonAdapterAnnotation(BiConsumer outputConsumer) { + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + TestExecutor.run(outputConsumer, "Write: JsonAdapter on fields", () -> toJson(gson, new ClassWithJsonAdapterAnnotation(1, 2, 3, 4))); + + String json = "{\"f1\": 1, \"f2\": 2, \"f3\": {\"s\": \"3\"}, \"f4\": 4}"; + TestExecutor.run(outputConsumer, "Read: JsonAdapter on fields", () -> fromJson(gson, json, ClassWithJsonAdapterAnnotation.class).toString()); + } + + private static void testGenericClasses(BiConsumer outputConsumer) { + Gson gson = new Gson(); + TestExecutor.run(outputConsumer, "Read: Generic TypeToken", () -> gson.fromJson("{\"t\": 1}", new TypeToken>() {}).toString()); + TestExecutor.run(outputConsumer, "Read: Using Generic", () -> fromJson(gson, "{\"g\": {\"t\": 1}}", UsingGenericClass.class).toString()); + TestExecutor.run(outputConsumer, "Read: Using Generic TypeToken", () -> gson.fromJson("{\"g\": {\"t\": 1}}", new TypeToken>() {}).toString()); + } +} diff --git a/shrinker-test/src/main/java/com/example/TestExecutor.java b/shrinker-test/src/main/java/com/example/TestExecutor.java new file mode 100644 index 0000000000..6ea9b9b927 --- /dev/null +++ b/shrinker-test/src/main/java/com/example/TestExecutor.java @@ -0,0 +1,34 @@ +package com.example; + +import java.util.Optional; +import java.util.function.BiConsumer; +import java.util.function.Supplier; + +public class TestExecutor { + /** + * Helper method for running individual tests. In case of an exception wraps it and + * includes the {@code name} of the test to make debugging issues with the obfuscated + * JARs a bit easier. + */ + public static void run(BiConsumer outputConsumer, String name, Supplier resultSupplier) { + String result; + try { + result = resultSupplier.get(); + } catch (Throwable t) { + throw new RuntimeException("Test failed: " + name, t); + } + outputConsumer.accept(name, result); + } + + /** + * Returns {@code t}, but in a way which (hopefully) prevents code shrinkers from + * simplifying this. + */ + public static T same(T t) { + // This is essentially `return t`, but contains some redundant code to try + // prevent the code shrinkers from simplifying this + return Optional.of(t) + .map(v -> Optional.of(v).get()) + .orElseThrow(() -> new AssertionError("unreachable")); + } +} diff --git a/shrinker-test/src/test/java/com/google/gson/it/ShrinkingIT.java b/shrinker-test/src/test/java/com/google/gson/it/ShrinkingIT.java new file mode 100644 index 0000000000..ddf6f34f19 --- /dev/null +++ b/shrinker-test/src/test/java/com/google/gson/it/ShrinkingIT.java @@ -0,0 +1,223 @@ +/* + * Copyright (C) 2023 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.it; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.fail; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.List; +import java.util.function.BiConsumer; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +/** + * Integration test verifying behavior of shrunken and obfuscated JARs. + */ +@RunWith(Parameterized.class) +public class ShrinkingIT { + // These JAR files are prepared by the Maven build + public static final Path PROGUARD_RESULT_PATH = Paths.get("target/proguard-output.jar"); + public static final Path R8_RESULT_PATH = Paths.get("target/r8-output.jar"); + + @Parameters(name = "{index}: {0}") + public static List jarsToTest() { + return Arrays.asList(PROGUARD_RESULT_PATH, R8_RESULT_PATH); + } + + @Parameter + public Path jarToTest; + + @Before + public void verifyJarExists() { + if (!Files.isRegularFile(jarToTest)) { + fail("JAR file " + jarToTest + " does not exist; run this test with `mvn clean verify`"); + } + } + + @FunctionalInterface + interface TestAction { + void run(Class c) throws Exception; + } + + private void runTest(String className, TestAction testAction) throws Exception { + // Use bootstrap class loader; load all custom classes from JAR and not + // from dependencies of this test + ClassLoader classLoader = null; + + // Load the shrunken and obfuscated JARs with a separate class loader, then load + // the main test class from it and let the test action invoke its test methods + try (URLClassLoader loader = new URLClassLoader(new URL[] {jarToTest.toUri().toURL()}, classLoader)) { + Class c = loader.loadClass(className); + testAction.run(c); + } + } + + @Test + public void test() throws Exception { + StringBuilder output = new StringBuilder(); + + runTest("com.example.Main", c -> { + Method m = c.getMethod("runTests", BiConsumer.class); + m.invoke(null, (BiConsumer) (name, content) -> output.append(name + "\n" + content + "\n===\n")); + }); + + assertThat(output.toString()).isEqualTo(String.join("\n", + "Write: TypeToken anonymous", + "[", + " {", + " \"custom\": 1", + " }", + "]", + "===", + "Read: TypeToken anonymous", + "[ClassWithAdapter[3]]", + "===", + "Write: TypeToken manual", + "[", + " {", + " \"custom\": 1", + " }", + "]", + "===", + "Read: TypeToken manual", + "[ClassWithAdapter[3]]", + "===", + "Write: Named fields", + "{", + " \"myField\": 2,", + " \"notAccessedField\": -1", + "}", + "===", + "Read: Named fields", + "3", + "===", + "Write: SerializedName", + "{", + " \"myField\": 2,", + " \"notAccessed\": -1", + "}", + "===", + "Read: SerializedName", + "3", + "===", + "Read: No JDK Unsafe; initial constructor value", + "-3", + "===", + "Read: No JDK Unsafe; custom value", + "3", + "===", + "Write: Enum", + "\"FIRST\"", + "===", + "Read: Enum", + "SECOND", + "===", + "Write: Enum SerializedName", + "\"one\"", + "===", + "Read: Enum SerializedName", + "SECOND", + "===", + "Write: @Expose", + "{\"i\":0}", + "===", + "Write: Version annotations", + "{\"i1\":0,\"i4\":0}", + "===", + "Write: JsonAdapter on fields", + "{", + " \"f\": \"adapter-null\",", + " \"f1\": \"adapter-1\",", + " \"f2\": \"factory-2\",", + " \"f3\": \"serializer-3\",", + // For f4 only a JsonDeserializer is registered, so serialization falls back to reflection + " \"f4\": {", + " \"s\": \"4\"", + " }", + "}", + "===", + "Read: JsonAdapter on fields", + // For f3 only a JsonSerializer is registered, so for deserialization value is read as is using reflection + "ClassWithJsonAdapterAnnotation[f1=adapter-1, f2=factory-2, f3=3, f4=deserializer-4]", + "===", + "Read: Generic TypeToken", + "{t=read-1}", + "===", + "Read: Using Generic", + "{g={t=read-1}}", + "===", + "Read: Using Generic TypeToken", + "{g={t=read-1}}", + "===", + "" + )); + } + + @Test + public void testDefaultConstructor() throws Exception { + runTest("com.example.DefaultConstructorMain", c -> { + Method m = c.getMethod("runTest"); + + if (jarToTest.equals(PROGUARD_RESULT_PATH)) { + Object result = m.invoke(null); + assertThat(result).isEqualTo("value"); + } else { + // R8 performs more aggressive optimizations + Exception e = assertThrows(InvocationTargetException.class, () -> m.invoke(null)); + assertThat(e).hasCauseThat().hasMessageThat().isEqualTo( + "Abstract classes can't be instantiated! Adjust the R8 configuration or register an InstanceCreator" + + " or a TypeAdapter for this type. Class name: com.example.DefaultConstructorMain$TestClass" + + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#r8-abstract-class" + ); + } + }); + } + + @Test + public void testDefaultConstructorNoJdkUnsafe() throws Exception { + runTest("com.example.DefaultConstructorMain", c -> { + Method m = c.getMethod("runTestNoJdkUnsafe"); + + if (jarToTest.equals(PROGUARD_RESULT_PATH)) { + Object result = m.invoke(null); + assertThat(result).isEqualTo("value"); + } else { + // R8 performs more aggressive optimizations + Exception e = assertThrows(InvocationTargetException.class, () -> m.invoke(null)); + assertThat(e).hasCauseThat().hasMessageThat().isEqualTo( + "Unable to create instance of class com.example.DefaultConstructorMain$TestClassNotAbstract;" + + " usage of JDK Unsafe is disabled. Registering an InstanceCreator or a TypeAdapter for this type," + + " adding a no-args constructor, or enabling usage of JDK Unsafe may fix this problem. Or adjust" + + " your R8 configuration to keep the no-args constructor of the class." + ); + } + }); + } +} From a8a928ee51452c3f8925fe07d64603abca8bf3d8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 30 May 2023 15:42:06 -0700 Subject: [PATCH 34/36] Bump guava-testlib from 31.1-jre to 32.0.0-jre (#2400) Bumps [guava-testlib](https://github.com/google/guava) from 31.1-jre to 32.0.0-jre. - [Release notes](https://github.com/google/guava/releases) - [Commits](https://github.com/google/guava/commits) --- updated-dependencies: - dependency-name: com.google.guava:guava-testlib dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- gson/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gson/pom.xml b/gson/pom.xml index 6e911c74de..f5c0b11feb 100644 --- a/gson/pom.xml +++ b/gson/pom.xml @@ -62,7 +62,7 @@ com.google.guava guava-testlib - 31.1-jre + 32.0.0-jre test From d3e17587fe8dbf1c6afd1acbad4878f19ff64c02 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 30 May 2023 15:42:32 -0700 Subject: [PATCH 35/36] Bump guava from 31.1-jre to 32.0.0-jre (#2399) Bumps [guava](https://github.com/google/guava) from 31.1-jre to 32.0.0-jre. - [Release notes](https://github.com/google/guava/releases) - [Commits](https://github.com/google/guava/commits) --- updated-dependencies: - dependency-name: com.google.guava:guava dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- proto/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proto/pom.xml b/proto/pom.xml index 313bb10613..ac04212ab7 100644 --- a/proto/pom.xml +++ b/proto/pom.xml @@ -50,7 +50,7 @@ com.google.guava guava - 31.1-jre + 32.0.0-jre From 481ac9b82cd5713f43018fafc16218d4045dc4c4 Mon Sep 17 00:00:00 2001 From: Marcono1234 Date: Wed, 31 May 2023 01:32:22 +0200 Subject: [PATCH 36/36] Use non-`null` `FormattingStyle`; configure space after separator (#2345) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Use non-`null` `FormattingStyle`; configure space after separator * Improve Javadoc and tests * Rename to plural separator*s* * Add explicit tests for default formatting styles --------- Co-authored-by: Éamonn McManus --- .../java/com/google/gson/FormattingStyle.java | 67 +++++-- gson/src/main/java/com/google/gson/Gson.java | 6 +- .../java/com/google/gson/GsonBuilder.java | 12 +- .../com/google/gson/stream/JsonWriter.java | 68 ++++--- .../test/java/com/google/gson/GsonTest.java | 4 +- .../gson/functional/FormattingStyleTest.java | 169 +++++++++++------- .../google/gson/stream/JsonWriterTest.java | 29 ++- 7 files changed, 240 insertions(+), 115 deletions(-) diff --git a/gson/src/main/java/com/google/gson/FormattingStyle.java b/gson/src/main/java/com/google/gson/FormattingStyle.java index ed9f86dd1f..ff031cd4bc 100644 --- a/gson/src/main/java/com/google/gson/FormattingStyle.java +++ b/gson/src/main/java/com/google/gson/FormattingStyle.java @@ -22,10 +22,15 @@ /** * A class used to control what the serialization output looks like. * - *

It currently defines the kind of newline to use, and the indent, but - * might add more in the future.

+ *

It currently has the following configuration methods, but more methods + * might be added in the future: + *

    + *
  • {@link #withNewline(String)} + *
  • {@link #withIndent(String)} + *
  • {@link #withSpaceAfterSeparators(boolean)} + *
* - * @see GsonBuilder#setPrettyPrinting(FormattingStyle) + * @see GsonBuilder#setFormattingStyle(FormattingStyle) * @see JsonWriter#setFormattingStyle(FormattingStyle) * @see Wikipedia Newline article * @@ -34,15 +39,30 @@ public class FormattingStyle { private final String newline; private final String indent; + private final boolean spaceAfterSeparators; /** - * The default pretty printing formatting style using {@code "\n"} as - * newline and two spaces as indent. + * The default compact formatting style: + *
    + *
  • no newline + *
  • no indent + *
  • no space after {@code ','} and {@code ':'} + *
*/ - public static final FormattingStyle DEFAULT = - new FormattingStyle("\n", " "); + public static final FormattingStyle COMPACT = new FormattingStyle("", "", false); - private FormattingStyle(String newline, String indent) { + /** + * The default pretty printing formatting style: + *
    + *
  • {@code "\n"} as newline + *
  • two spaces as indent + *
  • a space between {@code ':'} and the subsequent value + *
+ */ + public static final FormattingStyle PRETTY = + new FormattingStyle("\n", " ", true); + + private FormattingStyle(String newline, String indent, boolean spaceAfterSeparators) { Objects.requireNonNull(newline, "newline == null"); Objects.requireNonNull(indent, "indent == null"); if (!newline.matches("[\r\n]*")) { @@ -55,6 +75,7 @@ private FormattingStyle(String newline, String indent) { } this.newline = newline; this.indent = indent; + this.spaceAfterSeparators = spaceAfterSeparators; } /** @@ -70,7 +91,7 @@ private FormattingStyle(String newline, String indent) { * @return a newly created {@link FormattingStyle} */ public FormattingStyle withNewline(String newline) { - return new FormattingStyle(newline, this.indent); + return new FormattingStyle(newline, this.indent, this.spaceAfterSeparators); } /** @@ -82,11 +103,26 @@ public FormattingStyle withNewline(String newline) { * @return a newly created {@link FormattingStyle} */ public FormattingStyle withIndent(String indent) { - return new FormattingStyle(this.newline, indent); + return new FormattingStyle(this.newline, indent, this.spaceAfterSeparators); + } + + /** + * Creates a {@link FormattingStyle} which either uses a space after + * the separators {@code ','} and {@code ':'} in the JSON output, or not. + * + *

This setting has no effect on the {@linkplain #withNewline(String) configured newline}. + * If a non-empty newline is configured, it will always be added after + * {@code ','} and no space is added after the {@code ','} in that case.

+ * + * @param spaceAfterSeparators whether to output a space after {@code ','} and {@code ':'}. + * @return a newly created {@link FormattingStyle} + */ + public FormattingStyle withSpaceAfterSeparators(boolean spaceAfterSeparators) { + return new FormattingStyle(this.newline, this.indent, spaceAfterSeparators); } /** - * The string value that will be used as a newline. + * Returns the string value that will be used as a newline. * * @return the newline value. */ @@ -95,11 +131,18 @@ public String getNewline() { } /** - * The string value that will be used as indent. + * Returns the string value that will be used as indent. * * @return the indent value. */ public String getIndent() { return this.indent; } + + /** + * Returns whether a space will be used after {@code ','} and {@code ':'}. + */ + public boolean usesSpaceAfterSeparators() { + return this.spaceAfterSeparators; + } } diff --git a/gson/src/main/java/com/google/gson/Gson.java b/gson/src/main/java/com/google/gson/Gson.java index ec5f69f63e..ba7bf4b4ae 100644 --- a/gson/src/main/java/com/google/gson/Gson.java +++ b/gson/src/main/java/com/google/gson/Gson.java @@ -141,7 +141,7 @@ public final class Gson { static final boolean DEFAULT_JSON_NON_EXECUTABLE = false; static final boolean DEFAULT_LENIENT = false; - static final FormattingStyle DEFAULT_FORMATTING_STYLE = null; + static final FormattingStyle DEFAULT_FORMATTING_STYLE = FormattingStyle.COMPACT; static final boolean DEFAULT_ESCAPE_HTML = true; static final boolean DEFAULT_SERIALIZE_NULLS = false; static final boolean DEFAULT_COMPLEX_MAP_KEYS = false; @@ -205,7 +205,7 @@ public final class Gson { * means that all the unneeded white-space is removed. You can change this behavior with * {@link GsonBuilder#setPrettyPrinting()}. *
  • When the JSON generated contains more than one line, the kind of newline and indent to - * use can be configured with {@link GsonBuilder#setPrettyPrinting(FormattingStyle)}.
  • + * use can be configured with {@link GsonBuilder#setFormattingStyle(FormattingStyle)}. *
  • The generated JSON omits all the fields that are null. Note that nulls in arrays are * kept as is since an array is an ordered list. Moreover, if a field is not null, but its * generated JSON is empty, the field is kept. You can configure Gson to serialize null values @@ -894,7 +894,7 @@ public void toJson(JsonElement jsonElement, Appendable writer) throws JsonIOExce *
  • {@link GsonBuilder#serializeNulls()}
  • *
  • {@link GsonBuilder#setLenient()}
  • *
  • {@link GsonBuilder#setPrettyPrinting()}
  • - *
  • {@link GsonBuilder#setPrettyPrinting(FormattingStyle)}
  • + *
  • {@link GsonBuilder#setFormattingStyle(FormattingStyle)}
  • * */ public JsonWriter newJsonWriter(Writer writer) throws IOException { diff --git a/gson/src/main/java/com/google/gson/GsonBuilder.java b/gson/src/main/java/com/google/gson/GsonBuilder.java index d1508cfa04..e3d4818233 100644 --- a/gson/src/main/java/com/google/gson/GsonBuilder.java +++ b/gson/src/main/java/com/google/gson/GsonBuilder.java @@ -497,29 +497,27 @@ public GsonBuilder addDeserializationExclusionStrategy(ExclusionStrategy strateg * Configures Gson to output JSON that fits in a page for pretty printing. This option only * affects JSON serialization. * - *

    This is a convenience method which simply calls {@link #setPrettyPrinting(FormattingStyle)} - * with {@link FormattingStyle#DEFAULT}. + *

    This is a convenience method which simply calls {@link #setFormattingStyle(FormattingStyle)} + * with {@link FormattingStyle#PRETTY}. * * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern */ @CanIgnoreReturnValue public GsonBuilder setPrettyPrinting() { - return setPrettyPrinting(FormattingStyle.DEFAULT); + return setFormattingStyle(FormattingStyle.PRETTY); } /** * Configures Gson to output JSON that uses a certain kind of formatting style (for example newline and indent). * This option only affects JSON serialization. By default Gson produces compact JSON output without any formatting. * - *

    Has no effect if the serialized format is a single line.

    - * * @param formattingStyle the formatting style to use. * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern * @since $next-version$ */ @CanIgnoreReturnValue - public GsonBuilder setPrettyPrinting(FormattingStyle formattingStyle) { - this.formattingStyle = formattingStyle; + public GsonBuilder setFormattingStyle(FormattingStyle formattingStyle) { + this.formattingStyle = Objects.requireNonNull(formattingStyle); return this; } 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 4a2424e687..2eb56ed58b 100644 --- a/gson/src/main/java/com/google/gson/stream/JsonWriter.java +++ b/gson/src/main/java/com/google/gson/stream/JsonWriter.java @@ -25,6 +25,7 @@ import static com.google.gson.stream.JsonScope.NONEMPTY_OBJECT; import com.google.errorprone.annotations.CanIgnoreReturnValue; +import com.google.gson.FormattingStyle; import java.io.Closeable; import java.io.Flushable; import java.io.IOException; @@ -37,8 +38,6 @@ import java.util.concurrent.atomic.AtomicLong; import java.util.regex.Pattern; -import com.google.gson.FormattingStyle; - /** * Writes a JSON (RFC 7159) * encoded value to a stream, one token at a time. The stream includes both @@ -182,15 +181,12 @@ public class JsonWriter implements Closeable, Flushable { push(EMPTY_DOCUMENT); } - /** - * The settings used for pretty printing, or null for no pretty printing. - */ private FormattingStyle formattingStyle; - - /** - * The name/value separator; either ":" or ": ". - */ - private String separator = ":"; + // These fields cache data derived from the formatting style, to avoid having to + // re-evaluate it every time something is written + private String formattedColon; + private String formattedComma; + private boolean usesEmptyNewlineAndIndent; private boolean lenient; @@ -207,6 +203,7 @@ public class JsonWriter implements Closeable, Flushable { */ public JsonWriter(Writer out) { this.out = Objects.requireNonNull(out, "out == null"); + setFormattingStyle(FormattingStyle.COMPACT); } /** @@ -215,36 +212,49 @@ public JsonWriter(Writer out) { * will be compact. Otherwise the encoded document will be more * human-readable. * + *

    This is a convenience method which overwrites any previously + * {@linkplain #setFormattingStyle(FormattingStyle) set formatting style} with + * either {@link FormattingStyle#COMPACT} if the given indent string is + * empty, or {@link FormattingStyle#PRETTY} with the given indent if + * not empty. + * * @param indent a string containing only whitespace. */ public final void setIndent(String indent) { if (indent.isEmpty()) { - setFormattingStyle(null); + setFormattingStyle(FormattingStyle.COMPACT); } else { - setFormattingStyle(FormattingStyle.DEFAULT.withIndent(indent)); + setFormattingStyle(FormattingStyle.PRETTY.withIndent(indent)); } } /** - * Sets the pretty printing style to be used in the encoded document. - * No pretty printing is done if the given style is {@code null}. + * Sets the formatting style to be used in the encoded document. * - *

    Sets the various attributes to be used in the encoded document. - * For example the indentation string to be repeated for each level of indentation. - * Or the newline style, to accommodate various OS styles.

    + *

    The formatting style specifies for example the indentation string to be + * repeated for each level of indentation, or the newline style, to accommodate + * various OS styles.

    * - *

    Has no effect if the serialized format is a single line.

    - * - * @param formattingStyle the style used for pretty printing, no pretty printing if {@code null}. + * @param formattingStyle the formatting style to use, must not be {@code null}. * @since $next-version$ */ public final void setFormattingStyle(FormattingStyle formattingStyle) { - this.formattingStyle = formattingStyle; - if (formattingStyle == null) { - this.separator = ":"; + this.formattingStyle = Objects.requireNonNull(formattingStyle); + + this.formattedComma = ","; + if (this.formattingStyle.usesSpaceAfterSeparators()) { + this.formattedColon = ": "; + + // Only add space if no newline is written + if (this.formattingStyle.getNewline().isEmpty()) { + this.formattedComma = ", "; + } } else { - this.separator = ": "; + this.formattedColon = ":"; } + + this.usesEmptyNewlineAndIndent = this.formattingStyle.getNewline().isEmpty() + && this.formattingStyle.getIndent().isEmpty(); } /** @@ -419,7 +429,7 @@ private void replaceTop(int topOfStack) { /** * Encodes the property name. * - * @param name the name of the forthcoming value. May not be null. + * @param name the name of the forthcoming value. May not be {@code null}. * @return this writer. */ @CanIgnoreReturnValue @@ -693,7 +703,7 @@ private void string(String value) throws IOException { } private void newline() throws IOException { - if (formattingStyle == null) { + if (usesEmptyNewlineAndIndent) { return; } @@ -710,7 +720,7 @@ private void newline() throws IOException { private void beforeName() throws IOException { int context = peek(); if (context == NONEMPTY_OBJECT) { // first in object - out.write(','); + out.write(formattedComma); } else if (context != EMPTY_OBJECT) { // not in an object! throw new IllegalStateException("Nesting problem."); } @@ -742,12 +752,12 @@ private void beforeValue() throws IOException { break; case NONEMPTY_ARRAY: // another in array - out.append(','); + out.append(formattedComma); newline(); break; case DANGLING_NAME: // value for name - out.append(separator); + out.append(formattedColon); replaceTop(NONEMPTY_OBJECT); break; diff --git a/gson/src/test/java/com/google/gson/GsonTest.java b/gson/src/test/java/com/google/gson/GsonTest.java index c1e9e9d785..fcdc7cbc54 100644 --- a/gson/src/test/java/com/google/gson/GsonTest.java +++ b/gson/src/test/java/com/google/gson/GsonTest.java @@ -63,7 +63,7 @@ public final class GsonTest { public void testOverridesDefaultExcluder() { Gson gson = new Gson(CUSTOM_EXCLUDER, CUSTOM_FIELD_NAMING_STRATEGY, new HashMap>(), true, false, true, false, - FormattingStyle.DEFAULT, true, false, true, + FormattingStyle.PRETTY, true, false, true, LongSerializationPolicy.DEFAULT, null, DateFormat.DEFAULT, DateFormat.DEFAULT, new ArrayList(), new ArrayList(), new ArrayList(), @@ -80,7 +80,7 @@ public void testOverridesDefaultExcluder() { public void testClonedTypeAdapterFactoryListsAreIndependent() { Gson original = new Gson(CUSTOM_EXCLUDER, CUSTOM_FIELD_NAMING_STRATEGY, new HashMap>(), true, false, true, false, - FormattingStyle.DEFAULT, true, false, true, + FormattingStyle.PRETTY, true, false, true, LongSerializationPolicy.DEFAULT, null, DateFormat.DEFAULT, DateFormat.DEFAULT, new ArrayList(), new ArrayList(), new ArrayList(), diff --git a/gson/src/test/java/com/google/gson/functional/FormattingStyleTest.java b/gson/src/test/java/com/google/gson/functional/FormattingStyleTest.java index 170e0ff29d..920f820847 100644 --- a/gson/src/test/java/com/google/gson/functional/FormattingStyleTest.java +++ b/gson/src/test/java/com/google/gson/functional/FormattingStyleTest.java @@ -21,6 +21,11 @@ import com.google.gson.FormattingStyle; import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -33,94 +38,140 @@ @RunWith(JUnit4.class) public class FormattingStyleTest { - private static final String[] INPUT = {"v1", "v2"}; - private static final String EXPECTED = "[\"v1\",\"v2\"]"; - private static final String EXPECTED_OS = buildExpected(System.lineSeparator(), " "); - private static final String EXPECTED_CR = buildExpected("\r", " "); - private static final String EXPECTED_LF = buildExpected("\n", " "); - private static final String EXPECTED_CRLF = buildExpected("\r\n", " "); + // Create new input object every time to protect against tests accidentally modifying input + private static Map> createInput() { + Map> map = new LinkedHashMap<>(); + map.put("a", Arrays.asList(1, 2)); + return map; + } + + private static String buildExpected(String newline, String indent, boolean spaceAfterSeparators) { + String expected = "{\"a\":[1,2]}"; + String commaSpace = spaceAfterSeparators && newline.isEmpty() ? " " : ""; + return expected.replace("", newline).replace("", indent) + .replace("", spaceAfterSeparators ? " " : "") + .replace("", commaSpace); + } // Various valid strings that can be used for newline and indent private static final String[] TEST_NEWLINES = { "", "\r", "\n", "\r\n", "\n\r\r\n", System.lineSeparator() }; private static final String[] TEST_INDENTS = { - "", " ", " ", " ", "\t", " \t \t" + "", " ", " ", "\t", " \t \t" }; @Test public void testDefault() { Gson gson = new GsonBuilder().setPrettyPrinting().create(); - String json = gson.toJson(INPUT); - // Make sure the default uses LF, like before. - assertThat(json).isEqualTo(EXPECTED_LF); + String json = gson.toJson(createInput()); + assertThat(json).isEqualTo(buildExpected("\n", " ", true)); } @Test - public void testNewlineCrLf() { - FormattingStyle style = FormattingStyle.DEFAULT.withNewline("\r\n"); - Gson gson = new GsonBuilder().setPrettyPrinting(style).create(); - String json = gson.toJson(INPUT); - assertThat(json).isEqualTo(EXPECTED_CRLF); + public void testVariousCombinationsParse() { + // Mixing various indent and newline styles in the same string, to be parsed. + String jsonStringMix = "{\r\t'a':\r\n[ 1,2\t]\n}"; + TypeToken>> inputType = new TypeToken>>() {}; + + Map> actualParsed; + // Test all that all combinations of newline can be parsed and generate the same INPUT. + for (String indent : TEST_INDENTS) { + for (String newline : TEST_NEWLINES) { + FormattingStyle style = FormattingStyle.PRETTY.withNewline(newline).withIndent(indent); + Gson gson = new GsonBuilder().setFormattingStyle(style).create(); + + String toParse = buildExpected(newline, indent, true); + actualParsed = gson.fromJson(toParse, inputType); + assertThat(actualParsed).isEqualTo(createInput()); + + // Parse the mixed string with the gson parsers configured with various newline / indents. + actualParsed = gson.fromJson(jsonStringMix, inputType); + assertThat(actualParsed).isEqualTo(createInput()); + } + } + } + + private static String toJson(Object obj, FormattingStyle style) { + return new GsonBuilder().setFormattingStyle(style).create().toJson(obj); } @Test - public void testNewlineLf() { - FormattingStyle style = FormattingStyle.DEFAULT.withNewline("\n"); - Gson gson = new GsonBuilder().setPrettyPrinting(style).create(); - String json = gson.toJson(INPUT); - assertThat(json).isEqualTo(EXPECTED_LF); + public void testFormatCompact() { + String json = toJson(createInput(), FormattingStyle.COMPACT); + String expectedJson = buildExpected("", "", false); + assertThat(json).isEqualTo(expectedJson); + // Sanity check to verify that `buildExpected` works correctly + assertThat(json).isEqualTo("{\"a\":[1,2]}"); } @Test - public void testNewlineCr() { - FormattingStyle style = FormattingStyle.DEFAULT.withNewline("\r"); - Gson gson = new GsonBuilder().setPrettyPrinting(style).create(); - String json = gson.toJson(INPUT); - assertThat(json).isEqualTo(EXPECTED_CR); + public void testFormatPretty() { + String json = toJson(createInput(), FormattingStyle.PRETTY); + String expectedJson = buildExpected("\n", " ", true); + assertThat(json).isEqualTo(expectedJson); + // Sanity check to verify that `buildExpected` works correctly + assertThat(json).isEqualTo( + "{\n" + + " \"a\": [\n" + + " 1,\n" + + " 2\n" + + " ]\n" + + "}"); } @Test - public void testNewlineOs() { - FormattingStyle style = FormattingStyle.DEFAULT.withNewline(System.lineSeparator()); - Gson gson = new GsonBuilder().setPrettyPrinting(style).create(); - String json = gson.toJson(INPUT); - assertThat(json).isEqualTo(EXPECTED_OS); + public void testFormatPrettySingleLine() { + FormattingStyle style = FormattingStyle.COMPACT.withSpaceAfterSeparators(true); + String json = toJson(createInput(), style); + String expectedJson = buildExpected("", "", true); + assertThat(json).isEqualTo(expectedJson); + // Sanity check to verify that `buildExpected` works correctly + assertThat(json).isEqualTo("{\"a\": [1, 2]}"); } @Test - public void testVariousCombinationsToString() { - for (String indent : TEST_INDENTS) { - for (String newline : TEST_NEWLINES) { - FormattingStyle style = FormattingStyle.DEFAULT.withNewline(newline).withIndent(indent); - Gson gson = new GsonBuilder().setPrettyPrinting(style).create(); - String json = gson.toJson(INPUT); - assertThat(json).isEqualTo(buildExpected(newline, indent)); + public void testFormat() { + for (String newline : TEST_NEWLINES) { + for (String indent : TEST_INDENTS) { + for (boolean spaceAfterSeparators : new boolean[] {true, false}) { + FormattingStyle style = FormattingStyle.COMPACT.withNewline(newline) + .withIndent(indent).withSpaceAfterSeparators(spaceAfterSeparators); + + String json = toJson(createInput(), style); + String expectedJson = buildExpected(newline, indent, spaceAfterSeparators); + assertThat(json).isEqualTo(expectedJson); + } } } } + /** + * Should be able to convert {@link FormattingStyle#COMPACT} to {@link FormattingStyle#PRETTY} + * using the {@code withX} methods. + */ @Test - public void testVariousCombinationsParse() { - // Mixing various indent and newline styles in the same string, to be parsed. - String jsonStringMix = "[\r\t'v1',\r\n 'v2'\n]"; + public void testCompactToPretty() { + FormattingStyle style = FormattingStyle.COMPACT.withNewline("\n").withIndent(" ") + .withSpaceAfterSeparators(true); - String[] actualParsed; - // Test all that all combinations of newline can be parsed and generate the same INPUT. - for (String indent : TEST_INDENTS) { - for (String newline : TEST_NEWLINES) { - FormattingStyle style = FormattingStyle.DEFAULT.withNewline(newline).withIndent(indent); - Gson gson = new GsonBuilder().setPrettyPrinting(style).create(); + String json = toJson(createInput(), style); + String expectedJson = toJson(createInput(), FormattingStyle.PRETTY); + assertThat(json).isEqualTo(expectedJson); + } - String toParse = buildExpected(newline, indent); - actualParsed = gson.fromJson(toParse, INPUT.getClass()); - assertThat(actualParsed).isEqualTo(INPUT); + /** + * Should be able to convert {@link FormattingStyle#PRETTY} to {@link FormattingStyle#COMPACT} + * using the {@code withX} methods. + */ + @Test + public void testPrettyToCompact() { + FormattingStyle style = FormattingStyle.PRETTY.withNewline("").withIndent("") + .withSpaceAfterSeparators(false); - // Parse the mixed string with the gson parsers configured with various newline / indents. - actualParsed = gson.fromJson(jsonStringMix, INPUT.getClass()); - assertThat(actualParsed).isEqualTo(INPUT); - } - } + String json = toJson(createInput(), style); + String expectedJson = toJson(createInput(), FormattingStyle.COMPACT); + assertThat(json).isEqualTo(expectedJson); } @Test @@ -128,7 +179,7 @@ public void testStyleValidations() { try { // TBD if we want to accept \u2028 and \u2029. For now we don't because JSON specification // does not consider them to be newlines - FormattingStyle.DEFAULT.withNewline("\u2028"); + FormattingStyle.PRETTY.withNewline("\u2028"); fail("Gson should not accept anything but \\r and \\n for newline"); } catch (IllegalArgumentException expected) { assertThat(expected).hasMessageThat() @@ -136,7 +187,7 @@ public void testStyleValidations() { } try { - FormattingStyle.DEFAULT.withNewline("NL"); + FormattingStyle.PRETTY.withNewline("NL"); fail("Gson should not accept anything but \\r and \\n for newline"); } catch (IllegalArgumentException expected) { assertThat(expected).hasMessageThat() @@ -144,15 +195,11 @@ public void testStyleValidations() { } try { - FormattingStyle.DEFAULT.withIndent("\f"); + FormattingStyle.PRETTY.withIndent("\f"); fail("Gson should not accept anything but space and tab for indent"); } catch (IllegalArgumentException expected) { assertThat(expected).hasMessageThat() .isEqualTo("Only combinations of spaces and tabs are allowed in indent."); } } - - private static String buildExpected(String newline, String indent) { - return EXPECTED.replace("", newline).replace("", indent); - } } 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 2ee120f38e..2b9fb133ac 100644 --- a/gson/src/test/java/com/google/gson/stream/JsonWriterTest.java +++ b/gson/src/test/java/com/google/gson/stream/JsonWriterTest.java @@ -852,7 +852,9 @@ public void testSetGetFormattingStyle() throws IOException { StringWriter stringWriter = new StringWriter(); JsonWriter jsonWriter = new JsonWriter(stringWriter); - jsonWriter.setFormattingStyle(FormattingStyle.DEFAULT.withIndent(" \t ").withNewline(lineSeparator)); + // Default should be FormattingStyle.COMPACT + assertThat(jsonWriter.getFormattingStyle()).isSameInstanceAs(FormattingStyle.COMPACT); + jsonWriter.setFormattingStyle(FormattingStyle.PRETTY.withIndent(" \t ").withNewline(lineSeparator)); jsonWriter.beginArray(); jsonWriter.value(true); @@ -871,4 +873,29 @@ public void testSetGetFormattingStyle() throws IOException { assertThat(jsonWriter.getFormattingStyle().getNewline()).isEqualTo(lineSeparator); } + + @Test + public void testIndentOverwritesFormattingStyle() throws IOException { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + jsonWriter.setFormattingStyle(FormattingStyle.COMPACT); + // Should overwrite formatting style + jsonWriter.setIndent(" "); + + jsonWriter.beginObject(); + jsonWriter.name("a"); + jsonWriter.beginArray(); + jsonWriter.value(1); + jsonWriter.value(2); + jsonWriter.endArray(); + jsonWriter.endObject(); + + String expected = "{\n" + + " \"a\": [\n" + + " 1,\n" + + " 2\n" + + " ]\n" + + "}"; + assertThat(stringWriter.toString()).isEqualTo(expected); + } }