Issue #500: Serialization of a Map fails if the key uses a custom Serializer (#501)

Signed-off-by: rmartinc <rmartinc@redhat.com>
diff --git a/src/main/java/org/eclipse/yasson/internal/serializer/MapSerializer.java b/src/main/java/org/eclipse/yasson/internal/serializer/MapSerializer.java
index 9700b07..e5b8d8e 100644
--- a/src/main/java/org/eclipse/yasson/internal/serializer/MapSerializer.java
+++ b/src/main/java/org/eclipse/yasson/internal/serializer/MapSerializer.java
@@ -12,12 +12,17 @@
 
 package org.eclipse.yasson.internal.serializer;
 
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
 import java.util.Iterator;
 import java.util.Map;
+import java.util.Optional;
 
 import jakarta.json.bind.serializer.SerializationContext;
 import jakarta.json.stream.JsonGenerator;
 
+import org.eclipse.yasson.internal.ReflectionUtils;
+
 /**
  * Serialize {@link Map}.
  *
@@ -91,12 +96,18 @@
     private Delegate<K, V> serializer;
 
     /**
+     * Flag to know if the process is for the key (0) or the value (1).
+     */
+    private int actualTypeArgument;
+
+    /**
      * Creates an instance of {@link Map} serialization.
      *
      * @param builder current instance of {@link SerializerBuilder}
      */
     protected MapSerializer(SerializerBuilder builder) {
         super(builder);
+        actualTypeArgument = 0;
         nullable = builder.getJsonbContext().getConfigProperties().getConfigNullable();
         forceMapArraySerializerForNullKeys = builder.getJsonbContext().getConfigProperties().isForceMapArraySerializerForNullKeys();
         serializer = null;
@@ -203,4 +214,35 @@
         return nullable;
     }
 
+    /**
+     * Flag to serialize the key in the map.
+     */
+    protected void serializeKey() {
+        this.actualTypeArgument = 0;
+    }
+
+    /**
+     * Flag to serialize the value in the map.
+     */
+    protected void serializeValue() {
+        this.actualTypeArgument = 1;
+    }
+
+    /**
+     * In a map the type can refer to the key or the value type depending which
+     * one is currently being processed. The field <em>actualTypeArgument</em>
+     * controls which one is being serialized at the moment.
+     *
+     * @param valueType The value type which should be of type Map&lt;K,V&gt;
+     * @return The type for the key or the value
+     */
+    @Override
+    protected Type getValueType(Type valueType) {
+        if (valueType instanceof ParameterizedType && ((ParameterizedType) valueType).getActualTypeArguments().length > actualTypeArgument) {
+            Optional<Type> runtimeTypeOptional = ReflectionUtils
+                    .resolveOptionalType(this, ((ParameterizedType) valueType).getActualTypeArguments()[actualTypeArgument]);
+            return runtimeTypeOptional.orElse(Object.class);
+        }
+        return Object.class;
+    }
 }
diff --git a/src/main/java/org/eclipse/yasson/internal/serializer/MapToEntriesArraySerializer.java b/src/main/java/org/eclipse/yasson/internal/serializer/MapToEntriesArraySerializer.java
index 13d0cdc..7feec26 100644
--- a/src/main/java/org/eclipse/yasson/internal/serializer/MapToEntriesArraySerializer.java
+++ b/src/main/java/org/eclipse/yasson/internal/serializer/MapToEntriesArraySerializer.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2019, 2021 Oracle and/or its affiliates. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License v. 2.0 which is available at
@@ -106,8 +106,10 @@
         obj.forEach((key, value) -> {
             generator.writeStartObject();
             generator.writeKey(keyEntryName);
+            serializer.serializeKey();
             serializer.serializeItem(key, generator, ctx);
             generator.writeKey(valueEntryName);
+            serializer.serializeValue();
             serializer.serializeItem(value, generator, ctx);
             generator.writeEnd();
         });
diff --git a/src/main/java/org/eclipse/yasson/internal/serializer/MapToObjectSerializer.java b/src/main/java/org/eclipse/yasson/internal/serializer/MapToObjectSerializer.java
index bdc88b5..f77c46b 100644
--- a/src/main/java/org/eclipse/yasson/internal/serializer/MapToObjectSerializer.java
+++ b/src/main/java/org/eclipse/yasson/internal/serializer/MapToObjectSerializer.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2019, 2021 Oracle and/or its affiliates. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License v. 2.0 which is available at
@@ -96,6 +96,7 @@
                 continue;
             }
             generator.writeKey(keyString);
+            serializer.serializeValue();
             serializer.serializeItem(value, generator, ctx);
         }
     }
diff --git a/src/test/java/org/eclipse/yasson/serializers/MapToEntriesArraySerializerTest.java b/src/test/java/org/eclipse/yasson/serializers/MapToEntriesArraySerializerTest.java
index adce79d..eb430ab 100644
--- a/src/test/java/org/eclipse/yasson/serializers/MapToEntriesArraySerializerTest.java
+++ b/src/test/java/org/eclipse/yasson/serializers/MapToEntriesArraySerializerTest.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2019, 2021 Oracle and/or its affiliates. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License v. 2.0 which is available at
@@ -21,16 +21,25 @@
 import java.math.BigDecimal;
 import java.util.Comparator;
 import java.util.HashMap;
+import java.util.Locale;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 import java.util.TreeMap;
 
 import jakarta.json.Json;
 import jakarta.json.JsonArray;
 import jakarta.json.JsonObject;
+import jakarta.json.JsonValue;
 import jakarta.json.bind.Jsonb;
 import jakarta.json.bind.JsonbBuilder;
 import jakarta.json.bind.JsonbConfig;
+import jakarta.json.bind.serializer.DeserializationContext;
+import jakarta.json.bind.serializer.JsonbDeserializer;
+import jakarta.json.bind.serializer.JsonbSerializer;
+import jakarta.json.bind.serializer.SerializationContext;
+import jakarta.json.stream.JsonGenerator;
+import jakarta.json.stream.JsonParser;
 
 import org.eclipse.yasson.serializers.model.Pokemon;
 import org.eclipse.yasson.serializers.model.Trainer;
@@ -827,4 +836,103 @@
         }
     }
 
+    public static class LocaleSerializer implements JsonbSerializer<Locale> {
+
+        @Override
+        public void serialize(Locale obj, JsonGenerator generator, SerializationContext ctx) {
+            generator.write(obj.toLanguageTag());
+        }
+    }
+
+    public static class LocaleDeserializer implements JsonbDeserializer<Locale> {
+
+        @Override
+        public Locale deserialize(JsonParser parser, DeserializationContext ctx, Type rtType) {
+            return Locale.forLanguageTag(parser.getString());
+        }
+    }
+
+    public static class MapObject<K, V> {
+
+        private Map<K, V> values;
+
+        public MapObject() {
+            this.values = new HashMap<>();
+        }
+
+        public Map<K, V> getValues() {
+            return values;
+        }
+
+        public void setValues(Map<K, V> values) {
+            if (values == null) {
+                throw new IllegalArgumentException("values cannot be null");
+            }
+            this.values = values;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (o instanceof MapObject) {
+                MapObject<?,?> to = (MapObject<?,?>) o;
+                return values.equals(to.values);
+            }
+            return false;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hashCode(this.values);
+        }
+
+        @Override
+        public String toString() {
+            return values.toString();
+        }
+    }
+
+    public static class MapObjectLocaleString extends MapObject<Locale, String> {};
+
+    private void verifyMapObjectLocaleStringSerialization(JsonObject jsonObject, MapObjectLocaleString mapObject) {
+        // Expected serialization is: {"values":[{"key":"lang-tag","value":"string"},...]}
+        assertEquals(1, jsonObject.size());
+        assertNotNull(jsonObject.get("values"));
+        assertEquals(JsonValue.ValueType.ARRAY, jsonObject.get("values").getValueType());
+        JsonArray jsonArray = jsonObject.getJsonArray("values");
+        assertEquals(mapObject.getValues().size(), jsonArray.size());
+        MapObjectLocaleString resObject = new MapObjectLocaleString();
+        for (JsonValue jsonValue : jsonArray) {
+            assertEquals(JsonValue.ValueType.OBJECT, jsonValue.getValueType());
+            JsonObject entry = jsonValue.asJsonObject();
+            assertEquals(2, entry.size());
+            assertNotNull(entry.get("key"));
+            assertEquals(JsonValue.ValueType.STRING, entry.get("key").getValueType());
+            assertNotNull(entry.get("value"));
+            assertEquals(JsonValue.ValueType.STRING, entry.get("value").getValueType());
+            resObject.getValues().put(Locale.forLanguageTag(entry.getString("key")), entry.getString("value"));
+        }
+        assertEquals(mapObject, resObject);
+    }
+
+    /**
+     * Test a Locale/String map with custom Locale serializer and deserializer.
+     */
+    @Test
+    public void testMapLocaleString() {
+        Jsonb jsonb = JsonbBuilder.create(new JsonbConfig()
+                .withSerializers(new LocaleSerializer())
+                .withDeserializers(new LocaleDeserializer()));
+
+        MapObjectLocaleString mapObject = new MapObjectLocaleString();
+        mapObject.getValues().put(Locale.US, "us");
+        mapObject.getValues().put(Locale.ENGLISH, "en");
+        mapObject.getValues().put(Locale.JAPAN, "jp");
+
+        String json = jsonb.toJson(mapObject);
+        JsonObject jsonObject = Json.createReader(new StringReader(json)).read().asJsonObject();
+        verifyMapObjectLocaleStringSerialization(jsonObject, mapObject);
+
+        MapObjectLocaleString resObject = jsonb.fromJson(json, MapObjectLocaleString.class);
+        assertEquals(mapObject, resObject);
+    }
 }