JsonAdapterFactories can lookup types they create.

Without this, things fail with stack overflow exceptions.
This commit is contained in:
jwilson
2015-03-24 23:52:34 -04:00
parent 50598dd2cb
commit 144f57ad4c
6 changed files with 274 additions and 8 deletions

View File

@@ -48,7 +48,6 @@ final class ClassJsonAdapter<T> extends JsonAdapter<T> {
throw new IllegalArgumentException("cannot serialize abstract class " + rawType.getName());
}
ClassFactory<Object> classFactory = ClassFactory.get(rawType);
Map<String, FieldBinding<?>> fields = new TreeMap<>();
for (Type t = type; t != Object.class; t = Types.getGenericSuperclass(t)) {

View File

@@ -15,6 +15,8 @@
*/
package com.squareup.moshi;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Type;
import java.util.ArrayList;
@@ -26,13 +28,16 @@ import java.util.List;
*/
public final class Moshi {
private final List<JsonAdapter.Factory> factories;
private final ThreadLocal<List<DeferredAdapter<?>>> reentrantCalls = new ThreadLocal<>();
private Moshi(Builder builder) {
List<JsonAdapter.Factory> factories = new ArrayList<JsonAdapter.Factory>();
List<JsonAdapter.Factory> factories = new ArrayList<>();
factories.addAll(builder.factories);
factories.add(StandardJsonAdapters.FACTORY);
factories.add(CollectionJsonAdapter.FACTORY);
factories.add(MapJsonAdapter.FACTORY);
factories.add(ArrayJsonAdapter.FACTORY);
factories.add(ClassJsonAdapter.FACTORY);
this.factories = Collections.unmodifiableList(factories);
}
@@ -59,17 +64,43 @@ public final class Moshi {
@SuppressWarnings("unchecked") // Factories are required to return only matching JsonAdapters.
private <T> JsonAdapter<T> createAdapter(
int firstIndex, Type type, AnnotatedElement annotations) {
for (int i = firstIndex, size = factories.size(); i < size; i++) {
JsonAdapter<?> result = factories.get(i).create(type, annotations, this);
if (result != null) return (JsonAdapter<T>) result;
List<DeferredAdapter<?>> deferredAdapters = reentrantCalls.get();
if (deferredAdapters == null) {
deferredAdapters = new ArrayList<>();
reentrantCalls.set(deferredAdapters);
} else if (firstIndex == 0) {
// If this is a regular adapter lookup, check that this isn't a reentrant call.
for (DeferredAdapter<?> deferredAdapter : deferredAdapters) {
if (deferredAdapter.type.equals(type) && deferredAdapter.annotations.equals(annotations)) {
return (JsonAdapter<T>) deferredAdapter;
}
}
}
DeferredAdapter<T> deferredAdapter = new DeferredAdapter<>(type, annotations);
deferredAdapters.add(deferredAdapter);
try {
for (int i = firstIndex, size = factories.size(); i < size; i++) {
JsonAdapter<T> result = (JsonAdapter<T>) factories.get(i).create(type, annotations, this);
if (result != null) {
deferredAdapter.ready(result);
return result;
}
}
} finally {
deferredAdapters.remove(deferredAdapters.size() - 1);
}
throw new IllegalArgumentException("no JsonAdapter for " + type);
}
public static final class Builder {
private final List<JsonAdapter.Factory> factories = new ArrayList<JsonAdapter.Factory>();
private final List<JsonAdapter.Factory> factories = new ArrayList<>();
public <T> Builder add(final Type type, final JsonAdapter<T> jsonAdapter) {
if (type == null) throw new IllegalArgumentException("type == null");
if (jsonAdapter == null) throw new IllegalArgumentException("jsonAdapter == null");
return add(new JsonAdapter.Factory() {
@Override public JsonAdapter<?> create(
Type targetType, AnnotatedElement annotations, Moshi moshi) {
@@ -78,6 +109,22 @@ public final class Moshi {
});
}
public <T> Builder add(final Type type, final Class<? extends Annotation> annotation,
final JsonAdapter<T> jsonAdapter) {
if (type == null) throw new IllegalArgumentException("type == null");
if (annotation == null) throw new IllegalArgumentException("annotation == null");
if (jsonAdapter == null) throw new IllegalArgumentException("jsonAdapter == null");
return add(new JsonAdapter.Factory() {
@Override public JsonAdapter<?> create(
Type targetType, AnnotatedElement annotations, Moshi moshi) {
return Util.typesMatch(type, targetType) && annotations.isAnnotationPresent(annotation)
? jsonAdapter
: null;
}
});
}
public Builder add(JsonAdapter.Factory jsonAdapter) {
// TODO: define precedence order. Last added wins? First added wins?
factories.add(jsonAdapter);
@@ -88,4 +135,41 @@ public final class Moshi {
return new Moshi(this);
}
}
/**
* Sometimes a type adapter factory depends on its own product; either directly or indirectly.
* To make this work, we offer this type adapter stub while the final adapter is being computed.
* When it is ready, we wire this to delegate to that finished adapter.
*
* <p>Typically this is necessary in self-referential object models, such as an {@code Employee}
* class that has a {@code List<Employee>} field for an organization's management hierarchy.
*/
private static class DeferredAdapter<T> extends JsonAdapter<T> {
private Type type;
private AnnotatedElement annotations;
private JsonAdapter<T> delegate;
public DeferredAdapter(Type type, AnnotatedElement annotations) {
this.type = type;
this.annotations = annotations;
}
public void ready(JsonAdapter<T> delegate) {
this.delegate = delegate;
// Null out the type and annotations so they can be garbage collected.
this.type = null;
this.annotations = null;
}
@Override public T fromJson(JsonReader reader) throws IOException {
if (delegate == null) throw new IllegalStateException("type adapter isn't ready");
return delegate.fromJson(reader);
}
@Override public void toJson(JsonWriter writer, T value) throws IOException {
if (delegate == null) throw new IllegalStateException("type adapter isn't ready");
delegate.toJson(writer, value);
}
}
}

View File

@@ -0,0 +1,153 @@
/*
* Copyright (C) 2015 Square, 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.squareup.moshi;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Type;
import org.junit.Test;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import static org.assertj.core.api.Assertions.assertThat;
public final class CircularAdaptersTest {
static class Team {
final String lead;
final Project[] projects;
public Team(String lead, Project... projects) {
this.lead = lead;
this.projects = projects;
}
}
static class Project {
final String name;
final Team[] teams;
Project(String name, Team... teams) {
this.name = name;
this.teams = teams;
}
}
@Test public void circularAdapters() throws Exception {
Moshi moshi = new Moshi.Builder().build();
JsonAdapter<Team> teamAdapter = moshi.adapter(Team.class);
Team team = new Team("Alice", new Project("King", new Team("Charlie",
new Project("Delivery", null))));
assertThat(teamAdapter.toJson(team)).isEqualTo("{\"lead\":\"Alice\",\"projects\":[{\"name\":"
+ "\"King\",\"teams\":[{\"lead\":\"Charlie\",\"projects\":[{\"name\":\"Delivery\"}]}]}]}");
Team fromJson = teamAdapter.fromJson("{\"lead\":\"Alice\",\"projects\":[{\"name\":"
+ "\"King\",\"teams\":[{\"lead\":\"Charlie\",\"projects\":[{\"name\":\"Delivery\"}]}]}]}");
assertThat(fromJson.lead).isEqualTo("Alice");
assertThat(fromJson.projects[0].name).isEqualTo("King");
assertThat(fromJson.projects[0].teams[0].lead).isEqualTo("Charlie");
assertThat(fromJson.projects[0].teams[0].projects[0].name).isEqualTo("Delivery");
}
@Retention(RUNTIME)
public @interface Left {
}
@Retention(RUNTIME)
public @interface Right {
}
static class Node {
final String name;
final @Left Node left;
final @Right Node right;
Node(String name, Node left, Node right) {
this.name = name;
this.left = left;
this.right = right;
}
Node plusPrefix(String prefix) {
return new Node(prefix + name, left, right);
}
Node minusPrefix(String prefix) {
if (!name.startsWith(prefix)) throw new IllegalArgumentException();
return new Node(name.substring(prefix.length()), left, right);
}
}
/**
* This factory uses extensive delegation. Each node delegates to this for the left and right
* subtrees, and those delegate to the built-in class adapter to do most of the serialization
* work.
*/
static class PrefixingNodeFactory implements JsonAdapter.Factory {
@Override public JsonAdapter<?> create(Type type, AnnotatedElement annotations, Moshi moshi) {
if (type != Node.class) return null;
final String prefix;
if (annotations.isAnnotationPresent(Left.class)) {
prefix = "L ";
} else if (annotations.isAnnotationPresent(Right.class)) {
prefix = "R ";
} else {
return null;
}
final JsonAdapter<Node> delegate = moshi.nextAdapter(this, Node.class, annotations);
return new JsonAdapter<Node>() {
@Override public void toJson(JsonWriter writer, Node value) throws IOException {
delegate.toJson(writer, value.plusPrefix(prefix));
}
@Override public Node fromJson(JsonReader reader) throws IOException {
Node result = delegate.fromJson(reader);
return result.minusPrefix(prefix);
}
}.nullSafe();
}
}
@Test public void circularAdaptersAndAnnotations() throws Exception {
Moshi moshi = new Moshi.Builder()
.add(new PrefixingNodeFactory())
.build();
JsonAdapter<Node> nodeAdapter = moshi.adapter(Node.class);
Node tree = new Node("C",
new Node("A", null, new Node("B", null, null)),
new Node("D", null, new Node("E", null, null)));
assertThat(nodeAdapter.toJson(tree)).isEqualTo("{"
+ "\"left\":{\"name\":\"L A\",\"right\":{\"name\":\"R B\"}},"
+ "\"name\":\"C\","
+ "\"right\":{\"name\":\"R D\",\"right\":{\"name\":\"R E\"}}"
+ "}");
Node fromJson = nodeAdapter.fromJson("{"
+ "\"left\":{\"name\":\"L A\",\"right\":{\"name\":\"R B\"}},"
+ "\"name\":\"C\","
+ "\"right\":{\"name\":\"R D\",\"right\":{\"name\":\"R E\"}}"
+ "}");
assertThat(fromJson.name).isEqualTo("C");
assertThat(fromJson.left.name).isEqualTo("A");
assertThat(fromJson.left.right.name).isEqualTo("B");
assertThat(fromJson.right.name).isEqualTo("D");
assertThat(fromJson.right.right.name).isEqualTo("E");
}
}

View File

@@ -20,7 +20,7 @@ import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class JsonReaderPathTest {
public final class JsonReaderPathTest {
@Test public void path() throws IOException {
JsonReader reader = new JsonReader("{\"a\":[2,true,false,null,\"b\",{\"c\":\"d\"},[3]]}");
assertEquals("$", reader.getPath());

View File

@@ -552,6 +552,36 @@ public final class MoshiTest {
.isEqualTo(new MealDeal(new Pizza(18, true), "Coke"));
}
static class Message {
String speak;
@Uppercase String shout;
}
@Test public void registerJsonAdapterForAnnotatedType() throws Exception {
JsonAdapter<String> uppercaseAdapter = new JsonAdapter<String>() {
@Override public String fromJson(JsonReader reader) throws IOException {
throw new AssertionError();
}
@Override public void toJson(JsonWriter writer, String value) throws IOException {
writer.value(value.toUpperCase(Locale.US));
}
};
Moshi moshi = new Moshi.Builder()
.add(String.class, Uppercase.class, uppercaseAdapter)
.build();
JsonAdapter<Message> messageAdapter = moshi.adapter(Message.class);
Message message = new Message();
message.speak = "Yo dog";
message.shout = "What's up";
assertThat(messageAdapter.toJson(message))
.isEqualTo("{\"shout\":\"WHAT'S UP\",\"speak\":\"Yo dog\"}");
}
@Uppercase
static String uppercaseString;

View File

@@ -27,7 +27,7 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.fail;
public class TypesTest {
public final class TypesTest {
@Test public void newParameterizedType() throws Exception {
// List<A>. List is a top-level class.