From 99b1c31b2dfd1a59f7a280465f50995ada76d0d5 Mon Sep 17 00:00:00 2001 From: Sylvain Wallez Date: Fri, 4 Nov 2022 09:31:45 +0100 Subject: [PATCH] Add Type in addition to Class to map types with generic parameters (#438) --- .../java/co/elastic/clients/ApiClient.java | 7 ++-- .../clients/json/DelegatingJsonpMapper.java | 5 ++- .../co/elastic/clients/json/JsonData.java | 13 +++++++ .../co/elastic/clients/json/JsonDataImpl.java | 20 ++++++++-- .../clients/json/JsonpDeserializer.java | 15 ++++++-- .../co/elastic/clients/json/JsonpMapper.java | 10 ++++- .../elastic/clients/json/JsonpMapperBase.java | 37 +++++++++++------- .../clients/json/SimpleJsonpMapper.java | 11 +++--- .../json/jackson/JacksonJsonpMapper.java | 13 ++++--- .../clients/json/jsonb/JsonbJsonpMapper.java | 13 ++++--- .../MissingRequiredPropertyException.java | 5 ++- .../elasticsearch/json/JsonpMapperTest.java | 38 +++++++++++++++++++ .../elasticsearch/model/ModelTestCase.java | 11 +++++- 13 files changed, 152 insertions(+), 46 deletions(-) diff --git a/java-client/src/main/java/co/elastic/clients/ApiClient.java b/java-client/src/main/java/co/elastic/clients/ApiClient.java index 29f03c3f9..0b46e12b9 100644 --- a/java-client/src/main/java/co/elastic/clients/ApiClient.java +++ b/java-client/src/main/java/co/elastic/clients/ApiClient.java @@ -26,6 +26,7 @@ import co.elastic.clients.json.JsonpMapperBase; import javax.annotation.Nullable; +import java.lang.reflect.Type; import java.util.function.Function; public abstract class ApiClient> { @@ -38,14 +39,14 @@ protected ApiClient(T transport, TransportOptions transportOptions) { this.transportOptions = transportOptions; } - protected JsonpDeserializer getDeserializer(Class clazz) { + protected JsonpDeserializer getDeserializer(Type type) { // Try the built-in deserializers first to avoid repeated lookups in the Jsonp mapper for client-defined classes - JsonpDeserializer result = JsonpMapperBase.findDeserializer(clazz); + JsonpDeserializer result = JsonpMapperBase.findDeserializer(type); if (result != null) { return result; } - return JsonpDeserializer.of(clazz); + return JsonpDeserializer.of(type); } /** diff --git a/java-client/src/main/java/co/elastic/clients/json/DelegatingJsonpMapper.java b/java-client/src/main/java/co/elastic/clients/json/DelegatingJsonpMapper.java index c4b69a647..c43aabf72 100644 --- a/java-client/src/main/java/co/elastic/clients/json/DelegatingJsonpMapper.java +++ b/java-client/src/main/java/co/elastic/clients/json/DelegatingJsonpMapper.java @@ -24,6 +24,7 @@ import jakarta.json.stream.JsonParser; import javax.annotation.Nullable; +import java.lang.reflect.Type; public abstract class DelegatingJsonpMapper implements JsonpMapper { @@ -39,8 +40,8 @@ public JsonProvider jsonProvider() { } @Override - public T deserialize(JsonParser parser, Class clazz) { - return mapper.deserialize(parser, clazz); + public T deserialize(JsonParser parser, Type type) { + return mapper.deserialize(parser, type); } @Override diff --git a/java-client/src/main/java/co/elastic/clients/json/JsonData.java b/java-client/src/main/java/co/elastic/clients/json/JsonData.java index 9ff1950ff..1016b9ffd 100644 --- a/java-client/src/main/java/co/elastic/clients/json/JsonData.java +++ b/java-client/src/main/java/co/elastic/clients/json/JsonData.java @@ -26,6 +26,7 @@ import java.io.InputStream; import java.io.Reader; import java.io.StringReader; +import java.lang.reflect.Type; import java.util.EnumSet; /** @@ -59,11 +60,23 @@ public interface JsonData extends JsonpSerializable { */ T to(Class clazz); + /** + * Converts this object to a target type. A mapper must have been provided at creation time. + * + * @throws IllegalStateException if no mapper was provided at creation time. + */ + T to(Type type); + /** * Converts this object to a target class. */ T to(Class clazz, JsonpMapper mapper); + /** + * Converts this object to a target type. + */ + T to(Type type, JsonpMapper mapper); + /** * Converts this object using a deserializer. A mapper must have been provided at creation time. * diff --git a/java-client/src/main/java/co/elastic/clients/json/JsonDataImpl.java b/java-client/src/main/java/co/elastic/clients/json/JsonDataImpl.java index 1150c4ddc..ee1745aed 100644 --- a/java-client/src/main/java/co/elastic/clients/json/JsonDataImpl.java +++ b/java-client/src/main/java/co/elastic/clients/json/JsonDataImpl.java @@ -25,6 +25,7 @@ import java.io.StringReader; import java.io.StringWriter; +import java.lang.reflect.Type; class JsonDataImpl implements JsonData { private final Object value; @@ -65,19 +66,30 @@ public JsonValue toJson(JsonpMapper mapper) { @Override public T to(Class clazz) { + return to((Type)clazz, null); + } + + @Override + public T to(Type clazz) { return to(clazz, null); } @Override public T to(Class clazz, JsonpMapper mapper) { - if (clazz.isAssignableFrom(value.getClass())) { - return (T) value; + return to((Type)clazz, mapper); + } + + @Override + public T to(Type type, JsonpMapper mapper) { + if (type instanceof Class && ((Class)type).isAssignableFrom(value.getClass())) { + @SuppressWarnings("unchecked") + T result = (T) value; + return result; } mapper = getMapper(mapper); - JsonParser parser = getParser(mapper); - return mapper.deserialize(parser, clazz); + return mapper.deserialize(parser, type); } @Override diff --git a/java-client/src/main/java/co/elastic/clients/json/JsonpDeserializer.java b/java-client/src/main/java/co/elastic/clients/json/JsonpDeserializer.java index 345dd4d44..ce57ded84 100644 --- a/java-client/src/main/java/co/elastic/clients/json/JsonpDeserializer.java +++ b/java-client/src/main/java/co/elastic/clients/json/JsonpDeserializer.java @@ -25,6 +25,7 @@ import jakarta.json.stream.JsonParser.Event; import java.io.StringReader; +import java.lang.reflect.Type; import java.util.EnumSet; import java.util.List; import java.util.Map; @@ -88,15 +89,23 @@ default V deserialize(JsonParser parser, JsonpMapper mapper) { //--------------------------------------------------------------------------------------------- +// /** +// * Creates a deserializer for a class that delegates to the mapper provided to +// * {@link #deserialize(JsonParser, JsonpMapper)}. +// */ +// static JsonpDeserializer of(Class clazz) { +// return of((Type)clazz); +// } + /** - * Creates a deserializer for a class that delegates to the mapper provided to + * Creates a deserializer for a type that delegates to the mapper provided to * {@link #deserialize(JsonParser, JsonpMapper)}. */ - static JsonpDeserializer of (Class clazz) { + static JsonpDeserializer of(Type type) { return new JsonpDeserializerBase(EnumSet.allOf(JsonParser.Event.class)) { @Override public T deserialize(JsonParser parser, JsonpMapper mapper) { - return mapper.deserialize(parser, clazz); + return mapper.deserialize(parser, type); } @Override diff --git a/java-client/src/main/java/co/elastic/clients/json/JsonpMapper.java b/java-client/src/main/java/co/elastic/clients/json/JsonpMapper.java index a4a259875..e3b02c4de 100644 --- a/java-client/src/main/java/co/elastic/clients/json/JsonpMapper.java +++ b/java-client/src/main/java/co/elastic/clients/json/JsonpMapper.java @@ -24,6 +24,7 @@ import jakarta.json.stream.JsonParser; import javax.annotation.Nullable; +import java.lang.reflect.Type; /** * A {@code JsonpMapper} combines a JSON-P provider and object serialization/deserialization based on JSON-P events. @@ -44,7 +45,14 @@ public interface JsonpMapper { /** * Deserialize an object, given its class. */ - T deserialize(JsonParser parser, Class clazz); + default T deserialize(JsonParser parser, Class clazz) { + return deserialize(parser, (Type)clazz); + } + + /** + * Deserialize an object, given its type. + */ + T deserialize(JsonParser parser, Type type); /** * Serialize an object. diff --git a/java-client/src/main/java/co/elastic/clients/json/JsonpMapperBase.java b/java-client/src/main/java/co/elastic/clients/json/JsonpMapperBase.java index f24b3daf5..5f1f2594a 100644 --- a/java-client/src/main/java/co/elastic/clients/json/JsonpMapperBase.java +++ b/java-client/src/main/java/co/elastic/clients/json/JsonpMapperBase.java @@ -25,6 +25,7 @@ import javax.annotation.Nullable; import java.lang.reflect.Field; +import java.lang.reflect.Type; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -32,7 +33,7 @@ public abstract class JsonpMapperBase implements JsonpMapper { /** Get a serializer when none of the builtin ones are applicable */ - protected abstract JsonpDeserializer getDefaultDeserializer(Class clazz); + protected abstract JsonpDeserializer getDefaultDeserializer(Type type); private Map attributes; @@ -61,29 +62,39 @@ protected JsonpMapperBase addAttribute(String name, Object value) { } @Override - public T deserialize(JsonParser parser, Class clazz) { - JsonpDeserializer deserializer = findDeserializer(clazz); + public T deserialize(JsonParser parser, Type type) { + JsonpDeserializer deserializer = findDeserializer(type); if (deserializer != null) { return deserializer.deserialize(parser, this); } - return getDefaultDeserializer(clazz).deserialize(parser, this); + @SuppressWarnings("unchecked") + T result = (T)getDefaultDeserializer(type).deserialize(parser, this); + return result; } @Nullable - @SuppressWarnings("unchecked") public static JsonpDeserializer findDeserializer(Class clazz) { - JsonpDeserializable annotation = clazz.getAnnotation(JsonpDeserializable.class); - if (annotation != null) { - try { - Field field = clazz.getDeclaredField(annotation.field()); - return (JsonpDeserializer)field.get(null); - } catch (Exception e) { - throw new RuntimeException("No deserializer found in '" + clazz.getName() + "." + annotation.field() + "'"); + return findDeserializer((Type)clazz); + } + + @Nullable + @SuppressWarnings("unchecked") + public static JsonpDeserializer findDeserializer(Type type) { + if (type instanceof Class) { + Class clazz = (Class)type; + JsonpDeserializable annotation = clazz.getAnnotation(JsonpDeserializable.class); + if (annotation != null) { + try { + Field field = clazz.getDeclaredField(annotation.field()); + return (JsonpDeserializer)field.get(null); + } catch (Exception e) { + throw new RuntimeException("No deserializer found in '" + clazz.getName() + "." + annotation.field() + "'"); + } } } - if (clazz == Void.class) { + if (type == Void.class) { return (JsonpDeserializer)JsonpDeserializerBase.VOID; } diff --git a/java-client/src/main/java/co/elastic/clients/json/SimpleJsonpMapper.java b/java-client/src/main/java/co/elastic/clients/json/SimpleJsonpMapper.java index 98591b3d7..67a37428e 100644 --- a/java-client/src/main/java/co/elastic/clients/json/SimpleJsonpMapper.java +++ b/java-client/src/main/java/co/elastic/clients/json/SimpleJsonpMapper.java @@ -23,6 +23,7 @@ import jakarta.json.spi.JsonProvider; import jakarta.json.stream.JsonGenerator; +import java.lang.reflect.Type; import java.util.HashMap; import java.util.Map; @@ -37,8 +38,8 @@ public class SimpleJsonpMapper extends JsonpMapperBase { public static SimpleJsonpMapper INSTANCE = new SimpleJsonpMapper(true); public static SimpleJsonpMapper INSTANCE_REJECT_UNKNOWN_FIELDS = new SimpleJsonpMapper(false); - private static final Map, JsonpSerializer> serializers = new HashMap<>(); - private static final Map, JsonpDeserializer> deserializers = new HashMap<>(); + private static final Map> serializers = new HashMap<>(); + private static final Map> deserializers = new HashMap<>(); static { serializers.put(String.class, (JsonpSerializer) (value, generator, mapper) -> generator.write(value)); @@ -117,14 +118,14 @@ public void serialize(T value, JsonGenerator generator) { } @Override - protected JsonpDeserializer getDefaultDeserializer(Class clazz) { + protected JsonpDeserializer getDefaultDeserializer(Type type) { @SuppressWarnings("unchecked") - JsonpDeserializer deserializer = (JsonpDeserializer) deserializers.get(clazz); + JsonpDeserializer deserializer = (JsonpDeserializer) deserializers.get(type); if (deserializer != null) { return deserializer; } else { throw new JsonException( - "Cannot find a deserializer for type " + clazz.getName() + + "Cannot find a deserializer for type " + type.getTypeName() + ". Consider using a full-featured JsonpMapper" ); } diff --git a/java-client/src/main/java/co/elastic/clients/json/jackson/JacksonJsonpMapper.java b/java-client/src/main/java/co/elastic/clients/json/jackson/JacksonJsonpMapper.java index 54a009d74..ffa352240 100644 --- a/java-client/src/main/java/co/elastic/clients/json/jackson/JacksonJsonpMapper.java +++ b/java-client/src/main/java/co/elastic/clients/json/jackson/JacksonJsonpMapper.java @@ -32,6 +32,7 @@ import jakarta.json.stream.JsonParser; import java.io.IOException; +import java.lang.reflect.Type; import java.util.EnumSet; public class JacksonJsonpMapper extends JsonpMapperBase { @@ -76,8 +77,8 @@ public JsonProvider jsonProvider() { } @Override - protected JsonpDeserializer getDefaultDeserializer(Class clazz) { - return new JacksonValueParser<>(clazz); + protected JsonpDeserializer getDefaultDeserializer(Type type) { + return new JacksonValueParser<>(type); } @Override @@ -103,11 +104,11 @@ public void serialize(T value, JsonGenerator generator) { private class JacksonValueParser extends JsonpDeserializerBase { - private final Class clazz; + private final Type type; - protected JacksonValueParser(Class clazz) { + protected JacksonValueParser(Type type) { super(EnumSet.allOf(JsonParser.Event.class)); - this.clazz = clazz; + this.type = type; } @Override @@ -120,7 +121,7 @@ public T deserialize(JsonParser parser, JsonpMapper mapper, JsonParser.Event eve com.fasterxml.jackson.core.JsonParser jkParser = ((JacksonJsonpParser)parser).jacksonParser(); try { - return objectMapper.readValue(jkParser, clazz); + return objectMapper.readValue(jkParser, objectMapper().constructType(type)); } catch(IOException ioe) { throw JacksonUtils.convertException(ioe); } diff --git a/java-client/src/main/java/co/elastic/clients/json/jsonb/JsonbJsonpMapper.java b/java-client/src/main/java/co/elastic/clients/json/jsonb/JsonbJsonpMapper.java index d5f198fdd..c936f92f4 100644 --- a/java-client/src/main/java/co/elastic/clients/json/jsonb/JsonbJsonpMapper.java +++ b/java-client/src/main/java/co/elastic/clients/json/jsonb/JsonbJsonpMapper.java @@ -34,6 +34,7 @@ import java.io.CharArrayReader; import java.io.CharArrayWriter; +import java.lang.reflect.Type; import java.util.EnumSet; public class JsonbJsonpMapper extends JsonpMapperBase { @@ -60,8 +61,8 @@ public JsonpMapper withAttribute(String name, T value) { } @Override - protected JsonpDeserializer getDefaultDeserializer(Class clazz) { - return new Deserializer<>(clazz); + protected JsonpDeserializer getDefaultDeserializer(Type type) { + return new Deserializer<>(type); } @Override @@ -86,11 +87,11 @@ public JsonProvider jsonProvider() { } private class Deserializer extends JsonpDeserializerBase { - private final Class clazz; + private final Type type; - Deserializer(Class clazz) { + Deserializer(Type type) { super(EnumSet.allOf(JsonParser.Event.class)); - this.clazz = clazz; + this.type = type; } @Override @@ -106,7 +107,7 @@ public T deserialize(JsonParser parser, JsonpMapper mapper, JsonParser.Event eve generator.close(); CharArrayReader car = new CharArrayReader(caw.toCharArray()); - return jsonb.fromJson(car, clazz); + return jsonb.fromJson(car, type); } } diff --git a/java-client/src/main/java/co/elastic/clients/util/MissingRequiredPropertyException.java b/java-client/src/main/java/co/elastic/clients/util/MissingRequiredPropertyException.java index 0a628c11b..e5dd0d3e3 100644 --- a/java-client/src/main/java/co/elastic/clients/util/MissingRequiredPropertyException.java +++ b/java-client/src/main/java/co/elastic/clients/util/MissingRequiredPropertyException.java @@ -26,8 +26,9 @@ * available in {@link ApiTypeHelper} to disable checks. Use with caution. */ public class MissingRequiredPropertyException extends RuntimeException { - private Class clazz; - private String property; + private final Class clazz; + private final String property; + public MissingRequiredPropertyException(Object obj, String property) { super("Missing required property '" + obj.getClass().getSimpleName() + "." + property + "'"); this.clazz = obj.getClass(); diff --git a/java-client/src/test/java/co/elastic/clients/elasticsearch/json/JsonpMapperTest.java b/java-client/src/test/java/co/elastic/clients/elasticsearch/json/JsonpMapperTest.java index 02ecbf783..df2cae7fa 100644 --- a/java-client/src/test/java/co/elastic/clients/elasticsearch/json/JsonpMapperTest.java +++ b/java-client/src/test/java/co/elastic/clients/elasticsearch/json/JsonpMapperTest.java @@ -23,6 +23,7 @@ import co.elastic.clients.json.jackson.JacksonJsonpMapper; import co.elastic.clients.json.jsonb.JsonbJsonpMapper; import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.PropertyNamingStrategies; import jakarta.json.stream.JsonGenerator; @@ -32,8 +33,11 @@ import java.io.StringReader; import java.io.StringWriter; +import java.lang.reflect.Type; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; public class JsonpMapperTest extends Assertions { @@ -69,6 +73,40 @@ public void testJacksonCustomMapper() { testDeserialize(mapper, json); } + @Test + public void testDeserializeWithType() { + + String json = "{\"foo\":{\"int_value\":1,\"double_value\":1.0},\"bar\":{\"int_value\":2,\"double_value\":2.0}}"; + + ObjectMapper jkMapper = new ObjectMapper(); + jkMapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE); + JacksonJsonpMapper mapper = new JacksonJsonpMapper(jkMapper); + + // With type erasure, map values are raw json nodes + { + JsonParser parser = mapper.jsonProvider().createParser(new StringReader(json)); + Map map = new HashMap<>(); + map = mapper.deserialize(parser, (Type) map.getClass()); + + Map finalMap = map; + assertThrows(ClassCastException.class, () -> { + assertEquals(1, finalMap.get("foo").intValue); + }); + } + + // Use a j.l.reflect.Type to deserialize map values correctly + { + TypeReference> typeRef = new TypeReference>() {}; + + JsonParser parser = mapper.jsonProvider().createParser(new StringReader(json)); + Map map = mapper.deserialize(parser, typeRef.getType()); + + System.out.println(map); + assertEquals(1, map.get("foo").intValue); + assertEquals(2, map.get("bar").intValue); + } + } + private void testSerialize(JsonpMapper mapper, String expected) { SomeClass something = new SomeClass(); diff --git a/java-client/src/test/java/co/elastic/clients/elasticsearch/model/ModelTestCase.java b/java-client/src/test/java/co/elastic/clients/elasticsearch/model/ModelTestCase.java index c066c54b5..f8e10a643 100644 --- a/java-client/src/test/java/co/elastic/clients/elasticsearch/model/ModelTestCase.java +++ b/java-client/src/test/java/co/elastic/clients/elasticsearch/model/ModelTestCase.java @@ -32,6 +32,7 @@ import java.io.StringReader; import java.io.StringWriter; import java.lang.reflect.Method; +import java.lang.reflect.Type; import java.util.Random; /** @@ -93,14 +94,22 @@ public static String toJson(T value, JsonpMapper mapper) { } public static T fromJson(String json, Class clazz, JsonpMapper mapper) { + return fromJson(json, (Type)clazz, mapper); + } + + public static T fromJson(String json, Type type, JsonpMapper mapper) { JsonParser parser = mapper.jsonProvider().createParser(new StringReader(json)); - return mapper.deserialize(parser, clazz); + return mapper.deserialize(parser, type); } protected T fromJson(String json, Class clazz) { return fromJson(json, clazz, mapper); } + protected T fromJson(String json, Type type) { + return fromJson(json, type, mapper); + } + @SuppressWarnings("unchecked") protected T checkJsonRoundtrip(T value, String expectedJson) { assertEquals(expectedJson, toJson(value));