2012年4月5日木曜日

Bean Validation


Spring Frameworkリファレンスの“6.7 Spring 3 Validation”に、Spring 3 は Bean Validation API(JSR-303)を完全サポートし、デフォルトのリファレンス実装として Hibernate Validator を採用している と記されています。

今回は、この JSR-303 Validator を使って「Validator と MessageSource」で作成した login フォームのバリデーションを作り変えます。また、簡単な機能ですが、独自の制約条件(custom constraint)を定義した、カスタムメイドのアノテーションも作ってみます。

JSR と Bean Validation
JSR(Java Specification Requests) とは、Java 標準として JCP(Java Community Process)にポストされたリクエストで、JCP メンバーのレビューを経て認可されたリクエストは仕様化ステージに入ります。JSR 303: Bean Validation もそうしたリクエストの一つで、JCPサイトから仕様の最終リリースをダウンロードできます。

事前準備
Hibernate Validator サイトからリンクを辿ると SourceForge.net の /hibernate-validator フォルダーに飛べるので、そこから必要な Distribution bundle をダウンロードします。私は以下のバージョンをダウンロードしました。

hibernate-validator-4.2.0.Final-dist.zip

JAR の登録
今回の作業で必要なのは次の 4 つです。

解凍先フォルダー
hibernate-validator-4.2.0.Final.jar
hibernate-validator-annotation-processor-4.2.0.Final.jar

解凍先フォルダー/lib/required
slf4j-api-1.6.1.jar
validation-api-1.0.0.GA.jar

これらを図のように /WEB-INF/lib にコピーし、それをビルドパスに追加します。因みに、私は Eclipse(Helios SR2) + JDK6 + Java EE6 SDK + Tomcat 7.0.25 という環境です。


<mvc:annotation-driven/>
リファレンスの 6.7.4.3 Configuring a JSR-303 Validator for use by Spring MVC に従って [servlet-name]-servlet.xml に <mvc:annotation-driven/>を追加します。

[servlet-name]-servlet.xml(抜粋)
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xmlns:p="http://www.springframework.org/schema/p"
     xmlns:context="http://www.springframework.org/schema/context"
     xmlns:aop="http://www.springframework.org/schema/aop"
     xmlns:tx="http://www.springframework.org/schema/tx"
     xmlns:mvc="http://www.springframework.org/schema/mvc"
     xsi:schemaLocation="
       http://www.springframework.org/schema/beans 
       http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
       http://www.springframework.org/schema/context 
       http://www.springframework.org/schema/context/spring-context-3.0.xsd
       http://www.springframework.org/schema/aop 
       http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
       http://www.springframework.org/schema/tx 
       http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
       http://www.springframework.org/schema/mvc 
       http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd">
  
  <mvc:annotation-driven/>
  
  <context:component-scan base-package="wrider"/>
    :
    
</beans>

制約条件をアノテート
以上で準備は完了です。では早速、login フォームのデータを格納する IdCard クラスの各プロパティにビルトインのアノテーションで制約条件を定義してみます。

IdCard.java
まず「メールアドレス(email)」には@NotEmpty と @Email アノテーションで“必須”、“E-Mailのフォーマット”という制約条件を付けます。一方「パスワード(password)」については、@NotEmpty と @Pattern を使って“必須”、“8 文字以上 32 文字以下の半角英数”という制約条件を付加します。
package wrider.model;

import javax.validation.constraints.Pattern;

import org.hibernate.validator.constraints.Email;
import org.hibernate.validator.constraints.NotEmpty;

public class IdCard {
  
  @NotEmpty
  @Email
  private String email;
  
  public String getEmail() {
    return this.email;
  }
  
  public void setEmail(String email) {
    this.email = email;
  }
  
  @NotEmpty
  @Pattern(regexp = "^[a-zA-Z0-9]{8,32}$")
  private String password;
  
  public String getPassword() {
    return this.password;
  }
  
  public void setPassword(String password) {
    this.password = password;
  }
}

コントローラークラスの修正
独自に作成した IdCard 用のバリデーター(IdCardValidator)の代わりにアノテーションを使用することに伴う変更を施します。

AccountController.java(抜粋)
@RequestMapping で POST リクエストに紐付けられた login メソッドに渡す IdCard 型引数に @Valid アノテーションを付け、idCardValidator を呼び出す部分をコメントアウトしています。
package wrider.controller;

import java.util.Map;

import javax.validation.Valid;

import org.springframework.beans.factory.annotation.Autowired;
  :
import wrider.model.IdCard;
import wrider.model.UserProfile;

@Controller
@SessionAttributes({"idCard", "userProfile"})
public class AccountController {
  /*
  @Autowired
  private Validator idCardValidator;
  */
  @Autowired
  private Validator userProfileValidator;
  
  @ModelAttribute("idCard")
  public IdCard makeIdCard() {
    return new IdCard();
  }
  
  @ModelAttribute("userProfile")
  public UserProfile makeUserProfile() {
    return new UserProfile();
  }
  
  @RequestMapping(value="/account/login.html", method=RequestMethod.GET)
  public String login() {
    return "account/login";
  }
  
  @RequestMapping(value="/account/login.html", method=RequestMethod.POST)
  public ModelAndView login(@Valid IdCard idCard, BindingResult br) {
    
//    this.idCardValidator.validate(idCard, br);
    
    ModelAndView mav = new ModelAndView();
    mav.getModel().putAll(br.getModel());
    mav.setViewName("account/login");
    return mav;
  }
  :
}

エラーメッセージの登録
メッセージソースは「Validator と MessageSource」で行った ReloadableResourceBundleMessageSource の設定をそのまま使います。なので、Bean Validator が吐き出したエラーコードと、それらに対応するメッセージを errors.xml 追加するだけです。

バリデーションの結果、検出されたエラー情報は次のような感じで BindingResult に格納されます。
:
codes [Pattern.idCard.password,Pattern.password,Pattern.java.lang.String,Pattern];
    :
codes [NotEmpty.idCard.password,NotEmpty.password,NotEmpty.java.lang.String,NotEmpty];
    :
要は、各エラーにおいて上記コードのどれかに対応したメッセージが定義されていればいいわけです。従って、今回は次のように定義しました。

errors.xml(抜粋)
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
  <comment>field and form level error messages</comment>
    :
  <entry key="Email.email">指定外の文字列</entry>
  <entry key="NotEmpty.email">必須です</entry>
  <entry key="Pattern.password">8-32文字の半角英数</entry>
  <entry key="NotEmpty.password">必須です</entry>
</properties>

実行!
図は、以上の作業を経てできた login フォームに、メールアドレス「wrider」、パスワード「未入力」として submit した後の画面です。

メールアドレス欄には“指定外の文字列”と表示されています。ここを「未入力」にすると“必須です”と表示されます。

一方、パスワード欄は @NotEmpty で「未入力」が検出されたことを示す“必須です”と、@Patter の引数で指示した正規表現にマッチしていないことを示す「8-32文字の半角英数」の両方が表示されています。これではちょっとかっこ悪いです。

GrepCode を使って、@Email アノテーションで呼び出される EmailValidator のソースを調べて見ると、未入力(null または length() == 0)の場合は true を返すようになっています。つまり、「未入力チェックは @Email の仕事ではありません」ということなのでしょう。

カスタムアノテーションの実装
そこでパスワード(password)について、JSR-303: Bean Validator と Hibernate Validator リファレンスの「Chapter 3. Creating custom constraints」を参考に、@CheckPassword という独自の制約条件(Custom Constraint)を実装してみます。

CheckPassword.java
まず、アノテーション CheckPassword を定義します。@Target でアノテーションの付与対象を指定し、@Constraint の引数 validatedBy で、このアノテーションが呼び出すバリデーターを指示します。
package wrider.annotation;

import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.*;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import javax.validation.Constraint;
import javax.validation.Payload;

import wrider.validator.CheckPasswordValidator;

@Target({FIELD, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy = CheckPasswordValidator.class)
@Documented
public @interface CheckPassword {
  
  String message() default "{wrider.annotation.CheckPassword.message}";
  
  Class<?>[] groups() default {};
  
  Class<? extends Payload>[] payload() default {};
  
}

CheckPaswordValidator.java
実際のバリデーション処理を行うクラスです。未入力チェックは他のアノテーションに譲り、正規表現によるフォーマットチェックの結果を返すようにしています。
package wrider.validator;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

import org.springframework.util.StringUtils;

import wrider.annotation.CheckPassword;

public class CheckPasswordValidator implements ConstraintValidator<CheckPassword, String> {

  private static final String PASSWORD_PATTERN = "^[a-zA-Z0-9]{8,32}$";
  
  public void initialize(CheckPassword constraintAnnotation) {
    
  }
  
  public boolean isValid(String object, ConstraintValidatorContext constraintContext) {
    if (!StringUtils.hasLength(object)) {
      return true;
    }
    else {
      return this.patternMatching(PASSWORD_PATTERN, object);
    }
  }

  private boolean patternMatching(String patternStr, String targetStr) {
    Pattern pattern = Pattern.compile(patternStr);
    Matcher matcher = pattern.matcher(targetStr);
    return matcher.matches();
  }
  
}

IdCard.java(抜粋)
これで @CheckPassword の作成は完了です。これを password に対して @Pattern の変わりに付与します。
:
  @NotEmpty
  @CheckPassword
  private String password;
    :

errors.xml(抜粋)
後は、@CheckPassword に対応したエラーメッセージを errors.xml に追加します。
:
  <entry key="CheckPassword.password">8-32文字の半角英数</entry>
    :

再び実行!
今度は、未入力時には“必須です”、入力ルールに従っていない文字列の時には“8-32文字の半角英数”のメッセージだけがパスワード欄に表示されるようになりました。

様々な制約条件に柔軟に対応できる Bean Validation は、慣れてしまえば重宝する仕掛けだと感じました。今回は取り上げませんでしたがバリデーションの順序を指定できる @GroupSequence など、他にも色々と面白そうな機能があり、これからやみつきになりそうです。

0 件のコメント:

コメントを投稿