Skip to content

Commit

Permalink
Merge pull request quarkusio#43700 from danielbobbert/main
Browse files Browse the repository at this point in the history
Use generic return type of method to construct proper Jackson writer
  • Loading branch information
geoand authored Oct 16, 2024
2 parents 2c9a5ac + 13f9095 commit b8415d8
Show file tree
Hide file tree
Showing 8 changed files with 177 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package io.quarkus.resteasy.reactive.jackson.deployment.test;

import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(value = PolymorphicSub.class, name = "sub")
})
public class PolymorphicBase {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package io.quarkus.resteasy.reactive.jackson.deployment.test;

import java.util.List;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

@Path("poly")
public class PolymorphicEndpoint {

@Produces(MediaType.APPLICATION_JSON)
@GET
@Path("single")
public PolymorphicBase getSingle() {
return new PolymorphicSub();
}

@Produces(MediaType.APPLICATION_JSON)
@GET
@Path("many")
public List<PolymorphicBase> getMany() {
return List.of(new PolymorphicSub(), new PolymorphicSub());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package io.quarkus.resteasy.reactive.jackson.deployment.test;

import com.fasterxml.jackson.annotation.JsonTypeName;

@JsonTypeName("sub")
public class PolymorphicSub extends PolymorphicBase {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package io.quarkus.resteasy.reactive.jackson.deployment.test;

import java.util.function.Supplier;

import org.hamcrest.Matchers;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.test.QuarkusUnitTest;
import io.restassured.RestAssured;

public class PolymorphicTest {

@RegisterExtension
static QuarkusUnitTest test = new QuarkusUnitTest()
.setArchiveProducer(new Supplier<>() {
@Override
public JavaArchive get() {
return ShrinkWrap.create(JavaArchive.class)
.addClasses(PolymorphicEndpoint.class, PolymorphicBase.class, PolymorphicSub.class)
.addAsResource(new StringAsset(""), "application.properties");
}
});

@Test
public void testSingle() {
RestAssured.get("/poly/single")
.then()
.statusCode(200)
.contentType("application/json")
.body(Matchers.is("{\"type\":\"sub\"}"));
}

@Test
public void testMany() {
RestAssured.get("/poly/many")
.then()
.statusCode(200)
.contentType("application/json")
.body(Matchers.is("[{\"type\":\"sub\"},{\"type\":\"sub\"}]"));
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.quarkus.resteasy.reactive.jackson.runtime.mappers;

import java.lang.reflect.Array;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.Map;
import java.util.Optional;
Expand All @@ -9,6 +10,7 @@
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.databind.SerializerProvider;

import io.quarkus.arc.Arc;
Expand Down Expand Up @@ -47,6 +49,39 @@ public static boolean includeSecureField(String[] rolesAllowed) {
return false;
}

/**
* Determine the root type that should be used for serialization of generic types.
* Returns the appropriate root type or {@code null} if default serialization should be used.
*/
public static JavaType getGenericRootType(Type genericType, ObjectWriter defaultWriter) {
// Jackson needs additional type information when serializing generic types, as discussed here:
// https://github.com/FasterXML/jackson-databind/issues/336 and https://github.com/FasterXML/jackson-databind/issues/23
// Parts of the code were taken from org.jboss.resteasy.plugins.providers.jackson.ResteasyJackson2Provider
// which was used in quarkus-resteasy to handle this situation.
JavaType rootType = null;
if (genericType != null) {
/*
* 10-Jan-2011, tatu: as per [JACKSON-456], it's not safe to just force root
* type since it prevents polymorphic type serialization. Since we really
* just need this for generics, let's only use generic type if it's truly
* generic.
*/
if (genericType.getClass() != Class.class) {
rootType = defaultWriter.getTypeFactory().constructType(genericType);
/*
* 26-Feb-2011, tatu: To help with [JACKSON-518], we better recognize cases where
* type degenerates back into "Object.class" (as is the case with plain TypeVariable,
* for example), and not use that.
*/
if (rootType.getRawClass() == Object.class) {
rootType = null;
}
}
}

return rootType;
}

private static class RolesAllowedHolder {

private static final ArcContainer ARC_CONTAINER = Arc.container();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;

import jakarta.inject.Inject;
import jakarta.ws.rs.WebApplicationException;
Expand All @@ -17,26 +20,56 @@
import org.jboss.resteasy.reactive.server.spi.ServerMessageBodyWriter;
import org.jboss.resteasy.reactive.server.spi.ServerRequestContext;

import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;

import io.quarkus.resteasy.reactive.jackson.runtime.mappers.JacksonMapperUtil;

public class BasicServerJacksonMessageBodyWriter extends ServerMessageBodyWriter.AllWriteableMessageBodyWriter {

private final ObjectWriter defaultWriter;
private final Map<JavaType, ObjectWriter> genericWriters = new ConcurrentHashMap<>();

@Inject
public BasicServerJacksonMessageBodyWriter(ObjectMapper mapper) {
this.defaultWriter = createDefaultWriter(mapper);
}

private ObjectWriter getWriter(Type genericType, Object value) {
// make sure we properly handle polymorphism in generic collections
if (value != null && genericType != null) {
JavaType rootType = JacksonMapperUtil.getGenericRootType(genericType, defaultWriter);
// Check that the determined root type is really assignable from the given entity.
// A mismatch can happen, if a ServerResponseFilter replaces the response entity with another object
// that does not match the original signature of the method (see HalServerResponseFilter for an example)
if (rootType != null && rootType.isTypeOrSuperTypeOf(value.getClass())) {
ObjectWriter writer = genericWriters.get(rootType);
if (writer == null) {
// No cached writer for that type. Compute it once.
writer = genericWriters.computeIfAbsent(rootType, new Function<>() {
@Override
public ObjectWriter apply(JavaType type) {
return defaultWriter.forType(type);
}
});
}
return writer;
}
}

// no generic type given, or the generic type is just a class. Use the default writer.
return this.defaultWriter;
}

@Override
public void writeResponse(Object o, Type genericType, ServerRequestContext context)
throws WebApplicationException, IOException {
OutputStream stream = context.getOrCreateOutputStream();
if (o instanceof String) { // YUK: done in order to avoid adding extra quotes...
stream.write(((String) o).getBytes(StandardCharsets.UTF_8));
} else {
defaultWriter.writeValue(stream, o);
getWriter(genericType, o).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();
Expand All @@ -45,7 +78,7 @@ public void writeResponse(Object o, Type genericType, ServerRequestContext conte
@Override
public void writeTo(Object o, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType,
MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream) throws IOException, WebApplicationException {
doLegacyWrite(o, annotations, httpHeaders, entityStream, defaultWriter);
doLegacyWrite(o, annotations, httpHeaders, entityStream, getWriter(genericType, o));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,12 @@
import org.jboss.resteasy.reactive.server.spi.ServerMessageBodyWriter;
import org.jboss.resteasy.reactive.server.spi.ServerRequestContext;

import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;

import io.quarkus.resteasy.reactive.jackson.runtime.ResteasyReactiveServerJacksonRecorder;
import io.quarkus.resteasy.reactive.jackson.runtime.mappers.JacksonMapperUtil;

public class FullyFeaturedServerJacksonMessageBodyWriter extends ServerMessageBodyWriter.AllWriteableMessageBodyWriter {

Expand Down Expand Up @@ -76,6 +78,16 @@ public void writeResponse(Object o, Type genericType, ServerRequestContext conte

}
}
// make sure we properly handle polymorphism in generic collections
if (genericType != null && o != null) {
JavaType rootType = JacksonMapperUtil.getGenericRootType(genericType, effectiveWriter);
// Check that the determined root type is really assignable from the given entity.
// A mismatch can happen, if a ServerResponseFilter replaces the response entity with another object
// that does not match the original signature of the method (see HalServerResponseFilter for an example)
if (rootType != null && rootType.isTypeOrSuperTypeOf(o.getClass())) {
effectiveWriter = effectiveWriter.forType(rootType);
}
}
effectiveWriter.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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,10 @@ public static Type getEffectiveReturnType(Type returnType) {
if (rawType == CompletionStage.class) {
return getEffectiveReturnType(firstTypeArgument);
}
// do another check, using isAssignableFrom() instead of "==" to catch derived types such as "CompletableFuture" as well
if (rawType instanceof Class<?> rawClass && CompletionStage.class.isAssignableFrom(rawClass)) {
return getEffectiveReturnType(firstTypeArgument);
}
if (rawType == Uni.class) {
return getEffectiveReturnType(firstTypeArgument);
}
Expand Down

0 comments on commit b8415d8

Please sign in to comment.