From 8b7943a83dd6d4c042270fe3dc70c7e4525f9c7e Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Wed, 12 Jan 2022 17:04:15 +0100 Subject: [PATCH] Qute - support global variables - supplied by static fields/methods annotated with io.quarkus.qute.TemplateGlobal - resolves #21853 --- docs/src/main/asciidoc/qute-reference.adoc | 88 +++++++++++ ...GeneratedTemplateInitializerBuildItem.java | 17 +++ .../qute/deployment/QuteProcessor.java | 141 ++++++++++++++++-- .../deployment/TemplateGlobalBuildItem.java | 69 +++++++++ .../globals/TemplateGlobalDuplicityTest.java | 68 +++++++++ .../TemplateGlobalInvalidNameTest.java | 55 +++++++ .../globals/TemplateGlobalOverrideTest.java | 56 +++++++ .../globals/TemplateGlobalTest.java | 77 ++++++++++ .../TemplateGlobalValidationFailureTest.java | 56 +++++++ .../quarkus/qute/deployment/globals/User.java | 11 ++ .../quarkus/qute/runtime/EngineProducer.java | 20 +++ .../io/quarkus/qute/runtime/QuteRecorder.java | 12 +- .../qute/runtime/TemplateProducer.java | 10 +- .../src/main/java/io/quarkus/qute/Engine.java | 5 + .../java/io/quarkus/qute/EngineBuilder.java | 12 ++ .../main/java/io/quarkus/qute/EngineImpl.java | 8 + .../main/java/io/quarkus/qute/Template.java | 1 + .../io/quarkus/qute/TemplateExtension.java | 2 +- .../java/io/quarkus/qute/TemplateGlobal.java | 41 +++++ .../java/io/quarkus/qute/TemplateImpl.java | 8 +- .../io/quarkus/qute/TemplateInstance.java | 10 ++ .../qute/TemplateInstanceInitializerTest.java | 24 +++ independent-projects/qute/generator/pom.xml | 5 + .../quarkus/qute/generator/Descriptors.java | 3 + .../generator/TemplateGlobalGenerator.java | 122 +++++++++++++++ .../TemplateGlobalGeneratorTest.java | 78 ++++++++++ 26 files changed, 978 insertions(+), 21 deletions(-) create mode 100644 extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/GeneratedTemplateInitializerBuildItem.java create mode 100644 extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateGlobalBuildItem.java create mode 100644 extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/globals/TemplateGlobalDuplicityTest.java create mode 100644 extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/globals/TemplateGlobalInvalidNameTest.java create mode 100644 extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/globals/TemplateGlobalOverrideTest.java create mode 100644 extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/globals/TemplateGlobalTest.java create mode 100644 extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/globals/TemplateGlobalValidationFailureTest.java create mode 100644 extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/globals/User.java create mode 100644 independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateGlobal.java create mode 100644 independent-projects/qute/core/src/test/java/io/quarkus/qute/TemplateInstanceInitializerTest.java create mode 100644 independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/TemplateGlobalGenerator.java create mode 100644 independent-projects/qute/generator/src/test/java/io/quarkus/qute/generator/TemplateGlobalGeneratorTest.java diff --git a/docs/src/main/asciidoc/qute-reference.adoc b/docs/src/main/asciidoc/qute-reference.adoc index 356bc8441852e..9a944aca42b72 100644 --- a/docs/src/main/asciidoc/qute-reference.adoc +++ b/docs/src/main/asciidoc/qute-reference.adoc @@ -1741,6 +1741,94 @@ NOTE: `@TemplateEnum` declared on a non-enum class is ignored. Also if an enum a TIP: Quarkus detects possible namespace collisions and fails the build if a specific namespace is defined by multiple `@TemplateData` and/or `@TemplateEnum` annotations. +[[global_variables]] +=== Global Variables + +The `io.quarkus.qute.TemplateGlobal` annotation can be used to denote static fields and methods that supply _global variables_ which are accessible in any template. +Internally, each global variable is added to the data map of any `TemplateInstance` via the `TemplateInstance#data(String, Object)` method. + +.Global Variables Definition +[source,java] +---- +enum Color { RED, GREEN, BLUE } + +@TemplateGlobal <1> +public class Globals { + + static int age = 40; + + static Color[] myColors() { + return new Color[] { Color.RED, Color.BLUE }; + } + + @TemplateGlobal(name = "currentUser") <2> + static String user() { + return "Mia"; + } +} +---- +<1> If a class is annotated with `@TemplateGlobal` then every non-void non-private static method that declares no parameters and every non-private static field is considered a global variable. The name is defaulted, i.e. the name of the field/method is used. +<2> Method-level annotations override the class-level annotation. In this particular case, the name is not defaulted but selected explicitly. + +.A Template Accessing Global Variables +[source,html] +---- +User: {currentUser} <1> +Age: {age} <2> +Colors: {#each myColors}{it}{#if it_hasNext}, {/if}{/each} <3> +---- +<1> `currentUser` resolves to `Globals#user()`. +<2> `age` resolves to `Globals#age`. +<3> `myColors` resolves to `Globals#myColors()`. + +NOTE: Note that global variables implicitly add <> to all templates and so any expression that references a global variable is validated during build. + +.The Output +[source,html] +---- +User: Mia +Age: 40 +Colors: RED, BLUE +---- + +==== Resolving Conflicts + +Global variables may conflict with regular data objects. +<> override the global variables automatically. +For example, the following definition overrides the global variable supplied by the `Globals#user()` method: + +.Type-safe Template Definition +[source,java] +---- +import org.acme.User; + +@CheckedTemplate +public class Templates { + static native TemplateInstance hello(User currentUser); <1> +} +---- +<1> `currentUser` conflicts with the global variable supplied by `Globals#user()`. + +So the corresponding template does not result in a validation error even though the `Globals#user()` method returns `java.lang.String` which does not have the `name` property: + +.`templates/hello.txt` +[source,html] +---- +User name: {currentUser.name} <1> +---- +<1> `org.acme.User` has the `name` property. + +For other templates an explicit parameter declaration is needed: + +[source,html] +---- +{@org.acme.User currentUser} <1> + +User name: {currentUser.name} +---- +<1> This parameter declaration overrides the declaration added by the global variable supplied by the `Globals#user()` method. + + [[native_executables]] === Native Executables diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/GeneratedTemplateInitializerBuildItem.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/GeneratedTemplateInitializerBuildItem.java new file mode 100644 index 0000000000000..3101fa3a59c34 --- /dev/null +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/GeneratedTemplateInitializerBuildItem.java @@ -0,0 +1,17 @@ +package io.quarkus.qute.deployment; + +import io.quarkus.builder.item.MultiBuildItem; + +public final class GeneratedTemplateInitializerBuildItem extends MultiBuildItem { + + private final String className; + + public GeneratedTemplateInitializerBuildItem(String className) { + this.className = className; + } + + public String getClassName() { + return className; + } + +} 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 0e408ef41da0a..33592f624a31b 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 @@ -74,7 +74,6 @@ import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.ApplicationArchivesBuildItem; import io.quarkus.deployment.builditem.BytecodeTransformerBuildItem; -import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.builditem.FeatureBuildItem; import io.quarkus.deployment.builditem.GeneratedClassBuildItem; import io.quarkus.deployment.builditem.HotDeploymentWatchedFileBuildItem; @@ -104,6 +103,7 @@ import io.quarkus.qute.TemplateData; import io.quarkus.qute.TemplateException; import io.quarkus.qute.TemplateExtension; +import io.quarkus.qute.TemplateGlobal; import io.quarkus.qute.TemplateInstance; import io.quarkus.qute.TemplateLocator; import io.quarkus.qute.UserTagSectionHelper; @@ -116,6 +116,7 @@ import io.quarkus.qute.generator.ExtensionMethodGenerator.NamespaceResolverCreator; import io.quarkus.qute.generator.ExtensionMethodGenerator.NamespaceResolverCreator.ResolveCreator; import io.quarkus.qute.generator.ExtensionMethodGenerator.Param; +import io.quarkus.qute.generator.TemplateGlobalGenerator; import io.quarkus.qute.generator.ValueResolverGenerator; import io.quarkus.qute.runtime.ContentTypes; import io.quarkus.qute.runtime.EngineProducer; @@ -343,7 +344,7 @@ List collectCheckedTemplates(BeanArchiveIndexBuildItem @BuildStep TemplatesAnalysisBuildItem analyzeTemplates(List templatePaths, TemplateFilePathsBuildItem filePaths, List checkedTemplates, - List messageBundleMethods, QuteConfig config) { + List messageBundleMethods, List globals, QuteConfig config) { long start = System.nanoTime(); checkDuplicatePaths(templatePaths); @@ -431,6 +432,11 @@ public void beforeParsing(ParserHelper parserHelper) { break; } } + // Set the bindings for globals first so that type-safe templates can override them + for (TemplateGlobalBuildItem global : globals) { + parserHelper.addParameter(global.getName(), + JandexUtil.getBoxedTypeName(global.getVariableType()).toString()); + } for (CheckedTemplateBuildItem checkedTemplate : checkedTemplates) { if (checkedTemplate.templateId.equals(path)) { for (Entry entry : checkedTemplate.bindings.entrySet()) { @@ -1063,16 +1069,18 @@ static String findTemplatePath(TemplatesAnalysisBuildItem analysis, String id) { @BuildStep void generateValueResolvers(QuteConfig config, BuildProducer generatedClasses, - CombinedIndexBuildItem combinedIndex, BeanArchiveIndexBuildItem beanArchiveIndex, + BeanArchiveIndexBuildItem beanArchiveIndex, ApplicationArchivesBuildItem applicationArchivesBuildItem, List templatePaths, List templateExtensionMethods, List implicitClasses, TemplatesAnalysisBuildItem templatesAnalysis, + List panacheEntityClasses, + List templateData, + List templateGlobals, BuildProducer generatedResolvers, BuildProducer reflectiveClass, - List panacheEntityClasses, - List templateData) { + BuildProducer generatedInitializers) { IndexView index = beanArchiveIndex.getIndex(); ClassOutput classOutput = new GeneratedClassGizmoAdaptor(generatedClasses, new Function() { @@ -1088,6 +1096,9 @@ public String apply(String name) { if (idx == -1) { idx = name.lastIndexOf(ValueResolverGenerator.SUFFIX); } + if (idx == -1) { + idx = name.lastIndexOf(TemplateGlobalGenerator.SUFFIX); + } String className = name.substring(0, idx); if (className.contains(ValueResolverGenerator.NESTED_SEPARATOR)) { className = className.replace(ValueResolverGenerator.NESTED_SEPARATOR, "$"); @@ -1139,8 +1150,8 @@ public Function apply(ClassInfo clazz) { ValueResolverGenerator generator = builder.build(); generator.generate(); - Set generatedTypes = new HashSet<>(); - generatedTypes.addAll(generator.getGeneratedTypes()); + Set generatedValueResolvers = new HashSet<>(); + generatedValueResolvers.addAll(generator.getGeneratedTypes()); ExtensionMethodGenerator extensionMethodGenerator = new ExtensionMethodGenerator(index, classOutput); Map>> classToNamespaceExtensions = new HashMap<>(); @@ -1201,13 +1212,34 @@ public Function apply(ClassInfo clazz) { } } - generatedTypes.addAll(extensionMethodGenerator.getGeneratedTypes()); + generatedValueResolvers.addAll(extensionMethodGenerator.getGeneratedTypes()); - LOGGER.debugf("Generated types: %s", generatedTypes); + LOGGER.debugf("Generated value resolvers: %s", generatedValueResolvers); - for (String generateType : generatedTypes) { - generatedResolvers.produce(new GeneratedValueResolverBuildItem(generateType)); - reflectiveClass.produce(new ReflectiveClassBuildItem(false, false, generateType)); + for (String generatedType : generatedValueResolvers) { + generatedResolvers.produce(new GeneratedValueResolverBuildItem(generatedType)); + reflectiveClass.produce(new ReflectiveClassBuildItem(false, false, generatedType)); + } + + if (!templateGlobals.isEmpty()) { + TemplateGlobalGenerator globalGenerator = new TemplateGlobalGenerator(classOutput); + + Map> classToTargets = new HashMap<>(); + Map> classToGlobals = templateGlobals.stream() + .collect(Collectors.groupingBy(TemplateGlobalBuildItem::getDeclaringClass)); + for (Entry> entry : classToGlobals.entrySet()) { + classToTargets.put(entry.getKey(), entry.getValue().stream().collect( + Collectors.toMap(TemplateGlobalBuildItem::getName, TemplateGlobalBuildItem::getTarget))); + } + + for (Entry> e : classToTargets.entrySet()) { + globalGenerator.generate(index.getClassByName(e.getKey()), e.getValue()); + } + + for (String generatedType : globalGenerator.getGeneratedTypes()) { + generatedInitializers.produce(new GeneratedTemplateInitializerBuildItem(generatedType)); + reflectiveClass.produce(new ReflectiveClassBuildItem(false, false, generatedType)); + } } } @@ -1401,7 +1433,8 @@ public boolean test(TypeCheck check) { @Record(value = STATIC_INIT) void initialize(BuildProducer syntheticBeans, QuteRecorder recorder, List generatedValueResolvers, List templatePaths, - Optional templateVariants) { + Optional templateVariants, + List templateInitializers) { List templates = new ArrayList<>(); List tags = new ArrayList<>(); @@ -1424,7 +1457,8 @@ 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)) + tags, variants, templateInitializers.stream() + .map(GeneratedTemplateInitializerBuildItem::getClassName).collect(Collectors.toList()))) .done()); } @@ -1986,6 +2020,85 @@ private void processsTemplateData(TemplateDataBuildItem templateData, } } + @BuildStep + void collectTemplateGlobals(BeanArchiveIndexBuildItem beanArchiveIndex, BuildProducer globals) { + IndexView index = beanArchiveIndex.getIndex(); + Map nameToGlobal = new HashMap<>(); + for (AnnotationInstance annotation : index.getAnnotations(TemplateGlobalGenerator.TEMPLATE_GLOBAL)) { + switch (annotation.target().kind()) { + case CLASS: + addGlobalClass(annotation.target().asClass(), nameToGlobal); + break; + case FIELD: + addGlobalField(annotation.value(TemplateGlobalGenerator.NAME), annotation.target().asField(), nameToGlobal); + break; + case METHOD: + addGlobalMethod(annotation.value(TemplateGlobalGenerator.NAME), annotation.target().asMethod(), + nameToGlobal); + break; + default: + throw new TemplateException("Invalid annotation target for @TemplateGlobal: " + annotation); + } + } + nameToGlobal.values().forEach(globals::produce); + } + + private void addGlobalClass(ClassInfo clazz, Map nameToGlobal) { + for (FieldInfo field : clazz.fields()) { + if (Modifier.isStatic(field.flags()) + && !Modifier.isPrivate(field.flags()) + && !field.isSynthetic() + && !field.hasAnnotation(TemplateGlobalGenerator.TEMPLATE_GLOBAL)) { + addGlobalField(null, field, nameToGlobal); + } + } + for (MethodInfo method : clazz.methods()) { + if (Modifier.isStatic(method.flags()) + && !Modifier.isPrivate(method.flags()) + && method.returnType().kind() != org.jboss.jandex.Type.Kind.VOID + && !method.isSynthetic() + && !method.hasAnnotation(TemplateGlobalGenerator.TEMPLATE_GLOBAL)) { + addGlobalMethod(null, method, nameToGlobal); + } + } + } + + private void addGlobalMethod(AnnotationValue nameValue, MethodInfo method, + Map nameToGlobal) { + TemplateGlobalGenerator.validate(method); + String name = TemplateGlobal.ELEMENT_NAME; + if (nameValue != null) { + name = nameValue.asString(); + } + if (name.equals(TemplateGlobal.ELEMENT_NAME)) { + name = method.name(); + } + TemplateGlobalBuildItem global = new TemplateGlobalBuildItem(name, method, method.returnType()); + addGlobalVariable(global, nameToGlobal); + } + + private void addGlobalField(AnnotationValue nameValue, FieldInfo field, Map nameToGlobal) { + TemplateGlobalGenerator.validate(field); + String name = TemplateGlobal.ELEMENT_NAME; + if (nameValue != null) { + name = nameValue.asString(); + } + if (name.equals(TemplateGlobal.ELEMENT_NAME)) { + name = field.name(); + } + TemplateGlobalBuildItem global = new TemplateGlobalBuildItem(name, field, field.type()); + addGlobalVariable(global, nameToGlobal); + } + + private void addGlobalVariable(TemplateGlobalBuildItem global, Map nameToGlobal) { + TemplateGlobalBuildItem prev = nameToGlobal.put(global.getName(), global); + if (prev != null) { + throw new TemplateException( + String.format("Duplicate global variable defined via @TemplateGlobal for the name [%s]:\n\t- %s\n\t- %s", + global.getName(), global, prev)); + } + } + @BuildStep void collectTemplateDataAnnotations(BeanArchiveIndexBuildItem beanArchiveIndex, BuildProducer templateDataAnnotations) { diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateGlobalBuildItem.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateGlobalBuildItem.java new file mode 100644 index 0000000000000..53b47c31ae8d1 --- /dev/null +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateGlobalBuildItem.java @@ -0,0 +1,69 @@ +package io.quarkus.qute.deployment; + +import org.jboss.jandex.AnnotationTarget; +import org.jboss.jandex.AnnotationTarget.Kind; +import org.jboss.jandex.DotName; +import org.jboss.jandex.Type; + +import io.quarkus.builder.item.MultiBuildItem; +import io.quarkus.qute.Namespaces; +import io.quarkus.qute.TemplateGlobal; + +/** + * Represents a global variable field/method. + * + * @see TemplateGlobal + */ +public final class TemplateGlobalBuildItem extends MultiBuildItem { + + private final String name; + private final AnnotationTarget target; + private final Type variableType; + + public TemplateGlobalBuildItem(String name, AnnotationTarget target, Type matchType) { + if (!Namespaces.isValidNamespace(name)) { + throw new IllegalArgumentException( + String.format( + "Invalid global variable name found: %s\n\t- supplied by %s \n\t- a name may only consist of alphanumeric characters and underscores: ", + name, + target.kind() == Kind.FIELD + ? target.asField().declaringClass().name() + "." + target.asField().name() + : target.asMethod().declaringClass().name() + "." + target.asMethod().name() + "()")); + } + this.name = name; + this.target = target; + this.variableType = matchType; + } + + public AnnotationTarget getTarget() { + return target; + } + + public Type getVariableType() { + return variableType; + } + + public boolean isField() { + return target.kind() == Kind.FIELD; + } + + public boolean isMethod() { + return target.kind() == Kind.METHOD; + } + + public String getName() { + return name; + } + + public DotName getDeclaringClass() { + return isField() ? target.asField().declaringClass().name() : target.asMethod().declaringClass().name(); + } + + @Override + public String toString() { + return "Variable [" + name + + "] supplied by " + getDeclaringClass() + "." + + (isField() ? target.asField().name() : target.asMethod().name() + "()"); + } + +} diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/globals/TemplateGlobalDuplicityTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/globals/TemplateGlobalDuplicityTest.java new file mode 100644 index 0000000000000..dd2c191478c6d --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/globals/TemplateGlobalDuplicityTest.java @@ -0,0 +1,68 @@ +package io.quarkus.qute.deployment.globals; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.qute.TemplateException; +import io.quarkus.qute.TemplateGlobal; +import io.quarkus.test.QuarkusUnitTest; + +public class TemplateGlobalDuplicityTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot(root -> root + .addClasses(Globals.class, NextGlobals.class) + .addAsResource(new StringAsset( + "Hello {user}!"), + "templates/hello.txt")) + .assertException(t -> { + Throwable e = t; + TemplateException te = null; + while (e != null) { + if (e instanceof TemplateException) { + te = (TemplateException) e; + break; + } + e = e.getCause(); + } + assertNotNull(te); + assertTrue( + te.getMessage().contains("Duplicate global variable defined via @TemplateGlobal for the name [user]"), + te.getMessage()); + assertTrue( + te.getMessage().contains( + "Variable [user] supplied by io.quarkus.qute.deployment.globals.TemplateGlobalDuplicityTest$NextGlobals.user()"), + te.getMessage()); + assertTrue( + te.getMessage().contains( + "Variable [user] supplied by io.quarkus.qute.deployment.globals.TemplateGlobalDuplicityTest$Globals.user"), + te.getMessage()); + }); + + @Test + public void test() { + fail(); + } + + public static class Globals { + + @TemplateGlobal + static String user = "Fu"; + + } + + @TemplateGlobal + public static class NextGlobals { + + static String user() { + return "Lu"; + } + } + +} diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/globals/TemplateGlobalInvalidNameTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/globals/TemplateGlobalInvalidNameTest.java new file mode 100644 index 0000000000000..a28297dba6382 --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/globals/TemplateGlobalInvalidNameTest.java @@ -0,0 +1,55 @@ +package io.quarkus.qute.deployment.globals; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.qute.TemplateGlobal; +import io.quarkus.test.QuarkusUnitTest; + +public class TemplateGlobalInvalidNameTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot(root -> root + .addClasses(Globals.class) + .addAsResource(new StringAsset( + "Hello {user.name}!"), + "templates/hello.txt")) + .assertException(t -> { + Throwable e = t; + IllegalArgumentException iae = null; + while (e != null) { + if (e instanceof IllegalArgumentException) { + iae = (IllegalArgumentException) e; + break; + } + e = e.getCause(); + } + assertNotNull(iae); + assertTrue( + iae.getMessage().contains("Invalid global variable name found: -name!"), + iae.getMessage()); + assertTrue( + iae.getMessage().contains( + "supplied by io.quarkus.qute.deployment.globals.TemplateGlobalInvalidNameTest$Globals.user"), + iae.getMessage()); + }); + + @Test + public void test() { + fail(); + } + + public static class Globals { + + @TemplateGlobal(name = "-name!") + static String user = "Fu"; + + } + +} diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/globals/TemplateGlobalOverrideTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/globals/TemplateGlobalOverrideTest.java new file mode 100644 index 0000000000000..cad01d2da6f7d --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/globals/TemplateGlobalOverrideTest.java @@ -0,0 +1,56 @@ +package io.quarkus.qute.deployment.globals; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import javax.inject.Inject; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.qute.CheckedTemplate; +import io.quarkus.qute.Template; +import io.quarkus.qute.TemplateGlobal; +import io.quarkus.qute.TemplateInstance; +import io.quarkus.test.QuarkusUnitTest; + +public class TemplateGlobalOverrideTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot(root -> root + .addClasses(Globals.class, User.class) + .addAsResource(new StringAsset( + // Note that we need to override the param declaration as well + "{@io.quarkus.qute.deployment.globals.User user}Hello {user.name}!"), + "templates/hello.txt") + .addAsResource(new StringAsset( + // We don't need to override the param declaration for @CheckedTemplate + "Hello {user.name}!"), + "templates/foo/hello.txt")); + + @CheckedTemplate(basePath = "foo") + static class Templates { + + static native TemplateInstance hello(User user); + + } + + @Inject + Template hello; + + @Test + public void testOverride() { + assertEquals("Hello Morna!", hello.data("user", new User("Morna")).render()); + assertEquals("Hello Morna!", Templates.hello(new User("Morna")).render()); + } + + public static class Globals { + + // this global variable is overriden in both templates + @TemplateGlobal + static String user = "Fu"; + + } + +} diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/globals/TemplateGlobalTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/globals/TemplateGlobalTest.java new file mode 100644 index 0000000000000..8c4c898bcf9aa --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/globals/TemplateGlobalTest.java @@ -0,0 +1,77 @@ +package io.quarkus.qute.deployment.globals; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import javax.inject.Inject; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.qute.Qute; +import io.quarkus.qute.Template; +import io.quarkus.qute.TemplateGlobal; +import io.quarkus.test.QuarkusUnitTest; + +public class TemplateGlobalTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot(root -> root + .addClasses(Globals.class, NextGlobals.class) + .addAsResource(new StringAsset( + "Hello {currentUser}! Your name is {_name}. You're {age} years old."), + "templates/hello.txt")); + + @Inject + Template hello; + + @Test + public void testTemplateData() { + assertEquals("Hello Fu! Your name is Lu. You're 40 years old.", hello.render()); + assertEquals("Hello Fu! Your name is Lu. You're 40 years old.", + Qute.fmt("Hello {currentUser}! Your name is {_name}. You're {age} years old.").render()); + Globals.user = "Hu"; + assertEquals("Hello Hu! Your name is Lu. You're 20 years old.", hello.render()); + assertEquals("Hello Hu! Your name is Lu. You're 20 years old.", + Qute.fmt("Hello {currentUser}! Your name is {_name}. You're {age} years old.").render()); + + assertEquals("First color is: RED", Qute.fmt("First color is: {colors[0]}").render()); + } + + public static class Globals { + + @TemplateGlobal(name = "currentUser") + static String user = "Fu"; + + @TemplateGlobal + static int age() { + return user.equals("Fu") ? 40 : 20; + } + + } + + static enum Color { + RED, + GREEN, + BLUE + } + + @TemplateGlobal + public static class NextGlobals { + + // field-level annotation overrides the class-level one + @TemplateGlobal(name = "_name") + static final String NAME = user(); + + // this method is ignored + private static String user() { + return "Lu"; + } + + static Color[] colors() { + return new Color[] { Color.RED, Color.BLUE }; + } + } + +} diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/globals/TemplateGlobalValidationFailureTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/globals/TemplateGlobalValidationFailureTest.java new file mode 100644 index 0000000000000..b79e144e99c02 --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/globals/TemplateGlobalValidationFailureTest.java @@ -0,0 +1,56 @@ +package io.quarkus.qute.deployment.globals; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.qute.TemplateException; +import io.quarkus.qute.TemplateGlobal; +import io.quarkus.test.QuarkusUnitTest; + +public class TemplateGlobalValidationFailureTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot(root -> root + .addClasses(Globals.class) + .addAsResource(new StringAsset( + "Hello {user.name}!"), + "templates/hello.txt")) + .assertException(t -> { + Throwable e = t; + TemplateException te = null; + while (e != null) { + if (e instanceof TemplateException) { + te = (TemplateException) e; + break; + } + e = e.getCause(); + } + assertNotNull(te); + assertTrue( + te.getMessage().contains("Incorrect expression found: {user.name}"), + te.getMessage()); + assertTrue( + te.getMessage().contains( + "property/method [name] not found on class [java.lang.String] nor handled by an extension method"), + te.getMessage()); + }); + + @Test + public void test() { + fail(); + } + + public static class Globals { + + @TemplateGlobal + static String user = "Fu"; + + } + +} diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/globals/User.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/globals/User.java new file mode 100644 index 0000000000000..1576bb498eb07 --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/globals/User.java @@ -0,0 +1,11 @@ +package io.quarkus.qute.deployment.globals; + +public class User { + + public final String name; + + public User(String name) { + this.name = name; + } + +} 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 46463f7128ae7..af9ff02846e6d 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 @@ -33,6 +33,7 @@ import io.quarkus.qute.ReflectionValueResolver; import io.quarkus.qute.Resolver; import io.quarkus.qute.Results; +import io.quarkus.qute.TemplateInstance; import io.quarkus.qute.TemplateLocator.TemplateLocation; import io.quarkus.qute.UserTagSectionHelper; import io.quarkus.qute.ValueResolver; @@ -182,6 +183,11 @@ public Object apply(EvalContext ctx) { // Add a special parserk hook for Qute.fmt() methods builder.addParserHook(new Qute.IndexedArgumentsParserHook()); + // Add template initializers + for (String initializerClass : context.getTemplateInstanceInitializerClasses()) { + builder.addTemplateInstanceInitializer(createInitializer(initializerClass)); + } + engine = builder.build(); // Load discovered templates @@ -227,6 +233,20 @@ private Resolver createResolver(String resolverClassName) { } } + private TemplateInstance.Initializer createInitializer(String initializerClassName) { + try { + Class initializerClazz = Thread.currentThread() + .getContextClassLoader().loadClass(initializerClassName); + if (TemplateInstance.Initializer.class.isAssignableFrom(initializerClazz)) { + return (TemplateInstance.Initializer) initializerClazz.getDeclaredConstructor().newInstance(); + } + throw new IllegalStateException("Not an initializer: " + initializerClazz); + } catch (InstantiationException | IllegalAccessException | ClassNotFoundException | IllegalArgumentException + | InvocationTargetException | NoSuchMethodException | SecurityException e) { + throw new IllegalStateException("Unable to create initializer: " + initializerClassName, e); + } + } + private Optional locate(String path) { URL resource = null; String templatePath = basePath + path; 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 716ce9349a362..be9e699c827da 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 @@ -10,7 +10,8 @@ public class QuteRecorder { public Supplier createContext(List resolverClasses, - List templatePaths, List tags, Map> variants) { + List templatePaths, List tags, Map> variants, + List templateInstanceInitializerClasses) { return new Supplier() { @Override @@ -36,6 +37,12 @@ public List getResolverClasses() { public Map> getVariants() { return variants; } + + @Override + public List getTemplateInstanceInitializerClasses() { + return templateInstanceInitializerClasses; + } + }; } }; @@ -50,6 +57,9 @@ public interface QuteContext { List getTags(); Map> getVariants(); + + List getTemplateInstanceInitializerClasses(); + } } diff --git a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/TemplateProducer.java b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/TemplateProducer.java index eae0502da1370..8f5ab80f301d0 100644 --- a/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/TemplateProducer.java +++ b/extensions/qute/runtime/src/main/java/io/quarkus/qute/runtime/TemplateProducer.java @@ -171,11 +171,13 @@ public CompletionStage consume(Consumer consumer) { private TemplateInstance templateInstance() { TemplateInstance instance = template().instance(); - instance.data(data()); + if (dataMap != null) { + dataMap.forEach(instance::data); + } else if (data != null) { + instance.data(data); + } if (!attributes.isEmpty()) { - for (Entry entry : attributes.entrySet()) { - instance.setAttribute(entry.getKey(), entry.getValue()); - } + attributes.forEach(instance::setAttribute); } return instance; } diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Engine.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Engine.java index f0beda997ab7b..322aafc2e8f45 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Engine.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Engine.java @@ -135,4 +135,9 @@ default Template parse(String content, Variant variant) { */ public Evaluator getEvaluator(); + /** + * @return an immutable list of template instance initializers + */ + public List getTemplateInstanceInitializers(); + } diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/EngineBuilder.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/EngineBuilder.java index cf6a91708a86c..fdd0a5c56e330 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/EngineBuilder.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/EngineBuilder.java @@ -29,6 +29,7 @@ public final class EngineBuilder { final List namespaceResolvers; final List locators; final List resultMappers; + final List initializers; Function> sectionHelperFunc; final List parserHooks; boolean removeStandaloneLines; @@ -42,6 +43,7 @@ public final class EngineBuilder { this.locators = new ArrayList<>(); this.resultMappers = new ArrayList<>(); this.parserHooks = new ArrayList<>(); + this.initializers = new ArrayList<>(); this.strictRendering = true; this.removeStandaloneLines = true; this.iterationMetadataPrefix = LoopSectionHelper.Factory.ITERATION_METADATA_PREFIX_ALIAS_UNDERSCORE; @@ -171,6 +173,16 @@ public EngineBuilder addResultMapper(ResultMapper mapper) { return this; } + /** + * + * @param initializer + * @return self + */ + public EngineBuilder addTemplateInstanceInitializer(TemplateInstance.Initializer initializer) { + this.initializers.add(initializer); + return this; + } + /** * The function is used if no section helper registered via {@link #addSectionHelper(SectionHelperFactory)} matches a * section name. diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/EngineImpl.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/EngineImpl.java index 5faaf13350dda..adc156923451e 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/EngineImpl.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/EngineImpl.java @@ -1,6 +1,7 @@ package io.quarkus.qute; import io.quarkus.qute.Parser.StringReader; +import io.quarkus.qute.TemplateInstance.Initializer; import io.quarkus.qute.TemplateLocator.TemplateLocation; import java.io.BufferedReader; import java.io.IOException; @@ -31,6 +32,7 @@ class EngineImpl implements Engine { private final List resultMappers; private final AtomicLong idGenerator = new AtomicLong(0); private final List parserHooks; + final List initializers; final boolean removeStandaloneLines; EngineImpl(EngineBuilder builder) { @@ -45,6 +47,7 @@ class EngineImpl implements Engine { this.sectionHelperFunc = builder.sectionHelperFunc; this.parserHooks = ImmutableList.copyOf(builder.parserHooks); this.removeStandaloneLines = builder.removeStandaloneLines; + this.initializers = ImmutableList.copyOf(builder.initializers); } @Override @@ -124,6 +127,11 @@ public void removeTemplates(Predicate test) { templates.keySet().removeIf(test); } + @Override + public List getTemplateInstanceInitializers() { + return initializers; + } + String generateId() { return "" + idGenerator.incrementAndGet(); } diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Template.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Template.java index 710a74dcd5018..afe708eaf261b 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/Template.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/Template.java @@ -19,6 +19,7 @@ public interface Template { * Create a new template instance to configure the model data. * * @return a new template instance + * @see TemplateInstance.Initializer */ TemplateInstance instance(); diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateExtension.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateExtension.java index b66e07cf9162e..e875b17474995 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateExtension.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateExtension.java @@ -17,7 +17,7 @@ *

* A template extension method: *

    - *
  • must not be private
  • + *
  • must not be private,
  • *
  • must be static,
  • *
  • must not return {@code void}.
  • *
diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateGlobal.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateGlobal.java new file mode 100644 index 0000000000000..f97f4fae7b0d2 --- /dev/null +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateGlobal.java @@ -0,0 +1,41 @@ +package io.quarkus.qute; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Denotes static fields and methods that supply global variables which are accessible in every template. + *

+ * If a class is annotated with {@link TemplateGlobal} then every non-void non-private static method that declares no parameters + * and every non-private static field is considered a global variable. The name is defaulted, i.e. the name of the + * field/method is used. + *

+ * A global variable method: + *

    + *
  • must not be private,
  • + *
  • must be static,
  • + *
  • must not accept any parameter,
  • + *
  • must not return {@code void}.
  • + *
+ *

+ * A global variable field: + *

    + *
  • must not be private,
  • + *
  • must be static.
  • + *
+ */ +@Target({ ElementType.TYPE, ElementType.FIELD, ElementType.METHOD }) +@Retention(RUNTIME) +public @interface TemplateGlobal { + + /** + * Constant value for {@link #name()} indicating that the field/method name should be used. + */ + String ELEMENT_NAME = "<>"; + + String name() default ELEMENT_NAME; + +} diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateImpl.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateImpl.java index c205f9b1918b0..647de65d33381 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateImpl.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateImpl.java @@ -27,7 +27,13 @@ class TemplateImpl implements Template { @Override public TemplateInstance instance() { - return new TemplateInstanceImpl(); + TemplateInstance instance = new TemplateInstanceImpl(); + if (!engine.initializers.isEmpty()) { + for (TemplateInstance.Initializer initializer : engine.initializers) { + initializer.accept(instance); + } + } + return instance; } @Override diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateInstance.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateInstance.java index 05616a4dea1cf..1a9fe1060633a 100644 --- a/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateInstance.java +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateInstance.java @@ -104,4 +104,14 @@ public interface TemplateInstance { */ CompletionStage consume(Consumer consumer); + /** + * This component can be used to initialize a template instance, i.e. the data and attributes. + * + * @see TemplateInstance#data(String, Object) + * @see TemplateInstance#setAttribute(String, Object) + */ + interface Initializer extends Consumer { + + } + } diff --git a/independent-projects/qute/core/src/test/java/io/quarkus/qute/TemplateInstanceInitializerTest.java b/independent-projects/qute/core/src/test/java/io/quarkus/qute/TemplateInstanceInitializerTest.java new file mode 100644 index 0000000000000..ddbb1815226bb --- /dev/null +++ b/independent-projects/qute/core/src/test/java/io/quarkus/qute/TemplateInstanceInitializerTest.java @@ -0,0 +1,24 @@ +package io.quarkus.qute; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +public class TemplateInstanceInitializerTest { + + @Test + public void testInitializer() { + Engine engine = Engine.builder() + .addDefaults() + .addTemplateInstanceInitializer(instance -> instance.data("foo", "bar").setAttribute("alpha", Boolean.TRUE)) + .build(); + + Template hello = engine.parse("Hello {foo}!"); + TemplateInstance instance = hello.instance(); + assertEquals(Boolean.TRUE, instance.getAttribute("alpha")); + assertEquals("Hello bar!", instance.render()); + instance.data("foo", "baz"); + assertEquals("Hello baz!", instance.render()); + } + +} diff --git a/independent-projects/qute/generator/pom.xml b/independent-projects/qute/generator/pom.xml index 113e3850ba46b..366de51280d14 100644 --- a/independent-projects/qute/generator/pom.xml +++ b/independent-projects/qute/generator/pom.xml @@ -31,6 +31,11 @@ junit-jupiter test + + org.assertj + assertj-core + test + diff --git a/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/Descriptors.java b/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/Descriptors.java index c691385385290..430afd28c4bff 100644 --- a/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/Descriptors.java +++ b/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/Descriptors.java @@ -8,6 +8,7 @@ import io.quarkus.qute.Expression; import io.quarkus.qute.Results; import io.quarkus.qute.Results.NotFound; +import io.quarkus.qute.TemplateInstance; import java.util.Collection; import java.util.List; import java.util.concurrent.CompletableFuture; @@ -91,6 +92,8 @@ private Descriptors() { CompletionStage.class, EvalContext.class); public static final MethodDescriptor NOT_FOUND_FROM_EC = MethodDescriptor.ofMethod(NotFound.class, "from", NotFound.class, EvalContext.class); + public static final MethodDescriptor TEMPLATE_INSTANCE_DATA = MethodDescriptor.ofMethod(TemplateInstance.class, "data", + TemplateInstance.class, String.class, Object.class); public static final FieldDescriptor EVALUATED_PARAMS_STAGE = FieldDescriptor.of(EvaluatedParams.class, "stage", CompletionStage.class); 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 new file mode 100644 index 0000000000000..a4f8929c60983 --- /dev/null +++ b/independent-projects/qute/generator/src/main/java/io/quarkus/qute/generator/TemplateGlobalGenerator.java @@ -0,0 +1,122 @@ +package io.quarkus.qute.generator; + +import static io.quarkus.qute.generator.ValueResolverGenerator.generatedNameFromTarget; +import static io.quarkus.qute.generator.ValueResolverGenerator.packageName; +import static io.quarkus.qute.generator.ValueResolverGenerator.simpleName; +import static org.objectweb.asm.Opcodes.ACC_PUBLIC; + +import io.quarkus.gizmo.ClassCreator; +import io.quarkus.gizmo.ClassOutput; +import io.quarkus.gizmo.FieldDescriptor; +import io.quarkus.gizmo.MethodCreator; +import io.quarkus.gizmo.MethodDescriptor; +import io.quarkus.gizmo.ResultHandle; +import io.quarkus.qute.TemplateGlobal; +import io.quarkus.qute.TemplateInstance; +import java.lang.reflect.Modifier; +import java.util.HashSet; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import org.jboss.jandex.AnnotationTarget; +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.DotName; +import org.jboss.jandex.FieldInfo; +import org.jboss.jandex.MethodInfo; +import org.jboss.jandex.Type.Kind; + +/** + * Generates {@link TemplateInstance.Initializer}s for {@link TemplateGlobal} annotations. + */ +public class TemplateGlobalGenerator { + + public static final DotName TEMPLATE_GLOBAL = DotName.createSimple(TemplateGlobal.class.getName()); + public static final String NAME = "name"; + + public static final String SUFFIX = "_Globals"; + + private final Set generatedTypes; + private final ClassOutput classOutput; + + public TemplateGlobalGenerator(ClassOutput classOutput) { + this.generatedTypes = new HashSet<>(); + this.classOutput = classOutput; + } + + public void generate(ClassInfo declaringClass, Map targets) { + + String baseName; + if (declaringClass.enclosingClass() != null) { + baseName = simpleName(declaringClass.enclosingClass()) + ValueResolverGenerator.NESTED_SEPARATOR + + simpleName(declaringClass); + } else { + baseName = simpleName(declaringClass); + } + String targetPackage = packageName(declaringClass.name()); + String generatedName = generatedNameFromTarget(targetPackage, baseName, SUFFIX); + generatedTypes.add(generatedName.replace('/', '.')); + + ClassCreator initializer = ClassCreator.builder().classOutput(classOutput).className(generatedName) + .interfaces(TemplateInstance.Initializer.class).build(); + + MethodCreator accept = initializer.getMethodCreator("accept", void.class, Object.class) + .setModifiers(ACC_PUBLIC); + + for (Entry entry : targets.entrySet()) { + ResultHandle name = accept.load(entry.getKey()); + ResultHandle global; + switch (entry.getValue().kind()) { + case FIELD: + FieldInfo field = entry.getValue().asField(); + validate(field); + global = accept.readStaticField(FieldDescriptor.of(field)); + break; + case METHOD: + MethodInfo method = entry.getValue().asMethod(); + validate(method); + global = accept.invokeStaticMethod(MethodDescriptor.of(method)); + break; + default: + throw new IllegalStateException("Unsupported target: " + entry.getValue()); + } + accept.invokeInterfaceMethod(Descriptors.TEMPLATE_INSTANCE_DATA, accept.getMethodParam(0), name, global); + } + accept.returnValue(null); + initializer.close(); + } + + public Set getGeneratedTypes() { + return generatedTypes; + } + + public static void validate(MethodInfo method) { + if (!Modifier.isStatic(method.flags())) { + throw new IllegalStateException( + "Global variable method declared on " + method.declaringClass().name() + " must be static: " + + method); + } + if (method.returnType().kind() == Kind.VOID) { + throw new IllegalStateException("Global variable method declared on " + method.declaringClass().name() + + " must not return void: " + method); + } + if (!method.parameters().isEmpty()) { + throw new IllegalStateException("Global variable method declared on " + method.declaringClass().name() + + " must not accept any parameter: " + method); + } + if (Modifier.isPrivate(method.flags())) { + throw new IllegalStateException("Global variable method declared on " + method.declaringClass().name() + + " must not be private: " + method); + } + } + + public static void validate(FieldInfo field) { + if (!Modifier.isStatic(field.flags())) { + throw new IllegalStateException( + "Global variable field declared on " + field.declaringClass().name() + " must be static: " + field); + } + if (Modifier.isPrivate(field.flags())) { + throw new IllegalStateException("Global variable field declared on " + field.declaringClass().name() + + " must not be private: " + field); + } + } +} diff --git a/independent-projects/qute/generator/src/test/java/io/quarkus/qute/generator/TemplateGlobalGeneratorTest.java b/independent-projects/qute/generator/src/test/java/io/quarkus/qute/generator/TemplateGlobalGeneratorTest.java new file mode 100644 index 0000000000000..abd1c0daec8b1 --- /dev/null +++ b/independent-projects/qute/generator/src/test/java/io/quarkus/qute/generator/TemplateGlobalGeneratorTest.java @@ -0,0 +1,78 @@ +package io.quarkus.qute.generator; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +import io.quarkus.qute.TemplateGlobal; +import java.io.IOException; +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.DotName; +import org.jboss.jandex.Index; +import org.jboss.jandex.PrimitiveType; +import org.junit.jupiter.api.Test; + +public class TemplateGlobalGeneratorTest { + + @Test + public void testValidation() throws IOException { + Index index = SimpleGeneratorTest.index(MyGlobals.class); + ClassInfo myGlobals = index.getClassByName(DotName.createSimple(MyGlobals.class.getName())); + + // Fields + assertThatExceptionOfType(IllegalStateException.class) + .isThrownBy(() -> TemplateGlobalGenerator.validate(myGlobals.field("privateField"))) + .withMessage( + "Global variable field declared on io.quarkus.qute.generator.TemplateGlobalGeneratorTest$MyGlobals must not be private: int io.quarkus.qute.generator.TemplateGlobalGeneratorTest$MyGlobals.privateField"); + assertThatExceptionOfType(IllegalStateException.class) + .isThrownBy(() -> TemplateGlobalGenerator.validate(myGlobals.field("nonStaticField"))) + .withMessage( + "Global variable field declared on io.quarkus.qute.generator.TemplateGlobalGeneratorTest$MyGlobals must be static: int io.quarkus.qute.generator.TemplateGlobalGeneratorTest$MyGlobals.nonStaticField"); + + // Methods + assertThatExceptionOfType(IllegalStateException.class) + .isThrownBy(() -> TemplateGlobalGenerator.validate(myGlobals.method("privateMethod"))) + .withMessage( + "Global variable method declared on io.quarkus.qute.generator.TemplateGlobalGeneratorTest$MyGlobals must not be private: int privateMethod()"); + assertThatExceptionOfType(IllegalStateException.class) + .isThrownBy(() -> TemplateGlobalGenerator.validate(myGlobals.method("nonStaticMethod"))) + .withMessage( + "Global variable method declared on io.quarkus.qute.generator.TemplateGlobalGeneratorTest$MyGlobals must be static: int nonStaticMethod()"); + assertThatExceptionOfType(IllegalStateException.class) + .isThrownBy(() -> TemplateGlobalGenerator.validate(myGlobals.method("voidMethod"))) + .withMessage( + "Global variable method declared on io.quarkus.qute.generator.TemplateGlobalGeneratorTest$MyGlobals must not return void: void voidMethod()"); + assertThatExceptionOfType(IllegalStateException.class) + .isThrownBy(() -> TemplateGlobalGenerator.validate(myGlobals.method("withParamsMethod", PrimitiveType.INT))) + .withMessage( + "Global variable method declared on io.quarkus.qute.generator.TemplateGlobalGeneratorTest$MyGlobals must not accept any parameter: int withParamsMethod(int foo)"); + } + + static class MyGlobals { + + @TemplateGlobal + private static int privateField; + + @TemplateGlobal + private int nonStaticField; + + @TemplateGlobal + private static int privateMethod() { + return 1; + } + + @TemplateGlobal + private int nonStaticMethod() { + return 1; + } + + @TemplateGlobal + private static void voidMethod() { + } + + @TemplateGlobal + private static int withParamsMethod(int foo) { + return foo; + } + + } + +}