Spring MVCでグループヴァリデーションしたい時

JSR-303のjavax.validation.Validatorヴァリデーションのグループ化に対応していて、例えば同じモデルに対して新規作成時と更新時に違う検証を行う事が可能ですが(`・ω・´)
しかし、javax.validation.Validアノテーションにはグループ化の指定方法がないという…。
なので、Spring MVCを使ってControllerメソッドの引数に対してヴァリデーションを行うときに、グループの指定が出来ないという結果に(´・ω・`)


本家ではjavax.validation.Validとは別のアノテーションを用意してこの問題に対応するっぽいですが、とりあえずそれまでの対応方法について(・ω・)

基本方針

javax.validation.Validとは別にグループの指定ができるアノテーションを用意します。
次に、Spring MVCで使用する際のMethodInterceptorとヴァリデーション用のコンポーネントを用意します。

アノテーション

こんな感じでjavax.validation.Validにグループを追加してものを用意します。

@Target({ METHOD, FIELD, CONSTRUCTOR, PARAMETER })
@Retention(RUNTIME)
public @interface ValidGroups {

    Class<?>[] value();
}

ヴァリデーション用コンポーネント

javax.validation.Validatorを使ってヴァリデーションを行い、org.springframework.validation.Errorsを設定するコンポーネントとして以下のようなものを用意します。

import java.util.Set;

import javax.validation.ConstraintViolation;
import javax.validation.Path;
import javax.validation.Validator;
import javax.validation.Path.Node;
import javax.validation.groups.Default;

import org.springframework.validation.Errors;

public class GroupValidator {

    private static final Class<?>[] DEFAULT_CLASSES = new Class<?>[] {Default.class};

    private Validator validator;

    public void setValidator(final Validator validator) {
        this.validator = validator;
    }

    public boolean isValid(final Errors result, final Object object, final Class<?>... classes) {
        Set<ConstraintViolation<Object>> violations = validator.validate(object, isDefaultValidation(classes) ? DEFAULT_CLASSES : classes);
        for (ConstraintViolation<Object> v : violations) {
            Path path = v.getPropertyPath();
            String propertyName = "";
            if (path != null) {
                StringBuilder buffer = new StringBuilder();
                for (Node n : path) {
                    buffer.append(n.getName());
                    buffer.append(".");
                }
                propertyName = buffer.toString();
                propertyName = propertyName.substring(0, propertyName.length() - 1);
            }
            String constraintName = v.getConstraintDescriptor().getAnnotation().annotationType().getSimpleName();
            if (propertyName == null || "".equals(propertyName)) {
                result.reject(constraintName, v.getMessage());
            } else {
                result.rejectValue(propertyName, constraintName, v.getMessage());
            }
        }
        return violations.isEmpty();
    }

    private boolean isDefaultValidation(final Class<?>[] classes) {
        if ((classes == null) || (classes.length == 0) || (classes[0] == null)) {
            return true;
        }
        return false;
    }
}

ヴァリデーション用MethodInterceptor

Controllerのメソッドでjavax.validation.Validによるヴァリデーションと同じように書けるように、以下の様なMethodInterceptorを用意。

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.validation.Errors;

public class ValidGroupsInterceptor implements MethodInterceptor {

    private GroupValidator groupValidator;

    public void setGroupValidator(final GroupValidator groupValidator) {
        this.groupValidator = groupValidator;
    }

    @Override
    public Object invoke(final MethodInvocation invocation) throws Throwable {

        Method method = invocation.getMethod();
        Object[] arguments = invocation.getArguments();
        Class<?>[] parameterTypes = method.getParameterTypes();
        Annotation[][] annotations = method.getParameterAnnotations();

        for (int i = 0; i < arguments.length; i++) {
            if (annotations[i] == null) {
                continue;
            }
            for (Annotation annotation : annotations[i]) {
                if (!(annotation instanceof ValidGroups)) {
                    continue;
                }
                if ((i < arguments.length - 1) && (Errors.class.isAssignableFrom(parameterTypes[i + 1]))) {
                    groupValidator.isValid((Errors)arguments[i + 1], arguments[i], ((ValidGroups)annotation).value());
                }
            }
        }

        return invocation.proceed();
    }
}

設定

っで、Springの設定としてはこんな感じで、Controllerのメソッドに対してValidGroupsInterceptorを適用するように設定。

<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">
</bean>

<bean id="groupValidator" class="smart.extension.spring.validator.GroupValidator">
    <property name="validator" ref="validator" />
</bean>

<bean id="validGroupsInterceptor" class="smart.extension.spring.validator.ValidGroupsInterceptor">
    <property name="groupValidator" ref="groupValidator" />
</bean>

<aop:config proxy-target-class="true">
    <aop:advisor pointcut="execution(* sample..*Controller.*(..))" advice-ref="validGroupsInterceptor" />
</aop:config>

サンプル

使用例としては以下の様なモデルとController、jspになります。

public class MultiViewModel implements Serializable {

    private static final long serialVersionUID = 1L;

    public interface Rule1 {}

    public interface Rule2 {}

    @NotEmpty(groups = {Rule1.class})
    private String data1;

    @NotEmpty(groups = {Rule2.class})
    private String data2;

    public String getData1() {
        return data1;
    }

    public void setData1(final String data1) {
        this.data1 = data1;
    }

    public String getData2() {
        return data2;
    }

    public void setData2(final String data2) {
        this.data2 = data2;
    }
}
@Controller
@RequestMapping(value = "/sample/basic/multi")
public class MultiController extends SampleControllerBase {

    @RequestMapping(method = RequestMethod.GET)
    public String index(final ModelMap map) {
        map.put("model", new MultiViewModel());
        return "sample/basic/multi/index";
    }

    @RequestMapping(method = RequestMethod.POST, params = "rule1")
    public String rule1(@ModelAttribute("model") @ValidGroups(MultiViewModel.Rule1.class) final MultiViewModel model,
            final BindingResult binding, final ModelMap map) {
        return "sample/basic/multi/index";
    }

    @RequestMapping(method = RequestMethod.POST, params = "rule2")
    public String rule2(@ModelAttribute("model") @ValidGroups(MultiViewModel.Rule2.class) final MultiViewModel model,
            final BindingResult binding, final ModelMap map) {
        return "sample/basic/multi/index";
    }
}
<form:form method="post" modelAttribute="model">

<table>

<tr>
<th>data1</th>
<td>
<form:input path="data1"/>
<form:errors path="data1" cssClass="error" />
</td>
</tr>

<tr>
<th>data2</th>
<td>
<form:input path="data2"/>
<form:errors path="data2" cssClass="error" />
</td>
</tr>

</table>

<input type="submit" name="rule1" value="ルール1で更新"/>
<input type="submit" name="rule2" value="ルール2で更新"/>

</form:form>

これで、画面の[ルール1で更新]ボタンを押下するとdata1に対するチェックのみが、[ルール2で更新]ボタンを押下するとdata2に対するチェックのみが実行されることを確認できます(・∀・)


…などと、書きかけのネタを発掘したのでリサイクル投稿(・ω・;)
Spring MVCも、3.1になると色々改善されるっぽいんだけどね〜。