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

Reactive routes - support CompletionStage as a method return type #17045

Merged
merged 1 commit into from
May 6, 2021
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,6 +1,7 @@
package io.quarkus.vertx.web.deployment;

import java.util.List;
import java.util.concurrent.CompletionStage;

import org.jboss.jandex.DotName;

Expand Down Expand Up @@ -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());

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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() {
Expand All @@ -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.
Expand Down Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -755,24 +762,18 @@ 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
// If the provided item is null and the method does not return a Uni<Void>, we fail
// If the provided item is null, and the method return a Uni<Void>, 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()) {
Expand All @@ -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<Void>, we fail
// If the provided item is null, and the method return a CompletionStage<Void>, 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
Expand Down Expand Up @@ -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<Void> - so return a 204.
creator.invokeInterfaceMethod(Methods.SET_STATUS, response, creator.load(204));
creator.invokeInterfaceMethod(Methods.END, response);
Expand All @@ -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<Void> - 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);
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> hello(RoutingContext context) {
return CompletableFuture.completedFuture("Hello world!");
}

@Route(path = "hello-buffer")
CompletionStage<Buffer> helloWithBuffer(RoutingContext context) {
return CompletableFuture.completedFuture(Buffer.buffer("Buffer"));
}

@Route(path = "hello-mutiny-buffer")
CompletionStage<io.vertx.mutiny.core.buffer.Buffer> helloWithMutinyBuffer(RoutingContext context) {
return CompletableFuture.completedFuture(io.vertx.mutiny.core.buffer.Buffer.buffer("Mutiny Buffer"));
}

@Route(path = "failure")
CompletionStage<String> fail(RoutingContext context) {
CompletableFuture<String> ret = new CompletableFuture<>();
ret.completeExceptionally(new IOException("boom"));
return ret;
}

@Route(path = "sync-failure")
CompletionStage<String> failCsSync(RoutingContext context) {
throw new IllegalStateException("boom");
}

@Route(path = "null")
CompletionStage<String> csNull(RoutingContext context) {
return null;
}

@Route(path = "void")
CompletionStage<Void> csOfVoid() {
return CompletableFuture.completedFuture(null);
}

@Route(path = "cs-null")
CompletionStage<String> produceNull(RoutingContext context) {
return CompletableFuture.completedFuture(null);
}

@Route(path = "person", produces = "application/json")
CompletionStage<Person> getPersonAsCs(RoutingContext context) {
return CompletableFuture.completedFuture(new Person("neo", 12345));
}

@Route(path = "person-content-type-set", produces = "application/json")
CompletionStage<Person> 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;
}
}

}