上記ではDomainやFormでの検証結果については、Pageクラスで指定した順序でメッセージ出力する事が出来ました。
ですが、Application層以降の検証不正については、レイヤーを跨った関連付けをする仕組みを持たないと その順序性を管理する事ができません。ということで色々と考察をしたのが以下です。
そして今回は考察を踏まえて実装した機能について説明します。
実装例であれば「こういう実装をして実行したら、結果として こんな感じになります」ということになると思いますが、今回は 仕組みを中心に説明をしたいと思います。
つまりApplication層から、どうやってメッセージの順序を編集しているのか という感じで 遡るイメージです。
Code(関連するもの)
以下の説明でポイントになるコード。 (全コードは最後にリンクしています)
Service
package exsample.jsf.application.service; import spec.interfaces.application.commnand.Command; import spec.validation.PostConditionValidationGroups.PostCondition; import spec.validation.PreConditionValidationGroups.PreCondition; import spec.validation.Validator; import spec.annotation.application.Service; import exsample.jsf.domain.model.user.User; import exsample.jsf.domain.model.user.UserRepository; import javax.inject.Inject; import javax.validation.constraints.AssertTrue; @Service public class RegisterUser implements Command<User> { private User user; private UserRepository userRepository; private Validator validator; public static class Error { public static final String SAME_EMAIL_USER_ALREADY_EXIST = "{same.email.user.already.exist}"; public static final String USER_CANNOT_REGISTER = "{user.cannot.register}"; } public RegisterUser() { } @Inject public RegisterUser(UserRepository userRepository, Validator validator) { this.userRepository = userRepository; this.validator = validator; } @AssertTrue(message = Error.SAME_EMAIL_USER_ALREADY_EXIST, groups = PreCondition.class) private boolean isNotExistSameEmail() { return userRepository.isNotExistSameEmail(user); } @Override public void validatePreCondition(User entity) { this.user = entity; validator.validatePreCondition(this); } public void with(User user) { validatePreCondition(user); userRepository.register(user); validatePostCondition(userRepository.persistedUser(user)); } @AssertTrue(message = Error.USER_CANNOT_REGISTER, groups = PostCondition.class) private boolean isExistEntity() { return userRepository.isExistEntity(user); } @AssertTrue(message = Error.SAME_EMAIL_USER_ALREADY_EXIST, groups = PostCondition.class) private boolean isNotExistSameEmailAtOtherEntity() { return userRepository.isNotExistSameEmailAtOtherEntity(user); } @Override public void validatePostCondition(User entity) { this.user = entity; validator.validatePostCondition(this); } }
ポイント
public static class Error { public static final String SAME_EMAIL_USER_ALREADY_EXIST = "{same.email.user.already.exist}"; public static final String USER_CANNOT_REGISTER = "{user.cannot.register}"; }
ServiceとPageで使用するメッセージ情報を関連付けるために 定数のインナークラスを作成しました。
Controller
import spec.annotation.presentation.controller.Controller; import spec.annotation.presentation.controller.EndConversation; import spec.annotation.presentation.controller.ViewContext; import exsample.jsf.application.service.RegisterUser; import exsample.jsf.application.service.UserService; import exsample.jsf.domain.model.user.User; import javax.inject.Inject; @Controller public class UserRegistrationAction { @ViewContext private UserRegistrationPage registrationPage; private UserService userService; private RegisterUser registerUser; public UserRegistrationAction() { } @Inject public UserRegistrationAction(UserRegistrationPage registrationForm, UserService userService, RegisterUser registerUser) { this.registrationPage = registrationForm; this.userService = userService; this.registerUser = registerUser; } public String fwPersist() { this.registrationPage.init(); return "persist-edit.xhtml"; } public String confirm() { User requestUser = this.registrationPage.toUser(); registerUser.validatePreCondition(requestUser); return "persist-confirm.xhtml"; } public String modify() { return "persist-edit.xhtml"; } public String register() { User requestUser = this.registrationPage.toUser(); registerUser.with(requestUser); User responseUser = userService.persistedUser(requestUser); this.registrationPage.update(responseUser); return "persist-complete.xhtml"; } @EndConversation public String fwTop() { return "index.xhtml"; } }
ポイント
Serviceから発行された検証不正とPageのFieldを関連付ける対象は @ViewContext
で宣言したものを対象とします。
Page(Form)
import spec.annotation.FieldOrder; import spec.validation.Validator; import spec.annotation.presentation.view.InvalidMessageMapping; import spec.annotation.presentation.view.View; import exsample.jsf.application.service.RegisterUser; import exsample.jsf.domain.model.user.GenderType; import exsample.jsf.domain.model.user.User; import exsample.jsf.domain.model.user.UserId; import exsample.jsf.presentation.userregistration.form.DateOfBirthForm; import exsample.jsf.presentation.userregistration.form.EmailForm; import exsample.jsf.presentation.userregistration.form.GenderForm; import exsample.jsf.presentation.userregistration.form.NameForm; import exsample.jsf.presentation.userregistration.form.PhoneNumberForm; import java.io.Serializable; import java.util.HashMap; import java.util.Map; import javax.inject.Inject; import javax.validation.Valid; @View public class UserRegistrationPage implements Serializable { private static final long serialVersionUID = 1L; private Validator validator; private UserId userId; @Valid @FieldOrder(1) @InvalidMessageMapping(RegisterUser.Error.SAME_EMAIL_USER_ALREADY_EXIST) private EmailForm userEmail; @Valid @FieldOrder(2) private NameForm name; @Valid @FieldOrder(3) private DateOfBirthForm dateOfBirth; @Valid @FieldOrder(4) private PhoneNumberForm phoneNumber; @Valid @FieldOrder(5) private GenderForm gender; (メソッドは省略) }
ポイント
@FieldOrder(1) @InvalidMessageMapping(RegisterUser.Error.SAME_EMAIL_USER_ALREADY_EXIST) private EmailForm userEmail;
Serviceで発行した検証不正のメッセージと、Pageのフィールドを関連付け。
Interseptor
import ee.jsf.messages.JsfMessageConverter; import ee.validation.ConstraintViolationsHandler; import ee.validation.ViewContextScanner; import java.util.List; import javax.annotation.Priority; import javax.enterprise.context.Dependent; import javax.inject.Inject; import javax.interceptor.AroundInvoke; import javax.interceptor.Interceptor; import javax.interceptor.InvocationContext; import spec.annotation.presentation.controller.Action; import spec.interfaces.infrastructure.CurrentViewContext; import spec.interfaces.infrastructure.MessageConverter; import spec.interfaces.infrastructure.MessageHandler; import spec.validation.BeanValidationException; @Action @Interceptor @Priority(Interceptor.Priority.APPLICATION) @Dependent public class BeanValidationExceptionInterceptor { private final CurrentViewContext context; private final MessageConverter messageConverter; private final MessageHandler messageHandler; @Inject public BeanValidationExceptionInterceptor(CurrentViewContext context, JsfMessageConverter messageConverter, MessageHandler messageHandler) { this.context = context; this.messageConverter = messageConverter; this.messageHandler = messageHandler; } @AroundInvoke public Object invoke(InvocationContext ic) throws Exception { String currentViewId = context.currentViewId(); try { return ic.proceed(); } catch (BeanValidationException ex) { ConstraintViolationsHandler handler = new ConstraintViolationsHandler.Builder() .messageSortkeyMap(ViewContextScanner.of(ic.getTarget().getClass().getSuperclass()).scan()) .constraintViolationSet(ex.getValidatedResults()) .build(); List<String> messages = messageConverter.toMessages(handler.sortedConstraintViolations()); messageHandler.appendMessages(messages); return currentViewId; } } }
と、そのInterceptorで使っている主たる機能
public class ConstraintViolationsHandler { private final List<SortKeyConstraintViolation> sortKeyConstraintViolations; private ConstraintViolationsHandler(List<SortKeyConstraintViolation> sortKeyConstraintViolations) { this.sortKeyConstraintViolations = sortKeyConstraintViolations; } public List<ConstraintViolation<?>> sortedConstraintViolations() { return sortKeyConstraintViolations.stream() .sorted(comparing(SortKeyConstraintViolation::getSortkey) .thenComparing(s -> s.getConstraintViolation().getMessageTemplate())) .map(SortKeyConstraintViolation::getConstraintViolation) .collect(Collectors.toList()); } public static class Builder { private final MessageTmplateSortKeyMap messageTmplateSortKeyMap; private Set<ConstraintViolation<?>> constraintViolationSet; public Builder() { messageTmplateSortKeyMap = new MessageTmplateSortKeyMap(); constraintViolationSet = new HashSet<>(); } public Builder messageSortkeyMap(MessageTmplateSortKeyMap messageTmplateSortKeyMap) { this.messageTmplateSortKeyMap.putAll(messageTmplateSortKeyMap); return this; } public Builder constraintViolationSet(Set<ConstraintViolation<?>> constraintViolationSet) { this.constraintViolationSet = constraintViolationSet; return this; } public ConstraintViolationsHandler build() { return new ConstraintViolationsHandler( messageTmplateSortKeyMap.replaceSortKey( SortkeyConstraintViolationConverter.of(constraintViolationSet).toList() )); } } }
ポイント
前提として@Controller
は@Stereotype
で @Action
を組み込んでいます。
BeanValidationExceptionInterceptor
コードに説明用コメントを追記しました。
try { return ic.proceed(); } catch (BeanValidationException ex) { ConstraintViolationsHandler handler = new ConstraintViolationsHandler.Builder() // ViewContextScanner で @ViewContext で指定したクラスから // @InvalidMessageMapping から Serviceのメッセージとフィールドを関連付けて // @FiledOrderの順序を取得して ソートに必要な情報を編集 .messageSortkeyMap(ViewContextScanner.of(ic.getTarget().getClass().getSuperclass()).scan()) .constraintViolationSet(ex.getValidatedResults()) .build(); // 検証不正をソート順で並べてメッセージ出力する List<String> messages = messageConverter.toMessages(handler.sortedConstraintViolations()); messageHandler.appendMessages(messages); return currentViewId; }
ざっくり
上述のポイントを何となく見てもらえれば雰囲気は伝わるかと思いますが、ざっくりと流れを説明すると
- Serviceから検証不正を発行
- Presentation層のInterceptorで捕捉
- Interceptorでは Controllerで使用するPageクラスを@ViewContextで特定
- @ViewContextで特定したPageクラスの@InvalidMessageMappingとServiceのメッセージで検証不正の結果を関連付け
- 関連付けしたフィールドの@FieldOrderで順序を取得
- 「メッセージと順序」をペアを作成
- 検証不正情報から「メッセージと検証不正結果クラス」のペアを作成
- 「メッセージと順序」と「メッセージと検証不正結果クラス」で「順序と検証不正結果クラス」を作成
- 「順序と検証不正結果クラス」をソートして、検証不正結果クラスの情報から メッセージを取得して出力
という感じです。
最後の方の「順序と検証不正結果クラス」で まとめておいて、最終的にメッセージを出力するだけなところに冗長さを感じるかもしれませんが、この辺りは Domainの検証不正の仕組みとあわせて処理するようにしているための施策です。
Code
さいごに
今回は順序情報について取得・編集をしました。
この方法で、フィールド情報との関連付けができそうなので 色々と応用が出来る気がします。