Qute リファレンスガイド
Qute は、Quarkus のニーズを満たすために特別に設計されたテンプレートエンジンです。ネイティブイメージのサイズを小さくするために、リフレクションの使用は最小限に抑えられています。API は、命令型とノンブロッキングリアクティブ型の両方のスタイルのコーディングを組み合わせています。開発モードでは、src/main/resources/templates
フォルダーにあるすべてのファイルが変更があるかどうか監視され、変更はアプリケーションにすぐに表示されます。さらに、ビルド時にテンプレートの問題のほとんどの検出を試み、即座に失敗します。
このガイドでは、 入門編のサンプル 、 コア機能 の説明、 Quarkus 統合 の詳細について説明しています。
Qute は、主に Quarkus のエクステンションとして設計されています。 「スタンドアロン」のライブラリーとして使用することも可能です。 しかし、その場合、いくつかの機能は使用できません。 一般的に、 Quarkus 統合 のセクションで言及されている機能は使えません。 制限と可能性については、 スタンドアローンライブラリーとして使用する場合のQute セクションに詳細が記載されています。 |
1. 最も簡単な例
Qute を試す最も簡単な方法は、便利な io.quarkus.qute.Qute
クラスを使用し、簡単なメッセージをフォーマットするために使用できる fmt()
静的メソッドの 1 つを呼び出すことです。
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 | 空の式 {} はプレースホルダーであり、インデックスベースの配列アクセスサー (例えば {data[0]} ) に置き換えられます。 |
2 | 代わりにデータマップを提供することができます。 |
3 | より複雑なフォーマットを必要とする場合には、ビルダーライクな API が利用可能です。 |
4 | "text/html" テンプレートでは、デフォルトで特殊な char が html エンティティーに置き換えられることに注意してください。 |
5 | テンプレートには、任意の ビルディングブロック を使用することができます。この場合、If セクション は、入力データに基づいてメッセージの適切な部分をレンダリングするために使用されます。 |
Quarkus では、メッセージのフォーマットに使用されるエンジンは @Inject Engine によって注入されるものと同じです。したがって、テンプレート拡張メソッド、テンプレートに直接Beansを注入 または タイプセーフメッセージバンドル のような Quarkus 固有の統合機能を利用することができます。
|
Qute.fmt(String)
メソッドが返すフォーマットオブジェクトは、遅延評価され、たとえば、ログメッセージとして使用することができます。
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
詳細は io.quarkus.qute.Qute クラスの javadoc をお読みください。
|
2. ハローワールドの例
この例では、Quteのテンプレートを扱う際の基本的なワークフローをデモしたいと思います。まず、簡単なhello worldの例から始めましょう。 テンプレートのコンテンツ は必ず必要になります。
<html>
<p>Hello {name}! (1)
</html>
1 | {name} は、テンプレートがレンダリングされたときに評価される値の式です。 |
そして、その内容を テンプレート定義 Javaオブジェクトにパースする必要があります。テンプレート定義は、 io.quarkus.qute.Template
のインスタンスです。
Quteを"スタンドアロン"で使用する場合は、まず io.quarkus.qute.Engine
のインスタンスを作成する必要があります。 Engine
は、専用の設定でテンプレートを管理するための一元的なポイントを表しています。便利なビルダーを使ってみましょう。
Engine engine = Engine.builder().addDefaults().build();
Quarkusでは、あらかじめ設定された Engine がインジェクション用に利用可能です - quarkus_integration を参照してください。
|
Engine
のインスタンスがあれば、テンプレートの内容を解析することができます。
Template hello = engine.parse(helloHtmlContent);
Quarkusでは、テンプレートの定義を単純に注入することができます。テンプレートは自動的に解析され、キャッシュされます - quarkus_integration を参照してください。 |
最後に テンプレートインスタンス を作成し、データをセットして出力をレンダリングします。
// Renders <html><p>Hello Jim!</p></html>
hello.data("name", "Jim").render(); (1) (2)
1 | Template.data(String, Object) は、ワンステップでテンプレートのインスタンスを作成してデータを設定する便利なメソッドです。 |
2 | TemplateInstance.render() は同期レンダリングをトリガーします。つまり、レンダリングが終了するまで現在のスレッドはブロックされます。しかし、レンダリングをトリガーして結果を消費する非同期の方法もあります。たとえば、 CompletionStage<String> TemplateInstance.renderAsync() を返す メソッドや、Mutiny の Multi<String> TemplateInstance.createMulti() を返すメソッドがあります。 |
なので、ワークフローはシンプルです。
-
テンプレートコンテンツを作成する (
hello.html
) -
テンプレートの定義を解析する (
io.quarkus.qute.Template
) -
テンプレートインスタンスを作成する (
io.quarkus.qute.TemplateInstance
) -
出力をレンダリングする
Engine はテンプレート定義をキャッシュすることができるので、何度も内容をパースする必要がありません。Quarkus では、キャッシュは自動的に行われます。
|
3. コア機能
3.1. 基本的なビルディングブロック
テンプレートの動的なパーツには、コメント、式、セクション、パースされていない文字データなどがあります。
- コメント
-
コメントは
{!
というシーケンスで始まり、!}
というシーケンスで終わり、例えば{! This is a comment !}
のようになります。 複数行にすることができ、{! {#if true} !}
のように式やセクションを含むことができます。コメントの内容は、出力をレンダリングする際に完全に無視されます。 - 式
-
式 は評価値を出力します。これは 1つ以上のパーツから構成されます。パーツは
{foo}
、{item.name}
のような単純なプロパティーや、{item.get(name)}
、{name ?: 'John'}
のような仮想メソッドを表わすことができます。また、式は名前空間{inject:colors}
で始まることもあります。 - セクション
-
section には、静的テキスト、式、ネストされたセクションを含めることができます (
{#if foo.active}{foo.name}{/if}
)。 クロージングタグ内の名前はオプションです ({#if active}ACTIVE!{/}
)。 セクションは空にできます ({#myTag image=true /}
)。 一部のセクションでは、オプションの終了タグがサポートされます。つまり、終了タグがなければ、セクションは親セクションの終了位置で終了します。 セクションは、ネストしたセクションブロックを宣言することもでき ({#if item.valid} Valid. {#else} Invalid. {/if}
)、どのブロックをレンダリングするかを決定します。 - パース対象外文字データ
-
レンダリングは行うが パースは行わない という内容をマークするために使用されます。これは
{|
というシーケンスで始まり、|}
というシーケンスで終わり({|<script>if(true){alert('Qute is cute!')};</script>|}
、複数行になる可能性もあります。以前はパース対象外文字データを
{[
and end with]}
から始めることができました。この構文は、他の言語の構文とよく衝突するため、現在では削除されています。
3.2. 識別子とタグ
識別子は、式やセクションタグで使用されます。有効な識別子は、空白文字以外の文字列です。ただし、式の中では有効な Java 識別子のみを使用することが推奨されます。
ドットを含む識別子を指定する必要がある場合は、ブラケット表記を使用することができます (例: {map['my.key']} )。
|
テンプレート文書を解析する際、パーサはすべての タグ を識別します。タグは中括弧で始まり、中括弧で終わります。例: {foo}
タグの内容は次から始まる必要があります:
-
数字、または
-
アルファベット文字、または
-
アンダースコア、または
-
組み込みコマンド (
#
、!
、@
、/
)
上記のいずれかで始まらない場合は、パーサによって無視されます。
<html>
<body>
{_foo.bar} (1)
{! comment !}(2)
{ foo} (3)
{{foo}} (4)
{"foo":true} (5)
</body>
</html>
1 | パース対象: アンダースコアで始まる式 |
2 | パース対象: コメント |
3 | 無視対象: 空白で開始 |
4 | 無視対象: { で開始 |
5 | 無視対象: " で開始 |
エスケープシーケンス \{ and \} を使ってテキストにデリミタを挿入することも可能です。 実際には、エスケープシーケンスは通常、開始デリミタのためにのみ必要とされます。すなわち、\{foo} は {foo} として描画されます(パース、評価は行われません)。
|
3.3. テンプレートから独立した行を削除する
デフォルトでは、パーサはテンプレート出力からスタンドアロン行を削除します。スタンドアロン 行 とは、少なくとも1つのセクションタグ(例: {#each}
や {/each}
)、パラメーター宣言(例: {@org.acme.Foo foo}
)、コメントを含むが、式を含まず、空白以外の文字を含まない行のことです。言い換えれば、セクションタグやパラメーター宣言を含まない行は、独立した行ではあり ません 。同様に、 式 や 空白以外の文字 を含む行もスタンドアロン行ではあり ません 。
<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 | これはスタンドアロン行なので削除されます。 |
2 | スタンドアロン行ではない - 式と非空白文字を含む |
3 | スタンドアロン行ではない - セクションタグ/パラメーター宣言を含まない |
4 | これはスタンドアロン行です。 |
<html>
<body>
<ul>
<li>Foo 100</li>
</ul>
</body>
</html>
Quarkusでは、プロパティー( quarkus.qute.remove-standalone-lines )を false に設定することで、デフォルトの動作を無効にすることができます。この場合、スタンドアロン行のすべての空白文字が出力されます。
|
quarkus.qute.remove-standalone-lines=false
での出力<html>
<body>
<ul>
<li>Foo 100</li>
</ul>
</body>
</html>
3.4. 式
式が評価され、値が出力されます。これには 1 つ以上のパーツがあり、各パーツはプロパティーアクセサー (別名 Field Access Expression) または仮想メソッド呼び出し (別名 Method Invocation Expression) のいずれかを表します。
プロパティーにアクセスする際には、ドット表記とブラケット表記のどちらかを使用できます。
object.property
(ドット表記) 構文では、property
は valid identifier でなければなりません。
object[property_name]
(ブラケット表記) 構文では、property_name
は null 以外の literal 値でなければなりません。
式は、名前空間の後にコロン (:
) を付けて開始できます。
有効な名前空間は英数字とアンダースコアで構成されます。
名前空間の式は別の方法で解決されます。解決 も参照してください。
{name} (1)
{item.name} (2)
{item['name']} (3)
{global:colors} (4)
1 | 名前空間がなし、1 つの部分: name |
2 | 名前空間なし、2つの部分: item . name |
3 | {item.name} と同じですが、ブラケット表記を使います。 |
4 | 名前空間 global , 1つのパーツ: colors |
式の一部を 仮想メソッド とすることができ、その場合、名前の後にコンマで区切られたパラメーターのリストを括弧で囲みます。 仮想メソッドのパラメーターは、ネストされた式か、literal 値のどちらかにすることができます。 これらのメソッドを "仮想" と呼ぶのは、実際の Java メソッドによってバックアップされる必要がないためです。 仮想メソッドの詳細については、following section を参照してください。
{item.getLabels(1)} (1)
{name or 'John'} (2)
1 | 名前空間なし、2つのパーツ - item , getLabels(1) 、2番目のパーツは、名前 getLabels と パラメーター `1`を持つ仮想メソッドです。 |
2 | name.or('John') に翻訳される、単一のパラメーターを持つ仮想メソッドに使用できる 中置 (infix) 記法。名前空間なし、2 つのパーツ - name . or('John') |
3.4.1. サポートされているリテラル
リテラル | 例 |
---|---|
ブーリアン |
|
null |
|
string |
|
integer |
|
長 |
|
double |
|
float |
|
3.4.2. 解決
When evaluating expressions a list of registered value resolvers is used.
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.
例えば、式 {name}
には名前空間がなく、単一のパーツ - name
です。"name"は、カレントコンテキストオブジェクトに対して利用可能なすべての値リゾルバを使用して解決されます。しかし、式 {global:colors}
には、名前空間 global
と単一パーツ - colors
があります。まず、現在のコンテキストオブジェクトを見つけるために、利用可能なすべての NamespaceResolver
s が使用されます。その後、見つかったコンテキストオブジェクトに対して"colors"を解決するために値リゾルバが使用されます。
テンプレート・インスタンスに渡されたデータは、常に
|
3.4.3. カレントコンテキスト
式が名前空間を指定しない場合、カレントコンテキストオブジェクト はタグの位置から導出されます。
デフォルトでは、現在のコンテキストオブジェクトは、テンプレートインスタンスに渡されたデータを表します。
ただし、セクションは現在のコンテキストオブジェクトを変更することがあります。
典型的な例は、名前付きローカル変数を定義するために使用できる let
セクションです。
{#let myParent=order.item.parent myPrice=order.price} (1)
<h1>{myParent.name}</h1>
<p>Price: {myPrice}</p>
{/let}
1 | セクション内の現在のコンテキストオブジェクトは、解決されたパラメーターのマップです。 |
現在のコンテキストは、暗黙のバインディング this を通してアクセスできます。
|
3.4.4. 組込リゾルバ
Name | Description | 例 |
---|---|---|
エルビス演算子 |
前のパーツが解決できなかったり、 |
|
orEmpty |
前のパーツが解決できなかったり、 |
|
Ternary Operator |
if-then-else文の省略形。 if_section] とは異なり、入れ子になった演算子はサポートされていません。 |
|
論理 AND 演算子 |
If セクション で説明したように、両方のパーツが |
|
論理 OR 演算子 |
if_section] で説明したように、いずれかのパーツが |
|
If セクション で説明したように、三項演算子の条件は値が falsy とみなされない場合は true と評価されます。
|
実際、オペレーターは 1 つのパラメーターを消費する "仮想メソッド" として実装されており、infix 記法で使用することができます。例えば、{person.name or 'John'} は {person.name.or('John')} に、{item.isActive ? item.name : 'Inactive item'} は {item.isActive.ifTruthy(item.name).or('Inactive item')} に変換されます。
|
3.4.5. 配列
ループセクション を使用して、配列の要素を反復処理することができます。
また、指定した配列の長さを取得し、インデックス値を用いて直接要素にアクセスすることも可能です。
さらに、take(n)/takeLast(n)
メソッドを使用して最初または最後の n
要素にアクセスできます。
<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 | 配列の長さを出力します。 |
2 | 配列の最初の要素を出力します。 |
3 | ブラケット表記で配列の 2 つ目の要素を出力します。 |
4 | 配列の 3 番目の要素を仮想メソッド get() で出力します。 |
5 | 配列の最初の 2 要素を出力します。 |
3.4.6. 文字エスケープ
HTML と XML テンプレートの場合、'
、"
、<
, >
、&
の文字は、テンプレートバリアントが設定されている場合はデフォルトでエスケープされます。
Quarkusでは、 src/main/resources/templates にあるテンプレートに対してバリアントが自動的に設定されます。デフォルトでは、テンプレートファイルのコンテンツタイプを決定するために java.net.URLConnection#getFileNameMap() が使用されます。コンテンツタイプへのサフィックスの追加マップは、 quarkus.qute.content-types を通じて設定できます。
|
エスケープされていない値をレンダリングする必要がある場合:
-
java.lang.Object
の拡張メソッドとして実装されているraw
またはsafe
プロパティーを使用します。 -
String
の値をio.quarkus.qute.RawString
でラップします。
<html>
<h1>{title}</h1> (1)
{paragraph.raw} (2)
</html>
1 | title が Expressions & Escapes に解決した場合は、 Expressions & Escapes としてレンダリングされます。 |
2 | <p>My text!</p> に解決する paragraph は、<p>My text!</p> としてレンダリングされます。 |
デフォルトでは、text/html 、text/xml 、application/xml および application/xhtml+xml コンテンツタイプのいずれかを持つテンプレートがエスケープされます。しかし、quarkus.qute.escape-content-types 設定プロパティーで、このリストを拡張することができます。
|
3.4.7. 仮想メソッド
仮想メソッドとは、通常のJavaメソッド呼び出しのように見える 式のパーツ です。Javaクラスの実際のメソッドと一致する必要がないため、「仮想」と呼ばれています。実際には、通常のプロパティーと同様に、仮想メソッドも値リゾルバによって処理されます。唯一の違いは、仮想メソッドの場合、値リゾルバが式でもあるパラメーターを消費することです。
<html>
<h1>{item.buildName(item.name,5)}</h1> (1)
</html>
1 | buildName(item.name,5) は、名前 buildName と 2 つのパラメーター item.name と 5 を持つ仮想メソッドを表します。この仮想メソッドは、以下の Java クラスに対して生成された値リゾルバによって評価されます。
|
仮想メソッドは通常、 @TemplateExtension methods 、 @TemplateData 、または parameter declarations で使用されるクラスのために生成された値リゾルバによって評価されます。しかし、Javaのクラス/メソッドに基づかないカスタムの値リゾルバも登録できます。 |
単一のパラメーターを持つ仮想メソッドは 中置記法を使用して呼び出すことができます。
<html>
<p>{item.price or 5}</p> (1)
</html>
1 | item.price or 5 は item.price.or(5) に翻訳されます。 |
仮想メソッドのパラメーターは、「入れ子」仮想メソッドの呼び出しにすることができます。
<html>
<p>{item.subtractPrice(item.calculateDiscount(10))}</p> (1)
</html>
1 | item.calculateDiscount(10) が最初に評価され、その後 item.subtractPrice() への引数として渡されます。 |
3.4.8. CompletionStage
と Uni
オブジェクトの評価
java.util.concurrent.CompletionStage
および io.smallrye.mutiny.Uni
を実装したオブジェクトは、特別な方法で評価されます。式の一部が CompletionStage
に解決された場合、このステージが完了すると解決が継続され、式の次の部分(もしあれば)が完了したステージの結果に対して評価されます。たとえば、式 {foo.size}
があり、 foo
が CompletionStage<List<String>>
に解決された場合、 size
は完了した結果、すなわち List<String>
に対して解決されます。式の一部が Uni
に解決される場合、 CompletionStage
が Uni#subscribeAsCompletionStage()
を使用して Uni
から最初に作成され、その後上記のように評価されます。
各 Uni#subscribeAsCompletionStage() は新しいサブスクリプションになることに注目してください。 Uni の項目や障害がテンプレートデータとして使用される前に、メモ化を設定する必要があるかもしれません。つまり、 myUni.memoize().indefinitely() .
|
CompletionStage
が完了しなかったり、あるいは Uni
がアイテム/失敗を発しないということが起こり得ます。この場合、レンダリングメソッド (TemplateInstance#render()
および TemplateInstance#createUni()
) は、特定のタイムアウトを過ぎると失敗します。このタイムアウトは、テンプレートインスタンスの timeout
属性として指定することができます。timeout
属性が設定されていない場合、グローバルレンダリングタイムアウトが使用されます。
Quarkus では、デフォルトのタイムアウトは io.quarkus.qute.timeout 設定プロパティーで設定することができます。Qute をスタンドアロンで使用する場合は、EngineBuilder#timeout() メソッドを使用することができます。
|
以前のバージョンでは、TemplateInstance#render() メソッドだけがタイムアウト属性を尊重していました。たとえば、templateInstance.createUtni().ifNoItem().after(Duration.ofMillis(500)).fail() のように、io.quarkus.qute.useAsyncTimeout=false config プロパティーを使って、古い動作を保持しタイムアウトを自分で処理することが可能です。
|
3.4.8.1. テンプレートの問題個所の特定方法
タイムアウトが発生したときに、テンプレートの問題のあるパーツを見つけるのは簡単ではありません。ロガー io.quarkus.qute.nodeResolve
の TRACE
レベルを設定し、その後ログ出力の解析を試みることができます。
application.properties
の例quarkus.log.category."io.quarkus.qute.nodeResolve".min-level=TRACE
quarkus.log.category."io.quarkus.qute.nodeResolve".level=TRACE
テンプレートで使用されるすべての式とセクションについて、次のような一対のログメッセージが表示されるはずです。
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
もし、 completed
のログメッセージがない場合は、調査すべき良い候補があります。
3.4.9. 不明なプロパティ
式が実行時に評価されない場合があります。たとえば、{person.age}
という式があり、Person
クラスに age
というプロパティーが宣言されていない場合です。strict レンダリング が有効かどうかによって、動作が異なってきます。
これを有効にすると、プロパティーが見つからない場合、常に TemplateException
が発生し、レンダリングが中断されます。このエラーを抑制するために、default values や safe expressions を使用することができます。
無効な場合、デフォルトで特別な定数 NOT_FOUND
が出力に書き込まれます。
Quarkus では、[設定リファレンス] で説明しているように、quarkus.qute.property-not-found-strategy を使用して、デフォルトのストラテジーを変更することが可能です。
|
タイプセーフな式 と タイプセーフテンプレート を使用した場合、ビルド時に同様のエラーが検出されます。 |
3.5. セクション
セクションは #
で始まる開始タグを持ち、その後に {#if}
や {#each}
のようなセクション名が続きます。
また、空でも構いません。その場合、開始タグは /
で終了します ({#myEmptySection /}
)。
セクションには通常、ネストされた式や他のセクションが含まれています。
終了タグは /
で始まり、セクションの名前 (オプション) が含まれます ({#if foo}Foo!{/if}または `{#if foo}Foo!{/}
)。
一部のセクションではオプションの終了タグがサポートされています。終了タグがない場合、セクションは親セクションの終了位置で終了します。
#let
オプションの終了タグの例{#if item.isActive}
{#let price = item.price} (1)
{price}
// synthetic {/let} added here automatically
{/if}
// {price} cannot be used here!
1 | 親 {#if} セクション内で使用できるローカル変数を定義します。 |
組み込みセクション | オプションの終了タグをサポート |
---|---|
|
❌ |
|
❌ |
|
❌ |
|
✅ |
|
❌ |
|
✅ |
ユーザー定義タグ |
❌ |
|
❌ |
|
❌ |
3.5.1. パラメーター
開始タグはオプションの名前を持つパラメーターを定義できます (例: {#if item.isActive}
、{#let foo=1 bar=false}
)。
パラメーターは 1 つ以上のスペースで区切られます。
名前は、統合により値と区切られます。
名前と値には、任意の数のスペースを接頭辞と接尾辞として追加できます。たとえば {#let id='Foo'}
と {#let id = 'Foo'}
は、パラメーター名が id
で値が Foo
である点で同等です。
値は括弧を使用してグループ化できます。たとえば {#let id=(item.id ?: 42)}
は、名前が id
、値は item.id ?: 42
です。
セクションはパラメーター値を任意の方法で (たとえば値をそのまま) 解釈できます。
しかし、ほとんどの場合、パラメーター値は expression として登録され、使用前に評価されます。
1つのセクションには、いくつかのコンテンツ ブロック を含めることができます。"main" ブロックは常に存在します。追加/入れ子になったブロックも #
で始まり、パラメーターを持つことができます - {#else if item.isActive}
。セクションのロジックを定義するセクションヘルパーは、任意のブロックを「実行」し、パラメーターを評価することができます。
{#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 | これがメインブロックです。 |
2 | 追加ブロック。 |
3 | 追加ブロック。 |
3.5.2. ループセクション
The loop section makes it possible to iterate over an instance of Iterable
, Iterator
, array, Map
(element is a Map.Entry
), Stream
, Integer
, Long
, int
and long
(primitive value).
A null
parameter value results in a no-op.
このセクションには 2 つのフレーバーがあります。最初のものは each
という名前を使い、it
は反復処理要素の暗黙のエイリアスです。
{#each items}
{it.name} (1)
{/each}
1 | name は現在の反復要素に対して解決されます。 |
もう一つの形式は、for
という名前を使い、反復要素を参照するために使われるエイリアスを指定します。
{#for item in items} (1)
{item.name}
{/for}
1 | item は、反復処理要素に使用されるエイリアスです。 |
また、ループ内の反復処理メタデータには、以下のキーでアクセスすることが可能です。
-
count
- 1 を基準としたインデックス -
index
- ゼロ基準のインデックス -
hasNext
- イテレーションにさらに要素がある場合はtrue
とります。 -
isLast
-true
ifhasNext == false
-
isFirst
-true
ifcount == 1
-
odd
- 要素のカウントが奇数である場合true
-
hasNext
- イテレーションにさらに要素がある場合にtrue
-
indexParity
- は、カウント値に応じてodd
またはeven
を出力します
しかし、キーを直接使うことはできません。その代わり、外部スコープの変数と競合する可能性を避けるために、接頭辞が使用されます。デフォルトでは、反復処理された要素のエイリアスの後にアンダースコアを付けたものが接頭辞として使用されます。たとえば、{#each}
セクション ({it_hasNext}
) の中では、hasNext
キーは it_
を接頭辞として使用する必要があります。
each
イテレーションのメタデータの例{#each items}
{it_count}. {it.name} (1)
{#if it_hasNext}<br>{/if} (2)
{/each}
1 | it_count は 1 を基準としたインデックスを表します。 |
2 | <br> は、反復処理がより多くの要素を持っている場合にのみレンダリングされます。 |
また、{#for}
セクションの中で {item_hasNext}
の形式で、item
要素のエイリアスと使用する必要があります。
for
イテレーションのメタデータの例{#for item in items}
{item_count}. {item.name} (1)
{#if item_hasNext}<br>{/if} (2)
{/for}
1 | item_count は 1 を基準としたインデックスを表します。 |
2 | <br> は、反復処理がより多くの要素を持っている場合にのみレンダリングされます。 |
反復処理メタデータの接頭辞は、スタンドアロンの Qute では
|
for
文は、1 から始まる整数でも動作します。 以下の例では、 total = 3
です。
{#for i in total}
{i}: ({i_count} {i_indexParity} {i_even})<br>
{/for}
出力は、以下のようになります。
1: (1 odd false)
2: (2 even true)
3: (3 odd false)
ループセクションは、反復する項目がないときに実行される {#else}
ブロックも定義することができます。
{#for item in items}
{item.name}
{#else}
No items.
{/for}
3.5.3. If セクション
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}
条件では、以下の演算子を使うこともできます:
オペレーター | エイリアス | Precedence | 例 | Description |
---|---|---|---|---|
logical complement |
|
4 |
|
Inverts the evaluated value. |
greater than |
|
3 |
|
Evaluates to |
greater than or equal to |
|
3 |
|
Evaluates to |
less than |
|
3 |
|
Evaluates to |
less than or equal to |
|
3 |
|
Evaluates to |
equals |
|
2 |
|
Evaluates to |
not equals |
|
2 |
|
Evaluates to |
logical AND (short-circuiting) |
|
1 |
|
Evaluates to |
logical OR (short-circuiting) |
|
1 |
|
Evaluates to |
For >
, >=
, <
, and <=
the following rules are applied:
-
Neither of the operands may be
null
. -
If both operands are of the same type that implements the
java.lang.Comparable
then theComparable#compareTo(T)
method is used to perform comparison. -
Otherwise, both operands are coerced to
java.math.BigDecimal
first and then theBigDecimal#compareTo(BigDecimal)
method is used to perform comparison.
Types that support coercion include BigInteger , Integer , Long , Double , Float and String .
|
For ==
and !=
the following rules are applied:
-
Operands are first tested using the
java.util.Objects#equals(Object, Object)
method. If it returnstrue
the operands are considered equal. -
Otherwise, if both operands are not
null
and at least one of them is an instance ofjava.lang.Number
, then operands are coerced tojava.math.BigDecimal
and theBigDecimal#compareTo(BigDecimal)
method is used to perform comparison.
複数の条件にも対応しています。
{#if item.age > 10 && item.price > 500}
This item is very old and expensive.
{/if}
The default precedence rules (higher precedence wins) 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}
また、 else
ブロックを何個でも追加することができます。
{#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 セクション
このセクションは、Java の switch
や Kotlin の when
に似ています。条件が満たされるまで、 テストされた値 をすべてのブロックに対して順次マッチさせます。最初にマッチしたブロックが実行されます。他のすべてのブロックは無視されます(この動作は、 break
文が必要な Java switch
とは異なります)。
when
/ is
の名前のエイリアスを使用した例{#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 | パラメーターが1つだけの場合、それは等価かがテストされます。 |
2 | an operator を使用して、一致するロジックを指定できます。 If セクション とは異なり、ネストされた演算子はサポートされません。 |
3 | else は、他のブロックが値に一致しない場合に実行されます。 |
switch
/ case
の名前エイリアスを使用した例{#switch person.name}
{#case 'John'} (1)
Hey John!
{#case 'Mary'}
Hey Mary!
{/switch}
1 | case は is の別名です。 |
enum に解決されるテスト値は特別な扱いを受けます。 is
/ case
ブロックのパラメーターは式として評価されるのではなく、テストされた値に対して toString()
を呼び出した結果と比較されます。
{#when machine.status}
{#is ON}
It's running. (1)
{#is in OFF BROKEN}
It's broken or OFF. (2)
{/when}
1 | このブロックは machine.status.toString().equals("ON") の場合に実行されます |
2 | このブロックは、 machine.status.toString().equals("OFF") または machine.status.toString().equals("BROKEN") の場合に実行されます |
enum 定数は、テストされた値が利用可能な型情報を持ち、列挙型に解決された場合に検証されます。 |
is
/ case
ブロック条件では、以下の演算子がサポートされています。
オペレーター | エイリアス | 例 |
---|---|---|
not equal |
|
|
greater than |
|
|
greater than or equal to |
|
|
less than |
|
|
less than or equal to |
|
|
in |
|
|
not in |
|
|
3.5.5. Let セクション
このセクションでは、名前付きローカル変数を定義することができます。
{#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 | ローカル変数は、literal を表すことができる式 (つまり isActive=false と age=10 ) で初期化されます。 |
2 | 中置記法は、括弧を使用してグループ化する場合にのみサポートされます。例えば、 price=(order.price + 10) は price=order.price.plus(10) と同等です。 |
3 | この変数は、let セクションを定義している外では利用できないことに留意してください。 |
ローカル変数名などのセクションパラメータのキーが ?
で終わっている場合、 ?
を除いたキーが null
または "not found" に解決される場合にのみ、ローカル変数が設定されます:
{#let enabled?=true} (1) (2)
{#if enabled}ON{/if}
{/let}
1 | true は事実上 default value で、親スコープが enabled をすでに定義していない場合にのみ使用されます。 |
2 | enabled?=true は enabled=enabled.or(true) の短縮版です。 |
このセクションタグも set
のエイリアスで登録されています。
{#set myParent=item.parent price=item.price}
<h1>{myParent.name}</h1>
<p>Price: {price}
{/set}
3.5.6. With セクション
このセクションは、現在のコンテキストオブジェクトを設定するために使用することができます。これはテンプレート構造を単純化するのに便利かもしれません。
{#with item.parent}
<h1>{name}</h1> (1)
<p>{description}</p> (2)
{/with}
1 | name は item.parent に対して解決されます。 |
2 | description も item.parent に対して解決されます。 |
タイプセーフテンプレート や、タイプセーフな式 を定義するテンプレートでは、
|
このセクションは、複数の高価な呼び出しを避けたいときにも便利かもしれません。
{#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 は item.callExpensiveLogicToGetTheValue(1,'foo',bazinga) の結果です。結果が複数の式で使用されても、このメソッドは一度しか呼び出されません。 |
3.5.7. Include セクション
このセクションは、別のテンプレートをインクルードし、場合によってはテンプレートの一部をオーバーライドするために使用することができます (下記の テンプレートの継承 を参照)。
<html>
<head>
<meta charset="UTF-8">
<title>Simple Include</title>
</head>
<body>
{#include foo limit=10 /} (1)(2)
</body>
</html>
1 | id foo でテンプレートをインクルードします。インクルードされたテンプレートは、現在のコンテキストからデータを参照することができます。 |
2 | また、インクルードされたテンプレートで使用できるオプションのパラメーターを定義することも可能です。 |
テンプレートの継承 により、テンプレートレイアウトの再利用が可能になります。
<html>
<head>
<meta charset="UTF-8">
<title>{#insert title}Default Title{/}</title> (1)
</head>
<body>
{#insert}No body!{/} (2)
</body>
</html>
1 | insert セクションは、指定されたテンプレートをインクルードするテンプレートでオーバーライドできるパーツを指定するために使われます。 |
2 | insert セクションは、オーバーライドされない場合にレンダリングされるデフォルトのコンテンツを定義することができます。もし名前が与えられていない場合は、関連する {#include} セクションのメインブロックが使用されます。 |
{#include base} (1)
{#title}My Title{/title} (2)
<div> (3)
My body.
</div>
{/include}
1 | include セクションは、拡張テンプレートを指定するために使用されます。 |
2 | 入れ子になったブロックは、オーバーライドするパーツを指定するために使用されます。 |
3 | メインブロックのコンテンツは、name パラメーターが指定されていない {#insert} セクションに使用されます。 |
セクションブロックは、オプションのエンドタグ ({/title} ) を定義することもできます。
|
3.5.8. ユーザー定義タグ
ユーザー定義タグは、タグテンプレート をインクルードし、オプションでいくつかの引数を渡し、場合によってはテンプレートの一部をオーバーライドするために使用することができます。たとえば、itemDetail.html
というタグテンプレートがあるとします。
{#if showImage} (1)
{it.image} (2)
{nested-content} (3)
{/if}
1 | showImage は名前付きパラメーターです。 |
2 | it は、タグの最初の名前のないパラメーターに置き換えられる特別なキーです。 |
3 | (オプション) nested-content は、タグの内容に置き換えられる特別なキーです。 |
Quarkus では、src/main/resources/templates/tags
にあるすべてのファイルが、自動的に登録および監視されます。Qute スタンドアロンでは、解析されたテンプレートを itemDetail.html
という名前で置き、関連する UserTagSectionHelper
をエンジンに登録する必要があります。
Engine engine = Engine.builder()
.addSectionHelper(new UserTagSectionHelper.Factory("itemDetail","itemDetail.html"))
.build();
engine.putTemplate("itemDetail.html", engine.parse("..."));
そして、以下のようなタグを呼び出すことができます。
<ul>
{#for item in items}
<li>
{#itemDetail item showImage=true} (1)
= <b>{item.name}</b> (2)
{/itemDetail}
</li>
{/for}
</ul>
1 | item は反復要素に解決され、タグテンプレートで it キーを使用して参照することができます。 |
2 | タグテンプレートで nested-content キーを使用して注入されたタグコンテンツ。 |
デフォルトでは、タグテンプレートは親コンテキストからデータを参照できません。
Qute はタグを 分離された テンプレートとして実行します。つまり、タグを呼び出すテンプレートのコンテキストにアクセスしません。
ただし、デフォルトの動作を変更して分離を無効にすると便利な場合があります。
その場合、呼び出しサイトに _isolated=false
または _unisolated
引数を追加するします (例: {#itemDetail item showImage=true _isolated=false /}
または {#itemDetail item showImage=true _unisolated /}
)。
3.5.8.1. 引数
タグテンプレート内で、名前付き引数に直接アクセスできます。
ただし、最初の引数で名前を定義する必要はなく、it
エイリアスを使用してアクセスできます。
さらに、引数に名前が定義されておらず、値が foo
など単一の識別子である場合、名前はデフォルトで値の識別子に設定されます (たとえば {#myTag foo /}
は {#myTag foo=foo /}
になります)。
つまり、引数値 foo
は解決され、タグテンプレート内で {foo}
を使用してアクセスできます。
引数に名前がなく、値が "foo" のような 1 つの単語の文字列リテラルである場合、その名前がデフォルトに設定されて引用符は削除されます (たとえば {#myTag "foo" /} は {#myTag foo="foo" /} になります)。
|
io.quarkus.qute.UserTagSectionHelper.Arguments
メタデータは、_args
エイリアスを使用してタグ内でアクセスできます。
-
_args.size
- タグに渡された引数の実際の数を返します。 -
_args.empty
/_args.isEmpty
- 引数が渡されない場合はtrue
を返します。 -
_args.get(String name)
- 指定された名前の引数値またはnull
を返します。 -
_args.filter(String…)
- 指定された名前に一致する引数を返します。 -
_args.filterIdenticalKeyValue
- 値と等しい名前を持つ引数を返します。通常は{#test foo="foo" bar=true}
または{#test "foo" bar=true /}
から`foo` が返されます。 -
_args.skip(String…)
- 指定された名前と一致しない引数のみを返します。 -
_args.skipIdenticalKeyValue
- 値と等しくない名前を持つ引数のみを返します。通常は{#test foo="foo" bar=true /}
からbar
が返されます。 -
_args.skipIt
- 最初の名前なし引数を除くすべての引数を返します。通常は{#test foo bar=true /}
からbar
が返されます。 -
_args.asHtmlAttributes
- 引数を HTML 属性としてレンダリングします (例:foo="true" readonly="readonly"
)。引数は名前のアルファベット順にソートされ、'
、"
、<
,>
、&
の文字はエスケープされます。
_args
は 反復可能な java.util.Map.Entry
でもあります ({#each _args}{it.key}={it.value}{/each}
)。
たとえば、以下のように定義されたユーザータグを {#test 'Martin' readonly=true /}
で呼び出すことができます。
tags/test.html
{it} (1)
{readonly} (2)
{_args.filter('readonly').asHtmlAttributes} (3)
1 | it はタグの最初の名前なしパラメーターに置き換えられます。 |
2 | readonly は名前付きパラメーターです。 |
3 | _args は引数のメタデータを表します。 |
結果は次のようになります。
Martin
true
readonly="true"
3.5.8.2. 継承
ユーザータグも、通常の {#include}
セクションと同じように、テンプレートの継承を利用することができます。
myTag
This is {#insert title}my title{/title}! (1)
1 | insert セクションは、指定されたテンプレートをインクルードするテンプレートでオーバーライドできるパーツを指定するために使われます。 |
<p>
{#myTag}
{#title}my custom title{/title} (1)
{/myTag}
</p>
1 | 結果は、<p>これは私のカスタムタイトル!</p> のようになります。 |
3.5.9. フラグメント
フラグメントは、別個のテンプレートとして扱える、つまり別々にレンダリングできるテンプレートの一部分を表します。この機能を導入した主な動機の1つは、 htmxフラグメント のようなユースケースをサポートするためでした。
フラグメントは {#fragment}
セクションで定義することができます。 各フラグメントは、英数字とアンダースコアのみからなる識別子を持ちます。
フラグメント識別子は、テンプレート内で一意でなければならないことに注意してください。 |
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 | 識別子 item_aliases を持つフラグメントを定義します。識別子には、英数字とアンダースコアしか使用できないことに注意してください。 |
io.quarkus.qute.Template.getFragment(String)
メソッドにより、プログラム的にフラグメントを取得することができます。
@Inject
Template item;
String useTheFragment() {
return item.getFragment("item_aliases") (1)
.data("aliases", List.of("Foo","Bar")) (2)
.render();
}
1 | 識別子 item_aliases を持つテンプレートフラグメントを取得します。 |
2 | データが正しく設定されているようにします。 |
上記のスニペットは、次のようにレンダリングされるはずです:
<h2>Aliases</h2>
<ol>
<li>Foo</li>
<li>Bar</li>
</ol>
Quarkus では、type-safe fragment も定義できます。 |
また、 {#include}
セクションを持つフラグメントを、別のテンプレートやフラグメントを定義するテンプレートの中に含めることもできます。
<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 | ドル記号 $ を含むテンプレート識別子は、フラグメントを表します。 item$item_aliases の値は、次のように訳されます: テンプレート item からフラグメント item_aliases を使用します。 |
2 | aliases パラメーターは、関連するデータを渡すために使用されます。データが正しく設定されていることを確認する必要があります。この特定のケースでは、フラグメントは {#for alias in aliases} セクションの aliases の値として式 user.aliases を使用します。 |
同じテンプレートからフラグメントを参照する場合は、 $ の前の部分、つまり {#include $item_aliases /} のような部分をスキップしてください。
|
この機能を無効にするために、 {#include item$item_aliases _ignoreFragments=true /} を指定することができます。つまり、テンプレート識別子にドル記号 $ を指定しても、フラグメント検索は行われません。
|
3.5.9.1. 非表示 (hidden) フラグメント
デフォルトでは、フラグメントは通常、オリジナルテンプレートの一部としてレンダリングされます。しかし、 rendered=false
を使って、フラグメントを 非表示 にするのが便利な場合もあります。興味深い使用例として、フラグメントを定義したテンプレートの中で複数回使用できるフラグメントがあります。
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 | 識別子 strong を持つ隠しフラグメントを定義します。
この特定のケースでは、rendered パラメーターの値として false ブール値リテラルを使用します。
ただし、任意の式を使用することも可能です。 |
2 | フラグメント strong をインクルードし、値を渡します。構文 $strong に注意してください。これは、現在のテンプレートからフラグメント strong をインクルードするように処理されます。 |
3 | フラグメント strong をインクルードし、値を渡す。 |
上のスニペットは、次のようにレンダリングされます:
<h1>My page</h1>
<p>This document
<strong>contains</strong>
a lot of
<strong>information</strong>
!</p>
3.5.10. Eval セクション
このセクションは、テンプレートを動的に解析および評価するために使用できます。 動作は Include セクション によく似ています。ただし、
-
テンプレートの内容は、
io.quarkus.qute.TemplateLocator
を介して取得されるのではなく、直接渡されます。 -
評価済みテンプレートの一部をオーバーライドすることはできません。
{#eval myData.template name='Mia' /} (1)(2)(3)
1 | myData.template の結果がテンプレートとして使用されます。テンプレートは カレントコンテキスト で実行されます。つまり、インクルードされているテンプレートのデータを参照することができます。 |
2 | また、評価済みテンプレートで使用可能なオプションのパラメーターを定義することも可能です。 |
3 | セクションの内容は常に無視されます。 |
評価済みテンプレートは、セクションが実行されるたびに解析および評価されます。 つまり、リソースを節約してパフォーマンスを最適化するために、解析された値をキャッシュすることはできません。 |
3.5.11. キャッシュされたセクション
めったに変更されないテンプレートの一部をキャッシュすることが実用的な場合もあります。キャッシュ機能を使うには、組み込みの 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();
Quarkus アプリケーションに`quarkus-cache` エクステンションが存在する場合、CacheSectionHelper が登録され、自動的 に設定されます。キャッシュの名前は qute-cache です。これは in a standard way と定義でき、@Inject @CacheName("qute-cache") Cache を介してプログラムで管理することもできます。
|
その後、{#cached}` セクションをテンプレート内で使用できます。
{#cached} (1)
Result: {service.findResult} (2)
{/cached}
1 | key パラメーターが使用されていない場合は、テンプレートのすべてのクライアントが同じキャッシュ値を共有します。 |
2 | テンプレートのこの部分はキャッシュされ、キャッシュエントリーが見つからない、または無効になっている場合にのみ {service.findResult} 式が評価されます。 |
{#cached key=currentUser.username} (1)
User-specific result: {service.findResult(currentUser)}
{/cached}
1 | key パラメーターが設定されているため、{currentUser.username} の結果ごとに異なるキャッシュ値が使用されます。 |
キャッシュを使用する場合、特定のキーでキャッシュエントリーを無効にするオプションを持つことが非常に重要です。Qute では、キャッシュエントリーのキーは、テンプレート名、開始 {#cached} タグの行と列、およびオプションの key パラメーターで構成される String ({TEMPLATE}:{LINE}:{COLUMN}_{KEY} ) です。たとえば、foo.html:10:1_alpha はテンプレート`foo.html` のキャッシュされたセクションのキーであり、{#cached} タグは 10 行目の 1 列目に配置されます。オプションの key パラメーターは alpha に解決されます。
|
3.6. レンダリング出力
TemplateInstance
は、レンダリングをトリガーして結果を消費するためのいくつかの方法を提供しています。最も簡単な方法は TemplateInstance.render()
です。このメソッドは同期レンダリングをトリガーし、レンダリングが終了するまで現在のスレッドをブロックして出力を返します。対照的に、 TemplateInstance.renderAsync()
は CompletionStage<String>
を返し、レンダリングが終了すると完了します。
TemplateInstance.renderAsync()
Exampletemplate.data(foo).renderAsync().whenComplete((result, failure) -> { (1)
if (failure == null) {
// consume the output...
} else {
// process failure...
}
};
1 | レンダリング終了後に実行されるコールバックを登録します。 |
また、 Mutiny 型を返す2つのメソッドがあります。 TemplateInstance.createUni()
は新しい Uni<String>
オブジェクトを返します。 createUni()
を呼び出しても、テンプレートはすぐにレンダリングされません。その代わり、 Uni.subscribe()
が呼び出されるたびに、テンプレートの新しいレンダリングがトリガーされます。
TemplateInstance.createUni()
Exampletemplate.data(foo).createUni().subscribe().with(System.out::println);
TemplateInstance.createMulti()
は新しい Multi<String>
オブジェクトを返します。
各アイテムはレンダリングされたテンプレートの一部/チャンクを表します。
繰り返しになりますが、createMulti()
はレンダリングをトリガーしません。
代わりに、サブスクライバーによって計算がトリガーされるたびに、テンプレートが再びレンダリングされます。
TemplateInstance.createMulti()
Exampletemplate.data(foo).createMulti().subscribe().with(buffer:append,buffer::flush);
テンプレートのレンダリングは2つのフェーズに分かれています。最初のフェーズ(非同期)では、テンプレート内のすべての式が解決され、 結果ツリー が構築されます。同期的な第2フェーズでは、結果ツリーが マテリアライズされ 、結果ノードが1つずつ、特定のコンシューマによって消費/バッファリングされるチャンクを出力します。 |
3.7. エンジン設定
3.7.1. 値リゾルバ
Value resolvers are used when evaluating expressions.
First the resolvers that apply to the given EvalContext
are filtered.
Then the resolver with highest priority is used to resolve the data.
If a io.quarkus.qute.Results.NotFound
object is returned then the next available resolver is used instead.
However, null
return value is considered a valid result.
A custom io.quarkus.qute.ValueResolver
can be registered programmatically via EngineBuilder.addValueResolver()
.
ValueResolver
ビルダー例engineBuilder.addValueResolver(ValueResolver.builder()
.appliesTo(ctx -> ctx.getBase() instanceof Long && ctx.getName().equals("tenTimes"))
.resolveSync(ctx -> (Long) ctx.getBase() * 10)
.build());
In Quarkus, the @EngineConfiguration annotation can be used to register a ValueResolver implemented as a CDI bean.
|
Keep in mind that the reflection-based value resolver has priority -1 and the max priority value for resolvers generated from @TemplateData and type-safe expressions is 10 .
|
3.7.2. テンプレートロケーター
テンプレートは、手動で登録するか、テンプレートロケータを介して自動的に登録することができます。ロケーターは、 Engine.getTemplate()
メソッドが呼び出されたときに、エンジンがキャッシュに保存している所定の ID のテンプレートがない場合に使用されます。ロケーターは、テンプレートの内容を読み取る際に、正しい文字エンコーディングを使用する責任があります。
Quarkus では、src/main/resources/templates にあるすべてのテンプレートが自動的に配置され、quarkus.qute.default-charset (デフォルトは UTF-8) で設定したエンコーディングが使用されます。
カスタムロケーターは、@Locate アノテーションを使用して registered を行うことができます。
|
3.7.3. コンテンツフィルタ
コンテンツフィルタを使用して、解析前にテンプレートコンテンツを変更することができます。
engineBuilder.addParserHook(new ParserHook() {
@Override
public void beforeParsing(ParserHelper parserHelper) {
parserHelper.addContentFilter(contents -> contents.replace("${", "$\\{")); (1)
}
});
1 | すべての ${ との一致をエスケープします。 |
3.7.4. strict レンダリング
strict レンダリングにより、開発者は、タイプミスや無効な式によって引き起こされるわかりにくいエラーをキャッチすることができます。この機能を有効にすると、解決できない式、つまり io.quarkus.qute.Results.NotFound
のインスタンスとして評価される式は、常に TemplateException
となり、レンダリングは中断されます。NotFound
値は基本的に、値リゾルバーが式を正しく解決できなかったことを意味するため、エラーとみなされます。
しかし、null は有効な値です。If セクション にあるように falsy とみなされ、何も出力されません。
|
strict レンダリングはデフォルトで有効になっています。しかし、io.quarkus.qute.EngineBuilder.strictRendering(boolean)
によって、この機能を無効にすることができます。
Quarkus では、専用の config プロパティーである quarkus.qute.strict-rendering を代わりに使用することができます。
|
"not found" エラーを引き起こす可能性のある式をどうしても使いたい場合は、デフォルト値 および 安全な式 を使ってエラーを抑制することができます。デフォルト値は、式の前のパーツが解決できないか、null
に解決される場合に使用されます。エルビス演算子を使ってデフォルト値 {foo.bar ?: 'baz'}
を出力することができます。これは事実上、仮想メソッド {foo.bar.or('baz')}
と同じになります。安全な式は ??
という接尾辞で終わり、式が解決できない場合は null
という結果になります。これは、たとえば {#if}
セクションの {#if valueNotFound??}Only rendered if valueNotFound is truthy!{/if}
では非常に便利です。実際のところ、??
は .or(null)
の簡単な表記法に過ぎず、たとえば {#if valueNotFound??}
は {#if valueNotFound.or(null)}
となります。
4. Quarkus 統合
QuarkusアプリケーションでQuteを使用する場合は、次の依存関係をプロジェクトに追加します:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-qute</artifactId>
</dependency>
Quarkusでは、事前に設定されたエンジンインスタンスが提供され、インジェクションに利用できます。@ApplicationScoped
スコープのBean、 io.quarkus.qute.Engine
型のBean、 @Default
修飾子が自動的に登録されます。さらに、 src/main/resources/templates
ディレクトリーにあるすべてのテンプレートが検証され、簡単にインジェクションすることができます。
A valid template file name is a sequence of non-whitespace characters. For example, a template file named foo and bar.html will be ignored.
|
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 | Location 修飾子が与えられていない場合は、フィールド名がテンプレートを特定するために使われます。この特定のケースでは、コンテナーはパス src/main/resources/templates/items.html でテンプレートを見つけようとします。 |
2 | Location 修飾子は、 src/main/resources/templates からの相対パスからテンプレートを注入するようにコンテナーに指示します。この場合のフルパスは src/main/resources/templates/detail/items2_v1.html です。 |
3 | 設定された Engine インスタンスを注入します。 |
4.1. エンジンのカスタマイズ
追加のコンポーネントは、実行時に CDI オブザーバーメソッドの EngineBuilder
メソッドを使用して手動で登録できます。
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());
}
}
ただし、この特定のケースでは、ビルド時の検証でセクションヘルパーファクトリーは無視されます。
ビルド時のテンプレート検証に参加する登録する場合は、便利な @EngineConfiguration
アノテーションを使用します。
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 | @EngineConfiguration アノテーションが付けられた SectionHelperFactory は、ビルド時のテンプレート検証で使用され、実行時に (a) セクションファクトリー、および、(b) CDI Bean として自動的に登録されます。 |
2 | CDI Beanインスタンスは実行時に使用されます - これはファクトリーが注入ポイントを定義できることを意味します |
3 | foo パラメーターが常に存在することを検証します。たとえば、{#custom foo='bar' /} は問題ありませんが、{#custom /} はビルド時に失敗します。 |
4 | レンダリング時に注入された Service を使用します。 |
The @EngineConfiguration annotation can be also used to register ValueResolver , NamespaceResolver and ParserHook components.
|
4.1.1. テンプレートロケーターの登録
テンプレートロケーター を登録する最も簡単な方法は、これらを CDI Bean にすることです。テンプレート検証を行うビルド時にはカスタムロケーターは利用できないので、@Locate
アノテーションで検証を無効化する必要があります。
@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 | 実行時に、カスタムロケーターによって bar.html という名前のテンプレートが配置されます。 |
2 | 正規表現 foo.* は、名前が foo で始まるテンプレートのバリデーションを無効にします。 |
3 | 注入フィールドは @Locate でアノテーションされたテンプレートロケーターとして解決され、シングルトンセッション Bean として登録されます。 |
4.2. テンプレートバリアント
場合によっては、コンテンツネゴシエーションに基づき、テンプレートの特定バリアントをレンダリングすると便利です。
これは TemplateInstance.setVariant()
を使用して特別な属性を設定することで実行できます。
class MyService {
@Inject
Template items; (1)
@Inject
ItemManager manager;
String renderItems() {
return items.data("items", manager.findItems())
.setVariant(new Variant(Locale.getDefault(), "text/html", "UTF-8"))
.render();
}
}
quarkus-rest-qute または quarkus-resteasy-qute を使用すると、コンテンツネゴシエーションが自動的に実行されます。
詳細は、 REST インテグレーション セクションを参照してください。
|
4.3. テンプレートに直接Beansを注入
@Named
でアノテーションされた CDI Bean は、cdi
や inject
名前空間を通して、任意のテンプレートで参照することができます。
{cdi:personService.findPerson(10).name} (1)
{inject:foo.price} (2)
1 | まず、personService という名前の Bean が見つかり、それをベースオブジェクトとして使用します。 |
2 | まず、 foo という名前のBeanを見つけ、それをベースオブジェクトとして使用します。 |
@Named @Dependent Bean は、1 つのレンダリング操作用にテンプレート内のすべての式で共有され、レンダリングが終了した後に破棄されます。
|
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.
@Named でアノテーションされたすべてのBeanに対して ValueResolver も生成され、そのプロパティーにリフレクションなしでアクセスできるようになります。
|
アプリケーションが HTTP リクエスト を提供する場合、inject 名前空間を介して現在の io.vertx.core.http.HttpServerRequest を注入することもできます (例: {inject:vertxRequest.getParam('foo')} )。
|
Sometimes it may be necessary to access public methods and properties of a CDI bean that is not annotated with @Named
.
However, if you don’t control the source of the bean it is not possible to add the @Named
annotation.
Nevertheless, it is possible to create an intermediate CDI bean annotated with @Named
.
This intermediate bean can inject the bean in question and make it accessible.
A Java record is a very convenient way to define such an intermediate CDI bean.
@Named (1) (2)
public record UserData(UserInfo info, @LoggedIn String username) { (3)
}
1 | If no name is explicitly specified by the value member the default name is assigned - the simple name of the bean class, after converting the first character to lower case. In this particular case, the default name is userData . |
2 | The @Singleton scope is added automatically. |
3 | All parameters of the canonical constructor are injection points. The accessor methods can be used to obtain the injected bean. |
And then in a template you can simply use {cdi:userData.info}
or {cdi:userData.username}
.
4.4. タイプセーフな式
テンプレート式は、オプションでタイプセーフにすることができます。これは、式が既存のJavaの型とテンプレート拡張メソッドに対して検証されることを意味します。無効な/不正確な式が見つかった場合、ビルドは失敗します。
例えば、 item.name
という式があり、 item
が org.acme.Item
にマップされている場合、 Item
には name
というプロパティーがあるか、一致するテンプレート拡張メソッドが存在しなければなりません。
オプションの パラメーター宣言 は、最初のパートがパラメーター名と一致する式に Java 型をバインドするために使用されます。パラメーター宣言はテンプレートで直接指定します。
Java タイプは、java.lang
パッケージの JDK タイプでない限り、常に fully qualified name で識別される必要があります。この場合、パッケージ名はオプションになります。パラメーター化されたタイプもサポートされていますが、ワイルドカードは常に無視され、上限と下限のみが考慮されます。たとえば、パラメーター宣言 {@java.util.List<? extends org.acme.Foo> list}
は {@java.util.List<org.acme.Foo> list}
として認識されます。タイプ変数は特別な方法で扱われることはないため、決して使用しないでください。
{@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 | パラメーター宣言 - foo を org.acme.Foo にマップします。 |
2 | 検証されていません - パラメーター宣言にマッチしません。 |
3 | この式は検証されています。org.acme.Foo はプロパティー message を持つか、一致するテンプレート拡張メソッドが存在しなければなりません。 |
4 | 同様に、 foo.message から解決されたオブジェクトの Java 型は、プロパティー toLowerCase を持つか、一致するテンプレート拡張メソッドが存在しなければなりません。 |
パラメーター宣言で使用されるすべての型に対して値リゾルバが自動的に生成され、そのプロパティーにリフレクションなしでアクセスできるようになります。 |
type-safe templates のメソッドパラメーターは、自動的にパラメーター宣言に変換されます。 |
セクションは、パラメーター宣言にマッチする名前をオーバーライドできることに注意してください。
{@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 | org.acme.Foo に対して検証されています。 |
2 | 検証されていません - foo はループセクションでオーバーライドされています。 |
パラメーター宣言では、キーの後に default value を指定することができます。キーとデフォルト値は等号で区切られます ({@int age=10}
)。 デフォルト値は、パラメーターのキーが null
に解決されるか、見つからない場合に、テンプレート内で使用されます。
たとえば、パラメーター宣言 {@String foo="Ping"}
があり、foo
が見つからない場合、{foo}
を使用すると Ping
が出力されます。一方、(たとえば TemplateInstance.data("foo", "Pong")
を介して) 値が設定されている場合は、{foo}
の出力は Pong
となります。
デフォルト値の型は、パラメーター宣言の型に割り当て可能でなければなりません。これは、ビルドの失敗につながる誤ったパラメーター宣言の例です: {@org.acme.Foo foo=1}
。
実際のデフォルト値は expression です。そのため、デフォルト値はリテラル (42 や true など) である必要はありません。たとえば、@TemplateEnum を使用して、パラメーター宣言のデフォルト値として enum 定数を指定できます ({@org.acme.MyEnum myEnum=MyEnum:FOO} )。
しかし、括弧を使用してグループ化されていない限り (例: {@org.acme.Foo foo=(foo1 ?: foo2)} )、デフォルト値で中置記法はサポートされません。
|
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 | Java プリミティブタイプ。 |
2 | String` は java.lang.String ( {@java.util.List<java.lang.String> strings} ) に置き換えることができます。 |
3 | ワイルドカードは無視され、代わりに上限値が使用されます: {@java.util.Map<String,Number>} |
4 | ワイルドカードは無視され、代わりに上限値が使用されます: {@java.util.Optional<java.lang.Object>} |
5 | タイプは java.lang.String で、キーは name 、デフォルト値は Quarkus です。 |
4.5. タイプセーフテンプレート
Java コードでタイプセーフなテンプレートを定義できます。 タイプセーフなテンプレートのパラメーターは、タイプセーフな式 のバインドに使用される パラメーター宣言 に自動変換されます。 その後、タイプセーフな式はビルド時に検証されます。
タイプセーフなテンプレートは、2 つの方法で定義できます。
-
クラスにアノテーション
@io.quarkus.qute.CheckedTemplate
を付けると、そのすべてのstatic native
メソッドが、タイプセーフなテンプレートと、それに必要なパラメーターリストを定義するために使用されます。 -
io.quarkus.qute.TemplateInstance
を実装する Java レコードを使用します。レコードコンポーネントはテンプレートパラメーターを表し、オプションで@io.quarkus.qute.CheckedTemplate
を使用してテンプレートを設定できます。
4.5.1. ネストされたタイプセーフなテンプレート
templates in Jakarta REST resources を使用する場合、次の規則を適用できます。
-
テンプレートファイルを
/src/main/resources/templates
ディレクトリーに整理し、リソースクラスごとに 1 つのディレクトリーにグループ化します。つまり、ItemResource
クラスがhello
とgoodbye
の 2 つのテンプレートを参照している場合は、/src/main/resources/templates/ItemResource/hello.txt
と/src/main/resources/templates/ItemResource/goodbye.txt
に配置します。リソースクラスごとにテンプレートをグループ化することで、テンプレートへのナビゲートが容易になります。 -
各リソースクラスで、リソースクラス内で
@CheckedTemplate static class Template {}
クラスを宣言します。 -
リソースのテンプレートファイルごとに
public static native TemplateInstance method();
を 1 つずつ宣言します。 -
テンプレートインスタンスを構築するには、これらの静的メソッドを使用します。
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 | templates/ItemResource/item.html に対して TemplateInstance を与えるメソッドを宣言し、その Item item パラメーターを宣言することで、テンプレートを検証することができます。 |
2 | item パラメーターは自動的に parameter declaration に変換されるので、この名前を参照するすべての式が検証されます。 |
3 | テンプレート内で Item オブジェクトにアクセスできるようにします。 |
デフォルトでは、@CheckedTemplate でアノテーションされたクラスで定義されたテンプレートは、タイプセーフな式、つまり、ビルド時に検証可能な式のみを含むことができます。この要件を緩和するために、@CheckedTemplate(requireTypeSafeExpressions = false) を使用することができます。
|
4.5.2. トップレベルタイプセーフテンプレート
また、 @CheckedTemplate
でアノテーションされたトップレベルの Java クラスを宣言することもできます。
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 | これは、パス templates/hello.txt を持つテンプレートを宣言します。name パラメーターは自動的に parameter declaration に変換されるため、この名前を参照するすべての式が検証されます。 |
そして、テンプレートファイルごとに public static native TemplateInstance method();
を宣言します。これらの静的メソッドを使用してテンプレートインスタンスを構築します。
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. テンプレートレコード
io.quarkus.qute.TemplateInstance
を実装する Java レコードは、タイプセーフなテンプレートを表します。
レコードコンポーネントはテンプレートパラメーターを表し、オプションで @io.quarkus.qute.CheckedTemplate
を使用してテンプレートを設定できます。
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. The template is located at /src/main/resources/templates/HelloResource/Hello.html . |
2 | レコードをインスタンス化し、通常の TemplateInstance として使用します。 |
4.5.4. カスタマイズされたテンプレート・パス
The path of a type-safe template (@CheckedTemplate
method or record) consists of a base path and a defaulted name.
The base path is supplied by the @CheckedTemplate#basePath()
.
By default, the simple name of the enclosing 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/record is used as is.
A template record that is not annotated with @CheckedTemplate is treated as if it was annotated with @CheckedTemplate with default values.
|
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 | このメソッドのテンプレートパスは、 items/item-and-order になります。 |
4.5.5. タイプセーフフラグメント
Java コード内で、タイプセーフな fragment も定義できます。
名前にドル記号 $
が含まれる native static メソッドは、タイプセーフなテンプレートのフラグメントを表すメソッドを示します。
フラグメントの名前は、アノテーションが付けられたメソッド名から導出されます。
最後に現れるドル記号 $
の前にある部分は、関連するタイプセーフなテンプレートのメソッド名です。
最後に現れるドル記号の後にある部分は、フラグメント識別子です。
関連する CheckedTemplate#defaultName()
で定義されたストラテジーは、デフォルト名を構築する際に適用されます。
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では、ビルド時に、 Templates#items() に対応する各テンプレートに、識別子 item を持つフラグメントが含まれているかどうかが検証されます。さらに、フラグメントメソッドのパラメーターも検証されます。一般に、フラグメントに含まれ、オリジナル/アウターテンプレートのデータを参照するすべてのタイプセーフ 式には、特定のパラメータが必要です。 |
items.html
におけるフラグメントの定義<h1>Items</h1>
<ol>
{#for item in items}
{#fragment id=item} (1)
<li>{item.name}</li> (2)
{/fragment}
{/for}
</ol>
1 | 識別子 item を持つフラグメントを定義します |
2 | {item.name} 式は、 Templates#items$item() メソッドが、名前 item 、型 org.acme.Item のパラメータを宣言しなければならないことを意味します。 |
class ItemService {
String renderItem(Item item) {
// this would return something like "<li>Foo</li>"
return Templates.items$item(item).render();
}
}
この機能を無効にするために、 @CheckedTemplate#ignoreFragments=true を指定することができます。つまり、メソッド名にドル記号 $ を指定しても、チェックされたフラグメントメソッドになることはありません。
|
4.6. テンプレート拡張メソッド
拡張メソッドは、データクラスを新しい機能で拡張するため(アクセス可能なプロパティーとメソッドのセットを拡張するため)、または特定の 名前空間 のための式を解決するために使用することができます。例えば、 計算されたプロパティー や 仮想メソッド を追加することができます。
値リゾルバは、 @TemplateExtension
でアノテーションされたメソッドに対して自動的に生成されます。クラスが @TemplateExtension
でアノテーションされている場合、そのクラスで宣言されている プライベートではないスタティック・メソッド ごとに値リゾルバが生成されます。メソッドレベルのアノテーションは、クラスで定義された動作をオーバーライドします。以下の要件を満たさないメソッドは無視されます。
テンプレート拡張メソッド:
-
private
であってはなりません -
static
でなければなりません -
void
を返してはいけません
名前空間が定義されていない限り、@TemplateAttribute
でアノテーションされた最初のパラメーターのクラスがベース・オブジェクトにマッチするように使用されます。そうでなければ、名前空間が式をマッチするのに使用されます。
4.6.1. 名前によるマッチング
メソッド名は、デフォルトでプロパティ名と一致するように使用されます。
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 | このメソッドは、 Item.class 型のベース・オブジェクトと discountedPrice プロパティー名を持つ式にマッチします。 |
このテンプレート拡張メソッドを使うと、以下のテンプレートをレンダリングすることができます。
{item.discountedPrice} (1)
1 | item は org.acme.Item のインスタンスに解決されます。 |
ただし、 matchName()
でマッチング名を指定することは可能です。
TemplateInstance.createUni()
の例@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"));
}
特別な定数 - TemplateExtension#ANY
/ *
- を使用すると、拡張メソッドが任意の名前にマッチするように指定することができます。
TemplateExtension#ANY
の例@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 | 追加の文字列メソッドパラメータは、実際のプロパティ名を渡すために使用されます。 |
また、 matchRegex()
で指定された正規表現と名前を照合することも可能です。
TemplateInstance.createUni()
の例@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 | 追加の文字列メソッドパラメータは、実際のプロパティ名を渡すために使用されます。 |
最後に、 matchNames()
を使用して、一致する名前のコレクションを指定することができます。追加の文字列メソッド・パラメータも必須です。
TemplateInstance.createUni()
の例@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"
}
余分な一致条件は無視されます。優先順位の高い順に並べると、以下のようになります。 matchRegex() , matchNames() および matchName() 。
|
4.6.2. メソッドパラメーター
エクステンションメソッドはパラメーターを宣言できます。
名前空間が指定されていない場合、アノテーション @TemplateAttribute
が付けられていない最初のパラメーターがベースオブジェクトを渡すために使用されます。最初の例では、org.acme.Item
がこれに該当します。
任意の名前に一致する場合、または正規表現を使用する場合は、文字列メソッドパラメータを使用してプロパティ名を渡す必要があります。
アノテーション @ TemplateAttribute
が付けられたパラメーターは、TemplateInstance#getAttribute()
を介して取得されます。
他のすべてのパラメーターは、テンプレートをレンダリングするときに解決され、エクステンションメソッドに渡されます。
@TemplateExtension
class BigDecimalExtensions {
static BigDecimal scale(BigDecimal val, int scale, RoundingMode mode) { (1)
return val.setScale(scale, mode);
}
}
1 | このメソッドは、 BigDecimal.class 型の基底オブジェクトと、 scale の仮想メソッド名と 2 つの仮想メソッドパラメーターを持つ式にマッチします。 |
{item.discountedPrice.scale(2,mode)} (1)
1 | item.discountedPrice は BigDecimal のインスタンスに解決されます。 |
4.6.3. 名前空間拡張メソッド
TemplateExtension#namespace()
が指定された場合、拡張メソッドが、指定された namespace を持つ式を解決するために使用されます。同じ名前空間を共有するテンプレート拡張メソッドは、 TemplateExtension#priority()
で順に 1 つのリゾルバにグループ化されます。最初にマッチする拡張メソッドが式の解決に使用されます。
@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();
}
}
これらの拡張メソッドは、以下のように使用することができます。
{str:format('%s %s!','Hello', 'world')} (1)
{str:reverse('hello')} (2)
1 | 出力は Hello world! です |
2 | 出力は olleh です |
4.6.4. 組み込みテンプレートエクステンション
Quarkusは、組み込みの拡張メソッドのセットを提供しています。
4.6.4.1. マップ
-
keys
またはkeySet
: マップに含まれるキーのセットビューを返します。-
{#for key in map.keySet}
-
-
values
: マップに含まれる値のコレクションビューを返します。-
{#for value in map.values}
-
-
size
: マップ内のキーと値の対応付けの個数を返します。-
{map.size}
-
-
isEmpty
: マップにキーと値のマッピングが含まれていない場合に true を返します。-
{#if map.isEmpty}
-
-
get(key)
: 指定したキーがマッピングされた値を返します。-
{map.get('foo')}
-
マップ値は、直接アクセスすることもできます ({map.myKey} )。正規の識別子ではないキーにはブラケット表記を使用します ({map['my key']} )。
|
4.6.4.2. リスト
-
get(index)
: リストの指定した位置の要素を返す-
{list.get(0)}
-
-
reversed
: リストを逆にしたイテレータを返す-
{#for r in recordsList.reversed}
-
-
take
: 与えられたリストから最初のn
要素を返します。n
が範囲外の場合はIndexOutOfBoundsException
を投げます。-
{#for r in recordsList.take(3)}
-
-
takeLast
: 与えられたリストから最後のn
要素を返します。n
が範囲外の場合はIndexOutOfBoundsException
を投げます。-
{#for r in recordsList.takeLast(3)}
-
-
first
: 指定されたリストの最初の要素を返します。リストが空の場合、NoSuchElementException
をスローします。-
{recordsList.first}
-
-
last
: 指定されたリストの最後の要素を返します。リストが空の場合、NoSuchElementException
をスローします。-
{recordsList.last}
-
リストの要素にはインデックス ({list.10} または {list[10]} ) を介して直接アクセスすることができます。
|
4.6.4.3. 整数値
-
mod
: モデューロ演算-
{#if counter.mod(5) == 0}
-
-
plus
または+
: 加算-
{counter + 1}
-
{age plus 10}
-
{age.plus(10)}
-
-
minus
または-
: 引き算-
{counter - 1}
-
{age minus 10}
-
{age.minus(10)}
-
4.6.4.4. Strings
-
fmt
またはformat
:java.lang.String.format()
によって文字列のインスタンスをフォーマットします。-
{myStr.fmt("arg1","arg2")}
-
{myStr.format(locale,arg1)}
-
-
str:fmt
またはstr:format
: 与えられた文字列値をjava.lang.String.format()
でフォーマットします。-
{str:format("Hello %s!",name)}
-
{str:fmt(locale,'%tA',now)}
-
-
+
: 連結-
{item.name + '_' + mySuffix}
-
{name + 10}
-
4.6.4.5. 設定
-
config:<name>
またはconfig:[<name>]
: 与えられたプロパティー名に対する config 値を返します。-
{config:foo}
または{config:['property.with.dot.in.name']}
-
-
config:property(name)
: 指定したプロパティー名の設定値を返します。名前は式で動的に取得することが可能です。-
{config:property('quarkus.foo')}
-
{config:property(foo.getPropertyName())}
-
-
config:boolean(name)
: 指定したプロパティー名の設定値をbooleanとして返します。名前は式で動的に取得することが可能です。-
{config:boolean('quarkus.foo.boolean') ?: 'Not Found'}
-
{config:boolean(foo.getPropertyName()) ?: 'property is false'}
-
-
config:integer(name)
: 指定したプロパティー名の設定値をintegerで返します。名前は式で動的に取得することが可能です。-
{config:integer('quarkus.foo')}
-
{config:integer(foo.getPropertyName())}
-
4.6.4.6. 時間
-
format(pattern)
:java.time
パッケージの一時オブジェクトをフォーマットします。-
{dateTime.format('d MMM uuuu')}
-
-
format(pattern,locale)
:java.time
パッケージの一時オブジェクトをフォーマットします。-
{dateTime.format('d MMM uuuu',myLocale)}
-
-
format(pattern,locale,timeZone)
:java.time
パッケージの一時オブジェクトをフォーマットします。-
{dateTime.format('d MMM uuuu',myLocale,myTimeZoneId)}
-
-
time:format(dateTime,pattern)
:java.time
パッケージ、java.util.Date
、java.util.Calendar
、java.lang.Number
の一時オブジェクトをフォーマットします。-
{time:format(myDate,'d MMM uuuu')}
-
-
time:format(dateTime,pattern,locale)
:java.time
パッケージ、java.util.Date
、java.util.Calendar
、 `java.lang.Number`の一時オブジェクトをフォーマットします。-
{time:format(myDate,'d MMM uuuu', myLocale)}
-
-
time:format(dateTime,pattern,locale,timeZone)
:java.time
パッケージ、java.util.Date
、java.util.Calendar
、java.lang.Number
の一時オブジェクトをフォーマットします。-
{time:format(myDate,'d MMM uuuu',myLocale,myTimeZoneId)}
-
4.7. @TemplateData
値リゾルバは、 @TemplateData
でアノテーションされた型に対して自動的に生成されます。これにより、Quarkusでは、実行時にデータにアクセスするためのリフレクションの使用を避けることができます。
非publicのメンバー、コンストラクタ、静的イニシャライザ、静的メソッド、合成メソッド、ボイドメソッドは常に無視されます。 |
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"));
}
}
Item
の任意のインスタンスをテンプレート内で直接使用することができます。
{#each items} (1)
{it.price} / {it.discountedPrice}
{/each}
1 | items は org.acme.Item インスタンスのリストに解決されます。 |
さらに、 @TemplateData.properties()
と @TemplateData.ignore()
を使用して、生成されるリゾルバを微調整することができます。最後に、アノテーションの「ターゲット」を指定することも可能です。これは、アプリケーションによって管理されないサードパーティーのクラスに便利です。
@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 | 生成された値リゾルバは、 BigDecimal.setScale() メソッドを呼び出す方法を知っています。 |
4.7.1. 静的なフィールドとメソッドへのアクセス
もし @TemplateData#namespace()
が空でない値に設定されている場合、ターゲットクラスのパブリック静的フィールドとメソッドにアクセスするための名前空間リゾルバーが自動的に生成されます。デフォルトでは、名前空間はターゲットクラスの FQCN で、ドットやドル記号はアンダースコアに置き換えられます。たとえば、名前 org.acme.Foo
を持つクラスの名前空間は org_acme_Foo
となります。静的フィールド Foo.AGE
には、{org_acme_Foo:AGE}
からアクセスすることができます。静的メソッド Foo.computeValue(int number)
には、{org_acme_Foo:computeValue(10)}
を介してアクセスすることができます。
名前空間は、英数字とアンダースコアだけで構成されます。 |
@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_Statuses is generated automatically. |
{#if machine.status == model_Statuses:ON}
The machine is ON!
{/if}
4.7.2. enum の便利なアノテーション
また、enum 定数にアクセスするための便利なアノテーションとして、@io.quarkus.qute.TemplateEnum
があります。このアノテーションは、機能的には @TemplateData(namespace = TemplateData.SIMPLENAME)
と同等です。つまり、対象の enum に対して自動的に名前空間リゾルバーが生成され、対象の enum の単純名が名前空間として使用されます。
@TemplateEnum
でアノテーションされた enumpackage model;
@TemplateEnum (1)
public enum Status {
ON,
OFF
}
1 | 名前空間 Status を持つネームリゾルバーが自動的に生成される。 |
enum 以外のクラスで宣言された @TemplateEnum は無視されます。また、enum が @TemplateData アノテーションを宣言する場合は、@TemplateEnum アノテーションは無視されます。
|
{#if machine.status == Status:ON}
The machine is ON!
{/if}
Quarkus は、名前空間の競合の可能性を検出し、特定の名前空間が複数の @TemplateData や @TemplateEnum アノテーションによって定義されている場合、ビルドを失敗させます。
|
4.8. グローバル変数
アノテーション io.quarkus.qute.TemplateGlobal
は、任意のテンプレートからアクセス可能な グローバル変数 を提供する静的フィールドとメソッドを示すために使用できます。
グローバル変数は次のとおりです。
-
初期化中に、任意の
TemplateInstance
の 計算データ として追加されます。 -
global:
名前空間でアクセス可能です。
TemplateInstance#computedData(String, Function<String, Object>) を使用している場合、 マッピング関数は特定のキーに関連付けられ、指定されたキーの値が要求されるたびに、この関数が使用されます。グローバル変数の場合は、静的メソッドが呼び出されるか、マッピング関数で静的フィールドが読み取られます。
|
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 | クラスが @TemplateGlobal でアノテーションが付けられている場合、パラメーターを宣言しないすべての非 void 非プライベート静的メソッドとすべての非プライベート静的フィールドは、グローバル変数と見なされます。名前はデフォルトです。つまり、フィールド/メソッドの名前が使用されます。 |
2 | メソッドレベルのアノテーションは、クラスレベルのアノテーションをオーバーライドします。この特別なケースでは、名前はデフォルトではなく、明示的に選択されます。 |
User: {currentUser} (1)
Age: {global:age} (2)
Colors: {#each myColors}{it}{#if it_hasNext}, {/if}{/each} (3)
1 | currentUser resolves to Globals#user() . |
2 | global: 名前空間が使用され、age は Globals#age に解決されます。 |
3 | myColors resolves to Globals#myColors() . |
グローバル変数は暗黙のうちにすべてのテンプレートに parameter declarations を追加するため、グローバル変数を参照する式はビルド時に検証されることに注意してください。 |
User: Mia
Age: 40
Colors: RED, BLUE
4.8.1. 競合の解消
global:
名前空間経由でアクセスしない場合、グローバル変数は通常のデータオブジェクトと競合する可能性があります。
Type-safe templates は自動的にグローバル変数をオーバーライドします。
たとえば次の定義は、Globals#user()
メソッドが提供するグローバル変数をオーバーライドします。
import org.acme.User;
@CheckedTemplate
public class Templates {
static native TemplateInstance hello(User currentUser); (1)
}
1 | currentUser は Globals#user() が提供するグローバル変数と競合しています。 |
そのため、Globals#user()
メソッドが java.lang.String
を返し、それが name
プロパティーを持たないにもかかわらず、対応するテンプレートはバリデーションエラーにはなりません。
templates/hello.txt
User name: {currentUser.name} (1)
1 | org.acme.User には name プロパティーがあります。 |
その他のテンプレートでは、明示的なパラメーター宣言が必要です。
{@org.acme.User currentUser} (1)
User name: {currentUser.name}
1 | このパラメーター宣言は、Globals#user() メソッドが提供するグローバル変数によって追加される宣言を上書きします。 |
4.9. ネイティブ実行可能ファイル
JVM モードでは、リフレクションベースの値リゾルバーが、プロパティーにアクセスし、モデルクラスのメソッドを呼び出すために使われる場合があります。しかし、これは ネイティブ実行可能ファイル に対しては、そのままではうまくいきません。その結果、たとえ Foo
クラスが関連するゲッターメソッドを宣言していたとしても、Property "name" not found on the base object "org.acme.Foo" in expression {foo.name} in template hello.html
といったテンプレート例外に遭遇するかも知れません。
この問題を解決するには、いくつかの方法があります。
-
タイプセーフテンプレート または タイプセーフ式 をご利用ください。
-
この場合、最適化されたバリューリゾルバーが自動的に生成され、実行時に使用されます。
-
これが望ましい解決策です
-
-
モデルクラスに
@TemplateData
のアノテーションを付ける - 実行時に専用の値リゾルバーが生成され、使用されます。 -
リフレクションベースの値リゾルバーが機能するためには、モデルクラスにアノテーション
@io.quarkus.runtime.annotations.RegisterForReflection
が必要です。@RegisterForReflection
アノテーションの詳細は、ネイティブアプリケーションのヒント ページを参照してください。
4.10. REST インテグレーション
Jakarta REST アプリケーションで Qute を使用する場合は、使用している Jakarta REST スタックに応じて適切なエクステンションを最初に登録する必要があります。
quarkus-rest
エクステンションを介して Quarkus REST (旧 RESTEasy Reactive) を使用している場合は、pom.xml
ファイルに以下を追加します。
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-qute</artifactId>
</dependency>
従来の RESTEasy Classic ベースの quarkus-resteasy
エクステンションを使用している場合は、pom.xml
ファイルに以下を追加します。
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-qute</artifactId>
</dependency>
これら両方のエクステンションは、リソースメソッドが TemplateInstance
を返せるようにする特別な応答フィルターを登録します。そのため、ユーザーによる内部ステップの処理が不要になります。
Quarkus REST を使用する場合、TemplateInstance を返すリソースメソッドはブロッキングではないと見なされます。メソッドをブロッキングとしてマークするには、メソッドにアノテーション io.smallrye.common.annotation.Blocking を付ける必要があります。たとえば、@RunOnVirtualThread のアノテーションも付けられている場合などです。
|
最終的には、Jakarta REST リソース内で Qute を使用すると、次のようにシンプルになります。
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 | @Location の修飾子が指定されていない場合は、フィールド名がテンプレートの場所を特定するために使用されます。この特定のケースでは、パスが templates/hello.txt であるテンプレートを注入しています。 |
2 | Template.data() は、実際のレンダリングがトリガーされる前にカスタマイズできる新しいテンプレートインスタンスを返します。この場合、名前の値をキー name の下に置きます。データマップはレンダリング中にアクセス可能です。 |
3 | レンダリングをトリガーしないことに注意してください - これは特別な ContainerResponseFilter の実装によって自動的に行われます。 |
ユーザーは、特定の Jakarta REST リソースのテンプレートを整理し、自動的に type-safe expressions を有効化できる Type-safe templates を使用することが推奨されます。 |
コンテンツネゴシエーションは自動的に実行されます。結果の出力はクライアントから受け取った Accept
ヘッダーに依存します。
@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 | 注入されたフィールドから派生したベースパスを持つバリアントテンプレートを注入します - src/main/resources/templates/item |
2 | text/plain では src/main/resources/templates/item.txt のテンプレートが使用されます。 text/html では META-INF/resources/templates/item.html のテンプレートが使用されます。 |
RestTemplate
util クラスを使用すると、Jakarta REST リソースメソッドのボディからテンプレートインスタンスを取得することができます:
@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 | テンプレートの名前は、リソースクラスとメソッド名から派生しています。このケースでは、DetailResource/item です。 |
@Inject とは異なり、 RestTemplate 経由で取得したテンプレートは検証されません。即ち、テンプレートが存在しなくてもビルドは失敗しません。
|
4.11. Vert.x Integration
If you want to use io.vertx.core.json.JsonObject
as data in your templates, then you will need to add the quarkus-vertx
extension to your build file if not already part of your dependencies (most applications use this extension by default).
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-vertx</artifactId>
</dependency>
implementation("io.quarkus:quarkus-vertx")
With this dependency included, we have a special value resolver for io.vertx.core.json.JsonObject
which makes it possible to access the properties of a JSON object in a template:
{tool.name}
{tool.fieldNames}
{tool.fields}
{tool.size}
{tool.empty}
{tool.isEmpty}
{tool.get('name')}
{tool.containsKey('name')}
import java.util.HashMap;
import jakarta.inject.Inject;
import io.vertx.core.json.JsonObject;
import io.quarkus.qute.Template;
public class QuteVertxIntegration {
@Inject
Template foo;
public String render() {
HashMap<String, Object> toolMap = new Map<String, Object>();
toolMap.put("name", "Roq");
JsonObject jsonObject = new JsonObject(toolMap);
return foo.data("tool", jsonObject).render();
}
}
The QuteVertxIntegration#render()
output should look like:
Roq
[name]
[name]
1
false
false
Roq
true
4.12. 開発モード
開発モードでは、src/main/resources/templates
にあるすべてのファイルの変更が監視されます。
デフォルトでは、テンプレートを変更するとアプリケーションが再起動され、ビルド時の検証もトリガーされます。
ただし、quarkus.qute.dev-mode.no-restart-templates
設定プロパティーを使用して、アプリケーションを再起動しないテンプレートを指定することは可能です。
設定値は、templates
ディレクトリーからの相対テンプレートパスに一致する正規表現であり、パス区切り文字として /
が使用されます。
たとえば、quarkus.qute.dev-mode.no-restart-templates=templates/foo.html
はテンプレート src/main/resources/templates/foo.html
と一致します。
一致するテンプレートがリロードされ、実行時の検証のみが実行されます。
4.13. テスト
テストモードの場合、注入されたタイプセーフテンプレートのレンダリング結果は、CDI Bean として登録されている管理対象の io.quarkus.qute.RenderedResults
に記録されます。
テストまたは他の CDI Bean に RenderedResults
を注入し、結果をアサートできます。
この機能は、quarkus.qute.test-mode.record-rendered-results
設定プロパティーを false
に設定することで無効にできます。
4.14. タイプセーフメッセージバンドル
4.14.1. 基本概念
基本的な考え方として、すべてのメッセージを非常に単純なテンプレートである可能性があるとみなします。 タイプエラーを防ぐために、メッセージは メッセージバンドルインターフェイス のアノテーション付きメソッドとして定義されます。 Quarkus はビルド時に メッセージバンドル実装 を生成します。
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 | メッセージバンドルインターフェイスを表します。 バンドル名のデフォルトは msg で、テンプレート式の名前空間として使用されます (例: {msg:hello_name} )。 |
2 | 各メソッドには @Message のアノテーションが必要です。その値は qute テンプレートです。値が指定されていない場合、ローカライズされたファイルから対応する値が取得されます。該当するファイルがない場合は例外がスローされ、ビルドは失敗します。 |
3 | メソッドのパラメーターはテンプレートで使用することができます。 |
メッセージバンドルは、実行時に使用することができます:
-
io.quarkus.qute.i18n.MessageBundles#get()
を経由し、直接コードの中で使用。例:MessageBundles.get(AppMessages.class).hello_name("Lucie")
-
@Inject
であなたのBeanに注入されます。 例:@Inject AppMessages
-
メッセージ・バンドル名前空間を介してテンプレート内で参照。
{msg:hello_name('Lucie')} (1) (2) (3) {msg:message(myKey,'Lu')} (4)
1 msg
がデフォルトの名前空間です。2 hello_name
がメッセージキーです。3 Lucie
はメッセージバンドルインターフェースメソッドのパラメーターです。4 また、予約されたキー message
を使用して、実行時に解決されたキーのローカライズされたメッセージを取得することも可能である。この場合、検証はスキップされます。
4.14.2. デフォルトのバンドル名
@MessageBundle#value()
で指定されていない限り、バンドル名がデフォルトになります。
最上位クラスの場合、デフォルトで msg
値が使用されます。
ネストされたクラスの場合、名前は階層内に含まれるすべてのクラスの単純名 (最上位クラスが最初) で設定され、その後にメッセージバンドルインターフェイスの単純名が続きます。
名前はアンダースコアで区切られます。
例えば、以下のメッセージバンドルの名前は、デフォルトで Controller_index
になります:
class Controller {
@MessageBundle
interface index {
@Message("Hello {name}!")
String hello(String name); (1)
}
}
1 | このメッセージは、 {Controller_index:hello(name)} を介してテンプレートで使用することができます。 |
バンドル名は、ローカライズされたファイル名の一部としても使用されます。例えば、 Controller_index は、 Controller_index_de.properties となります。
|
4.14.3. バンドル名とメッセージキー
メッセージキーはテンプレート内で直接使用されます。
バンドル名はテンプレート式の名前空間として使用されます。
@MessageBundle
は、メソッド名からメッセージキーを生成するために使用するデフォルトストラテジーを定義するために使用できます。
ただし、@Message
はこのストラテジーをオーバーライドし、カスタムキーを定義することもできます。
デフォルトでは、アノテーションされた要素の名前がそのまま使用されます。
他の可能性としては、以下のようなものがあります。
-
キャメルケースを止め、ハイフン化したもの。 例:
helloName()
→hello-name
-
キャメルケースを止め、パーツをアンダースコア区切りとしたもの。例:
helloName()
→hello_name
4.14.4. バリデーション
-
すべてのメッセージバンドルテンプレートは検証されます。
-
名前空間を持たないすべての式は、パラメーターにマッピングする必要があります。たとえば、
Hello {foo}
→ の場合、メソッドにはfoo
という名前のパラメーターが必要です。 -
すべての式は、パラメーターのタイプに対して検証されます。たとえば、パラメーター
foo
のタイプがorg.acme.Foo
→org.acme.Foo
であるHello {foo.bar}
場合、bar
という名前のプロパティーを持つ必要があります。未使用の パラメーターごとに警告メッセージが記録されます。
-
-
{msg:hello(item.name)}
のようなメッセージバンドルメソッドを参照する式も検証されます。
4.14.5. ローカライゼーション
デフォルトでは、@MessageBundle
インターフェイスには quarkus.default-locale
設定プロパティーで指定したデフォルトロケールが使用されます。しかし、io.quarkus.qute.i18n.MessageBundle#locale()
を使用して、カスタムのロケールを指定することができます。さらに、ローカライズされたバンドルを定義するには、以下の 2 つの方法があります。
-
@Localized
でアノテーションされたデフォルトのインターフェイスを拡張するインターフェイスを作成します。 -
アプリケーションアーカイブの
src/main/resources/messages
ディレクトリーにある UTF-8 エンコードファイルを作成します (例:msg_de.properties
)。
ローカライズされたインターフェイスはリファクタリングを容易にしますが、外部ファイルの方が便利な場面も多いかもしれません。 |
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 | 値はロケールタグ文字列(IETF)です。 |
2 | 値はローカライズされたテンプレートです。 |
メッセージバンドルファイルは UTF-8 でエンコードされていなければなりません。
ファイル名は、関連するバンドル名 (例: msg
) とアンダースコアで構成され、その後に言語タグ (IETF、たとえば en-US
) が続きます。
言語タグは省略できます。その場合、デフォルトのバンドルロケールの言語タグが使用されます。
たとえば、バンドル msg
のデフォルトロケールが en
の場合、msg.properties
は msg_en.properties
として扱われます。
msg.properties
と msg_en.properties
の両方が検出されると、例外がスローされ、ビルドは失敗します。
ファイル形式は非常にシンプルで、各行はキーと値のペア (等号を区切り文字として使用) またはコメント (行頭は #
) を表します。
空行は無視されます。
キーは、対応するメッセージバンドルインターフェースから メソッド名にマッピング されます。
値は、通常 io.quarkus.qute.i18n.Message#value()
で定義されるテンプレートを表します。
値は、隣接する複数の通常行をまたぐことがあります。
その場合、行の終端をバックスラッシュ文字 \
でエスケープする必要があります。
動作は java.util.Properties.load(Reader)
メソッドの動作によく似ています。
msg_de.properties
# This comment is ignored
hello_name=Hallo {name}! (1) (2)
1 | ローカライズされたファイルの各行は、キーと値のペアを表します。キーは、メッセージバンドルインターフェイスで宣言されたメソッドに対応しなければなりません。値はメッセージテンプレートになります。 |
2 | キーと値は等号で区切られています。 |
この例では .properties という接尾辞を使用していますが、これはほとんどの IDE やテキストエディターが .properties ファイルのシンタックスハイライトをサポートしているからです。しかし、実際には接尾辞は何でもよいため、単に無視されます。
|
メッセージバンドルインターフェイスごとに、ターゲットディレクトリーにサンプルのプロパティーファイルが自動的に生成されます。たとえば、デフォルトで @MessageBundle に名前が指定されない場合、mvn clean package を用いてアプリケーションをビルドすると、target/qute-i18n-examples/msg.properties というファイルが生成されます。このファイルは、特定のロケールのベースとして使用することができます。単にファイル名を変更 (例: msg_fr.properties ) して メッセージのテンプレートを変更し、これを src/main/resources/messages ディレクトリーに移動します。
|
hello=Hello \
{name} and \
good morning!
行の終端はバックスラッシュ文字 \
でエスケープされ、次の行の先頭の空白は無視されることに注意してください。つまり、{msg:hello('Edgar')}
は Hello Edgar and good morning!
とレンダリングされます。
ローカライズされたバンドルを定義した後、特定のテンプレートインスタンスに対して正しいバンドルを 選択 する方法、つまりテンプレート内のすべてのメッセージバンドル式に対してロケールを指定する方法が必要です。
デフォルトでは、quarkus.default-locale
設定プロパティーで指定したロケールが、バンドルの選択に使用されます。
また、テンプレートインスタンスの locale
属性を指定することもできます。
locale
属性の例@Singleton
public class MyBean {
@Inject
Template hello;
String render() {
return hello.instance().setLocale("cs").render(); (1)
}
}
1 | Locale インスタンスかロケールタグ文字列 (IETF) をセットすることが出来ます。 |
quarkus-rest-qute を使用する場合にユーザーが locale 属性を設定していなければ、それは Accept-Language ヘッダーから導出されます。
|
@Localized
修飾子を使用して、ローカライズされたMessage Bundleインタフェースを注入することができます。
@Singleton
public class MyBean {
@Localized("cs") (1)
AppMessages msg;
String render() {
return msg.hello_name("Jachym");
}
}
1 | アノテーションの値はロケールタグ文字列(IETF)です。 |
4.14.5.1. 列挙
列挙は、便利な方法でローカライズできます。 列挙型の単一のパラメーターを受け入れ、メッセージテンプレートが未定義のメッセージバンドルメソッドがある場合:
@Message (1)
String methodName(MyEnum enum);
1 | The value is intentionally not provided. There’s also no key/value pair for this method in a localized file. |
Then it receives a generated template like:
{#when enumParamName}
{#is CONSTANT1}{msg:methodName_CONSTANT1}
{#is CONSTANT2}{msg:methodName_CONSTANT2}
{/when}
Furthermore, a special message method is generated for each enum constant. Finally, each localized file must contain keys and values for all enum constants:
methodName_CONSTANT1=Value 1
methodName_CONSTANT2=Value 2
Message keys for enum constants
By default, the message key consists of the method name followed by the |
In a template, the localized message for an enum constant can be obtained with a message bundle method like {msg:methodName(enumConstant)}
.
@TemplateEnum もあります。これは、テンプレート内の列挙定数にアクセスするために使用できるアノテーションです。
|
4.14.6. メッセージテンプレート
メッセージバンドルインターフェースの全てのメソッドは、メッセージテンプレートを定義しなければなりません。通常は io.quarkus.qute.i18n.Message#value()
で定義しますが、利便性のため、ローカライズしたファイルに値を定義するオプションも用意されています。
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 | アノテーションの値が定義されていない。この場合、ローカライズされた補足ファイルの値が使用される。 |
2 | アノテーションの値は、ローカライズされたファイルに定義された値を優先して定義される。 |
hello_name=Hello \
{name} and \
good morning!
goodbye=Best regards, {name} (1)
1 | io.quarkus.qute.i18n.Message#value() は常に優先されるため、この値は無視されます。 |
メッセージテンプレートはビルド時に検証されます。メッセージテンプレートの欠落が検出された場合、例外がスローされ、ビルドに失敗します。
4.15. 設定リファレンス
ビルド時に固定される構成プロパティ - 他のすべての構成プロパティは実行時にオーバーライド可能
Configuration property |
型 |
デフォルト |
---|---|---|
The list of suffixes used when attempting to locate a template file. By default, Environment variable: Show more |
list of string |
|
The additional map of suffixes to content types. This map is used when working with template variants. By default, the Environment variable: Show more |
Map<String,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:
By default, the 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 |
||
By default, the rendering results of injected and type-safe templates are recorded in the managed Environment variable: Show more |
boolean |
|
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 |
長 |
|
If set to Environment variable: Show more |
boolean |
|
5. スタンドアローンライブラリーとして使用する場合のQute
Quteは、主にQuarkusのエクステンションとして設計されています。しかし、「スタンドアロン」のライブラリとして使用することも可能です。この場合、いくつかの機能は利用できず、いくつかの追加設定が必要となります。
- エンジン
-
-
まず第一に、すぐに使用できるマネージド
Engine
インスタンスはありません。新しいインスタンスをEngine.builder()
で設定する必要があります。
-
- テンプレートロケーター
-
-
デフォルトでは、テンプレートロケーター は登録されていません。つまり、
Engine.getTemplate(String)
は機能しません。 -
カスタムテンプレートロケーターは
EngineBuilder.addLocator()
を使って登録するか、テンプレートを手動で解析して、Engine.putTemplate(String, Template)
で結果をキャッシュに格納することができます。
-
- テンプレートイニシャライザー
-
-
デフォルトで
TemplateInstance.Initializer
は登録されていないため、@TemplateGlobal
アノテーションは無視されます。 -
カスタム
TemplateInstance.Initializer
は、EngineBuilder#addTemplateInstanceInitializer()
を使用して登録でき、任意のデータと属性を使用してテンプレートインスタンスを初期化できます。
-
- セクション
-
-
デフォルトでは、セクションヘルパーは登録されていません。
-
デフォルトの値リゾルバーのセットは、便利な
EngineBuilder.addDefaultSectionHelpers()
メソッドとEngineBuilder.addDefaults()
メソッドを介して、それぞれ登録することができます。
-
- 値リゾルバー
-
-
ValueResolver
s は自動生成されません。-
@TemplateExtension
methods は動作しません。 -
@TemplateData
と@TemplateEnum
のアノテーションは無視されます。
-
-
デフォルトの値リゾルバーのセットは、便利な
EngineBuilder.addDefaultValueResolvers()
メソッドとEngineBuilder.addDefaults()
メソッドを介して、それぞれ登録することができます。組み込みの拡張メソッドによって提供されるすべての機能が、デフォルトの値リゾルバーによってカバーされているわけではありません。ただし、カスタムの値リゾルバーは ValueResolver.builder()
を使って簡単に構築することができます。 -
Qute がオブジェクトのプロパティーにアクセスし、パブリックメソッドを呼び出せるように、
Engine.addValueResolver (new ReflectionValueResolver())
を介してReflectionValueResolver
インスタンスを登録することが推奨されます。リフレクションは、一部の制限された環境では正しく動作しないか、GraalVM ネイティブイメージの場合の登録など、追加の設定が必要な場合があることに留意してください。
-
- ユーザー定義タグ
-
-
ユーザー定義のタグは自動的に登録されません。
-
タグは、
Engine.builder().addSectionHelper(new UserTagSectionHelper.Factory("tagName","tagTemplate.html")).build()
を介して手動で登録することができます。
-
- タイプセーフ
-
-
タイプセーフな式 は検証されていません。
-
タイプセーフメッセージバンドル はサポートされていません。
-
- インジェクション
-
Template
インスタンスを注入することはできませんし、その逆もできません。テンプレートは、inject:
およびcdi:
の名前空間を介して@Named
CDI Bean を注入することはできません。