Implement all but the most difficult part of graph type adapter's deserialization. The catch is we want to return an instance that we don't have yet. It's on the stack, but we don't have a handle to it because it's inside the 'nextTypeAdapter' who is busy populating its fields.

This commit is contained in:
Jesse Wilson 2011-12-30 08:27:24 +00:00
parent 6ec6caa49d
commit 4057b98bab
9 changed files with 156 additions and 56 deletions

View File

@ -17,14 +17,18 @@
package com.google.gson.graph;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.internal.bind.JsonTreeReader;
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.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.LinkedList;
@ -55,6 +59,7 @@ public final class GraphTypeAdapterFactory implements TypeAdapterFactory {
}
final TypeAdapter<T> typeAdapter = gson.getNextAdapter(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) {
@ -63,62 +68,141 @@ public final class GraphTypeAdapterFactory implements TypeAdapterFactory {
}
Graph graph = graphThreadLocal.get();
boolean writeEntireGraph = false;
// this is the top-level object in the graph; write the whole graph recursively
if (graph == null) {
graph = new Graph();
graphThreadLocal.set(graph);
Element element = new Element<T>(value, graph.elements.size() + 1, typeAdapter);
graph.elements.put(value, element);
writeEntireGraph = true;
graph = new Graph(new IdentityHashMap<Object, Element<?>>());
}
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);
}
out.beginObject();
Element current;
while ((current = graph.queue.poll()) != null) {
out.name(current.getName());
current.write(out);
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();
}
out.endObject();
graphThreadLocal.remove();
// this is an element nested in the graph; just reference it by ID
} else {
Element element = graph.elements.get(value);
if (element == null) {
element = new Element<T>(value, graph.elements.size() + 1, typeAdapter);
graph.elements.put(value, element);
graph.queue.add(element);
}
out.value(element.getName());
out.value(element.id);
}
}
@Override public T read(JsonReader in) throws IOException {
// TODO:
return null;
if (in.peek() == JsonToken.NULL) {
in.nextNull();
return null;
}
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 {
Element<T> element = (Element<T>) graph.map.get(currentName);
if (element.value == null) {
element.typeAdapter = typeAdapter;
element.read();
}
return element.value;
} finally {
if (readEntireGraph) {
graphThreadLocal.remove();
}
}
}
};
}
static class Graph {
private final Map<Object, Element> elements = new IdentityHashMap<Object, Element>();
/**
* 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;
private final Queue<Element> queue = new LinkedList<Element>();
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);
}
}
static class Element<T> {
private final T value;
private final int id;
private final TypeAdapter<T> typeAdapter;
Element(T value, int id, TypeAdapter<T> typeAdapter) {
private final String id;
private T value;
private TypeAdapter<T> typeAdapter;
private final JsonElement element;
private boolean reading = false;
Element(T value, String id, TypeAdapter<T> typeAdapter, JsonElement element) {
this.value = value;
this.id = id;
this.typeAdapter = typeAdapter;
this.element = element;
}
private String getName() {
return "0x" + Integer.toHexString(id);
}
private void write(JsonWriter out) throws IOException {
typeAdapter.write(out, value);
}
private void read() throws IOException {
if (reading) {
// TODO: this currently fails because we don't have the instance we want yet
System.out.println("ALREADY READING " + id);
return;
}
reading = true;
try {
// TODO: use TypeAdapter.fromJsonTree() when that's public
value = typeAdapter.read(new JsonTreeReader(element));
if (value == null) {
throw new IllegalStateException("non-null value deserialized to null: " + element);
}
} finally {
reading = false;
}
}
}
}

View File

@ -25,7 +25,7 @@ import com.google.gson.JsonPrimitive;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.internal.Streams;
import com.google.gson.internal.bind.JsonElementWriter;
import com.google.gson.internal.bind.JsonTreeWriter;
import com.google.gson.internal.bind.JsonTreeReader;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
@ -252,7 +252,7 @@ public final class RuntimeTypeAdapterFactory<T> implements TypeAdapterFactory {
// TODO: remove this when TypeAdapter.toJsonTree() is public
private JsonElement toJsonTree(TypeAdapter<T> delegate, T value) {
try {
JsonElementWriter jsonWriter = new JsonElementWriter();
JsonTreeWriter jsonWriter = new JsonTreeWriter();
jsonWriter.setLenient(true);
delegate.write(jsonWriter, value);
return jsonWriter.get();

View File

@ -21,7 +21,7 @@ import com.google.gson.GsonBuilder;
import junit.framework.TestCase;
public final class GraphTypeAdapterFactoryTest extends TestCase {
public void testBasicCycle() {
public void testSerialization() {
Roshambo rock = new Roshambo("ROCK");
Roshambo scissors = new Roshambo("SCISSORS");
Roshambo paper = new Roshambo("PAPER");
@ -39,6 +39,24 @@ public final class GraphTypeAdapterFactoryTest extends TestCase {
gson.toJson(rock).replace('\"', '\''));
}
public void testDeserialization() {
String json = "{'0x1':{'name':'ROCK','beats':'0x2'}," +
"'0x2':{'name':'SCISSORS','beats':'0x3'}," +
"'0x3':{'name':'PAPER','beats':'0x1'}}";
Gson gson = new GsonBuilder()
.registerTypeAdapterFactory(GraphTypeAdapterFactory.of(Roshambo.class))
.create();
Roshambo rock = gson.fromJson(json, Roshambo.class);
assertEquals("ROCK", rock.name);
Roshambo scissors = rock.beats;
assertEquals("SCISSORS", scissors.name);
Roshambo paper = scissors.beats;
assertEquals("PAPER", paper.name);
assertSame(rock, paper.beats); // TODO: currently fails
}
static class Roshambo {
String name;
Roshambo beats;

View File

@ -26,7 +26,7 @@ import com.google.gson.internal.bind.BigIntegerTypeAdapter;
import com.google.gson.internal.bind.CollectionTypeAdapterFactory;
import com.google.gson.internal.bind.DateTypeAdapter;
import com.google.gson.internal.bind.JsonTreeReader;
import com.google.gson.internal.bind.JsonElementWriter;
import com.google.gson.internal.bind.JsonTreeWriter;
import com.google.gson.internal.bind.MapTypeAdapterFactory;
import com.google.gson.internal.bind.ObjectTypeAdapter;
import com.google.gson.internal.bind.ReflectiveTypeAdapterFactory;
@ -437,7 +437,7 @@ public final class Gson {
* @since 1.4
*/
public JsonElement toJsonTree(Object src, Type typeOfSrc) {
JsonElementWriter writer = new JsonElementWriter();
JsonTreeWriter writer = new JsonTreeWriter();
toJson(src, typeOfSrc, writer);
return writer.get();
}

View File

@ -16,9 +16,8 @@
package com.google.gson;
import com.google.gson.internal.bind.JsonElementWriter;
import com.google.gson.internal.bind.JsonTreeWriter;
import com.google.gson.internal.bind.JsonTreeReader;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
@ -224,7 +223,7 @@ public abstract class TypeAdapter<T> {
*/
/*public*/ final JsonElement toJsonTree(T value) {
try {
JsonElementWriter jsonWriter = new JsonElementWriter();
JsonTreeWriter jsonWriter = new JsonTreeWriter();
jsonWriter.setLenient(true);
write(jsonWriter, value);
return jsonWriter.get();

View File

@ -30,7 +30,7 @@ import java.util.List;
/**
* This writer creates a JsonElement.
*/
public final class JsonElementWriter extends JsonWriter {
public final class JsonTreeWriter extends JsonWriter {
private static final Writer UNWRITABLE_WRITER = new Writer() {
@Override public void write(char[] buffer, int offset, int counter) {
throw new AssertionError();
@ -54,7 +54,7 @@ public final class JsonElementWriter extends JsonWriter {
/** the JSON element constructed by this writer. */
private JsonElement product = JsonNull.INSTANCE; // TODO: is this really what we want?;
public JsonElementWriter() {
public JsonTreeWriter() {
super(UNWRITABLE_WRITER);
}

View File

@ -266,7 +266,7 @@ public final class MapTypeAdapterFactory implements TypeAdapterFactory {
// TODO: remove this when TypeAdapter.toJsonTree() is public
private static <T> JsonElement toJsonTree(TypeAdapter<T> typeAdapter, T value) {
try {
JsonElementWriter jsonWriter = new JsonElementWriter();
JsonTreeWriter jsonWriter = new JsonTreeWriter();
jsonWriter.setLenient(true);
typeAdapter.write(jsonWriter, value);
return jsonWriter.get();

View File

@ -32,7 +32,6 @@ 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.AccessibleObject;
import java.lang.reflect.Field;
import java.lang.reflect.Type;
import java.util.LinkedHashMap;

View File

@ -20,9 +20,9 @@ import com.google.gson.JsonNull;
import java.io.IOException;
import junit.framework.TestCase;
public final class JsonElementWriterTest extends TestCase {
public final class JsonTreeWriterTest extends TestCase {
public void testArray() throws IOException {
JsonElementWriter writer = new JsonElementWriter();
JsonTreeWriter writer = new JsonTreeWriter();
writer.beginArray();
writer.value(1);
writer.value(2);
@ -32,7 +32,7 @@ public final class JsonElementWriterTest extends TestCase {
}
public void testNestedArray() throws IOException {
JsonElementWriter writer = new JsonElementWriter();
JsonTreeWriter writer = new JsonTreeWriter();
writer.beginArray();
writer.beginArray();
writer.endArray();
@ -45,7 +45,7 @@ public final class JsonElementWriterTest extends TestCase {
}
public void testObject() throws IOException {
JsonElementWriter writer = new JsonElementWriter();
JsonTreeWriter writer = new JsonTreeWriter();
writer.beginObject();
writer.name("A").value(1);
writer.name("B").value(2);
@ -54,7 +54,7 @@ public final class JsonElementWriterTest extends TestCase {
}
public void testNestedObject() throws IOException {
JsonElementWriter writer = new JsonElementWriter();
JsonTreeWriter writer = new JsonTreeWriter();
writer.beginObject();
writer.name("A");
writer.beginObject();
@ -70,7 +70,7 @@ public final class JsonElementWriterTest extends TestCase {
}
public void testWriteAfterClose() throws Exception {
JsonElementWriter writer = new JsonElementWriter();
JsonTreeWriter writer = new JsonTreeWriter();
writer.setLenient(true);
writer.beginArray();
writer.value("A");
@ -84,7 +84,7 @@ public final class JsonElementWriterTest extends TestCase {
}
public void testPrematureClose() throws Exception {
JsonElementWriter writer = new JsonElementWriter();
JsonTreeWriter writer = new JsonTreeWriter();
writer.setLenient(true);
writer.beginArray();
try {
@ -95,7 +95,7 @@ public final class JsonElementWriterTest extends TestCase {
}
public void testSerializeNullsFalse() throws IOException {
JsonElementWriter writer = new JsonElementWriter();
JsonTreeWriter writer = new JsonTreeWriter();
writer.setSerializeNulls(false);
writer.beginObject();
writer.name("A");
@ -105,7 +105,7 @@ public final class JsonElementWriterTest extends TestCase {
}
public void testSerializeNullsTrue() throws IOException {
JsonElementWriter writer = new JsonElementWriter();
JsonTreeWriter writer = new JsonTreeWriter();
writer.setSerializeNulls(true);
writer.beginObject();
writer.name("A");
@ -115,12 +115,12 @@ public final class JsonElementWriterTest extends TestCase {
}
public void testEmptyWriter() {
JsonElementWriter writer = new JsonElementWriter();
JsonTreeWriter writer = new JsonTreeWriter();
assertEquals(JsonNull.INSTANCE, writer.get());
}
public void testLenientNansAndInfinities() throws IOException {
JsonElementWriter writer = new JsonElementWriter();
JsonTreeWriter writer = new JsonTreeWriter();
writer.setLenient(true);
writer.beginArray();
writer.value(Double.NaN);
@ -131,7 +131,7 @@ public final class JsonElementWriterTest extends TestCase {
}
public void testStrictNansAndInfinities() throws IOException {
JsonElementWriter writer = new JsonElementWriter();
JsonTreeWriter writer = new JsonTreeWriter();
writer.setLenient(false);
writer.beginArray();
try {
@ -152,7 +152,7 @@ public final class JsonElementWriterTest extends TestCase {
}
public void testStrictBoxedNansAndInfinities() throws IOException {
JsonElementWriter writer = new JsonElementWriter();
JsonTreeWriter writer = new JsonTreeWriter();
writer.setLenient(false);
writer.beginArray();
try {