Skip to content

Commit

Permalink
Provider tighter integration of Qute into RESTEasy Reactive
Browse files Browse the repository at this point in the history
This change uses the ServerRestHandler mechanism instead of a response filter
to convert a template into a response.

This also paves the way to implement returning template responses
as chunks (using Qute's createMulti)
  • Loading branch information
geoand committed May 20, 2022
1 parent 8022fe5 commit 26933d1
Show file tree
Hide file tree
Showing 8 changed files with 165 additions and 98 deletions.
Original file line number Diff line number Diff line change
@@ -1,37 +1,81 @@
package io.quarkus.resteasy.reactive.qute.deployment;

import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.COMPLETION_STAGE;
import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.UNI;

import java.util.Collections;
import java.util.List;
import java.util.Map;

import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.DotName;
import org.jboss.jandex.MethodInfo;
import org.jboss.jandex.ParameterizedType;
import org.jboss.jandex.Type;
import org.jboss.resteasy.reactive.server.handlers.UniResponseHandler;
import org.jboss.resteasy.reactive.server.model.FixedHandlersChainCustomizer;
import org.jboss.resteasy.reactive.server.model.HandlerChainCustomizer;
import org.jboss.resteasy.reactive.server.processor.scanning.MethodScanner;

import io.quarkus.deployment.Feature;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.builditem.FeatureBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveHierarchyIgnoreWarningBuildItem;
import io.quarkus.qute.TemplateInstance;
import io.quarkus.resteasy.reactive.qute.runtime.TemplateResponseFilter;
import io.quarkus.resteasy.reactive.qute.runtime.TemplateResponseUniHandler;
import io.quarkus.resteasy.reactive.server.spi.MethodScannerBuildItem;
import io.quarkus.resteasy.reactive.server.spi.NonBlockingReturnTypeBuildItem;
import io.quarkus.resteasy.reactive.spi.CustomContainerResponseFilterBuildItem;

public class ResteasyReactiveQuteProcessor {

private static final DotName TEMPLATE_INSTANCE = DotName.createSimple(TemplateInstance.class.getName());

@BuildStep
FeatureBuildItem feature() {
return new FeatureBuildItem(Feature.RESTEASY_REACTIVE_QUTE);
}

@BuildStep
CustomContainerResponseFilterBuildItem registerProviders() {
return new CustomContainerResponseFilterBuildItem(TemplateResponseFilter.class.getName());
}

@BuildStep
ReflectiveHierarchyIgnoreWarningBuildItem ignoreReflectiveWarning() {
return new ReflectiveHierarchyIgnoreWarningBuildItem(new ReflectiveHierarchyIgnoreWarningBuildItem.DotNameExclusion(
DotName.createSimple(TemplateInstance.class.getName())));
return new ReflectiveHierarchyIgnoreWarningBuildItem(
new ReflectiveHierarchyIgnoreWarningBuildItem.DotNameExclusion(TEMPLATE_INSTANCE));
}

@BuildStep
NonBlockingReturnTypeBuildItem nonBlockingTemplateInstance() {
return new NonBlockingReturnTypeBuildItem(DotName.createSimple(TemplateInstance.class.getName()));
return new NonBlockingReturnTypeBuildItem(TEMPLATE_INSTANCE);
}

@BuildStep
public MethodScannerBuildItem configureHandler() {
return new MethodScannerBuildItem(new MethodScanner() {
@Override
public List<HandlerChainCustomizer> scan(MethodInfo method, ClassInfo actualEndpointClass,
Map<String, Object> methodContext) {
if (method.returnType().name().equals(TEMPLATE_INSTANCE) || isAsyncTemplateInstance(method.returnType())) {
// TemplateResponseUniHandler creates a Uni, so we also need to introduce another Uni handler
// so RR actually gets the result
// the reason why we use AFTER_METHOD_INVOKE_SECOND_ROUND is to be able to properly support Uni<TemplateInstance>
return Collections.singletonList(
new FixedHandlersChainCustomizer(
List.of(new TemplateResponseUniHandler(), new UniResponseHandler()),
HandlerChainCustomizer.Phase.AFTER_METHOD_INVOKE_SECOND_ROUND));
}
return Collections.emptyList();
}

private boolean isAsyncTemplateInstance(Type type) {
boolean isAsyncTemplateInstance = false;
if (type.kind() == Type.Kind.PARAMETERIZED_TYPE) {
ParameterizedType parameterizedType = type.asParameterizedType();
if ((parameterizedType.name().equals(UNI) || parameterizedType.name().equals(COMPLETION_STAGE))
&& (parameterizedType.arguments().size() == 1)) {
DotName firstParameterType = parameterizedType.arguments().get(0).name();
isAsyncTemplateInstance = firstParameterType.equals(TEMPLATE_INSTANCE);
}
}
return isAsyncTemplateInstance;
}
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,18 @@
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;

import org.jboss.resteasy.reactive.ResponseHeader;
import org.jboss.resteasy.reactive.ResponseStatus;

import io.quarkus.qute.CheckedTemplate;
import io.quarkus.qute.Template;
import io.quarkus.qute.TemplateInstance;
import io.quarkus.resteasy.reactive.qute.RestTemplate;
import io.smallrye.mutiny.Uni;

@Path("hello")
public class HelloResource {
Expand Down Expand Up @@ -44,11 +50,11 @@ public TemplateInstance get(@QueryParam("name") String name) {

@Path("no-injection")
@GET
public TemplateInstance hello(@QueryParam("name") String name) {
public Uni<TemplateInstance> hello(@QueryParam("name") String name) {
if (name == null) {
name = "world";
}
return RestTemplate.data("name", name);
return Uni.createFrom().item(RestTemplate.data("name", name));
}

@Path("type-error")
Expand Down Expand Up @@ -80,4 +86,13 @@ public TemplateInstance nativeToplevelTypedTemplate(@QueryParam("name") String n
}
return io.quarkus.resteasy.reactive.qute.deployment.Templates.toplevel(name);
}

@ResponseStatus(201)
@ResponseHeader(name = "foo", value = { "bar" })
@GET
@Produces(MediaType.TEXT_PLAIN)
@Path("status-and-headers")
public TemplateInstance setStatusAndHeaders() {
return hello.data("name", "world");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import io.restassured.RestAssured;
import io.restassured.http.ContentType;

public class TemplateResponseFilterTest {
public class TemplateResultTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
Expand All @@ -26,8 +26,8 @@ public class TemplateResponseFilterTest {
.addAsResource(new StringAsset("Hello {name}!"), "templates/hello.txt"));

@Test
public void testFilter() {
when().get("/hello").then().body(Matchers.is("Hello world!"));
public void test() {
when().get("/hello").then().statusCode(200).body(Matchers.is("Hello world!"));
when().get("/hello?name=Joe").then().body(Matchers.is("Hello Joe!"));
when().get("/hello/no-injection").then().body(Matchers.is("Salut world!"));
when().get("/hello/no-injection?name=Joe").then().body(Matchers.is("Salut Joe!"));
Expand All @@ -40,6 +40,7 @@ public void testFilter() {
when().get("/hello/native/typed-template-primitives").then()
.body(Matchers.is("Byte: 0 Short: 1 Int: 2 Long: 3 Char: a Boolean: true Float: 4.0 Double: 5.0"));
when().get("/hello/native/toplevel?name=Joe").then().body(Matchers.is("Salut Joe!"));
when().get("/hello/status-and-headers").then().statusCode(201).header("foo", "bar").body(Matchers.is("Hello world!"));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ public class VariantTemplateTest {

@Test
public void testVariant() {
given().when().accept("text/plain").get("/item/10").then().body(Matchers.is("Item foo: 10"));
given().when().accept("text/html").get("/item/20").then().body(Matchers.is("<html><body>Item foo: 20</body></html>"));
given().when().accept("text/plain").get("/item/10").then().contentType("text/plain").body(Matchers.is("Item foo: 10"));
given().when().accept("text/html").get("/item/20").then().contentType("text/html")
.body(Matchers.is("<html><body>Item foo: 20</body></html>"));
}

}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package io.quarkus.resteasy.reactive.qute.runtime;

import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Variant;

import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext;
import org.jboss.resteasy.reactive.server.spi.ServerRestHandler;

import io.quarkus.arc.Arc;
import io.quarkus.qute.Engine;
import io.quarkus.qute.TemplateException;
import io.quarkus.qute.TemplateInstance;
import io.smallrye.mutiny.Uni;

public class TemplateResponseUniHandler implements ServerRestHandler {

private volatile Engine engine;

@Override
public void handle(ResteasyReactiveRequestContext requestContext) {
Object result = requestContext.getResult();
if (!(result instanceof TemplateInstance)) {
return;
}

if (engine == null) {
synchronized (this) {
if (engine == null) {
engine = Arc.container().instance(Engine.class).get();
}
}
}
requestContext.setResult(createUni(requestContext, (TemplateInstance) result));
}

@SuppressWarnings("unchecked")
private Uni<String> createUni(ResteasyReactiveRequestContext requestContext, TemplateInstance result) {
Object variantsAttr = result.getAttribute(TemplateInstance.VARIANTS);
if (variantsAttr != null) {
List<io.quarkus.qute.Variant> quteVariants = (List<io.quarkus.qute.Variant>) variantsAttr;
List<Variant> jaxRsVariants = new ArrayList<>(quteVariants.size());
for (io.quarkus.qute.Variant variant : quteVariants) {
jaxRsVariants.add(new Variant(MediaType.valueOf(variant.getMediaType()), variant.getLocale(),
variant.getEncoding()));
}
Variant selected = requestContext.getRequest()
.selectVariant(jaxRsVariants);

if (selected != null) {
Locale selectedLocale = selected.getLanguage();
if (selectedLocale == null) {
List<Locale> acceptableLocales = requestContext.getHttpHeaders().getAcceptableLanguages();
if (!acceptableLocales.isEmpty()) {
selectedLocale = acceptableLocales.get(0);
}
}
result.setAttribute(TemplateInstance.SELECTED_VARIANT,
new io.quarkus.qute.Variant(selectedLocale, selected.getMediaType().toString(),
selected.getEncoding()));
}
}

Uni<String> uni = result.createUni();
if (!engine.useAsyncTimeout()) {
// Make sure the timeout is always used
long timeout = result.getTimeout();
uni = uni.ifNoItem().after(Duration.ofMillis(timeout))
.failWith(() -> new TemplateException(result + " rendering timeout [" + timeout + "ms] occured"));
}
return uni;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,9 @@ public RuntimeResource buildResourceMethod(ResourceClass clazz,
}
boolean afterMethodInvokeHandlersAdded = addHandlers(handlers, clazz, method, info,
HandlerChainCustomizer.Phase.AFTER_METHOD_INVOKE);
if (afterMethodInvokeHandlersAdded) {
boolean afterMethodInvokeHandlersSecondRoundAdded = addHandlers(handlers, clazz, method, info,
HandlerChainCustomizer.Phase.AFTER_METHOD_INVOKE_SECOND_ROUND);
if (afterMethodInvokeHandlersAdded || afterMethodInvokeHandlersSecondRoundAdded) {
addStreamingResponseCustomizers(method, handlers);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,12 @@ enum Phase {
* handlers are invoked just after the resource method is invoked
*/
AFTER_METHOD_INVOKE,

/**
* handlers are invoked after the handlers that run after the method invocation
*/
AFTER_METHOD_INVOKE_SECOND_ROUND,

/**
* handlers are invoked just after the resource method result has been turned into a {@link Response}
*/
Expand Down

0 comments on commit 26933d1

Please sign in to comment.