KM18923006_TP_V

commons-Validator。strutsやspringでおなじみだけど、単独で導入されることがあまりないような気がする。 「準備したりいろいろ調べるぐらいならソースでベタ書きチェックしたほうがマシ」でスルーされた感。

今から導入するのであれば、Java EE 6以降の「Beans Validation」のほうがいいと思います。

だけど、せっかく勉強したのでメモ。
(現場がJDK6でBeans validationがJava EE7以降と思っててスルー。あとでJava EE6からあるって知ったわけさ。)

とりあえず簡単なサンプル(必須チェックのみ)

「Commons Validator」を簡単に説明すると、

  • 「飽くまで入力チェックしてその実行結果をオブジェクトにするまでをフレームワーク化」
  • 「エラーメッセージは、XMLに埋め込んだ情報をオブジェクト化してるから、参照しながら自分で実装してね」
  • 「XML内でオプション情報の埋め込み方法はvarやargだよ。strutsとかで書いただろ?」

という感じになってる。入力チェック以降のハンドリングは実装が必要。
何はともあれ入力チェックメソッドを実装。(commons validatorには標準的なチェックも入ってないっぽい)
staticでbooleanを返すメソッドを作ればいい。

package validate1;
import org.apache.commons.validator.Field;
import org.apache.commons.validator.GenericValidator;
import org.apache.commons.validator.util.ValidatorUtils;

public class Validator {
  
  /**
   * nullと空文字列チェック
   */
  public static boolean validateRequired(Object bean, Field field){
    String value = ValidatorUtils.getValueAsString(bean, field.getProperty());
    return !GenericValidator.isBlankOrNull(value);
  }
}

続いてチェック対象のbeanクラス

package validate1;

public class User {
  
    private String userName;

    public User(String userName) {
        this.userName = userName;
    }

    public String getUserName() {
      return userName;
    }

    public void setUserName(String userName) {
      this.userName = userName;
    }
}

定義ファイルはこんな感じ。strutsやspringで同じみだから、説明は割愛。

validator-definition.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE form-validation PUBLIC
     "-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.1//EN"
     "http://jakarta.apache.org/commons/dtds/validator_1_1.dtd">

<form-validation>
  <global>
    <validator name="required"
               classname="validate1.Validator" 
               method="validateRequired"
               methodParams="java.lang.Object, org.apache.commons.validator.Field"
               msg="{0}は必須です。"/>
 </global>
 <formset>
   <form name="userCheck">
     <field property="userName" depends="required">
       <!-- argをvalidatorのmsg値に埋め込むのは自分で実装 -->
       <arg key="ユーザ名" />
     </field>
   </form>
 </formset>
</form-validation>

エラーメッセージ作成はやってくれないので、ValidateExecutorというクラスを作った。
「どのチェックでエラーが発生したか?」という情報がいろんなオブジェクトに散らばってて結構手間。
そこで、このValidateExecutorでエラーメッセージを作成しつつ必要な情報だけを格納したExceptionを投げるようにしたわけさ。

package validate1;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.commons.validator.Arg;
import org.apache.commons.validator.Field;
import org.apache.commons.validator.Validator;
import org.apache.commons.validator.ValidatorAction;
import org.apache.commons.validator.ValidatorException;
import org.apache.commons.validator.ValidatorResources;
import org.apache.commons.validator.ValidatorResult;
import org.apache.commons.validator.ValidatorResults;
import org.apache.commons.validator.Var;
import org.xml.sax.SAXException;

public class ValidateExecutor {
  
  private final ValidatorResources resources;
  
  public ValidateExecutor(String fileName) throws SAXException, IOException{
    InputStream in = super.getClass().getResourceAsStream(fileName);
    this.resources = new ValidatorResources(in);
  }
  
  public void validate(Object bean, String formName) throws ValidatorException,InputException{
    
    //指定されたチェックを実行
    Validator validator = new Validator(resources, formName);
    validator.setParameter(Validator.BEAN_PARAM, bean);
    ValidatorResults results = validator.validate();
    
    //
    for (Iterator<?> ite =  results.getPropertyNames().iterator();ite.hasNext();) {
      String propertyName = (String)ite.next();
      ValidatorResult result = results.getValidatorResult(propertyName);
      
      // 
      List<InputException.Error> errorList = new ArrayList<>();
      @SuppressWarnings("unchecked")
      Iterator<String> actions = result.getActions();
      while (actions.hasNext()) {
        String dependName = actions.next();
        if (result.isValid(dependName)) {
          continue;
        }
        ValidatorAction action = resources.getValidatorAction(dependName);
        //エラー対象フィールっどの情報を取得するためのオブジェクト
        Field field = result.getField();
        
        /*
         * エラーメッセージの作成
         * XMLのfieldタグに固有メッセージ(errorMessage)が指定されていれば、そのメッセージを使用する。
         * 存在しない場合は、validatorに指定された標準メッセージを使用する。
         */
        String errorMessage = action.getMsg();
        //XMLのfieldに指定されたargでメッセージ内に埋め込まれた引数を置換する。
        int i=0;
        Arg curArg;
        while((curArg =  field.getArg(i)) != null){
          errorMessage = errorMessage.replace("{" + i + "}", curArg.getKey());
          i++;
        }
        //XMLのfieldにvar指定があれば、メッセージ内に埋め込まれた変数名で置換
        Map<?,?> varMap = field.getVars();
        for(Object curVarKey: varMap.keySet()){
          Var curVar = (Var)varMap.get(curVarKey);
          errorMessage = errorMessage.replace("{" + curVar.getName() + "}", curVar.getValue());
        }
        
        //エラー情報をリスト化して格納
        errorList.add(new InputException.Error(formName,(String)propertyName,dependName,errorMessage));
      }
      //入力チェックエラーがあれば例外を投げる。
      if(!errorList.isEmpty()){
        throw new InputException(errorList);
      }
    }
  }
}

「エラーメッセージの作成」という複数行コメントから始まる置換処理がちょっと長いけど、それ以外はシンプルです。
XMLのfieldに埋め込まれたargは起動引数のように0から取得していくので、strutsのようにエラーメッセージに埋め込まれた{0}{1}・・・を置換するように実装。
fieldに埋め込まれたvarもエラーメッセージに置換できるように作ってますが、これは次のサンプルで理解していただければ。
リスト化したオブジェクトを格納できる例外が必要なので、例外クラスも作りました。

package validate1;

import java.util.Collections;
import java.util.List;

public class InputException extends Exception{

  private static final long serialVersionUID = 1L;

  private final List<Error>  errors;
  
  public InputException(List<Error> errors){
    super(errors.toString());
    //例外の標準メッセージを作成する
    StringBuilder sb = new StringBuilder();
    for(Error error:errors){
      sb.append(error.getMsg());
    }
    this.errors = Collections.unmodifiableList(errors);
  }
  
  public List<InputException.Error> getErrors(){
    return errors;
  }
  
  /**
   * 一つのエラー情報を格納するクラス
   */
  public static class Error{
    
    private final String formName;
    
    private final String propertyName;
    
    private final String validate;
    
    private final String msg;
    
    public Error(String formName,
                  String propertyName,
                  String validate,
                  String msg){
      this.formName = formName;
      this.propertyName = propertyName;
      this.validate = validate;
      this.msg = msg;
    }

    public String getFormName() {
      return formName;
    }

    public String getPropertyName() {
      return propertyName;
    }

    public String getValidate() {
      return validate;
    }

    public String getMsg() {
      return msg;
    }
    
    @Override
    public String toString(){
      return validate + "(" + propertyName + "@" +  formName+ "):" + msg;
    }
  }
}

すいません。。やっと実行

package validate1;

public class ValidatorTest {
  
    public static void main(String[] args) throws Exception {
      //ValidateExecutor生成
      ValidateExecutor validateExecutor = new ValidateExecutor("validator-definition.xml");
      
      // 正しい値
      User user1 = new User("田中");
      validateExecutor.validate(user1, "userCheck");
      System.out.println("user1のチェック正常終了");

      // 誤った値
      User user2 = new User("");
      validateExecutor.validate(user2, "userCheck");
      System.out.println("user2のチェック正常終了");
    }
}
実行結果
user1のチェック正常終了
Exception in thread "main" validate1.InputException: [required(userName@userCheck):ユーザ名は必須です。]
	at validate1.ValidateExecutor.validate(ValidateExecutor.java:79)
	at validate1.ValidatorTest.main(ValidatorTest.java:16)

XMLのfieldタグにvarタグを埋め込んで使いたい場合

文字列チェックなどでチェックする文字列数は定義ファイルに書きたいはず。 commons-validatorは参照できるように作ってくれてます。まず定義ファイル。

validator-definition.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE form-validation PUBLIC
     "-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.1//EN"
     "http://jakarta.apache.org/commons/dtds/validator_1_1.dtd">

<form-validation>
  <global>
    <validator name="required"
               classname="validate1.Validator" 
               method="validateRequired"
               methodParams="java.lang.Object, org.apache.commons.validator.Field"
               msg="{0}は必須です。"/>
               
    <validator name="lengthBetween"
               classname="validate1.Validator" 
               method="isRange"
               methodParams="java.lang.Object, org.apache.commons.validator.Field"
               msg="{0}は{minlength}文字以上、{maxlength}文字以下に設定してください。"/>
 </global>
 <formset>
   <form name="userCheck">
     <field property="userName" depends="required,lengthBetween">
       <arg key="ユーザ名" />
       <var>
         <var-name>minlength</var-name>
         <var-value>2</var-value>
       </var>
       <var>
         <var-name>maxlength</var-name>
         <var-value>6</var-value>
       </var>
     </field>
   </form>
 </formset>
</form-validation>

varで指定した定義を入力チェックメソッドで参照すればいいだけ。

package validate1;
import org.apache.commons.validator.Field;
import org.apache.commons.validator.GenericValidator;
import org.apache.commons.validator.util.ValidatorUtils;

public class Validator {
  
  /**
   * nullと空文字列チェック
   */
  public static boolean validateRequired(Object bean, Field field){
    String value = ValidatorUtils.getValueAsString(bean, field.getProperty());
    return !GenericValidator.isBlankOrNull(value);
  }
  
  /**
   * 文字列数の範囲チェック
   */
  public static boolean isRange(Object bean, Field field){
    String value = (ValidatorUtils.getValueAsString(bean, field.getProperty()));
    if(GenericValidator.isBlankOrNull(value)){
      return true;
    }
    int length = value.length();
    int minlength = Integer.parseInt(field.getVarValue("minlength"));
    int maxlength = Integer.parseInt(field.getVarValue("maxlength"));
    return GenericValidator.isInRange(length, minlength, maxlength);
  }
}

いざ実行

package validate1;

public class ValidatorTest {
    public static void main(String[] args) throws Exception {
      
      //ValidateExecutor生成
      ValidateExecutor validateExecutor = new ValidateExecutor("validator-definition.xml");
      
      // 正しい値を設定した例
      User user1 = new User("田中");
      validateExecutor.validate(user1, "userCheck");
      System.out.println("user1のチェック正常終了");

      // 誤った値を設定した例
      User user2 = new User("a");
      validateExecutor.validate(user2, "userCheck");
      System.out.println("user2のチェック正常終了");
    }
}
実行結果
user1のチェック正常終了
Exception in thread "main" validate1.InputException: [lengthBetween(userName@userCheck):ユーザ名は2文字以上、6文字以下に設定してください。]
	at validate1.ValidateExecutor.validate(ValidateExecutor.java:80)
	at validate1.ValidatorTest.main(ValidatorTest.java:16)

ValidateExecutorは、エラーメッセージにvar名で埋め込まれると置換するように作りました。

相関チェック

相関チェックもvalidatorで定義したいと思って。
以下のサンプルは、「どちらか一方は必ず入力してること」をチェック
まず定義ファイル

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE form-validation PUBLIC
     "-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.1//EN"
     "http://jakarta.apache.org/commons/dtds/validator_1_1.dtd">

<form-validation>
  <global>
    <validator name="required"
               classname="validate1.Validator" 
               method="validateRequired"
               methodParams="java.lang.Object, org.apache.commons.validator.Field"
               msg="{0}は必須です。"/>
               
    <validator name="lengthBetween"
               classname="validate1.Validator" 
               method="isRange"
               methodParams="java.lang.Object, org.apache.commons.validator.Field"
               msg="{0}は{minlength}文字以上、{maxlength}文字以下に設定してください。"/>
               
    <validator name="requiredEither"
               classname="validate1.Validator" 
               method="requiredEither"
               methodParams="java.lang.Object, org.apache.commons.validator.Field"
               msg="{0}、{1}は少なくともどちらかは入力してください。"/>
 </global>
 <formset>
   <form name="userCheck">
     <field property="userName" depends="lengthBetween">
       <arg key="ユーザ名" />
       <var>
         <var-name>minlength</var-name>
         <var-value>2</var-value>
       </var>
       <var>
         <var-name>maxlength</var-name>
         <var-value>6</var-value>
       </var>
     </field>
     
     <field property="nickName" depends="lengthBetween">
       <arg key="ニックネーム" />
       <var>
         <var-name>minlength</var-name>
         <var-value>2</var-value>
       </var>
       <var>
         <var-name>maxlength</var-name>
         <var-value>6</var-value>
       </var>
     </field>
     
     <field property="userName,nickName" depends="requiredEither">
       <arg key="ユーザ名" />
       <arg key="ニックネーム" />
     </field>
   </form>
 </formset>
</form-validation>

入力チェッククラスにメソッド追加(requiredEither)

package validate1;
import org.apache.commons.validator.Field;
import org.apache.commons.validator.GenericValidator;
import org.apache.commons.validator.util.ValidatorUtils;

public class Validator {
  
  /**
   * nullと空文字列チェック
   */
  public static boolean validateRequired(Object bean, Field field){
    String value = ValidatorUtils.getValueAsString(bean, field.getProperty());
    return !GenericValidator.isBlankOrNull(value);
  }
  
  /**
   * 文字列数の範囲チェック
   */
  public static boolean isRange(Object bean, Field field){
    String value = ValidatorUtils.getValueAsString(bean, field.getProperty());
    if(GenericValidator.isBlankOrNull(value)){
      return true;
    }
    int length = value.length();
    int minlength = Integer.parseInt(field.getVarValue("minlength"));
    int maxlength = Integer.parseInt(field.getVarValue("maxlength"));
    return GenericValidator.isInRange(length, minlength, maxlength);
  }
  
  /**
   * どちらかは必ず入力が必要ですよチェック
   */
  public static boolean requiredEither(Object bean, Field field){
    String[] properties = field.getProperty().split(",");
    String value1 = ValidatorUtils.getValueAsString(bean, properties[0]);
    String value2 = ValidatorUtils.getValueAsString(bean, properties[1]);
    
    return !(GenericValidator.isBlankOrNull(value1) & GenericValidator.isBlankOrNull(value2));
  }
}

カンマで区切ってValidatorのメソッド内でスプリットしてます。
※beanクラスにnickNameをセッターゲッターしてください。(長いから割愛。。)
いざ実行

package validate1;

public class ValidatorTest {
    public static void main(String[] args) throws Exception {
      
      //ValidateExecutor生成
      ValidateExecutor validateExecutor = new ValidateExecutor("validator-definition.xml");
      
      // 正しい値を設定した例
      User user1 = new User("田中","");
      validateExecutor.validate(user1, "userCheck");
      System.out.println("user1のチェック正常終了");

      // 誤った値を設定した例
      User user2 = new User("","");
      validateExecutor.validate(user2, "userCheck");
      System.out.println("user2のチェック正常終了");
 
    }
}
実行結果
user1のチェック正常終了
Exception in thread "main" validate1.InputException: [requiredEither(userName,nickName@userCheck):ユーザ名、ニックネームは少なくともどちらかは入力してください。]
	at validate1.ValidateExecutor.validate(ValidateExecutor.java:80)
	at validate1.ValidatorTest.main(ValidatorTest.java:16)

以上です。。
独自Validator実装時のvarやargの参照方法や、相関チェック方法の提案だけを書きたかったのですが、長くなってしまいました。。
時間あるときにもう少しシンプルにします。すません。