Skip to content

Commit

Permalink
Properly cache ContextResolver usage for ObjectMapper in server code
Browse files Browse the repository at this point in the history
(cherry picked from commit 7b1221e)
  • Loading branch information
geoand authored and gsmet committed Oct 3, 2023
1 parent de76fa1 commit ad8af4f
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 14 deletions.
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand All @@ -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() {

Expand Down Expand Up @@ -85,14 +162,21 @@ public int hashCode() {
}
}

public static class Request2 extends Request {
}

@Provider
@Unremovable
public static class CustomObjectMapperContextResolver implements ContextResolver<ObjectMapper> {

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;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ public class FullyFeaturedServerJacksonMessageBodyReader extends JacksonBasicMes
private final Providers providers;
private final ConcurrentMap<String, ObjectReader> perMethodReader = new ConcurrentHashMap<>();
private final ConcurrentMap<String, ObjectReader> perTypeReader = new ConcurrentHashMap<>();
private final ConcurrentMap<ObjectMapper, ObjectReader> contextResolverMap = new ConcurrentHashMap<>();
private final ConcurrentMap<Class<?>, ObjectMapper> contextResolverMap = new ConcurrentHashMap<>();
private final ConcurrentMap<ObjectMapper, ObjectReader> objectReaderMap = new ConcurrentHashMap<>();

@Inject
public FullyFeaturedServerJacksonMessageBodyReader(ObjectMapper mapper, Providers providers) {
Expand Down Expand Up @@ -154,7 +155,7 @@ private ObjectReader getEffectiveReader(Class<Object> 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();
Expand Down Expand Up @@ -201,7 +202,16 @@ private ObjectMapper getEffectiveMapper(Class<Object> 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ public class FullyFeaturedServerJacksonMessageBodyWriter extends ServerMessageBo
private final ObjectWriter defaultWriter;
private final ConcurrentMap<String, ObjectWriter> perMethodWriter = new ConcurrentHashMap<>();
private final ConcurrentMap<String, ObjectWriter> perTypeWriter = new ConcurrentHashMap<>();
private final ConcurrentMap<ObjectMapper, ObjectWriter> contextResolverMap = new ConcurrentHashMap<>();
private final ConcurrentMap<Class<?>, ObjectMapper> contextResolverMap = new ConcurrentHashMap<>();
private final ConcurrentMap<ObjectMapper, ObjectWriter> objectWriterMap = new ConcurrentHashMap<>();

@Inject
public FullyFeaturedServerJacksonMessageBodyWriter(ObjectMapper mapper, Providers providers) {
Expand Down Expand Up @@ -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);
Expand All @@ -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;
}
Expand Down

0 comments on commit ad8af4f

Please sign in to comment.