package io.norberg.automatter.gson; import com.google.gson.Gson; import com.google.gson.TypeAdapter; import com.google.gson.TypeAdapterFactory; import com.google.gson.annotations.SerializedName; import com.google.gson.reflect.TypeToken; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import io.norberg.automatter.AutoMatter; import static io.norberg.automatter.gson.AutoMatterTypeAdapter.createForInterface; import static io.norberg.automatter.gson.AutoMatterTypeAdapter.createForValue; import static java.util.Arrays.asList; public class AutoMatterTypeAdapterFactory implements TypeAdapterFactory { private static final String VALUE_SUFFIX = "Builder$Value"; private final ConcurrentMap<TypeToken, TypeAdapter> adapters = new ConcurrentHashMap<>(); @SuppressWarnings("unchecked") @Override public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> type) { final TypeAdapter<T> materialized; final AutoMatter annotation = type.getRawType().getAnnotation(AutoMatter.class); if (annotation != null) { // We are now the proud owners of an AutoMatter annotated interface. // Return the cached type, if present. final TypeAdapter cached = adapters.get(type); if (cached != null) { return cached; } // Look up and instantiate the value class final String name = type.getRawType().getName(); final int lastDollar = name.lastIndexOf("$"); final String valueName; if (lastDollar > -1) { final int lastDot = name.lastIndexOf("."); valueName = name.substring(0, lastDot + 1).concat(name.substring(lastDollar + 1)) + VALUE_SUFFIX; } else { valueName = name + VALUE_SUFFIX; } final Class<T> cls; try { cls = (Class<T>) Class.forName(valueName); } catch (ClassNotFoundException e) { throw new IllegalArgumentException("No builder found for @AutoMatter type: " + name, e); } // Find those magic remapping-of-name-annotations (SerializedName) final Map<String, List<String>> serializedNameMethods = getSerializedNameMethods(gson, type.getRawType(), cls); // If the interface passed to us didn't have any SerializedName annotations, go the fast path // and just pass it on the type adapter chain, // it will most likely end up in the ReflectiveTypeAdapterFactory, good riddance! // If it was annotated, we create a TypeAdapter that knows how to poke the json into // Java world submission. materialized = serializedNameMethods.isEmpty() ? gson.getAdapter(cls) : createForInterface(gson, cls, serializedNameMethods); } else { // Maybe a value class with SerializedName annotations? final Map<String, List<String>> serializedNameMethodsbuilder = new HashMap<>(); // Since AutoMatter supports inheritance we need to walk through all of the interfaces with // AutoMatter annotations. final Class<? super T> valueClass = type.getRawType(); for (Class<?> itf : valueClass.getInterfaces()) { if (itf.getAnnotation(AutoMatter.class) != null) { serializedNameMethodsbuilder.putAll(getSerializedNameMethods(gson, itf, valueClass)); } } final Map<String, List<String>> serializedNameMethods = serializedNameMethodsbuilder; // Either what we were passed wasn't a value class or it was not annotated with SerializedName // either way, we don't have to care, pass it on to the chain of factories that might be // applicable. Bye, bye! This should be the fast path. if (serializedNameMethods.isEmpty()) { return null; } // We create a TypeAdapter that knows how to read between the lines (A.K.A is annotation aware) // and can translate the restricted Java world names to beautiful free form json fields. Nom! materialized = createForValue(gson, this, type, serializedNameMethods); } // Cache the materialized type before returning final TypeAdapter<T> existing = adapters.putIfAbsent(type, materialized); return (existing != null) ? existing : materialized; } private <T> Map<String, List<String>> getSerializedNameMethods( final Gson gson, final Class<T> c, final Class<?> valueClass) { final Map<String, List<String>> methodToAnnotation = new HashMap<>(); for (Method method : c.getMethods()) { if (method.isAnnotationPresent(SerializedName.class)) { final SerializedName serializedName = method.getAnnotation(SerializedName.class); List<String> values = new ArrayList<>(); values.add(serializedName.value()); values.addAll(asList(serializedName.alternate())); methodToAnnotation.put( translateField(gson, method.getName(), valueClass), values ); } } return methodToAnnotation; } private String translateField(final Gson gson, final String fieldName, final Class<?> valueClass) { final Field field; try { field = valueClass.getDeclaredField(fieldName); } catch (NoSuchFieldException e) { throw new RuntimeException(e); } return gson.fieldNamingStrategy().translateName(field); } }