Convert RequiredFieldsCheckOnReadTypeAdapterFactory to kotlin

This commit is contained in:
Paul Hawke 2024-12-01 21:19:56 -06:00
parent aa05bce593
commit a5fb255bb3
4 changed files with 87 additions and 115 deletions

View file

@ -1,94 +0,0 @@
package fr.free.nrw.commons.wikidata.json;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.collection.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 fr.free.nrw.commons.wikidata.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).
*/
public 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;
}
}
}

View file

@ -0,0 +1,75 @@
package fr.free.nrw.commons.wikidata.json
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 fr.free.nrw.commons.wikidata.json.annotations.Required
import java.io.IOException
import java.lang.reflect.Field
/**
* 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 : TypeAdapterFactory {
override fun <T> create(gson: Gson, typeToken: TypeToken<T>): TypeAdapter<T>? {
val rawType: Class<*> = typeToken.rawType
val requiredFields = collectRequiredFields(rawType)
if (requiredFields.isEmpty()) {
return null
}
for (field in requiredFields) {
field.isAccessible = true
}
return Adapter(gson.getDelegateAdapter(this, typeToken), requiredFields)
}
private fun collectRequiredFields(clazz: Class<*>): Set<Field> = buildSet {
for (field in clazz.declaredFields) {
if (field.isAnnotationPresent(Required::class.java)) add(field)
}
}
private class Adapter<T>(
private val delegate: TypeAdapter<T>,
private val requiredFields: Set<Field>
) : TypeAdapter<T>() {
@Throws(IOException::class)
override fun write(out: JsonWriter, value: T?) =
delegate.write(out, value)
@Throws(IOException::class)
override fun read(reader: JsonReader): T? =
if (allRequiredFieldsPresent(delegate.read(reader), requiredFields))
delegate.read(reader)
else
null
fun allRequiredFieldsPresent(deserialized: T, required: Set<Field>): Boolean {
for (field in required) {
try {
if (field[deserialized] == null) return false
} catch (e: IllegalArgumentException) {
throw JsonParseException(e)
} catch (e: IllegalAccessException) {
throw JsonParseException(e)
}
}
return true
}
}
}

View file

@ -1,21 +0,0 @@
package fr.free.nrw.commons.wikidata.json.annotations;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.FIELD;
/**
* Annotate fields in Retrofit POJO classes with this to enforce their presence in order to return
* an instantiated object.
*
* E.g.: @NonNull @Required private String title;
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(FIELD)
public @interface Required {
}

View file

@ -0,0 +1,12 @@
package fr.free.nrw.commons.wikidata.json.annotations
/**
* Annotate fields in Retrofit POJO classes with this to enforce their presence in order to return
* an instantiated object.
*
* E.g.: @NonNull @Required private String title;
*/
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FIELD)
annotation class Required