Skip to content

Commit

Permalink
Qute - introduce the TemplateEnum annotation
Browse files Browse the repository at this point in the history
- resolves #21854
  • Loading branch information
mkouba committed Dec 9, 2021
1 parent 7ac7d02 commit 772f18b
Show file tree
Hide file tree
Showing 13 changed files with 493 additions and 71 deletions.
38 changes: 34 additions & 4 deletions docs/src/main/asciidoc/qute-reference.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1632,35 +1632,65 @@ class Item {

==== Accessing Static Fields and Methods

If `@TemplateData#namespace()` is set to a non-empty value then a namespace resolver is automatically generated to access static fields and methods of the target class.
If `@TemplateData#namespace()` is set to a non-empty value then a namespace resolver is automatically generated to access the public static fields and methods of the target class.
By default, the namespace is the FQCN of the target class where dots and dollar signs are replaced by underscores.
For example, the namespace for a class with name `org.acme.Foo` is `org_acme_Foo`.
The static field `Foo.AGE` can be accessed via `{org_acme_Foo:AGE}`.
The static method `Foo.computeValue(int number)` can be accessed via `{org_acme_Foo:computeValue(10)}`.

NOTE: A namespace can only consist of alphanumeric characters and underscores.

.Enum Annotated With `@TemplateData`
.Class Annotated With `@TemplateData`
[source,java]
----
package model;
@TemplateData <1>
public class Statuses {
public static final String ON = "on";
public static final String OFF = "off";
}
----
<1> A name resolver with the namespace `model_Status` is generated automatically.

.Template Accessing Class Constants
[source,html]
----
{#if machine.status == model_Status:ON}
The machine is ON!
{/if}
----

==== Convenient Annotation For Enums

There's also a convenient annotation to access enum constants: `@io.quarkus.qute.TemplateEnum`.
This annotation is functionally equivalent to `@TemplateData(namespace = TemplateData.SIMPLENAME)`, i.e. a namespace resolver is automatically generated for the target enum and the simple name of the target enum is used as the namespace.

.Enum Annotated With `@TemplateEnum`
[source,java]
----
package model;
@TemplateEnum <1>
public enum Status {
ON,
OFF
}
----
<1> A name resolver with the namespace `model_Status` is generated automatically.
<1> A name resolver with the namespace `Status` is generated automatically.

NOTE: `@TemplateEnum` declared on a non-enum class is ignored. Also if an enum also declares the `@TemplateData` annotation then the `@TemplateEnum` annotation is ignored.

.Template Accessing Enum Constants
[source,html]
----
{#if machine.status == model_Status:ON}
{#if machine.status == Status:ON}
The machine is ON!
{/if}
----

TIP: Quarkus detects possible namespace collisions and fails the build if a specific namespace is defined by multiple `@TemplateData` and/or `@TemplateEnum` annotations.

[[native_executables]]
=== Native Executables

Expand Down
6 changes: 5 additions & 1 deletion extensions/qute/deployment/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,11 @@
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import io.quarkus.qute.Location;
import io.quarkus.qute.Template;
import io.quarkus.qute.TemplateEnum;
import io.quarkus.qute.TemplateInstance;
import io.quarkus.qute.i18n.Localized;
import io.quarkus.qute.i18n.Message;
Expand All @@ -36,6 +37,7 @@ final class Names {
static final DotName UNI = DotName.createSimple(Uni.class.getName());
static final DotName LOCATION = DotName.createSimple(Location.class.getName());
static final DotName CHECKED_TEMPLATE = DotName.createSimple(io.quarkus.qute.CheckedTemplate.class.getName());
static final DotName TEMPLATE_ENUM = DotName.createSimple(TemplateEnum.class.getName());

private Names() {
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1072,7 +1072,8 @@ void generateValueResolvers(QuteConfig config, BuildProducer<GeneratedClassBuild
TemplatesAnalysisBuildItem templatesAnalysis,
BuildProducer<GeneratedValueResolverBuildItem> generatedResolvers,
BuildProducer<ReflectiveClassBuildItem> reflectiveClass,
List<PanacheEntityClassesBuildItem> panacheEntityClasses) {
List<PanacheEntityClassesBuildItem> panacheEntityClasses,
List<TemplateDataBuildItem> templateData) {

IndexView index = beanArchiveIndex.getIndex();
ClassOutput classOutput = new GeneratedClassGizmoAdaptor(generatedClasses, new Function<String, String>() {
Expand Down Expand Up @@ -1117,13 +1118,8 @@ public Function<FieldInfo, String> apply(ClassInfo clazz) {

Set<DotName> controlled = new HashSet<>();
Map<DotName, AnnotationInstance> uncontrolled = new HashMap<>();
for (AnnotationInstance templateData : index.getAnnotations(ValueResolverGenerator.TEMPLATE_DATA)) {
processsTemplateData(index, templateData, templateData.target(), controlled, uncontrolled, builder);
}
for (AnnotationInstance containerInstance : index.getAnnotations(ValueResolverGenerator.TEMPLATE_DATA_CONTAINER)) {
for (AnnotationInstance templateData : containerInstance.value().asNestedArray()) {
processsTemplateData(index, templateData, containerInstance.target(), controlled, uncontrolled, builder);
}
for (TemplateDataBuildItem data : templateData) {
processsTemplateData(data, controlled, uncontrolled, builder);
}

for (ImplicitValueResolverBuildItem implicit : implicitClasses) {
Expand Down Expand Up @@ -1961,35 +1957,17 @@ private static boolean methodMatches(MethodInfo method, VirtualMethodPart virtua
return matches;
}

private void processsTemplateData(IndexView index, AnnotationInstance templateData, AnnotationTarget annotationTarget,
private void processsTemplateData(TemplateDataBuildItem templateData,
Set<DotName> controlled, Map<DotName, AnnotationInstance> uncontrolled, ValueResolverGenerator.Builder builder) {
AnnotationValue targetValue = templateData.value(ValueResolverGenerator.TARGET);
if (targetValue == null || targetValue.asClass().name().equals(ValueResolverGenerator.TEMPLATE_DATA)) {
ClassInfo annotationTargetClass = annotationTarget.asClass();
controlled.add(annotationTargetClass.name());
builder.addClass(annotationTargetClass, templateData);
if (templateData.isTargetAnnotatedType()) {
controlled.add(templateData.getTargetClass().name());
builder.addClass(templateData.getTargetClass(), templateData.getAnnotationInstance());
} else {
ClassInfo uncontrolledClass = index.getClassByName(targetValue.asClass().name());
if (uncontrolledClass != null) {
uncontrolled.compute(uncontrolledClass.name(), (c, v) -> {
if (v == null) {
builder.addClass(uncontrolledClass, templateData);
return templateData;
}
if (!Objects.equals(v.value(ValueResolverGenerator.IGNORE),
templateData.value(ValueResolverGenerator.IGNORE))
|| !Objects.equals(v.value(ValueResolverGenerator.PROPERTIES),
templateData.value(ValueResolverGenerator.PROPERTIES))
|| !Objects.equals(v.value(ValueResolverGenerator.IGNORE_SUPERCLASSES),
templateData.value(ValueResolverGenerator.IGNORE_SUPERCLASSES))) {
throw new IllegalStateException(
"Multiple unequal @TemplateData declared for " + c + ": " + v + " and " + templateData);
}
return v;
});
} else {
LOGGER.warnf("@TemplateData#target() not available: %s", annotationTarget.asClass().name());
}
// At this point we can be sure that multiple unequal @TemplateData do not exist for a specific target
uncontrolled.computeIfAbsent(templateData.getTargetClass().name(), name -> {
builder.addClass(templateData.getTargetClass(), templateData.getAnnotationInstance());
return templateData.getAnnotationInstance();
});
}
}

Expand All @@ -2007,38 +1985,87 @@ void collectTemplateDataAnnotations(BeanArchiveIndexBuildItem beanArchiveIndex,
}
}

Map<DotName, AnnotationInstance> uncontrolled = new HashMap<>();

for (AnnotationInstance templateData : annotationInstances) {
AnnotationValue targetValue = templateData.value(ValueResolverGenerator.TARGET);
AnnotationValue ignoreValue = templateData.value(ValueResolverGenerator.IGNORE);
AnnotationValue propertiesValue = templateData.value(ValueResolverGenerator.PROPERTIES);
AnnotationValue namespaceValue = templateData.value(ValueResolverGenerator.NAMESPACE);
AnnotationValue ignoreSuperclassesValue = templateData.value(ValueResolverGenerator.IGNORE_SUPERCLASSES);

ClassInfo targetClass = null;
if (targetValue == null || targetValue.asClass().name().equals(ValueResolverGenerator.TEMPLATE_DATA)) {
targetClass = templateData.target().asClass();
} else {
targetClass = index.getClassByName(targetValue.asClass().name());
}
if (targetClass == null) {
LOGGER.warnf("@TemplateData declared on %s is ignored: target %s it is not available in the index",
templateData.target(), targetClass);
continue;
}
uncontrolled.compute(targetClass.name(), (c, v) -> {
if (v == null) {
return templateData;
}
if (!Objects.equals(v.value(ValueResolverGenerator.IGNORE),
templateData.value(ValueResolverGenerator.IGNORE))
|| !Objects.equals(v.value(ValueResolverGenerator.PROPERTIES),
templateData.value(ValueResolverGenerator.PROPERTIES))
|| !Objects.equals(v.value(ValueResolverGenerator.IGNORE_SUPERCLASSES),
templateData.value(ValueResolverGenerator.IGNORE_SUPERCLASSES))
|| !Objects.equals(v.value(ValueResolverGenerator.NAMESPACE),
templateData.value(ValueResolverGenerator.NAMESPACE))) {
throw new IllegalStateException(
"Multiple unequal @TemplateData declared for " + c + ": " + v + " and " + templateData);
}
return v;
});
templateDataAnnotations.produce(new TemplateDataBuildItem(templateData, targetClass));
}

if (targetClass != null) {
String namespace = namespaceValue != null ? namespaceValue.asString() : TemplateData.UNDERSCORED_FQCN;
if (namespace.equals(TemplateData.UNDERSCORED_FQCN)) {
namespace = ValueResolverGenerator
.underscoredFullyQualifiedName(targetClass.name().toString());
} else if (namespace.equals(TemplateData.SIMPLENAME)) {
namespace = ValueResolverGenerator.simpleName(targetClass);
}
templateDataAnnotations.produce(new TemplateDataBuildItem(targetClass,
namespace,
ignoreValue != null ? ignoreValue.asStringArray() : new String[] {},
ignoreSuperclassesValue != null ? ignoreSuperclassesValue.asBoolean() : false,
propertiesValue != null ? propertiesValue.asBoolean() : false));
// Add synthetic @TemplateData for template enums
for (AnnotationInstance templateEnum : index.getAnnotations(Names.TEMPLATE_ENUM)) {
ClassInfo targetEnum = templateEnum.target().asClass();
if (!targetEnum.isEnum()) {
LOGGER.warnf("@TemplateEnum declared on %s is ignored: the target of this annotation must be an enum type",
targetEnum);
continue;
}
if (targetEnum.classAnnotation(ValueResolverGenerator.TEMPLATE_DATA) != null) {
LOGGER.debugf("@TemplateEnum declared on %s is ignored: enum is annotated with @TemplateData", targetEnum);
continue;
}
AnnotationInstance uncontrolledDeclaration = uncontrolled.get(targetEnum.name());
if (uncontrolledDeclaration != null) {
LOGGER.debugf("@TemplateEnum declared on %s is ignored: %s declared on %s", targetEnum, uncontrolledDeclaration,
uncontrolledDeclaration.target());
continue;
}
templateDataAnnotations.produce(new TemplateDataBuildItem(
new TemplateDataBuilder().annotationTarget(templateEnum.target()).namespace(TemplateData.SIMPLENAME)
.build(),
targetEnum));
}

}

@BuildStep
void validateTemplateDataNamespaces(List<TemplateDataBuildItem> templateData,
BuildProducer<ServiceStartBuildItem> serviceStart) {

Map<String, List<TemplateDataBuildItem>> namespaceToData = templateData.stream()
.filter(TemplateDataBuildItem::hasNamespace)
.collect(Collectors.groupingBy(TemplateDataBuildItem::getNamespace));
for (Map.Entry<String, List<TemplateDataBuildItem>> e : namespaceToData.entrySet()) {
if (e.getValue().size() > 1) {
throw new TemplateException(
String.format(
"The namespace [%s] is defined by multiple @TemplateData and/or @TemplateEnum annotations; make sure the annotation declared on the following classes do not collide:\n\t- %s",
e.getKey(), e.getValue()
.stream().map(TemplateDataBuildItem::getAnnotationInstance)
.map(AnnotationInstance::target).map(Object::toString)
.collect(Collectors.joining("\n\t- "))));
}
}
}

static Map<TemplateAnalysis, Set<Expression>> collectNamespaceExpressions(TemplatesAnalysisBuildItem analysis,
String namespace) {
Map<TemplateAnalysis, Set<Expression>> namespaceExpressions = new HashMap<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@
import java.util.Arrays;
import java.util.regex.Pattern;

import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.AnnotationTarget;
import org.jboss.jandex.AnnotationTarget.Kind;
import org.jboss.jandex.AnnotationValue;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.FieldInfo;
import org.jboss.jandex.MethodInfo;

import io.quarkus.builder.item.MultiBuildItem;
import io.quarkus.qute.TemplateData;
import io.quarkus.qute.generator.ValueResolverGenerator;

final class TemplateDataBuildItem extends MultiBuildItem {

Expand All @@ -19,14 +23,26 @@ final class TemplateDataBuildItem extends MultiBuildItem {
private final Pattern[] ignorePatterns;
private final boolean ignoreSuperclasses;
private final boolean properties;
private final AnnotationInstance annotationInstance;

TemplateDataBuildItem(AnnotationInstance annotationInstance, ClassInfo targetClass) {
this.annotationInstance = annotationInstance;

AnnotationValue ignoreValue = annotationInstance.value(ValueResolverGenerator.IGNORE);
AnnotationValue propertiesValue = annotationInstance.value(ValueResolverGenerator.PROPERTIES);
AnnotationValue namespaceValue = annotationInstance.value(ValueResolverGenerator.NAMESPACE);
AnnotationValue ignoreSuperclassesValue = annotationInstance.value(ValueResolverGenerator.IGNORE_SUPERCLASSES);

public TemplateDataBuildItem(ClassInfo targetClass, String namespace, String[] ignore, boolean ignoreSuperclasses,
boolean properties) {
this.targetClass = targetClass;
String namespace = namespaceValue != null ? namespaceValue.asString() : TemplateData.UNDERSCORED_FQCN;
if (namespace.equals(TemplateData.UNDERSCORED_FQCN)) {
namespace = ValueResolverGenerator
.underscoredFullyQualifiedName(targetClass.name().toString());
} else if (namespace.equals(TemplateData.SIMPLENAME)) {
namespace = ValueResolverGenerator.simpleName(targetClass);
}
this.namespace = namespace;
this.ignore = ignore;
this.ignoreSuperclasses = ignoreSuperclasses;
this.properties = properties;
this.ignore = ignoreValue != null ? ignoreValue.asStringArray() : new String[] {};
if (ignore.length > 0) {
ignorePatterns = new Pattern[ignore.length];
for (int i = 0; i < ignore.length; i++) {
Expand All @@ -35,32 +51,42 @@ public TemplateDataBuildItem(ClassInfo targetClass, String namespace, String[] i
} else {
ignorePatterns = null;
}
this.ignoreSuperclasses = ignoreSuperclassesValue != null ? ignoreSuperclassesValue.asBoolean() : false;
this.properties = propertiesValue != null ? propertiesValue.asBoolean() : false;
}

public ClassInfo getTargetClass() {
boolean isTargetAnnotatedType() {
return targetClass.asClass().name().equals(ValueResolverGenerator.TEMPLATE_DATA);
}

ClassInfo getTargetClass() {
return targetClass;
}

public boolean hasNamespace() {
boolean hasNamespace() {
return namespace != null;
}

public String getNamespace() {
String getNamespace() {
return namespace;
}

public String[] getIgnore() {
String[] getIgnore() {
return ignore;
}

public boolean isIgnoreSuperclasses() {
boolean isIgnoreSuperclasses() {
return ignoreSuperclasses;
}

public boolean isProperties() {
boolean isProperties() {
return properties;
}

AnnotationInstance getAnnotationInstance() {
return annotationInstance;
}

boolean filter(AnnotationTarget target) {
String name = null;
if (target.kind() == Kind.METHOD) {
Expand Down
Loading

0 comments on commit 772f18b

Please sign in to comment.