mirror of
https://github.com/fankes/moshi.git
synced 2025-10-18 23:49:21 +08:00
Merge pull request #21 from square/jwilson_0324_circles
JsonAdapterFactories can lookup types they create.
This commit is contained in:
@@ -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)) {
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
153
moshi/src/test/java/com/squareup/moshi/CircularAdaptersTest.java
Normal file
153
moshi/src/test/java/com/squareup/moshi/CircularAdaptersTest.java
Normal 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");
|
||||
}
|
||||
}
|
@@ -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());
|
||||
|
@@ -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;
|
||||
|
||||
|
@@ -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.
|
||||
|
Reference in New Issue
Block a user