package org.openflexo.ws.jira; import java.io.IOException; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.lang.reflect.TypeVariable; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import org.openflexo.ws.jira.model.JIRAObject; import com.google.gson.Gson; import com.google.gson.JsonSyntaxException; import com.google.gson.TypeAdapter; import com.google.gson.TypeAdapterFactory; import com.google.gson.annotations.SerializedName; import com.google.gson.internal.$Gson$Types; import com.google.gson.internal.Primitives; import com.google.gson.internal.bind.ReflectiveTypeAdapterFactory; import com.google.gson.reflect.TypeToken; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonToken; import com.google.gson.stream.JsonWriter; public class JiraObjectTypeAdapterFactory implements TypeAdapterFactory { public class TypeAdapterRuntimeTypeWrapper<T> extends TypeAdapter<T> { private final Gson context; private final TypeAdapter<T> delegate; private final Type type; TypeAdapterRuntimeTypeWrapper(Gson context, TypeAdapter<T> delegate, Type type) { this.context = context; this.delegate = delegate; this.type = type; } @Override public T read(JsonReader in) throws IOException { return delegate.read(in); } @SuppressWarnings({ "rawtypes", "unchecked" }) @Override public void write(JsonWriter out, T value) throws IOException { // Order of preference for choosing type adapters // First preference: a type adapter registered for the runtime type // Second preference: a type adapter registered for the declared type // Third preference: reflective type adapter for the runtime type (if it is a sub class of the declared type) // Fourth preference: reflective type adapter for the declared type TypeAdapter chosen = delegate; Type runtimeType = getRuntimeTypeIfMoreSpecific(type, value); if (runtimeType != type) { TypeAdapter runtimeTypeAdapter = context.getAdapter(TypeToken.get(runtimeType)); if (!(runtimeTypeAdapter instanceof ReflectiveTypeAdapterFactory.Adapter)) { // The user registered a type adapter for the runtime type, so we will use that chosen = runtimeTypeAdapter; } else if (!(delegate instanceof ReflectiveTypeAdapterFactory.Adapter)) { // The user registered a type adapter for Base class, so we prefer it over the // reflective type adapter for the runtime type chosen = delegate; } else { // Use the type adapter for runtime type chosen = runtimeTypeAdapter; } } chosen.write(out, value); } /** * Finds a compatible runtime type if it is more specific */ private Type getRuntimeTypeIfMoreSpecific(Type type, Object value) { if (value != null && (type == Object.class || type instanceof TypeVariable<?> || type instanceof Class<?>)) { type = value.getClass(); } return type; } } private String getFieldName(Field f) { SerializedName serializedName = f.getAnnotation(SerializedName.class); return serializedName == null ? f.getName() : serializedName.value(); } @Override public <T> TypeAdapter<T> create(Gson gson, final TypeToken<T> type) { Class<?> raw = type.getRawType(); if (JIRAObject.class.isAssignableFrom(raw)) { Type valueType = $Gson$Types.getMapKeyAndValueTypes(type.getType(), raw)[1]; TypeAdapter<Object> valueAdapter = (TypeAdapter<Object>) gson.getAdapter(TypeToken.get(valueType)); return new Adapter(type, new TypeAdapterRuntimeTypeWrapper(gson, valueAdapter, valueType), getBoundFields(gson, type, raw)); } if (List.class.isAssignableFrom(raw)) { Type parameter = ((ParameterizedType) type.getType()).getActualTypeArguments()[0]; if (parameter instanceof TypeVariable) { Type type2 = ((TypeVariable) parameter).getBounds()[0]; if (JIRAObject.class.isAssignableFrom($Gson$Types.getRawType(type2))) { TypeAdapter<Collection> collectionAdapter = gson.getAdapter(Collection.class); TypeAdapter<JIRAObject> jiraObjectAdapter = gson.getAdapter(JIRAObject.class); return new JIRAObjectListAdapter<T>(collectionAdapter, jiraObjectAdapter); } } } return null; } private static class JIRAObjectListAdapter<T> extends TypeAdapter<T> { private final TypeAdapter<Collection> collectionAdapter; private final TypeAdapter<JIRAObject> jiraObjectAdapter; public JIRAObjectListAdapter(TypeAdapter<Collection> collectionAdapter, TypeAdapter<JIRAObject> jiraObjectAdapter) { this.collectionAdapter = collectionAdapter; this.jiraObjectAdapter = jiraObjectAdapter; } @Override public void write(JsonWriter out, T value) throws IOException { collectionAdapter.write(out, (Collection) value); } @Override public T read(JsonReader in) throws IOException { if (in.peek() == JsonToken.NULL) { in.nextNull(); return null; } Collection collection = new ArrayList(); in.beginArray(); while (in.hasNext()) { JIRAObject<?> instance = jiraObjectAdapter.read(in); collection.add(instance); } in.endArray(); return (T) collection; } } private JiraObjectTypeAdapterFactory.BoundField createBoundField(final Gson context, final Field field, final String name, final TypeToken<?> fieldType) { final boolean isPrimitive = Primitives.isPrimitive(fieldType.getRawType()); // special casing primitives here saves ~5% on Android... return new JiraObjectTypeAdapterFactory.BoundField(name) { final TypeAdapter<?> typeAdapter = context.getAdapter(fieldType); @SuppressWarnings({ "unchecked", "rawtypes" }) // the type adapter and field type always agree @Override void write(JsonWriter writer, Object value) throws IOException, IllegalAccessException { Object fieldValue = field.get(value); TypeAdapter t = new TypeAdapterRuntimeTypeWrapper(context, this.typeAdapter, fieldType.getType()); t.write(writer, fieldValue); } @Override void read(JsonReader reader, Object value) throws IOException, IllegalAccessException { Object fieldValue = typeAdapter.read(reader); if (fieldValue != null || !isPrimitive) { field.set(value, fieldValue); } } }; } private Map<String, BoundField> getBoundFields(Gson context, TypeToken<?> type, Class<?> raw) { Map<String, BoundField> result = new LinkedHashMap<String, BoundField>(); if (raw.isInterface()) { return result; } Type declaredType = type.getType(); while (raw != null && raw != HashMap.class) { Field[] fields = raw.getDeclaredFields(); for (Field field : fields) { if (Modifier.isStatic(field.getModifiers()) || Modifier.isFinal(field.getModifiers()) || Modifier.isTransient(field.getModifiers())) { continue; } field.setAccessible(true); Type fieldType = $Gson$Types.resolve(type.getType(), raw, field.getGenericType()); BoundField boundField = createBoundField(context, field, getFieldName(field), TypeToken.get(fieldType)); BoundField previous = result.put(boundField.name, boundField); if (previous != null) { throw new IllegalArgumentException(declaredType + " declares multiple JSON fields named " + previous.name); } } type = TypeToken.get($Gson$Types.resolve(type.getType(), raw, raw.getGenericSuperclass())); raw = type.getRawType(); } return result; } static abstract class BoundField { final String name; protected BoundField(String name) { this.name = name; } abstract void write(JsonWriter writer, Object value) throws IOException, IllegalAccessException; abstract void read(JsonReader reader, Object value) throws IOException, IllegalAccessException; } public final class Adapter<T extends JIRAObject<T>> extends TypeAdapter<T> { private final Map<String, BoundField> boundFields; private final TypeToken<T> type; private final TypeAdapter<Object> valueTypeAdapter; public Adapter(TypeToken<T> type, TypeAdapter<Object> valueTypeAdapter, Map<String, BoundField> boundFields) { this.type = type; this.valueTypeAdapter = valueTypeAdapter; this.boundFields = boundFields; } @Override public T read(JsonReader in) throws IOException { if (in.peek() == JsonToken.NULL) { in.nextNull(); return null; } T instance; try { instance = (T) type.getRawType().newInstance(); } catch (InstantiationException e1) { throw new RuntimeException(e1); } catch (IllegalAccessException e1) { throw new RuntimeException(e1); } try { in.beginObject(); while (in.hasNext()) { String name = in.nextName(); BoundField field = boundFields.get(name); if (field == null) { instance.put(name, valueTypeAdapter.read(in)); } else { field.read(in, instance); } } } catch (IllegalStateException e) { throw new JsonSyntaxException(e); } catch (IllegalAccessException e) { throw new AssertionError(e); } in.endObject(); return instance; } @Override public void write(JsonWriter out, T value) throws IOException { if (value == null) { out.nullValue(); // TODO: better policy here? return; } out.beginObject(); try { for (BoundField boundField : boundFields.values()) { out.name(boundField.name); boundField.write(out, value); } for (Map.Entry<String, Object> e : value.entrySet()) { out.name(e.getKey()); valueTypeAdapter.write(out, e.getValue()); } } catch (IllegalAccessException e) { throw new AssertionError(); } out.endObject(); } } }