From 17eecf23817b0bd50e40085d8e84aa3971fe6b66 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Wed, 23 Nov 2022 15:43:34 +0200 Subject: [PATCH] Take conditional annotation into account for @ServerExceptionMapper Closes: #29043 --- .../spi/ExceptionMapperBuildItem.java | 20 +++ .../deployment/ResteasyReactiveProcessor.java | 3 +- .../ResteasyReactiveScanningProcessor.java | 46 +++++- .../ConditionalExceptionMappersTest.java | 141 ++++++++++++++++++ ...InvalidConditional\316\234appersTest.java" | 62 ++++++++ .../runtime/ResteasyReactiveRecorder.java | 14 ++ .../common/model/ResourceExceptionMapper.java | 36 ++++- .../ServerExceptionMapperGenerator.java | 29 +++- .../ServerExceptionMappingFeature.java | 2 +- .../server/core/ExceptionMapping.java | 98 +++++++++++- .../server/core/RuntimeExceptionMapper.java | 2 +- 11 files changed, 436 insertions(+), 17 deletions(-) create mode 100644 extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/customexceptions/ConditionalExceptionMappersTest.java create mode 100644 "extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/customexceptions/InvalidConditional\316\234appersTest.java" diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/spi-deployment/src/main/java/io/quarkus/resteasy/reactive/spi/ExceptionMapperBuildItem.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/spi-deployment/src/main/java/io/quarkus/resteasy/reactive/spi/ExceptionMapperBuildItem.java index 727213f77799f..1479091055640 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/spi-deployment/src/main/java/io/quarkus/resteasy/reactive/spi/ExceptionMapperBuildItem.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/spi-deployment/src/main/java/io/quarkus/resteasy/reactive/spi/ExceptionMapperBuildItem.java @@ -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 { @@ -8,12 +10,14 @@ 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) { @@ -21,6 +25,7 @@ private ExceptionMapperBuildItem(Builder builder) { this.handledExceptionName = builder.handledExceptionName; this.priority = builder.priority; this.registerAsBean = builder.registerAsBean; + this.declaringClass = builder.declaringClass; } public String getClassName() { @@ -40,6 +45,10 @@ public boolean isRegisterAsBean() { return registerAsBean; } + public ClassInfo getDeclaringClass() { + return declaringClass; + } + public static class Builder { private final String className; private final String handledExceptionName; @@ -47,6 +56,12 @@ public static class Builder { 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; @@ -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); } 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 fdfab1e721441..407faa9ee5491 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 @@ -1106,8 +1106,9 @@ public void setupDeployment(BeanContainerBuildItem beanContainerBuildItem, Function> 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(); 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 aacd2043ff891..c535061abd937 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 @@ -123,7 +123,6 @@ public List defaultUnwrappedException() { new UnwrappedExceptionBuildItem(RollbackException.class)); } - @SuppressWarnings({ "unchecked", "rawtypes" }) @BuildStep public ExceptionMappersBuildItem scanForExceptionMappers(CombinedIndexBuildItem combinedIndexBuildItem, ApplicationResultBuildItem applicationResultBuildItem, @@ -163,12 +162,32 @@ public ExceptionMappersBuildItem scanForExceptionMappers(CombinedIndexBuildItem ResourceExceptionMapper 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 mapper) { + ClassInfo declaringClass = additionalExceptionMapper.getDeclaringClass(); + if (declaringClass != null) { + boolean needsRuntimeCheck = false; + List 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 additionalBeanBuildItemBuildProducer, @@ -387,10 +406,31 @@ public void handleCustomAnnotatedMethods( additionalBeans.addBeanClass(methodInfo.declaringClass().name().toString()); Map 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 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 classAnnotations = m.declaringClass().declaredAnnotations(); + for (AnnotationInstance classAnnotation : classAnnotations) { + if (CONDITIONAL_BEAN_ANNOTATIONS.contains(classAnnotation.name())) { + return true; + } + } + return false; + })); for (Map.Entry 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()); diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/customexceptions/ConditionalExceptionMappersTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/customexceptions/ConditionalExceptionMappersTest.java new file mode 100644 index 0000000000000..31c3270b60806 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/customexceptions/ConditionalExceptionMappersTest.java @@ -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 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 third() { + return Uni.createFrom().item(Response.status(555).build()); + } + } +} diff --git "a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/customexceptions/InvalidConditional\316\234appersTest.java" "b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/customexceptions/InvalidConditional\316\234appersTest.java" new file mode 100644 index 0000000000000..08cb2f23ae8fc --- /dev/null +++ "b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/customexceptions/InvalidConditional\316\234appersTest.java" @@ -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(); + } + + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveRecorder.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveRecorder.java index 8cc7fde9154a7..c0a81b7d277bb 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveRecorder.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveRecorder.java @@ -333,6 +333,20 @@ public void handle(RoutingContext event) { }; } + public Supplier 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 { @Override diff --git a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/ResourceExceptionMapper.java b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/ResourceExceptionMapper.java index 2d695d3c645ab..705e560e66530 100644 --- a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/ResourceExceptionMapper.java +++ b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/model/ResourceExceptionMapper.java @@ -1,16 +1,20 @@ package org.jboss.resteasy.reactive.common.model; +import java.util.function.Supplier; + import javax.ws.rs.Priorities; import javax.ws.rs.ext.ExceptionMapper; import org.jboss.resteasy.reactive.spi.BeanFactory; -public class ResourceExceptionMapper { +public class ResourceExceptionMapper implements Comparable> { private BeanFactory> factory; private int priority = Priorities.USER; private String className; + private Supplier discardAtRuntime; + public void setFactory(BeanFactory> factory) { this.factory = factory; } @@ -35,4 +39,34 @@ public ResourceExceptionMapper setClassName(String className) { this.className = className; return this; } + + public Supplier getDiscardAtRuntime() { + return discardAtRuntime; + } + + public void setDiscardAtRuntime(Supplier discardAtRuntime) { + this.discardAtRuntime = discardAtRuntime; + } + + @Override + public int compareTo(ResourceExceptionMapper o) { + return Integer.compare(this.priority, o.priority); + } + + public static final class DiscardAtRuntimeIfBeanIsUnavailable implements Supplier { + private final String beanClass; + + public DiscardAtRuntimeIfBeanIsUnavailable(String beanClass) { + this.beanClass = beanClass; + } + + @Override + public Boolean get() { + throw new IllegalStateException("should never be called"); + } + + public String getBeanClass() { + return beanClass; + } + } } diff --git a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/exceptionmappers/ServerExceptionMapperGenerator.java b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/exceptionmappers/ServerExceptionMapperGenerator.java index c5ac16bb7d8c6..e40a932562258 100644 --- a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/exceptionmappers/ServerExceptionMapperGenerator.java +++ b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/exceptionmappers/ServerExceptionMapperGenerator.java @@ -24,7 +24,9 @@ import java.util.Map; import java.util.Set; import java.util.function.BiFunction; +import java.util.function.Predicate; +import javax.enterprise.inject.Instance; import javax.inject.Inject; import javax.inject.Singleton; import javax.ws.rs.container.ResourceInfo; @@ -240,7 +242,7 @@ public static Map generatePerClassMapper(MethodInfo targetMethod * Returns a map containing the handled exception name as the key and the generated class name as the value */ public static Map generateGlobalMapper(MethodInfo targetMethod, ClassOutput classOutput, - Set unwrappableTypes, Set additionalBeanAnnotations) { + Set unwrappableTypes, Set additionalBeanAnnotations, Predicate isOptionalMapper) { ReturnType returnType = determineReturnType(targetMethod); checkModifiers(targetMethod); @@ -264,14 +266,31 @@ public static Map generateGlobalMapper(MethodInfo targetMethod, .setModifiers(Modifier.PRIVATE | Modifier.FINAL) .getFieldDescriptor(); - // generate a constructor that takes the target class as an argument - this class is a CDI bean so Arc will be able to inject into the generated class - MethodCreator ctor = cc.getMethodCreator("", void.class, targetClass.name().toString()); + MethodCreator ctor; + + boolean isOptional = isOptionalMapper.test(targetMethod); + if (isOptional) { + // generate a constructor that takes the Instance as an argument in order to avoid missing bean issues if the target has been conditionally disabled + // the body can freely read the instance value because if the target has been conditionally disabled, the generated class will not be instantiated + ctor = cc.getMethodCreator("", void.class, Instance.class).setSignature( + String.format("(Ljavax/enterprise/inject/Instance;)V", + targetClass.name().toString().replace('.', '/'))); + } else { + // generate a constructor that takes the target class as an argument - this class is a CDI bean so Arc will be able to inject into the generated class + ctor = cc.getMethodCreator("", void.class, targetClass.name().toString()); + } + ctor.setModifiers(Modifier.PUBLIC); ctor.addAnnotation(Inject.class); ctor.invokeSpecialMethod(ofConstructor(Object.class), ctor.getThis()); ResultHandle self = ctor.getThis(); - ResultHandle config = ctor.getMethodParam(0); - ctor.writeInstanceField(delegateField, self, config); + ResultHandle param = ctor.getMethodParam(0); + if (isOptional) { + ctor.writeInstanceField(delegateField, self, ctor + .invokeInterfaceMethod(MethodDescriptor.ofMethod(Instance.class, "get", Object.class), param)); + } else { + ctor.writeInstanceField(delegateField, self, param); + } ctor.returnValue(null); if (returnType == ReturnType.RESPONSE || returnType == ReturnType.REST_RESPONSE) { diff --git a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/exceptionmappers/ServerExceptionMappingFeature.java b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/exceptionmappers/ServerExceptionMappingFeature.java index ef2e8ce4c1ae3..7c6e8e9c73710 100644 --- a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/exceptionmappers/ServerExceptionMappingFeature.java +++ b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/generation/exceptionmappers/ServerExceptionMappingFeature.java @@ -76,7 +76,7 @@ public FeatureScanResult integrate(IndexView index, ScannedApplication scannedAp // the user class itself is made to be a bean as we want the user to be able to declare dependencies //additionalBeans.addBeanClass(methodInfo.declaringClass().name().toString()); Map generatedClassNames = ServerExceptionMapperGenerator.generateGlobalMapper(methodInfo, - classOutput, unwrappableTypes, additionalBeanAnnotations); + classOutput, unwrappableTypes, additionalBeanAnnotations, (m) -> false); for (Map.Entry entry : generatedClassNames.entrySet()) { ResourceExceptionMapper mapper = new ResourceExceptionMapper<>().setClassName(entry.getValue()); scannedApplication.getExceptionMappers().addExceptionMapper(entry.getKey(), mapper); 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 680cd22425ae2..cbf952beb1f2f 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 @@ -1,6 +1,7 @@ package org.jboss.resteasy.reactive.server.core; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -8,13 +9,28 @@ import java.util.Set; import java.util.function.Function; import java.util.function.Predicate; +import java.util.function.Supplier; import org.jboss.resteasy.reactive.common.model.ResourceExceptionMapper; import org.jboss.resteasy.reactive.spi.BeanFactory; +@SuppressWarnings({ "unchecked", "unused" }) public class ExceptionMapping { + /** + * The idea behind having two different maps is the following: + * Under normal circumstances, mappers are added to the first map, + * and we don't need to track multiple mappings because the priority is enough + * to distinguish. + * + * However, in the case where ExceptionMapper classes may not be active at runtime + * (due to the presence of conditional bean annotations), then we need to track + * all the possible mappings and at runtime determine the one with the lowest priority + * value that is active. + */ final Map> mappers = new HashMap<>(); + // this is going to be used when there are mappers that are removable at runtime + final Map>> runtimeCheckMappers = new HashMap<>(); /** * Exceptions that indicate an blocking operation was performed on an IO thread. @@ -61,17 +77,44 @@ public void addExceptionMapper(String exceptionClass, Reso ResourceExceptionMapper existing = mappers.get(exceptionClass); if (existing != null) { if (existing.getPriority() < mapper.getPriority()) { - //already a higher priority mapper + // we already have a lower priority mapper registered return; + } else { + mappers.remove(exceptionClass); + List> list = new ArrayList<>(2); + list.add(mapper); + list.add(existing); + runtimeCheckMappers.put(exceptionClass, list); + } + } else { + var list = runtimeCheckMappers.get(exceptionClass); + if (list == null) { + if (mapper.getDiscardAtRuntime() == null) { + mappers.put(exceptionClass, mapper); + } else { + list = new ArrayList<>(1); + list.add(mapper); + runtimeCheckMappers.put(exceptionClass, list); + } + } else { + list.add(mapper); + Collections.sort(list); } } - mappers.put(exceptionClass, mapper); } public void initializeDefaultFactories(Function> factoryCreator) { - for (Map.Entry> entry : mappers.entrySet()) { - if (entry.getValue().getFactory() == null) { - entry.getValue().setFactory((BeanFactory) factoryCreator.apply(entry.getValue().getClassName())); + for (var resourceExceptionMapper : mappers.values()) { + if (resourceExceptionMapper.getFactory() == null) { + resourceExceptionMapper.setFactory((BeanFactory) factoryCreator.apply(resourceExceptionMapper.getClassName())); + } + } + for (var list : runtimeCheckMappers.values()) { + for (var resourceExceptionMapper : list) { + if (resourceExceptionMapper.getFactory() == null) { + resourceExceptionMapper + .setFactory((BeanFactory) factoryCreator.apply(resourceExceptionMapper.getClassName())); + } } } } @@ -80,6 +123,51 @@ public Map> getMappers() { return mappers; } + public Map>> getRuntimeCheckMappers() { + return runtimeCheckMappers; + } + + public Map> effectiveMappers() { + if (runtimeCheckMappers.isEmpty()) { + return mappers; + } + Map> result = new HashMap<>(); + for (var entry : runtimeCheckMappers.entrySet()) { + String exceptionClass = entry.getKey(); + var list = entry.getValue(); + for (var resourceExceptionMapper : list) { + if (resourceExceptionMapper.getDiscardAtRuntime() == null) { + result.put(exceptionClass, resourceExceptionMapper); + break; + } else { + if (!resourceExceptionMapper.getDiscardAtRuntime().get()) { + result.put(exceptionClass, resourceExceptionMapper); + break; + } + } + } + } + result.putAll(mappers); + return result; + } + + public void replaceDiscardAtRuntimeIfBeanIsUnavailable(Function> function) { + if (runtimeCheckMappers.isEmpty()) { + return; + } + for (var list : runtimeCheckMappers.values()) { + for (var resourceExceptionMapper : list) { + if (resourceExceptionMapper + .getDiscardAtRuntime() instanceof ResourceExceptionMapper.DiscardAtRuntimeIfBeanIsUnavailable) { + var discardAtRuntimeIfBeanIsUnavailable = (ResourceExceptionMapper.DiscardAtRuntimeIfBeanIsUnavailable) resourceExceptionMapper + .getDiscardAtRuntime(); + resourceExceptionMapper + .setDiscardAtRuntime(function.apply(discardAtRuntimeIfBeanIsUnavailable.getBeanClass())); + } + } + } + } + public static class ExceptionTypePredicate implements Predicate { private Class throwable; diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/RuntimeExceptionMapper.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/RuntimeExceptionMapper.java index 04be9002bf53b..a72022cea3288 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/RuntimeExceptionMapper.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/RuntimeExceptionMapper.java @@ -45,7 +45,7 @@ public class RuntimeExceptionMapper { public RuntimeExceptionMapper(ExceptionMapping mapping, ClassLoader classLoader) { try { mappers = new HashMap<>(); - for (var i : mapping.mappers.entrySet()) { + for (var i : mapping.effectiveMappers().entrySet()) { mappers.put((Class) Class.forName(i.getKey(), false, classLoader), i.getValue()); } blockingProblemPredicates = new ArrayList<>(mapping.blockingProblemPredicates);