JAX-RSをやってみる (14) - Bean Validation -

2014-07-07T00:00:00+00:00 Java JAX-RS

公式リファレンス: https://jersey.java.net/documentation/latest/user-guide.html#bean-validation

単純にアノテーションベースで入力検証のルールを定義出来る。別に説明しなくても良いんじゃねって事で

SampleBean.java

package sample.bean;

import java.io.Serializable;
import javax.validation.constraints.NotNull;
import javax.ws.rs.QueryParam;
import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement
public class SampleBean implements Serializable {

    private static final long serialVersionUID = 1L;

    @QueryParam("name")
    @NotNull
    private String name;

    public SampleBean() {
    }

    public SampleBean(String name) {
        setName(name);
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

んな感じでアノテーションベースで@NotNull等の検証ルール定義を付与する

Home.java

package sample.controller;

import javax.validation.Valid;
import javax.ws.rs.BeanParam;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

import sample.bean.SampleBean;

@Path("/sample")
public class Home {

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public SampleBean index(@Valid @BeanParam SampleBean bean) {
        return bean;
    }
}

な感じで@Validで指定している型のバリデーションを行い正常で無い場合にはValidationException(正確にはConstraiuntViolationException?)がスローされる(但し、ValidationExceptionMapperがあるのでレスポンスは変換される)

ValidationMessages.properties

Spring MVCと同様にバリデーションを行った際のエラーメッセージはカスタム化出来る

javax.validation.constraints.NotNull.message=may not be null

HomeTest.java

package sample.controller;

import javax.ws.rs.core.Application;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

import org.glassfish.jersey.test.JerseyTest;
import org.glassfish.jersey.test.TestProperties;
import org.junit.Test;

import sample.SampleApplication;
import static org.junit.Assert.*;
import static org.hamcrest.Matchers.*;

public class HomeTest extends JerseyTest {

    @Override
    protected Application configure() {
        enable(TestProperties.LOG_TRAFFIC);
        enable(TestProperties.DUMP_ENTITY);

        return new SampleApplication();
    }

    @Test
    public void test_index() {
        Response response = target("/sample").request(MediaType.APPLICATION_JSON).get();

        // Validationにfailすると400になる
        assertThat(response.getStatus(), is(200));
    }
}

上記の場合データ(QueryParam)が指定されてないのでテストはコケる。そしてエラーメッセージがJSONで取得できる(正確には要求したMediaTypeにより変わる)

[
  {
    "message": "may not be null",
    "messageTemplate": "{javax.validation.constraints.NotNull.message}",
    "path": "Home.index.arg0.name"
  }
]

検証アノテーションを自作する場合

@ConstraintでvalidatedByに指定したConstraintValidatorクラスにてバリデーション実装を定義すればいい

Hoge.java

package sample.validator;

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

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

import static java.lang.annotation.ElementType.*;

@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = HogeValidator.class)
public @interface Hoge {

    // ValidationMessagesでsample.validator.hoge.messageでエラーメッセージを設定出来るように
    String message() default "{sample.validator.hoge.message}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    /* 公式的には載ってないけどBean Validationのリポジトリとかにはこれを持つべきっぽい(現時点で理由は不明)

    @Retention(RetentionPolicy.RUNTIME)
    @interface List {
        Hoge[] value();
    }
    */
}

ってな感じでアノテーションを作る。messageを{}で囲う事でMessageResource周りでエラーメッセージ等を定義出来たりすると思われる

HogeValidator.java

package sample.validator;

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

// StringじゃなくてTにして型検証を含めるべきなのか
public class HogeValidator implements ConstraintValidator<Hoge, String> {

    @Override
    public void initialize(Hoge constraintAnnotation) {
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (value != null && "hoge".equals(value)) {
            return true;
        }

        /* エラーメッセージのConstraintViolationを自処理で構築する場合?
        context.disableDefaultConstraintViolation();
        context.buildConstraintViolationWithTemplate("err").addConstraintViolation();
        */

        return false;
    }
}

isValidメソッドで検証を実装するだけ。あとは普通に

package sample.controller;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;

import sample.validator.Hoge;

@Path("/sample")
public class Home {

    @QueryParam("name")
    @Hoge
    private String name;

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public String index() {
        return name;
    }
}

な感じでアノテーションを付与して使うだけ

余談1: エラー情報を取得する必要が無いような場合

package sample;

import javax.ws.rs.ApplicationPath;

import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.ServerProperties;

@ApplicationPath("/")
public class SampleApplication extends ResourceConfig {

    public SampleApplication() {
        // falseにするとエラー情報が取得出来ない
        property(ServerProperties.BV_SEND_ERROR_IN_RESPONSE, false);
        packages("sample");
    }
}

余談2: バリデーションを自前で動かす方法

package sample.controller;

import java.util.Set;
import javax.inject.Inject;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.Validator;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;

import sample.bean.SampleBean;

@Path("/sample")
public class Home {

    @Inject
    private Validator validator;

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public SampleBean index(@QueryParam("name") String name) {

        SampleBean bean = new SampleBean(name);
        Set<ConstraintViolation<SampleBean>> results = validator.validate(bean);

        if (!results.isEmpty()) {
            throw new ConstraintViolationException(results);
        }

        return bean;
    }
}

てな感じで@Injectでjavax.validation.Validatorを使えば出来るっぽい。で上記でも書いたようにValidation ErrorなレスポンスはValidationExceptionMapperによってConstraintViolationExceptionが処理される?はずなのでそれをスローする形で良い。でその際の引数にvalidateをした結果のSet<ConstraintViolation<T>>を指定してやれば良い模様

とりあえずはこんな感じで。まだまだ曖昧な所あるので追記するか新しくネタ書くかも

スクロールによるページング処理に関して#2 angular.jsをやってみる (26) - ngMessages -