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

value-resolvers.mddocs/

Qute Value Resolvers

Value resolvers are the core extensibility mechanism in Qute for controlling how expressions are evaluated. They enable custom logic for property access, method calls, and data retrieval from Java objects during template rendering.

Capabilities: Core Resolution Interfaces

ValueResolver Interface

The ValueResolver interface is the main extension point for custom value resolution. Resolvers are applied in priority order to resolve each part of an expression.

package io.quarkus.qute;

public interface ValueResolver extends Resolver, WithPriority {

    /**
     * Value resolvers with higher priority take precedence.
     * @return the priority value
     */
    default int getPriority() {
        return WithPriority.DEFAULT_PRIORITY; // 1
    }

    /**
     * @param context the evaluation context
     * @return true if this resolver applies to the given context
     */
    default boolean appliesTo(EvalContext context) {
        return true;
    }

    /**
     * Resolve the value for the given context.
     * Should return Results.NotFound if resolution is not possible.
     * Any other value (including null) is considered valid.
     *
     * @param context the evaluation context
     * @return the resolved value or Results.NotFound
     */
    CompletionStage<Object> resolve(EvalContext context);

    /**
     * Returns the resolver to cache for this context.
     * By default, returns this resolver, but can return an optimized version.
     *
     * @param context the evaluation context
     * @return the resolver to cache
     */
    default ValueResolver getCachedResolver(EvalContext context) {
        return this;
    }

    /**
     * Returns property names for code completion in the Qute debugger.
     * @return set of supported property names
     */
    default Set<String> getSupportedProperties() {
        return Collections.emptySet();
    }

    /**
     * Returns method signatures for code completion in the Qute debugger.
     * Use ${param} syntax to indicate selectable parameters.
     * @return set of supported method signatures
     */
    default Set<String> getSupportedMethods() {
        return Collections.emptySet();
    }

    // Builder and utility methods
    static ValueResolverBuilder builder() {
        return new ValueResolverBuilder();
    }

    static boolean matchClass(EvalContext ctx, Class<?> clazz) {
        return ValueResolvers.matchClass(ctx, clazz);
    }
}

Key Concepts:

  • Priority-based: Higher priority resolvers are tried first (default is 1)
  • Applicability filtering: appliesTo() determines if resolver should be used for a context
  • NotFound vs null: Results.NotFound means "try next resolver"; null is a valid result
  • Caching: Successful resolutions can be cached for performance
  • Debugger support: Provide property/method names for code completion

NamespaceResolver Interface

Namespace resolvers handle expressions that start with a namespace declaration (e.g., {data:colors}, {config:appName}).

package io.quarkus.qute;

public interface NamespaceResolver extends Resolver, WithPriority {

    /**
     * The namespace this resolver handles.
     * Must be alphanumeric with underscores only.
     * @return the namespace
     */
    String getNamespace();

    /**
     * Resolve the value for the given context.
     * Note: context.getBase() will always be null for namespace resolvers.
     *
     * @param context the evaluation context
     * @return the resolved value or Results.NotFound
     */
    CompletionStage<Object> resolve(EvalContext context);

    /**
     * @return the priority value
     */
    default int getPriority() {
        return WithPriority.DEFAULT_PRIORITY;
    }

    // Builder for convenient creation
    static Builder builder(String namespace) {
        return new Builder(namespace);
    }
}

NamespaceResolver.Builder:

public final class Builder {

    // Synchronous resolution
    public Builder resolve(Function<EvalContext, Object> func);

    // Asynchronous resolution
    public Builder resolveAsync(Function<EvalContext, CompletionStage<Object>> func);

    // Set priority
    public Builder priority(int priority);

    // Build the resolver
    public NamespaceResolver build();
}

Capabilities: Evaluation Context

EvalContext Interface

The EvalContext provides information about the expression part being evaluated and access to the resolution environment.

package io.quarkus.qute;

public interface EvalContext {

    /**
     * The base object being accessed.
     * Always null for namespace resolvers.
     * @return the base object or null
     */
    Object getBase();

    /**
     * The property name or method name being accessed.
     * @return the name
     */
    String getName();

    /**
     * The parameter expressions for virtual methods.
     * E.g., for {item.getDiscount(10, 'USD')} returns list of 2 expressions.
     * Never null, but may be empty.
     * @return list of parameter expressions
     */
    List<Expression> getParams();

    /**
     * Parse and evaluate an expression string.
     * @param expression the expression string
     * @return completion stage with result
     */
    CompletionStage<Object> evaluate(String expression);

    /**
     * Evaluate an expression.
     * @param expression the expression
     * @return completion stage with result
     */
    CompletionStage<Object> evaluate(Expression expression);

    /**
     * Get an attribute from the template instance.
     * @param key the attribute key
     * @return the attribute value or null
     */
    Object getAttribute(String key);

    /**
     * @return the current resolution context
     */
    ResolutionContext resolutionContext();
    
    /**
     * Get the origin of the expression being evaluated.
     * @return the origin
     */
    Origin getOrigin();
}

ResolutionContext Interface

The ResolutionContext maintains the context object chain during template rendering (e.g., loop iterations, with blocks).

package io.quarkus.qute;

public interface ResolutionContext {

    /**
     * Parse and evaluate an expression.
     * @return completion stage with result
     */
    CompletionStage<Object> evaluate(String expression);

    /**
     * Evaluate an expression.
     * @return completion stage with result
     */
    CompletionStage<Object> evaluate(Expression expression);

    /**
     * Create a child context with new data.
     * Used by sections like {#for} and {#with}.
     * @return new child context
     */
    ResolutionContext createChild(Object data, Map<String, SectionBlock> extendingBlocks);

    /**
     * @return the current data object
     */
    Object getData();

    /**
     * @return the parent context or null
     */
    ResolutionContext getParent();

    /**
     * Get an extending block by name (queries parent if not found).
     * @return the extending block or null
     */
    SectionBlock getExtendingBlock(String name);

    /**
     * Get an extending block by name (does not query parent).
     * @return the extending block or null
     */
    SectionBlock getCurrentExtendingBlock(String name);

    /**
     * Get a template instance attribute.
     * @return the attribute value or null
     */
    Object getAttribute(String key);

    /**
     * @return the current template
     */
    Template getTemplate();

    /**
     * @return the evaluator
     */
    Evaluator getEvaluator();
}

Capabilities: Mapper Interface

The Mapper interface provides a stateless, dynamic alternative to Map for key-value lookups in templates. Unlike Maps, Mappers can perform lookups dynamically without storing all data in memory.

package io.quarkus.qute;

/**
 * Maps keys to values similar to java.util.Map, but can be stateless.
 * Lookups may be performed dynamically at resolution time.
 */
public interface Mapper {

    /**
     * Get value for key synchronously.
     * @param key the key to lookup
     * @return the value or null
     */
    default Object get(String key) {
        return null;
    }

    /**
     * Get value for key asynchronously.
     * @param key the key to lookup
     * @return CompletionStage with the value or null
     */
    default CompletionStage<Object> getAsync(String key) {
        return CompletedStage.of(get(key));
    }

    /**
     * Check if mapper should handle this key.
     * @param key the key to check
     * @return true if this mapper applies to the key
     */
    default boolean appliesTo(String key) {
        return true;
    }

    /**
     * Get set of known mapped keys.
     * May be a subset of all possible keys for dynamic mappers.
     * @return set of mapped keys (empty by default)
     */
    default Set<String> mappedKeys() {
        return Set.of();
    }

    /**
     * Wrap a Map as a Mapper.
     * @param map the map to wrap
     * @return a Mapper backed by the map
     */
    static Mapper wrap(Map<String, ?> map) {
        return new MapperMapWrapper(map);
    }
}

Creating Custom Mappers

Mappers are ideal for dynamic lookups, database queries, API calls, or computed properties.

Example 1: Database Lookup Mapper

import io.quarkus.qute.Mapper;
import java.util.concurrent.CompletionStage;

public class DatabaseMapper implements Mapper {

    private final UserRepository repository;

    public DatabaseMapper(UserRepository repository) {
        this.repository = repository;
    }

    @Override
    public boolean appliesTo(String key) {
        // Only handle keys that match user ID pattern
        return key.matches("user_\\d+");
    }

    @Override
    public CompletionStage<Object> getAsync(String key) {
        if (key.startsWith("user_")) {
            Long userId = Long.parseLong(key.substring(5));
            return repository.findById(userId)
                .thenApply(opt -> opt.orElse(null));
        }
        return CompletedStage.of(null);
    }

    @Override
    public Set<String> mappedKeys() {
        // Return empty set - keys are dynamic
        return Set.of();
    }
}

Template usage:

{dbMapper.user_123.name}
{dbMapper.user_456.email}

Example 2: Configuration Mapper

public class ConfigMapper implements Mapper {

    private final Config config;

    public ConfigMapper(Config config) {
        this.config = config;
    }

    @Override
    public Object get(String key) {
        return config.getOptionalValue(key, String.class).orElse(null);
    }

    @Override
    public Set<String> mappedKeys() {
        // Return known config keys for code completion
        return Set.of("app.name", "app.version", "app.env");
    }
}

Template usage:

{config.app.name}
{config.app.version}

Example 3: Computed Properties Mapper

public class ComputedMapper implements Mapper {

    @Override
    public Object get(String key) {
        return switch (key) {
            case "timestamp" -> System.currentTimeMillis();
            case "uuid" -> UUID.randomUUID().toString();
            case "now" -> LocalDateTime.now();
            default -> null;
        };
    }

    @Override
    public boolean appliesTo(String key) {
        return Set.of("timestamp", "uuid", "now").contains(key);
    }

    @Override
    public Set<String> mappedKeys() {
        return Set.of("timestamp", "uuid", "now");
    }
}

Template usage:

Request ID: {computed.uuid}
Generated at: {computed.now}
Timestamp: {computed.timestamp}

Mapper vs Map

Use Map when:

  • All data is in memory
  • Key set is known and finite
  • Data doesn't change during rendering

Use Mapper when:

  • Data is fetched dynamically (DB, API, cache)
  • Infinite or large key space
  • Computed values
  • Conditional availability of keys
  • Async resolution required

MapperResolver

The MapperResolver (priority 15) automatically resolves properties on Mapper instances:

// Pass mapper as template data
Mapper dbMapper = new DatabaseMapper(repository);
Template template = engine.parse("{db.user_1.name}");
String result = template.data("db", dbMapper).render();
{! Template usage !}
{db.user_1.name}
{db.user_2.email}

Wrapping Maps

Convert existing Maps to Mappers:

Map<String, Object> data = Map.of(
    "name", "John",
    "age", 30,
    "city", "NYC"
);

Mapper mapper = Mapper.wrap(data);
template.data("info", mapper).render();
{info.name}
{info.age}
{info.city}

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

Performance Considerations

  • getAsync() is preferred for I/O-bound operations
  • appliesTo() can optimize by rejecting keys early
  • mappedKeys() enables IDE code completion (when supported)
  • MapperResolver has high priority (15) for loop performance

Capabilities: Result Handling

Results Class and NotFound

The Results class provides utilities for handling resolution results, including the special NotFound type.

package io.quarkus.qute;

public final class Results {

    // Pre-built completion stages
    public static final CompletedStage<Object> FALSE;
    public static final CompletedStage<Object> TRUE;
    public static final CompletedStage<Object> NULL;

    /**
     * Check if a result is NotFound.
     * @return true if result is NotFound
     */
    public static boolean isNotFound(Object result);

    /**
     * Create NotFound from evaluation context.
     * @return completion stage with NotFound
     */
    public static CompletionStage<Object> notFound(EvalContext evalContext);

    /**
     * Create NotFound from a name.
     * @return completion stage with NotFound
     */
    public static CompletionStage<Object> notFound(String name);

    /**
     * Create empty NotFound.
     * @return completion stage with NotFound
     */
    public static CompletionStage<Object> notFound();
}

Results.NotFound Class:

public static final class NotFound {

    public static final NotFound EMPTY;

    public static NotFound from(EvalContext context);
    public static NotFound from(String name);

    /**
     * @return the base object or empty
     */
    public Optional<Object> getBase();

    /**
     * @return the name of the property/function
     */
    public Optional<String> getName();

    /**
     * @return the list of parameters (never null)
     */
    public List<Expression> getParams();

    /**
     * Convert to human-readable error message.
     * @return error message string
     */
    public String asMessage();
}

NotFound Behavior:

  • Results.NotFound signals "this resolver cannot handle this context"
  • The engine tries the next resolver in priority order
  • null is a valid result (does not trigger next resolver)
  • NotFound messages include helpful context about what was not found

Capabilities: Priority System

WithPriority Interface

The WithPriority interface controls the order in which resolvers are tried.

package io.quarkus.qute;

public interface WithPriority {

    int DEFAULT_PRIORITY = 1;

    /**
     * Higher values take precedence.
     * @return the priority value
     */
    default int getPriority() {
        return DEFAULT_PRIORITY;
    }
}

Priority Guidelines:

Priority RangeUsage
100+Very high priority - overrides everything
15-99High priority - specialized resolvers
10-14Above default - common overrides
5-9Template extensions default range
1-4Default range - most custom resolvers
0Low priority
-1Fallback (e.g., ReflectionValueResolver)
< -1Very low priority - last resort

Resolution Order:

  1. Filter all resolvers where appliesTo(context) returns true
  2. Sort by priority (highest first)
  3. Try each resolver in order until one returns non-NotFound result
  4. Cache the successful resolver for this expression part

Capabilities: Building Custom Resolvers

ValueResolverBuilder

The builder provides a fluent API for creating custom resolvers without implementing the interface.

package io.quarkus.qute;

public final class ValueResolverBuilder {

    /**
     * Set the priority value.
     * @return this builder
     */
    public ValueResolverBuilder priority(int value);

    /**
     * Apply to base class matching the specified class.
     * @return this builder
     */
    public ValueResolverBuilder applyToBaseClass(Class<?> baseClass);

    /**
     * Apply to parts with the specified name.
     * @return this builder
     */
    public ValueResolverBuilder applyToName(String name);

    /**
     * Apply to parts with no parameters.
     * @return this builder
     */
    public ValueResolverBuilder applyToNoParameters();

    /**
     * Apply to parts with specified number of parameters.
     * @return this builder
     */
    public ValueResolverBuilder applyToParameters(int size);

    /**
     * Set custom applicability predicate.
     * @return this builder
     */
    public ValueResolverBuilder appliesTo(Predicate<EvalContext> predicate);

    /**
     * Set synchronous resolution function.
     * @return this builder
     */
    public ValueResolverBuilder resolveSync(Function<EvalContext, Object> fun);

    /**
     * Set asynchronous resolution function.
     * @return this builder
     */
    public ValueResolverBuilder resolveAsync(Function<EvalContext, CompletionStage<Object>> fun);

    /**
     * Resolve with a constant value.
     * @return this builder
     */
    public ValueResolverBuilder resolveWith(Object value);

    /**
     * Build the resolver.
     * @return built value resolver
     */
    public ValueResolver build();
}

Capabilities: Built-in Value Resolvers

Qute includes 17 built-in value resolvers registered via EngineBuilder.addDefaultValueResolvers():

Built-in Resolver Summary Table

ResolverPriorityPurpose
MapperResolver15Resolves Mapper interface properties
ListResolver2List-specific properties and methods
MapResolver1Map properties and entries
MapEntryResolver1Map.Entry key and value properties
CollectionResolver1Collection properties (size, isEmpty, contains)
ArrayResolver1Array properties and element access
ThisResolver1Resolves this keyword
OrResolver1Elvis operator (null-safe default)
OrEmptyResolver1Returns empty list for null
TrueResolver1Ternary operator (conditional)
LogicalAndResolver1Short-circuit logical AND
LogicalOrResolver1Short-circuit logical OR
PlusResolver1Addition for Integer and Long
MinusResolver1Subtraction for Integer and Long
ModResolver1Modulo for Integer and Long
EqualsResolver1Equality comparison
NumberValueResolver1Number conversion methods
ReflectionValueResolver-1Fallback reflection-based resolver

1. MapResolver

Resolves Map properties and entries.

Template Usage:

{map.size}           <!-- Map size -->
{map.keys}           <!-- Key set -->
{map.values}         <!-- Values collection -->
{map.entrySet}       <!-- Entry set -->
{map.isEmpty}        <!-- Boolean -->
{map.someKey}        <!-- Direct key access -->
{map.get('key')}     <!-- Method syntax -->
{map.containsKey('x')} <!-- Check key existence -->

Implementation: Handles java.util.Map instances, providing access to map properties and dynamic key lookups.

2. MapperResolver

Resolves Mapper interface properties (stateless map-like objects).

Priority: 15 (high, optimized for loop iterations)

Template Usage:

{mapper.someKey}     <!-- Dynamic lookup -->

Implementation: Uses Mapper.appliesTo() and Mapper.getAsync() for dynamic resolution.

3. MapEntryResolver

Resolves Map.Entry key and value properties.

Template Usage:

{#for entry in map.entrySet}
  {entry.key}: {entry.value}
{/for}

Implementation: Provides key, getKey, value, and getValue properties.

4. CollectionResolver

Resolves Collection properties.

Template Usage:

{list.size}          <!-- Collection size -->
{list.isEmpty}       <!-- Boolean -->
{list.empty}         <!-- Alias for isEmpty -->
{list.contains(item)} <!-- Contains check -->

Implementation: Handles java.util.Collection instances.

5. ListResolver

Resolves List-specific properties and methods.

Priority: 2 (higher than CollectionResolver)

Template Usage:

{list.first}         <!-- First element -->
{list.last}          <!-- Last element -->
{list.0}             <!-- Index access -->
{list.get(2)}        <!-- Method syntax -->
{list.take(3)}       <!-- First 3 elements -->
{list.takeLast(2)}   <!-- Last 2 elements -->

Implementation: Provides indexed access and list manipulation methods.

6. ArrayResolver

Resolves array properties and element access.

Template Usage:

{array.length}       <!-- Array length -->
{array.size}         <!-- Alias for length -->
{array.0}            <!-- Index access -->
{array.get(1)}       <!-- Method syntax -->
{array.take(3)}      <!-- First 3 elements -->
{array.takeLast(2)}  <!-- Last 2 elements -->

Implementation: Uses reflection to access Java arrays.

7. ThisResolver

Resolves the this keyword to return the base object itself.

Template Usage:

{#with person}
  {this}             <!-- Returns person object -->
{/with}

8. OrResolver (Elvis Operator)

Provides default values for null, empty Optional, or NotFound results.

Template Usage:

{name ?: 'Anonymous'}     <!-- Elvis operator -->
{name or 'Anonymous'}     <!-- Word form -->
{name.or('Anonymous')}    <!-- Method form -->

Implementation: Returns default if base is null, NotFound, or empty Optional.

9. OrEmptyResolver

Returns empty list if base is null or NotFound.

Template Usage:

{#for item in items.orEmpty}
  {item}
{/for}

10. TrueResolver (Ternary Operator)

Conditional operator for ternary expressions.

Template Usage:

{isElvis ? 'Elvis' : 'Not Elvis'}
{value.ifTruthy('yes')}

Implementation: Returns NotFound if base is falsy, otherwise evaluates parameter.

11. LogicalAndResolver

Performs short-circuit logical AND operation.

Template Usage:

{isActive && hasPermission}
{user.isAdmin && feature.enabled}

Implementation: Returns false if base is falsy, otherwise evaluates parameter.

12. LogicalOrResolver

Performs short-circuit logical OR operation.

Template Usage:

{isAdmin || isModerator}
{value1 || value2}

Implementation: Returns true if base is truthy, otherwise evaluates parameter.

13. PlusResolver

Addition operator for Integer and Long.

Template Usage:

{count + 5}
{count.plus(10)}

Implementation: Handles Integer + Integer → Integer, Long + Long → Long, and mixed types.

14. MinusResolver

Subtraction operator for Integer and Long.

Template Usage:

{total - discount}
{value.minus(10)}

15. ModResolver

Modulo operator for Integer and Long.

Template Usage:

{index.mod(2)}       <!-- Check if even: {index.mod(2) == 0} -->

16. EqualsResolver

Equality comparison operator.

Template Usage:

{item == other}
{item.eq(other)}
{item is other}

Implementation: Uses Objects.equals() for null-safe comparison.

17. NumberValueResolver

Provides number conversion methods.

Template Usage:

{value.intValue}
{value.longValue}
{value.floatValue}
{value.doubleValue}
{value.byteValue}
{value.shortValue}

Implementation: Calls corresponding methods on java.lang.Number instances.

Capabilities: Built-in Namespace Resolvers

Qute includes specialized namespace resolvers for advanced template operations.

StrEvalNamespaceResolver

The StrEvalNamespaceResolver evaluates string content as a Qute template. This allows dynamic template parsing and rendering within templates.

Namespace: str

Priority: -3 (low priority)

Signature:

package io.quarkus.qute;

/**
 * Evaluates the string representation of the first parameter as a template.
 * Example: {str:eval('Hello {name}!')}
 */
public class StrEvalNamespaceResolver implements NamespaceResolver {
    public StrEvalNamespaceResolver();
    public StrEvalNamespaceResolver(int priority);

    String getNamespace(); // Returns "str"
    int getPriority();     // Returns -3 by default
}

Template Usage:

{! Evaluate literal string as template !}
{str:eval('Hello {name}!')}

{! Evaluate variable content as template !}
{str:eval(templateString)}

{! Dynamic template with data object !}
{#let template = 'Item: {item.name} - Price: {item.price}'}
  {str:eval(template)}
{/let}

Java Example:

Template template = engine.parse("{str:eval('Hello {name}!')}");
String result = template.data("name", "World").render();
// Output: Hello World!

// Dynamic template from data
Template dynamicTemplate = engine.parse("{str:eval(content)}");
String result2 = dynamicTemplate
    .data("content", "Total: {count} items")
    .data("count", 42)
    .render();
// Output: Total: 42 items

Features:

  • Literal optimization: String literals are cached for performance
  • Variant support: Evaluated templates inherit the current template's variant
  • Nested evaluation: Can access all data from the parent resolution context
  • Async support: Returns CompletionStage<Object> for async rendering

Use Cases:

  1. Dynamic template content - Load template strings from database or config
  2. Template composition - Build templates programmatically
  3. Conditional templates - Select different template strings based on logic
  4. Message formatting - Evaluate message templates with parameters

Registration:

The StrEvalNamespaceResolver is typically added automatically in Quarkus, but can be registered manually:

Engine engine = Engine.builder()
    .addDefaults()
    .addNamespaceResolver(new StrEvalNamespaceResolver())
    .build();

FragmentNamespaceResolver

The FragmentNamespaceResolver renders specific fragments from the current or other templates. This enables template composition and partial rendering.

Namespaces: frg, fragment (aliases)

Priority: -1 (fallback priority)

Signature:

package io.quarkus.qute;

/**
 * Renders a matching fragment from the current or specified template.
 * Example: {frg:myFragment} or {fragment:otherTemplate$myFragment}
 */
public class FragmentNamespaceResolver implements NamespaceResolver {
    public static final String FRG = "frg";
    public static final String FRAGMENT = "fragment";

    public FragmentNamespaceResolver();
    public FragmentNamespaceResolver(String namespace);
    public FragmentNamespaceResolver(String namespace, int priority);

    String getNamespace(); // Returns "frg" by default
    int getPriority();     // Returns -1 by default
}

Template Usage:

{! Current template fragments !}
{#fragment header}
  <header>Site Header</header>
{/fragment}

{#fragment footer}
  <footer>Site Footer</footer>
{/fragment}

{! Render fragment from current template !}
{frg:header}
{frg:footer}

{! Render fragment from other template !}
{fragment:layouts/base$header}
{frg:shared/components$button}

{! Pass named arguments to fragment !}
{frg:card(title='Hello', content='World')}

Fragment Definitions:

{! Template: shared/card.html !}
{#fragment card}
  <div class="card">
    <h3>{title}</h3>
    <p>{content}</p>
  </div>
{/fragment}

Accessing External Template Fragments:

Use the templateId$fragmentId syntax:

{! Render 'nav' fragment from 'layouts/header' template !}
{frg:layouts/header$nav}

{! With parameters !}
{fragment:components/button$primary(label='Click Me', action='submit')}

Named Arguments:

Fragments can accept named arguments passed via the namespace call:

{! Fragment definition !}
{#fragment userCard}
  <div class="user">
    <img src="{avatar}" />
    <h4>{username}</h4>
    <p>{bio}</p>
  </div>
{/fragment}

{! Fragment usage with arguments !}
{frg:userCard(
  avatar='/img/user.jpg',
  username='johndoe',
  bio='Software developer'
)}

Features:

  • Current template fragments: Access fragments without template ID prefix
  • Cross-template fragments: Use templateId$fragmentId syntax
  • Named parameters: Pass data via named arguments
  • Template inheritance: Fragments work with template extension
  • Async rendering: Fully supports asynchronous rendering

Use Cases:

  1. Template composition - Reuse common UI components
  2. Partial rendering - Render specific parts of templates
  3. AJAX responses - Return only fragment HTML for dynamic updates
  4. Modular templates - Break large templates into smaller fragments
  5. Component library - Build reusable fragment components

Registration:

The FragmentNamespaceResolver is typically added automatically in Quarkus, but can be registered manually:

// Default namespace "frg"
Engine engine = Engine.builder()
    .addDefaults()
    .addNamespaceResolver(new FragmentNamespaceResolver())
    .build();

// Custom namespace
Engine engineCustom = Engine.builder()
    .addDefaults()
    .addNamespaceResolver(new FragmentNamespaceResolver("fragment"))
    .build();

Fragment vs Include/Insert:

FeatureFragment NamespaceInclude SectionInsert Section
Syntax{frg:fragmentId}{#include template}{#insert template}
ScopeSingle fragmentWhole templateTemplate with blocks
ArgumentsNamed paramsData objectBlock overrides
Use caseComponent renderingTemplate reuseTemplate extension

Capabilities: ReflectionValueResolver

The ReflectionValueResolver is a special fallback resolver that uses Java reflection to access public members.

Priority: -1 (lowest, used as fallback)

Features:

  • Field access: Public fields by name
  • Getter methods: getName(){obj.name}
  • Boolean accessors: isActive(), hasPermission(){obj.active}, {obj.permission}
  • Method calls: Any public method with matching parameter count
  • Caching: Caches reflection lookups for performance

Example Java Class:

public class Product {
    public String name;           // Field access
    private double price;

    public double getPrice() {    // Getter
        return price;
    }

    public boolean isAvailable() { // Boolean accessor
        return true;
    }

    public String formatPrice(String currency) { // Method
        return currency + " " + price;
    }
}

Template Usage:

{product.name}              <!-- Field access -->
{product.price}             <!-- Getter method -->
{product.available}         <!-- Boolean accessor -->
{product.formatPrice('USD')} <!-- Method call -->

Getter Matching Rules:

  1. Try exact method name match: getName()
  2. Try getter prefix: namegetName()
  3. Try boolean prefix: activeisActive() or hasActive()
  4. Decapitalization preserves multiple capitals: URLEncoder stays URLEncoder

Capabilities: Creating Custom Resolvers

Example 1: Simple Property Resolver

Add a reversed property to strings:

import io.quarkus.qute.*;
import java.util.concurrent.CompletionStage;

public class StringReverseResolver implements ValueResolver {

    @Override
    public boolean appliesTo(EvalContext context) {
        return context.getBase() instanceof String
            && "reversed".equals(context.getName())
            && context.getParams().isEmpty();
    }

    @Override
    public CompletionStage<Object> resolve(EvalContext context) {
        String str = (String) context.getBase();
        return CompletedStage.of(new StringBuilder(str).reverse().toString());
    }
}

// Register with engine
Engine engine = Engine.builder()
    .addValueResolver(new StringReverseResolver())
    .build();

// Use in template: {message.reversed}

Example 2: Using ValueResolverBuilder

Create a resolver for a custom fullName property:

import io.quarkus.qute.*;

public class Person {
    public String firstName;
    public String lastName;
}

// Build resolver
ValueResolver fullNameResolver = ValueResolver.builder()
    .applyToBaseClass(Person.class)
    .applyToName("fullName")
    .applyToNoParameters()
    .resolveSync(ctx -> {
        Person person = (Person) ctx.getBase();
        return person.firstName + " " + person.lastName;
    })
    .build();

// Register with engine
Engine engine = Engine.builder()
    .addValueResolver(fullNameResolver)
    .build();

// Use in template: {person.fullName}

Example 3: Virtual Method Resolver

Add a repeat method to strings:

public class StringRepeatResolver implements ValueResolver {

    @Override
    public int getPriority() {
        return 5; // Higher than default
    }

    @Override
    public boolean appliesTo(EvalContext context) {
        return context.getBase() instanceof String
            && "repeat".equals(context.getName())
            && context.getParams().size() == 1;
    }

    @Override
    public CompletionStage<Object> resolve(EvalContext context) {
        String str = (String) context.getBase();

        // Evaluate the parameter
        return context.evaluate(context.getParams().get(0))
            .thenApply(count -> {
                if (count instanceof Integer) {
                    return str.repeat((Integer) count);
                }
                return Results.NotFound.from(context);
            });
    }
}

// Use in template: {message.repeat(3)}

Example 4: Namespace Resolver

Create a custom namespace for environment variables:

NamespaceResolver envResolver = NamespaceResolver.builder("env")
    .resolve(ctx -> {
        String varName = ctx.getName();
        String value = System.getenv(varName);
        return value != null ? value : Results.NotFound.from(ctx);
    })
    .priority(5)
    .build();

// Register with engine
Engine engine = Engine.builder()
    .addNamespaceResolver(envResolver)
    .build();

// Use in template: {env:HOME}, {env:PATH}

Example 5: Async Resolver

Create a resolver that fetches data asynchronously:

public class AsyncDataResolver implements ValueResolver {

    private final DataService dataService;

    public AsyncDataResolver(DataService dataService) {
        this.dataService = dataService;
    }

    @Override
    public boolean appliesTo(EvalContext context) {
        return context.getBase() instanceof String
            && "fetchData".equals(context.getName())
            && context.getParams().isEmpty();
    }

    @Override
    public CompletionStage<Object> resolve(EvalContext context) {
        String key = (String) context.getBase();

        // Return a CompletableFuture
        return dataService.fetchAsync(key)
            .thenApply(data -> data != null ? data : Results.NotFound.from(context));
    }
}

// Use in template: {key.fetchData}

Example 6: Cached Resolver Optimization

Create a resolver that returns an optimized cached version:

public class OptimizedResolver implements ValueResolver {

    @Override
    public boolean appliesTo(EvalContext context) {
        return context.getBase() instanceof ExpensiveObject
            && "computedValue".equals(context.getName());
    }

    @Override
    public CompletionStage<Object> resolve(EvalContext context) {
        ExpensiveObject obj = (ExpensiveObject) context.getBase();
        Object result = obj.expensiveComputation();
        return CompletedStage.of(result);
    }

    @Override
    public ValueResolver getCachedResolver(EvalContext context) {
        // Compute once and cache the result
        Object result = ((ExpensiveObject) context.getBase()).expensiveComputation();

        return ValueResolver.builder()
            .applyToBaseClass(ExpensiveObject.class)
            .applyToName("computedValue")
            .resolveWith(result) // Constant value
            .build();
    }
}

Capabilities: Advanced Patterns

Pattern 1: Multi-Class Resolver

Handle multiple base classes in a single resolver:

public class SizeResolver implements ValueResolver {

    @Override
    public boolean appliesTo(EvalContext context) {
        if (!"size".equals(context.getName()) || !context.getParams().isEmpty()) {
            return false;
        }
        Object base = context.getBase();
        return base instanceof String
            || base instanceof Collection
            || base instanceof Map
            || (base != null && base.getClass().isArray());
    }

    @Override
    public CompletionStage<Object> resolve(EvalContext context) {
        Object base = context.getBase();

        if (base instanceof String) {
            return CompletedStage.of(((String) base).length());
        } else if (base instanceof Collection) {
            return CompletedStage.of(((Collection<?>) base).size());
        } else if (base instanceof Map) {
            return CompletedStage.of(((Map<?, ?>) base).size());
        } else if (base.getClass().isArray()) {
            return CompletedStage.of(Array.getLength(base));
        }

        return Results.notFound(context);
    }
}

Pattern 2: Resolver with Context Attributes

Access template instance attributes in resolvers:

public class LocalizedResolver implements ValueResolver {

    @Override
    public boolean appliesTo(EvalContext context) {
        return context.getBase() instanceof Message
            && "localized".equals(context.getName());
    }

    @Override
    public CompletionStage<Object> resolve(EvalContext context) {
        Message message = (Message) context.getBase();

        // Get locale from template instance attribute
        Object localeAttr = context.getAttribute(TemplateInstance.LOCALE);
        Locale locale = localeAttr instanceof Locale
            ? (Locale) localeAttr
            : Locale.getDefault();

        return CompletedStage.of(message.getText(locale));
    }
}

// Set locale on template instance
template.instance()
    .setLocale(Locale.FRENCH)
    .render();

Pattern 3: Resolver with Nested Evaluation

Evaluate nested expressions within the resolver:

public class TemplateResolver implements ValueResolver {

    @Override
    public boolean appliesTo(EvalContext context) {
        return context.getBase() instanceof String
            && "eval".equals(context.getName())
            && context.getParams().size() == 1;
    }

    @Override
    public CompletionStage<Object> resolve(EvalContext context) {
        String template = (String) context.getBase();

        // Evaluate the parameter to get the data object
        return context.evaluate(context.getParams().get(0))
            .thenCompose(data -> {
                // Parse and render the template string with the data
                Engine engine = context.resolutionContext().getEvaluator().getEngine();
                return CompletedStage.of(engine.parse(template).data(data).render());
            });
    }
}

// Use in template: {template.eval(person)}

Pattern 4: Resolver with Default Values

Provide multiple fallback options:

public class SafeAccessResolver implements ValueResolver {

    @Override
    public int getPriority() {
        return 10; // Higher priority
    }

    @Override
    public boolean appliesTo(EvalContext context) {
        return "safeGet".equals(context.getName())
            && context.getParams().size() >= 1;
    }

    @Override
    public CompletionStage<Object> resolve(EvalContext context) {
        List<Expression> params = context.getParams();

        // Try each parameter in order until one succeeds
        return tryParam(context, params, 0);
    }

    private CompletionStage<Object> tryParam(EvalContext context,
                                              List<Expression> params,
                                              int index) {
        if (index >= params.size()) {
            return Results.notFound(context);
        }

        return context.evaluate(params.get(index))
            .thenCompose(result -> {
                if (result == null || Results.isNotFound(result)) {
                    // Try next parameter
                    return tryParam(context, params, index + 1);
                }
                return CompletedStage.of(result);
            });
    }
}

// Use in template: {obj.safeGet(field1, field2, 'default')}

Capabilities: Resolver Priority and Order

Understanding Resolution Order

When Qute evaluates an expression part like {person.name}:

  1. Filtering Phase: All registered resolvers are filtered

    • Only resolvers where appliesTo(context) returns true are kept
    • Context includes base object (person), name ("name"), and params (empty)
  2. Sorting Phase: Filtered resolvers are sorted by priority

    • Higher priority values come first
    • Resolvers with same priority maintain registration order
  3. Resolution Phase: Try resolvers in order

    • Call resolve() on highest priority resolver
    • If it returns Results.NotFound, try next resolver
    • If it returns any other value (including null), use that value
    • If all resolvers return NotFound, throw error or use strict mode behavior
  4. Caching Phase: Cache successful resolver

    • Call getCachedResolver() on the successful resolver
    • Store cached resolver for this expression part
    • Subsequent evaluations use cached resolver directly

Priority Examples

// Very high priority - overrides everything
public class HighPriorityResolver implements ValueResolver {
    @Override
    public int getPriority() {
        return 100;
    }
}

// High priority - overrides defaults
public class SpecializedResolver implements ValueResolver {
    @Override
    public int getPriority() {
        return 10;
    }
}

// Default priority
public class NormalResolver implements ValueResolver {
    // Uses DEFAULT_PRIORITY = 1
}

// Fallback priority - tried last
public class FallbackResolver implements ValueResolver {
    @Override
    public int getPriority() {
        return -1;
    }
}

Built-in Resolver Priorities

ResolverPriorityReason
MapperResolver15High performance for loop iterations
ListResolver2Override CollectionResolver for lists
Most built-ins1Default priority
FragmentNamespaceResolver-1Fallback namespace resolver
ReflectionValueResolver-1Fallback when nothing else matches
StrEvalNamespaceResolver-3Low priority namespace evaluation

Capabilities: Debugger Support

Code Completion in Qute Debugger

Resolvers can provide code completion hints for the Qute debugger:

public class EnhancedListResolver implements ValueResolver {

    @Override
    public boolean appliesTo(EvalContext context) {
        return ValueResolver.matchClass(context, List.class);
    }

    @Override
    public Set<String> getSupportedProperties() {
        return Set.of("first", "last", "size", "isEmpty");
    }

    @Override
    public Set<String> getSupportedMethods() {
        return Set.of(
            "get(${index})",      // Selectable parameter
            "take(${n})",
            "takeLast(${n})",
            "contains(${item})"
        );
    }

    @Override
    public CompletionStage<Object> resolve(EvalContext context) {
        // Implementation
    }
}

Completion Syntax:

  • "propertyName" - Simple property
  • "methodName(param)" - Method with parameter
  • "methodName(${param})" - Method with selectable parameter (cursor placed)

Capabilities: Performance Optimization

Optimization Strategies

1. Caching via getCachedResolver()

@Override
public ValueResolver getCachedResolver(EvalContext context) {
    // Compute expensive result once
    Object result = expensiveOperation(context);

    // Return resolver that always returns this result
    return ValueResolver.builder()
        .resolveWith(result)
        .build();
}

2. Early Applicability Checks

@Override
public boolean appliesTo(EvalContext context) {
    // Fast checks first
    if (context.getParams().size() != 1) {
        return false;
    }

    // Class check (optimized by Qute)
    if (!ValueResolver.matchClass(context, MyClass.class)) {
        return false;
    }

    // Name check last
    return "myProperty".equals(context.getName());
}

3. CompletedStage for Synchronous Results

@Override
public CompletionStage<Object> resolve(EvalContext context) {
    // Use CompletedStage instead of CompletableFuture
    // for synchronous results (much faster)
    Object result = computeSync();
    return CompletedStage.of(result);
}

4. Avoid Reflection When Possible

// Slow: reflection every time
Method method = obj.getClass().getMethod("getValue");
Object result = method.invoke(obj);

// Fast: use instanceof and direct calls
if (obj instanceof KnownType) {
    Object result = ((KnownType) obj).getValue();
}

Capabilities: Error Handling

Providing Helpful Error Messages

Use Results.NotFound with context to generate meaningful error messages:

@Override
public CompletionStage<Object> resolve(EvalContext context) {
    MyObject obj = (MyObject) context.getBase();

    if (!obj.hasProperty(context.getName())) {
        // Will generate: Property "xyz" not found on base object "com.example.MyObject"
        return Results.notFound(context);
    }

    try {
        return CompletedStage.of(obj.getProperty(context.getName()));
    } catch (Exception e) {
        // Convert exception to template exception
        throw new TemplateException("Failed to access property: " + context.getName(), e);
    }
}

Handling Null Values

Distinguish between "not found" and "found but null":

@Override
public CompletionStage<Object> resolve(EvalContext context) {
    Map<String, Object> map = (Map<String, Object>) context.getBase();
    String key = context.getName();

    if (!map.containsKey(key)) {
        // Key doesn't exist - try next resolver
        return Results.notFound(context);
    }

    // Key exists but value is null - this is valid!
    // Return NULL completion stage, not notFound
    Object value = map.get(key);
    return value != null ? CompletedStage.of(value) : Results.NULL;
}

Capabilities: Integration with Engine

Registering Resolvers

// Single resolver
Engine engine = Engine.builder()
    .addValueResolver(new MyResolver())
    .build();

// Multiple resolvers
Engine engine = Engine.builder()
    .addValueResolvers(
        new Resolver1(),
        new Resolver2(),
        new Resolver3()
    )
    .build();

// From supplier (for lazy initialization)
Engine engine = Engine.builder()
    .addValueResolver(() -> new MyResolver())
    .build();

// Include defaults plus custom
Engine engine = Engine.builder()
    .addDefaultValueResolvers()  // Built-in resolvers
    .addValueResolver(new MyResolver())
    .build();

Registering Namespace Resolvers

NamespaceResolver customNS = NamespaceResolver.builder("custom")
    .resolve(ctx -> {
        // Resolution logic
    })
    .priority(5)
    .build();

Engine engine = Engine.builder()
    .addNamespaceResolver(customNS)
    .build();

Engine Configuration

Engine engine = Engine.builder()
    // Disable defaults to use only custom resolvers
    // .addDefaultValueResolvers() <- Don't call this

    // Add custom resolvers in desired order
    .addValueResolver(highPriorityResolver)
    .addValueResolver(mediumPriorityResolver)
    .addValueResolver(new ReflectionValueResolver()) // Fallback

    // Configure other engine settings
    .strictRendering(true)
    .timeout(10000)
    .build();

Edge Cases and Troubleshooting

Handling Null Base Objects

@Override
public boolean appliesTo(EvalContext context) {
    // Always check for null base
    Object base = context.getBase();
    if (base == null) {
        return false;
    }
    return base instanceof MyType;
}

Circular Reference Detection

public class SafeResolver implements ValueResolver {
    private final ThreadLocal<Set<Object>> visitedObjects = 
        ThreadLocal.withInitial(HashSet::new);
    
    @Override
    public CompletionStage<Object> resolve(EvalContext context) {
        Object base = context.getBase();
        
        if (visitedObjects.get().contains(base)) {
            // Circular reference detected
            return Results.notFound(context);
        }
        
        visitedObjects.get().add(base);
        try {
            return doResolve(context);
        } finally {
            visitedObjects.get().remove(base);
        }
    }
    
    private CompletionStage<Object> doResolve(EvalContext context) {
        // Implementation
        return Results.notFound(context);
    }
}

Exception Handling in Async Resolvers

@Override
public CompletionStage<Object> resolve(EvalContext context) {
    return asyncOperation(context)
        .exceptionally(throwable -> {
            // Log error
            logger.error("Async resolution failed", throwable);
            // Return NotFound to try next resolver
            return Results.NotFound.from(context);
        });
}

Performance: Avoiding Reflection

// Slow: Using reflection
@Override
public CompletionStage<Object> resolve(EvalContext context) {
    try {
        Method method = context.getBase().getClass()
            .getMethod("get" + capitalize(context.getName()));
        return CompletedStage.of(method.invoke(context.getBase()));
    } catch (Exception e) {
        return Results.notFound(context);
    }
}

// Fast: Using instanceof and direct calls
@Override
public CompletionStage<Object> resolve(EvalContext context) {
    Object base = context.getBase();
    String name = context.getName();
    
    if (base instanceof Product product) {
        return switch(name) {
            case "name" -> CompletedStage.of(product.getName());
            case "price" -> CompletedStage.of(product.getPrice());
            default -> Results.notFound(context);
        };
    }
    return Results.notFound(context);
}

Mapper Best Practices

// Good: Efficient appliesTo check
@Override
public boolean appliesTo(String key) {
    return key.startsWith("user_") && key.length() > 5;
}

// Bad: Expensive appliesTo check
@Override
public boolean appliesTo(String key) {
    // Don't do expensive operations in appliesTo!
    return database.exists(key);  // Too slow!
}

// Good: Async for I/O
@Override
public CompletionStage<Object> getAsync(String key) {
    return database.findAsync(key);
}

// Bad: Blocking in get()
@Override
public Object get(String key) {
    return database.findBlocking(key);  // Blocks rendering!
}

Edge Cases

Empty Parameter List Handling

@Override
public boolean appliesTo(EvalContext context) {
    // Check for no parameters
    return context.getParams().isEmpty()
        && "property".equals(context.getName());
}

// vs method with parameters
@Override
public boolean appliesTo(EvalContext context) {
    // Check for exact parameter count
    return context.getParams().size() == 2
        && "method".equals(context.getName());
}

Parameter Type Checking

@Override
public CompletionStage<Object> resolve(EvalContext context) {
    // Evaluate parameter and check type
    return context.evaluate(context.getParams().get(0))
        .thenCompose(param -> {
            if (!(param instanceof Integer)) {
                // Wrong type - return NotFound
                return Results.notFound(context);
            }
            
            Integer value = (Integer) param;
            return CompletedStage.of(processValue(value));
        });
}

Namespace Resolver with Missing Names

public class SafeNamespaceResolver implements NamespaceResolver {
    
    @Override
    public String getNamespace() {
        return "safe";
    }
    
    @Override
    public CompletionStage<Object> resolve(EvalContext context) {
        String name = context.getName();
        
        if (name == null || name.isEmpty()) {
            // No name provided
            return Results.notFound(context);
        }
        
        // Resolution logic
        return doResolve(name);
    }
    
    private CompletionStage<Object> doResolve(String name) {
        // Implementation
        return Results.notFound();
    }
}

Resolution with Null Results

// Scenario 1: Null is a valid value (don't try next resolver)
@Override
public CompletionStage<Object> resolve(EvalContext context) {
    MyObject obj = (MyObject) context.getBase();
    String property = context.getName();
    
    if (!obj.hasProperty(property)) {
        return Results.notFound(context);  // Try next resolver
    }
    
    // Property exists but value is null - this is valid!
    Object value = obj.getProperty(property);
    return CompletedStage.of(value);  // Returns null without trying next resolver
}

// Scenario 2: Distinguish between null and missing
@Override
public CompletionStage<Object> resolve(EvalContext context) {
    Map<String, Object> map = (Map<String, Object>) context.getBase();
    
    if (!map.containsKey(context.getName())) {
        return Results.notFound(context);  // Key doesn't exist
    }
    
    // Key exists, value might be null
    return CompletedStage.of(map.get(context.getName()));
}

Multiple Namespace Resolvers

// Register multiple namespace resolvers
Engine engine = Engine.builder()
    .addNamespaceResolver(NamespaceResolver.builder("env")
        .resolve(ctx -> System.getenv(ctx.getName()))
        .build())
    .addNamespaceResolver(NamespaceResolver.builder("sys")
        .resolve(ctx -> System.getProperty(ctx.getName()))
        .build())
    .addNamespaceResolver(NamespaceResolver.builder("config")
        .resolve(ctx -> loadConfig(ctx.getName()))
        .build())
    .build();

// Use in templates:
// {env:HOME}
// {sys:java.version}
// {config:app.name}

Resolver Caching Edge Cases

// Scenario 1: Don't cache if value changes
@Override
public ValueResolver getCachedResolver(EvalContext context) {
    if (valueIsStable(context)) {
        // Cache for stable values
        Object result = computeValue(context);
        return ValueResolver.builder().resolveWith(result).build();
    }
    // Don't cache for volatile values
    return this;
}

// Scenario 2: Conditional caching based on type
@Override
public ValueResolver getCachedResolver(EvalContext context) {
    Object result = resolve(context).toCompletableFuture().join();
    
    if (result instanceof Cacheable) {
        // Cache cacheable results
        return ValueResolver.builder().resolveWith(result).build();
    }
    
    // Don't cache other results
    return this;
}

Error Propagation

@Override
public CompletionStage<Object> resolve(EvalContext context) {
    return asyncOperation(context)
        .handle((result, throwable) -> {
            if (throwable != null) {
                // Wrap in TemplateException with context
                throw TemplateException.builder()
                    .message("Failed to resolve {name}")
                    .argument("name", context.getName())
                    .origin(context.getOrigin())
                    .cause(throwable)
                    .build();
            }
            return result;
        });
}

Parameter Evaluation Order

// Parameters are evaluated lazily - control evaluation order
@Override
public CompletionStage<Object> resolve(EvalContext context) {
    List<Expression> params = context.getParams();
    
    // Evaluate first parameter
    return context.evaluate(params.get(0))
        .thenCompose(first -> {
            // Only evaluate second if first succeeds
            if (first != null) {
                return context.evaluate(params.get(1))
                    .thenApply(second -> combineResults(first, second));
            }
            return CompletedStage.of(first);
        });
}

Base Object Type Variance

// Handle inheritance hierarchy
@Override
public boolean appliesTo(EvalContext context) {
    Object base = context.getBase();
    // Matches Animal and all subclasses (Dog, Cat, etc.)
    return base instanceof Animal;
}

@Override
public CompletionStage<Object> resolve(EvalContext context) {
    Animal animal = (Animal) context.getBase();
    
    // Handle specific subtypes
    if (animal instanceof Dog dog) {
        return CompletedStage.of(dog.bark());
    } else if (animal instanceof Cat cat) {
        return CompletedStage.of(cat.meow());
    }
    
    // Default behavior for all animals
    return CompletedStage.of(animal.makeSound());
}

Summary

Value resolvers are the core extensibility mechanism in Qute:

  • ValueResolver: Main interface for custom property/method resolution
  • NamespaceResolver: Handle namespaced expressions (e.g., {data:value})
  • EvalContext: Provides context about the expression being evaluated
  • ResolutionContext: Maintains the data context chain during rendering
  • Mapper: Stateless map-like interface for dynamic lookups
  • Results.NotFound: Signals "try next resolver" vs valid results
  • Priority System: Control resolution order with getPriority()
  • 17 Built-in Resolvers: Map, List, Array, operators, reflection, and more
  • ValueResolverBuilder: Fluent API for creating resolvers
  • Caching: Optimize performance with getCachedResolver()
  • Async Support: Resolvers can return CompletionStage for async operations
  • Debugger Integration: Provide code completion with getSupportedProperties/Methods()

Resolvers enable complete control over how expressions are evaluated while maintaining compatibility with Qute's async, caching, and type-checking features.