diff --git a/moshi/src/main/java/com/squareup/moshi/AdapterMethodsFactory.java b/moshi/src/main/java/com/squareup/moshi/AdapterMethodsFactory.java index 80e88d0..437d956 100644 --- a/moshi/src/main/java/com/squareup/moshi/AdapterMethodsFactory.java +++ b/moshi/src/main/java/com/squareup/moshi/AdapterMethodsFactory.java @@ -16,29 +16,30 @@ package com.squareup.moshi; import java.io.IOException; -import java.lang.reflect.AnnotatedElement; +import java.lang.annotation.Annotation; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Type; -import java.util.LinkedHashMap; -import java.util.Map; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; -// TODO: support qualifier annotations. // TODO: support @Nullable // TODO: path in JsonWriter. final class AdapterMethodsFactory implements JsonAdapter.Factory { - private final Map toAdapters; - private final Map fromAdapters; + private final List toAdapters; + private final List fromAdapters; - AdapterMethodsFactory(Map toAdapters, Map fromAdapters) { + public AdapterMethodsFactory(List toAdapters, List fromAdapters) { this.toAdapters = toAdapters; this.fromAdapters = fromAdapters; } - @Override public JsonAdapter create(Type type, AnnotatedElement annotations, final Moshi moshi) { - final ToAdapter toAdapter = toAdapters.get(type); - final FromAdapter fromAdapter = fromAdapters.get(type); + @Override public JsonAdapter create( + Type type, Set annotations, final Moshi moshi) { + final AdapterMethod toAdapter = get(toAdapters, type, annotations); + final AdapterMethod fromAdapter = get(fromAdapters, type, annotations); if (toAdapter == null && fromAdapter == null) return null; final JsonAdapter delegate = toAdapter == null || fromAdapter == null @@ -56,7 +57,7 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory { throw new AssertionError(); } catch (InvocationTargetException e) { if (e.getCause() instanceof IOException) throw (IOException) e.getCause(); - throw new JsonDataException(e.getCause().getMessage()); // TODO: more context? + throw new JsonDataException(e.getCause()); // TODO: more context? } } } @@ -71,7 +72,7 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory { throw new AssertionError(); } catch (InvocationTargetException e) { if (e.getCause() instanceof IOException) throw (IOException) e.getCause(); - throw new JsonDataException(e.getCause().getMessage()); // TODO: more context? + throw new JsonDataException(e.getCause()); // TODO: more context? } } } @@ -79,29 +80,31 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory { } public static AdapterMethodsFactory get(Object adapter) { - Map toAdapters = new LinkedHashMap<>(); - Map fromAdapters = new LinkedHashMap<>(); + List toAdapters = new ArrayList<>(); + List fromAdapters = new ArrayList<>(); for (Class c = adapter.getClass(); c != Object.class; c = c.getSuperclass()) { for (Method m : c.getDeclaredMethods()) { if (m.isAnnotationPresent(ToJson.class)) { - ToAdapter toAdapter = toAdapter(adapter, m); - ToAdapter replaced = toAdapters.put(toAdapter.type, toAdapter); - if (replaced != null) { + AdapterMethod toAdapter = toAdapter(adapter, m); + AdapterMethod conflicting = get(toAdapters, toAdapter.type, toAdapter.annotations); + if (conflicting != null) { throw new IllegalArgumentException("Conflicting @ToJson methods:\n" - + " " + replaced.method + "\n" + + " " + conflicting.method + "\n" + " " + toAdapter.method); } + toAdapters.add(toAdapter); } if (m.isAnnotationPresent(FromJson.class)) { - FromAdapter fromAdapter = fromAdapter(adapter, m); - FromAdapter replaced = fromAdapters.put(fromAdapter.type, fromAdapter); - if (replaced != null) { + AdapterMethod fromAdapter = fromAdapter(adapter, m); + AdapterMethod conflicting = get(fromAdapters, fromAdapter.type, fromAdapter.annotations); + if (conflicting != null) { throw new IllegalArgumentException("Conflicting @FromJson methods:\n" - + " " + replaced.method + "\n" + + " " + conflicting.method + "\n" + " " + fromAdapter.method); } + fromAdapters.add(fromAdapter); } } } @@ -118,7 +121,7 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory { * Returns an object that calls a {@code method} method on {@code adapter} in service of * converting an object to JSON. */ - static ToAdapter toAdapter(Object adapter, Method method) { + static AdapterMethod toAdapter(Object adapter, Method method) { method.setAccessible(true); Type[] parameterTypes = method.getGenericParameterTypes(); final Type returnType = method.getGenericReturnType(); @@ -126,7 +129,10 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory { if (parameterTypes.length == 2 && parameterTypes[0] == JsonWriter.class && returnType == void.class) { - return new ToAdapter(parameterTypes[1], adapter, method) { + // public void pointToJson(JsonWriter jsonWriter, Point point) throws Exception { + Set parameterAnnotations + = Util.jsonAnnotations(method.getParameterAnnotations()[1]); + return new AdapterMethod(parameterTypes[1], parameterAnnotations, adapter, method) { @Override public void toJson(Moshi moshi, JsonWriter writer, Object value) throws IOException, InvocationTargetException, IllegalAccessException { method.invoke(adapter, writer, value); @@ -134,10 +140,14 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory { }; } else if (parameterTypes.length == 1 && returnType != void.class) { - return new ToAdapter(parameterTypes[0], adapter, method) { + // public List pointToJson(Point point) throws Exception { + final Set returnTypeAnnotations = Util.jsonAnnotations(method); + Set parameterAnnotations = + Util.jsonAnnotations(method.getParameterAnnotations()[0]); + return new AdapterMethod(parameterTypes[0], parameterAnnotations, adapter, method) { @Override public void toJson(Moshi moshi, JsonWriter writer, Object value) throws IOException, InvocationTargetException, IllegalAccessException { - JsonAdapter delegate = moshi.adapter(returnType, method); + JsonAdapter delegate = moshi.adapter(returnType, returnTypeAnnotations); Object intermediate = method.invoke(adapter, value); delegate.toJson(writer, intermediate); } @@ -151,26 +161,11 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory { } } - static abstract class ToAdapter { - final Type type; - final Object adapter; - final Method method; - - public ToAdapter(Type type, Object adapter, Method method) { - this.type = type; - this.adapter = adapter; - this.method = method; - } - - public abstract void toJson(Moshi moshi, JsonWriter writer, Object value) - throws IOException, IllegalAccessException, InvocationTargetException; - } - /** * Returns an object that calls a {@code method} method on {@code adapter} in service of * converting an object from JSON. */ - static FromAdapter fromAdapter(Object adapter, Method method) { + static AdapterMethod fromAdapter(Object adapter, Method method) { method.setAccessible(true); final Type[] parameterTypes = method.getGenericParameterTypes(); final Type returnType = method.getGenericReturnType(); @@ -179,7 +174,8 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory { && parameterTypes[0] == JsonReader.class && returnType != void.class) { // public Point pointFromJson(JsonReader jsonReader) throws Exception { - return new FromAdapter(returnType, adapter, method) { + Set returnTypeAnnotations = Util.jsonAnnotations(method); + return new AdapterMethod(returnType, returnTypeAnnotations, adapter, method) { @Override public Object fromJson(Moshi moshi, JsonReader reader) throws IOException, IllegalAccessException, InvocationTargetException { return method.invoke(adapter, reader); @@ -188,10 +184,13 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory { } else if (parameterTypes.length == 1 && returnType != void.class) { // public Point pointFromJson(List o) throws Exception { - return new FromAdapter(returnType, adapter, method) { + Set returnTypeAnnotations = Util.jsonAnnotations(method); + final Set parameterAnnotations + = Util.jsonAnnotations(method.getParameterAnnotations()[0]); + return new AdapterMethod(returnType, returnTypeAnnotations, adapter, method) { @Override public Object fromJson(Moshi moshi, JsonReader reader) throws IOException, IllegalAccessException, InvocationTargetException { - JsonAdapter delegate = moshi.adapter(parameterTypes[0]); + JsonAdapter delegate = moshi.adapter(parameterTypes[0], parameterAnnotations); Object intermediate = delegate.fromJson(reader); return method.invoke(adapter, intermediate); } @@ -205,18 +204,40 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory { } } - static abstract class FromAdapter { + /** Returns the matching adapter method from the list. */ + private static AdapterMethod get( + List adapterMethods, Type type, Set annotations) { + for (int i = 0, size = adapterMethods.size(); i < size; i++) { + AdapterMethod adapterMethod = adapterMethods.get(i); + if (adapterMethod.type.equals(type) && adapterMethod.annotations.equals(annotations)) { + return adapterMethod; + } + } + return null; + } + + static abstract class AdapterMethod { final Type type; + final Set annotations; final Object adapter; final Method method; - public FromAdapter(Type type, Object adapter, Method method) { + public AdapterMethod(Type type, + Set annotations, Object adapter, Method method) { this.type = type; + this.annotations = annotations; this.adapter = adapter; this.method = method; } - public abstract Object fromJson(Moshi moshi, JsonReader reader) - throws IOException, IllegalAccessException, InvocationTargetException; + public void toJson(Moshi moshi, JsonWriter writer, Object value) + throws IOException, IllegalAccessException, InvocationTargetException { + throw new AssertionError(); + } + + public Object fromJson(Moshi moshi, JsonReader reader) + throws IOException, IllegalAccessException, InvocationTargetException { + throw new AssertionError(); + } } } diff --git a/moshi/src/main/java/com/squareup/moshi/ArrayJsonAdapter.java b/moshi/src/main/java/com/squareup/moshi/ArrayJsonAdapter.java index aaae560..ed88107 100644 --- a/moshi/src/main/java/com/squareup/moshi/ArrayJsonAdapter.java +++ b/moshi/src/main/java/com/squareup/moshi/ArrayJsonAdapter.java @@ -16,11 +16,12 @@ package com.squareup.moshi; import java.io.IOException; -import java.lang.reflect.AnnotatedElement; +import java.lang.annotation.Annotation; import java.lang.reflect.Array; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.List; +import java.util.Set; /** * Converts arrays to JSON arrays containing their converted contents. This @@ -28,9 +29,11 @@ import java.util.List; */ final class ArrayJsonAdapter extends JsonAdapter { public static final Factory FACTORY = new Factory() { - @Override public JsonAdapter create(Type type, AnnotatedElement annotations, Moshi moshi) { + @Override public JsonAdapter create( + Type type, Set annotations, Moshi moshi) { Type elementType = Types.arrayComponentType(type); if (elementType == null) return null; + if (!annotations.isEmpty()) return null; Class elementClass = Types.getRawType(elementType); JsonAdapter elementAdapter = moshi.adapter(elementType); return new ArrayJsonAdapter(elementClass, elementAdapter).nullSafe(); diff --git a/moshi/src/main/java/com/squareup/moshi/ClassJsonAdapter.java b/moshi/src/main/java/com/squareup/moshi/ClassJsonAdapter.java index ec44698..a6b4361 100644 --- a/moshi/src/main/java/com/squareup/moshi/ClassJsonAdapter.java +++ b/moshi/src/main/java/com/squareup/moshi/ClassJsonAdapter.java @@ -16,12 +16,13 @@ package com.squareup.moshi; import java.io.IOException; -import java.lang.reflect.AnnotatedElement; +import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Modifier; import java.lang.reflect.Type; import java.util.Map; +import java.util.Set; import java.util.TreeMap; /** @@ -31,9 +32,11 @@ import java.util.TreeMap; */ final class ClassJsonAdapter extends JsonAdapter { public static final JsonAdapter.Factory FACTORY = new JsonAdapter.Factory() { - @Override public JsonAdapter create(Type type, AnnotatedElement annotations, Moshi moshi) { + @Override public JsonAdapter create( + Type type, Set annotations, Moshi moshi) { Class rawType = Types.getRawType(type); if (rawType.isInterface() || rawType.isEnum() || isPlatformType(rawType)) return null; + if (!annotations.isEmpty()) return null; if (rawType.getEnclosingClass() != null && !Modifier.isStatic(rawType.getModifiers())) { if (rawType.getSimpleName().isEmpty()) { @@ -66,7 +69,8 @@ final class ClassJsonAdapter extends JsonAdapter { // Look up a type adapter for this type. Type fieldType = Types.resolve(type, rawType, field.getGenericType()); - JsonAdapter adapter = moshi.adapter(fieldType, field); + Set annotations = Util.jsonAnnotations(field); + JsonAdapter adapter = moshi.adapter(fieldType, annotations); // Create the binding between field and JSON. field.setAccessible(true); diff --git a/moshi/src/main/java/com/squareup/moshi/CollectionJsonAdapter.java b/moshi/src/main/java/com/squareup/moshi/CollectionJsonAdapter.java index f3df24d..39d8745 100644 --- a/moshi/src/main/java/com/squareup/moshi/CollectionJsonAdapter.java +++ b/moshi/src/main/java/com/squareup/moshi/CollectionJsonAdapter.java @@ -16,7 +16,7 @@ package com.squareup.moshi; import java.io.IOException; -import java.lang.reflect.AnnotatedElement; +import java.lang.annotation.Annotation; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Collection; @@ -27,8 +27,10 @@ import java.util.Set; /** Converts collection types to JSON arrays containing their converted contents. */ abstract class CollectionJsonAdapter, T> extends JsonAdapter { public static final JsonAdapter.Factory FACTORY = new JsonAdapter.Factory() { - @Override public JsonAdapter create(Type type, AnnotatedElement annotations, Moshi moshi) { + @Override public JsonAdapter create( + Type type, Set annotations, Moshi moshi) { Class rawType = Types.getRawType(type); + if (!annotations.isEmpty()) return null; if (rawType == List.class || rawType == Collection.class) { return newArrayListAdapter(type, moshi).nullSafe(); } else if (rawType == Set.class) { diff --git a/moshi/src/main/java/com/squareup/moshi/JsonAdapter.java b/moshi/src/main/java/com/squareup/moshi/JsonAdapter.java index dec374f..7c780e8 100644 --- a/moshi/src/main/java/com/squareup/moshi/JsonAdapter.java +++ b/moshi/src/main/java/com/squareup/moshi/JsonAdapter.java @@ -16,8 +16,9 @@ package com.squareup.moshi; import java.io.IOException; -import java.lang.reflect.AnnotatedElement; +import java.lang.annotation.Annotation; import java.lang.reflect.Type; +import java.util.Set; import okio.Buffer; import okio.BufferedSink; import okio.BufferedSource; @@ -107,6 +108,6 @@ public abstract class JsonAdapter { *

Implementations may use to {@link Moshi#adapter} to compose adapters of other types, or * {@link Moshi#nextAdapter} to delegate to the underlying adapter of the same type. */ - JsonAdapter create(Type type, AnnotatedElement annotations, Moshi moshi); + JsonAdapter create(Type type, Set annotations, Moshi moshi); } } diff --git a/moshi/src/main/java/com/squareup/moshi/JsonDataException.java b/moshi/src/main/java/com/squareup/moshi/JsonDataException.java index 093fb72..e3fe4eb 100644 --- a/moshi/src/main/java/com/squareup/moshi/JsonDataException.java +++ b/moshi/src/main/java/com/squareup/moshi/JsonDataException.java @@ -17,6 +17,9 @@ package com.squareup.moshi; /** Thrown when a JSON document doesn't match the expected format. */ public final class JsonDataException extends RuntimeException { + public JsonDataException() { + } + public JsonDataException(String message) { super(message); } diff --git a/moshi/src/main/java/com/squareup/moshi/JsonQualifier.java b/moshi/src/main/java/com/squareup/moshi/JsonQualifier.java new file mode 100644 index 0000000..10c2f1e --- /dev/null +++ b/moshi/src/main/java/com/squareup/moshi/JsonQualifier.java @@ -0,0 +1,30 @@ +/* + * 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.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** Annotates another annotation, causing it to specialize how values are encoded and decoded. */ +@Target(ANNOTATION_TYPE) +@Retention(RUNTIME) +@Documented +public @interface JsonQualifier { +} diff --git a/moshi/src/main/java/com/squareup/moshi/MapJsonAdapter.java b/moshi/src/main/java/com/squareup/moshi/MapJsonAdapter.java index ad14cbd..6667fc3 100644 --- a/moshi/src/main/java/com/squareup/moshi/MapJsonAdapter.java +++ b/moshi/src/main/java/com/squareup/moshi/MapJsonAdapter.java @@ -16,9 +16,10 @@ package com.squareup.moshi; import java.io.IOException; -import java.lang.reflect.AnnotatedElement; +import java.lang.annotation.Annotation; import java.lang.reflect.Type; import java.util.Map; +import java.util.Set; /** * Converts maps with string keys to JSON objects. @@ -27,7 +28,9 @@ import java.util.Map; */ final class MapJsonAdapter extends JsonAdapter> { public static final Factory FACTORY = new Factory() { - @Override public JsonAdapter create(Type type, AnnotatedElement annotations, Moshi moshi) { + @Override public JsonAdapter create( + Type type, Set annotations, Moshi moshi) { + if (!annotations.isEmpty()) return null; Class rawType = Types.getRawType(type); if (rawType != Map.class) return null; Type[] keyAndValue = Types.mapKeyAndValueTypes(type, rawType); diff --git a/moshi/src/main/java/com/squareup/moshi/Moshi.java b/moshi/src/main/java/com/squareup/moshi/Moshi.java index 895cb5e..059ecaf 100644 --- a/moshi/src/main/java/com/squareup/moshi/Moshi.java +++ b/moshi/src/main/java/com/squareup/moshi/Moshi.java @@ -17,11 +17,11 @@ 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; import java.util.Collections; import java.util.List; +import java.util.Set; /** * Coordinates binding between JSON values and Java objects. @@ -51,19 +51,22 @@ public final class Moshi { return adapter(type, Util.NO_ANNOTATIONS); } - public JsonAdapter adapter(Type type, AnnotatedElement annotations) { - // TODO: support re-entrant calls. + public JsonAdapter adapter(Type type, Set annotations) { return createAdapter(0, type, annotations); } public JsonAdapter nextAdapter(JsonAdapter.Factory skipPast, Type type, - AnnotatedElement annotations) { + Set annotations) { return createAdapter(factories.indexOf(skipPast) + 1, type, annotations); } + public JsonAdapter nextAdapter(JsonAdapter.Factory skipPast, Type type) { + return nextAdapter(skipPast, type, Util.NO_ANNOTATIONS); + } + @SuppressWarnings("unchecked") // Factories are required to return only matching JsonAdapters. private JsonAdapter createAdapter( - int firstIndex, Type type, AnnotatedElement annotations) { + int firstIndex, Type type, Set annotations) { List> deferredAdapters = reentrantCalls.get(); if (deferredAdapters == null) { deferredAdapters = new ArrayList<>(); @@ -91,7 +94,7 @@ public final class Moshi { deferredAdapters.remove(deferredAdapters.size() - 1); } - throw new IllegalArgumentException("no JsonAdapter for " + type); + throw new IllegalArgumentException("no JsonAdapter for " + type + " annotated " + annotations); } public static final class Builder { @@ -103,8 +106,8 @@ public final class Moshi { return add(new JsonAdapter.Factory() { @Override public JsonAdapter create( - Type targetType, AnnotatedElement annotations, Moshi moshi) { - return Util.typesMatch(type, targetType) ? jsonAdapter : null; + Type targetType, Set annotations, Moshi moshi) { + return annotations.isEmpty() && Util.typesMatch(type, targetType) ? jsonAdapter : null; } }); } @@ -114,13 +117,16 @@ public final class Moshi { if (type == null) throw new IllegalArgumentException("type == null"); if (annotation == null) throw new IllegalArgumentException("annotation == null"); if (jsonAdapter == null) throw new IllegalArgumentException("jsonAdapter == null"); + if (!annotation.isAnnotationPresent(JsonQualifier.class)) { + throw new IllegalArgumentException(annotation + " does not have @JsonQualifier"); + } 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; + Type targetType, Set annotations, Moshi moshi) { + if (!Util.typesMatch(type, targetType)) return null; + if (!Util.isAnnotationPresent(annotations, annotation)) return null; + return jsonAdapter; } }); } @@ -150,10 +156,10 @@ public final class Moshi { */ private static class DeferredAdapter extends JsonAdapter { private Type type; - private AnnotatedElement annotations; + private Set annotations; private JsonAdapter delegate; - public DeferredAdapter(Type type, AnnotatedElement annotations) { + public DeferredAdapter(Type type, Set annotations) { this.type = type; this.annotations = annotations; } diff --git a/moshi/src/main/java/com/squareup/moshi/StandardJsonAdapters.java b/moshi/src/main/java/com/squareup/moshi/StandardJsonAdapters.java index 8c645fc..53eed50 100644 --- a/moshi/src/main/java/com/squareup/moshi/StandardJsonAdapters.java +++ b/moshi/src/main/java/com/squareup/moshi/StandardJsonAdapters.java @@ -16,13 +16,16 @@ package com.squareup.moshi; import java.io.IOException; -import java.lang.reflect.AnnotatedElement; +import java.lang.annotation.Annotation; import java.lang.reflect.Type; import java.util.Arrays; +import java.util.Set; final class StandardJsonAdapters { public static final JsonAdapter.Factory FACTORY = new JsonAdapter.Factory() { - @Override public JsonAdapter create(Type type, AnnotatedElement annotations, Moshi moshi) { + @Override public JsonAdapter create( + Type type, Set annotations, Moshi moshi) { + if (!annotations.isEmpty()) return null; if (type == boolean.class) return BOOLEAN_JSON_ADAPTER; if (type == byte.class) return BYTE_JSON_ADAPTER; if (type == char.class) return CHARACTER_JSON_ADAPTER; diff --git a/moshi/src/main/java/com/squareup/moshi/Util.java b/moshi/src/main/java/com/squareup/moshi/Util.java index 3ad3faf..114bb49 100644 --- a/moshi/src/main/java/com/squareup/moshi/Util.java +++ b/moshi/src/main/java/com/squareup/moshi/Util.java @@ -18,27 +18,39 @@ package com.squareup.moshi; import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Type; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; final class Util { - public static final Annotation[] EMPTY_ANNOTATIONS_ARRAY = new Annotation[0]; - - public static final AnnotatedElement NO_ANNOTATIONS = new AnnotatedElement() { - @Override public boolean isAnnotationPresent(Class aClass) { - return false; - } - @Override public T getAnnotation(Class tClass) { - return null; - } - @Override public Annotation[] getAnnotations() { - return EMPTY_ANNOTATIONS_ARRAY; - } - @Override public Annotation[] getDeclaredAnnotations() { - return EMPTY_ANNOTATIONS_ARRAY; - } - }; + public static final Set NO_ANNOTATIONS = Collections.emptySet(); public static boolean typesMatch(Type pattern, Type candidate) { // TODO: permit raw types (like Set.class) to match non-raw candidates (like Set). return pattern.equals(candidate); } + + public static Set jsonAnnotations(AnnotatedElement annotatedElement) { + return jsonAnnotations(annotatedElement.getAnnotations()); + } + + public static Set jsonAnnotations(Annotation[] annotations) { + Set result = null; + for (Annotation annotation : annotations) { + if (annotation.annotationType().isAnnotationPresent(JsonQualifier.class)) { + if (result == null) result = new LinkedHashSet<>(); + result.add(annotation); + } + } + return result != null ? Collections.unmodifiableSet(result) : Util.NO_ANNOTATIONS; + } + + public static boolean isAnnotationPresent( + Set annotations, Class annotationClass) { + if (annotations.isEmpty()) return false; // Save an iterator in the common case. + for (Annotation annotation : annotations) { + if (annotation.annotationType() == annotationClass) return true; + } + return false; + } } diff --git a/moshi/src/test/java/com/squareup/moshi/CircularAdaptersTest.java b/moshi/src/test/java/com/squareup/moshi/CircularAdaptersTest.java index f90e253..f5652e9 100644 --- a/moshi/src/test/java/com/squareup/moshi/CircularAdaptersTest.java +++ b/moshi/src/test/java/com/squareup/moshi/CircularAdaptersTest.java @@ -16,9 +16,10 @@ package com.squareup.moshi; import java.io.IOException; +import java.lang.annotation.Annotation; import java.lang.annotation.Retention; -import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Type; +import java.util.Set; import org.junit.Test; import static java.lang.annotation.RetentionPolicy.RUNTIME; @@ -63,10 +64,12 @@ public final class CircularAdaptersTest { } @Retention(RUNTIME) + @JsonQualifier public @interface Left { } @Retention(RUNTIME) + @JsonQualifier public @interface Right { } @@ -97,19 +100,20 @@ public final class CircularAdaptersTest { * work. */ static class PrefixingNodeFactory implements JsonAdapter.Factory { - @Override public JsonAdapter create(Type type, AnnotatedElement annotations, Moshi moshi) { + @Override public JsonAdapter create( + Type type, Set annotations, Moshi moshi) { if (type != Node.class) return null; final String prefix; - if (annotations.isAnnotationPresent(Left.class)) { + if (Util.isAnnotationPresent(annotations, Left.class)) { prefix = "L "; - } else if (annotations.isAnnotationPresent(Right.class)) { + } else if (Util.isAnnotationPresent(annotations, Right.class)) { prefix = "R "; } else { return null; } - final JsonAdapter delegate = moshi.nextAdapter(this, Node.class, annotations); + final JsonAdapter delegate = moshi.nextAdapter(this, Node.class); return new JsonAdapter() { @Override public void toJson(JsonWriter writer, Node value) throws IOException { diff --git a/moshi/src/test/java/com/squareup/moshi/ClassJsonAdapterTest.java b/moshi/src/test/java/com/squareup/moshi/ClassJsonAdapterTest.java index fe7ba1b..aa02dbc 100644 --- a/moshi/src/test/java/com/squareup/moshi/ClassJsonAdapterTest.java +++ b/moshi/src/test/java/com/squareup/moshi/ClassJsonAdapterTest.java @@ -391,7 +391,7 @@ public final class ClassJsonAdapterTest { private String toJson(Class type, T value) throws IOException { @SuppressWarnings("unchecked") // Factory.create returns an adapter that matches its argument. - JsonAdapter jsonAdapter = (JsonAdapter) ClassJsonAdapter.FACTORY.create( + JsonAdapter jsonAdapter = (JsonAdapter) ClassJsonAdapter.FACTORY.create( type, NO_ANNOTATIONS, moshi); // Wrap in an array to avoid top-level object warnings without going completely lenient. @@ -409,7 +409,7 @@ public final class ClassJsonAdapterTest { private T fromJson(Class type, String json) throws IOException { @SuppressWarnings("unchecked") // Factory.create returns an adapter that matches its argument. - JsonAdapter jsonAdapter = (JsonAdapter) ClassJsonAdapter.FACTORY.create( + JsonAdapter jsonAdapter = (JsonAdapter) ClassJsonAdapter.FACTORY.create( type, NO_ANNOTATIONS, moshi); // Wrap in an array to avoid top-level object warnings without going completely lenient. JsonReader jsonReader = newReader("[" + json + "]"); diff --git a/moshi/src/test/java/com/squareup/moshi/JsonQualifiersTest.java b/moshi/src/test/java/com/squareup/moshi/JsonQualifiersTest.java new file mode 100644 index 0000000..f1a252a --- /dev/null +++ b/moshi/src/test/java/com/squareup/moshi/JsonQualifiersTest.java @@ -0,0 +1,367 @@ +/* + * 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.util.Date; +import org.junit.Test; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.fail; + +public final class JsonQualifiersTest { + @Test public void builtInTypes() throws Exception { + Moshi moshi = new Moshi.Builder() + .add(new BuiltInTypesJsonAdapter()) + .build(); + JsonAdapter adapter = moshi.adapter(StringAndFooString.class); + + StringAndFooString v1 = new StringAndFooString(); + v1.a = "aa"; + v1.b = "bar"; + assertThat(adapter.toJson(v1)).isEqualTo("{\"a\":\"aa\",\"b\":\"foobar\"}"); + + StringAndFooString v2 = adapter.fromJson("{\"a\":\"aa\",\"b\":\"foobar\"}"); + assertThat(v2.a).isEqualTo("aa"); + assertThat(v2.b).isEqualTo("bar"); + } + + static class BuiltInTypesJsonAdapter { + @ToJson String fooPrefixStringToString(@FooPrefix String s) { + return "foo" + s; + } + + @FromJson @FooPrefix String fooPrefixStringFromString(String s) throws Exception { + if (!s.startsWith("foo")) throw new JsonDataException(); + return s.substring(3); + } + } + + @Test public void readerWriterJsonAdapter() throws Exception { + Moshi moshi = new Moshi.Builder() + .add(new ReaderWriterJsonAdapter()) + .build(); + JsonAdapter adapter = moshi.adapter(StringAndFooString.class); + + StringAndFooString v1 = new StringAndFooString(); + v1.a = "aa"; + v1.b = "bar"; + assertThat(adapter.toJson(v1)).isEqualTo("{\"a\":\"aa\",\"b\":\"foobar\"}"); + + StringAndFooString v2 = adapter.fromJson("{\"a\":\"aa\",\"b\":\"foobar\"}"); + assertThat(v2.a).isEqualTo("aa"); + assertThat(v2.b).isEqualTo("bar"); + } + + static class ReaderWriterJsonAdapter { + @ToJson void fooPrefixStringToString(JsonWriter jsonWriter, @FooPrefix String s) + throws IOException { + jsonWriter.value("foo" + s); + } + + @FromJson @FooPrefix String fooPrefixStringFromString(JsonReader reader) throws Exception { + String s = reader.nextString(); + if (!s.startsWith("foo")) throw new JsonDataException(); + return s.substring(3); + } + } + + /** Fields with this annotation get "foo" as a prefix in the JSON. */ + @Retention(RUNTIME) + @JsonQualifier + public @interface FooPrefix { + } + + /** Fields with this annotation get "baz" as a suffix in the JSON. */ + @Retention(RUNTIME) + @JsonQualifier + public @interface BazSuffix { + } + + static class StringAndFooString { + String a; + @FooPrefix String b; + } + + static class StringAndFooBazString { + String a; + @FooPrefix @BazSuffix String b; + } + + @Test public void builtInTypesWithMultipleAnnotations() throws Exception { + Moshi moshi = new Moshi.Builder() + .add(new BuiltInTypesWithMultipleAnnotationsJsonAdapter()) + .build(); + JsonAdapter adapter = moshi.adapter(StringAndFooBazString.class); + + StringAndFooBazString v1 = new StringAndFooBazString(); + v1.a = "aa"; + v1.b = "bar"; + assertThat(adapter.toJson(v1)).isEqualTo("{\"a\":\"aa\",\"b\":\"foobarbaz\"}"); + + StringAndFooBazString v2 = adapter.fromJson("{\"a\":\"aa\",\"b\":\"foobarbaz\"}"); + assertThat(v2.a).isEqualTo("aa"); + assertThat(v2.b).isEqualTo("bar"); + } + + static class BuiltInTypesWithMultipleAnnotationsJsonAdapter { + @ToJson String fooPrefixAndBazSuffixStringToString(@FooPrefix @BazSuffix String s) { + return "foo" + s + "baz"; + } + + @FromJson @FooPrefix @BazSuffix String fooPrefixAndBazSuffixStringFromString( + String s) throws Exception { + if (!s.startsWith("foo")) throw new JsonDataException(); + if (!s.endsWith("baz")) throw new JsonDataException(); + return s.substring(3, s.length() - 3); + } + } + + @Test public void readerWriterWithMultipleAnnotations() throws Exception { + Moshi moshi = new Moshi.Builder() + .add(new ReaderWriterWithMultipleAnnotationsJsonAdapter()) + .build(); + JsonAdapter adapter = moshi.adapter(StringAndFooBazString.class); + + StringAndFooBazString v1 = new StringAndFooBazString(); + v1.a = "aa"; + v1.b = "bar"; + assertThat(adapter.toJson(v1)).isEqualTo("{\"a\":\"aa\",\"b\":\"foobarbaz\"}"); + + StringAndFooBazString v2 = adapter.fromJson("{\"a\":\"aa\",\"b\":\"foobarbaz\"}"); + assertThat(v2.a).isEqualTo("aa"); + assertThat(v2.b).isEqualTo("bar"); + } + + static class ReaderWriterWithMultipleAnnotationsJsonAdapter { + @ToJson void fooPrefixAndBazSuffixStringToString( + JsonWriter jsonWriter, @FooPrefix @BazSuffix String s) throws IOException { + jsonWriter.value("foo" + s + "baz"); + } + + @FromJson @FooPrefix @BazSuffix String fooPrefixAndBazSuffixStringFromString( + JsonReader reader) throws Exception { + String s = reader.nextString(); + if (!s.startsWith("foo")) throw new JsonDataException(); + if (!s.endsWith("baz")) throw new JsonDataException(); + return s.substring(3, s.length() - 3); + } + } + + @Test public void basicTypesAnnotationDelegating() throws Exception { + Moshi moshi = new Moshi.Builder() + .add(new BuiltInTypesDelegatingJsonAdapter()) + .add(new BuiltInTypesJsonAdapter()) + .build(); + JsonAdapter adapter = moshi.adapter(StringAndFooBazString.class); + + StringAndFooBazString v1 = new StringAndFooBazString(); + v1.a = "aa"; + v1.b = "bar"; + assertThat(adapter.toJson(v1)).isEqualTo("{\"a\":\"aa\",\"b\":\"foobarbaz\"}"); + + StringAndFooBazString v2 = adapter.fromJson("{\"a\":\"aa\",\"b\":\"foobarbaz\"}"); + assertThat(v2.a).isEqualTo("aa"); + assertThat(v2.b).isEqualTo("bar"); + } + + static class BuiltInTypesDelegatingJsonAdapter { + @ToJson @FooPrefix String fooPrefixAndBazSuffixStringToString(@FooPrefix @BazSuffix String s) { + return s + "baz"; + } + + @FromJson @FooPrefix @BazSuffix String fooPrefixAndBazSuffixStringFromString( + @FooPrefix String s) throws Exception { + if (!s.endsWith("baz")) throw new JsonDataException(); + return s.substring(0, s.length() - 3); + } + } + + @Test public void readerWriterAnnotationDelegating() throws Exception { + Moshi moshi = new Moshi.Builder() + .add(new BuiltInTypesDelegatingJsonAdapter()) + .add(new ReaderWriterJsonAdapter()) + .build(); + JsonAdapter adapter = moshi.adapter(StringAndFooBazString.class); + + StringAndFooBazString v1 = new StringAndFooBazString(); + v1.a = "aa"; + v1.b = "bar"; + assertThat(adapter.toJson(v1)).isEqualTo("{\"a\":\"aa\",\"b\":\"foobarbaz\"}"); + + StringAndFooBazString v2 = adapter.fromJson("{\"a\":\"aa\",\"b\":\"foobarbaz\"}"); + assertThat(v2.a).isEqualTo("aa"); + assertThat(v2.b).isEqualTo("bar"); + } + + @Test public void manualJsonAdapter() throws Exception { + JsonAdapter fooPrefixAdapter = new JsonAdapter() { + @Override public String fromJson(JsonReader reader) throws IOException { + String s = reader.nextString(); + if (!s.startsWith("foo")) throw new JsonDataException(); + return s.substring(3); + } + + @Override public void toJson(JsonWriter writer, String value) throws IOException { + writer.value("foo" + value); + } + }; + + Moshi moshi = new Moshi.Builder() + .add(String.class, FooPrefix.class, fooPrefixAdapter) + .build(); + JsonAdapter adapter = moshi.adapter(StringAndFooString.class); + + StringAndFooString v1 = new StringAndFooString(); + v1.a = "aa"; + v1.b = "bar"; + assertThat(adapter.toJson(v1)).isEqualTo("{\"a\":\"aa\",\"b\":\"foobar\"}"); + + StringAndFooString v2 = adapter.fromJson("{\"a\":\"aa\",\"b\":\"foobar\"}"); + assertThat(v2.a).isEqualTo("aa"); + assertThat(v2.b).isEqualTo("bar"); + } + + @Test public void noJsonAdapterForAnnotatedType() throws Exception { + Moshi moshi = new Moshi.Builder().build(); + try { + moshi.adapter(StringAndFooString.class); + fail(); + } catch (IllegalArgumentException expected) { + } + } + + @Test public void annotationWithoutJsonQualifierIsIgnoredByAdapterMethods() throws Exception { + Moshi moshi = new Moshi.Builder() + .add(new MissingJsonQualifierJsonAdapter()) + .build(); + JsonAdapter adapter = moshi.adapter(DateAndMillisDate.class); + + DateAndMillisDate v1 = new DateAndMillisDate(); + v1.a = new Date(5); + v1.b = new Date(7); + assertThat(adapter.toJson(v1)).isEqualTo("{\"a\":5,\"b\":7}"); + + DateAndMillisDate v2 = adapter.fromJson("{\"a\":5,\"b\":7}"); + assertThat(v2.a).isEqualTo(new Date(5)); + assertThat(v2.b).isEqualTo(new Date(7)); + } + + /** Despite the fact that these methods are annotated, they match all dates. */ + static class MissingJsonQualifierJsonAdapter { + @ToJson long dateToJson(@Millis Date d) { + return d.getTime(); + } + + @FromJson @Millis Date jsonToDate(long value) throws Exception { + return new Date(value); + } + } + + /** This annotation does nothing. */ + @Retention(RUNTIME) + public @interface Millis { + } + + static class DateAndMillisDate { + Date a; + @Millis Date b; + } + + @Test public void annotationWithoutJsonQualifierIsRejectedOnRegistration() throws Exception { + JsonAdapter jsonAdapter = new JsonAdapter() { + @Override public Date fromJson(JsonReader reader) throws IOException { + throw new AssertionError(); + } + + @Override public void toJson(JsonWriter writer, Date value) throws IOException { + throw new AssertionError(); + } + }; + + try { + new Moshi.Builder().add(Date.class, Millis.class, jsonAdapter); + fail(); + } catch (IllegalArgumentException expected) { + assertThat(expected).hasMessage("interface com.squareup.moshi.JsonQualifiersTest$Millis " + + "does not have @JsonQualifier"); + } + } + + @Test public void annotationsConflict() throws Exception { + try { + new Moshi.Builder().add(new AnnotationsConflictJsonAdapter()); + fail(); + } catch (IllegalArgumentException expected) { + assertThat(expected).hasMessageContaining("Conflicting @ToJson methods"); + } + } + + static class AnnotationsConflictJsonAdapter { + @ToJson String fooPrefixStringToString(@FooPrefix String s) { + return "foo" + s; + } + + @ToJson String fooPrefixStringToString2(@FooPrefix String s) { + return "foo" + s; + } + } + + @Test public void toButNoFromJson() throws Exception { + // Building it is okay. + Moshi moshi = new Moshi.Builder() + .add(new ToButNoFromJsonAdapter()) + .build(); + + try { + moshi.adapter(StringAndFooString.class); + fail(); + } catch (IllegalArgumentException expected) { + assertThat(expected).hasMessage("no JsonAdapter for class java.lang.String " + + "annotated [@com.squareup.moshi.JsonQualifiersTest$FooPrefix()]"); + } + } + + static class ToButNoFromJsonAdapter { + @ToJson String fooPrefixStringToString(@FooPrefix String s) { + return "foo" + s; + } + } + + @Test public void fromButNoToJson() throws Exception { + // Building it is okay. + Moshi moshi = new Moshi.Builder() + .add(new FromButNoToJsonAdapter()) + .build(); + + try { + moshi.adapter(StringAndFooString.class); + fail(); + } catch (IllegalArgumentException expected) { + assertThat(expected).hasMessage("no JsonAdapter for class java.lang.String " + + "annotated [@com.squareup.moshi.JsonQualifiersTest$FooPrefix()]"); + } + } + + static class FromButNoToJsonAdapter { + @FromJson @FooPrefix String fooPrefixStringFromString(String s) throws Exception { + if (!s.startsWith("foo")) throw new JsonDataException(); + return s.substring(3); + } + } +} diff --git a/moshi/src/test/java/com/squareup/moshi/MoshiTest.java b/moshi/src/test/java/com/squareup/moshi/MoshiTest.java index b589383..d46e93b 100644 --- a/moshi/src/test/java/com/squareup/moshi/MoshiTest.java +++ b/moshi/src/test/java/com/squareup/moshi/MoshiTest.java @@ -16,8 +16,8 @@ package com.squareup.moshi; import java.io.IOException; +import java.lang.annotation.Annotation; import java.lang.annotation.Retention; -import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Field; import java.lang.reflect.Type; import java.util.ArrayDeque; @@ -32,6 +32,7 @@ import org.junit.Test; import static com.squareup.moshi.TestUtil.newReader; import static java.lang.annotation.RetentionPolicy.RUNTIME; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; public final class MoshiTest { @@ -591,7 +592,8 @@ public final class MoshiTest { .add(new UppercaseAdapterFactory()) .build(); - AnnotatedElement annotations = MoshiTest.class.getDeclaredField("uppercaseString"); + Field uppercaseString = MoshiTest.class.getDeclaredField("uppercaseString"); + Set annotations = Util.jsonAnnotations(uppercaseString); JsonAdapter adapter = moshi.adapter(String.class, annotations).lenient(); assertThat(adapter.toJson("a")).isEqualTo("\"A\""); assertThat(adapter.fromJson("\"b\"")).isEqualTo("B"); @@ -638,10 +640,14 @@ public final class MoshiTest { .build(); Field uppercaseStringsField = MoshiTest.class.getDeclaredField("uppercaseStrings"); - JsonAdapter> adapter = moshi.adapter(uppercaseStringsField.getGenericType(), - uppercaseStringsField); - assertThat(adapter.toJson(Arrays.asList("a"))).isEqualTo("[\"a\"]"); - assertThat(adapter.fromJson("[\"b\"]")).isEqualTo(Arrays.asList("b")); + try { + moshi.adapter(uppercaseStringsField.getGenericType(), + Util.jsonAnnotations(uppercaseStringsField)); + fail(); + } catch (IllegalArgumentException expected) { + assertEquals("no JsonAdapter for java.util.List annotated " + + "[@com.squareup.moshi.MoshiTest$Uppercase()]", expected.getMessage()); + } } @Test public void objectArray() throws Exception { @@ -753,9 +759,8 @@ public final class MoshiTest { static class MealDealAdapterFactory implements JsonAdapter.Factory { @Override public JsonAdapter create( - Type type, AnnotatedElement annotations, Moshi moshi) { + Type type, Set annotations, Moshi moshi) { if (!type.equals(MealDeal.class)) return null; - final JsonAdapter pizzaAdapter = moshi.adapter(Pizza.class); final JsonAdapter drinkAdapter = moshi.adapter(String.class); return new JsonAdapter() { @@ -778,16 +783,17 @@ public final class MoshiTest { } @Retention(RUNTIME) + @JsonQualifier public @interface Uppercase { } static class UppercaseAdapterFactory implements JsonAdapter.Factory { @Override public JsonAdapter create( - Type type, AnnotatedElement annotations, Moshi moshi) { + Type type, Set annotations, Moshi moshi) { if (!type.equals(String.class)) return null; - if (!annotations.isAnnotationPresent(Uppercase.class)) return null; + if (!Util.isAnnotationPresent(annotations, Uppercase.class)) return null; - final JsonAdapter stringAdapter = moshi.nextAdapter(this, String.class, annotations); + final JsonAdapter stringAdapter = moshi.nextAdapter(this, String.class); return new JsonAdapter() { @Override public String fromJson(JsonReader reader) throws IOException { String s = stringAdapter.fromJson(reader);