Skip to content

Commit

Permalink
Merge pull request #25709 from geoand/rr-qute-handler
Browse files Browse the repository at this point in the history
  • Loading branch information
gastaldi authored May 24, 2022
2 parents b823cb9 + efecf5e commit e9b3c80
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 @@ -366,7 +366,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 e9b3c80

Please sign in to comment.