From 5e237c26085139659d6372335de6fcabdcf8ffad Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Mon, 4 Oct 2021 11:47:29 +0300 Subject: [PATCH] Allow suspend functions as to be uses as custom filters Resolves: #19013 --- .../common/deployment/KotlinUtils.java | 23 ++ .../QuarkusResteasyReactiveDotNames.java | 4 + .../kotlin/AbstractSuspendedRequestFilter.kt | 38 +++ .../kotlin/AbstractSuspendedResponseFilter.kt | 36 +++ .../server/runtime/kotlin/FilterUtils.kt | 17 ++ .../deployment/CustomFilterGenerator.java | 223 ++++++++++++++---- .../server/runtime/filters/FilterUtil.java | 7 +- .../it/resteasy/reactive/kotlin/Filters.kt | 38 +++ .../reactive/kotlin/GreetingResource.kt | 3 +- .../src/main/resources/application.properties | 2 + .../reactive/kotlin/GreetingResourceTest.kt | 13 +- 11 files changed, 356 insertions(+), 48 deletions(-) create mode 100644 extensions/resteasy-reactive/quarkus-resteasy-reactive-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/KotlinUtils.java create mode 100644 extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin/runtime/src/main/kotlin/org/jboss/resteasy/reactive/server/runtime/kotlin/AbstractSuspendedRequestFilter.kt create mode 100644 extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin/runtime/src/main/kotlin/org/jboss/resteasy/reactive/server/runtime/kotlin/AbstractSuspendedResponseFilter.kt create mode 100644 extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin/runtime/src/main/kotlin/org/jboss/resteasy/reactive/server/runtime/kotlin/FilterUtils.kt create mode 100644 integration-tests/resteasy-reactive-kotlin/standard/src/main/kotlin/io/quarkus/it/resteasy/reactive/kotlin/Filters.kt diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/KotlinUtils.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/KotlinUtils.java new file mode 100644 index 0000000000000..af84c6a368b85 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/KotlinUtils.java @@ -0,0 +1,23 @@ +package io.quarkus.resteasy.reactive.common.deployment; + +import static io.quarkus.resteasy.reactive.common.deployment.QuarkusResteasyReactiveDotNames.CONTINUATION; + +import java.util.List; + +import org.jboss.jandex.MethodInfo; +import org.jboss.jandex.Type; + +public class KotlinUtils { + + private KotlinUtils() { + } + + public static boolean isSuspendMethod(MethodInfo methodInfo) { + List parameters = methodInfo.parameters(); + if (parameters.isEmpty()) { + return false; + } + + return CONTINUATION.equals(parameters.get(parameters.size() - 1).name()); + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/QuarkusResteasyReactiveDotNames.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/QuarkusResteasyReactiveDotNames.java index 6b1c3ad78f09c..8f195b3548585 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/QuarkusResteasyReactiveDotNames.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/QuarkusResteasyReactiveDotNames.java @@ -16,6 +16,10 @@ public class QuarkusResteasyReactiveDotNames { public static final DotName HTTP_SERVER_RESPONSE = DotName.createSimple(HttpServerResponse.class.getName()); public static final DotName JSON_IGNORE = DotName.createSimple("com.fasterxml.jackson.annotation.JsonIgnore"); public static final DotName JSONB_TRANSIENT = DotName.createSimple("javax.json.bind.annotation.JsonbTransient"); + + public static final DotName CONTINUATION = DotName.createSimple("kotlin.coroutines.Continuation"); + public static final DotName KOTLIN_UNIT = DotName.createSimple("kotlin.Unit"); + public static final IgnoreTypeForReflectionPredicate IGNORE_TYPE_FOR_REFLECTION_PREDICATE = new IgnoreTypeForReflectionPredicate(); public static final IgnoreFieldForReflectionPredicate IGNORE_FIELD_FOR_REFLECTION_PREDICATE = new IgnoreFieldForReflectionPredicate(); public static final IgnoreMethodForReflectionPredicate IGNORE_METHOD_FOR_REFLECTION_PREDICATE = new IgnoreMethodForReflectionPredicate(); diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin/runtime/src/main/kotlin/org/jboss/resteasy/reactive/server/runtime/kotlin/AbstractSuspendedRequestFilter.kt b/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin/runtime/src/main/kotlin/org/jboss/resteasy/reactive/server/runtime/kotlin/AbstractSuspendedRequestFilter.kt new file mode 100644 index 0000000000000..d3a9aecf52fa3 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin/runtime/src/main/kotlin/org/jboss/resteasy/reactive/server/runtime/kotlin/AbstractSuspendedRequestFilter.kt @@ -0,0 +1,38 @@ +package org.jboss.resteasy.reactive.server.runtime.kotlin + +import io.smallrye.mutiny.Uni +import io.smallrye.mutiny.coroutines.asUni +import kotlinx.coroutines.async +import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext +import org.jboss.resteasy.reactive.server.spi.ResteasyReactiveContainerRequestContext +import org.jboss.resteasy.reactive.server.spi.ResteasyReactiveContainerRequestFilter + +/** + * Base class used by Quarkus to generate an implementation at build-time that calls + * a {@code suspend} method annotated with {@code @ServerRequestFilter} + */ +@Suppress("unused") +abstract class AbstractSuspendedRequestFilter : ResteasyReactiveContainerRequestFilter { + + abstract suspend fun doFilter(containerRequestContext: ResteasyReactiveContainerRequestContext): Any + + abstract fun handleResult(containerRequestContext: ResteasyReactiveContainerRequestContext, uniResult: Uni<*>) + + + private val originalTCCL: ClassLoader = Thread.currentThread().contextClassLoader + + override fun filter(containerRequestContext: ResteasyReactiveContainerRequestContext) { + val (dispatcher,coroutineScope) = prepareExecution(containerRequestContext.serverRequestContext as ResteasyReactiveRequestContext) + + val uni = coroutineScope.async(context = dispatcher) { + // ensure the proper CL is not lost in dev-mode + Thread.currentThread().contextClassLoader = originalTCCL + + // the implementation gets the proper values from the context and invokes the user supplied method + doFilter(containerRequestContext) + }.asUni() + + // the implementation should call the appropriate FilterUtil method + handleResult(containerRequestContext, uni) + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin/runtime/src/main/kotlin/org/jboss/resteasy/reactive/server/runtime/kotlin/AbstractSuspendedResponseFilter.kt b/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin/runtime/src/main/kotlin/org/jboss/resteasy/reactive/server/runtime/kotlin/AbstractSuspendedResponseFilter.kt new file mode 100644 index 0000000000000..deafbac496c47 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin/runtime/src/main/kotlin/org/jboss/resteasy/reactive/server/runtime/kotlin/AbstractSuspendedResponseFilter.kt @@ -0,0 +1,36 @@ +package org.jboss.resteasy.reactive.server.runtime.kotlin + +import kotlinx.coroutines.launch +import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext +import org.jboss.resteasy.reactive.server.spi.ResteasyReactiveContainerRequestContext +import org.jboss.resteasy.reactive.server.spi.ResteasyReactiveContainerResponseFilter +import javax.ws.rs.container.ContainerResponseContext + +/** + * Base class used by Quarkus to generate an implementation at build-time that calls + * a {@code suspend} method annotated with {@code @ServerResponseFilter} + */ +@Suppress("unused") +abstract class AbstractSuspendedResponseFilter : ResteasyReactiveContainerResponseFilter { + + abstract suspend fun doFilter(requestContext: ResteasyReactiveContainerRequestContext, responseContext: ContainerResponseContext): Any + + private val originalTCCL: ClassLoader = Thread.currentThread().contextClassLoader + + override fun filter(requestContext: ResteasyReactiveContainerRequestContext, responseContext: ContainerResponseContext) { + val (dispatcher,coroutineScope) = prepareExecution(requestContext.serverRequestContext as ResteasyReactiveRequestContext) + + + requestContext.suspend() + coroutineScope.launch(context = dispatcher) { + // ensure the proper CL is not lost in dev-mode + Thread.currentThread().contextClassLoader = originalTCCL + try { + doFilter(requestContext, responseContext) + } catch (t: Throwable) { + (requestContext.serverRequestContext as ResteasyReactiveRequestContext).handleException(t, true) + } + requestContext.resume() + } + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin/runtime/src/main/kotlin/org/jboss/resteasy/reactive/server/runtime/kotlin/FilterUtils.kt b/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin/runtime/src/main/kotlin/org/jboss/resteasy/reactive/server/runtime/kotlin/FilterUtils.kt new file mode 100644 index 0000000000000..8ebd7e758208b --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-kotlin/runtime/src/main/kotlin/org/jboss/resteasy/reactive/server/runtime/kotlin/FilterUtils.kt @@ -0,0 +1,17 @@ +package org.jboss.resteasy.reactive.server.runtime.kotlin + +import io.vertx.core.Vertx +import kotlinx.coroutines.CoroutineDispatcher +import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext +import javax.enterprise.inject.spi.CDI + +fun prepareExecution(requestContext: ResteasyReactiveRequestContext): Pair { + val requestScope = requestContext.captureCDIRequestScope() + val dispatcher: CoroutineDispatcher = Vertx.currentContext()?.let {VertxDispatcher(it,requestScope)} + ?: throw IllegalStateException("No Vertx context found") + + val coroutineScope = CDI.current().select(ApplicationCoroutineScope::class.java) + requestContext.suspend() + + return Pair(dispatcher, coroutineScope.get()) +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/CustomFilterGenerator.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/CustomFilterGenerator.java index 73f6888dfd693..087095f756e7b 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/CustomFilterGenerator.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/CustomFilterGenerator.java @@ -2,8 +2,10 @@ import static io.quarkus.gizmo.MethodDescriptor.ofConstructor; import static io.quarkus.gizmo.MethodDescriptor.ofMethod; +import static io.quarkus.resteasy.reactive.common.deployment.QuarkusResteasyReactiveDotNames.CONTINUATION; import static io.quarkus.resteasy.reactive.common.deployment.QuarkusResteasyReactiveDotNames.HTTP_SERVER_REQUEST; import static io.quarkus.resteasy.reactive.common.deployment.QuarkusResteasyReactiveDotNames.HTTP_SERVER_RESPONSE; +import static io.quarkus.resteasy.reactive.common.deployment.QuarkusResteasyReactiveDotNames.KOTLIN_UNIT; import static io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveServerDotNames.QUARKUS_REST_CONTAINER_REQUEST_CONTEXT; import static io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveServerDotNames.ROUTING_CONTEXT; import static io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveServerDotNames.SERVER_REQUEST_FILTER; @@ -39,6 +41,7 @@ import org.jboss.jandex.ParameterizedType; import org.jboss.jandex.Type; import org.jboss.resteasy.reactive.RestResponse; +import org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames; import org.jboss.resteasy.reactive.server.ServerRequestFilter; import org.jboss.resteasy.reactive.server.ServerResponseFilter; import org.jboss.resteasy.reactive.server.SimpleResourceInfo; @@ -60,6 +63,7 @@ import io.quarkus.gizmo.MethodCreator; import io.quarkus.gizmo.MethodDescriptor; import io.quarkus.gizmo.ResultHandle; +import io.quarkus.resteasy.reactive.common.deployment.KotlinUtils; import io.quarkus.resteasy.reactive.server.runtime.filters.FilterUtil; import io.quarkus.resteasy.reactive.server.runtime.filters.PreventAbortResteasyReactiveContainerRequestContext; import io.smallrye.mutiny.Uni; @@ -73,9 +77,87 @@ */ final class CustomFilterGenerator { + private static final String ABSTRACT_SUSPENDED_REQ_FILTER = "org.jboss.resteasy.reactive.server.runtime.kotlin.AbstractSuspendedRequestFilter"; + private static final String ABSTRACT_SUSPENDED_RES_FILTER = "org.jboss.resteasy.reactive.server.runtime.kotlin.AbstractSuspendedResponseFilter"; + private CustomFilterGenerator() { } + static String generateContainerRequestFilter(MethodInfo targetMethod, ClassOutput classOutput) { + checkModifiers(targetMethod, SERVER_REQUEST_FILTER); + if (KotlinUtils.isSuspendMethod(targetMethod)) { + return generateRequestFilterForSuspendedMethod(targetMethod, classOutput); + } + return generateStandardRequestFilter(targetMethod, classOutput); + } + + private static String generateRequestFilterForSuspendedMethod(MethodInfo targetMethod, ClassOutput classOutput) { + DotName returnDotName = determineReturnDotNameOfSuspendMethod(targetMethod); + ReturnType returnType; + if (returnDotName.equals(VOID)) { + returnType = ReturnType.VOID; + } else if (returnDotName.equals(RESPONSE)) { + returnType = ReturnType.RESPONSE; + } else if (returnDotName.equals(REST_RESPONSE)) { + returnType = ReturnType.REST_RESPONSE; + } else { + throw new RuntimeException("Suspend method '" + targetMethod.name() + " of class '" + + targetMethod.declaringClass().name() + + "' cannot be used as a request filter as it does not declare 'void', 'Response', 'RestResponse' as its return type."); + } + + String generatedClassName = getGeneratedClassName(targetMethod, SERVER_REQUEST_FILTER); + DotName declaringClassName = targetMethod.declaringClass().name(); + try (ClassCreator cc = ClassCreator.builder().classOutput(classOutput) + .className(generatedClassName) + .superClass(ABSTRACT_SUSPENDED_REQ_FILTER) + .build()) { + FieldDescriptor delegateField = generateConstructorAndDelegateField(cc, declaringClassName.toString(), + ABSTRACT_SUSPENDED_REQ_FILTER); + + // generate the implementation of the 'doFilter' method + MethodCreator doFilterMethod = cc.getMethodCreator("doFilter", Object.class.getName(), + ResteasyReactiveContainerRequestContext.class.getName(), CONTINUATION.toString()); + // call the target method + ResultHandle resultHandle = doFilterMethod.invokeVirtualMethod(targetMethod, + doFilterMethod.readInstanceField(delegateField, doFilterMethod.getThis()), + getRequestFilterResultHandles(targetMethod, declaringClassName, doFilterMethod, 2, + getRRReqCtxHandle(doFilterMethod, getRRContainerReqCtxHandle(doFilterMethod, 0)))); + doFilterMethod.returnValue(resultHandle); + + // generate the implementation of the 'handleResult' method which simply delegates the Uni handling + // (which is created by AbstractFilterCoroutineInvoker) to FilterUtil + MethodCreator handleResultMethod = cc.getMethodCreator("handleResult", void.class, + ResteasyReactiveContainerRequestContext.class, Uni.class); + String methodName; + switch (returnType) { + case VOID: + methodName = "handleUniVoid"; + break; + case RESPONSE: + methodName = "handleUniResponse"; + break; + case REST_RESPONSE: + methodName = "handleUniRestResponse"; + break; + default: + throw new IllegalStateException("ReturnType: '" + returnType + "' is not supported, in method " + + targetMethod.declaringClass() + "." + targetMethod.name()); + } + handleResultMethod + .invokeStaticMethod( + ofMethod(FilterUtil.class, + methodName, + void.class, + Uni.class, ResteasyReactiveContainerRequestContext.class), + handleResultMethod.getMethodParam(1), getRRContainerReqCtxHandle(handleResultMethod, 0)); + + handleResultMethod.returnValue(null); + } + + return generatedClassName; + } + /** * Generates an implementation of {@link javax.ws.rs.container.ContainerRequestFilter} that delegates to the method * annotated with {@code @ServerRequestFilter}. @@ -105,31 +187,16 @@ private CustomFilterGenerator() { * * */ - static String generateContainerRequestFilter(MethodInfo targetMethod, ClassOutput classOutput) { + private static String generateStandardRequestFilter(MethodInfo targetMethod, ClassOutput classOutput) { ReturnType returnType = determineRequestFilterReturnType(targetMethod); - checkModifiers(targetMethod, SERVER_REQUEST_FILTER); String generatedClassName = getGeneratedClassName(targetMethod, SERVER_REQUEST_FILTER); DotName declaringClassName = targetMethod.declaringClass().name(); try (ClassCreator cc = ClassCreator.builder().classOutput(classOutput) .className(generatedClassName) .interfaces(determineRequestInterfaceType(returnType)) .build()) { - cc.addAnnotation(Singleton.class); - cc.addAnnotation(Unremovable.class); - - FieldDescriptor delegateField = cc.getFieldCreator("delegate", declaringClassName.toString()) - .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, declaringClassName.toString()); - ctor.setModifiers(Modifier.PUBLIC); - ctor.addAnnotation(Inject.class); - ctor.invokeSpecialMethod(MethodDescriptor.ofConstructor(Object.class), ctor.getThis()); - ResultHandle self = ctor.getThis(); - ResultHandle config = ctor.getMethodParam(0); - ctor.writeInstanceField(delegateField, self, config); - ctor.returnValue(null); + FieldDescriptor delegateField = generateConstructorAndDelegateField(cc, declaringClassName.toString(), + Object.class.getName()); if (returnType == ReturnType.VOID || returnType == ReturnType.OPTIONAL_RESPONSE @@ -142,7 +209,7 @@ static String generateContainerRequestFilter(MethodInfo targetMethod, ClassOutpu // call the target method ResultHandle resultHandle = filterMethod.invokeVirtualMethod(targetMethod, filterMethod.readInstanceField(delegateField, filterMethod.getThis()), - getRequestFilterResultHandles(targetMethod, declaringClassName, filterMethod, + getRequestFilterResultHandles(targetMethod, declaringClassName, filterMethod, 1, getRRReqCtxHandle(filterMethod, rrContainerReqCtxHandle))); if (returnType == ReturnType.OPTIONAL_RESPONSE) { // invoke utility class that deals with Optional @@ -174,7 +241,7 @@ static String generateContainerRequestFilter(MethodInfo targetMethod, ClassOutpu ResultHandle rrContainerReqCtxHandle = getRRContainerReqCtxHandle(filterMethod, 0); ResultHandle uniHandle = filterMethod.invokeVirtualMethod(targetMethod, filterMethod.readInstanceField(delegateField, filterMethod.getThis()), - getRequestFilterResultHandles(targetMethod, declaringClassName, filterMethod, + getRequestFilterResultHandles(targetMethod, declaringClassName, filterMethod, 1, getRRReqCtxHandle(filterMethod, rrContainerReqCtxHandle))); String methodName; switch (returnType) { @@ -209,7 +276,7 @@ static String generateContainerRequestFilter(MethodInfo targetMethod, ClassOutpu } private static ResultHandle[] getRequestFilterResultHandles(MethodInfo targetMethod, DotName declaringClassName, - MethodCreator filterMethod, ResultHandle rrReqCtxHandle) { + MethodCreator filterMethod, int filterMethodParamCount, ResultHandle rrReqCtxHandle) { // for each of the parameters of the user method, generate bytecode that pulls the argument outs of QuarkusRestRequestContext ResultHandle[] targetMethodParamHandles = new ResultHandle[targetMethod.parameters().size()]; for (int i = 0; i < targetMethod.parameters().size(); i++) { @@ -248,6 +315,9 @@ private static ResultHandle[] getRequestFilterResultHandles(MethodInfo targetMet targetMethodParamHandles[i] = getSimpleResourceInfoHandle(filterMethod, rrReqCtxHandle); } else if (ROUTING_CONTEXT.equals(paramDotName)) { targetMethodParamHandles[i] = GeneratorUtils.routingContextHandler(filterMethod, rrReqCtxHandle); + } else if (CONTINUATION.equals(paramDotName)) { + // the continuation to pass on to the target is retrieved from the last parameter of the filter method + targetMethodParamHandles[i] = filterMethod.getMethodParam(filterMethodParamCount - 1); } else { String parameterName = targetMethod.parameterName(i); throw new RuntimeException("Parameter '" + parameterName + "' of method '" + targetMethod.name() @@ -258,6 +328,48 @@ private static ResultHandle[] getRequestFilterResultHandles(MethodInfo targetMet return targetMethodParamHandles; } + static String generateContainerResponseFilter(MethodInfo targetMethod, ClassOutput classOutput) { + checkModifiers(targetMethod, SERVER_RESPONSE_FILTER); + if (KotlinUtils.isSuspendMethod(targetMethod)) { + return generateResponseFilterForSuspendedMethod(targetMethod, classOutput); + } + return generateStandardContainerResponseFilter(targetMethod, classOutput); + } + + private static String generateResponseFilterForSuspendedMethod(MethodInfo targetMethod, ClassOutput classOutput) { + DotName returnDotName = determineReturnDotNameOfSuspendMethod(targetMethod); + if (!returnDotName.equals(VOID)) { + throw new RuntimeException("Suspend method '" + targetMethod.name() + " of class '" + + targetMethod.declaringClass().name() + + "' cannot be used as a request filter as it does not declare 'void' as its return type."); + } + String generatedClassName = getGeneratedClassName(targetMethod, SERVER_RESPONSE_FILTER); + DotName declaringClassName = targetMethod.declaringClass().name(); + try (ClassCreator cc = ClassCreator.builder().classOutput(classOutput) + .className(generatedClassName) + .superClass(ABSTRACT_SUSPENDED_RES_FILTER) + .build()) { + FieldDescriptor delegateField = generateConstructorAndDelegateField(cc, declaringClassName.toString(), + ABSTRACT_SUSPENDED_RES_FILTER); + + // generate the implementation of the filter method + MethodCreator doFilterMethod = cc.getMethodCreator("doFilter", Object.class.getName(), + ResteasyReactiveContainerRequestContext.class.getName(), ContainerResponseContext.class.getName(), + CONTINUATION.toString()); + + ResultHandle rrContainerReqCtxHandle = getRRContainerReqCtxHandle(doFilterMethod, 0); + ResultHandle rrReqCtxHandle = getRRReqCtxHandle(doFilterMethod, rrContainerReqCtxHandle); + + // call the target method + ResultHandle resultHandle = doFilterMethod.invokeVirtualMethod(targetMethod, + doFilterMethod.readInstanceField(delegateField, doFilterMethod.getThis()), + getResponseFilterResultHandles(targetMethod, declaringClassName, + doFilterMethod, 3, rrReqCtxHandle)); + doFilterMethod.returnValue(resultHandle); + } + return generatedClassName; + } + /** * Generates an implementation of {@link javax.ws.rs.container.ContainerResponseFilter} that delegates to the method * annotated with {@code @ServerResponseFilter}. @@ -287,31 +399,16 @@ private static ResultHandle[] getRequestFilterResultHandles(MethodInfo targetMet * * */ - static String generateContainerResponseFilter(MethodInfo targetMethod, ClassOutput classOutput) { + private static String generateStandardContainerResponseFilter(MethodInfo targetMethod, ClassOutput classOutput) { ReturnType returnType = determineResponseFilterReturnType(targetMethod); - checkModifiers(targetMethod, SERVER_RESPONSE_FILTER); String generatedClassName = getGeneratedClassName(targetMethod, SERVER_RESPONSE_FILTER); DotName declaringClassName = targetMethod.declaringClass().name(); try (ClassCreator cc = ClassCreator.builder().classOutput(classOutput) .className(generatedClassName) .interfaces(determineResponseInterfaceType(returnType)) .build()) { - cc.addAnnotation(Singleton.class); - cc.addAnnotation(Unremovable.class); - - FieldDescriptor delegateField = cc.getFieldCreator("delegate", declaringClassName.toString()) - .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, declaringClassName.toString()); - ctor.setModifiers(Modifier.PUBLIC); - ctor.addAnnotation(Inject.class); - ctor.invokeSpecialMethod(MethodDescriptor.ofConstructor(Object.class), ctor.getThis()); - ResultHandle self = ctor.getThis(); - ResultHandle config = ctor.getMethodParam(0); - ctor.writeInstanceField(delegateField, self, config); - ctor.returnValue(null); + FieldDescriptor delegateField = generateConstructorAndDelegateField(cc, declaringClassName.toString(), + Object.class.getName()); if (returnType == ReturnType.VOID) { // generate the implementation of the filter method @@ -325,7 +422,7 @@ static String generateContainerResponseFilter(MethodInfo targetMethod, ClassOutp filterMethod.invokeVirtualMethod(targetMethod, filterMethod.readInstanceField(delegateField, filterMethod.getThis()), getResponseFilterResultHandles(targetMethod, declaringClassName, - filterMethod, rrReqCtxHandle)); + filterMethod, 2, rrReqCtxHandle)); filterMethod.returnValue(null); } else if (returnType == ReturnType.UNI_VOID) { // generate the implementation of the filter method @@ -340,7 +437,7 @@ static String generateContainerResponseFilter(MethodInfo targetMethod, ClassOutp ResultHandle uniHandle = filterMethod.invokeVirtualMethod(targetMethod, filterMethod.readInstanceField(delegateField, filterMethod.getThis()), getResponseFilterResultHandles(targetMethod, declaringClassName, - filterMethod, rrReqCtxHandle)); + filterMethod, 2, rrReqCtxHandle)); // invoke utility class that deals with Optional filterMethod.invokeStaticMethod( @@ -353,8 +450,29 @@ static String generateContainerResponseFilter(MethodInfo targetMethod, ClassOutp return generatedClassName; } + private static FieldDescriptor generateConstructorAndDelegateField(ClassCreator cc, String declaringClassName, + String superClassName) { + cc.addAnnotation(Singleton.class); + cc.addAnnotation(Unremovable.class); + + FieldDescriptor delegateField = cc.getFieldCreator("delegate", declaringClassName) + .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, declaringClassName); + ctor.setModifiers(Modifier.PUBLIC); + ctor.addAnnotation(Inject.class); + ctor.invokeSpecialMethod(MethodDescriptor.ofConstructor(superClassName), ctor.getThis()); + ResultHandle self = ctor.getThis(); + ResultHandle delegate = ctor.getMethodParam(0); + ctor.writeInstanceField(delegateField, self, delegate); + ctor.returnValue(null); + return delegateField; + } + private static ResultHandle[] getResponseFilterResultHandles(MethodInfo targetMethod, DotName declaringClassName, - MethodCreator filterMethod, ResultHandle rrReqCtxHandle) { + MethodCreator filterMethod, int filterMethodParamCount, ResultHandle rrReqCtxHandle) { ResultHandle[] targetMethodParamHandles = new ResultHandle[targetMethod.parameters().size()]; for (int i = 0; i < targetMethod.parameters().size(); i++) { Type param = targetMethod.parameters().get(i); @@ -385,6 +503,9 @@ private static ResultHandle[] getResponseFilterResultHandles(MethodInfo targetMe GeneratorUtils.paramHandleFromReqContextMethod(filterMethod, rrReqCtxHandle, targetMethodParamHandles, i, "getThrowable", THROWABLE); + } else if (CONTINUATION.equals(paramDotName)) { + // the continuation to pass on to the target is retrieved from the last parameter of the filter method + targetMethodParamHandles[i] = filterMethod.getMethodParam(filterMethodParamCount - 1); } else { String parameterName = targetMethod.parameterName(i); throw new RuntimeException("Parameter '" + parameterName + "' of method '" + targetMethod.name() @@ -520,6 +641,24 @@ private static Class determineResponseInterfaceType(ReturnType returnType) { throw new IllegalStateException("ReturnType: '" + returnType + "' is not supported"); } + private static DotName determineReturnDotNameOfSuspendMethod(MethodInfo methodInfo) { + Type lastParamType = methodInfo.parameters().get(methodInfo.parameters().size() - 1); + if (lastParamType.kind() != Type.Kind.PARAMETERIZED_TYPE) { + throw new IllegalStateException("Something went wrong during parameter type resolution - expected " + + lastParamType + " to be a Continuation with a generic type"); + } + lastParamType = lastParamType.asParameterizedType().arguments().get(0); + if (lastParamType.kind() != Type.Kind.WILDCARD_TYPE) { + throw new IllegalStateException("Something went wrong during parameter type resolution - expected " + + lastParamType + " to be a Continuation with a generic type"); + } + lastParamType = lastParamType.asWildcardType().superBound(); + if (lastParamType.name().equals(KOTLIN_UNIT)) { + return ResteasyReactiveDotNames.VOID; + } + return lastParamType.name(); + } + private enum ReturnType { VOID, RESPONSE, diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/filters/FilterUtil.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/filters/FilterUtil.java index 406098241f2eb..3d73627caff2c 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/filters/FilterUtil.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/filters/FilterUtil.java @@ -19,6 +19,7 @@ * that don't return {@code void}. * See {@code io.quarkus.resteasy.reactive.server.deployment.CustomFilterGenerator} */ +@SuppressWarnings("unused") public final class FilterUtil { private FilterUtil() { @@ -52,14 +53,14 @@ public static void handleRestResponse(RestResponse response, } } - public static void handleUniVoid(Uni uni, ResteasyReactiveContainerRequestContext context) { + public static void handleUniVoid(Uni uni, ResteasyReactiveContainerRequestContext context) { if (uni == null) { return; } context.suspend(); - uni.subscribe().with(new Consumer() { + uni.subscribe().with(new Consumer() { @Override - public void accept(Void unused) { + public void accept(Object unused) { context.resume(); } }, new Consumer() { diff --git a/integration-tests/resteasy-reactive-kotlin/standard/src/main/kotlin/io/quarkus/it/resteasy/reactive/kotlin/Filters.kt b/integration-tests/resteasy-reactive-kotlin/standard/src/main/kotlin/io/quarkus/it/resteasy/reactive/kotlin/Filters.kt new file mode 100644 index 0000000000000..dfefb296a7097 --- /dev/null +++ b/integration-tests/resteasy-reactive-kotlin/standard/src/main/kotlin/io/quarkus/it/resteasy/reactive/kotlin/Filters.kt @@ -0,0 +1,38 @@ +package io.quarkus.it.resteasy.reactive.kotlin + +import kotlinx.coroutines.delay +import org.jboss.resteasy.reactive.server.ServerRequestFilter +import org.jboss.resteasy.reactive.server.ServerResponseFilter +import org.jboss.resteasy.reactive.server.SimpleResourceInfo +import javax.ws.rs.container.ContainerRequestContext +import javax.ws.rs.container.ContainerResponseContext +import javax.ws.rs.core.Response +import javax.ws.rs.core.UriInfo + +class Filters { + + @ServerRequestFilter + suspend fun addHeader(uriInfo: UriInfo, context: ContainerRequestContext){ + delay(100) + context.headers.add("firstName", "foo") + delay(100) + } + + @ServerRequestFilter + suspend fun addHeaderOrAbort(context: ContainerRequestContext): Response?{ + delay(100) + if (context.headers.containsKey("abort")) { + return Response.noContent().build() + } + context.headers.add("lastName", "bar") + delay(100) + return null + } + + @ServerResponseFilter + suspend fun addResponseHeader(context: ContainerResponseContext, simpleResourceInfo: SimpleResourceInfo) { + delay(100) + context.headers.add("method", simpleResourceInfo.methodName) + delay(100) + } +} diff --git a/integration-tests/resteasy-reactive-kotlin/standard/src/main/kotlin/io/quarkus/it/resteasy/reactive/kotlin/GreetingResource.kt b/integration-tests/resteasy-reactive-kotlin/standard/src/main/kotlin/io/quarkus/it/resteasy/reactive/kotlin/GreetingResource.kt index 9cca758710b92..cb3e6529c0d21 100644 --- a/integration-tests/resteasy-reactive-kotlin/standard/src/main/kotlin/io/quarkus/it/resteasy/reactive/kotlin/GreetingResource.kt +++ b/integration-tests/resteasy-reactive-kotlin/standard/src/main/kotlin/io/quarkus/it/resteasy/reactive/kotlin/GreetingResource.kt @@ -1,5 +1,6 @@ package io.quarkus.it.resteasy.reactive.kotlin +import org.jboss.resteasy.reactive.RestHeader import javax.ws.rs.GET import javax.ws.rs.Path @@ -7,7 +8,7 @@ import javax.ws.rs.Path class GreetingResource { @GET - suspend fun testSuspend() = Greeting("hello") + suspend fun testSuspend(@RestHeader("firstName") firstName: String, @RestHeader("lastName") lastName: String) = Greeting("hello $firstName $lastName") } data class Greeting(val message:String) diff --git a/integration-tests/resteasy-reactive-kotlin/standard/src/main/resources/application.properties b/integration-tests/resteasy-reactive-kotlin/standard/src/main/resources/application.properties index d959d746c9825..69c63e7aa9816 100644 --- a/integration-tests/resteasy-reactive-kotlin/standard/src/main/resources/application.properties +++ b/integration-tests/resteasy-reactive-kotlin/standard/src/main/resources/application.properties @@ -26,3 +26,5 @@ mp.messaging.incoming.countries-t2-in.connector=smallrye-kafka mp.messaging.incoming.countries-t2-in.topic=countries-t2 mp.messaging.incoming.countries-t2-in.auto.offset.reset=earliest mp.messaging.incoming.countries-t2-in.value.deserializer=org.apache.kafka.common.serialization.StringDeserializer + +quarkus.package.fernflower.enabled=true diff --git a/integration-tests/resteasy-reactive-kotlin/standard/src/test/kotlin/io/quarkus/it/resteasy/reactive/kotlin/GreetingResourceTest.kt b/integration-tests/resteasy-reactive-kotlin/standard/src/test/kotlin/io/quarkus/it/resteasy/reactive/kotlin/GreetingResourceTest.kt index 6beceb9df8f1f..660449b5288e1 100644 --- a/integration-tests/resteasy-reactive-kotlin/standard/src/test/kotlin/io/quarkus/it/resteasy/reactive/kotlin/GreetingResourceTest.kt +++ b/integration-tests/resteasy-reactive-kotlin/standard/src/test/kotlin/io/quarkus/it/resteasy/reactive/kotlin/GreetingResourceTest.kt @@ -10,12 +10,21 @@ import org.junit.jupiter.api.Test class GreetingResourceTest { @Test - fun testDataClass() { + fun testDataClassAndCustomFilters() { RestAssured.given() .`when`()["/greeting"] .then() .statusCode(200) .contentType(ContentType.JSON) - .body("message", CoreMatchers.`is`("hello")) + .body("message", CoreMatchers.`is`("hello foo bar")) + .header("method", "testSuspend") + } + + @Test + fun testAbortingCustomFilters() { + RestAssured.given().header("abort", "true") + .`when`()["/greeting"] + .then() + .statusCode(204) } }