tessl install tessl/maven-io-quarkus--quarkus-qute@3.30.0Offer templating support for web, email, etc in a build time, type-safe way
The Qute Engine is the central orchestration point for all template operations in Qute. It manages template parsing, caching, value resolution, section helpers, result mapping, and provides extensive customization capabilities through a fluent builder API. This document covers advanced engine configuration, custom component creation, template lifecycle management, and debugging with the tracing API.
The Engine is thread-safe and designed to be a singleton in your application. All advanced features like custom resolvers, section helpers, parser hooks, and locators are registered through the EngineBuilder during engine construction.
The Engine interface provides complete control over template management, parsing, caching, and resolution.
package io.quarkus.qute;
/**
* Central point for template management with dedicated configuration
* and template caching.
*/
public interface Engine extends ErrorInitializer {
// Factory method
static EngineBuilder builder();
// Template parsing
Template parse(String content);
Template parse(String content, Variant variant);
Template parse(String content, Variant variant, String id);
// Template cache management
Template putTemplate(String id, Template template);
Template getTemplate(String id);
boolean isTemplateLoaded(String id);
void clearTemplates();
void removeTemplates(Predicate<String> test);
// Result mapping
List<ResultMapper> getResultMappers();
String mapResult(Object result, Expression expression);
// Section helper access
SectionHelperFactory<?> getSectionHelperFactory(String name);
Map<String, SectionHelperFactory<?>> getSectionHelperFactories();
// Resolver access
List<ValueResolver> getValueResolvers();
List<NamespaceResolver> getNamespaceResolvers();
Evaluator getEvaluator();
// Template locators
Optional<TemplateLocation> locate(String id);
// Instance initialization
List<TemplateInstance.Initializer> getTemplateInstanceInitializers();
// Configuration access
long getTimeout();
boolean useAsyncTimeout();
boolean removeStandaloneLines();
// Tracing API
TraceManager getTraceManager();
void addTraceListener(TraceListener listener);
void removeTraceListener(TraceListener listener);
// Builder creation
EngineBuilder newBuilder();
}The Engine provides three parse methods for creating Template instances:
// Basic parsing
Engine engine = Engine.builder().addDefaults().build();
Template template = engine.parse("<h1>{title}</h1>");
// Parse with content type variant
Variant htmlVariant = Variant.forContentType("text/html");
Template htmlTemplate = engine.parse("<h1>{title}</h1>", htmlVariant);
// Parse with variant and template ID
Template idTemplate = engine.parse(
"<h1>{title}</h1>",
htmlVariant,
"my-custom-template-id"
);The Engine maintains an internal cache of parsed templates. You can manage this cache directly:
// Register a template manually
Template myTemplate = engine.parse("<div>{content}</div>");
engine.putTemplate("custom-template", myTemplate);
// Retrieve cached template
Template cached = engine.getTemplate("custom-template");
// Check if template is loaded (doesn't trigger locators)
boolean loaded = engine.isTemplateLoaded("custom-template");
// Clear all templates
engine.clearTemplates();
// Remove specific templates by predicate
engine.removeTemplates(id -> id.startsWith("temp-"));Control how expression results are converted to strings:
List<ResultMapper> mappers = engine.getResultMappers();
// Manually map a result
Object result = evaluateSomething();
Expression expr = template.getExpressions().get(0);
String stringValue = engine.mapResult(result, expr);The EngineBuilder provides a comprehensive fluent API for configuring all aspects of the Engine. This is the primary way to customize Qute behavior.
package io.quarkus.qute;
/**
* Builder for Engine with fluent configuration API.
* Not thread-safe - should not be reused.
*/
public final class EngineBuilder {
// Section helpers
EngineBuilder addSectionHelper(SectionHelperFactory<?> factory);
EngineBuilder addSectionHelpers(SectionHelperFactory<?>... factories);
EngineBuilder addSectionHelper(String name, SectionHelperFactory<?> factory);
EngineBuilder addDefaultSectionHelpers();
EngineBuilder computeSectionHelper(Function<String, SectionHelperFactory<?>> func);
// Value resolvers
EngineBuilder addValueResolver(ValueResolver resolver);
EngineBuilder addValueResolvers(ValueResolver... resolvers);
EngineBuilder addValueResolver(Supplier<ValueResolver> resolverSupplier);
EngineBuilder addDefaultValueResolvers();
// Namespace resolvers
EngineBuilder addNamespaceResolver(NamespaceResolver resolver);
// Result mappers
EngineBuilder addResultMapper(ResultMapper mapper);
// Template locators
EngineBuilder addLocator(TemplateLocator locator);
// Parser hooks
EngineBuilder addParserHook(ParserHook parserHook);
// Instance initializers
EngineBuilder addTemplateInstanceInitializer(TemplateInstance.Initializer initializer);
// Engine listeners
EngineBuilder addEngineListener(EngineListener listener);
// Default components
EngineBuilder addDefaults();
// Parser configuration
EngineBuilder removeStandaloneLines(boolean value);
EngineBuilder strictRendering(boolean value);
EngineBuilder iterationMetadataPrefix(String prefix);
// Timeout configuration
EngineBuilder timeout(long value);
EngineBuilder useAsyncTimeout(boolean value);
// Tracing
EngineBuilder enableTracing(boolean value);
// Build
Engine build();
/**
* Receives notifications about Engine lifecycle.
*/
interface EngineListener {
default void engineBuilt(Engine engine);
}
}// Minimal configuration with defaults
Engine engine = Engine.builder()
.addDefaults() // Adds default section helpers and value resolvers
.build();
// Custom configuration
Engine customEngine = Engine.builder()
.addDefaultSectionHelpers() // if, loop, with, include, etc.
.addDefaultValueResolvers() // map, collection, list, logical operators, etc.
.timeout(30_000) // 30 second timeout
.strictRendering(true) // Fail on unresolved expressions
.removeStandaloneLines(true) // Clean up template whitespace
.build();Engine advancedEngine = Engine.builder()
// Add section helpers
.addSectionHelper(new CustomSectionHelper.Factory())
.addSectionHelper("myCustomTag", new MyTagFactory())
// Add value resolvers
.addValueResolver(new MyCustomResolver())
.addValueResolvers(resolver1, resolver2, resolver3)
// Add namespace resolvers
.addNamespaceResolver(NamespaceResolver.builder("mydata")
.resolve(ctx -> loadMyData(ctx.getName()))
.build())
// Add result mappers
.addResultMapper(new JsonResultMapper())
// Add template locators
.addLocator(new DatabaseTemplateLocator())
.addLocator(new S3TemplateLocator())
// Add parser hooks
.addParserHook(new CustomParserHook())
// Add instance initializers
.addTemplateInstanceInitializer(instance -> {
instance.setAttribute("defaultLocale", Locale.US);
})
// Configure iteration metadata
.iterationMetadataPrefix("item_") // Use {item_index} instead of {_index}
// Configure timeouts
.timeout(5000) // 5 second global timeout
.useAsyncTimeout(true) // Apply timeout to async rendering
// Enable tracing for debugging
.enableTracing(true)
// Add engine listener
.addEngineListener(engine -> {
System.out.println("Engine built with " +
engine.getValueResolvers().size() + " resolvers");
})
.build();Use computeSectionHelper to dynamically provide section helpers for unknown tags:
Engine engine = Engine.builder()
.addDefaults()
.computeSectionHelper(name -> {
if (name.startsWith("custom-")) {
return new DynamicSectionFactory(name);
}
return null; // No factory for this name
})
.build();Section helpers define custom logic for template sections (tags). They are the primary extension point for custom template directives.
package io.quarkus.qute;
/**
* Defines the logic of a section node.
*/
@FunctionalInterface
public interface SectionHelper {
/**
* Resolve the section and return result.
*/
CompletionStage<ResultNode> resolve(SectionResolutionContext context);
/**
* Context for section resolution.
*/
interface SectionResolutionContext {
// Evaluate expressions
CompletionStage<Map<String, Object>> evaluate(Map<String, Expression> expressions);
CompletionStage<Object> evaluate(Expression expression);
// Resolution context
ResolutionContext resolutionContext();
ResolutionContext newResolutionContext(Object data,
Map<String, SectionBlock> extendingBlocks);
// Execute blocks
CompletionStage<ResultNode> execute();
CompletionStage<ResultNode> execute(ResolutionContext context);
CompletionStage<ResultNode> execute(SectionBlock block,
ResolutionContext context);
// Parameters
Map<String, Object> getParameters();
}
}package io.quarkus.qute;
/**
* Factory to create SectionHelper instances.
*/
public interface SectionHelperFactory<T extends SectionHelper> {
String HINT_METADATA = "<metadata>";
String MAIN_BLOCK_NAME = "$main";
// Configuration
List<String> getDefaultAliases();
ParametersInfo getParameters();
List<String> getBlockLabels();
boolean cacheFactoryConfig();
boolean treatUnknownSectionsAsBlocks();
MissingEndTagStrategy missingEndTagStrategy();
// Initialization
T initialize(SectionInitContext context);
Scope initializeBlock(Scope outerScope, BlockInfo block);
/**
* Missing end tag strategy.
*/
enum MissingEndTagStrategy {
ERROR, // End tag is mandatory
BIND_TO_PARENT // End tag is optional
}
/**
* Section initialization context.
*/
interface SectionInitContext extends ParserDelegate {
String getName();
Map<String, String> getParameters();
boolean hasParameter(String name);
String getParameter(String name);
String getParameter(int position);
String getParameterOrDefault(String name, String defaultValue);
Expression getExpression(String parameterName);
Expression parseValue(String value);
List<SectionBlock> getBlocks();
SectionBlock getBlock(String label);
Engine getEngine();
Origin getOrigin();
Supplier<Template> getCurrentTemplate();
}
/**
* Block information during initialization.
*/
interface BlockInfo extends ParserDelegate, WithOrigin {
boolean isMainBlock();
String getLabel();
Map<String, String> getParameters();
String getParameter(String name);
boolean hasParameter(String name);
String getParameter(int position);
Expression addExpression(String param, String value);
}
/**
* Parameter metadata for factory.
*/
final class ParametersInfo implements Iterable<List<Parameter>> {
static Builder builder();
List<Parameter> get(String blockLabel);
boolean isCheckNumberOfParams();
class Builder {
Builder addParameter(String name);
Builder addParameter(String name, String defaultValue);
Builder addParameter(Parameter.Builder param);
Builder addParameter(Parameter param);
Builder addParameter(String blockLabel, String name, String defaultValue);
Builder addParameter(String blockLabel, Parameter.Builder parameter);
Builder addParameter(String blockLabel, Parameter parameter);
Builder checkNumberOfParams(boolean value);
ParametersInfo build();
}
}
}Here's a complete example of a custom section helper that repeats content N times:
import io.quarkus.qute.*;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
/**
* Custom section: {#repeat times=3}Hello{/repeat}
*/
public class RepeatSectionHelper implements SectionHelper {
private final Expression timesExpr;
public RepeatSectionHelper(Expression timesExpr) {
this.timesExpr = timesExpr;
}
@Override
public CompletionStage<ResultNode> resolve(SectionResolutionContext context) {
return context.evaluate(timesExpr).thenCompose(timesValue -> {
int times = ((Number) timesValue).intValue();
// Execute the main block multiple times
CompletionStage<ResultNode> result = CompletableFuture.completedFuture(
ResultNode.NOOP);
for (int i = 0; i < times; i++) {
result = result.thenCompose(prevResult ->
context.execute().thenApply(currentResult ->
ResultNode.multi(List.of(prevResult, currentResult))));
}
return result;
});
}
public static class Factory implements SectionHelperFactory<RepeatSectionHelper> {
@Override
public List<String> getDefaultAliases() {
return List.of("repeat");
}
@Override
public ParametersInfo getParameters() {
return ParametersInfo.builder()
.addParameter("times") // Required parameter
.build();
}
@Override
public Scope initializeBlock(Scope outerScope, BlockInfo block) {
if (block.isMainBlock()) {
// Register the 'times' expression
block.addExpression("times", block.getParameter("times"));
}
return outerScope; // Don't create new scope
}
@Override
public RepeatSectionHelper initialize(SectionInitContext context) {
// Get the registered expression
Expression timesExpr = context.getExpression("times");
return new RepeatSectionHelper(timesExpr);
}
}
}
// Register the section helper
Engine engine = Engine.builder()
.addDefaults()
.addSectionHelper(new RepeatSectionHelper.Factory())
.build();
// Use in template
Template template = engine.parse("{#repeat times=3}Hi! {/repeat}");
System.out.println(template.render()); // Output: Hi! Hi! Hi!/**
* Custom section with blocks: {#choose}{#when value=1}One{#else}Other{/choose}
*/
public class ChooseSectionHelper implements SectionHelper {
private final Expression valueExpr;
private final SectionBlock whenBlock;
private final SectionBlock elseBlock;
public ChooseSectionHelper(Expression valueExpr, SectionBlock whenBlock,
SectionBlock elseBlock) {
this.valueExpr = valueExpr;
this.whenBlock = whenBlock;
this.elseBlock = elseBlock;
}
@Override
public CompletionStage<ResultNode> resolve(SectionResolutionContext context) {
return context.evaluate(valueExpr).thenCompose(actualValue -> {
// Evaluate when block's expected value
Expression whenValueExpr = whenBlock.expressions.get("value");
return context.evaluate(whenValueExpr).thenCompose(expectedValue -> {
if (actualValue.equals(expectedValue)) {
return context.execute(whenBlock, context.resolutionContext());
} else {
return context.execute(elseBlock, context.resolutionContext());
}
});
});
}
public static class Factory implements SectionHelperFactory<ChooseSectionHelper> {
@Override
public List<String> getDefaultAliases() {
return List.of("choose");
}
@Override
public List<String> getBlockLabels() {
return List.of("when", "else");
}
@Override
public ParametersInfo getParameters() {
return ParametersInfo.builder()
.addParameter("value") // Main block parameter
.addParameter("when", "value", null) // When block parameter
.build();
}
@Override
public Scope initializeBlock(Scope outerScope, BlockInfo block) {
if (block.isMainBlock()) {
block.addExpression("value", block.getParameter("value"));
} else if ("when".equals(block.getLabel())) {
block.addExpression("value", block.getParameter("value"));
}
return outerScope;
}
@Override
public ChooseSectionHelper initialize(SectionInitContext context) {
Expression valueExpr = context.getExpression("value");
SectionBlock whenBlock = context.getBlock("when");
SectionBlock elseBlock = context.getBlock("else");
return new ChooseSectionHelper(valueExpr, whenBlock, elseBlock);
}
}
}Parser hooks allow you to hook into the template parsing process before the template content is parsed. This is useful for adding implicit parameters or filtering content.
package io.quarkus.qute;
/**
* Hook into parser logic before template parsing.
*/
public interface ParserHook {
/**
* Invoked before template contents is parsed.
*/
void beforeParsing(ParserHelper parserHelper);
}package io.quarkus.qute;
/**
* Helper for parser hooks.
*/
public interface ParserHelper {
/**
* Get the template ID being parsed.
*/
String getTemplateId();
/**
* Add implicit parameter declaration.
* Alternative to explicit declarations like {@org.acme.Foo foo}.
*/
void addParameter(String name, String type);
/**
* Add content filter applied before parsing.
*/
void addContentFilter(Function<String, String> filter);
}import io.quarkus.qute.*;
import java.util.function.Function;
/**
* Add implicit parameters for all templates.
*/
public class ImplicitParametersHook implements ParserHook {
@Override
public void beforeParsing(ParserHelper parserHelper) {
// Add implicit 'user' parameter to all templates
parserHelper.addParameter("user", "org.acme.User");
// Add implicit 'config' parameter
parserHelper.addParameter("config", "org.acme.Config");
System.out.println("Added implicit parameters to: " +
parserHelper.getTemplateId());
}
}
/**
* Filter template content before parsing.
*/
public class ContentFilterHook implements ParserHook {
@Override
public void beforeParsing(ParserHelper parserHelper) {
// Replace custom syntax with Qute syntax
parserHelper.addContentFilter(content ->
content.replace("[[", "{")
.replace("]]", "}")
);
// Strip comments before parsing
parserHelper.addContentFilter(content ->
content.replaceAll("<!--.*?-->", "")
);
}
}
/**
* Add parameters based on template path.
*/
public class ConditionalParameterHook implements ParserHook {
@Override
public void beforeParsing(ParserHelper parserHelper) {
String templateId = parserHelper.getTemplateId();
if (templateId.startsWith("admin/")) {
parserHelper.addParameter("adminUser", "org.acme.AdminUser");
parserHelper.addParameter("permissions", "java.util.List<String>");
} else if (templateId.startsWith("public/")) {
parserHelper.addParameter("visitor", "org.acme.Visitor");
}
}
}
// Register parser hooks
Engine engine = Engine.builder()
.addDefaults()
.addParserHook(new ImplicitParametersHook())
.addParserHook(new ContentFilterHook())
.addParserHook(new ConditionalParameterHook())
.build();
// Now all templates have implicit 'user' and 'config' parameters
Template template = engine.parse("{user.name} - {config.appName}");Result mappers control how expression evaluation results are converted to strings for output. They are applied in priority order, and the first applicable mapper is used.
package io.quarkus.qute;
/**
* Maps expression results to string values.
*/
@FunctionalInterface
public interface ResultMapper extends WithPriority {
/**
* Check if mapper applies to the result.
*/
default boolean appliesTo(Origin origin, Object result);
/**
* Map result to string value.
*
* @param result The result, never null
* @param expression The original expression
* @return the string value
*/
String map(Object result, Expression expression);
/**
* Get priority (higher = higher precedence).
* Default is 1.
*/
default int getPriority();
}import io.quarkus.qute.*;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAccessor;
/**
* Map objects to JSON strings.
*/
public class JsonResultMapper implements ResultMapper {
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public boolean appliesTo(Origin origin, Object result) {
// Apply to complex objects but not primitives or strings
return result != null
&& !result.getClass().isPrimitive()
&& !(result instanceof String)
&& !(result instanceof Number)
&& !(result instanceof Boolean);
}
@Override
public String map(Object result, Expression expression) {
try {
return objectMapper.writeValueAsString(result);
} catch (Exception e) {
return result.toString();
}
}
@Override
public int getPriority() {
return 5; // Higher priority than default
}
}
/**
* Format date/time objects with custom format.
*/
public class DateTimeResultMapper implements ResultMapper {
private final DateTimeFormatter formatter =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Override
public boolean appliesTo(Origin origin, Object result) {
return result instanceof TemporalAccessor;
}
@Override
public String map(Object result, Expression expression) {
return formatter.format((TemporalAccessor) result);
}
@Override
public int getPriority() {
return 10; // Very high priority
}
}
/**
* Map null values to custom string.
*/
public class NullResultMapper implements ResultMapper {
@Override
public boolean appliesTo(Origin origin, Object result) {
return result == null;
}
@Override
public String map(Object result, Expression expression) {
return "N/A";
}
@Override
public int getPriority() {
return 100; // Highest priority
}
}
/**
* Add metadata wrapper for specific templates.
*/
public class MetadataResultMapper implements ResultMapper {
@Override
public boolean appliesTo(Origin origin, Object result) {
// Only apply in debug templates
return origin != null
&& origin.getTemplateId() != null
&& origin.getTemplateId().contains("debug");
}
@Override
public String map(Object result, Expression expression) {
return String.format("[%s: %s]",
result.getClass().getSimpleName(),
result.toString());
}
}
// Register result mappers
Engine engine = Engine.builder()
.addDefaults()
.addResultMapper(new JsonResultMapper())
.addResultMapper(new DateTimeResultMapper())
.addResultMapper(new NullResultMapper())
.addResultMapper(new MetadataResultMapper())
.build();Template locators enable loading templates from custom sources like databases, cloud storage, or external APIs.
package io.quarkus.qute;
/**
* Locates template sources.
* Higher priority locators take precedence.
*/
public interface TemplateLocator extends WithPriority {
/**
* Locate template by ID.
* Must return empty Optional if not found.
*/
Optional<TemplateLocation> locate(String id);
/**
* Get priority (higher = higher precedence).
*/
default int getPriority();
/**
* Template location with content reader.
*/
interface TemplateLocation {
Reader read();
Optional<Variant> getVariant();
Optional<URI> getSource();
}
}import io.quarkus.qute.*;
import java.io.*;
import java.net.URI;
import java.nio.file.*;
import java.sql.*;
import java.util.Optional;
/**
* Load templates from database.
*/
public class DatabaseTemplateLocator implements TemplateLocator {
private final DataSource dataSource;
public DatabaseTemplateLocator(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public Optional<TemplateLocation> locate(String id) {
try (Connection conn = dataSource.getConnection()) {
String sql = "SELECT content, content_type FROM templates WHERE id = ?";
try (PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setString(1, id);
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
String content = rs.getString("content");
String contentType = rs.getString("content_type");
return Optional.of(new DbTemplateLocation(content, contentType, id));
}
}
}
} catch (SQLException e) {
throw new RuntimeException("Failed to load template: " + id, e);
}
return Optional.empty();
}
@Override
public int getPriority() {
return 10; // Higher than file system locators
}
private static class DbTemplateLocation implements TemplateLocation {
private final String content;
private final String contentType;
private final String id;
public DbTemplateLocation(String content, String contentType, String id) {
this.content = content;
this.contentType = contentType;
this.id = id;
}
@Override
public Reader read() {
return new StringReader(content);
}
@Override
public Optional<Variant> getVariant() {
return Optional.of(Variant.forContentType(contentType));
}
@Override
public Optional<URI> getSource() {
return Optional.of(URI.create("db://templates/" + id));
}
}
}
/**
* Load templates from S3 bucket.
*/
public class S3TemplateLocator implements TemplateLocator {
private final S3Client s3Client;
private final String bucketName;
public S3TemplateLocator(S3Client s3Client, String bucketName) {
this.s3Client = s3Client;
this.bucketName = bucketName;
}
@Override
public Optional<TemplateLocation> locate(String id) {
String key = "templates/" + id;
try {
GetObjectRequest request = GetObjectRequest.builder()
.bucket(bucketName)
.key(key)
.build();
ResponseInputStream<?> response = s3Client.getObject(request);
String content = new String(response.readAllBytes());
String contentType = response.response().contentType();
return Optional.of(new S3TemplateLocation(content, contentType, key));
} catch (NoSuchKeyException e) {
return Optional.empty();
} catch (Exception e) {
throw new RuntimeException("Failed to load template from S3: " + id, e);
}
}
@Override
public int getPriority() {
return 5;
}
private static class S3TemplateLocation implements TemplateLocation {
private final String content;
private final String contentType;
private final String key;
public S3TemplateLocation(String content, String contentType, String key) {
this.content = content;
this.contentType = contentType;
this.key = key;
}
@Override
public Reader read() {
return new StringReader(content);
}
@Override
public Optional<Variant> getVariant() {
if (contentType != null) {
return Optional.of(Variant.forContentType(contentType));
}
return Optional.empty();
}
@Override
public Optional<URI> getSource() {
return Optional.of(URI.create("s3://" + key));
}
}
}
/**
* Fallback locator for classpath resources.
*/
public class ClasspathTemplateLocator implements TemplateLocator {
private final String basePath;
public ClasspathTemplateLocator(String basePath) {
this.basePath = basePath;
}
@Override
public Optional<TemplateLocation> locate(String id) {
String resourcePath = basePath + "/" + id;
InputStream stream = getClass().getClassLoader()
.getResourceAsStream(resourcePath);
if (stream == null) {
return Optional.empty();
}
return Optional.of(new ClasspathTemplateLocation(stream, resourcePath));
}
@Override
public int getPriority() {
return 1; // Lowest priority - fallback
}
private static class ClasspathTemplateLocation implements TemplateLocation {
private final InputStream stream;
private final String path;
public ClasspathTemplateLocation(InputStream stream, String path) {
this.stream = stream;
this.path = path;
}
@Override
public Reader read() {
return new InputStreamReader(stream);
}
@Override
public Optional<Variant> getVariant() {
if (path.endsWith(".html")) {
return Optional.of(Variant.forContentType("text/html"));
} else if (path.endsWith(".txt")) {
return Optional.of(Variant.forContentType("text/plain"));
}
return Optional.empty();
}
@Override
public Optional<URI> getSource() {
return Optional.of(URI.create("classpath:" + path));
}
}
}
// Register multiple locators with priority order
Engine engine = Engine.builder()
.addDefaults()
.addLocator(new DatabaseTemplateLocator(dataSource)) // Priority 10
.addLocator(new S3TemplateLocator(s3Client, "my-bucket")) // Priority 5
.addLocator(new ClasspathTemplateLocator("templates")) // Priority 1
.build();
// Engine will try locators in priority order: DB, S3, then classpath
Template template = engine.getTemplate("my-template.html");The @Locate annotation declares which template IDs a custom TemplateLocator can locate. This is required for build-time validation to work correctly with custom locators.
IMPORTANT: This annotation only works in fully integrated environments like Quarkus applications.
package io.quarkus.qute;
/**
* Declares template IDs that a TemplateLocator can locate.
* Required for custom locators to disable build-time validation for those templates.
*/
@Target(ElementType.TYPE)
@Retention(RUNTIME)
@Repeatable(Locates.class)
public @interface Locate {
/**
* Regex pattern matching all Template IDs located by the TemplateLocator.
* @return the pattern
*/
String value();
/**
* Container annotation for repeatable @Locate.
*/
@Target(ElementType.TYPE)
@Retention(RUNTIME)
@interface Locates {
Locate[] value();
}
}Usage Example:
import io.quarkus.qute.Locate;
import io.quarkus.qute.TemplateLocator;
import io.quarkus.qute.TemplateLocation;
import java.util.Optional;
// Single location pattern
@Locate("/my/custom/location/.*")
public class MyCustomLocator implements TemplateLocator {
@Override
public Optional<TemplateLocation> locate(String id) {
if (id.startsWith("/my/custom/location/")) {
// Load template from custom source
return loadFromCustomSource(id);
}
return Optional.empty();
}
private Optional<TemplateLocation> loadFromCustomSource(String id) {
// Implementation
return Optional.empty();
}
}
// Multiple location patterns
@Locate("emails/.*")
@Locate("reports/.*\\.pdf")
public class MultiPatternLocator implements TemplateLocator {
@Override
public Optional<TemplateLocation> locate(String id) {
if (id.startsWith("emails/")) {
return locateEmail(id);
} else if (id.startsWith("reports/") && id.endsWith(".pdf")) {
return locatePdfReport(id);
}
return Optional.empty();
}
private Optional<TemplateLocation> locateEmail(String id) {
// Implementation
return Optional.empty();
}
private Optional<TemplateLocation> locatePdfReport(String id) {
// Implementation
return Optional.empty();
}
}Why @Locate is Required:
Custom TemplateLocators load templates dynamically at runtime. Since these templates aren't available during build time, Qute cannot validate them. The @Locate annotation tells the build system which template IDs will be handled by the locator, allowing it to skip validation for those specific templates.
Pattern Matching:
The value is a regex pattern that matches against Template.getId(). Common patterns:
"mytemplate" - Exact template ID"emails/.*" - All templates in emails directory".*\\.json" - All templates ending with .json"reports/[0-9]+/.*" - Templates in numbered report directoriesThe @EngineConfiguration annotation enables automatic registration of custom components (section helpers, value resolvers, namespace resolvers, parser hooks) to the Quarkus Qute engine.
IMPORTANT: This annotation only works in fully integrated environments like Quarkus applications.
package io.quarkus.qute;
/**
* Enables registration of additional components to the preconfigured Engine.
*
* Supported component interfaces:
* - SectionHelperFactory
* - ValueResolver
* - NamespaceResolver
* - ParserHook
*/
@Target(ElementType.TYPE)
@Retention(RUNTIME)
public @interface EngineConfiguration {
}Capabilities:
When a class is annotated with @EngineConfiguration:
Requirements:
SectionHelperFactory and ParserHook: Must be public with a no-args constructor for build-time instantiation@Dependent is usedUsage Examples:
import io.quarkus.qute.EngineConfiguration;
import io.quarkus.qute.ValueResolver;
import io.quarkus.qute.EvalContext;
import java.util.concurrent.CompletionStage;
// Example 1: Custom Value Resolver
@EngineConfiguration
public class CustomValueResolver implements ValueResolver {
@Override
public boolean appliesTo(EvalContext context) {
return context.getBase() instanceof MyCustomType;
}
@Override
public CompletionStage<Object> resolve(EvalContext context) {
MyCustomType obj = (MyCustomType) context.getBase();
return CompletedStage.of(obj.getProperty(context.getName()));
}
}
// Example 2: Custom Section Helper Factory
@EngineConfiguration
public class RepeatSectionFactory implements SectionHelperFactory<RepeatSectionHelper> {
// No-args constructor required for build-time instantiation
public RepeatSectionFactory() {
}
@Override
public List<String> getDefaultAliases() {
return List.of("repeat");
}
@Override
public ParametersInfo getParameters() {
return ParametersInfo.builder()
.addParameter("times")
.build();
}
@Override
public RepeatSectionHelper initialize(SectionInitContext context) {
return new RepeatSectionHelper(context.getExpression("times"));
}
}
// Example 3: Custom Namespace Resolver
@EngineConfiguration
public class UtilNamespaceResolver implements NamespaceResolver {
@Override
public String getNamespace() {
return "util";
}
@Override
public CompletionStage<Object> resolve(EvalContext context) {
String name = context.getName();
return switch (name) {
case "now" -> CompletedStage.of(LocalDateTime.now());
case "uuid" -> CompletedStage.of(UUID.randomUUID().toString());
default -> Results.notFound(context);
};
}
}
// Example 4: With CDI Injection (runtime only)
@EngineConfiguration
@ApplicationScoped
public class DataResolverWithInjection implements NamespaceResolver {
@Inject
DataService dataService; // Only injected at runtime
@Override
public String getNamespace() {
return "data";
}
@Override
public CompletionStage<Object> resolve(EvalContext context) {
// Can use injected services at runtime
return CompletedStage.of(dataService.getData(context.getName()));
}
}Benefits:
vs Manual Registration:
Without @EngineConfiguration, you would need to manually register via CDI producers:
// Manual approach (not using @EngineConfiguration)
@ApplicationScoped
public class EngineProducer {
@Inject
Engine engine;
@PostConstruct
void init() {
engine = Engine.builder()
.addValueResolver(new CustomValueResolver())
.build();
}
}With @EngineConfiguration, registration is automatic and happens at the right time for both build and runtime.
The Evaluator is responsible for evaluating expressions within templates. It is configured by the Engine and uses value resolvers and namespace resolvers.
package io.quarkus.qute;
/**
* Evaluates expressions.
*/
public interface Evaluator {
/**
* Evaluate expression in resolution context.
*/
CompletionStage<Object> evaluate(Expression expression,
ResolutionContext resolutionContext);
/**
* Check if strict rendering is enforced.
*/
boolean strictRendering();
}// Get evaluator from engine
Engine engine = Engine.builder().addDefaults().build();
Evaluator evaluator = engine.getEvaluator();
// Check if strict rendering is enabled
boolean strict = evaluator.strictRendering();
// The evaluator is primarily used internally by the engine
// but can be accessed for custom resolution logic in section helpersThe TemplateNode interface represents nodes in the parsed template Abstract Syntax Tree (AST). Templates are parsed into a tree of nodes that can be inspected, traversed, and analyzed for template introspection, validation, and custom processing.
package io.quarkus.qute;
/**
* Tree node of a parsed template.
*
* @see Template#getNodes()
* @see Template#findNodes(java.util.function.Predicate)
*/
public interface TemplateNode {
/**
* Resolves this node within the given context.
*
* @param context the resolution context
* @return the result node
*/
CompletionStage<ResultNode> resolve(ResolutionContext context);
/**
* Returns all expressions contained within this node.
*
* @return a list of expressions, empty if no expressions
*/
default List<Expression> getExpressions() {
return Collections.emptyList();
}
/**
* Returns the parameter declarations defined in this template node.
*
* @return a list of param declarations
*/
default List<ParameterDeclaration> getParameterDeclarations() {
return Collections.emptyList();
}
/**
* Returns the origin (source location) of the node.
*
* @return the origin of the node
*/
Origin getOrigin();
/**
* Constant means a static text or a literal output expression.
*
* @return {@code true} if the node represents a constant
*/
default boolean isConstant() {
return false;
}
/**
* Tests if this node is a section node.
*
* @return {@code true} if the node represents a section
* @see SectionNode
*/
default boolean isSection() {
return kind() == Kind.SECTION;
}
/**
* Tests if this node is a text node.
*
* @return {@code true} if the node represents text
* @see TextNode
*/
default boolean isText() {
return kind() == Kind.TEXT;
}
/**
* Tests if this node is an expression node.
*
* @return {@code true} if the node represents an output expression
* @see ExpressionNode
*/
default boolean isExpression() {
return kind() == Kind.EXPRESSION;
}
/**
* Returns the kind of this node.
* <p>
* Note that comments and line separators are never preserved in the parsed template tree.
*
* @return the kind
*/
Kind kind();
/**
* Casts this node to TextNode.
*
* @return this node as TextNode
* @throws IllegalStateException if not a text node
*/
default TextNode asText() {
throw new IllegalStateException();
}
/**
* Casts this node to SectionNode.
*
* @return this node as SectionNode
* @throws IllegalStateException if not a section node
*/
default SectionNode asSection() {
throw new IllegalStateException();
}
/**
* Casts this node to ExpressionNode.
*
* @return this node as ExpressionNode
* @throws IllegalStateException if not an expression node
*/
default ExpressionNode asExpression() {
throw new IllegalStateException();
}
/**
* Casts this node to ParameterDeclarationNode.
*
* @return this node as ParameterDeclarationNode
* @throws IllegalStateException if not a parameter declaration node
*/
default ParameterDeclarationNode asParamDeclaration() {
throw new IllegalStateException();
}
/**
* Node type enumeration.
*/
enum Kind {
TEXT, // TextNode - static text
SECTION, // SectionNode - section like {#if}, {#each}, etc.
EXPRESSION, // ExpressionNode - value expression like {name}
PARAM_DECLARATION // ParameterDeclarationNode - {!param name type}
}
/**
* Represents the origin (source location) of a template node.
*/
interface Origin {
/**
* Returns the line number where the node can be found.
*
* @return the line number (1-based)
*/
int getLine();
/**
* Returns the starting character position within the line.
* <p>
* Note: This information is not available for all nodes,
* but is always available for expression nodes.
*
* @return the line character start position
*/
int getLineCharacterStart();
/**
* Returns the ending character position within the line.
* <p>
* Note: This information is not available for all nodes,
* but is always available for expression nodes.
*
* @return the line character end position
*/
int getLineCharacterEnd();
/**
* Returns the template ID.
*
* @return the template ID
*/
String getTemplateId();
/**
* Returns the generated template ID.
*
* @return the generated template ID
*/
String getTemplateGeneratedId();
/**
* Checks if the template has a non-generated ID.
*
* @return true if the template ID is not auto-generated
*/
default boolean hasNonGeneratedTemplateId() {
return !getTemplateId().equals(getTemplateGeneratedId());
}
/**
* Returns the template variant.
*
* @return the template variant
*/
Optional<Variant> getVariant();
/**
* Checks if this node was synthetically generated.
*
* @return {@code true} if the template node was not part of the original template
*/
default boolean isSynthetic() {
return getLine() == -1;
}
}
}TextNode - Represents static text in the template:
public class TextNode extends ResultNode implements TemplateNode {
public TextNode(String value, Origin origin);
// Always returns Kind.TEXT
// isConstant() returns true
}ExpressionNode - Represents value expressions like {name} or {item.price}:
public class ExpressionNode implements TemplateNode {
// Contains a single Expression
// getExpressions() returns list with this expression
// isConstant() returns true for literal expressions
// Always returns Kind.EXPRESSION
}SectionNode - Represents section helpers like {#if}, {#each}, etc.:
public class SectionNode implements TemplateNode {
// Contains blocks and section helper
// getExpressions() returns expressions from all blocks
// Always returns Kind.SECTION
}ParameterDeclarationNode - Represents parameter declarations like {!param name String}:
public class ParameterDeclarationNode implements TemplateNode, ParameterDeclaration {
// Implements both TemplateNode and ParameterDeclaration
// Always returns Kind.PARAM_DECLARATION
}Example 1: Finding All Expressions in a Template
Template template = engine.parse("{name}, {age}, {email}");
// Get all nodes
List<TemplateNode> nodes = template.getNodes();
// Collect all expressions
List<Expression> expressions = nodes.stream()
.flatMap(node -> node.getExpressions().stream())
.collect(Collectors.toList());
// expressions contains: name, age, emailExample 2: Finding Specific Node Types
Template template = engine.parse("""
{! param user User }
<div>
{#if user.active}
Welcome {user.name}!
{/if}
</div>
""");
// Find all section nodes
List<SectionNode> sections = template.findNodes(TemplateNode::isSection)
.stream()
.map(TemplateNode::asSection)
.collect(Collectors.toList());
// Find parameter declarations
List<ParameterDeclaration> params = template.getParameterDeclarations();Example 3: Template Validation
Template template = engine.getTemplate("user-profile");
// Validate all expressions have required properties
template.getNodes().forEach(node -> {
if (node.isExpression()) {
ExpressionNode exprNode = node.asExpression();
Expression expr = exprNode.getExpressions().get(0);
if (!expr.isLiteral() && !expr.hasTypeInfo()) {
Origin origin = node.getOrigin();
System.err.printf(
"Warning: Expression without type info at %s:%d - %s%n",
origin.getTemplateId(),
origin.getLine(),
expr.toOriginalString()
);
}
}
});Example 4: Extracting All Template Dependencies
/**
* Finds all referenced template fragments and includes.
*/
public Set<String> findTemplateDependencies(Template template) {
Set<String> dependencies = new HashSet<>();
template.findNodes(TemplateNode::isSection).forEach(node -> {
SectionNode section = node.asSection();
String name = section.getName();
if ("include".equals(name) || "insert".equals(name)) {
// Extract template reference from section parameters
section.getExpressions().stream()
.filter(Expression::isLiteral)
.findFirst()
.ifPresent(expr -> {
String templateId = expr.getLiteral().toString();
dependencies.add(templateId);
});
}
});
return dependencies;
}Example 5: Template Complexity Analysis
public class TemplateAnalysis {
public static Map<String, Object> analyze(Template template) {
Map<String, Object> analysis = new HashMap<>();
List<TemplateNode> nodes = template.getNodes();
long textNodes = nodes.stream().filter(TemplateNode::isText).count();
long expressionNodes = nodes.stream().filter(TemplateNode::isExpression).count();
long sectionNodes = nodes.stream().filter(TemplateNode::isSection).count();
int maxNestingDepth = calculateMaxDepth(nodes);
int totalExpressions = nodes.stream()
.mapToInt(node -> node.getExpressions().size())
.sum();
analysis.put("totalNodes", nodes.size());
analysis.put("textNodes", textNodes);
analysis.put("expressionNodes", expressionNodes);
analysis.put("sectionNodes", sectionNodes);
analysis.put("maxNestingDepth", maxNestingDepth);
analysis.put("totalExpressions", totalExpressions);
return analysis;
}
}The tracing API enables logging, profiling, and building interactive debugging tools by providing callbacks at key points during template rendering.
package io.quarkus.qute;
/**
* Manager for trace listeners.
*/
public interface TraceManager {
void addTraceListener(TraceListener listener);
void removeTraceListener(TraceListener listener);
boolean hasTraceListeners();
}package io.quarkus.qute.trace;
/**
* Listener for template rendering events.
*/
public interface TraceListener {
/**
* Called when template rendering starts.
*/
default void onStartTemplate(TemplateEvent event);
/**
* Called before a template node is resolved.
*/
default void onBeforeResolve(ResolveEvent event);
/**
* Called after a template node has been resolved.
*/
default void onAfterResolve(ResolveEvent event);
/**
* Called when template rendering ends.
*/
default void onEndTemplate(TemplateEvent event);
}package io.quarkus.qute.trace;
/**
* Base event with timing information.
*/
public abstract class BaseEvent {
public Engine getEngine();
public void done();
public long getEllapsedTime(); // Returns -1 if not done
}
/**
* Template rendering lifecycle event.
*/
public final class TemplateEvent extends BaseEvent {
public TemplateInstance getTemplateInstance();
}
/**
* Node resolution event.
*/
public final class ResolveEvent extends BaseEvent {
public TemplateNode getTemplateNode();
public ResolutionContext getContext();
public ResultNode getResultNode();
public Throwable getError();
public void resolve(ResultNode resultNode, Throwable error);
}import io.quarkus.qute.*;
import io.quarkus.qute.trace.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.Map;
/**
* Simple logging trace listener.
*/
public class LoggingTraceListener implements TraceListener {
@Override
public void onStartTemplate(TemplateEvent event) {
System.out.println("Started rendering: " +
event.getTemplateInstance().getTemplate().getId());
}
@Override
public void onBeforeResolve(ResolveEvent event) {
TemplateNode node = event.getTemplateNode();
System.out.println(" Resolving node: " + node);
}
@Override
public void onAfterResolve(ResolveEvent event) {
long elapsed = event.getEllapsedTime();
if (event.getError() != null) {
System.out.println(" Resolved with error in " + elapsed + "ns: " +
event.getError().getMessage());
} else {
System.out.println(" Resolved successfully in " + elapsed + "ns");
}
}
@Override
public void onEndTemplate(TemplateEvent event) {
long elapsed = event.getEllapsedTime();
System.out.println("Completed rendering in " + elapsed + "ns");
}
}
/**
* Profiling trace listener - tracks performance metrics.
*/
public class ProfilingTraceListener implements TraceListener {
private final Map<String, Statistics> stats = new ConcurrentHashMap<>();
@Override
public void onStartTemplate(TemplateEvent event) {
String templateId = event.getTemplateInstance().getTemplate().getId();
stats.computeIfAbsent(templateId, k -> new Statistics()).incrementInvocations();
}
@Override
public void onAfterResolve(ResolveEvent event) {
String templateId = event.getEngine()
.getTemplate(event.getContext().getTemplate().getId()).getId();
long elapsed = event.getEllapsedTime();
stats.computeIfAbsent(templateId, k -> new Statistics())
.addNodeResolutionTime(elapsed);
}
@Override
public void onEndTemplate(TemplateEvent event) {
String templateId = event.getTemplateInstance().getTemplate().getId();
long elapsed = event.getEllapsedTime();
stats.get(templateId).addTotalTime(elapsed);
}
public void printStatistics() {
System.out.println("\n=== Template Rendering Statistics ===");
stats.forEach((templateId, stat) -> {
System.out.printf("Template: %s\n", templateId);
System.out.printf(" Invocations: %d\n", stat.invocations);
System.out.printf(" Avg Total Time: %.2f ms\n",
stat.avgTotalTime() / 1_000_000.0);
System.out.printf(" Avg Node Resolution: %.2f μs\n",
stat.avgNodeTime() / 1_000.0);
});
}
private static class Statistics {
long invocations = 0;
long totalTime = 0;
long nodeResolutionTime = 0;
long nodeCount = 0;
void incrementInvocations() {
invocations++;
}
void addTotalTime(long nanos) {
totalTime += nanos;
}
void addNodeResolutionTime(long nanos) {
nodeResolutionTime += nanos;
nodeCount++;
}
double avgTotalTime() {
return invocations == 0 ? 0 : (double) totalTime / invocations;
}
double avgNodeTime() {
return nodeCount == 0 ? 0 : (double) nodeResolutionTime / nodeCount;
}
}
}
/**
* Error detection trace listener.
*/
public class ErrorDetectionTraceListener implements TraceListener {
private final List<String> errors = new ArrayList<>();
@Override
public void onAfterResolve(ResolveEvent event) {
if (event.getError() != null) {
String error = String.format(
"Error in template '%s' at node '%s': %s",
event.getContext().getTemplate().getId(),
event.getTemplateNode(),
event.getError().getMessage()
);
errors.add(error);
}
}
@Override
public void onEndTemplate(TemplateEvent event) {
if (!errors.isEmpty()) {
System.err.println("Errors detected during rendering:");
errors.forEach(System.err::println);
errors.clear();
}
}
}
// Enable tracing and register listeners
Engine engine = Engine.builder()
.addDefaults()
.enableTracing(true) // Must enable tracing
.build();
// Add trace listeners
ProfilingTraceListener profiler = new ProfilingTraceListener();
engine.addTraceListener(new LoggingTraceListener());
engine.addTraceListener(profiler);
engine.addTraceListener(new ErrorDetectionTraceListener());
// Render templates - listeners will receive callbacks
Template template = engine.parse("<h1>{title}</h1>");
template.data("title", "Hello World").render();
// Print profiling statistics
profiler.printStatistics();
// Remove listener when no longer needed
engine.removeTraceListener(profiler);The Engine provides comprehensive template cache management for controlling memory usage and supporting dynamic template reloading.
Engine engine = Engine.builder().addDefaults().build();
// Register template manually
Template myTemplate = engine.parse("<div>{content}</div>");
engine.putTemplate("my-template", myTemplate);
// Check if template is loaded (doesn't trigger locators)
if (engine.isTemplateLoaded("my-template")) {
System.out.println("Template is cached");
}
// Get template (checks cache first, then uses locators)
Template cached = engine.getTemplate("my-template");
// Clear all templates from cache
engine.clearTemplates();
// Remove specific templates by predicate
engine.removeTemplates(id -> id.startsWith("temp-"));
engine.removeTemplates(id -> id.endsWith("-old"));
// Remove templates modified before certain time
long cutoffTime = System.currentTimeMillis() - (24 * 60 * 60 * 1000); // 24 hours
engine.removeTemplates(id -> {
// Custom logic to check template age
return isTemplateOlderThan(id, cutoffTime);
});/**
* Template cache manager with automatic reloading.
*/
public class ReloadingTemplateManager {
private final Engine engine;
private final Map<String, Long> lastModified = new ConcurrentHashMap<>();
private final ScheduledExecutorService scheduler =
Executors.newScheduledThreadPool(1);
public ReloadingTemplateManager(Engine engine) {
this.engine = engine;
startAutoReload();
}
private void startAutoReload() {
scheduler.scheduleAtFixedRate(() -> {
// Check for modified templates
engine.removeTemplates(templateId -> {
long currentModified = getTemplateModificationTime(templateId);
Long previousModified = lastModified.get(templateId);
if (previousModified == null || currentModified > previousModified) {
lastModified.put(templateId, currentModified);
System.out.println("Reloading template: " + templateId);
return true; // Remove from cache to force reload
}
return false;
});
}, 5, 5, TimeUnit.SECONDS);
}
private long getTemplateModificationTime(String templateId) {
// Implementation depends on template storage
try {
Path path = Paths.get("templates/" + templateId);
return Files.getLastModifiedTime(path).toMillis();
} catch (IOException e) {
return 0;
}
}
public void shutdown() {
scheduler.shutdown();
}
}The Engine provides multiple parse methods with different options for template ID and variant configuration.
Engine engine = Engine.builder().addDefaults().build();
// Basic parse - no variant, auto-generated ID
Template t1 = engine.parse("<h1>{title}</h1>");
System.out.println(t1.getId()); // Generated ID like "template_1"
// Parse with variant (affects escaping and content type)
Variant htmlVariant = Variant.forContentType("text/html");
Template t2 = engine.parse("<h1>{title}</h1>", htmlVariant);
// HTML special chars will be escaped
Variant jsonVariant = Variant.forContentType("application/json");
Template t3 = engine.parse("{\"title\": \"{title}\"}", jsonVariant);
// JSON special chars will be escaped
// Parse with variant and custom ID
Template t4 = engine.parse(
"<h1>{title}</h1>",
htmlVariant,
"my-custom-template-id"
);
System.out.println(t4.getId()); // "my-custom-template-id"
// Parse with locale-specific variant
Variant germanHtml = new Variant(
Locale.GERMAN,
Charset.forName("UTF-8"),
"text/html"
);
Template t5 = engine.parse("<h1>{title}</h1>", germanHtml);
// Complex variant with all options
Variant complexVariant = new Variant(
Locale.forLanguageTag("en-US"),
"text/html",
"UTF-8"
);
Template t6 = engine.parse("<h1>{title}</h1>", complexVariant, "us-template");// Parser with standalone line removal (default)
Engine withStandalone = Engine.builder()
.addDefaults()
.removeStandaloneLines(true)
.build();
Template t1 = withStandalone.parse(
"{#if user}\n" +
" <div>{user.name}</div>\n" +
"{/if}\n"
);
// Empty lines with only section tags will be removed
// Parser without standalone line removal
Engine withoutStandalone = Engine.builder()
.addDefaults()
.removeStandaloneLines(false)
.build();
Template t2 = withoutStandalone.parse(
"{#if user}\n" +
" <div>{user.name}</div>\n" +
"{/if}\n"
);
// All whitespace preserved exactly as writtenQute provides comprehensive error handling through TemplateException and the ErrorInitializer interface.
package io.quarkus.qute;
/**
* Template exception with support for Qute templates in error messages.
*/
public class TemplateException extends RuntimeException {
static Builder builder();
public Origin getOrigin();
public ErrorCode getCode();
public Map<String, Object> getArguments();
public String getMessageTemplate();
public Optional<String> getCodeName();
/**
* Builder for creating exceptions.
*/
public static class Builder {
Builder message(String message); // Can be Qute template
Builder cause(Throwable cause);
Builder origin(Origin origin);
Builder code(ErrorCode code);
Builder argument(String key, Object value);
Builder arguments(Map<String, Object> arguments);
Builder arguments(Object... arguments); // Uses {} placeholders
TemplateException build();
}
}package io.quarkus.qute;
/**
* Provides error builder initialization.
*/
public interface ErrorInitializer {
/**
* Create initialized TemplateException builder.
*/
default TemplateException.Builder error(String message);
}package io.quarkus.qute;
/**
* Error code interface.
*/
public interface ErrorCode {
String getName(); // Use prefix like "PARSER_" for related errors
}import io.quarkus.qute.*;
/**
* Custom error codes.
*/
public enum CustomErrorCode implements ErrorCode {
VALIDATION_FAILED("VALIDATION_FAILED"),
DATA_NOT_FOUND("DATA_NOT_FOUND"),
PERMISSION_DENIED("PERMISSION_DENIED");
private final String name;
CustomErrorCode(String name) {
this.name = name;
}
@Override
public String getName() {
return name;
}
}
/**
* Custom component with error handling.
*/
public class ValidatingResolver implements ValueResolver, ErrorInitializer {
@Override
public boolean appliesTo(EvalContext context) {
return context.getBase() instanceof User;
}
@Override
public CompletionStage<Object> resolve(EvalContext context) {
User user = (User) context.getBase();
String property = context.getName();
// Validate access
if (!hasPermission(user, property)) {
throw error("User {user.id} does not have permission to access {property}")
.code(CustomErrorCode.PERMISSION_DENIED)
.origin(context.getOrigin())
.argument("user", user)
.argument("property", property)
.build();
}
// Resolve normally
return resolveProperty(user, property);
}
private boolean hasPermission(User user, String property) {
// Permission check logic
return true;
}
}
/**
* Using error builder with Qute templates in messages.
*/
public void handleTemplateError() {
try {
// Some template operation
template.render();
} catch (Exception e) {
throw TemplateException.builder()
.message("Failed to render template '{template.id}' with data: {#each data.keys}{it}{#if hasNext}, {/if}{/each}")
.cause(e)
.argument("template", template)
.argument("data", dataMap)
.build();
}
}
/**
* Using indexed arguments with {} placeholders.
*/
public void throwIndexedError(String name, int value) {
throw TemplateException.builder()
.message("Invalid parameter: {} with value {}")
.arguments(name, value)
.code(CustomErrorCode.VALIDATION_FAILED)
.build();
}
/**
* Custom section helper with error handling.
*/
public class CustomSectionHelper implements SectionHelper, ErrorInitializer {
@Override
public CompletionStage<ResultNode> resolve(SectionResolutionContext context) {
return context.evaluate(expression).thenApply(result -> {
if (result == null) {
throw error("Expression evaluated to null: {expr}")
.origin(expression.getOrigin())
.argument("expr", expression.toOriginalString())
.build();
}
return processResult(result);
});
}
private ResultNode processResult(Object result) {
// Processing logic
return ResultNode.NOOP;
}
}Here's a complete example bringing together all advanced features:
import io.quarkus.qute.*;
import io.quarkus.qute.trace.*;
import javax.sql.DataSource;
import java.util.*;
public class AdvancedEngineConfiguration {
public static Engine createProductionEngine(DataSource dataSource) {
// Create profiling listener
ProfilingTraceListener profiler = new ProfilingTraceListener();
// Build comprehensive engine
Engine engine = Engine.builder()
// Add all default components
.addDefaults()
// Custom section helpers
.addSectionHelper(new RepeatSectionHelper.Factory())
.addSectionHelper(new ChooseSectionHelper.Factory())
// Custom value resolvers
.addValueResolver(new JsonValueResolver())
.addValueResolver(new DatabaseEntityResolver(dataSource))
// Namespace resolvers
.addNamespaceResolver(NamespaceResolver.builder("env")
.resolve(ctx -> System.getenv(ctx.getName()))
.build())
.addNamespaceResolver(NamespaceResolver.builder("sys")
.resolve(ctx -> System.getProperty(ctx.getName()))
.build())
// Result mappers
.addResultMapper(new JsonResultMapper())
.addResultMapper(new DateTimeResultMapper())
// Template locators (priority order)
.addLocator(new DatabaseTemplateLocator(dataSource)) // Highest
.addLocator(new S3TemplateLocator(s3Client, "templates"))
.addLocator(new ClasspathTemplateLocator("templates")) // Fallback
// Parser hooks
.addParserHook(new ImplicitParametersHook())
.addParserHook(new ConditionalParameterHook())
// Instance initializers
.addTemplateInstanceInitializer(instance -> {
instance.setAttribute("requestId", UUID.randomUUID().toString());
instance.setAttribute("timestamp", System.currentTimeMillis());
})
// Dynamic section helper computation
.computeSectionHelper(name -> {
if (name.startsWith("custom-")) {
return new DynamicSectionFactory(name);
}
return null;
})
// Parser configuration
.removeStandaloneLines(true)
.strictRendering(true)
.iterationMetadataPrefix("item_")
// Timeout configuration
.timeout(30_000) // 30 seconds
.useAsyncTimeout(true)
// Enable tracing
.enableTracing(true)
// Engine lifecycle listener
.addEngineListener(e -> {
System.out.println("Engine initialized with:");
System.out.println(" - " + e.getValueResolvers().size() + " value resolvers");
System.out.println(" - " + e.getSectionHelperFactories().size() + " section helpers");
System.out.println(" - " + e.getResultMappers().size() + " result mappers");
})
.build();
// Add trace listeners after engine creation
engine.addTraceListener(new LoggingTraceListener());
engine.addTraceListener(profiler);
engine.addTraceListener(new ErrorDetectionTraceListener());
// Setup automatic cache management
setupCacheManagement(engine);
return engine;
}
private static void setupCacheManagement(Engine engine) {
// Periodic cache cleanup
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate(() -> {
// Remove old temporary templates
engine.removeTemplates(id ->
id.startsWith("temp-") && isOlderThan(id, Duration.ofHours(1))
);
}, 1, 1, TimeUnit.HOURS);
}
public static void main(String[] args) {
// Create production engine
Engine engine = createProductionEngine(dataSource);
// Use the engine
Template template = engine.parse("""
{#if env:DEBUG_MODE}
<div class="debug">
Request ID: {requestId}
System: {sys:java.version}
</div>
{/if}
<h1>{title}</h1>
{#repeat times=3}
<p>Repeated content</p>
{/repeat}
{#for item in items}
<div>Item {item_index}: {item.name}</div>
{/for}
""");
String result = template
.data("title", "Advanced Qute")
.data("items", List.of(
Map.of("name", "First"),
Map.of("name", "Second"),
Map.of("name", "Third")
))
.render();
System.out.println(result);
}
}The Qute Engine provides comprehensive customization capabilities through:
All components follow consistent patterns with priority-based ordering, optional defaults, and extensive configuration options. The fluent builder API makes it straightforward to create engines tailored to specific application requirements while maintaining clean, readable configuration code.
The Scope class tracks type information for template variables during parsing and validation. It provides a hierarchical context for binding variable names to their types, enabling type-safe template checking and IDE support.
package io.quarkus.qute;
/**
* Tracks type bindings and attributes in a hierarchical scope structure.
* Used during template parsing to enable type checking and validation.
*/
public class Scope {
/**
* Immutable empty scope constant.
*/
public static final Scope EMPTY;
/**
* Create a new scope with an optional parent scope.
*
* @param parentScope the parent scope for hierarchical lookup, or null
*/
public Scope(Scope parentScope);
/**
* Register a type binding for a variable name.
*
* @param binding the variable name
* @param type the fully qualified type name
*/
public void putBinding(String binding, String type);
/**
* Look up a type binding by variable name.
* Searches this scope and parent scopes hierarchically.
*
* @param binding the variable name
* @return the type or null if not found
*/
public String getBinding(String binding);
/**
* Look up a type binding with a default value.
*
* @param binding the variable name
* @param defaultValue the default type to return if not found
* @return the type or defaultValue
*/
public String getBindingTypeOrDefault(String binding, String defaultValue);
/**
* Store an arbitrary attribute in this scope.
*
* @param name the attribute name
* @param value the attribute value
*/
public void putAttribute(String name, Object value);
/**
* Retrieve an attribute from this scope.
*
* @param name the attribute name
* @return the attribute value or null
*/
public Object getAttribute(String name);
/**
* Get the type hint for the last part of an expression.
*
* @return the type hint or null
*/
public String getLastPartHint();
/**
* Set the type hint for the last part of an expression.
*
* @param lastPartHint the type hint
*/
public void setLastPartHint(String lastPartHint);
}Usage in Custom Section Helpers:
When implementing SectionHelperFactory.initializeBlock(), you receive an outer scope and should create a new child scope with additional bindings:
@Override
public Scope initializeBlock(Scope outerScope, BlockInfo block) {
Scope newScope = new Scope(outerScope);
// Add type binding for loop variable
if (block.getParameter("item") != null) {
String iterableType = block.getParameter("items");
// Extract element type from iterable
String elementType = extractElementType(iterableType);
newScope.putBinding("item", elementType);
}
// Add loop metadata bindings
newScope.putBinding("item_index", "int");
newScope.putBinding("item_hasNext", "boolean");
return newScope;
}Type Information for IDE Support:
Scope enables IDE autocompletion and validation:
{#each items}
{it.name} <!-- IDE knows 'it' type from scope -->
{it_index} <!-- IDE knows this is int -->
{/each}The Mapper interface provides a flexible alternative to java.util.Map for dynamic key-value lookups in templates. Unlike maps, mappers can be stateless and perform lookups dynamically.
package io.quarkus.qute;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletionStage;
/**
* Maps keys to values dynamically. Can be stateless with lookups
* performed on-demand.
*
* Mappers are resolved via the mapper value resolver.
*/
public interface Mapper {
/**
* Synchronously get a value for a key.
*
* @param key the key to look up
* @return the value or null
*/
default Object get(String key);
/**
* Asynchronously get a value for a key.
* Default implementation wraps get() in a completed stage.
*
* @param key the key to look up
* @return a CompletionStage resolving to the value
*/
default CompletionStage<Object> getAsync(String key);
/**
* Check if this mapper applies to a given key.
* Allows mappers to handle only specific key patterns.
*
* @param key the key to check
* @return true if this mapper should handle the key
*/
default boolean appliesTo(String key);
/**
* Get the set of known keys that this mapper provides.
* May return a subset or empty set if keys are dynamic.
*
* @return the set of known keys
*/
default Set<String> mappedKeys();
/**
* Wrap a standard Java Map as a Mapper.
*
* @param map the map to wrap
* @return a Mapper backed by the map
*/
static Mapper wrap(Map<String, ?> map);
}Usage Example:
import io.quarkus.qute.Mapper;
// Stateless mapper for dynamic lookups
public class ConfigMapper implements Mapper {
@Override
public Object get(String key) {
// Dynamically fetch from config system
return ConfigProvider.getConfig()
.getOptionalValue(key, String.class)
.orElse(null);
}
@Override
public boolean appliesTo(String key) {
// Only handle config keys
return key.startsWith("config.");
}
@Override
public Set<String> mappedKeys() {
// Return known config keys (or empty if too many)
return ConfigProvider.getConfig()
.getPropertyNames();
}
}
// In template:
Engine engine = Engine.builder()
.addValueResolver(ValueResolvers.mapperResolver())
.build();
ConfigMapper config = new ConfigMapper();
Template template = engine.parse("{config.get('app.name')}");
String result = template.data("config", config).render();Wrapping Standard Maps:
Map<String, String> settings = Map.of(
"theme", "dark",
"lang", "en"
);
Mapper mapper = Mapper.wrap(settings);
template.data("settings", mapper).render();
// In template:
// {settings.get('theme')} -> "dark"Async Mapper Example:
public class AsyncDatabaseMapper implements Mapper {
@Override
public CompletionStage<Object> getAsync(String key) {
// Async database lookup
return database.findByKey(key)
.thenApply(entity -> entity.getValue());
}
}The ParserError enum provides typed error codes for all template parsing errors. These codes are used in TemplateException to identify specific parsing issues.
package io.quarkus.qute;
/**
* Error codes for template parsing errors.
* Each code corresponds to a specific parsing issue.
*/
public enum ParserError implements ErrorCode {
/**
* General parsing error with no specific classification.
*/
GENERAL_ERROR,
/**
* Invalid identifier in expression.
* Example: {fo\to}
*/
INVALID_IDENTIFIER,
/**
* Empty expression (no content between braces).
* Example: {data: }
*/
EMPTY_EXPRESSION,
/**
* Section requires parameters but none provided.
* Example: {#include /}
*/
MANDATORY_SECTION_PARAMS_MISSING,
/**
* String literal not properly terminated.
* Example: {#if 'foo is null}{/}
*/
UNTERMINATED_STRING_LITERAL,
/**
* Section has no name specified.
* Example: {# foo=1 /}
*/
NO_SECTION_NAME,
/**
* No section helper registered for the section name.
* Example: {#foo test /} with no foo helper
*/
NO_SECTION_HELPER_FOUND,
/**
* Section end tag doesn't match start tag.
* Example: {#if test}Hello {name}!{/for}
*/
SECTION_END_DOES_NOT_MATCH_START,
/**
* Section block end tag doesn't match block start.
* Example: {#if test}Hello{#else}Hi{/elsa}{/if}
*/
SECTION_BLOCK_END_DOES_NOT_MATCH_START,
/**
* Section end tag has no corresponding start tag.
* Example: {#if true}Bye...{/if} Hello {/if}
*/
SECTION_START_NOT_FOUND,
/**
* Invalid parameter declaration syntax.
* Example: {@com.foo.Foo }
*/
INVALID_PARAM_DECLARATION,
/**
* Section not properly closed.
* Example: {#if test}Hello {name}
*/
UNTERMINATED_SECTION,
/**
* Expression not properly closed.
* Example: {name
*/
UNTERMINATED_EXPRESSION,
/**
* String literal or composite parameter not properly closed.
* Example: {#if (foo || bar}{/}
*/
UNTERMINATED_STRING_LITERAL_OR_COMPOSITE_PARAMETER,
/**
* Invalid virtual method syntax.
* Example: {foo.baz()(}
*/
INVALID_VIRTUAL_METHOD,
/**
* Invalid bracket notation syntax.
* Example: {foo.baz[}
*/
INVALID_BRACKET_EXPRESSION,
/**
* Bracket notation used with invalid value.
* Example: {foo[bar]} - bar must be string literal or integer
*/
INVALID_VALUE_BRACKET_NOTATION;
@Override
public String getName() {
return "PARSER_" + name();
}
}Usage in Error Handling:
Parser errors are included in TemplateException instances:
try {
Template template = engine.parse("{invalid");
} catch (TemplateException e) {
ErrorCode code = e.getCode();
if (code == ParserError.UNTERMINATED_EXPRESSION) {
// Handle specific error type
System.err.println("Expression not closed properly");
System.err.println("At: " + e.getOrigin().getLine());
}
}Error Code Names:
All parser error codes are prefixed with PARSER_:
ParserError.INVALID_IDENTIFIER.getName() // Returns: "PARSER_INVALID_IDENTIFIER"
ParserError.NO_SECTION_HELPER_FOUND.getName() // Returns: "PARSER_NO_SECTION_HELPER_FOUND"Qute provides comprehensive testing utilities for collecting and inspecting template rendering results.
/**
* Collects rendering results for testing and inspection.
*/
class RenderedResults implements BiConsumer<TemplateInstance, String> {
/**
* Create with default key extractor.
*/
public RenderedResults();
/**
* Create with custom key extractor.
* @param keyExtractor function to extract key from template instance
*/
public RenderedResults(Function<TemplateInstance, String> keyExtractor);
/**
* Accept and record rendering result.
* @param templateInstance the template instance
* @param result the rendering result
*/
@Override
public void accept(TemplateInstance templateInstance, String result);
/**
* Get all results for template ID.
* @param templateId template identifier
* @return list of results
*/
public List<RenderedResult> getResults(String templateId);
/**
* Iterate all results.
* @return iterator over entries
*/
public Iterator<Entry<String, List<RenderedResult>>> iterator();
/**
* Clear all collected results.
*/
public void clear();
/**
* Remove matching results.
* @param predicate filter predicate
*/
public void remove(Predicate<RenderedResult> predicate);
/**
* Set filter for recording.
* @param filter filter predicate
*/
public void setFilter(BiPredicate<TemplateInstance, RenderedResult> filter);
/**
* Record containing template ID, result, and timestamp.
*/
record RenderedResult(String templateId, String result, long timestamp) { }
}Usage in Tests:
import io.quarkus.qute.RenderedResults;
import io.quarkus.qute.Template;
import io.quarkus.qute.TemplateInstance;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
@QuarkusTest
public class TemplateRenderingTest {
@Inject
Template email;
@Inject
RenderedResults results;
@Test
void testEmailRendering() {
// Render template
String output = email
.data("name", "Alice")
.data("subject", "Welcome")
.render();
// Verify result was recorded
assertFalse(results.getResults("email").isEmpty());
// Inspect rendered content
RenderedResults.RenderedResult result = results.getResults("email").get(0);
assertTrue(result.result().contains("Alice"));
assertTrue(result.result().contains("Welcome"));
}
@Test
void testMultipleRenderings() {
// Render multiple times
email.data("name", "Bob").render();
email.data("name", "Charlie").render();
// Verify all results collected
assertEquals(2, results.getResults("email").size());
// Clear for next test
results.clear();
}
}/**
* Template instance that automatically collects rendering results.
*/
class ResultsCollectingTemplateInstance implements TemplateInstance {
/**
* Create collecting instance.
* @param delegate the underlying template instance
* @param results the results collector
* @param keyExtractor function to extract result key
*/
public ResultsCollectingTemplateInstance(
TemplateInstance delegate,
RenderedResults results,
Function<TemplateInstance, String> keyExtractor
);
// Implements all TemplateInstance methods
// Automatically records results to RenderedResults
}Configuration:
Enable automatic result collection in tests:
# application.properties (test profile)
%test.quarkus.qute.test-mode.record-rendered-results=trueWhen enabled, Quarkus automatically wraps template instances with ResultsCollectingTemplateInstance.
Advanced Testing Patterns:
import io.quarkus.qute.RenderedResults;
import org.junit.jupiter.api.*;
@QuarkusTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class AdvancedTemplateTest {
@Inject
RenderedResults results;
@Inject
Template notification;
@BeforeEach
void clearResults() {
results.clear();
}
@Test
void testFilteredResults() {
// Set filter to only record certain templates
results.setFilter((instance, result) ->
result.result().contains("important"));
notification.data("type", "important").render();
notification.data("type", "normal").render();
// Only important notification recorded
assertEquals(1, results.getResults("notification").size());
}
@Test
void testRemoveOldResults() {
// Render multiple times
for (int i = 0; i < 5; i++) {
notification.data("index", i).render();
}
// Remove old results (before a timestamp)
long cutoffTime = System.currentTimeMillis() - 1000;
results.remove(result -> result.timestamp() < cutoffTime);
}
@Test
void testIterateAllResults() {
// Render different templates
notification.data("msg", "A").render();
email.data("msg", "B").render();
// Iterate all results
results.iterator().forEachRemaining(entry -> {
String templateId = entry.getKey();
List<RenderedResults.RenderedResult> templateResults = entry.getValue();
System.out.println(templateId + ": " + templateResults.size());
});
}
}public class ProductionEngineConfig {
public static Engine createProductionEngine() {
return Engine.builder()
.addDefaults()
// Strict validation
.strictRendering(true)
// Reasonable timeout
.timeout(10_000)
// Apply timeout to async operations
.useAsyncTimeout(true)
// Clean output
.removeStandaloneLines(true)
// Production-ready result mappers
.addResultMapper(new HtmlEscaper(List.of("text/html", "text/xml")))
.addResultMapper(new JsonEscaper())
.build();
}
}public class DevelopmentEngineConfig {
public static Engine createDevelopmentEngine() {
return Engine.builder()
.addDefaults()
// More lenient for development
.strictRendering(false)
// Longer timeout for debugging
.timeout(60_000)
// Enable tracing for debugging
.enableTracing(true)
// Add development-specific resolvers
.addValueResolver(new DebugInfoResolver())
.build();
}
}public class TestEngineConfig {
public static Engine createTestEngine() {
return Engine.builder()
.addDefaults()
// Strict for catching issues
.strictRendering(true)
// Short timeout to catch slow templates
.timeout(5_000)
// No tracing overhead
.enableTracing(false)
.build();
}
}// Wrong: No default resolvers
Engine engine = Engine.builder()
.addValueResolver(new MyResolver())
.build();
// Result: Basic operations like {map.size} won't work!
// Correct: Include defaults
Engine engine = Engine.builder()
.addDefaults() // Essential!
.addValueResolver(new MyResolver())
.build();// Wrong: Low priority can't override defaults
public class MyResolver implements ValueResolver {
@Override
public int getPriority() {
return 1; // Same as defaults, may not win
}
}
// Correct: Higher priority to override
public class MyResolver implements ValueResolver {
@Override
public int getPriority() {
return 10; // Higher than defaults
}
}// Wrong: Blocking I/O in resolve()
@Override
public CompletionStage<Object> resolve(EvalContext context) {
Object result = database.blockingQuery(); // Blocks thread!
return CompletedStage.of(result);
}
// Correct: Use async operations
@Override
public CompletionStage<Object> resolve(EvalContext context) {
return database.asyncQuery()
.thenApply(result -> result);
}// Wrong: Returning null instead of NotFound
@Override
public CompletionStage<Object> resolve(EvalContext context) {
if (!canResolve(context)) {
return CompletedStage.of(null); // Wrong! Stops resolution chain
}
return doResolve(context);
}
// Correct: Return NotFound to continue chain
@Override
public CompletionStage<Object> resolve(EvalContext context) {
if (!canResolve(context)) {
return Results.notFound(context); // Correct! Tries next resolver
}
return doResolve(context);
}// Wrong: Caching large objects
@Override
public ValueResolver getCachedResolver(EvalContext context) {
LargeObject result = computeLargeResult(context); // Memory leak!
return ValueResolver.builder()
.resolveWith(result) // Holds reference forever
.build();
}
// Correct: Cache only when beneficial
@Override
public ValueResolver getCachedResolver(EvalContext context) {
if (isExpensiveToCompute(context)) {
Object result = compute(context);
return ValueResolver.builder().resolveWith(result).build();
}
return this; // Don't cache, recompute each time
}