org.coursera.courier.android.runtime.UnionAdapterFactory Maven / Gradle / Ivy
package org.coursera.courier.android.runtime;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
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 java.io.IOException;
import java.lang.reflect.Field;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
/**
* GSON {@link com.google.gson.TypeAdapterFactory} for Pegasus style unions.
*
* For example, consider a union "AnswerFormat" with the union member options of "TextEntry" and
* "MultipleChoice".
*
* Example JSON:
*
*
* {
* "org.example.TextEntry": { "textEntryField1": ... }
* }
*
*
* Example Usage with a GSON Java class:
*
*
* {@literal}JsonAdapter(AnswerFormat.Adapter.class)
* interface AnswerFormat {
* public final class TextEntryMember implements AnswerFormat {
* private static final String MEMBER_KEY = "org.example.TextEntry";
*
* {@literal}SerializedName(MEMBER_KEY)
* public TextEntry member;
* }
*
* public final class MultipleChoiceMember implements AnswerFormat {
* // ...
* }
*
* final class Adapter extends UnionAdapterFactory<AnswerFormat> {
* Adapter() {
* super(AnswerFormat.class, new Resolver<AnswerFormat>() {
* public Class<? extends AnswerFormat> resolve(String memberKey) {
* switch(memberKey) {
* case AnswerFormat.MEMBER_KEY: return TextEntryMember.class;
* case MultipleChoice.MEMBER_KEY: return MultipleChoice.class;
* // ...
* }
* }
* });
* }
* }
* }
*
*
* @param provides the marker interface that identifies the union. All union members wrappers
* must implement this interface.
*/
public class UnionAdapterFactory implements TypeAdapterFactory {
/**
* Provides a mapping of typedDefinition "typeName"s (which are the union tags) to their
* corresponding Java classes.
*
* @param provides the marker interface that identifies the union.
*/
public interface Resolver {
public Class extends T> resolve(String memberKey);
}
private Class unionClass;
private Resolver resolver;
public UnionAdapterFactory(Class unionClass, Resolver resolver) {
this.unionClass = unionClass;
this.resolver = resolver;
}
@Override
public TypeAdapter create(Gson gson, TypeToken type) {
if (unionClass.equals(type.getType())) {
return new UnionAdapter(gson);
} else {
return null;
}
}
private class UnionAdapter extends TypeAdapter {
private Gson gson;
public UnionAdapter(Gson gson) {
this.gson = gson;
}
@Override
@SuppressWarnings("unchecked")
public void write(JsonWriter out, T value) throws IOException {
if (!unionClass.equals(value.getClass())) {
TypeToken actualType = TypeToken.get((Class)value.getClass());
gson.getDelegateAdapter(UnionAdapterFactory.this, actualType).write(out, value);
}
// else GSON is actually trying to serialize an abstract class, which would be a GSON bug
}
@Override
@SuppressWarnings("unchecked")
public T read(JsonReader reader) throws IOException {
// Ideally we would stream the data from the reader here, but we need to inspect the
// 'typeName' field, which may appear after the 'definition' field.
JsonElement element = new JsonParser().parse(reader);
Set> entries = element.getAsJsonObject().entrySet();
if (entries.size() != 1) {
StringBuilder keys = new StringBuilder();
Iterator> iter = entries.iterator();
while (iter.hasNext()) {
keys.append("'").append(iter.next().getKey()).append("'");
if (iter.hasNext()) keys.append(", ");
}
throw new IOException(
"JSON object for '" + unionClass.getName() + "' union must contain exactly one " +
"'memberKey' field but contains " +
(keys.length() > 0 ? keys.toString() : "no fields") + " at path " + reader.getPath());
}
Map.Entry member = entries.iterator().next();
Class extends T> clazz = (Class extends T>) resolver.resolve(member.getKey());
return gson.getAdapter(clazz).fromJsonTree(element);
}
}
}