ユーザーメッセージ (UserMessages)
ユーザーメッセージとは?
誰向けのメッセージ?
基本的に、アプリケーションのユーザー (一般消費者の会員や内部管理とするオペレーターなど) に対するメッセージを指します。 つまり、コンピューター向けではなく人間に対するメッセージで、そして、ディベロッパー (エンジニア、プログラマー) ではない人のためのメッセージです。 画面に表示されることが前提です。ゆえに、体裁も整えつつ丁寧に状況を伝える必要がありますし、逆にあまりセキュアな情報は載せてはいけません。
主に、"何々は必須です" と言ったようなバリデーションエラーのメッセージが相当しますが、業務例外のときのメッセージも同じです。 主には、この二つの事象をユーザーに伝えるメッセージと考えて良いでしょう。
ただ、それが必ずしも自然言語とは限りません。例えば、JSON APIのアプリでクライアントメッセージ方式であれば、サーバーAPIからすると、ユーザーメッセージはクライアントアプリが解釈できる情報を戻すことになります。 とはいえ、もちろんのこと、システム全体から見れば最終的には自然言語のメッセージに翻訳されることにはなるでしょう。
どこで管理してる?
LastaFluteでは、src/main/resources の [app]_message.properties にて管理しています。 UTF-8で読み込んでいるので、日本語をユニコードエスケープする必要はありません。
e.g. validation error and application exception message @[app]_message.properties
constraints.Required.message = is required
constraints.TypeAny.message = should be {propertyType}
constraints.TypeInteger.message = should be number
...
errors.login.failure=could not login
errors.app.illegal.transition=retry because of illegal transition
errors.app.db.already.deleted=others might be deleted, so retry
継承できてタイプセーフ
継承関係を持っているので、マルチプロジェクトでもプロジェクト間で再利用もできますし、オーバーライドを活用してプロジェクトごとに微調整することもできます。
e.g. リファレンス実装のMaihamaプロジェクトでのProperties継承構造 @Directory
maihama-common
|-src/main/resources
|-maihama_message.properties // 共通のメッセージリソース
maihama-dockside
|-src/main/resources
| |-dockside_message.properties // アプリ固有のメッセージリソース
また、DBFlute の FreeGen を使って、propertiesに対応した [App]Messages クラスを自動生成することができます。 プログラム上で、properties のキーをハードコートすることは基本的にありません。
e.g. auto-generated method for message @Java
messages.addConstraintsRequiredMessage(...);
メッセージの指定方法
Validatorアノテーションなら
それぞれValidatorアノテーションごとに、対応するメッセージが決まっています。 例えば、@Requiredアノテーションであれば、constraints.Required.message になります。 ゆえに、ディベロッパーが意識して指定するということはあまりありません。
e.g. property for message @Java
@Required // constraints.Required.message が利用される
public String mysticOneman;
とある状況だけ、Validatorアノテーションのメッセージを差し替えたい場合は、message属性にメッセージのキーを指定します。 そのとき、ハードコードで指定するのではなく、FreeGenで自動生成された [App]Messages の定義を使って指定しましょう。
e.g. property for message @Java
@Required(message=DocksideMessages.ERRORS_SEA_LAND)
public String mysticOneman;
組み込みの業務例外なら
LastaFluteに組み込まれてる業務例外であれば、例外ごとに対応するメッセージが決まっています。
- LoginFailureException
- ログイン画面へリダイレクト (errors.login.failure)
- RequestIllegalTransitionEx...
- show_errors.html (errors.app.illegal.transition)
- DoubleSubmittedRequestEx...
- verifyToken()で指定 (errors.app.double.submit.request)
- EntityAlreadyDeletedEx...
- show_errors.html (errors.app.db.already.deleted)
- EntityAlreadyUpdatedEx...
- show_errors.html (errors.app.db.already.updated)
- EntityAlreadyExistsEx...
- show_errors.html (errors.app.db.already.exists)
ゆえに、それらメッセージのキーは、削除したり変更したりしてはいけません。(文言の修正はOK)
e.g. six framework-embedded messages @[app]_message.properties
# ----------------------------------------------------------
# Application Exception
# ---------------------
# /- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# six framework-embedded messages (don't change key names)
# - - - - - - - - - -/
errors.login.failure=could not login
errors.app.illegal.transition=retry because of illegal transition
errors.app.db.already.deleted=others might be deleted, so retry
errors.app.db.already.updated=others might be updated, so retry
errors.app.db.already.exists=already existing data, so retry
errors.app.double.submit.request=double submit might be requested
あとはプログラム上で指定する
FreeGenで自動生成された [App]Messages の add...() メソッドを使って指定します。
例えば、相関バリエーションなどはプログラムで実装するので、そのとき活用します。
e.g. moreValidate() uses add...() @Java
@Execute
public HtmlResponse index(int productId, SeaLandForm form) {
validate(form, messages -> moreValidate(form, messages), () -> {
return asHtml(path_Sea_SeaLandHtml);
});
...
}
private void moreValidate(SeaLandForm form, HarborMessages messages) {
if (form.sea != null && form.land != null) {
if (form.sea.length() > form.land.length()) {
messages.addErrorsSeaLand("sea");
}
}
}
また、独自の業務例外であれば、同じように [App]Messages の add...() メソッドを使います。
e.g. ActionでUserMessagesを使った業務例外のthrow @Java
throw new MessagingApplicationException("..."
, new HarborMessages().addErrorsSignup...(UserMessages.GLOBAL, ...));
メッセージ修正の仕方
src/main/resources の [app]_message.properties のメッセージを修正します。 (UTF-8で読み込んでいるので、日本語をユニコードエスケープする必要はありません)
文言の修正だけであれば、FreeGenを叩かなくても動作に影響はないですが、[App]Messagesクラスの JavaDoc を更新できるので、一応叩いておくと良いでしょう。
マルチプロジェクトの場合、継承関係を構築していますので、うまくプロジェクト間での再利用や、オーバーライドなどを活かしましょう。
メッセージ追加の仕方
src/main/resources の [app]_message.properties にメッセージを追加します。 (UTF-8で読み込んでいるので、日本語をユニコードエスケープする必要はありません)
追加したら、FreeGenを叩きます。[App]Messagesに add...() メソッドが追加され、アプリで呼べるようになります。
どの項目のためのメッセージか?
[App]Messages で add...() を使うときに、必ず第一引数で 関連する項目のプロパティ名 を指定します。どの項目のためのメッセージか?を表現するためです(画面側でその名前で制御します)。
サーバーサイドHTMLであればHTML上の名前、JSON APIであればJSON上の名前が適しています。 自然と、FormクラスやBodyクラスの変数名と同じになることが多いでしょう。
e.g. property for message @Java
messages.addConstraintsRequiredMessage("productName");
特に項目に依存しないトップレベルのメッセージであれば、_global を指定するのが LastaFlute での慣習です。UserMessages.GLOBAL 定数を使って指定できます。業務例外などではこちらを利用することがほとんどでしょう。
e.g. property for message @Java
messages.addConstraintsRequiredMessage(UserMessages.GLOBAL);
ちなみに、Validatorアノテーションによるメッセージの場合は、アノテーションが付与されているプロパティの名前がそのまま利用されます。
主語ありメッセージ
メッセージを主語ありにしたいときは、メッセージに {item} を付与します。
e.g. subjective message @[app]_message.properties
constraints.Required.message = {item} is required
[app]_label.properties に定義されている labels.[変数名] に合致するラベルがあれば、その値が表示されます。 存在しない場合は、変数名がそのまま表示されます。(変数名がそのまま表示されていいことはまずないと思うので、labelを用意しましょう)
動的変数付きメッセージ
インデックス変数 e.g. {0} of dreams
プログラムからの動的な値をメッセージの中に埋め込みたいときは、 中括弧 {} で囲って、その中に番号を付けましょう。e.g. {0}, {1}
e.g. indexed variable message @[app]_message.properties
errors.maihama = {0} of dreams
- ゼロ始まりであること e.g. {0} of dreams, sea of {1}
- 抜け番がないこと
- 同じ番号を使わないこと
FreeGenをすると、自動生成された [App]Messages の add...() メソッドの引数で、動的パラメーターを指定することができます。
DBFlute-1.1.4 より
指定された番号の順番で、引数になります。
- {0} of {1} であれば、String arg0, String arg1
- {1} of {0} であっても、String arg0, String arg1
つまり、メッセージ上の番号の出現位置は、引数の順番に関係ありません。 これは、以下のことを考慮してあえてこのようにしています。
- 後からメッセージ(自然言語)の都合で、変数位置を変えてもプログラムに影響がない
- 多言語対応のとき他の言語では入れ替わる可能性があるので、出現位置への意識は少ない方が良い
※基本的に、LastaFlute-1.0.0 以上であれば DBFlute-1.1.4 以上を利用しましょう。 互いに処理を合わせて作っているので、一番スマートに利用できます。
DBFlute-1.1.3 まで
メッセージの出現位置の順番で、引数になります。
- {0} of {1} であれば、String arg0, String arg1
- {1} of {0} だと逆に、String arg1, String arg0
これだと、後からメッセージの都合で変数位置を変えた場合、プログラムの引数指定も修正してあげないといけません。
名前付き変数 (@since LastaFlute-1.0.0)
変数に名前を付けることもできます。中括弧 {} で囲って、その中に名前を入れます。e.g. {sea}
e.g. named variable message @[app]_message.properties
errors.maihama = {sea} of dreams
- ただし、{item} は予約されている言葉なので、動的パラメーターの名前としては利用できない
- Javaの引数名で使える文字であること (記号とかは利用できない)
- 同じ名前は使わないこと
同じく、FreeGenをすると、自動生成された [App]Messages の add...() メソッドの引数で、動的パラメーターを指定することができます。 指定された名前がそのまま引数名になります。
DBFlute-1.1.4 より
指定された名前がソートされて引数の順番になります。
- {sea} of {land} であれば、String land, String sea つまり自然ソート
- ただ、特別なのもあって、{min} and {max} ならString min, String max
実際に、どうソートされるかはあまり意識する必要はなく、固定化されるということに意義があります。 これは、以下のことを考慮してあえてこのようにしています。
- 後からメッセージ(自然言語)の都合で、変数位置を変えてもプログラムに影響がない
- 多言語対応のとき他の言語では入れ替わる可能性があるので、出現位置への意識は少ない方が良い
- ☆多言語対応のとき他の言語で入れ替わっても、正確にマッピングできるようにするため
※基本的に、LastaFlute-1.0.0 以上であれば DBFlute-1.1.4 以上を利用しましょう。 互いに処理を合わせて作っているので、一番スマートに利用できます。
DBFlute-1.1.3 まで
メッセージの出現位置の順番で、引数になります。
- {sea} of {land} であれば、String sea, String land
- {land} of {sea} だと逆に、String land, String sea
これだと、後からメッセージの都合で変数位置を変えた場合、プログラムの引数指定も修正してあげないといけません。 また、内部的な話ですが、多言語対応のときに他の言語で入れ替わったときに、正確にマッピングができません。
もろもろ補足
古いバージョン (@before LastaFlute-1.0.0) では、名前付きパラメーターが利用できません。 FreeGenで自動生成はされますが、実際に処理をすると NumberFormatException が発生します。 厳密には、Validatorアノテーションによるメッセージでは利用できますが、messages.add...()メソッドによるメッセージでは利用できません。 そのときはインデックス変数を使いましょう。
ちなみに、組み込みのメッセージ (Hibernate Validator のアノテーションに対応したメッセージ) に関しては、渡せるパラメーターが固定化されているので、そちらで使うことは基本的になく、アプリ独自のメッセージを追加するときに使うことが想定されます。
あと、メッセージに変数をたくさん付けることはあまりないと思うので、インデックス変数で十分かもしれません。 Hibernate Validator の都合により、名前付き変数もサポートしているだけと言えるかもです。
メッセージの多言語対応
TODO jflute now writing... [app]_message_ja.properties を作って、UserLocale...