From 6113722c70a27be1426ef35265ffa652b797208d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20=C3=89pardaud?= Date: Thu, 9 Nov 2023 16:21:29 +0100 Subject: [PATCH] Greatly simplify TemplateInstance passing of locale and renderArgs Fixes localisation via type-safe messages for emails --- deployment/pom.xml | 5 ++ .../renarde/deployment/RenardeProcessor.java | 44 ----------------- .../renarde/test/LanguageTest.java | 44 +++++++++++++++++ .../io/quarkiverse/renarde/util/Filters.java | 23 --------- .../renarde/util/QuteResolvers.java | 18 +++++++ .../renarde/util/TemplateResponseHandler.java | 47 ------------------- 6 files changed, 67 insertions(+), 114 deletions(-) delete mode 100644 runtime/src/main/java/io/quarkiverse/renarde/util/TemplateResponseHandler.java diff --git a/deployment/pom.xml b/deployment/pom.xml index aa6d8a85..fdfa8f81 100644 --- a/deployment/pom.xml +++ b/deployment/pom.xml @@ -63,6 +63,11 @@ quarkus-junit5-internal test + + io.quarkus + quarkus-mailer + test + io.rest-assured rest-assured diff --git a/deployment/src/main/java/io/quarkiverse/renarde/deployment/RenardeProcessor.java b/deployment/src/main/java/io/quarkiverse/renarde/deployment/RenardeProcessor.java index cb2e2066..53e861e7 100644 --- a/deployment/src/main/java/io/quarkiverse/renarde/deployment/RenardeProcessor.java +++ b/deployment/src/main/java/io/quarkiverse/renarde/deployment/RenardeProcessor.java @@ -1,8 +1,5 @@ package io.quarkiverse.renarde.deployment; -import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.COMPLETION_STAGE; -import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.UNI; - import java.io.File; import java.io.FileWriter; import java.io.IOException; @@ -18,7 +15,6 @@ import java.util.Arrays; import java.util.Base64; import java.util.Collection; -import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -49,7 +45,6 @@ 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.logging.Logger; import org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames; @@ -57,9 +52,6 @@ import org.jboss.resteasy.reactive.common.processor.transformation.AnnotationsTransformer.TransformationContext; import org.jboss.resteasy.reactive.common.processor.transformation.Transformation; import org.jboss.resteasy.reactive.common.util.URLUtils; -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.quarkiverse.renarde.Controller; import io.quarkiverse.renarde.deployment.ControllerVisitor.ControllerClass; @@ -83,7 +75,6 @@ import io.quarkiverse.renarde.util.RenardeJWTAuthMechanism; import io.quarkiverse.renarde.util.RenardeValidationLocaleResolver; import io.quarkiverse.renarde.util.RenderArgs; -import io.quarkiverse.renarde.util.TemplateResponseHandler; import io.quarkiverse.renarde.util.Validation; import io.quarkus.arc.Unremovable; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; @@ -132,7 +123,6 @@ import io.quarkus.hibernate.validator.runtime.jaxrs.ResteasyReactiveEndPointValidationInterceptor; import io.quarkus.qute.TemplateInstance; import io.quarkus.resteasy.reactive.server.spi.AnnotationsTransformerBuildItem; -import io.quarkus.resteasy.reactive.server.spi.MethodScannerBuildItem; import io.quarkus.resteasy.reactive.spi.AdditionalResourceClassBuildItem; import io.quarkus.resteasy.reactive.spi.CustomExceptionMapperBuildItem; import io.quarkus.resteasy.reactive.spi.ParamConverterBuildItem; @@ -882,38 +872,4 @@ void findMessageFiles(RenardeRecorder recorder, logger.infof("Supported locales with messages: %s", languageToPath.keySet()); } } - - @BuildStep - public MethodScannerBuildItem configureHandler() { - // we register a scanner that runs in the first phase, to be before Qute's TemplateResponseUniHandler - // which means we run the risk of not having any Uni or CompletionStage unpacked yet, but that's fine - // we will hook into them - return new MethodScannerBuildItem(new MethodScanner() { - @Override - public List scan(MethodInfo method, ClassInfo actualEndpointClass, - Map methodContext) { - if (method.returnType().name().equals(DOTNAME_TEMPLATE_INSTANCE) - || isAsyncTemplateInstance(method.returnType())) { - return Collections.singletonList( - new FixedHandlersChainCustomizer( - List.of(new TemplateResponseHandler()), - HandlerChainCustomizer.Phase.AFTER_METHOD_INVOKE)); - } - 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(DOTNAME_TEMPLATE_INSTANCE); - } - } - return isAsyncTemplateInstance; - } - }); - } } diff --git a/deployment/src/test/java/io/quarkiverse/renarde/test/LanguageTest.java b/deployment/src/test/java/io/quarkiverse/renarde/test/LanguageTest.java index a7445c79..dd9562b0 100644 --- a/deployment/src/test/java/io/quarkiverse/renarde/test/LanguageTest.java +++ b/deployment/src/test/java/io/quarkiverse/renarde/test/LanguageTest.java @@ -1,9 +1,11 @@ package io.quarkiverse.renarde.test; import java.net.URL; +import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; +import jakarta.inject.Inject; import jakarta.validation.constraints.NotEmpty; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; @@ -16,11 +18,15 @@ import org.jboss.shrinkwrap.api.asset.EmptyAsset; import org.jboss.shrinkwrap.api.asset.StringAsset; import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import io.quarkiverse.renarde.Controller; import io.quarkiverse.renarde.util.I18N; +import io.quarkus.mailer.Mail; +import io.quarkus.mailer.MailTemplate.MailTemplateInstance; +import io.quarkus.mailer.MockMailbox; import io.quarkus.qute.CheckedTemplate; import io.quarkus.qute.TemplateInstance; import io.quarkus.qute.i18n.Message; @@ -67,6 +73,9 @@ public class LanguageTest { + "{m:my.params('STEF')}\n" + "{m:missing}"), "templates/MyController/typeUnsafe.txt") + .addAsResource(new StringAsset("{m:my_greeting}\n" + + "{msg:my_greeting}"), + "templates/MyController/mail.txt") .addAsResource(new StringAsset("quarkus.locales=en,fr\n" + "quarkus.default-locale=en"), "application.properties") @@ -75,6 +84,9 @@ public class LanguageTest { @TestHTTPResource URL url; + @Inject + MockMailbox mailbox; + @Test public void testDefaultLanguage() { RestAssured @@ -197,6 +209,30 @@ public void testTypeUnsafeLanguage() { .statusCode(500); } + @Test + public void testMail() { + mailbox.clear(); + RestAssured + .get("/mail").then() + .statusCode(200) + .body(Matchers.is("OK")); + List mails = mailbox.getMailsSentTo("foo@example.com"); + Assertions.assertEquals(1, mails.size()); + Assertions.assertEquals("english message\nenglish message", mails.get(0).getText()); + + mailbox.clear(); + RestAssured + .given() + .cookie(I18N.LOCALE_COOKIE_NAME, "fr") + .get("/mail").then() + .statusCode(200) + .body(Matchers.is("OK")); + + mails = mailbox.getMailsSentTo("foo@example.com"); + Assertions.assertEquals(1, mails.size()); + Assertions.assertEquals("message français\nmessage français", mails.get(0).getText()); + } + @Test public void testValidationLanguage() { RestAssured @@ -220,6 +256,8 @@ public static class Templates { public static native TemplateInstance qute(); public static native TemplateInstance typeUnsafe(Object val); + + public static native MailTemplateInstance mail(); } @Path("/qute") @@ -258,6 +296,12 @@ public String validation(@NotEmpty @RestForm String param) { return validation.getError("param"); } + @Path("/mail") + public String mail() { + Templates.mail().to("foo@example.com").send().await().indefinitely(); + return "OK"; + } + @Path("/lang") public String lang() { return i18n.getLanguage(); diff --git a/runtime/src/main/java/io/quarkiverse/renarde/util/Filters.java b/runtime/src/main/java/io/quarkiverse/renarde/util/Filters.java index 3cc0c41e..6f398be5 100644 --- a/runtime/src/main/java/io/quarkiverse/renarde/util/Filters.java +++ b/runtime/src/main/java/io/quarkiverse/renarde/util/Filters.java @@ -1,7 +1,5 @@ package io.quarkiverse.renarde.util; -import java.util.Map.Entry; - import jakarta.inject.Inject; import jakarta.ws.rs.Priorities; import jakarta.ws.rs.container.ContainerResponseContext; @@ -10,16 +8,11 @@ import org.jboss.resteasy.reactive.server.ServerResponseFilter; import org.jboss.resteasy.reactive.server.spi.ResteasyReactiveContainerRequestContext; -import io.quarkus.qute.TemplateInstance; -import io.quarkus.qute.i18n.MessageBundles; import io.vertx.core.http.HttpServerRequest; import io.vertx.core.http.HttpServerResponse; public class Filters { - @Inject - RenderArgs renderArgs; - @Inject Flash flash; @@ -35,23 +28,7 @@ public void filterRequest(ResteasyReactiveContainerRequestContext requestContext // this must run before the Qute response filter @ServerResponseFilter(priority = Priorities.USER + 1000) // sorted in reverse order public void filterResponse(ContainerResponseContext responseContext, HttpServerResponse resp) { - Object entity = responseContext.getEntity(); - // this pass only handles methods that return Response or RestResponse, the others are handled in TemplateResponseHandler - if (entity instanceof TemplateInstance) { - TemplateInstance template = (TemplateInstance) entity; - setTemplateLocaleAndRenderArgs(template); - } i18n.setLanguageCookie(); flash.setFlashCookie(); } - - void setTemplateLocaleAndRenderArgs(TemplateInstance template) { - // extra parameters - for (Entry entry : renderArgs.entrySet()) { - template.data(entry.getKey(), entry.getValue()); - } - // set the proper locale - template.setAttribute(MessageBundles.ATTRIBUTE_LOCALE, i18n.getLanguage()); - } - } diff --git a/runtime/src/main/java/io/quarkiverse/renarde/util/QuteResolvers.java b/runtime/src/main/java/io/quarkiverse/renarde/util/QuteResolvers.java index ec207eaf..806df6b8 100644 --- a/runtime/src/main/java/io/quarkiverse/renarde/util/QuteResolvers.java +++ b/runtime/src/main/java/io/quarkiverse/renarde/util/QuteResolvers.java @@ -4,6 +4,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map.Entry; import java.util.concurrent.CompletionStage; import java.util.function.BiFunction; @@ -17,6 +18,7 @@ import io.quarkus.qute.Expression; import io.quarkus.qute.NamespaceResolver; import io.quarkus.qute.ValueResolver; +import io.quarkus.qute.i18n.MessageBundles; import io.smallrye.mutiny.Uni; public class QuteResolvers { @@ -128,6 +130,22 @@ void configureEngine(@Observes EngineBuilder builder) { .build()); } + void registerTemplateInstanceLocaleAndRenderArgs(@Observes EngineBuilder engineBuilder, I18N i18n, RenderArgs renderArgs) { + engineBuilder.addTemplateInstanceInitializer(templateInstance -> { + // This should work if `I18N` is a request scoped bean + if (Arc.container().requestContext().isActive()) { + if (i18n.getLocale() != null + && templateInstance.getAttribute(MessageBundles.ATTRIBUTE_LOCALE) == null) { + templateInstance.setAttribute(MessageBundles.ATTRIBUTE_LOCALE, i18n.getLocale()); + } + // extra parameters + for (Entry entry : renderArgs.entrySet()) { + templateInstance.data(entry.getKey(), entry.getValue()); + } + } + }); + } + private URI findURI(EvalContext ctx, List paramValues) { BoundRouter boundRouter = (BoundRouter) ctx.getBase(); // FIXME: make it work for multiple sets of parameters? with optional query params that's a bit harder diff --git a/runtime/src/main/java/io/quarkiverse/renarde/util/TemplateResponseHandler.java b/runtime/src/main/java/io/quarkiverse/renarde/util/TemplateResponseHandler.java deleted file mode 100644 index 6c1c7a2f..00000000 --- a/runtime/src/main/java/io/quarkiverse/renarde/util/TemplateResponseHandler.java +++ /dev/null @@ -1,47 +0,0 @@ -package io.quarkiverse.renarde.util; - -import java.util.concurrent.CompletionStage; - -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.TemplateInstance; -import io.smallrye.mutiny.Uni; - -public class TemplateResponseHandler implements ServerRestHandler { - - @Override - public void handle(ResteasyReactiveRequestContext requestContext) { - Object result = requestContext.getResult(); - // we run in the first phase, so we could have a TemplateInstance, a Uni or a CompletionStage - // we do not care about Reponse and RestResponse, this is handled by Filters - if (result instanceof TemplateInstance) { - setRenderArgsAndLocale(requestContext, (TemplateInstance) result); - } else if (result instanceof Uni) { - Uni res = (Uni) result; - // hook into the Uni - requestContext.setResult(res.invoke(resolved -> { - if (resolved instanceof TemplateInstance) { - setRenderArgsAndLocale(requestContext, (TemplateInstance) resolved); - } - })); - } else if (result instanceof CompletionStage) { - CompletionStage res = (CompletionStage) result; - // hook into the CompletionStage - requestContext.setResult(res.thenApply(resolved -> { - if (resolved instanceof TemplateInstance) { - setRenderArgsAndLocale(requestContext, (TemplateInstance) resolved); - } - return resolved; - })); - } - } - - private void setRenderArgsAndLocale(ResteasyReactiveRequestContext requestContext, TemplateInstance templateInstance) { - requestContext.requireCDIRequestScope(); - Filters filters = Arc.container().instance(Filters.class).get(); - filters.setTemplateLocaleAndRenderArgs(templateInstance); - } - -}