From 538b3b71230dd110823a4538fd2856ea3628b6f6 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Thu, 23 Sep 2021 19:12:05 +0300 Subject: [PATCH] Add the exception unwrapping capabilities to RESTEasy Reactive Fixes: #20357 --- .../ResteasyReactiveScanningProcessor.java | 2 + .../UnwrappedExceptionTest.java | 88 +++++++++++++++++++ .../server/core/ExceptionMapping.java | 67 ++++++++++---- .../reactive/server/jaxrs/ProvidersImpl.java | 9 +- 4 files changed, 146 insertions(+), 20 deletions(-) create mode 100644 extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/customexceptions/UnwrappedExceptionTest.java diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveScanningProcessor.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveScanningProcessor.java index 5be00deb77e53..ef21c0b76abf3 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveScanningProcessor.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveScanningProcessor.java @@ -42,6 +42,7 @@ import org.jboss.resteasy.reactive.server.processor.scanning.ResteasyReactiveFeatureScanner; import org.jboss.resteasy.reactive.server.processor.scanning.ResteasyReactiveParamConverterScanner; +import io.quarkus.arc.ArcUndeclaredThrowableException; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.arc.deployment.GeneratedBeanBuildItem; import io.quarkus.arc.deployment.GeneratedBeanGizmoAdaptor; @@ -109,6 +110,7 @@ public ExceptionMappersBuildItem scanForExceptionMappers(CombinedIndexBuildItem exceptions.addBlockingProblem(BlockingOperationNotAllowedException.class); exceptions.addBlockingProblem(BlockingNotAllowedException.class); + exceptions.addUnwrappedException(ArcUndeclaredThrowableException.class); if (capabilities.isPresent(Capability.HIBERNATE_REACTIVE)) { exceptions.addNonBlockingProblem( new ExceptionMapping.ExceptionTypeAndMessageContainsPredicate(IllegalStateException.class, "HR000068")); diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/customexceptions/UnwrappedExceptionTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/customexceptions/UnwrappedExceptionTest.java new file mode 100644 index 0000000000000..44325f8175fa3 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/customexceptions/UnwrappedExceptionTest.java @@ -0,0 +1,88 @@ +package io.quarkus.resteasy.reactive.server.test.customexceptions; + +import static io.quarkus.resteasy.reactive.server.test.ExceptionUtil.removeStackTrace; + +import java.util.function.Supplier; + +import javax.ws.rs.Path; +import javax.ws.rs.core.Response; + +import org.jboss.resteasy.reactive.server.ServerExceptionMapper; +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.arc.ArcUndeclaredThrowableException; +import io.quarkus.resteasy.reactive.server.test.ExceptionUtil; +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class UnwrappedExceptionTest { + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest() + .setArchiveProducer(new Supplier<>() { + @Override + public JavaArchive get() { + return ShrinkWrap.create(JavaArchive.class) + .addClasses(ExceptionResource.class, ExceptionMappers.class, ExceptionUtil.class); + } + }); + + @Test + public void testWrapperWithUnmappedException() { + RestAssured.get("/hello/wrapperOfIAE") + .then().statusCode(500); + } + + @Test + public void testWrapperWithMappedException() { + RestAssured.get("/hello/wrapperOfISE") + .then().statusCode(999); + } + + @Test + public void testUnmappedException() { + RestAssured.get("/hello/iae") + .then().statusCode(500); + } + + @Test + public void testMappedException() { + RestAssured.get("/hello/ise") + .then().statusCode(999); + } + + @Path("hello") + public static class ExceptionResource { + + @Path("wrapperOfIAE") + public String wrapperOfIAE() { + throw removeStackTrace(new ArcUndeclaredThrowableException(removeStackTrace(new IllegalArgumentException()))); + } + + @Path("wrapperOfISE") + public String wrapperOfISE() { + throw removeStackTrace(new ArcUndeclaredThrowableException(removeStackTrace(new IllegalStateException()))); + } + + @Path("iae") + public String iae() { + throw removeStackTrace(new IllegalArgumentException()); + } + + @Path("ise") + public String ise() { + throw removeStackTrace(new IllegalStateException()); + } + } + + public static class ExceptionMappers { + + @ServerExceptionMapper + Response mapISE(IllegalStateException e) { + return Response.status(999).build(); + } + } +} diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/ExceptionMapping.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/ExceptionMapping.java index 7be4aa24fe6cf..f1d0d44ea47d6 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/ExceptionMapping.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/ExceptionMapping.java @@ -3,12 +3,15 @@ import io.smallrye.common.annotation.Blocking; import io.smallrye.common.annotation.NonBlocking; import java.io.IOException; +import java.util.AbstractMap; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -36,6 +39,7 @@ public class ExceptionMapping { */ private final List> blockingProblemPredicates = new ArrayList<>(); private final List> nonBlockingProblemPredicate = new ArrayList<>(); + private final Set> unwrappedExceptions = new HashSet<>(); @SuppressWarnings({ "unchecked", "rawtypes" }) public void mapException(Throwable throwable, ResteasyReactiveRequestContext context) { @@ -54,23 +58,26 @@ public void mapException(Throwable throwable, ResteasyReactiveRequestContext con } // we match superclasses only if not a WebApplicationException according to spec 3.3.4 Exceptions - ExceptionMapper exceptionMapper = getExceptionMapper((Class) klass, context); - if (exceptionMapper != null) { + Map.Entry> entry = getExceptionMapper((Class) klass, context, + throwable); + if (entry != null) { + ExceptionMapper exceptionMapper = entry.getValue(); + Throwable mappedException = entry.getKey(); context.requireCDIRequestScope(); if (exceptionMapper instanceof ResteasyReactiveAsyncExceptionMapper) { - ((ResteasyReactiveAsyncExceptionMapper) exceptionMapper).asyncResponse(throwable, + ((ResteasyReactiveAsyncExceptionMapper) exceptionMapper).asyncResponse(mappedException, new AsyncExceptionMapperContextImpl(context)); - logBlockingErrorIfRequired(throwable, context); - logNonBlockingErrorIfRequired(throwable, context); + logBlockingErrorIfRequired(mappedException, context); + logNonBlockingErrorIfRequired(mappedException, context); return; } else if (exceptionMapper instanceof ResteasyReactiveExceptionMapper) { - response = ((ResteasyReactiveExceptionMapper) exceptionMapper).toResponse(throwable, context); + response = ((ResteasyReactiveExceptionMapper) exceptionMapper).toResponse(mappedException, context); } else { - response = exceptionMapper.toResponse(throwable); + response = exceptionMapper.toResponse(mappedException); } context.setResult(response); - logBlockingErrorIfRequired(throwable, context); - logNonBlockingErrorIfRequired(throwable, context); + logBlockingErrorIfRequired(mappedException, context); + logNonBlockingErrorIfRequired(mappedException, context); return; } if (isWebApplicationException) { @@ -142,20 +149,26 @@ private boolean isKnownProblem(Throwable throwable, List> p } /** - * Return the proper Exception that handles {@param throwable} or {@code null} + * Return the proper Exception that handles {@param clazz} or {@code null} * if none is found. - * First checks if the Resource class that contained the Resource method contained class-level exception mappers + * First checks if the Resource class that contained the Resource method contained class-level exception mappers. + * {@param throwable} is optional and is used to when no mapper has been found for the original exception type, but the + * application + * has been configured to unwrap certain exceptions. */ - public ExceptionMapper getExceptionMapper(Class clazz, ResteasyReactiveRequestContext context) { + public Map.Entry> getExceptionMapper(Class clazz, + ResteasyReactiveRequestContext context, + T throwable) { Map, ResourceExceptionMapper> classExceptionMappers = getClassExceptionMappers( context); if ((classExceptionMappers != null) && !classExceptionMappers.isEmpty()) { - ExceptionMapper result = doGetExceptionMapper(clazz, classExceptionMappers); + Map.Entry> result = doGetExceptionMapper(clazz, + classExceptionMappers, throwable); if (result != null) { return result; } } - return doGetExceptionMapper(clazz, mappers); + return doGetExceptionMapper(clazz, mappers, throwable); } private Map, ResourceExceptionMapper> getClassExceptionMappers( @@ -166,19 +179,27 @@ private Map, ResourceExceptionMapper ExceptionMapper doGetExceptionMapper(Class clazz, - Map, ResourceExceptionMapper> mappers) { + @SuppressWarnings({ "unchecked", "rawtypes" }) + private Map.Entry> doGetExceptionMapper( + Class clazz, + Map, ResourceExceptionMapper> mappers, + Throwable throwable) { Class klass = clazz; do { ResourceExceptionMapper mapper = mappers.get(klass); if (mapper != null) { - return (ExceptionMapper) mapper.getFactory() - .createInstance().getInstance(); + return new AbstractMap.SimpleEntry(throwable, mapper.getFactory() + .createInstance().getInstance()); } klass = klass.getSuperclass(); } while (klass != null); + if ((throwable != null) && unwrappedExceptions.contains(clazz)) { + Throwable cause = throwable.getCause(); + if (cause != null) { + return doGetExceptionMapper(cause.getClass(), mappers, cause); + } + } return null; } @@ -198,6 +219,10 @@ public void addNonBlockingProblem(Predicate predicate) { nonBlockingProblemPredicate.add(predicate); } + public void addUnwrappedException(Class clazz) { + unwrappedExceptions.add(clazz); + } + public void addExceptionMapper(Class exceptionClass, ResourceExceptionMapper mapper) { ResourceExceptionMapper existing = mappers.get(exceptionClass); if (existing != null) { @@ -221,6 +246,10 @@ public List> getNonBlockingProblemPredicate() { return nonBlockingProblemPredicate; } + public Set> getUnwrappedExceptions() { + return unwrappedExceptions; + } + public void initializeDefaultFactories(Function> factoryCreator) { for (Map.Entry, ResourceExceptionMapper> entry : mappers.entrySet()) { if (entry.getValue().getFactory() == null) { diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/jaxrs/ProvidersImpl.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/jaxrs/ProvidersImpl.java index f8ef7cb587560..3da74a5a65ab6 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/jaxrs/ProvidersImpl.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/jaxrs/ProvidersImpl.java @@ -3,6 +3,7 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Type; import java.util.List; +import java.util.Map; import javax.ws.rs.RuntimeType; import javax.ws.rs.core.MediaType; import javax.ws.rs.ext.ContextResolver; @@ -44,9 +45,15 @@ public MessageBodyWriter getMessageBodyWriter(Class type, Type generic return null; } + @SuppressWarnings("unchecked") @Override public ExceptionMapper getExceptionMapper(Class type) { - return deployment.getExceptionMapping().getExceptionMapper(type, null); + Map.Entry> entry = deployment.getExceptionMapping() + .getExceptionMapper(type, null, null); + if (entry != null) { + return (ExceptionMapper) entry.getValue(); + } + return null; } @Override