Qute Reference Guide
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. |
1. The Simplest Example
The easiest way to try Qute is to use the convenient io.quarkus.qute.Qute
class and call one of its fmt()
static methods that can be used to format
simple messages:
import io.quarkus.qute.Qute;
Qute.fmt("Hello {}!", "Lucy"); (1)
// => 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!
1 | The empty expression {} is a placeholder that is replaced with an
index-based array accessor, i.e. {data[0]} . |
2 | You can provide a data map instead. |
3 | A builder-like API is available for more complex formatting requirements. |
4 | Note that for a "text/html" template the special chars are replaced with html entities by default. |
5 | 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 Quarkus, the engine used to format the messages
is the same as the one injected by @Inject Engine . Therefore, you can make
use of any Quarkus-specific integration feature such as
Template Extension Methods,
Injecting Beans Directly In
Templates or even Type-safe Message
Bundles.
|
The format object returned by the Qute.fmt(String)
method can be evaluated
lazily and used e.g. as a log message:
LOG.info(Qute.fmt("Hello {name}!").data("name", "Foo"));
// => Hello Foo! and the message template is only evaluated if the log level INFO is used for the specific logger
Please read the javadoc of the io.quarkus.qute.Qute class for more
details.
|
2. Hello World Example
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:
<html>
<p>Hello {name}! (1)
</html>
1 | {name} is a value expression that is evaluated when the template is
rendered. |
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();
In Quarkus, there is a preconfigured Engine available for injection - see
Quarkus Integration.
|
Once we have an Engine
instance we could parse the template contents:
Template hello = engine.parse(helloHtmlContent);
In Quarkus, you can simply inject the template definition. The template is automatically parsed and cached - see Quarkus Integration. |
Finally, create a template instance, set the data and render the output:
// Renders <html><p>Hello Jim!</p></html>
hello.data("name", "Jim").render(); (1) (2)
1 | Template.data(String, Object) is a convenient method that creates a
template instance and sets the data in one step. |
2 | 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> . |
So the workflow is simple:
-
Create the template contents (
hello.html
), -
Parse the template definition (
io.quarkus.qute.Template
), -
Create a template instance (
io.quarkus.qute.TemplateInstance
), -
Render the output.
The Engine is able to cache the template definitions so that it’s not
necessary to parse the contents again and again. In Quarkus, the caching is
done automatically.
|
3. Core Features
3.1. Basic Building Blocks
The dynamic parts of a template include comments, expressions, sections and unparsed character data.
- Comments
-
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. - Expressions
-
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}
. - Sections
-
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. - Unparsed Character Data
-
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.Previously, unparsed character data could start with
{[
and end with]}
. This syntax is now removed due to common collisions with constructs from other languages.
3.2. Identifiers and Tags
Identifiers are used in expressions and section tags. A valid identifier is a sequence of non-whitespace characters. However, users are encouraged to only use valid Java identifiers in expressions.
You can use bracket notation if you need to specify an identifier that
contains a dot, e.g. {map['my.key']} .
|
When parsing a template document the parser identifies all tags. A tag
starts and ends with a curly bracket, e.g. {foo}
. The content of a tag
must start with:
-
a digit, or
-
an alphabet character, or
-
underscore, or
-
a built-in command:
#
,!
,@
,/
.
If it does not start with any of the above it is ignored by the parser.
<html>
<body>
{_foo.bar} (1)
{! comment !}(2)
{ foo} (3)
{{foo}} (4)
{"foo":true} (5)
</body>
</html>
1 | Parsed: an expression that starts with underscore. |
2 | Parsed: a comment |
3 | Ignored: starts with whitespace. |
4 | Ignored: starts with { . |
5 | Ignored: starts with " . |
It is also possible to use escape sequences \{ and \} to insert
delimiters in the text. In fact, an escape sequence is usually only needed
for the start delimiter, i.e. \{foo} will be rendered as {foo} (no
parsing/evaluation will happen).
|
3.3. Removing Standalone Lines From the Template
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.
<html>
<body>
<ul>
{#for item in items} (1)
<li>{item.name} {#if item.active}{item.price}{/if}</li> (2)
(3)
{/for} (4)
</ul>
<body>
</html>
1 | This is a standalone line and will be removed. |
2 | Not a standalone line - contains an expression and non-whitespace characters |
3 | Not a standalone line - contains no section tag/parameter declaration |
4 | This is a standalone line. |
<html>
<body>
<ul>
<li>Foo 100</li>
</ul>
<body>
</html>
In Quarkus, the default behavior can be disabled by setting the property
quarkus.qute.remove-standalone-lines to false . In this case, all
whitespace characters from a standalone line will be printed to the output.
|
quarkus.qute.remove-standalone-lines=false
<html>
<body>
<ul>
<li>Foo 100</li>
</ul>
<body>
</html>
3.4. Expressions
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)
1 | no namespace, one part: name |
2 | no namespace, two parts: item , name |
3 | equivalent to {item.name} but using the bracket notation |
4 | namespace global , one part: colors |
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)
1 | no namespace, two parts - item , getLabels(1) , the second part is a
virtual method with name getLabels and params 1 |
2 | infix notation that can be used for virtual methods with single parameter,
translated to name.or('John') ; no namespace, two parts - name ,
or('John') |
3.4.1. Supported Literals
Literal |
Examples |
boolean |
|
null |
|
string |
|
integer |
|
long |
|
double |
|
float |
|
3.4.2. Resolution
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.
Data passed to the template instance are always accessible using the
|
3.4.3. Current Context
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}
1 | The current context object inside the section is the map of resolved parameters. |
The current context can be accessed via the implicit binding this .
|
3.4.4. Built-in Resolvers
Name |
Description |
Examples |
Elvis Operator |
Outputs the default value if the previous part cannot be resolved or resolves to |
|
orEmpty |
Outputs an empty list if the previous part cannot be resolved or resolves to |
|
Ternary Operator |
Shorthand for if-then-else statement. Unlike in If Section nested operators are not supported. |
|
Logical AND Operator |
Outputs |
|
Logical OR Operator |
Outputs |
|
The condition in a ternary operator evaluates to true if the value is not
considered falsy as described in If Section.
|
In fact, the operators are implemented as "virtual methods" that consume one
parameter and can be used with infix notation. For example {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')}
|
3.4.5. Arrays
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)
<ul>
<li>First: {myArray.0}</li> (2)
<li>Second: {myArray[1]}</li> (3)
<li>Third: {myArray.get(2)}</li> (4)
</ul>
<ol>
{#for element in myArray}
<li>{element}</li>
{/for}
</ol>
First two elements: {#each myArray.take(2)}{it}{/each} (5)
1 | Outputs the length of the array. |
2 | Outputs the first element of the array. |
3 | Outputs the second element of the array using the bracket notation. |
4 | Outputs the third element of the array via the virtual method get() . |
5 | Outputs the first two elements of the array. |
3.4.6. Character Escapes
For HTML and XML templates the '
, "
, <
, >
, &
characters are
escaped by default if a template variant is set.
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 .
|
If you need to render the unescaped value:
-
Either use the
raw
orsafe
properties implemented as extension methods of thejava.lang.Object
, -
Or wrap the
String
value in aio.quarkus.qute.RawString
.
<html>
<h1>{title}</h1> (1)
{paragraph.raw} (2)
</html>
1 | title that resolves to Expressions & Escapes will be rendered as
Expressions & Escapes |
2 | paragraph that resolves to <p>My text!</p> will be rendered as <p>My
text!</p> |
By default, a template with one of the following content types is escaped:
text/html , text/xml , application/xml and
application/xhtml+xml . However, it’s possible to extend this list via the
quarkus.qute.escape-content-types configuration property.
|
3.4.7. Virtual Methods
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.
<html>
<h1>{item.buildName(item.name,5)}</h1> (1)
</html>
1 | 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:
|
Virtual methods are usually evaluated by value resolvers generated for @TemplateExtension methods, @TemplateData or classes used in parameter declarations. However, a custom value resolver that is not backed by any Java class/method can be registered as well. |
A virtual method with single parameter can be called using the infix notation:
<html>
<p>{item.price or 5}</p> (1)
</html>
1 | item.price or 5 is translated to item.price.or(5) . |
Virtual method parameters can be "nested" virtual method invocations.
<html>
<p>{item.subtractPrice(item.calculateDiscount(10))}</p> (1)
</html>
1 | item.calculateDiscount(10) is evaluated first and then passed as an
argument to item.subtractPrice() . |
3.4.8. Evaluation of CompletionStage
and Uni
Objects
Objects that implement java.util.concurrent.CompletionStage
and
io.smallrye.mutiny.Uni
are evaluated in a special way. If a part of an
expression resolves to a CompletionStage
, 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}
and foo
resolves to
CompletionStage<List<String>>
then size
is resolved against the
completed result, i.e. List<String>
. If a part of an expression resolves
to a Uni
, a CompletionStage
is first created from Uni
using
Uni#subscribeAsCompletionStage()
and then evaluated as described above.
Note that each Uni#subscribeAsCompletionStage() results in a new
subscription. You might need to configure memoization of the Uni item or
failure before it’s used as template data,
i.e. myUni.memoize().indefinitely() .
|
It can happen that a CompletionStage
never completes or a Uni
emits no
item/failure. In this case, the rendering methods (such as
TemplateInstance#render()
and TemplateInstance#createUni()
) fail after a
specific timeout. The timeout can be specified as a template instance
timeout
attribute. If no timeout
attribute is set the global rendering
timeout is used.
In Quarkus, the default timeout can be set via the io.quarkus.qute.timeout
configuration property. If using Qute standalone then the
EngineBuilder#timeout() method can be used.
|
In previous versions, only the TemplateInstance#render() method honored
the timeout attribute. You can use the
io.quarkus.qute.useAsyncTimeout=false config property to preserve the old
behavior and take care of the timeout yourself, for example
templateInstance.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 TRACE
level for the logger
io.quarkus.qute.nodeResolve
and try to analyze the log output afterwards.
application.properties
Examplequarkus.log.category."io.quarkus.qute.nodeResolve".min-level=TRACE
quarkus.log.category."io.quarkus.qute.nodeResolve".level=TRACE
You 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 8
If a completed
log 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 property
age
declared on the Person
class. The behavior differs based on whether
the Strict Rendering is enabled or not.
If enabled then a missing property will always result in a
TemplateException
and 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_FOUND
is written to the output
by default.
In Quarkus, it’s possible to change the default strategy via the
quarkus.qute.property-not-found-strategy as described in the
Configuration Reference.
|
Similar errors are detected at build time if Type-safe Expressions and Type-safe Templates are used. |
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.
#let
Optional End Tag Example{#if item.isActive}
{#let price = item.price} (1)
{price}
// synthetic {/let} added here automatically
{/if}
// {price} cannot be used here!
1 | Defines the local variable that can be used inside the parent {#if}
section. |
<1> Built-in section |
<1> Supports Optional End Tag |
<1> |
<1> ❌ |
<1> |
<1> ❌ |
<1> |
<1> ❌ |
<1> |
<1> ✅ |
<1> |
<1> ❌ |
<1> |
<1> ✅ |
<1> User-defined Tags |
<1> ❌ |
<1> |
<1> ❌ |
<1> |
<1> ❌ |
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 is id
and the value is Foo
. Values can be
grouped using parentheses, e.g. {#let id=(item.id ?: 42)}
where the name
is id
and the value is item.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.
#if
Section 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}
1 | This is the main block. |
2 | Additional block. |
3 | Additional block. |
3.5.2. Loop Section
The loop section makes it possible to iterate over an instance of
Iterable
, Iterator
, array, Map
(element is a Map.Entry
), Stream
,
Integer
and int
(primitive value). A null
parameter value results in
a no-op.
This section has two flavors. The first one is using the name each
and
it
is an implicit alias for the iteration element.
{#each items}
{it.name} (1)
{/each}
1 | name is resolved against the current iteration element. |
The other form is using the name for
and specifies the alias used to
reference the iteration element:
{#for item in items} (1)
{item.name}
{/for}
1 | item is the alias used for the iteration element. |
It’s also possible to access the iteration metadata inside the loop via the following keys:
-
count
- 1-based index -
index
- zero-based index -
hasNext
-true
if the iteration has more elements -
isLast
-true
ifhasNext == false
-
isFirst
-true
ifcount == 1
-
odd
-true
if the element’s count is odd -
even
-true
if the element’s count is even -
indexParity
- outputsodd
oreven
based on the count value
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 hasNext
key must be prefixed with it_
inside
an {#each}
section: {it_hasNext}
.
each
Iteration Metadata Example{#each items}
{it_count}. {it.name} (1)
{#if it_hasNext}<br>{/if} (2)
{/each}
1 | it_count represents one-based index. |
2 | <br> is only rendered if the iteration has more elements. |
And must be used in a form of {item_hasNext}
inside a {#for}
section
with the item
element alias.
for
Iteration Metadata Example{#for item in items}
{item_count}. {item.name} (1)
{#if item_hasNext}<br>{/if} (2)
{/for}
1 | item_count represents one-based index. |
2 | <br> is only rendered if the iteration has more elements. |
The iteration metadata prefix is configurable either via
|
The for
statement also works with integers, starting from 1. In the
example below, considering that total = 3
:
{#for i in total}
{i}: ({i_count} {i_indexParity} {i_even})<br>
{/for}
And the output will be:
1: (1 odd false)
2: (2 even true)
3: (3 odd false)
A loop section may also define the {#else}
block that is executed when
there are no items to iterate:
{#for item in items}
{item.name}
{#else}
No items.
{/for}
3.5.3. If Section
The if
section represents a basic control flow section. The simplest
possible version accepts a single parameter and renders the content if the
condition is evaluated to true
. A condition without an operator evaluates
to true
if the value is not considered falsy
, i.e. if the value is not
null
, 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:
Operator |
Aliases |
Precedence (higher wins) |
logical complement |
|
4 |
greater than |
|
3 |
greater than or equal to |
|
3 |
less than |
|
3 |
less than or equal to |
|
3 |
equals |
|
2 |
not equals |
|
2 |
logical AND (short-circuiting) |
|
1 |
logical OR (short-circuiting) |
|
1 |
{#if item.age > 10}
This item is very old.
{/if}
Multiple conditions are also supported.
{#if item.age > 10 && item.price > 500}
This item is very old and expensive.
{/if}
Precedence rules can be overridden by parentheses.
{#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 else
blocks:
{#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 switch
or Kotlin’s when
constructs.
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 Java switch
where a
break
statement is necessary).
when
/is
name aliases{#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}
1 | If there is exactly one parameter it’s tested for equality. |
2 | It is possible to use an operator to specify the matching logic. Unlike in the If Section nested operators are not supported. |
3 | else is block is executed if no other block matches the value. |
switch
/case
name aliases{#switch person.name}
{#case 'John'} (1)
Hey John!
{#case 'Mary'}
Hey Mary!
{/switch}
1 | case is an alias for is . |
A tested value that resolves to an enum is handled specifically. The
parameters of an is
/case
block are not evaluated as expressions but
compared with the result of toString()
invocation upon the tested value.
{#when machine.status}
{#is ON}
It's running. (1)
{#is in OFF BROKEN}
It's broken or OFF. (2)
{/when}
1 | This block is executed if machine.status.toString().equals("ON") . |
2 | This block is executed if machine.status.toString().equals("OFF") or
machine.status.toString().equals("BROKEN") . |
An enum constant is validated if the tested value has a type information available and resolves to an enum type. |
The following operators are supported in is
/case
block conditions:
Operator |
Aliases |
Example |
not equal |
|
|
greater than |
|
|
greater than or equal to |
|
|
less than |
|
|
less than or equal to |
|
|
in |
|
|
not in |
|
|
3.5.5. Let Section
This section allows you to define named local variables:
{#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)
1 | The local variable is initialized with an expression that can also represent
a literal, i.e. isActive=false and age=10 . |
2 | The infix notation is only supported if parentheses are used for grouping,
e.g. price=(order.price + 10) is equivalent to
price=order.price.plus(10) . |
3 | Keep in mind that the variable is not available outside the let section
that defines it. |
If a key of a section parameter, such as the name of the local variable,
ends with a ?
, then the local variable is only set if the key without the
?
suffix resolves to null
or "not found":
{#let enabled?=true} (1) (2)
{#if enabled}ON{/if}
{/let}
1 | true is effectively a default value that is only used if the parent
scope does not define enabled already. |
2 | enabled?=true is a short version of enabled=enabled.or(true) . |
This section tag is also registered under the set
alias:
{#set myParent=item.parent price=item.price}
<h1>{myParent.name}</h1>
<p>Price: {price}
{/set}
3.5.6. With Section
This section can be used to set the current context object. This could be useful to simplify the template structure:
{#with item.parent}
<h1>{name}</h1> (1)
<p>{description}</p> (2)
{/with}
1 | The name will be resolved against the item.parent . |
2 | The description will be also resolved against the item.parent . |
Note that the
|
This section might also come in handy when we’d like to avoid multiple expensive invocations:
{#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}
1 | this is the result of
item.callExpensiveLogicToGetTheValue(1,'foo',bazinga) . The method is only
invoked once even though the result may be used in multiple expressions. |
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).
<html>
<head>
<meta charset="UTF-8">
<title>Simple Include</title>
</head>
<body>
{#include foo limit=10 /} (1)(2)
</body>
</html>
1 | Include a template with id foo . The included template can reference data
from the current context. |
2 | It’s also possible to define optional parameters that can be used in the included template. |
Template inheritance makes it possible to reuse template layouts.
<html>
<head>
<meta charset="UTF-8">
<title>{#insert title}Default Title{/}</title> (1)
</head>
<body>
{#insert}No body!{/} (2)
</body>
</html>
1 | insert sections are used to specify parts that could be overridden by a
template that includes the given template. |
2 | 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. |
{#include base} (1)
{#title}My Title{/title} (2)
<div> (3)
My body.
</div>
{/include}
1 | include section is used to specify the extended template. |
2 | Nested blocks are used to specify the parts that should be overridden. |
3 | The content of the main block is used for an {#insert} section with no
name parameter specified. |
Section blocks can also define an optional end tag - {/title} .
|
3.5.8. User-defined Tags
User-defined tags can be used to include a tag template, optionally pass
some arguments and possibly override some parts of the template. Let’s
suppose we have a tag template called itemDetail.html
:
{#if showImage} (1)
{it.image} (2)
{nested-content} (3)
{/if}
1 | showImage is a named parameter. |
2 | it is a special key that is replaced with the first unnamed parameter of
the tag. |
3 | (optional) nested-content is a special key that will be replaced by the
content of the tag. |
In Quarkus, all files from the src/main/resources/templates/tags
are
registered and monitored automatically. For Qute standalone, you need to
put the parsed template under the name itemDetail.html
and register a
relevant UserTagSectionHelper
to the engine:
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:
<ul>
{#for item in items}
<li>
{#itemDetail item showImage=true} (1)
= <b>{item.name}</b> (2)
{/itemDetail}
</li>
{/for}
</ul>
1 | item is resolved to an iteration element and can be referenced using the
it key in the tag template. |
2 | Tag content injected using the nested-content key in the tag template. |
By default, a tag template cannot reference the data from the parent
context. Qute executes the tag as an isolated template, i.e. without
access to the context of the template that calls the tag. However,
sometimes it might be useful to change the default behavior and disable the
isolation. In this case, just add _isolated=false
or _unisolated
argument to the call site, for example {#itemDetail item showImage=true
_isolated=false /}
or {#itemDetail item showImage=true _unisolated /}
.
3.5.8.1. Arguments
Named arguments can be accessed directly in a tag template. The first
argument does not have to define a name but can be accessed using the it
alias. Furthermore, arguments metadata are accessible in a tag using the
_args
alias.
-
_args.size
- returns the actual number of arguments passed to a tag -
_args.empty
- returnstrue
if no arguments are passed -
_args.get(String name)
- returns the argument value of the given name -
_args.filter(String…)
- returns the arguments matching the given names -
_args.skip(String…)
- returns only the arguments that do not match the given names -
_args.asHtmlAttributes
- renders the arguments as HTML attributes; e.g.foo="true" bar="false"
(the arguments are sorted by name in alphabetical order)
_args
is also iterable: {#each _args}{it.key}={it.value}{/each}
.
For example, we can call the user tag defined below with {#test 'Martin'
readonly=true /}
.
tags/test.html
{it} (1)
{readonly} (2)
{_args.filter('readonly').asHtmlAttributes} (3)
1 | it is replaced with the first unnamed parameter of the tag. |
2 | readonly is a named parameter. |
3 | _args represents arguments metadata. |
The result would be:
Martin
true
readonly="true"
3.5.8.2. Inheritance
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)
1 | insert sections are used to specify parts that could be overridden by a
template that includes the given template. |
<p>
{#myTag}
{#title}my custom title{/title} (1)
{/myTag}
</p>
1 | The result would be something like <p>This is my custom title!</p> . |
3.5.9. Fragments
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.
Note that a fragment identifier must be unique in a template. |
item.html
{@org.acme.Item item}
{@java.util.List<String> aliases}
<h1>Item - {item.name}</h1>
<p>This document contains a detailed info about an item.</p>
{#fragment id=item_aliases} (1)
<h2>Aliases</h2>
<ol>
{#for alias in aliases}
<li>{alias}</li>
{/for}
</ol>
{/fragment}
1 | Defines a fragment with identifier item_aliases . Note that only
alphanumeric characters and underscores can be used in the identifier. |
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();
}
1 | Obtains the template fragment with identifier item_aliases . |
2 | Make sure the data are set correctly. |
The snippet above should render something like:
<h2>Aliases</h2>
<ol>
<li>Foo</li>
<li>Bar</li>
</ol>
In Quarkus, it is also possible to define a type-safe fragment. |
You can also include a fragment with an {#include}
section inside another
template or the template that defines the fragment.
user.html
<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)
1 | A template identifier that contains a dollar sign $ denotes a
fragment. The item$item_aliases value is translated as: Use the fragment
item_aliases from the template item . |
2 | The aliases parameter 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 expression user.aliases as the value of aliases in the
{#for alias in aliases} section. |
If you want to reference a fragment from the same template, skip the part
before $ , i.e. something like {#include $item_aliases /} .
|
You can specify {#include item$item_aliases _ignoreFragments=true /} in
order to disable this feature, i.e. a dollar sign $ in the template
identifier does not result in a fragment lookup.
|
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.
item.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)
!</p>
1 | Defines a hidden fragment with identifier strong . In this particular
case, we use the false boolean literal as the value of the rendered
parameter. However, it’s possible to use any expression there. |
2 | Include the fragment strong and pass the value. Note the syntax $strong
which is translated to include the fragment strong from the current
template. |
3 | Include the fragment strong and pass the value. |
The snippet above renders something like:
<h1>My page</h1>
<p>This document
<strong>contains</strong>
a lot of
<strong>information</strong>
!</p>
3.5.10. Eval Section
This section can be used to parse and evaluate a template dynamically. The behavior is very similar to Include Section but:
-
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.
{#eval myData.template name='Mia' /} (1)(2)(3)
1 | The result of myData.template will be used as the template. The template
is executed with the Current Context, i.e. can
reference data from the template it’s included into. |
2 | It’s also possible to define optional parameters that can be used in the evaluated template. |
3 | The content of the section is always ignored. |
The evaluated template is parsed and evaluated every time the section is executed. In other words, it is not possible to cache the parsed value to conserve resources and optimize performance. |
3.5.11. Cached Section
Sometimes it’s practical to cache parts of the template that rarely change.
In order to use the caching capability, register and configure the built-in
io.quarkus.qute.CacheSectionHelper.Factory
:
// 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();
If the quarkus-cache extension is present in a Quarkus application then
the CacheSectionHelper is registered and configured automatically. The
name of the cache is qute-cache . It can be configured
in a standard
way and even managed programmatically via @Inject @CacheName("qute-cache")
Cache .
|
Then, the {#cached}
section can be used in a template:
{#cached} (1)
Result: {service.findResult} (2)
{/cached}
1 | If the key param is not used then all clients of the template share the
same cached value. |
2 | This part of the template will be cached and the {service.findResult}
expression is only evaluated when a cache entry is missing/invalidated. |
{#cached key=currentUser.username} (1)
User-specific result: {service.findResult(currentUser)}
{/cached}
1 | The key param is set and so a different cached value is used for each
result of the {currentUser.username} expression. |
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
String that consist of the template name, line and column of the starting
{#cached} tag and the optional key parameter:
{TEMPLATE}:{LINE}:{COLUMN}_{KEY} . For example, foo.html:10:1_alpha is a
key for the cached section in a template foo.html , the {#cached} tag is
placed on the line 10, column 1. And the optional key parameter resolves
to alpha .
|
3.6. Rendering Output
TemplateInstance
provides several ways to trigger the rendering and
consume the result. The most straightforward approach is represented by
TemplateInstance.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 a
CompletionStage<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...
}
};
1 | Register a callback that is executed once the rendering is finished. |
There are also two methods that return
Mutiny types.
TemplateInstance.createUni()
returns a new Uni<String>
object. If you
call createUni()
the template is not rendered right away. Instead, every
time Uni.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 new Multi<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()
Exampletemplate.data(foo).createMulti().subscribe().with(buffer:append,buffer::flush);
The 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. |
3.7. Engine Configuration
3.7.1. Value Resolvers
Value resolvers are used when evaluating expressions. A custom
io.quarkus.qute.ValueResolver
can be registered programmatically via
EngineBuilder.addValueResolver()
.
ValueResolver
Builder 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.
In Quarkus, all templates from the src/main/resources/templates are
located automatically and the encoding set via
quarkus.qute.default-charset (UTF-8 by default) is used. Custom locators
can be registered by using the @Locate
annotation.
|
3.7.3. Content Filters
Content filters can be used to modify the template contents before parsing.
engineBuilder.addParserHook(new ParserHook() {
@Override
public void beforeParsing(ParserHelper parserHelper) {
parserHelper.addContentFilter(contents -> contents.replace("${", "$\\{")); (1)
}
});
1 | Escape all occurrences of ${ . |
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 a
TemplateException
and the rendering is aborted. A NotFound
value is
considered an error because it basically means that no value resolver was
able to resolve the expression correctly.
null is a valid value though. It is considered falsy as described in
If Section and does not produce any output.
|
Strict rendering is enabled by default. However, you can disable this
functionality via io.quarkus.qute.EngineBuilder.strictRendering(boolean)
.
In Quarkus, a dedicated config property can be used instead:
quarkus.qute.strict-rendering .
|
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 in null
if 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)}
.
4. Quarkus Integration
If you want to use Qute in your Quarkus application, add the following dependency to your project:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-qute</artifactId>
</dependency>
In Quarkus, a preconfigured engine instance is provided and available for
injection - a bean with scope @ApplicationScoped
, bean type
io.quarkus.qute.Engine
and qualifier @Default
is registered
automatically. Moreover, all templates located in the
src/main/resources/templates
directory 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)
}
1 | If there is no Location qualifier provided, the field name is used to
locate the template. In this particular case, the container will attempt to
locate a template with path src/main/resources/templates/items.html . |
2 | The Location qualifier instructs the container to inject a template from a
path relative from src/main/resources/templates . In this case, the full
path is src/main/resources/templates/detail/items2_v1.html . |
3 | Inject the configured Engine instance. |
4.1. Engine Customization
Additional components can be registered manually via EngineBuilder
methods
in a CDI observer method at runtime:
import io.quarkus.qute.EngineBuilder;
class MyBean {
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 @EngineConfiguration
annotation:
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)
}
}
}
1 | A SectionHelperFactory annotated with @EngineConfiguration is used
during validation of templates at build time and automatically registered at
runtime (a) as a section factory and (b) as a CDI bean. |
2 | A CDI bean instance is used at runtime - this means that the factory can define injection points |
3 | Validate that foo parameter is always present; e.g. {#custom foo='bar'
/} is ok but {#custom /} results in a build failure. |
4 | Use the injected Service during rendering. |
The @EngineConfiguration
annotation can be also used to register
ValueResolver
s and NamespaceResolver
s.
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 @Locate
annotation.
@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);
}
}
1 | A template named bar.html is located by the custom locator at runtime. |
2 | A regular expression foo.* disables validation for templates whose name is
starting with foo . |
3 | Injection fields are resolved as template locators annotated with @Locate
are registered as singleton session beans. |
4.2. Template Variants
Sometimes it’s useful to render a specific variant of the template based on
the content negotiation. This can be done by setting a special attribute
via TemplateInstance.setAttribute()
:
class MyService {
@Inject
Template items; (1)
@Inject
ItemManager manager;
String renderItems() {
return items.data("items",manager.findItems()).setAttribute(TemplateInstance.SELECTED_VARIANT, new Variant(Locale.getDefault(),"text/html","UTF-8")).render();
}
}
When using quarkus-resteasy-qute the content negotiation is performed
automatically. For more information, see the
RESTEasy Integration section.
|
4.3. Injecting Beans Directly In Templates
A CDI bean annotated with @Named
can be referenced in any template through
cdi
and/or inject
namespaces:
{cdi:personService.findPerson(10).name} (1)
{inject:foo.price} (2)
1 | First, a bean with name personService is found and then used as the base
object. |
2 | First, a bean with name foo is found and then used as the base object. |
@Named @Dependent beans are shared across all expressions in a template
for a single rendering operation, and destroyed after the rendering
finished.
|
All expressions with cdi
and inject
namespaces are validated during
build.
For the expression cdi:personService.findPerson(10).name
, the
implementation class of the injected bean must either declare the
findPerson
method or a matching template
extension method must exist.
For the expression inject:foo.price
, the implementation class of the
injected bean must either have the price
property (e.g. a getPrice()
method) or a matching template extension
method must exist.
A ValueResolver is also generated for all beans annotated with @Named so
that it’s possible to access its properties without reflection.
|
If your application serves HTTP requests you can
also inject the current io.vertx.core.http.HttpServerRequest via the
inject namespace, e.g. {inject:vertxRequest.getParam('foo')} .
|
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.name
where item
maps to
org.acme.Item
then Item
must have a property name
or 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.lang
package - 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.
{@org.acme.Foo foo} (1)
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Qute Hello</title>
</head>
<body>
<h1>{title}</h1> (2)
Hello {foo.message.toLowerCase}! (3) (4)
</body>
</html>
1 | Parameter declaration - maps foo to org.acme.Foo . |
2 | Not validated - not matching a param declaration. |
3 | This expression is validated. org.acme.Foo must have a property message
or a matching template extension method must exist. |
4 | Likewise, the Java type of the object resolved from foo.message must have
a property toLowerCase or a matching template extension method must exist. |
A value resolver is automatically generated for all types used in parameter declarations so that it’s possible to access its properties without reflection. |
Method parameters of type-safe templates are automatically turned into parameter declarations. |
Note that sections can override names that would otherwise match a parameter declaration:
{@org.acme.Foo foo}
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Qute Hello</title>
</head>
<body>
<h1>{foo.message}</h1> (1)
{#for foo in baz.foos}
<p>Hello {foo.message}!</p> (2)
{/for}
</body>
</html>
1 | Validated against org.acme.Foo . |
2 | Not validated - foo is overridden in the loop section. |
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 to
null
or is not found.
For example, if there’s a parameter declaration {@String foo="Ping"}
and
foo
is not found then you can use {foo}
and the output will be Ping
.
On the other hand, if the value is set (e.g. via
TemplateInstance.data("foo", "Pong")
) then the output of {foo}
will be
Pong
.
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: {@org.acme.Foo foo=1}
.
The default value is actually an expression. So the
default value does not have to be a literal (such as 42 or true ). For
example, you can leverage the @TemplateEnum and 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 type of a default value is not validated in Qute standalone. |
{@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)
1 | A primitive type. |
2 | String is replaced with java.lang.String :
{@java.util.List<java.lang.String> strings} |
3 | The wildcard is ignored and the upper bound is used instead:
{@java.util.Map<String,Number>} |
4 | The wildcard is ignored and the java.lang.Object is used instead:
{@java.util.Optional<java.lang.Object>} |
5 | The type is java.lang.String , the key is name and the default value is
Quarkus . |
4.5. Type-safe Templates
You can define type-safe templates in your Java code. Parameters of type-safe templates are automatically turned into parameter declarations that are used to bind Type-safe Expressions. The type-safe expressions are then validated at build time.
There are two ways to define a type-safe template:
-
Annotate a class with
@io.quarkus.qute.CheckedTemplate
and all itsstatic native
methods will be used to define type-safe templates and the list of parameters they require. -
Use a Java record that implements
io.quarkus.qute.TemplateInstance
; the record components represent the template parameters and@io.quarkus.qute.CheckedTemplate
can be optionally used to configure the template.
4.5.1. Nested Type-safe Templates
If using templates in Jakarta REST resources, you can rely on the following convention:
-
Organise your template files in the
/src/main/resources/templates
directory, by grouping them into one directory per resource class. So, if yourItemResource
class references two templateshello
andgoodbye
, place them at/src/main/resources/templates/ItemResource/hello.txt
and/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.
package org.acme.quarkus.sample;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
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)
}
@GET
@Path("{id}")
@Produces(MediaType.TEXT_HTML)
public TemplateInstance get(Integer id) {
return Templates.item(service.findItem(id)); (3)
}
}
1 | Declare a method that gives us a TemplateInstance for
templates/ItemResource/item.html and declare its Item item parameter so
we can validate the template. |
2 | The item parameter is automatically turned into a
parameter declaration and so all expressions that
reference this name will be validated. |
3 | Make the Item object accessible in the template. |
By default, the templates defined in a class annotated with
@CheckedTemplate can only contain type-safe expressions, i.e. expressions
that can be validated at build time. You can use
@CheckedTemplate(requireTypeSafeExpressions = false) to relax this
requirement.
|
4.5.2. Top-level Type-safe Templates
You can also declare a top-level Java class annotated with
@CheckedTemplate
:
package 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)
}
1 | This declares a template with path templates/hello.txt . The name
parameter is automatically turned into a parameter
declaration, so that all expressions referencing this name will be
validated. |
Then declare one public static native TemplateInstance method();
per
template file. Use those static methods to build your template instances:
package 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 {
@GET
@Produces(MediaType.TEXT_PLAIN)
public TemplateInstance get(@QueryParam("name") String name) {
return Templates.hello(name);
}
}
4.5.3. Template Records
A Java record that implements io.quarkus.qute.TemplateInstance
denotes a
type-safe template. The record components represent the template parameters
and @io.quarkus.qute.CheckedTemplate
can be optionally used to configure
the template.
package 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 {
record Hello(String name) implements TemplateInstance {} (1)
@GET
@Produces(MediaType.TEXT_PLAIN)
public TemplateInstance get(@QueryParam("name") String name) {
return new Hello(name); (2)
}
}
1 | Declares a type-safe template with the Java record. |
2 | Instantiate the record and use it as an ordinary TemplateInstance . |
4.5.4. Customized Template Path
The template path of a @CheckedTemplate
method 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
@CheckedTemplate
method is used as is.
package 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)
}
}
1 | The template path for this method will be items/item-and-order . |
4.5.5. 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 relevant
CheckedTemplate#defaultName()
is honored when constructing the defaulted
names.
import 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)
}
1 | Quarkus validates at build time that each template that corresponds to the
Templates#items() contains a fragment with identifier item . 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. |
items.html
<h1>Items</h1>
<ol>
{#for item in items}
{#fragment id=item} (1)
<li>{item.name}</li> (2)
{/fragment}
{/for}
</ol>
1 | Defines a fragment with identifier item . |
2 | The {item.name} expression implies that the Templates#items$item()
method must declare a parameter of name item and type org.acme.Item . |
class ItemService {
String renderItem(Item item) {
// this would return something like "<li>Foo</li>"
return Templates.items$item(item).render();
}
}
You can specify @CheckedTemplate#ignoreFragments=true in order to disable
this feature, i.e. a dollar sign $ in the method name will not result in a
checked fragment method.
|
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 @TemplateExtension
then 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:
-
must not be
private
-
must be static,
-
must not return
void
.
If there is no namespace defined the class of the first parameter that is
not annotated with @TemplateAttribute
is 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.
package 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"));
}
}
1 | This method matches an expression with base object of the type Item.class
and the discountedPrice property name. |
This template extension method makes it possible to render the following template:
{item.discountedPrice} (1)
1 | item is resolved to an instance of org.acme.Item . |
However, it is possible to specify the matching name with matchName()
.
TemplateExtension#matchName()
Example@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#ANY
Example@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"
}
1 | A additional string method parameter is used to pass the actual property name. |
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"
}
1 | A additional string method parameter is used to pass the actual property name. |
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"
}
Superfluous matching conditions are ignored. The conditions sorted by
priority in descending order are: matchRegex() , matchNames() and
matchName() .
|
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 @TemplateAttribute
is
used to pass the base object, i.e. org.acme.Item
in 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 @TemplateAttribute
are obtained via
TemplateInstance#getAttribute()
. All other parameters are resolved when
rendering the template and passed to the extension method.
@TemplateExtension
class BigDecimalExtensions {
static BigDecimal scale(BigDecimal val, int scale, RoundingMode mode) { (1)
return val.setScale(scale, mode);
}
}
1 | This method matches an expression with base object of the type
BigDecimal.class , with the scale virtual method name and two virtual
method parameters. |
{item.discountedPrice.scale(2,mode)} (1)
1 | item.discountedPrice is resolved to an instance of BigDecimal . |
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 by TemplateExtension#priority()
. The first matching
extension method is used to resolve an expression.
@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)
1 | The output is Hello world! |
2 | The output is olleh |
4.6.4. Built-in Template Extensions
Quarkus provides a set of built-in extension methods.
4.6.4.1. Maps
-
keys
orkeySet
: Returns a Set view of the keys contained in a map-
{#for key in map.keySet}
-
-
values
: Returns a Collection view of the values contained in a map-
{#for value in map.values}
-
-
size
: Returns the number of key-value mappings in a map-
{map.size}
-
-
isEmpty
: Returns true if a map contains no key-value mappings-
{#if map.isEmpty}
-
-
get(key)
: Returns the value to which the specified key is mapped-
{map.get('foo')}
-
A map value can be also accessed directly: {map.myKey} . Use the bracket
notation for keys that are not legal identifiers: {map['my key']} .
|
4.6.5. Lists
-
get(index)
: Returns the element at the specified position in a list-
{list.get(0)}
-
-
reversed
: Returns a reversed iterator over a list-
{#for r in recordsList.reversed}
-
-
take
: Returns the firstn
elements from the given list; throws anIndexOutOfBoundsException
ifn
is out of range-
{#for r in recordsList.take(3)}
-
-
takeLast
: Returns the lastn
elements from the given list; throws anIndexOutOfBoundsException
ifn
is out of range-
{#for r in recordsList.takeLast(3)}
-
-
first
: Returns the first element of the given list; throws anNoSuchElementException
if the list is empty-
{recordsList.first}
-
-
last
: Returns the last element of the given list; throws anNoSuchElementException
if the list is empty-
{recordsList.last}
-
A list element can be accessed directly via an index: {list.10} or even
{list[10]} .
|
4.6.5.1. Integer Numbers
-
mod
: Modulo operation-
{#if counter.mod(5) == 0}
-
-
plus
or+
: Addition-
{counter + 1}
-
{age plus 10}
-
{age.plus(10)}
-
-
minus
or-
: Subtraction-
{counter - 1}
-
{age minus 10}
-
{age.minus(10)}
-
4.6.5.2. Strings
-
fmt
orformat
: Formats the string instance viajava.lang.String.format()
-
{myStr.fmt("arg1","arg2")}
-
{myStr.format(locale,arg1)}
-
-
str:fmt
orstr:format
: Formats the supplied string value viajava.lang.String.format()
-
{str:format("Hello %s!",name)}
-
{str:fmt(locale,'%tA',now)}
-
-
+
: Concatenation-
{item.name + '_' + mySuffix}
-
{name + 10}
-
4.6.5.3. Config
-
config:<name>
orconfig:[<name>]
: Returns the config value for the given property name-
{config:foo}
or{config:['property.with.dot.in.name']}
-
-
config:property(name)
: Returns the config value for the given property name; the name can be obtained dynamically by an expression-
{config:property('quarkus.foo')}
-
{config:property(foo.getPropertyName())}
-
-
config:boolean(name)
: Returns the config value for the given property name as a boolean; the name can be obtained dynamically by an expression-
{config:boolean('quarkus.foo.boolean') ?: 'Not Found'}
-
{config:boolean(foo.getPropertyName()) ?: 'property is false'}
-
-
config:integer(name)
: Returns the config value for the given property name as an integer; the name can be obtained dynamically by an expression-
{config:integer('quarkus.foo')}
-
{config:integer(foo.getPropertyName())}
-
4.6.5.4. Time
-
format(pattern)
: Formats temporal objects from thejava.time
package-
{dateTime.format('d MMM uuuu')}
-
-
format(pattern,locale)
: Formats temporal objects from thejava.time
package-
{dateTime.format('d MMM uuuu',myLocale)}
-
-
format(pattern,locale,timeZone)
: Formats temporal objects from thejava.time
package-
{dateTime.format('d MMM uuuu',myLocale,myTimeZoneId)}
-
-
time:format(dateTime,pattern)
: Formats temporal objects from thejava.time
package,java.util.Date
,java.util.Calendar
andjava.lang.Number
-
{time:format(myDate,'d MMM uuuu')}
-
-
time:format(dateTime,pattern,locale)
: Formats temporal objects from thejava.time
package,java.util.Date
,java.util.Calendar
andjava.lang.Number
-
{time:format(myDate,'d MMM uuuu', myLocale)}
-
-
time:format(dateTime,pattern,locale,timeZone)
: Formats temporal objects from thejava.time
package,java.util.Date
,java.util.Calendar
andjava.lang.Number
-
{time:format(myDate,'d MMM uuuu',myLocale,myTimeZoneId)}
-
4.7. @TemplateData
A value resolver is automatically generated for a type annotated with
@TemplateData
. This allows Quarkus to avoid using reflection to access
the data at runtime.
Non-public members, constructors, static initializers, static, synthetic and void methods are always ignored. |
package org.acme;
@TemplateData
class Item {
public final BigDecimal price;
public Item(BigDecimal price) {
this.price = price;
}
public BigDecimal getDiscountedPrice() {
return price.multiply(new BigDecimal("0.9"));
}
}
Any instance of Item
can be used directly in the template:
{#each items} (1)
{it.price} / {it.discountedPrice}
{/each}
1 | items is resolved to a list of org.acme.Item instances. |
Furthermore, @TemplateData.properties()
and @TemplateData.ignore()
can
be used to fine-tune the generated resolver. Finally, it is also possible
to specify the "target" of the annotation - this could be useful for
third-party classes not controlled by the application:
@TemplateData(target = BigDecimal.class)
@TemplateData
class Item {
public final BigDecimal price;
public Item(BigDecimal price) {
this.price = price;
}
}
{#each items}
{it.price.setScale(2, rounding)} (1)
{/each}
1 | The generated value resolver knows how to invoke the BigDecimal.setScale()
method. |
4.7.1. Accessing Static Fields and Methods
If @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 name org.acme.Foo
is
org_acme_Foo
. The static field Foo.AGE
can be accessed via
{org_acme_Foo:AGE}
. The static method Foo.computeValue(int number)
can
be accessed via {org_acme_Foo:computeValue(10)}
.
A namespace can only consist of alphanumeric characters and underscores. |
@TemplateData
package model;
@TemplateData (1)
public class Statuses {
public static final String ON = "on";
public static final String OFF = "off";
}
1 | A name resolver with the namespace model_Status is generated
automatically. |
{#if machine.status == model_Status:ON}
The machine is ON!
{/if}
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.
@TemplateEnum
package model;
@TemplateEnum (1)
public enum Status {
ON,
OFF
}
1 | A name resolver with the namespace Status is generated automatically. |
@TemplateEnum declared on a non-enum class is ignored. Also, if an enum
also declares the @TemplateData annotation, then the @TemplateEnum
annotation is ignored.
|
{#if machine.status == Status:ON}
The machine is ON!
{/if}
Quarkus detects possible namespace collisions and fails the build if a
specific namespace is defined by multiple @TemplateData and/or
@TemplateEnum annotations.
|
4.8. Global Variables
The io.quarkus.qute.TemplateGlobal
annotation 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
any TemplateInstance
via the TemplateInstance#data(String, Object)
method.
enum 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";
}
}
1 | If a class is annotated with @TemplateGlobal then 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. |
2 | Method-level annotations override the class-level annotation. In this particular case, the name is not defaulted but selected explicitly. |
User: {currentUser} (1)
Age: {age} (2)
Colors: {#each myColors}{it}{#if it_hasNext}, {/if}{/each} (3)
1 | currentUser resolves to Globals#user() . |
2 | age resolves to Globals#age . |
3 | myColors resolves to Globals#myColors() . |
Note that global variables implicitly add parameter declarations to all templates and so any expression that references a global variable is validated during build. |
User: Mia
Age: 40
Colors: RED, BLUE
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:
import org.acme.User;
@CheckedTemplate
public class Templates {
static native TemplateInstance hello(User currentUser); (1)
}
1 | currentUser conflicts with the global variable supplied by
Globals#user() . |
So the corresponding template does not result in a validation error even
though the Globals#user()
method returns java.lang.String
which does not
have the name
property:
templates/hello.txt
User name: {currentUser.name} (1)
1 | org.acme.User has the name property. |
For other templates an explicit parameter declaration is needed:
{@org.acme.User currentUser} (1)
User name: {currentUser.name}
1 | This parameter declaration overrides the declaration added by the global
variable supplied by the Globals#user() method. |
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.html
even if the Foo
class declares a relevant getter method.
There are several ways to solve this problem:
-
Make use of type-safe templates or type-safe expressions
-
In this case, an optimized value resolver is generated automatically and used at runtime
-
This is the preferred solution
-
-
Annotate the model class with
@TemplateData
- a specialized value resolver is generated and used at runtime -
Annotate the model class with
@io.quarkus.runtime.annotations.RegisterForReflection
to make the reflection-based value resolver work
4.10. RESTEasy Integration
If you want to use Qute in your Jakarta REST application, then depending on
which Jakarta REST stack you are using, you’ll need to register the proper
extension first. If you are using the traditional quakus-resteasy
extension, then in your pom.xml
file, add:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-qute</artifactId>
</dependency>
If instead you are using RESTEasy Reactive via the
quarkus-resteasy-reactive
extension, then in your pom.xml
file, add:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-reactive-qute</artifactId>
</dependency>
Both of these extensions register a special ContainerResponseFilter
implementation which enables resource methods to return a
TemplateInstance
, 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:
package 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)
@GET
@Produces(MediaType.TEXT_PLAIN)
public TemplateInstance get(@QueryParam("name") String name) {
return hello.data("name", name); (2) (3)
}
}
1 | If there is no @Location qualifier provided, the field name is used to
locate the template. In this particular case, we’re injecting a template
with path templates/hello.txt . |
2 | 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 key name . The data map is accessible during rendering. |
3 | Note that we don’t trigger the rendering - this is done automatically by a
special ContainerResponseFilter implementation. |
Users are encouraged to use Type-safe templates that help to organize the templates for a specific Jakarta REST resource and enable type-safe expressions automatically. |
The content negotiation is performed automatically. The resulting output
depends on the Accept
header received from the client.
@Path("/detail")
class DetailResource {
@Inject
Template item; (1)
@GET
@Produces({ MediaType.TEXT_HTML, MediaType.TEXT_PLAIN })
public TemplateInstance item() {
return item.data("myItem", new Item("Alpha", 1000)); (2)
}
}
1 | Inject a variant template with base path derived from the injected field -
src/main/resources/templates/item . |
2 | For text/plain the src/main/resources/templates/item.txt template is
used. For text/html the META-INF/resources/templates/item.html template
is used. |
The RestTemplate
util class can be used to obtain a template instance from
a body of a Jakarta REST resource method:
@Path("/detail")
class DetailResource {
@GET
@Produces({ MediaType.TEXT_HTML, MediaType.TEXT_PLAIN })
public TemplateInstance item() {
return RestTemplate.data("myItem", new Item("Alpha", 1000)); (1)
}
}
1 | The name of the template is derived from the resource class and method name;
DetailResource/item in this particular case. |
Unlike with @Inject the templates obtained via RestTemplate are not
validated, i.e. the build does not fail if a template does not exist.
|
4.11. Development Mode
In the development mode, all files located in src/main/resources/templates
are watched for changes. By default, a template modification results in an
application restart that also triggers build-time validations.
However, it’s possible to use the
quarkus.qute.dev-mode.no-restart-templates
configuration property to
specify the templates for which the application is not restarted. The
configration value is a regular expression that matches the template path
relative from the templates
directory and /
is used as a path
separator. For example,
quarkus.qute.dev-mode.no-restart-templates=templates/foo.html
matches the
template src/main/resources/templates/foo.html
. The matching templates
are reloaded and only runtime validations are performed.
4.12. Type-safe Message Bundles
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.
import 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)
}
1 | Denotes a message bundle interface. The bundle name is defaulted to msg
and is used as a namespace in templates expressions,
e.g. {msg:hello_name} . |
2 | 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. |
3 | The method parameters can be used in the template. |
The message bundles can be used at runtime:
-
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 AppMessages
-
Referenced in the templates via the message bundle namespace:
{msg:hello_name('Lucie')} (1) (2) (3) {msg:message(myKey,'Lu')} (4)
1 msg
is the default namespace.2 hello_name
is the message key.3 Lucie
is the parameter of the message bundle interface method.4 It is also possible to obtain a localized message for a key resolved at runtime using a reserved key message
. The validation is skipped in this case though.
4.12.2. Default Bundle Name
The bundle name is defaulted unless it’s specified with
@MessageBundle#value()
. For a top-level class the msg
value 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)
}
}
1 | This message could be used in a template via
{Controller_index:hello(name)} . |
The bundle name is also used as a part of the name of a localized file,
e.g. Controller_index in the Controller_index_de.properties .
|
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 @MessageBundle
can be used to
define the default strategy used to generate message keys from method
names. However, the @Message
can override this strategy and even define a
custom key. By default, the annotated element’s name is used as-is. Other
possibilities are:
-
De-camel-cased and hyphenated; e.g.
helloName()
→hello-name
-
De-camel-cased and parts separated by underscores; e.g.
helloName()
→hello_name
.
4.12.4. Validation
-
All message bundle templates are validated:
-
All expressions without a namespace must map to a parameter; e.g.
Hello {foo}
→ the method must have a param of namefoo
-
All expressions are validated against the types of the parameters; e.g.
Hello {foo.bar}
where the parameterfoo
is of typeorg.acme.Foo
→org.acme.Foo
must have a property of namebar
A warning message is logged for each unused parameter.
-
-
Expressions that reference a message bundle method, such as
{msg:hello(item.name)}
, are validated too.
4.12.5. Localization
The default locale specified via the quarkus.default-locale
config
property is used for the @MessageBundle
interface by default. However,
the io.quarkus.qute.i18n.MessageBundle#locale()
can be used to specify a
custom locale. Additionally, there are two ways to define a localized
bundle:
-
Create an interface that extends the default interface that is annotated with
@Localized
-
Create an UTF-8 encoded file located in the
src/main/resources/messages
directory of an application archive; e.g.msg_de.properties
.
While a localized interface enables easy refactoring, an external file might be more convenient in many situations. |
import io.quarkus.qute.i18n.Localized;
import io.quarkus.qute.i18n.Message;
@Localized("de") (1)
public interface GermanAppMessages extends AppMessages {
@Override
@Message("Hallo {name}!") (2)
String hello_name(String name);
}
1 | The value is the locale tag string (IETF). |
2 | The value is the localized template. |
Message 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
bundle msg
has default locale en
, then msg.properties
is going to be
treated as msg_en.properties
. If both msg.properties
and
msg_en.properties
are 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 by io.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 the java.util.Properties.load(Reader)
method.
msg_de.properties
# This comment is ignored
hello_name=Hallo {name}! (1) (2)
1 | Each 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. |
2 | Keys and values are separated by the equals sign. |
We use the .properties suffix in our example because most IDEs and text
editors support syntax highlighting of .properties files. But in fact, the
suffix could be anything - it is just ignored.
|
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 @MessageBundle the file
target/qute-i18n-examples/msg.properties is generated when the application
is build via mvn 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 the src/main/resources/messages
directory.
|
hello=Hello \
{name} and \
good morning!
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 as Hello 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-locale
configuration property is used to
select the bundle. Alternatively, you can specify the locale
attribute of
a template instance.
locale
Attribute Example@Singleton
public class MyBean {
@Inject
Template hello;
String render() {
return hello.instance().setAttribute("locale", Locale.forLanguageTag("cs")).render(); (1)
}
}
1 | You can set a Locale instance or a locale tag string (IETF). |
When using quarkus-resteasy-qute the locale
attribute is derived from the Accept-Language header if not set by a user.
|
The @Localized
qualifier can be used to inject a localized message bundle
interface.
@Singleton
public class MyBean {
@Localized("cs") (1)
AppMessages msg;
String render() {
return msg.hello_name("Jachym");
}
}
1 | The annotation value is a locale tag string (IETF). |
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.
import 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);
}
1 | The annotation value is not defined. In such a case, the value from supplementary localized file is taken. |
2 | The annotation value is defined and preferred to the value defined in the localized file. |
hello_name=Hello \
{name} and \
good morning!
goodbye=Best regards, {name} (1)
1 | The value is ignored as io.quarkus.qute.i18n.Message#value() is always
prioritized. |
Message templates are validated during the build. If a missing message template is detected, an exception is thrown and build fails.
4.13. Configuration Reference
Configuration property fixed at build time - All other configuration properties are overridable at runtime
Type |
Default |
|
---|---|---|
The list of suffixes used when attempting to locate a template file. By default, Environment variable: Show more |
list of string |
|
The list of exclude rules used to intentionally ignore some parts of an expression when performing type-safe validation. An element value must have at least two parts separated by dot. The last part is used to match the property/method name. The prepended parts are used to match the class name. The value Examples:
Environment variable: Show more |
list of string |
|
This regular expression is used to exclude template files from the The matched input is the file path relative from the By default, the hidden files are excluded. The name of a hidden file starts with a dot. Environment variable: Show more |
|
|
The prefix is used to access the iteration metadata inside a loop section. A valid prefix consists of alphanumeric characters and underscores. Three special constants can be used:
Environment variable: Show more |
string |
|
The list of content types for which the Environment variable: Show more |
list of string |
|
The default charset of the templates files. Environment variable: Show more |
|
|
By default, a template modification results in an application restart that triggers build-time validations. This regular expression can be used to specify the templates for which the application is not restarted. I.e. the templates are reloaded and only runtime validations are performed. The matched input is the template path that starts with a template root, and the Environment variable: Show more |
||
The strategy used when a standalone expression evaluates to a "not found" value at runtime and the This strategy is never used when evaluating section parameters, e.g. By default, the Environment variable: Show more |
|
|
Specify whether the parser should remove standalone lines from the output. A standalone line is a line that contains at least one section tag, parameter declaration, or comment but no expression and no non-whitespace character. Environment variable: Show more |
boolean |
|
If set to Note that the Environment variable: Show more |
boolean |
|
The global rendering timeout in milliseconds. It is used if no Environment variable: Show more |
long |
|
If set to Environment variable: Show more |
boolean |
|
The additional map of suffixes to content types. This map is used when working with template variants. By default, the Environment variable: Show more |
|
5. Qute Used as a Standalone Library
Qute is primarily designed as a Quarkus extension. However. it is possible to use it as a "standalone" library. In this case, some features are not available and some additional configuration is needed.
- Engine
-
-
First, no managed
Engine
instance is available out of the box. You’ll need to configure a new instance viaEngine.builder()
.
-
- Template locators
-
-
By default, no template locators are registered, i.e.
Engine.getTemplate(String)
will not work. -
You can register a custom template locator using
EngineBuilder.addLocator()
or parse a template manually and put the result in the cache viaEngine.putTemplate(String, Template)
.
-
- Template initializers
-
-
No
TemplateInstance.Initializer
is registered by default, therefore@TemplateGlobal
annotations are ignored. -
A custom
TemplateInstance.Initializer
can be registered withEngineBuilder#addTemplateInstanceInitializer()
and initialize a template instance with any data and attributes.
-
- Sections
-
-
No section helpers are registered by default.
-
The default set of value resolvers can be registered via the convenient
EngineBuilder.addDefaultSectionHelpers()
method and theEngineBuilder.addDefaults()
method respectively.
-
- Value resolvers
-
-
No
ValueResolver
s are generated automatically.-
@TemplateExtension
methods will not work. -
@TemplateData
and@TemplateEnum
annotations are ignored.
-
-
The default set of value resolvers can be registered via the convenient
EngineBuilder.addDefaultValueResolvers()
method and theEngineBuilder.addDefaults()
method respectively.Not all functionality provided by the built-in extension methods is covered by the default value resolvers. However, a custom value resolver can be easily built via the ValueResolver.builder()
. -
It’s recommended to register a
ReflectionValueResolver
instance viaEngine.addValueResolver(new ReflectionValueResolver())
so that Qute can access object properties and call public methods.Keep in mind that reflection may not work correctly in some restricted environments or may require additional configuration, e.g. registration in case of a GraalVM native image.
-
- User-defined Tags
-
-
No user-defined tags are registered automatically.
-
A tag can be registered manually via
Engine.builder().addSectionHelper(new UserTagSectionHelper.Factory("tagName","tagTemplate.html")).build()
-
- Type-safety
-
-
Type-safe Expressions are not validated.
-
Type-safe message bundles are not supported.
-
- Injection
-
It is not possible to inject a
Template
instance and vice versa - a template cannot inject a@Named
CDI bean via theinject:
andcdi:
namespace.