From 3cda45da00cf7268afe62aceaf6d283bbfad591e Mon Sep 17 00:00:00 2001 From: Jose Date: Tue, 1 Aug 2023 14:19:33 +0200 Subject: [PATCH] Allow customizing ObjectReader via `@CustomDeserialization` in Resteasy To customize the `com.fasterxml.jackson.databind.ObjectReader` to read JSON requests with unquoted field names: ```java @CustomDeserialization(SupportUnquotedFields.class) @POST @Path("/use-of-custom-deserializer") public void useOfCustomSerializer(User request) { // ... } ``` where `SupportUnquotedFields` is a `BiFunction` defined as so: ```java public static class SupportUnquotedFields implements BiFunction { @Override public ObjectReader apply(ObjectMapper objectMapper, Type type) { return objectMapper.reader().with(JsonReadFeature.ALLOW_UNQUOTED_FIELD_NAMES); } } ``` --- docs/src/main/asciidoc/resteasy-reactive.adoc | 33 ++++++- .../processor/JacksonFeatureBuildItem.java | 3 +- .../ResteasyReactiveJacksonProcessor.java | 32 +++++++ .../deployment/test/SimpleJsonResource.java | 41 +++++++-- .../deployment/test/SimpleJsonTest.java | 56 ++++++++++-- .../jackson/CustomDeserialization.java | 43 +++++++++ ...ResteasyReactiveServerJacksonRecorder.java | 13 +++ ...eaturedServerJacksonMessageBodyReader.java | 87 ++++++++++++++++--- 8 files changed, 280 insertions(+), 28 deletions(-) create mode 100644 extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/CustomDeserialization.java diff --git a/docs/src/main/asciidoc/resteasy-reactive.adoc b/docs/src/main/asciidoc/resteasy-reactive.adoc index c87c7ce8e9324..f6b28fdd1b785 100644 --- a/docs/src/main/asciidoc/resteasy-reactive.adoc +++ b/docs/src/main/asciidoc/resteasy-reactive.adoc @@ -1425,12 +1425,12 @@ public User userPrivate() { When the result the `userPublic` method is serialized, the `id` field will not be contained in the response as the `Public` view does not include it. The result of `userPrivate` however will include the `id` as expected when serialized. -===== Completely customized per method serialization +===== Completely customized per method serialization/deserialization -There are times when you need to completely customize the serialization of a POJO on a per Jakarta REST method basis. For such use cases, the `@io.quarkus.resteasy.reactive.jackson.CustomSerialization` annotation -is a great tool, as it allows you to configure a per-method `com.fasterxml.jackson.databind.ObjectWriter` which can be configured at will. +There are times when you need to completely customize the serialization/deserialization of a POJO on a per Jakarta REST method basis. For such use cases, the `@io.quarkus.resteasy.reactive.jackson.CustomSerialization` and `@io.quarkus.resteasy.reactive.jackson.CustomDeserialization` annotations. +is a great tool, as it allows you to configure a per-method `com.fasterxml.jackson.databind.ObjectWriter`/`com.fasterxml.jackson.databind.ObjectReader` which can be configured at will. -Here is an example use case: +Here is an example use case to customize the `com.fasterxml.jackson.databind.ObjectWriter`: [source,java] ---- @@ -1459,6 +1459,31 @@ Essentially what this class does is force Jackson to not include quotes in the f It is important to note that this customization is only performed for the serialization of the Jakarta REST methods that use `@CustomSerialization(UnquotedFields.class)`. +Following the previous example, let's now customize the `com.fasterxml.jackson.databind.ObjectReader` to read JSON requests with unquoted field names: + +[source,java] +---- +@CustomDeserialization(SupportUnquotedFields.class) +@POST +@Path("/use-of-custom-deserializer") +public void useOfCustomSerializer(User request) { + // ... +} +---- + +where `SupportUnquotedFields` is a `BiFunction` defined as so: + +[source,java] +---- +public static class SupportUnquotedFields implements BiFunction { + + @Override + public ObjectReader apply(ObjectMapper objectMapper, Type type) { + return objectMapper.reader().with(JsonReadFeature.ALLOW_UNQUOTED_FIELD_NAMES); + } +} +---- + === XML serialisation [[xml]] diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/JacksonFeatureBuildItem.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/JacksonFeatureBuildItem.java index fe314c204686e..899201760cba9 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/JacksonFeatureBuildItem.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/JacksonFeatureBuildItem.java @@ -19,6 +19,7 @@ public Feature getFeature() { public enum Feature { JSON_VIEW, - CUSTOM_SERIALIZATION + CUSTOM_SERIALIZATION, + CUSTOM_DESERIALIZATION } } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/ResteasyReactiveJacksonProcessor.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/ResteasyReactiveJacksonProcessor.java index fcb10ce690838..07b8f7610214d 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/ResteasyReactiveJacksonProcessor.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/ResteasyReactiveJacksonProcessor.java @@ -45,6 +45,7 @@ import io.quarkus.resteasy.reactive.common.deployment.JaxRsResourceIndexBuildItem; import io.quarkus.resteasy.reactive.common.deployment.ResourceScanningResultBuildItem; import io.quarkus.resteasy.reactive.common.deployment.ServerDefaultProducesHandlerBuildItem; +import io.quarkus.resteasy.reactive.jackson.CustomDeserialization; import io.quarkus.resteasy.reactive.jackson.CustomSerialization; import io.quarkus.resteasy.reactive.jackson.DisableSecureSerialization; import io.quarkus.resteasy.reactive.jackson.EnableSecureSerialization; @@ -77,6 +78,7 @@ public class ResteasyReactiveJacksonProcessor { private static final DotName JSON_VIEW = DotName.createSimple(JsonView.class.getName()); private static final DotName CUSTOM_SERIALIZATION = DotName.createSimple(CustomSerialization.class.getName()); + private static final DotName CUSTOM_DESERIALIZATION = DotName.createSimple(CustomDeserialization.class.getName()); private static final DotName SECURE_FIELD = DotName.createSimple(SecureField.class.getName()); private static final DotName DISABLE_SECURE_SERIALIZATION = DotName .createSimple(DisableSecureSerialization.class.getName()); @@ -265,6 +267,36 @@ void handleJsonAnnotations(Optional resourceSca biFunctionType.name().toString()); } } + if (resourceClass.annotationsMap().containsKey(CUSTOM_DESERIALIZATION)) { + jacksonFeatures.add(JacksonFeatureBuildItem.Feature.CUSTOM_DESERIALIZATION); + for (AnnotationInstance instance : resourceClass.annotationsMap().get(CUSTOM_DESERIALIZATION)) { + AnnotationValue annotationValue = instance.value(); + if (annotationValue == null) { + continue; + } + if (instance.target().kind() != AnnotationTarget.Kind.METHOD) { + continue; + } + Type biFunctionType = annotationValue.asClass(); + if (biFunctionType == null) { + continue; + } + ClassInfo biFunctionClassInfo = index.getIndex().getClassByName(biFunctionType.name()); + if (biFunctionClassInfo == null) { + // be lenient + } else { + if (!biFunctionClassInfo.hasNoArgsConstructor()) { + throw new IllegalArgumentException( + "Class '" + biFunctionClassInfo.name() + "' must contain a no-args constructor"); + } + } + reflectiveClassProducer.produce( + ReflectiveClassBuildItem.builder(biFunctionType.name().toString()) + .build()); + recorder.recordCustomDeserialization(getMethodId(instance.target().asMethod()), + biFunctionType.name().toString()); + } + } } for (ResourceMethodCustomSerializationBuildItem bi : resourceMethodCustomSerializationBuildItems) { diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonResource.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonResource.java index 4e68d3eadb5cd..8e5f215debb6b 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonResource.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonResource.java @@ -24,10 +24,13 @@ import org.jboss.resteasy.reactive.server.ServerExceptionMapper; import com.fasterxml.jackson.annotation.JsonView; +import com.fasterxml.jackson.core.json.JsonReadFeature; import com.fasterxml.jackson.core.json.JsonWriteFeature; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; import com.fasterxml.jackson.databind.ObjectWriter; +import io.quarkus.resteasy.reactive.jackson.CustomDeserialization; import io.quarkus.resteasy.reactive.jackson.CustomSerialization; import io.quarkus.resteasy.reactive.jackson.DisableSecureSerialization; import io.quarkus.resteasy.reactive.jackson.EnableSecureSerialization; @@ -56,13 +59,20 @@ public Person getPerson() { return person; } - @CustomSerialization(UnquotedFieldsPersonBiFunction.class) + @CustomSerialization(UnquotedFieldsPersonSerialization.class) @GET @Path("custom-serialized-person") public Person getCustomSerializedPerson() { return getPerson(); } + @CustomDeserialization(UnquotedFieldsPersonDeserialization.class) + @POST + @Path("custom-deserialized-person") + public Person echoCustomDeserializedPerson(Person request) { + return request; + } + @EnableSecureSerialization @GET @Path("secure-person") @@ -180,7 +190,8 @@ public List getPeople(List people) { return reversed; } - @CustomSerialization(UnquotedFieldsPersonBiFunction.class) + @CustomDeserialization(UnquotedFieldsPersonDeserialization.class) + @CustomSerialization(UnquotedFieldsPersonSerialization.class) @POST @Path("/custom-serialized-people") @Consumes(MediaType.APPLICATION_JSON) @@ -261,7 +272,7 @@ public User userWithPrivateView() { return testUser(); } - @CustomSerialization(UnquotedFieldsPersonBiFunction.class) + @CustomSerialization(UnquotedFieldsPersonSerialization.class) @GET @Path("/invalid-use-of-custom-serializer") public User invalidUseOfCustomSerializer() { @@ -308,11 +319,11 @@ public String genericInputTest(DataItem item) { return item.getContent().getName(); } - public static class UnquotedFieldsPersonBiFunction implements BiFunction { + public static class UnquotedFieldsPersonSerialization implements BiFunction { public static final AtomicInteger count = new AtomicInteger(); - public UnquotedFieldsPersonBiFunction() { + public UnquotedFieldsPersonSerialization() { count.incrementAndGet(); } @@ -328,4 +339,24 @@ public ObjectWriter apply(ObjectMapper objectMapper, Type type) { } } + public static class UnquotedFieldsPersonDeserialization implements BiFunction { + + public static final AtomicInteger count = new AtomicInteger(); + + public UnquotedFieldsPersonDeserialization() { + count.incrementAndGet(); + } + + @Override + public ObjectReader apply(ObjectMapper objectMapper, Type type) { + if (type instanceof ParameterizedType) { + type = ((ParameterizedType) type).getActualTypeArguments()[0]; + } + if (!type.getTypeName().equals(Person.class.getName())) { + throw new IllegalArgumentException("Only Person type can be handled"); + } + return objectMapper.reader().with(JsonReadFeature.ALLOW_UNQUOTED_FIELD_NAMES); + } + } + } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonTest.java index ba64b502ffc03..9ebb1b30d6d5e 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonTest.java @@ -248,7 +248,7 @@ public void testJsonMulti() { @Test public void testCustomSerialization() { - assertEquals(0, SimpleJsonResource.UnquotedFieldsPersonBiFunction.count.intValue()); + assertEquals(0, SimpleJsonResource.UnquotedFieldsPersonSerialization.count.intValue()); // assert that we get a proper response // we can't use json-path to assert because the returned string is not proper json as it does not have quotes around the field names @@ -261,7 +261,7 @@ public void testCustomSerialization() { .body(containsString("Bob")) .body(containsString("Builder")); // assert that our bi-function was created - assertEquals(1, SimpleJsonResource.UnquotedFieldsPersonBiFunction.count.intValue()); + assertEquals(1, SimpleJsonResource.UnquotedFieldsPersonSerialization.count.intValue()); // assert with a list of people RestAssured @@ -279,7 +279,7 @@ public void testCustomSerialization() { .body(containsString("Bob2")) .body(containsString("Builder2")); // assert that another instance of our bi-function was created as a different resource method was used - assertEquals(2, SimpleJsonResource.UnquotedFieldsPersonBiFunction.count.intValue()); + assertEquals(2, SimpleJsonResource.UnquotedFieldsPersonSerialization.count.intValue()); RestAssured.get("/simple/custom-serialized-person") .then() @@ -294,13 +294,59 @@ public void testCustomSerialization() { .statusCode(200) .contentType("application/json"); // assert that the instances were re-used as we simply invoked methods that should have already created their object writers - assertEquals(2, SimpleJsonResource.UnquotedFieldsPersonBiFunction.count.intValue()); + assertEquals(2, SimpleJsonResource.UnquotedFieldsPersonSerialization.count.intValue()); RestAssured.get("/simple/invalid-use-of-custom-serializer") .then() .statusCode(500); // a new instance should have been created - assertEquals(3, SimpleJsonResource.UnquotedFieldsPersonBiFunction.count.intValue()); + assertEquals(3, SimpleJsonResource.UnquotedFieldsPersonSerialization.count.intValue()); + } + + @Test + public void testCustomDeserialization() { + int currentCounter = SimpleJsonResource.UnquotedFieldsPersonDeserialization.count.intValue(); + + // assert that the reader support the unquoted fields (because we have used a custom object reader + // via `@CustomDeserialization` + Person actual = RestAssured.given() + .body("{first: \"Hello\", last: \"Deserialization\"}") + .contentType("application/json; charset=utf-8") + .post("/simple/custom-deserialized-person") + .then() + .statusCode(200) + .contentType("application/json") + .header("transfer-encoding", nullValue()) + .header("content-length", notNullValue()) + .extract().as(Person.class); + assertEquals("Hello", actual.getFirst()); + assertEquals("Deserialization", actual.getLast()); + assertEquals(currentCounter + 1, SimpleJsonResource.UnquotedFieldsPersonDeserialization.count.intValue()); + + // assert that the instances were re-used as we simply invoked methods that should have already created their object readers + RestAssured.given() + .body("{first: \"Hello\", last: \"Deserialization\"}") + .contentType("application/json; charset=utf-8") + .post("/simple/custom-deserialized-person") + .then() + .statusCode(200); + assertEquals(currentCounter + 1, SimpleJsonResource.UnquotedFieldsPersonDeserialization.count.intValue()); + + // assert with a list of people + RestAssured + .with() + .body("[{first: \"Bob\", last: \"Builder\"}, {first: \"Bob2\", last: \"Builder2\"}]") + .contentType("application/json; charset=utf-8") + .post("/simple/custom-serialized-people") + .then() + .statusCode(200) + .contentType("application/json") + .header("transfer-encoding", nullValue()) + .header("content-length", notNullValue()) + .body(containsString("Bob")) + .body(containsString("Builder")) + .body(containsString("Bob2")) + .body(containsString("Builder2")); } @Test diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/CustomDeserialization.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/CustomDeserialization.java new file mode 100644 index 0000000000000..7001155cdae09 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/CustomDeserialization.java @@ -0,0 +1,43 @@ +package io.quarkus.resteasy.reactive.jackson; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Type; +import java.util.function.BiFunction; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; + +import io.smallrye.common.annotation.Experimental; + +/** + * Annotation that can be used on RESTEasy Reactive Resource method to allow users to configure Jackson deserialization + * for that method only, without affecting the global Jackson configuration. + */ +@Experimental(value = "Remains to be determined if this is the best possible API for users to configure per Resource Method Deserialization") +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD }) +public @interface CustomDeserialization { + + /** + * A {@code BiFunction} that converts the global {@code ObjectMapper} and type for which a custom {@code ObjectReader} is + * needed + * (this type will be a generic type if the method returns such a generic type) and returns the instance of the custom + * {@code ObjectReader}. + *

+ * Quarkus will construct one instance of this {@code BiFunction} for each JAX-RS resource method that is annotated with + * {@code CustomDeserialization} and once an instance is created it will be cached for subsequent usage by that resource + * method. + *

+ * The {@code BiFunction} MUST contain a no-args constructor. + *

+ * Furthermore, it is advisable that it contains no state that is updated outside + * its constructor. + *

+ * Finally and most importantly, the {@code ObjectMapper} should NEVER be changed any way as it is the global ObjectMapper + * that is accessible to the entire Quarkus application. + */ + Class> value(); +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/ResteasyReactiveServerJacksonRecorder.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/ResteasyReactiveServerJacksonRecorder.java index 99d9b5b142744..0e023e9a7e5d8 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/ResteasyReactiveServerJacksonRecorder.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/ResteasyReactiveServerJacksonRecorder.java @@ -6,6 +6,7 @@ import java.util.function.BiFunction; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; import com.fasterxml.jackson.databind.ObjectWriter; import io.quarkus.runtime.ShutdownContext; @@ -16,6 +17,7 @@ public class ResteasyReactiveServerJacksonRecorder { private static final Map> jsonViewMap = new HashMap<>(); private static final Map> customSerializationMap = new HashMap<>(); + private static final Map> customDeserializationMap = new HashMap<>(); public void recordJsonView(String methodId, String className) { jsonViewMap.put(methodId, loadClass(className)); @@ -25,12 +27,17 @@ public void recordCustomSerialization(String methodId, String className) { customSerializationMap.put(methodId, loadClass(className)); } + public void recordCustomDeserialization(String methodId, String className) { + customDeserializationMap.put(methodId, loadClass(className)); + } + public void configureShutdown(ShutdownContext shutdownContext) { shutdownContext.addShutdownTask(new Runnable() { @Override public void run() { jsonViewMap.clear(); customSerializationMap.clear(); + customDeserializationMap.clear(); } }); } @@ -44,6 +51,12 @@ public static Class> cust return (Class>) customSerializationMap.get(methodId); } + @SuppressWarnings("unchecked") + public static Class> customDeserializationForMethod( + String methodId) { + return (Class>) customDeserializationMap.get(methodId); + } + private Class loadClass(String className) { try { return Thread.currentThread().getContextClassLoader().loadClass(className); diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/serialisers/FullyFeaturedServerJacksonMessageBodyReader.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/serialisers/FullyFeaturedServerJacksonMessageBodyReader.java index 6ccb2595e3492..2f8fc76f1c892 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/serialisers/FullyFeaturedServerJacksonMessageBodyReader.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/serialisers/FullyFeaturedServerJacksonMessageBodyReader.java @@ -1,11 +1,14 @@ package io.quarkus.resteasy.reactive.jackson.runtime.serialisers; +import static org.jboss.resteasy.reactive.server.jackson.JacksonMessageBodyWriterUtil.setNecessaryJsonFactoryConfig; + import java.io.IOException; import java.io.InputStream; import java.lang.annotation.Annotation; import java.lang.reflect.Type; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.util.function.BiFunction; import java.util.function.Function; import jakarta.inject.Inject; @@ -17,6 +20,7 @@ import jakarta.ws.rs.ext.Providers; import org.jboss.resteasy.reactive.common.util.StreamUtil; +import org.jboss.resteasy.reactive.server.core.CurrentRequestManager; import org.jboss.resteasy.reactive.server.jackson.JacksonBasicMessageBodyReader; import org.jboss.resteasy.reactive.server.spi.ResteasyReactiveResourceInfo; import org.jboss.resteasy.reactive.server.spi.ServerMessageBodyReader; @@ -29,15 +33,20 @@ import com.fasterxml.jackson.databind.exc.InvalidDefinitionException; import com.fasterxml.jackson.databind.exc.MismatchedInputException; +import io.quarkus.resteasy.reactive.jackson.runtime.ResteasyReactiveServerJacksonRecorder; + public class FullyFeaturedServerJacksonMessageBodyReader extends JacksonBasicMessageBodyReader implements ServerMessageBodyReader { + private final ObjectMapper originalMapper; private final Providers providers; + private final ConcurrentMap perMethodReader = new ConcurrentHashMap<>(); private final ConcurrentMap contextResolverMap = new ConcurrentHashMap<>(); @Inject public FullyFeaturedServerJacksonMessageBodyReader(ObjectMapper mapper, Providers providers) { super(mapper); + this.originalMapper = mapper; this.providers = providers; } @@ -93,7 +102,7 @@ private Object doReadFrom(Class type, Type genericType, MediaType respon return null; } try { - ObjectReader reader = getEffectiveReader(type, responseMediaType); + ObjectReader reader = getEffectiveReader(type, genericType, responseMediaType); return reader.forType(reader.getTypeFactory().constructType(genericType != null ? genericType : type)) .readValue(entityStream); } catch (MismatchedInputException e) { @@ -109,23 +118,50 @@ private boolean isEmptyInputException(MismatchedInputException e) { return e.getMessage().startsWith("No content"); } - private ObjectReader getEffectiveReader(Class type, MediaType responseMediaType) { - ObjectMapper effectiveMapper = getObjectMapperFromContext(type, responseMediaType); - if (effectiveMapper == null) { - return getEffectiveReader(); + private ObjectReader getEffectiveReader(Class type, Type genericType, MediaType responseMediaType) { + ObjectMapper effectiveMapper = getEffectiveMapper(type, responseMediaType); + ObjectReader effectiveReader = defaultReader; + if (effectiveMapper != originalMapper) { + // Effective reader based on the context + effectiveReader = contextResolverMap.computeIfAbsent(effectiveMapper, new Function<>() { + @Override + public ObjectReader apply(ObjectMapper objectMapper) { + return objectMapper.reader(); + } + }); } - return contextResolverMap.computeIfAbsent(effectiveMapper, new Function<>() { - @Override - public ObjectReader apply(ObjectMapper objectMapper) { - return objectMapper.reader(); + // Get object reader from context if configured + ServerRequestContext context = CurrentRequestManager.get(); + if (context != null) { + ResteasyReactiveResourceInfo resourceInfo = context.getResteasyReactiveResourceInfo(); + if (resourceInfo != null) { + String methodId = resourceInfo.getMethodId(); + var customDeserializationValue = ResteasyReactiveServerJacksonRecorder.customDeserializationForMethod(methodId); + if (customDeserializationValue != null) { + ObjectReader objectReader = perMethodReader.computeIfAbsent(methodId, + new MethodObjectReaderFunction(customDeserializationValue, genericType, effectiveMapper)); + Class jsonViewValue = ResteasyReactiveServerJacksonRecorder.jsonViewForMethod(methodId); + if (jsonViewValue != null) { + objectReader = objectReader.withView(jsonViewValue); + } + + return objectReader; + } + + Class jsonViewValue = ResteasyReactiveServerJacksonRecorder.jsonViewForMethod(methodId); + if (jsonViewValue != null) { + return effectiveReader.withView(jsonViewValue); + } } - }); + } + + return effectiveReader; } - private ObjectMapper getObjectMapperFromContext(Class type, MediaType responseMediaType) { + private ObjectMapper getEffectiveMapper(Class type, MediaType responseMediaType) { if (providers == null) { - return null; + return originalMapper; } ContextResolver contextResolver = providers.getContextResolver(ObjectMapper.class, @@ -138,6 +174,31 @@ private ObjectMapper getObjectMapperFromContext(Class type, MediaType re return contextResolver.getContext(type); } - return null; + return originalMapper; + } + + private static class MethodObjectReaderFunction implements Function { + private final Class> clazz; + private final Type genericType; + private final ObjectMapper originalMapper; + + public MethodObjectReaderFunction(Class> clazz, Type genericType, + ObjectMapper originalMapper) { + this.clazz = clazz; + this.genericType = genericType; + this.originalMapper = originalMapper; + } + + @Override + public ObjectReader apply(String methodId) { + try { + BiFunction biFunctionInstance = clazz.getDeclaredConstructor().newInstance(); + ObjectReader objectReader = biFunctionInstance.apply(originalMapper, genericType); + setNecessaryJsonFactoryConfig(objectReader.getFactory()); + return objectReader; + } catch (Exception e) { + throw new RuntimeException(e); + } + } } }