From 1a0929469016f9fc46dcf376b285682225a808c3 Mon Sep 17 00:00:00 2001 From: JFronny Date: Thu, 25 Apr 2024 15:07:40 +0200 Subject: [PATCH] fix(serialize-databind): support custom map subtypes and collections --- .../adapter/CollectionTypeAdapterFactory.java | 89 +++++++++++++++++++ .../impl/adapter/MapTypeAdapterFactory.java | 7 +- ...mons.serialize.databind.TypeAdapterFactory | 3 +- ...AdapterTest.java => ObjectMapperTest.java} | 44 ++++++++- 4 files changed, 140 insertions(+), 3 deletions(-) create mode 100644 commons-serialize-databind/src/main/java/io/gitlab/jfronny/commons/serialize/databind/impl/adapter/CollectionTypeAdapterFactory.java rename commons-serialize-databind/src/test/java/io/gitlab/jfronny/commons/serialize/databind/test/{MapAdapterTest.java => ObjectMapperTest.java} (59%) diff --git a/commons-serialize-databind/src/main/java/io/gitlab/jfronny/commons/serialize/databind/impl/adapter/CollectionTypeAdapterFactory.java b/commons-serialize-databind/src/main/java/io/gitlab/jfronny/commons/serialize/databind/impl/adapter/CollectionTypeAdapterFactory.java new file mode 100644 index 0000000..e787b11 --- /dev/null +++ b/commons-serialize-databind/src/main/java/io/gitlab/jfronny/commons/serialize/databind/impl/adapter/CollectionTypeAdapterFactory.java @@ -0,0 +1,89 @@ +package io.gitlab.jfronny.commons.serialize.databind.impl.adapter; + +import io.gitlab.jfronny.commons.serialize.MalformedDataException; +import io.gitlab.jfronny.commons.serialize.SerializeReader; +import io.gitlab.jfronny.commons.serialize.SerializeWriter; +import io.gitlab.jfronny.commons.serialize.Token; +import io.gitlab.jfronny.commons.serialize.databind.ObjectMapper; +import io.gitlab.jfronny.commons.serialize.databind.TypeAdapterFactory; +import io.gitlab.jfronny.commons.serialize.databind.api.TypeAdapter; +import io.gitlab.jfronny.commons.serialize.databind.api.TypeToken; +import io.gitlab.jfronny.commons.serialize.databind.api.TypeUtils; + +import java.lang.reflect.Type; +import java.util.*; + +public class CollectionTypeAdapterFactory implements TypeAdapterFactory { + @Override + public TypeAdapter create(ObjectMapper mapper, TypeToken type) { + Class rawType = type.getRawType(); + if (!Collection.class.isAssignableFrom(rawType)) { + return null; + } + + Type elementType = TypeUtils.getCollectionElementType(type.getType(), rawType); + TypeAdapter elementTypeAdapter = mapper.getAdapter(TypeToken.get(elementType)); + + @SuppressWarnings({"unchecked", "rawtypes"}) // create() doesn't define a type parameter + TypeAdapter result = new Adapter(mapper, elementType, elementTypeAdapter, rawType); + return result; + } + + private static final class Adapter extends TypeAdapter> { + private static final Set> RAW_TYPES_LIST = Set.of(List.class, AbstractList.class, SequencedCollection.class, Collection.class, AbstractCollection.class, Iterable.class); + private static final Set> RAW_TYPES_LINKED = Set.of(Deque.class, Queue.class, AbstractSequentialList.class); + private static final Set> RAW_TYPES_SET = Set.of(Set.class, SequencedSet.class, AbstractSet.class); + private static final Set> RAW_TYPES_SSET = Set.of(SortedSet.class, NavigableSet.class); + + private final TypeAdapter elementTypeAdapter; + private final Class implClass; + + public Adapter( + ObjectMapper context, + Type elementType, + TypeAdapter elementTypeAdapter, Class implClass) { + this.elementTypeAdapter = new TypeAdapterRuntimeTypeWrapper<>(context, elementTypeAdapter, elementType); + this.implClass = RAW_TYPES_LIST.contains(implClass) ? ArrayList.class + : RAW_TYPES_LINKED.contains(implClass) ? LinkedList.class + : RAW_TYPES_SET.contains(implClass) ? LinkedHashSet.class + : RAW_TYPES_SSET.contains(implClass) ? TreeSet.class + : implClass; + } + + @Override + public > void serialize(Collection value, Writer writer) throws TEx, MalformedDataException { + if (value == null) { + writer.nullValue(); + return; + } + + writer.beginArray(); + for (E element : value) { + elementTypeAdapter.serialize(element, writer); + } + writer.endArray(); + } + + @Override + public > Collection deserialize(Reader reader) throws TEx, MalformedDataException { + if (reader.peek() == Token.NULL) { + reader.nextNull(); + return null; + } + + Collection collection = (Collection) TypeUtils.instantiate(implClass).orElseThrow(() -> new MalformedDataException("Cannot instantiate collection of type " + implClass.getName())); + if (!reader.isLenient() || reader.peek() == Token.BEGIN_ARRAY) { + reader.beginArray(); + while (reader.hasNext()) { + E instance = elementTypeAdapter.deserialize(reader); + collection.add(instance); + } + reader.endArray(); + } else { + // Coerce + collection.add(elementTypeAdapter.deserialize(reader)); + } + return collection; + } + } +} diff --git a/commons-serialize-databind/src/main/java/io/gitlab/jfronny/commons/serialize/databind/impl/adapter/MapTypeAdapterFactory.java b/commons-serialize-databind/src/main/java/io/gitlab/jfronny/commons/serialize/databind/impl/adapter/MapTypeAdapterFactory.java index ce88690..459abad 100644 --- a/commons-serialize-databind/src/main/java/io/gitlab/jfronny/commons/serialize/databind/impl/adapter/MapTypeAdapterFactory.java +++ b/commons-serialize-databind/src/main/java/io/gitlab/jfronny/commons/serialize/databind/impl/adapter/MapTypeAdapterFactory.java @@ -37,6 +37,9 @@ public class MapTypeAdapterFactory implements TypeAdapterFactory { } private static class MapTypeAdapter extends TypeAdapter> { + private static final Set> RAW_TYPES_L = Set.of(Map.class, SequencedMap.class, AbstractMap.class); + private static final Set> RAW_TYPES_S = Set.of(SortedMap.class, NavigableMap.class); + private final TypeAdapter keyTypeAdapter; private final TypeAdapter valueTypeAdapter; private final Class implClass; @@ -50,7 +53,9 @@ public class MapTypeAdapterFactory implements TypeAdapterFactory { Class implClass) { this.keyTypeAdapter = new TypeAdapterRuntimeTypeWrapper<>(context, keyTypeAdapter, keyType); this.valueTypeAdapter = new TypeAdapterRuntimeTypeWrapper<>(context, valueTypeAdapter, valueType); - this.implClass = implClass.equals(Map.class) ? LinkedHashMap.class : implClass; + this.implClass = RAW_TYPES_L.contains(implClass) ? LinkedHashMap.class + : RAW_TYPES_S.contains(implClass) ? TreeMap.class + : implClass; } @Override diff --git a/commons-serialize-databind/src/main/resources/META-INF/services/io.gitlab.jfronny.commons.serialize.databind.TypeAdapterFactory b/commons-serialize-databind/src/main/resources/META-INF/services/io.gitlab.jfronny.commons.serialize.databind.TypeAdapterFactory index 17e8d9c..25acfa5 100644 --- a/commons-serialize-databind/src/main/resources/META-INF/services/io.gitlab.jfronny.commons.serialize.databind.TypeAdapterFactory +++ b/commons-serialize-databind/src/main/resources/META-INF/services/io.gitlab.jfronny.commons.serialize.databind.TypeAdapterFactory @@ -1,4 +1,5 @@ io.gitlab.jfronny.commons.serialize.databind.impl.adapter.SerializationAdapterTypeAdapterFactory io.gitlab.jfronny.commons.serialize.databind.impl.adapter.ArrayTypeAdapterFactory io.gitlab.jfronny.commons.serialize.databind.impl.adapter.EnumTypeAdapterFactory -io.gitlab.jfronny.commons.serialize.databind.impl.adapter.MapTypeAdapterFactory \ No newline at end of file +io.gitlab.jfronny.commons.serialize.databind.impl.adapter.MapTypeAdapterFactory +io.gitlab.jfronny.commons.serialize.databind.impl.adapter.CollectionTypeAdapterFactory \ No newline at end of file diff --git a/commons-serialize-databind/src/test/java/io/gitlab/jfronny/commons/serialize/databind/test/MapAdapterTest.java b/commons-serialize-databind/src/test/java/io/gitlab/jfronny/commons/serialize/databind/test/ObjectMapperTest.java similarity index 59% rename from commons-serialize-databind/src/test/java/io/gitlab/jfronny/commons/serialize/databind/test/MapAdapterTest.java rename to commons-serialize-databind/src/test/java/io/gitlab/jfronny/commons/serialize/databind/test/ObjectMapperTest.java index 61df2d5..8be738b 100644 --- a/commons-serialize-databind/src/test/java/io/gitlab/jfronny/commons/serialize/databind/test/MapAdapterTest.java +++ b/commons-serialize-databind/src/test/java/io/gitlab/jfronny/commons/serialize/databind/test/ObjectMapperTest.java @@ -11,10 +11,12 @@ import io.gitlab.jfronny.commons.serialize.emulated.EmulatedWriter; import org.junit.jupiter.api.Test; import java.util.Map; +import java.util.Set; +import java.util.TreeSet; import static org.junit.jupiter.api.Assertions.assertEquals; -public class MapAdapterTest { +public class ObjectMapperTest { @Test public void simpleMapTEst() throws MalformedDataException { DataElement.Object de = new DataElement.Object(); @@ -58,4 +60,44 @@ public class MapAdapterTest { assertEquals(de, ew.get()); } } + + @Test + public void testSet() throws MalformedDataException { + DataElement.Array de = new DataElement.Array(); + de.elements().add(new DataElement.Primitive.String("value")); + de.elements().add(new DataElement.Primitive.String("value2")); + de.elements().add(new DataElement.Primitive.String("value3")); + ObjectMapper om = new ObjectMapper(); + TypeAdapter> adapter = om.getAdapter(new TypeToken<>() {}); + Set set; + try (EmulatedReader er = new EmulatedReader(de)) { + set = adapter.deserialize(er); + } + assertEquals(3, set.size()); + assertEquals(Set.of("value", "value2", "value3"), set); + try (EmulatedWriter ew = new EmulatedWriter()) { + adapter.serialize(set, ew); + assertEquals(de, ew.get()); + } + } + + @Test + public void testSet2() throws MalformedDataException { + DataElement.Array de = new DataElement.Array(); + de.elements().add(new DataElement.Primitive.String("value")); + de.elements().add(new DataElement.Primitive.String("value2")); + de.elements().add(new DataElement.Primitive.String("value3")); + ObjectMapper om = new ObjectMapper(); + TypeAdapter> adapter = om.getAdapter(new TypeToken<>() {}); + TreeSet set; + try (EmulatedReader er = new EmulatedReader(de)) { + set = adapter.deserialize(er); + } + assertEquals(3, set.size()); + assertEquals(Set.of("value", "value2", "value3"), set); + try (EmulatedWriter ew = new EmulatedWriter()) { + adapter.serialize(set, ew); + assertEquals(de, ew.get()); + } + } }