【Springフレームワーク】独自バリデータを使った相関チェックを実装してみよう!

前回のブログでアノテーションによるバリデーションの実装方法を見てきましたが、今回は複数の項目間でチェックを行う相関チェックについて見ていきたいと思います。

相関チェックを行う制約アノテーションは Bean Validator に用意されていないため、独自のバリデータを作成して相関チェックを行うアノテーションを実装します。

独自のバリデータと言っても、バリデータ用インターフェースとバリデータ実装用クラスを作成するだけです。

アノテーションを使ったバリデーションの実装方法については下記で詳しく書いているのでよろしければ参考にして下さい。

実装手順

独自バリデータを作成して相関チェックを行う手順は下記の通りになります。

  1. バリデータ用インターフェースの作成
  2. バリデータ実装用クラスの作成
  3. 相関チェック用アノテーションをチェッククラスに追加
1.と2.を実装することで相関チェック用のアノテーションが作成されるので、1.と2.で作成したアノテーションをチェック用クラスに追加すれば実装完了です。

バリデータ用インターフェースの作成

バリデータ用インターフェースを作成します。

  1. @ConstraintのvalidatedBy属性にバリデータ実装用クラスを設定します。
  2. チェック対象と比較先のプロパティ名を指定します。(property, comparingProperty)

インターフェース名がアノテーション名になります。

package com.itengpom.validation;

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

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

@Documented
@Constraint(validatedBy = EqualsPropertyValuesValidator.class)
@Target({ ElementType.TYPE, ElementType.ANNOTATION_TYPE} )
@Retention(RetentionPolicy.RUNTIME)
public @interface EqualsPropertyValues {

    String message() default "com.itengpom.validation.EqualsPropertyValues.message";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};

    String property();
    String comparingProperty();

    @Target({ ElementType.TYPE, ElementType.ANNOTATION_TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface List {
        EqualsPropertyValues[] value();
    }
}

バリデータ実装用クラスの作成

バリデータ実装用クラスを作成します。

  1. initializeメソッドに初期化処理を実装します。
  2. isValidメソッドに検証処理を実装します。
  3. 検証が成功した場合はtrue、検証が失敗した場合はfalseを返却します。
package com.itengpom.validation;

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

import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.util.ObjectUtils;

public class EqualsPropertyValuesValidator
        implements ConstraintValidator<EqualsPropertyValues, Object> {

    private String property;
    private String comparingProperty;
    private String message;

    @Override
    public void initialize(EqualsPropertyValues constraintAnnotation) {
        this.property = constraintAnnotation.property();
        this.comparingProperty = constraintAnnotation.comparingProperty();
        this.message = constraintAnnotation.message();
    }

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext context) {
        // 2つのプロパティ値を取得して比較
        BeanWrapper beanWrapper = new BeanWrapperImpl(value);
        Object propertyValue = beanWrapper.getPropertyValue(property);
        Object comparingPropertyValue = beanWrapper.getPropertyValue(comparingProperty);
        boolean matched = ObjectUtils.nullSafeEquals(propertyValue, comparingPropertyValue);

        if (matched) {
            return true;
        } else {
            context.disableDefaultConstraintViolation();
            context.buildConstraintViolationWithTemplate(message)
                    .addPropertyNode(property).addConstraintViolation();
            return false;
        }
    }
}

相関チェック用アノテーションをチェッククラスに追加

相関チェック用アノテーションをチェッククラスに追加します。

  1. 相関チェックはクラスレベルに制約アノテーションを指定します。
  2. チェック対象(property)と比較先(comparingProperty)のプロパティ名とエラーメッセージを指定します。
package com.itengpom.form;

import org.hibernate.validator.constraints.NotEmpty;

import com.itengpom.validation.EqualsPropertyValues;

@EqualsPropertyValues(property = "password", comparingProperty = "passwordconfirm", message = "新パスワードと新パスワード(確認)が一致しません。")
public class ManageForm {
    private String userid;

    @NotEmpty(message="古いパスワードを入力して下さい。")
    private String passwordold;

    @NotEmpty(message="新パスワードを入力して下さい。")
    private String password;

    @NotEmpty(message="新パスワード(確認)を入力して下さい。")
    private String passwordconfirm;

    //エラーメッセージ出力用
    private String message;

    //ゲッター、セッター
    public String getUserid() {
        return userid;
    }
    public void setUserid(String userid) {
        this.userid = userid;
    }
    public String getPasswordold() {
        return passwordold;
    }
    public void setPasswordold(String passwordold) {
        this.passwordold = passwordold;
    }
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
    public String getPasswordconfirm() {
        return passwordconfirm;
    }
    public void setPasswordconfirm(String passwordconfirm) {
        this.passwordconfirm = passwordconfirm;
    }
    public String getMessage() {
        return message;
    }
    public void setMessage(String message) {
        this.message = message;
    }
}

コントローラ

コントローラを実装します。

  1. 管理画面を表示するmanageFormメソッド
  2. パスワード変更を行うchangePasswordメソッド
package com.itengpom.controller;

import java.util.Optional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.ObjectUtils;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import com.itengpom.dao.UserInfoDao;
import com.itengpom.entity.UserInfo;
import com.itengpom.form.ManageForm;

@Controller
@RequestMapping("/manage")
public class ManageController {
    //DB操作を行うサービスを注入する。
    @Autowired
    private UserInfoDao service;

    @RequestMapping("")
    String manageForm(Model model) {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        UserDetails principal = (UserDetails) auth.getPrincipal();
        //System.out.println("  username : " + principal.getUsername());
        String userid = principal.getUsername();

        ManageForm form = new ManageForm();
        form.setUserid(userid);
        model.addAttribute("manageForm", form);

        return "/manageForm";
    }

    @PostMapping("/changePassword")
    public String changePassword(
            @ModelAttribute("manageForm") @Validated ManageForm form,
            BindingResult result) {
        if (result.hasErrors()) {
            //(1)バリデーションでエラーがあった場合

            return "/manageForm";
        } else {
            //(2)バリデーションでエラーがなかった場合

            UserInfo entity = Optional.ofNullable(service.findByUserId(form.getUserid()))
                    .orElseThrow(() -> new UsernameNotFoundException("ユーザが見つかりません")).get(0);

            if (ObjectUtils.nullSafeEquals(form.getPasswordold(), entity.getPassword())) {
                //古いパスワードと保存されたパスワードが一致

                //新しいパスワードをEntityに設定
                entity.setPassword(form.getPassword());

                if (service.update(entity)) {
                    //パスワードの更新に成功
                    return "redirect:/list";
                } else {
                    //パスワードの更新に失敗
                    form.setMessage("パスワードの更新に失敗しました。");
                    return "/manageForm";
                }
            } else {
                //古いパスワードと保存されたパスワードが一致しない

                form.setMessage("古いパスワードと保存されたパスワードが一致しません。");
                return "/manageForm";
            }
        }
    }
}

ビュー

パスワード変更を行う管理画面を作成します。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8" />
    <title>パスワード管理画面</title>
    <style>
    .err {
        color: red;
    }
    </style>
</head>
<body>
<h3>パスワード管理フォーム</h3>

<form th:action="@{/manage/changePassword}" method="POST" th:object="${manageForm}">
    <ul>
        <!-- 自分で設定したエラーメッセージを表示する場合 -->
        <li th:if="*{message}" class="err" th:text="*{message}" />
        <!-- @Validatedで設定したエラーメッセージを表示する場合 -->
        <li th:each="error:${#fields.detailedErrors()}"
            class="err" th:text="${error.message}" />
    </ul>

    <table>
        <tr>
            <td><label for="userid">ユーザID:</label></td>
            <td><input type="text" name="userid" th:field="*{userid}" readonly="readonly" /></td>
        </tr>
        <tr>
            <td><label for="passwordold">古いパスワード:</label></td>
            <td><input type="password" name="passwordold" th:field="*{passwordold}" /></td>
        </tr>
        <tr>
            <td><label for="password">新パスワード:</label></td>
            <td><input type="password" name="password" th:field="*{password}" /></td>
        </tr>
        <tr>
            <td><label for="passwordconfirm">新パスワード(確認):</label></td>
            <td><input type="password" name="passwordconfirm" th:field="*{passwordconfirm}" /></td>
        </tr>
        <tr>
            <td> </td>
            <td style="text-align:right;"><button type="submit">変更</button></td>
        </tr>
    </table>
</form>
</body>
</html>

実行結果

  • 新パスワードと新パスワード(確認)が一致しない場合

正しく相関チェックが行われています。

  • 古いパスワードと保存されたパスワードが一致しない場合

(注意)2つ目の実行結果は相関チェックとは関係ありません。

参考にした書籍

今回の相関チェックに関する内容は下記の書籍を参考にしました。


まとめ

独自バリデータを作成して相関チェックを行う方法を見てきました。

独自バリデータを作成する方法は型が決まっているので、あとは相関チェックするチェック内容によってisValidメソッドの中身を変えてあげればよさそうです。

最後までお読み頂きありがとうございました。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です