diff --git a/core/src/main/java/org/infinispan/protostream/annotations/impl/ProtoMessageTypeMetadata.java b/core/src/main/java/org/infinispan/protostream/annotations/impl/ProtoMessageTypeMetadata.java index 84644d890..b46ad3ad3 100644 --- a/core/src/main/java/org/infinispan/protostream/annotations/impl/ProtoMessageTypeMetadata.java +++ b/core/src/main/java/org/infinispan/protostream/annotations/impl/ProtoMessageTypeMetadata.java @@ -41,6 +41,8 @@ import org.infinispan.protostream.containers.IndexedElementContainerAdapter; import org.infinispan.protostream.containers.IterableElementContainer; import org.infinispan.protostream.containers.IterableElementContainerAdapter; +import org.infinispan.protostream.containers.MapElementContainer; +import org.infinispan.protostream.containers.MapElementContainerAdapter; import org.infinispan.protostream.descriptors.JavaType; import org.infinispan.protostream.descriptors.Type; import org.infinispan.protostream.impl.Log; @@ -70,6 +72,8 @@ public class ProtoMessageTypeMetadata extends ProtoTypeMetadata { private final boolean isIterableContainer; + private final boolean isMapContainer; + private final boolean isOrderedMarshallable; private XExecutable factory; @@ -90,6 +94,7 @@ protected ProtoMessageTypeMetadata(BaseProtoSchemaGenerator protoSchemaGenerator this.isAdapter = javaClass != annotatedClass; this.isIndexedContainer = annotatedClass.isAssignableTo(isAdapter ? IndexedElementContainerAdapter.class : IndexedElementContainer.class); this.isIterableContainer = annotatedClass.isAssignableTo(isAdapter ? IterableElementContainerAdapter.class : IterableElementContainer.class); + this.isMapContainer = annotatedClass.isAssignableTo(isAdapter ? MapElementContainerAdapter.class : MapElementContainer.class); this.isOrderedMarshallable = protoSchemaGenerator.orderedMarshaller(); checkInstantiability(); @@ -125,8 +130,12 @@ public boolean isIterableContainer() { return isIterableContainer; } + public boolean isMapContainer() { + return isMapContainer; + } + public boolean isContainer() { - return isIterableContainer || isIndexedContainer; + return isIterableContainer || isIndexedContainer || isMapContainer; } public boolean isOrderedMarshallable() { diff --git a/core/src/main/java/org/infinispan/protostream/containers/MapElementContainer.java b/core/src/main/java/org/infinispan/protostream/containers/MapElementContainer.java new file mode 100644 index 000000000..f8a410e35 --- /dev/null +++ b/core/src/main/java/org/infinispan/protostream/containers/MapElementContainer.java @@ -0,0 +1,11 @@ +package org.infinispan.protostream.containers; + +import java.util.Map; + +/** + * Container adapter interface for {@link Map} implementations. + * + * @author José Bolina + * @since 6.0 + */ +public interface MapElementContainer extends IterableElementContainer> { } diff --git a/core/src/main/java/org/infinispan/protostream/containers/MapElementContainerAdapter.java b/core/src/main/java/org/infinispan/protostream/containers/MapElementContainerAdapter.java new file mode 100644 index 000000000..72c6999e4 --- /dev/null +++ b/core/src/main/java/org/infinispan/protostream/containers/MapElementContainerAdapter.java @@ -0,0 +1,17 @@ +package org.infinispan.protostream.containers; + +import java.util.Map; + +/** + * Container adapter interface for {@link Map} implementations. + * + * @author José Bolina + * @since 6.0 + */ +public interface MapElementContainerAdapter> extends IterableElementContainerAdapter> { + + @Override + default void appendElement(M container, Map.Entry element) { + container.put(element.getKey(), element.getValue()); + } +} diff --git a/core/src/main/java/org/infinispan/protostream/impl/json/ContainerObjectWriter.java b/core/src/main/java/org/infinispan/protostream/impl/json/ContainerObjectWriter.java index 501a15d93..993e1997e 100644 --- a/core/src/main/java/org/infinispan/protostream/impl/json/ContainerObjectWriter.java +++ b/core/src/main/java/org/infinispan/protostream/impl/json/ContainerObjectWriter.java @@ -29,6 +29,7 @@ final class ContainerObjectWriter extends BaseJsonWriter { private int containerFields = 1; + private boolean writtenElements = false; ContainerObjectWriter(ImmutableSerializationContext ctx, List ast, FieldDescriptor descriptor) { super(ctx, ast, descriptor); @@ -99,6 +100,7 @@ public void onTag(int fieldNumber, FieldDescriptor fieldDescriptor, Object tagVa GenericDescriptor descriptor = ctx.getDescriptorByTypeId(WrappedMessage.PROTOBUF_TYPE_ID); TagHandler delegate = new RootJsonWriter(ctx, ast); delegate.onStart(descriptor); + writtenElements |= lastToken() == JsonToken.LEFT_BRACKET; delegate.onTag(fieldNumber, fieldDescriptor, tagValue); delegate.onEnd(); } @@ -111,6 +113,7 @@ protected boolean isRoot() { } private void writePrimitiveContainer(FieldDescriptor fieldDescriptor, Object tagValue) { + writtenElements = true; pushToken(JsonToken.LEFT_BRACE); pushToken(JsonTokenWriter.string(fieldDescriptor.getTypeName())); pushToken(JsonToken.COLON); @@ -120,6 +123,7 @@ private void writePrimitiveContainer(FieldDescriptor fieldDescriptor, Object tag @Override public void onEnd() { - pushToken(JsonToken.RIGHT_BRACKET); + if (writtenElements) + pushToken(JsonToken.RIGHT_BRACKET); } } diff --git a/processor/src/main/java/org/infinispan/protostream/processor/MarshallerSourceCodeGenerator.java b/processor/src/main/java/org/infinispan/protostream/processor/MarshallerSourceCodeGenerator.java index 1bbdff3d4..58285300b 100644 --- a/processor/src/main/java/org/infinispan/protostream/processor/MarshallerSourceCodeGenerator.java +++ b/processor/src/main/java/org/infinispan/protostream/processor/MarshallerSourceCodeGenerator.java @@ -9,6 +9,7 @@ import java.util.Arrays; import java.util.HashSet; import java.util.LinkedHashSet; +import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @@ -31,6 +32,7 @@ import org.infinispan.protostream.annotations.impl.types.XTypeFactory; import org.infinispan.protostream.containers.IndexedElementContainerAdapter; import org.infinispan.protostream.containers.IterableElementContainerAdapter; +import org.infinispan.protostream.containers.MapElementContainerAdapter; import org.infinispan.protostream.impl.Log; import org.infinispan.protostream.processor.types.HasModelElement; @@ -184,6 +186,13 @@ private void generateMessageMarshaller(ProtoMessageTypeMetadata pmtm) throws IOE if (pmtm.isIndexedContainer()) { elementType = pmtm.getAnnotatedClass().getGenericInterfaceParameterTypes(IndexedElementContainerAdapter.class)[1]; iw.printf(", %s<%s, %s>", IndexedElementContainerAdapter.class.getName(), pmtm.getJavaClassName(), elementType); + } else if (pmtm.isMapContainer()) { + String[] types = pmtm.getAnnotatedClass().getGenericInterfaceParameterTypes(MapElementContainerAdapter.class); + String map = pmtm.getJavaClassName(); + String key = types[0]; + String value = types[1]; + elementType = Map.Entry.class.getCanonicalName(); + iw.printf(", %s<%s, %s, %s<%s, %s>>", MapElementContainerAdapter.class.getName(), key, value, map, key, value); } else if (pmtm.isIterableContainer()) { elementType = pmtm.getAnnotatedClass().getGenericInterfaceParameterTypes(IterableElementContainerAdapter.class)[1]; iw.printf(", %s<%s, %s>", IterableElementContainerAdapter.class.getName(), pmtm.getJavaClassName(), elementType); diff --git a/types/src/main/java/org/infinispan/protostream/types/java/CommonContainerTypes.java b/types/src/main/java/org/infinispan/protostream/types/java/CommonContainerTypes.java index a1500495d..4bb7c2739 100644 --- a/types/src/main/java/org/infinispan/protostream/types/java/CommonContainerTypes.java +++ b/types/src/main/java/org/infinispan/protostream/types/java/CommonContainerTypes.java @@ -22,6 +22,8 @@ import org.infinispan.protostream.types.java.collections.LinkedHashSetAdapter; import org.infinispan.protostream.types.java.collections.LinkedListAdapter; import org.infinispan.protostream.types.java.collections.TreeSetAdapter; +import org.infinispan.protostream.types.java.util.MapAdapters; +import org.infinispan.protostream.types.java.util.MapEntryAdapter; /** * Support for marshalling various {@link java.util.Collection} implementations and array or primitives. @@ -56,7 +58,21 @@ BoxedFloatArrayAdapter.class, BoxedDoubleArrayAdapter.class, StringArrayAdapter.class, - ObjectArrayAdapter.class + ObjectArrayAdapter.class, + + // maps + MapEntryAdapter.class, + MapAdapters.HashMapAdapter.class, + MapAdapters.ConcurrentHashMapAdapter.class, + MapAdapters.LinkedHashMapAdapter.class, + MapAdapters.TreeMapAdapter.class, + MapAdapters.WeakHashMapAdapter.class, + MapAdapters.IdentityHashMapAdapter.class, + MapAdapters.ConcurrentSkipListMapAdapter.class, + MapAdapters.HashtableAdapter.class, + MapAdapters.PropertiesAdapter.class, + MapAdapters.CollectionsEmptyMap.class, + MapAdapters.CollectionSingletonMap.class, } ) public interface CommonContainerTypes extends GeneratedSchema { diff --git a/types/src/main/java/org/infinispan/protostream/types/java/util/AbstractMapAdapter.java b/types/src/main/java/org/infinispan/protostream/types/java/util/AbstractMapAdapter.java new file mode 100644 index 000000000..3aa7c157c --- /dev/null +++ b/types/src/main/java/org/infinispan/protostream/types/java/util/AbstractMapAdapter.java @@ -0,0 +1,94 @@ +package org.infinispan.protostream.types.java.util; + +import java.util.Iterator; +import java.util.Map; + +import org.infinispan.protostream.containers.MapElementContainerAdapter; + +/** + * Base adapter for {@link Map} implementations. + * + * @author José Bolina + * @since 6.0 + */ +public abstract class AbstractMapAdapter> implements MapElementContainerAdapter { + + abstract public M create(int size); + + @Override + public Iterator> getElements(M container) { + return AbstractMapAdapter.toIterator(container); + } + + public static MapEntryWrapper entry(Map.Entry entry) { + if (entry instanceof AbstractMapAdapter.MapEntryWrapper) { + return (AbstractMapAdapter.MapEntryWrapper) entry; + } + return new MapEntryWrapper<>(entry); + } + + static Iterator> toIterator(Map map) { + Iterator> delegate = map.entrySet().iterator(); + return new Iterator<>() { + + @Override + public boolean hasNext() { + return delegate.hasNext(); + } + + @Override + public Map.Entry next() { + Map.Entry entry = delegate.next(); + // Wrap entries in our MapEntryWrapper to provide a consistent serializable type + return MapEntryWrapper.create(entry); + } + }; + } + + @Override + public final int getNumElements(M container) { + return container.size(); + } + + /** + * Wrapper for {@link Map.Entry} instances. + *

+ * This wrapper is necessary because there are several implementations of the {@link Map.Entry} interface + * across different map types (e.g., HashMap.Node, TreeMap.Entry, etc.). Instead of creating a separate + * ProtoStream adapter for each implementation, we wrap all entries in this single, consistent type that + * can be serialized uniformly. + * + * @param the type of the entry key + * @param the type of the entry value + */ + public static class MapEntryWrapper implements Map.Entry { + private final Map.Entry entry; + + private MapEntryWrapper(Map.Entry entry) { + this.entry = entry; + } + + public static Map.Entry create(K key, V value) { + return new MapEntryWrapper<>(Map.entry(key, value)); + } + + public static Map.Entry create(Map.Entry entry) { + return new MapEntryWrapper<>(entry); + } + + @Override + public K getKey() { + return entry.getKey(); + } + + @Override + public V getValue() { + return entry.getValue(); + } + + @Override + public V setValue(V v) { + return entry.setValue(v); + } + } +} diff --git a/types/src/main/java/org/infinispan/protostream/types/java/util/MapAdapters.java b/types/src/main/java/org/infinispan/protostream/types/java/util/MapAdapters.java new file mode 100644 index 000000000..5aabf9344 --- /dev/null +++ b/types/src/main/java/org/infinispan/protostream/types/java/util/MapAdapters.java @@ -0,0 +1,225 @@ +package org.infinispan.protostream.types.java.util; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Properties; +import java.util.TreeMap; +import java.util.WeakHashMap; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentSkipListMap; + +import org.infinispan.protostream.annotations.ProtoAdapter; +import org.infinispan.protostream.annotations.ProtoFactory; +import org.infinispan.protostream.annotations.ProtoField; + +/** + * ProtoStream adapters for various {@link Map} implementations. + * + * @author José Bolina + * @since 6.0 + */ +public final class MapAdapters { + + /** + * Adapter for {@link HashMap} and immutable Map implementations. + * + * @author José Bolina + * @since 6.0 + */ + @ProtoAdapter( + value = Map.class, + subClassNames = { + "java.util.HashMap", + "java.util.ImmutableCollections$Map1", + "java.util.ImmutableCollections$MapN", + } + ) + public static class HashMapAdapter extends AbstractMapAdapter> { + + @ProtoFactory + @Override + public Map create(int size) { + return new HashMap<>(size); + } + } + + /** + * Adapter for {@link ConcurrentHashMap}. + * + * @author José Bolina + * @since 6.0 + */ + @ProtoAdapter(ConcurrentHashMap.class) + public static class ConcurrentHashMapAdapter extends AbstractMapAdapter> { + + @ProtoFactory + @Override + public ConcurrentHashMap create(int size) { + return new ConcurrentHashMap<>(size); + } + } + + /** + * Adapter for {@link LinkedHashMap}. + * + * @author José Bolina + * @since 6.0 + */ + @ProtoAdapter(LinkedHashMap.class) + public static class LinkedHashMapAdapter extends AbstractMapAdapter> { + + @ProtoFactory + @Override + public LinkedHashMap create(int size) { + return new LinkedHashMap<>(size); + } + } + + /** + * Adapter for {@link TreeMap}. + * + * @author José Bolina + * @since 6.0 + */ + @ProtoAdapter(TreeMap.class) + public static class TreeMapAdapter extends AbstractMapAdapter> { + + @ProtoFactory + @Override + public TreeMap create(int ignore) { + return new TreeMap<>(); + } + } + + /** + * Adapter for {@link WeakHashMap}. + * + * @author José Bolina + * @since 6.0 + */ + @ProtoAdapter(WeakHashMap.class) + public static class WeakHashMapAdapter extends AbstractMapAdapter> { + + @ProtoFactory + @Override + public WeakHashMap create(int size) { + return new WeakHashMap<>(size); + } + } + + /** + * Adapter for {@link IdentityHashMap}. + * + * @author José Bolina + * @since 6.0 + */ + @ProtoAdapter(IdentityHashMap.class) + public static class IdentityHashMapAdapter extends AbstractMapAdapter> { + + @ProtoFactory + @Override + public IdentityHashMap create(int size) { + return new IdentityHashMap<>(size); + } + } + + /** + * Adapter for {@link ConcurrentSkipListMap}. + * + * @author José Bolina + * @since 6.0 + */ + @ProtoAdapter(ConcurrentSkipListMap.class) + public static class ConcurrentSkipListMapAdapter extends AbstractMapAdapter> { + + @ProtoFactory + @Override + public ConcurrentSkipListMap create(int ignore) { + return new ConcurrentSkipListMap<>(); + } + } + + /** + * Adapter for {@link Hashtable}. + * + * @author José Bolina + * @since 6.0 + */ + @ProtoAdapter(Hashtable.class) + public static class HashtableAdapter extends AbstractMapAdapter> { + + @ProtoFactory + @Override + public Hashtable create(int size) { + return new Hashtable<>(size); + } + } + + /** + * Adapter for {@link Properties}. + * + * @author José Bolina + * @since 6.0 + */ + @ProtoAdapter( + value = Map.class, + subClassNames = "java.util.Properties" + ) + public static class PropertiesAdapter extends AbstractMapAdapter> { + + @ProtoFactory + @Override + public Map create(int ignore) { + return new Properties(); + } + } + + /** + * Adapter for {@link Collections#emptyMap()}. + * + * @author José Bolina + * @since 6.0 + */ + @ProtoAdapter( + value = Map.class, + subClassNames = { + "java.util.Collections$EmptyMap", + } + ) + public static class CollectionsEmptyMap { + + @ProtoFactory + public Map create() { + return Collections.emptyMap(); + } + } + + /** + * Adapter for {@link Collections#singletonMap(Object, Object)}. + * + * @author José Bolina + * @since 6.0 + */ + @ProtoAdapter( + value = Map.class, + subClassNames = "java.util.Collections$SingletonMap" + ) + public static class CollectionSingletonMap { + + @ProtoFactory + public Map create(AbstractMapAdapter.MapEntryWrapper entry) { + return Collections.singletonMap(entry.getKey(), entry.getValue()); + } + + @ProtoField(number = 1) + AbstractMapAdapter.MapEntryWrapper getEntry(Map map) { + Iterator> iterator = map.entrySet().iterator(); + return AbstractMapAdapter.entry(iterator.next()); + } + } +} diff --git a/types/src/main/java/org/infinispan/protostream/types/java/util/MapEntryAdapter.java b/types/src/main/java/org/infinispan/protostream/types/java/util/MapEntryAdapter.java new file mode 100644 index 000000000..dce1fba5f --- /dev/null +++ b/types/src/main/java/org/infinispan/protostream/types/java/util/MapEntryAdapter.java @@ -0,0 +1,34 @@ +package org.infinispan.protostream.types.java.util; + +import java.util.Map; + +import org.infinispan.protostream.WrappedMessage; +import org.infinispan.protostream.annotations.ProtoAdapter; +import org.infinispan.protostream.annotations.ProtoFactory; +import org.infinispan.protostream.annotations.ProtoField; + +/** + * Adapter for {@link Map.Entry} wrapper used in map serialization. + * + * @author José Bolina + * @since 6.0 + */ +@SuppressWarnings("rawtypes") +@ProtoAdapter(AbstractMapAdapter.MapEntryWrapper.class) +public class MapEntryAdapter { + + @ProtoFactory + AbstractMapAdapter.MapEntryWrapper create(WrappedMessage key, WrappedMessage value) { + return (AbstractMapAdapter.MapEntryWrapper) AbstractMapAdapter.MapEntryWrapper.create(key.getValue(), value.getValue()); + } + + @ProtoField(number = 1) + WrappedMessage getKey(Map.Entry entry) { + return new WrappedMessage(entry.getKey()); + } + + @ProtoField(number = 2) + WrappedMessage getValue(Map.Entry entry) { + return new WrappedMessage(entry.getValue()); + } +} diff --git a/types/src/test/java/org/infinispan/protostream/types/java/TypesMarshallingTest.java b/types/src/test/java/org/infinispan/protostream/types/java/TypesMarshallingTest.java index 53bb4bde6..7f5185366 100644 --- a/types/src/test/java/org/infinispan/protostream/types/java/TypesMarshallingTest.java +++ b/types/src/test/java/org/infinispan/protostream/types/java/TypesMarshallingTest.java @@ -31,13 +31,20 @@ import java.util.Date; import java.util.HashMap; import java.util.HashSet; +import java.util.Hashtable; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Properties; import java.util.Set; +import java.util.TreeMap; import java.util.TreeSet; import java.util.UUID; +import java.util.WeakHashMap; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentSkipListMap; import java.util.concurrent.ThreadLocalRandom; import java.util.function.Supplier; import java.util.stream.Stream; @@ -141,6 +148,46 @@ public void testManyCollections() throws IOException { testConfiguration.method.marshallAndUnmarshallTest(msg, context, false); } + @Test + public void testManyMaps() throws IOException { + assumeTrue(testConfiguration.runTest); + testConfiguration.method.marshallAndUnmarshallTest(new HashMap<>(Map.of("key", "value")), context, false); + testConfiguration.method.marshallAndUnmarshallTest(new ConcurrentHashMap<>(Map.of("key", "value")), context, false); + testConfiguration.method.marshallAndUnmarshallTest(new LinkedHashMap<>(Map.of("key", "value")), context, false); + testConfiguration.method.marshallAndUnmarshallTest(new TreeMap<>(Map.of("key", "value")), context, false); + testConfiguration.method.marshallAndUnmarshallTest(new WeakHashMap<>(Map.of("key", "value")), context, false); + testConfiguration.method.marshallAndUnmarshallTest(new ConcurrentSkipListMap<>(Map.of("key", "value")), context, false); + testConfiguration.method.marshallAndUnmarshallTest(new Hashtable<>(Map.of("key", "value")), context, false); + testConfiguration.method.marshallAndUnmarshallTest(Collections.singletonMap("key", "value"), context, false); + testConfiguration.method.marshallAndUnmarshallTest(Collections.emptyMap(), context, false); + testConfiguration.method.marshallAndUnmarshallTest(Map.of(), context, false); + testConfiguration.method.marshallAndUnmarshallTest(Map.of("k", "v"), context, false); + testConfiguration.method.marshallAndUnmarshallTest(Map.of("k", "v", "k1", "v1"), context, false); + } + + @Test + public void testPropertiesMap() throws IOException { + assumeTrue(testConfiguration.runTest); + var props = new Properties(); + props.setProperty("key1", "value1"); + props.setProperty("key2", "value2"); + testConfiguration.method.marshallAndUnmarshallTest(props, context, false); + } + + @Test + public void testMapWithComplexTypes() throws IOException { + assumeTrue(testConfiguration.runTest); + var bookMap = new HashMap(); + bookMap.put("book1", new Book("Title1", "Desc1", 2020)); + bookMap.put("book2", new Book("Title2", "Desc2", 2021)); + testConfiguration.method.marshallAndUnmarshallTest(bookMap, context, false); + + var nestedMap = new HashMap>(); + nestedMap.put(1, List.of("a", "b", "c")); + nestedMap.put(2, List.of("d", "e", "f")); + testConfiguration.method.marshallAndUnmarshallTest(nestedMap, context, false); + } + @Test public void testInstant() throws IOException { testConfiguration.method.marshallAndUnmarshallTest(Instant.EPOCH, context, false);