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

template-extensions.mddocs/

Qute Template Extensions

Template extensions provide a powerful way to add custom methods and properties to objects used in Qute templates. This document covers the @TemplateExtension annotation for creating custom extensions, as well as all built-in extension methods provided by Quarkus.

Overview

Template extensions allow you to:

  • Add custom methods to any type without modifying the original class
  • Create reusable utilities accessible from templates
  • Define namespace resolvers for global functions (e.g., str:, config:, time:)
  • Extend built-in types with convenient template operations

Extensions are implemented as static methods annotated with @TemplateExtension and automatically generate ValueResolver instances at build time.

Creating Custom Template Extensions

The @TemplateExtension Annotation

The @TemplateExtension annotation generates a ValueResolver for annotated methods or all eligible methods in an annotated class.

@Retention(RUNTIME)
@Target({ METHOD, TYPE })
public @interface TemplateExtension {
    String METHOD_NAME = "<<method name>>";
    String ANY = "*";
    int DEFAULT_PRIORITY = 5;

    String matchName() default METHOD_NAME;
    String[] matchNames() default {};
    String matchRegex() default "";
    int priority() default DEFAULT_PRIORITY;
    String namespace() default "";

    @Retention(RUNTIME)
    @Target(PARAMETER)
    @interface TemplateAttribute {
        String PARAMETER_NAME = "<<parameter name>>";
        String value() default PARAMETER_NAME;
    }
}

Extension Method Requirements

Template extension methods must:

  • Not be private - Must be accessible
  • Be static - Must be a static method
  • Not return void - Must return a value

Basic Extension Example

The simplest extension method uses the method name to match template property access:

@TemplateExtension
public class ItemExtensions {
    static BigDecimal discountedPrice(Item item) {
        return item.getPrice().multiply(new BigDecimal("0.9"));
    }
}

Template usage:

{item.discountedPrice}

The first parameter (Item item) determines the base object type. This method matches when item resolves to an Item instance.

Matching by Custom Name

Use matchName to specify a different property name:

@TemplateExtension(matchName = "discounted")
static BigDecimal discountedPrice(Item item) {
    return item.getPrice().multiply(new BigDecimal("0.9"));
}

Template usage:

{item.discounted}

Matching Multiple Names

Use matchNames to match several property names. Requires an additional String parameter for the actual name:

@TemplateExtension(matchNames = {"plus", "+"})
static Integer add(Integer number, String name, Integer other) {
    return number + other;
}

Template usage:

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

Matching Any Name

Use matchName = ANY (or matchName = "*") to match any property name. Requires an additional String parameter:

@TemplateExtension(matchName = ANY)
static String itemProperty(Item item, String name) {
    // name contains the actual property name accessed
    return switch(name) {
        case "foo" -> item.getFoo();
        case "bar" -> item.getBar();
        default -> null;
    };
}

Template usage:

{item.foo}
{item.bar}
{item.anything}

Matching by Regular Expression

Use matchRegex to match property names against a regex pattern. Requires an additional String parameter:

@TemplateExtension(matchRegex = "\\d{1,10}")
static String getByIndex(List<String> list, String index) {
    return list.get(Integer.parseInt(index));
}

Template usage:

{list.0}
{list.5}
{list.42}

Priority

Extensions with higher priority take precedence when multiple extensions match:

@TemplateExtension(priority = 10)
static String highPriority(Item item) {
    return "high priority";
}

@TemplateExtension(priority = 1)
static String lowPriority(Item item) {
    return "low priority";
}

Priority reference:

PriorityUsage
10+Type-safe expressions maximum priority
5Default extension priority
1Standard resolver priority
-1Built-in reflection resolver

Namespace Resolvers

Extensions can define namespace resolvers for global functions not tied to a base object:

@TemplateExtension(namespace = "util")
public class UtilExtensions {
    static String format(String pattern, Object... args) {
        return String.format(pattern, args);
    }
}

Template usage:

{util:format("Hello %s", name)}

Multiple methods can share the same namespace within a class, but different classes cannot share namespaces.

Template Attributes

Use @TemplateAttribute to inject attributes from the template instance:

@TemplateExtension
static String localizedPrice(Item item,
                             @TemplateAttribute Locale locale) {
    return NumberFormat.getCurrencyInstance(locale)
                      .format(item.getPrice());
}

Template usage:

template.data("item", item)
       .setAttribute("locale", Locale.FRANCE)
       .render();
{item.localizedPrice}

Class-Level Annotation

Apply @TemplateExtension to a class to generate resolvers for all eligible methods:

@TemplateExtension
public class StringExtensions {
    static String upper(String str) {
        return str.toUpperCase();
    }

    static String lower(String str) {
        return str.toLowerCase();
    }
}

Method-level annotations override class-level settings.

Built-in String Extensions

The StringTemplateExtensions class provides string formatting, concatenation, and the str: namespace.

String Instance Methods

fmt / format

Format a string using String.format() syntax:

@TemplateExtension(matchNames = {"fmt", "format"}, priority = 2)
static String fmtInstance(String format, String ignoredPropertyName,
                          Object... args)

@TemplateExtension(matchNames = {"fmt", "format"}, priority = 3)
static String fmtInstance(String format, String ignoredPropertyName,
                          Locale locale, Object... args)

Template usage:

{pattern.fmt(name, age)}
{"Hello %s, you are %d".fmt(name, age)}
{"Total: %.2f".format(total)}
{"Date: %1$tB %1$te, %1$tY".fmt(locale, date)}

+ Operator (String Concatenation)

Concatenate values to a string:

@TemplateExtension(matchName = "+")
static String plus(String str, Object val)

Template usage:

{firstName + " " + lastName}
{"Hello " + name}

str: Namespace Methods

The str: namespace provides global string utilities.

str:fmt / str:format

Format strings using the namespace syntax:

@TemplateExtension(namespace = "str", matchNames = {"fmt", "format"},
                   priority = 2)
static String fmt(String ignoredPropertyName, String format,
                  Object... args)

@TemplateExtension(namespace = "str", matchNames = {"fmt", "format"},
                   priority = 3)
static String fmt(String ignoredPropertyName, Locale locale,
                  String format, Object... args)

Template usage:

{str:fmt("Hello %s", name)}
{str:format("Total: $%.2f", amount)}
{str:fmt(locale, "Date: %1$tB %1$te, %1$tY", date)}

str:concat

Concatenate multiple arguments into a single string:

@TemplateExtension(namespace = "str", priority = 1)
static String concat(Object... args)

Template usage:

{str:concat("Hello", " ", firstName, " ", lastName)}
{str:concat(prefix, name, suffix)}

str:join

Join multiple arguments with a delimiter:

@TemplateExtension(namespace = "str", priority = 0)
static String join(String delimiter, Object... args)

Template usage:

{str:join(", ", firstName, middleName, lastName)}
{str:join("_", "a", "b", "c")}
{str:join(" | ", item1, item2, item3)}

str:builder

Create a StringBuilder instance:

@TemplateExtension(namespace = "str", priority = -1)
static StringBuilder builder()

@TemplateExtension(namespace = "str", priority = -2)
static StringBuilder builder(Object val)

Template usage:

{#let sb = str:builder}
  {sb + "Hello"}
  {sb + " World"}
  {sb}
{/let}

{#let sb = str:builder("Initial")}
  {sb + " value"}
{/let}

str:self (String Literals)

Access string literals via the namespace (matches any name with lowest priority):

@TemplateExtension(namespace = "str", priority = -10, matchName = ANY)
static String self(String name)

Template usage:

{str:['Literal string']}
{str:['Any text here']}
{str:foo}

StringBuilder + Operator

Append to a StringBuilder:

@TemplateExtension(matchName = "+")
static StringBuilder plus(StringBuilder builder, Object val)

Template usage:

{#let sb = str:builder}
  {sb + "Hello"}
  {sb + " "}
  {sb + name}
  {sb}
{/let}

Built-in Collection Extensions

The CollectionTemplateExtensions class provides methods for working with lists and collections.

get

Get an element at a specific index:

@TemplateExtension
static <T> T get(List<T> list, int index)

Template usage:

{list.get(0)}
{list.get(5)}
{items.get(idx)}

Numeric Index Access

Access list elements using numeric property names:

@TemplateExtension(matchRegex = "\\d{1,10}")
static <T> T getByIndex(List<T> list, String index)

Template usage:

{list.0}
{list.1}
{items.42}

Matches property names that are 1-10 digit numbers.

reversed

Get a reverse iterator for the list:

@TemplateExtension
static <T> Iterator<T> reversed(List<T> list)

Template usage:

{#for item in list.reversed}
  {item.name}
{/for}

take

Get the first n elements:

@TemplateExtension
static <T> List<T> take(List<T> list, int n)

Template usage:

{#for item in items.take(5)}
  {item.name}
{/for}

Throws IndexOutOfBoundsException if n < 1 or n > list.size().

takeLast

Get the last n elements:

@TemplateExtension
static <T> List<T> takeLast(List<T> list, int n)

Template usage:

{#for item in items.takeLast(3)}
  {item.name}
{/for}

Throws IndexOutOfBoundsException if n < 1 or n > list.size().

orEmpty

Return an empty list if the collection is null:

@TemplateExtension
static <T> Collection<T> orEmpty(Collection<T> iterable)

Template usage:

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

This extension has higher priority than the built-in ValueResolvers.orEmpty() and enables validation of expressions derived from {list.orEmpty}.

first

Get the first element:

@TemplateExtension
static <T> T first(List<T> list)

Template usage:

{items.first.name}
{list.first}

Throws NoSuchElementException if the list is empty.

last

Get the last element:

@TemplateExtension
static <T> T last(List<T> list)

Template usage:

{items.last.name}
{list.last}

Throws NoSuchElementException if the list is empty.

Built-in Map Extensions

The MapTemplateExtensions class provides methods for working with maps.

Map Property Access

Access map properties and entries using a catch-all resolver:

@TemplateExtension(matchName = ANY)
static Object map(Map map, String name)

Special property names:

  • keys or keySet - Returns map.keySet()
  • values - Returns map.values()
  • entrySet - Returns map.entrySet()
  • size - Returns map.size()
  • empty or isEmpty - Returns map.isEmpty()

For other names: Attempts map.get(name) and returns Results.NotFound if the key doesn't exist and the value is null.

Template usage:

{map.keys}
{map.values}
{map.size}
{map.isEmpty}
{map.myKey}
{map.customProperty}

get

Explicitly get a value by key:

@TemplateExtension
static <V> V get(Map<?, V> map, Object key)

Template usage:

{map.get('someKey')}
{map.get(keyVariable)}
{data.get('my-key-with-dashes')}

containsKey

Check if a map contains a key:

@TemplateExtension
static boolean containsKey(Map<?, ?> map, Object key)

Template usage:

{#if map.containsKey('foo')}
  Found foo: {map.foo}
{/if}

{map.containsKey(dynamicKey)}

Built-in Number Extensions

The NumberTemplateExtensions class provides arithmetic operations for numbers.

mod (Modulo)

Calculate the modulo of two integers:

@TemplateExtension
static Integer mod(Integer number, Integer mod)

Template usage:

{count.mod(2)}
{index.mod(10)}

{#if count.mod(2) == 0}
  Even number
{/if}

Addition Operations

Add integers and longs:

@TemplateExtension(matchNames = {"plus", "+"})
static Integer addToInt(Integer number, String name, Integer other)

@TemplateExtension(matchNames = {"plus", "+"})
static Long addToInt(Integer number, String name, Long other)

@TemplateExtension(matchNames = {"plus", "+"})
static Long addToLong(Long number, String name, Integer other)

@TemplateExtension(matchNames = {"plus", "+"})
static Long addToLong(Long number, String name, Long other)

Template usage:

{count.plus(5)}
{count + 5}
{total + adjustment}
{price.plus(tax)}

Type conversions:

  • Integer + IntegerInteger
  • Integer + LongLong
  • Long + IntegerLong
  • Long + LongLong

Subtraction Operations

Subtract integers and longs:

@TemplateExtension(matchNames = {"minus", "-"})
static Integer subtractFromInt(Integer number, String name, Integer other)

@TemplateExtension(matchNames = {"minus", "-"})
static Long subtractFromInt(Integer number, String name, Long other)

@TemplateExtension(matchNames = {"minus", "-"})
static Long subtractFromLong(Long number, String name, Integer other)

@TemplateExtension(matchNames = {"minus", "-"})
static Long subtractFromLong(Long number, String name, Long other)

Template usage:

{count.minus(3)}
{count - 3}
{total - discount}
{price.minus(reduction)}

Type conversions:

  • Integer - IntegerInteger
  • Integer - LongLong
  • Long - IntegerLong
  • Long - LongLong

Built-in Time Extensions

The TimeTemplateExtensions class provides date/time formatting using the time: namespace and instance methods. Formatters are cached for performance.

Instance Format Methods

Format temporal objects directly:

@TemplateExtension
static String format(TemporalAccessor temporal, String pattern)

@TemplateExtension
static String format(TemporalAccessor temporal, String pattern,
                     Locale locale)

@TemplateExtension
static String format(TemporalAccessor temporal, String pattern,
                     Locale locale, ZoneId timeZone)

Template usage:

{now.format('yyyy-MM-dd')}
{timestamp.format('HH:mm:ss')}
{date.format('MMMM dd, yyyy', locale)}
{instant.format('yyyy-MM-dd HH:mm:ss', locale, timezone)}

time: Namespace Methods

Format various date/time objects via the namespace:

@TemplateExtension(namespace = "time")
static String format(Object dateTimeObject, String pattern)

@TemplateExtension(namespace = "time")
static String format(Object dateTimeObject, String pattern, Locale locale)

@TemplateExtension(namespace = "time")
static String format(Object dateTimeObject, String pattern,
                     Locale locale, ZoneId timeZone)

Supported object types:

  • TemporalAccessor (LocalDateTime, Instant, ZonedDateTime, etc.)
  • java.util.Date
  • java.util.Calendar
  • Number (interpreted as epoch milliseconds)

Template usage:

{time:format(now, 'yyyy-MM-dd')}
{time:format(date, 'MMMM dd, yyyy')}
{time:format(timestamp, 'HH:mm:ss', locale)}
{time:format(instant, 'yyyy-MM-dd HH:mm:ss', locale, timezone)}
{time:format(1234567890000, 'yyyy-MM-dd HH:mm:ss')}

Pattern examples:

{time:format(now, 'yyyy-MM-dd')}               <!-- 2024-03-15 -->
{time:format(now, 'HH:mm:ss')}                <!-- 14:30:45 -->
{time:format(now, 'EEEE, MMMM dd, yyyy')}     <!-- Friday, March 15, 2024 -->
{time:format(now, 'yyyy-MM-dd HH:mm:ss')}     <!-- 2024-03-15 14:30:45 -->
{time:format(now, "dd MMM yyyy")}             <!-- 15 Mar 2024 -->

Cache Management

Clear the formatter cache (for testing or memory management):

public static void clearCache()

Formatters are cached by pattern, locale, and timezone combination for optimal performance.

Built-in Config Extensions

The ConfigTemplateExtensions class provides access to MicroProfile Config properties via the config: namespace.

Property Access by Name

Access any configuration property directly:

@TemplateExtension(namespace = "config", matchName = ANY)
static Object getConfigProperty(String propertyName)

Template usage:

{config:appName}
{config:['my.dotted.property']}
{config:version}

property

Get a string property explicitly:

@TemplateExtension(namespace = "config", priority = DEFAULT_PRIORITY + 1)
static Object property(String propertyName)

Template usage:

{config:property('app.name')}
{config:property('database.url')}

Returns Results.NotFound if the property doesn't exist.

boolean

Get a boolean property:

@TemplateExtension(namespace = "config", priority = DEFAULT_PRIORITY + 2,
                   matchName = "boolean")
static Object booleanProperty(String propertyName)

Template usage:

{#if config:boolean('feature.enabled')}
  Feature is enabled
{/if}

{config:boolean('debug.mode')}

Returns Results.NotFound if the property doesn't exist.

integer

Get an integer property:

@TemplateExtension(namespace = "config", priority = DEFAULT_PRIORITY + 2,
                   matchName = "integer")
static Object integerProperty(String propertyName)

Template usage:

{config:integer('timeout.seconds')}
{config:integer('max.connections')}

{#if config:integer('retry.count') > 3}
  Too many retries
{/if}

Returns Results.NotFound if the property doesn't exist.

Built-in Object Extensions

The ObjectsTemplateExtensions class provides equality comparison methods.

eq / == / is

Compare two objects for equality using Objects.equals():

@TemplateExtension(matchNames = {"eq", "==", "is"})
static boolean eq(Object value, String ignoredName, Object other)

Template usage:

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

{#if status.eq('active')}
  Active
{/if}

{#if user == currentUser}
  This is you
{/if}

{#if value is null}
  No value
{/if}

Built-in OR Operator Extension

The OrOperatorTemplateExtensions class provides the OR fallback operator.

or

Return a fallback value if the first value is null, not found, or an empty Optional:

@TemplateExtension
static <T> T or(T value, T other)

Template usage:

{name.or('Anonymous')}
{user.email.or('no-email@example.com')}
{optionalValue.or(defaultValue)}

{#for item in list.or(emptyList)}
  {item}
{/for}

Fallback conditions:

  • Value is null
  • Value is Results.NotFound
  • Value is an empty Optional

Extension Priority and Resolution

When multiple extensions match the same expression, Qute uses priority to determine which to apply:

Priority levels:

  • 10 - Maximum for type-safe expressions
  • 5 - Default for @TemplateExtension
  • 1 - Higher priority custom extensions
  • 0 and below - Lower priority extensions
  • -1 - Reflection-based resolver

Priority considerations:

  • Higher priority extensions are evaluated first
  • Use higher priority to override default behavior
  • Use lower priority for fallback behavior
  • Built-in extensions use various priorities to avoid conflicts

Example with multiple matching extensions:

@TemplateExtension(priority = 10)
static String format(String str) {
    return "high priority: " + str;
}

@TemplateExtension(priority = 1)
static String format(String str) {
    return "low priority: " + str;
}

The high-priority extension will be used when {str.format} is encountered.

Complete Extension Examples

Custom Domain Extensions

@TemplateExtension
public class ProductExtensions {

    // Simple property
    static BigDecimal discountedPrice(Product product) {
        return product.getPrice().multiply(new BigDecimal("0.9"));
    }

    // Method with parameter
    static BigDecimal priceWithDiscount(Product product, int percent) {
        BigDecimal discount = BigDecimal.valueOf(100 - percent)
                                       .divide(BigDecimal.valueOf(100));
        return product.getPrice().multiply(discount);
    }

    // Formatted output
    @TemplateExtension(matchName = "priceFormatted")
    static String formatPrice(Product product,
                             @TemplateAttribute Locale locale) {
        return NumberFormat.getCurrencyInstance(locale)
                          .format(product.getPrice());
    }
}

Template usage:

<p>Original: {product.price}</p>
<p>Discounted: {product.discountedPrice}</p>
<p>10% off: {product.priceWithDiscount(10)}</p>
<p>Formatted: {product.priceFormatted}</p>

Namespace Utility Extensions

@TemplateExtension
public class MathExtensions {

    @TemplateExtension(namespace = "math")
    static double sqrt(Number value) {
        return Math.sqrt(value.doubleValue());
    }

    @TemplateExtension(namespace = "math")
    static int abs(int value) {
        return Math.abs(value);
    }

    @TemplateExtension(namespace = "math")
    static long max(long a, long b) {
        return Math.max(a, b);
    }

    @TemplateExtension(namespace = "math")
    static long min(long a, long b) {
        return Math.min(a, b);
    }
}

Template usage:

{math:sqrt(144)}
{math:abs(-42)}
{math:max(count1, count2)}
{math:min(limit, available)}

Generic Collection Extensions

@TemplateExtension
public class EnhancedCollectionExtensions {

    // Check if collection contains element
    static <T> boolean contains(Collection<T> collection, T element) {
        return collection.contains(element);
    }

    // Get collection as comma-separated string
    static <T> String joinWithComma(Collection<T> collection) {
        return collection.stream()
                        .map(Object::toString)
                        .collect(Collectors.joining(", "));
    }

    // Filter by predicate (example for simple conditions)
    @TemplateExtension(matchName = "notEmpty")
    static <T> List<T> filterNotEmpty(List<T> list) {
        return list.stream()
                  .filter(Objects::nonNull)
                  .collect(Collectors.toList());
    }
}

Template usage:

{#if items.contains(selectedItem)}
  Selected item is in the list
{/if}

<p>Items: {items.joinWithComma}</p>

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

Best Practices

  1. Keep extensions focused - Each extension should do one thing well
  2. Use appropriate priorities - Set priority based on specificity and override needs
  3. Document parameter expectations - Especially for ANY and regex matchers
  4. Consider null safety - Handle null inputs gracefully
  5. Use namespaces for utilities - Group related functions under namespaces
  6. Cache expensive operations - Like TimeTemplateExtensions formatter cache
  7. Provide type information - Use specific types for better validation
  8. Follow naming conventions - Use clear, descriptive method names
  9. Test edge cases - Empty collections, null values, boundary conditions
  10. Use @TemplateAttribute sparingly - Only for truly context-dependent behavior

Common Pitfalls

Pitfall 1: Wrong First Parameter Type

// Wrong: First parameter should be the base object type
@TemplateExtension
static String format(String pattern, MyObject obj) {  // Wrong order!
    return String.format(pattern, obj.getValue());
}

// Correct: Base object is first parameter
@TemplateExtension
static String format(MyObject obj, String pattern) {
    return String.format(pattern, obj.getValue());
}
// Usage: {myObject.format('Value: %s')}

Pitfall 2: Forgetting matchName Parameter

// Wrong: matchNames without name parameter
@TemplateExtension(matchNames = {"plus", "+"})
static Integer add(Integer number, Integer other) {  // Missing name param!
    return number + other;
}

// Correct: Add String parameter for name
@TemplateExtension(matchNames = {"plus", "+"})
static Integer add(Integer number, String name, Integer other) {
    return number + other;
}

Pitfall 3: Namespace Conflicts

// Wrong: Same namespace in different classes
@TemplateExtension(namespace = "util")
public class UtilExtensions1 {
    static String foo() { return "foo"; }
}

@TemplateExtension(namespace = "util")
public class UtilExtensions2 {  // Conflict!
    static String bar() { return "bar"; }
}

// Correct: Different namespaces or same class
@TemplateExtension(namespace = "util")
public class UtilExtensions {
    static String foo() { return "foo"; }
    static String bar() { return "bar"; }
}

Pitfall 4: Not Handling Null

// Wrong: Doesn't handle null
@TemplateExtension
static String upper(String str) {
    return str.toUpperCase();  // NullPointerException if str is null!
}

// Correct: Handle null gracefully
@TemplateExtension
static String upper(String str) {
    return str != null ? str.toUpperCase() : "";
}

Advanced Extension Patterns

Conditional Extensions

@TemplateExtension
public class ConditionalExtensions {
    
    // Only applies in debug mode
    static String debug(Object obj, @TemplateAttribute("debug") Boolean debugMode) {
        if (Boolean.TRUE.equals(debugMode)) {
            return obj.getClass().getName() + ": " + obj.toString();
        }
        return obj.toString();
    }
}

// Usage
template.setAttribute("debug", true);
// Template: {myObject.debug}

Chainable Extensions

@TemplateExtension
public class ChainableExtensions {
    
    // Returns same type for chaining
    static List<String> filterEmpty(List<String> list) {
        return list.stream()
            .filter(s -> s != null && !s.isEmpty())
            .collect(Collectors.toList());
    }
    
    static List<String> sort(List<String> list) {
        return list.stream()
            .sorted()
            .collect(Collectors.toList());
    }
}

// Template: Chain multiple extensions
// {items.filterEmpty.sort}

Generic Type Extensions

@TemplateExtension
public class GenericExtensions {
    
    // Works with any List<T>
    static <T> int count(List<T> list, T element) {
        return (int) list.stream()
            .filter(item -> Objects.equals(item, element))
            .count();
    }
    
    // Works with any Collection<T>
    static <T> List<T> distinct(Collection<T> collection) {
        return collection.stream()
            .distinct()
            .collect(Collectors.toList());
    }
}

Edge Cases

Extension with Varargs

@TemplateExtension(namespace = "str")
static String concat(Object... args) {
    // Varargs allows any number of arguments
    return Arrays.stream(args)
        .map(String::valueOf)
        .collect(Collectors.joining());
}

Template usage:

{str:concat("a")}              <!-- Single arg -->
{str:concat("a", "b", "c")}   <!-- Multiple args -->
{str:concat()}                 <!-- No args (empty array) -->

Extension Return Type Matters

// Returns primitive - autoboxed for template use
@TemplateExtension
static int length(String str) {
    return str.length();
}

// Returns null - renders as empty or "null" depending on config
@TemplateExtension
static String nullReturning(String str) {
    return null;
}

// Returns collection - can be iterated in template
@TemplateExtension
static List<Character> chars(String str) {
    return str.chars()
        .mapToObj(c -> (char) c)
        .collect(Collectors.toList());
}

Extension with Multiple TemplateAttributes

@TemplateExtension
static String formatted(Product product,
                       @TemplateAttribute("locale") Locale locale,
                       @TemplateAttribute("currency") String currency,
                       @TemplateAttribute("debug") Boolean debug) {
    String formatted = formatPrice(product.getPrice(), currency, locale);
    if (Boolean.TRUE.equals(debug)) {
        formatted += " [" + product.getClass().getName() + "]";
    }
    return formatted;
}

Regex Pattern Edge Cases

// Match only 2-digit numbers
@TemplateExtension(matchRegex = "\\d{2}")
static String getTwoDigit(List<String> list, String index) {
    return list.get(Integer.parseInt(index));
}

// Match properties starting with "get"
@TemplateExtension(matchRegex = "get[A-Z].*")
static Object dynamicGetter(MyObject obj, String name) {
    // Extract property name after "get"
    String propertyName = name.substring(3);
    return obj.getProperty(propertyName);
}

// Match hex color codes
@TemplateExtension(matchRegex = "#[0-9a-fA-F]{6}")
static Color parseColor(String base, String hex) {
    return Color.decode(hex);
}

Extension Parameter Type Coercion

@TemplateExtension
static String repeat(String str, int times) {
    return str.repeat(times);
}

// In template - integer is passed directly
// {text.repeat(3)}

// What if expression evaluates to Long or String?
// Qute will attempt type coercion:
// - Long → int (if in range)
// - String → int (parsing, may fail)
// - Double → int (truncation)

// For safety, validate parameter types:
@TemplateExtension
static String safeRepeat(String str, Object times) {
    if (times instanceof Number) {
        int count = ((Number) times).intValue();
        return str.repeat(count);
    }
    throw new IllegalArgumentException("times must be a number");
}

Class-Level vs Method-Level Priority

@TemplateExtension(priority = 5)  // Class-level default
public class MyExtensions {
    
    // Uses class-level priority (5)
    static String method1(String str) {
        return str;
    }
    
    // Overrides class-level priority
    @TemplateExtension(priority = 10)
    static String method2(String str) {
        return str;
    }
    
    // Uses class-level priority (5)
    static String method3(String str) {
        return str;
    }
}

Summary

Template extensions provide a powerful mechanism for enhancing Qute templates with custom functionality:

  • @TemplateExtension annotation generates ValueResolver instances
  • Instance methods add properties/methods to specific types
  • Namespace methods provide global utility functions
  • Built-in extensions cover strings, collections, maps, numbers, dates, and config
  • Priority system ensures correct resolution order
  • Flexible matching supports names, patterns, and wildcards
  • Null safety should be considered in all custom extensions
  • Chainable patterns enable fluent template expressions

The built-in extensions provide over 40 methods across 8 extension classes, covering the most common template operations. Custom extensions follow the same patterns to add domain-specific functionality.