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 24, 2022
1 parent 6b03da0 commit efecf5e
Show file tree
Hide file tree
Showing 9 changed files with 221 additions and 50 deletions.
Original file line number Diff line number Diff line change
@@ -1,18 +1,37 @@
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);
Expand All @@ -25,13 +44,45 @@ CustomContainerResponseFilterBuildItem registerProviders() {

@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,20 @@
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 javax.ws.rs.core.Response;

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

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 +52,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 +88,27 @@ 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");
}

@GET
@Produces(MediaType.TEXT_PLAIN)
@Path("rest-response")
public RestResponse<TemplateInstance> restResponse() {
return RestResponse.status(RestResponse.Status.ACCEPTED, hello.data("name", "world"));
}

@GET
@Produces(MediaType.TEXT_PLAIN)
@Path("response")
public Response response() {
return Response.status(203).entity(hello.data("name", "world")).build();
}
}
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,9 @@ 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!"));
when().get("/hello/rest-response").then().statusCode(202).body(Matchers.is("Hello world!"));
when().get("/hello/response").then().statusCode(203).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>"));
}

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

import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import static io.quarkus.resteasy.reactive.qute.runtime.Util.setSelectedVariant;
import static io.quarkus.resteasy.reactive.qute.runtime.Util.toUni;

import javax.inject.Inject;
import javax.ws.rs.container.ContainerResponseContext;
import javax.ws.rs.core.MediaType;

import org.jboss.resteasy.reactive.common.headers.HeaderUtil;
import org.jboss.resteasy.reactive.server.ServerResponseFilter;
import org.jboss.resteasy.reactive.server.spi.ResteasyReactiveContainerRequestContext;

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

/**
* This class is needed in order to support handling {@link javax.ws.rs.core.Response} that contains a TemplateInstance...
*/
public class TemplateResponseFilter {

@Inject
Engine engine;

@SuppressWarnings("unchecked")
@ServerResponseFilter
public Uni<Void> filter(ResteasyReactiveContainerRequestContext requestContext, ContainerResponseContext responseContext) {
Object entity = responseContext.getEntity();
Expand All @@ -33,41 +32,15 @@ public Uni<Void> filter(ResteasyReactiveContainerRequestContext requestContext,

MediaType mediaType;
TemplateInstance instance = (TemplateInstance) entity;
Object variantsAttr = instance.getAttribute(TemplateInstance.VARIANTS);
if (variantsAttr != null) {
List<javax.ws.rs.core.Variant> variants = new ArrayList<>();
for (Variant variant : (List<Variant>) variantsAttr) {
variants.add(new javax.ws.rs.core.Variant(MediaType.valueOf(variant.getMediaType()), variant.getLocale(),
variant.getEncoding()));
}
javax.ws.rs.core.Variant selected = requestContext.getRequest()
.selectVariant(variants);

if (selected != null) {
Locale selectedLocale = selected.getLanguage();
if (selectedLocale == null) {
List<Locale> acceptableLocales = requestContext.getAcceptableLanguages();
if (!acceptableLocales.isEmpty()) {
selectedLocale = acceptableLocales.get(0);
}
}
instance.setAttribute(TemplateInstance.SELECTED_VARIANT,
new Variant(selectedLocale, selected.getMediaType().toString(), selected.getEncoding()));
mediaType = selected.getMediaType();
} else {
mediaType = responseContext.getMediaType();
}
} else {
MediaType selectedMediaType = setSelectedVariant(instance, requestContext.getRequest(),
HeaderUtil.getAcceptableLanguages(requestContext.getHeaders()));
if (selectedMediaType == null) {
mediaType = responseContext.getMediaType();
} else {
mediaType = selectedMediaType;
}

Uni<String> uni = instance.createUni();
if (!engine.useAsyncTimeout()) {
// Make sure the timeout is always used
long timeout = instance.getTimeout();
uni = uni.ifNoItem().after(Duration.ofMillis(timeout))
.failWith(() -> new TemplateException(instance + " rendering timeout [" + timeout + "ms] occured"));
}
Uni<String> uni = toUni(instance, engine);
return uni.chain(r -> {
if (mediaType != null) {
responseContext.setEntity(r, null, mediaType);
Expand All @@ -77,4 +50,5 @@ public Uni<Void> filter(ResteasyReactiveContainerRequestContext requestContext,
return Uni.createFrom().nullItem();
});
}

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

import static io.quarkus.resteasy.reactive.qute.runtime.Util.*;
import static io.quarkus.resteasy.reactive.qute.runtime.Util.toUni;

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.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, engine));
}

private Uni<String> createUni(ResteasyReactiveRequestContext requestContext, TemplateInstance result, Engine engine) {
setSelectedVariant(result, requestContext.getRequest(),
requestContext.getHttpHeaders().getAcceptableLanguages());
return toUni(result, engine);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
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.Request;

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

final class Util {

private Util() {
}

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

@SuppressWarnings("unchecked")
static MediaType setSelectedVariant(TemplateInstance result,
Request request, List<Locale> acceptableLanguages) {
Object variantsAttr = result.getAttribute(TemplateInstance.VARIANTS);
if (variantsAttr != null) {
List<Variant> quteVariants = (List<Variant>) variantsAttr;
List<javax.ws.rs.core.Variant> jaxRsVariants = new ArrayList<>(quteVariants.size());
for (Variant variant : quteVariants) {
jaxRsVariants.add(new javax.ws.rs.core.Variant(MediaType.valueOf(variant.getMediaType()), variant.getLocale(),
variant.getEncoding()));
}
javax.ws.rs.core.Variant selected = request
.selectVariant(jaxRsVariants);

if (selected != null) {
Locale selectedLocale = selected.getLanguage();
if (selectedLocale == null) {
if (!acceptableLanguages.isEmpty()) {
selectedLocale = acceptableLanguages.get(0);
}
}
result.setAttribute(TemplateInstance.SELECTED_VARIANT,
new Variant(selectedLocale, selected.getMediaType().toString(),
selected.getEncoding()));
return selected.getMediaType();
}
}
return null;
}
}
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
Loading

0 comments on commit efecf5e

Please sign in to comment.