ラベル Validator の投稿を表示しています。 すべての投稿を表示
ラベル Validator の投稿を表示しています。 すべての投稿を表示

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 など、他にも色々と面白そうな機能があり、これからやみつきになりそうです。

2012年4月2日月曜日

DI による Validator の再利用


今回の課題は「Validator と MessageSource」で作成したパーツを再利用した登録フォームにします。

データオブジェクトの再利用
とりあえず登録フォームの入力項目は以下のように考えました。
  • メールアドレス: email
  • パスワード: password
  • 名字: firstName
  • 名前: lastName
この中で「メールアドレス」と「パスワード」は前に作った IdCard と重複しているので、登録フォーム用のデータオブジェクト(UserProfile)には、これを再利用することにします。

UserProfile.java
このクラスは IdCard クラスを extends しています。名字(firstName)と名前(lastName)は、プロパティとそれらに対応する setter/getter を定義していますが、メールアドレス(email)とパスワード(password)については、スーパークラスの setter/getter を呼び出しています。
package wrider.model;

public class UserProfile extends IdCard {
  
  private String lastName;
  
  public String getLastName() {
    return this.lastName;
  }
  
  public void setLastName(String lastName) {
    this.lastName = lastName;
  }
  
  private String firstName;
  
  public String getFirstName() {
    return this.firstName;
  }
  
  public void setFirstName(String firstName) {
    this.firstName = firstName;
  }
  
  public String getEmail() {
    return super.getEmail();
  }
  
  public void setEmail(String email) {
    super.setEmail(email);
  }
  
  public String getPassword() {
    return super.getPassword();
  }
  
  public void setPassword(String password) {
    super.setPassword(password);
  }

}

Validator の再利用
Validator は、IdCard 用の IdCardValidator を DI(Dependency injection)します。

UserProfileValidator.java
リファレンスの“6.2 Validation using Spring's Validator interface”に習い、コンストラクター引数として IdCardValidator を DI しています。この方法を Constructor-based DI というらしいです。

上記 UserProfile クラスと同じように登録フォーム独自の項目である「名字」と「名前」の未入力チャックとエラー情報の reject を ValidationUtilsrejectIfEmptyOrWhitespace(..) メソッドで行い、「メールアドレス」と「パスワード」に関しては、DI した IdCardValidator をinvokeValidator(..) メソッドで呼び出して検証させます。
package wrider.validator;

import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;
import org.springframework.validation.Errors;

import wrider.model.UserProfile;

public class UserProfileValidator implements Validator {
  
  private final Validator idCardValidator;
  
  private static final String ERROR_FIELD_EMPTY = "error.field.empty";
  
  public UserProfileValidator(Validator idCardValidator) {
    this.idCardValidator = idCardValidator;
  }
  
  public boolean supports(Class<?> clazz) {
    return UserProfile.class.isAssignableFrom(clazz);
  }
  
  public void validate(Object target, Errors errors) {
    
    ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", ERROR_FIELD_EMPTY);
    ValidationUtils.rejectIfEmptyOrWhitespace(errors, "lastName", ERROR_FIELD_EMPTY);
    
    UserProfile userProfile = (UserProfile)target;
    
    ValidationUtils.invokeValidator(idCardValidator, userProfile, errors);
    
  }
}

Validator の登録
新しく作成した UserProfileValidator を applicationContext.xml に登録します。

applicationContext.xml(抜粋)
リファレンスの“4.4.1.1 Constructor-based dependency injection”に習い <constructor-arg> 要素で userProfileValidator ビーンのコンストラクター引数として idCardValidator を DI するよう指示しています。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    :
  http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd">
  
  <!-- Validator -->
  <bean id="idCardValidator"
    class="wrider.validator.IdCardValidator"/>
  
  <bean id="userProfileValidator"
    class="wrider.validator.UserProfileValidator">
    <constructor-arg ref="idCardValidator"/>
  </bean>
    :
</beans>

エラーコードの修正
今回は IdCard と UserProfile のバリデーションエラーに共通のエラーコードで対応したいので、以下のように errors.xml を修正しました。

修正前 [エラーコード].[オブジェクト名].[フィールド名]
 ↓
修正後 [エラーコード].[フィールド名]

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="error.field.empty.email">必須です</entry>
  <entry key="error.field.illigal.email">あなたのメルアドです</entry>
  <entry key="error.field.empty.password">必須です</entry>
  <entry key="error.field.illigal.password">内容をお確かめ下さい</entry>
  <entry key="error.field.empty.firstName">必須です</entry>
  <entry key="error.field.empty.lastName">必須です</entry>
  <entry key="error.form.invalid.idCard">入力ミスがあります</entry>
  <entry key="error.form.invalid.userProfile">入力ミスがあります</entry>
</properties>

registrationForm.jsp(抜粋)
登録フォームの jsp です。とりあえず先の login.jsp に名字と名前を追加して、<form:form> タグの modelAttribute と、POST リクエストを投げる先を変えただけです。
<body>
  <h1>WriDer's Demo Site</h1>
  <form:form action="register.html" method="POST" modelAttribute="userProfile">
    <label for="email">メールアドレス</label>
    <input type="text" id="email" name="email" value="${userProfile.email}"/>
    <span class="red"><form:errors path="email"/></span>
    <br />
    <label for="password">パスワード</label>
    <input type="password" id="password" name="password" value="${userProfile.password}"/>
    <span class="red"><form:errors path="password"/></span>
    <br />
    <label for="firstName">名字</label>
    <input type="text" id="firstName" name="firstName" value="${userProfile.firstName}"/>
    <span class="red"><form:errors path="firstName"/></span>
    <br />
    <label for="lastName">名前</label>
    <input type="text" id="lastName" name="lastName" value="${userProfile.lastName}"/>
    <span class="red"><form:errors path="lastName"/></span>
    <br />
    <input type="submit" value="決定"/>
  </form:form>
</body>

コントローラーの変更
今回の登録フォームも AccountController クラスで対応します。@RequestMapping アノテーションで /account/register.html に対する GET/POST リクエストを振り分けています。POST で実行される register メソッドで userProfileValidator を呼び出し、バリデーションを行っています。

また、@SessionAttributes, @Autowired, @ModelAttribute アノテーション、および import に UserProfile に関する記述を追加しています。

AccountController.java(抜粋)
package wrider.controller;
    :
import wrider.model.UserProfile;

@Controller
@SessionAttributes({"idCard", "userProfile"})
public class AccountController {
  
  @Autowired
  private Validator idCardValidator;
  
  @Autowired
  private Validator userProfileValidator;

    :
  @ModelAttribute("userProfile")
  public UserProfile makeUserProfile() {
    return new UserProfile();
  }
    :

  @RequestMapping(value="/account/register.html", method=RequestMethod.GET)
  public String register() {
    return "account/registrationForm";
  }
  
  @RequestMapping(value="/account/register.html", method=RequestMethod.POST)
  public ModelAndView register(UserProfile userProfile, BindingResult br) {
    
    this.userProfileValidator.validate(userProfile, br);
    
    ModelAndView mav = new ModelAndView();
    mav.getModel().putAll(br.getModel());
    mav.setViewName("account/registrationForm");
    return mav;
  }
    :
}

実行
図はブラウザーで /account/register.html にアクセスし、「名前」を未入力にして submit した後の画面です。

POST リクエストを受けた Spring Framework は IdCard インスタンスに「メールアドレス」と「パスワード」、UserProfile インスタンスに「名字」「名前」をセットし、AccountController の register(..)メソッドを呼び出します。同メソッドは UserProfileValidator のバリデーション結果を BindingResult から取り出して ModelAndView にセットします。

Java は元々、コンポーネントを再利用する仕掛けを持っていますが、Spring Framework が提供する DI(または IoC)の仕掛けを使えば、依存関係を記した XML(やアノテーション)に従って、インスタンス化や注入を行ってくれます。使いたいコンポーネントを直に import して new する頻度を減らせるので、うまく使いこなせば大規模アプリケーションの改修なども楽になるかも、と感じました。

2012年3月31日土曜日

Validator と MessageSource


前回の「ModelAttribute と SessionAttributes」で、単純なフォーム処理の仕掛けを作ったので、今回はバリデーションの機能を追加してみます。

Validator の実装
リファレンス「6.2 Validation using Spring's Validator interface」の例に従って ValidationUtils クラスの rejectIfEmpty メソッドや rejectIfEmptyOrWhitespace メソッドを使えば、未入力フィールドとエラーコードの設定が簡単に行えます。

ただ今回は、未入力だけでなく、文字列のフォーマットまでチェックしたいので、StringUtils クラスの hasLength メソッドと java.util.regex パッケージが提供する Pattern クラス及び Matcher クラスを使うことにします

エラーコード
エラーコードは DefaultMessageCodesResolver に記されたルールに従って作ります。

例えば、今回の場合
  • 未入力によるフィールドエラー : error.field.empty
  • 指定外フォーマットによるフィールドエラー : error.field.illigal
  • フォームレベルで拾ったグローバルエラー : error.form.invalid
のような感じにしてみました。

因みに validation 後の BindingResult の内容を見ると Validator 実装クラス(IdCardValidator)の中で errors.rejectValue(..) を使って設定したエラー情報は、上記のルールに則っていることがわかります。

IdCardValidator.java
package wrider.validator;

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

import org.springframework.validation.Validator;
import org.springframework.validation.Errors;
import org.springframework.util.StringUtils;

import wrider.model.IdCard;

public class IdCardValidator implements Validator {
 
 private static final String EMAIL_PATTERN = "^[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)*@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)*(\\.[a-zA-Z]{2,})$";
 private static final String PASSWORD_PATTERN = "^[a-zA-Z0-9]{8,32}$";
 
 private static final String ERROR_FIELD_EMPTY = "error.field.empty";
 private static final String ERROR_FIELD_ILLIGAL = "error.field.illigal";
 private static final String ERROR_FORM_INVALID = "error.form.invalid";
 
 public boolean supports(Class<?> clazz) {
  return IdCard.class.isAssignableFrom(clazz);
 }
 
 public void validate(Object target, Errors errors) {
  
  IdCard idCard = (IdCard)target;
  
  if (!StringUtils.hasLength(idCard.getEmail())) {
   errors.rejectValue("email", ERROR_FIELD_EMPTY);
  }
  else if (!patternMatching(EMAIL_PATTERN, idCard.getEmail())) {
   errors.rejectValue("email", ERROR_FIELD_ILLIGAL);
  }
  
  if (!StringUtils.hasLength(idCard.getPassword())) {
   errors.rejectValue("password", ERROR_FIELD_EMPTY);
  }
  else if (!patternMatching(PASSWORD_PATTERN, idCard.getPassword())) {
   errors.rejectValue("password", ERROR_FIELD_ILLIGAL);
  }
  
  if (errors.hasErrors()) {
   errors.reject(ERROR_FORM_INVALID);
  }
  
 }

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

Validator と messageSource の登録
上記 Validator の実装クラス(idCardValidator)を applicationContext.xml に登録して、コントローラークラスに DI できるようにします。また、メッセージコード(含むエラーコード)を実際のメッセージに解決する messageSource ですが、今回は ReloadableResourceBundleMessageSource を指定しています。尚、classpath: プレフィックスでクラスパス上の場所を指定することもできます。また、プロパティファイルが一つだけのときは、以下のように basenames 要素(複数形)の代わりに basename 要素(単数形)が使えます。

<property name="basename" value="classpath:messages"/>

applicationContext.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:tx="http://www.springframework.org/schema/tx"
xmlns:jee="http://www.springframework.org/schema/jee"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-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/context http://www.springframework.org/schema/context/spring-context-2.5.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd">

<!-- Validator -->
<bean id="idCardValidator"
class="wrider.validator.IdCardValidator"/>

<!-- Message Source -->
<bean id="messageSource"
class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
<property name="basenames">
<list>
<value>/WEB-INF/messages/errors</value>
</list>
</property>
<property name="defaultEncoding" value="utf-8"/>
<property name="fileEncodings" value="utf-8"/>
<property name="cacheSeconds" value="0"/>
</bean>

</beans>

プロパティファイル
プロパティファイルは、http://java.sun.com/dtd/properties.dtdに従って作った XML 形式のファイル(error.xml)を上記 applicationContext.xml で指示した /WEB-INF/messages/ に置いています。

errors.xml
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>field level error messages</comment>
<entry key="error.field.empty.idCard.email">必須です</entry>
<entry key="error.field.illigal.idCard.email">内容をお確かめ下さい</entry>
<entry key="error.field.empty.idCard.password">必須です</entry>
<entry key="error.field.illigal.idCard.password">内容をお確かめ下さい</entry>
<entry key="error.form.invalid.idCard">入力ミスがあります</entry>
</properties>

コントローラーへのDI
前回作ったコントローラー(AccountController.java)に、上記 applicationContext.xml で定義した idCardValidator ビーンを @Autowired アノテーションで DI(Dependency Injection)し、validation メソッドで POST されたデータを検証するコードを追加しています(赤字の箇所)。

AccountController.java
package wrider.controller;

import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.validation.Validator;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.servlet.ModelAndView;

import wrider.model.IdCard;

@Controller
@SessionAttributes("idCard")
public class AccountController {

@Autowired
private Validator idCardValidator;

private int testCounter = 0;

@ModelAttribute("idCard")
public IdCard makeIdCard() {
printOrder("makeIdCard");
return new IdCard();
}

@RequestMapping(value="/account/login.html", method=RequestMethod.GET)
public String login() {
printOrder("login[GET]");
return "account/login";
}

@RequestMapping(value="/account/login.html", method=RequestMethod.POST)
public ModelAndView login(IdCard idCard, BindingResult br) {

this.idCardValidator.validate(idCard, br);

printOrder("login[POST]");

System.out.println(idCard.getEmail());

System.out.println("BindingResult has ..");
for (Map.Entry<String, Object> entry : br.getModel().entrySet()) {
System.out.println(" key: " + entry.getKey());
System.out.println(" value: " + entry.getValue().toString());
System.out.println(" -----------------------------------");
}

ModelAndView mav = new ModelAndView();
mav.getModel().putAll(br.getModel());
mav.setViewName("account/login");
return mav;
}

private void printOrder(String methodName) {
this.testCounter++;
System.out.println(this.testCounter + ": " + methodName + " has been invoked!");
}

}

メッセージの表示
Spring Framework の <form:errors> タグの path 属性で、Validator が上げてきたエラーオブジェクトへのパスを指定しています(赤字の箇所)。

login.jsp(抜粋)
<body>
<style>
.red {
color: #ff0000;
}
</style>
<h1>WriDer's Demo Site</h1>
<form:form action="login.html" method="POST" modelAttribute="idCard">
<label for="email">メールアドレス</label>
<input type="text" id="email" name="email" value="${idCard.email}"/>
<span class="red"><form:errors path="email"/></span>
<br />
<label for="password">パスワード</label>
<input type="password" id="password" name="password" value="${idCard.password}"/>
<span class="red"><form:errors path="password"/></span>
<br />
<input type="submit" value="決定"/>
<h3>POSTed email: ${idCard.email}</h3>
<h3>POSTed password: ${idCard.password}</h3>
</form:form>
</body>

動作確認
何も入力せずに submit すると図のようにフィールドの横に赤字で「必須です」のメッセージが表示されます。これらは errors.xml の error.field.empty.idCard.email 及び passwod で定義したメッセージです。

次にメールアドレスに「wrider」という文字列を入力してみます。メールアドレス欄のエラーメッセージが「内容をお確かめ下さい」となりました。これはエラーコード error.field.illigal.idCard.email に対応したメッセージです。

errors.rejectValue と reject が吐き出す中身
IdCardValidator 内で rejectValue/reject されたエラー情報をコンソールで確認してみると、以下のようになっています。これは未入力時の状態ですが、青字で示した部分に、DefaultMessageCodesResolver の仕様どおりの順番でエラーコードが格納されているのがわかります。

BindingResult has ..
 key: idCard
 value: wrider.model.IdCard@471b39
 -----------------------------------
 key: org.springframework.validation.BindingResult.idCard
 value: org.springframework.validation.BeanPropertyBindingResult: 3 errors
Field error in object 'idCard' on field 'email':
 rejected value [];
 codes [
  error.field.empty.idCard.email,
  error.field.empty.email,
  error.field.empty.java.lang.String,
  error.field.empty
 ];
 arguments [];
 default message [null]
Field error in object 'idCard' on field 'password':
 :
Error in object 'idCard':
 codes [
  error.form.invalid.idCard,
  error.form.invalid
 ];
 arguments [];
 default message [null]

リローダブル!
今回最も興味があったのが ReloadableResourceBundleMessageSource の Reloadable の部分です。アプリケーションが動作している状態で errors.xml を以下のように変更し、メールアドレスに先ほどエラーとなった「wrider」を入力してみます。

 <entry key="error.field.illigal.idCard.email">内容をお確かめ下さい</entry>
   ↓
 <entry key="error.field.illigal.idCard.email">あなたのメルアドです</entry>

すると、エラーメッセージが下図のように変わりました。

Spring の API ドキュメントによれば、ReloadableResourceBundleMessageSource の cacheSeconds プロパティを“0(ゼロ)”に設定すると、メッセージへのアクセスが発生する度に、ファイルのタイプスタンプをチェックするそうです。0設定については“本番環境では使うな”と書いてありますが、適度な間隔を設定すれば、再起動することなくファイルの変更が反映されるので、とても便利だと思います。

ResourceBundleMessageSource にまつわる話
Spring が提供している MessageSource のもう一つの実装 ResourceBundleMessageSourceに関しては、「文字化け」とか「native2ascii が必要」とか properties ファイルにまつわる面倒くさそうな話を散見します。Aleksa Vukotic 氏の投稿“UTF-8 encoding and Spring message sources”によると、同クラスで使われている java.util.Properties が ISO 8859-1 しかサポートしていないことに起因するようです。

ただ J2SE 5.0 の java.util.Properties から XML 形式がサポートされ、デフォルトの UTF-8 以外も指定できるみたいなので、Spring の方でも対応してもよさそうですが、おそらくそうした使い方をしたいときは ReloadableResourceBundleMessageSource を使え、ということでなのしょうね。