From 3ffc3b5a1d6d47195ffdefb2db9d601ab4b9c94e Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Thu, 6 May 2021 15:31:45 +0200 Subject: [PATCH] Reactive routes - support CompletionStage as a method return type - resolves #17037 --- .../vertx/web/deployment/DotNames.java | 2 + .../web/deployment/HandlerDescriptor.java | 28 +++-- .../quarkus/vertx/web/deployment/Methods.java | 6 + .../web/deployment/VertxWebProcessor.java | 77 ++++++++++-- .../web/cs/CompletionStageRouteTest.java | 119 ++++++++++++++++++ 5 files changed, 210 insertions(+), 22 deletions(-) create mode 100644 extensions/vertx-web/deployment/src/test/java/io/quarkus/vertx/web/cs/CompletionStageRouteTest.java diff --git a/extensions/vertx-web/deployment/src/main/java/io/quarkus/vertx/web/deployment/DotNames.java b/extensions/vertx-web/deployment/src/main/java/io/quarkus/vertx/web/deployment/DotNames.java index a46028c85546d..b0a81b5784a68 100644 --- a/extensions/vertx-web/deployment/src/main/java/io/quarkus/vertx/web/deployment/DotNames.java +++ b/extensions/vertx-web/deployment/src/main/java/io/quarkus/vertx/web/deployment/DotNames.java @@ -1,6 +1,7 @@ package io.quarkus.vertx.web.deployment; import java.util.List; +import java.util.concurrent.CompletionStage; import org.jboss.jandex.DotName; @@ -48,5 +49,6 @@ final class DotNames { static final DotName EXCEPTION = DotName.createSimple(Exception.class.getName()); static final DotName THROWABLE = DotName.createSimple(Throwable.class.getName()); static final DotName BLOCKING = DotName.createSimple(Blocking.class.getName()); + static final DotName COMPLETION_STAGE = DotName.createSimple(CompletionStage.class.getName()); } diff --git a/extensions/vertx-web/deployment/src/main/java/io/quarkus/vertx/web/deployment/HandlerDescriptor.java b/extensions/vertx-web/deployment/src/main/java/io/quarkus/vertx/web/deployment/HandlerDescriptor.java index 492ccaac49f33..8672d08fa074e 100644 --- a/extensions/vertx-web/deployment/src/main/java/io/quarkus/vertx/web/deployment/HandlerDescriptor.java +++ b/extensions/vertx-web/deployment/src/main/java/io/quarkus/vertx/web/deployment/HandlerDescriptor.java @@ -3,6 +3,7 @@ import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.MethodInfo; import org.jboss.jandex.Type; +import org.jboss.jandex.Type.Kind; import io.quarkus.hibernate.validator.spi.BeanValidationAnnotationsBuildItem; import io.quarkus.vertx.http.runtime.HandlerType; @@ -15,11 +16,23 @@ class HandlerDescriptor { private final MethodInfo method; private final BeanValidationAnnotationsBuildItem validationAnnotations; private final HandlerType handlerType; + private final Type contentType; HandlerDescriptor(MethodInfo method, BeanValidationAnnotationsBuildItem bvAnnotations, HandlerType handlerType) { this.method = method; this.validationAnnotations = bvAnnotations; this.handlerType = handlerType; + Type returnType = method.returnType(); + if (returnType.kind() == Kind.VOID) { + contentType = null; + } else { + if (returnType.name().equals(DotNames.UNI) || returnType.name().equals(DotNames.MULTI) + || returnType.name().equals(DotNames.COMPLETION_STAGE)) { + contentType = returnType.asParameterizedType().arguments().get(0); + } else { + contentType = returnType; + } + } } Type getReturnType() { @@ -38,6 +51,10 @@ boolean isReturningMulti() { return method.returnType().name().equals(DotNames.MULTI); } + boolean isReturningCompletionStage() { + return method.returnType().name().equals(DotNames.COMPLETION_STAGE); + } + /** * @return {@code true} if the method is annotated with a constraint or {@code @Valid} or any parameter has such kind of * annotation. @@ -70,16 +87,7 @@ boolean isProducedResponseValidated() { } Type getContentType() { - if (isReturningVoid()) { - return null; - } - if (isReturningUni()) { - return getReturnType().asParameterizedType().arguments().get(0); - } - if (isReturningMulti()) { - return getReturnType().asParameterizedType().arguments().get(0); - } - return getReturnType(); + return contentType; } boolean isContentTypeString() { diff --git a/extensions/vertx-web/deployment/src/main/java/io/quarkus/vertx/web/deployment/Methods.java b/extensions/vertx-web/deployment/src/main/java/io/quarkus/vertx/web/deployment/Methods.java index 02f5bb89f32b3..b122c6ea278ca 100644 --- a/extensions/vertx-web/deployment/src/main/java/io/quarkus/vertx/web/deployment/Methods.java +++ b/extensions/vertx-web/deployment/src/main/java/io/quarkus/vertx/web/deployment/Methods.java @@ -5,6 +5,8 @@ import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.concurrent.CompletionStage; +import java.util.function.BiConsumer; import java.util.function.Consumer; import javax.enterprise.context.spi.Context; @@ -212,6 +214,10 @@ class Methods { static final MethodDescriptor ITERATOR_NEXT = MethodDescriptor.ofMethod(Iterator.class, "next", Object.class); static final MethodDescriptor ITERATOR_HAS_NEXT = MethodDescriptor.ofMethod(Iterator.class, "hasNext", boolean.class); + public static final MethodDescriptor CS_WHEN_COMPLETE = MethodDescriptor.ofMethod(CompletionStage.class, + "whenComplete", + CompletionStage.class, BiConsumer.class); + private Methods() { // Avoid direct instantiation } diff --git a/extensions/vertx-web/deployment/src/main/java/io/quarkus/vertx/web/deployment/VertxWebProcessor.java b/extensions/vertx-web/deployment/src/main/java/io/quarkus/vertx/web/deployment/VertxWebProcessor.java index d5a94b9e2d99b..b35b965f0e0c8 100644 --- a/extensions/vertx-web/deployment/src/main/java/io/quarkus/vertx/web/deployment/VertxWebProcessor.java +++ b/extensions/vertx-web/deployment/src/main/java/io/quarkus/vertx/web/deployment/VertxWebProcessor.java @@ -19,6 +19,8 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.concurrent.CompletionStage; +import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; @@ -481,12 +483,15 @@ private void validateRouteMethod(BeanInfo bean, MethodInfo method, Route.HandlerType handlerType = typeValue == null ? Route.HandlerType.NORMAL : Route.HandlerType.from(typeValue.asEnum()); - if ((method.returnType().name().equals(io.quarkus.vertx.web.deployment.DotNames.UNI) - || method.returnType().name().equals(io.quarkus.vertx.web.deployment.DotNames.MULTI)) + DotName returnTypeName = method.returnType().name(); + + if ((returnTypeName.equals(DotNames.UNI) + || returnTypeName.equals(DotNames.MULTI) + || returnTypeName.equals(DotNames.COMPLETION_STAGE)) && method.returnType().kind() == Kind.CLASS) { throw new IllegalStateException( String.format( - "Route business method returning a Uni/Multi must have a generic parameter [method: %s, bean: %s]", + "Route business method returning a Uni/Multi/CompletionStage must have a generic parameter [method: %s, bean: %s]", method, bean)); } boolean canEndResponse = false; @@ -728,6 +733,8 @@ void implementInvoke(HandlerDescriptor descriptor, BeanInfo bean, MethodInfo met res = invoke.createVariable(Uni.class); } else if (descriptor.isReturningMulti()) { res = invoke.createVariable(Multi.class); + } else if (descriptor.isReturningCompletionStage()) { + res = invoke.createVariable(CompletionStage.class); } else { res = invoke.createVariable(Object.class); } @@ -755,7 +762,6 @@ void implementInvoke(HandlerDescriptor descriptor, BeanInfo bean, MethodInfo met // Get the response: HttpServerResponse response = rc.response() MethodDescriptor end = Methods.getEndMethodForContentType(descriptor); if (descriptor.isReturningUni()) { - ResultHandle response = invoke.invokeInterfaceMethod(Methods.RESPONSE, routingContext); // The method returns a Uni. // We subscribe to this Uni and write the provided item in the HTTP response // If the method returned null, we fail @@ -763,16 +769,11 @@ void implementInvoke(HandlerDescriptor descriptor, BeanInfo bean, MethodInfo met // If the provided item is null, and the method return a Uni, we reply with a 204 - NO CONTENT // If the provided item is not null, if it's a string or buffer, the response.end method is used to write the response // If the provided item is not null, and it's an object, the item is mapped to JSON and written into the response - - FunctionCreator successCallback = getUniOnItemCallback(descriptor, invoke, routingContext, end, response, - validatorField); - + FunctionCreator successCallback = getUniOnItemCallback(descriptor, invoke, routingContext, end, validatorField); ResultHandle failureCallback = getUniOnFailureCallback(invoke, routingContext); - ResultHandle sub = invoke.invokeInterfaceMethod(Methods.UNI_SUBSCRIBE, res); invoke.invokeVirtualMethod(Methods.UNI_SUBSCRIBE_WITH, sub, successCallback.getInstance(), failureCallback); - registerForReflection(descriptor.getContentType(), reflectiveHierarchy); } else if (descriptor.isReturningMulti()) { @@ -794,6 +795,17 @@ void implementInvoke(HandlerDescriptor descriptor, BeanInfo bean, MethodInfo met isNotSSE.close(); registerForReflection(descriptor.getContentType(), reflectiveHierarchy); + } else if (descriptor.isReturningCompletionStage()) { + // The method returns a CompletionStage - we write the provided item in the HTTP response + // If the method returned null, we fail + // If the provided item is null and the method does not return a CompletionStage, we fail + // If the provided item is null, and the method return a CompletionStage, we reply with a 204 - NO CONTENT + // If the provided item is not null, if it's a string or buffer, the response.end method is used to write the response + // If the provided item is not null, and it's an object, the item is mapped to JSON and written into the response + ResultHandle consumer = getWhenCompleteCallback(descriptor, invoke, routingContext, end, validatorField) + .getInstance(); + invoke.invokeInterfaceMethod(Methods.CS_WHEN_COMPLETE, res, consumer); + registerForReflection(descriptor.getContentType(), reflectiveHierarchy); } else if (descriptor.getContentType() != null) { // The method returns "something" in a synchronous manner, write it into the response @@ -986,9 +998,11 @@ private void handleJsonArrayMulti(HandlerDescriptor descriptor, BytecodeCreator * @return the function creator */ private FunctionCreator getUniOnItemCallback(HandlerDescriptor descriptor, MethodCreator invoke, ResultHandle rc, - MethodDescriptor end, ResultHandle response, FieldCreator validatorField) { + MethodDescriptor end, FieldCreator validatorField) { FunctionCreator callback = invoke.createFunction(Consumer.class); BytecodeCreator creator = callback.getBytecode(); + ResultHandle response = creator.invokeInterfaceMethod(Methods.RESPONSE, rc); + if (Methods.isNoContent(descriptor)) { // Uni - so return a 204. creator.invokeInterfaceMethod(Methods.SET_STATUS, response, creator.load(204)); creator.invokeInterfaceMethod(Methods.END, response); @@ -1012,6 +1026,44 @@ private FunctionCreator getUniOnItemCallback(HandlerDescriptor descriptor, Metho return callback; } + private FunctionCreator getWhenCompleteCallback(HandlerDescriptor descriptor, MethodCreator invoke, ResultHandle rc, + MethodDescriptor end, FieldCreator validatorField) { + FunctionCreator callback = invoke.createFunction(BiConsumer.class); + BytecodeCreator creator = callback.getBytecode(); + ResultHandle response = creator.invokeInterfaceMethod(Methods.RESPONSE, rc); + + ResultHandle throwable = creator.getMethodParam(1); + BranchResult failureCheck = creator.ifNotNull(throwable); + + BytecodeCreator failure = failureCheck.trueBranch(); + failure.invokeInterfaceMethod(Methods.FAIL, rc, throwable); + + BytecodeCreator success = failureCheck.falseBranch(); + + if (Methods.isNoContent(descriptor)) { + // CompletionStage - so always return a 204 + success.invokeInterfaceMethod(Methods.SET_STATUS, response, success.load(204)); + success.invokeInterfaceMethod(Methods.END, response); + } else { + // First check if the item is null + ResultHandle item = success.getMethodParam(0); + BranchResult itemNullCheck = success.ifNull(item); + + BytecodeCreator itemNotNull = itemNullCheck.falseBranch(); + ResultHandle content = getContentToWrite(descriptor, response, item, itemNotNull, validatorField, + invoke.getThis()); + itemNotNull.invokeInterfaceMethod(end, response, content); + + BytecodeCreator itemNull = itemNullCheck.trueBranch(); + ResultHandle npe = itemNull.newInstance(MethodDescriptor.ofConstructor(NullPointerException.class, String.class), + itemNull.load("Null is not a valid return value for @Route method with return type: " + + descriptor.getReturnType())); + itemNull.invokeInterfaceMethod(Methods.FAIL, rc, npe); + } + Methods.returnAndClose(creator); + return callback; + } + private ResultHandle getUniOnFailureCallback(MethodCreator writer, ResultHandle routingContext) { return writer.newInstance(MethodDescriptor.ofConstructor(UniFailureCallback.class, RoutingContext.class), routingContext); @@ -1030,7 +1082,8 @@ private ResultHandle getContentToWrite(HandlerDescriptor descriptor, ResultHandl // Encode to Json Methods.setContentTypeToJson(response, writer); // Validate res if needed - if (descriptor.isProducedResponseValidated() && (descriptor.isReturningUni() || descriptor.isReturningMulti())) { + if (descriptor.isProducedResponseValidated() + && (descriptor.isReturningUni() || descriptor.isReturningMulti() || descriptor.isReturningCompletionStage())) { return Methods.validateProducedItem(response, writer, res, validatorField, owner); } else { return writer.invokeStaticMethod(Methods.JSON_ENCODE, res); diff --git a/extensions/vertx-web/deployment/src/test/java/io/quarkus/vertx/web/cs/CompletionStageRouteTest.java b/extensions/vertx-web/deployment/src/test/java/io/quarkus/vertx/web/cs/CompletionStageRouteTest.java new file mode 100644 index 0000000000000..0d8fc91cecd39 --- /dev/null +++ b/extensions/vertx-web/deployment/src/test/java/io/quarkus/vertx/web/cs/CompletionStageRouteTest.java @@ -0,0 +1,119 @@ +package io.quarkus.vertx.web.cs; + +import static io.restassured.RestAssured.when; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.hasLength; +import static org.hamcrest.Matchers.is; + +import java.io.IOException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +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.test.QuarkusUnitTest; +import io.quarkus.vertx.web.Route; +import io.vertx.core.buffer.Buffer; +import io.vertx.ext.web.RoutingContext; + +public class CompletionStageRouteTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class).addClasses(SimpleBean.class)); + + @Test + public void testCsRoute() { + when().get("/hello").then().statusCode(200).body(is("Hello world!")); + when().get("/hello-buffer").then().statusCode(200).body(is("Buffer")); + when().get("/hello-mutiny-buffer").then().statusCode(200).body(is("Mutiny Buffer")); + + when().get("/person").then().statusCode(200) + .body("name", is("neo")) + .body("id", is(12345)) + .header("content-type", "application/json"); + + when().get("/person-content-type-set").then().statusCode(200) + .body("name", is("neo")) + .body("id", is(12345)) + .header("content-type", "application/json;charset=utf-8"); + + when().get("/failure").then().statusCode(500).body(containsString("boom")); + when().get("/sync-failure").then().statusCode(500).body(containsString("boom")); + + when().get("/null").then().statusCode(500).body(containsString("null")); + when().get("/cs-null").then().statusCode(500); + when().get("/void").then().statusCode(204).body(hasLength(0)); + } + + static class SimpleBean { + + @Route(path = "hello") + CompletionStage hello(RoutingContext context) { + return CompletableFuture.completedFuture("Hello world!"); + } + + @Route(path = "hello-buffer") + CompletionStage helloWithBuffer(RoutingContext context) { + return CompletableFuture.completedFuture(Buffer.buffer("Buffer")); + } + + @Route(path = "hello-mutiny-buffer") + CompletionStage helloWithMutinyBuffer(RoutingContext context) { + return CompletableFuture.completedFuture(io.vertx.mutiny.core.buffer.Buffer.buffer("Mutiny Buffer")); + } + + @Route(path = "failure") + CompletionStage fail(RoutingContext context) { + CompletableFuture ret = new CompletableFuture<>(); + ret.completeExceptionally(new IOException("boom")); + return ret; + } + + @Route(path = "sync-failure") + CompletionStage failCsSync(RoutingContext context) { + throw new IllegalStateException("boom"); + } + + @Route(path = "null") + CompletionStage csNull(RoutingContext context) { + return null; + } + + @Route(path = "void") + CompletionStage csOfVoid() { + return CompletableFuture.completedFuture(null); + } + + @Route(path = "cs-null") + CompletionStage produceNull(RoutingContext context) { + return CompletableFuture.completedFuture(null); + } + + @Route(path = "person", produces = "application/json") + CompletionStage getPersonAsCs(RoutingContext context) { + return CompletableFuture.completedFuture(new Person("neo", 12345)); + } + + @Route(path = "person-content-type-set", produces = "application/json") + CompletionStage getPersonAsCsUtf8(RoutingContext context) { + context.response().putHeader("content-type", "application/json;charset=utf-8"); + return CompletableFuture.completedFuture(new Person("neo", 12345)); + } + + } + + static class Person { + public String name; + public int id; + + public Person(String name, int id) { + this.name = name; + this.id = id; + } + } + +}