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;
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<Type, ToAdapter> toAdapters;
private final Map<Type, FromAdapter> fromAdapters;
private final List<AdapterMethod> toAdapters;
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.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<? extends Annotation> 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<Object> 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<Type, ToAdapter> toAdapters = new LinkedHashMap<>();
Map<Type, FromAdapter> fromAdapters = new LinkedHashMap<>();
List<AdapterMethod> toAdapters = new ArrayList<>();
List<AdapterMethod> 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<? 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)
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<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)
throws IOException, InvocationTargetException, IllegalAccessException {
JsonAdapter<Object> delegate = moshi.adapter(returnType, method);
JsonAdapter<Object> 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<? extends Annotation> 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<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)
throws IOException, IllegalAccessException, InvocationTargetException {
JsonAdapter<Object> delegate = moshi.adapter(parameterTypes[0]);
JsonAdapter<Object> 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<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 Set<? extends Annotation> annotations;
final Object adapter;
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.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();
}
}
}

View File

@@ -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<Object> {
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);
if (elementType == null) return null;
if (!annotations.isEmpty()) return null;
Class<?> elementClass = Types.getRawType(elementType);
JsonAdapter<Object> elementAdapter = moshi.adapter(elementType);
return new ArrayJsonAdapter(elementClass, elementAdapter).nullSafe();

View File

@@ -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<T> extends JsonAdapter<T> {
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);
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<T> extends JsonAdapter<T> {
// Look up a type adapter for this type.
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.
field.setAccessible(true);

View File

@@ -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<C extends Collection<T>, T> extends JsonAdapter<C> {
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);
if (!annotations.isEmpty()) return null;
if (rawType == List.class || rawType == Collection.class) {
return newArrayListAdapter(type, moshi).nullSafe();
} else if (rawType == Set.class) {

View File

@@ -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<T> {
* <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.
*/
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. */
public final class JsonDataException extends RuntimeException {
public JsonDataException() {
}
public JsonDataException(String 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;
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<K, V> extends JsonAdapter<Map<K, V>> {
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);
if (rawType != Map.class) return null;
Type[] keyAndValue = Types.mapKeyAndValueTypes(type, rawType);

View File

@@ -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 <T> JsonAdapter<T> adapter(Type type, AnnotatedElement annotations) {
// TODO: support re-entrant calls.
public <T> JsonAdapter<T> adapter(Type type, Set<? extends Annotation> annotations) {
return createAdapter(0, type, annotations);
}
public <T> JsonAdapter<T> nextAdapter(JsonAdapter.Factory skipPast, Type type,
AnnotatedElement annotations) {
Set<? extends Annotation> 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.
private <T> JsonAdapter<T> createAdapter(
int firstIndex, Type type, AnnotatedElement annotations) {
int firstIndex, Type type, Set<? extends Annotation> annotations) {
List<DeferredAdapter<?>> 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<? extends Annotation> 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<? extends Annotation> 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<T> extends JsonAdapter<T> {
private Type type;
private AnnotatedElement annotations;
private Set<? extends Annotation> annotations;
private JsonAdapter<T> delegate;
public DeferredAdapter(Type type, AnnotatedElement annotations) {
public DeferredAdapter(Type type, Set<? extends Annotation> annotations) {
this.type = type;
this.annotations = annotations;
}

View File

@@ -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<? extends Annotation> 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;

View File

@@ -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<? 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 final Set<Annotation> 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<Long>).
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;
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<? extends Annotation> 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<Node> delegate = moshi.nextAdapter(this, Node.class, annotations);
final JsonAdapter<Node> delegate = moshi.nextAdapter(this, Node.class);
return new JsonAdapter<Node>() {
@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 {
@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);
// 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 {
@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);
// Wrap in an array to avoid top-level object warnings without going completely lenient.
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;
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<? extends Annotation> annotations = Util.jsonAnnotations(uppercaseString);
JsonAdapter<String> adapter = moshi.<String>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<List<String>> 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<java.lang.String> 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<? extends Annotation> annotations, Moshi moshi) {
if (!type.equals(MealDeal.class)) return null;
final JsonAdapter<Pizza> pizzaAdapter = moshi.adapter(Pizza.class);
final JsonAdapter<String> drinkAdapter = moshi.adapter(String.class);
return new JsonAdapter<MealDeal>() {
@@ -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<? extends Annotation> 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<String> stringAdapter = moshi.nextAdapter(this, String.class, annotations);
final JsonAdapter<String> stringAdapter = moshi.nextAdapter(this, String.class);
return new JsonAdapter<String>() {
@Override public String fromJson(JsonReader reader) throws IOException {
String s = stringAdapter.fromJson(reader);