Merge pull request #197 from square/jwilson.1013.json_adapter_parameters

Permit JsonAdapters to be injected in toJson(), fromJson() methods.
This commit is contained in:
Jesse Wilson
2016-10-14 08:11:16 -04:00
committed by GitHub
2 changed files with 191 additions and 42 deletions

View File

@@ -19,11 +19,14 @@ import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import static com.squareup.moshi.Util.jsonAnnotations;
final class AdapterMethodsFactory implements JsonAdapter.Factory {
private final List<AdapterMethod> toAdapters;
private final List<AdapterMethod> fromAdapters;
@@ -52,6 +55,9 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
delegate = null;
}
if (toAdapter != null) toAdapter.bind(moshi);
if (fromAdapter != null) fromAdapter.bind(moshi);
return new JsonAdapter<Object>() {
@Override public void toJson(JsonWriter writer, Object value) throws IOException {
if (toAdapter == null) {
@@ -61,8 +67,6 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
} else {
try {
toAdapter.toJson(moshi, writer, value);
} catch (IllegalAccessException e) {
throw new AssertionError();
} catch (InvocationTargetException e) {
Throwable cause = e.getCause();
if (cause instanceof IOException) throw (IOException) cause;
@@ -80,8 +84,6 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
} else {
try {
return fromAdapter.fromJson(moshi, reader);
} catch (IllegalAccessException e) {
throw new AssertionError();
} catch (InvocationTargetException e) {
Throwable cause = e.getCause();
if (cause instanceof IOException) throw (IOException) cause;
@@ -140,34 +142,42 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
*/
static AdapterMethod toAdapter(Object adapter, Method method) {
method.setAccessible(true);
Type[] parameterTypes = method.getGenericParameterTypes();
final Type returnType = method.getGenericReturnType();
final Type[] parameterTypes = method.getGenericParameterTypes();
final Annotation[][] parameterAnnotations = method.getParameterAnnotations();
if (parameterTypes.length == 2
if (parameterTypes.length >= 2
&& parameterTypes[0] == JsonWriter.class
&& returnType == void.class) {
// public void pointToJson(JsonWriter jsonWriter, Point point) throws Exception {
Set<? extends Annotation> parameterAnnotations
= Util.jsonAnnotations(method.getParameterAnnotations()[1]);
return new AdapterMethod(parameterTypes[1], parameterAnnotations, adapter, method, false) {
&& returnType == void.class
&& parametersAreJsonAdapters(2, parameterTypes)) {
// void pointToJson(JsonWriter jsonWriter, Point point) {
// void pointToJson(JsonWriter jsonWriter, Point point, JsonAdapter<?> adapter, ...) {
Set<? extends Annotation> qualifierAnnotations = jsonAnnotations(parameterAnnotations[1]);
return new AdapterMethod(parameterTypes[1], qualifierAnnotations, adapter, method,
parameterTypes.length, 2, false) {
@Override public void toJson(Moshi moshi, JsonWriter writer, Object value)
throws IOException, InvocationTargetException, IllegalAccessException {
method.invoke(adapter, writer, value);
throws IOException, InvocationTargetException {
invoke(writer, value);
}
};
} else if (parameterTypes.length == 1 && returnType != void.class) {
// public List<Integer> pointToJson(Point point) throws Exception {
final Set<? extends Annotation> returnTypeAnnotations = Util.jsonAnnotations(method);
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
Set<? extends Annotation> qualifierAnnotations =
Util.jsonAnnotations(parameterAnnotations[0]);
// List<Integer> pointToJson(Point point) {
final Set<? extends Annotation> returnTypeAnnotations = jsonAnnotations(method);
Set<? extends Annotation> qualifierAnnotations = jsonAnnotations(parameterAnnotations[0]);
boolean nullable = Util.hasNullable(parameterAnnotations[0]);
return new AdapterMethod(parameterTypes[0], qualifierAnnotations, adapter, method, nullable) {
return new AdapterMethod(parameterTypes[0], qualifierAnnotations, adapter, method,
parameterTypes.length, 1, nullable) {
private JsonAdapter<Object> delegate;
@Override public void bind(Moshi moshi) {
super.bind(moshi);
delegate = moshi.adapter(returnType, returnTypeAnnotations);
}
@Override public void toJson(Moshi moshi, JsonWriter writer, Object value)
throws IOException, InvocationTargetException, IllegalAccessException {
JsonAdapter<Object> delegate = moshi.adapter(returnType, returnTypeAnnotations);
Object intermediate = method.invoke(adapter, value);
throws IOException, InvocationTargetException {
Object intermediate = invoke(value);
delegate.toJson(writer, intermediate);
}
};
@@ -180,40 +190,58 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
}
}
/** Returns true if {@code parameterTypes[offset..]} contains only JsonAdapters. */
private static boolean parametersAreJsonAdapters(int offset, Type[] parameterTypes) {
for (int i = offset, length = parameterTypes.length; i < length; i++) {
if (!(parameterTypes[i] instanceof ParameterizedType)) return false;
if (((ParameterizedType) parameterTypes[i]).getRawType() != JsonAdapter.class) return false;
}
return true;
}
/**
* Returns an object that calls a {@code method} method on {@code adapter} in service of
* converting an object from JSON.
*/
static AdapterMethod fromAdapter(Object adapter, Method method) {
method.setAccessible(true);
final Type[] parameterTypes = method.getGenericParameterTypes();
final Type returnType = method.getGenericReturnType();
Set<? extends Annotation> returnTypeAnnotations = jsonAnnotations(method);
final Type[] parameterTypes = method.getGenericParameterTypes();
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
if (parameterTypes.length == 1
if (parameterTypes.length >= 1
&& parameterTypes[0] == JsonReader.class
&& returnType != void.class) {
// public Point pointFromJson(JsonReader jsonReader) throws Exception {
Set<? extends Annotation> returnTypeAnnotations = Util.jsonAnnotations(method);
return new AdapterMethod(returnType, returnTypeAnnotations, adapter, method, false) {
&& returnType != void.class
&& parametersAreJsonAdapters(1, parameterTypes)) {
// Point pointFromJson(JsonReader jsonReader) {
// Point pointFromJson(JsonReader jsonReader, JsonAdapter<?> adapter, ...) {
return new AdapterMethod(returnType, returnTypeAnnotations, adapter, method,
parameterTypes.length, 1, false) {
@Override public Object fromJson(Moshi moshi, JsonReader reader)
throws IOException, IllegalAccessException, InvocationTargetException {
return method.invoke(adapter, reader);
throws IOException, InvocationTargetException {
return invoke(reader);
}
};
} else if (parameterTypes.length == 1 && returnType != void.class) {
// public Point pointFromJson(List<Integer> o) throws Exception {
Set<? extends Annotation> returnTypeAnnotations = Util.jsonAnnotations(method);
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
// Point pointFromJson(List<Integer> o) {
final Set<? extends Annotation> qualifierAnnotations
= Util.jsonAnnotations(parameterAnnotations[0]);
= jsonAnnotations(parameterAnnotations[0]);
boolean nullable = Util.hasNullable(parameterAnnotations[0]);
return new AdapterMethod(returnType, returnTypeAnnotations, adapter, method, nullable) {
return new AdapterMethod(returnType, returnTypeAnnotations, adapter, method,
parameterTypes.length, 1, nullable) {
JsonAdapter<Object> delegate;
@Override public void bind(Moshi moshi) {
super.bind(moshi);
delegate = moshi.adapter(parameterTypes[0], qualifierAnnotations);
}
@Override public Object fromJson(Moshi moshi, JsonReader reader)
throws IOException, IllegalAccessException, InvocationTargetException {
JsonAdapter<Object> delegate = moshi.adapter(parameterTypes[0], qualifierAnnotations);
throws IOException, InvocationTargetException {
Object intermediate = delegate.fromJson(reader);
return method.invoke(adapter, intermediate);
return invoke(intermediate);
}
};
@@ -242,25 +270,68 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
final Set<? extends Annotation> annotations;
final Object adapter;
final Method method;
final int adaptersOffset;
final JsonAdapter<?>[] jsonAdapters;
final boolean nullable;
public AdapterMethod(Type type,
Set<? extends Annotation> annotations, Object adapter, Method method, boolean nullable) {
public AdapterMethod(Type type, Set<? extends Annotation> annotations, Object adapter,
Method method, int parameterCount, int adaptersOffset, boolean nullable) {
this.type = Types.canonicalize(type);
this.annotations = annotations;
this.adapter = adapter;
this.method = method;
this.adaptersOffset = adaptersOffset;
this.jsonAdapters = new JsonAdapter[parameterCount - adaptersOffset];
this.nullable = nullable;
}
public void bind(Moshi moshi) {
if (jsonAdapters.length > 0) {
Type[] parameterTypes = method.getGenericParameterTypes();
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
for (int i = adaptersOffset, size = parameterTypes.length; i < size; i++) {
jsonAdapters[i - adaptersOffset] = moshi.adapter(
((ParameterizedType) parameterTypes[i]).getActualTypeArguments()[0],
jsonAnnotations(parameterAnnotations[i]));
}
}
}
public void toJson(Moshi moshi, JsonWriter writer, Object value)
throws IOException, IllegalAccessException, InvocationTargetException {
throws IOException, InvocationTargetException {
throw new AssertionError();
}
public Object fromJson(Moshi moshi, JsonReader reader)
throws IOException, IllegalAccessException, InvocationTargetException {
throws IOException, InvocationTargetException {
throw new AssertionError();
}
/** Invoke the method with one fixed argument, plus any number of JSON adapter arguments. */
protected Object invoke(Object a1) throws InvocationTargetException {
Object[] args = new Object[1 + jsonAdapters.length];
args[0] = a1;
System.arraycopy(jsonAdapters, 0, args, 1, jsonAdapters.length);
try {
return method.invoke(adapter, args);
} catch (IllegalAccessException e) {
throw new AssertionError();
}
}
/** Invoke the method with two fixed arguments, plus any number of JSON adapter arguments. */
protected Object invoke(Object a1, Object a2) throws InvocationTargetException {
Object[] args = new Object[2 + jsonAdapters.length];
args[0] = a1;
args[1] = a2;
System.arraycopy(jsonAdapters, 0, args, 2, jsonAdapters.length);
try {
return method.invoke(adapter, args);
} catch (IllegalAccessException e) {
throw new AssertionError();
}
}
}
}

View File

@@ -406,6 +406,66 @@ public final class AdapterMethodsTest {
assertThat(moshi.adapter(c)).isSameAs(moshi.adapter(a));
}
@Test public void writerAndReaderTakingJsonAdapterParameter() throws Exception {
Moshi moshi = new Moshi.Builder()
.add(new PointWriterAndReaderJsonAdapter())
.add(new JsonAdapterWithWriterAndReaderTakingJsonAdapterParameter())
.build();
JsonAdapter<Line> lineAdapter = moshi.adapter(Line.class);
Line line = new Line(new Point(5, 8), new Point(3, 2));
assertThat(lineAdapter.toJson(line)).isEqualTo("[[5,8],[3,2]]");
assertThat(lineAdapter.fromJson("[[5,8],[3,2]]")).isEqualTo(line);
}
static class JsonAdapterWithWriterAndReaderTakingJsonAdapterParameter {
@ToJson void lineToJson(
JsonWriter writer, Line line, JsonAdapter<Point> pointAdapter) throws IOException {
writer.beginArray();
pointAdapter.toJson(writer, line.a);
pointAdapter.toJson(writer, line.b);
writer.endArray();
}
@FromJson Line lineFromJson(
JsonReader reader, JsonAdapter<Point> pointAdapter) throws Exception {
reader.beginArray();
Point a = pointAdapter.fromJson(reader);
Point b = pointAdapter.fromJson(reader);
reader.endArray();
return new Line(a, b);
}
}
@Test public void noToJsonAdapterTakingJsonAdapterParameter() throws Exception {
try {
new Moshi.Builder().add(new ToJsonAdapterTakingJsonAdapterParameter());
fail();
} catch (IllegalArgumentException expected) {
assertThat(expected).hasMessageStartingWith("Unexpected signature");
}
}
static class ToJsonAdapterTakingJsonAdapterParameter {
@ToJson String lineToJson(Line line, JsonAdapter<Point> pointAdapter) throws IOException {
throw new AssertionError();
}
}
@Test public void noFromJsonAdapterTakingJsonAdapterParameter() throws Exception {
try {
new Moshi.Builder().add(new FromJsonAdapterTakingJsonAdapterParameter());
fail();
} catch (IllegalArgumentException expected) {
assertThat(expected).hasMessageStartingWith("Unexpected signature");
}
}
static class FromJsonAdapterTakingJsonAdapterParameter {
@FromJson Line lineFromJson(String value, JsonAdapter<Point> pointAdapter) throws Exception {
throw new AssertionError();
}
}
static class Point {
final int x;
final int y;
@@ -424,6 +484,24 @@ public final class AdapterMethodsTest {
}
}
static class Line {
final Point a;
final Point b;
public Line(Point a, Point b) {
this.a = a;
this.b = b;
}
@Override public boolean equals(Object o) {
return o instanceof Line && ((Line) o).a.equals(a) && ((Line) o).b.equals(b);
}
@Override public int hashCode() {
return a.hashCode() * 37 + b.hashCode();
}
}
interface Shape {
String draw();
}