I have a Multimap<Class<?>, Object>
populated like
multimap.put(o.getClass, o)
i.e., each object gets put in a proper bucket according to its class. I need to serialize and deserialize the multimap using Gson. All the objects belong to simple classes having no type parameters. I mean, each of them can be deserialized by using gson.fromJson(json, someClass)
; no TypeToken
needed here.
If it helps, I could use a TypeToken
or whatever as the key; I don't care. All used classes subclass a class of mine, if it helps. What I don't want is splitting the multimap into multiple homogeneous lists as there will be tens of them. As it's actually an ImmutableMultimap
, it'd mean many more lines which I want to avoid.
What I've tried: Not worth mentioning. None of the Adapters I wrote or saw does anything similar.
If I understand you correctly, you can accomplish such a type adapter relatively easy.
First, let's create a Multimap
type adapter.
The following Multimap
type adapter can work with any multimap, however Class
-related keys will be specialized below.
final class MultimapTypeAdapter<K, V>
extends TypeAdapter<Multimap<K, V>> {
private final Converter<K, String> keyConverter;
private final Function<? super K, ? extends TypeAdapter<V>> valueTypeAdapterProvider;
private MultimapTypeAdapter(
final Converter<K, String> keyConverter,
final Function<? super K, ? extends TypeAdapter<V>> valueTypeAdapterProvider
) {
this.keyConverter = keyConverter;
this.valueTypeAdapterProvider = valueTypeAdapterProvider;
}
static <K, V> TypeAdapter<Multimap<K, V>> multimapTypeAdapter(
final Converter<K, String> keyConverter,
final Function<? super K, ? extends TypeAdapter<V>> valueTypeAdapterProvider
) {
return new MultimapTypeAdapter<>(keyConverter, valueTypeAdapterProvider).nullSafe();
}
@Override
@SuppressWarnings("resource")
public void write(final JsonWriter jsonWriter, final Multimap<K, V> multimap)
throws IOException {
jsonWriter.beginObject();
for ( final K key : multimap.keySet() ) {
jsonWriter.name(keyConverter.convert(key));
final TypeAdapter<? super V> typeAdapter = valueTypeAdapterProvider.apply(key);
jsonWriter.beginArray();
for ( final V value : multimap.get(key) ) {
typeAdapter.write(jsonWriter, value);
}
jsonWriter.endArray();
}
jsonWriter.endObject();
}
@Override
public Multimap<K, V> read(final JsonReader jsonReader)
throws IOException {
final ImmutableMultimap.Builder<K, V> multimapBuilder = new ImmutableMultimap.Builder<>();
jsonReader.beginObject();
while ( jsonReader.hasNext() ) {
final K key = keyConverter.reverse().convert(jsonReader.nextName());
final TypeAdapter<V> typeAdapter = valueTypeAdapterProvider.apply(key);
jsonReader.beginArray();
while ( jsonReader.hasNext() ) {
final V value = typeAdapter.read(jsonReader);
multimapBuilder.put(key, value);
}
jsonReader.endArray();
}
jsonReader.endObject();
return multimapBuilder.build();
}
}
Now, you can create a simple Class
key converter:
The converter is pretty straight-forward and self-descriptive.
More complex converting strategies can be found, for example, here and here (the latter does not support arrays in full).
final class ClassKeyConverter
extends Converter<Class<?>, String> {
private static final Converter<Class<?>, String> classKeyConverter = new ClassKeyConverter();
private ClassKeyConverter() {
}
static Converter<Class<?>, String> classKeyConverter() {
return classKeyConverter;
}
@Override
protected String doForward(final Class<?> a) {
return a.toString();
}
@Override
public Class<?> doBackward(final String b) {
final Class<?> primitiveType = primitiveTypes.get(b);
if ( primitiveType != null ) {
return primitiveType;
}
final int prefix = b.startsWith(CLASS) ? CLASS.length()
: b.startsWith(INTERFACE) ? INTERFACE.length()
: -1;
if ( prefix >= 0 ) {
try {
return Class.forName(b.substring(prefix));
} catch ( final ClassNotFoundException ex ) {
throw new RuntimeException(ex);
}
}
throw new IllegalArgumentException(b);
}
private static final Map<String, Class<?>> primitiveTypes = ImmutableMap.<String, Class<?>>builder()
.put("boolean", boolean.class)
.put("byte", byte.class)
.put("short", short.class)
.put("int", int.class)
.put("long", long.class)
.put("float", float.class)
.put("double", double.class)
.put("char", char.class)
.build();
private static final String CLASS = "class ";
private static final String INTERFACE = "interface ";
}
And now you can create a type adapter factory that can process such a multimap:
final class ClassKeyMultimapTypeAdapterFactory
implements TypeAdapterFactory {
private static final TypeAdapterFactory classKeyMultimapTypeAdapterFactory = new ClassKeyMultimapTypeAdapterFactory();
static final Type classKeyMultimapType = TypeToken.getParameterized(Multimap.class, Class.class, Object.class).getType();
private ClassKeyMultimapTypeAdapterFactory() {
}
static TypeAdapterFactory classKeyMultimapTypeAdapterFactory() {
return classKeyMultimapTypeAdapterFactory;
}
@Override
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
if ( !isClassKeyMultimap(typeToken) ) {
return null;
}
@SuppressWarnings("unchecked")
final TypeAdapter<T> typeAdapter = (TypeAdapter<T>) multimapTypeAdapter(classKeyConverter(), type -> gson.getDelegateAdapter(this, TypeToken.get(type)));
return typeAdapter;
}
private static boolean isClassKeyMultimap(final TypeToken<?> typeToken) {
if ( Multimap.class.isAssignableFrom(typeToken.getRawType()) ) {
final Type type = typeToken.getType();
if ( type instanceof ParameterizedType ) {
final ParameterizedType parameterizedType = (ParameterizedType) type;
if ( Class.class.equals(parameterizedType.getActualTypeArguments()[0]) ) {
// We expect to process `Multimap<Class<?>, ?>` only
return true;
}
}
}
return false;
}
}
Finally, you can test it out:
private static final Gson gson = new GsonBuilder()
.disableHtmlEscaping()
.registerTypeAdapterFactory(classKeyMultimapTypeAdapterFactory())
.create();
public static void main(final String... args) {
final Multimap<Class<?>, Object> multimapBefore = ImmutableMultimap.<Class<?>, Object>builder()
.put(int.class, 2)
.put(int.class, 3)
.put(int.class, 4)
.put(Integer.class, 2)
.put(Integer.class, 3)
.put(Integer.class, 4)
.put(String.class, "foo")
.put(String.class, "bar")
.put(String.class, "baz")
.build();
System.out.println(multimapBefore);
final String json = gson.toJson(multimapBefore, classKeyMultimapType);
System.out.println(json);
final Multimap<Class<?>, Object> multimapAfter = gson.fromJson(json, classKeyMultimapType);
System.out.println(multimapAfter);
if ( !multimapBefore.equals(multimapAfter) ) {
throw new AssertionError("multimaps do not equal");
}
}
Output:
{int=[2, 3, 4], class java.lang.Integer=[2, 3, 4], class java.lang.String=[foo, bar, baz]}
{"int":[2,3,4],"class java.lang.Integer":[2,3,4],"class java.lang.String":["foo","bar","baz"]}
{int=[2, 3, 4], class java.lang.Integer=[2, 3, 4], class java.lang.String=[foo, bar, baz]}
Ok, let's proceed making the isClassKeyMultimap
method a bit smarter.
I'd like to work with a
Multimap<Class<Something>, Something>
, too.
You're, I guess, are talking about TypeToken
literals.
Yep, I used TypeToken.getParameterized(...)
forgetting that a Class
instance can be parameterized as well.
All you have to do to make it smarter is just adding an additional check to the method.
if ( Multimap.class.isAssignableFrom(typeToken.getRawType()) ) {
final Type type = typeToken.getType();
if ( type instanceof ParameterizedType ) {
final ParameterizedType parameterizedType = (ParameterizedType) type;
final Type actualTypeArg0 = parameterizedType.getActualTypeArguments()[0];
// raw java.lang.Class (Class.class, Class.forName("java.lang.Class"), etc)
if ( actualTypeArg0 == Class.class ) {
return true;
}
// or maybe it's something like a Class<...> instance that:
// * can be generated by javac when you parameterize a type (this is why Gson TypeToken's look "weird")
// * or create a ParameterizedType instance yourself, say using TypeToken.getParameterized or your custom ParameterizedType implementation
if ( actualTypeArg0 instanceof ParameterizedType && ((ParameterizedType) actualTypeArg0).getRawType() == Class.class ) {
return true;
}
}
}
return false;
The ad-hoc comments should explain why the previous implementation did not cover all the cases. Even more, you can write a reusable utility method that would recognize the raw class itself.
private static Type getRawClass(final Type type) {
if ( type instanceof ParameterizedType ) {
return ((ParameterizedType) type).getRawType();
}
return type;
}
And then those two checks could be collapsed into a single one:
if ( getRawClass(actualTypeArg0) == Class.class ) {
return true;
}
==
should work just fine with java.class.Class
instances since its instances are effective flyweights, and can improve readability here.