tessl install tessl/maven-io-quarkus--quarkus-qute@3.30.0Offer templating support for web, email, etc in a build time, type-safe way
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.
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:
appliesTo() determines if resolver should be used for a contextResults.NotFound means "try next resolver"; null is a valid resultNamespace 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();
}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();
}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();
}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);
}
}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}Use Map when:
Use Mapper when:
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}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());
}
}getAsync() is preferred for I/O-bound operationsappliesTo() can optimize by rejecting keys earlymappedKeys() enables IDE code completion (when supported)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"null is a valid result (does not trigger next resolver)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 Range | Usage |
|---|---|
| 100+ | Very high priority - overrides everything |
| 15-99 | High priority - specialized resolvers |
| 10-14 | Above default - common overrides |
| 5-9 | Template extensions default range |
| 1-4 | Default range - most custom resolvers |
| 0 | Low priority |
| -1 | Fallback (e.g., ReflectionValueResolver) |
| < -1 | Very low priority - last resort |
Resolution Order:
appliesTo(context) returns trueThe 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();
}Qute includes 17 built-in value resolvers registered via EngineBuilder.addDefaultValueResolvers():
| Resolver | Priority | Purpose |
|---|---|---|
| MapperResolver | 15 | Resolves Mapper interface properties |
| ListResolver | 2 | List-specific properties and methods |
| MapResolver | 1 | Map properties and entries |
| MapEntryResolver | 1 | Map.Entry key and value properties |
| CollectionResolver | 1 | Collection properties (size, isEmpty, contains) |
| ArrayResolver | 1 | Array properties and element access |
| ThisResolver | 1 | Resolves this keyword |
| OrResolver | 1 | Elvis operator (null-safe default) |
| OrEmptyResolver | 1 | Returns empty list for null |
| TrueResolver | 1 | Ternary operator (conditional) |
| LogicalAndResolver | 1 | Short-circuit logical AND |
| LogicalOrResolver | 1 | Short-circuit logical OR |
| PlusResolver | 1 | Addition for Integer and Long |
| MinusResolver | 1 | Subtraction for Integer and Long |
| ModResolver | 1 | Modulo for Integer and Long |
| EqualsResolver | 1 | Equality comparison |
| NumberValueResolver | 1 | Number conversion methods |
| ReflectionValueResolver | -1 | Fallback reflection-based resolver |
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.
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.
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.
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.
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.
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.
Resolves the this keyword to return the base object itself.
Template Usage:
{#with person}
{this} <!-- Returns person object -->
{/with}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.
Returns empty list if base is null or NotFound.
Template Usage:
{#for item in items.orEmpty}
{item}
{/for}Conditional operator for ternary expressions.
Template Usage:
{isElvis ? 'Elvis' : 'Not Elvis'}
{value.ifTruthy('yes')}Implementation: Returns NotFound if base is falsy, otherwise evaluates parameter.
Performs short-circuit logical AND operation.
Template Usage:
{isActive && hasPermission}
{user.isAdmin && feature.enabled}Implementation: Returns false if base is falsy, otherwise evaluates parameter.
Performs short-circuit logical OR operation.
Template Usage:
{isAdmin || isModerator}
{value1 || value2}Implementation: Returns true if base is truthy, otherwise evaluates parameter.
Addition operator for Integer and Long.
Template Usage:
{count + 5}
{count.plus(10)}Implementation: Handles Integer + Integer → Integer, Long + Long → Long, and mixed types.
Subtraction operator for Integer and Long.
Template Usage:
{total - discount}
{value.minus(10)}Modulo operator for Integer and Long.
Template Usage:
{index.mod(2)} <!-- Check if even: {index.mod(2) == 0} -->Equality comparison operator.
Template Usage:
{item == other}
{item.eq(other)}
{item is other}Implementation: Uses Objects.equals() for null-safe comparison.
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.
Qute includes specialized namespace resolvers for advanced template operations.
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 itemsFeatures:
CompletionStage<Object> for async renderingUse Cases:
Registration:
The StrEvalNamespaceResolver is typically added automatically in Quarkus, but can be registered manually:
Engine engine = Engine.builder()
.addDefaults()
.addNamespaceResolver(new StrEvalNamespaceResolver())
.build();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:
templateId$fragmentId syntaxUse Cases:
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:
| Feature | Fragment Namespace | Include Section | Insert Section |
|---|---|---|---|
| Syntax | {frg:fragmentId} | {#include template} | {#insert template} |
| Scope | Single fragment | Whole template | Template with blocks |
| Arguments | Named params | Data object | Block overrides |
| Use case | Component rendering | Template reuse | Template extension |
The ReflectionValueResolver is a special fallback resolver that uses Java reflection to access public members.
Priority: -1 (lowest, used as fallback)
Features:
getName() → {obj.name}isActive(), hasPermission() → {obj.active}, {obj.permission}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:
getName()name → getName()active → isActive() or hasActive()URLEncoder stays URLEncoderAdd 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}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}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)}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}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}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();
}
}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);
}
}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();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)}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')}When Qute evaluates an expression part like {person.name}:
Filtering Phase: All registered resolvers are filtered
appliesTo(context) returns true are keptperson), name ("name"), and params (empty)Sorting Phase: Filtered resolvers are sorted by priority
Resolution Phase: Try resolvers in order
resolve() on highest priority resolverResults.NotFound, try next resolvernull), use that valueNotFound, throw error or use strict mode behaviorCaching Phase: Cache successful resolver
getCachedResolver() on the successful resolver// 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;
}
}| Resolver | Priority | Reason |
|---|---|---|
| MapperResolver | 15 | High performance for loop iterations |
| ListResolver | 2 | Override CollectionResolver for lists |
| Most built-ins | 1 | Default priority |
| FragmentNamespaceResolver | -1 | Fallback namespace resolver |
| ReflectionValueResolver | -1 | Fallback when nothing else matches |
| StrEvalNamespaceResolver | -3 | Low priority namespace evaluation |
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)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();
}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);
}
}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;
}// 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();NamespaceResolver customNS = NamespaceResolver.builder("custom")
.resolve(ctx -> {
// Resolution logic
})
.priority(5)
.build();
Engine engine = Engine.builder()
.addNamespaceResolver(customNS)
.build();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();@Override
public boolean appliesTo(EvalContext context) {
// Always check for null base
Object base = context.getBase();
if (base == null) {
return false;
}
return base instanceof MyType;
}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);
}
}@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);
});
}// 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);
}// 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!
}@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());
}@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));
});
}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();
}
}// 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()));
}// 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}// 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;
}@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;
});
}// 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);
});
}// 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());
}Value resolvers are the core extensibility mechanism in Qute:
{data:value})getPriority()getCachedResolver()CompletionStage for async operationsgetSupportedProperties/Methods()Resolvers enable complete control over how expressions are evaluated while maintaining compatibility with Qute's async, caching, and type-checking features.