Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Take conditional annotation into account for @ServerExceptionMapper #29459

Merged
merged 1 commit into from
Dec 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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