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>
とりあえず、目的達成。
今回作成したコードは、こちらに置いています。
https://github.com/kazuhira-r/javaee7-scala-examples/tree/master/jaxrs-with-template