2014/8

[Xitrumことはじめ][基本編] 6. レスポンスとビュー: テンプレートエンジンとXitrum-Scalate

Xitrumことはじめ (基本編)

Xitrumことはじめシリーズでは、Xitrumを使ったWebアプリケーション開発を勉強します。

目次はこちら

記事とサンプルコードはMITライセンスでgithubで公開します。

6. レスポンスとビュー:

今回はテンプレートエンジンについてです。

公式ドキュメントは以下のページが参考になります。

6-3. テンプレートエンジンとXitrum-Scalate

Xitrumのテンプレートエンジンのインターフェイスはこちらにあり、
デフォルトテンプレートエンジンはxitrum-scalateです。
xitrum-scalateは内部でScalateを使用しています。

Xitrum-scalateの設定

テンプレートエンジンの指定はxitrum.confで行います。
xitrum-scalateを使用するには、xitrum.view.TemplateEngineの実装であるxitrum.view.Scalateを指定します。

xitrum.conf
# Comment out if you don't use template engine
template {
  "xitrum.view.Scalate" {
    defaultType = jade  # jade, mustache, scaml, or ssp
  }
}

テンプレートのsyntaxについてはjadeの他mustachescamlsspが使用可能です。
configでは指定したシンタックス以外を使用する場合は、以下のように指定します。

val options = Map("type" ->"mustache")
respondView(options)

また、sbtの設定を以下のように行います。

project/plugins.sbt
// For precompiling Scalate templates in the compile phase of SBT
addSbtPlugin("com.mojolly.scalate" % "xsbt-scalate-generator" % "0.5.0")
build.sbt
// Scalate template engine config for Xitrum -----------------------------------
 
libraryDependencies += "tv.cntt" %% "xitrum-scalate" % "2.2"
 
// Precompile Scalate templates
seq(scalateSettings:_*)
 
ScalateKeys.scalateTemplateConfig in Compile := Seq(TemplateConfig(
  file("src") / "main" / "scalate",
  Seq(),
  Seq(Binding("helper", "xitrum.Action", true))
))

なお、Viewを必要としないアプリケーションの場合これらのテンプレートエンジンに関する設定は全て削除することもできます。

Xitrum-scalateの機能

xitrum-scalateはAction名からテンプレートファイルを選択しViewを生成する他にいくつかの機能を持っています。

1つ目はActionの各メソッドをhelperとしてバインドします。
そのため、テンプレートファイルの中では、Scalateのメソッドの他に、
publicUrlなどのxitrum.ActionのAPIを使用することができます。

2つ目は、現在のActionのインスタンスをcurrentActionという変数から取得することができます。

- val myAction = currentAction.asInstanceOf[MyAction];

次回はこれらを使用したサンプルを作成します。

Scalateについての補足

タグ名に続き.で文字列を追加すればCSSクラス名に、#で文字列を追加すればDOMのIDに変換されます。
また、()で引数を渡すことでDOMエレメントの属性に変換されます。
Scalateについて詳しくは公式ドキュメントが参考になります。
個人的な感覚ですがXitrumアプリケーションの開発において一番エラーが発生するのはテンプレートファイルの
シンタックスエラーのように感じます。最初は難しいですが慣れると便利です。
例えば以下はログインフォームを作成した例となります。

- import mypackage.LoginAction
div.col-md-4.col-md-offset-4
  form.form-signin(method="post" action={url[LoginAction]})
    h4.form-signin-heading = t("Please sign in")
    != antiCsrfInput
    div.control-group
      div.controls
        label(for="userName") =t("User Name")
        input.form-control#userName(placeholder={t("Type your name")} type="text" name="name" minlength=5 maxlenght=10 required=true)
    div.control-group
      div.controls
        label(for="password") =t("Password")
        input.form-control#password(placeholder={t("Type password")} type="password" name="password" minlength=8 required=true)
    button.btn.btn-large.btn-primary#loginSubmit(type="submit") = t("login")

出力されるレスポンス

<div class="col-md-4 col-md-offset-4">
  <form class="form-signin" method="post" action="/login">
    <h4 class="form-signin-heading">Please sign in</h4>
    <input type="hidden" name="csrf-token" value="3e8f6edf-ea44-4de8-8ab1-962609b821ce">
    <div class="control-group">
      <div class="controls">
        <label for="userName">User Name</label>
        <input id="userName" class="form-control" placeholder="Type your name" type="text" name="name" minlength="5" maxlenght="10" required="required">
      </div>
    </div>
    <div class="control-group">
      <div class="controls">
        <label for="password">Password</label>
        <input id="password" class="form-control" placeholder="Type password" type="password" name="password" minlength="8" required="required">
      </div>
    </div>
    <button id="loginSubmit" class="btn btn-large btn-primary" type="submit">login</button>
  </form>
</div>

※上記の例で使用しているtファンクションについてはi18nに向けた機能なのでまた後程。

[Xitrumことはじめ][基本編] 6. レスポンスとビュー: Templateを使用してViewをレスポンスする

Xitrumことはじめ (基本編)

Xitrumことはじめシリーズでは、Xitrumを使ったWebアプリケーション開発を勉強します。

目次はこちら

記事とサンプルコードはMITライセンスでgithubで公開します。

6. レスポンスとビュー:

今回はレイアウトファイルを使用してViewを表示するところを試します。

公式ドキュメントは以下のページが参考になります。

6-2. Templateを使用してViewをレスポンスする

テンプレートエンジンはデフォルトのまま、xitrum-scalateを使用します。
カスタムテンプレートエンジンの使用については今後やってみたいと思います。

Scaffoldのコードリーディングで確認しましたが、respondViewを使用することで、
テンプレートエンジンが生成したViewを(xitrum-scalateの場合、Actionのクラス名に対応するScalateファイルがテンプレートになる)
が生成し、クライアントにレスポンスすることができます。

現在のActionに対応するViewをレスポンスする

RespondViewExample.scala
@GET("respond/view1")
class RespondViewExample1 extends Action {
  def execute() {
    respondView()
  }
}
RespondViewExample1.jade
p This is a "RespondViewExample1" template

これによって、RespondViewExample1.jadeを元に生成されたViewがレスポンスされます。
http://localhost:8000/respond/view1

現在のActionとは異なるViewをレスポンスする

Actionの型を指定することで、指定したActionに対応するテンプレートを利用することができます。

RespondViewExample.scala
@GET("respond/view2")
class RespondViewExample2 extends Action {
  def execute() {
    respondView[RespondViewExample1]()
  }
}

これも同じく、RespondViewExample1.jadeを元に生成されたViewがレスポンスされます。
http://localhost:8000/respond/view2

レイアウトを使用する

レイアウトとなるViewを指定し、その中でrenderViewを呼び出すことでレイアウトを使用したレスポンスを返すことができます。
htmlタグやheaderタグなどの共通項目をtraitとして再利用することができます。

RespondViewExample.scala
trait CustomLayout extends Action {
  override def layout = renderViewNoLayout[CustomLayout]()
}
 
@GET("respond/view3")
class RespondViewExample3 extends CustomLayout {
  def execute() {
    respondView()
  }
}
CustomeLayout.jade
p
  This is a "CustomLayout" template
 
div
  != renderedView

この場合、CustomeLayout.jadeにRespondViewExample3.jadeがネストしてレスポンスされます。
http://localhost:8000/respond/view3

フラグメントを使用する

複数のActionでViewを共有するには、上記のようにレイアウトを使用する他に、
renderFragmentを使用することでも実現できます。
fragmenはscalateフォルダ内に、使用するアクションのパッケージディレクトリに"_"prefixとして保存します。

RespondViewExample.scala
@GET("respond/fragment1")
class RespondFragmentExample1 extends CustomLayout {
  def execute() {
    respondView()
  }
}
RespondFragmentExample1.jade
p This is a "RespondFragmentExample1" template
div
  != renderFragment("myfragment")
_myfragment.jade
p This is a "myfragment" fragment

この場合、ResondFragmentExampleX.jade内でrenderFragment("myfragment")とすることで
_myfragment.jadeがRespondViewFragment1/RespondViewFragment2の両方で使用できます。
http://localhost:8000/respond/fragment1
http://localhost:8000/respond/fragment2

fragmentの他に、Componentを使用するやり方もあります。
fragmentはViewテンプレートを複数のActionで共有する仕組みですが、Componentはさらに高度な仕組みとして使うことができます。
Componentについては応用編で詳しく掘り下げます。


ActionとViewファイルの関係は以上です。
MVCフレームワークに当てはめると

  • M: Actionクラスがexecuteメソッド内で呼び出す処理が該当
  • V: Actionクラス名に対応したscalateファイル(Actionクラスからはexecuteメソッドの最後にrespondViewで呼び出す)が該当
  • C: Actionクラスが該当

といった感じでしょうか。

Viewの返し型が分かったので、次回以降はViewに関するヘルパーや、Xitrum-Scalateについて掘り下げます。

  • Aug
  • 24
  • 2014

IT

Xitrum 3.18

Xitrum 3.18 Released!

https://groups.google.com/d/msg/xitrum-framework/9s6Vbenh1xE/nG73JyDmXvAJ

主な変更点


1.

レスポンスのコンテンツサイズが1KB未満の場合のキャッシュページ・キャッシュアクションのバグフィックス

http://xitrum-framework.github.io/guide/3.18/en/cache.html

2.

Akkaのアップデート from 2.3.4 to 2.3.5:
https://groups.google.com/forum/#!topic/akka-user/c9tS2Q7pctA

Nettyのアップデート from 4.0.21 to 4.0.23:
http://netty.io/news/2014/08/14/4-0-22-Final.html
http://netty.io/news/2014/08/15/4-0-23-Final-and-4-1-0-Beta3.html

3.

高パフォーマンスと低レイテンシを実現するために、epollをエッジトリガモードで使用することができるようになりました。
NettyがLinuxのみで使用可能なネイティブコードを使用しているため、
この機能はいまのところLinuxのみで使用可能です。(Java NIOはレベルトリガモードのみサポートしています)

<追記>
epollについての参考
http://netty.io/news/2014/02/25/4-0-17-Final.html#main-content
http://ymmt.hatenablog.com/entry/2013/09/05/150116
http://www.linux-cmd.com/epoll.html

4.

デベロップメントモードのパフォーマンス改善
Xitrum 3.17ではリクエスト毎にルートの収集を行いましたが、これからはファイル変更時のみルート収集が行われるようになりました。

5.

クライアントサイドのソースコードを自動生成するswagger-codegenが使用可能になりました。
https://github.com/wordnik/swagger-codegen
http://xitrum-framework.github.io/guide/3.18/en/restful.html#documenting-api-with-swagger

-----

3.17 から 3.18 へのアップデートは非常に簡単です。
例:

https://github.com/xitrum-framework/xitrum-new/commit/cb88c3e38bdd356eb6eb150a7ebb93642bfe6eb5

1. build.sbtの修正:

旧:
libraryDependencies += "tv.cntt" %% "xitrum" % "3.17"

新:

libraryDependencies += "tv.cntt" %% "xitrum" % "3.18"


2. config/xitrum.confの修正:

追加:
edgeTriggeredEpoll = false (or true, see above.)

旧:
useOpenSSL = false (or true)
新:
openSSL = false (or true)

[Xitrumことはじめ][基本編] 6. レスポンスとビュー: textをレスポンスする

Xitrumことはじめ (基本編)

Xitrumことはじめシリーズでは、Xitrumを使ったWebアプリケーション開発を勉強します。

目次はこちら

記事とサンプルコードはMITライセンスでgithubで公開します。

6. レスポンスとビュー:

ルーティングとActionの関連が分かったので
今回からはActionがレスポンスを返す方法について確認していきます。

公式ドキュメントは以下のページが参考になります。

ドキュメントにあるクライアントへのレスポンス送信パターンは以下の通りです。

  • respondView: レイアウトファイルを使用または使用せずに、Viewテンプレートファイルを送信します
  • respondInlineView: レイアウトファイルを使用または使用せずに、インライン記述されたテンプレートを送信します
  • respondText("hello"): レイアウトファイルを使用せずに文字列を送信します
  • respondHtml("..."): contentTypeを”text/html”として文字列を送信します
  • respondJson(List(1, 2, 3)): ScalaオブジェクトをJSONに変換し、contentTypeを”application/json”として送信します
  • respondJs("myFunction([1, 2, 3])") contentTypeを”application/javascript”として文字列を送信します
  • respondJsonP(List(1, 2, 3), "myFunction"): 上記2つの組み合わせをJSONPとして送信します
  • respondJsonText("[1, 2, 3]"): contentTypeを”application/javascript”として文字列として送信します
  • respondJsonPText("[1, 2, 3]", "myFunction"): respondJs 、 respondJsonText の2つの組み合わせをJSONPとして送信します
  • respondBinary: バイト配列を送信します
  • respondFile: ディスクからファイルを直接送信します。 zero-copy を使用するため非常に高速です。
  • respondEventSource("data", "event"): チャンクレスポンスを送信します

Viewファイルを使用する前にの前に単純なパターンから確認していきます。

6-1. textをレスポンスする

Actionがレスポンスを返すときの最も単純なパターンはテキストを返すだけの処理です。

これまでのサンプルで処理を実行したActionのクラス名を出力するためにも使用しました。

trait ClassNameResponder extends Action {
                def respondClassNameAsText(){
                  respondText(getClass)
                }
              }
              

respondTextを行うと

> curl http://localhost:8000/path/to/myaction -v                                                                                                                            19:01:17  ☁  master ☂ ⚡ ✭
              * About to connect() to localhost port 8000 (#0)
              *   Trying ::1...
              * Adding handle: conn: 0x7fbde8806e00
              * Adding handle: send: 0
              * Adding handle: recv: 0
              * Curl_addHandleToPipeline: length: 1
              * - Conn 0 (0x7fbde8806e00) send_pipe: 1, recv_pipe: 0
              * Connected to localhost (::1) port 8000 (#0)
              > GET /path/to/myaction HTTP/1.1
              > User-Agent: curl/7.32.0
              > Host: localhost:8000
              > Accept: */*
              >
              < HTTP/1.1 200 OK
              < Connection: keep-alive
              < Content-Type: text/plain; charset=UTF-8
              < Access-Control-Allow-Origin: *
              < Access-Control-Allow-Credentials: true
              < Access-Control-Allow-Methods: OPTIONS, GET, HEAD
              < Content-Length: 32
              < ETag: "9HVM4haHpRzVyN0-1nuYdA"
              <
              * Connection #0 to host localhost left intact
              class quickstart.action.MyAction%
              

Content-Typeに "text/plain"が指定されます。

Scaladocには以下のように定義してあります。

def respondText(text: Any, fallbackContentType: String = null, convertXmlToXhtml: Boolean = true): ChannelFuture
@fallbackContentType
Only used if Content-Type header has not been set. If not given and Content-Type header is not set, it is set to "application/xml" if text param is Node or NodeSeq, otherwise it is set to "text/plain".
@convertXmlToXhtml
.toString by default returns <br></br> which is rendered as 2 <br />tags on some browsers!
Set to false if you really want XML, not XHTML. See http://www.scala-lang.org/node/492 and http://www.ne.jp/asahi/hishidama/home/tech/scala/xml.html

textの型はAnyとなっています。これはStringの他にNodeNodeSeqを渡せるようにするためでしょう。
fallbackContentTypeを指定しない場合はtext/plainもしくはtextの型に応じて、application/xmlが自動で設定されるようです。

respondHtmlrespondJsonrespondJsrespondJsonPrespondJsonTextrespondJsonPTextもこの応用で
指定した内容に対して、contet-typeを決定してレスポンスを返してくれます。
respondJsonrespondJsonPについては、ScalaObjectを直接渡すことでXitrumがJSONに変換してくれます。

各メソッドを使用して以下のようなサンプルを作成してみました。
まず、"respond/html"に対するアクセスに対して、RespondExample1respondHtmlを使用して
インラインで記述されたhtmlを返却します。
返却されるhtml内ではscriptタグで各URLへのリクエストが行われます。

RexpondTextExample.scala

@GET("respond/html")
              class RespondExample1 extends ClassNameResponder {
                def execute() {
                  respondHtml("""
              <html>
                  <head>
                    <script src="//code.jquery.com/jquery-2.1.1.min.js"></script>
                    <script src="/respond/js"></script>
                    <script src="/respond/jsonp"></script>
                    <script src="/respond/jsonptext"></script>
                  </head>
                  <body>
                    <p>This is respondHtml</p>
                    <script>
                      $.ajax("/respond/json")
                      .done(function(d){
                        console.log("Response from respondJson")
                        console.log(d);
                      });
                      $.ajax("/respond/jsontext")
                      .done(function(d){
                        console.log("Response from respondJsonText")
                        console.log(d);
                      });
                      </script>
                  </body>
              </html>
                  """)
                }
              }
              
              @GET("respond/js")
              class RespondExample2 extends ClassNameResponder {
                def execute() {
                  val jsText = "function myCallback(x){console.log("This is Callback for jsonP"); console.log(x);}"
                  respondJs(jsText)
                }
              }
              
              @GET("respond/json")
              class RespondExample3 extends ClassNameResponder {
                def execute() {
                  val jsonObj = Map[String, Any](
                                  "key1" -> "this is json",
                                  "key2" -> List("x","y",true),
                                  "key3" -> Map("nest" -> "foo")
                                )
                  respondJson(jsonObj)
                }
              }
              
              @GET("respond/jsontext")
              class RespondExample4 extends ClassNameResponder {
                def execute() {
                  val jsonText = """
                                 {"key1":"this is jsonText","key2":[1,2,3,true]}
                                 """
                  respondJsonText(jsonText)
                }
              }
              
              @GET("respond/jsonp")
              class RespondExample5 extends ClassNameResponder {
                def execute() {
                  val jsonpObj = Map("key" -> "this is jsonP","key2":[1,2,3,true])
                  respondJsonP(jsonpObj, "myCallback")
                }
              }
              
              @GET("respond/jsonptext")
              class RespondExample6 extends ClassNameResponder {
                def execute() {
                  val jsonptext = """
                                  {"key1":"this is jsonPText","key2":[1,2,3,true]}
                                  """
                  respondJsonPText(jsonptext,"myCallback")
                }
              }
              

http://localhost:8000/respond/htmlをブラウザで表示して、
JavaScriptコンソールを見ると以下の様になりました。

> Object {key: "jsonpObj"}
              > Object {key1: "this is jsonPtext", key2: Array[4]}
              > Response from respondJsonText
              > Object {key1: "this is jsontext", key2: Array[4]}
              > Response from respondJson
              > Object {key1: 1, key2: Array[3], key3: Object}
              

簡単ですね。
次回はレイアウトテンプレートを使用したViewの表示について確認します。

[Xitrumことはじめ][基本編] 5.ルーティングを追加する: 静的リソースとindex.html

Xitrumことはじめ (基本編)

Xitrumことはじめシリーズでは、Xitrumを使ったWebアプリケーション開発を勉強します。

目次はこちら

記事とサンプルコードはMITライセンスでgithubで公開します。

5. ルーティングを追加する:

前回はActionとURLの関連付けを確認したので、
今回はAction以外のリソースのURLについて見ていきます。

公式ドキュメントは以下のページが参考になります。

5-3. 静的リソースとindex.html

XitrumのScaffoldプロジェクトにpublicというディレクトリが含まれていることを以前確認しました
Xitrumはpublicディレクトリに含まれる静的ファイルを自動で配信します。
public内に配置されたファイルは

http(s)://<host>:<port>/<ファイルパス>
              

のURLでアクセスすることができます。

プログラムから上記のURLを取得するためのヘルパーとして、ActionにはpublicUrlというメソッドが用意されています。
publicUrlメソッドは、主にScalateテンプレート内で使用されることを想定しており、
開発環境とプロダクション環境における圧縮ファイルと非圧縮ファイルの出し分けや、Etagに応じたクエリストリングの付加を自動で行ってくれます。

Scaffoldでは以下の用に使用されています。

// DefaultLayout.jade内
              link(rel="shortcut icon" href={publicUrl("favicon.ico")})
              link(type="text/css" rel="stylesheet" media="all" href={publicUrl("app.css")})
              
              // SiteIndex.jade内
              img(src={publicUrl("whale.png")})
              

publicUrlの使い方の詳細についてはViewの章で詳しく見たいと思います。

また、JavaScriptライブラリやCSSライブラリなどのフロントエンドリソースの配信については、
WebJarsを使用することができます。WebJarsについては今後改めて掘り下げたいと思いますので、
今回は触れません。

パスの優先順位とindex.htmlフォールバック

public内にあるファイルと、Actionに定義したルーティングの優先順位について見てみます。
以下のような実験用ファイルを作成します。

PublicRoot.scala
@GET("static")
              class PublicRootAction1 extends ClassNameResponder {
                def execute() {
                  log.debug("PublicRootAction1")
                  respondClassNameAsText()
                }
              }
              
              @GET("static/index")
              class PublicRootAction2 extends ClassNameResponder {
                def execute() {
                  log.debug("PublicRootAction2")
                  respondClassNameAsText()
                }
              }
              
              @GET("static/index.html")
              class PublicRootAction3 extends ClassNameResponder {
                def execute() {
                  log.debug("PublicRootAction3")
                  respondClassNameAsText()
                }
              }
              
              @GET("static/image")
              class PublicRootAction4 extends ClassNameResponder {
                def execute() {
                  log.debug("PublicRootAction4")
                  respondClassNameAsText()
                }
              }
              
              @GET("static/image.png")
              class PublicRootAction5 extends ClassNameResponder {
                def execute() {
                  log.debug("PublicRootAction5")
                  respondClassNameAsText()
                }
              }
              

またpublicディレクトリ内には以下のファイルを用意します。

.
              └── static
                  ├── file.xls
                  ├── foo
                  │   └── index.html
                  ├── image.png
                  └── index.html
              

アプリを起動すると以下のルーティングテーブルが出力されます

GET     /static                                quickstart.action.PublicRootAction1
              GET     /static/image                          quickstart.action.PublicRootAction4
              GET     /static/image.png                      quickstart.action.PublicRootAction5
              GET     /static/index                          quickstart.action.PublicRootAction2
              GET     /static/index.html                     quickstart.action.PublicRootAction3
              

この状態で各URLにアクセスすると以下のようになりました。

URL Reponse(対応したサーバリソース)
http://localhost:8000/static PublicRootAction1
http://localhost:8000/static/index PublicRootAction2
http://localhost:8000/static/index.html static/index.html
http://localhost:8000/static/index.htm NotFoundError
http://localhost:8000/static/image PublicRootAction4
http://localhost:8000/static/image.png static/image.png
http://localhost:8000/static/file.xls NotFoundError
http://localhost:8000/static/foo static/foo/index.html
http://localhost:8000/static/foo/index NotFoundError
http://localhost:8000/static/foo/index.html static/foo/index.html

注目すべきはhttp://localhost:8000/static/fooにアクセスした場合です。
xitrumは対応するpublicリソースおよび、Actionが存在しない場合該当のパスに対応するpublicディレクトリ内のindex.htmlを探してフォールバックします。

すなわち優先順位は 静的ファイル > Action > index.htmlフォールバック > 404ということになります。

ファイル拡張子とContent-Type

上記の実験でhttp://localhost:8000/static/file.xlsにアクセスした場合404となっています。
file.xlsというファイルは、publicディレクトリ内に存在しますが何故でしょうか。

Xitrumには不要なファイル存在チェックを避けるための機能があります。
.xlsという拡張子はWEBサイトにおいて使用される拡張子として一般的ではないため、
Xitrumはファイルの存在チェックを行いません。
もし、.xls形式のファイルを配信したい場合、xitrum.confstaticFile/pathRegexの項に追加する必要があります。
デフォルトでは一般的にWEBサイトで使用される拡張子が正規表現で指定されています。

xitrum.conf
staticFile {
                # This regex is to optimize static file serving speed by avoiding unnecessary
                # file existance check. Ex:
                # - "\\.(ico|txt)$": files should end with .txt or .ico extension
                # - ".*": file existance will be checked for all requests (not recommended)
                pathRegex = "\\.(ico|jpg|jpeg|gif|png|html|htm|txt|css|js|map)$"
              

xitrum.confを以下のように修正してみます。

pathRegex = "\\.(xls|ico|jpg|jpeg|gif|png|html|htm|txt|css|js|map)$"
              

再起動後にhttp://localhost:8000/static/file.xlsにアクセスしてみます。

> curl http://localhost:8000/static/file.xls -v                                                                                                                                               11:15:45
              * About to connect() to localhost port 8000 (#0)
              *   Trying ::1...
              * Adding handle: conn: 0x7fd7dc003000
              * Adding handle: send: 0
              * Adding handle: recv: 0
              * Curl_addHandleToPipeline: length: 1
              * - Conn 0 (0x7fd7dc003000) send_pipe: 1, recv_pipe: 0
              * Connected to localhost (::1) port 8000 (#0)
              > GET /static/file.xls HTTP/1.1
              > User-Agent: curl/7.32.0
              > Host: localhost:8000
              > Accept: */*
              >
              < HTTP/1.1 200 OK
              < Connection: keep-alive
              < Cache-Control: public, max-age=31536000
              < Access-Control-Max-Age: 31536000
              < Expires: Sat, 08 Aug 2015 02:17:31 GMT
              < Content-Type: application/vnd.ms-excel
              < Content-Length: 17
              < Access-Control-Allow-Origin: *
              < Access-Control-Allow-Credentials: true
              < Access-Control-Allow-Methods: OPTIONS, GET, HEAD
              < ETag: ""IBAo8Vbj6iq2b0Njv5A1Ew""
              <
              This is file.xls
              

無事file.xlsがレスポンスされました。
この時、Content-Typeにはapplication/vnd.ms-excelというものが設定されています。file.xlsの拡張子から自動でXitrumが判定されました。
このContent-Typeの仕組みは
xitrum.util.Mineが、mime.typesを元に自動で設定してくれます。

404.htmlと500.html

@Error400@Error500のエラーアノテーションがプロジェクトで使用されていない場合、
エラー発生時にXitrumがpublicディレクトリ内の404.htmlまたは500.htmlがを自動的にレスポンスします。

エラーアノテーションも、エラーhtmlもプロジェクトで使用されていない場合、
エラー発生時には
HTTPステータスコードに404500が設定され、
レスポンスヘッダにはContent-Length:0、レスポンスボディは空というレスポンスが返却される事になります。

一時的に@404Errorをコメントアウト、404.htmlをリネームして実験してみると分かります。

最適化のためのキャッシュ、ETag、GZIPと設定ファイル

xitrum.confには静的ファイル配信の最適化のためのいくつかの設定があります。

Xitrumは静的リソースのファイルのディスクからの読み込み負荷を避けるため、
サイズに応じてファイルをメモリ上にキャッシュします。
キャッシュするサイズのしきい値と個数は、staticFilemaxSizeInKBOfCachedFilesmaxNumberOfCachedFiles
設定することができます。

maxSizeInKBOfCachedFiles = 512
              maxNumberOfCachedFiles   = 1024
              

また、クライアントサイドへファイルをキャッシュさせるためのレスポンスヘッダには、
Etagヘッダはファイルの更新日時等に応じてXitrumが自動で設定してくれます。
Etagに応じた304 Not Modifiedレスポンスも上記でキャッシュした内容に応じてXitrumが自動で判定してくれます。
また、Cache-Control:max-ageExpireもXitrumが自動で1年の期間を設定してくれます。
クライアントにEtag問い合わせ矯正したい場合、staticFilerevalidatetrueに設定します。

# true:  ETag response header is set for  static files.
              #        Before reusing the files, clients must send requests to server
              #        to revalidate if the files have been changed. Use this when you
              #        create HTML directly with static files.
              # false: Response headers are set so that clients will cache static files
              #        for one year. Use this when you create HTML from templates and use
              #        publicUrl("path/to/static/file") in templates.
              revalidate = false
              

response.autoGziptrueにセットすると、
Content-Typeがテキストベースの場合Xitrumは自動的にGZIP圧縮したレスポンスを返してくれます。
なお、この設定は、Actionがレスポンスした内容など静的ファイルに以外にも適用されます。

response {
                # Set to true to tell Xitrum to gzip big textual response when
                # request header Accept-Encoding contains "gzip"
                # http://en.wikipedia.org/wiki/HTTP_compression
                autoGzip = true
              

以上で、ルーティングおよびActionについての章は完了です。
次回からはViewの書き方について勉強します。

[Xitrumことはじめ][応用編] 1. クラスタリング: Akka/Glokka

Xitrumことはじめ (応用編)

Xitrumことはじめシリーズでは、Xitrumを使ったWebアプリケーション開発を勉強します。

目次はこちら

記事とサンプルコードはMITライセンスでgithubで公開します。

1. クラスタリング:

ブログ的な順番は崩れるけど、せっかく作ったので先に公開。 応用編の第1弾はクラスタリングについて勉強します。
Xitrumアプリケーションにおけるクラスタリングの方法は、何をクラスタリングするかによってやり方はいくつかあります。
今回はGlokkaを使用したActorのクラスタリングをやってみます。
作成したサンプルはglokka-demoにコミットしてあります。

1-1. Akka/Glokka

Glokka = Global + Akka

Akka自体にクラスタリングを実現する方法がありますが、
Glokkaはそのcluster-singletonを、Erlangのglobalモジュールのように使いやすくしてくれるライブラリです。

Xitrum自体も、SockJS機能やMetrics機能を提供するために内部的にGlokkaを使用しています。

Xitrumアプリケーション起動時のログに

[INFO] [14-08-06 18:17:16] g.Registry$: Glokka actor registry "xitrum.sockjs.SockJsAction$" starts in cluster mode
              [INFO] [14-08-06 18:17:16] g.Registry$: Glokka actor registry "metrics" starts in cluster mode
              

というログが出力されるのはこのためです。

1−1−1. Glokkaの機能

Glokkaが提供する機能はざっくりいうと、

  • クラスタ間で共有されたRegistryという領域を作成する

     val registry  = Registry.start(system, proxyName)
                  
  • Registryに名前付きでActorを登録できる

     registry ! Registry.Register(actorName, props)
                  
  • Registryから名前を指定してActorを取得できる

     registry ! Lookup(actorName)
                  

という3点になります。

1−1−2. サンプルアプリケーションの構成

   +--------------------+             +--------------------+
                 |        Xitrum      |             |        Xitrum      |
                 | +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+ |
                 | | Glokka                                            | |
                 | |             _ _ _ [ Hub Actor ] _ _ _             | |
                 | |           /                           \           | |
                 | +~~~~~~~~~~/~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\~~~~~~~~~~+ |
                 |           /        |             |        \           |
                 |  [HubClientActor]  |             |  [HubClientActor]  |
                 |         |          |             |          |         |
                 +---------|----------+             +----------|---------+
                           |                                   |
              +------------|------------+         +------------|------------+
              |  Browser                |         |  Browser                |
              |                         |         |                         |
              | var sock = SockJS       |         | var sock = SockJS       |
              | (http://localhost:8000) |         | (http://localhost:8001) |
              |                         |         |                         |
              +-------------------------+         +-------------------------+
              

今回は、このregistryにHUBとなるActorを登録して、
各ノードのActorがHUBを介してメッセージをやりとりする構成にします。
HUBに接続するActorは実際のクライアントと直接やりとりを行うものである必要はありませんが、
サンプルとして分かりやすいことや、利用用途としてこの仕組にマッチするため、
今回は各ノードのActorは実際のクライアントからのリクエストをSockJS(WebSocket)で処理する、
SockJsActionとして、以下の処理の流れがシームレスに行われるアプリケーションを作成します。

[ブラウザ①] <-> [SockJsAction①] <-> [HUB] <-> [SockJsAction②] <-> [ブラウザ②]
              

Glokkaの機能によって、各XitrumノードにおいてHUBとなるActorにメッセージを送ることができるようになります。
HUB自体はただのActorなのでどのようなメッセージを送受信し、どう振る舞うかはアプリケーションが実装する必要があります。
今回のアプリケーションでは以下のようなメッセージ設計としました。

  • Subscribe(option:Map[String, Any])

    HUBに接続するためのメッセージ
    HUBはこのメッセージを受け取ったら、senderをクライアントとして保持する
    senderにはDoneを返却する。

     [HubClientActor] - Subscribe -> [Hub Actor]
                                                       |
                                     <--- Done --------」
                  
  • Unsubscribe(option:Map[String, Any])

    HUBから離脱するためのメッセージ
    HUBはこのメッセージを受け取ったら、senderを保持しているクライアントから削除する。
    senderにはDoneを返却する。

    [HubClientActor] - Unubscribe -> [Hub Actor]
                                                      |
                                    <--- Done --------」
                  
  • Push(option:Map[String, Any])

    HUBに何かを送りつけるためのメッセージ
    HUBはこのメッセージを受け取ったら、何かしら処理を行った後に保持しているクライアントにたいしてPublish(option:Map[String, Any])を送信する。
    senderにはDoneを返却する。

    [HubClientActor] - Push -> [Hub Actor] ---- Publish ---> [(another) HubClient Actor]
                                                |          \
                                                |           ---- Publish ---> [(another) HubClient Actor]
                                   <--- Done ---」           \
                                                              --- Publish ---> [(another) HubClient Actor]
                  
  • Pull(option:Map[String, Any])

    HUBにから情報を引き出すためのメッセージ
    HUBはこのメッセージを受け取ったら、何かしら処理を行った後にsenderに対して結果をDone(option:Map[String, Any])として送信する。
    senderにはDoneを返却する。

    [HubClientActor] - Pull -> [Hub Actor]
                                                 |
                                   <--- Done ----」
                  

では早速アプリケーションを作成します。
アプリの雛形にはXitrum-newを使用します。

1−1−3. Registryの作成

HUB.scala
object Hub {
                val KEY_PROXY = "HUB_PROXY"
                // Glokka registry
                val actorRegistry = Registry.start(Config.actorSystem, KEY_PROXY)
              
                // To force start registry at process start up,
                // Call this method at `main` before start `xitrum.Server`
                def start(){}
              }
              

Registryの作成にはactorSystemとプロキシ名を指定します。
actorSystemはXitrumが内部で使用している(Config.actorSystem)ものをそのまま流用できます。
アプリケーション開始時にレジストリーを確実にスタートさせるために、
xitrum.Server.start()の前にHub.start()を呼び出します。

Boot.scala
object Boot {
                def main(args: Array[String]) {
                  Hub.start()
                  Server.start()
                }
              }
              

1-1-4. RegistryへのHUBの登録と取得

作成したRegistryへHubとなるActorを登録、または取得するための処理は以下のようにしました。
複数の用途に使いまわせるように、traitとしています。

HUB.scala
trait HubClient extends Actor {
                protected lazy val node = self.toString
                def lookUpHub(key: String, hubProps: Props, option: Any = None) {
                  xitrum.Log.debug(s"[HubClient][${node}] Searching HUB node...")
                  Hub.actorRegistry ! Registry.Register(key, hubProps)
                  context.become {
                    case result: Registry.FoundOrCreated => doWithHub(result.ref, option)
                    case ignore =>
                      xitrum.Log.warn(s"[HubClient][${node}] Unexpected message: $ignore")
                  }
                }
              
                // Implement these method as you like
                def doWithHub(publisher: ActorRef, option: Any)
              }
              

Registryに対してRegistry.Registerというメッセージで名前とPropsを指定すると、
GlokkaはRegistry内に指定した名前のActorが存在しなければ、Propsを元に新しく登録したものを、
既に指定の名前で登録されたActorが存在する場合、それを返却してくれます。
いずれの場合Registry.FoundOrCreatedというメッセージとなります。

1-1-5. Hubの実装

Registryに登録するHubとなるActorは以下のようにしました。
Subscribe(またはUnsubscribe)メッセージに応じて、senderwatch(またはunwatch)し、clientsに保持(または削除)します。
PushPullメッセージを受け取った際は何かしらの処理handlePushhandlePullを行い、
他のクライアントにPublishメッセージを送信したり、senderDoneを返却します。
こちらについてもtraitとして、メッセージの型だけで判定しています。
具体的な処理はoption[Map[String,Any]]を元にアプリケーションの各ロジックで実装します。

watchしているActor(この場合はclientsの1つ)が死んだ場合、Terminatedというメッセージを受け取るため、
その場合も該当のclientを削除します。

Hub.scala
trait Hub extends Actor {
                protected var clients = Seq[ActorRef]()
                private lazy val node = self.toString
              
                def receive = {
                  case Push(option) =>
                    xitrum.Log.debug(s"[Hub][${node}] Received Push request")
                    val result = handlePush(option)
                    clients.foreach { client =>
                      if (client != sender) client ! Publish(result)
                    }
                    sender ! Done(result)
              
                  case Pull(option) =>
                    xitrum.Log.debug(s"[Hub][${node}] Received Pull request")
                    sender ! Done(handlePull(option))
              
                  case Subscribe(option) =>
                    xitrum.Log.debug(s"[Hub][${node}] Received Subscribe request")
                    clients = clients.filterNot(_ == sender) :+ sender
                    context.watch(sender)
                    sender ! Done(option)
              
                  case UnSubscribe(option) =>
                    xitrum.Log.debug(s"[Hub][${node}] Received UnSubscribe request")
                    clients =  clients.filterNot(_ == sender)
                    context.unwatch(sender)
                    sender ! Done(option)
              
                  case Terminated(client) =>
                    xitrum.Log.debug(s"[Hub][${node}] Received Terminated event"+client.toString)
                    clients = clients.filterNot(_ == client)
              
                  case ignore =>
                    xitrum.Log.warn(s"[Hub][${node}] Unexpected message: $ignore")
                }
              
                // Implement these method as you like
                def handlePush(msg: Map[String, Any]): Map[String, Any]
                def handlePull(option: Map[String, Any]): Map[String, Any]
              }
              

Hubのベースとなる振る舞いは以上のとおりで、
実際にHubに処理してもらう内容を以下のようにしました。(ちょっと保存したファイルは微妙ですが...)
Pushメッセージを受け取った際には、option内のcmdに応じて処理を返します。
今回は"text"というコマンドの場合、その内容を転送します。
それ以外の場合はエラーとして転送します。HUBはPushの処理結果を全てにclientに
Publishで転送するので、受け取ったclientが無視できるように(後述)targetsというフィールドに空文字を指定しておきます。

HubClientActor.scala
class HubImpl extends Hub {
                override def handlePush(msg: Map[String, Any]):  Map[String, Any] = {
                  msg.getOrElse("cmd", "invalid") match {
                    case "text" =>
                      Map(
                        "error"   -> SUCCESS,
                        "seq"       -> msg.getOrElse("seq", -1),
                        "targets"   -> msg.getOrElse("targets", "*"),
                        "tag"       -> "text",
                        "body"      -> msg.getOrElse("body",""),
                        "senderName"-> msg.getOrElse("senderName","Anonymous"),
                        "senderId"  -> msg.getOrElse("senderId","")
                      )
              
                    case unknown =>
                      Map(
                        "tag"     -> "system",
                        "error"   -> INVALID_CMD,
                        "seq"     -> msg.getOrElse("seq", -1),
                        "targets" -> msg.getOrElse("uuid", "")
                      )
                  }
                }
              
                override def handlePull(msg: Map[String, Any]):  Map[String, Any] = {
                  msg.getOrElse("cmd", "invalid") match {
                    case "count" =>
                      Map(
                        "tag"     -> "system",
                        "error"   -> SUCCESS,
                        "seq"     -> msg.getOrElse("seq", -1),
                        "count"   -> clients.size
                      )
                    case unknown =>
                      Map(
                        "tag"     -> "system",
                        "error"   -> INVALID_CMD,
                        "seq"     -> msg.getOrElse("seq", -1)
                      )
                  }
                }
              }
              

1-1-6. HubClientの実装

Hubに対して接続するHubClientの実際の処理の流れは以下のようになります。

  • クライアント(ブラウザ)からのリクエスト(socket.open)を受け付ける
  • Xitrum内でActionが生成される
  • executeメソッドが呼び出される
  • 認証処理(checkAPIKey)を行う(これはHUBの機能とは直接関係ありません)
  • 認証OKの場合、HUBを探す(lookUpHub(hubKey, hubProps, parsed))
    このActionで使用するHubは以下のようになります。
    private val hubKey    = "glokkaExampleHub"
                  private val hubProps  = Props[HubImpl]
                  
  • HUBが見つかったあと(override def doWithHub)は、
    • クライアントからのメッセージは、tagを解析してHUBへ送る
    • HUBからのメッセージはtargetsを解析してクライアントへ送る

SockJS(WebSocket)クライアントとのやりとりは全てJSON形式で行うようにしています。
クライアントとのやりとりには、tagcmdseqなどのキーをAPIとして
アプリに応じてAPIとして定義すると良いと思います。

@SOCKJS("connect")
              class HubClientActor extends SockJsAction with HubClient {
                private val hubKey    = "glokkaExampleHub"
                private val hubProps  = Props[HubImpl]
              
                def execute() {
                  log.debug(s"[HubClient][$node] is assigned to client")
                  checkAPIKey()
                }
              
                private def checkAPIKey() {
                  context.become {
                    case SockJsText(msg) =>
                      log.debug(s"[HubClient][$node] Received first frame from client")
                      parse2MapWithTag(msg) match {
                        case ("login", parsed) =>
                          if (Utils.auth(parsed.getOrElse("apikey", ""))) {
                            lookUpHub(hubKey, hubProps, parsed)
                          } else {
                            respondSockJsTextWithLog(
                              parse2JSON(
                                Map(
                                  "error"   -> INVALID_APIKEY,
                                  "tag"     -> "system",
                                  "seq"     -> parsed.getOrElse("seq", -1),
                                  "message" -> "Invalid api key"
                                )
                              )
                            )
                            log.debug(s"Auth error: ${parsed.toString}")
                            respondSockJsClose()
                          }
                        case (_, parsed) =>
                          respondSockJsTextWithLog(
                            parse2JSON(
                              Map(
                                "error"   -> NOT_CONNECTED,
                                "tag"     -> "system",
                                "seq"     -> parsed.getOrElse("seq", -1),
                                "message" -> "First frame must be `login` request"
                              )
                            )
                          )
                          log.debug(s"Unexpected first frame: ${parsed.toString}")
                          respondSockJsClose()
                      }
              
                    case ignore =>
                      log.warn(s"Unexpected message: ${ignore}")
                  }
                }
              
                override def doWithHub(hub: ActorRef, option: Any) {
                  val loginRequest = option.asInstanceOf[Map[String, Any]]
                  val name         = loginRequest.getOrElse("name", "Anonymous").toString
                  val uuid         = node //Utils.md5(node)
              
                  log.debug(s"[HubClient][${uuid}] HUB found: " + hub.toString)
              
                  // Start subscribing
                  log.debug(s"[HubClient][${uuid}] Start Subscribing HUB")
                  hub ! Subscribe()
                  context.watch(hub)
              
                  respondSockJsText(
                    parse2JSON(
                      Map(
                        "error"   -> SUCCESS,
                        "seq"     -> loginRequest.getOrElse("seq", -1),
                        "tag"     -> "system",
                        "node"    -> node,
                        "hub"     -> hub.toString,
                        "uuid"    -> uuid,
                        "message" -> s"Welcome ${name}!. Your uuid is ${uuid}"
                      )
                    )
                  )
              
                  context.become {
              
                    // (AnotherNode -> ) Hub -> LocalNode
                    case Publish(msg) =>
                      if (!msg.isEmpty) {
                        log.debug(s"[HubClient][${uuid}] Received Publish message from HUB")
                        msg.getOrElse("targets", "*") match {
                          case list:Array[String] if (list.contains(uuid)) =>
                            // LocalNode -> client
                            respondSockJsTextWithLog(parse2JSON(msg - ("error", "seq", "targets")))
                          case targetId:String if (targetId == uuid) =>
                            // LocalNode -> client
                            respondSockJsTextWithLog(parse2JSON(msg - ("error", "seq", "targets")))
                          case "*" =>
                            // LocalNode -> client
                            respondSockJsTextWithLog(parse2JSON(msg - ("error", "seq", "targets")))
                          case ignore =>
                        }
                      }
              
                    // (LocalNode ->) Hub -> LocalNode
                    case Done(result) =>
                      log.debug(s"[HubClient][${uuid}] Received Done message from HUB")
                      if (!result.isEmpty) respondSockJsTextWithLog(parse2JSON(result + ("tag" -> "system")))
              
                      // Client -> LocalNode
                    case SockJsText(msg) =>
                      log.debug(s"[HubClient][${uuid}] Received message from client: $msg")
                      parse2MapWithTag(msg) match {
                        case ("subscribe", parsed) =>
                          // LocalNode -> Hub (-> LocalNode)
                          log.debug(s"[HubClient][${uuid}] Send Subscribe request to HUB")
                          hub ! Subscribe(Map(
                                              "error"   -> SUCCESS,
                                              "tag"     -> "system",
                                              "seq"     -> parsed.getOrElse("seq", -1)
                                            ))
              
                        case ("unsubscribe", parsed) =>
                          // LocalNode -> Hub (-> LocalNode)
                          log.debug(s"[HubClient][${uuid}] Send UnSubscribe request to HUB")
                          hub ! UnSubscribe(Map(
                                              "error"   -> SUCCESS,
                                              "tag"     -> "system",
                                              "seq"     -> parsed.getOrElse("seq", -1)
                                            ))
              
                        case ("pull", parsed) =>
                          // LocalNode -> Hub (-> LocalNode)
                          log.debug(s"[${uuid}] Send Pull request to HUB")
                          hub ! Pull(parsed + ("uuid" -> uuid))
              
                        case ("push", parsed) =>
                          // LocalNode -> Hub (-> AnotherNode)
                          log.debug(s"[HubClient][${uuid}] Send Push request to HUB")
                          hub ! Push(parsed + ("senderName" -> name, "senderId" -> uuid))
              
                        case (invalid, parsed) =>
                          // LocalNode -> client
                          respondSockJsTextWithLog(
                            parse2JSON(
                              Map(
                                "error"   -> INVALID_TAG,
                                "tag"     -> "system",
                                "seq"     -> parsed.getOrElse("seq", -1),
                                "message" -> s"Invalid tag:${invalid}. Tag must be `subscribe` or `unsubscribe` or `pull` or `push`."
                              )
                            )
                          )
                      }
              
                    case Terminated(hub) =>
                      log.warn("Hub is terminatad")
                      // Retry to lookup hub
                      Thread.sleep(100L * (scala.util.Random.nextInt(3) + 1))
                      lookUpHub(hubKey, hubProps, option)
              
                    case ignore =>
                      log.warn(s"Unexpected message: $ignore")
                  }
                }
              
                private def parse2MapWithTag(jsonStr: String): (String, Map[String, String]) = {
                  SeriDeseri.fromJson[Map[String, String]](jsonStr) match {
                    case Some(json) =>
                        (json.getOrElse("tag", "invalidTag"), json)
                    case None =>
                      log.warn(s"Failed to parse request: $jsonStr")
                      ("invalid", Map.empty)
                  }
                }
              
                private def parse2JSON(ref: AnyRef) = SeriDeseri.toJson(ref)
              
                private def respondSockJsTextWithLog(text: String):Unit = {
                  log.debug(s"[HubClient][${node}] send message to client")
                  respondSockJsText(text)
                }
              }
              

1-1-7. JavaScriptクライアントの実装

サーバ側の実装が終わったので、
視覚的にわかるようにJavaScriptクライアントを書きます。
今回はGlokkaによるクラスタリングの理解が目的のため、JavaScriptはとりあえずの実装です。

JavaScript側からはSockJSで

  • HubClientActor(/connect)に接続する
  • 接続したら認証用のリクエストを送る
  • 認証完了後、ボタンに応じて各種コマンドを送る
  • サーバ側からのメッセージ受信時は"tag"や"seq"に応じてコールバックを行う

という流れです。
クライアント側でクラスタリングを意識することはありません。
自分が見ているhtmlと同一のホストに対して接続しているだけで、CORS等も必要としません。
テンプレートファイルにjsAddToViewを使用して直接JavaScriptを書いてあります。

SiteIndex.jade
"var url = '" + sockJsUrl[HubClientActor] + "';" +
              """
              var socket;
              var counter = -1;
              var callbacks = {};
              
              $("#btn_connect").on("click",function(e){
                e.preventDefault();
                var loginRequest = {
                  tag:"login",
                  apikey:$("#api").val(),
                  name:$("#name").val(),
                  seq:counter
                }
                callbacks[counter] = function(obj){
                  var text;
                  if(obj.error === 0){
                    text = '<b>[Success: Connect with HUB]</b><br />';
                    text = text+"hub-node: " + obj.hub +'<br />';
                    text = text+"your-node: " + obj.node +'<br />';
                    $("#controller").show();
                    $("login").hide();
                  } else {
                    text = '<b style="color:red">[Fail: Connect with HUB]</b>'+obj.message+'<br />';
                  }
                  xitrum.appendAndScroll('#output', text);
                }
                socket.send(JSON.stringify(loginRequest));
                counter++;
              });
              
              $("#btn_send").on("click",function(e){...
              
              //省略
              
              var initSocket = function() {
                socket = new SockJS(url);
                socket.counter = 0;
              
                socket.onopen = function(event) {
                  var text = '<b>[Socket is open]</b><br />';
                  xitrum.appendAndScroll('#output', text);
                };
              
                socket.onclose = function(event) {
                  var text = '<b>[Socket is closed]</b><br />';
                  xitrum.appendAndScroll('#output', text);
                  $('#controller').hide();
                };
              
                socket.onmessage = function(event) {
                  var obj = JSON.parse(event.data);
                  var text;
                  if (obj.tag === "system") {
                    text = '<b>[SYSTEM MESSAGE from HUB]</b><br />';
                  } else {
                    text = '<b>['+obj.tag.toUpperCase()+' from '+ obj.senderId+' via HUB]</b><br />';
                  }
                  xitrum.appendAndScroll('#output', text);
                  text = '- ' + xitrum.escapeHtml(event.data) + '<br />';
                  xitrum.appendAndScroll('#output', text);
                  if (typeof callbacks[obj.seq] === "function") callbacks[obj.seq](obj);
                };
              };
              initSocket();
              

1-1-8. クラスタリング設定

今回は2つのインスタンスを使用するので使用するportがかぶらないように、
akka_forN.confとxitrum_forN.confをそれぞれ作成し、
アプリ起動時にapplication.confを修正して対象の設定ファイルを読み込むようにします。

Akkaのクラスタリングを有効にする設定はakka_forN.confに記載します。
remoteの項には自身が使用するポートを、
clusterseed-nodesには自身とクラスタリングする別のインスタンスを指定します。
Xitrumアプリケーションの場合actorSystemは、Config.actorSystemxitrumという名前のため
"akka.tcp://ClusterSystem@host:port"のClusterSystemは"xitrum"となります。

akka_for1.conf
# Config Akka cluster if you want distributed SockJS
              akka {
                loggers = ["akka.event.slf4j.Slf4jLogger"]
                logger-startup-timeout = 30s
              
                 actor {
                   provider = "akka.cluster.ClusterActorRefProvider"
                 }
              
                 # This node
                 remote {
                   log-remote-lifecycle-events = off
                   netty.tcp {
                     hostname = "127.0.0.1"
                     port = 2551  # 0 means random port
                   }
                 }
              
                 cluster {
                   seed-nodes = [
                     "akka.tcp://xitrum@127.0.0.1:2551",
                     "akka.tcp://xitrum@127.0.0.1:2552"]
              
                   auto-down-unreachable-after = 10s
                 }
              }
              

HTTPサーバも起動するポートがかぶらないようにそれぞれ設定します。

xitrum_forN.conf
# Comment out the one you don't want to start
              port {
                http              = 8000
                https             = 4430
                #flashSocketPolicy = 8430  # flash_socket_policy.xml will be returned
              }
              

1-1-9. アプリケーションの実行

sbt/sbt xitrum-packageでパッケージ化したら、2箇所にコピーします。

sbt/sbt xitrum-package
                cd /path/to/temp
                cp -r /path/to/glokka-demo/target/xitrum node1
                cp -r /path/to/glokka-demo/target/xitrum node2
              

node2の方はakka_for2.confとxitrum_for2.confを読み込むようにconfig/application.confを修正します。
準備ができたらそれぞれのアプリを起動します。

script/runner glokka.demo.Boot
              

実際に操作してnode1とnode2のクライアントがメッセージをやりとりできれば完成です。

glokka_cluster

[Xitrumことはじめ][基本編] 5.ルーティングを追加する: ActionとURL

Xitrumことはじめ (基本編)

Xitrumことはじめシリーズでは、Xitrumを使ったWebアプリケーション開発を勉強します。

目次はこちら

記事とサンプルコードはMITライセンスでgithubで公開します。

5. ルーティングを追加する:

前回はHTTPメソッドとActionの関連付けを確認したので、
今回はURLとActionの関連について見ていきます。

公式ドキュメントは以下のページが参考になります。

5-2. ActionとURL

Xitrumのソースを見るとAnnotationの実態はcase classでpathというフィールドを設定できることがわかります。

https://github.com/xitrum-framework/xitrum/blob/master/src/main/scala/xitrum/annotation/Routes.scala#L9-L13

package xitrum.annotation
              
              import scala.annotation.StaticAnnotation
              
              sealed trait Route        extends StaticAnnotation
              sealed trait Error        extends StaticAnnotation
              sealed trait RouteOrder   extends StaticAnnotation
              
              case class GET   (paths: String*) extends Route
              case class POST  (paths: String*) extends Route
              case class PUT   (paths: String*) extends Route
              case class PATCH (paths: String*) extends Route
              case class DELETE(paths: String*) extends Route
              

前回 httpの各メソッドに対応するActionは全てPOST("httpcrud")など、
httpcrudという引数のアノテーションを指定しました。

その結果それぞれのルートは、

[INFO] Normal routes:
              GET     /          quickstart.action.SiteIndex
              GET     /httpcrud  quickstart.action.getIndex
              POST    /httpcrud  quickstart.action.postIndex
              PUT     /httpcrud  quickstart.action.putIndex
              PATCH   /httpcrud  quickstart.action.patchIndex
              DELETE  /httpcrud  quickstart.action.deleteIndex
              

という風に、/httpcrudというURLに対応してルーティングテーブルに登録されました。
ではこのpaths: String*という引数にはどのようなものが指定できるのかをXitrumガイドを中心に見ていきます。

rootExample.scala

通常パターン

まず通常のパターンは、SiteIndexや、httpcrudの各Actionのように単一の文字列を指定するパターンとなります。
この場合、/<指定された文字列> というURLがActionに対応するルートとなります。

文字列中に/を含むことでURLに階層を持たせる事もできます。

@GET("/path/to/myaction")
              class MyAction extends ClassNameResponder {
                def execute() {
                  log.debug("MyAction")
                  respondClassNameAsText()
                }
              }
              
複数パスの関連付け

続いて、複数の文字列をアノテーションで指定するパターンでは、それぞれのURLを同じActionが対応するというルートになります。

@GET("one" ,"two")
              class MultiPathAction extends ClassNameResponder {
                def execute() {
                  log.debug("MultiPathAction")
                  respondClassNameAsText()
                }
              }
              
.(ドット)を含むルート

パスに.(ドット)を含むことも可能です。

@GET("/dot.html")
              class DotInPathAction extends ClassNameResponder {
                def execute() {
                  log.debug("DotInPathAction")
                  respondClassNameAsText()
                }
              }
              
pathParam

Xitrumにはリクエストスコープの1つにpathParamsというものがあります。
リクエストスコープやパラメータ取得について詳しくは今後掘り下げますが、ルーティングという視点から、pathParamをどう定義することができるかを確認します。

pathParamを定義するには:(コロン)で区切ります。
以下の例の場合、/item/lsit/1/item/list/mobileというURLがItemListActionにルーティングされることになります。

@GET("/item/list/:categoryId")
              class ItemListAction extends ClassNameResponder {
                def execute() {
                  log.debug("ItemListAction")
              
                  // pathParamの取得
                  val categoryId = param("categoryId")
                  log.debug(categoryId)
              
                  respondClassNameAsText()
                }
              }
              
ルートの優先順位

同じHTTPメソッドに対するルーティングが競合した場合に優先順位を指定することができます。

例えば以下の例では、/item/{カテゴリーID}/{アイテムID}というルーティングを期待しています。

@GET("/item/:categoryId/:itemId")
              class ItemDetailAction extends ClassNameResponder {
                def execute() {
                  log.debug("ItemDetailAction")
              
                  // pathParamの取得
                  val categoryId = param("categoryId")
                  log.debug(categoryId)
              
                  val itemId = param("itemId")
                  log.debug(itemId)
              
                  respondClassNameAsText()
                }
              }
              

しかし、一つ前のサンプルのItemListActionのルーティングである、/item/list/mobileというパスにアクセスした場合、
listという文字列がカテゴリーIDとして認識される可能性があります。

ためしに、/item/list/10に何度かリクエストを投げてみますと、以下のように同じURLに対してその時によって異なるActionが実行されていることがわかります。

[DEBUG] ItemDetailAction
              [INFO] 0:0:0:0:0:0:0:1 GET /item/list/10 -> quickstart.action.ItemDetailAction, pathParams: {categoryId: list, itemId: 10} -> 304, 1 [ms]
              [DEBUG] ItemListAction
              [INFO] 0:0:0:0:0:0:0:1 GET /item/list/10 -> quickstart.action.ItemListAction, pathParams: {categoryId: 10} -> 200, 1 [ms]
              

このような事故を避けるために、Xitrumには@First@Lastアノテーションが用意されています。
今回の場合、/item/list/で始まる場合は、ItemListActionを優先したいため、ItemListAction@Firstを指定します。(または、ItemDetailAction@Lastを指定)。

これによって、/item/listで始まるURLはItemListActionが優先的に使用される事になります。
同じURLで3つのActionに対応させる必要がある場合は、@Firstを最も優先すべきActionに、@Lastを最も優先度が低いActionに、残った1つのActionには優先度は指定しないことで実現できます。
先ほどのように優先度を指定しなかった場合の処理はどのActionが使用されるかは神のみぞ知るという状態です。
すなわち、4つ以上のActionを同じURLで処理するには、Xitrumでは優先度の判定手段がありません。その場合URL設計を見なおした方が良いということですね。

正規表現によるpathParamの指定

pathParamに正規表現でフィルターを追加することも可能です。
正規表現のフィルターを使用するには<>をpathParamに続けて指定します。
以下の例では、前述のItemDetailActionItemListActionの関係と全く同じAnimalDetailActionAnimalListActionがあります。
ただし、AnimalDetailActionのcategoryIdは数字であることを制限する正規表現が追加されています。

@GET("animal/:categoryId<[0-9]+>/:animalId")
              class AnimalDetailAction extends ClassNameResponder {
                def execute() {
                  log.debug("AnimalDetailAction")
              
                  // pathParamの取得
                  val categoryId = param("categoryId")
                  log.debug(categoryId)
              
                  val animalId = param("animalId")
                  log.debug(animalId)
              
                  respondClassNameAsText()
                }
              }
              
              @GET("/animal/list/:categoryId")
              class AnimalListAction extends ClassNameResponder {
                def execute() {
                  log.debug("AnimalListAction")
              
                  // pathParamの取得
                  val categoryId = param("categoryId")
                  log.debug(categoryId)
              
                  respondClassNameAsText()
                }
              }
              

上記の場合ルーティングは以下のように行われます。

url Action
http://localhost:8000/animal/10/1001 AnimalDetailAction
http://localhost:8000/animal/10/monkey AnimalDetailAction
http://localhost:8000/animal/list/1001 AnimalListAction
http://localhost:8000/animal/list/monkey AnimalListAction
http://localhost:8000/animal/fish/tuna ErrorAction(404エラー)

fishのようにcategoryIdがの正規表現にマッチしないリクエストの場合、該当のルートが見つからないためXitrumは404を返します。


以上ActionとURLについてでした。
次回はAction以外のリソースについてのルーティングとURLについて見て行きたいと思います。

[Xitrumことはじめ][基本編] 5.ルーティングを追加する: ActionとHTTPメソッドアノテーション

Xitrumことはじめ (基本編)

Xitrumことはじめシリーズでは、Xitrumを使ったWebアプリケーション開発を勉強します。

目次はこちら

記事とサンプルコードはMITライセンスでgithubで公開します。

5. ルーティングを追加する:

今回から実際にソースコードを作成して、Xitrumアプリケーション開発を行います。
まずは、Webアプリケーションとして画面表示やAPIのエンドポイントとなるルーティングを追加していきます。

公式ドキュメントは以下のページが参考になります。

5-1. ActionとHTTPメソッドアノテーション

Xitrumの特徴の1つにルートの自動収集があります。

JAX-RSとRailsエンジンの思想に基づく自動ルートコレクション。全てのルートを1箇所に宣言する必要はありません。 この機能は分散ルーティングと捉えることができます。この機能のおかげでアプリケーションを他のアプリケーションに取り込むことが可能になります。 もしあなたがブログエンジンを作ったならそれをJARにして別のアプリケーションに取り込むだけですぐにブログ機能が使えるようになるでしょう。 ルーティングには更に2つの特徴があります。 ルートの作成(リバースルーティング)は型安全に実施され、 Swagger Doc を使用したルーティングに関するドキュメント作成も可能となります。

アプリケーションがルートを追加する場合、RoRのconfig/routes.rbのような設定ファイルは特に必要ありません。
xitrum.Actionを継承して、アノテーションを宣言することでそれがWebアプリケーションのルートの一つに成ります。

HTTPメソッドに対応したクラスを作成します。

quickstart.action.httpCRUD.scala

package quickstart.action
              
              import xitrum.Action
              import xitrum.annotation.{GET, POST, PUT, DELETE, PATCH}
              
              trait HttpCRUD extends Action {
                // この処理は現在のクラス名をtextをレスポンスとして返す
                def respondClassNameAsText(){
                  respondText(getClass)
                }
              }
              
              @GET("/httpcrud")
              class GetIndex extends HttpCRUD {
                def execute() {
                  log.debug("getIndex")
                  respondClassNameAsText()
                }
              }
              
              @POST("/httpcrud")
              class PostIndex extends HttpCRUD {
                def execute() {
                  log.debug("postIndex")
                  respondClassNameAsText()
                }
              }
              
              @PUT("/httpcrud")
              class PutIndex extends HttpCRUD {
                def execute() {
                  log.debug("putIndex")
                  respondClassNameAsText()
                }
              }
              
              @DELETE("/httpcrud")
              class DeleteIndex extends HttpCRUD {
                def execute() {
                  log.debug("deleteIndex")
                  respondClassNameAsText()
                }
              }
              
              @PATCH("/httpcrud")
              class PatchIndex extends HttpCRUD {
                def execute() {
                  log.debug("patchIndex")
                  respondClassNameAsText()
                }
              }
              

各HTTPメソッドに対応したActionを作成しました。
サーバ側のログと、クライアントへのレスポンスに現在実行されているAction名を出力する簡単な例です。

Xitrumを起動します。

sbt/sbt run

Xitrumを起動すると、以下の様なログが表示されます。

[INFO] Normal routes:
              GET     /          quickstart.action.SiteIndex
              GET     /httpcrud  quickstart.action.GetIndex
              POST    /httpcrud  quickstart.action.PostIndex
              PUT     /httpcrud  quickstart.action.PutIndex
              PATCH   /httpcrud  quickstart.action.PatchIndex
              DELETE  /httpcrud  quickstart.action.DeleteIndex
              

各HTTPメソッドに対応したActionがルーティングテーブルに追加されました。

リクエストを投げてみます。

> curl http://localhost:8000/httpcrud                                                                                                                               10:32:42
              class quickstart.action.GetIndex%
              

getIndexが実行されました。

サーバ側のログ(sbt/sbt run コンソール)には

[DEBUG] GetIndex
              [INFO] 0:0:0:0:0:0:0:1 GET /httpcrud -> quickstart.action.GetIndex -> 200, 2 [ms]
              

と表示されました。
1行目はプログラム中で記載したlog.debugによる出力。
2行目はXitrumデフォルトのアクセスログとなります。

次に別のHTTPメソッドを投げてみます。

> curl -X POST http://localhost:8000/httpcrud                                                                                                                               10:32:42
              Missing param: csrf-token%
              

csrf-tokenが無いという文字列が帰ってきました。

サーバ側のログ(sbt/sbt run コンソール)には

[INFO] 0:0:0:0:0:0:0:1 POST /httpcrud -> quickstart.action.PostIndex -> 400, 34 [ms]
              

とあります。HTTPステータスコードは400となっています。
これはXitrumがデフォルトでCSRF対策を行っているため、curlで実施したリクエストにトークンが含まれないことに起因します。

https://github.com/xitrum-framework/xitrum/blob/b360234713562409e5a3d00ebdd0d9deb8664953/src/main/scala/xitrum/Action.scala#L85-L90

if ((request.getMethod == HttpMethod.POST ||
                   request.getMethod == HttpMethod.PUT ||
                   request.getMethod == HttpMethod.PATCH ||
                   request.getMethod == HttpMethod.DELETE) &&
                  !isInstanceOf[SkipCsrfCheck] &&
                  !Csrf.isValidToken(this)) throw new InvalidAntiCsrfToken
              

Xitrumが発行するCSRFトークンについては、フォーム画面作成時に詳しく見ていきますので、
ここではこれを一旦無効にします。

// import xitrum.Action
              import xitrum.{Action, SkipCsrfCheck}
              
              //class PostIndex extends HttpCRUD {
              class PostIndex extends HttpCRUD with SkipCsrfCheck {
              

SkipCsrfCheckというtraitをimportして、with 句でそれを継承します。
XitrumによるCSRFチェックが有効になるHTTPメソッドは、POST,PUT,PATCH,DELETEであるため、
GET以外のリクエストを扱うActionにそれぞれ追記します。

修正を反映するにはXitrumを再起動します。
Ctrl +cで プロセスを停止し、再度sbt/sbt runとします。
ただし、ちょっとしたソース修正の度に毎回再起動を行うと時間がとてもかかってしまいますので、
次からはソース修正時にXitrumの再起動をいちいち行わなくて済むようにします。
DCEVMをalternativeインストール使用している場合は、sbt/sbtファイルに-XXaltjvm=dcevmオプションを追記します。

ターミナルウィンドウを2つ用意して
1つ目は

sbt/sbt run

もう一方は

sbt/sbt ~compile (zshを使用している場合は sbt/sbt "~compile")

とします。~をつけてコンパイルを実行した場合、sbtがファイルを監視してコンパイルを自動で実行してくれます。
コンパイルされたclassはAgent7によって稼働中のアプリケーションにロードされます。

では再びリクエストを行ってみます。

curl -X POST http://localhost:8000/httpcrud                                                                                                                       11:21:15
              class quickstart.action.PostIndex%
              

POSTリクエストにはpostIndexが動作していることが確認できました。

HEADリクエストとOPTIONSリクエストについて

ここまでCRUD操作を行うHTTPメソッドに対するアノテーションとルーティングを見てきましたが、
HEADメソッドとOPTIONメソッドについてはどうでしょうか。

まず、HEADメソッドについては、Xitrumではbodyの無いGETメソッドとして扱われます。

curl -X HEAD http://localhost:8000/httpcrud -v                                                                                                                    11:24:17
              * Adding handle: conn: 0x7ff049003a00
              * Adding handle: send: 0
              * Adding handle: recv: 0
              * Curl_addHandleToPipeline: length: 1
              * - Conn 0 (0x7ff049003a00) send_pipe: 1, recv_pipe: 0
              * About to connect() to localhost port 8000 (#0)
              *   Trying ::1...
              * Connected to localhost (::1) port 8000 (#0)
              > HEAD /httpcrud HTTP/1.1
              > User-Agent: curl/7.30.0
              > Host: localhost:8000
              > Accept: */*
              >
              < HTTP/1.1 200 OK
              < Connection: keep-alive
              < Content-Type: text/plain; charset=UTF-8
              < Content-Length: 32
              <
              

サーバ側のログではgetIndexが動作していることがわかります。

[INFO] 0:0:0:0:0:0:0:1 HEAD /httpcrud -> quickstart.action.GetIndex -> 200, 3 [ms]
              

OPTIONSメソッドについては、主にSOCKJSのCORS対応目的でXitrumには実装されています。
デフォルトではCORSは無効となっています。

curl -X OPTIONS http://localhost:8000/httpcrud -v 11:32:11

  • Adding handle: conn: 0x7fc0f1803c00
  • Adding handle: send: 0
  • Adding handle: recv: 0
  • Curl_addHandleToPipeline: length: 1
    • Conn 0 (0x7fc0f1803c00) send_pipe: 1, recv_pipe: 0
  • About to connect() to localhost port 8000 (#0)
  • Trying ::1...
  • Connected to localhost (::1) port 8000 (#0)

    OPTIONS /httpcrud HTTP/1.1
    User-Agent: curl/7.30.0
    Host: localhost:8000
    Accept: /

    < HTTP/1.1 204 No Content
    < Connection: keep-alive
    < Cache-Control: public, max-age=31536000
    < Access-Control-Max-Age: 31536000
    < Expires: Mon, 03 Aug 2015 02:32:15 GMT
    < Content-Length: 0
    <

  • Connection #0 to host localhost left intact

サーバ側のログ

[INFO] OPTIONS /httpcrud
              

CORS対応を有効にするには、
xitrum.conf内の、corsAllowOriginsに配列形式で許可するoriginを記載します。全てのサイトを許可する場合*を指定します。
デフォルトではコメントアウトされているので有効にします。

corsAllowOrigins = ["*"]
              

configファイルを修正した場合はXitrumの再起動が必要となるため、
sbt/sbt runを実行しているターミナルから再起動します。

OPTIONSリクエストをもう一度投げてみます。

curl -X OPTIONS http://localhost:8000/httpcrud -v                                                                                                                 11:32:15
              * Adding handle: conn: 0x7fd32180e600
              * Adding handle: send: 0
              * Adding handle: recv: 0
              * Curl_addHandleToPipeline: length: 1
              * - Conn 0 (0x7fd32180e600) send_pipe: 1, recv_pipe: 0
              * About to connect() to localhost port 8000 (#0)
              *   Trying ::1...
              * Connected to localhost (::1) port 8000 (#0)
              > OPTIONS /httpcrud HTTP/1.1
              > User-Agent: curl/7.30.0
              > Host: localhost:8000
              > Accept: */*
              >
              < HTTP/1.1 204 No Content
              < Connection: keep-alive
              < Cache-Control: public, max-age=31536000
              < Access-Control-Max-Age: 31536000
              < Expires: Mon, 03 Aug 2015 02:37:08 GMT
              < Access-Control-Allow-Origin: *
              < Access-Control-Allow-Credentials: true
              < Access-Control-Allow-Methods: OPTIONS, GET, HEAD, POST, PUT, PATCH, DELETE
              < Content-Length: 0
              <
              * Connection #0 to host localhost left intact
              

CORS対応したレスポンスヘッダーが帰ってくることが確認できました。


ここまでHTTPメソッドとActionのルーティングについて見てきました。
次回は、アノテーション内に指定するパスとActionのルーティングについて勉強します。

  • Aug
  • 1
  • 2014

IT

Xitrum 3.17

Xitrum 3.17 Released!

https://groups.google.com/d/msg/xitrum-framework/fdf7Q06z4kU/oMqlIDMRs3QJ

  1. Xitrumフレームワークの修正

    以前のバージョンのXitrumではクラスとルーティングの自動リロード機能はClassLoaderを使い捨てることで実現していました。しかし、その実装では異なるClassLoaderによって生成されたインスタンスのコンフリクトなど幾つかの問題がありました。

    Xitrum3.17では、"javaagent"を使用した実装に差し替えることで、それらの問題を解決しました。
    http://xitrum-framework.github.io/guide/3.17/en/tutorial.html#autoreload

    自動リロード機能はこれまでに比べてかなり良くなりました。
    スクリーンキャストも近く更新されるのでご期待ください。
     https://www.youtube.com/watch?v=Ds7kQ0w70Kk

  2. Xitrum プロジェクトスケルトンの修正

    スケルトンプロジェクトが更新されました:
    https://github.com/xitrum-framework/xitrum-new

    次のコミットを参考にXitrum 3.16からXitrum 3.17へのマイグレーションは次のコミットを参考にしてください。(xitrum-scalate 2.2を使用する必要があります。以前のバージョンのxitrum-scalateはXitrum 3.17と互換性がありません。)
    https://github.com/xitrum-framework/xitrum-new/commit/3074328a31a26af2543da70276ee7ef4d49f34e6

    Scalaが2.11.1から2.11.2へ更新されました:
    http://www.scala-lang.org/news/2.11.2

    scala-xgettextが1.0から1.1へ更新されました。
    これまでは i18n の文字列のキーはランダムだったため.pot/.poファイルのバージョン比較が困難でしたが、各ファイルのコンテンツはキーでソートされるようになりましたた。


    agent7-1.0.jar (
    https://github.com/xitrum-framework/agent7) がsbtディレクトリに同封されました。
    SBTでプロジェクトを起動した際に自動リロード機能のためのライブラリになります。詳しくはガイドを参照してください。
    http://xitrum-framework.github.io/guide/3.17/ja/tutorial.html#id6

  3. XitrumガイドとScaladocが更新されました:
    これまでは最新版のみが公開されていましたが、バージョン毎に公開されるようになりました。
    http://xitrum-framework.github.io/guide.html
    http://xitrum-framework.github.io/api.html