Skip to content

Commit

Permalink
Introduce support for ContextResolver<ObjectMapper> in server part of…
Browse files Browse the repository at this point in the history
… RESTEasy Reactive

Relates to: quarkusio#26152
  • Loading branch information
geoand committed Jul 28, 2022
1 parent 80304e4 commit aaa5253
Show file tree
Hide file tree
Showing 3 changed files with 159 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import org.jboss.resteasy.reactive.server.util.MethodId;

import com.fasterxml.jackson.annotation.JsonView;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;

import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
Expand Down Expand Up @@ -58,6 +59,7 @@
import io.quarkus.resteasy.reactive.jackson.runtime.serialisers.vertx.VertxJsonArrayMessageBodyWriter;
import io.quarkus.resteasy.reactive.jackson.runtime.serialisers.vertx.VertxJsonObjectMessageBodyReader;
import io.quarkus.resteasy.reactive.jackson.runtime.serialisers.vertx.VertxJsonObjectMessageBodyWriter;
import io.quarkus.resteasy.reactive.server.deployment.ContextResolversBuildItem;
import io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveResourceMethodEntriesBuildItem;
import io.quarkus.resteasy.reactive.spi.CustomExceptionMapperBuildItem;
import io.quarkus.resteasy.reactive.spi.ExceptionMapperBuildItem;
Expand Down Expand Up @@ -122,10 +124,13 @@ AdditionalBeanBuildItem beans() {
}

@BuildStep
void additionalProviders(List<JacksonFeatureBuildItem> jacksonFeatureBuildItems,
void additionalProviders(ContextResolversBuildItem contextResolversBuildItem,
List<JacksonFeatureBuildItem> jacksonFeatureBuildItems,
BuildProducer<MessageBodyReaderBuildItem> additionalReaders,
BuildProducer<MessageBodyWriterBuildItem> additionalWriters) {
boolean applicationNeedsSpecialJacksonFeatures = jacksonFeatureBuildItems.isEmpty();
boolean applicationDoesNotNeedSpecialJacksonFeatures = jacksonFeatureBuildItems.isEmpty();
boolean hasObjectMapperContextResolver = contextResolversBuildItem.getContextResolvers().getResolvers()
.containsKey(ObjectMapper.class);

additionalReaders
.produce(
Expand All @@ -150,7 +155,9 @@ void additionalProviders(List<JacksonFeatureBuildItem> jacksonFeatureBuildItems,
additionalWriters
.produce(
new MessageBodyWriterBuildItem.Builder(
getJacksonMessageBodyWriter(applicationNeedsSpecialJacksonFeatures), Object.class.getName())
getJacksonMessageBodyWriter(
!applicationDoesNotNeedSpecialJacksonFeatures || hasObjectMapperContextResolver),
Object.class.getName())
.setMediaTypeStrings(HANDLED_MEDIA_TYPES)
.setBuiltin(true)
.build());
Expand All @@ -170,9 +177,9 @@ void additionalProviders(List<JacksonFeatureBuildItem> jacksonFeatureBuildItems,
.build());
}

private String getJacksonMessageBodyWriter(boolean applicationNeedsSpecialJacksonFeatures) {
return applicationNeedsSpecialJacksonFeatures ? BasicServerJacksonMessageBodyWriter.class.getName()
: FullyFeaturedServerJacksonMessageBodyWriter.class.getName();
private String getJacksonMessageBodyWriter(boolean needsFullFeatureSet) {
return needsFullFeatureSet ? FullyFeaturedServerJacksonMessageBodyWriter.class.getName()
: BasicServerJacksonMessageBodyWriter.class.getName();
}

@Record(ExecutionTime.STATIC_INIT)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package io.quarkus.resteasy.reactive.jackson.deployment.test;

import static com.fasterxml.jackson.databind.SerializationFeature.WRITE_ENUMS_USING_INDEX;
import static io.restassured.RestAssured.*;
import static org.hamcrest.CoreMatchers.equalTo;

import java.util.function.Supplier;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.ext.ContextResolver;
import javax.ws.rs.ext.Provider;

import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import com.fasterxml.jackson.databind.ObjectMapper;

import io.quarkus.test.QuarkusUnitTest;
import io.restassured.http.ContentType;

public class ContextResolverTest {

@RegisterExtension
static QuarkusUnitTest test = new QuarkusUnitTest()
.setArchiveProducer(new Supplier<>() {
@Override
public JavaArchive get() {
return ShrinkWrap.create(JavaArchive.class)
.addClasses(EnumsResource.class, Type.class, TypeContextResolver.class);
}
});

@Test
public void shouldUseCustomObjectMapper() {
with().accept(ContentType.JSON).get("/enums/type/foo")
.then().statusCode(200).body(equalTo("0"));
with().accept(ContentType.JSON).get("/enums/type/bar")
.then().statusCode(200).body(equalTo("1"));
}

@Test
public void shouldUseDefaultObjectMapper() {
with().accept(ContentType.JSON).get("/enums/color/red")
.then().statusCode(200).body(equalTo("\"RED\""));
with().accept(ContentType.JSON).get("/enums/color/black")
.then().statusCode(200).body(equalTo("\"BLACK\""));
}

@Path("enums")
public static class EnumsResource {

@Path("type/foo")
@GET
public Type foo() {
return Type.FOO;
}

@Path("type/bar")
@GET
public Type bar() {
return Type.BAR;
}

@Path("color/red")
@GET
public Color red() {
return Color.RED;
}

@Path("color/black")
@GET
public Color black() {
return Color.BLACK;
}
}

public enum Type {
FOO,
BAR
}

public enum Color {
RED,
BLACK
}

@Provider
public static class TypeContextResolver implements ContextResolver<ObjectMapper> {
@Override
public ObjectMapper getContext(Class<?> type) {
if (!type.isAssignableFrom(Type.class)) {
return null;
}
ObjectMapper result = new ObjectMapper();
result.enable(WRITE_ENUMS_USING_INDEX);
return result;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.ext.ContextResolver;
import javax.ws.rs.ext.Providers;

import org.jboss.resteasy.reactive.server.spi.ResteasyReactiveResourceInfo;
import org.jboss.resteasy.reactive.server.spi.ServerMessageBodyWriter;
Expand All @@ -31,13 +33,16 @@
public class FullyFeaturedServerJacksonMessageBodyWriter extends ServerMessageBodyWriter.AllWriteableMessageBodyWriter {

private final ObjectMapper originalMapper;
private final Providers providers;
private final ObjectWriter defaultWriter;
private final ConcurrentMap<String, ObjectWriter> perMethodWriter = new ConcurrentHashMap<>();
private final ConcurrentMap<ObjectMapper, ObjectWriter> contextResolverMap = new ConcurrentHashMap<>();

@Inject
public FullyFeaturedServerJacksonMessageBodyWriter(ObjectMapper mapper) {
public FullyFeaturedServerJacksonMessageBodyWriter(ObjectMapper mapper, Providers providers) {
this.originalMapper = mapper;
this.defaultWriter = createDefaultWriter(mapper);
this.providers = providers;
}

@Override
Expand All @@ -47,6 +52,8 @@ public void writeResponse(Object o, Type genericType, ServerRequestContext conte
if (o instanceof String) { // YUK: done in order to avoid adding extra quotes...
stream.write(((String) o).getBytes(StandardCharsets.UTF_8));
} else {
ObjectMapper effectiveMapper = getEffectiveMapper(o, context);

// First test the names to see if JsonView is used. We do this to avoid doing reflection for the common case
// where JsonView is not used
ResteasyReactiveResourceInfo resourceInfo = context.getResteasyReactiveResourceInfo();
Expand All @@ -55,23 +62,56 @@ public void writeResponse(Object o, Type genericType, ServerRequestContext conte
var customSerializationValue = ResteasyReactiveServerJacksonRecorder.customSerializationForMethod(methodId);
if (customSerializationValue != null) {
ObjectWriter objectWriter = perMethodWriter.computeIfAbsent(methodId,
new MethodObjectWriterFunction(customSerializationValue, genericType, originalMapper));
new MethodObjectWriterFunction(customSerializationValue, genericType, effectiveMapper));
objectWriter.writeValue(stream, o);
return;
}

Class<?> jsonViewValue = ResteasyReactiveServerJacksonRecorder.jsonViewForMethod(methodId);
if (jsonViewValue != null) {
defaultWriter.withView(jsonViewValue).writeValue(stream, o);
getEffectiveWriter(effectiveMapper).withView(jsonViewValue).writeValue(stream, o);
return;
}
}
defaultWriter.writeValue(stream, o);
getEffectiveWriter(effectiveMapper).writeValue(stream, o);
}
// we don't use try-with-resources because that results in writing to the http output without the exception mapping coming into play
stream.close();
}

private ObjectWriter getEffectiveWriter(ObjectMapper effectiveMapper) {
if (effectiveMapper == originalMapper) {
return defaultWriter;
}
return contextResolverMap.computeIfAbsent(effectiveMapper, new Function<>() {
@Override
public ObjectWriter apply(ObjectMapper objectMapper) {
return createDefaultWriter(effectiveMapper);
}
});
}

/**
* Obtains the user configured {@link ObjectMapper} if there is a {@link ContextResolver} configured.
* Otherwise, returns the default {@link ObjectMapper}.
*/
private ObjectMapper getEffectiveMapper(Object o, ServerRequestContext context) {
ObjectMapper effectiveMapper = originalMapper;
ContextResolver<ObjectMapper> contextResolver = providers.getContextResolver(ObjectMapper.class,
context.getResponseMediaType());
if (contextResolver == null) {
// TODO: not sure if this is correct, but Jackson does this as well...
contextResolver = providers.getContextResolver(ObjectMapper.class, null);
}
if (contextResolver != null) {
ObjectMapper mapperFromContextResolver = contextResolver.getContext(o.getClass());
if (mapperFromContextResolver != null) {
effectiveMapper = mapperFromContextResolver;
}
}
return effectiveMapper;
}

@Override
public void writeTo(Object o, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType,
MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream) throws IOException, WebApplicationException {
Expand Down

0 comments on commit aaa5253

Please sign in to comment.