vermeer.hatenablog.jp
の続き。
メッセージ一覧の出力順序について もう少し具体的にイメージを整理して どのパターンのまで 実装のルール決めで出来そうか考えてみたいと思います。
項目毎にメッセージ出力する というパターンは今回は扱いません。
画面イメージのルールとして、順序は数字・アルファベットの順番 とします。
FromXXは、Formクラスの型とします。
ラベル(項目名)を、Formクラスで指定しても良い、してはいけない というのを判断するための材料にします。
メッセージ編集は
vermeer.hatenablog.jp
を前提としています。
ざっくりの仕様
- Formのラベルをプロパティを管理
Formのクラスパス+[.label]をキー、値にFormのラベルを保持しておいて、変換時に使用する。
ラベルを一括で管理できるので、表記の統一がしやすい。
国際化対応時に対応漏れが起きにくい
- ラベルアノテーションで直接指定
Formのラベルを使用するクラス側で指定する。
クラスパスで指定したラベルがあった場合は、直接指定側が優先される。
独自のラベルを設けたいときに使用する。
指定したラベルがプロパティのキーと一致すれば、キーと一致したプロパティの値を使用する。
(なければ指定した文字列が使用される)
これにより国際化対応(ラベルのルール化)ができる。
これに加えて、順序指定についても考えていきたいと思います。
フラット
AAはXXです。
BBはXXです。
CCはXXです。
DDはXXです。
順序はフィールドに対して指定すれば良く、ラベルについても画面内で重複する表現もないので、Formのラベルをそのまま使って特に問題無さそうです。
同じ組み合わせのコンポーネントが複数あるパターンです。
階層を使って実現
メッセージは、以下のような感じになることを想定します。
コンポーネント1 AAはXXです。
コンポーネント1 BBはXXです。
コンポーネント1 CCはXXです。
コンポーネント1 DDはXXです。
コンポーネント2 AAはXXです。
コンポーネント2 BBはXXです。
コンポーネント2 CCはXXです。
コンポーネント2 DDはXXです。
順序
class Page {
@Order(1)
コンポーネント1 component1;
@Order(2)
コンポーネント2 component2;
}
class コンポーネント1 {
@Order(1)
FormAA formAA;
@Order(2)
FormBB formBB;
@Order(3)
FormCC FormCC;
@Order(4)
FormDD FormDD;
}
というように、各フィールドの順番を指定しておいて階層を踏まえて順序を評価すれば出来そうです。
ラベル
コンポーネント型クラスにラベルを付与して、配下のクラスにラベルがある場合はプリフィックスとしてForm型クラスのラベルと組み合わせて表現します。
コンポーネント1と、2で、構造が同じでも、それぞれに相当する型クラスを準備する必要がありますが 型が同じでもインスタンスとしては別物なので、これは こうするしかないかな?という感じです。
message.properties としては
validate.XX.message = {label}はXXです。
コードとしては、上位階層にラベルがあったら 順番に連結をする という仕組みになるでしょうか。
たとえば、階層を考慮した表記について
コンポーネント1の AAはXXです。
と表現したい場合は、コンポーネント1のラベルに
コンポーネントの
というように助詞まで含めるか、上述のように スペースをライブラリ側で挟むようにするというのを標準仕様としておけば良いかな、という感じです。
メリットは、ラベルを Formやコンポーネントの単位で考えれば良いというところ。
あとは単純結合をして{label}
と置換をしてメッセージを完成させます。
ただ、これは日本語だったら良さそうに思えるのですが、英語など文法が異なると、、どうでしょうかねぇ。。
of
とか GestUser's Name
みたいに、単純な結合では ちょっと駄目な気がします。
あと、ラベルの仕様として上位階層のもので上書きする としているので、上書きではなく プリフィクスとして追記させる、というような変更も必要になるでしょう。
コンポーネントクラスでフィールド毎に独自のラベルを指定するパターン。
上述のパターンと同じことも 下のような項目名だけで表現することができます。
AAはXXです。
BBはXXです。
CCはXXです。
DDはXXです。
EEはXXです。
FFはXXです。
GGはXXです。
HHはXXです。
Formでデフォルトのラベルを設けていても、それを使えず いちいち 全てのフィールドに対してラベルを指定しないといけませんが 表現力としては、こちらのパターンの方が良さそうです。
ただし
コンポーネント1にエラーがあります。
というような、コンポーネント単位のラベルを用いることは出来ません。
コンポーネントクラス自身、または上位となるページクラスから ラベルを指定すると 配下のFormクラスのラベルが上書きされてしまうためです。
コンポーネントなどの「領域」に対するメッセージを付与したい場合は、そのために使用するフィールドを設けておいて、それに対して コンポーネント配下のFormでエラーをまとめる というような工夫が必要になりそうです。
順序
階層指定で問題なさそうです。
(記述例は省略)
ラベル
ラベルもコンポーネントで指定する、ということで問題無さそうです。
(記述例は省略)
順序
階層指定で問題なさそうです。
(記述例は省略)
ラベル
ラベルもコンポーネントで指定する、ということで問題無さそうです。
(記述例は省略)
テーブル
便宜上 セルの色を変えていますが、今回はセル色の制御については扱いません。
TitleAはキー項目という前提です。
TitleAは入力必須です。
TitleA(A3)のTitleC は 10桁以内にしてください。
TitleA(A5)のTitleB に重複した値が指定されています。重複しない値を指定してください。
TitleA(A6)のTitleB に重複した値が指定されています。重複しない値を指定してください。
という結果になることを想定しています。
キー項目の入力必須については、特定のために 行番号など 当該項目に関連する情報を付与したいケースも考えられます。
例えば
2行目のTitleAは入力必須です。
4行目のTitleAは入力必須です。
のように。
明細を1つ1つ表示していますが、そもそもテーブルのような大量な情報量に対して1つ1つエラーメッセージを一覧形式で表記すること自体が良くないかもしれません。
例えば
XX表にエラーがあります。
とか
TitleAは入力必須です。
TitleC は 10桁以内にしてください。
TitleB に重複した値が指定されています。重複しない値を指定してください。
のような 列単位、エラー分類単位で集約した表記をして、
個々のエラー情報はセルに対してツールチップで表現する というのが妥当かもしれません。
今回は順序だけに絞って整理をしたいので、ツールチップについては保留にしておくとしても、集約については もう少し考えておきたいところです。
順序
詳細を出力するときは階層指定で問題なさそうですが、集約表記における優先度順についてはどうでしょう?
たとえば
TitleAは入力必須です。
TitleC は 10桁以内にしてください。
TitleB に重複した値が指定されています。重複しない値を指定してください。
が、桁チェックの結果を最後にしたい というのが要件であった場合
(タイトル順ではない)
TitleAは入力必須です。
TitleB に重複した値が指定されています。重複しない値を指定してください。
TitleC は 10桁以内にしてください。
のような表記順序になる、という感じです。
正直な意見としては、同一優先度内であれば順不同を仕様としたい、ところです。
ただ、これだと出力順が一致しないというのが なんとなく嫌です。*1
集約単位の出力メッセージについては
- MessageTemplateの文字列ソートをデフォルトとする
- 表示順序を制御したメッセージのプロパティーキーと優先度をControllerで指定
- 未指定と指定が混在している場合は、指定しているものの優先度を高くする
という感じでしょうか。
では順序指定は どうやって指定するのが良いでしょう?
メッセージは実行時例外をInterceptorで捕捉して処理をするという前提なので、Annotationで宣言的に指定しておくと 情報として取得しやすいように思います。
ただし、Controller毎に発生するかもしれない例外を全て列挙しなくても良いような工夫は必要だと思います。
優先度とプロパティキーを列挙するだけであれば静的な情報なので、通常のAnnotationで扱えそうな気がしますが、継承的なことや 委譲的なことをして分類をしたものを使用したいとなった場合を考えると 動的な指定も考えておきたいところです。
*2
ラベル
グループ集約表記に関するラベルから考えてみたいと思います。
通常のラベルを そのまま使うと上書きされるというのが、現状の機能仕様です。
ということで、通常のラベルとは別に集約用のグループラベルを別途設けて対応をするのが良さそうに思います。
Formのアノテーション指定としては
class TableRow {
@GroupLabel("TitleA")
FormA formA;
(省略)
}
メッセージ置換用のプロパティは
FormA.NotBlank.message.group={groupLabel}は入力必須です
FormA.NotBlank.message={targetItem}の{groupLabel}は入力必須です
メッセージの指定は
@NotBlank(groups = FormValidation.class, message="FormA.NotBlank.message")
private final String value;
メッセージは グループを示さないキー値にしておいて、メッセージ変換をする際に @GroupLabel
があったら、「メッセージキー」+「.group」でpropertiesから 検索をしてヒットしたら それを使うという感じです。
ここまでは、なんとなくできそうな気がしますが、{targetItem}
という変数は どうすれば実現できるのでしょうか?
これは、キー項目(ここだとTitleA)における 行番号を含めた情報を メッセージにどうやって反映すれば良いのだろう? にも関連します。
実現性の可否は分かりませんが、とりあえず思いつく方策を考えてみたいと思います。
Annotationで動的情報取得
class Page{
FormA formA;
@TargetItem(this.getTargetItems())
FormB formB;
private String getTargetItemId(){
return this.formA.value();
}
こんな感じのイメージ。
実際は @TargetItem(this.getTargetItems())
のような動的な指定はできないです。
でも、できたら結構広がりのある仕組みになると思うんですけどねぇ。
ただ、こうなってくると宣言的とは、なんなのか意味が良く分からないことにもなっているような気もします。
isValidで編集
全ての標準Validationについて、キー項目を統一的に渡せるようにTable用のものを新たに作り #isValid
で編集をするという方法。
#isValid
のパラメータConstraintValidatorContext
を使うことで、messageの一部({targetItem})だけを事前に置換するというやり方です。
雰囲気先行で考えているので、実際に出来うる やり方なのか。。。
まとめ
- フィールドの順番は、階層的に指定する。
- ラベルはコンポーネントで指定する。
ということで テーブル表記および それに準ずる階層構造の場合は除いて、 概ねカバーできそうです。
テーブル表記のメッセージ出力の課題は
- メッセージ優先度の指定方法を どうやるのか?
- 表形式でキー項目の値をメッセージに出力できるようにするのか?
- グループラベルの置換をどうするのか?
という感じでしょうか。
結局、何も解決に向かっていない気がします。
参考資料
SpringのBean Validationでエラーメッセージを動的に切り替える。
Java8でAnnotationを動的に差し替えたい時
アノテーションを動的に上書きする - 丸いタイヤを四角く作る日記
私のBeanValidationの使い方(Java EE Advent Calendar 2013) — 裏紙
さいごに
次は、今回分かった課題を1つ1つ解消すべく実装を考えるか
各項目とFormクラスのフィールドとの関連付けの やり方を考えるか、どっちにしようかな。
しかし、どんどん沼にはまっている気がする。。
BeanValidationを使うということ自体が制約(目的)となって複雑度が上がっている気がしています。
なんか、もっとシンプルなことなのかもなぁと思ったり、変に難しく考えてしまっているような気もしています。
とりあえず、まとまらないなら まとまらないなりに吐き出した という感じです。