diff --git a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/providers/ContainerResponseFilterContentTypeNoResolutionTest.java b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/providers/ContainerResponseFilterContentTypeNoResolutionTest.java new file mode 100644 index 0000000000000..c1b1e1e2f0e87 --- /dev/null +++ b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/providers/ContainerResponseFilterContentTypeNoResolutionTest.java @@ -0,0 +1,60 @@ +package io.quarkus.resteasy.reactive.server.test.providers; + +import java.io.IOException; + +import jakarta.annotation.Priority; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.container.ContainerResponseContext; +import jakarta.ws.rs.container.ContainerResponseFilter; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.Provider; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class ContainerResponseFilterContentTypeNoResolutionTest { + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest().withApplicationRoot( + jar -> jar.addClasses(MediaTypeContainerResponseFilter.class, TestResource.class)); + + @Test + public void producesMediaTypePresentInWriterInterceptor() { + RestAssured + .given().accept("text/*") + .when().get("/test").then().statusCode(406); + } + + @Path("/test") + public static class TestResource { + + @GET + @Produces("text/*") + public Response hello() { + Greeting greeting = new Greeting("Hello"); + return Response.ok(greeting).build(); + } + } + + public record Greeting(String message) { + } + + @Priority(5000) + @Provider + public static class MediaTypeContainerResponseFilter implements ContainerResponseFilter { + + @Override + public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) + throws IOException { + if (responseContext.getMediaType() != null) { + throw new IllegalStateException( + "MediaType shouldn't have been resolved but got: " + responseContext.getMediaType()); + } + } + } +} diff --git a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/providers/ContainerResponseFilterContentTypeOverrideTest.java b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/providers/ContainerResponseFilterContentTypeOverrideTest.java new file mode 100644 index 0000000000000..627246b0e45d1 --- /dev/null +++ b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/providers/ContainerResponseFilterContentTypeOverrideTest.java @@ -0,0 +1,61 @@ +package io.quarkus.resteasy.reactive.server.test.providers; + +import java.io.IOException; + +import jakarta.annotation.Priority; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.container.ContainerResponseContext; +import jakarta.ws.rs.container.ContainerResponseFilter; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.Provider; + +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class ContainerResponseFilterContentTypeOverrideTest { + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest().withApplicationRoot( + jar -> jar.addClasses(MediaTypeContainerResponseFilter.class, TestResource.class)); + + @Test + public void producesMediaTypePresentInWriterInterceptor() { + RestAssured.when().get("/test").then().body(Matchers.containsString("Hello")); + } + + @Path("/test") + public static class TestResource { + + @GET + @Produces(MediaType.TEXT_XML) + public Response hello() { + Greeting greeting = new Greeting("Hello"); + return Response.ok(greeting).type(MediaType.TEXT_PLAIN).build(); + } + } + + public record Greeting(String message) { + } + + @Priority(5000) + @Provider + public static class MediaTypeContainerResponseFilter implements ContainerResponseFilter { + + @Override + public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) + throws IOException { + if (!responseContext.getMediaType().isCompatible(MediaType.TEXT_PLAIN_TYPE)) { + throw new IllegalStateException( + "MediaType was not overridden by Response, got: " + responseContext.getMediaType() + + " instead of expected: " + MediaType.TEXT_PLAIN); + } + } + } +} diff --git a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/providers/ContainerResponseFilterContentTypeTest.java b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/providers/ContainerResponseFilterContentTypeTest.java new file mode 100644 index 0000000000000..7c290278784e4 --- /dev/null +++ b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/providers/ContainerResponseFilterContentTypeTest.java @@ -0,0 +1,60 @@ +package io.quarkus.resteasy.reactive.server.test.providers; + +import java.io.IOException; + +import jakarta.annotation.Priority; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.container.ContainerResponseContext; +import jakarta.ws.rs.container.ContainerResponseFilter; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.Provider; + +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class ContainerResponseFilterContentTypeTest { + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest().withApplicationRoot( + jar -> jar.addClasses(MediaTypeContainerResponseFilter.class, TestResource.class)); + + @Test + public void producesMediaTypePresentInWriterInterceptor() { + RestAssured.when().get("/test").then().body(Matchers.containsString("Hello")); + } + + @Path("/test") + public static class TestResource { + + @GET + @Produces(MediaType.TEXT_XML) + public Response hello() { + Greeting greeting = new Greeting("Hello"); + return Response.ok(greeting).build(); + } + } + + public record Greeting(String message) { + } + + @Priority(5000) + @Provider + public static class MediaTypeContainerResponseFilter implements ContainerResponseFilter { + + @Override + public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) + throws IOException { + if (!responseContext.getMediaType().isCompatible(MediaType.TEXT_XML_TYPE)) { + throw new IllegalStateException("MediaType was not provided, got: " + responseContext.getMediaType() + + " instead of expected: " + MediaType.TEXT_XML); + } + } + } +} diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeResourceDeployment.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeResourceDeployment.java index 2bc5999117537..ebb44b796a686 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeResourceDeployment.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeResourceDeployment.java @@ -70,6 +70,7 @@ import org.jboss.resteasy.reactive.server.handlers.BlockingHandler; import org.jboss.resteasy.reactive.server.handlers.ExceptionHandler; import org.jboss.resteasy.reactive.server.handlers.FixedProducesHandler; +import org.jboss.resteasy.reactive.server.handlers.FixedProducesSetDefaultContentTypeResponseHandler; import org.jboss.resteasy.reactive.server.handlers.FormBodyHandler; import org.jboss.resteasy.reactive.server.handlers.InputHandler; import org.jboss.resteasy.reactive.server.handlers.InstanceHandler; @@ -85,6 +86,7 @@ import org.jboss.resteasy.reactive.server.handlers.ResponseWriterHandler; import org.jboss.resteasy.reactive.server.handlers.SseResponseWriterHandler; import org.jboss.resteasy.reactive.server.handlers.VariableProducesHandler; +import org.jboss.resteasy.reactive.server.handlers.VariableProducesSetDefaultContentTypeResponseHandler; import org.jboss.resteasy.reactive.server.mapping.RuntimeResource; import org.jboss.resteasy.reactive.server.mapping.URITemplate; import org.jboss.resteasy.reactive.server.model.HandlerChainCustomizer; @@ -472,6 +474,18 @@ public RuntimeResource buildResourceMethod(ResourceClass clazz, : ScoreSystem.Diagnostic.WriterNotRequired); } } else { + if (method.getProduces() != null && method.getProduces().length > 0) { + if (method.getProduces().length == 1) { + MediaType mediaType = MediaType.valueOf(method.getProduces()[0]); + if (mediaType.isWildcardType() || mediaType.isWildcardSubtype()) { + handlers.add(new VariableProducesSetDefaultContentTypeResponseHandler(serverMediaType)); + } else { + handlers.add(new FixedProducesSetDefaultContentTypeResponseHandler(mediaType)); + } + } else { + handlers.add(new VariableProducesSetDefaultContentTypeResponseHandler(serverMediaType)); + } + } score.add(ScoreSystem.Category.Writer, ScoreSystem.Diagnostic.WriterRunTime); } diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/FixedProducesSetDefaultContentTypeResponseHandler.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/FixedProducesSetDefaultContentTypeResponseHandler.java new file mode 100644 index 0000000000000..7f2ace85ffb6e --- /dev/null +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/handlers/FixedProducesSetDefaultContentTypeResponseHandler.java @@ -0,0 +1,57 @@ +package org.jboss.resteasy.reactive.server.handlers; + +import java.util.List; +import java.util.Locale; + +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.MediaType; + +import org.jboss.resteasy.reactive.server.core.EncodedMediaType; +import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; +import org.jboss.resteasy.reactive.server.spi.ServerRestHandler; + +/** + * Handler that defines the default content type when a Response is returned. + * While it might not be the final content type, we still need to make sure + * the default content type is provided to {@code ContainerResponseFilter}. + *
+ * This particular one is for endpoints that only produce one content type.
+ */
+@SuppressWarnings("ForLoopReplaceableByForEach")
+public class FixedProducesSetDefaultContentTypeResponseHandler implements ServerRestHandler {
+
+ private final EncodedMediaType mediaType;
+ private final String mediaTypeString;
+ private final String mediaTypeSubstring;
+
+ public FixedProducesSetDefaultContentTypeResponseHandler(MediaType mediaType) {
+ this.mediaType = new EncodedMediaType(mediaType);
+ this.mediaTypeString = mediaType.getType() + "/" + mediaType.getSubtype();
+ this.mediaTypeSubstring = mediaType.getType() + "/*";
+ }
+
+ @Override
+ public void handle(ResteasyReactiveRequestContext requestContext) throws Exception {
+ List
+ * This particular one negotiates the content type when there are multiple ones defined.
+ */
+public class VariableProducesSetDefaultContentTypeResponseHandler implements ServerRestHandler {
+
+ private final ServerMediaType mediaTypeList;
+
+ public VariableProducesSetDefaultContentTypeResponseHandler(ServerMediaType mediaTypeList) {
+ this.mediaTypeList = mediaTypeList;
+ }
+
+ @Override
+ public void handle(ResteasyReactiveRequestContext requestContext) throws Exception {
+ MediaType res = null;
+ List