Skip to content

Commit

Permalink
Qute: add Singleton scope to a Named Java record
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
mkouba committed Oct 8, 2024
1 parent 7f5dc76 commit 12d862e
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 2 deletions.
20 changes: 18 additions & 2 deletions docs/src/main/asciidoc/qute-reference.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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 <<template_extension_methods,template extension method>> 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 <<template_extension_methods,template extension method>> must exist.

Check warning on line 1714 in docs/src/main/asciidoc/qute-reference.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsWarnings] Consider using 'for example' rather than 'e.g.' unless updating existing content that uses the term. Raw Output: {"message": "[Quarkus.TermsWarnings] Consider using 'for example' rather than 'e.g.' unless updating existing content that uses the term.", "location": {"path": "docs/src/main/asciidoc/qute-reference.adoc", "range": {"start": {"line": 1714, "column": 125}}}, "severity": "WARNING"}

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')}`.

Check warning on line 1718 in docs/src/main/asciidoc/qute-reference.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsWarnings] Consider using 'through', 'by', 'from', 'on', or 'by using' rather than 'via' unless updating existing content that uses the term. Raw Output: {"message": "[Quarkus.TermsWarnings] Consider using 'through', 'by', 'from', 'on', or 'by using' rather than 'via' unless updating existing content that uses the term.", "location": {"path": "docs/src/main/asciidoc/qute-reference.adoc", "range": {"start": {"line": 1718, "column": 144}}}, "severity": "WARNING"}

Sometimes it may be necessary to access public methods and properties of a CDI bean that is not annotated with `@Named`.

Check warning on line 1720 in docs/src/main/asciidoc/qute-reference.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsWarnings] Consider using 'might (for possibility)' or 'can (for ability)' rather than 'may' unless updating existing content that uses the term. Raw Output: {"message": "[Quarkus.TermsWarnings] Consider using 'might (for possibility)' or 'can (for ability)' rather than 'may' unless updating existing content that uses the term.", "location": {"path": "docs/src/main/asciidoc/qute-reference.adoc", "range": {"start": {"line": 1720, "column": 14}}}, "severity": "WARNING"}
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`.

Check warning on line 1732 in docs/src/main/asciidoc/qute-reference.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsSuggestions] Depending on the context, consider using 'because' or 'while' rather than 'as'. Raw Output: {"message": "[Quarkus.TermsSuggestions] Depending on the context, consider using 'because' or 'while' rather than 'as'.", "location": {"path": "docs/src/main/asciidoc/qute-reference.adoc", "range": {"start": {"line": 1732, "column": 158}}}, "severity": "INFO"}
<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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -3338,6 +3340,20 @@ void validateTemplateDataNamespaces(List<TemplateDataBuildItem> 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<AnnotationInstance> annotations, IndexView index) {
return clazz.isRecord();
}

static Map<TemplateAnalysis, Set<Expression>> collectNamespaceExpressions(TemplatesAnalysisBuildItem analysis,
String namespace) {
Map<TemplateAnalysis, Set<Expression>> namespaceExpressions = new HashMap<>();
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> names) {
}

@Singleton
public static class ListProducer {

@Produces
List<String> names() {
return List.of("Jachym", "Vojtech", "Ondrej");
}
}

}

0 comments on commit 12d862e

Please sign in to comment.