そもそも「いや、それJSFだと亜流ですので」って終わりそうな話だと思いますが、誰かの何かに役立つかもしれないので。
環境
Payara5
Java EE7
Java8
事象
コンテナ(CDI)で生成したインスタンスのフィールドってnullなんだ。。
— Yamashita (@_vermeer_) 2018年7月10日
アクセッサ経由じゃないと値が取得できないと。。
なので、フィールドにValidation用のAnnotationをつけても値はnullで評価される、と。。
となると、私の構想は瓦解する、と。。
Controller(もしくは ManagedBeanのAction相当)
@Named @RequestScoped public class UserRegistrationAction { private UserRegistrationPage registrationForm; private UserService userService; private ViewMessage viewMessage; public UserRegistrationAction() { } @Inject public UserRegistrationAction(UserRegistrationPage registrationForm, UserService userService, ViewMessage viewMessage) { this.registrationForm = registrationForm; this.userService = userService; this.viewMessage = viewMessage; } public String fwPersist() { this.registrationForm.init(); return "persistedit.xhtml?faces-redirect=true"; } public String confirm() { Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); //validateしても、対象のクラスフィールドに @Validと記述しても無視される //理由は、コンテナで生成したインスタンスのフィールドが nullだから。 //ただし、アクセッサを経由したら値は取得できることは デバッグモードで確認済み Set<ConstraintViolation<Object>> results = validator.validate(registrationForm, ValidationPriority.class); this.viewMessage.appendMessage(results); if (results.isEmpty() == false) { return "persistedit.xhtml?faces-redirect=true"; } return "persistconfirm.xhtml?faces-redirect=true"; } public String register() { User requestUser = this.registrationForm.toUser(); this.userService.register(requestUser); Optional<User> responseUser = this.userService.findByKey(requestUser); //とりあえず、登録要素が無いという有り得ない状況だったら 自画面にそのまま遷移させる(ありえないケース) if (responseUser.isPresent() == false) { return null; } this.registrationForm.update(responseUser.get()); return "persistcomplete.xhtml?faces-redirect=true"; } }
Form(もしくはManagedBeanのActionFrom相当)
@Named @SessionScoped public class UserRegistrationPage implements Serializable { private static final long serialVersionUID = 1L; private UserId userId; private EmailForm userEmail; @Valid private NameForm name; @Valid private DateOfBirthForm dateOfBirth; @Valid private PhoneNumberForm phoneNumber; private GenderForm gender; public UserRegistrationPage() { } (以下略)
public class NameForm implements DefaultForm<UserName>, Serializable { private static final long serialVersionUID = 1L; @NotBlank(groups = ValidationGroups.Form.class) private String value = ""; public NameForm() { } public NameForm(String userName) { this.value = userName; } /** * @inheritDoc */ @Override public String display() { return this.getValue().getValue(); } /** * @inheritDoc */ @Override public UserName getValue() { return new UserName(this.value); } }
フィールドに@Valid
でアノテーションしていても、対象クラス配下の@NotBlank
が 有効にならず、次の画面に遷移してしまいます。
コード全量
Bitbucket
対応後
同一フォームでもActionによって検証対象フィールドが違う場合の条件分けをクラス単位で管理できるので、見通しが良さそう。
— Yamashita (@_vermeer_) 2018年7月10日
個人的にはgroupで場合分けをしない、よさげな方法を考えていたので、むしろ Action毎に検証対象とするフィールドを管理するクラスを準備しておく設計思想は
悪くないかも。
Controller(もしくは ManagedBeanのAction相当)
@Named @RequestScoped public class UserRegistrationAction { private UserRegistrationPage registrationForm; private UserService userService; private ViewMessage viewMessage; public UserRegistrationAction() { } @Inject public UserRegistrationAction(UserRegistrationPage registrationForm, UserService userService, ViewMessage viewMessage) { this.registrationForm = registrationForm; this.userService = userService; this.viewMessage = viewMessage; } public String fwPersist() { this.registrationForm.init(); return "persistedit.xhtml?faces-redirect=true"; } public String confirm() { Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); //CDI管理外のインスタンスを引数として検証を行えば @Valid が有効(普通のBeanValidationの挙動) Set<ConstraintViolation<Object>> results = validator.validate(registrationForm.getValidationPersistUser(), ValidationPriority.class); this.viewMessage.appendMessage(results); if (results.isEmpty() == false) { return "persistedit.xhtml?faces-redirect=true"; } return "persistconfirm.xhtml?faces-redirect=true"; } public String register() { User requestUser = this.registrationForm.toUser(); this.userService.register(requestUser); Optional<User> responseUser = this.userService.findByKey(requestUser); //とりあえず、登録要素が無いという有り得ない状況だったら 自画面にそのまま遷移させる(ありえないケース) if (responseUser.isPresent() == false) { return null; } this.registrationForm.update(responseUser.get()); return "persistcomplete.xhtml?faces-redirect=true"; } }
Form(もしくはManagedBeanのActionFrom相当)
@Named @SessionScoped public class UserRegistrationPage implements Serializable { //@Valid を外す(意味がないので) private static final long serialVersionUID = 1L; private UserId userId; private EmailForm userEmail; private NameForm name; private DateOfBirthForm dateOfBirth; private PhoneNumberForm phoneNumber; private GenderForm gender; public UserRegistrationPage() { } //(省略) public Object getValidationPersistUser() { ValidationPersistUser obj = new ValidationPersistUser(); //private インナークラスなので、外部から更新されることは気にする必要は無いと考え //コンストラクタ実装など手間はかけずにフィールドに直接設定 obj.userEmail = userEmail; obj.name = name; obj.dateOfBirth = dateOfBirth; obj.phoneNumber = phoneNumber; return obj; } // 追加Actionで必須検証が必要なフィールドだけを抜粋して検証を行う. // 本メソッドを見れば、検証対象が分かるので groups による指定よりも見通しは良い(と思う) private static class ValidationPersistUser { @Valid private EmailForm userEmail; @Valid private NameForm name; @Valid private DateOfBirthForm dateOfBirth; @Valid private PhoneNumberForm phoneNumber; } (省略)
コード全量
Bitbucket
動機
Form:ConversationScope、Action:RequestScope を前提とした構想なので
— Yamashita (@_vermeer_) 2018年7月10日
ActionをFormにあわせるとServiceのRequestScopeの生存期間と相性が悪くなる。。
というか、Scopeは自分なりに考えたインスタンスの生存期間の こうありたい なので正直変えたくない
追記メモ
javax.inject.Provider<UserRegistrationPage>をインジェクションして、そいつをgetして得たCDI管理ビーンなら問題なくvalidateできるかもしれません(未検証)。
— うらがみ⛄ (@backpaper0) 2018年7月10日
もしくはUserRegistrationPageにメソッドを追加して、その中でvalidate(this)しても良いんじゃないかなぁ、と思います。
改めて コメント、本当に ありがとうございます。
javax.inject.Provider
は初見だったので、関連しそうなところの勉強用のリンク
toydi/README.md at master · tokuhirom/toydi · GitHub
[dependency-injection] CDI注入ループ [circular-dependency] | CODE Q&A 問題解決 [日本語]
さいごに
でも挙げた通り、これは私の実験場みたいなものなので、これがJSFの通常実装のパターンとは思わないでいただければと思います。