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 checked templates - require type-safe expressions by default #16466

Merged
merged 1 commit into from
Apr 15, 2021
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
5 changes: 4 additions & 1 deletion docs/src/main/asciidoc/qute-reference.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1148,7 +1148,7 @@ Note that sections can override names that would otherwise match a parameter dec

[[typesafe_templates]]
=== Type-safe Templates
You can also declare your templates in your Java code.
You can also define type-safe templates in your Java code.
If using <<resteasy_integration,templates in JAX-RS resources>>, you can rely on the following convention:

- Organise your template files in the `/src/main/resources/templates` directory, by grouping them into one directory per resource class. So, if
Expand Down Expand Up @@ -1193,6 +1193,9 @@ public class ItemResource {
<1> Declare a method that gives us a `TemplateInstance` for `templates/ItemResource/item.html` and declare its `Item item` parameter so we can validate the template.
<2> The `item` parameter is automatically turned into a <<typesafe_expressions,parameter declaration>> and so all expressions that reference this name will be validated.
<3> Make the `Item` object accessible in the template.

TIP: By default, the templates defined in a class annotated with `@CheckedTemplate` can only contain type-safe expressions, i.e. expressions that can be validated at build time. You can use `@CheckedTemplate(requireTypeSafeExpressions = false)` to relax this requirement.


You can also declare a top-level Java class annotated with `@CheckedTemplate`:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,26 @@
import org.jboss.jandex.MethodInfo;

import io.quarkus.builder.item.MultiBuildItem;
import io.quarkus.qute.CheckedTemplate;

/**
* Represents a method of a class annotated with {@link CheckedTemplate}.
*/
public final class CheckedTemplateBuildItem extends MultiBuildItem {

// A template path, potentially incomplete
public final String templateId;

public final Map<String, String> bindings;
public final MethodInfo method;
public final boolean requireTypeSafeExpressions;

public CheckedTemplateBuildItem(String templateId, Map<String, String> bindings, MethodInfo method) {
public CheckedTemplateBuildItem(String templateId, Map<String, String> bindings, MethodInfo method,
boolean requireTypeSafeExpressions) {
this.templateId = templateId;
this.bindings = bindings;
this.method = method;
this.requireTypeSafeExpressions = requireTypeSafeExpressions;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,9 @@ public class QuteProcessor {

private static final Logger LOGGER = Logger.getLogger(QuteProcessor.class);

private static final String CHECKED_TEMPLATE_REQUIRE_TYPE_SAFE = "requireTypeSafeExpressions";
private static final String CHECKED_TEMPLATE_BASE_PATH = "basePath";

private static final Function<FieldInfo, String> GETTER_FUN = new Function<FieldInfo, String>() {
@Override
public String apply(FieldInfo field) {
Expand Down Expand Up @@ -216,7 +219,7 @@ List<CheckedTemplateBuildItem> collectCheckedTemplates(BeanArchiveIndexBuildItem
BuildProducer<BytecodeTransformerBuildItem> transformers,
List<TemplatePathBuildItem> templatePaths,
List<CheckedTemplateAdapterBuildItem> templateAdaptorBuildItems,
QuteConfig config) {
TemplateFilePathsBuildItem filePaths) {
List<CheckedTemplateBuildItem> ret = new ArrayList<>();

Map<DotName, CheckedTemplateAdapter> adaptors = new HashMap<>();
Expand Down Expand Up @@ -246,20 +249,6 @@ List<CheckedTemplateBuildItem> collectCheckedTemplates(BeanArchiveIndexBuildItem
Set<AnnotationInstance> checkedTemplateAnnotations = new HashSet<>();
checkedTemplateAnnotations.addAll(index.getIndex().getAnnotations(Names.CHECKED_TEMPLATE));

// Build a set of file paths for validation
Set<String> filePaths = new HashSet<String>();
for (TemplatePathBuildItem templatePath : templatePaths) {
String path = templatePath.getPath();
filePaths.add(path);
// Also add version without suffix from the path
// For example for "items.html" also add "items"
for (String suffix : config.suffixes) {
if (path.endsWith(suffix)) {
filePaths.add(path.substring(0, path.length() - (suffix.length() + 1)));
}
}
}

for (AnnotationInstance annotation : checkedTemplateAnnotations) {
if (annotation.target().kind() != Kind.CLASS) {
continue;
Expand Down Expand Up @@ -288,7 +277,7 @@ List<CheckedTemplateBuildItem> collectCheckedTemplates(BeanArchiveIndexBuildItem
}

StringBuilder templatePathBuilder = new StringBuilder();
AnnotationValue basePathValue = annotation.value("basePath");
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) {
Expand All @@ -309,7 +298,7 @@ List<CheckedTemplateBuildItem> collectCheckedTemplates(BeanArchiveIndexBuildItem
}
if (!filePaths.contains(templatePath)) {
List<String> startsWith = new ArrayList<>();
for (String filePath : filePaths) {
for (String filePath : filePaths.getFilePaths()) {
if (filePath.startsWith(templatePath)) {
startsWith.add(filePath);
}
Expand Down Expand Up @@ -338,7 +327,9 @@ List<CheckedTemplateBuildItem> collectCheckedTemplates(BeanArchiveIndexBuildItem
bindings.put(name, JandexUtil.getBoxedTypeName(type));
parameterNames.add(name);
}
ret.add(new CheckedTemplateBuildItem(templatePath, bindings, methodInfo));
AnnotationValue requireTypeSafeExpressions = annotation.value(CHECKED_TEMPLATE_REQUIRE_TYPE_SAFE);
ret.add(new CheckedTemplateBuildItem(templatePath, bindings, methodInfo,
requireTypeSafeExpressions != null ? requireTypeSafeExpressions.asBoolean() : true));
enhancer.implement(methodInfo, templatePath, parameterNames, adaptor);
}
transformers.produce(new BytecodeTransformerBuildItem(classInfo.name().toString(),
Expand All @@ -350,7 +341,8 @@ List<CheckedTemplateBuildItem> collectCheckedTemplates(BeanArchiveIndexBuildItem

@BuildStep
TemplatesAnalysisBuildItem analyzeTemplates(List<TemplatePathBuildItem> templatePaths,
List<CheckedTemplateBuildItem> checkedTemplates, List<MessageBundleMethodBuildItem> messageBundleMethods) {
TemplateFilePathsBuildItem filePaths, List<CheckedTemplateBuildItem> checkedTemplates,
List<MessageBundleMethodBuildItem> messageBundleMethods, QuteConfig config) {
long start = System.currentTimeMillis();
List<TemplateAnalysis> analysis = new ArrayList<>();

Expand Down Expand Up @@ -409,28 +401,54 @@ public Optional<Variant> getVariant() {
}
return Optional.empty();
}
}).addParserHook(new ParserHook() {
});

Map<String, MessageBundleMethodBuildItem> messageBundleMethodsMap;
if (messageBundleMethods.isEmpty()) {
messageBundleMethodsMap = Collections.emptyMap();
} else {
messageBundleMethodsMap = new HashMap<>();
for (MessageBundleMethodBuildItem messageBundleMethod : messageBundleMethods) {
messageBundleMethodsMap.put(messageBundleMethod.getTemplateId(), messageBundleMethod);
}
}

builder.addParserHook(new ParserHook() {

@Override
public void beforeParsing(ParserHelper parserHelper) {
for (CheckedTemplateBuildItem checkedTemplate : checkedTemplates) {
// FIXME: check for dot/extension?
if (parserHelper.getTemplateId().startsWith(checkedTemplate.templateId)) {
for (Entry<String, String> entry : checkedTemplate.bindings.entrySet()) {
parserHelper.addParameter(entry.getKey(), entry.getValue());
// The template id may be the full path, e.g. "items.html" or a path without the suffic, e.g. "items"
String templateId = parserHelper.getTemplateId();

if (filePaths.contains(templateId)) {
// It's a file-based template
// We need to find out whether the parsed template represents a checked template
String path = templateId;
for (String suffix : config.suffixes) {
if (path.endsWith(suffix)) {
// Remove the suffix
path = path.substring(0, path.length() - (suffix.length() + 1));
break;
}
}
}
// Add params to message bundle templates
for (MessageBundleMethodBuildItem messageBundleMethod : messageBundleMethods) {
if (parserHelper.getTemplateId().equals(messageBundleMethod.getTemplateId())) {
MethodInfo method = messageBundleMethod.getMethod();
for (ListIterator<Type> it = method.parameters().listIterator(); it.hasNext();) {
Type paramType = it.next();
parserHelper.addParameter(method.parameterName(it.previousIndex()),
JandexUtil.getBoxedTypeName(paramType));
for (CheckedTemplateBuildItem checkedTemplate : checkedTemplates) {
if (checkedTemplate.templateId.equals(path)) {
for (Entry<String, String> entry : checkedTemplate.bindings.entrySet()) {
parserHelper.addParameter(entry.getKey(), entry.getValue());
}
break;
}
break;
}
}

// If needed add params to message bundle templates
MessageBundleMethodBuildItem messageBundleMethod = messageBundleMethodsMap.get(templateId);
if (messageBundleMethod != null) {
MethodInfo method = messageBundleMethod.getMethod();
for (ListIterator<Type> it = method.parameters().listIterator(); it.hasNext();) {
Type paramType = it.next();
parserHelper.addParameter(method.parameterName(it.previousIndex()),
JandexUtil.getBoxedTypeName(paramType));
}
}
}
Expand Down Expand Up @@ -460,12 +478,15 @@ public void beforeParsing(ParserHelper parserHelper) {
}

@BuildStep
void validateExpressions(TemplatesAnalysisBuildItem templatesAnalysis, BeanArchiveIndexBuildItem beanArchiveIndex,
void validateExpressions(TemplatesAnalysisBuildItem templatesAnalysis,
BeanArchiveIndexBuildItem beanArchiveIndex,
List<TemplateExtensionMethodBuildItem> templateExtensionMethods,
List<TypeCheckExcludeBuildItem> excludes,
BuildProducer<IncorrectExpressionBuildItem> incorrectExpressions,
BuildProducer<ImplicitValueResolverBuildItem> implicitClasses,
BeanDiscoveryFinishedBuildItem beanDiscovery) {
BeanDiscoveryFinishedBuildItem beanDiscovery,
List<CheckedTemplateBuildItem> checkedTemplates,
QuteConfig config) {

IndexView index = beanArchiveIndex.getIndex();
Function<String, String> templateIdToPathFun = new Function<String, String>() {
Expand All @@ -485,6 +506,23 @@ public String apply(String id) {
Map<DotName, Set<String>> implicitClassToMembersUsed = new HashMap<>();

for (TemplateAnalysis templateAnalysis : templatesAnalysis.getAnalysis()) {

// Try to find the checked template
String path = templateAnalysis.path;
for (String suffix : config.suffixes) {
if (path.endsWith(suffix)) {
path = path.substring(0, path.length() - (suffix.length() + 1));
break;
}
}
CheckedTemplateBuildItem checkedTemplate = null;
for (CheckedTemplateBuildItem item : checkedTemplates) {
if (item.templateId.equals(path)) {
checkedTemplate = item;
break;
}
}

// Maps an expression generated id to the last match of an expression (i.e. the type of the last part)
Map<Integer, Match> generatedIdsToMatches = new HashMap<>();

Expand All @@ -502,6 +540,18 @@ public String apply(String id) {
continue;
}
} else {
if (checkedTemplate != null && checkedTemplate.requireTypeSafeExpressions && !expression.hasTypeInfo()) {
incorrectExpressions.produce(new IncorrectExpressionBuildItem(expression.toOriginalString(),
"Only type-safe expressions are allowed in the checked template defined via: "
+ checkedTemplate.method.declaringClass().name() + "."
+ checkedTemplate.method.name()
+ "(); an expression must be based on a checked template parameter "
+ checkedTemplate.bindings.keySet()
+ ", or bound via a param declaration, or the requirement must be relaxed via @CheckedTemplate(requireTypeSafeExpressions = false)",
expression.getOrigin()));
continue;
}

generatedIdsToMatches.put(expression.getGeneratedId(),
validateNestedExpressions(templateAnalysis, null, new HashMap<>(), templateExtensionMethods,
excludes,
Expand Down Expand Up @@ -1032,10 +1082,7 @@ void collectTemplates(ApplicationArchivesBuildItem applicationArchivesBuildItem,
}

@BuildStep
void validateTemplateInjectionPoints(QuteConfig config, List<TemplatePathBuildItem> templatePaths,
ValidationPhaseBuildItem validationPhase,
BuildProducer<ValidationErrorBuildItem> validationErrors) {

TemplateFilePathsBuildItem collectTemplateFilePaths(QuteConfig config, List<TemplatePathBuildItem> templatePaths) {
Set<String> filePaths = new HashSet<String>();
for (TemplatePathBuildItem templatePath : templatePaths) {
String path = templatePath.getPath();
Expand All @@ -1048,6 +1095,13 @@ void validateTemplateInjectionPoints(QuteConfig config, List<TemplatePathBuildIt
}
}
}
return new TemplateFilePathsBuildItem(filePaths);
}

@BuildStep
void validateTemplateInjectionPoints(TemplateFilePathsBuildItem filePaths, List<TemplatePathBuildItem> templatePaths,
ValidationPhaseBuildItem validationPhase,
BuildProducer<ValidationErrorBuildItem> validationErrors) {

for (InjectionPointInfo injectionPoint : validationPhase.getContext().getInjectionPoints()) {
if (injectionPoint.getRequiredType().name().equals(Names.TEMPLATE)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.quarkus.qute.deployment;

import java.util.Set;

import io.quarkus.builder.item.SimpleBuildItem;
import io.quarkus.qute.runtime.QuteConfig;

/**
* Holds all template file paths, including the versions without suffixes configured via {@link QuteConfig#suffixes}.
* <p>
* For example, for the template {@code items.html} the set will contain {@code items.html} and {@code items}.
*/
public final class TemplateFilePathsBuildItem extends SimpleBuildItem {

private final Set<String> filePaths;

public TemplateFilePathsBuildItem(Set<String> filePaths) {
this.filePaths = filePaths;
}

public Set<String> getFilePaths() {
return filePaths;
}

public boolean contains(String path) {
return filePaths.contains(path);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,20 @@ public List<TemplateAnalysis> getAnalysis() {
return analysis;
}

/**
* Analysis of a particular template found in the given path.
*/
static class TemplateAnalysis {

// Path or other user-defined id; may be null
// A user-defined id; may be null
public final String id;

// The id generated by the parser
public final String generatedId;

public final List<Expression> expressions;

// File path, e.g. hello.html or ItemResource/items.html
public final String path;

public TemplateAnalysis(String id, String generatedId, List<Expression> expressions, String path) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package io.quarkus.qute.deployment.typesafe;

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

import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

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

public class CheckedTemplateDoNotRequireTypeSafeTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
.addClasses(Templates.class)
.addAsResource(new StringAsset("Hello {name}!{any}"),
"templates/CheckedTemplateDoNotRequireTypeSafeTest/hola.txt"));

@Test
public void testValidation() {
assertEquals("Hello Ondrej!!", Templates.hola("Ondrej").data("any", "!").render());
}

@CheckedTemplate(requireTypeSafeExpressions = false)
static class Templates {

static native TemplateInstance hola(String name);

}

}
Loading