から、多少の肉付けをした版。
やったこと
Validation
ValidationExceptionの制御
メッセージ(出力まで)
ConversationScope制御
Redirectの強制
基本は過去の記事などで取り扱った要素を組み込んだ感じです。
DDD的なところ?
あんまりありませんね(笑)
どっちかというと、JSFとCDIによる肉付けをしました。という感じです。
パッケージ構成
まだまだ試行錯誤中
やろうとしていることは
設計思想に関する関心事
汎用実装に関する関心事
プロダクトに関する関心事
を それぞれコンテキストとして分けて、最終的には それぞれを別のプロジェクトにする、というイメージです。
設計思想と汎用実装を オレオレフレームワークにして、今後のプロダクトは 使っていくという感じにしたいなぁと。
あえていえば、今回のDDD的なところ、になるのはパッケージ構成くらいでしょうか。
Validation
概ね、こんな感じで良いだろう、というところまで出来た気がします。
public class DateOfBirthForm implements DefaultForm<DateOfBirth>, Serializable { private static final long serialVersionUID = 1L; @NotBlank(groups = ValidationGroups.Form.class) private String value = ""; public DateOfBirthForm() { } public DateOfBirthForm(String dateOfBirth) { this.value = dateOfBirth; } /** * @inheritDoc */ @Override public String display() { return this.getValue().getValue(); } /** * @inheritDoc */ @Valid @Override public DateOfBirth getValue() { return new DateOfBirth(this.value); } }
Formの主な関心事であるValueObjectのアクセッサ(getXXX)に@Valid
を付与して、検証対象としておく。
public class DateOfBirth { @DateTimeFormat private final String value; public DateOfBirth(String value) { this.value = value; } public Age currentAge() { return new Age(this.date()); } public String getValue() { return this.value; } public LocalDate date() { return LocalDate.parse(value, DateTimeFormatter.ofPattern("yyyy-MM-dd")); } }
ValueObjectのプロパティに対して、不変条件である@DateTimeFormat
(自作)を注釈。
今回はしませんでしたが「過去の日付であること」とか「年齢が20歳以下」とか 要件によって条件を追加するイメージ。
もし「過去の日付であること」とする場合、標準アノテーションで検証をしようと思ったら プロパティの型がLocalDate
じゃないといけないので、そこは悩ましいところ。
現状のアイディアとしては、戻り値の型がLocalDate
検証用アクセッサを準備しておいて そこに検証アノテーションを付与するか、もともと「現在日付」については テストの容易性を考えて アプリケーション側で制御できるように 別途 インスタンスを設けるつもりもあったりするので、検証対象のクラスの型をString
とした自作検証アノテーションを設けるかもしれないです。
そのあたりは、実際に そういうことを実装するときに肉付けすれば良いかな。
@Documented @Constraint(validatedBy = {ddd.domain.validation.annotation.DateTimeFormat.Validator.class}) @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER}) @Retention(RUNTIME) public @interface DateTimeFormat { String message() default "{ddd.domain.validation.annotation.DateTimeFormat.message}"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER}) @Retention(RUNTIME) @Documented public @interface List { DateTimeFormat[] value(); } public class Validator implements ConstraintValidator<DateTimeFormat, String> { @Override public void initialize(DateTimeFormat value) { } @Override public boolean isValid(String value, ConstraintValidatorContext context) { try { LocalDate.parse(value, DateTimeFormatter.ofPattern("uuuu-MM-dd").withResolverStyle(ResolverStyle.STRICT)); } catch (DateTimeParseException ex) { return false; } return true; } } }
自作の検証アノテーションについては、アノテーションと検証実装をペアとして1つのクラスで管理することにしました。
アノテーションはインターフェースみたいなものとして、別に実装をして 検証クラス(Validator)を独自に拡張できるように してみては?とも考えましたが、検証アノテーションでの検証クラスの指定の仕方が クラスパス直接指定であったため 分けても意味がないと考えて 1つにしました。
もし、検証クラスで検証ロジッククラスをDI出来るのであれば そういう方式で 拡張性が良さそうな仕組みになりそう、と思ったのですが。。
Hibernateなどインターフェースと実装を分けているので、出来るは出来ると思うのですが 私の力不足で ここは断念しました。
@Controller public class UserRegistrationAction { private UserRegistrationPage registrationPage; private UserService userService; private Validator validator; public UserRegistrationAction() { } @Inject public UserRegistrationAction(UserRegistrationPage registrationForm, UserService userService, Validator validator) { this.registrationPage = registrationForm; this.userService = userService; this.validator = validator; } public String fwPersist() { this.registrationPage.init(); return "persistedit.xhtml"; } public String confirm() { validator.validate(registrationPage.getValidationForm()); return "persistconfirm.xhtml"; } public String modify() { return "persistedit.xhtml"; } public String register() { User requestUser = this.registrationPage.toUser(); this.userService.register(requestUser); Optional<User> responseUser = this.userService.findByKey(requestUser); //とりあえず、登録要素が無いという有り得ない状況だったら 自画面にそのまま遷移させる(ありえないケース) if (responseUser.isPresent() == false) { return null; } this.registrationPage.update(responseUser.get()); return "persistcomplete.xhtml"; } @EndConversation public String fwTop() { return "index.xhtml"; } }
Contorllerは こんな感じ
検証実行は
validator.validate(registrationPage.getValidationForm());
で やっていて、その対象情報は Formクラスから生成している、という感じです。
詳細は以下。
アドバイスもいただいたのですが、力不足でjavax.inject.Provider
を使った方法では 出来ませんでした*1。
ValidationExceptionの制御
上述の検証ロジックの
validator.validate();
の実装は
@Named @Dependent public class BeanValidatorProducer { @Produces public BeanValidator getBeanValidator() { return new BeanValidator(); } }
public class BeanValidator implements Validator { /** * {@inheritDoc } */ @Override public void validate(Object validateTarget) { javax.validation.Validator validator = new GroupSequenceValidator(ValidationPriority.class); Set<ConstraintViolation<Object>> results = validator.validate(validateTarget); if (results.isEmpty() == false) { throw new BeanValidationException(results); } } }
GroupSequenceValidator
は自作の拡張Validatior。
詳細は こちら
vermeerlab / beanvalidation / source / — Bitbucket
ですが、やっていることは
通常のValidatorは、validateする際に 摘要する validation group を指定しますが、 本クラスはインスタンスを生成する際に 適用する GroupSequenceを保持させておくことで validateの実装簡素化を図れます.
です(JavaDoc引用)。
で、検証例外が発生した時の挙動ですが
ExceptionHandler
Interceptor
の2段構えとしました。
通常で考えれば、Interceptorだけで良いと思います。
本当は、アノテーションを強要しないExceptionHandlerだけで解決したかったのですが、標準出力にエラーログが出力されてしまうので断念。
ログだけだったら、まぁ良いのですが(良くはないけれど)、ConversationIdが無くなったりとか まぁ、結局のところFacesContextsの参照とかしたいし、@Controller
の実装漏れを検知できて、ユーザーには「ちょっと変」くらいの印象を与えるだけで収まるセイフティーネットとしてExceptionHandlerを残して、Interceptorを基本とする方式にしました。
メッセージ
@Action @Interceptor @Priority(Interceptor.Priority.APPLICATION) @Dependent public class BeanValidationExceptionInterceptor { @Inject UrlContext urlContext; @Inject MessageHandler messageHandler; @AroundInvoke public Object invoke(InvocationContext ic) throws Exception { String currentViewId = urlContext.currentViewId(); try { return ic.proceed(); } catch (BeanValidationException ex) { Set<ConstraintViolation<Object>> results = ex.getValidatedResults(); messageHandler.appendMessage(results); return currentViewId; } } }
InterceptorでBeanValidationException からメッセージ出力をしています。
で、実際のメッセージ出力クラスは以下。
public class JsfMessageHandler implements MessageHandler { /** * {@inheritDoc } */ @Override public void appendMessage(Set<ConstraintViolation<Object>> validatedResults) { FacesContext facesContext = FacesContext.getCurrentInstance(); MessageInterpolatorFactory interpolatorFactory = MessageInterpolatorFactory.of("Messages", "FormMessages", "FormLabels"); MessageInterpolator interpolator = interpolatorFactory.create(); validatedResults.stream() .map(result -> { return interpolator.toMessage(result); }) .distinct() .forEach(message -> { FacesMessage facemsg = new FacesMessage(FacesMessage.SEVERITY_ERROR, message, null); facesContext.addMessage(null, facemsg); }); // リダイレクトしてもFacesMessageが消えないように設定 facesContext.getExternalContext().getFlash().setKeepMessages(true); } }
メッセージ出力自体の制御は上述と同じく、自作のメッセージ編集ライブラリを使っています。
版数は少し古いですが、やっていることは
の通りです。
これを使うことで、標準検証アノテーションのメッセージに対して、「Formのラベル(表示用表現)+ 検証結果」を組み合わせた内容編集を 程良き感じで実現しています。
日本語だけだったら 検証アノテーションのmessage
で直接指定しても良いんでしょうけど、オレオレとはいえフレームワーク的なものを考えているので 国際化対応は初めから考慮したいところ、ということで組み込みました。
ConversationScope制御
を 余計な標準出力とかを除外して ほぼ そのまま移植。
Redirectの強制
これも余計な標準出力とかを除外して ほぼ そのまま移植。
Code
vermeer_etc / jsf-ddd / source / — Bitbucket
E2E test
vermeer_etc / jsf-ddd-e2etest / source / — Bitbucket
さいごに
これまで部分的に試行錯誤していたものを組み込んで1つの形にしたという感じです。
まぁまぁ形になった気がします。
まだまだ、やりたいことは山積みな訳ですが、Validationとその結果出力まで、というところで 一段落はついたので、そこまでを まとめてみました。
次は、絶対に避けれないセキュリティって思ったりするけど、先に出力メッセージに関する制御を やろうかな、とか正直 気分次第です(笑)。