M12i.

学術書・マンガ・アニメ・映画の消費活動とプログラミングについて

テンプレート・エンジンThymeleafのチュートリアルを読む(1)

f:id:m12i:20141031013800p:plain

・・・続いては、Spring Bootのリファレンスにも登場したテンプレート・エンジンThymeleaf(タイムリーフと読むのかな?)のチュートリアルです。

今回読んでみたのは同ライブラリのチュートリアル "Tutorial: Using Thymeleaf"の第4章です(2014/10/29 取得)。

なお、この章では各種の式について説明がなされていますが、繰り返し(イテレーション)やテンプレートのインクルードのようなトピックは登場せず、次章以降で取り扱われています。

          * * *

4 標準式構文

ここで、架空の食料雑貨店の開発の手を一旦止めて、Thymeleaf標準語〔Thymeleaf Standard Dialect〕のもっとも重要な部分──Thymeleaf標準式構文〔Thymeleaf Standard Expression syntax〕について学びましょう。

この構文で記述された妥当な属性値としては、これまでにすでに2つの例──メッセージと変数式とを見てきました:

<p th:utext="#{home.welcome}">Welcome to our grocery store!</p> 
<p>Today is: <span th:text="${today}">13 february 2011 </span></p>

しかしながら私たちの知らない値のタイプがまだあります。すでに知っている2つのタイプにもより興味深い側面が見つかるでしょう。はじめに、標準式が備える機能の簡潔な要約をみてみましょう:

  • 単純な式:
    • 変数式〔Variable Expressions〕: ${...}
    • 選択変数式〔Selection Variable Expressions〕: *{...}
    • メッセージ式〔Message Expressions〕: #{...}
    • リンクURL式〔Link URL Expressions〕: @{...}
  • リテラル
  • テキスト演算:
    • 文字列結合: +
    • リテラル置換: |The name is ${name}|
  • 算術演算:
  • 論理演算子:
  • 比較と同値性:
    • 比較演算子: >, <, >=, <= (gt, lt, ge, le)
    • 等号: ==, != (eq, ne)
  • 条件演算:
    • もし(if)ならば(then): (if) ? (then)
    • もし(if)ならば(then)さもなくば(else): (if) ? (then) : (else)
    • (value)でなければ(defaultValue): (value) ?: (defaultvalue)

これらすべては組み合わせたり入れ子にしたりすることができます:

'User is of type ' + (${user.isAdmin()} ? 'Administrator' : (${user.type} ?: 'Unknown'))

4.1 メッセージ

メッセージ式#{...}については私たちはすでに知っています。以下のテンプレートと:

<p th:utext="#{home.welcome}">Welcome to our grocery store!</p>

次のプロパティを結びつけるものでした:

home.welcome=¡Bienvenido a nuestra tienda de comestibles!

しかし私たちの知らない側面がまだあるのです。メッセージ・テキストが完全に静的なものでなかったら何が起こるのでしょうか? 例えば、私たちのアプリケーションは、そのときどきにサイトを訪れているユーザが誰であるか知っていて、彼/彼女の名前を使ったあいさつ文を表示したいとしましょう。〔例えば以下のように:〕

<p>¡Bienvenido a nuestra tienda de comestibles, John Apricot!</p>

すると私たちのメッセージにはパラメータを追加しなくてはなりませんね。ちょうどこんな感じです:

home.welcome=¡Bienvenido a nuestra tienda de comestibles, {0}!

パラメータはjava.text.MessageFormatの標準構文にしたがって指定されています。ということは、このクラスのJavadocに規定された方法で数値や日付にフォーマットを指定することもできるということです。

私たちのパラメータを指定し、userという名前のHTTPセッション属性を渡すためには次にようにします:

<p th:utext="#{home.welcome(${session.user.name})}"> Welcome to our grocery store, Sebastian Pepper! </p>

必要に応じて、カンマで区切る形式でいくつものパラメータを指定することもできます。ちなみにメッセージ・キーは変数から割り当てることもできます:

<p th:utext="#{${welcomeMsgKey}(${session.user.name})}"> Welcome to our grocery store, Sebastian Pepper! </p>

4.2 変数

先だって説明をさせていただいたとおり〔訳注:この訳出の範囲外です〕、変数式${...}は実際のところOGNL(Object-Graph Navigation Language)にほかなりません。OGNLの式はコンテキストに含まれる変数のマップにアクセスするものです。

OGNL構文と機能について詳しい情報を得たい場合は、こちらのページでOGNL言語ガイドを参照してください。

私たちが先ほど見てきたとおり、OGNLの構文にしたがうと:

<p>Today is: <span th:text="${today}">13 february 2011</span>.</p>

これ〔訳注:「これ」は${today}を指しています〕は、次のコードと同義です:

ctx.getVariables().get("today");

しかしながら次のようにして、OGNLはよりパワフルな式をつくるのにも使えます:

<p th:utext="#{home.welcome(${session.user.name})}"> Welcome to our grocery store, Sebastian Pepper! </p>

プログラム・コードでユーザ名を取得しようとすると:

((User) ctx.getVariables().get("session").get("user")).getName();

ゲッター・メソッドによる値の取得はOGNLの提供する多くの機能のうちのひとつに過ぎません。その他のものについて見てみましょう:

/*
 * ドット(.)はプロパティ〔訳注:ここではJavaBeansプロパティを指している〕にアクセスするのに使えます。プロパティのゲッター・メソッドを呼び出しているのと同義なのです。
 */
${person.father.name}

/*
 *ブラケット([])を使うことでもプロパティにアクセスすることは可能です。プロパティ名は変数として記述するかシングル・クオテーションで囲んで記述します。
 */
${person['father']['name']}

/*
 *対象のオブジェクトがMapインスタンスの場合、ドット記法もブラケット記法もget(...)メソッドの呼び出しと同義になります。
 */
${countriesByCode.ES}
${personsByName['Stephen Zucchini'].age}

/*
 *配列やコレクションの要素への添字によるアクセスもブラケットを使って行います。添字は引用符で囲いません。
 */
${personsArray[0].name}

/*
 *メソッドを呼び出すこともできます。引数をともなうそれも可能です。
 */
${person.createCompleteName()}
${person.createCompleteNameWithSeparator('-')}
式の中で使える基本的なオブジェクト

コンテキスト変数の内容に基づいてOGNLの式が評価されるとき、より高度な柔軟性のためにいくつかのオブジェクトが式の中で利用可能になります。これらのオブジェクトは(OGNL標準にしたがい)#記号ではじまる名前で参照されます:

  • #ctx:コンテキスト・オブジェクト
  • #vars:コンテキスト変数〔のマップ〕
  • #locale:コンテキストのロケール
  • #httpServletRequest:(Webコンテキストにおいてのみ有効)HttpServletRequestオブジェクト
  • #httpSession:(Webコンテキストにおいてのみ有効)HttpSessionオブジェクト

というわけで以下のような記述が可能になります:

Established locale country: <span th:text="${#locale.country}">US</span>.

Appendix Aのなかでこれらのオブジェクトについて完全なリファレンスを読むことができます。

式ユーティリティ・オブジェクト

上記のオブジェクト以外に、Thymeleafは一連のユーティリティ・オブジェクトを提供しています。これらのオブジェクトは私たちが式を記述するなかで一般的なタスクをこなすのを助けてくれます。

  • #dates: フォーマット化や部分値の抽出などjava.util.Dateオブジェクトのためのユーティリティ・メソッドを提供します。
  • #calendars: #datesに似ていますがこちらはjava.util.Calendarオブジェクトのためのものです。
  • #numbers:数値系オブジェクトのフォーマット化のためのユーティリティ・メソッドを提供します。
  • #strings: Stringオブジェクトのためのユーティリティ・メソッド──containsstartsWithのようなメソッドや、プリペンド/アペンドその他の操作を行うメソッドを提供します。
  • #objects:オブジェクト一般のためのユーティリティ・メソッドを提供します。
  • #bools:真偽値評価のためのユーティリティ・メソッドを提供します。
  • #arrays:配列のためのユーティリティ・メソッドを提供します。
  • #lists:リストのためのユーティリティ・メソッドを提供します。
  • #sets:セットのためのユーティリティ・メソッドを提供します。
  • #maps:マップのためのユーティリティ・メソッドを提供します。
  • #aggregates:配列やコレクションの集約を行うためのユーティリティ・メソッドを提供します。
  • #messages:変数式の中から外部化されたメッセージを取得するためのユーティリティ・メソッドを提供します。#{…}構文を使うことでも同じことができます。
  • #ids:繰り返し表示される要素(例えばイテレーションによりタグをレンダリングした結果としてそのようなことが起こる)のID属性の一意性問題を解決するためのユーティリティ・メソッドを提供します

それぞれのユーティリティ・オブジェクトが提供している関数についてはAppendix Bにも掲載されています。

私たちのホームページの日付を再フォーマット化する

さて、ユーティリティ・オブジェクトについての知見を得たので、それらを使って私たちのホームページに日付を表示する方法を変更することができるようになりました。HomeControllerのなかで行っていたことを:

SimpleDateFormat dateFormat = new SimpleDateFormat("dd MMMM yyyy");
Calendar cal = Calendar.getInstance();

WebContext ctx = new WebContext(request, servletContext, request.getLocale());
ctx.setVariable("today", dateFormat.format(cal.getTime()));

templateEngine.process("home", ctx, response.getWriter());

次のように変更することができます:

WebContext ctx = new WebContext(request, servletContext, request.getLocale());
ctx.setVariable("today", Calendar.getInstance());

templateEngine.process("home", ctx, response.getWriter());

そして日付のフォーマット化はビュー・レイヤで行わせることにしましょう:

<p> Today is: <span th:text="${#calendars.format(today,'dd MMMM yyyy')}">13 May 2011</span></p>

4.3 選択のための式(アスタリスク構文)

変数式は${...}という書式だけでなく、*{...}という書式でも記述することができます。

両者の間には重要な違いがあります:アスタリスク構文では、コンテキスト変数マップ全体に対してではなく、あらかじめ選択されたオブジェクトに対して評価が行われるのです。オブジェクトが選択されていない状態である限りは、アスタリスク構文とダラー構文〔${...}〕はまったく同義です。

では、オブジェクトの選択とはどういうことなのでしょうか? th:object属性による指定がそれです。実際に私たちのユーザ・プロフィール・ページ(userprofile.html)で使ってみましょう:

  <div th:object="${session.user}">
    <p>Name: <span th:text="*{firstName}">Sebastian</span>.</p>
    <p>Surname: <span th:text="*{lastName}">Pepper</span>.</p>
    <p>Nationality: <span th:text="*{nationality}">Saturn</span>.</p>
  </div>

これは次のように書くのとまったく同じ意味になります:

<div>
  <p>Name: <span th:text="${session.user.firstName}">Sebastian</span>.</p>
  <p>Surname: <span th:text="${session.user.lastName}">Pepper</span>.</p>
  <p>Nationality: <span th:text="${session.user.nationality}">Saturn</span>.</p>
</div>

もちろんダラー構文とアスタリスク構文は併用可能です:

<div th:object="${session.user}">
  <p>Name: <span th:text="*{firstName}">Sebastian</span>.</p>
  <p>Surname: <span th:text="${session.user.lastName}">Pepper</span>.</p>
  <p>Nationality: <span th:text="*{nationality}">Saturn</span>.</p>
</div>

何かしらのオブジェクトが選択されている状態のときは、#object変数を通じてダラー構文の中からも当該オブジェクトにアクセスすることが可能です:

<div th:object="${session.user}">
  <p>Name: <span th:text="${#object.firstName}">Sebastian</span>.</p>
  <p>Surname: <span th:text="${session.user.lastName}">Pepper</span>.</p>
  <p>Nationality: <span th:text="*{nationality}">Saturn</span>.</p>
</div>

前述のとおり、オブジェクトが選択されていない場合、ダラー構文とアスタリスク構文はまったく同義になります。

<div>
  <p>Name: <span th:text="*{session.user.name}">Sebastian</span>.</p>
  <p>Surname: <span th:text="*{session.user.surname}">Pepper</span>.</p>
  <p>Nationality: <span th:text="*{session.user.nationality}">Saturn</span>.</p>
</div>

4.4 リンクURL

その重要性により、URLはWebアプリケーション・テンプレートの世界において第一級の市民となっています。そのような次第でThymeleaf標準語ではURLのために特別な構文──@構文を用意しています: @{...}

URLにはいくつかの異なるタイプがあります:

  • http://www.thymeleaf.orgのような絶対URL
  • 次のような各種の相対URL:
    • user/login.htmlのようなページ相対URL
    • /itemdetails?id=3のようなコンテキスト相対URL(URLの先頭に当該サーバにおけるコンテキスト名が自動的に付与される)
    • ~/billing/processInvoiceのようなサーバ相対URL(同じサーバ上の別コンテキスト(=別アプリケーション)から呼ぶことも可能なURL)
    • //code.jquery.com/jquery-2.0.3.min.jsのようなプロトコル相対URL

Thymeleafはいかなる状況下でも絶対URLを処理できます。しかし相対URLについては、IWebContextインターフェースを実装したコンテキスト・オブジェクトを使用することが条件になります。このオブジェクトはHTTPリクエストに由来し、相対リンクを生成するのに必要となるいくつか情報を内包しているのです。

それでは、この新しい構文を使ってみましょう。th:href属性との初対面です:

<!—次のリンクは'http://localhost:8080/gtvg/order/details?orderId=3'を指すものになります (置換も行われています) -->
<a href="details.html" 
   th:href="@{http://localhost:8080/gtvg/order/details(orderId=${o.id})}">view</a>

<!—次のリンクは'/gtvg/order/details?orderId=3'を指すものになります (置換も行われています) -->
<a href="details.html" th:href="@{/order/details(orderId=${o.id})}">view</a>

<!--次のリンクは'/gtvg/order/3/details'を指すものになります (置換も行われています)  -->
<a href="details.html" th:href="@{/order/{orderId}/details(orderId=${o.id})}">view</a>

ここでいくつか気に留めておいてほしいことがあります:

  • th:hrefは属性を変更する属性です。処理が行われると、リンクURLが導出され、アンカー・タグのhref属性の値がこのURLで置き換えられます。
  • URLパラメータのために式を使用できます(先ほどの例に登場したorderId=${o.id}のように)。必要なURLエンコーディング処理は自動で行われます。
  • いくつかのパラメータが必要な場合は、次に示すようにカンマ区切りで指定します:@{/order/process(execId=${execId},execType='FAST')}
  • URLを構成するフラグメントのなかで変数が使用できます: @{/order/{orderId}/details(orderId=${orderId}))
  • 相対URLは/ではじまります(/order/detailsのように)。アプリケーション・コンテキスト名は接頭辞として自動で付与されます。
  • クッキーが有効でないかあるいは不明な状態である場合、セッションが持続するように相対URLの末尾に";jsessionid=..."という接尾辞が付与されます。これはURLリライティングと呼ばれるもので、あなたがサーブレットAPIを通じて、response.encodeURL(...)のメカニズムを使用した独自のリライティング・フィルターを組み込むことを可能にします。
  • 私たちのテンプレートではth:hrefが付与されたアンカー・タグには依然として静的なhref属性が残されていました(これはオプションです)。こうすることで画面のプロトタイピングの目的でテンプレートを直接ブラウザで表示するような場合でも、リンクは正しく機能する状態を保つことができます。

メッセージ構文(#{...})でもそうだったように、URL構文もまた他の式の評価結果から構成することが可能です:

<a th:href="@{${url}(orderId=${o.id})}">view</a>
<a th:href="@{'/details/'+${user.login}(orderId=${o.id})}">view</a>
ホームページにメニューを用意する

さて、リンクURLをつくる方法を理解したところで、私たちのサイトのホームにサイト内の他のページへの移動を可能にするちょっとしたメニューを追加してみましょうか。

<p>Please select an option</p>
<ol>
  <li><a href="product/list.html" th:href="@{/product/list}">Product List</a></li>
  <li><a href="order/list.html" th:href="@{/order/list}">Order List</a></li>
  <li><a href="subscribe.html" th:href="@{/subscribe}">Subscribe to our Newsletter</a></li>
  <li><a href="userprofile.html" th:href="@{/userprofile}">See User Profile</a></li>
</ol>

4.5 リテラル

テキスト・リテラル

テキスト・リテラルはシングル・クオテーションで挟まれた単なる文字列です。いかなる文字でも含めることができます。ただしリテラルのなかでシングル・クオテーションを使用するときは\'というようにエスケープする必要があります。

<p>
  Now you are looking at a <span th:text="'working web application'">template file</span>.
</p>
数値リテラル

数値リテラルは数値そのものです。

<p>The year is <span th:text="2013">1492</span>.</p>
<p>In two years, it will be <span th:text="2013 + 2">1494</span>.</p>
真偽値リテラル

真偽値リテラルtruefalse.です。例えば:

<div th:if="${user.isAdmin()} == false"> ...

上記の例では、== falseが波括弧の外側に記述されているため、Thymeleafエンジンがこれを処理しています。一方、波括弧の内側に記述をした場合、その処理はOGNL/SpringELエンジンの責任のもとに行われることになります:

<div th:if="${user.isAdmin() == false}"> ...
ヌル・リテラル

nullリテラルも式の中で使用することができます:

<div th:if="${variable.something} == null"> ...
リテラル・トークン

数値リテラル、真偽値リテラル、そしてヌル・リテラルは、実のところリテラル・トークンの特殊ケースなのです。

これらのトークンは標準式のなかでちょっとばかり簡略化を許されているのです。リテラル・トークンは〔訳注:ここでは一転してリテラル・トークンの一般論を述べています〕テキスト・リテラル('...')と同じようにはたらきますが、文字(A-Z とa-z)、数字(0-9),、ブラケット([ と])、ドット(.)、ハイフン(-)、そしてアンダースコア(_)のみ許されているのです。ホワイト・スペースやカンマ、あるいはその他の文字は使用できません。

そんなものが何の役に立つのでしょうか? トークンは引用符で囲ってやる必要がありません。したがって次のように記述することが可能なのです:

<div th:class="content">...</div>

つまり次のようにかく必要がないのです:

<div th:class="'content'">...</div>

4.6 テキストを連結

テキストは、それがリテラルであろうが何がしかの変数やメッセージの評価結果であろうが関係なく、+演算子を使って簡単に連結を行うことができます:

th:text="'The name of the user is ' + ${user.name}"

4.7リテラル置換

リテラル置換は+演算子によるリテラルの連結なしに、変数由来の値を含む文字列を書式化するための簡単な方法です。
置換をするにはリテラルをヴァーティカル・バー(|)で囲ってあげる必要があります:

<span th:text="|Welcome to our application, ${user.name}!|">

これは以下のようにするのと事実上同義です:

<span th:text="'Welcome to our application, ' + ${user.name} + '!'">

リテラル置換もまた他のタイプの式と組み合わせることができます:

<span th:text="${onevar} + ' ' + |${twovar}, ${threevar}|">

Note リテラルの中で変数式(${...})が使用できるのはリテラル置換の中においてだけです。他のリテラル──文字列リテラルや真偽値・数値トークン〔訳注:前述のとおり真偽値と数値はリテラルというよりはトークンのサブセットなのだそうです〕、条件式などの中では使用できません。

4.8 算術演算

いくつかの算術演算もまた可能です:+-*/ そして %

th:with="isEven=(${prodStat.count} % 2 == 0)"

Note これらの演算子OGNLの変数式それ自体の中でも使用可能です(そしてこの場合、Thymeleaf標準語エンジンではなくOGNLのエンジンが演算処理を行います):

th:with="isEven=${prodStat.count % 2 == 0}"

Note 演算子のなかにはテキスト・エイリアスを持つものもあります:div (/)、 mod (%)。

4.9比較演算子と等価性

Note 式の中の値は><>= そして <=記号により比較することができ、また同様にして== と !=という2つの演算子で等価性(あるいは非等価性)を検証することができます。XMLの制約により<>の2つの記号はそのままのかたちでは属性値のなかでは使用できず、&lt; &>にそれぞれ置き換えてやる必要があります。

th:if="${prodStat.count} &gt; 1" th:text="'Execution mode is ' + ( (${execMode} == 'dev')? 'Development' : 'Production')"

Note これらの演算子にはテキスト・エイリアスが用意されています:gt (>)、lt (<)、ge (>=)、le (<=)、not (!)。 そしてまたeq (==)、neq/ne (!=)。

4.10 条件式

条件式は、条件(それ自体が式です)の評価結果に基づき、2つの式のうちいずれか1つのみが評価されるというものです。
具体例を見てみましょう(今回はth:classという新しい属性修飾子が登場しています):

<tr th:class="${row.even}? 'even' : 'odd'"> ... </tr>

条件式の3部分 (condition、then、そしてelse)はそれ自体が式です。ということはつまり、そこでは変数(${...}*{...})やメッセージ(#{...})、URL(@{...})やリテラル('...')が使用可能だということです。

条件式は丸括弧で入れ子にすることもできます:

<tr th:class="${row.even}? (${row.first}? 'first' : 'even') : 'odd'"> ... </tr>

「さもなくば」の部分は省略も可能です。この場合、条件部の評価結果がfalseとなると条件式全体ではnullが返されることになります:

<tr th:class="${row.even}? 'alt'"> ... </tr>

4.11 デフォルト式(エルヴィス演算子)

デフォルト式は「ならば〜」〔then〕の部分を伴わない特殊な条件式です。Groovyなどいくつかの言語で提供されているのと同じで、条件式には2つの式を指定します。そして2つめの式が評価されるのは、1つめの式がnullを返す時だけです。
私たちのユーザ・プロフィール・ページで実際に試してみましょう:

<div th:object="${session.user}"> ... <p>Age: <span th:text="*{age}?: '(no age specified)'">27</span>.</p> </div>

ご覧のように、?:演算子とともに、*{age}という式の評価結果がnullとなった時にだけ使用されるデフォルトの名前(ここではリテラル値が使用されています)が指定されています。すでに述べてきたとおりこれは次の記述と同義です:

<p>Age: <span th:text="*{age != null}? *{age} : '(no age specified)'">27</span>.</p>

デフォルト式も条件式の仲間なので、丸括弧で入れ子になった式を内包することができます:

<p> Name: <span th:text="*{firstName}?: (*{admin}? 'Admin' : #{default.username})">Sebastian</span> </p>

4.12 事前処理

これら各種の式の処理に加えて、Thymeleafは事前処理式〔preprocessing expressions〕の機能も提供しています。

事前処理とはどのようなものなのでしょうか? それは通常の式の処理よりも前に実行されるもので、最終的に処理されることになる実際の式を事前に変更することを可能にします。

事前処理式は、二連のアンダースコア記号に囲われているほかは通常の式とまったく同じに見えます(__${expression}__というように)。

ここで国際化のために用意されたMessages_fr.propertiesファイルを想像してみてください。このファイルでは言語固有の静的メソッドを呼び出すOGNL式を内包したプロパティが宣言されています:

article.text=@myapp.translator.Translator@translateToFrench({0})

そしてMessages_es.propertiesファイルでは同様に:

article.text=@myapp.translator.Translator@translateToSpanish({0})

私たちは単一の式やロケールに依存するその他のものを評価するマークアップ・フラグメントを作成することができます。そのためには、まず(事前処理によって)式を選択し、続いてそれをThymeleafに実行させるのです:

<p th:text="${__#{article.text('textVar')}__}">Some text here...</p>

Frenchロケールにおける事前処理の結果は次の記述と同等のものになります:

<p th:text="${@myapp.translator.Translator@translateToFrench(textVar)}">Some text here...</p>

二重のアンダースコアは\_\_というかたちで エスケープ可能です。

          * * *

以上、原典はThymeleafライブラリのチュートリアル "Tutorial: Using Thymeleaf"の第4章です(2014/10/29 取得)。


Spring関連の訳出記事まとめ - M12i.