> 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;
- }
- }
-}