JerseyMVCとThymeleafを組み合わせる

【前提条件】

[環境]
[参考資料]

JerseyMVC Template
Jersey

[参考にさせていただいたサイト]

jersey-thymeleaf using ViewProcessor - Mitsuyuki.Shiiba

【概要】

Jersey MVC + Thymeleafでビューを表示させます。

JerseyMVCではMustache/Freemarker/JSPは専用のテンプレートエンジン用のクラスが
用意されていますがThymeleafはサポートしていないのです。

Thymeleafの場合は独自に作成する必要があります。
今回はJersey MVCでThmeleafで動作する設定を見ていきます。

【Gradleの設定】

今回はGradleを使っていますが、
サンプルでは依存関係のみの定義なのでmavenなどでも使えると思います。

apply plugin: 'war'

def defaultEncoding = 'UTF-8'
[compileJava, compileTestJava]*.options*.encoding = defaultEncoding

repositories {
    mavenCentral()
    maven {url 'https://oss.sonatype.org/content/repositories/snapshots/'}
}
dependencies {
    compile 'org.glassfish.jersey.core:jersey-server:2.12'
    compile 'org.glassfish.jersey.containers:jersey-container-servlet:2.12'

    compile 'org.glassfish.jersey.ext:jersey-mvc:2.12'

    compile 'org.thymeleaf:thymeleaf:2.1.3.RELEASE'
}

【TemplateProcessor】

TemplateProcessorはテンプレートエンジンと連携するためのインターフェイスです。
JerseyMVCのちょっと前のバージョンではViewProcessorだったもののようです。

import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.ext.Provider;
import org.glassfish.jersey.server.mvc.Viewable;
import org.glassfish.jersey.server.mvc.spi.TemplateProcessor;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.WebContext;
import org.thymeleaf.templateresolver.ServletContextTemplateResolver;
import org.thymeleaf.templateresolver.TemplateResolver;

@Provider
public class ThymeleafViewProcessor implements TemplateProcessor<String> {

    @Context
    private HttpServletRequest request;

    @Context
    private HttpServletResponse response;

    @Context
    private ServletContext servletContext;

    private final TemplateEngine templateEngine;

    public ThymeleafViewProcessor() {
        TemplateResolver resolver = new ServletContextTemplateResolver();
        resolver.setPrefix("/WEB-INF/view/");
        resolver.setSuffix(".html");
        resolver.setTemplateMode("HTML5");
        resolver.setCacheTTLMs(3600000L);

        templateEngine = new TemplateEngine();
        templateEngine.setTemplateResolver(resolver);
    }

    @Override
    public String resolve(String name, MediaType mediaType) {

        return name;
    }

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

        WebContext context = new WebContext(request, response, servletContext);

        context.setVariable("item", viewable.getModel());

        Writer writer = new OutputStreamWriter(out);
        templateEngine.process(templateReference, context, writer);

        writer.flush();
    }
}
[基本設定]

参考にさせていただいたサイトのコードのほぼ丸コピーですね・・・

まず、TemplateProcessorインターフェイスを実装して、
@Providerアノテーションをつけます。

コンストラクタ内でThymeleafのTmplateEnginieクラスの設定を行います。
今回は「/src/main/webapp/WEB-INF/view」にHTMLファイルを置くので、
prefixは「/WEB-INF/view」、sufixは「.html」としています。

[resolveメソッド]

resolveメソッドは参照させるファイル名を解決するためのメソッドです。

resolveメソッドは今回はパラメータの値をそのまま返します。
TemplateEngineでprefix/suffixを設定しているので、「"/WEB-INF/view" + namte + ".html"」になってくれます。

[writeToメソッド]

実際のレスポンスを出力する処理です。
templateReferenceにはresolveの戻り値が渡されるっぽいです。

writeToではWebContextにモデルオブジェクトの設定と、TemplateEngineの処理を実行させています。

WebContext#setVariableでHTML内でモデルオブジェクトを使えるようにしています。
今回は変数名を「item」として取り扱っています。

続いてTemplateEngineに渡すWriterを作成します。
Writerはパラメータで渡されたoutをラップして使います。

最後にTemplateEngine#processを実行します。

[つまづいたところとか]

最初は↓のように書いていたのですが・・・

    templateEngine.process(templateReference, context, response.getWriter());

JerseyMVC内部でもresponse#getWriterを呼び出していて、
動きはするけどログ上にエラーが出力される状態になってしまいました。

また、writer#flushを呼び出さずにいると
レスポンスがコミットされずにブラウザからアクセスした時に真っ白い画面が表示されます。

【ResourceConfig】

ResourceConfigはJerseyMVCのリソース設定を行うクラスです。
javax.ws.rs.core.Applicationを拡張してクラスです。

import javax.ws.rs.ApplicationPath;
import jp.ne.glory.web.framework.ThymeleafViewProcessor;
import org.glassfish.jersey.filter.LoggingFilter;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.mvc.MvcFeature;

@ApplicationPath("/service")
public class ApplicationSetting extends ResourceConfig {

    public ApplicationSetting() {

        packages(this.getClass().getPackage().getName());

        register(ThymeleafViewProcessor.class);

        register(MvcFeature.class);

        register(LoggingFilter.class);
    }
}
[packageメソッド]

packageメソッドは各リソースクラスが
どこのパッケージに置かれているかを指定するメソッドです。

今回はApplicationSettingクラスと同じパッケージ配下に
リソースクラスを置こうとしているので↑のような書き方になっています。

[registerメソッド]

registerメソッドはJerseyMVCを動かす時に使うコンポーネントを登録します。
今回は先ほど作成したTemplateProcessor、
MVC用のデフォルトが設定されているMvcFeature、
ログ用のLoggingFilterを登録しています。

【そのほか】

あとは動かすのに必要なソースです。
特に何もないので解説なしです。

[表示用のPOJO]
import java.time.LocalDateTime;

public class TopInfo {
    public final LocalDateTime now;

    public final String lastName;

    public final String firstName;

    public TopInfo(final LocalDateTime now, final String lastName, final String firstName) {
        this.now = now;
        this.lastName = lastName;
        this.firstName = firstName;
    }
}
[リソースクラス]
import java.time.LocalDateTime;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import jp.ne.glory.web.top.bean.TopInfo;
import org.glassfish.jersey.server.mvc.Viewable;

@Path("top")
public class Top {

    @GET
    public Viewable test() {

        final TopInfo topInfo = new TopInfo(LocalDateTime.now(), "Taro", "Yamada");

        return new Viewable("/top/top", topInfo);
    }
[HTML]
<html>
    <head>
        <title>Top Page</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <div th:text="${item.now}">test</div>
        <div th:text="${item.lastName}"></div>
        <div th:text="${item.firstName}"></div>
    </body>
</html>

【まとめ】

JerseyMVCの2系の途中でViewProcessorからTemplateProcessorに変わったらしく、
TemplateProcessorに変わってからのThyemeleafと連携する情報がなくて結構苦労しました。

でも、最終的な結果を見ると非常に簡単に設定ができるなぁと思います。
JerseyMVCは非常によいと思います。