package org.wikipedia.json; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.util.ArraySet; import com.google.gson.Gson; import com.google.gson.JsonParseException; import com.google.gson.TypeAdapter; import com.google.gson.TypeAdapterFactory; import com.google.gson.reflect.TypeToken; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; import org.wikipedia.json.annotations.Required; import java.io.IOException; import java.lang.reflect.Field; import java.util.Collections; import java.util.Set; /** * TypeAdapterFactory that provides TypeAdapters that return null values for objects that are * missing fields annotated with @Required. * * BEWARE: This means that a List or other Collection of objects that have @Required fields can * contain null elements after deserialization! * * TODO: Handle null values in lists during deserialization, perhaps with a new @RequiredElements * annotation and another corresponding TypeAdapter(Factory). */ class RequiredFieldsCheckOnReadTypeAdapterFactory implements TypeAdapterFactory { @Nullable @Override public final <T> TypeAdapter<T> create(@NonNull Gson gson, @NonNull TypeToken<T> typeToken) { Class<?> rawType = typeToken.getRawType(); Set<Field> requiredFields = collectRequiredFields(rawType); if (requiredFields.isEmpty()) { return null; } setFieldsAccessible(requiredFields, true); return new Adapter<>(gson.getDelegateAdapter(this, typeToken), requiredFields); } @NonNull private Set<Field> collectRequiredFields(@NonNull Class<?> clazz) { Field[] fields = clazz.getDeclaredFields(); Set<Field> required = new ArraySet<>(); for (Field field : fields) { if (field.isAnnotationPresent(Required.class)) { required.add(field); } } return Collections.unmodifiableSet(required); } private void setFieldsAccessible(Iterable<Field> fields, boolean accessible) { for (Field field : fields) { field.setAccessible(accessible); } } private static final class Adapter<T> extends TypeAdapter<T> { @NonNull private final TypeAdapter<T> delegate; @NonNull private final Set<Field> requiredFields; private Adapter(@NonNull TypeAdapter<T> delegate, @NonNull final Set<Field> requiredFields) { this.delegate = delegate; this.requiredFields = requiredFields; } @Override public void write(JsonWriter out, T value) throws IOException { delegate.write(out, value); } @Override @Nullable public T read(JsonReader in) throws IOException { T deserialized = delegate.read(in); return allRequiredFieldsPresent(deserialized, requiredFields) ? deserialized : null; } private boolean allRequiredFieldsPresent(@NonNull T deserialized, @NonNull Set<Field> required) { for (Field field : required) { try { if (field.get(deserialized) == null) { return false; } } catch (IllegalArgumentException | IllegalAccessException e) { throw new JsonParseException(e); } } return true; } } }