From 12d862e22d2f8326cdeaa21c48c81c2f2de401a0 Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Tue, 8 Oct 2024 17:06:24 +0200 Subject: [PATCH] Qute: add Singleton scope to a Named Java record - so that it can be easily used as an intermediate CDI bean for beans that are not annotated with jakarta.inject.Named - related to #41932 --- docs/src/main/asciidoc/qute-reference.adoc | 20 ++++++- .../qute/deployment/QuteProcessor.java | 16 ++++++ .../deployment/inject/NamedRecordTest.java | 52 +++++++++++++++++++ 3 files changed, 86 insertions(+), 2 deletions(-) create mode 100644 extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/inject/NamedRecordTest.java diff --git a/docs/src/main/asciidoc/qute-reference.adoc b/docs/src/main/asciidoc/qute-reference.adoc index 5cdeab97b1833..68a15868301c5 100644 --- a/docs/src/main/asciidoc/qute-reference.adoc +++ b/docs/src/main/asciidoc/qute-reference.adoc @@ -1710,15 +1710,31 @@ A CDI bean annotated with `@Named` can be referenced in any template through `cd NOTE: `@Named @Dependent` beans are shared across all expressions in a template for a single rendering operation, and destroyed after the rendering finished. All expressions with `cdi` and `inject` namespaces are validated during build. - For the expression `cdi:personService.findPerson(10).name`, the implementation class of the injected bean must either declare the `findPerson` method or a matching <> must exist. - For the expression `inject:foo.price`, the implementation class of the injected bean must either have the `price` property (e.g. a `getPrice()` method) or a matching <> must exist. NOTE: A `ValueResolver` is also generated for all beans annotated with `@Named` so that it's possible to access its properties without reflection. TIP: If your application serves xref:http-reference.adoc[HTTP requests] you can also inject the current `io.vertx.core.http.HttpServerRequest` via the `inject` namespace, e.g. `{inject:vertxRequest.getParam('foo')}`. +Sometimes it may be necessary to access public methods and properties of a CDI bean that is not annotated with `@Named`. +However, if you don't control the source of the bean it is not possible to add the `@Named` annotation. +Nevertheless, it is possible to create an intermediate CDI bean annotated with `@Named`. +This intermediate bean can inject the bean in question and make it accessible. +A Java record is a very convenient way to define such an intermediate CDI bean. + +[source,java] +---- +@Named <1> <2> +public record UserData(UserInfo info, @LoggedIn String username) { <3> +} +---- +<1> If no name is explicitly specified by the `value` member the https://jakarta.ee/specifications/cdi/4.1/jakarta-cdi-spec-4.1#default_name[default name is assigned] - the simple name of the bean class, after converting the first character to lower case. In this particular case, the default name is `userData`. +<2> The `@Singleton` scope is added automatically. +<3> All parameters of the canonical constructor are injection points. The accessor methods can be used to obtain the injected bean. + +And then in a template you can simply use `{cdi:userData.info}` or `{cdi:userData.username}`. + [[typesafe_expressions]] === Type-safe Expressions diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java index fd1604fe59cbf..f69aab1b19df1 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java @@ -38,6 +38,7 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; +import jakarta.inject.Named; import jakarta.inject.Singleton; import org.jboss.jandex.AnnotationInstance; @@ -60,6 +61,7 @@ import org.jboss.logging.Logger; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; +import io.quarkus.arc.deployment.AutoAddScopeBuildItem; import io.quarkus.arc.deployment.BeanArchiveIndexBuildItem; import io.quarkus.arc.deployment.BeanContainerBuildItem; import io.quarkus.arc.deployment.BeanDefiningAnnotationBuildItem; @@ -3338,6 +3340,20 @@ void validateTemplateDataNamespaces(List templateData, } } + @BuildStep + AutoAddScopeBuildItem addSingletonToNamedRecords() { + return AutoAddScopeBuildItem.builder() + .isAnnotatedWith(DotName.createSimple(Named.class)) + .and(this::isRecord) + .defaultScope(BuiltinScope.SINGLETON) + .reason("Found Java record annotated with @Named") + .build(); + } + + private boolean isRecord(ClassInfo clazz, Collection annotations, IndexView index) { + return clazz.isRecord(); + } + static Map> collectNamespaceExpressions(TemplatesAnalysisBuildItem analysis, String namespace) { Map> namespaceExpressions = new HashMap<>(); diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/inject/NamedRecordTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/inject/NamedRecordTest.java new file mode 100644 index 0000000000000..455213262cab4 --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/inject/NamedRecordTest.java @@ -0,0 +1,52 @@ +package io.quarkus.qute.deployment.inject; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; + +import jakarta.enterprise.inject.Produces; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.inject.Singleton; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.qute.Engine; +import io.quarkus.test.QuarkusUnitTest; + +public class NamedRecordTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot(root -> root + .addClasses(Beans.class, ListProducer.class) + .addAsResource( + new StringAsset( + "{#each cdi:beans.names}{it}::{/each}"), + "templates/foo.html")); + + @Inject + Engine engine; + + @Test + public void testResult() { + assertEquals("Jachym::Vojtech::Ondrej::", engine.getTemplate("foo").render()); + } + + // @Singleton is added automatically + @Named + public record Beans(List names) { + } + + @Singleton + public static class ListProducer { + + @Produces + List names() { + return List.of("Jachym", "Vojtech", "Ondrej"); + } + } + +}