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 する頻度を減らせるので、うまく使いこなせば大規模アプリケーションの改修なども楽になるかも、と感じました。

0 件のコメント:

コメントを投稿