ryhmrt’s blog

意識低い系プログラマの雑記

Spring MVC で独自バリデーション

組込のバリデーション

Spring MVC では JSR-303 のバリデーションが使える。

@NotNull
@Size(min=1, max=50)
@Pattern(regexp = "[a-zA-Z-_.]+@[a-zA-Z-\\.]+")

とかのバリデーションをBeanのフィールドに宣言して、アクションのメソッド

public String save(Model model, @Valid @ModelAttribute("form") HogeForm form, BindingResult bindingResult) {
    if (bindingResult.hasErrors()) {
        model.addAttribute("form", form);
        return "hoge/edit";
    }

こんな感じにエラー分岐すれば、form:errorsタグでエラーが表示できる。

<form:errors path="email" cssClass="text-error" />

独自のバリデーション

標準のバリデーションで足りない分をどうすすか。

最初は org.springframework.validation.Validator を実装して、アクションの頭で

ValidationUtils.invokeValidator(hogeValidator, form, bindingResult);

とかやってみたけど、これはきれいじゃない。

調べたら、独自のアノテーションを定義できるらしい。

以下のように ConstraintValidator と、ペアになるアノテーションをセットで定義してやればOK

/**
 * JSR-303 Validation Annotation for List empty check
 */
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy=ListNotEmpty.ListNotEmptyValidator.class)
public @interface ListNotEmpty {
    public String message() default "{ListNotEmpty}";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};

    /**
     * Validator
     */
    public static class ListNotEmptyValidator implements ConstraintValidator<ListNotEmpty, List<?>>  {

        @Override
        public void initialize(ListNotEmpty annotation) {
        }

        @Override
        public boolean isValid(List<?> target, ConstraintValidatorContext context) {
            if (target == null || target.size() == 0) {
                return false;
            }
            return true;
        }

    }
}

複数フィールドをチェックする必要がある場合はクラスをバリデーションの対象にすればOK

/**
 * JSR-303 Validation Annotation for Password confirmation.
 * Target bean should implement PasswordConfirmation.PasswordConfirmable.
 * (No need to put PasswordConfirmation annotation)
 */
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy=PasswordConfirmation.PasswordConfirmationValidator.class)
public @interface PasswordConfirmation {
    public String message() default "{PasswordConfirmation}";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};

    /**
     * Interface for password confirmation
     */
    @PasswordConfirmation
    public interface PasswordConfirmable {
        public String getPassword();
        public String getPasswordConfirmation();
    }

    /**
     * Validator
     */
    public class PasswordConfirmationValidator implements ConstraintValidator<PasswordConfirmation, PasswordConfirmable> {

        @Override
        public void initialize(PasswordConfirmation annotation) {
        }

        @Override
        public boolean isValid(PasswordConfirmable bean, ConstraintValidatorContext context) {
            if (bean.getPassword() != null && !bean.getPassword().equals(bean.getPasswordConfirmation())) {
                context.disableDefaultConstraintViolation();
                context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate())
                    .addNode("passwordConfirmation")
                    .addConstraintViolation();
                return false;
            }
            return true;
        }

    }
}