New @JsonQualifier annotation.

Works like JSR-330's @Qualifier annotation. You may have
multiple qualifiers, or none.
This commit is contained in:
jwilson
2015-05-30 16:06:55 -07:00
parent 629181ff8c
commit d6d9f9ead3
15 changed files with 575 additions and 110 deletions

View File

@@ -16,29 +16,30 @@
package com.squareup.moshi; package com.squareup.moshi;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.AnnotatedElement; 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.Type; import java.lang.reflect.Type;
import java.util.LinkedHashMap; import java.util.ArrayList;
import java.util.Map; import java.util.List;
import java.util.Set;
// TODO: support qualifier annotations.
// TODO: support @Nullable // TODO: support @Nullable
// TODO: path in JsonWriter. // TODO: path in JsonWriter.
final class AdapterMethodsFactory implements JsonAdapter.Factory { final class AdapterMethodsFactory implements JsonAdapter.Factory {
private final Map<Type, ToAdapter> toAdapters; private final List<AdapterMethod> toAdapters;
private final Map<Type, FromAdapter> fromAdapters; private final List<AdapterMethod> fromAdapters;
AdapterMethodsFactory(Map<Type, ToAdapter> toAdapters, Map<Type, FromAdapter> fromAdapters) { public AdapterMethodsFactory(List<AdapterMethod> toAdapters, List<AdapterMethod> fromAdapters) {
this.toAdapters = toAdapters; this.toAdapters = toAdapters;
this.fromAdapters = fromAdapters; this.fromAdapters = fromAdapters;
} }
@Override public JsonAdapter<?> create(Type type, AnnotatedElement annotations, final Moshi moshi) { @Override public JsonAdapter<?> create(
final ToAdapter toAdapter = toAdapters.get(type); Type type, Set<? extends Annotation> annotations, final Moshi moshi) {
final FromAdapter fromAdapter = fromAdapters.get(type); final AdapterMethod toAdapter = get(toAdapters, type, annotations);
final AdapterMethod fromAdapter = get(fromAdapters, type, annotations);
if (toAdapter == null && fromAdapter == null) return null; if (toAdapter == null && fromAdapter == null) return null;
final JsonAdapter<Object> delegate = toAdapter == null || fromAdapter == null final JsonAdapter<Object> delegate = toAdapter == null || fromAdapter == null
@@ -56,7 +57,7 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
throw new AssertionError(); throw new AssertionError();
} catch (InvocationTargetException e) { } catch (InvocationTargetException e) {
if (e.getCause() instanceof IOException) throw (IOException) e.getCause(); 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(); throw new AssertionError();
} catch (InvocationTargetException e) { } catch (InvocationTargetException e) {
if (e.getCause() instanceof IOException) throw (IOException) e.getCause(); 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) { public static AdapterMethodsFactory get(Object adapter) {
Map<Type, ToAdapter> toAdapters = new LinkedHashMap<>(); List<AdapterMethod> toAdapters = new ArrayList<>();
Map<Type, FromAdapter> fromAdapters = new LinkedHashMap<>(); List<AdapterMethod> fromAdapters = new ArrayList<>();
for (Class<?> c = adapter.getClass(); c != Object.class; c = c.getSuperclass()) { for (Class<?> c = adapter.getClass(); c != Object.class; c = c.getSuperclass()) {
for (Method m : c.getDeclaredMethods()) { for (Method m : c.getDeclaredMethods()) {
if (m.isAnnotationPresent(ToJson.class)) { if (m.isAnnotationPresent(ToJson.class)) {
ToAdapter toAdapter = toAdapter(adapter, m); AdapterMethod toAdapter = toAdapter(adapter, m);
ToAdapter replaced = toAdapters.put(toAdapter.type, toAdapter); AdapterMethod conflicting = get(toAdapters, toAdapter.type, toAdapter.annotations);
if (replaced != null) { if (conflicting != null) {
throw new IllegalArgumentException("Conflicting @ToJson methods:\n" throw new IllegalArgumentException("Conflicting @ToJson methods:\n"
+ " " + replaced.method + "\n" + " " + conflicting.method + "\n"
+ " " + toAdapter.method); + " " + toAdapter.method);
} }
toAdapters.add(toAdapter);
} }
if (m.isAnnotationPresent(FromJson.class)) { if (m.isAnnotationPresent(FromJson.class)) {
FromAdapter fromAdapter = fromAdapter(adapter, m); AdapterMethod fromAdapter = fromAdapter(adapter, m);
FromAdapter replaced = fromAdapters.put(fromAdapter.type, fromAdapter); AdapterMethod conflicting = get(fromAdapters, fromAdapter.type, fromAdapter.annotations);
if (replaced != null) { if (conflicting != null) {
throw new IllegalArgumentException("Conflicting @FromJson methods:\n" throw new IllegalArgumentException("Conflicting @FromJson methods:\n"
+ " " + replaced.method + "\n" + " " + conflicting.method + "\n"
+ " " + fromAdapter.method); + " " + 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 * Returns an object that calls a {@code method} method on {@code adapter} in service of
* converting an object to JSON. * converting an object to JSON.
*/ */
static ToAdapter toAdapter(Object adapter, Method method) { static AdapterMethod toAdapter(Object adapter, Method method) {
method.setAccessible(true); method.setAccessible(true);
Type[] parameterTypes = method.getGenericParameterTypes(); Type[] parameterTypes = method.getGenericParameterTypes();
final Type returnType = method.getGenericReturnType(); final Type returnType = method.getGenericReturnType();
@@ -126,7 +129,10 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
if (parameterTypes.length == 2 if (parameterTypes.length == 2
&& parameterTypes[0] == JsonWriter.class && parameterTypes[0] == JsonWriter.class
&& returnType == void.class) { && returnType == void.class) {
return new ToAdapter(parameterTypes[1], adapter, method) { // 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) {
@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, IllegalAccessException {
method.invoke(adapter, writer, value); method.invoke(adapter, writer, value);
@@ -134,10 +140,14 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
}; };
} else if (parameterTypes.length == 1 && returnType != void.class) { } else if (parameterTypes.length == 1 && returnType != void.class) {
return new ToAdapter(parameterTypes[0], adapter, method) { // public List<Integer> pointToJson(Point point) throws Exception {
final Set<? extends Annotation> returnTypeAnnotations = Util.jsonAnnotations(method);
Set<? extends Annotation> parameterAnnotations =
Util.jsonAnnotations(method.getParameterAnnotations()[0]);
return new AdapterMethod(parameterTypes[0], parameterAnnotations, adapter, method) {
@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, IllegalAccessException {
JsonAdapter<Object> delegate = moshi.adapter(returnType, method); JsonAdapter<Object> delegate = moshi.adapter(returnType, returnTypeAnnotations);
Object intermediate = method.invoke(adapter, value); Object intermediate = method.invoke(adapter, value);
delegate.toJson(writer, intermediate); 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 * 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 FromAdapter 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[] parameterTypes = method.getGenericParameterTypes();
final Type returnType = method.getGenericReturnType(); final Type returnType = method.getGenericReturnType();
@@ -179,7 +174,8 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
&& parameterTypes[0] == JsonReader.class && parameterTypes[0] == JsonReader.class
&& returnType != void.class) { && returnType != void.class) {
// public Point pointFromJson(JsonReader jsonReader) throws Exception { // public Point pointFromJson(JsonReader jsonReader) throws Exception {
return new FromAdapter(returnType, adapter, method) { Set<? extends Annotation> returnTypeAnnotations = Util.jsonAnnotations(method);
return new AdapterMethod(returnType, returnTypeAnnotations, adapter, method) {
@Override public Object fromJson(Moshi moshi, JsonReader reader) @Override public Object fromJson(Moshi moshi, JsonReader reader)
throws IOException, IllegalAccessException, InvocationTargetException { throws IOException, IllegalAccessException, InvocationTargetException {
return method.invoke(adapter, reader); return method.invoke(adapter, reader);
@@ -188,10 +184,13 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
} else if (parameterTypes.length == 1 && returnType != void.class) { } else if (parameterTypes.length == 1 && returnType != void.class) {
// public Point pointFromJson(List<Integer> o) throws Exception { // public Point pointFromJson(List<Integer> o) throws Exception {
return new FromAdapter(returnType, adapter, method) { Set<? extends Annotation> returnTypeAnnotations = Util.jsonAnnotations(method);
final Set<? extends Annotation> parameterAnnotations
= Util.jsonAnnotations(method.getParameterAnnotations()[0]);
return new AdapterMethod(returnType, returnTypeAnnotations, adapter, method) {
@Override public Object fromJson(Moshi moshi, JsonReader reader) @Override public Object fromJson(Moshi moshi, JsonReader reader)
throws IOException, IllegalAccessException, InvocationTargetException { throws IOException, IllegalAccessException, InvocationTargetException {
JsonAdapter<Object> delegate = moshi.adapter(parameterTypes[0]); JsonAdapter<Object> delegate = moshi.adapter(parameterTypes[0], parameterAnnotations);
Object intermediate = delegate.fromJson(reader); Object intermediate = delegate.fromJson(reader);
return method.invoke(adapter, intermediate); 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<AdapterMethod> adapterMethods, Type type, Set<? extends Annotation> 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 Type type;
final Set<? extends Annotation> annotations;
final Object adapter; final Object adapter;
final Method method; final Method method;
public FromAdapter(Type type, Object adapter, Method method) { public AdapterMethod(Type type,
Set<? extends Annotation> annotations, Object adapter, Method method) {
this.type = type; this.type = type;
this.annotations = annotations;
this.adapter = adapter; this.adapter = adapter;
this.method = method; this.method = method;
} }
public abstract Object fromJson(Moshi moshi, JsonReader reader) public void toJson(Moshi moshi, JsonWriter writer, Object value)
throws IOException, IllegalAccessException, InvocationTargetException; throws IOException, IllegalAccessException, InvocationTargetException {
throw new AssertionError();
}
public Object fromJson(Moshi moshi, JsonReader reader)
throws IOException, IllegalAccessException, InvocationTargetException {
throw new AssertionError();
}
} }
} }

View File

@@ -16,11 +16,12 @@
package com.squareup.moshi; package com.squareup.moshi;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.AnnotatedElement; import java.lang.annotation.Annotation;
import java.lang.reflect.Array; import java.lang.reflect.Array;
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;
/** /**
* Converts arrays to JSON arrays containing their converted contents. This * Converts arrays to JSON arrays containing their converted contents. This
@@ -28,9 +29,11 @@ import java.util.List;
*/ */
final class ArrayJsonAdapter extends JsonAdapter<Object> { final class ArrayJsonAdapter extends JsonAdapter<Object> {
public static final Factory FACTORY = new Factory() { public static final Factory FACTORY = new Factory() {
@Override public JsonAdapter<?> create(Type type, AnnotatedElement annotations, Moshi moshi) { @Override public JsonAdapter<?> create(
Type type, Set<? extends Annotation> annotations, Moshi moshi) {
Type elementType = Types.arrayComponentType(type); Type elementType = Types.arrayComponentType(type);
if (elementType == null) return null; if (elementType == null) return null;
if (!annotations.isEmpty()) return null;
Class<?> elementClass = Types.getRawType(elementType); Class<?> elementClass = Types.getRawType(elementType);
JsonAdapter<Object> elementAdapter = moshi.adapter(elementType); JsonAdapter<Object> elementAdapter = moshi.adapter(elementType);
return new ArrayJsonAdapter(elementClass, elementAdapter).nullSafe(); return new ArrayJsonAdapter(elementClass, elementAdapter).nullSafe();

View File

@@ -16,12 +16,13 @@
package com.squareup.moshi; package com.squareup.moshi;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.AnnotatedElement; import java.lang.annotation.Annotation;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier; import java.lang.reflect.Modifier;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.TreeMap; import java.util.TreeMap;
/** /**
@@ -31,9 +32,11 @@ import java.util.TreeMap;
*/ */
final class ClassJsonAdapter<T> extends JsonAdapter<T> { final class ClassJsonAdapter<T> extends JsonAdapter<T> {
public static final JsonAdapter.Factory FACTORY = new JsonAdapter.Factory() { 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<? extends Annotation> annotations, Moshi moshi) {
Class<?> rawType = Types.getRawType(type); Class<?> rawType = Types.getRawType(type);
if (rawType.isInterface() || rawType.isEnum() || isPlatformType(rawType)) return null; if (rawType.isInterface() || rawType.isEnum() || isPlatformType(rawType)) return null;
if (!annotations.isEmpty()) return null;
if (rawType.getEnclosingClass() != null && !Modifier.isStatic(rawType.getModifiers())) { if (rawType.getEnclosingClass() != null && !Modifier.isStatic(rawType.getModifiers())) {
if (rawType.getSimpleName().isEmpty()) { if (rawType.getSimpleName().isEmpty()) {
@@ -66,7 +69,8 @@ final class ClassJsonAdapter<T> extends JsonAdapter<T> {
// Look up a type adapter for this type. // Look up a type adapter for this type.
Type fieldType = Types.resolve(type, rawType, field.getGenericType()); Type fieldType = Types.resolve(type, rawType, field.getGenericType());
JsonAdapter<Object> adapter = moshi.adapter(fieldType, field); Set<? extends Annotation> annotations = Util.jsonAnnotations(field);
JsonAdapter<Object> adapter = moshi.adapter(fieldType, annotations);
// Create the binding between field and JSON. // Create the binding between field and JSON.
field.setAccessible(true); field.setAccessible(true);

View File

@@ -16,7 +16,7 @@
package com.squareup.moshi; package com.squareup.moshi;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.AnnotatedElement; import java.lang.annotation.Annotation;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
@@ -27,8 +27,10 @@ import java.util.Set;
/** Converts collection types to JSON arrays containing their converted contents. */ /** Converts collection types to JSON arrays containing their converted contents. */
abstract class CollectionJsonAdapter<C extends Collection<T>, T> extends JsonAdapter<C> { abstract class CollectionJsonAdapter<C extends Collection<T>, T> extends JsonAdapter<C> {
public static final JsonAdapter.Factory FACTORY = new JsonAdapter.Factory() { 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<? extends Annotation> annotations, Moshi moshi) {
Class<?> rawType = Types.getRawType(type); Class<?> rawType = Types.getRawType(type);
if (!annotations.isEmpty()) return null;
if (rawType == List.class || rawType == Collection.class) { if (rawType == List.class || rawType == Collection.class) {
return newArrayListAdapter(type, moshi).nullSafe(); return newArrayListAdapter(type, moshi).nullSafe();
} else if (rawType == Set.class) { } else if (rawType == Set.class) {

View File

@@ -16,8 +16,9 @@
package com.squareup.moshi; package com.squareup.moshi;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.AnnotatedElement; import java.lang.annotation.Annotation;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.util.Set;
import okio.Buffer; import okio.Buffer;
import okio.BufferedSink; import okio.BufferedSink;
import okio.BufferedSource; import okio.BufferedSource;
@@ -107,6 +108,6 @@ public abstract class JsonAdapter<T> {
* <p>Implementations may use to {@link Moshi#adapter} to compose adapters of other types, or * <p>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. * {@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<? extends Annotation> annotations, Moshi moshi);
} }
} }

View File

@@ -17,6 +17,9 @@ package com.squareup.moshi;
/** Thrown when a JSON document doesn't match the expected format. */ /** Thrown when a JSON document doesn't match the expected format. */
public final class JsonDataException extends RuntimeException { public final class JsonDataException extends RuntimeException {
public JsonDataException() {
}
public JsonDataException(String message) { public JsonDataException(String message) {
super(message); super(message);
} }

View File

@@ -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 {
}

View File

@@ -16,9 +16,10 @@
package com.squareup.moshi; package com.squareup.moshi;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.AnnotatedElement; import java.lang.annotation.Annotation;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.util.Map; import java.util.Map;
import java.util.Set;
/** /**
* Converts maps with string keys to JSON objects. * Converts maps with string keys to JSON objects.
@@ -27,7 +28,9 @@ import java.util.Map;
*/ */
final class MapJsonAdapter<K, V> extends JsonAdapter<Map<K, V>> { final class MapJsonAdapter<K, V> extends JsonAdapter<Map<K, V>> {
public static final Factory FACTORY = new Factory() { public static final Factory FACTORY = new Factory() {
@Override public JsonAdapter<?> create(Type type, AnnotatedElement annotations, Moshi moshi) { @Override public JsonAdapter<?> create(
Type type, Set<? extends Annotation> annotations, Moshi moshi) {
if (!annotations.isEmpty()) return null;
Class<?> rawType = Types.getRawType(type); Class<?> rawType = Types.getRawType(type);
if (rawType != Map.class) return null; if (rawType != Map.class) return null;
Type[] keyAndValue = Types.mapKeyAndValueTypes(type, rawType); Type[] keyAndValue = Types.mapKeyAndValueTypes(type, rawType);

View File

@@ -17,11 +17,11 @@ package com.squareup.moshi;
import java.io.IOException; import java.io.IOException;
import java.lang.annotation.Annotation; import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Set;
/** /**
* Coordinates binding between JSON values and Java objects. * Coordinates binding between JSON values and Java objects.
@@ -51,19 +51,22 @@ public final class Moshi {
return adapter(type, Util.NO_ANNOTATIONS); return adapter(type, Util.NO_ANNOTATIONS);
} }
public <T> JsonAdapter<T> adapter(Type type, AnnotatedElement annotations) { public <T> JsonAdapter<T> adapter(Type type, Set<? extends Annotation> annotations) {
// TODO: support re-entrant calls.
return createAdapter(0, type, annotations); return createAdapter(0, type, annotations);
} }
public <T> JsonAdapter<T> nextAdapter(JsonAdapter.Factory skipPast, Type type, public <T> JsonAdapter<T> nextAdapter(JsonAdapter.Factory skipPast, Type type,
AnnotatedElement annotations) { Set<? extends Annotation> annotations) {
return createAdapter(factories.indexOf(skipPast) + 1, type, annotations); return createAdapter(factories.indexOf(skipPast) + 1, type, annotations);
} }
public <T> JsonAdapter<T> nextAdapter(JsonAdapter.Factory skipPast, Type type) {
return nextAdapter(skipPast, type, Util.NO_ANNOTATIONS);
}
@SuppressWarnings("unchecked") // Factories are required to return only matching JsonAdapters. @SuppressWarnings("unchecked") // Factories are required to return only matching JsonAdapters.
private <T> JsonAdapter<T> createAdapter( private <T> JsonAdapter<T> createAdapter(
int firstIndex, Type type, AnnotatedElement annotations) { int firstIndex, Type type, Set<? extends Annotation> annotations) {
List<DeferredAdapter<?>> deferredAdapters = reentrantCalls.get(); List<DeferredAdapter<?>> deferredAdapters = reentrantCalls.get();
if (deferredAdapters == null) { if (deferredAdapters == null) {
deferredAdapters = new ArrayList<>(); deferredAdapters = new ArrayList<>();
@@ -91,7 +94,7 @@ public final class Moshi {
deferredAdapters.remove(deferredAdapters.size() - 1); 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 { public static final class Builder {
@@ -103,8 +106,8 @@ public final class Moshi {
return add(new JsonAdapter.Factory() { return add(new JsonAdapter.Factory() {
@Override public JsonAdapter<?> create( @Override public JsonAdapter<?> create(
Type targetType, AnnotatedElement annotations, Moshi moshi) { Type targetType, Set<? extends Annotation> annotations, Moshi moshi) {
return Util.typesMatch(type, targetType) ? jsonAdapter : null; 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 (type == null) throw new IllegalArgumentException("type == null");
if (annotation == null) throw new IllegalArgumentException("annotation == null"); if (annotation == null) throw new IllegalArgumentException("annotation == null");
if (jsonAdapter == null) throw new IllegalArgumentException("jsonAdapter == 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() { return add(new JsonAdapter.Factory() {
@Override public JsonAdapter<?> create( @Override public JsonAdapter<?> create(
Type targetType, AnnotatedElement annotations, Moshi moshi) { Type targetType, Set<? extends Annotation> annotations, Moshi moshi) {
return Util.typesMatch(type, targetType) && annotations.isAnnotationPresent(annotation) if (!Util.typesMatch(type, targetType)) return null;
? jsonAdapter if (!Util.isAnnotationPresent(annotations, annotation)) return null;
: null; return jsonAdapter;
} }
}); });
} }
@@ -150,10 +156,10 @@ public final class Moshi {
*/ */
private static class DeferredAdapter<T> extends JsonAdapter<T> { private static class DeferredAdapter<T> extends JsonAdapter<T> {
private Type type; private Type type;
private AnnotatedElement annotations; private Set<? extends Annotation> annotations;
private JsonAdapter<T> delegate; private JsonAdapter<T> delegate;
public DeferredAdapter(Type type, AnnotatedElement annotations) { public DeferredAdapter(Type type, Set<? extends Annotation> annotations) {
this.type = type; this.type = type;
this.annotations = annotations; this.annotations = annotations;
} }

View File

@@ -16,13 +16,16 @@
package com.squareup.moshi; package com.squareup.moshi;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.AnnotatedElement; import java.lang.annotation.Annotation;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.util.Arrays; import java.util.Arrays;
import java.util.Set;
final class StandardJsonAdapters { final class StandardJsonAdapters {
public static final JsonAdapter.Factory FACTORY = new JsonAdapter.Factory() { 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<? extends Annotation> annotations, Moshi moshi) {
if (!annotations.isEmpty()) return null;
if (type == boolean.class) return BOOLEAN_JSON_ADAPTER; if (type == boolean.class) return BOOLEAN_JSON_ADAPTER;
if (type == byte.class) return BYTE_JSON_ADAPTER; if (type == byte.class) return BYTE_JSON_ADAPTER;
if (type == char.class) return CHARACTER_JSON_ADAPTER; if (type == char.class) return CHARACTER_JSON_ADAPTER;

View File

@@ -18,27 +18,39 @@ package com.squareup.moshi;
import java.lang.annotation.Annotation; import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement; import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
final class Util { final class Util {
public static final Annotation[] EMPTY_ANNOTATIONS_ARRAY = new Annotation[0]; public static final Set<Annotation> NO_ANNOTATIONS = Collections.emptySet();
public static final AnnotatedElement NO_ANNOTATIONS = new AnnotatedElement() {
@Override public boolean isAnnotationPresent(Class<? extends Annotation> aClass) {
return false;
}
@Override public <T extends Annotation> T getAnnotation(Class<T> tClass) {
return null;
}
@Override public Annotation[] getAnnotations() {
return EMPTY_ANNOTATIONS_ARRAY;
}
@Override public Annotation[] getDeclaredAnnotations() {
return EMPTY_ANNOTATIONS_ARRAY;
}
};
public static boolean typesMatch(Type pattern, Type candidate) { public static boolean typesMatch(Type pattern, Type candidate) {
// TODO: permit raw types (like Set.class) to match non-raw candidates (like Set<Long>). // TODO: permit raw types (like Set.class) to match non-raw candidates (like Set<Long>).
return pattern.equals(candidate); return pattern.equals(candidate);
} }
public static Set<? extends Annotation> jsonAnnotations(AnnotatedElement annotatedElement) {
return jsonAnnotations(annotatedElement.getAnnotations());
}
public static Set<? extends Annotation> jsonAnnotations(Annotation[] annotations) {
Set<Annotation> 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<? extends Annotation> annotations, Class<? extends Annotation> 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;
}
} }

View File

@@ -16,9 +16,10 @@
package com.squareup.moshi; package com.squareup.moshi;
import java.io.IOException; import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.util.Set;
import org.junit.Test; import org.junit.Test;
import static java.lang.annotation.RetentionPolicy.RUNTIME; import static java.lang.annotation.RetentionPolicy.RUNTIME;
@@ -63,10 +64,12 @@ public final class CircularAdaptersTest {
} }
@Retention(RUNTIME) @Retention(RUNTIME)
@JsonQualifier
public @interface Left { public @interface Left {
} }
@Retention(RUNTIME) @Retention(RUNTIME)
@JsonQualifier
public @interface Right { public @interface Right {
} }
@@ -97,19 +100,20 @@ public final class CircularAdaptersTest {
* work. * work.
*/ */
static class PrefixingNodeFactory implements JsonAdapter.Factory { static class PrefixingNodeFactory implements JsonAdapter.Factory {
@Override public JsonAdapter<?> create(Type type, AnnotatedElement annotations, Moshi moshi) { @Override public JsonAdapter<?> create(
Type type, Set<? extends Annotation> annotations, Moshi moshi) {
if (type != Node.class) return null; if (type != Node.class) return null;
final String prefix; final String prefix;
if (annotations.isAnnotationPresent(Left.class)) { if (Util.isAnnotationPresent(annotations, Left.class)) {
prefix = "L "; prefix = "L ";
} else if (annotations.isAnnotationPresent(Right.class)) { } else if (Util.isAnnotationPresent(annotations, Right.class)) {
prefix = "R "; prefix = "R ";
} else { } else {
return null; return null;
} }
final JsonAdapter<Node> delegate = moshi.nextAdapter(this, Node.class, annotations); final JsonAdapter<Node> delegate = moshi.nextAdapter(this, Node.class);
return new JsonAdapter<Node>() { return new JsonAdapter<Node>() {
@Override public void toJson(JsonWriter writer, Node value) throws IOException { @Override public void toJson(JsonWriter writer, Node value) throws IOException {

View File

@@ -391,7 +391,7 @@ public final class ClassJsonAdapterTest {
private <T> String toJson(Class<T> type, T value) throws IOException { private <T> String toJson(Class<T> type, T value) throws IOException {
@SuppressWarnings("unchecked") // Factory.create returns an adapter that matches its argument. @SuppressWarnings("unchecked") // Factory.create returns an adapter that matches its argument.
JsonAdapter<T> jsonAdapter = (JsonAdapter<T>) ClassJsonAdapter.FACTORY.create( JsonAdapter<T> jsonAdapter = (JsonAdapter<T>) ClassJsonAdapter.FACTORY.create(
type, NO_ANNOTATIONS, moshi); type, NO_ANNOTATIONS, moshi);
// Wrap in an array to avoid top-level object warnings without going completely lenient. // Wrap in an array to avoid top-level object warnings without going completely lenient.
@@ -409,7 +409,7 @@ public final class ClassJsonAdapterTest {
private <T> T fromJson(Class<T> type, String json) throws IOException { private <T> T fromJson(Class<T> type, String json) throws IOException {
@SuppressWarnings("unchecked") // Factory.create returns an adapter that matches its argument. @SuppressWarnings("unchecked") // Factory.create returns an adapter that matches its argument.
JsonAdapter<T> jsonAdapter = (JsonAdapter<T>) ClassJsonAdapter.FACTORY.create( JsonAdapter<T> jsonAdapter = (JsonAdapter<T>) ClassJsonAdapter.FACTORY.create(
type, NO_ANNOTATIONS, moshi); type, NO_ANNOTATIONS, moshi);
// Wrap in an array to avoid top-level object warnings without going completely lenient. // Wrap in an array to avoid top-level object warnings without going completely lenient.
JsonReader jsonReader = newReader("[" + json + "]"); JsonReader jsonReader = newReader("[" + json + "]");

View File

@@ -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<StringAndFooString> 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<StringAndFooString> 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<StringAndFooBazString> 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<StringAndFooBazString> 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<StringAndFooBazString> 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<StringAndFooBazString> 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<String> fooPrefixAdapter = new JsonAdapter<String>() {
@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<StringAndFooString> 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<DateAndMillisDate> 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<Date> jsonAdapter = new JsonAdapter<Date>() {
@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);
}
}
}

View File

@@ -16,8 +16,8 @@
package com.squareup.moshi; package com.squareup.moshi;
import java.io.IOException; import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.util.ArrayDeque; import java.util.ArrayDeque;
@@ -32,6 +32,7 @@ import org.junit.Test;
import static com.squareup.moshi.TestUtil.newReader; import static com.squareup.moshi.TestUtil.newReader;
import static java.lang.annotation.RetentionPolicy.RUNTIME; import static java.lang.annotation.RetentionPolicy.RUNTIME;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
public final class MoshiTest { public final class MoshiTest {
@@ -591,7 +592,8 @@ public final class MoshiTest {
.add(new UppercaseAdapterFactory()) .add(new UppercaseAdapterFactory())
.build(); .build();
AnnotatedElement annotations = MoshiTest.class.getDeclaredField("uppercaseString"); Field uppercaseString = MoshiTest.class.getDeclaredField("uppercaseString");
Set<? extends Annotation> annotations = Util.jsonAnnotations(uppercaseString);
JsonAdapter<String> adapter = moshi.<String>adapter(String.class, annotations).lenient(); JsonAdapter<String> adapter = moshi.<String>adapter(String.class, annotations).lenient();
assertThat(adapter.toJson("a")).isEqualTo("\"A\""); assertThat(adapter.toJson("a")).isEqualTo("\"A\"");
assertThat(adapter.fromJson("\"b\"")).isEqualTo("B"); assertThat(adapter.fromJson("\"b\"")).isEqualTo("B");
@@ -638,10 +640,14 @@ public final class MoshiTest {
.build(); .build();
Field uppercaseStringsField = MoshiTest.class.getDeclaredField("uppercaseStrings"); Field uppercaseStringsField = MoshiTest.class.getDeclaredField("uppercaseStrings");
JsonAdapter<List<String>> adapter = moshi.adapter(uppercaseStringsField.getGenericType(), try {
uppercaseStringsField); moshi.adapter(uppercaseStringsField.getGenericType(),
assertThat(adapter.toJson(Arrays.asList("a"))).isEqualTo("[\"a\"]"); Util.jsonAnnotations(uppercaseStringsField));
assertThat(adapter.fromJson("[\"b\"]")).isEqualTo(Arrays.asList("b")); fail();
} catch (IllegalArgumentException expected) {
assertEquals("no JsonAdapter for java.util.List<java.lang.String> annotated "
+ "[@com.squareup.moshi.MoshiTest$Uppercase()]", expected.getMessage());
}
} }
@Test public void objectArray() throws Exception { @Test public void objectArray() throws Exception {
@@ -753,9 +759,8 @@ public final class MoshiTest {
static class MealDealAdapterFactory implements JsonAdapter.Factory { static class MealDealAdapterFactory implements JsonAdapter.Factory {
@Override public JsonAdapter<?> create( @Override public JsonAdapter<?> create(
Type type, AnnotatedElement annotations, Moshi moshi) { Type type, Set<? extends Annotation> annotations, Moshi moshi) {
if (!type.equals(MealDeal.class)) return null; if (!type.equals(MealDeal.class)) return null;
final JsonAdapter<Pizza> pizzaAdapter = moshi.adapter(Pizza.class); final JsonAdapter<Pizza> pizzaAdapter = moshi.adapter(Pizza.class);
final JsonAdapter<String> drinkAdapter = moshi.adapter(String.class); final JsonAdapter<String> drinkAdapter = moshi.adapter(String.class);
return new JsonAdapter<MealDeal>() { return new JsonAdapter<MealDeal>() {
@@ -778,16 +783,17 @@ public final class MoshiTest {
} }
@Retention(RUNTIME) @Retention(RUNTIME)
@JsonQualifier
public @interface Uppercase { public @interface Uppercase {
} }
static class UppercaseAdapterFactory implements JsonAdapter.Factory { static class UppercaseAdapterFactory implements JsonAdapter.Factory {
@Override public JsonAdapter<?> create( @Override public JsonAdapter<?> create(
Type type, AnnotatedElement annotations, Moshi moshi) { Type type, Set<? extends Annotation> annotations, Moshi moshi) {
if (!type.equals(String.class)) return null; if (!type.equals(String.class)) return null;
if (!annotations.isAnnotationPresent(Uppercase.class)) return null; if (!Util.isAnnotationPresent(annotations, Uppercase.class)) return null;
final JsonAdapter<String> stringAdapter = moshi.nextAdapter(this, String.class, annotations); final JsonAdapter<String> stringAdapter = moshi.nextAdapter(this, String.class);
return new JsonAdapter<String>() { return new JsonAdapter<String>() {
@Override public String fromJson(JsonReader reader) throws IOException { @Override public String fromJson(JsonReader reader) throws IOException {
String s = stringAdapter.fromJson(reader); String s = stringAdapter.fromJson(reader);