Skip to content

Commit

Permalink
Merge pull request #22861 from mkouba/issue-21853
Browse files Browse the repository at this point in the history
Qute - support global variables
  • Loading branch information
mkouba authored Jan 13, 2022
2 parents b2135d7 + 8b7943a commit 9c9bff9
Show file tree
Hide file tree
Showing 26 changed files with 978 additions and 21 deletions.
88 changes: 88 additions & 0 deletions docs/src/main/asciidoc/qute-reference.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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 <<typesafe_expressions, parameter declarations>> 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.
<<typesafe_templates,Type-safe templates>> 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

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -343,7 +344,7 @@ List<CheckedTemplateBuildItem> collectCheckedTemplates(BeanArchiveIndexBuildItem
@BuildStep
TemplatesAnalysisBuildItem analyzeTemplates(List<TemplatePathBuildItem> templatePaths,
TemplateFilePathsBuildItem filePaths, List<CheckedTemplateBuildItem> checkedTemplates,
List<MessageBundleMethodBuildItem> messageBundleMethods, QuteConfig config) {
List<MessageBundleMethodBuildItem> messageBundleMethods, List<TemplateGlobalBuildItem> globals, QuteConfig config) {
long start = System.nanoTime();

checkDuplicatePaths(templatePaths);
Expand Down Expand Up @@ -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<String, String> entry : checkedTemplate.bindings.entrySet()) {
Expand Down Expand Up @@ -1063,16 +1069,18 @@ static String findTemplatePath(TemplatesAnalysisBuildItem analysis, String id) {

@BuildStep
void generateValueResolvers(QuteConfig config, BuildProducer<GeneratedClassBuildItem> generatedClasses,
CombinedIndexBuildItem combinedIndex, BeanArchiveIndexBuildItem beanArchiveIndex,
BeanArchiveIndexBuildItem beanArchiveIndex,
ApplicationArchivesBuildItem applicationArchivesBuildItem,
List<TemplatePathBuildItem> templatePaths,
List<TemplateExtensionMethodBuildItem> templateExtensionMethods,
List<ImplicitValueResolverBuildItem> implicitClasses,
TemplatesAnalysisBuildItem templatesAnalysis,
List<PanacheEntityClassesBuildItem> panacheEntityClasses,
List<TemplateDataBuildItem> templateData,
List<TemplateGlobalBuildItem> templateGlobals,
BuildProducer<GeneratedValueResolverBuildItem> generatedResolvers,
BuildProducer<ReflectiveClassBuildItem> reflectiveClass,
List<PanacheEntityClassesBuildItem> panacheEntityClasses,
List<TemplateDataBuildItem> templateData) {
BuildProducer<GeneratedTemplateInitializerBuildItem> generatedInitializers) {

IndexView index = beanArchiveIndex.getIndex();
ClassOutput classOutput = new GeneratedClassGizmoAdaptor(generatedClasses, new Function<String, String>() {
Expand All @@ -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, "$");
Expand Down Expand Up @@ -1139,8 +1150,8 @@ public Function<FieldInfo, String> apply(ClassInfo clazz) {
ValueResolverGenerator generator = builder.build();
generator.generate();

Set<String> generatedTypes = new HashSet<>();
generatedTypes.addAll(generator.getGeneratedTypes());
Set<String> generatedValueResolvers = new HashSet<>();
generatedValueResolvers.addAll(generator.getGeneratedTypes());

ExtensionMethodGenerator extensionMethodGenerator = new ExtensionMethodGenerator(index, classOutput);
Map<DotName, Map<String, List<TemplateExtensionMethodBuildItem>>> classToNamespaceExtensions = new HashMap<>();
Expand Down Expand Up @@ -1201,13 +1212,34 @@ public Function<FieldInfo, String> 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<DotName, Map<String, AnnotationTarget>> classToTargets = new HashMap<>();
Map<DotName, List<TemplateGlobalBuildItem>> classToGlobals = templateGlobals.stream()
.collect(Collectors.groupingBy(TemplateGlobalBuildItem::getDeclaringClass));
for (Entry<DotName, List<TemplateGlobalBuildItem>> entry : classToGlobals.entrySet()) {
classToTargets.put(entry.getKey(), entry.getValue().stream().collect(
Collectors.toMap(TemplateGlobalBuildItem::getName, TemplateGlobalBuildItem::getTarget)));
}

for (Entry<DotName, Map<String, AnnotationTarget>> 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));
}
}
}

Expand Down Expand Up @@ -1401,7 +1433,8 @@ public boolean test(TypeCheck check) {
@Record(value = STATIC_INIT)
void initialize(BuildProducer<SyntheticBeanBuildItem> syntheticBeans, QuteRecorder recorder,
List<GeneratedValueResolverBuildItem> generatedValueResolvers, List<TemplatePathBuildItem> templatePaths,
Optional<TemplateVariantsBuildItem> templateVariants) {
Optional<TemplateVariantsBuildItem> templateVariants,
List<GeneratedTemplateInitializerBuildItem> templateInitializers) {

List<String> templates = new ArrayList<>();
List<String> tags = new ArrayList<>();
Expand All @@ -1424,7 +1457,8 @@ void initialize(BuildProducer<SyntheticBeanBuildItem> 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());
}

Expand Down Expand Up @@ -1986,6 +2020,85 @@ private void processsTemplateData(TemplateDataBuildItem templateData,
}
}

@BuildStep
void collectTemplateGlobals(BeanArchiveIndexBuildItem beanArchiveIndex, BuildProducer<TemplateGlobalBuildItem> globals) {
IndexView index = beanArchiveIndex.getIndex();
Map<String, TemplateGlobalBuildItem> 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<String, TemplateGlobalBuildItem> 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<String, TemplateGlobalBuildItem> 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<String, TemplateGlobalBuildItem> 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<String, TemplateGlobalBuildItem> 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<TemplateDataBuildItem> templateDataAnnotations) {
Expand Down
Loading

0 comments on commit 9c9bff9

Please sign in to comment.