Skip to content

Commit

Permalink
Take conditional annotation into account for @ServerExceptionMapper
Browse files Browse the repository at this point in the history
Closes: #29043
  • Loading branch information
geoand committed Dec 5, 2022
1 parent 1a5dd2a commit 17eecf2
Show file tree
Hide file tree
Showing 11 changed files with 436 additions and 17 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package io.quarkus.resteasy.reactive.spi;

import org.jboss.jandex.ClassInfo;

import io.quarkus.builder.item.MultiBuildItem;

public final class ExceptionMapperBuildItem extends MultiBuildItem implements CheckBean {
Expand All @@ -8,19 +10,22 @@ public final class ExceptionMapperBuildItem extends MultiBuildItem implements Ch
private final Integer priority;
private final String handledExceptionName;
private final boolean registerAsBean;
private final ClassInfo declaringClass;

public ExceptionMapperBuildItem(String className, String handledExceptionName, Integer priority, boolean registerAsBean) {
this.className = className;
this.priority = priority;
this.handledExceptionName = handledExceptionName;
this.registerAsBean = registerAsBean;
this.declaringClass = null;
}

private ExceptionMapperBuildItem(Builder builder) {
this.className = builder.className;
this.handledExceptionName = builder.handledExceptionName;
this.priority = builder.priority;
this.registerAsBean = builder.registerAsBean;
this.declaringClass = builder.declaringClass;
}

public String getClassName() {
Expand All @@ -40,13 +45,23 @@ public boolean isRegisterAsBean() {
return registerAsBean;
}

public ClassInfo getDeclaringClass() {
return declaringClass;
}

public static class Builder {
private final String className;
private final String handledExceptionName;

private Integer priority;
private boolean registerAsBean = true;

/**
* Used to track the class that resulted in the registration of the exception mapper.
* This is only set for exception mappers created from {@code @ServerExceptionMapper}
*/
private ClassInfo declaringClass;

public Builder(String className, String handledExceptionName) {
this.className = className;
this.handledExceptionName = handledExceptionName;
Expand All @@ -62,6 +77,11 @@ public Builder setRegisterAsBean(boolean registerAsBean) {
return this;
}

public Builder setDeclaringClass(ClassInfo declaringClass) {
this.declaringClass = declaringClass;
return this;
}

public ExceptionMapperBuildItem build() {
return new ExceptionMapperBuildItem(this);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1106,8 +1106,9 @@ public void setupDeployment(BeanContainerBuildItem beanContainerBuildItem,
Function<String, BeanFactory<?>> factoryFunction = s -> FactoryUtils.factory(s, singletonClasses, recorder,
beanContainerBuildItem);
interceptors.initializeDefaultFactories(factoryFunction);
exceptionMapping.initializeDefaultFactories(factoryFunction);
contextResolvers.initializeDefaultFactories(factoryFunction);
exceptionMapping.initializeDefaultFactories(factoryFunction);
exceptionMapping.replaceDiscardAtRuntimeIfBeanIsUnavailable(className -> recorder.beanUnavailable(className));

paramConverterProviders.initializeDefaultFactories(factoryFunction);
paramConverterProviders.sort();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,6 @@ public List<UnwrappedExceptionBuildItem> defaultUnwrappedException() {
new UnwrappedExceptionBuildItem(RollbackException.class));
}

@SuppressWarnings({ "unchecked", "rawtypes" })
@BuildStep
public ExceptionMappersBuildItem scanForExceptionMappers(CombinedIndexBuildItem combinedIndexBuildItem,
ApplicationResultBuildItem applicationResultBuildItem,
Expand Down Expand Up @@ -163,12 +162,32 @@ public ExceptionMappersBuildItem scanForExceptionMappers(CombinedIndexBuildItem
ResourceExceptionMapper<Throwable> mapper = new ResourceExceptionMapper<>();
mapper.setPriority(priority);
mapper.setClassName(additionalExceptionMapper.getClassName());
addRuntimeCheckIfNecessary(additionalExceptionMapper, mapper);
exceptions.addExceptionMapper(additionalExceptionMapper.getHandledExceptionName(), mapper);
}
additionalBeanBuildItemBuildProducer.produce(beanBuilder.build());
return new ExceptionMappersBuildItem(exceptions);
}

private static void addRuntimeCheckIfNecessary(ExceptionMapperBuildItem additionalExceptionMapper,
ResourceExceptionMapper<Throwable> mapper) {
ClassInfo declaringClass = additionalExceptionMapper.getDeclaringClass();
if (declaringClass != null) {
boolean needsRuntimeCheck = false;
List<AnnotationInstance> classAnnotations = declaringClass.declaredAnnotations();
for (AnnotationInstance classAnnotation : classAnnotations) {
if (CONDITIONAL_BEAN_ANNOTATIONS.contains(classAnnotation.name())) {
needsRuntimeCheck = true;
break;
}
}
if (needsRuntimeCheck) {
mapper.setDiscardAtRuntime(new ResourceExceptionMapper.DiscardAtRuntimeIfBeanIsUnavailable(
declaringClass.name().toString()));
}
}
}

@BuildStep
public ParamConverterProvidersBuildItem scanForParamConverters(
BuildProducer<AdditionalBeanBuildItem> additionalBeanBuildItemBuildProducer,
Expand Down Expand Up @@ -387,10 +406,31 @@ public void handleCustomAnnotatedMethods(
additionalBeans.addBeanClass(methodInfo.declaringClass().name().toString());
Map<String, String> generatedClassNames = ServerExceptionMapperGenerator.generateGlobalMapper(methodInfo,
new GeneratedBeanGizmoAdaptor(generatedBean),
Set.of(HTTP_SERVER_REQUEST, HTTP_SERVER_RESPONSE, ROUTING_CONTEXT), Set.of(Unremovable.class.getName()));
Set.of(HTTP_SERVER_REQUEST, HTTP_SERVER_RESPONSE, ROUTING_CONTEXT), Set.of(Unremovable.class.getName()),
(m -> {
List<AnnotationInstance> methodAnnotations = m.annotations();
for (AnnotationInstance methodAnnotation : methodAnnotations) {
if (CONDITIONAL_BEAN_ANNOTATIONS.contains(methodAnnotation.name())) {
throw new RuntimeException(
"The combination of '@" + methodAnnotation.name().withoutPackagePrefix()
+ "' and '@ServerExceptionMapper' is not allowed. Offending method is '"
+ m.name() + "' of class '" + m.declaringClass().name() + "'");
}
}

List<AnnotationInstance> classAnnotations = m.declaringClass().declaredAnnotations();
for (AnnotationInstance classAnnotation : classAnnotations) {
if (CONDITIONAL_BEAN_ANNOTATIONS.contains(classAnnotation.name())) {
return true;
}
}
return false;
}));
for (Map.Entry<String, String> entry : generatedClassNames.entrySet()) {
ExceptionMapperBuildItem.Builder builder = new ExceptionMapperBuildItem.Builder(entry.getValue(),
entry.getKey()).setRegisterAsBean(false);// it has already been made a bean
entry.getKey())
.setRegisterAsBean(false) // it has already been made a bean
.setDeclaringClass(methodInfo.declaringClass()); // we'll use this later on
AnnotationValue priorityValue = instance.value("priority");
if (priorityValue != null) {
builder.setPriority(priorityValue.asInt());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package io.quarkus.resteasy.reactive.server.test.customexceptions;

import static io.restassured.RestAssured.*;

import java.util.function.Supplier;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Priorities;
import javax.ws.rs.core.Response;

import org.jboss.resteasy.reactive.RestResponse;
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.lookup.LookupUnlessProperty;
import io.quarkus.arc.profile.IfBuildProfile;
import io.quarkus.test.QuarkusUnitTest;
import io.smallrye.mutiny.Uni;

public class ConditionalExceptionMappersTest {

@RegisterExtension
static QuarkusUnitTest test = new QuarkusUnitTest()
.setArchiveProducer(new Supplier<>() {
@Override
public JavaArchive get() {
return ShrinkWrap.create(JavaArchive.class)
.addClasses(AbstractException.class, FirstException.class, SecondException.class,
WontBeEnabledMappers.class, WillBeEnabledMappers.class, AlwaysEnabledMappers.class,
TestResource.class);
}
});

@Test
public void test() {
get("/first").then().statusCode(903);
get("/second").then().statusCode(801);
get("/third").then().statusCode(555);
}

@Path("")
public static class TestResource {

@Path("first")
@GET
public String first() {
throw new FirstException();
}

@Path("second")
@GET
public String second() {
throw new SecondException();
}

@Path("third")
@GET
public String third() {
throw new ThirdException();
}
}

public static abstract class AbstractException extends RuntimeException {

public AbstractException() {
setStackTrace(new StackTraceElement[0]);
}
}

public static class FirstException extends AbstractException {

}

public static class SecondException extends AbstractException {

}

public static class ThirdException extends AbstractException {

}

@IfBuildProfile("dummy")
public static class WontBeEnabledMappers {

@ServerExceptionMapper(FirstException.class)
public Response first() {
return Response.status(900).build();
}

@ServerExceptionMapper(value = FirstException.class, priority = Priorities.USER - 100)
public Response firstWithLowerPriority() {
return Response.status(901).build();
}

@ServerExceptionMapper(priority = Priorities.USER - 100)
public Response second(SecondException ignored) {
return Response.status(800).build();
}
}

@LookupUnlessProperty(name = "notexistingproperty", stringValue = "true", lookupIfMissing = true)
public static class WillBeEnabledMappers {

@ServerExceptionMapper(value = FirstException.class, priority = Priorities.USER + 10)
public Response first() {
return Response.status(902).build();
}

@ServerExceptionMapper(value = FirstException.class, priority = Priorities.USER - 10)
public Response firstWithLowerPriority() {
return Response.status(903).build();
}

@ServerExceptionMapper(priority = Priorities.USER - 10)
public RestResponse<Void> second(SecondException ignored) {
return RestResponse.status(801);
}
}

public static class AlwaysEnabledMappers {

@ServerExceptionMapper(value = FirstException.class, priority = Priorities.USER + 1000)
public Response first() {
return Response.status(555).build();
}

@ServerExceptionMapper(value = SecondException.class, priority = Priorities.USER + 1000)
public Response second() {
return Response.status(555).build();
}

@ServerExceptionMapper(value = ThirdException.class, priority = Priorities.USER + 1000)
public Uni<Response> third() {
return Uni.createFrom().item(Response.status(555).build());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package io.quarkus.resteasy.reactive.server.test.customexceptions;

import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;

import java.util.function.Supplier;

import javax.ws.rs.GET;
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.profile.IfBuildProfile;
import io.quarkus.test.QuarkusUnitTest;

public class InvalidConditionalΜappersTest {

@RegisterExtension
static QuarkusUnitTest test = new QuarkusUnitTest()
.setArchiveProducer(new Supplier<>() {
@Override
public JavaArchive get() {
return ShrinkWrap.create(JavaArchive.class)
.addClasses(TestResource.class, Mappers.class);
}
}).assertException(t -> {
String message = t.getMessage();
assertTrue(message.contains("@ServerExceptionMapper"));
assertTrue(message.contains("request"));
assertTrue(message.contains(Mappers.class.getName()));
});

@Test
public void test() {
fail("Should never have been called");
}

@Path("test")
public static class TestResource {

@GET
public String hello() {
return "hello";
}

}

public static class Mappers {

@IfBuildProfile("test")
@ServerExceptionMapper
public Response request(IllegalArgumentException ignored) {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,20 @@ public void handle(RoutingContext event) {
};
}

public Supplier<Boolean> beanUnavailable(String className) {
return new Supplier<>() {
@Override
public Boolean get() {
try {
return !Arc.container().select(Class.forName(className, false, Thread.currentThread()
.getContextClassLoader())).isResolvable();
} catch (ClassNotFoundException e) {
throw new RuntimeException("Unable to determine if bean '" + className + "' is available", e);
}
}
};
}

private static final class FailingDefaultAuthFailureHandler implements BiConsumer<RoutingContext, Throwable> {

@Override
Expand Down
Loading

0 comments on commit 17eecf2

Please sign in to comment.