tessl install tessl/maven-io-quarkus--quarkus-qute@3.30.0Offer templating support for web, email, etc in a build time, type-safe way
Type-safe template declaration and validation using @CheckedTemplate annotation. Checked templates enable compile-time validation of template parameters and expressions, providing enhanced reliability and IDE support for template development.
@CheckedTemplate configures type-safe templates on static nested classes or template record types. All native static methods in the annotated class declare templates with compile-time parameter validation.
/**
* Configures type-safe templates.
* Place on static nested class with native static methods, or on template records.
* Each native static method declares a template with validated parameters.
*
* IMPORTANT: This annotation only works in fully integrated environments like Quarkus applications.
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface CheckedTemplate {
/** Constant for defaulted base path */
String DEFAULTED = "<<defaulted>>";
/** Constant for element name strategy */
String ELEMENT_NAME = "<<element name>>";
/** Constant for hyphenated element name strategy */
String HYPHENATED_ELEMENT_NAME = "<<hyphenated element name>>";
/** Constant for underscored element name strategy */
String UNDERSCORED_ELEMENT_NAME = "<<underscored element name>>";
/**
* Base path relative to templates root.
* Default: simple name of enclosing class for nested class, empty for top-level class.
* @return base path
*/
String basePath() default DEFAULTED;
/**
* If true, templates can only contain type-safe expressions.
* Default: true
* @return whether type-safe expressions are required
*/
boolean requireTypeSafeExpressions() default true;
/**
* Default naming strategy for template methods/records.
* Values: ELEMENT_NAME, HYPHENATED_ELEMENT_NAME, UNDERSCORED_ELEMENT_NAME
* Default: ELEMENT_NAME
* @return naming strategy
*/
String defaultName() default ELEMENT_NAME;
/**
* If true, methods with $ in name are not treated as fragments.
* Default: false
* @return whether to ignore fragments
*/
boolean ignoreFragments() default false;
}Usage Examples:
import io.quarkus.qute.CheckedTemplate;
import io.quarkus.qute.TemplateInstance;
import java.util.List;
// Example 1: Nested static class with defaulted base path
public class ItemResource {
@CheckedTemplate
public static class Templates {
// Template at: templates/ItemResource/item.html
public static native TemplateInstance item(Item item);
// Template at: templates/ItemResource/items.html
public static native TemplateInstance items(List<Item> items);
// Template at: templates/ItemResource/details.html
public static native TemplateInstance details(Item item, User user);
}
public TemplateInstance showItem(Long id) {
Item item = findItem(id);
return Templates.item(item); // Type-safe: parameter validated at build time
}
public TemplateInstance listItems() {
List<Item> items = findAllItems();
return Templates.items(items);
}
}
// Example 2: Custom base path
public class ReportController {
@CheckedTemplate(basePath = "reports/v2")
public static class Templates {
// Template at: templates/reports/v2/monthly.html
public static native TemplateInstance monthly(ReportData data);
// Template at: templates/reports/v2/annual.html
public static native TemplateInstance annual(ReportData data);
}
public TemplateInstance generateMonthly(int year, int month) {
ReportData data = prepareMonthlyData(year, month);
return Templates.monthly(data);
}
}
// Example 3: Top-level class (templates at root)
@CheckedTemplate
public class GlobalTemplates {
// Template at: templates/error.html
public static native TemplateInstance error(String message);
// Template at: templates/success.html
public static native TemplateInstance success(String message);
}
// Example 4: Hyphenated naming
public class DashboardResource {
@CheckedTemplate(defaultName = CheckedTemplate.HYPHENATED_ELEMENT_NAME)
public static class Templates {
// Template at: templates/DashboardResource/user-profile.html
public static native TemplateInstance userProfile(User user);
// Template at: templates/DashboardResource/admin-panel.html
public static native TemplateInstance adminPanel(Admin admin);
}
}
// Example 5: Underscored naming
public class AdminResource {
@CheckedTemplate(defaultName = CheckedTemplate.UNDERSCORED_ELEMENT_NAME)
public static class Templates {
// Template at: templates/AdminResource/user_profile.html
public static native TemplateInstance userProfile(User user);
// Template at: templates/AdminResource/admin_panel.html
public static native TemplateInstance adminPanel(Admin admin);
}
}
// Example 6: Type-safe expressions disabled (legacy templates)
public class LegacyResource {
@CheckedTemplate(requireTypeSafeExpressions = false)
public static class Templates {
// Type checking relaxed for legacy template compatibility
public static native TemplateInstance legacy(Object data);
}
}Fragments within checked templates are declared using methods with $ in the name. The part before $ is the template method name, and the part after is the fragment identifier.
Usage Examples:
import io.quarkus.qute.CheckedTemplate;
import io.quarkus.qute.TemplateInstance;
import java.util.List;
public class ProductResource {
@CheckedTemplate
public static class Templates {
// Main template at: templates/ProductResource/products.html
public static native TemplateInstance products(List<Product> products);
// Fragment "item" in products template
// Fragment identifier derived from part after $
public static native TemplateInstance products$item(Product product);
// Fragment "header" in products template
public static native TemplateInstance products$header(int total);
}
// Render full template
public TemplateInstance showProducts() {
List<Product> products = findAllProducts();
return Templates.products(products);
}
// Render just the item fragment (e.g., for AJAX partial updates)
public TemplateInstance renderSingleProduct(Long id) {
Product product = findProduct(id);
return Templates.products$item(product);
}
// Render just the header fragment
public TemplateInstance renderHeader() {
int total = countProducts();
return Templates.products$header(total);
}
}
// Disable fragment interpretation
public class SimpleResource {
@CheckedTemplate(ignoreFragments = true)
public static class Templates {
// Method name with $ NOT treated as fragment
public static native TemplateInstance data$export(Data data);
}
}Java records implementing TemplateInstance can be annotated with @CheckedTemplate for concise type-safe template declarations.
Usage Example:
import io.quarkus.qute.CheckedTemplate;
import io.quarkus.qute.TemplateInstance;
// Template record at: templates/UserPage.html
@CheckedTemplate
public record UserPage(User user, String title) implements TemplateInstance {
}
// Template record with custom base path
@CheckedTemplate(basePath = "emails")
public record WelcomeEmail(User user, String verificationLink) implements TemplateInstance {
// Template at: templates/emails/WelcomeEmail.html
}
// Usage
public class UserController {
public TemplateInstance showUser(Long id) {
User user = findUser(id);
return new UserPage(user, "User Profile");
}
public TemplateInstance sendWelcome(User user) {
String link = generateVerificationLink(user);
return new WelcomeEmail(user, link);
}
}Checked templates provide comprehensive build-time validation:
Example Template with Parameter Declaration:
{! Template: templates/ItemResource/item.html !}
{@org.acme.Item item}
<h1>{item.name}</h1>
<p>Price: ${item.price}</p>
<p>Stock: {item.stock}</p>Corresponding Java Code:
@CheckedTemplate
public static class Templates {
// Build validates that:
// 1. templates/ItemResource/item.html exists
// 2. Template declares parameter: {@org.acme.Item item}
// 3. All expressions (item.name, item.price, item.stock) are valid for Item type
public static native TemplateInstance item(Item item);
}Checked templates integrate seamlessly with Quarkus REST (formerly RESTEasy Reactive) for automatic rendering.
Usage Example:
import io.quarkus.qute.CheckedTemplate;
import io.quarkus.qute.TemplateInstance;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import java.util.List;
@Path("/items")
public class ItemResource {
@CheckedTemplate
public static class Templates {
public static native TemplateInstance item(Item item);
public static native TemplateInstance items(List<Item> items);
}
@GET
@Path("/{id}")
@Produces(MediaType.TEXT_HTML)
public TemplateInstance get(@PathParam("id") Long id) {
Item item = findItem(id);
// Return TemplateInstance directly - Quarkus REST renders automatically
return Templates.item(item);
}
@GET
@Produces(MediaType.TEXT_HTML)
public TemplateInstance list() {
List<Item> items = findAllItems();
return Templates.items(items);
}
}Checked templates support complex parameter types including generics, collections, and custom types.
Usage Example:
import io.quarkus.qute.CheckedTemplate;
import io.quarkus.qute.TemplateInstance;
import java.util.List;
import java.util.Map;
import java.util.Optional;
public class AdvancedResource {
@CheckedTemplate
public static class Templates {
// Generic types
public static native TemplateInstance genericList(List<String> items);
// Map types
public static native TemplateInstance mapData(Map<String, Object> data);
// Optional types
public static native TemplateInstance optionalUser(Optional<User> user);
// Nested generics
public static native TemplateInstance nestedData(Map<String, List<Item>> categories);
// Multiple parameters with different types
public static native TemplateInstance complex(
User user,
List<Order> orders,
Map<String, String> metadata,
int pageNumber
);
// Varargs
public static native TemplateInstance tags(String... tags);
}
}@TemplateData marks a target type for automatic value resolver generation. This annotation is repeatable, allowing multiple configurations for the same class or targeting different classes.
IMPORTANT: This annotation only works in fully integrated environments like Quarkus applications.
/**
* Configures value resolver generation for a type.
* Controls property and method accessibility in templates.
* Non-public members, constructors, static initializers, static, synthetic and void methods are always ignored.
*/
@Retention(RUNTIME)
@Target(TYPE)
@Repeatable(Container.class)
@interface TemplateData {
/**
* Constant for namespace indicating underscored FQCN should be used.
*/
String UNDERSCORED_FQCN = "<<undescored fqcn>>";
/**
* Constant for namespace indicating simple name should be used.
*/
String SIMPLENAME = "<<simplename>>";
/**
* Target class for which to generate value resolver.
* Default: TemplateData.class (meaning the annotated class itself)
*/
Class<?> target() default TemplateData.class;
/**
* Regular expressions to match members that should be ignored.
* Use patterns to exclude specific fields or methods from template access.
*/
String[] ignore() default {};
/**
* If true, do not automatically analyze superclasses.
* Default: false (superclass members are included)
*/
boolean ignoreSuperclasses() default false;
/**
* If true, include only properties: instance fields and methods without params.
* Excludes methods with parameters from template access.
* Default: false (all public methods accessible)
*/
boolean properties() default false;
/**
* Namespace for accessing static fields and methods in templates.
* If set to non-empty value, a namespace resolver is automatically generated.
* Default: UNDERSCORED_FQCN (e.g., org.acme.Foo becomes org_acme_Foo)
* Special values: UNDERSCORED_FQCN, SIMPLENAME
* Note: Namespace can only contain alphanumeric characters and underscores.
*/
String namespace() default UNDERSCORED_FQCN;
/**
* Container annotation for repeatable @TemplateData.
*/
@Retention(RUNTIME)
@Target(TYPE)
@interface Container {
TemplateData[] value();
}
}Usage Examples:
import io.quarkus.qute.TemplateData;
// Example 1: Basic usage - expose all public members
@TemplateData
public class Product {
private String name;
private BigDecimal price;
private int stock;
public String getName() { return name; }
public BigDecimal getPrice() { return price; }
public int getStock() { return stock; }
public boolean isAvailable() { return stock > 0; }
}
// In template: {product.name}, {product.price}, {product.available}
// Example 2: Ignore specific members using regex
@TemplateData(ignore = {"password", "internal.*"})
public class User {
private String name;
private String email;
private String password; // Ignored - exact match
private Long internalId; // Ignored - pattern match
private String internalCode; // Ignored - pattern match
// Getters...
}
// In template: {user.name}, {user.email} accessible
// {user.password}, {user.internalId} not accessible
// Example 3: Properties-only mode (no methods with parameters)
@TemplateData(properties = true)
public class Item {
private String name;
public String getName() {
return name; // Accessible - no params
}
public String format(String pattern) {
return String.format(pattern, name); // NOT accessible - has param
}
}
// In template: {item.name} works, {item.format('...')} fails
// Example 4: Namespace resolver for static methods
@TemplateData(namespace = "math")
public class MathUtils {
public static int abs(int value) {
return Math.abs(value);
}
public static double round(double value) {
return Math.round(value);
}
public static final double PI = Math.PI;
}
// In template: {math:abs(-5)}, {math:round(3.7)}, {math:PI}
// Example 5: Namespace with simple name
@TemplateData(namespace = TemplateData.SIMPLENAME)
public class StringHelper {
public static String upper(String s) {
return s.toUpperCase();
}
}
// In template: {StringHelper:upper('hello')}
// Example 6: Ignore superclass members
@TemplateData(ignoreSuperclasses = true)
public class SpecialUser extends User {
private String specialField;
// Only SpecialUser members accessible, User members ignored
}
// Example 7: Multiple @TemplateData annotations
@TemplateData // Instance resolver for the class itself
@TemplateData(target = OtherClass.class) // Generate resolver for OtherClass too
public class MyClass {
// Members
}
// Example 8: Target a different class
@TemplateData(target = ThirdPartyClass.class, ignore = {"sensitiveMethod"})
public class Adapters {
// This annotation generates resolver for ThirdPartyClass
}Namespace Resolver Behavior:
When namespace is set to a non-empty value:
{namespace:memberName} or {namespace:methodName(args)}SIMPLENAME constant for shorter namespace (just class name)Build-Time Processing:
@TemplateData triggers build-time value resolver generation:
properties = true)ignore patterns to exclude members@TemplateGlobal defines global variables accessible in all templates without explicitly passing data. Can be applied to individual static fields/methods or to entire classes.
IMPORTANT: This annotation only works in fully integrated environments like Quarkus applications.
/**
* Denotes static fields and methods that supply global variables accessible in every template.
*
* When applied to a class: Every non-void non-private static method with no parameters
* and every non-private static field becomes a global variable.
*
* Global variable method requirements:
* - Must not be private
* - Must be static
* - Must not accept any parameters
* - Must not return void
*
* Global variable field requirements:
* - Must not be private
* - Must be static
*/
@Retention(RUNTIME)
@Target({ TYPE, METHOD, FIELD })
@interface TemplateGlobal {
/**
* Constant indicating element name should be used.
*/
String ELEMENT_NAME = "<<element name>>";
/**
* Variable name in templates.
* Default: ELEMENT_NAME (use field/method name)
*/
String name() default ELEMENT_NAME;
}Usage Examples:
import io.quarkus.qute.TemplateGlobal;
import java.time.LocalDate;
import java.time.LocalDateTime;
// Example 1: Individual field annotation
public class AppConfig {
@TemplateGlobal
public static final String APP_NAME = "MyApp";
@TemplateGlobal
public static final String VERSION = "1.0.0";
private static final String SECRET = "xyz"; // Not annotated, not global
}
// Example 2: Individual method annotation
public class GlobalMethods {
@TemplateGlobal
public static String currentYear() {
return String.valueOf(LocalDate.now().getYear());
}
@TemplateGlobal(name = "now")
public static LocalDateTime currentDateTime() {
return LocalDateTime.now();
}
// Not annotated - not a global variable
public static String helper(String input) {
return input.toUpperCase();
}
}
// Example 3: Class-level annotation (all qualifying members become globals)
@TemplateGlobal
public class Globals {
// Field - becomes global variable "APP_NAME"
public static final String APP_NAME = "MyApp";
// Field - becomes global variable "VERSION"
public static final String VERSION = "2.0.0";
// Method with no params - becomes global variable "currentYear"
public static int currentYear() {
return LocalDate.now().getYear();
}
// Method with no params - becomes global variable "serverTime"
public static LocalDateTime serverTime() {
return LocalDateTime.now();
}
// Private field - NOT a global variable (excluded)
private static final String SECRET = "xyz";
// Method with parameters - NOT a global variable (excluded)
public static String format(String pattern) {
return pattern;
}
// Void method - NOT a global variable (excluded)
public static void doSomething() {
// ...
}
}
// Example 4: Custom names for global variables
public class NamedGlobals {
@TemplateGlobal(name = "version")
public static String appVersion() {
return "1.0.0";
}
@TemplateGlobal(name = "env")
public static String environment() {
return System.getenv("APP_ENV");
}
@TemplateGlobal(name = "debug")
public static final boolean DEBUG_MODE = true;
}Template Usage:
<!-- Access global variables without data() call -->
<footer>
© {currentYear} {APP_NAME} - Version {version}
<span>Environment: {env}</span>
{#if debug}
<div class="debug-info">Debug mode enabled</div>
{/if}
</footer>Best Practices:
@TemplateEnum generates a namespace resolver for enum types, making all enum constants accessible in templates.
/**
* Generate namespace resolver for enum type.
* Makes enum constants accessible via namespace.
*/
@Retention(RUNTIME)
@Target(TYPE)
@interface TemplateEnum {
// No attributes - uses simple name as namespace
}Usage Examples:
import io.quarkus.qute.TemplateEnum;
@TemplateEnum
public enum Status {
ACTIVE,
INACTIVE,
PENDING
}
@TemplateEnum
public enum Priority {
LOW,
MEDIUM,
HIGH
}Template usage:
{#if user.status == Status:ACTIVE}
User is active
{/if}
{#when task.priority}
{#is Priority:HIGH}
<span class="high">High Priority</span>
{#is Priority:MEDIUM}
<span class="medium">Medium Priority</span>
{#is Priority:LOW}
<span class="low">Low Priority</span>
{/when}Java usage:
// Enum constants accessible as namespace
template.data("expectedStatus", Status.ACTIVE).render();@TemplateContents allows you to specify template contents inline instead of in separate template files. This is useful for small templates or when you want to keep template code with the Java code that uses it.
IMPORTANT: This annotation only works in fully integrated environments like Quarkus applications.
/**
* Specifies inline contents for a type-safe template.
* Can be applied to @CheckedTemplate methods or template records.
*/
@Retention(RUNTIME)
@Target({ TYPE, METHOD })
@interface TemplateContents {
/**
* The template content string.
*/
String value();
/**
* File suffix to determine content type for template variants.
* Default: "txt" (text/plain)
* Use "html" for text/html content type.
*/
String suffix() default "txt";
}Usage Examples:
import io.quarkus.qute.CheckedTemplate;
import io.quarkus.qute.TemplateContents;
import io.quarkus.qute.TemplateInstance;
// Example 1: Inline template for method
public class MessageResource {
@CheckedTemplate
public static class Templates {
@TemplateContents(value = "<h1>Hello {name}!</h1>", suffix = "html")
public static native TemplateInstance greeting(String name);
@TemplateContents("Welcome, {user.name}. You have {count} messages.")
public static native TemplateInstance welcome(User user, int count);
}
public TemplateInstance getGreeting(String name) {
return Templates.greeting(name);
}
}
// Example 2: Inline template for record
@CheckedTemplate
@TemplateContents(value = "<div class=\"alert\">{message}</div>", suffix = "html")
public record AlertMessage(String message) implements TemplateInstance {
}
// Usage
public TemplateInstance showAlert() {
return new AlertMessage("Operation completed successfully");
}
// Example 3: Plain text template (default)
@CheckedTemplate
public static class Templates {
@TemplateContents("Hello {name}, your order #{orderId} is ready.")
public static native TemplateInstance orderReady(String name, String orderId);
}Benefits:
When to use:
// Pattern 1: One Templates class per resource
@Path("/products")
public class ProductResource {
@CheckedTemplate
public static class Templates {
public static native TemplateInstance list(List<Product> products);
public static native TemplateInstance detail(Product product);
public static native TemplateInstance edit(Product product);
}
}
// Pattern 2: Shared templates class
@CheckedTemplate(basePath = "shared")
public class SharedTemplates {
public static native TemplateInstance header(User user);
public static native TemplateInstance footer();
public static native TemplateInstance error(String message);
}
// Pattern 3: Feature-based organization
@CheckedTemplate(basePath = "orders")
public class OrderTemplates {
public static native TemplateInstance confirmation(Order order);
public static native TemplateInstance invoice(Order order);
public static native TemplateInstance shipping(Order order, Address address);
}// Ensure template parameter declarations match method signatures
@CheckedTemplate
public static class Templates {
// Method signature
public static native TemplateInstance user(User user, boolean showEmail);
}
// Corresponding template (templates/MyClass/user.html)
// Must have matching parameter declarations:
// {@org.acme.User user}
// {@boolean showEmail}@CheckedTemplate
public static class Templates {
// Use Optional for truly optional parameters
public static native TemplateInstance profile(User user, Optional<String> message);
}
// Template: templates/profile.html
// {@org.acme.User user}
// {@java.util.Optional<java.lang.String> message}
// {#if message.isPresent}{message.get}{/if}@CheckedTemplate
public static class Templates {
// Main template
public static native TemplateInstance products(List<Product> products);
// Fragment: "item" in products template
public static native TemplateInstance products$item(Product product);
// Fragment: "header" in products template
public static native TemplateInstance products$header(String title);
// Fragment: "footer" in products template
public static native TemplateInstance products$footer(int total);
}
// Template file: templates/products.html
// {#fragment item}...{/fragment}
// {#fragment header}...{/fragment}
// {#fragment footer}...{/fragment}@CheckedTemplate
public static class Templates {
public static native TemplateInstance user(User user);
}
public TemplateInstance getUser(Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new NotFoundException("User not found"));
try {
return Templates.user(user);
} catch (TemplateException e) {
logger.error("Failed to render user template", e);
return Templates.error(e.getMessage());
}
}@QuarkusTest
public class TemplateTest {
@Test
public void testUserTemplate() {
User testUser = new User("John", "john@example.com");
TemplateInstance instance = Templates.user(testUser);
String result = instance.render();
assertNotNull(result);
assertTrue(result.contains("John"));
assertTrue(result.contains("john@example.com"));
}
@Test
public void testFragmentRendering() {
Product product = new Product("Widget", 19.99);
TemplateInstance fragment = Templates.products$item(product);
String result = fragment.render();
assertTrue(result.contains("Widget"));
assertTrue(result.contains("19.99"));
}
}Error: Template not found: templates/MyClass/myMethod.html
Solutions:
src/main/resources/templates/Error: Parameter mismatch in template
Solutions:
{@org.acme.User user}Error: Expression validation failed: property not found
Solutions:
@TemplateData to the classquarkus.qute.type-check-excludes if dynamicrequireTypeSafeExpressions = false for legacy templatesError: Fragment "xyz" not found in template
Solutions:
{#fragment xyz}...{/fragment}$ignoreFragments = false (default)@CheckedTemplate(defaultName = CheckedTemplate.HYPHENATED_ELEMENT_NAME)
public static class Templates {
// Method: userProfile
// File: templates/MyClass/user-profile.html
public static native TemplateInstance userProfile(User user);
// Method: adminDashBoard (capital B)
// File: templates/MyClass/admin-dash-board.html
public static native TemplateInstance adminDashBoard();
}@CheckedTemplate
public static class Templates {
// Generic type information preserved at compile time
public static native TemplateInstance process(List<String> strings);
// Multiple generics
public static native TemplateInstance map(Map<String, List<Item>> data);
}
// Template must declare full generic types:
// {@java.util.List<java.lang.String> strings}
// {@java.util.Map<java.lang.String, java.util.List<org.acme.Item>> data}@CheckedTemplate
public static class Templates {
// Normal fragment
public static native TemplateInstance page$header(String title);
// Multiple dollar signs - only first is fragment separator
public static native TemplateInstance page$fragment$with$dollars(String data);
// Template: templates/page.html
// Fragment ID: "fragment$with$dollars"
}
@CheckedTemplate(ignoreFragments = true)
public static class Templates {
// Dollar sign in method name, but NOT a fragment
public static native TemplateInstance export$csv(Data data);
// Template: templates/export$csv.html
}@CheckedTemplate
public static class Templates {
// No parameters - template has no declared parameters
public static native TemplateInstance empty();
// Template can still use global variables
}
// Template: templates/empty.html
// No {@...} declarations needed
// Can still access: {APP_NAME}, {currentYear}, etc.@CheckedTemplate
public static class Templates {
// Varargs parameter
public static native TemplateInstance tags(String... tags);
}
// Template declaration:
// {@java.lang.String[] tags} // Note: varargs become array
// Usage in Java:
Templates.tags("java", "quarkus", "qute");
Templates.tags(new String[]{"a", "b", "c"});
Templates.tags(); // Empty array@CheckedTemplate
public static class Templates {
public static native TemplateInstance numbers(int count, long total, boolean flag);
}
// Template declarations (primitives, not wrappers):
// {@int count}
// {@long total}
// {@boolean flag}
// In template, autoboxing applies:
// {count.compareTo(10)} // Works, int autoboxed to Integer@CheckedTemplate
public static class Templates {
public static native TemplateInstance display(User user);
}
// Passing null is allowed, but template must handle it:
Templates.display(null).render();
// Template should use null-safe operators:
// {user ?: 'No user'}
// {#if user}{user.name}{/if}
// {user.name ?: 'Anonymous'}@CheckedTemplate
public record ValidatedEmail(
@NotNull @Email String recipient,
@NotNull String subject,
@NotNull String body
) implements TemplateInstance {
public ValidatedEmail {
// Compact constructor for validation
Objects.requireNonNull(recipient, "recipient cannot be null");
Objects.requireNonNull(subject, "subject cannot be null");
Objects.requireNonNull(body, "body cannot be null");
}
}
// Template: templates/ValidatedEmail.html
// {@java.lang.String recipient}
// {@java.lang.String subject}
// {@java.lang.String body}// Nested class template path calculation:
package com.example;
public class MyController {
@CheckedTemplate
public static class Templates {
public static native TemplateInstance index();
}
}
// Template path: templates/MyController/index.html
// Top-level class:
@CheckedTemplate
public class TopTemplates {
public static native TemplateInstance index();
}
// Template path: templates/index.html
// With custom base path:
@CheckedTemplate(basePath = "admin/reports")
public static class Templates {
public static native TemplateInstance summary();
}
// Template path: templates/admin/reports/summary.html@CheckedTemplate(requireTypeSafeExpressions = true) // Default
public static class StrictTemplates {
// Build fails if template has unvalidated expressions
public static native TemplateInstance strict(User user);
}
@CheckedTemplate(requireTypeSafeExpressions = false)
public static class LenientTemplates {
// Build succeeds even with unvalidated expressions
// Runtime validation only
public static native TemplateInstance lenient(Object data);
}