blob: 8e0cc3f222e18701944dde386539a1dce1f6e334 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.jclouds.json.internal;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Predicates.in;
import static com.google.common.collect.Iterables.filter;
import static com.google.common.collect.Iterables.get;
import static com.google.common.collect.Iterables.size;
import static com.google.common.collect.Iterables.transform;
import static com.google.common.collect.Iterables.tryFind;
import static org.jclouds.reflect.Reflection2.constructors;
import java.beans.ConstructorProperties;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import javax.inject.Named;
import org.jclouds.json.SerializedNames;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Predicate;
import com.google.common.base.Supplier;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.Maps;
import com.google.common.reflect.Invokable;
import com.google.common.reflect.Parameter;
import com.google.common.reflect.TypeToken;
import com.google.gson.FieldNamingStrategy;
import com.google.gson.annotations.SerializedName;
/**
* NamingStrategies used for JSON deserialization using GSON
*/
public class NamingStrategies {
/**
* Specifies how to extract the name from an annotation for use in determining the serialized name.
*
* @see com.google.gson.annotations.SerializedName
* @see ExtractSerializedName
*/
public abstract static class NameExtractor<A extends Annotation> implements Function<Annotation, String>,
Supplier<Predicate<Annotation>> {
protected final Class<A> annotationType;
protected final Predicate<Annotation> predicate;
protected NameExtractor(final Class<A> annotationType) {
this.annotationType = checkNotNull(annotationType, "annotationType");
this.predicate = new Predicate<Annotation>() {
public boolean apply(Annotation input) {
return input.getClass().equals(annotationType);
}
};
}
@SuppressWarnings("unchecked")
public Class<Annotation> annotationType() {
return (Class<Annotation>) annotationType;
}
@Override
public String apply(Annotation in) {
return extractName(annotationType.cast(in));
}
protected abstract String extractName(A cast);
@Override
public Predicate<Annotation> get() {
return predicate;
}
@Override
public String toString() {
return "nameExtractor(" + annotationType.getSimpleName() + ")";
}
@Override
public int hashCode() {
return annotationType.hashCode();
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null || getClass() != obj.getClass())
return false;
return annotationType.equals(NameExtractor.class.cast(obj).annotationType);
}
}
public static class ExtractSerializedName extends NameExtractor<SerializedName> {
public ExtractSerializedName() {
super(SerializedName.class);
}
public String extractName(SerializedName in) {
return checkNotNull(in, "input annotation").value();
}
}
public static class ExtractNamed extends NameExtractor<Named> {
public ExtractNamed() {
super(Named.class);
}
@Override
public String extractName(Named in) {
return checkNotNull(in, "input annotation").value();
}
}
public abstract static class AnnotationBasedNamingStrategy {
protected final Map<Class<? extends Annotation>, ? extends NameExtractor<?>> annotationToNameExtractor;
protected final String forToString;
public AnnotationBasedNamingStrategy(Iterable<? extends NameExtractor<?>> extractors) {
checkNotNull(extractors, "means to extract names by annotations");
this.annotationToNameExtractor = Maps.uniqueIndex(extractors,
new Function<NameExtractor<?>, Class<? extends Annotation>>() {
@Override
public Class<? extends Annotation> apply(NameExtractor<?> input) {
return input.annotationType();
}
});
this.forToString = Joiner.on(",").join(transform(extractors, new Function<NameExtractor<?>, String>() {
@Override
public String apply(NameExtractor<?> input) {
return input.annotationType().getName();
}
}));
}
@Override
public String toString() {
return "AnnotationBasedNamingStrategy requiring one of " + forToString;
}
}
/**
* Definition of field naming policy for annotation-based field
*/
public static class AnnotationFieldNamingStrategy extends AnnotationBasedNamingStrategy implements
FieldNamingStrategy {
public AnnotationFieldNamingStrategy(Iterable<? extends NameExtractor<?>> extractors) {
super(extractors);
checkArgument(extractors.iterator().hasNext(), "you must supply at least one name extractor, for example: "
+ ExtractSerializedName.class.getSimpleName());
}
@Override
public String translateName(Field f) {
// Determining if AutoValue is tough, since annotations are SOURCE retention.
if (Modifier.isAbstract(f.getDeclaringClass().getSuperclass().getModifiers())) { // AutoValue means abstract.
for (Invokable<?, ?> target : constructors(TypeToken.of(f.getDeclaringClass().getSuperclass()))) {
SerializedNames names = target.getAnnotation(SerializedNames.class);
if (names != null && target.isStatic()) { // == factory method
// Fields and constructor params are written by AutoValue in same order as methods are declared.
// By contract, SerializedNames factory methods must declare its names in the same order.
List<Field> declaredFields = Arrays.asList(f.getDeclaringClass().getDeclaredFields());
// Instrumentation libraries, such as JaCoCo might introduce synthetic fields to the class.
// We should ignore them
Iterable<Field> fields = filter(declaredFields, new Predicate<Field>() {
@Override
public boolean apply(Field input) {
return !input.isSynthetic();
}
});
int numFields = size(fields);
if (numFields != names.value().length) {
String message = "Incorrect number of names on %s. Class [%s]. Annotation config: [%s]. Fields in object; [%s]";
String types = Joiner.on(",").join(transform(fields, new Function<Field, String>() {
@Override
public String apply(Field input) {
return input.getType().getName();
}
}));
throw new IllegalStateException(String.format(message, names, f.getDeclaringClass().getName(),
Joiner.on(",").join(names.value()), types));
}
for (int i = 0; i < numFields; i++) {
if (get(fields, i).equals(f)) {
return names.value()[i];
}
}
// The input field was not a declared field. Accidentally placed on something not AutoValue?
throw new IllegalStateException("Inconsistent state. Ensure type is AutoValue on " + target);
}
}
}
for (Annotation annotation : f.getAnnotations()) {
if (annotationToNameExtractor.containsKey(annotation.annotationType())) {
return annotationToNameExtractor.get(annotation.annotationType()).apply(annotation);
}
}
return null;
}
}
public static class AnnotationOrNameFieldNamingStrategy extends AnnotationFieldNamingStrategy implements
FieldNamingStrategy {
public AnnotationOrNameFieldNamingStrategy(Iterable<? extends NameExtractor<?>> extractors) {
super(extractors);
}
@Override
public String translateName(Field f) {
String result = super.translateName(f);
return result == null ? f.getName() : result;
}
}
/**
* Determines field naming from constructor annotations
*/
public static final class AnnotationConstructorNamingStrategy extends AnnotationBasedNamingStrategy {
private final Predicate<Invokable<?, ?>> hasMarker;
private final Collection<? extends Class<? extends Annotation>> markers;
public AnnotationConstructorNamingStrategy(Collection<? extends Class<? extends Annotation>> markers,
Iterable<? extends NameExtractor<?>> extractors) {
super(extractors);
this.markers = checkNotNull(markers,
"you must supply at least one annotation to mark deserialization constructors");
this.hasMarker = hasAnnotationIn(markers);
}
private static Predicate<Invokable<?, ?>> hasAnnotationIn(
final Collection<? extends Class<? extends Annotation>> markers) {
return new Predicate<Invokable<?, ?>>() {
public boolean apply(Invokable<?, ?> input) {
return FluentIterable.from(Arrays.asList(input.getAnnotations()))
.transform(new Function<Annotation, Class<? extends Annotation>>() {
public Class<? extends Annotation> apply(Annotation input) {
return input.annotationType();
}
}).anyMatch((Predicate<Class<? extends Annotation>>) in(markers));
}
};
}
@VisibleForTesting
<T> Invokable<T, T> getDeserializer(TypeToken<T> token) {
return tryFind(constructors(token), hasMarker).orNull();
}
@SuppressWarnings("CollectionIncompatibleType")
@VisibleForTesting
<T> String translateName(Invokable<T, T> c, int index) {
String name = null;
if ((markers.contains(ConstructorProperties.class) && c.getAnnotation(ConstructorProperties.class) != null)
|| (markers.contains(SerializedNames.class) && c.getAnnotation(SerializedNames.class) != null)) {
String[] names = c.getAnnotation(SerializedNames.class) != null
? c.getAnnotation(SerializedNames.class).value()
: c.getAnnotation(ConstructorProperties.class).value();
if (names.length != c.getParameters().size()) {
String message = "Incorrect count of names on annotation of %s. Class: [%s]. Annotation config: [%s]. Parameters; [%s]";
String types = Joiner.on(",").join(transform(c.getParameters(), new Function<Parameter, String>() {
@Override
public String apply(Parameter input) {
return input.getClass().getName();
}
}));
throw new IllegalArgumentException(String.format(message, c, c.getDeclaringClass().getName(),
Joiner.on(",").join(names), types));
}
if (names != null && names.length > index) {
name = names[index];
}
} else {
for (Annotation annotation : c.getParameters().get(index).getAnnotations()) {
if (annotationToNameExtractor.containsKey(annotation.annotationType())) {
name = annotationToNameExtractor.get(annotation.annotationType()).apply(annotation);
break;
}
}
}
return name;
}
}
}