Glassfish4.0.1でJersey MVCのvalidationを試してみた

動機

このあたり便利そうなので試してみました
https://jersey.java.net/documentation/2.5.1/mvc.html#d0e12836

最終的にはバリデーションエラーの際に戻った画面上で入力フィールドに値を残しておきつつ、エラーのあったフィールドにエラーメッセージを出したい、という目標があります。

ただ、JAX-RSの目的からしてそういうのはあまり考えられてなさそうで、サーバ側ではRESTのインタフェースを用意しておいて、クライアントはAjaxでリクエストして結果が返ってきたらその情報をもとに適切な処理(成功の場合は画面遷移や部分更新、失敗したらエラーの表示とか)を行うというのが使いやすい方法なのかなと思ってます。

そんな中、Jersey MVCでは2.3から@ErrorTemplateアノテーションが追加されてバリデーションエラー時(実際は「any exception thrown after the resource matching phase」)に表示するテンプレートファイルを指定できるようになってました。
今回はこれを試してみました。

設定

Glassfish4.0.1 build4を使用します。
参考

pom.xmlに依存関係を追加します。
今回はテンプレートエンジンとしてjspではなくfreemarkerを使いました。

        <dependency>
            <groupId>org.glassfish.jersey.ext</groupId>
            <artifactId>jersey-mvc</artifactId>
            <version>2.5.1</version>
            <scope>provided</scope>
            <exclusions>
                <exclusion>
                    <groupId>javax.servlet</groupId>
                    <artifactId>servlet-api</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.glassfish.jersey.ext</groupId>
            <artifactId>jersey-mvc-freemarker</artifactId>
            <version>2.5.1</version>
        </dependency>
        <dependency>
            <groupId>org.glassfish.jersey.ext</groupId>
            <artifactId>jersey-mvc-bean-validation</artifactId>
            <version>2.5.1</version>
        </dependency>

リソースの作成

@Path("mvc")
public class MvcAction {
    private static final String TEMPLATE_NAME = "/demo/mvc.ftl";

    @GET
    @Template(name = TEMPLATE_NAME)
    public String newPage() {
        return "new";
    }

    @POST
    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
    @Template(name = TEMPLATE_NAME)
    @ErrorTemplate(name = TEMPLATE_NAME)
    @Valid
    public String create(@NotEmpty @FormParam("code") String code, @NotEmpty @FormParam("name") String name) {
        return "create";
    }
}

ApplicationPathを設定します

@ApplicationPath("demo")
public class DemoConfig extends ResourceConfig {

    public DemoConfig() {
        register(FreemarkerMvcFeature.class);
        property(FreemarkerMvcFeature.TEMPLATES_BASE_PATH, "/freemarker");
        register(MvcBeanValidationFeature.class);

        packages(MvcAction.class.getPackage().getName());
    }
}

テンプレートファイルを作成します(/src/main/webapp/freemarker/demo/mvc.ftl

<!DOCTYPE html>
<html>
<body>
    <#if model?? && model?is_sequence>
        <#list model as error>
            <div>${error_index}:${error.message} ${error.invalidValue} ${error.path}</div>
        </#list>
    <#elseif model?? && model?is_string>
        <span>${model!''}</span>
    </#if>
    <form method="post" action="/dbdos/demo/mvc">
        <input type="text" name="code"/>
        <input type="text" name="name"/>
        <input type="submit" value="create">
    </form>
</body>
</html>

これでhttp://localhost:8080/コンテキストパス/demo/mvcに接続すると文字列「new」とフォームが表示され、値を入力してボタンを押すと「create」と表示されるはずです。また、値をからのままボタンを押すとエラーが表示されるはずです。

実行

試してみます。
フォームの表示と、値を入れたときの動きはOKでした。
値を入れずにボタンを押すとエラーになりました。

org.jboss.weld.exceptions.UnsatisfiedResolutionException: WELD-001308 Unable to resolve any beans for Types: [interface org.glassfish.jersey.server.ExtendedUriInfo]; Bindings: [QualifierInstance{annotationClass=interface javax.enterprise.inject.Default, values={}, hashCode=-1405755808}]
     at org.jboss.weld.manager.BeanManagerImpl.getBean(BeanManagerImpl.java:820)
     at org.jboss.weld.bean.builtin.InstanceImpl.get(InstanceImpl.java:78)
     at org.glassfish.jersey.server.mvc.spi.AbstractErrorTemplateMapper.getErrorTemplate(AbstractErrorTemplateMapper.java:87)
     at org.glassfish.jersey.server.mvc.spi.AbstractErrorTemplateMapper.isMappable(AbstractErrorTemplateMapper.java:78)

試しに同じ様なコードでTomcat7でも試してみましたがうまくいったようなのでGlassfish出の問題のようです。
このあたりのCDIの挙動についてはよく分からないのですが、ここで使っているExceptionMapper(ValidationErrorTemplateExceptionMapperのsuperクラス)のuriInfoProviderフィールドへのインジェクションがうまくいってないようです。
エラー時はorg.jboss.weeld.bean.builtin.InstanceImpl
正常時はorg.jvnet.hk2.internal.IterableProviderImpl
がインジェクションされているところまでは分かりました。
それ以上はちょっと分かりませんでした。

解決策

正しい方法か分かりませんが、下記のようにして動くようにはなりました。

まず、ExceptionMapperをValidationErrorTemplateExceptionMapperを参考に作ります。
変えているのは@Singletonアノテーションを削除して@Providerと@Priorityアノテーションを追加している点だけです

@Provider
@Priority(Priorities.USER)
public class ConstraintViolationExceptionMapper extends AbstractErrorTemplateMapper<ConstraintViolationException> {

    @Context HttpServletRequest request;

    @Override
    protected Response.Status getErrorStatus(final ConstraintViolationException cve) {
        return ValidationHelper.getResponseStatus(cve);
    }

    @Override
    protected Object getErrorModel(final ConstraintViolationException cve) {
        return ValidationHelper.constraintViolationToValidationErrors(cve);
    }
}

DemoConfigを書き換えます

    public DemoConfig() {
        register(FreemarkerMvcFeature.class);
        property(FreemarkerMvcFeature.TEMPLATES_BASE_PATH, "/freemarker");
        register(ValidationFeature.class);
        register(ConstraintViolationExceptionMapper.class);

        packages(MvcAction.class.getPackage().getName());
    }

これで、画面に次のように表示されるようになりました。

0:may not be empty MvcAction.create.arg1
1:may not be empty MvcAction.create.arg0

なお、この変更により
jersey-mvc-bean-validationは必要なくてjersey-bean-validationで十分です。
pom.xmlを書き換えます(変えなくても支障ないはず)。

ただ、これだとarg0とかarg1くらいしか識別する手立てがないのでこのままだと適切にエラー表示させるのは難しそう。