Java
バグ
バリデーション
hibernate-validator

Hibernate Validator の @Email を使ってはいけない?

有名な話だったらゴメンよ!

Hibernate Validator 先生はめっちゃ便利なわけですが、ある日こんなことが。

「おい! hoge@+gmail.com とか入力されてメール送信系が例外吐いてんぞ! 誰だバリデーションしてねえ馬鹿は!?」

まずそんな馬鹿な作りにしたやつは誰だ。
この時、Hibernate Validator のバージョンは 5.2.2 Final でした。なんで最新じゃないのかって? 知るもんかい。

なお何ら問題なくバリデーションされていた模様

どこを見ても @Validated は付いています。あれ? と思って hoge@!@#$%^&()_+ とかやると……引っかかる。動いてるよなー。
ってことはこいつドメイン部の+通しちまうんじゃねえか!!

なお最新版

5.2.2 で存在した EmailValidator は 最新版 (2018/01/17) では @Deprecated 。では現行はどうなんでしょう?
https://github.com/hibernate/hibernate-validator/blob/7c0afb18cc83aab5e73827a6659af23970dd26f5/engine/src/main/java/org/hibernate/validator/internal/constraintvalidators/bv/EmailValidator.java
ありゃ、見当たらない。Abstract の方でしょうか。

https://github.com/hibernate/hibernate-validator/blob/35ba4c157e81ce6a54e8717418d2718d384ed144/engine/src/main/java/org/hibernate/validator/internal/constraintvalidators/AbstractEmailValidator.java

DomainNameUtil.isValidEmailDomainAddress( domainPart );

ほうほう、DomainNameUtil 。

https://github.com/hibernate/hibernate-validator/blob/911efbad82d0c81e2ae4aa8b5b412865ed6bd127/engine/src/main/java/org/hibernate/validator/internal/util/DomainNameUtil.java

private static final String DOMAIN_CHARS_WITHOUT_DASH = "[a-z\u0080-\uFFFF0-9!#$%&'*+/=?^_`{|}~]";
private static final String DOMAIN_LABEL = "(" + DOMAIN_CHARS_WITHOUT_DASH + "-*)*" + DOMAIN_CHARS_WITHOUT_DASH + "+";
private static final String DOMAIN = DOMAIN_LABEL + "+(\\." + DOMAIN_LABEL + "+)*";

デデドン!!(絶望)
……これは参ったなあ。ドメインは RFC1035 ベースで見たほうがいいんじゃないの?って Issue を書くべきかしら。

じゃあ何使えばいいのよ?

Apache commons-validator が割といいらしいっすよ1
……もっとも、日本語アドレス (Gmailとかは対応してるらしい) みたいなキワモノまで使えるかは試してませんが。

commons-validator ってアノテーションでついてたっけ?

ついてません。さくっとこんな感じで書きませう。

@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE,  ElementType.CONSTRUCTOR, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {CommonsEmailValidator.class})
@Documented
@ReportAsSingleViolation
public @interface Email {

    String message() default "{org.hibernate.validator.constraints.Email.message}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    @Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface List {
        Email[] value();
    }
}

public class CommonsEmailValidator implements ConstraintValidator<Email, String> {

    @Override
    public void initialize(Email email) {
        // nothing to do
    }

    @Override
    public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
        if( s == null || s.isEmpty() ) return true;
        return EmailValidator.getInstance(false).isValid(s);
    }
}