相変わらず、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つがあります。
どちらもすごく簡潔な実装なので、迷ったらコードを見た方が早そうな…。
まあ、とりあえず使ってみましょう。
フォワード先のテンプレートエンジンとしては、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.scalaとScala 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>価格: ¥$!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スケーラブルプログラミング第2版 <ul> <li>ISBN: 978-4844330844</li> <li>価格: ¥4,968</li> </ul> </li> <li> JavaによるRESTfulシステム構築 <ul> <li>ISBN: 978-4873114675</li> <li>価格: ¥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>価格: ¥{{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>価格: ¥4968</li> </ul> </li> <li> JavaによるRESTfulシステム構築 <ul> <li>ISBN: 978-4873114675</li> <li>価格: ¥3456</li> </ul> </li> </ul> </body> </html>
Handlebars.scalaも、ちゃんと使い方を覚えたいですねー。