mirror of
https://github.com/fankes/moshi.git
synced 2025-10-19 16:09:21 +08:00
Permit JsonAdapters to be injected in toJson(), fromJson() methods.
Closes: https://github.com/square/moshi/issues/142
This commit is contained in:
@@ -19,11 +19,14 @@ import java.io.IOException;
|
|||||||
import java.lang.annotation.Annotation;
|
import java.lang.annotation.Annotation;
|
||||||
import java.lang.reflect.InvocationTargetException;
|
import java.lang.reflect.InvocationTargetException;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
|
import java.lang.reflect.ParameterizedType;
|
||||||
import java.lang.reflect.Type;
|
import java.lang.reflect.Type;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static com.squareup.moshi.Util.jsonAnnotations;
|
||||||
|
|
||||||
final class AdapterMethodsFactory implements JsonAdapter.Factory {
|
final class AdapterMethodsFactory implements JsonAdapter.Factory {
|
||||||
private final List<AdapterMethod> toAdapters;
|
private final List<AdapterMethod> toAdapters;
|
||||||
private final List<AdapterMethod> fromAdapters;
|
private final List<AdapterMethod> fromAdapters;
|
||||||
@@ -52,6 +55,9 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
|
|||||||
delegate = null;
|
delegate = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (toAdapter != null) toAdapter.bind(moshi);
|
||||||
|
if (fromAdapter != null) fromAdapter.bind(moshi);
|
||||||
|
|
||||||
return new JsonAdapter<Object>() {
|
return new JsonAdapter<Object>() {
|
||||||
@Override public void toJson(JsonWriter writer, Object value) throws IOException {
|
@Override public void toJson(JsonWriter writer, Object value) throws IOException {
|
||||||
if (toAdapter == null) {
|
if (toAdapter == null) {
|
||||||
@@ -61,8 +67,6 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
|
|||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
toAdapter.toJson(moshi, writer, value);
|
toAdapter.toJson(moshi, writer, value);
|
||||||
} catch (IllegalAccessException e) {
|
|
||||||
throw new AssertionError();
|
|
||||||
} catch (InvocationTargetException e) {
|
} catch (InvocationTargetException e) {
|
||||||
Throwable cause = e.getCause();
|
Throwable cause = e.getCause();
|
||||||
if (cause instanceof IOException) throw (IOException) cause;
|
if (cause instanceof IOException) throw (IOException) cause;
|
||||||
@@ -80,8 +84,6 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
|
|||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
return fromAdapter.fromJson(moshi, reader);
|
return fromAdapter.fromJson(moshi, reader);
|
||||||
} catch (IllegalAccessException e) {
|
|
||||||
throw new AssertionError();
|
|
||||||
} catch (InvocationTargetException e) {
|
} catch (InvocationTargetException e) {
|
||||||
Throwable cause = e.getCause();
|
Throwable cause = e.getCause();
|
||||||
if (cause instanceof IOException) throw (IOException) cause;
|
if (cause instanceof IOException) throw (IOException) cause;
|
||||||
@@ -140,34 +142,42 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
|
|||||||
*/
|
*/
|
||||||
static AdapterMethod toAdapter(Object adapter, Method method) {
|
static AdapterMethod toAdapter(Object adapter, Method method) {
|
||||||
method.setAccessible(true);
|
method.setAccessible(true);
|
||||||
Type[] parameterTypes = method.getGenericParameterTypes();
|
|
||||||
final Type returnType = method.getGenericReturnType();
|
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
|
&& parameterTypes[0] == JsonWriter.class
|
||||||
&& returnType == void.class) {
|
&& returnType == void.class
|
||||||
// public void pointToJson(JsonWriter jsonWriter, Point point) throws Exception {
|
&& parametersAreJsonAdapters(2, parameterTypes)) {
|
||||||
Set<? extends Annotation> parameterAnnotations
|
// void pointToJson(JsonWriter jsonWriter, Point point) {
|
||||||
= Util.jsonAnnotations(method.getParameterAnnotations()[1]);
|
// void pointToJson(JsonWriter jsonWriter, Point point, JsonAdapter<?> adapter, ...) {
|
||||||
return new AdapterMethod(parameterTypes[1], parameterAnnotations, adapter, method, false) {
|
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)
|
@Override public void toJson(Moshi moshi, JsonWriter writer, Object value)
|
||||||
throws IOException, InvocationTargetException, IllegalAccessException {
|
throws IOException, InvocationTargetException {
|
||||||
method.invoke(adapter, writer, value);
|
invoke(writer, value);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
} else if (parameterTypes.length == 1 && returnType != void.class) {
|
} else if (parameterTypes.length == 1 && returnType != void.class) {
|
||||||
// public List<Integer> pointToJson(Point point) throws Exception {
|
// List<Integer> pointToJson(Point point) {
|
||||||
final Set<? extends Annotation> returnTypeAnnotations = Util.jsonAnnotations(method);
|
final Set<? extends Annotation> returnTypeAnnotations = jsonAnnotations(method);
|
||||||
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
|
Set<? extends Annotation> qualifierAnnotations = jsonAnnotations(parameterAnnotations[0]);
|
||||||
Set<? extends Annotation> qualifierAnnotations =
|
|
||||||
Util.jsonAnnotations(parameterAnnotations[0]);
|
|
||||||
boolean nullable = Util.hasNullable(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)
|
@Override public void toJson(Moshi moshi, JsonWriter writer, Object value)
|
||||||
throws IOException, InvocationTargetException, IllegalAccessException {
|
throws IOException, InvocationTargetException {
|
||||||
JsonAdapter<Object> delegate = moshi.adapter(returnType, returnTypeAnnotations);
|
Object intermediate = invoke(value);
|
||||||
Object intermediate = method.invoke(adapter, value);
|
|
||||||
delegate.toJson(writer, intermediate);
|
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
|
* Returns an object that calls a {@code method} method on {@code adapter} in service of
|
||||||
* converting an object from JSON.
|
* converting an object from JSON.
|
||||||
*/
|
*/
|
||||||
static AdapterMethod fromAdapter(Object adapter, Method method) {
|
static AdapterMethod fromAdapter(Object adapter, Method method) {
|
||||||
method.setAccessible(true);
|
method.setAccessible(true);
|
||||||
final Type[] parameterTypes = method.getGenericParameterTypes();
|
|
||||||
final Type returnType = method.getGenericReturnType();
|
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
|
&& parameterTypes[0] == JsonReader.class
|
||||||
&& returnType != void.class) {
|
&& returnType != void.class
|
||||||
// public Point pointFromJson(JsonReader jsonReader) throws Exception {
|
&& parametersAreJsonAdapters(1, parameterTypes)) {
|
||||||
Set<? extends Annotation> returnTypeAnnotations = Util.jsonAnnotations(method);
|
// Point pointFromJson(JsonReader jsonReader) {
|
||||||
return new AdapterMethod(returnType, returnTypeAnnotations, adapter, method, false) {
|
// 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)
|
@Override public Object fromJson(Moshi moshi, JsonReader reader)
|
||||||
throws IOException, IllegalAccessException, InvocationTargetException {
|
throws IOException, InvocationTargetException {
|
||||||
return method.invoke(adapter, reader);
|
return invoke(reader);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
} else if (parameterTypes.length == 1 && returnType != void.class) {
|
} else if (parameterTypes.length == 1 && returnType != void.class) {
|
||||||
// public Point pointFromJson(List<Integer> o) throws Exception {
|
// Point pointFromJson(List<Integer> o) {
|
||||||
Set<? extends Annotation> returnTypeAnnotations = Util.jsonAnnotations(method);
|
|
||||||
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
|
|
||||||
final Set<? extends Annotation> qualifierAnnotations
|
final Set<? extends Annotation> qualifierAnnotations
|
||||||
= Util.jsonAnnotations(parameterAnnotations[0]);
|
= jsonAnnotations(parameterAnnotations[0]);
|
||||||
boolean nullable = Util.hasNullable(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)
|
@Override public Object fromJson(Moshi moshi, JsonReader reader)
|
||||||
throws IOException, IllegalAccessException, InvocationTargetException {
|
throws IOException, InvocationTargetException {
|
||||||
JsonAdapter<Object> delegate = moshi.adapter(parameterTypes[0], qualifierAnnotations);
|
|
||||||
Object intermediate = delegate.fromJson(reader);
|
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 Set<? extends Annotation> annotations;
|
||||||
final Object adapter;
|
final Object adapter;
|
||||||
final Method method;
|
final Method method;
|
||||||
|
final int adaptersOffset;
|
||||||
|
final JsonAdapter<?>[] jsonAdapters;
|
||||||
final boolean nullable;
|
final boolean nullable;
|
||||||
|
|
||||||
public AdapterMethod(Type type,
|
public AdapterMethod(Type type, Set<? extends Annotation> annotations, Object adapter,
|
||||||
Set<? extends Annotation> annotations, Object adapter, Method method, boolean nullable) {
|
Method method, int parameterCount, int adaptersOffset, boolean nullable) {
|
||||||
this.type = Types.canonicalize(type);
|
this.type = Types.canonicalize(type);
|
||||||
this.annotations = annotations;
|
this.annotations = annotations;
|
||||||
this.adapter = adapter;
|
this.adapter = adapter;
|
||||||
this.method = method;
|
this.method = method;
|
||||||
|
this.adaptersOffset = adaptersOffset;
|
||||||
|
this.jsonAdapters = new JsonAdapter[parameterCount - adaptersOffset];
|
||||||
this.nullable = nullable;
|
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)
|
public void toJson(Moshi moshi, JsonWriter writer, Object value)
|
||||||
throws IOException, IllegalAccessException, InvocationTargetException {
|
throws IOException, InvocationTargetException {
|
||||||
throw new AssertionError();
|
throw new AssertionError();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Object fromJson(Moshi moshi, JsonReader reader)
|
public Object fromJson(Moshi moshi, JsonReader reader)
|
||||||
throws IOException, IllegalAccessException, InvocationTargetException {
|
throws IOException, InvocationTargetException {
|
||||||
throw new AssertionError();
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -406,6 +406,66 @@ public final class AdapterMethodsTest {
|
|||||||
assertThat(moshi.adapter(c)).isSameAs(moshi.adapter(a));
|
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 {
|
static class Point {
|
||||||
final int x;
|
final int x;
|
||||||
final int y;
|
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 {
|
interface Shape {
|
||||||
String draw();
|
String draw();
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user