From d0035ca0bbf74769ef57cba2a5ac6935f547878b Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Fri, 20 May 2022 17:15:36 +0300 Subject: [PATCH] Provider tighter integration of Qute into RESTEasy Reactive 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) --- .../ResteasyReactiveQuteProcessor.java | 57 ++++++++++++++++- .../qute/deployment/HelloResource.java | 35 ++++++++++- ...ilterTest.java => TemplateResultTest.java} | 9 ++- .../qute/deployment/VariantTemplateTest.java | 5 +- .../qute/runtime/TemplateResponseFilter.java | 52 ++++------------ .../runtime/TemplateResponseUniHandler.java | 41 ++++++++++++ .../resteasy/reactive/qute/runtime/Util.java | 62 +++++++++++++++++++ .../startup/RuntimeResourceDeployment.java | 4 +- .../server/model/HandlerChainCustomizer.java | 6 ++ 9 files changed, 221 insertions(+), 50 deletions(-) rename extensions/resteasy-reactive/quarkus-resteasy-reactive-qute/deployment/src/test/java/io/quarkus/resteasy/reactive/qute/deployment/{TemplateResponseFilterTest.java => TemplateResultTest.java} (81%) create mode 100644 extensions/resteasy-reactive/quarkus-resteasy-reactive-qute/runtime/src/main/java/io/quarkus/resteasy/reactive/qute/runtime/TemplateResponseUniHandler.java create mode 100644 extensions/resteasy-reactive/quarkus-resteasy-reactive-qute/runtime/src/main/java/io/quarkus/resteasy/reactive/qute/runtime/Util.java diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-qute/deployment/src/main/java/io/quarkus/resteasy/reactive/qute/deployment/ResteasyReactiveQuteProcessor.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-qute/deployment/src/main/java/io/quarkus/resteasy/reactive/qute/deployment/ResteasyReactiveQuteProcessor.java index 730c615064f8d..9e5609ab16d74 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-qute/deployment/src/main/java/io/quarkus/resteasy/reactive/qute/deployment/ResteasyReactiveQuteProcessor.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-qute/deployment/src/main/java/io/quarkus/resteasy/reactive/qute/deployment/ResteasyReactiveQuteProcessor.java @@ -1,6 +1,21 @@ 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; @@ -8,11 +23,15 @@ 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); @@ -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 scan(MethodInfo method, ClassInfo actualEndpointClass, + Map 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 + 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; + } + }); + } } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-qute/deployment/src/test/java/io/quarkus/resteasy/reactive/qute/deployment/HelloResource.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-qute/deployment/src/test/java/io/quarkus/resteasy/reactive/qute/deployment/HelloResource.java index 177fb2f4f7b9e..d635cbe214ed2 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-qute/deployment/src/test/java/io/quarkus/resteasy/reactive/qute/deployment/HelloResource.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-qute/deployment/src/test/java/io/quarkus/resteasy/reactive/qute/deployment/HelloResource.java @@ -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 { @@ -44,11 +52,11 @@ public TemplateInstance get(@QueryParam("name") String name) { @Path("no-injection") @GET - public TemplateInstance hello(@QueryParam("name") String name) { + public Uni 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") @@ -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 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(); + } } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-qute/deployment/src/test/java/io/quarkus/resteasy/reactive/qute/deployment/TemplateResponseFilterTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-qute/deployment/src/test/java/io/quarkus/resteasy/reactive/qute/deployment/TemplateResultTest.java similarity index 81% rename from extensions/resteasy-reactive/quarkus-resteasy-reactive-qute/deployment/src/test/java/io/quarkus/resteasy/reactive/qute/deployment/TemplateResponseFilterTest.java rename to extensions/resteasy-reactive/quarkus-resteasy-reactive-qute/deployment/src/test/java/io/quarkus/resteasy/reactive/qute/deployment/TemplateResultTest.java index 4f1df75d7bd6a..32e80ecb0c07c 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-qute/deployment/src/test/java/io/quarkus/resteasy/reactive/qute/deployment/TemplateResponseFilterTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-qute/deployment/src/test/java/io/quarkus/resteasy/reactive/qute/deployment/TemplateResultTest.java @@ -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() @@ -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!")); @@ -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!")); } } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-qute/deployment/src/test/java/io/quarkus/resteasy/reactive/qute/deployment/VariantTemplateTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-qute/deployment/src/test/java/io/quarkus/resteasy/reactive/qute/deployment/VariantTemplateTest.java index c60ce2222b587..3aeea509de1c9 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-qute/deployment/src/test/java/io/quarkus/resteasy/reactive/qute/deployment/VariantTemplateTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-qute/deployment/src/test/java/io/quarkus/resteasy/reactive/qute/deployment/VariantTemplateTest.java @@ -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("Item foo: 20")); + 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("Item foo: 20")); } } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-qute/runtime/src/main/java/io/quarkus/resteasy/reactive/qute/runtime/TemplateResponseFilter.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-qute/runtime/src/main/java/io/quarkus/resteasy/reactive/qute/runtime/TemplateResponseFilter.java index 5cc8c7cc53de7..f82d62ac5570a 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-qute/runtime/src/main/java/io/quarkus/resteasy/reactive/qute/runtime/TemplateResponseFilter.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-qute/runtime/src/main/java/io/quarkus/resteasy/reactive/qute/runtime/TemplateResponseFilter.java @@ -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 filter(ResteasyReactiveContainerRequestContext requestContext, ContainerResponseContext responseContext) { Object entity = responseContext.getEntity(); @@ -33,41 +32,15 @@ public Uni filter(ResteasyReactiveContainerRequestContext requestContext, MediaType mediaType; TemplateInstance instance = (TemplateInstance) entity; - Object variantsAttr = instance.getAttribute(TemplateInstance.VARIANTS); - if (variantsAttr != null) { - List variants = new ArrayList<>(); - for (Variant variant : (List) 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 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 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 uni = toUni(instance, engine); return uni.chain(r -> { if (mediaType != null) { responseContext.setEntity(r, null, mediaType); @@ -77,4 +50,5 @@ public Uni filter(ResteasyReactiveContainerRequestContext requestContext, return Uni.createFrom().nullItem(); }); } + } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-qute/runtime/src/main/java/io/quarkus/resteasy/reactive/qute/runtime/TemplateResponseUniHandler.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-qute/runtime/src/main/java/io/quarkus/resteasy/reactive/qute/runtime/TemplateResponseUniHandler.java new file mode 100644 index 0000000000000..c54f4a3c43eda --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-qute/runtime/src/main/java/io/quarkus/resteasy/reactive/qute/runtime/TemplateResponseUniHandler.java @@ -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)); + } + + private Uni createUni(ResteasyReactiveRequestContext requestContext, TemplateInstance result) { + setSelectedVariant(result, requestContext.getRequest(), + requestContext.getHttpHeaders().getAcceptableLanguages()); + return toUni(result, engine); + } + +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-qute/runtime/src/main/java/io/quarkus/resteasy/reactive/qute/runtime/Util.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-qute/runtime/src/main/java/io/quarkus/resteasy/reactive/qute/runtime/Util.java new file mode 100644 index 0000000000000..0efd0bc28c2b0 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-qute/runtime/src/main/java/io/quarkus/resteasy/reactive/qute/runtime/Util.java @@ -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 toUni(TemplateInstance instance, Engine engine) { + Uni 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 acceptableLanguages) { + Object variantsAttr = result.getAttribute(TemplateInstance.VARIANTS); + if (variantsAttr != null) { + List quteVariants = (List) variantsAttr; + List 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; + } +} diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeResourceDeployment.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeResourceDeployment.java index e155cc0c16a5a..68026d254ba36 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeResourceDeployment.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeResourceDeployment.java @@ -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); } diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/model/HandlerChainCustomizer.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/model/HandlerChainCustomizer.java index 568bbcfc5e0bc..ba82360ad8927 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/model/HandlerChainCustomizer.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/model/HandlerChainCustomizer.java @@ -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} */