JAX-RSをやってみる (12) - MVC -

2014-06-25T00:00:00+00:00 Java JAX-RS

公式ドキュメント: https://jersey.java.net/documentation/latest/user-guide.html#mvc

参考: http://qiita.com/opengl-8080/items/f4c25ad671e8a6dac743

jersey-mvcなAPIを使えばViewableをぶん投げたりする事によりテンプレートエンジンによりレスポンスを出したりする事も出来る。※現時点で公式でサポートされているテンプレートエンジンがfreemarkerとmustacheとjsp

っていう事でやってみる

その前に

jersey-mvc-jspなJSPをレンダリングする仕組みを使う場合にテスト書くとエラーを引き起こす。※freemarkerとmustacheでは起きない(HttpServletRequest等をマッピングしない為かと)

っていう事でthymeleafを使ってテンプレートエンジンによりレスポンスを送信する仕組みのTemplateProcessorを作りながらやる

依存性

apply plugin: "java"
apply plugin: "jetty"
apply plugin: "eclipse"

repositories {
    mavenCentral()
}

dependencies {
    compile "org.slf4j:slf4j-simple:1.7.7"
    compile "org.thymeleaf:thymeleaf:+"
    providedCompile "org.glassfish.jersey.containers:jersey-container-servlet:2.9.+"
    providedCompile "org.glassfish.jersey.ext:jersey-mvc:2.9.+"

    // 他のを使う場合には以下を使えば良い
    //compile "org.glassfish.jersey.ext:jersey-mvc-freemarker:2.9.+"
    //compile "org.glassfish.jersey.ext:jersey-mvc-mustache:2.9.+"
    //compile "org.glassfish.jersey.ext:jersey-mvc-jsp:2.9.+"

    testCompile "org.glassfish.jersey.test-framework.providers:jersey-test-framework-provider-bundle:2.9.+"
    // テスト時にMock系を使う為
    testCompile "org.springframework:spring-test:+"
    testCompile "org.hamcrest:hamcrest-all:+"
}

んでまずThymeleafなテンプレートエンジンを使ってレスポンスを処理するのを作る

ThymeleafFeature.java

package sample;

import javax.ws.rs.ConstrainedTo;
import javax.ws.rs.RuntimeType;
import javax.ws.rs.core.Configuration;
import javax.ws.rs.core.Feature;
import javax.ws.rs.core.FeatureContext;

import org.glassfish.jersey.server.mvc.MvcFeature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@ConstrainedTo(RuntimeType.SERVER)
public class ThymeleafFeature implements Feature {
    @Override
    public boolean configure(FeatureContext context) {
        final Configuration config = context.getConfiguration();

        if (!config.isRegistered(ThymeleafTemplateProcessor.class)) {
            context.register(ThymeleafTemplateProcessor.class);

            if (!config.isRegistered(MvcFeature.class)) {
                // これしないとViewableMessageBodyWriter周りが作用しないので
                context.register(MvcFeature.class);
            }
        }

        return true;
    }
}

ThymeleafTemplateProcessor.java

package sample;

import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.util.HashMap;
import java.util.Map;

import javax.inject.Inject;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.core.Configuration;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;

import org.glassfish.jersey.server.mvc.Viewable;
import org.glassfish.jersey.server.mvc.spi.AbstractTemplateProcessor;
import org.jvnet.hk2.annotations.Optional;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.WebContext;
import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver;

public class ThymeleafTemplateProcessor extends AbstractTemplateProcessor<String> {

    @Inject
    HttpServletRequest request;

    @Inject
    HttpServletResponse response;

    @Inject
    ServletContext servletContext;

    @Inject
    public ThymeleafTemplateProcessor(Configuration config, @Optional ServletContext servletContext) {
        super(config, servletContext, "thymeleaf", "html");
        this.servletContext = servletContext;
    }

    @SuppressWarnings("unchecked")
    @Override
    public void writeTo(String templateReference, Viewable viewable,
        MediaType mediaType, MultivaluedMap<tring, Object> httpHeaders,
        OutputStream out) throws IOException {


        try(Writer writer = new OutputStreamWriter(out)) {
            Object o = viewable.getModel();
            Map<String, Object> stash;

            if (o instanceof Map) {
                stash = (Map<String, Object>)o;
            } else {
                stash = new HashMap<String, Object>();

                // @ErrorTemplate時はパラメーターがExceptionで返ってくる

                if (o instanceof Exception) {
                    stash.put("error", o);
                } else {
                    // 返ってきた値がMapでない場合にitなキーで参照出来るように
                    stash.put("it", o);
                }
            }

            WebContext context = new WebContext(request, response, servletContext);
            context.setVariables(stash);

            TemplateEngine engine = new TemplateEngine();
            engine.setTemplateResolver(new ClassLoaderTemplateResolver());
            engine.process(templateReference, context, writer);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    protected String resolve(String templatePath, Reader reader) throws Exception {
        return templatePath;
    }
}

@Injectで注入するのが正しいかは微妙。mvc-jspな奴はProvider<Ref<HttpServletRequest>>的な感じなのを注入しているが、これだとどうもテスト時の注入でAbstractBinder使っても上手く作用してくれないので

んまぁてな感じでThymeleafを使ってテンプレートエンジンによるレスポンスの処理なところはこれだけ。設定しないと使えませんが

web.xml

MVCによる処理を利用する場合においてはweb.xmlでfilter周りを設定しないといけないみたいな制約がある模様なので

<?xml version="1.0" encoding="UTF-8"?>
<web-app
    xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
    version="3.0">

    <filter>
        <filter-name>jersey</filter-name>
        <filter-class>org.glassfish.jersey.servlet.ServletContainer</filter-class>

        <init-param>
            <param-name>javax.ws.rs.Application</param-name>
            <param-value>sample.SampleApplication</param-value>
        </init-param>
    </filter>

    <filter-mapping>
        <filter-name>jersey</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
</web-app>

な感じで

beans.xml

glassfishとかで動かす場合に

<?xml version="1.0" ?>
<beans
    xmlns="http://xmlns.jcp.org/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd"
    version="1.1"
    bean-discovery-mode="all">
</beans>

となっているとデプロイエラーを起こすので、bean-discovery-mode="annotated"にしておく

SampleApplication.java

package sample;

import javax.ws.rs.ApplicationPath;

import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.mvc.MvcFeature;

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

    public SampleApplication() {
        // 設定しておかないとエラーになる
        property(MvcFeature.TEMPLATE_BASE_PATH, "templates");

        // ThymeleafTemplateProcessorをregisterするFeatureをregister
        register(ThymeleafFeature.class);

        packages("sample");
    }
}

あとはリソースで使うだけ。

Home.java

package sample.controller;

import java.util.HashMap;
import java.util.Map;

import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;

import org.glassfish.jersey.server.mvc.ErrorTemplate;
import org.glassfish.jersey.server.mvc.Template;
import org.glassfish.jersey.server.mvc.Viewable;

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

    public String name;

    /* Template
    <!DOCTYPE html>
    <html xmlns:th="http://www.thymeleaf.org">
      <body>
        <p th:text="${it.name}" />
      </body>
    </html>
    */
    @Path("test1")
    @GET
    public Viewable test1(@QueryParam("name") @DefaultValue("hoge") String name) {
        this.name = name;
        return new Viewable("/test1.html", this);
    }

    /* Template
    <!DOCTYPE html>
    <html xmlns:th="http://www.thymeleaf.org">
      <body>
        <p th:text="${it}" />
      </body>
    </html>
    */
    @Path("test2")
    @GET
    @Template(name = "/test2.html")
    public String test2() {
        // Stringで返しているのでitとして参照出来るようになる
        return "test2";
    }

    /* Template
    <!DOCTYPE html>
    <html xmlns:th="http://www.thymeleaf.org">
      <body>
        <p th:text="${message}" />
      </body>
    </html>
    */
    @Path("test3")
    @GET
    @Template(name = "/test3.html")
    public Map<String, Object> test3() {
        // Map型で返しているのでその内部のデータがそのままマッピングされる。
        Map<String, Object> data = new HashMap<String, Object>(1);
        data.put("message", "hello world");

        return data;
    }

    /* Template
    <!DOCTYPE html>
    <html xmlns:th="http://www.thymeleaf.org">
      <body>
        <span th:text="|error: ${error.message}|" />
      </body>
    </html>
    */
    @Path("test4")
    @GET
    @ErrorTemplate(name = "/error.html")
    public void error() throws Exception {
        throw new Exception("hoge");
    }
}

んまぁという感じで大体どういうレスポンスになるかは想像出来るのはないかと

HomeTest.java

テストする際にHttpServletRequest等の@Injectは行われないのでエラーになる。なのでSpring Framework Testingに入ってるMockHttpServletRequest等をAbstractBinderを使って差し込む的な事をする事で対処出来る模様

package sample.controller;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.core.Application;

import org.glassfish.hk2.utilities.binding.AbstractBinder;
import org.glassfish.jersey.test.JerseyTest;
import org.junit.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.mock.web.MockServletContext;

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

public class HomeTest extends JerseyTest {

    MockHttpServletRequest  request  = new MockHttpServletRequest();
    MockHttpServletResponse response = new MockHttpServletResponse();

    @Override
    protected Application configure() {
        return new SampleApplication()
            .register(new AbstractBinder() {
                @Override
                protected void configure() {
                    bind(request).to(HttpServletRequest.class);
                    bind(response).to(HttpServletResponse.class);
                    bind(new MockServletContext()).to(ServletContext.class);
                }
            });
    }

    @Test
    public void test1() {
        String response = target("/sample/test1").request().get(String.class);
        assertThat(response, containsString("hoge"));
    }

    @Test
    public void test2() {
        String response = target("/sample/test2").request().get(String.class);
        assertThat(response, containsString("test2"));
    }

    @Test
    public void test3() {
        String response = target("/sample/test3").request().get(String.class);
        assertThat(response, containsString("hello world"));
    }

    @Test
    public void test4() {
        String response = target("/sample/test4").request().get(String.class);
        assertThat(response, containsString("error: hoge"));
    }
}

終わり。まぁテスト出来ないとか難がちょいとあったけど、リソースメソッドでViewableを返す事でテンプレートエンジンを利用してレスポンスを出したり出来る仕組みを利用するのであればこれ使えば良い

あとテスト動いてもglassfishとかじゃ動かないとかあるあるなのでちゃんと確認した方が良い。ちなみにそういうテストをサポートするTestContainerFactoryがあるっぽいので

JAX-RSをやってみる (13) - Refの依存性注入に関して - JAX-RSをやってみる (11) - Server Sent Events -