CLOVER🍀

That was when it all began.

RESTEasyのHTML Providerで遊んでみる

相変わらず、JAX-RSはRESTEasyで遊んでいる自分でございます。

ところで、JAX-RSの参照実装Jerseyには、Jersery MVCというものが人気のようです。RESTEasyにもそんなのないのかなぁ?と思ってちょっと調べたところ、以下のものに辿り着きました。

RESTEasy HTML Provider
https://github.com/resteasy/Resteasy/tree/master/jaxrs/providers/resteasy-html

これ、ドキュメントにも載ってませんよね…?

Javadocには、一応ありますが。
http://docs.jboss.org/resteasy/docs/3.0.7.Final/javadocs/org/jboss/resteasy/plugins/providers/html/package-summary.html

その他のProvider。
https://github.com/resteasy/Resteasy/tree/master/jaxrs/providers

Jersey MVCを使ったことないので比べてどう?とか言えませんが、HTMLテンプレートを作るのには向いた感じでしょうか?

中身を見た感じでは、割と簡単でHTML Provider側でMessageBodyWriterインターフェースを実装したクラスを提供しており、MIME-TYPEがtext/htmlでかつメソッドの戻り値がRenderableインターフェースを実装した型であれば処理がProvider側に移るようです。

Renderableインターフェースの実装としては、以下の2つがあります。

  • View … 指定されたパスへフォワードする。リクエストスコープにオブジェクトを登録することができる
  • Redirect … 指定されたURIへリダイレクトする

どちらもすごく簡潔な実装なので、迷ったらコードを見た方が早そうな…。

まあ、とりあえず使ってみましょう。

フォワード先のテンプレートエンジンとしては、Velocityを使用することにします。デプロイ先のアプリケーションサーバは、WildFly 8.1.0.Finalです。

準備

まずは、依存関係などの定義から。
build.sbt

name := "reseteasy-html-template"

version := "0.0.1-SNAPSHOT"

scalaVersion := "2.11.2"

organization := "org.littlewings"

scalacOptions ++= Seq("-Xlint", "-deprecation", "-unchecked", "-feature")

incOptions := incOptions.value.withNameHashing(true)

jetty()

artifactName := { (version: ScalaVersion, module: ModuleID, artifact: Artifact) =>
  //artifact.name + "." + artifact.extension
  "javaee7-web." + artifact.extension
}

val jettyVersion = "9.2.2.v20140723"

libraryDependencies ++= Seq(
  "org.eclipse.jetty" % "jetty-webapp" % jettyVersion % "container",
  "org.eclipse.jetty" % "jetty-plus"   % jettyVersion % "container",
  "javax" % "javaee-web-api" % "7.0" % "provided",
  "org.jboss.resteasy" % "resteasy-html" % "3.0.8.Final" exclude("org.jboss.resteasy", "resteasy-jaxrs"),
  "org.apache.velocity" % "velocity" % "1.7",
  "org.apache.velocity" % "velocity-tools" % "2.0" exclude("javax.servlet", "servlet-api"),
  "com.gilt" %% "handlebars-scala" % "2.0.1",
  "com.jsuereth" %% "scala-arm" % "1.4"
)

Handlebars.scalaScala ARMが入っているのは、オマケで遊んだからです。

xsbt-web-pluginも使用。
project/plugins.sbt

addSbtPlugin("com.earldouglas" % "xsbt-web-plugin" % "1.0.0-M6")

web.xml

VelocityViewServletを使用するので、web.xmlを用意します。
src/main/webapp/WEB-INF/web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app 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/web-app_3_1.xsd"
         version="3.1">
  <servlet>
    <servlet-name>velocity</servlet-name>
    <servlet-class>org.apache.velocity.tools.view.VelocityViewServlet</servlet-class>
  </servlet>

  <servlet-mapping>
    <servlet-name>velocity</servlet-name>
    <url-pattern>*.vm</url-pattern>
  </servlet-mapping>
</web-app>

velocity.propertiesも簡単に用意。
src/main/webapp/WEB-INF/velocity.properties

## base velocity.properties
## https://github.com/apache/velocity-tools/blob/trunk/velocity-tools-examples/velocity-tools-examples-struts/src/main/webapp/WEB-INF/velocity.properties

runtime.log.logsystem.class = org.apache.velocity.runtime.log.NullLogChute

input.encoding=UTF-8
output.encoding=UTF-8

directive.foreach.counter.name = velocityCount
directive.foreach.counter.initial.value = 1

directive.include.output.errormsg.start = <!-- include error :
directive.include.output.errormsg.end   =  see error log -->

directive.parse.max.depth = 10

webapp.resource.loader.cache = true
webapp.resource.loader.modificationCheckInterval = 5
velocimacro.library.autoreload = false
# velocimacro.library = /WEB-INF/VM_global_library.vm

velocimacro.permissions.allow.inline = true
velocimacro.permissions.allow.inline.to.replace.global = false
velocimacro.permissions.allow.inline.local.scope = false

velocimacro.context.localscope = false

runtime.interpolate.string.literals = true

resource.manager.class = org.apache.velocity.runtime.resource.ResourceManagerImpl
resource.manager.cache.class = org.apache.velocity.runtime.resource.ResourceCacheImpl

JAX-RSのコードとVelocityテンプレートの作成

まずはJAX-RS側、Applicationのサブクラスを用意。
src/main/scala/org/littlewings/javaee7/rest/JaxrsApplicationl.scala

package org.littlewings.javaee7.rest

import javax.ws.rs.ApplicationPath
import javax.ws.rs.core.Application

@ApplicationPath("rest")
class JaxrsApplication extends Application

パスは、「rest」で。

次に、リソースクラス。まずはimport文とクラスの宣言から。
src/main/scala/org/littlewings/javaee7/rest/TemplateResource.scala

package org.littlewings.javaee7.rest

import scala.collection.JavaConverters._

import javax.ws.rs.{GET, Path, Produces}
import javax.ws.rs.core.{Context, MediaType, UriBuilder, UriInfo}

import org.jboss.resteasy.plugins.providers.html.{Renderable, Redirect, View}

@Path("template")
class TemplateResource {
  // 後で
}

ここから先は、リソースクラスのメソッドとVelocityテンプレート、それからデプロイ後のレスポンスを順に並べていきましょう。

Velocityテンプレート表示

単純にViewクラスを使用したパターン。

  @GET
  @Path("simple")
  @Produces(Array(MediaType.TEXT_HTML))
  def simple: Renderable =
    new View("/WEB-INF/velocity/simple.vm",
             "Hello Velocity!!")

Viewクラスのコンストラクタに、フォワード先のパスとViewクラスにバインドするオブジェクトを設定します。バインドするオブジェクトに対して、別名は与えていません。

MediaTypeは、「text/html」にします。

  @Produces(Array(MediaType.TEXT_HTML))

Velocityテンプレート側。
src/main/webapp/WEB-INF/velocity/simple.vm

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>サンプル</title>
</head>
<body>
  <h1>Velocityテンプレートのサンプル</h1>
  <p>$!esc.html($model)</p>
</body>
</html>

Viewにバインドするオブジェクトに対して、特に何も別名を設定しなかった場合は、リクエストスコープに「model」という名前で登録されます。

動作確認。

$ curl http://localhost:8080/javaee7-web/rest/templ
ate/simple
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>サンプル</title>
</head>
<body>
  <h1>Velocityテンプレートのサンプル</h1>
  <p>Hello Velocity!!</p>
</body>
</html>

続いて、MapやListなどを使ってみたパターン。

  @GET
  @Path("complex")
  @Produces(Array(MediaType.TEXT_HTML))
  def complex: Renderable =
    new View("/WEB-INF/velocity/complex.vm",
             Map("message" -> "Hello Velocity",
                 "languages" -> List("Java", "Scala", "Groovy", "Clojure").asJava).asJava,
             "it")

相手がVelocityなので、JavaConvertersを使ってJavaのコレクションに変換しています。

オブジェクトは、「it」で登録してみました(笑)。

Velocityテンプレートは、このようなものとしました。
src/main/webapp/WEB-INF/velocity/complex.vm

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>サンプル</title>
</head>
<body>
  <h1>Velocityテンプレートのサンプル</h1>
  <p>$!esc.html($!it['message'])</p>
  <p>Languages</p>
  <ul>
  #foreach ($l in $!it['languages'])
    <li>$!esc.html($l)</li>
  #end
  </ul>
</body>
</html>

実行結果。

$ curl http://localhost:8080/javaee7-web/rest/template/complex
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>サンプル</title>
</head>
<body>
  <h1>Velocityテンプレートのサンプル</h1>
  <p>Hello Velocity</p>
  <p>Languages</p>
  <ul>
      <li>Java</li>
      <li>Scala</li>
      <li>Groovy</li>
      <li>Clojure</li>
    </ul>
</body>
</html>

ScalaのCase Classを使ってみたサンプル。用意したCase Class。
src/main/scala/org/littlewings/javaee7/rest/Book.scala

package org.littlewings.javaee7.rest

case class Book(isbn: String,
                title: String,
                price: Int)

リソースクラスのメソッド。

  @GET
  @Path("case-class")
  @Produces(Array(MediaType.TEXT_HTML))
  def caseClass: Renderable =
    new View("/WEB-INF/velocity/case-class.vm",
             List(Book("978-4844330844",
                       "Scalaスケーラブルプログラミング第2版",
                       4968),
                  Book("978-4873114675",
                       "JavaによるRESTfulシステム構築",
                       3456)).asJava,
             "books")

Velocityテンプレート。
src/main/webapp/WEB-INF/velocity/case-class.vm

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>サンプル</title>
</head>
<body>
  <h1>Velocityテンプレートのサンプル</h1>
  <p>Books</p>
  <ul>
  #foreach ($book in $!books)
    <li>
      $!esc.html($!book.title())
      <ul>
        <li>ISBN: $!esc.html($!book.isbn())</li>
        <li>価格: &#165;$!esc.html($!number.format('#,###', $!book.price()))</li>
      </ul>
    </li>
  #end
  </ul>
</body>
</html>

Case Classのメンバーアクセスは、メソッド呼び出しとなっております…。

実行結果。

$ curl http://localhost:8080/javaee7-web/rest/template/case-class
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>サンプル</title>
</head>
<body>
  <h1>Velocityテンプレートのサンプル</h1>
  <p>Books</p>
  <ul>
      <li>
      Scala&#12473;&#12465;&#12540;&#12521;&#12502;&#12523;&#12503;&#12525;&#12464;&#12521;&#12511;&#12531;&#12464;&#31532;2&#29256;
      <ul>
        <li>ISBN: 978-4844330844</li>
        <li>価格: &#165;4,968</li>
      </ul>
    </li>
      <li>
      Java&#12395;&#12424;&#12427;RESTful&#12471;&#12473;&#12486;&#12512;&#27083;&#31689;
      <ul>
        <li>ISBN: 978-4873114675</li>
        <li>価格: &#165;3,456</li>
      </ul>
    </li>
    </ul>
</body>
</html>
リダイレクト

Redirectクラスを戻り値にすると、リダイレクトを行うことができます。

  @GET
  @Path("redirect")
  @Produces(Array(MediaType.TEXT_HTML))
  def redirect(@Context uriInfo: UriInfo): Renderable =
    new Redirect(uriInfo
                  .getBaseUriBuilder
                  .path(classOf[TemplateResource])
                  .path(classOf[TemplateResource],
                        "caseClass")
                          .build())

Redirectクラスのコンストラクタに渡したパスが、まんまLocationヘッダに使用されるようなので、ちゃんと絶対URIで作る必要があるみたいです。

実行結果。

$ curl -i http://localhost:8080/javaee7-web/rest/te
mplate/redirect
HTTP/1.1 303 See Other
Connection: keep-alive
X-Powered-By: Undertow/1
Server: WildFly/8
Location: http://localhost:8080/javaee7-web/rest/template/case-class
Content-Type: text/html
Content-Length: 0
Date: Sat, 06 Sep 2014 07:55:28 GMT

とりあえず、使えましたね。

まあ、凝ったことするなら、自分でMessageBodyWriterインターフェースの実装を書いてもいいような気がしますが…。

今回作成したコードは、こちらに置いています。
https://github.com/kazuhira-r/javaee7-scala-examples/tree/master/resteasy-html-template

オマケ

オマケで、Handlebars.scalaフォワード先のテンプレートとして使用してみるサンプルを。

  @GET
  @Path("handlebars")
  @Produces(Array(MediaType.TEXT_HTML))
  def handlebars: Renderable =
    new View("/WEB-INF/handlebars/case-class.handlebars",
             List(Book("978-4844330844",
                       "Scalaスケーラブルプログラミング第2版",
                       4968),
                  Book("978-4873114675",
                       "JavaによるRESTfulシステム構築",
                       3456)),
             "books")

用意したテンプレート。
src/main/webapp/WEB-INF/handlebars/case-class.handlebars

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>サンプル</title>
</head>
<body>
  <h1>Velocityテンプレートのサンプル</h1>
  <p>Books</p>
  <ul>
  {{#books}}
    <li>
      {{title}}
      <ul>
        <li>ISBN: {{isbn}}</li>
        <li>価格: &#165;{{price}}</li>
      </ul>
    </li>
  {{/books}}
  </ul>
</body>
</html>

フォワード時のリクエストの受け口として、サーブレットを用意しました。Requestスコープの内容を、丸ごとMapに放り込むちょっと乱暴な実装ですが…。あと、テンプレートが未存在の場合も考慮してません。
src/main/scala/org/littlewings/javaee7/servlet/HanlebarsScalaServlet.scala

package org.littlewings.javaee7.servlet

import scala.collection.JavaConverters._

import java.io.{BufferedReader, InputStreamReader, IOException}
import java.nio.charset.StandardCharsets

import javax.servlet.ServletException
import javax.servlet.annotation.WebServlet
import javax.servlet.http.{HttpServlet, HttpServletRequest, HttpServletResponse}

import com.gilt.handlebars.scala.binding.dynamic._
import com.gilt.handlebars.scala.Handlebars

import resource._

@WebServlet(Array("*.handlebars"))
class HandlebarsScalaServlet extends HttpServlet {
  private var prefix: String = _

  @throws(classOf[IOException])
  @throws(classOf[ServletException])
  override protected def doGet(req: HttpServletRequest, res: HttpServletResponse): Unit =
    process(req, res)

  @throws(classOf[IOException])
  @throws(classOf[ServletException])
  override protected def doPost(req: HttpServletRequest, res: HttpServletResponse): Unit =
    process(req, res)

  @throws(classOf[IOException])
  @throws(classOf[ServletException])
  protected def process(req: HttpServletRequest, res: HttpServletResponse): Unit = {
    res.setCharacterEncoding(StandardCharsets.UTF_8.toString)

    val writer = res.getWriter

    val context = req.getServletContext
    val path =
      if (req.getPathInfo != null)
          req.getServletPath + req.getPathInfo
        else
          req.getServletPath

    val contents =
      for {
        is <- managed(context.getResourceAsStream(path))
        isr <- managed(new InputStreamReader(is, StandardCharsets.UTF_8))
        reader <- managed(new BufferedReader(isr))
      } yield {
        Iterator
          .continually(reader.read())
          .takeWhile(_ != -1)
          .map(_.asInstanceOf[Char])
          .mkString
      }

    val template = Handlebars(contents.acquireAndGet(identity))

    val bindings =
      req.getAttributeNames.asScala.foldLeft(Map.empty[String, Any]) { (acc, n) =>
        acc + (n -> req.getAttribute(n))
      }

    writer.write(template(bindings))
    writer.flush()
  }
}

実行結果。

$ curl http://localhost:8080/javaee7-web/rest/template/handlebars
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>サンプル</title>
</head>
<body>
  <h1>Velocityテンプレートのサンプル</h1>
  <p>Books</p>
  <ul>
  
    <li>
      Scalaスケーラブルプログラミング第2<ul>
        <li>ISBN: 978-4844330844</li>
        <li>価格: &#165;4968</li>
      </ul>
    </li>
  
    <li>
      JavaによるRESTfulシステム構築
      <ul>
        <li>ISBN: 978-4873114675</li>
        <li>価格: &#165;3456</li>
      </ul>
    </li>
  
  </ul>
</body>
</html>

Handlebars.scalaも、ちゃんと使い方を覚えたいですねー。