diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java index f86b63e796e8d..a7670795d7dd0 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java @@ -94,6 +94,7 @@ import org.jboss.resteasy.reactive.server.core.ServerSerialisers; import org.jboss.resteasy.reactive.server.handlers.RestInitialHandler; import org.jboss.resteasy.reactive.server.model.ContextResolvers; +import org.jboss.resteasy.reactive.server.model.DelegatingServerRestHandler; import org.jboss.resteasy.reactive.server.model.DynamicFeatures; import org.jboss.resteasy.reactive.server.model.Features; import org.jboss.resteasy.reactive.server.model.HandlerChainCustomizer; @@ -109,6 +110,7 @@ import org.jboss.resteasy.reactive.server.processor.scanning.ResponseStatusMethodScanner; import org.jboss.resteasy.reactive.server.processor.util.ResteasyReactiveServerDotNames; import org.jboss.resteasy.reactive.server.spi.RuntimeConfiguration; +import org.jboss.resteasy.reactive.server.spi.ServerRestHandler; import org.jboss.resteasy.reactive.server.vertx.serializers.ServerMutinyAsyncFileMessageBodyWriter; import org.jboss.resteasy.reactive.server.vertx.serializers.ServerMutinyBufferMessageBodyWriter; import org.jboss.resteasy.reactive.server.vertx.serializers.ServerVertxAsyncFileMessageBodyWriter; @@ -180,6 +182,7 @@ import io.quarkus.resteasy.reactive.server.spi.HandlerConfigurationProviderBuildItem; import io.quarkus.resteasy.reactive.server.spi.MethodScannerBuildItem; import io.quarkus.resteasy.reactive.server.spi.NonBlockingReturnTypeBuildItem; +import io.quarkus.resteasy.reactive.server.spi.PreExceptionMapperHandlerBuildItem; import io.quarkus.resteasy.reactive.server.spi.ResumeOn404BuildItem; import io.quarkus.resteasy.reactive.spi.CustomExceptionMapperBuildItem; import io.quarkus.resteasy.reactive.spi.DynamicFeatureBuildItem; @@ -1098,6 +1101,7 @@ public void setupDeployment(BeanContainerBuildItem beanContainerBuildItem, HttpBuildTimeConfig vertxConfig, SetupEndpointsResultBuildItem setupEndpointsResult, ServerSerialisersBuildItem serverSerialisersBuildItem, + List preExceptionMapperHandlerBuildItems, List dynamicFeatures, List features, Optional requestContextFactoryBuildItem, @@ -1212,6 +1216,7 @@ public void setupDeployment(BeanContainerBuildItem beanContainerBuildItem, .setFactoryCreator(recorder.factoryCreator(beanContainerBuildItem.getValue())) .setDynamicFeatures(dynamicFeats) .setSerialisers(serialisers) + .setPreExceptionMapperHandler(determinePreExceptionMapperHandler(preExceptionMapperHandlerBuildItems)) .setApplicationPath(applicationPath) .setGlobalHandlerCustomizers(Collections.singletonList(new SecurityContextOverrideHandler.Customizer())) //TODO: should be pluggable .setResourceClasses(resourceClasses) @@ -1282,6 +1287,19 @@ public void setupDeployment(BeanContainerBuildItem beanContainerBuildItem, } } + private ServerRestHandler determinePreExceptionMapperHandler( + List preExceptionMapperHandlerBuildItems) { + if ((preExceptionMapperHandlerBuildItems == null) || preExceptionMapperHandlerBuildItems.isEmpty()) { + return null; + } + if (preExceptionMapperHandlerBuildItems.size() == 1) { + return preExceptionMapperHandlerBuildItems.get(0).getHandler(); + } + Collections.sort(preExceptionMapperHandlerBuildItems); + return new DelegatingServerRestHandler(preExceptionMapperHandlerBuildItems.stream() + .map(PreExceptionMapperHandlerBuildItem::getHandler).collect(toList())); + } + private static boolean notFoundCustomExMapper(String builtInExSignature, String builtInMapperSignature, ExceptionMapping exceptionMapping) { for (var entry : exceptionMapping.getMappers().entrySet()) { diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/PreExceptionMapperHandlerTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/PreExceptionMapperHandlerTest.java new file mode 100644 index 0000000000000..b55931a66c327 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/PreExceptionMapperHandlerTest.java @@ -0,0 +1,99 @@ +package io.quarkus.resteasy.reactive.server.test; + +import static io.restassured.RestAssured.get; +import static org.assertj.core.api.Assertions.*; + +import java.util.function.Consumer; +import java.util.function.Supplier; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Response; + +import org.jboss.resteasy.reactive.server.ServerExceptionMapper; +import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; +import org.jboss.resteasy.reactive.server.spi.ResteasyReactiveContainerRequestContext; +import org.jboss.resteasy.reactive.server.spi.ServerRestHandler; +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 io.quarkus.builder.BuildChainBuilder; +import io.quarkus.builder.BuildContext; +import io.quarkus.builder.BuildStep; +import io.quarkus.resteasy.reactive.server.spi.PreExceptionMapperHandlerBuildItem; +import io.quarkus.test.QuarkusUnitTest; +import io.smallrye.mutiny.Uni; + +public class PreExceptionMapperHandlerTest { + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest() + .setArchiveProducer(new Supplier<>() { + @Override + public JavaArchive get() { + return ShrinkWrap.create(JavaArchive.class) + .addClasses(Resource.class, Mappers.class, DummyPreExceptionMapperHandler.class); + } + }).addBuildChainCustomizer(new Consumer<>() { + @Override + public void accept(BuildChainBuilder buildChainBuilder) { + buildChainBuilder.addBuildStep(new BuildStep() { + @Override + public void execute(BuildContext context) { + context.produce( + new PreExceptionMapperHandlerBuildItem(new DummyPreExceptionMapperHandler())); + } + }).produces(PreExceptionMapperHandlerBuildItem.class).build(); + } + }); + + @Test + public void test() { + get("/test") + .then() + .statusCode(999) + .header("foo", "bar"); + + get("/test/uni") + .then() + .statusCode(999) + .header("foo", "bar"); + } + + @Path("test") + public static class Resource { + + @GET + public String get() { + throw new RuntimeException("dummy"); + } + + @Path("uni") + @GET + public Uni uniGet() { + return Uni.createFrom().item(() -> { + throw new RuntimeException("dummy"); + }); + } + } + + public static class Mappers { + + @ServerExceptionMapper(RuntimeException.class) + Response handle(ResteasyReactiveContainerRequestContext requestContext) { + return Response.status(999).header("foo", requestContext.getProperty("foo")).build(); + } + + } + + public static class DummyPreExceptionMapperHandler implements ServerRestHandler { + + @Override + public void handle(ResteasyReactiveRequestContext requestContext) throws Exception { + assertThat(requestContext.getThrowable()).isInstanceOf(RuntimeException.class); + requestContext.setProperty("foo", "bar"); + } + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/spi-deployment/src/main/java/io/quarkus/resteasy/reactive/server/spi/PreExceptionMapperHandlerBuildItem.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/spi-deployment/src/main/java/io/quarkus/resteasy/reactive/server/spi/PreExceptionMapperHandlerBuildItem.java new file mode 100644 index 0000000000000..ecb6b37bc9699 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/spi-deployment/src/main/java/io/quarkus/resteasy/reactive/server/spi/PreExceptionMapperHandlerBuildItem.java @@ -0,0 +1,42 @@ +package io.quarkus.resteasy.reactive.server.spi; + +import jakarta.ws.rs.Priorities; + +import org.jboss.resteasy.reactive.server.spi.ServerRestHandler; + +import io.quarkus.builder.item.MultiBuildItem; + +/** + * A build item that allows extension to define a {@link ServerRestHandler} that runs write before + * RESTEasy Reactive attempt to do exception mapping according to the JAX-RS spec. + * This is only meant to be used in very advanced use cases. + */ +public final class PreExceptionMapperHandlerBuildItem extends MultiBuildItem + implements Comparable { + + private final ServerRestHandler handler; + private final int priority; + + public PreExceptionMapperHandlerBuildItem(ServerRestHandler handler, int priority) { + this.handler = handler; + this.priority = priority; + } + + public PreExceptionMapperHandlerBuildItem(ServerRestHandler handler) { + this.handler = handler; + this.priority = Priorities.USER; + } + + @Override + public int compareTo(PreExceptionMapperHandlerBuildItem o) { + return Integer.compare(priority, o.priority); + } + + public ServerRestHandler getHandler() { + return handler; + } + + public int getPriority() { + return priority; + } +} diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/DeploymentInfo.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/DeploymentInfo.java index 043ba32cad7c2..1f7c97d13b485 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/DeploymentInfo.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/DeploymentInfo.java @@ -15,6 +15,7 @@ import org.jboss.resteasy.reactive.server.model.Features; import org.jboss.resteasy.reactive.server.model.HandlerChainCustomizer; import org.jboss.resteasy.reactive.server.model.ParamConverterProviders; +import org.jboss.resteasy.reactive.server.spi.ServerRestHandler; import org.jboss.resteasy.reactive.spi.BeanFactory; public class DeploymentInfo { @@ -25,6 +26,8 @@ public class DeploymentInfo { private Features features; private DynamicFeatures dynamicFeatures; private ServerSerialisers serialisers; + + private ServerRestHandler preExceptionMapperHandler; private List resourceClasses; private List locatableResourceClasses; private ParamConverterProviders paramConverterProviders; @@ -91,6 +94,15 @@ public DeploymentInfo setSerialisers(ServerSerialisers serialisers) { return this; } + public ServerRestHandler getPreExceptionMapperHandler() { + return preExceptionMapperHandler; + } + + public DeploymentInfo setPreExceptionMapperHandler(ServerRestHandler preExceptionMapperHandler) { + this.preExceptionMapperHandler = preExceptionMapperHandler; + return this; + } + public List getResourceClasses() { return resourceClasses; } diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeDeploymentManager.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeDeploymentManager.java index fb3e64b16110d..6b254608a29a2 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeDeploymentManager.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeDeploymentManager.java @@ -198,6 +198,9 @@ public BeanFactory.BeanInstance apply(Class aClass) { if (interceptorDeployment.getGlobalInterceptorHandler() != null) { abortHandlingChain.add(interceptorDeployment.getGlobalInterceptorHandler()); } + if (info.getPreExceptionMapperHandler() != null) { + abortHandlingChain.add(info.getPreExceptionMapperHandler()); + } abortHandlingChain.add(new ExceptionHandler()); abortHandlingChain.add(ResponseHandler.NO_CUSTOMIZER_INSTANCE); if (!interceptors.getContainerResponseFilters().getGlobalResourceInterceptors().isEmpty()) { 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 a03c161fa1106..f3ef562e331b5 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 @@ -197,7 +197,8 @@ public RuntimeResource buildResourceMethod(ResourceClass clazz, //setup reader and writer interceptors first ServerRestHandler interceptorHandler = interceptorDeployment.setupInterceptorHandler(); //we want interceptors in the abort handler chain - List abortHandlingChain = new ArrayList<>(3 + (interceptorHandler != null ? 1 : 0)); + List abortHandlingChain = new ArrayList<>( + 3 + (interceptorHandler != null ? 1 : 0) + (info.getPreExceptionMapperHandler() != null ? 1 : 0)); List handlers = new ArrayList<>(HANDLERS_CAPACITY); // we add null as the first item to make sure that subsequent items are added in the proper positions @@ -487,6 +488,9 @@ public RuntimeResource buildResourceMethod(ResourceClass clazz, // so we can invoke it abortHandlingChain.add(instanceHandler); } + if (info.getPreExceptionMapperHandler() != null) { + abortHandlingChain.add(info.getPreExceptionMapperHandler()); + } abortHandlingChain.add(ExceptionHandler.INSTANCE); abortHandlingChain.add(ResponseHandler.NO_CUSTOMIZER_INSTANCE); abortHandlingChain.addAll(responseFilterHandlers); diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/model/DelegatingServerRestHandler.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/model/DelegatingServerRestHandler.java new file mode 100644 index 0000000000000..a074f126513f1 --- /dev/null +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/model/DelegatingServerRestHandler.java @@ -0,0 +1,34 @@ +package org.jboss.resteasy.reactive.server.model; + +import java.util.List; + +import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; +import org.jboss.resteasy.reactive.server.spi.ServerRestHandler; + +public class DelegatingServerRestHandler implements ServerRestHandler { + + private List delegates; + + public DelegatingServerRestHandler(List delegates) { + this.delegates = delegates; + } + + // for bytecode recording + public DelegatingServerRestHandler() { + } + + public List getDelegates() { + return delegates; + } + + public void setDelegates(List delegates) { + this.delegates = delegates; + } + + @Override + public void handle(ResteasyReactiveRequestContext requestContext) throws Exception { + for (int i = 0; i < delegates.size(); i++) { + delegates.get(0).handle(requestContext); + } + } +}