diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/CustomObjectMapperTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/CustomObjectMapperTest.java index 99e66a254a9fa..a3d5c8a84382e 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/CustomObjectMapperTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/CustomObjectMapperTest.java @@ -1,11 +1,14 @@ package io.quarkus.resteasy.reactive.jackson.deployment.test; import static io.restassured.RestAssured.given; +import static io.restassured.RestAssured.when; import static org.hamcrest.CoreMatchers.equalTo; import java.util.Objects; +import java.util.concurrent.atomic.AtomicLong; import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; import jakarta.ws.rs.core.MediaType; @@ -18,6 +21,7 @@ import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; import io.quarkus.arc.Unremovable; import io.quarkus.test.QuarkusUnitTest; @@ -32,26 +36,99 @@ public class CustomObjectMapperTest { * `objectMapper.enable(SerializationFeature.WRAP_ROOT_VALUE);` */ @Test - void serverShouldUnwrapRootElement() { - given().body("{\"Request\":{\"value\":\"good\"}}") + void test() { + given().body("{\"Request\":{\"value\":\"FIRST\"}}") .contentType(ContentType.JSON) - .post("/server") + .post("/server/dummy") .then() .statusCode(HttpStatus.SC_OK) - .body(equalTo("good")); + .body(equalTo("0")); + + // ContextResolver was invoked for both reader and writer + when().get("/server/count") + .then() + .statusCode(HttpStatus.SC_OK) + .body(equalTo("2")); + + given().body("{\"Request2\":{\"value\":\"FIRST\"}}") + .contentType(ContentType.JSON) + .post("/server/dummy2") + .then() + .statusCode(HttpStatus.SC_OK) + .body(equalTo("0")); + + // ContextResolver was invoked for both reader and writer because different types where used + when().get("/server/count") + .then() + .statusCode(HttpStatus.SC_OK) + .body(equalTo("4")); + + given().body("{\"Request\":{\"value\":\"FIRST\"}}") + .contentType(ContentType.JSON) + .post("/server/dummy") + .then() + .statusCode(HttpStatus.SC_OK) + .body(equalTo("0")); + + // ContextResolver was not invoked because the types have already been cached + when().get("/server/count") + .then() + .statusCode(HttpStatus.SC_OK) + .body(equalTo("4")); + + given().body("{\"Request2\":{\"value\":\"FIRST\"}}") + .contentType(ContentType.JSON) + .post("/server/dummy2") + .then() + .statusCode(HttpStatus.SC_OK) + .body(equalTo("0")); + + // ContextResolver was not invoked because the types have already been cached + when().get("/server/count") + .then() + .statusCode(HttpStatus.SC_OK) + .body(equalTo("4")); + } + + private static void doTest() { + } @Path("/server") public static class MyResource { @POST @Consumes(MediaType.APPLICATION_JSON) - public String post(Request request) { - return request.value; + @Path("dummy") + public Dummy dummy(Request request) { + return Dummy.valueOf(request.value); + } + + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Path("dummy2") + public Dummy2 dummy2(Request2 request) { + return Dummy2.valueOf(request.value); + } + + @GET + @Path("count") + public long count() { + return CustomObjectMapperContextResolver.COUNT.get(); } } + public enum Dummy { + FIRST, + SECOND + } + + public enum Dummy2 { + FIRST, + SECOND + } + public static class Request { - private String value; + protected String value; public Request() { @@ -85,14 +162,21 @@ public int hashCode() { } } + public static class Request2 extends Request { + } + @Provider @Unremovable public static class CustomObjectMapperContextResolver implements ContextResolver { + static final AtomicLong COUNT = new AtomicLong(); + @Override public ObjectMapper getContext(final Class type) { + COUNT.incrementAndGet(); final ObjectMapper objectMapper = new ObjectMapper(); - objectMapper.enable(DeserializationFeature.UNWRAP_ROOT_VALUE); + objectMapper.enable(DeserializationFeature.UNWRAP_ROOT_VALUE) + .enable(SerializationFeature.WRITE_ENUMS_USING_INDEX); return objectMapper; } } 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 89dbe47c6d36a..863d2b396f877 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 @@ -43,7 +43,8 @@ public class FullyFeaturedServerJacksonMessageBodyReader extends JacksonBasicMes private final Providers providers; private final ConcurrentMap perMethodReader = new ConcurrentHashMap<>(); private final ConcurrentMap perTypeReader = new ConcurrentHashMap<>(); - private final ConcurrentMap contextResolverMap = new ConcurrentHashMap<>(); + private final ConcurrentMap, ObjectMapper> contextResolverMap = new ConcurrentHashMap<>(); + private final ConcurrentMap objectReaderMap = new ConcurrentHashMap<>(); @Inject public FullyFeaturedServerJacksonMessageBodyReader(ObjectMapper mapper, Providers providers) { @@ -154,7 +155,7 @@ private ObjectReader getEffectiveReader(Class type, Type genericType, Me ObjectReader effectiveReader = defaultReader; if (effectiveMapper != originalMapper) { // Effective reader based on the context - effectiveReader = contextResolverMap.computeIfAbsent(effectiveMapper, new Function<>() { + effectiveReader = objectReaderMap.computeIfAbsent(effectiveMapper, new Function<>() { @Override public ObjectReader apply(ObjectMapper objectMapper) { return objectMapper.reader(); @@ -201,7 +202,16 @@ private ObjectMapper getEffectiveMapper(Class type, MediaType responseMe contextResolver = providers.getContextResolver(ObjectMapper.class, null); } if (contextResolver != null) { - return contextResolver.getContext(type); + var cr = contextResolver; + ObjectMapper result = contextResolverMap.computeIfAbsent(type, new Function<>() { + @Override + public ObjectMapper apply(Class aClass) { + return cr.getContext(type); + } + }); + if (result != null) { + return result; + } } return originalMapper; diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/serialisers/FullyFeaturedServerJacksonMessageBodyWriter.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/serialisers/FullyFeaturedServerJacksonMessageBodyWriter.java index 171b6843fa62d..df6601849601f 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/serialisers/FullyFeaturedServerJacksonMessageBodyWriter.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/serialisers/FullyFeaturedServerJacksonMessageBodyWriter.java @@ -38,7 +38,8 @@ public class FullyFeaturedServerJacksonMessageBodyWriter extends ServerMessageBo private final ObjectWriter defaultWriter; private final ConcurrentMap perMethodWriter = new ConcurrentHashMap<>(); private final ConcurrentMap perTypeWriter = new ConcurrentHashMap<>(); - private final ConcurrentMap contextResolverMap = new ConcurrentHashMap<>(); + private final ConcurrentMap, ObjectMapper> contextResolverMap = new ConcurrentHashMap<>(); + private final ConcurrentMap objectWriterMap = new ConcurrentHashMap<>(); @Inject public FullyFeaturedServerJacksonMessageBodyWriter(ObjectMapper mapper, Providers providers) { @@ -112,7 +113,7 @@ private ObjectWriter getEffectiveWriter(ObjectMapper effectiveMapper) { if (effectiveMapper == originalMapper) { return defaultWriter; } - return contextResolverMap.computeIfAbsent(effectiveMapper, new Function<>() { + return objectWriterMap.computeIfAbsent(effectiveMapper, new Function<>() { @Override public ObjectWriter apply(ObjectMapper objectMapper) { return createDefaultWriter(effectiveMapper); @@ -133,7 +134,13 @@ private ObjectMapper getEffectiveMapper(Object o, ServerRequestContext context) contextResolver = providers.getContextResolver(ObjectMapper.class, null); } if (contextResolver != null) { - ObjectMapper mapperFromContextResolver = contextResolver.getContext(o.getClass()); + var cr = contextResolver; + ObjectMapper mapperFromContextResolver = contextResolverMap.computeIfAbsent(o.getClass(), new Function<>() { + @Override + public ObjectMapper apply(Class aClass) { + return cr.getContext(o.getClass()); + } + }); if (mapperFromContextResolver != null) { effectiveMapper = mapperFromContextResolver; }