CLOVER🍀

That was when it all began.

JAX-RSでHandlebars.scala、Velocityをテンプレートにする

JAX-RSのMessageBodyWriterを使ったことがないのと、これを使うとJAX-RSでもテンプレートエンジンが使えるという話を見て、ちょっと試してみることに。

今回は、テンプレートエンジンとしてHandlebars.scala、Velocityを選びました。

Handlebars.scala
https://github.com/mwunsch/handlebars.scala

Apache Velocity
http://velocity.apache.org/engine/

ホントはScalateでやろうとしたのですが、ScalateがWildFlyで簡単に動きそうにないので、ちょっと方向転換したというか…。

WildFly上で)Scalateが動かない → Velocityで試してみる → 動いた → Scalaのテンプレートも使いたい → Handlebars.scala

という流れです。

依存関係の定義

とりあえず、build.sbt。

name := "jaxrs-with-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",
  "com.gilt" %% "handlebars-scala" % "2.0.0",
  "org.apache.velocity" % "velocity" % "1.7",
  "org.scala-lang.modules" %% "scala-xml" % "1.0.2"
)

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

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

MessageBodyWriterを書く

JAX-RSでMessageBodyWriterというインターフェースを実装したクラスを作成すると、独自のマーシャリングを行うことができるそうで。

まずは、作成するMessageBodyWriterでどのテンプレートエンジンを使うのか見分けるためのcase classを作成。
src/main/scala/org/littlewings/javaee7/rest/View.scala

package org.littlewings.javaee7.rest

trait View {
  val templatePath: String
  val binding: Map[String, _]
}

case class VelocityView(val templatePath: String, val binding: Map[String, _]) extends View

case class HandlebarsView(val templatePath: String, val binding: Map[String, _]) extends View

そして、これらを判定して使うMessageBodyWriterの実装を作成します。

あ、そうそう、テンプレートはHandlebars.scala、Velocityともにクラスパス上から取得するものとします。

まずは、Handlerbars.scala用。
src/main/scala/org/littlewings/javaee7/rest/HandlebarsMessageBodyWriter.scala

package org.littlewings.javaee7.rest

import java.lang.annotation.Annotation
import java.lang.reflect.Type
import java.io._
import java.nio.charset.StandardCharsets

import javax.ws.rs.Produces
import javax.ws.rs.core.{MediaType, MultivaluedMap}
import javax.ws.rs.ext.{MessageBodyWriter, Provider}

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

@Provider
@Produces(Array(MediaType.TEXT_HTML))
class HandlebarsMessageBodyWriter extends MessageBodyWriter[HandlebarsView] {
  override def getSize(view: HandlebarsView,
                       tpe: Class[_],
                       genericType: Type,
                       annotations: Array[Annotation],
                       mediaType: MediaType): Long =
    -1

  override def isWriteable(tpe: Class[_],
                           genericType: Type,
                           annotations: Array[Annotation],
                           mediaType: MediaType): Boolean =
    tpe.isAssignableFrom(classOf[HandlebarsView])

  override def writeTo(view: HandlebarsView,
                       tpe: Class[_],
                       genericType: Type,
                       annotations: Array[Annotation],
                       mediaType: MediaType,
                       httpHeaders: MultivaluedMap[String, AnyRef],
                       entityStream: OutputStream): Unit = {
    val writer = new OutputStreamWriter(entityStream, StandardCharsets.UTF_8)

    val cl = Thread.currentThread.getContextClassLoader
    
    val is = cl.getResourceAsStream(view.templatePath)
    val templateAsString =
      try {
        val reader = new InputStreamReader(is, StandardCharsets.UTF_8)
        try {
          Iterator
            .continually(reader.read())
            .takeWhile(_ != -1)
            .map(_.asInstanceOf[Char])
            .mkString
        } finally {
          reader.close()
        }
      } finally {
        is.close()
      }

    val template = Handlebars(templateAsString)
    writer.write(template(view.binding))
    writer.flush()
  }
}

続いて、Velocity用。
src/main/scala/org/littlewings/javaee7/rest/VelocityMessageBodyWriter.scala

package org.littlewings.javaee7.rest

import java.lang.annotation.Annotation
import java.lang.reflect.Type
import java.io._
import java.nio.charset.StandardCharsets

import javax.ws.rs.Produces
import javax.ws.rs.core.{MediaType, MultivaluedMap}
import javax.ws.rs.ext.{MessageBodyWriter, Provider}

import org.apache.velocity.VelocityContext
import org.apache.velocity.app.VelocityEngine
import org.apache.velocity.runtime.RuntimeConstants
import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader

@Provider
@Produces(Array(MediaType.TEXT_HTML))
class VelocityMessageBodyWriter extends MessageBodyWriter[VelocityView] {
  val engine: VelocityEngine = {
    val ve = new VelocityEngine
    ve.addProperty(RuntimeConstants.RESOURCE_LOADER, "classpath")
    ve.addProperty("classpath.resource.loader.class", classOf[ClasspathResourceLoader].getName)
    ve
  }

  override def getSize(view: VelocityView,
                       tpe: Class[_],
                       genericType: Type,
                       annotations: Array[Annotation],
                       mediaType: MediaType): Long =
    -1

  override def isWriteable(tpe: Class[_],
                           genericType: Type,
                           annotations: Array[Annotation],
                           mediaType: MediaType): Boolean =
    tpe.isAssignableFrom(classOf[VelocityView])

  override def writeTo(view: VelocityView,
                       tpe: Class[_],
                       genericType: Type,
                       annotations: Array[Annotation],
                       mediaType: MediaType,
                       httpHeaders: MultivaluedMap[String, AnyRef],
                       entityStream: OutputStream): Unit = {
    val writer = new OutputStreamWriter(entityStream, StandardCharsets.UTF_8)

    val template =
      engine.getTemplate(view.templatePath, StandardCharsets.UTF_8.toString)
    val context = new VelocityContext

    for ((k, v) <- view.binding) {
      context.put(k, v)
    }

    template.merge(context, writer)
    writer.flush()
  }
}

両者とも、そんなに変わらないんですけどね…。

オーバーライドするメソッドですが、getSizeはコンテンツのサイズ(Content-Length)を決めるためのメソッドだそうで。決められない場合は、-1を指定するそうで。

isWriteableで、指定された引数に対するマーシャリングが行えるかどうかを返却します。マーシャリング可能な場合は、ここでtrueを返せばよい、と。

最後のwriteToで、実際にマーシャリングを行うというそんな感じのようです。

MessageBodyWriterには、少なくとも@Providerアノテーションは付与しないといけないようですが、@Producesアノテーションは付与しなくてもisWriteableで渡ってくるMediaTypeで判定してもよいみたいです。

JAX-RSリソースの作成

では、JAX-RSの有効化とリソースクラスの作成。
src/main/scala/org/littlewings/javaee7/rest/JaxrsApplication.scala

package org.littlewings.javaee7.rest

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

@ApplicationPath("/rest")
class JaxrsApplication extends Application

リソースクラス。
src/main/scala/org/littlewings/javaee7/rest/HelloResource.scala

package org.littlewings.javaee7.rest

import scala.collection.JavaConverters._

import javax.ws.rs.{GET, Path, Produces, QueryParam}
import javax.ws.rs.core.MediaType

@Path("hello")
class HelloResource {
  @GET
  @Path("handlebars")
  @Produces(Array(MediaType.TEXT_HTML))
  def handlebars(@QueryParam("param") param: String): View =
    HandlebarsView("template/hello.handlebars", Map("name" -> "Kazuhira",
                                                    "param" -> Option(param).getOrElse("empty"),
                                                    "languages" -> Seq("Java", "Scala", "Groovy", "Clojure")))

  @GET
  @Path("velocity")
  @Produces(Array(MediaType.TEXT_HTML))
  def velocity(@QueryParam("param") param: String): View =
    VelocityView("template/hello.vm", Map("name" -> "Kazuhira",
                                          "param" -> Option(param).getOrElse("empty"),
                                          "languages" -> Seq("Java", "Scala", "Groovy", "Clojure").asJava))

  @GET
  @Path("plain")
  @Produces(Array(MediaType.TEXT_HTML))
  def plain(@QueryParam("param") param: String): String =
    <html>
      <body>
        <h1>XML Literal Template</h1>
        <p>{"name=Kazuhira"}</p>
        <p>{"param=" + Option(param).getOrElse("empty")}</p>
        <div>
          <p>Languages</p>
          <ul>
            {Seq("Java", "Scala", "Groovy", "Clojure").map(l => <li>{l}</li>)}
          </ul>
        </div>
      </body>
    </html>.toString
}

デバッグ目的のために、XMLリテラルで出力するものも入れています。

その他のメソッドは、返却するcase classで選択するテンプレートが変わる、そんな感じです。

あとは、WildFlyにデプロイして確認。

$ cp /path/to/target/scala-2.11/javaee7-web.war wildfly-8.1.0.Final/standalone/deployments/

Handlebars.scala

$ curl http://localhost:8080/javaee7-web/rest/hello/handlebars?param=test
<!DOCTYPE html>
<html>
  <body>
    <h1>Handlebars.scala Template</h1>
    <p>name = Kazuhira</p>
    <p>param = test</p>
    <div>
      <p>Languages</p>
      <ul>
      
        <li>Java</li>
      
        <li>Scala</li>
      
        <li>Groovy</li>
      
        <li>Clojure</li>
      
      </ul>
    </div>
  </body>
</html>

Velocity

$ curl http://localhost:8080/javaee7-web/rest/hello/velocity?param=test
<!DOCTYPE html>
<html>
  <body>
    <h1>Velocity Template</h1>
    <p>name = Kazuhira</p>
    <p>param = test</p>
    <div>
      <p>Languages</p>
      <ul>
              <li>Java</li>
              <li>Scala</li>
              <li>Groovy</li>
              <li>Clojure</li>
            </ul>
    </div>
  </body>
</html>

XMLリテラルは、省略…。

とりあえず、目的達成。

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