Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Qute: introduce the TemplateContents annotation #45499

Merged
merged 1 commit into from
Jan 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions docs/src/main/asciidoc/qute-reference.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -2095,6 +2095,44 @@ class ItemService {

NOTE: You can specify `@CheckedTemplate#ignoreFragments=true` in order to disable this feature, i.e. a dollar sign `$` in the method name will not result in a checked fragment method.

[[template_contents]]
==== Template Contents

It is also possible to specify the contents for a type-safe template directly in your Java code.
A `static native` method of a class annotated with `@CheckedTemplate` or a Java record that implements `TemplateInstance` may be annotated with `@io.quarkus.qute.TemplateContents`.
The annotation value is used as the template contents.
The template id/path is derived from the type-safe template.

.Template Contents Example
[source,java]
----
package org.acme.quarkus.sample;

import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

import io.quarkus.qute.TemplateContents;
import io.quarkus.qute.TemplateInstance;

@Path("hello")
public class HelloResource {

@TemplateContents("Hello {name}!") <1>
record Hello(String name) implements TemplateInstance {}

@GET
@Produces(MediaType.TEXT_PLAIN)
public TemplateInstance get(@QueryParam("name") String name) {
return new Hello(name);
}
}
----
<1> Defines the contents for the type-safe template represented by the `Hello` record. The derived template id is `HelloResource/Hello`.

[[template_extension_methods]]
=== Template Extension Methods

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import io.quarkus.qute.ParserHook;
import io.quarkus.qute.SectionHelperFactory;
import io.quarkus.qute.Template;
import io.quarkus.qute.TemplateContents;
import io.quarkus.qute.TemplateEnum;
import io.quarkus.qute.TemplateInstance;
import io.quarkus.qute.TemplateLocator;
Expand Down Expand Up @@ -54,6 +55,7 @@ final class Names {
static final DotName VALUE_RESOLVER = DotName.createSimple(ValueResolver.class.getName());
static final DotName NAMESPACE_RESOLVER = DotName.createSimple(NamespaceResolver.class.getName());
static final DotName PARSER_HOOK = DotName.createSimple(ParserHook.class);
static final DotName TEMPLATE_CONTENTS = DotName.createSimple(TemplateContents.class);

private Names() {
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -325,53 +325,41 @@ List<CheckedTemplateBuildItem> collectCheckedTemplates(BeanArchiveIndexBuildItem
if (annotation.target().kind() != Kind.CLASS) {
continue;
}
ClassInfo classInfo = annotation.target().asClass();
if (classInfo.isRecord()) {
ClassInfo targetClass = annotation.target().asClass();
if (targetClass.isRecord()) {
// Template records are processed separately
continue;
}
NativeCheckedTemplateEnhancer enhancer = new NativeCheckedTemplateEnhancer();
for (MethodInfo methodInfo : classInfo.methods()) {
for (MethodInfo method : targetClass.methods()) {
// only keep native static methods
if (!Modifier.isStatic(methodInfo.flags())
|| !Modifier.isNative(methodInfo.flags())) {
if (!Modifier.isStatic(method.flags())
|| !Modifier.isNative(method.flags())) {
continue;
}
// check its return type
if (methodInfo.returnType().kind() != Type.Kind.CLASS) {
throw new TemplateException("Incompatible checked template return type: " + methodInfo.returnType()
if (method.returnType().kind() != Type.Kind.CLASS) {
throw new TemplateException("Incompatible checked template return type: " + method.returnType()
+ " only " + supportedAdaptors);
}
DotName returnTypeName = methodInfo.returnType().asClassType().name();
DotName returnTypeName = method.returnType().asClassType().name();
CheckedTemplateAdapter adaptor = null;
// if it's not the default template instance, try to find an adapter
if (!returnTypeName.equals(Names.TEMPLATE_INSTANCE)) {
adaptor = adaptors.get(returnTypeName);
if (adaptor == null)
throw new TemplateException("Incompatible checked template return type: " + methodInfo.returnType()
throw new TemplateException("Incompatible checked template return type: " + method.returnType()
+ " only " + supportedAdaptors);
}
String fragmentId = getCheckedFragmentId(methodInfo, annotation);
StringBuilder templatePathBuilder = new StringBuilder();
AnnotationValue basePathValue = annotation.value(CHECKED_TEMPLATE_BASE_PATH);
if (basePathValue != null && !basePathValue.asString().equals(CheckedTemplate.DEFAULTED)) {
templatePathBuilder.append(basePathValue.asString());
} else if (classInfo.enclosingClass() != null) {
ClassInfo enclosingClass = index.getIndex().getClassByName(classInfo.enclosingClass());
templatePathBuilder.append(enclosingClass.simpleName());
}
if (templatePathBuilder.length() > 0 && templatePathBuilder.charAt(templatePathBuilder.length() - 1) != '/') {
templatePathBuilder.append('/');
}
String templatePath = templatePathBuilder
.append(getCheckedTemplateName(methodInfo, annotation, fragmentId != null)).toString();
String fragmentId = getCheckedFragmentId(method, annotation);
String templatePath = getCheckedTemplatePath(index.getIndex(), annotation, fragmentId, targetClass, method);
String fullPath = templatePath + (fragmentId != null ? "$" + fragmentId : "");
AnnotationTarget checkedTemplate = checkedTemplates.putIfAbsent(fullPath, methodInfo);
AnnotationTarget checkedTemplate = checkedTemplates.putIfAbsent(fullPath, method);
if (checkedTemplate != null) {
throw new TemplateException(
String.format(
"Multiple checked templates exist for the template path %s:\n\t- %s: %s\n\t- %s",
fullPath, methodInfo.declaringClass().name(), methodInfo,
fullPath, method.declaringClass().name(), method,
checkedTemplate));
}
if (!filePaths.contains(templatePath)
Expand All @@ -387,7 +375,7 @@ && isNotLocatedByCustomTemplateLocator(locatorPatternsBuildItem.getLocationPatte
if (startsWith.isEmpty()) {
throw new TemplateException(
"No template matching the path " + templatePath + " could be found for: "
+ classInfo.name() + "." + methodInfo.name());
+ targetClass.name() + "." + method.name());
} else {
throw new TemplateException(
startsWith + " match the path " + templatePath
Expand All @@ -396,24 +384,24 @@ && isNotLocatedByCustomTemplateLocator(locatorPatternsBuildItem.getLocationPatte
}

Map<String, String> bindings = new HashMap<>();
List<Type> parameters = methodInfo.parameterTypes();
List<Type> parameters = method.parameterTypes();
List<String> parameterNames = new ArrayList<>(parameters.size());
for (int i = 0; i < parameters.size(); i++) {
Type type = parameters.get(i);
String name = methodInfo.parameterName(i);
String name = method.parameterName(i);
if (name == null) {
throw new TemplateException("Parameter names not recorded for " + classInfo.name()
throw new TemplateException("Parameter names not recorded for " + targetClass.name()
+ ": compile the class with -parameters");
}
bindings.put(name, getCheckedTemplateParameterTypeName(type));
parameterNames.add(name);
}
AnnotationValue requireTypeSafeExpressions = annotation.value(CHECKED_TEMPLATE_REQUIRE_TYPE_SAFE);
ret.add(new CheckedTemplateBuildItem(templatePath, fragmentId, bindings, methodInfo, null,
ret.add(new CheckedTemplateBuildItem(templatePath, fragmentId, bindings, method, null,
requireTypeSafeExpressions != null ? requireTypeSafeExpressions.asBoolean() : true));
enhancer.implement(methodInfo, templatePath, fragmentId, parameterNames, adaptor);
enhancer.implement(method, templatePath, fragmentId, parameterNames, adaptor);
}
transformers.produce(new BytecodeTransformerBuildItem(classInfo.name().toString(),
transformers.produce(new BytecodeTransformerBuildItem(targetClass.name().toString(),
enhancer));
}

Expand Down Expand Up @@ -443,21 +431,8 @@ && isNotLocatedByCustomTemplateLocator(locatorPatternsBuildItem.getLocationPatte

AnnotationInstance checkedTemplateAnnotation = recordClass.declaredAnnotation(Names.CHECKED_TEMPLATE);
String fragmentId = getCheckedFragmentId(recordClass, checkedTemplateAnnotation);
StringBuilder templatePathBuilder = new StringBuilder();
AnnotationValue basePathValue = checkedTemplateAnnotation != null
? checkedTemplateAnnotation.value(CHECKED_TEMPLATE_BASE_PATH)
: null;
if (basePathValue != null && !basePathValue.asString().equals(CheckedTemplate.DEFAULTED)) {
templatePathBuilder.append(basePathValue.asString());
} else if (recordClass.enclosingClass() != null) {
ClassInfo enclosingClass = index.getIndex().getClassByName(recordClass.enclosingClass());
templatePathBuilder.append(enclosingClass.simpleName());
}
if (templatePathBuilder.length() > 0 && templatePathBuilder.charAt(templatePathBuilder.length() - 1) != '/') {
templatePathBuilder.append('/');
}
String templatePath = templatePathBuilder
.append(getCheckedTemplateName(recordClass, checkedTemplateAnnotation, fragmentId != null)).toString();
String templatePath = getCheckedTemplatePath(index.getIndex(), checkedTemplateAnnotation, fragmentId,
recordClass);
String fullPath = templatePath + (fragmentId != null ? "$" + fragmentId : "");
AnnotationTarget checkedTemplate = checkedTemplates.putIfAbsent(fullPath, recordClass);
if (checkedTemplate != null) {
Expand Down Expand Up @@ -518,6 +493,42 @@ && isNotLocatedByCustomTemplateLocator(locatorPatternsBuildItem.getLocationPatte
return ret;
}

private String getCheckedTemplatePath(IndexView index, AnnotationInstance annotation, String fragmentId,
ClassInfo classInfo, MethodInfo method) {
StringBuilder templatePathBuilder = new StringBuilder();
AnnotationValue basePathValue = annotation.value(CHECKED_TEMPLATE_BASE_PATH);
if (basePathValue != null && !basePathValue.asString().equals(CheckedTemplate.DEFAULTED)) {
templatePathBuilder.append(basePathValue.asString());
} else if (classInfo.enclosingClass() != null) {
ClassInfo enclosingClass = index.getClassByName(classInfo.enclosingClass());
templatePathBuilder.append(enclosingClass.simpleName());
}
if (templatePathBuilder.length() > 0 && templatePathBuilder.charAt(templatePathBuilder.length() - 1) != '/') {
templatePathBuilder.append('/');
}
return templatePathBuilder
.append(getCheckedTemplateName(method, annotation, fragmentId != null)).toString();
}

private String getCheckedTemplatePath(IndexView index, AnnotationInstance annotation, String fragmentId,
ClassInfo recordClass) {
StringBuilder templatePathBuilder = new StringBuilder();
AnnotationValue basePathValue = annotation != null
? annotation.value(CHECKED_TEMPLATE_BASE_PATH)
: null;
if (basePathValue != null && !basePathValue.asString().equals(CheckedTemplate.DEFAULTED)) {
templatePathBuilder.append(basePathValue.asString());
} else if (recordClass.enclosingClass() != null) {
ClassInfo enclosingClass = index.getClassByName(recordClass.enclosingClass());
templatePathBuilder.append(enclosingClass.simpleName());
}
if (templatePathBuilder.length() > 0 && templatePathBuilder.charAt(templatePathBuilder.length() - 1) != '/') {
templatePathBuilder.append('/');
}
return templatePathBuilder
.append(getCheckedTemplateName(recordClass, annotation, fragmentId != null)).toString();
}

private String getCheckedTemplateName(AnnotationTarget target, AnnotationInstance checkedTemplateAnnotation,
boolean checkedFragment) {
AnnotationValue nameValue = checkedTemplateAnnotation != null
Expand Down Expand Up @@ -2456,6 +2467,52 @@ public boolean test(TypeCheck check) {
}
}

@BuildStep
void collecTemplateContents(BeanArchiveIndexBuildItem index, List<CheckedTemplateAdapterBuildItem> templateAdaptors,
BuildProducer<TemplatePathBuildItem> templatePaths) {

Set<DotName> recordInterfaces = new HashSet<>();
recordInterfaces.add(Names.TEMPLATE_INSTANCE);
templateAdaptors.stream().map(ta -> DotName.createSimple(ta.adapter.templateInstanceBinaryName().replace('/', '.')))
.forEach(recordInterfaces::add);

for (AnnotationInstance annotation : index.getImmutableIndex().getAnnotations(Names.TEMPLATE_CONTENTS)) {
AnnotationValue suffixValue = annotation.value("suffix");
String suffix = suffixValue != null ? "." + suffixValue.asString() : ".txt";
if (annotation.target().kind() == Kind.CLASS) {
ClassInfo target = annotation.target().asClass();
if (target.isRecord() && target.interfaceNames().stream().anyMatch(recordInterfaces::contains)) {
AnnotationInstance checkedTemplateAnnotation = target.declaredAnnotation(Names.CHECKED_TEMPLATE);
String fragmentId = getCheckedFragmentId(target, checkedTemplateAnnotation);
templatePaths.produce(TemplatePathBuildItem.builder()
.content(annotation.value().asString())
.path(getCheckedTemplatePath(index.getIndex(), checkedTemplateAnnotation, fragmentId, target)
+ suffix)
.extensionInfo(target.toString())
.build());
continue;
}
} else if (annotation.target().kind() == Kind.METHOD) {
MethodInfo method = annotation.target().asMethod();
if (Modifier.isStatic(method.flags())
&& Modifier.isNative(method.flags())
&& method.declaringClass().hasAnnotation(Names.CHECKED_TEMPLATE)) {
AnnotationInstance checkedTemplateAnnotation = method.declaringClass()
.declaredAnnotation(Names.CHECKED_TEMPLATE);
String fragmentId = getCheckedFragmentId(method, checkedTemplateAnnotation);
templatePaths.produce(TemplatePathBuildItem.builder()
.content(annotation.value().asString())
.path(getCheckedTemplatePath(index.getIndex(), checkedTemplateAnnotation, fragmentId,
method.declaringClass(), method) + suffix)
.extensionInfo(method.toString())
.build());
continue;
}
}
throw new TemplateException("Invalid annotation target for @TemplateContents: " + annotation.target());
}
}

@BuildStep
@Record(value = STATIC_INIT)
void initialize(BuildProducer<SyntheticBeanBuildItem> syntheticBeans, QuteRecorder recorder,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package io.quarkus.qute.deployment.contents;

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.util.concurrent.ExecutionException;

import jakarta.inject.Inject;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.qute.CheckedTemplate;
import io.quarkus.qute.Engine;
import io.quarkus.qute.TemplateContents;
import io.quarkus.qute.TemplateInstance;
import io.quarkus.test.QuarkusUnitTest;

public class TemplateContentsCheckedTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.withApplicationRoot(root -> root.addClass(Templates.class));

@Inject
Engine engine;

@Test
public void testTemplateContents() throws InterruptedException, ExecutionException {
assertEquals("Hello 42!", Templates.helloInt(42).render());
assertEquals("Hello 1!", engine.getTemplate("TemplateContentsCheckedTest/helloInt").data("val", 1).render());
}

@CheckedTemplate
static class Templates {

@TemplateContents("Hello {val}!")
static native TemplateInstance helloInt(int val);

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package io.quarkus.qute.deployment.contents;

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.util.concurrent.ExecutionException;

import jakarta.inject.Inject;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.qute.CheckedTemplate;
import io.quarkus.qute.Engine;
import io.quarkus.qute.TemplateContents;
import io.quarkus.qute.TemplateInstance;
import io.quarkus.test.QuarkusUnitTest;

public class TemplateContentsRecordTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.withApplicationRoot(root -> root.addClass(helloInt.class));

@Inject
Engine engine;

@Test
public void testTemplateContents() throws InterruptedException, ExecutionException {
assertEquals("Hello 42!", new helloInt(42).render());
assertEquals("Hello 1!", engine.getTemplate("TemplateContentsRecordTest/helloInt").data("val", 1).render());
assertEquals("Hello 42!", new helloLong(42).render());
assertEquals("Hello 1!", engine.getTemplate("foo/helloLong").data("val", 1).render());

assertEquals("<p>Hello &quot;Martin&quot;!</p>", new helloHtml("\"Martin\"").render());
assertEquals("<p>Hello Lu!</p>",
engine.getTemplate("TemplateContentsRecordTest/helloHtml.html").data("name", "Lu").render());
}

@TemplateContents("Hello {val}!")
record helloInt(int val) implements TemplateInstance {
}

@CheckedTemplate(basePath = "foo")
@TemplateContents("Hello {val}!")
record helloLong(long val) implements TemplateInstance {
}

@TemplateContents(value = "<p>Hello {name}!</p>", suffix = "html")
record helloHtml(String name) implements TemplateInstance {
}

}
Loading
Loading