From c9f1df18b0cf5320b4ed5e227dd00b81f313a121 Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Wed, 22 Jan 2025 09:37:42 +0100 Subject: [PATCH] Qute: fix template global class generation in the dev mode - if a non-application template global class is present we have to reflect this fact when assigning the priority for an application template global resolver; otherwise a conflict may occur --- .../arc/deployment/BeanArchiveProcessor.java | 13 +++--- .../deployment/GeneratedBeanBuildItem.java | 10 +++++ .../deployment/GeneratedBeanGizmoAdaptor.java | 16 ++++++- .../qute/deployment/QuteDevModeProcessor.java | 40 +++++++++++++++++ .../qute/deployment/QuteProcessor.java | 35 ++++++++++++--- .../ExistingValueResolversDevModeTest.java | 4 +- .../QuteDummyTemplateGlobalMarker.java | 8 ++++ .../devmode/TemplateGlobalDevModeTest.java | 45 +++++++++++++++++++ .../qute/deployment/devmode/TestGlobals.java | 11 +++++ .../qute/deployment/devmode/TestRoute.java | 4 +- .../generator/TemplateGlobalGenerator.java | 10 +++-- 11 files changed, 176 insertions(+), 20 deletions(-) create mode 100644 extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/devmode/QuteDummyTemplateGlobalMarker.java create mode 100644 extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/devmode/TemplateGlobalDevModeTest.java create mode 100644 extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/devmode/TestGlobals.java diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/BeanArchiveProcessor.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/BeanArchiveProcessor.java index 7f0b09a4b42ad..f97651cc65af2 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/BeanArchiveProcessor.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/BeanArchiveProcessor.java @@ -95,12 +95,13 @@ public void transform(TransformationContext ctx) { knownMissingClasses, Thread.currentThread().getContextClassLoader()); } Set generatedClassNames = new HashSet<>(); - for (GeneratedBeanBuildItem generatedBeanClass : generatedBeans) { - IndexingUtil.indexClass(generatedBeanClass.getName(), additionalBeanIndexer, applicationIndex, additionalIndex, - knownMissingClasses, Thread.currentThread().getContextClassLoader(), generatedBeanClass.getData()); - generatedClassNames.add(DotName.createSimple(generatedBeanClass.getName().replace('/', '.'))); - generatedClass.produce(new GeneratedClassBuildItem(true, generatedBeanClass.getName(), generatedBeanClass.getData(), - generatedBeanClass.getSource())); + for (GeneratedBeanBuildItem generatedBean : generatedBeans) { + IndexingUtil.indexClass(generatedBean.getName(), additionalBeanIndexer, applicationIndex, additionalIndex, + knownMissingClasses, Thread.currentThread().getContextClassLoader(), generatedBean.getData()); + generatedClassNames.add(DotName.createSimple(generatedBean.getName().replace('/', '.'))); + generatedClass.produce(new GeneratedClassBuildItem(generatedBean.isApplicationClass(), generatedBean.getName(), + generatedBean.getData(), + generatedBean.getSource())); } PersistentClassIndex index = liveReloadBuildItem.getContextObject(PersistentClassIndex.class); diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/GeneratedBeanBuildItem.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/GeneratedBeanBuildItem.java index 03bc7572c4edf..4908805c2d32d 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/GeneratedBeanBuildItem.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/GeneratedBeanBuildItem.java @@ -8,6 +8,7 @@ */ public final class GeneratedBeanBuildItem extends MultiBuildItem { + private final boolean applicationClass; private final String name; private final byte[] data; private final String source; @@ -17,9 +18,14 @@ public GeneratedBeanBuildItem(String name, byte[] data) { } public GeneratedBeanBuildItem(String name, byte[] data, String source) { + this(name, data, source, true); + } + + public GeneratedBeanBuildItem(String name, byte[] data, String source, boolean applicationClass) { this.name = name; this.data = data; this.source = source; + this.applicationClass = applicationClass; } public String getName() { @@ -38,4 +44,8 @@ public String getSource() { return source; } + public boolean isApplicationClass() { + return applicationClass; + } + } diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/GeneratedBeanGizmoAdaptor.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/GeneratedBeanGizmoAdaptor.java index 6adbd0d320685..211cfd188267e 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/GeneratedBeanGizmoAdaptor.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/GeneratedBeanGizmoAdaptor.java @@ -4,6 +4,7 @@ import java.io.Writer; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Predicate; import io.quarkus.bootstrap.BootstrapDebug; import io.quarkus.deployment.annotations.BuildProducer; @@ -13,10 +14,23 @@ public class GeneratedBeanGizmoAdaptor implements ClassOutput { private final BuildProducer classOutput; private final Map sources; + private final Predicate applicationClassPredicate; public GeneratedBeanGizmoAdaptor(BuildProducer classOutput) { + this(classOutput, new Predicate() { + + @Override + public boolean test(String t) { + return true; + } + }); + } + + public GeneratedBeanGizmoAdaptor(BuildProducer classOutput, + Predicate applicationClassPredicate) { this.classOutput = classOutput; this.sources = BootstrapDebug.debugSourcesDir() != null ? new ConcurrentHashMap<>() : null; + this.applicationClassPredicate = applicationClassPredicate; } @Override @@ -28,7 +42,7 @@ public void write(String className, byte[] bytes) { source = sw.toString(); } } - classOutput.produce(new GeneratedBeanBuildItem(className, bytes, source)); + classOutput.produce(new GeneratedBeanBuildItem(className, bytes, source, applicationClassPredicate.test(className))); } @Override diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteDevModeProcessor.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteDevModeProcessor.java index 4baf3b0756616..efb936fd9da38 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteDevModeProcessor.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteDevModeProcessor.java @@ -1,15 +1,26 @@ package io.quarkus.qute.deployment; +import java.lang.reflect.Modifier; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Predicate; +import org.jboss.jandex.DotName; + +import io.quarkus.arc.deployment.GeneratedBeanBuildItem; +import io.quarkus.arc.deployment.GeneratedBeanGizmoAdaptor; import io.quarkus.arc.deployment.ValidationPhaseBuildItem.ValidationErrorBuildItem; import io.quarkus.deployment.IsDevelopment; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.BuildSteps; +import io.quarkus.deployment.builditem.ApplicationIndexBuildItem; import io.quarkus.dev.console.DevConsoleManager; +import io.quarkus.gizmo.ClassCreator; +import io.quarkus.gizmo.FieldCreator; +import io.quarkus.gizmo.MethodCreator; +import io.quarkus.qute.TemplateGlobal; import io.quarkus.qute.runtime.devmode.QuteErrorPageSetup; @BuildSteps(onlyIf = IsDevelopment.class) @@ -28,4 +39,33 @@ void collectGeneratedContents(List templatePaths, DevConsoleManager.setGlobal(QuteErrorPageSetup.GENERATED_CONTENTS, contents); } + // This build step is only used to for a QuarkusDevModeTest that contains the QuteDummyTemplateGlobalMarker interface + @BuildStep + void generateTestTemplateGlobal(ApplicationIndexBuildItem applicationIndex, + BuildProducer generatedBeanClasses) { + if (applicationIndex.getIndex().getClassByName( + DotName.createSimple("io.quarkus.qute.deployment.devmode.QuteDummyTemplateGlobalMarker")) != null) { + // If the marker interface is present then we generate a dummy class annotated with @TemplateGlobal + GeneratedBeanGizmoAdaptor gizmoAdaptor = new GeneratedBeanGizmoAdaptor(generatedBeanClasses, + new Predicate() { + @Override + public boolean test(String t) { + return false; + } + }); + try (ClassCreator classCreator = ClassCreator.builder().className("io.quarkus.qute.test.QuteDummyGlobals") + .classOutput(gizmoAdaptor).build()) { + classCreator.addAnnotation(TemplateGlobal.class); + + FieldCreator quteDummyFoo = classCreator.getFieldCreator("quteDummyFoo", String.class); + quteDummyFoo.setModifiers(Modifier.STATIC); + + MethodCreator staticInitializer = classCreator.getMethodCreator("", void.class); + staticInitializer.setModifiers(Modifier.STATIC); + staticInitializer.writeStaticField(quteDummyFoo.getFieldDescriptor(), staticInitializer.load("bar")); + staticInitializer.returnVoid(); + } + } + } + } 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 ae41dc3ee4c79..8e7de3c2924cc 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 @@ -2094,7 +2094,13 @@ public Function apply(ClassInfo clazz) { } if (!templateGlobals.isEmpty()) { - TemplateGlobalGenerator globalGenerator = new TemplateGlobalGenerator(classOutput, GLOBAL_NAMESPACE, -1000, index); + Set generatedGlobals = new HashSet<>(); + // The initial priority is increased during live reload so that priorities of non-application globals + // do not conflict with priorities of application globals + int initialPriority = -1000 + existingValueResolvers.globals.size(); + + TemplateGlobalGenerator globalGenerator = new TemplateGlobalGenerator(classOutput, GLOBAL_NAMESPACE, + initialPriority, index); Map> classToTargets = new HashMap<>(); Map> classToGlobals = templateGlobals.stream() @@ -2105,12 +2111,19 @@ public Function apply(ClassInfo clazz) { } for (Entry> e : classToTargets.entrySet()) { - globalGenerator.generate(index.getClassByName(e.getKey()), e.getValue()); + String generatedClass = existingValueResolvers.getGeneratedGlobalClass(e.getKey()); + if (generatedClass != null) { + generatedGlobals.add(generatedClass); + } else { + generatedClass = globalGenerator.generate(index.getClassByName(e.getKey()), e.getValue()); + existingValueResolvers.addGlobal(e.getKey(), generatedClass, applicationClassPredicate); + } } + generatedGlobals.addAll(globalGenerator.getGeneratedTypes()); - for (String generatedType : globalGenerator.getGeneratedTypes()) { - globalProviders.produce(new TemplateGlobalProviderBuildItem(generatedType)); - reflectiveClass.produce(ReflectiveClassBuildItem.builder(generatedType).build()); + for (String globalType : generatedGlobals) { + globalProviders.produce(new TemplateGlobalProviderBuildItem(globalType)); + reflectiveClass.produce(ReflectiveClassBuildItem.builder(globalType).build()); } } } @@ -2122,12 +2135,18 @@ public Function apply(ClassInfo clazz) { static class ExistingValueResolvers { final Map identifiersToGeneratedClass = new HashMap<>(); + // class declaring globals -> generated type + final Map globals = new HashMap<>(); boolean contains(MethodInfo extensionMethod) { return identifiersToGeneratedClass .containsKey(toKey(extensionMethod)); } + String getGeneratedGlobalClass(DotName declaringClassName) { + return globals.get(declaringClassName.toString()); + } + String getGeneratedClass(MethodInfo extensionMethod) { return identifiersToGeneratedClass.get(toKey(extensionMethod)); } @@ -2138,6 +2157,12 @@ void add(MethodInfo extensionMethod, String className, Predicate applic } } + void addGlobal(DotName declaringClassName, String generatedClassName, Predicate applicationClassPredicate) { + if (!applicationClassPredicate.test(declaringClassName)) { + globals.put(declaringClassName.toString(), generatedClassName); + } + } + private String toKey(MethodInfo extensionMethod) { return extensionMethod.declaringClass().toString() + "#" + extensionMethod.toString(); } diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/devmode/ExistingValueResolversDevModeTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/devmode/ExistingValueResolversDevModeTest.java index d8069523c9d12..942e8f8372c43 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/devmode/ExistingValueResolversDevModeTest.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/devmode/ExistingValueResolversDevModeTest.java @@ -20,7 +20,7 @@ public class ExistingValueResolversDevModeTest { .addClass(TestRoute.class) .addAsResource(new StringAsset( "{#let a = 3}{#let b = a.minus(2)}b={b}{/}{/}"), - "templates/let.html")); + "templates/test.html")); @Test public void testExistingValueResolvers() { @@ -29,7 +29,7 @@ public void testExistingValueResolvers() { .statusCode(200) .body(Matchers.equalTo("b=1")); - config.modifyResourceFile("templates/let.html", t -> t.concat("::MODIFIED")); + config.modifyResourceFile("templates/test.html", t -> t.concat("::MODIFIED")); given().get("test") .then() diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/devmode/QuteDummyTemplateGlobalMarker.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/devmode/QuteDummyTemplateGlobalMarker.java new file mode 100644 index 0000000000000..c2beb9f787d9d --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/devmode/QuteDummyTemplateGlobalMarker.java @@ -0,0 +1,8 @@ +package io.quarkus.qute.deployment.devmode; + +/** + * Marker interface for {@link TemplateGlobalDevModeTest}. + */ +public interface QuteDummyTemplateGlobalMarker { + +} diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/devmode/TemplateGlobalDevModeTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/devmode/TemplateGlobalDevModeTest.java new file mode 100644 index 0000000000000..a9ab34023584a --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/devmode/TemplateGlobalDevModeTest.java @@ -0,0 +1,45 @@ +package io.quarkus.qute.deployment.devmode; + +import static io.restassured.RestAssured.given; + +import org.hamcrest.Matchers; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusDevModeTest; + +/** + * Test that template globals added in the dev mode are generated correctly after live reload. + *

+ * The {@link QuteDummyTemplateGlobalMarker} is used to identify an application archive where a dummy built-in class with + * template globals is added. + */ +public class TemplateGlobalDevModeTest { + + @RegisterExtension + static final QuarkusDevModeTest config = new QuarkusDevModeTest() + .withApplicationRoot(root -> root + .addClasses(TestRoute.class, QuteDummyTemplateGlobalMarker.class) + .addAsResource(new StringAsset( + "{quteDummyFoo}:{testFoo ?: 'NA'}"), + "templates/test.html")); + + @Test + public void testTemplateGlobals() { + given().get("test") + .then() + .statusCode(200) + .body(Matchers.equalTo("bar:NA")); + + // Add application globals - the priority sequence should be automatically + // increased before it's used for TestGlobals + config.addSourceFile(TestGlobals.class); + + given().get("test") + .then() + .statusCode(200) + .body(Matchers.equalTo("bar:baz")); + } + +} diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/devmode/TestGlobals.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/devmode/TestGlobals.java new file mode 100644 index 0000000000000..8a432c9533123 --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/devmode/TestGlobals.java @@ -0,0 +1,11 @@ +package io.quarkus.qute.deployment.devmode; + +import io.quarkus.qute.TemplateGlobal; + +@TemplateGlobal +public class TestGlobals { + + public static String testFoo() { + return "baz"; + } +} diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/devmode/TestRoute.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/devmode/TestRoute.java index 15fbd2054ded6..06107c21c5441 100644 --- a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/devmode/TestRoute.java +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/devmode/TestRoute.java @@ -9,11 +9,11 @@ public class TestRoute { @Inject - Template let; + Template test; @Route(path = "test") public void test(RoutingContext ctx) { - ctx.end(let.render()); + ctx.end(test.render()); } } diff --git a/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/TemplateGlobalGenerator.java b/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/TemplateGlobalGenerator.java index 02dfc4bee58d0..6b9b68e76360e 100644 --- a/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/TemplateGlobalGenerator.java +++ b/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/TemplateGlobalGenerator.java @@ -48,13 +48,13 @@ public class TemplateGlobalGenerator extends AbstractGenerator { private final String namespace; private int priority; - public TemplateGlobalGenerator(ClassOutput classOutput, String namespace, int priority, IndexView index) { + public TemplateGlobalGenerator(ClassOutput classOutput, String namespace, int initialPriority, IndexView index) { super(index, classOutput); this.namespace = namespace; - this.priority = priority; + this.priority = initialPriority; } - public void generate(ClassInfo declaringClass, Map targets) { + public String generate(ClassInfo declaringClass, Map targets) { String baseName; if (declaringClass.enclosingClass() != null) { @@ -65,7 +65,8 @@ public void generate(ClassInfo declaringClass, Map tar } String targetPackage = packageName(declaringClass.name()); String generatedName = generatedNameFromTarget(targetPackage, baseName, SUFFIX); - generatedTypes.add(generatedName.replace('/', '.')); + String generatedClassName = generatedName.replace('/', '.'); + generatedTypes.add(generatedClassName); ClassCreator provider = ClassCreator.builder().classOutput(classOutput).className(generatedName) .interfaces(TemplateGlobalProvider.class).build(); @@ -141,6 +142,7 @@ public void accept(BytecodeCreator bc) { resolve.returnValue(resolve.invokeStaticMethod(Descriptors.RESULTS_NOT_FOUND_EC, evalContext)); provider.close(); + return generatedClassName; } public Set getGeneratedTypes() {