diff --git a/.github/workflows/check-android-compatibility.yml b/.github/workflows/check-android-compatibility.yml index 9122d4207c..939582e547 100644 --- a/.github/workflows/check-android-compatibility.yml +++ b/.github/workflows/check-android-compatibility.yml @@ -26,4 +26,5 @@ jobs: - name: Check Android compatibility run: | # Run 'test' phase because plugin normally expects to be executed after tests have been compiled - mvn --batch-mode --no-transfer-progress test animal-sniffer:check@check-android-compatibility -DskipTests + # Have to skip 'jpms-test' module because it requires that full Gson JAR has been built + mvn --batch-mode --no-transfer-progress test animal-sniffer:check@check-android-compatibility -DskipTests --projects '!jpms-test' diff --git a/extras/pom.xml b/extras/pom.xml index 50a65f7136..020ab45f48 100644 --- a/extras/pom.xml +++ b/extras/pom.xml @@ -31,6 +31,9 @@ 2024-05-19T18:54:10Z + + + true @@ -64,21 +67,6 @@ - - - - - org.apache.maven.plugins - maven-deploy-plugin - - - true - - - - - - Inderjeet Singh diff --git a/graal-native-image-test/pom.xml b/graal-native-image-test/pom.xml index 7254f2fb10..6f556c1ad6 100644 --- a/graal-native-image-test/pom.xml +++ b/graal-native-image-test/pom.xml @@ -14,7 +14,6 @@ limitations under the License. --> - 4.0.0 @@ -23,16 +22,16 @@ 2.11.1-SNAPSHOT graal-native-image-test + Test: GraalVM Native Image - - - 2024-05-19T18:54:10Z - 11 **/Java17* + + + true @@ -63,22 +62,6 @@ - - com.github.siom79.japicmp - japicmp-maven-plugin - - - true - - - - org.codehaus.mojo - animal-sniffer-maven-plugin - - - true - - org.apache.maven.plugins maven-jar-plugin @@ -87,31 +70,6 @@ true - - org.apache.maven.plugins - maven-install-plugin - - - true - - - - org.apache.maven.plugins - maven-deploy-plugin - - - true - - - - org.apache.maven.plugins - maven-gpg-plugin - - - true - - diff --git a/gson/src/main/java/module-info.java b/gson/src/main/java/module-info.java index 1b951ceafa..6692639673 100644 --- a/gson/src/main/java/module-info.java +++ b/gson/src/main/java/module-info.java @@ -25,7 +25,7 @@ exports com.google.gson.reflect; exports com.google.gson.stream; - // Dependency on Error Prone Annotations + // Optional dependency on Error Prone Annotations requires static com.google.errorprone.annotations; // Optional dependency on java.sql diff --git a/jpms-test/README.md b/jpms-test/README.md new file mode 100644 index 0000000000..c667c0dad1 --- /dev/null +++ b/jpms-test/README.md @@ -0,0 +1,5 @@ +# jpms-test + +This Maven module contains tests to verify that Gson's `module-info.class` which is used by the Java Platform Module System (JPMS) works properly and can be used by other projects. The module declaration file `src/test/java/module-info.java` uses Gson's module. + +This is a separate Maven module to test Gson's final JAR instead of just the compiled classes. diff --git a/jpms-test/pom.xml b/jpms-test/pom.xml new file mode 100644 index 0000000000..5589549ad1 --- /dev/null +++ b/jpms-test/pom.xml @@ -0,0 +1,53 @@ + + + 4.0.0 + + + com.google.code.gson + gson-parent + 2.11.1-SNAPSHOT + + jpms-test + Test: Java Platform Module System (JPMS) + + + 11 + ${maven.compiler.release} + + + true + + + + + com.google.code.gson + gson + ${project.parent.version} + + + + junit + junit + test + + + com.google.truth + truth + test + + + diff --git a/jpms-test/src/main/java/module-info.java b/jpms-test/src/main/java/module-info.java new file mode 100644 index 0000000000..04360c0fc3 --- /dev/null +++ b/jpms-test/src/main/java/module-info.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 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. + */ + +/** + * Dummy module to prevent Maven Compiler Plugin from failing if {@code module-info.java} exists + * only in test sources: + * + *
+ * + * Can't compile test sources when main sources are missing a module descriptor + * + *
+ */ +module com.google.gson.dummy {} diff --git a/jpms-test/src/test/java/com/google/gson/jpms_test/ExportedPackagesTest.java b/jpms-test/src/test/java/com/google/gson/jpms_test/ExportedPackagesTest.java new file mode 100644 index 0000000000..c30b3fb75e --- /dev/null +++ b/jpms-test/src/test/java/com/google/gson/jpms_test/ExportedPackagesTest.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2024 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.jpms_test; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.gson.Gson; +import com.google.gson.annotations.SerializedName; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import java.io.IOException; +import java.io.StringReader; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InaccessibleObjectException; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import org.junit.Test; + +/** + * Verifies that Gson's {@code module-info.class} properly 'exports' all its packages containing + * public API. + */ +public class ExportedPackagesTest { + /** Tests package {@code com.google.gson} */ + @Test + public void testMainPackage() { + Gson gson = new Gson(); + assertThat(gson.toJson(1)).isEqualTo("1"); + } + + /** Tests package {@code com.google.gson.annotations} */ + @Test + public void testAnnotationsPackage() throws Exception { + class Annotated { + @SerializedName("custom-name") + int i; + } + + Field field = Annotated.class.getDeclaredField("i"); + SerializedName annotation = field.getAnnotation(SerializedName.class); + assertThat(annotation.value()).isEqualTo("custom-name"); + } + + /** Tests package {@code com.google.gson.reflect} */ + @Test + public void testReflectPackage() { + var typeToken = TypeToken.get(String.class); + assertThat(typeToken.getRawType()).isEqualTo(String.class); + } + + /** Tests package {@code com.google.gson.stream} */ + @Test + public void testStreamPackage() throws IOException { + JsonReader jsonReader = new JsonReader(new StringReader("2")); + assertThat(jsonReader.nextInt()).isEqualTo(2); + } + + /** Verifies that Gson packages are only 'exported' but not 'opened' for reflection. */ + @Test + public void testReflectionInternalField() throws Exception { + Gson gson = new Gson(); + + // Get an arbitrary non-public instance field + Field field = + Arrays.stream(Gson.class.getDeclaredFields()) + .filter( + f -> !Modifier.isStatic(f.getModifiers()) && !Modifier.isPublic(f.getModifiers())) + .findFirst() + .get(); + assertThrows(InaccessibleObjectException.class, () -> field.setAccessible(true)); + assertThrows(IllegalAccessException.class, () -> field.get(gson)); + } + + @Test + public void testInaccessiblePackage() throws Exception { + // Note: In case this class is renamed / removed, can change this to any other internal class + Class internalClass = Class.forName("com.google.gson.internal.LinkedTreeMap"); + assertThat(Modifier.isPublic(internalClass.getModifiers())).isTrue(); + // Get the public constructor + Constructor constructor = internalClass.getConstructor(); + assertThrows(InaccessibleObjectException.class, () -> constructor.setAccessible(true)); + assertThrows(IllegalAccessException.class, () -> constructor.newInstance()); + } +} diff --git a/jpms-test/src/test/java/com/google/gson/jpms_test/ModuleTest.java b/jpms-test/src/test/java/com/google/gson/jpms_test/ModuleTest.java new file mode 100644 index 0000000000..14e2917b77 --- /dev/null +++ b/jpms-test/src/test/java/com/google/gson/jpms_test/ModuleTest.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2024 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.jpms_test; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.gson.Gson; +import java.lang.module.ModuleDescriptor; +import java.lang.module.ModuleDescriptor.Exports; +import java.lang.module.ModuleDescriptor.Requires; +import java.net.URL; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.Test; + +/** + * Verifies that this test project is properly set up and has a module descriptor, and checks Gson's + * module descriptor. + */ +public class ModuleTest { + @Test + public void testOwnModule() { + Module module = getClass().getModule(); + assertThat(module.getName()).isEqualTo("com.google.gson.jpms_test"); + } + + @Test + public void testGsonModule() { + // Verify that this test actually loads the final Gson JAR, and not only compiled classes + // Note: This might fail when run from the IDE, but should succeed when run with Maven from + // command line + URL gsonLocation = Gson.class.getProtectionDomain().getCodeSource().getLocation(); + assertThat(gsonLocation.getPath()).containsMatch("gson/target/gson-[^/]+\\.jar"); + + Module module = Gson.class.getModule(); + ModuleDescriptor moduleDescriptor = module.getDescriptor(); + + assertThat(moduleDescriptor.name()).isEqualTo("com.google.gson"); + // Permit `Modifier.SYNTHETIC`; current versions of Moditect seem to set that + assertThat(moduleDescriptor.modifiers()) + .containsNoneOf( + ModuleDescriptor.Modifier.AUTOMATIC, + ModuleDescriptor.Modifier.MANDATED, + ModuleDescriptor.Modifier.OPEN); + // Should have implicitly included the Maven project version + assertThat(moduleDescriptor.rawVersion()).isPresent(); + + Set moduleRequires = moduleDescriptor.requires(); + assertThat(getModuleDependencies(moduleRequires)) + .containsExactly("com.google.errorprone.annotations", "java.sql", "jdk.unsupported"); + assertThat(getTransitiveModuleDependencies(moduleRequires)).isEmpty(); + assertThat(getOptionalModuleDependencies(moduleRequires)) + .containsExactly("com.google.errorprone.annotations", "java.sql", "jdk.unsupported"); + + Set packageExports = moduleDescriptor.exports(); + assertThat(packageExports.stream().map(Exports::source)) + .containsExactly( + "com.google.gson", + "com.google.gson.annotations", + "com.google.gson.reflect", + "com.google.gson.stream"); + // Gson currently does not export packages to specific modules only + assertThat(packageExports.stream().filter(Exports::isQualified)).isEmpty(); + + // Gson does not allow access to its implementation details using reflection + assertThat(moduleDescriptor.opens()).isEmpty(); + // Gson currently does not use or provide any services + assertThat(moduleDescriptor.uses()).isEmpty(); + assertThat(moduleDescriptor.provides()).isEmpty(); + // Gson has no main class + assertThat(moduleDescriptor.mainClass()).isEmpty(); + } + + private static Stream filterImplicitRequires(Set requires) { + return requires.stream() + .filter( + r -> + !r.modifiers().contains(Requires.Modifier.MANDATED) + && !r.modifiers().contains(Requires.Modifier.SYNTHETIC)); + } + + private static Set getModuleDependencies(Set requires) { + return filterImplicitRequires(requires).map(Requires::name).collect(Collectors.toSet()); + } + + private static Set getTransitiveModuleDependencies(Set requires) { + return filterImplicitRequires(requires) + .filter(r -> r.modifiers().contains(Requires.Modifier.TRANSITIVE)) + .map(Requires::name) + .collect(Collectors.toSet()); + } + + private static Set getOptionalModuleDependencies(Set requires) { + return filterImplicitRequires(requires) + .filter(r -> r.modifiers().contains(Requires.Modifier.STATIC)) + .map(Requires::name) + .collect(Collectors.toSet()); + } +} diff --git a/jpms-test/src/test/java/com/google/gson/jpms_test/ReflectionInaccessibleTest.java b/jpms-test/src/test/java/com/google/gson/jpms_test/ReflectionInaccessibleTest.java new file mode 100644 index 0000000000..0358b78df9 --- /dev/null +++ b/jpms-test/src/test/java/com/google/gson/jpms_test/ReflectionInaccessibleTest.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2024 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.jpms_test; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.gson.Gson; +import com.google.gson.JsonIOException; +import org.junit.Test; + +/** + * Verifies that Gson cannot use reflection for classes in a package if it has not been 'opened' to + * the Gson module in the {@code module-info.java} of the project. + */ +public class ReflectionInaccessibleTest { + private static class MyClass { + @SuppressWarnings("unused") + int i; + } + + @Test + public void testDeserialization() { + Gson gson = new Gson(); + var e = assertThrows(JsonIOException.class, () -> gson.fromJson("{\"i\":1}", MyClass.class)); + assertThat(e) + .hasMessageThat() + .isEqualTo( + "Failed making field '" + + MyClass.class.getName() + + "#i' accessible; either increase its visibility or write a custom TypeAdapter for" + + " its declaring type.\n" + + "See https://github.com/google/gson/blob/main/Troubleshooting.md#reflection-inaccessible-to-module-gson"); + } + + @Test + public void testSerialization() { + Gson gson = new Gson(); + + MyClass obj = new MyClass(); + obj.i = 1; + var e = assertThrows(JsonIOException.class, () -> gson.toJson(obj)); + assertThat(e) + .hasMessageThat() + .isEqualTo( + "Failed making field '" + + MyClass.class.getName() + + "#i' accessible; either increase its visibility or write a custom TypeAdapter for" + + " its declaring type.\n" + + "See https://github.com/google/gson/blob/main/Troubleshooting.md#reflection-inaccessible-to-module-gson"); + } +} diff --git a/jpms-test/src/test/java/com/google/gson/jpms_test/opened/ReflectionTest.java b/jpms-test/src/test/java/com/google/gson/jpms_test/opened/ReflectionTest.java new file mode 100644 index 0000000000..140472cfa8 --- /dev/null +++ b/jpms-test/src/test/java/com/google/gson/jpms_test/opened/ReflectionTest.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2024 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. + */ + +// This package is opened for reflection, see `module-info.java` +package com.google.gson.jpms_test.opened; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.gson.Gson; +import org.junit.Test; + +/** + * Verifies that Gson can use reflection for classes in a package if it has been 'opened' to the + * Gson module in the {@code module-info.java} of the project. + */ +public class ReflectionTest { + private static class MyClass { + int i; + } + + @Test + public void testDeserialization() { + Gson gson = new Gson(); + MyClass deserialized = gson.fromJson("{\"i\":1}", MyClass.class); + assertThat(deserialized.i).isEqualTo(1); + } + + @Test + public void testSerialization() { + Gson gson = new Gson(); + + MyClass obj = new MyClass(); + obj.i = 1; + String serialized = gson.toJson(obj); + assertThat(serialized).isEqualTo("{\"i\":1}"); + } +} diff --git a/jpms-test/src/test/java/module-info.java b/jpms-test/src/test/java/module-info.java new file mode 100644 index 0000000000..b4ecf2d1f4 --- /dev/null +++ b/jpms-test/src/test/java/module-info.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2024 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. + */ + +@SuppressWarnings("requires-automatic") // for automatic module names for 'junit' and 'truth' +module com.google.gson.jpms_test { + requires com.google.gson; + + // Test dependencies + requires junit; + requires truth; // has no proper module name yet, see https://github.com/google/truth/issues/605 + + opens com.google.gson.jpms_test to + junit; + opens com.google.gson.jpms_test.opened to + junit, + com.google.gson; +} diff --git a/metrics/pom.xml b/metrics/pom.xml index 325ab891db..09091795f4 100644 --- a/metrics/pom.xml +++ b/metrics/pom.xml @@ -28,9 +28,8 @@ Performance Metrics for Google Gson library - - - 2024-05-19T18:54:10Z + + true @@ -63,37 +62,6 @@
- - - - - com.github.siom79.japicmp - japicmp-maven-plugin - - - true - - - - org.codehaus.mojo - animal-sniffer-maven-plugin - - - true - - - - org.apache.maven.plugins - maven-deploy-plugin - - - true - - - - - - Inderjeet Singh diff --git a/pom.xml b/pom.xml index 61fdf48279..f755076592 100644 --- a/pom.xml +++ b/pom.xml @@ -28,6 +28,7 @@ gson + jpms-test graal-native-image-test shrinker-test extras @@ -43,6 +44,12 @@ 2024-05-19T18:54:10Z + + + + false + + ${gson.isTestModule} 11 @@ -340,16 +349,33 @@ org.apache.maven.plugins maven-install-plugin 3.1.2 + + ${gson.isTestModule} + org.apache.maven.plugins maven-source-plugin 3.3.1 + + ${gson.isTestModule} + org.apache.maven.plugins maven-gpg-plugin 3.2.4 + + ${gson.isTestModule} + + + + org.apache.maven.plugins + maven-deploy-plugin + 3.1.2 + + ${gson.isInternalModule} + org.apache.maven.plugins @@ -444,6 +470,8 @@ japicmp-maven-plugin 0.21.1 + ${gson.isTestModule} + ${project.groupId} @@ -485,6 +513,8 @@ check + ${gson.isTestModule} + - 4.0.0 com.google.code.gson @@ -33,6 +32,9 @@ 2024-05-19T18:54:10Z 4.26.1 + + + true @@ -102,19 +104,6 @@ - - - - - org.apache.maven.plugins - maven-deploy-plugin - - - true - - - - diff --git a/shrinker-test/pom.xml b/shrinker-test/pom.xml index 293fb059a6..10ec6f1f1c 100644 --- a/shrinker-test/pom.xml +++ b/shrinker-test/pom.xml @@ -22,15 +22,14 @@ gson-parent 2.11.1-SNAPSHOT - shrinker-test + Test: Code shrinking (ProGuard / R8) - - - 2024-05-19T18:54:10Z - 8 + + + true @@ -61,35 +60,6 @@ - - - - com.github.siom79.japicmp - japicmp-maven-plugin - - - true - - - - org.codehaus.mojo - animal-sniffer-maven-plugin - - - true - - - - org.apache.maven.plugins - maven-deploy-plugin - - - true - - - - -