aboutsummaryrefslogtreecommitdiffstats
path: root/extras/src/main/java/com/google/gson/graph/GraphAdapterBuilder.java
diff options
context:
space:
mode:
Diffstat (limited to 'extras/src/main/java/com/google/gson/graph/GraphAdapterBuilder.java')
-rw-r--r--extras/src/main/java/com/google/gson/graph/GraphAdapterBuilder.java308
1 files changed, 308 insertions, 0 deletions
diff --git a/extras/src/main/java/com/google/gson/graph/GraphAdapterBuilder.java b/extras/src/main/java/com/google/gson/graph/GraphAdapterBuilder.java
new file mode 100644
index 00000000..cd8ea00f
--- /dev/null
+++ b/extras/src/main/java/com/google/gson/graph/GraphAdapterBuilder.java
@@ -0,0 +1,308 @@
+/*
+ * 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.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.HashMap;
+import java.util.IdentityHashMap;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.Queue;
+
+/**
+ * Writes a graph of objects as a list of named nodes.
+ */
+// TODO: proper documentation
+@SuppressWarnings("rawtypes")
+public final class GraphAdapterBuilder {
+ private final Map<Type, InstanceCreator<?>> instanceCreators;
+ private final ConstructorConstructor constructorConstructor;
+
+ public GraphAdapterBuilder() {
+ this.instanceCreators = new HashMap<Type, InstanceCreator<?>>();
+ this.constructorConstructor = new ConstructorConstructor(instanceCreators);
+ }
+ public GraphAdapterBuilder addType(Type type) {
+ final ObjectConstructor<?> objectConstructor = constructorConstructor.get(TypeToken.get(type));
+ InstanceCreator<Object> instanceCreator = new InstanceCreator<Object>() {
+ public Object createInstance(Type type) {
+ return objectConstructor.construct();
+ }
+ };
+ return addType(type, instanceCreator);
+ }
+
+ public GraphAdapterBuilder addType(Type type, InstanceCreator<?> instanceCreator) {
+ if (type == null || instanceCreator == null) {
+ throw new NullPointerException();
+ }
+ instanceCreators.put(type, instanceCreator);
+ return this;
+ }
+
+ public void registerOn(GsonBuilder gsonBuilder) {
+ Factory factory = new Factory(instanceCreators);
+ gsonBuilder.registerTypeAdapterFactory(factory);
+ for (Map.Entry<Type, InstanceCreator<?>> entry : instanceCreators.entrySet()) {
+ gsonBuilder.registerTypeAdapter(entry.getKey(), factory);
+ }
+ }
+
+ static class Factory implements TypeAdapterFactory, InstanceCreator {
+ private final Map<Type, InstanceCreator<?>> instanceCreators;
+ private final ThreadLocal<Graph> graphThreadLocal = new ThreadLocal<Graph>();
+
+ Factory(Map<Type, InstanceCreator<?>> instanceCreators) {
+ this.instanceCreators = instanceCreators;
+ }
+
+ public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
+ if (!instanceCreators.containsKey(type.getType())) {
+ return null;
+ }
+
+ final TypeAdapter<T> typeAdapter = gson.getDelegateAdapter(this, type);
+ final TypeAdapter<JsonElement> elementAdapter = gson.getAdapter(JsonElement.class);
+ return new TypeAdapter<T>() {
+ @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<Object, Element<?>>());
+ }
+
+ @SuppressWarnings("unchecked") // graph.map guarantees consistency between value and T
+ Element<T> element = (Element<T>) graph.map.get(value);
+ if (element == null) {
+ element = new Element<T>(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<Object, Element<?>>());
+ 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<T>(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<T> element = (Element<T>) 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.
+ *
+ * <p>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.
+ */
+ @SuppressWarnings("unchecked")
+ 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<Object, Element<?>> map;
+
+ /**
+ * The queue of elements to write during serialization. Unused during
+ * deserialization.
+ */
+ private final Queue<Element> queue = new LinkedList<Element>();
+
+ /**
+ * 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<Object, Element<?>> 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<T> {
+ /**
+ * 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<T> typeAdapter;
+
+ /**
+ * The element to deserialize. Unused in serialization.
+ */
+ private final JsonElement element;
+
+ Element(T value, String id, TypeAdapter<T> 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);
+ }
+
+ void read(Graph graph) throws IOException {
+ if (graph.nextCreate != null) {
+ throw new IllegalStateException("Unexpected recursive call to read() for " + id);
+ }
+ graph.nextCreate = this;
+ value = typeAdapter.fromJsonTree(element);
+ if (value == null) {
+ throw new IllegalStateException("non-null value deserialized to null: " + element);
+ }
+ }
+ }
+}