diff --git a/docs/src/main/asciidoc/qute-reference.adoc b/docs/src/main/asciidoc/qute-reference.adoc index 33ae9229fc32b..e9713c453f07e 100644 --- a/docs/src/main/asciidoc/qute-reference.adoc +++ b/docs/src/main/asciidoc/qute-reference.adoc @@ -1279,6 +1279,8 @@ A CDI bean annotated with `@Named` can be referenced in any template through `cd <1> First, a bean with name `personService` is found and then used as the base object. <2> First, a bean with name `foo` is found and then used as the base object. +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. diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/PropertyNotFoundDevModeTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/PropertyNotFoundDevModeTest.java index 2eb14743b3e15..7f5fcbe56d0c7 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/PropertyNotFoundDevModeTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/PropertyNotFoundDevModeTest.java @@ -26,10 +26,10 @@ public class PropertyNotFoundDevModeTest { @Test public void testExceptionIsThrown() { - assertEquals("Entry \"foo\" not found in the data map in expression {foo.surname} in template foo on line 1", + assertEquals("Entry \"foo\" not found in the data map in expression {foo.surname} in template foo.html on line 1", RestAssured.get("test-foo").then().statusCode(200).extract().body().asString()); assertEquals( - "Property \"name\" not found on the base object \"java.lang.String\" in expression {bar.name} in template bar on line 1", + "Property \"name\" not found on the base object \"java.lang.String\" in expression {bar.name} in template bar.html on line 1", RestAssured.get("test-bar").then().statusCode(200).extract().body().asString()); } diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/inject/InjectNamespaceResolverTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/inject/InjectNamespaceResolverTest.java index e06cd09c13a1b..5e7ca7bb96d2c 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/inject/InjectNamespaceResolverTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/inject/InjectNamespaceResolverTest.java @@ -2,6 +2,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.LongAdder; import javax.annotation.PreDestroy; @@ -34,24 +35,30 @@ public class InjectNamespaceResolverTest { @Test public void testInjection() { - assertEquals("pong != simple and pong != simple", foo.render()); - assertEquals(2, SimpleBean.DESTROYS.longValue()); + assertEquals("pong != simple1 and pong != simple1", foo.render()); + assertEquals(1, SimpleBean.DESTROYS.longValue()); // Test the convenient Qute class // By default, the content type is plain text - assertEquals("pong::
", Qute.fmt("{cdi:hello.ping}::{}", "
")); + assertEquals("pong::simple2::simple2::
", + Qute.fmt("{cdi:hello.ping}::{cdi:simple.ping}::{inject:simple.ping}::{}", "
")); assertEquals("pong::<br>", Qute.fmt("{cdi:hello.ping}::{newLine}").contentType("text/html").data("newLine", "
").render()); + assertEquals(2, SimpleBean.DESTROYS.longValue()); } @Named("simple") @Dependent public static class SimpleBean { + static final AtomicInteger COUNTER = new AtomicInteger(); + static final LongAdder DESTROYS = new LongAdder(); + private final int id = COUNTER.incrementAndGet(); + public String ping() { - return "simple"; + return "simple" + id; } @PreDestroy diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/EngineProducer.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/EngineProducer.java index ea5bbc2e64752..d5ba946c79870 100644 --- a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/EngineProducer.java +++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/EngineProducer.java @@ -7,13 +7,19 @@ import java.net.URL; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; import java.util.Optional; -import java.util.function.Function; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import java.util.regex.Pattern; import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.context.Dependent; import javax.enterprise.event.Event; import javax.enterprise.event.Observes; import javax.enterprise.inject.Produces; @@ -23,17 +29,22 @@ import org.jboss.logging.Logger; import io.quarkus.arc.Arc; +import io.quarkus.arc.ArcContainer; +import io.quarkus.arc.InjectableBean; import io.quarkus.arc.InstanceHandle; import io.quarkus.qute.Engine; import io.quarkus.qute.EngineBuilder; import io.quarkus.qute.EvalContext; +import io.quarkus.qute.Expression; import io.quarkus.qute.HtmlEscaper; import io.quarkus.qute.NamespaceResolver; import io.quarkus.qute.Qute; import io.quarkus.qute.ReflectionValueResolver; import io.quarkus.qute.Resolver; import io.quarkus.qute.Results; +import io.quarkus.qute.Template; import io.quarkus.qute.TemplateInstance; +import io.quarkus.qute.TemplateInstance.Initializer; import io.quarkus.qute.TemplateLocator.TemplateLocation; import io.quarkus.qute.UserTagSectionHelper; import io.quarkus.qute.ValueResolver; @@ -51,6 +62,7 @@ public class EngineProducer { public static final String INJECT_NAMESPACE = "inject"; public static final String CDI_NAMESPACE = "cdi"; + public static final String DEPENDENT_INSTANCES = "q_dep_inst"; private static final String TAGS = "tags/"; @@ -65,6 +77,7 @@ public class EngineProducer { private final Pattern templatePathExclude; private final Locale defaultLocale; private final Charset defaultCharset; + private final ArcContainer container; public EngineProducer(QuteContext context, QuteConfig config, QuteRuntimeConfig runtimeConfig, Event builderReady, Event engineReady, ContentTypes contentTypes, LaunchMode launchMode, @@ -77,6 +90,7 @@ public EngineProducer(QuteContext context, QuteConfig config, QuteRuntimeConfig this.templatePathExclude = config.templatePathExclude; this.defaultLocale = locales.defaultLocale; this.defaultCharset = config.defaultCharset; + this.container = Arc.container(); LOGGER.debugf("Initializing Qute [templates: %s, tags: %s, resolvers: %s", context.getTemplatePaths(), tags, context.getResolverClasses()); @@ -146,17 +160,8 @@ public EngineProducer(QuteContext context, QuteConfig config, QuteRuntimeConfig builderReady.fire(builder); // Resolve @Named beans - Function cdiFun = new Function() { - - @Override - public Object apply(EvalContext ctx) { - try (InstanceHandle bean = Arc.container().instance(ctx.getName())) { - return bean.isAvailable() ? bean.get() : Results.NotFound.from(ctx); - } - } - }; - builder.addNamespaceResolver(NamespaceResolver.builder(INJECT_NAMESPACE).resolve(cdiFun).build()); - builder.addNamespaceResolver(NamespaceResolver.builder(CDI_NAMESPACE).resolve(cdiFun).build()); + builder.addNamespaceResolver(NamespaceResolver.builder(INJECT_NAMESPACE).resolve(this::resolveInject).build()); + builder.addNamespaceResolver(NamespaceResolver.builder(CDI_NAMESPACE).resolve(this::resolveInject).build()); // Add generated resolvers for (String resolverClass : context.getResolverClasses()) { @@ -187,15 +192,70 @@ public Object apply(EvalContext ctx) { builder.addTemplateInstanceInitializer(createInitializer(initializerClass)); } + // Add a special initializer for templates that contain a inject/cdi namespace expressions + Map discoveredInjectTemplates = new HashMap<>(); + builder.addTemplateInstanceInitializer(new Initializer() { + + @Override + public void accept(TemplateInstance instance) { + Boolean hasInject = discoveredInjectTemplates.get(instance.getTemplate().getGeneratedId()); + if (hasInject == null) { + hasInject = hasInjectExpression(instance.getTemplate()); + } + if (hasInject) { + // Add dependent beans map if the template contains a cdi namespace expression + instance.setAttribute(DEPENDENT_INSTANCES, new ConcurrentHashMap<>()); + // Add a close action to destroy all dependent beans + instance.onRendered(new Runnable() { + @Override + public void run() { + Object dependentInstances = instance.getAttribute(EngineProducer.DEPENDENT_INSTANCES); + if (dependentInstances != null) { + @SuppressWarnings("unchecked") + ConcurrentMap> existing = (ConcurrentMap>) dependentInstances; + if (!existing.isEmpty()) { + for (InstanceHandle handle : existing.values()) { + handle.close(); + } + } + } + } + }); + } + } + }); + builder.timeout(runtimeConfig.timeout); builder.useAsyncTimeout(runtimeConfig.useAsyncTimeout); engine = builder.build(); - // Load discovered templates + // Load discovered template files + Map> discovered = new HashMap<>(); for (String path : context.getTemplatePaths()) { - engine.getTemplate(path); + Template template = engine.getTemplate(path); + if (template != null) { + for (String suffix : config.suffixes) { + if (path.endsWith(suffix)) { + String pathNoSuffix = path.substring(0, path.length() - (suffix.length() + 1)); + List