From 31848b81b124e066ca666d84ece6c57b52e8460b Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Tue, 30 Jul 2024 13:20:18 +0200 Subject: [PATCH] Qute: support synthetic named CDI beans injected in templates - this requires an additional late initialization of the QuteContext bean --- .../qute/deployment/QuteProcessor.java | 34 ++++++++++------ .../inject/InjectNamespaceResolverTest.java | 23 ++++++++++- .../io/quarkus/qute/runtime/QuteRecorder.java | 40 +++++++++++++++++-- 3 files changed, 80 insertions(+), 17 deletions(-) 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 f40f1eeb724aa..193fd8f588f78 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 @@ -61,15 +61,17 @@ import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.arc.deployment.BeanArchiveIndexBuildItem; +import io.quarkus.arc.deployment.BeanContainerBuildItem; import io.quarkus.arc.deployment.BeanDefiningAnnotationBuildItem; -import io.quarkus.arc.deployment.BeanDiscoveryFinishedBuildItem; import io.quarkus.arc.deployment.CompletedApplicationClassPredicateBuildItem; import io.quarkus.arc.deployment.QualifierRegistrarBuildItem; +import io.quarkus.arc.deployment.SynthesisFinishedBuildItem; import io.quarkus.arc.deployment.SyntheticBeanBuildItem; import io.quarkus.arc.deployment.ValidationPhaseBuildItem; import io.quarkus.arc.deployment.ValidationPhaseBuildItem.ValidationErrorBuildItem; import io.quarkus.arc.processor.Annotations; import io.quarkus.arc.processor.BeanInfo; +import io.quarkus.arc.processor.BuiltinScope; import io.quarkus.arc.processor.DotNames; import io.quarkus.arc.processor.InjectionPointInfo; import io.quarkus.arc.processor.QualifierRegistrar; @@ -951,7 +953,7 @@ void validateExpressions(TemplatesAnalysisBuildItem templatesAnalysis, BuildProducer incorrectExpressions, BuildProducer implicitClasses, BuildProducer expressionMatches, - BeanDiscoveryFinishedBuildItem beanDiscovery, + SynthesisFinishedBuildItem synthesisFinished, List checkedTemplates, List templateData, QuteConfig config, @@ -970,10 +972,7 @@ public String apply(String id) { return findTemplatePath(templatesAnalysis, id); } }; - // IMPLEMENTATION NOTE: - // We do not support injection of synthetic beans with names - // Dependency on the ValidationPhaseBuildItem would result in a cycle in the build chain - Map namedBeans = beanDiscovery.beanStream().withName() + Map namedBeans = synthesisFinished.beanStream().withName() .collect(toMap(BeanInfo::getName, Function.identity())); // Map implicit class -> set of used members Map> implicitClassToMembersUsed = new HashMap<>(); @@ -2447,9 +2446,7 @@ public boolean test(TypeCheck check) { @BuildStep @Record(value = STATIC_INIT) void initialize(BuildProducer syntheticBeans, QuteRecorder recorder, - List generatedValueResolvers, List templatePaths, - Optional templateVariants, - List templateInitializers, + List templatePaths, Optional templateVariants, TemplateRootsBuildItem templateRoots) { List templates = new ArrayList<>(); @@ -2475,14 +2472,25 @@ void initialize(BuildProducer syntheticBeans, QuteRecord } syntheticBeans.produce(SyntheticBeanBuildItem.configure(QuteContext.class) - .supplier(recorder.createContext(generatedValueResolvers.stream() - .map(GeneratedValueResolverBuildItem::getClassName).collect(Collectors.toList()), templates, - tags, variants, templateInitializers.stream() - .map(TemplateGlobalProviderBuildItem::getClassName).collect(Collectors.toList()), + .scope(BuiltinScope.SINGLETON.getInfo()) + .supplier(recorder.createContext(templates, + tags, variants, templateRoots.getPaths().stream().map(p -> p + "/").collect(Collectors.toSet()), templateContents)) .done()); } + @BuildStep + @Record(value = STATIC_INIT) + void initializeGeneratedClasses(BeanContainerBuildItem beanContainer, QuteRecorder recorder, + List generatedValueResolvers, + List templateInitializers) { + // The generated classes must be initialized after the template expressions are validated in order to break the cycle in the build chain + recorder.initializeGeneratedClasses(generatedValueResolvers.stream() + .map(GeneratedValueResolverBuildItem::getClassName).collect(Collectors.toList()), + templateInitializers.stream() + .map(TemplateGlobalProviderBuildItem::getClassName).collect(Collectors.toList())); + } + @BuildStep QualifierRegistrarBuildItem turnLocationIntoQualifier() { 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 e3bca7a111002..6ba4437a63ec8 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 @@ -14,10 +14,14 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; +import io.quarkus.arc.deployment.SyntheticBeanBuildItem; +import io.quarkus.builder.BuildContext; +import io.quarkus.builder.BuildStep; import io.quarkus.qute.Qute; import io.quarkus.qute.Template; import io.quarkus.qute.deployment.Hello; import io.quarkus.test.QuarkusUnitTest; +import io.smallrye.common.annotation.Identifier; public class InjectNamespaceResolverTest { @@ -28,7 +32,21 @@ public class InjectNamespaceResolverTest { .addAsResource( new StringAsset( "{inject:hello.ping} != {inject:simple.ping} and {cdi:hello.ping} != {cdi:simple.ping}"), - "templates/foo.html")); + "templates/foo.html")) + .addBuildChainCustomizer(bcb -> { + bcb.addBuildStep(new BuildStep() { + @Override + public void execute(BuildContext context) { + context.produce(SyntheticBeanBuildItem.configure(String.class) + .addQualifier().annotation(Identifier.class).addValue("value", "synthetic").done() + .name("synthetic") + .creator(mc -> { + mc.returnValue(mc.load("Yes!")); + }) + .done()); + } + }).produces(SyntheticBeanBuildItem.class).build(); + }); @Inject Template foo; @@ -45,6 +63,9 @@ public void testInjection() { assertEquals("pong::<br>", Qute.fmt("{cdi:hello.ping}::{newLine}").contentType("text/html").data("newLine", "
").render()); assertEquals(2, SimpleBean.DESTROYS.longValue()); + + // Test a synthetic named bean injected in a template + assertEquals("YES!", Qute.fmt("{cdi:synthetic.toUpperCase}").render()); } @Named("simple") diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteRecorder.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteRecorder.java index 2e53fe166580d..60797098731d9 100644 --- a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteRecorder.java +++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/QuteRecorder.java @@ -5,20 +5,23 @@ import java.util.Set; import java.util.function.Supplier; +import io.quarkus.arc.Arc; import io.quarkus.runtime.annotations.Recorder; @Recorder public class QuteRecorder { - public Supplier createContext(List resolverClasses, - List templatePaths, List tags, Map> variants, - List templateGlobalProviderClasses, Set templateRoots, Map templateContents) { + public Supplier createContext(List templatePaths, List tags, Map> variants, + Set templateRoots, Map templateContents) { return new Supplier() { @Override public Object get() { return new QuteContext() { + volatile List resolverClasses; + volatile List templateGlobalProviderClasses; + @Override public List getTemplatePaths() { return templatePaths; @@ -31,6 +34,9 @@ public List getTags() { @Override public List getResolverClasses() { + if (resolverClasses == null) { + throw generatedClassesNotInitialized(); + } return resolverClasses; } @@ -41,6 +47,9 @@ public Map> getVariants() { @Override public List getTemplateGlobalProviderClasses() { + if (templateGlobalProviderClasses == null) { + throw generatedClassesNotInitialized(); + } return templateGlobalProviderClasses; } @@ -53,11 +62,27 @@ public Set getTemplateRoots() { public Map getTemplateContents() { return templateContents; } + + @Override + public void setGeneratedClasses(List resolverClasses, List templateGlobalProviderClasses) { + this.resolverClasses = resolverClasses; + this.templateGlobalProviderClasses = templateGlobalProviderClasses; + } + + private IllegalStateException generatedClassesNotInitialized() { + return new IllegalStateException("Generated classes not initialized yet!"); + } + }; } }; } + public void initializeGeneratedClasses(List resolverClasses, List templateGlobalProviderClasses) { + QuteContext context = Arc.container().instance(QuteContext.class).get(); + context.setGeneratedClasses(resolverClasses, templateGlobalProviderClasses); + } + public interface QuteContext { List getResolverClasses(); @@ -74,6 +99,15 @@ public interface QuteContext { Map getTemplateContents(); + /** + * The generated classes must be initialized after the template expressions are validated (later during the STATIC_INIT + * bootstrap phase) in order to break the cycle in the build chain. + * + * @param resolverClasses + * @param templateGlobalProviderClasses + */ + void setGeneratedClasses(List resolverClasses, List templateGlobalProviderClasses); + } }