2013年12月25日水曜日

GAE + SBT + Unfiltered + Scalate


GAEの環境でUnfilteredScalateを使ってみました。

SBTでGAEを開発するときの設定はこちらを参照。
SBTでUnfilteredの設定をするにはこちらを参照。

Unfilteredのintentメソッドの実装

まず、前回の復習から。

GAEでは、サーブレットを使って開発するので、サーブレット対応のUnfilteredであるunfiltered-filterパッケージを使用します。

unfiltered-filterを使った基本的な開発の流れ

  1. unfiltered.filter.Planを継承したフィルターを作る
  2. intentメソッドを実装する
  3. web.xmlにフィルターを登録する

unfiltered.filter.Planは、javax.servlet.Filterを継承しています。javax.servlet.Filterを継承したクラスは、doFilterメソッドを実装しないといけないんですが、それはunfiltered.filter.Planクラスで実装されているので実装する必要はありません。その代わりにintentメソッドを実装しなければいけません。

intentメソッドは、unfiltered.request.HttpRequest[HttpServletRequest]を受け取り、unfiltered.response.ResponseFunction[HttpServletResponse]を返すPartialFunctionを返すメソッドです。PartialFunctionになっているので、リクエストの処理をmatch式でやろうというわけですね。

unfiltered.request.HttpRequestは、HttpServletRequestのラッパーで、unfiltered.response.ResponseFunctionは、HttpServletResponseOutputStreamにレスポンスを書き込むトレイトです。

レスポンスを返すには、unfiltered.response.ResponseFunctionを実装したインスタンスを返さなければいけないんですが、それが面倒な人の為にちゃんと予め実装されたサブクラスが幾つか用意されいます。よく使うであろうと思われるのは、ResponseStringでしょう。これを使ってintentメソッドを実装すると、例えば次のようになります。

class Filter extends unfiltered.filter.Plan {
  def intent = {
    case Path("/index.html") => new ResponseString( "Hoge" )
  }
}

web.xmlへの登録は、前回やったので、スキップしま~す。

GAEでのUnfiltered

今回は、Scalateを使ってレスポンスを返すので、ResponseStringは使えません。なぜなら、Scalateの処理をするためには、HttpServletResponseが持っているOutputStreamインスタンスが必要だからです。少なくとも上記の例では、OutputStreamは登場してません。

そこでScalateを使うために、unfiltered.response.ResponseFunctionのサブトレイトResponseWriterを使います。このトレイトは、writeメソッドを実装する必要があるます。このwriteメソッドは、引数としてHttpServletResponseOutputStreamWriterを取るので、このメソッドの中でOutputStreamを使ってScalateの処理を行うことができます。

intentメソッド内でScalateを使う

Scalateとは

Scalateとは、テンプレートエンジンです。いろいろな書式のテンプレートに対応しています。
  • Jade
  • Mustache
  • Scaml(HamlのScala版)
  • SSP(Scala Server Page, JSPのScala版)
  • Scuery

基本的な使い方

Scalateの基本的な使い方は、とても簡単です。TemplateEngineのインスタンスを生成して、layoutメソッドを呼び出すだけです。layoutは、String型のHTMLを返します。

val engine = new TemplateEngine
val html = engine.layout("index.ssp"Map("name" -> "Hoge")List(Biding("name""String")))

layoutは、テンプレートのソースコードをコンパイルして一時領域にクラスファイルを出力します。キャッシュが効くので、テンプレートのソースコードに変更がなければ、2度目移行の呼び出しは、コンパイルは回避されて、一時領域のクライスファイルを使ってHTMLを出力します。

layoutメソッドの詳細
def layout (uri: String, attributes: Map[String, Any] = Map(), extraBindings: Traversable[Binding] = Nil): String
引数:
  • uri
  • Ssp(Scala Server Page)やScaml(HamlのScala版)のテンプレートのソースコードのパス。
  • attributes
  • URIの場所にあるテンプレートに渡す値。複数渡せます。上記の例では、Stringの"Hoge"という値をnameという変数で渡しています。テンプレート側で使用するには、テンプレート側でnameという変数を宣言する必要があります。次の引数のextraBindingsを渡せば宣言も必要なくなります。
  • extraBindings
  • テンプレート側で暗黙的に使える変数の宣言。上記の例では、nameという変数をテンプレート側で宣言せずに使えるようになります。

ただし、GAEのような環境で使用するには、上記では上手く行きません。サーブレット環境であることとファイルの書き込みができないからです。

プリコンパイル

ファイルの書き込みができない場合は、予めコンパイルしてクラスファイルを作っておくという手があります。

Scalateでは、テンプレートのソースコードを動的にコンパイルしJavaのクラスファイルを生成します。そして、そのクラスファイルをシステムの一時領域に保存します。GAEでは、ファイルの書き込みができないので、一時的にコンパイルしたクラスファイルを保存しておくことはできません。これを避ける為に、予めテンプレートをコンパイルしておく必要があるのです。Scalateでは、プリコンパイルされていればテンプレートのソースコードを再度コンパイルすることはないのでGAEでも大丈夫です。プリコンパイルするには、SBTのプラグインxsbt-scalate-generatorを使います。

SBTの設定
project/plugins.sbtに次のコードを追加します。
addSbtPlugin("com.mojolly.scalate" % "xsbt-scalate-generator" % "0.4.2")

build.sbtに次のコードを追記します。
10 
11 
12 
13 
14 
15 
16 
seq(scalateSettings:_*)

scalateTemplateConfig in Compile <<= (sourceDirectory in Compile){ base =>
  Seq(
    TemplateConfig(
      // テンプレートの場所
      base / "webapp" / "WEB-INF"
      // 暗黙的にテンプレートにインポートします
      List("import scala.math._")
      // 暗黙的にテンプレートに変数を宣言します
      List(Binding("userAgent""String"))
      // 出力クラスのパッケージ
      Some("webTmpl") 
    )
  )
}

プリコンパイル
特別に何もする必要はありません。sbtのcompileコマンドやpackageコマンドでwarファイル作成時に自動的にテンプレートをコンパイルしてくれます。出力先は、targetディレクトリです。

サーブレット対応

サーブレット対応したintentメソッドは次のようになります。
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
    def intent = {
      // ResponseWriterを返すクロージャ
      case req @ Path(Seg( _ :: Nil )) => new ResponseWriter {
        def write( writer: OutputStreamWriter ) = {
          val engine = new TemplateEngine
          // サーブレットの環境を設定する
          engine.resourceLoader = new ServletResourceLoader(config.getServletContext)
          // build.sbtで指定したディレクトリ
          engine.workingDirectory = new File("WEB-INF")
          // build.sbtで指定したパッケージ
          engine.packagePrefix = "webTmpl"
          // layoutテンプレートの場所の設定
          engine.layoutStrategy = new DefaultLayoutStrategy(engine, TemplateEngine.templateTypes.map("WEB-INF/layouts/default." + _):_*)
          // プリコンパイルされている場合は、URIと同じ名前のクラスファイルを読み込む。
          // そうでない場合はテンプレートをコンパイルする。
          val scalateTemplate = engine.load(req.underlying.getServletPath)
          // 描画環境を設定
          val context = new DefaultRenderContext(req.underlying.getServletPath, engine, new PrintWriter(writer))
          // テンプレートを描画
          engine.layout( scalateTemplate, context )
        }
      }
    }

ちなみに、ServletTemplateEngineServletRenderContextとうクラスもあるので、こっちを使ってもできるかもしれませんが、試してません。

テンプレートファイルの作成

では、簡単なテンプレートを作成してみましょう。今回はScamlを使います。

レイアウトテンプレート
src/main/webapp/WEB-INF/layouts/default.scaml
-@ var body: String
%html
  %head
    %title Title
    %meta(http-equiv="Content-Type" content="text/html; charset=UTF-8")
  %body
    #{unescape(body)}

レイアウトテンプレートは、全てのテンプレートに自動的に適応されます。テンプレートの内容が#{unescape(body)}の部分に置換されます。

ページのテンプレート
src/main/webapp/index.scaml
%div Hello World

この場合は、%div Hello Worldがレイアウトテンプレートの#{unescape(body)}の部分に置換されます。

コンパイル

> sbt package

サーバを起動してブラウザで確認

開発用サーバを起動:
> appengine-java-sdk-***/bin/dev_appserver.cmd sbt/target/webapp

ブラウザでhttp://localhost:8080/index.scamlに行って、動作を確認してみましょう。ブラウザにHello Worldと表示されていれば成功です。

関連記事

0 件のコメント:

コメントを投稿