Skip to content

Commit

Permalink
Merge pull request #17045 from mkouba/issue-17037
Browse files Browse the repository at this point in the history
  • Loading branch information
cescoffier authored May 6, 2021
2 parents 75536ae + 3ffc3b5 commit 9aee2a0
Show file tree
Hide file tree
Showing 5 changed files with 210 additions and 22 deletions.
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;
}
}

}

0 comments on commit 9aee2a0

Please sign in to comment.