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 e065360752006..e362d555376dd 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 @@ -1742,11 +1742,14 @@ public String apply(String name) { } }); + // NOTE: We can't use this optimization for classes generated by ValueResolverGenerator because we cannot easily + // map a target class to a specific set of generated classes ExistingValueResolvers existingValueResolvers = liveReloadBuildItem.getContextObject(ExistingValueResolvers.class); if (existingValueResolvers == null) { existingValueResolvers = new ExistingValueResolvers(); liveReloadBuildItem.setContextObject(ExistingValueResolvers.class, existingValueResolvers); } + Set generatedValueResolvers = new HashSet<>(); ValueResolverGenerator.Builder builder = ValueResolverGenerator.builder() .setIndex(index).setClassOutput(classOutput); @@ -1770,15 +1773,11 @@ public Function apply(ClassInfo clazz) { Set controlled = new HashSet<>(); Map uncontrolled = new HashMap<>(); for (TemplateDataBuildItem data : templateData) { - processTemplateData(data, controlled, uncontrolled, builder, existingValueResolvers, applicationClassPredicate); + processTemplateData(data, controlled, uncontrolled, builder); } for (ImplicitValueResolverBuildItem implicit : implicitClasses) { DotName implicitClassName = implicit.getClazz().name(); - if (existingValueResolvers.contains(implicitClassName)) { - // A non-application value resolver already generated - continue; - } if (controlled.contains(implicitClassName)) { LOGGER.debugf("Implicit value resolver for %s ignored: class is annotated with @TemplateData", implicitClassName); @@ -1790,13 +1789,10 @@ public Function apply(ClassInfo clazz) { continue; } builder.addClass(implicit.getClazz(), implicit.getTemplateData()); - existingValueResolvers.add(implicitClassName, applicationClassPredicate); } ValueResolverGenerator generator = builder.build(); generator.generate(); - - Set generatedValueResolvers = new HashSet<>(); generatedValueResolvers.addAll(generator.getGeneratedTypes()); ExtensionMethodGenerator extensionMethodGenerator = new ExtensionMethodGenerator(index, classOutput); @@ -1804,10 +1800,12 @@ public Function apply(ClassInfo clazz) { Map namespaceToClass = new HashMap<>(); for (TemplateExtensionMethodBuildItem templateExtension : templateExtensionMethods) { - if (existingValueResolvers.contains(templateExtension.getMethod())) { + String generatedValueResolverClass = existingValueResolvers.getGeneratedClass(templateExtension.getMethod()); + if (generatedValueResolverClass != null) { + // A ValueResolver of a non-application class was already generated + generatedValueResolvers.add(generatedValueResolverClass); continue; } - existingValueResolvers.add(templateExtension.getMethod(), applicationClassPredicate); if (templateExtension.hasNamespace()) { // Group extension methods declared on the same class by namespace @@ -1835,8 +1833,10 @@ public Function apply(ClassInfo clazz) { namespaceMethods.add(templateExtension); } else { // Generate ValueResolver per extension method - extensionMethodGenerator.generate(templateExtension.getMethod(), templateExtension.getMatchName(), + String generatedClass = extensionMethodGenerator.generate(templateExtension.getMethod(), + templateExtension.getMatchName(), templateExtension.getMatchNames(), templateExtension.getMatchRegex(), templateExtension.getPriority()); + existingValueResolvers.add(templateExtension.getMethod(), generatedClass, applicationClassPredicate); } } @@ -1853,6 +1853,10 @@ public Function apply(ClassInfo clazz) { try (NamespaceResolverCreator namespaceResolverCreator = extensionMethodGenerator .createNamespaceResolver(priorityEntry.getValue().get(0).getMethod().declaringClass(), nsEntry.getKey(), priorityEntry.getKey())) { + for (TemplateExtensionMethodBuildItem extensionMethod : priorityEntry.getValue()) { + existingValueResolvers.add(extensionMethod.getMethod(), namespaceResolverCreator.getClassName(), + applicationClassPredicate); + } try (ResolveCreator resolveCreator = namespaceResolverCreator.implementResolve()) { for (TemplateExtensionMethodBuildItem method : priorityEntry.getValue()) { resolveCreator.addMethod(method.getMethod(), method.getMatchName(), method.getMatchNames(), @@ -1901,28 +1905,25 @@ public Function apply(ClassInfo clazz) { */ static class ExistingValueResolvers { - final Set identifiers = new HashSet<>(); - - boolean contains(DotName className) { - return identifiers.contains(className.toString()); - } + final Map identifiersToGeneratedClass = new HashMap<>(); boolean contains(MethodInfo extensionMethod) { - return identifiers.contains(extensionMethod.declaringClass().toString() + "#" + extensionMethod.toString()); + return identifiersToGeneratedClass + .containsKey(toKey(extensionMethod)); } - boolean add(DotName className, Predicate applicationClassPredicate) { - if (!applicationClassPredicate.test(className)) { - return identifiers.add(className.toString()); - } - return false; + String getGeneratedClass(MethodInfo extensionMethod) { + return identifiersToGeneratedClass.get(toKey(extensionMethod)); } - boolean add(MethodInfo extensionMethod, Predicate applicationClassPredicate) { + void add(MethodInfo extensionMethod, String className, Predicate applicationClassPredicate) { if (!applicationClassPredicate.test(extensionMethod.declaringClass().name())) { - return identifiers.add(extensionMethod.declaringClass().toString() + "#" + extensionMethod.toString()); + identifiersToGeneratedClass.put(toKey(extensionMethod), className); } - return false; + } + + private String toKey(MethodInfo extensionMethod) { + return extensionMethod.declaringClass().toString() + "#" + extensionMethod.toString(); } } @@ -2942,22 +2943,15 @@ private static boolean methodMatches(MethodInfo method, VirtualMethodPart virtua } private void processTemplateData(TemplateDataBuildItem templateData, - Set controlled, Map uncontrolled, ValueResolverGenerator.Builder builder, - ExistingValueResolvers existingValueResolvers, - CompletedApplicationClassPredicateBuildItem applicationClassPredicate) { + Set controlled, Map uncontrolled, ValueResolverGenerator.Builder builder) { DotName targetClassName = templateData.getTargetClass().name(); - if (existingValueResolvers.contains(targetClassName)) { - return; - } if (templateData.isTargetAnnotatedType()) { controlled.add(targetClassName); builder.addClass(templateData.getTargetClass(), templateData.getAnnotationInstance()); - existingValueResolvers.add(targetClassName, applicationClassPredicate); } else { // At this point we can be sure that multiple unequal @TemplateData do not exist for a specific target uncontrolled.computeIfAbsent(targetClassName, name -> { builder.addClass(templateData.getTargetClass(), templateData.getAnnotationInstance()); - existingValueResolvers.add(targetClassName, applicationClassPredicate); return templateData.getAnnotationInstance(); }); } 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 new file mode 100644 index 0000000000000..d8069523c9d12 --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/devmode/ExistingValueResolversDevModeTest.java @@ -0,0 +1,40 @@ +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 built-in extension value resolvers are correctly registered after live reload. + */ +public class ExistingValueResolversDevModeTest { + + @RegisterExtension + static final QuarkusDevModeTest config = new QuarkusDevModeTest() + .withApplicationRoot(root -> root + .addClass(TestRoute.class) + .addAsResource(new StringAsset( + "{#let a = 3}{#let b = a.minus(2)}b={b}{/}{/}"), + "templates/let.html")); + + @Test + public void testExistingValueResolvers() { + given().get("test") + .then() + .statusCode(200) + .body(Matchers.equalTo("b=1")); + + config.modifyResourceFile("templates/let.html", t -> t.concat("::MODIFIED")); + + given().get("test") + .then() + .statusCode(200) + .body(Matchers.equalTo("b=1::MODIFIED")); + } + +} 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 new file mode 100644 index 0000000000000..15fbd2054ded6 --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/devmode/TestRoute.java @@ -0,0 +1,19 @@ +package io.quarkus.qute.deployment.devmode; + +import jakarta.inject.Inject; + +import io.quarkus.qute.Template; +import io.quarkus.vertx.web.Route; +import io.vertx.ext.web.RoutingContext; + +public class TestRoute { + + @Inject + Template let; + + @Route(path = "test") + public void test(RoutingContext ctx) { + ctx.end(let.render()); + } + +} diff --git a/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/ExtensionMethodGenerator.java b/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/ExtensionMethodGenerator.java index bbc9b8f3cb965..bb1a50f4b6bfe 100644 --- a/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/ExtensionMethodGenerator.java +++ b/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/ExtensionMethodGenerator.java @@ -103,7 +103,16 @@ public static void validate(MethodInfo method, String namespace) { } } - public void generate(MethodInfo method, String matchName, List matchNames, String matchRegex, Integer priority) { + /** + * + * @param method + * @param matchName + * @param matchNames + * @param matchRegex + * @param priority + * @return the fully qualified name of the generated class + */ + public String generate(MethodInfo method, String matchName, List matchNames, String matchRegex, Integer priority) { AnnotationInstance extensionAnnotation = method.annotation(TEMPLATE_EXTENSION); List parameters = method.parameterTypes(); @@ -199,6 +208,7 @@ public void generate(MethodInfo method, String matchName, List matchName implementResolve(valueResolver, declaringClass, method, matchName, matchNames, patternField, params); valueResolver.close(); + return generatedName.replace('/', '.'); } public NamespaceResolverCreator createNamespaceResolver(ClassInfo declaringClass, String namespace, int priority) { @@ -449,6 +459,10 @@ public NamespaceResolverCreator(ClassInfo declaringClass, String namespace, int implementGetPriority(namespaceResolver, priority); } + public String getClassName() { + return namespaceResolver.getClassName().replace('/', '.'); + } + public ResolveCreator implementResolve() { return new ResolveCreator(); }