or run

tessl search
Log in

Version

Workspace
tessl
Visibility
Public
Created
Last updated
Describes
mavenpkg:maven/io.quarkus/quarkus-qute@3.30.x

docs

checked-templates.mdconfiguration.mdengine-advanced.mdindex.mdmessage-bundles.mdtemplate-basics.mdtemplate-extensions.mdtemplate-syntax.mdutilities.mdvalue-resolvers.md
tile.json

tessl/maven-io-quarkus--quarkus-qute

tessl install tessl/maven-io-quarkus--quarkus-qute@3.30.0

Offer templating support for web, email, etc in a build time, type-safe way

engine-advanced.mddocs/

Qute Engine and Advanced Features

Overview

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.

Capabilities: Engine Interface

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();
}

Parsing Templates

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"
);

Template Caching

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-"));

Result Mapping

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);

Capabilities: EngineBuilder - Fluent Configuration API

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);
    }
}

Basic Engine Configuration

// 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();

Advanced Builder Configuration

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();

Compute Section Helper (Dynamic Registration)

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();

Capabilities: SectionHelper and SectionHelperFactory

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();
        }
    }
}

Creating a Custom Section Helper

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!

Section Helper with Multiple Blocks

/**
 * 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);
        }
    }
}

Capabilities: ParserHook - Template Parsing Customization

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);
}

Creating a Custom ParserHook

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}");

Capabilities: ResultMapper - Custom Result Mapping

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();
}

Creating Custom ResultMappers

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();

Capabilities: TemplateLocator - Custom Template Loading

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();
    }
}

Creating Custom TemplateLocators

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");

@Locate Annotation for Custom Locators

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 directories

Capabilities: @EngineConfiguration - Component Registration

The @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:

  1. Can be used during validation of templates at build time
  2. Automatically registered to the preconfigured Engine at runtime
  3. Automatically registered as a CDI bean

Requirements:

  • Must be a non-abstract, top-level or static nested class
  • Must implement one of the supported component interfaces
  • For SectionHelperFactory and ParserHook: Must be public with a no-args constructor for build-time instantiation
  • For all types: CDI injection points only work at runtime (not at build time)
  • If no CDI scope is defined, @Dependent is used

Usage 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:

  • Declarative component registration
  • No need to manually configure EngineBuilder
  • Build-time validation support
  • Automatic CDI bean registration
  • Consistent with Quarkus conventions

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.

Capabilities: Evaluator - Expression Evaluation

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();
}

Using the Evaluator

// 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 helpers

Capabilities: TemplateNode - Template AST Introspection

The 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;
        }
    }
}

Node Type Implementations

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
}

Template Introspection Examples

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, email

Example 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;
    }
}

Use Cases for TemplateNode

  1. Template Analysis - Analyze template complexity, find patterns, calculate metrics
  2. Validation - Custom validation rules, unused variable detection, type checking
  3. Optimization - Identify optimization opportunities, constant folding
  4. Documentation Generation - Extract parameters, dependencies, used variables
  5. IDE Support - Syntax highlighting, code completion, navigation
  6. Template Migration - Transform templates from one format to another
  7. Security Auditing - Find potentially unsafe expressions, validate data flow

Capabilities: Tracing API - Debugging and Profiling

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);
}

Creating Trace Listeners

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);

Capabilities: Template Cache Management

The Engine provides comprehensive template cache management for controlling memory usage and supporting dynamic template reloading.

Cache Operations

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);
});

Dynamic Template Reloading

/**
 * 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();
    }
}

Capabilities: Advanced Parsing Options

The Engine provides multiple parse methods with different options for template ID and variant configuration.

Parse Method Variants

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 Configuration Effects

// 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 written

Capabilities: Error Handling

Qute 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
}

Creating Custom 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;
    }
}

Complete Advanced Engine Configuration Example

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);
    }
}

Summary

The Qute Engine provides comprehensive customization capabilities through:

  1. Engine Interface - Template parsing, caching, and management
  2. EngineBuilder - Fluent API for complete engine configuration
  3. SectionHelper/Factory - Custom template directives and control structures
  4. ParserHook - Pre-parsing customization and implicit parameters
  5. ResultMapper - Custom result-to-string conversion logic
  6. TemplateLocator - Load templates from any source (DB, S3, etc.)
  7. Evaluator - Expression evaluation with configured resolvers
  8. Tracing API - Debugging, profiling, and monitoring
  9. Template Caching - Fine-grained cache control
  10. Error Handling - Rich exception system with template messages

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.

Capabilities: Scope - Type Information and Attributes

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}

Capabilities: Mapper - Dynamic Data Access

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());
    }
}

Capabilities: ParserError - Error Codes

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"

Capabilities: Testing Support

Qute provides comprehensive testing utilities for collecting and inspecting template rendering results.

RenderedResults Class

/**
 * 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();
    }
}

ResultsCollectingTemplateInstance

/**
 * 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=true

When 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());
        });
    }
}

Best Practices for Engine Configuration

Production Engine Setup

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();
    }
}

Development Engine Setup

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();
    }
}

Testing Engine Setup

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();
    }
}

Common Pitfalls and Solutions

Pitfall 1: Forgetting to Add Defaults

// 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();

Pitfall 2: Incorrect Priority

// 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
    }
}

Pitfall 3: Blocking Operations in Resolvers

// 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);
}

Pitfall 4: Not Handling NotFound Correctly

// 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);
}

Pitfall 5: Memory Leaks in Cached Resolvers

// 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
}