diff --git a/.github/workflows/check-api-compatibility.yml b/.github/workflows/check-api-compatibility.yml index 63d6a95fe..e01af367c 100644 --- a/.github/workflows/check-api-compatibility.yml +++ b/.github/workflows/check-api-compatibility.yml @@ -51,7 +51,7 @@ jobs: - name: Check API compatibility id: check-compatibility run: | - mvn package japicmp:cmp --fail-at-end -Dmaven.test.skip --projects '!metrics,!test-graal-native-image,!test-jpms,!test-shrinker' + mvn package japicmp:cmp --fail-at-end -Dmaven.test.skip --projects '!extras,!metrics,!test-graal-native-image,!test-jpms,!test-shrinker' - name: Upload API differences artifacts uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 diff --git a/extras/src/main/java/com/google/gson/graph/GraphAdapterBuilder.java b/extras/src/main/java/com/google/gson/graph/GraphAdapterBuilder.java deleted file mode 100644 index cd360a4c4..000000000 --- a/extras/src/main/java/com/google/gson/graph/GraphAdapterBuilder.java +++ /dev/null @@ -1,360 +0,0 @@ -/* - * Copyright (C) 2011 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.graph; - -import com.google.errorprone.annotations.CanIgnoreReturnValue; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.InstanceCreator; -import com.google.gson.JsonElement; -import com.google.gson.TypeAdapter; -import com.google.gson.TypeAdapterFactory; -import com.google.gson.internal.ConstructorConstructor; -import com.google.gson.internal.ObjectConstructor; -import com.google.gson.reflect.TypeToken; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonToken; -import com.google.gson.stream.JsonWriter; -import java.io.IOException; -import java.lang.reflect.Type; -import java.util.ArrayDeque; -import java.util.Collections; -import java.util.HashMap; -import java.util.IdentityHashMap; -import java.util.Map; -import java.util.Queue; - -/** - * A builder for constructing a graph-aware type adapter. This class allows you to register types - * for which cyclic references are allowed by serializing the graph of objects as a list of named - * nodes. This approach ensures that objects referencing each other (or themselves) are properly - * serialized and deserialized. - * - *

The builder maintains a mapping between types and their corresponding {@link InstanceCreator} - * instances. When a type is registered, it will be serialized using a graph adapter that assigns a - * unique identifier to each object instance. During deserialization, the graph adapter first builds - * a mapping from these identifiers to their JSON representations and then reconstructs the object - * graph. - * - *

Example usage: - * - *

- *   GraphAdapterBuilder graphBuilder = new GraphAdapterBuilder();
- *   graphBuilder.addType(MyClass.class);
- *
- *   GsonBuilder gsonBuilder = new GsonBuilder();
- *   graphBuilder.registerOn(gsonBuilder);
- *   Gson gson = gsonBuilder.create();
- *
- *   // Serialization
- *   String json = gson.toJson(myObject);
- *
- *   // Deserialization
- *   MyClass deserialized = gson.fromJson(json, MyClass.class);
- * 
- * - * @see Gson - * @see GsonBuilder - */ -public final class GraphAdapterBuilder { - private final Map> instanceCreators; - private final ConstructorConstructor constructorConstructor; - - public GraphAdapterBuilder() { - this.instanceCreators = new HashMap<>(); - this.constructorConstructor = - new ConstructorConstructor(Collections.emptyMap(), true, Collections.emptyList()); - } - - /** - * Registers the specified type with a default instance creator. - * - * @param type the type to register - * @return this builder instance for chaining - */ - @CanIgnoreReturnValue - public GraphAdapterBuilder addType(Type type) { - ObjectConstructor objectConstructor = constructorConstructor.get(TypeToken.get(type)); - InstanceCreator instanceCreator = - new InstanceCreator() { - @Override - public Object createInstance(Type type) { - return objectConstructor.construct(); - } - }; - return addType(type, instanceCreator); - } - - /** - * Registers the specified type with the provided instance creator. - * - * @param type the type to register - * @param instanceCreator the instance creator used to create instances of the type during - * deserialization - * @return this builder instance for chaining - */ - @CanIgnoreReturnValue - public GraphAdapterBuilder addType(Type type, InstanceCreator instanceCreator) { - if (type == null || instanceCreator == null) { - throw new NullPointerException(); - } - instanceCreators.put(type, instanceCreator); - return this; - } - - /** - * Registers the graph adapter on the provided {@link GsonBuilder}. This method adds a {@link - * TypeAdapterFactory} and registers the necessary type adapters for all types previously - * registered via {@link #addType(Type)}. - * - * @param gsonBuilder the {@code GsonBuilder} on which to register the graph adapter - */ - public void registerOn(GsonBuilder gsonBuilder) { - // Create copy to allow reusing GraphAdapterBuilder without affecting adapter factory - Map> instanceCreators = new HashMap<>(this.instanceCreators); - Factory factory = new Factory(instanceCreators); - gsonBuilder.registerTypeAdapterFactory(factory); - for (Map.Entry> entry : instanceCreators.entrySet()) { - gsonBuilder.registerTypeAdapter(entry.getKey(), factory); - } - } - - /** - * A factory that creates type adapters capable of serializing and deserializing object graphs. - * - *

This factory implements both {@link TypeAdapterFactory} and {@link InstanceCreator} - * interfaces. It is responsible for handling cyclic references by assigning unique names to - * objects and managing a graph during both serialization and deserialization. - */ - static class Factory implements TypeAdapterFactory, InstanceCreator { - private final Map> instanceCreators; - - @SuppressWarnings("ThreadLocalUsage") - private final ThreadLocal graphThreadLocal = new ThreadLocal<>(); - - Factory(Map> instanceCreators) { - this.instanceCreators = instanceCreators; - } - - @Override - public TypeAdapter create(Gson gson, TypeToken type) { - if (!instanceCreators.containsKey(type.getType())) { - return null; - } - - TypeAdapter typeAdapter = gson.getDelegateAdapter(this, type); - TypeAdapter elementAdapter = gson.getAdapter(JsonElement.class); - return new TypeAdapter() { - @Override - public void write(JsonWriter out, T value) throws IOException { - if (value == null) { - out.nullValue(); - return; - } - - Graph graph = graphThreadLocal.get(); - boolean writeEntireGraph = false; - - /* - * We have one of two cases: - * 1. We've encountered the first known object in this graph. Write - * out the graph, starting with that object. - * 2. We've encountered another graph object in the course of #1. - * Just write out this object's name. We'll circle back to writing - * out the object's value as a part of #1. - */ - - if (graph == null) { - writeEntireGraph = true; - graph = new Graph(new IdentityHashMap>()); - } - - @SuppressWarnings("unchecked") // graph.map guarantees consistency between value and T - Element element = (Element) graph.map.get(value); - if (element == null) { - element = new Element<>(value, graph.nextName(), typeAdapter, null); - graph.map.put(value, element); - graph.queue.add(element); - } - - if (writeEntireGraph) { - graphThreadLocal.set(graph); - try { - out.beginObject(); - Element current; - while ((current = graph.queue.poll()) != null) { - out.name(current.id); - current.write(out); - } - out.endObject(); - } finally { - graphThreadLocal.remove(); - } - } else { - out.value(element.id); - } - } - - @Override - public T read(JsonReader in) throws IOException { - if (in.peek() == JsonToken.NULL) { - in.nextNull(); - return null; - } - - /* - * Again we have one of two cases: - * 1. We've encountered the first known object in this graph. Read - * the entire graph in as a map from names to their JsonElements. - * Then convert the first JsonElement to its Java object. - * 2. We've encountered another graph object in the course of #1. - * Read in its name, then deserialize its value from the - * JsonElement in our map. We need to do this lazily because we - * don't know which TypeAdapter to use until a value is - * encountered in the wild. - */ - - String currentName = null; - Graph graph = graphThreadLocal.get(); - boolean readEntireGraph = false; - - if (graph == null) { - graph = new Graph(new HashMap>()); - readEntireGraph = true; - - // read the entire tree into memory - in.beginObject(); - while (in.hasNext()) { - String name = in.nextName(); - if (currentName == null) { - currentName = name; - } - JsonElement element = elementAdapter.read(in); - graph.map.put(name, new Element<>(null, name, typeAdapter, element)); - } - in.endObject(); - } else { - currentName = in.nextString(); - } - - if (readEntireGraph) { - graphThreadLocal.set(graph); - } - try { - @SuppressWarnings("unchecked") // graph.map guarantees consistency between value and T - Element element = (Element) graph.map.get(currentName); - // now that we know the typeAdapter for this name, go from JsonElement to 'T' - if (element.value == null) { - element.typeAdapter = typeAdapter; - element.read(graph); - } - return element.value; - } finally { - if (readEntireGraph) { - graphThreadLocal.remove(); - } - } - } - }; - } - - /** - * Hook for the graph adapter to get a reference to a deserialized value before that value is - * fully populated. This is useful to deserialize values that directly or indirectly reference - * themselves: we can hand out an instance before read() returns. - * - *

Gson should only ever call this method when we're expecting it to; that is only when we've - * called back into Gson to deserialize a tree. - */ - @Override - public Object createInstance(Type type) { - Graph graph = graphThreadLocal.get(); - if (graph == null || graph.nextCreate == null) { - throw new IllegalStateException("Unexpected call to createInstance() for " + type); - } - InstanceCreator creator = instanceCreators.get(type); - Object result = creator.createInstance(type); - graph.nextCreate.value = result; - graph.nextCreate = null; - return result; - } - } - - static class Graph { - /** - * The graph elements. On serialization keys are objects (using an identity hash map) and on - * deserialization keys are the string names (using a standard hash map). - */ - private final Map> map; - - /** The queue of elements to write during serialization. Unused during deserialization. */ - private final Queue> queue = new ArrayDeque<>(); - - /** - * The instance currently being deserialized. Used as a backdoor between the graph traversal - * (which needs to know instances) and instance creators which create them. - */ - private Element nextCreate; - - private Graph(Map> map) { - this.map = map; - } - - /** Returns a unique name for an element to be inserted into the graph. */ - public String nextName() { - return "0x" + Integer.toHexString(map.size() + 1); - } - } - - /** An element of the graph during serialization or deserialization. */ - static class Element { - /** This element's name in the top level graph object. */ - private final String id; - - /** The value if known. During deserialization this is lazily populated. */ - private T value; - - /** This element's type adapter if known. During deserialization this is lazily populated. */ - private TypeAdapter typeAdapter; - - /** The element to deserialize. Unused in serialization. */ - private final JsonElement element; - - Element(T value, String id, TypeAdapter typeAdapter, JsonElement element) { - this.value = value; - this.id = id; - this.typeAdapter = typeAdapter; - this.element = element; - } - - void write(JsonWriter out) throws IOException { - typeAdapter.write(out, value); - } - - @SuppressWarnings("unchecked") - void read(Graph graph) { - if (graph.nextCreate != null) { - throw new IllegalStateException("Unexpected recursive call to read() for " + id); - } - graph.nextCreate = (Element) this; - value = typeAdapter.fromJsonTree(element); - if (value == null) { - throw new IllegalStateException("non-null value deserialized to null: " + element); - } - } - } -} diff --git a/extras/src/test/java/com/google/gson/graph/GraphAdapterBuilderTest.java b/extras/src/test/java/com/google/gson/graph/GraphAdapterBuilderTest.java deleted file mode 100644 index 90612a016..000000000 --- a/extras/src/test/java/com/google/gson/graph/GraphAdapterBuilderTest.java +++ /dev/null @@ -1,265 +0,0 @@ -/* - * Copyright (C) 2011 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.graph; - -import static com.google.common.truth.Truth.assertThat; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.reflect.TypeToken; -import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.List; -import org.junit.Test; - -public final class GraphAdapterBuilderTest { - @Test - public void testSerialization() { - Roshambo rock = new Roshambo("ROCK"); - Roshambo scissors = new Roshambo("SCISSORS"); - Roshambo paper = new Roshambo("PAPER"); - rock.beats = scissors; - scissors.beats = paper; - paper.beats = rock; - - GsonBuilder gsonBuilder = new GsonBuilder(); - new GraphAdapterBuilder().addType(Roshambo.class).registerOn(gsonBuilder); - Gson gson = gsonBuilder.create(); - - assertThat(gson.toJson(rock).replace('"', '\'')) - .isEqualTo( - "{'0x1':{'name':'ROCK','beats':'0x2'}," - + "'0x2':{'name':'SCISSORS','beats':'0x3'}," - + "'0x3':{'name':'PAPER','beats':'0x1'}}"); - } - - @Test - public void testDeserialization() { - String json = - "{'0x1':{'name':'ROCK','beats':'0x2'}," - + "'0x2':{'name':'SCISSORS','beats':'0x3'}," - + "'0x3':{'name':'PAPER','beats':'0x1'}}"; - - GsonBuilder gsonBuilder = new GsonBuilder(); - new GraphAdapterBuilder().addType(Roshambo.class).registerOn(gsonBuilder); - Gson gson = gsonBuilder.create(); - - Roshambo rock = gson.fromJson(json, Roshambo.class); - assertThat(rock.name).isEqualTo("ROCK"); - Roshambo scissors = rock.beats; - assertThat(scissors.name).isEqualTo("SCISSORS"); - Roshambo paper = scissors.beats; - assertThat(paper.name).isEqualTo("PAPER"); - assertThat(paper.beats).isSameInstanceAs(rock); - } - - @Test - public void testDeserializationDirectSelfReference() { - String json = "{'0x1':{'name':'SUICIDE','beats':'0x1'}}"; - - GsonBuilder gsonBuilder = new GsonBuilder(); - new GraphAdapterBuilder().addType(Roshambo.class).registerOn(gsonBuilder); - Gson gson = gsonBuilder.create(); - - Roshambo suicide = gson.fromJson(json, Roshambo.class); - assertThat(suicide.name).isEqualTo("SUICIDE"); - assertThat(suicide.beats).isSameInstanceAs(suicide); - } - - @Test - public void testAddTypeCustomInstanceCreator() { - GsonBuilder gsonBuilder = new GsonBuilder(); - new GraphAdapterBuilder() - .addType(Company.class, type -> new Company("custom")) - .addType(Employee.class) - .registerOn(gsonBuilder); - Gson gson = gsonBuilder.create(); - - Company company = - gson.fromJson( - "{'0x1':{'employees':['0x2']},'0x2':{'name':'Jesse','company':'0x1'}}", Company.class); - assertThat(company.name).isEqualTo("custom"); - Employee employee = company.employees.get(0); - assertThat(employee.name).isEqualTo("Jesse"); - assertThat(employee.company).isSameInstanceAs(company); - } - - @Test - public void testAddTypeOverwrite() { - GsonBuilder gsonBuilder = new GsonBuilder(); - new GraphAdapterBuilder() - .addType(Company.class, type -> new Company("custom")) - // Overwrite Company creator with different custom one - .addType(Company.class, type -> new Company("custom-2")) - .addType(Employee.class) - .registerOn(gsonBuilder); - Gson gson = gsonBuilder.create(); - - Company company = gson.fromJson("{'0x1':{}}", Company.class); - assertThat(company.name).isEqualTo("custom-2"); - - gsonBuilder = new GsonBuilder(); - new GraphAdapterBuilder() - .addType(Company.class, type -> new Company("custom")) - // Overwrite Company creator with default one - .addType(Company.class) - .addType(Employee.class) - .registerOn(gsonBuilder); - gson = gsonBuilder.create(); - - company = gson.fromJson("{'0x1':{}}", Company.class); - assertThat(company.name).isNull(); - } - - @Test - public void testSerializeListOfLists() { - Type listOfListsType = new TypeToken>>() {}.getType(); - Type listOfAnyType = new TypeToken>() {}.getType(); - - List> listOfLists = new ArrayList<>(); - listOfLists.add(listOfLists); - listOfLists.add(new ArrayList<>()); - - GsonBuilder gsonBuilder = new GsonBuilder(); - new GraphAdapterBuilder() - .addType(listOfListsType) - .addType(listOfAnyType) - .registerOn(gsonBuilder); - Gson gson = gsonBuilder.create(); - - String json = gson.toJson(listOfLists, listOfListsType); - assertThat(json.replace('"', '\'')).isEqualTo("{'0x1':['0x1','0x2'],'0x2':[]}"); - } - - @Test - public void testDeserializeListOfLists() { - Type listOfAnyType = new TypeToken>() {}.getType(); - Type listOfListsType = new TypeToken>>() {}.getType(); - - GsonBuilder gsonBuilder = new GsonBuilder(); - new GraphAdapterBuilder() - .addType(listOfListsType) - .addType(listOfAnyType) - .registerOn(gsonBuilder); - Gson gson = gsonBuilder.create(); - - List> listOfLists = gson.fromJson("{'0x1':['0x1','0x2'],'0x2':[]}", listOfListsType); - assertThat(listOfLists).hasSize(2); - assertThat(listOfLists.get(0)).isSameInstanceAs(listOfLists); - assertThat(listOfLists.get(1)).isEmpty(); - } - - @Test - public void testSerializationWithMultipleTypes() { - Company google = new Company("Google"); - // Employee constructor adds `this` to the given Company object - Employee unused1 = new Employee("Jesse", google); - Employee unused2 = new Employee("Joel", google); - - GsonBuilder gsonBuilder = new GsonBuilder(); - new GraphAdapterBuilder() - .addType(Company.class) - .addType(Employee.class) - .registerOn(gsonBuilder); - Gson gson = gsonBuilder.create(); - - assertThat(gson.toJson(google).replace('"', '\'')) - .isEqualTo( - "{'0x1':{'name':'Google','employees':['0x2','0x3']}," - + "'0x2':{'name':'Jesse','company':'0x1'}," - + "'0x3':{'name':'Joel','company':'0x1'}}"); - } - - @Test - public void testDeserializationWithMultipleTypes() { - GsonBuilder gsonBuilder = new GsonBuilder(); - new GraphAdapterBuilder() - .addType(Company.class) - .addType(Employee.class) - .registerOn(gsonBuilder); - Gson gson = gsonBuilder.create(); - - String json = - "{'0x1':{'name':'Google','employees':['0x2','0x3']}," - + "'0x2':{'name':'Jesse','company':'0x1'}," - + "'0x3':{'name':'Joel','company':'0x1'}}"; - Company company = gson.fromJson(json, Company.class); - assertThat(company.name).isEqualTo("Google"); - Employee jesse = company.employees.get(0); - assertThat(jesse.name).isEqualTo("Jesse"); - assertThat(jesse.company).isSameInstanceAs(company); - Employee joel = company.employees.get(1); - assertThat(joel.name).isEqualTo("Joel"); - assertThat(joel.company).isSameInstanceAs(company); - } - - @Test - public void testBuilderReuse() { - GsonBuilder gsonBuilder = new GsonBuilder(); - GraphAdapterBuilder graphAdapterBuilder = - new GraphAdapterBuilder() - .addType(Company.class, type -> new Company("custom")) - .addType(Employee.class); - graphAdapterBuilder.registerOn(gsonBuilder); - Gson gson = gsonBuilder.create(); - - Company company = gson.fromJson("{'0x1':{}}", Company.class); - assertThat(company.name).isEqualTo("custom"); - - GsonBuilder gsonBuilder2 = new GsonBuilder(); - // Reuse builder and overwrite creator - graphAdapterBuilder.addType(Company.class, type -> new Company("custom-2")); - graphAdapterBuilder.registerOn(gsonBuilder2); - Gson gson2 = gsonBuilder2.create(); - - company = gson2.fromJson("{'0x1':{}}", Company.class); - assertThat(company.name).isEqualTo("custom-2"); - - // But first adapter should not have been affected - company = gson.fromJson("{'0x1':{}}", Company.class); - assertThat(company.name).isEqualTo("custom"); - } - - static class Roshambo { - String name; - Roshambo beats; - - Roshambo(String name) { - this.name = name; - } - } - - static class Employee { - final String name; - final Company company; - - Employee(String name, Company company) { - this.name = name; - this.company = company; - this.company.employees.add(this); - } - } - - static class Company { - final String name; - final List employees = new ArrayList<>(); - - Company(String name) { - this.name = name; - } - } -}