New extension: handle circular references.

Serialize graphs of objects by assigning each instance a generated ID and writing the complete graph out as a list. The output for a cycle of Rock/Scissors/Paper looks like this:

{
  '0x1':{'name':'ROCK','beats':'0x2'},
  '0x2':{'name':'SCISSORS','beats':'0x3'},
  '0x3':{'name':'PAPER','beats':'0x1'}
}

This is work towards issue 137. The hard part is going to be deserializing that back into a graph.
This commit is contained in:
Jesse Wilson 2011-12-30 07:34:43 +00:00
parent bcaf56079c
commit 6ec6caa49d
2 changed files with 173 additions and 0 deletions

View File

@ -0,0 +1,124 @@
/*
* 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.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
/**
* Writes a graph of objects as a list of named nodes.
*/
// TODO: proper documentation
public final class GraphTypeAdapterFactory implements TypeAdapterFactory {
private final ThreadLocal<Graph> graphThreadLocal = new ThreadLocal<Graph>();
private final Set<Type> graphTypes;
private GraphTypeAdapterFactory(Type... graphTypes) {
this.graphTypes = new HashSet<Type>();
this.graphTypes.addAll(Arrays.asList(graphTypes));
}
public static GraphTypeAdapterFactory of(Type... graphTypes) {
return new GraphTypeAdapterFactory(graphTypes);
}
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
if (!graphTypes.contains(type.getType())) {
return null;
}
final TypeAdapter<T> typeAdapter = gson.getNextAdapter(this, type);
return new TypeAdapter<T>() {
@Override public void write(JsonWriter out, T value) throws IOException {
if (value == null) {
out.nullValue();
return;
}
Graph graph = graphThreadLocal.get();
// 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);
graph.queue.add(element);
out.beginObject();
Element current;
while ((current = graph.queue.poll()) != null) {
out.name(current.getName());
current.write(out);
}
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());
}
}
@Override public T read(JsonReader in) throws IOException {
// TODO:
return null;
}
};
}
static class Graph {
private final Map<Object, Element> elements = new IdentityHashMap<Object, Element>();
private final Queue<Element> queue = new LinkedList<Element>();
}
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) {
this.value = value;
this.id = id;
this.typeAdapter = typeAdapter;
}
private String getName() {
return "0x" + Integer.toHexString(id);
}
private void write(JsonWriter out) throws IOException {
typeAdapter.write(out, value);
}
}
}

View File

@ -0,0 +1,49 @@
/*
* 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 junit.framework.TestCase;
public final class GraphTypeAdapterFactoryTest extends TestCase {
public void testBasicCycle() {
Roshambo rock = new Roshambo("ROCK");
Roshambo scissors = new Roshambo("SCISSORS");
Roshambo paper = new Roshambo("PAPER");
rock.beats = scissors;
scissors.beats = paper;
paper.beats = rock;
Gson gson = new GsonBuilder()
.registerTypeAdapterFactory(GraphTypeAdapterFactory.of(Roshambo.class))
.create();
assertEquals("{'0x1':{'name':'ROCK','beats':'0x2'}," +
"'0x2':{'name':'SCISSORS','beats':'0x3'}," +
"'0x3':{'name':'PAPER','beats':'0x1'}}",
gson.toJson(rock).replace('\"', '\''));
}
static class Roshambo {
String name;
Roshambo beats;
Roshambo(String name) {
this.name = name;
}
}
}