Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Qute: add Singleton scope to a Named Java record #43775

Merged
merged 1 commit into from
Oct 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
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");
}
}

}