Skip to content

Commit

Permalink
Add the exception unwrapping capabilities to RESTEasy Reactive
Browse files Browse the repository at this point in the history
  • Loading branch information
geoand committed Sep 23, 2021
1 parent d3ed65b commit 538b3b7
Show file tree
Hide file tree
Showing 4 changed files with 146 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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"));
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -36,6 +39,7 @@ public class ExceptionMapping {
*/
private final List<Predicate<Throwable>> blockingProblemPredicates = new ArrayList<>();
private final List<Predicate<Throwable>> nonBlockingProblemPredicate = new ArrayList<>();
private final Set<Class<? extends Throwable>> unwrappedExceptions = new HashSet<>();

@SuppressWarnings({ "unchecked", "rawtypes" })
public void mapException(Throwable throwable, ResteasyReactiveRequestContext context) {
Expand All @@ -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<Throwable>) klass, context);
if (exceptionMapper != null) {
Map.Entry<Throwable, ExceptionMapper<? extends Throwable>> entry = getExceptionMapper((Class<Throwable>) 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) {
Expand Down Expand Up @@ -142,20 +149,26 @@ private boolean isKnownProblem(Throwable throwable, List<Predicate<Throwable>> 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 <T extends Throwable> ExceptionMapper<T> getExceptionMapper(Class<T> clazz, ResteasyReactiveRequestContext context) {
public <T extends Throwable> Map.Entry<Throwable, ExceptionMapper<? extends Throwable>> getExceptionMapper(Class<T> clazz,
ResteasyReactiveRequestContext context,
T throwable) {
Map<Class<? extends Throwable>, ResourceExceptionMapper<? extends Throwable>> classExceptionMappers = getClassExceptionMappers(
context);
if ((classExceptionMappers != null) && !classExceptionMappers.isEmpty()) {
ExceptionMapper<T> result = doGetExceptionMapper(clazz, classExceptionMappers);
Map.Entry<Throwable, ExceptionMapper<? extends Throwable>> result = doGetExceptionMapper(clazz,
classExceptionMappers, throwable);
if (result != null) {
return result;
}
}
return doGetExceptionMapper(clazz, mappers);
return doGetExceptionMapper(clazz, mappers, throwable);
}

private Map<Class<? extends Throwable>, ResourceExceptionMapper<? extends Throwable>> getClassExceptionMappers(
Expand All @@ -166,19 +179,27 @@ private Map<Class<? extends Throwable>, ResourceExceptionMapper<? extends Throwa
return context.getTarget() != null ? context.getTarget().getClassExceptionMappers() : null;
}

@SuppressWarnings("unchecked")
private <T extends Throwable> ExceptionMapper<T> doGetExceptionMapper(Class<T> clazz,
Map<Class<? extends Throwable>, ResourceExceptionMapper<? extends Throwable>> mappers) {
@SuppressWarnings({ "unchecked", "rawtypes" })
private <T extends Throwable> Map.Entry<Throwable, ExceptionMapper<? extends Throwable>> doGetExceptionMapper(
Class<T> clazz,
Map<Class<? extends Throwable>, ResourceExceptionMapper<? extends Throwable>> mappers,
Throwable throwable) {
Class<?> klass = clazz;
do {
ResourceExceptionMapper<? extends Throwable> mapper = mappers.get(klass);
if (mapper != null) {
return (ExceptionMapper<T>) 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;
}

Expand All @@ -198,6 +219,10 @@ public void addNonBlockingProblem(Predicate<Throwable> predicate) {
nonBlockingProblemPredicate.add(predicate);
}

public void addUnwrappedException(Class<? extends Throwable> clazz) {
unwrappedExceptions.add(clazz);
}

public <T extends Throwable> void addExceptionMapper(Class<T> exceptionClass, ResourceExceptionMapper<T> mapper) {
ResourceExceptionMapper<? extends Throwable> existing = mappers.get(exceptionClass);
if (existing != null) {
Expand All @@ -221,6 +246,10 @@ public List<Predicate<Throwable>> getNonBlockingProblemPredicate() {
return nonBlockingProblemPredicate;
}

public Set<Class<? extends Throwable>> getUnwrappedExceptions() {
return unwrappedExceptions;
}

public void initializeDefaultFactories(Function<String, BeanFactory<?>> factoryCreator) {
for (Map.Entry<Class<? extends Throwable>, ResourceExceptionMapper<? extends Throwable>> entry : mappers.entrySet()) {
if (entry.getValue().getFactory() == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -44,9 +45,15 @@ public <T> MessageBodyWriter<T> getMessageBodyWriter(Class<T> type, Type generic
return null;
}

@SuppressWarnings("unchecked")
@Override
public <T extends Throwable> ExceptionMapper<T> getExceptionMapper(Class<T> type) {
return deployment.getExceptionMapping().getExceptionMapper(type, null);
Map.Entry<Throwable, ExceptionMapper<? extends Throwable>> entry = deployment.getExceptionMapping()
.getExceptionMapper(type, null, null);
if (entry != null) {
return (ExceptionMapper<T>) entry.getValue();
}
return null;
}

@Override
Expand Down

0 comments on commit 538b3b7

Please sign in to comment.