diff --git a/moshi/src/main/java/com/squareup/moshi/AdapterMethodsFactory.java b/moshi/src/main/java/com/squareup/moshi/AdapterMethodsFactory.java index cb51422..a979660 100644 --- a/moshi/src/main/java/com/squareup/moshi/AdapterMethodsFactory.java +++ b/moshi/src/main/java/com/squareup/moshi/AdapterMethodsFactory.java @@ -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 toAdapters; private final List 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() { @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 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 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 pointToJson(Point point) throws Exception { - final Set returnTypeAnnotations = Util.jsonAnnotations(method); - Annotation[][] parameterAnnotations = method.getParameterAnnotations(); - Set qualifierAnnotations = - Util.jsonAnnotations(parameterAnnotations[0]); + // List pointToJson(Point point) { + final Set returnTypeAnnotations = jsonAnnotations(method); + Set 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 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 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 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 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 o) throws Exception { - Set returnTypeAnnotations = Util.jsonAnnotations(method); - Annotation[][] parameterAnnotations = method.getParameterAnnotations(); + // Point pointFromJson(List o) { final Set 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 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 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 annotations; final Object adapter; final Method method; + final int adaptersOffset; + final JsonAdapter[] jsonAdapters; final boolean nullable; - public AdapterMethod(Type type, - Set annotations, Object adapter, Method method, boolean nullable) { + public AdapterMethod(Type type, Set 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(); + } + } } } diff --git a/moshi/src/test/java/com/squareup/moshi/AdapterMethodsTest.java b/moshi/src/test/java/com/squareup/moshi/AdapterMethodsTest.java index 3a3b2fc..7d55ebc 100644 --- a/moshi/src/test/java/com/squareup/moshi/AdapterMethodsTest.java +++ b/moshi/src/test/java/com/squareup/moshi/AdapterMethodsTest.java @@ -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 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 pointAdapter) throws IOException { + writer.beginArray(); + pointAdapter.toJson(writer, line.a); + pointAdapter.toJson(writer, line.b); + writer.endArray(); + } + + @FromJson Line lineFromJson( + JsonReader reader, JsonAdapter 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 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 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(); }