|
|
淡定的可乐 · 亚马逊海外购靠谱吗?为什么便宜?-西格跨境网· 1 年前 · |
|
|
神勇威武的香菜 · 九江雙蒸博物館門票【2025】- ...· 1 年前 · |
|
|
腹黑的松鼠 · 普陀区教育局关于印发《2025年普陀区高中阶 ...· 1 年前 · |
|
|
傻傻的薯片 · 「北極・夏日世界! ~迦勒底盛夏魔園觀光~」開幕!· 1 年前 · |
|
|
帅呆的海龟 · 盘点国内程序员不常用的热门iOS第三方库:看 ...· 1 年前 · |
Qute is a templating engine designed specifically to meet the Quarkus needs. The usage of reflection is minimized to reduce the size of native images. The API combines both the imperative and the non-blocking reactive style of coding. In the development mode, all files located in the
src/main/resources/templates
folder are watched for changes and modifications are immediately visible in your application. Furthermore, Qute attempts to detect most of the template problems at build time and fail fast.
In this guide, you will find an introductory example , the description of the core features and Quarkus integration details.
Qute is primarily designed as a Quarkus extension. It is possible to use it as a "standalone" library too. However, in such case some features are not available. In general, any feature mentioned under the Quarkus Integration section is missing. Find more information about the limitations and possibilities in the Qute Used as a Standalone Library section.
// => Hello Lucy! Qute.fmt("Hello {name} {surname ?: 'Default'}!", Map.of("name", "Andy")); (2) // => Hello Andy Default! Qute.fmt("<html>{header}</html>").contentType("text/html").data("header", "<h1>My header</h1>").render(); (3) // <html><h1>Header</h1></html> (4) Qute.fmt("I am {#if ok}happy{#else}sad{/if}!", Map.of("ok", true)); (5) // => I am happy! The empty expression
{}
is a placeholder that is replaced with an index-based array accessor, i.e.
{data[0]}
.
You can provide a data map instead.
A builder-like API is available for more complex formatting requirements.
Note that for a "text/html" template the special chars are replaced with html entities by default.
You can use any
building block
in the template. In this case, the
If Section
is used to render the appropriate part of the message based on the input data.
In this example, we would like to demonstrate the basic workflow when working with Qute templates. Let’s start with a simple "hello world" example. We will always need some template contents :
Then, we will need to parse the contents into a
template definition
Java object. A template definition is an instance of
io.quarkus.qute.Template
.
If using Qute "standalone" you’ll need to create an instance of
io.quarkus.qute.Engine
first. The
Engine
represents a central point for template management with dedicated configuration. Let’s use the convenient builder:
Engine engine = Engine.builder().addDefaults().build();
Template.data(String, Object)
is a convenient method that creates a template instance and sets the data in one step.
TemplateInstance.render()
triggers a synchronous rendering, i.e. the current thread is blocked until the rendering is finished. However, there are also asynchronous ways to trigger the rendering and consume the results. For example, there is the
TemplateInstance.renderAsync()
method that returns
CompletionStage<String>
or
TemplateInstance.createMulti()
that returns Mutiny’s
Multi<String>
.
A comment starts with the sequence
{!
and ends with the sequence
!}
, e.g.
{! This is a comment !}
. Can be multiline and may contain expressions and sections:
{! {#if true} !}
. The content of a comment is completely ignored when rendering the output.
An
expression
outputs an evaluated value. It consists of one or more parts. A part may represent simple properties:
{foo}
,
{item.name}
, and virtual methods:
{item.get(name)}
,
{name ?: 'John'}
. An expression may also start with a namespace:
{inject:colors}
.
A
section
may contain static text, expressions and nested sections:
{#if foo.active}{foo.name}{/if}
. The name in the closing tag is optional:
{#if active}ACTIVE!{/}
. A section can be empty:
{#myTag image=true /}
. Some sections support optional end tags, i.e. if the end tag is missing then the section ends where the parent section ends. A section may also declare nested section blocks:
{#if item.valid} Valid. {#else} Invalid. {/if}
and decide which block to render.
It is used to mark the content that should be rendered but
not parsed
. It starts with the sequence
{|
and ends with the sequence
|}
:
{| <script>if(true){alert('Qute is cute!')};</script> |}
, and could be multi-line.
By default, the parser removes standalone lines from the template output. A
standalone line
is a line that contains at least one section tag (e.g.
{#each}
and
{/each}
), parameter declaration (e.g.
{@org.acme.Foo foo}
) or comment but no expression and no non-whitespace character. In other words, a line that contains no section tag or a parameter declaration is
not
a standalone line. Likewise, a line that contains an
expression
or a
non-whitespace character
is
not
a standalone line.
An expression is evaluated and outputs the value. It has one or more parts, where each part represents either a property accessor (aka Field Access Expression) or a virtual method invocation (aka Method Invocation Expression).
When accessing the properties you can either use the dot notation or bracket notation. In the
object.property
(dot notation) syntax, the
property
must be a
valid identifier
. In the
object[property_name]
(bracket notation) syntax, the
property_name
has to be a non-null
literal
value.
An expression can start with an optional namespace followed by a colon (
:
). A valid namespace consist of alphanumeric characters and underscores. Namespace expressions are resolved differently - see also
Resolution
.
{name} (1)
{item.name} (2)
{item['name']} (3)
{global:colors} (4)
A part of an expression can be a virtual method in which case the name can be followed by a list of comma-separated parameters in parentheses. A parameter of a virtual method can be either a nested expression or a literal value. We call these methods "virtual" because they do not have to be backed by a real Java method. You can learn more about virtual methods in the following section .
{item.getLabels(1)} (1)
{name or 'John'} (2)
no namespace, two parts -
item
,
getLabels(1)
, the second part is a virtual method with name
getLabels
and params
1
infix notation that can be used for virtual methods with single parameter, translated to
name.or('John')
; no namespace, two parts -
name
,
or('John')
The first part of the expression is always resolved against the
current context object
. If no result is found for the first part it’s resolved against the parent context object (if available). For an expression that starts with a namespace the current context object is found using all the available
NamespaceResolver
s. For an expression that does not start with a namespace the current context object is
derived from the position
of the tag. All other parts of an expression are resolved using all
ValueResolver
s against the result of the previous resolution.
For example, expression
{name}
has no namespace and single part -
name
. The "name" will be resolved using all available value resolvers against the current context object. However, the expression
{global:colors}
has the namespace
global
and single part -
colors
. First, all available
NamespaceResolver
s will be used to find the current context object. And afterwards value resolvers will be used to resolve "colors" against the context object found.
If an expression does not specify a namespace, the
current context object
is derived from the position of the tag. By default, the current context object represents the data passed to the template instance. However, sections may change the current context object. A typical example is the
let
section that can be used to define named local variables:
{#let myParent=order.item.parent myPrice=order.price} (1)
<h1>{myParent.name}</h1>
<p>Price: {myPrice}</p>
{/let}
Elvis Operator
Outputs the default value if the previous part cannot be resolved or resolves to
null
.
{person.name ?: 'John'}
,
{person.name or 'John'}
,
{person.name.or('John')}
orEmpty
Outputs an empty list if the previous part cannot be resolved or resolves to
null
.
{pets.orEmpty.size}
outputs
0
if
pets
is not resolvable or
null
Ternary Operator
Shorthand for if-then-else statement. Unlike in If Section nested operators are not supported.
{item.isActive ? item.name : 'Inactive item'}
outputs the value of
item.name
if
item.isActive
resolves to
true
.
Logical AND Operator
Outputs
true
if both parts are not
falsy
as described in the
If Section
.
The parameter is only evaluated if needed.
{person.isActive && person.hasStyle}
Logical OR Operator
Outputs
true
if any of the parts is not
falsy
as described in the
If Section
.
The parameter is only evaluated if needed.
{person.isActive || person.hasStyle}
{person.name or 'John'}
is translated to
{person.name.or('John')}
and
{item.isActive ? item.name : 'Inactive item'}
is translated to
{item.isActive.ifTruthy(item.name).or('Inactive item')}
You can iterate over elements of an array with
Loop Section
. Moreover, it’s also possible to get the length of the specified array and access the elements directly via an index value. Additionally, you can access the first/last
n
elements via the
take(n)/takeLast(n)
methods.
<h1>Array of length: {myArray.length}</h1> (1)
<li>First: {myArray.0}</li> (2)
<li>Second: {myArray[1]}</li> (3)
<li>Third: {myArray.get(2)}</li> (4)
{#for element in myArray}
<li>{element}</li>
{/for}
First two elements: {#each myArray.take(2)}{it}{/each} (5)
In Quarkus, a variant is set automatically for templates located in the
src/main/resources/templates
. By default, the
java.net.URLConnection#getFileNameMap()
is used to determine the content-type of a template file. The additional map of suffixes to content types can be set via
quarkus.qute.content-types
.
Either use the
raw
or
safe
properties implemented as extension methods of the
java.lang.Object
,
Or wrap the
String
value in a
io.quarkus.qute.RawString
.
A virtual method is a part of an expression that looks like a regular Java method invocation. It’s called "virtual" because it does not have to match the actual method of a Java class. In fact, like normal properties a virtual method is also handled by a value resolver. The only difference is that for virtual methods a value resolver consumes parameters that are also expressions.
buildName(item.name,5)
represents a virtual method with name
buildName
and two parameters:
item.name
and
5
. The virtual method could be evaluated by a value resolver generated for the following Java class:
Include a template with idclass Item { String buildName(String name, int age) { return name + ":" + age;3.4.8. Evaluation of
CompletionStageandUniObjectsObjects that implement
java.util.concurrent.CompletionStageandio.smallrye.mutiny.Uniare evaluated in a special way. If a part of an expression resolves to aCompletionStage, the resolution continues once this stage is completed and the next part of the expression (if any) is evaluated against the result of the completed stage. For example, if there is an expression{foo.size}andfooresolves toCompletionStage<List<String>>thensizeis resolved against the completed result, i.e.List<String>. If a part of an expression resolves to aUni, aCompletionStageis first created fromUniusingUni#subscribeAsCompletionStage()and then evaluated as described above.It can happen that a
In previous versions, only theCompletionStagenever completes or aUniemits no item/failure. In this case, the rendering methods (such asTemplateInstance#render()andTemplateInstance#createUni()) fail after a specific timeout. The timeout can be specified as a template instancetimeoutattribute. If notimeoutattribute is set the global rendering timeout is used.TemplateInstance#render()method honored the timeout attribute. You can use theio.quarkus.qute.useAsyncTimeout=falseconfig property to preserve the old behavior and take care of the timeout yourself, for exampletemplateInstance.createUtni().ifNoItem().after(Duration.ofMillis(500)).fail().3.4.8.1. How to Identify a Problematic Part of the Template
It’s not easy to find the problematic part of a template when a timeout occurs. You can set the
TRACElevel for the loggerio.quarkus.qute.nodeResolveand try to analyze the log output afterwards.application.propertiesExamplequarkus.log.category."io.quarkus.qute.nodeResolve".min-level=TRACE quarkus.log.category."io.quarkus.qute.nodeResolve".level=TRACEYou should see the following pair of log messages for every expression and section used in a template:
TRACE [io.qua.qut.nodeResolve] Resolve {name} started: Template hello.html at line 8 TRACE [io.qua.qut.nodeResolve] Resolve {name} completed: Template hello.html at line 8If a
completedlog message is missing then you have a good candidate to explore.3.4.9. Missing Properties
It can happen that an expression may not be evaluated at runtime. For example, if there is an expression
{person.age}and there is no propertyagedeclared on thePersonclass. The behavior differs based on whether the Strict Rendering is enabled or not.If enabled then a missing property will always result in a
TemplateExceptionand the rendering is aborted. You can use default values and safe expressions in order to suppress the error.If disabled then the special constant
NOT_FOUNDis written to the output by default.3.5. Sections
A section has a start tag that starts with
#, followed by the name of the section such as{#if}and{#each}. It may be empty, i.e. the start tag ends with/:{#myEmptySection /}. Sections usually contain nested expressions and other sections. The end tag starts with/and contains the name of the section (optional):{#if foo}Foo!{/if}or{#if foo}Foo!{/}. Some sections support optional end tags, i.e. if the end tag is missing then the section ends where the parent section ends.#letOptional End Tag Example{#if item.isActive} {#let price = item.price} (1) {price} // synthetic {/let} added here automatically {/if} // {price} cannot be used here!3.5.1. Parameters
A start tag can define parameters with optional names, e.g.
{#if item.isActive}and{#let foo=1 bar=false}. Parameters are separated by one or more spaces. Names are separated from the values by the equals sign. Names and values can be prefixed and suffixed with any number of spaces, e.g.{#let id='Foo'}and{#let id = 'Foo'}are equivalents where the name of the parameter isidand the value isFoo. Values can be grouped using parentheses, e.g.{#let id=(item.id ?: 42)}where the name isidand the value isitem.id ?: 42. Sections can interpret parameter values in any way, e.g. take the value as is. However, in most cases, the parameter value is registered as an expression and evaluated before use.A section may contain several content blocks. The "main" block is always present. Additional/nested blocks also start with
#and can have parameters too -{#else if item.isActive}. A section helper that defines the logic of a section can "execute" any of the blocks and evaluate the parameters.#ifSection Example{#if item.name is 'sword'} It's a sword! (1) {#else if item.name is 'shield'} It's a shield! (2) {#else} Item is neither a sword nor a shield. (3) {/if}3.5.2. Loop Section
The loop section makes it possible to iterate over an instance of
Iterable,Iterator, array,Map(element is aMap.Entry),Stream,Integerandint(primitive value). Anullparameter value results in a no-op.This section has two flavors. The first one is using the name
eachanditis an implicit alias for the iteration element.{#each items} {it.name} (1) {/each}However, the keys cannot be used directly. Instead, a prefix is used to avoid possible collisions with variables from the outer scope. By default, the alias of an iterated element suffixed with an underscore is used as a prefix. For example, the
hasNextkey must be prefixed withit_inside an{#each}section:{it_hasNext}.eachIteration Metadata Example{#each items} {it_count}. {it.name} (1) {#if it_hasNext}<br>{/if} (2) {/each}And must be used in a form of
{item_hasNext}inside a{#for}section with theitemelement alias.forIteration Metadata Example{#for item in items} {item_count}. {item.name} (1) {#if item_hasNext}<br>{/if} (2) {/for}3.5.3. If Section
The
ifsection represents a basic control flow section. The simplest possible version accepts a single parameter and renders the content if the condition is evaluated totrue. A condition without an operator evaluates totrueif the value is not consideredfalsy, i.e. if the value is notnull,false, an empty collection, an empty map, an empty array, an empty string/char sequence or a number equal to zero.{#if item.active} This item is active. {/if}You can also use the following operators in a condition:
{#if (item.age > 10 || item.price > 500) && user.loggedIn} User must be logged in and item age must be > 10 or price must be > 500. {/if}You can also add any number of
elseblocks:{#if item.age > 10} This item is very old. {#else if item.age > 5} This item is quite old. {#else if item.age > 2} This item is old. {#else} This item is not old at all! {/if}3.5.4. When Section
This section is similar to Java’s
switchor Kotlin’swhenconstructs. It matches a tested value against all blocks sequentially until a condition is satisfied. The first matching block is executed. All other blocks are ignored (this behavior differs to the Javaswitchwhere abreakstatement is necessary).Example using thewhen/isname aliasesIt is possible to use an operator to specify the matching logic. Unlike in the If Section nested operators are not supported.{#when items.size} {#is 1} (1) There is exactly one item! {#is > 10} (2) There are more than 10 items! {#else} (3) There are 2 -10 items! {/when}elseis block is executed if no other block matches the value.The local variable is initialized with an expression that can also represent a literal, i.e.{#let myParent=order.item.parent isActive=false age=10 price=(order.price + 10)} (1)(2) <h1>{myParent.name}</h1> Is active: {isActive} Age: {age} {/let} (3)isActive=falseandage=10. The infix notation is only supported if parentheses are used for grouping, e.g.price=(order.price + 10)is equivalent toprice=order.price.plus(10). Keep in mind that the variable is not available outside theletsection that defines it.trueis effectively a default value that is only used if the parent scope does not defineenabledalready.enabled?=trueis a short version ofenabled=enabled.or(true).{#with item.callExpensiveLogicToGetTheValue(1,'foo',bazinga)} {#if this is "fun"} (1) <h1>Yay!</h1> {#else} <h1>{this} is not fun at all!</h1> {/if} {/with}3.5.7. Include Section
This section can be used to include another template and possibly override some parts of the template (see the template inheritance below).
Simple Example<meta charset="UTF-8"> <title>Simple Include</title> </head> {#include foo limit=10 /} (1)(2) </body> </html>
foo
. The included template can reference data from the current context.
It’s also possible to define optional parameters that can be used in the included template.
insert
sections are used to specify parts that could be overridden by a template that includes the given template.
An
insert
section may define the default content that is rendered if not overridden. If there is no name supplied then the main block of the relevant
{#include}
section is used.
Engine engine = Engine.builder()
.addSectionHelper(new UserTagSectionHelper.Factory("itemDetail","itemDetail.html"))
.build();
engine.putTemplate("itemDetail.html", engine.parse("..."));
Then, we can call the tag like this:
{#for item in items} {#itemDetail item showImage=true} (1) = <b>{item.name}</b> (2) {/itemDetail} {/for}
item
is resolved to an iteration element and can be referenced using the
it
key in the tag template.
Tag content injected using the
nested-content
key in the tag template.
By default, the tag template can reference data from the parent context. For example, the tag above could use the following expression
{items.size}
. However, sometimes it might be useful to disable this behavior and execute the tag as an
isolated
template, i.e. without access to the context of the template that calls the tag. In this case, just add
_isolated
or
_isolated=true
argument to the call site, e.g.
{#itemDetail item showImage=true _isolated /}
.
User tags can also make use of the template inheritance in the same way as regular
{#include}
sections do.
myTag
This is {#insert title}my title{/title}! (1)
A fragment represents a part of the template that can be treated as a separate template, i.e. rendered separately. One of the main motivations to introduce this feature was to support use cases like htmx fragments .
Fragments can be defined with the
{#fragment}
section. Each fragment has an identifier that can only consist of alphanumeric characters and underscores.
You can obtain a fragment programmatically via the
io.quarkus.qute.Template.getFragment(String)
method.
@Inject Template item; String useTheFragment() { return item.getFragment("item_aliases") (1) .data("aliases", List.of("Foo","Bar")) (2) .render();You can also include a fragment with an
{#include}section inside another template or the template that defines the fragment.Including a Fragment inuser.htmlA template identifier that contains a dollar sign<h1>User - {user.name}</h1> <p>This document contains a detailed info about a user.</p> {#include item$item_aliases aliases=user.aliases /} (1)(2)$denotes a fragment. Theitem$item_aliasesvalue is translated as: Use the fragmentitem_aliasesfrom the templateitem. Thealiasesparameter is used to pass the relevant data. We need to make sure that the data are set correctly. In this particular case the fragment will use the expressionuser.aliasesas the value ofaliasesin the{#for alias in aliases}section.3.5.9.1. Hidden Fragments
By default, a fragment is normally rendered as a part of the original template. However, sometimes it might be useful to mark a fragment as hidden with
rendered=false. An interesting use case would be a fragment that can be used multiple-times inside the template that defines it.Fragment Definition initem.html{#fragment id=strong rendered=false} (1) <strong>{val}</strong> {/fragment} <h1>My page</h1> <p>This document {#include $strong val='contains' /} (2) a lot of {#include $strong val='information' /} (3) Defines a hidden fragment with identifierstrong. In this particular case, we use thefalseboolean literal as the value of therenderedparameter. However, it’s possible to use any expression there. Include the fragmentstrongand pass the value. Note the syntax$strongwhich is translated to include the fragmentstrongfrom the current template. Include the fragmentstrongand pass the value.The template content is passed directly, i.e. not obtained via an
io.quarkus.qute.TemplateLocator,It’s not possible to override parts of the evaluated template.
The result ofmyData.templatewill be used as the template. The template is executed with theCurrent Context, i.e. can reference data from the template it’s included into. It’s also possible to define optional parameters that can be used in the evaluated template. The content of the section is always ignored.If the// A simple map-based cache ConcurrentMap<String, CompletionStage<ResultNode>> map = new ConcurrentHashMap<>(); engineBuilder .addSectionHelper(new CacheSectionHelper.Factory(new Cache() { @Override public CompletionStage<ResultNode> getValue(String key, Function<String, CompletionStage<ResultNode>> loader) { return map.computeIfAbsent(key, k -> loader.apply(k)); })).build();keyparam is not used then all clients of the template share the same cached value. This part of the template will be cached and the{service.findResult}expression is only evaluated when a cache entry is missing/invalidated.When using cache it’s very often important to have the option to invalidate a cache entry by the specific key. In Qute the key of a cache entry is a{#cached key=currentUser.username} (1) User-specific result: {service.findResult(currentUser)} {/cached}Stringthat consist of the template name, line and column of the starting{#cached}tag and the optionalkeyparameter:{TEMPLATE}:{LINE}:{COLUMN}_{KEY}. For example,foo.html:10:1_alphais a key for the cached section in a templatefoo.html, the{#cached}tag is placed on the line 10, column 1. And the optionalkeyparameter resolves toalpha.3.6. Rendering Output
TemplateInstanceprovides several ways to trigger the rendering and consume the result. The most straightforward approach is represented byTemplateInstance.render(). This method triggers a synchronous rendering, i.e. the current thread is blocked until the rendering is finished, and returns the output. By contrast,TemplateInstance.renderAsync()returns aCompletionStage<String>which is completed when the rendering is finished.TemplateInstance.renderAsync()Exampletemplate.data(foo).renderAsync().whenComplete((result, failure) -> { (1) if (failure == null) { // consume the output... } else { // process failure...There are also two methods that return Mutiny types.
TemplateInstance.createUni()returns a newUni<String>object. If you callcreateUni()the template is not rendered right away. Instead, every timeUni.subscribe()is called a new rendering of the template is triggered.TemplateInstance.createUni()Exampletemplate.data(foo).createUni().subscribe().with(System.out::println);
TemplateInstance.createMulti()returns a newMulti<String>object. Each item represents a part/chunk of the rendered template. Again,createMulti()does not trigger rendering. Instead, every time a computation is triggered by a subscriber the template is rendered again.TemplateInstance.createMulti()ExampleThe template rendering is divided in two phases. During the first phase, which is asynchronous, all expressions in the template are resolved and a result tree is built. In the second phase, which is synchronous, the result tree is materialized, i.e. one by one the result nodes emit chunks that are consumed/buffered by the specific consumer.template.data(foo).createMulti().subscribe().with(buffer:append,buffer::flush);3.7.1. Value Resolvers
Value resolvers are used when evaluating expressions. A custom
io.quarkus.qute.ValueResolvercan be registered programmatically viaEngineBuilder.addValueResolver().ValueResolverBuilder ExampleengineBuilder.addValueResolver(ValueResolver.builder() .appliesTo(ctx -> ctx.getBase() instanceof Long && ctx.getName().equals("tenTimes")) .resolveSync(ctx -> (Long) ctx.getBase() * 10) .build());3.7.2. Template Locator
A template can be either registered manually or automatically via a template locator. The locators are used whenever the
Engine.getTemplate()method is called, and the engine has no template for a given id stored in the cache. The locator is responsible for using the correct character encoding when reading the contents of a template.3.7.3. Content Filters
Content filters can be used to modify the template contents before parsing.
Content Filter ExampleengineBuilder.addParserHook(new ParserHook() { @Override public void beforeParsing(ParserHelper parserHelper) { parserHelper.addContentFilter(contents -> contents.replace("${", "$\\{")); (1)3.7.4. Strict Rendering
The strict rendering enables the developers to catch insidious errors caused by typos and invalid expressions. If enabled then any expression that cannot be resolved, i.e. is evaluated to an instance of
io.quarkus.qute.Results.NotFound, will always result in aTemplateExceptionand the rendering is aborted. ANotFoundvalue is considered an error because it basically means that no value resolver was able to resolve the expression correctly.If you really need to use an expression which can potentially lead to a "not found" error, you can use default values and safe expressions in order to suppress the error. A default value is used if the previous part of an expression cannot be resolved or resolves to
null. You can use the elvis operator to output the default value:{foo.bar ?: 'baz'}, which is effectively the same as the following virtual method:{foo.bar.or('baz')}. A safe expression ends with the??suffix and results innullif the expression cannot be resolved. It can be very useful e.g. in{#if}sections:{#if valueNotFound??}Only rendered if valueNotFound is truthy!{/if}. In fact,??is just a shorthand notation for.or(null), i.e.{#if valueNotFound??}becomes{#if valueNotFound.or(null)}.In Quarkus, a preconfigured engine instance is provided and available for injection - a bean with scope
@ApplicationScoped, bean typeio.quarkus.qute.Engineand qualifier@Defaultis registered automatically. Moreover, all templates located in thesrc/main/resources/templatesdirectory are validated and can be easily injected.import io.quarkus.qute.Engine; import io.quarkus.qute.Template; import io.quarkus.qute.Location; class MyBean { @Inject Template items; (1) @Location("detail/items2_v1.html") (2) Template items2; @Inject Engine engine; (3) If there is noLocationqualifier provided, the field name is used to locate the template. In this particular case, the container will attempt to locate a template with pathsrc/main/resources/templates/items.html. TheLocationqualifier instructs the container to inject a template from a path relative fromsrc/main/resources/templates. In this case, the full path issrc/main/resources/templates/detail/items2_v1.html. Inject the configuredEngineinstance. void configureEngine(@Observes EngineBuilder builder) { // Add a custom section helper builder.addSectionHelper(new CustomSectionFactory()); // Add a custom value resolver builder.addValueResolver(ValueResolver.builder() .appliesTo(ctx -> ctx.getBase() instanceof Long && ctx.getName().equals("tenTimes")) .resolveSync(ctx -> (Long) ec.getBase() * 10) .build());However, in this particular case the section helper factory is ignored during validation at build time. If you want to register a section that participates in validation of templates at build time then use the convenient
@EngineConfigurationannotation:import io.quarkus.qute.EngineConfiguration; import io.quarkus.qute.SectionHelper; import io.quarkus.qute.SectionHelperFactory; @EngineConfiguration (1) public class CustomSectionFactory implements SectionHelperFactory<CustomSectionFactory.CustomSectionHelper> { @Inject Service service; (2) @Override public List<String> getDefaultAliases() { return List.of("custom"); @Override public ParametersInfo getParameters() { // Param "foo" is required return ParametersInfo.builder().addParameter("foo").build(); (3) @Override public Scope initializeBlock(Scope outerScope, BlockInfo block) { block.addExpression("foo", block.getParameter("foo")); return outerScope; @Override public CustomSectionHelper initialize(SectionInitContext context) { return new CustomSectionHelper(); class CustomSectionHelper implements SectionHelper { private final Expression foo; public CustomSectionHelper(Expression foo) { this.foo = foo; @Override public CompletionStage<ResultNode> resolve(SectionResolutionContext context) { return context.evaluate(foo).thenApply(fooVal -> new SingleResultNode(service.getValueForFoo(fooVal))); (4) ASectionHelperFactoryannotated with@EngineConfigurationis used during validation of templates at build time and automatically registered at runtime (a) as a section factory and (b) as a CDI bean. A CDI bean instance is used at runtime - this means that the factory can define injection points Validate thatfooparameter is always present; e.g.{#custom foo='bar' /}is ok but{#custom /}results in a build failure. Use the injectedServiceduring rendering.The
@EngineConfigurationannotation can be also used to registerValueResolvers andNamespaceResolvers.4.1.1. Template Locator Registration
The easiest way to register template locators is to make them CDI beans. As the custom locator is not available during the build time when a template validation is done, you need to disable the validation via the
@Locateannotation.Custom Locator Example@Locate("bar.html") (1) @Locate("foo.*") (2) public class CustomLocator implements TemplateLocator { @Inject (3) MyLocationService myLocationService; @Override public Optional<TemplateLocation> locate(String templateId) { return myLocationService.getTemplateLocation(templateId); A regular expressionfoo.*disables validation for templates whose name is starting withfoo. Injection fields are resolved as template locators annotated with@Locateare registered as singleton session beans. String renderItems() { return items.data("items",manager.findItems()).setAttribute(TemplateInstance.SELECTED_VARIANT, new Variant(Locale.getDefault(),"text/html","UTF-8")).render();For the expression
cdi:personService.findPerson(10).name, the implementation class of the injected bean must either declare thefindPersonmethod or a matching template extension method must exist.For the expression
inject:foo.price, the implementation class of the injected bean must either have thepriceproperty (e.g. agetPrice()method) or a matching template extension method must exist.4.4. Type-safe Expressions
Template expressions can be optionally type-safe. Which means that an expression is validated against the existing Java types and template extension methods. If an invalid/incorrect expression is found then the build fails.
For example, if there is an expression
item.namewhereitemmaps toorg.acme.ItemthenItemmust have a propertynameor a matching template extension method must exist.An optional parameter declaration is used to bind a Java type to expressions whose first part matches the parameter name. Parameter declarations are specified directly in a template.
A Java type should be always identified with a fully qualified name unless it’s a JDK type from the
java.langpackage - in this case, the package name is optional. Parameterized types are supported, however wildcards are always ignored - only the upper/lower bound is taken into account. For example, the parameter declaration{@java.util.List<? extends org.acme.Foo> list}is recognized as{@java.util.List<org.acme.Foo> list}. Type variables are not handled in a special way and should never be used.Parameter Declaration ExampleThis expression is validated.{@org.acme.Foo foo} (1) <!DOCTYPE html> <meta charset="UTF-8"> <title>Qute Hello</title> </head> <h1>{title}</h1> (2) Hello {foo.message.toLowerCase}! (3) (4) </body> </html>org.acme.Foomust have a propertymessageor a matching template extension method must exist. Likewise, the Java type of the object resolved fromfoo.messagemust have a propertytoLowerCaseor a matching template extension method must exist.A parameter declaration may specify the default value after the key. The key and the default value are separated by an equals sign:
{@int age=10}. The default value is used in the template if the parameter key resolves tonullor is not found.For example, if there’s a parameter declaration
{@String foo="Ping"}andfoois not found then you can use{foo}and the output will bePing. On the other hand, if the value is set (e.g. viaTemplateInstance.data("foo", "Pong")) then the output of{foo}will bePong.The type of a default value must be assignable to the type of the parameter declaration. For example, see the incorrect parameter declaration that results in a build failure:
The default value is actually an expression. So the default value does not have to be a literal (such as{@org.acme.Foo foo=1}.42ortrue). For example, you can leverage the@TemplateEnumand specify an enum constant as a default value of a parameter declaration:{@org.acme.MyEnum myEnum=MyEnum:FOO}. However, the infix notation is not supported in default values unless the parentheses are used for grouping, e.g.{@org.acme.Foo foo=(foo1 ?: foo2)}.The wildcard is ignored and the upper bound is used instead:{@int pages} (1) {@java.util.List<String> strings} (2) {@java.util.Map<String,? extends Number> numbers} (3) {@java.util.Optional<?> param} (4) {@String name="Quarkus"} (5){@java.util.Map<String,Number>}The wildcard is ignored and thejava.lang.Objectis used instead:{@java.util.Optional<java.lang.Object>}The type isjava.lang.String, the key isnameand the default value isQuarkus.Organise your template files in the
/src/main/resources/templatesdirectory, by grouping them into one directory per resource class. So, if yourItemResourceclass references two templateshelloandgoodbye, place them at/src/main/resources/templates/ItemResource/hello.txtand/src/main/resources/templates/ItemResource/goodbye.txt. Grouping templates per resource class makes it easier to navigate to them.In each of your resource class, declare a
@CheckedTemplate static class Template {}class within your resource class.Declare one
public static native TemplateInstance method();per template file for your resource.Use those static methods to build your template instances.
import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; import io.quarkus.qute.TemplateInstance; import io.quarkus.qute.Template; import io.quarkus.qute.CheckedTemplate; @Path("item") public class ItemResource { @CheckedTemplate public static class Templates { public static native TemplateInstance item(Item item); (1) (2) @Path("{id}") @Produces(MediaType.TEXT_HTML) public TemplateInstance get(Integer id) { return Templates.item(service.findItem(id)); (3) Declare a method that gives us aTemplateInstancefortemplates/ItemResource/item.htmland declare itsItem itemparameter so we can validate the template. Theitemparameter is automatically turned into a parameter declaration and so all expressions that reference this name will be validated. Make theItemobject accessible in the template.4.5.1. Top-level Type-safe Templates
You can also declare a top-level Java class annotated with
@CheckedTemplate:Top-level checked templatespackage org.acme.quarkus.sample; import io.quarkus.qute.TemplateInstance; import io.quarkus.qute.Template; import io.quarkus.qute.CheckedTemplate; @CheckedTemplate public class Templates { public static native TemplateInstance hello(String name); (1)Then declare one
public static native TemplateInstance method();per template file. Use those static methods to build your template instances:HelloResource.javapackage org.acme.quarkus.sample; import jakarta.inject.Inject; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; import io.quarkus.qute.TemplateInstance; @Path("hello") public class HelloResource { @Produces(MediaType.TEXT_PLAIN) public TemplateInstance get(@QueryParam("name") String name) { return Templates.hello(name);4.5.2. Customized Template Path
The template path of a
@CheckedTemplatemethod consists of the base path and a defaulted name. The base path is supplied by the@CheckedTemplate#basePath(). By default, the simple name of the declaring class for a nested static class or an empty string for a top level class is used. The defaulted name is derived by the strategy specified in@CheckedTemplate#defaultName(). By default, the name of the@CheckedTemplatemethod is used as is.Customized Template Path Examplepackage org.acme.quarkus.sample; import jakarta.ws.rs.Path; import io.quarkus.qute.TemplateInstance; import io.quarkus.qute.CheckedTemplate; @Path("item") public class ItemResource { @CheckedTemplate(basePath = "items", defaultName = CheckedTemplate.HYPHENATED_ELEMENT_NAME) static class Templates { static native TemplateInstance itemAndOrder(Item item); (1)4.5.3. Type-safe Fragments
You can also define a type-safe fragment in your Java code. A native static method with the name that contains a dollar sign
$denotes a method that represents a fragment of a type-safe template. The name of the fragment is derived from the annotated method name. The part before the last occurence of a dollar sign$is the method name of the related type-safe template. The part after the last occurence of a dollar sign is the fragment identifier. The strategy defined by the relevantCheckedTemplate#defaultName()is honored when constructing the defaulted names.Type-safe Fragment Exampleimport io.quarkus.qute.CheckedTemplate; import org.acme.Item; @CheckedTemplate class Templates { // defines a type-safe template static native TemplateInstance items(List<Item> items); // defines a fragment of Templates#items() with identifier "item" static native TemplateInstance items$item(Item item); (1) Quarkus validates at build time that each template that corresponds to theTemplates#items()contains a fragment with identifieritem. Moreover, the parameters of the fragment method are validated too. In general, all type-safe expressions that are found in the fragment and that reference some data from the original/outer template require a specific parameter to be present. String renderItem(Item item) { // this would return something like "<li>Foo</li>" return Templates.items$item(item).render();4.6. Template Extension Methods
Extension methods can be used to extend the data classes with new functionality (to extend the set of accessible properties and methods) or to resolve expressions for a specific namespace. For example, it is possible to add computed properties and virtual methods.
A value resolver is automatically generated for a method annotated with
@TemplateExtension. If a class is annotated with@TemplateExtensionthen a value resolver is generated for every non-private static method declared on the class. Method-level annotations override the behavior defined on the class. Methods that do not meet the following requirements are ignored.A template extension method:
If there is no namespace defined the class of the first parameter that is not annotated with
@TemplateAttributeis used to match the base object. Otherwise, the namespace is used to match an expression.4.6.1. Matching by Name
The method name is used to match the property name by default.
Extension Method Examplepackage org.acme; class Item { public final BigDecimal price; public Item(BigDecimal price) { this.price = price; @TemplateExtension class MyExtensions { static BigDecimal discountedPrice(Item item) { (1) return item.getPrice().multiply(new BigDecimal("0.9"));@TemplateExtension(matchName = "discounted") static BigDecimal discountedPrice(Item item) { // this method matches {item.discounted} if "item" resolves to an object assignable to "Item" return item.getPrice().multiply(new BigDecimal("0.9"));A special constant -
TemplateExtension#ANY/*- can be used to specify that the extension method matches any name.TemplateExtension#ANYExample@TemplateExtension(matchName = "*") static String itemProperty(Item item, String name) { (1) // this method matches {item.foo} if "item" resolves to an object assignable to "Item" // the value of the "name" argument is "foo"It’s also possible to match the name against a regular expression specified in
matchRegex().TemplateExtension#matchRegex()Example@TemplateExtension(matchRegex = "foo|bar") static String itemProperty(Item item, String name) { (1) // this method matches {item.foo} and {item.bar} if "item" resolves to an object assignable to "Item" // the value of the "name" argument is "foo" or "bar"Finally,
matchNames()can be used to specify a collection of matching names. An additional string method parameter is mandatory as well.TemplateExtension#matchNames()Example@TemplateExtension(matchNames = {"foo", "bar"}) static String itemProperty(Item item, String name) { // this method matches {item.foo} and {item.bar} if "item" resolves to an object assignable to "Item" // the value of the "name" argument is "foo" or "bar"4.6.2. Method Parameters
An extension method may declare parameters. If no namespace is specified then the first parameter that is not annotated with
@TemplateAttributeis used to pass the base object, i.e.org.acme.Itemin the first example. If matching any name or using a regular expression, then a string method parameter needs to be used to pass the property name. Parameters annotated with@TemplateAttributeare obtained viaTemplateInstance#getAttribute(). All other parameters are resolved when rendering the template and passed to the extension method.Multiple Parameters Example@TemplateExtension class BigDecimalExtensions { static BigDecimal scale(BigDecimal val, int scale, RoundingMode mode) { (1) return val.setScale(scale, mode);4.6.3. Namespace Extension Methods
If
TemplateExtension#namespace()is specified then the extension method is used to resolve expressions with the given namespace. Template extension methods that share the same namespace are grouped in one resolver ordered byTemplateExtension#priority(). The first matching extension method is used to resolve an expression.Namespace Extension Method Example@TemplateExtension(namespace = "str") public class StringExtensions { static String format(String fmt, Object... args) { return String.format(fmt, args); static String reverse(String val) { return new StringBuilder(val).reverse().toString();These extension methods can be used as follows.
{str:format('%s %s!','Hello', 'world')} (1) {str:reverse('hello')} (2)4.7.1. Accessing Static Fields and Methods
If
public class Statuses { public static final String ON = "on"; public static final String OFF = "off";@TemplateData#namespace()is set to a non-empty value then a namespace resolver is automatically generated to access the public static fields and methods of the target class. By default, the namespace is the FQCN of the target class where dots and dollar signs are replaced by underscores. For example, the namespace for a class with nameorg.acme.Fooisorg_acme_Foo. The static fieldFoo.AGEcan be accessed via{org_acme_Foo:AGE}. The static methodFoo.computeValue(int number)can be accessed via{org_acme_Foo:computeValue(10)}.4.7.2. Convenient Annotation For Enums
There’s also a convenient annotation to access enum constants:
@io.quarkus.qute.TemplateEnum. This annotation is functionally equivalent to@TemplateData(namespace = TemplateData.SIMPLENAME), i.e. a namespace resolver is automatically generated for the target enum and the simple name of the target enum is used as the namespace.Enum Annotated With@TemplateEnumpackage model; @TemplateEnum (1) public enum Status {4.8. Global Variables
The
io.quarkus.qute.TemplateGlobalannotation can be used to denote static fields and methods that supply global variables which are accessible in any template. Internally, each global variable is added to the data map of anyTemplateInstancevia theTemplateInstance#data(String, Object)method.Global Variables Definitionenum Color { RED, GREEN, BLUE } @TemplateGlobal (1) public class Globals { static int age = 40; static Color[] myColors() { return new Color[] { Color.RED, Color.BLUE }; @TemplateGlobal(name = "currentUser") (2) static String user() { return "Mia"; If a class is annotated with@TemplateGlobalthen every non-void non-private static method that declares no parameters and every non-private static field is considered a global variable. The name is defaulted, i.e. the name of the field/method is used. Method-level annotations override the class-level annotation. In this particular case, the name is not defaulted but selected explicitly.4.8.1. Resolving Conflicts
Global variables may conflict with regular data objects. Type-safe templates override the global variables automatically. For example, the following definition overrides the global variable supplied by the
Globals#user()method:Type-safe Template Definitionimport org.acme.User; @CheckedTemplate public class Templates { static native TemplateInstance hello(User currentUser); (1)So the corresponding template does not result in a validation error even though the
Globals#user()method returnsjava.lang.Stringwhich does not have thenameproperty:templates/hello.txtUser name: {currentUser.name} (1)4.9. Native Executables
In the JVM mode a reflection-based value resolver may be used to access properties and call methods of the model classes. But this does not work for a native executable out of the box. As a result, you may encounter template exceptions like
Property "name" not found on the base object "org.acme.Foo" in expression {foo.name} in template hello.htmleven if theFooclass declares a relevant getter method.There are several ways to solve this problem:
Annotate the model class with
@TemplateData- a specialized value resolver is generated and used at runtimeAnnotate the model class with
@io.quarkus.runtime.annotations.RegisterForReflectionto make the reflection-based value resolver work<dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-resteasy-reactive-qute</artifactId> </dependency>Both of these extensions register a special
ContainerResponseFilterimplementation which enables resource methods to return aTemplateInstance, thus freeing users of having to take care of all necessary internal steps.The end result is that a using Qute within a Jakarta REST resource may look as simple as:
HelloResource.javapackage org.acme.quarkus.sample; import jakarta.inject.Inject; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; import io.quarkus.qute.TemplateInstance; import io.quarkus.qute.Template; @Path("hello") public class HelloResource { @Inject Template hello; (1) @Produces(MediaType.TEXT_PLAIN) public TemplateInstance get(@QueryParam("name") String name) { return hello.data("name", name); (2) (3) If there is no@Locationqualifier provided, the field name is used to locate the template. In this particular case, we’re injecting a template with pathtemplates/hello.txt.Template.data()returns a new template instance that can be customized before the actual rendering is triggered. In this case, we put the name value under the keyname. The data map is accessible during rendering. Note that we don’t trigger the rendering - this is done automatically by a specialContainerResponseFilterimplementation. @Produces({ MediaType.TEXT_HTML, MediaType.TEXT_PLAIN }) public TemplateInstance item() { return item.data("myItem", new Item("Alpha", 1000)); (2) Inject a variant template with base path derived from the injected field -src/main/resources/templates/item. Fortext/plainthesrc/main/resources/templates/item.txttemplate is used. Fortext/htmltheMETA-INF/resources/templates/item.htmltemplate is used.The
RestTemplateutil class can be used to obtain a template instance from a body of a Jakarta REST resource method:RestTemplate Example@Path("/detail") class DetailResource { @Produces({ MediaType.TEXT_HTML, MediaType.TEXT_PLAIN }) public TemplateInstance item() { return RestTemplate.data("myItem", new Item("Alpha", 1000)); (1)4.12.1. Basic Concepts
The basic idea is that every message is potentially a very simple template. In order to prevent type errors, a message is defined as an annotated method of a message bundle interface. Quarkus generates the message bundle implementation at build time.
Message Bundle Interface Exampleimport io.quarkus.qute.i18n.Message; import io.quarkus.qute.i18n.MessageBundle; @MessageBundle (1) public interface AppMessages { @Message("Hello {name}!") (2) String hello_name(String name); (3) Denotes a message bundle interface. The bundle name is defaulted tomsgand is used as a namespace in templates expressions, e.g.{msg:hello_name}. Each method must be annotated with@Message. The value is a qute template. If no value is provided, then a corresponding value from a localized file is taken. If no such file exists, an exception is thrown and the build fails. The method parameters can be used in the template.Directly in your code via
io.quarkus.qute.i18n.MessageBundles#get(); e.g.MessageBundles.get(AppMessages.class).hello_name("Lucie")Injected in your beans via
@Inject; e.g.@Inject AppMessagesReferenced in the templates via the message bundle namespace:
{msg:hello_name('Lucie')} (1) (2) (3) {msg:message(myKey,'Lu')} (4)4.12.2. Default Bundle Name
The bundle name is defaulted unless it’s specified with
@MessageBundle#value(). For a top-level class themsgvalue is used by default. For a nested class the name consists of the simple names of all enclosing classes in the hierarchy (top-level class goes first), followed by the simple name of the message bundle interface. Names are separated by underscores.For example, the name of the following message bundle will be defaulted to
Controller_index:class Controller { @MessageBundle interface index { @Message("Hello {name}!") String hello(String name); (1)4.12.3. Bundle Name and Message Keys
Message keys are used directly in templates. The bundle name is used as a namespace in template expressions. The
@MessageBundlecan be used to define the default strategy used to generate message keys from method names. However, the@Messagecan override this strategy and even define a custom key. By default, the annotated element’s name is used as-is. Other possibilities are:All expressions without a namespace must map to a parameter; e.g.
Hello {foo}→ the method must have a param of namefooAll expressions are validated against the types of the parameters; e.g.
Hello {foo.bar}where the parameterfoois of typeorg.acme.Foo→org.acme.Foomust have a property of namebarMessage bundle files must be encoded in UTF-8. The file name consists of the relevant bundle name (e.g.
msg) and underscore followed by a language tag (IETF; e.g.en-US). The language tag may be omitted, in which case the language tag of the default bundle locale is used. For example, if bundlemsghas default localeen, thenmsg.propertiesis going to be treated asmsg_en.properties. If bothmsg.propertiesandmsg_en.propertiesare detected, an exception is thrown and build fails. The file format is very simple: each line represents either a key/value pair with the equals sign used as a separator or a comment (line starts with#). Blank lines are ignored. Keys are mapped to method names from the corresponding message bundle interface. Values represent the templates normally defined byio.quarkus.qute.i18n.Message#value(). A value may be spread out across several adjacent normal lines. In such case, the line terminator must be escaped with a backslash character\. The behavior is very similar to the behavior of thejava.util.Properties.load(Reader)method.Localized File Example -msg_de.propertiesEach line in a localized file represents a key/value pair. The key must correspond to a method declared on the message bundle interface. The value is the message template. Keys and values are separated by the equals sign. An example properties file is generated into the target directory for each message bundle interface automatically. For example, by default if no name is specified for# This comment is ignored hello_name=Hallo {name}! (1) (2)@MessageBundlethe filetarget/qute-i18n-examples/msg.propertiesis generated when the application is build viamvn clean package. You can use this file as a base for a specific locale. Just rename the file - e.g.msg_fr.properties, change the message templates and move it in thesrc/main/resources/messagesdirectory.Note that the line terminator is escaped with a backslash character
\and white space at the start of the following line is ignored. I.e.{msg:hello('Edgar')}would be rendered asHello Edgar and good morning!.Once we have the localized bundles defined, we need a way to select the correct bundle for a specific template instance, i.e. to specify the locale for all message bundle expressions in the template. By default, the locale specified via the
quarkus.default-localeconfiguration property is used to select the bundle. Alternatively, you can specify thelocaleattribute of a template instance.localeAttribute Example@Singleton public class MyBean { @Inject Template hello; String render() { return hello.instance().setAttribute("locale", Locale.forLanguageTag("cs")).render(); (1)4.12.6. Message Templates
Every method of a message bundle interface must define a message template. The value is normally defined by
io.quarkus.qute.i18n.Message#value(), but for convenience, there is also an option to define the value in a localized file.Example of the Message Bundle Interface without the valueimport io.quarkus.qute.i18n.Message; import io.quarkus.qute.i18n.MessageBundle; @MessageBundle public interface AppMessages { @Message (1) String hello_name(String name); @Message("Goodbye {name}!") (2) String goodbye(String name); The annotation value is not defined. In such a case, the value from supplementary localized file is taken. The annotation value is defined and preferred to the value defined in the localized file.By default,
engine.getTemplate("foo")would result in several lookups:foo,foo.html,foo.txt, etc.