Apache Commons Validator

バリデーションライブラリJavaApache CommonsXML設定ビルトインバリデータエンタープライズ

バリデーションライブラリ

Apache Commons Validator

概要

Apache Commons Validatorは、Javaアプリケーション向けの包括的なバリデーションフレームワークです。クライアントサイドとサーバーサイドの両方でデータ検証を行うための基盤を提供し、XML設定による宣言的なバリデーション定義をサポートします。メールアドレス、URL、クレジットカード番号、ISBNなど、一般的な検証ルーチンを組み込みで提供し、StrutsやSpring MVCなどのフレームワークとも統合可能です。

詳細

Apache Commons Validatorは、再利用可能なバリデーションコンポーネントのセットを提供するフレームワークです。XMLファイルでバリデーションルールを定義でき、JavaBeanのプロパティ検証を抽象化します。組み込みのバリデータルーチンには、メール検証、URL検証、日付検証、数値検証、正規表現検証などが含まれます。

主要な特徴:

  • XML設定ベース: バリデーションルールをXMLで定義し、Javaコードから分離
  • 豊富な組み込みバリデータ: EmailValidator、UrlValidator、CreditCardValidator、ISBNValidatorなど多数の実用的なバリデータ
  • ロケール対応: 言語・地域に応じたバリデーションルールの切り替え
  • カスタムバリデータ: 独自のバリデーションロジックの実装と統合
  • ネストプロパティ対応: Apache Commons BeanUtilsを使用した複雑なオブジェクト構造の検証
  • フレームワーク統合: Struts、Spring MVCなどの主要フレームワークとの連携

バージョン1.9.0では、Java 8以上をサポートし、モダンなJava開発環境に対応しています。

メリット・デメリット

メリット

  • 実績と信頼性: Apache Software Foundationによる長年の開発実績
  • 包括的なバリデータセット: 実用的な組み込みバリデータが豊富
  • 設定の外部化: XMLベースで検証ルールを管理
  • 国際化対応: 多言語・多地域対応が容易
  • 軽量: 最小限の依存関係で動作
  • 拡張性: カスタムバリデータの追加が簡単

デメリット

  • XML設定の複雑さ: 大規模プロジェクトでは設定が煩雑になりがち
  • アノテーション非対応: Bean Validationと比較して記述が冗長
  • 学習曲線: XMLスキーマとフレームワーク概念の理解が必要
  • 保守性: XML設定とJavaコードの分離によりリファクタリングが困難
  • モダンではない: 最新のJava開発手法と比較して古典的なアプローチ

参考ページ

書き方の例

インストールと基本セットアップ

<!-- Maven dependency -->
<dependency>
    <groupId>commons-validator</groupId>
    <artifactId>commons-validator</artifactId>
    <version>1.9.0</version>
</dependency>
// Gradle dependency
dependencies {
    implementation 'commons-validator:commons-validator:1.9.0'
}

組み込みバリデータの使用例

import org.apache.commons.validator.routines.*;
import java.util.Locale;

public class BuiltInValidatorExample {
    
    public static void main(String[] args) {
        // メールアドレスのバリデーション
        EmailValidator emailValidator = EmailValidator.getInstance();
        String email = "[email protected]";
        if (emailValidator.isValid(email)) {
            System.out.println("有効なメールアドレス: " + email);
        } else {
            System.out.println("無効なメールアドレス: " + email);
        }
        
        // URLのバリデーション
        UrlValidator urlValidator = new UrlValidator();
        String url = "https://www.example.com";
        if (urlValidator.isValid(url)) {
            System.out.println("有効なURL: " + url);
        }
        
        // クレジットカード番号のバリデーション
        CreditCardValidator creditCardValidator = new CreditCardValidator();
        String creditCard = "4111111111111111"; // テスト用Visa番号
        if (creditCardValidator.isValid(creditCard)) {
            System.out.println("有効なクレジットカード番号");
        }
        
        // ISBNのバリデーション(ISBN-10とISBN-13対応)
        ISBNValidator isbnValidator = new ISBNValidator();
        String isbn10 = "0-306-40615-2";
        String isbn13 = "978-0-306-40615-7";
        
        if (isbnValidator.isValidISBN10(isbn10)) {
            System.out.println("有効なISBN-10: " + isbn10);
        }
        
        if (isbnValidator.isValidISBN13(isbn13)) {
            System.out.println("有効なISBN-13: " + isbn13);
        }
        
        // 日付のバリデーション
        DateValidator dateValidator = DateValidator.getInstance();
        String dateStr = "2024/12/31";
        if (dateValidator.isValid(dateStr, "yyyy/MM/dd", Locale.JAPAN)) {
            System.out.println("有効な日付: " + dateStr);
        }
        
        // 整数のバリデーション
        IntegerValidator intValidator = IntegerValidator.getInstance();
        String intStr = "12345";
        Integer value = intValidator.validate(intStr);
        if (value != null) {
            System.out.println("有効な整数: " + value);
        }
        
        // 正規表現によるバリデーション
        RegexValidator regexValidator = new RegexValidator("^[0-9]{3}-[0-9]{4}$");
        String postalCode = "123-4567";
        if (regexValidator.isValid(postalCode)) {
            System.out.println("有効な郵便番号: " + postalCode);
        }
    }
}

XMLベースのバリデーション設定

<!-- validation.xml -->
<!DOCTYPE form-validation PUBLIC
    "-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.4.0//EN"
    "http://commons.apache.org/dtds/validator_1_4_0.dtd">

<form-validation>
    <!-- グローバルバリデータ定義 -->
    <global>
        <validator name="required"
            classname="org.apache.commons.validator.ValidatorAction"
            method="validateRequired"
            methodParams="java.lang.Object,
                         org.apache.commons.validator.Field,
                         org.apache.commons.validator.ValidatorAction"
            msg="errors.required"/>
            
        <validator name="email"
            classname="org.apache.commons.validator.routines.EmailValidator"
            method="isValid"
            methodParams="java.lang.String"
            msg="errors.email"/>
            
        <validator name="mask"
            classname="org.apache.commons.validator.ValidatorAction"
            method="validateMask"
            methodParams="java.lang.Object,
                         org.apache.commons.validator.Field,
                         org.apache.commons.validator.ValidatorAction"
            msg="errors.invalid"/>
    </global>
    
    <!-- フォーム定義 -->
    <formset>
        <form name="userForm">
            <field property="username" depends="required">
                <arg key="label.username"/>
            </field>
            
            <field property="email" depends="required,email">
                <arg key="label.email"/>
            </field>
            
            <field property="phoneNumber" depends="required,mask">
                <arg key="label.phoneNumber"/>
                <var>
                    <var-name>mask</var-name>
                    <var-value>^\d{3}-\d{4}-\d{4}$</var-value>
                </var>
            </field>
            
            <field property="age" depends="required,integer,intRange">
                <arg key="label.age"/>
                <var>
                    <var-name>min</var-name>
                    <var-value>18</var-value>
                </var>
                <var>
                    <var-name>max</var-name>
                    <var-value>120</var-value>
                </var>
            </field>
        </form>
        
        <!-- 日本語ロケール用の設定 -->
        <form name="userForm" locale="ja">
            <field property="postalCode" depends="required,mask">
                <arg key="label.postalCode"/>
                <var>
                    <var-name>mask</var-name>
                    <var-value>^\d{3}-\d{4}$</var-value>
                </var>
            </field>
        </form>
    </formset>
</form-validation>

Javaでのバリデーション実行

import org.apache.commons.validator.*;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;

public class FormValidationExample {
    
    private ValidatorResources resources;
    
    public FormValidationExample() throws Exception {
        // XMLファイルからバリデーション設定を読み込み
        InputStream in = getClass().getResourceAsStream("/validation.xml");
        resources = new ValidatorResources(in);
    }
    
    public void validateUserForm(UserBean user) throws ValidatorException {
        // Validatorインスタンスの作成
        Validator validator = new Validator(resources, "userForm");
        
        // バリデーション対象のBeanを設定
        validator.setParameter(Validator.BEAN_PARAM, user);
        
        // エラーメッセージを格納するマップ
        Map<String, String> errors = new HashMap<>();
        validator.setParameter("errors", errors);
        
        // バリデーション実行
        ValidatorResults results = validator.validate();
        
        // 結果の確認
        if (!errors.isEmpty()) {
            System.out.println("バリデーションエラー:");
            for (Map.Entry<String, String> entry : errors.entrySet()) {
                System.out.println(entry.getKey() + ": " + entry.getValue());
            }
        } else {
            System.out.println("バリデーション成功");
        }
    }
}

// バリデーション対象のBean
public class UserBean {
    private String username;
    private String email;
    private String phoneNumber;
    private Integer age;
    private String postalCode;
    
    // コンストラクタ
    public UserBean() {}
    
    // getters and setters
    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }
    
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
    
    public String getPhoneNumber() { return phoneNumber; }
    public void setPhoneNumber(String phoneNumber) { this.phoneNumber = phoneNumber; }
    
    public Integer getAge() { return age; }
    public void setAge(Integer age) { this.age = age; }
    
    public String getPostalCode() { return postalCode; }
    public void setPostalCode(String postalCode) { this.postalCode = postalCode; }
}

カスタムバリデータの作成

import org.apache.commons.validator.Field;
import org.apache.commons.validator.GenericValidator;
import org.apache.commons.validator.ValidatorAction;
import org.apache.commons.validator.util.ValidatorUtils;

public class CustomValidators {
    
    /**
     * 日本の電話番号形式をバリデート
     */
    public static boolean validateJapanesePhoneNumber(
            Object bean, 
            Field field, 
            ValidatorAction va) {
        
        String value = ValidatorUtils.getValueAsString(bean, field.getProperty());
        
        if (GenericValidator.isBlankOrNull(value)) {
            return true; // 空の場合はrequiredバリデータに任せる
        }
        
        // 日本の電話番号パターン
        String phonePattern = "^(0[0-9]{1,4}-[0-9]{1,4}-[0-9]{4}|0[0-9]{9,10})$";
        
        return value.matches(phonePattern);
    }
    
    /**
     * パスワード強度のバリデート
     */
    public static boolean validatePasswordStrength(
            Object bean,
            Field field,
            ValidatorAction va) {
        
        String password = ValidatorUtils.getValueAsString(bean, field.getProperty());
        
        if (GenericValidator.isBlankOrNull(password)) {
            return true;
        }
        
        // パスワードポリシー:
        // - 8文字以上
        // - 大文字・小文字・数字・特殊文字を含む
        boolean hasLength = password.length() >= 8;
        boolean hasUpperCase = password.matches(".*[A-Z].*");
        boolean hasLowerCase = password.matches(".*[a-z].*");
        boolean hasDigit = password.matches(".*[0-9].*");
        boolean hasSpecialChar = password.matches(".*[!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>\\/?].*");
        
        return hasLength && hasUpperCase && hasLowerCase && hasDigit && hasSpecialChar;
    }
    
    /**
     * 開始日と終了日の整合性チェック
     */
    public static boolean validateDateRange(
            Object bean,
            Field field,
            ValidatorAction va) {
        
        String startDateStr = field.getVarValue("startDateProperty");
        String endDateStr = field.getVarValue("endDateProperty");
        String datePattern = field.getVarValue("datePattern");
        
        if (startDateStr == null || endDateStr == null) {
            return false;
        }
        
        String startValue = ValidatorUtils.getValueAsString(bean, startDateStr);
        String endValue = ValidatorUtils.getValueAsString(bean, endDateStr);
        
        if (GenericValidator.isBlankOrNull(startValue) || 
            GenericValidator.isBlankOrNull(endValue)) {
            return true;
        }
        
        DateValidator dateValidator = DateValidator.getInstance();
        Date startDate = dateValidator.validate(startValue, datePattern);
        Date endDate = dateValidator.validate(endValue, datePattern);
        
        if (startDate == null || endDate == null) {
            return false;
        }
        
        return startDate.before(endDate) || startDate.equals(endDate);
    }
}

Spring Framework統合

import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import org.apache.commons.validator.ValidatorResources;
import org.apache.commons.validator.Validator as CommonsValidator;

@Component
public class CommonsValidatorAdapter implements Validator {
    
    private ValidatorResources validatorResources;
    
    @Autowired
    public CommonsValidatorAdapter(ValidatorResources validatorResources) {
        this.validatorResources = validatorResources;
    }
    
    @Override
    public boolean supports(Class<?> clazz) {
        return UserBean.class.isAssignableFrom(clazz);
    }
    
    @Override
    public void validate(Object target, Errors errors) {
        try {
            CommonsValidator validator = new CommonsValidator(
                validatorResources, "userForm");
            validator.setParameter(CommonsValidator.BEAN_PARAM, target);
            
            ValidatorResults results = validator.validate();
            
            // エラーをSpringのErrorsオブジェクトに変換
            if (results != null) {
                for (Object propertyName : results.getPropertyNames()) {
                    String property = (String) propertyName;
                    ValidatorResult result = results.getValidatorResult(property);
                    
                    for (Object actionName : result.getActions()) {
                        String action = (String) actionName;
                        if (!result.isValid(action)) {
                            errors.rejectValue(property, 
                                "error." + property + "." + action,
                                "Validation failed for " + property);
                        }
                    }
                }
            }
        } catch (ValidatorException e) {
            errors.reject("validation.error", "Validation error occurred");
        }
    }
}

// Controllerでの使用
@Controller
public class UserController {
    
    @Autowired
    private CommonsValidatorAdapter validator;
    
    @PostMapping("/users")
    public String createUser(@ModelAttribute UserBean user, 
                           BindingResult result) {
        validator.validate(user, result);
        
        if (result.hasErrors()) {
            return "userForm";
        }
        
        // ユーザー作成処理
        return "redirect:/users";
    }
}

高度なバリデーション設定

<!-- validation-rules.xml - カスタムバリデータの定義 -->
<form-validation>
    <global>
        <!-- カスタムバリデータの登録 -->
        <validator name="japanesePhone"
            classname="com.example.validators.CustomValidators"
            method="validateJapanesePhoneNumber"
            methodParams="java.lang.Object,
                         org.apache.commons.validator.Field,
                         org.apache.commons.validator.ValidatorAction"
            msg="errors.japanesePhone"/>
            
        <validator name="passwordStrength"
            classname="com.example.validators.CustomValidators"
            method="validatePasswordStrength"
            methodParams="java.lang.Object,
                         org.apache.commons.validator.Field,
                         org.apache.commons.validator.ValidatorAction"
            msg="errors.weakPassword"/>
            
        <validator name="dateRange"
            classname="com.example.validators.CustomValidators"
            method="validateDateRange"
            methodParams="java.lang.Object,
                         org.apache.commons.validator.Field,
                         org.apache.commons.validator.ValidatorAction"
            msg="errors.invalidDateRange"/>
    </global>
    
    <formset>
        <!-- 複雑なフォームのバリデーション設定 -->
        <form name="registrationForm">
            <field property="username" depends="required,minlength,maxlength">
                <arg position="0" key="label.username"/>
                <arg position="1" key="${var:minlength}"/>
                <arg position="2" key="${var:maxlength}"/>
                <var>
                    <var-name>minlength</var-name>
                    <var-value>3</var-value>
                </var>
                <var>
                    <var-name>maxlength</var-name>
                    <var-value>20</var-value>
                </var>
            </field>
            
            <field property="password" depends="required,passwordStrength">
                <arg key="label.password"/>
            </field>
            
            <field property="phoneNumber" depends="required,japanesePhone">
                <arg key="label.phoneNumber"/>
            </field>
            
            <field property="dateRange" depends="dateRange">
                <arg key="label.dateRange"/>
                <var>
                    <var-name>startDateProperty</var-name>
                    <var-value>startDate</var-value>
                </var>
                <var>
                    <var-name>endDateProperty</var-name>
                    <var-value>endDate</var-value>
                </var>
                <var>
                    <var-name>datePattern</var-name>
                    <var-value>yyyy/MM/dd</var-value>
                </var>
            </field>
        </form>
    </formset>
</form-validation>