diff --git a/docs/src/main/asciidoc/qute-reference.adoc b/docs/src/main/asciidoc/qute-reference.adoc index ccaee4d47fbd4..5d55dd19b6ccc 100644 --- a/docs/src/main/asciidoc/qute-reference.adoc +++ b/docs/src/main/asciidoc/qute-reference.adoc @@ -1632,7 +1632,7 @@ 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}`. @@ -1640,27 +1640,57 @@ The static method `Foo.computeValue(int number)` can be accessed via `{org_acme_ 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 diff --git a/extensions/qute/deployment/pom.xml b/extensions/qute/deployment/pom.xml index 293b2eda1ac38..6ec874959be41 100644 --- a/extensions/qute/deployment/pom.xml +++ b/extensions/qute/deployment/pom.xml @@ -58,7 +58,11 @@ rest-assured test - + + org.assertj + assertj-core + test + diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/Names.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/Names.java index 32a3f4923155f..d0b2c5c6e0bf0 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/Names.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/Names.java @@ -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; @@ -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() { } 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 3594df36ea84e..9620f79d8fc23 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 @@ -1072,7 +1072,8 @@ void generateValueResolvers(QuteConfig config, BuildProducer generatedResolvers, BuildProducer reflectiveClass, - List panacheEntityClasses) { + List panacheEntityClasses, + List templateData) { IndexView index = beanArchiveIndex.getIndex(); ClassOutput classOutput = new GeneratedClassGizmoAdaptor(generatedClasses, new Function() { @@ -1117,13 +1118,8 @@ public Function apply(ClassInfo clazz) { Set controlled = new HashSet<>(); Map 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) { @@ -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 controlled, Map 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(); + }); } } @@ -2007,38 +1985,87 @@ void collectTemplateDataAnnotations(BeanArchiveIndexBuildItem beanArchiveIndex, } } + Map 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 templateData, + BuildProducer serviceStart) { + + Map> namespaceToData = templateData.stream() + .filter(TemplateDataBuildItem::hasNamespace) + .collect(Collectors.groupingBy(TemplateDataBuildItem::getNamespace)); + for (Map.Entry> 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> collectNamespaceExpressions(TemplatesAnalysisBuildItem analysis, String namespace) { Map> namespaceExpressions = new HashMap<>(); diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateDataBuildItem.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateDataBuildItem.java index 4d77f114a9794..ae6bad7fe92a4 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateDataBuildItem.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateDataBuildItem.java @@ -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 { @@ -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++) { @@ -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) { diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateDataBuilder.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateDataBuilder.java index 423e996f508e8..58606a3aacd34 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateDataBuilder.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/TemplateDataBuilder.java @@ -2,9 +2,13 @@ import java.util.ArrayList; import java.util.List; +import java.util.Objects; import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.AnnotationTarget; import org.jboss.jandex.AnnotationValue; +import org.jboss.jandex.Type; +import org.jboss.jandex.Type.Kind; import io.quarkus.qute.TemplateData; import io.quarkus.qute.generator.ValueResolverGenerator; @@ -14,11 +18,14 @@ public final class TemplateDataBuilder { private final List ignores; private boolean ignoreSuperclasses; private boolean properties; + private String namespace; + private AnnotationTarget annotationTarget; public TemplateDataBuilder() { ignores = new ArrayList<>(); ignoreSuperclasses = false; properties = false; + namespace = TemplateData.UNDERSCORED_FQCN; } /** @@ -46,6 +53,16 @@ public TemplateDataBuilder properties(boolean value) { return this; } + public TemplateDataBuilder namespace(String value) { + namespace = Objects.requireNonNull(value); + return this; + } + + public TemplateDataBuilder annotationTarget(AnnotationTarget value) { + annotationTarget = Objects.requireNonNull(value); + return this; + } + public AnnotationInstance build() { AnnotationValue ignoreValue; if (ignores.isEmpty()) { @@ -60,8 +77,11 @@ public AnnotationInstance build() { AnnotationValue propertiesValue = AnnotationValue.createBooleanValue(ValueResolverGenerator.PROPERTIES, properties); AnnotationValue ignoreSuperclassesValue = AnnotationValue.createBooleanValue(ValueResolverGenerator.IGNORE_SUPERCLASSES, ignoreSuperclasses); - return AnnotationInstance.create(ValueResolverGenerator.TEMPLATE_DATA, null, - new AnnotationValue[] { ignoreValue, propertiesValue, ignoreSuperclassesValue }); + AnnotationValue namespaceValue = AnnotationValue.createStringValue("namespace", namespace); + AnnotationValue targetValue = AnnotationValue.createClassValue("target", + Type.create(ValueResolverGenerator.TEMPLATE_DATA, Kind.CLASS)); + return AnnotationInstance.create(ValueResolverGenerator.TEMPLATE_DATA, annotationTarget, + new AnnotationValue[] { targetValue, ignoreValue, propertiesValue, ignoreSuperclassesValue, namespaceValue }); } } diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/enums/TemplateEnumIgnoredTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/enums/TemplateEnumIgnoredTest.java new file mode 100644 index 0000000000000..765737622afc8 --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/enums/TemplateEnumIgnoredTest.java @@ -0,0 +1,47 @@ +package io.quarkus.qute.deployment.enums; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import javax.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.qute.Engine; +import io.quarkus.qute.TemplateData; +import io.quarkus.qute.TemplateEnum; +import io.quarkus.qute.TemplateException; +import io.quarkus.test.QuarkusUnitTest; + +public class TemplateEnumIgnoredTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot(root -> root + .addClasses(TransactionType.class)); + + @Inject + Engine engine; + + @Test + public void testTemplateData() { + assertEquals("FOO", + engine.parse("{io_quarkus_qute_deployment_enums_TemplateEnumIgnoredTest_TransactionType:FOO}").render()); + assertThatExceptionOfType(TemplateException.class) + .isThrownBy(() -> engine.parse("{TransactionType:FOO}", null, "bar").render()) + .withMessage( + "No namespace resolver found for [TransactionType] in expression {TransactionType:FOO} in template bar on line 1"); + + } + + @TemplateEnum // ignored + @TemplateData(namespace = TemplateData.UNDERSCORED_FQCN) + public static enum TransactionType { + + FOO, + BAR + + } + +} diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/enums/TemplateEnumInvalidTargetTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/enums/TemplateEnumInvalidTargetTest.java new file mode 100644 index 0000000000000..c0ac7eefd33ab --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/enums/TemplateEnumInvalidTargetTest.java @@ -0,0 +1,41 @@ +package io.quarkus.qute.deployment.enums; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +import javax.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.qute.Engine; +import io.quarkus.qute.TemplateEnum; +import io.quarkus.qute.TemplateException; +import io.quarkus.test.QuarkusUnitTest; + +public class TemplateEnumInvalidTargetTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot(root -> root + .addClasses(Transactions.class)); + + @Inject + Engine engine; + + @Test + public void testTemplateEnum() { + assertThatExceptionOfType(TemplateException.class) + .isThrownBy(() -> engine.parse("{Transactions:VAL}", null, "bar").render()) + .withMessage( + "No namespace resolver found for [Transactions] in expression {Transactions:VAL} in template bar on line 1"); + + } + + @TemplateEnum // ignored + public static class Transactions { + + public static final int VAL = 10; + + } + +} diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/enums/TemplateEnumNamespaceValidationTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/enums/TemplateEnumNamespaceValidationTest.java new file mode 100644 index 0000000000000..6527dcd4afdb4 --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/enums/TemplateEnumNamespaceValidationTest.java @@ -0,0 +1,56 @@ +package io.quarkus.qute.deployment.enums; + +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.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.qute.TemplateData; +import io.quarkus.qute.TemplateEnum; +import io.quarkus.qute.TemplateException; +import io.quarkus.test.QuarkusUnitTest; + +public class TemplateEnumNamespaceValidationTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(TransactionType.class, Transactions.class)) + .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( + "The namespace [TransactionType] is defined by multiple @TemplateData and/or @TemplateEnum annotations"), + te.getMessage()); + }); + + @Test + public void test() { + fail(); + } + + // namespace is TransactionType + @TemplateEnum + public static enum TransactionType { + + FOO, + BAR + + } + + @TemplateData(namespace = "TransactionType") + public static class Transactions { + + } + +} diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/enums/TemplateEnumTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/enums/TemplateEnumTest.java new file mode 100644 index 0000000000000..c8cf86ed2b530 --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/enums/TemplateEnumTest.java @@ -0,0 +1,42 @@ +package io.quarkus.qute.deployment.enums; + +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.Template; +import io.quarkus.qute.TemplateEnum; +import io.quarkus.test.QuarkusUnitTest; + +public class TemplateEnumTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(TransactionType.class) + .addAsResource(new StringAsset( + "{#if tx == TransactionType:FOO}OK{/if}::{TransactionType:BAR}::{TransactionType:values[0]}"), + "templates/bar.txt")); + + @Inject + Template bar; + + @Test + public void testTemplateData() { + assertEquals("OK::BAR::FOO", bar.data("tx", TransactionType.FOO).render()); + } + + // namespace is TransactionType + @TemplateEnum + public static enum TransactionType { + + FOO, + BAR + + } + +} diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/enums/TemplateEnumValidationFailureTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/enums/TemplateEnumValidationFailureTest.java new file mode 100644 index 0000000000000..7d5dd2270bc8a --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/enums/TemplateEnumValidationFailureTest.java @@ -0,0 +1,56 @@ +package io.quarkus.qute.deployment.enums; + +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.TemplateEnum; +import io.quarkus.qute.TemplateException; +import io.quarkus.test.QuarkusUnitTest; + +public class TemplateEnumValidationFailureTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot(root -> root + .addClasses(TransactionType.class) + .addAsResource(new StringAsset( + "{TransactionType:FOO}{TransactionType:BAR.scores}"), + "templates/foo.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("Found template problems (1)"), te.getMessage()); + assertTrue(te.getMessage().contains("TransactionType:BAR.scores"), te.getMessage()); + }); + + @Test + public void test() { + fail(); + } + + @TemplateEnum + public static enum TransactionType { + + FOO, + BAR; + + public int getScore() { + return 42; + } + + } + +} diff --git a/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/enums/TemplateEnumValidationSuccessTest.java b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/enums/TemplateEnumValidationSuccessTest.java new file mode 100644 index 0000000000000..382ae1cfca9d1 --- /dev/null +++ b/extensions/qute/deployment/src/test/java/io/quarkus/qute/deployment/enums/TemplateEnumValidationSuccessTest.java @@ -0,0 +1,45 @@ +package io.quarkus.qute.deployment.enums; + +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.Template; +import io.quarkus.qute.TemplateEnum; +import io.quarkus.test.QuarkusUnitTest; + +public class TemplateEnumValidationSuccessTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot(root -> root + .addClasses(TransactionType.class) + .addAsResource(new StringAsset( + "{TransactionType:FOO}::{TransactionType:BAR.score}"), + "templates/foo.txt")); + + @Inject + Template foo; + + @Test + public void testEnum() { + assertEquals("FOO::42", foo.render()); + } + + @TemplateEnum + public static enum TransactionType { + + FOO, + BAR; + + public int getScore() { + return 42; + } + + } + +} diff --git a/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateEnum.java b/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateEnum.java new file mode 100644 index 0000000000000..a7c737b93b2f3 --- /dev/null +++ b/independent-projects/qute/core/src/main/java/io/quarkus/qute/TemplateEnum.java @@ -0,0 +1,26 @@ +package io.quarkus.qute; + +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * This annotation is functionally equivalent to {@code @TemplateData(namespace = TemplateData.SIMPLENAME)}, i.e. a namespace + * resolver is automatically generated for the target enum. The simple name of the target enum is used as the namespace. The + * generated namespace resolver can be used to access enum constants, static methods, etc. + *

+ * If an enum also declares the {@link TemplateData} annotation or is specified by any {@link TemplateData#target()} then the + * {@link TemplateEnum} annotation is ignored. + *

+ * {@link TemplateEnum} declared on non-enum classes is ignored. + * + * @see TemplateData + * @see NamespaceResolver + */ +@Target(TYPE) +@Retention(RUNTIME) +public @interface TemplateEnum { + +}