LastaFlute の Thymeleaf

概要

Thymeleafを利用

LastaFluteで、サーバーサイドHTMLでWebアプリを作成する場合、Thymeleaf がオススメです。Exampleでも Thymeleaf を利用しています。

LastaFlute と Thymeleaf の連携ライブラリ Lasta Thymeleaf も提供しています。

FreeGenを使ってタイプセーフ指定

DBFluteのFreeGenの仕組みを使って、HTMLテンプレートの指定をタイプセーフにします。

  1. ThymeleafのHTMLテンプレートを作成 (src/main/webapp/WEB-INF/view配下)
  2. DBFlute の FreeGen を叩く (manage.sh 12)
  3. Actionクラスで自動生成されたパス定義を利用 (asHtml(path_...))
e.g. you can use Template Path for HtmlResponse @Java
@Execute
public HtmlResponse index() {
    ...
    return asHtml(path_Mypage_MypageHtml);
}

la:属性を使ってスムーズ連携

la:[attr-name] というLastaFluteオリジナルの属性が幾つか提供されていて、LastaFluteやDBFluteとのスムーズな連携ができます。 (特に、区分値のCDefとの連携やバリデーションエラーとの連携は便利です)

e.g. la:property as productName, related to form property @Html
<input type="text" la:property="productName"/>
e.g. la:optionCls as productStatus, related to CDef.ProductStatus @Html
<select la:property="productStatus">
    <option value="" th:text="#{labels.listbox.caption.tell}"></option>
    <option la:optionCls="ProductStatus"></option>
</select>

Lasta Thymeleafのアーキテクチャ

LastaFlute Thymeleaf Architecture

HtmlPathを自動生成しよう

自動生成のコンセプト

JavaプログラムからHTMLテンプレートのファイルを指定するとき、ファイル名をハードコードするのではなく、自動生成された定義を利用します。 自動生成の仕組みを利用して、HTMLテンプレートとプログラム上の指定にズレが発生しないようにしています。

e.g. you can use Template Path for HtmlResponse @Java
@Execute
public HtmlResponse index() {
    ...
    return asHtml(path_Mypage_MypageHtml);
}

自動生成のやり方

まず、HTMLテンプレートを作成 (まだ空っぽでもOK)

WEB-INF/viewの下であればファイル名や配置ディレクトリは任意ですが、対応する Action クラスがわかるような名前を付けましょう。例えば、以下のように。

product/product_list.html
/product/list/
profile/profile_password_change.html
/profile/password/change/
mypage/mypage.html
/mypage/
e.g. Template File Location @Directory
src/main/webapp
 |-WEB-INF
    |-view // HtmlTemplateの配置場所
    |  |-product // productパッケージに対応
    |  |  |-product_list.html     // /product/list/
    |  |  |-product_purchase.html // /product/purchase/
    |  |
    |  |-profile
    |  |  |-profile_password_change.html // /profile/password/change/
    |  |  |-profile_withdrawal.html      // /profile/withdrawal/
    |  |
    |  |-mypage
    |  |  |-mypage.html // /mypage/
    |  |
    |  |-root.jsp // '/'
    |
    |-web.xml

そして、DBFlute の FreeGen を叩くと、パス定義が自動生成されて、Actionクラスで補完できるようになります。 (自動生成された定義は Action クラスが implements しているのでそのまま補完できます)

自動生成のタイミング

少なくとも、自動生成してからでないと Action の実装は完結しません。

自動生成は中身を見ませんので、とりあえず空っぽの状態でHTMLテンプレートのファイルだけ作って自動生成をしておいて、Actionの実装をしながらテンプレートも同時に実装していく というやり方でも良いでしょう。

コンフリクトはしない?

自動生成されるクラスは単純な定義だけなので、基本的にGitの機能で自動マージされますので、気にせずトピックブランチでコミットしてOKでしょう。 もし、同じディレクトリに同じファイル名を作った場合は、そもそもHTMLテンプレート自体がコンフリクトするでしょう。

LastaFluteオリジナル表現

la:property="プロパティ名"

入力項目のinputタグと、Formのプロパティを紐付ける属性です。

e.g. la:property as productName, related to form property @Html
<input type="text" la:property="productName"/>

リクエスト時は対応するFormのプロパティにマッピングされ、レスポンス時は対応するFormのプロパティの値が表示されます。 (単に、name属性とvalue属性で、同じプロパティ名を二回書くのを省略しているだけとも言えます)

バリデーションエラー表示のときは、class属性に "validError" (validation の error という意味) が付与されます(既存のclass属性に対して追加になる)。 バリデーションエラー対象の項目 (テキストボックスなど) に色を付けたりするのに利用できます。

e.g. la:errors result, required check @Html
<input name="productName" value="" class="validError" type="text">

対応するクラスは、PropertyAttrProcessor です。

la:optionCls="区分値名"

リストボックスのoptionタグを、CDef や AppCDef の区分値を使って構築する属性です。

e.g. la:optionCls as productStatus, related to CDef.ProductStatus @Html
<select la:property="productStatus">
    <option value="" th:text="#{labels.listbox.caption.tell}"></option>
    <option la:optionCls="ProductStatus"></option>
</select>

CDef や AppCDef は、sponsor.[App]ListedClassificationProvider にて解決されます。 アプリのクラスなので、新しく NamedCls を使ったり、全体の優先順位を変えたりなど、自由にカスタマイズできます。

対応するクラスは、OptionClsAttrProcessor です。

la:errors="プロパティ名"

バリデーションエラーや業務例外のメッセージを表示するための属性です。

e.g. la:errors as productStatus, related to CDef.ProductStatus @Html
<form th:action="@{/member/edit/}" action="#" method="post">
    <span la:errors="_global"/>
    ...
    <input type="text" la:property="memberName"/>
    <span la:errors="memberName"/>

指定されたプロパティに対応するエラーメッセージが表示されます。

e.g. la:errors result, required check @Html
<input name="productName" value="" class="validError" type="text">
<span class="errors">is required</span>

エラー発生時は、そのspanタグのclass属性に errors というスタイルが自動的に付与されます(既存のclass属性に対して追加となる)。エラーメッセージに色を付けたりするのに利用できます。エラーが発生しないときは、spanタグ自体が消えます。

一つのプロパティに複数のエラーメッセージが関連付けられることもあります。 (そのままのspanだと、ブラウザのデフォルトとして半角空白一つ挟んで並んで表示されます。それが見た目として不都合な場合は、class の errors を使ったりタグを変えたりしてデザインを調整すると良いでしょう)

e.g. la:errors result, plural messages for one property @Html
<input name="productName" value="sealandpiari" class="validError" type="text">
<span class="errors">length must be between 0 and 10</span>
<span class="errors">not a well-formed email address</span>

主語なしメッセージ方式 であれば、spanタグで入力項目のタグのすぐ近くに配置するのがオーソドックスです。違うタグでも利用できますので、そこはHTMLのコーディングポリシーに合わせましょう。 一方で、特定のプロパティに属さないメッセージ (業務例外のメッセージも含む) も表示する場合は、_global を指定した la:errors を上部にひとつ配置しておくと良いでしょう。

主語ありメッセージ方式 であれば、プロパティ名に all と指定することで、すべてのプロパティに対するメッセージが展開されます。 この場合は、画面の上部にひとつ配置するのがオーソドックスです。liタグなどで利用することが想定されます。 加えて、[app]_message.propertiesにて、{item}を使って主語ありメッセージにしておく必要があります。

対応するクラスは、ErrorsAttrProcessor です。

la:token="true"

二度押し防止のトークンを出力します。

e.g. la:token on hidden field @Html
<form th:action="@{/member/edit/}" action="#" method="post">
    <input type="hidden" la:token="true"/>

formタグの直下などに配置したhiddenフィールドに付与するのがオーソドックスです。 Actionで発行したトークンが展開され、submit時にリクエストパラメーターとして送信されて二度押し防止チェックされます。

Actionでトークンの発行処理をする必要があります。

対応するクラスは、TokenAttrProcessor です。

#{labels.キー}

HTMLテンプレートのなかで、[app]_label.properties を利用することができます。

e.g. use label of [app]_label.properties @Html
<span th:text="#{labels.productName}">labels.productName</span>
<input type="text" la:property="productName"/>

項目表現を統一したり、多言語対応するときなどに役に立ちます。 プロジェクトで利用するかしないかは、統一的なポリシーにした方が良いでしょう。 (割り切って全く利用しないという選択肢もあるでしょう)

#handy.format(日付)

日付のフォーマットを、HTMLテンプレート内で実行することができます。

e.g. format date of Birthdate as config's pattern @Html
<span th:text="${#handy.format(member.birthdate)}">dummy</span>

日付フォーマット (Pattern) を指定しないメソッドの場合は、[app]_config.properties で定義されているフォーマットが利用されます。 統一的なフォーマット管理ができます。(デフォルトのフォーマットと考えて良いでしょう)

e.g. date patterns in [app]_config.properites @Html
# The pattern of date as application standard used by e.g. Thymeleaf handy.format()
app.standard.date.pattern = yyyy/MM/dd

# The pattern of date-time as application standard used by e.g. Thymeleaf handy.format()
app.standard.datetime.pattern = yyyy/MM/dd HH:mm:ss

# The pattern of time as application standard used by e.g. Thymeleaf handy.format()
app.standard.time.pattern = HH:mm:ss

"デフォルトとは違うフォーマットで表示" というような場合は、第二引数でフォーマットを指定します。

e.g. format date of Birthdate as specified pattern @Html
<span th:text="${#handy.format(member.birthdate, 'yyyy@MM@dd')}">dummy</span>

"日付部分(yyyy/MM/dd)はデフォルトと同じだけど、時間部分(HH:mm:ss)だけがデフォルトとは違う" というような場合に、$$date$$, $$datetime$$, $$time$$ が利用できます。

e.g. format date of FormalizedDatetime as specified pattern @Html
<span th:text="${#handy.format(member.formalizedDatetime, '$$date$$ HH@mm@ss')}">dummy</span>

HtmlBeanのプロパティは、LocalDate, LocalDateTime など日付型で定義します。 プロパティの値が null なら format() の戻りは null になり、th:textの制御によって画面の表示上は "空" になります。 (そもそも必須項目であれば、@Required を付けておきましょう)

e.g. date property as LocalDate in HtmlBean @Java
@Required
public LocalDate latestPurchaseDate;

Action側でフォーマットするやり方もできますが、実装ポリシーを定めないとフォーマットの実装の仕方や管理がバラバラになる可能性もあります。 HTMLテンプレートでのフォーマットでも統一的な管理ができますし、さらにプロパティの型を日付型のまま扱えてタイプセーフになるので、特に大きな理由がなければ #handy.format() を使うのをオススメしています。(いずれにせよ、画面ごとに変えるのではなくプロジェクト全体で統一しましょう)

#handy は、他にもメソッドがあります。詳しくは HandyDateExpressionProcessor を参照。

${errors.exists('プロパティ名')}

エラーメッセージがあるかどうか、判定することができます。 例えば、バリデーションエラーのときに、エラー対象のテキストボックスにスタイルを付けたい時に利用できます。

e.g. add valid-error if validation error to memberName @Html
<input type="text"
    la:property="memberName"
    th:class="${errors.exists('memberName')} ? 'valid-error'"/>

la:errorsは、内部的にはこの "errorsオブジェクト" を使っています。

errorsオブジェクトには、他にもメソッドがあります。詳しくは ErrorMessages を参照。

HtmlBeanを作ろう

HtmlBeanに詰め替えよう

DBFluteから検索したEntityは、HtmlBeanに詰め替えて registerData() に登録します。Entityのまま registerData() に登録することはできません。(LastaFluteが、できる範囲でチェックしています)

e.g. mapping entity to HTML bean in Action class @Java
@Execute
public HtmlResponse index(int productId, SeaLandForm form) {
    ...
    ListResultBean<Purchase> purchaseList = purchaseBhv.selectList(cb -> {
        ...
    });
    List<SeaLandRowBean> beans = purchaseList.stream().map(purchase -> {
        SeaLandRowBean bean = new SeaLandRowBean();
        bean.purchaseId = purchase.getPurchaseId();
        purchase.getMember().alwaysPresent(member -> {
            bean.memberName = member.getMemberName();
        });
        ...
        return bean;
    }).collect(Collectors.toList());
    return asHtml(path_Sea_SeaLandHtml).renderWith(data -> {
        data.register("beans", beans);
    });
}

HTMLテンプレートの世界はコンパイルセーフではないので、Entityを直接渡して参照すると、DB変更時のコンパイルエラー検出ができなくなります。 HTMLテンプレートの中でDBのスキーマ構造を意識した実装をしないようにしましょう。

HtmlBeanのバリデーション

Thymeleafに渡す HtmlBean に、@Required などの "Validatorアノテーション" を付与してバリデーションすることができます。

e.g. Required annotation in HTML bean class @Java
public class SeaLandBean {

    @Required
    public Integer memberId;

    @Required
    public String memberName;

    ...
}

不正なデータを渡してしまっても、HTMLテンプレートの処理の中ではエラーにならず表示されてしまう可能性も高いですし、エラーになったとしてもテンプレート処理内だとデバッグがしづらいものです。 渡す前にチェックをかけられるとデバッグコストが減るでしょう。また、可読性も良くなります。

キリがないというのもあるので、@Required (+ @Valid) だけ付けるというのでも良いでしょう。

Lasta Thymeleafの環境準備

Lasta Thymeleaf を使った "Exampleプロジェクト" (e.g. harbor, dockside) からスタートアップすれば自然と組み込まれていますが、もしゼロから準備するのであれば、このようになります。

lasta-thymeleafの依存ライブラリ定義

pom.xml に lasta-remoteapi を定義します。

e.g. Lasta Thymeleaf dependency @pom.xml
<lasta.thymeleaf.version>0.3.1</lasta.thymeleaf.version>

...

<dependency>
    <groupId>org.lastaflute.html</groupId>
    <artifactId>lasta-thymeleaf</artifactId>
    <version>${lasta.thymeleaf.version}</version>
</dependency>

AssistantDirectorで組み込み

AssistantDirector の prepareWebDirection() にて LastaThymeleaf を組み込みます。

e.g. Lasta Thymeleaf in AssistantDirector @Java
@Override
protected void prepareWebDirection(FwWebDirection direction) {
    direction.directRequest(createUserLocaleProcessProvider(), createUserTimeZoneProcessProvider());
    ...
    direction.directHtmlRendering(createHtmlRenderingProvider());
}

...

protected HtmlRenderingProvider createHtmlRenderingProvider() {
    return new ThymeleafRenderingProvider().asDevelopment(config.isDevelopmentHere());
}

...

asDevelopment()で、[app]_env.properties の development.here (開発環境かどうか?) のプロパティと連動させるのがオススメです。

lastafluteMap.dfpropで自動生成定義

HTMLテンプレートのパス定義が自動生成されるようにするために、lastafluteMap.dfprop の freeGenList に html を追加します。

e.g. add html to freeGenList @lastafluteMap.dfprop
; appMap = map:{
    ; harbor = map:{
        ; path = ..
        ; freeGenList = list:{ env ; ... ; template ; html ; doc ; ... }
        ; propertiesHtmlList = list:{ env ; config ; label ; message }
    }
}

Thymeleaf3対応を進めています

2018年4月時点の最新バージョン 0.3.1 では、Thymeleaf2 を利用しています。

現場の協力も得ながら(大感謝!)、Thymeleaf3対応を進めています。

ユーザーの方のブログをご紹介

実際に業務で Lasta Thymeleaf を利用されていた方が素敵なブログを書いてくださっているのでご紹介します。 ありがとうございます!(リンクの承諾も頂いています)

とっても嬉しいものですね。しかも、すごくわかりやく丁寧に書かれています。 もちろん、もっと早くドキュメントができていたら、ユーザーの方がこういった試行錯誤せずにスムーズに利用できたのかもしれませんので、 そこは大変申し訳ないところでごめんなさい...。

Lasta Thymeleaf 以外の話題も書いてくださっています。