Xitrum_kotohajime

[Xitrumことはじめ][基本編] 7. リクエストとスコープ: CSRF対策

Xitrumことはじめ (基本編)

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

目次はこちら

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

7. リクエストとスコープ:

今回はリクエストに直接アクセスする方法を取り上げたいと思います。

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

7-4. CSRF対策

7-3-1. XitrumのCSRF対策の仕組み

[Xitrum.Action](https://github.com/xitrum-framework/xitrum/blob/561d214d2af847c7ad4ab7bf1b3b1b9835f0f9a0/src/main/scala/xitrum/Action.scala#L85-L90)は、
リクエストメソッドが、POSTPUTPATCHDELETEの場合、デフォルトでCSRF対策トークンチェックを行います。

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

では、このCSRFトークンチェックの内容はというと、
[Csrf.isValidToken](https://github.com/xitrum-framework/xitrum/blob/46f330ac6c360688417406dcc1539ebb8704b721/src/main/scala/xitrum/scope/session/Csrf.scala#L18-L29)の処理は次の用に、
リクエストヘッダーあるいはリクエストボディに指定されたキーでセットされたトークンと、セッション内のトークンが一致しているかを判定しています。
トークン自体はランダムな文字列ですが、シリアライズされた形式でセッションに保存されています。

def isValidToken(action: Action): Boolean = {
  // The token must be in the request body for more security
  val bodyTextParams = action.handlerEnv.bodyTextParams
  val headers        = action.handlerEnv.request.headers
  val tokenInRequest = Option(headers.get(X_CSRF_HEADER)).getOrElse(action.param(TOKEN, bodyTextParams))
 
  // Cleaner for application developers when seeing access log
  bodyTextParams.remove(TOKEN)
 
  val tokenInSession = action.antiCsrfToken
  tokenInRequest == tokenInSession
}

7-3-2. AntiCSRFトークンのセット

クライアントはリクエストヘッダーまたはリクエストボディにセッション内のトークンと一致する値を含める必要があります。

HTMLメタタグとXitrum.jsを用いてリクエストヘッダーにトークンを含める方法

レイアウト内で、{antiCsrfMeta}を使用することでメタタグが生成されます。
Scaffoldプロジェクトにあるように通常デフォルトレイアウトで用います。

!!! 5
html
  head
    != antiCsrfMeta
    != xitrumCss
    meta(content="text/html; charset=utf-8" http-equiv="content-type")

こう書くことで、以下のようにHTMLに展開されます。

<!DOCTYPE html>
<html>
  <head>
    <meta name="csrf-token" content="9f16c39b-3456-4020-9695-484d101908ca"/>
    <link href="/webjars/xitrum/3.18/xitrum.css?mhIAFrxv3tBMQXtHcoYT7w" type="text/css" rel="stylesheet" media="all"/>
    <meta content="text/html; charset=utf-8" http-equiv="content-type"/>

Xitrum.jsを使用した場合AJAXリクエスト送信時に、このメタタグの値をリクエストヘッダーの値に自動的に含めてくれます。
Xitrum.jsのインポートの仕方は以下のとおりです。

!= jsDefaults
 
または、
 
script(type="text/javascript" src={url[xitrum.js]})

リクエストボディにトークンを含める方法

form(method="post" action={url[SiteIndex]})
  != antiCsrfInput
 
#または
 
form(method="post" action={url[SiteIndex]})
  input(type="hidden" name="csrf-token" value={antiCsrfToken})

いずれも以下のように展開されます。

<form method="post" action="/">
  <input type="hidden" name="csrf-token" value="9f16c39b-3456-4020-9695-484d101908ca"/>
</form>

7-3-3. CSRF対策をスキップする

debug時やcurlクライントなどCSRF対策を省略したい場合があります。
ActionとHTTPメソッドアノテーションの回で確認したように、
SkipCsrfCheckを継承したActionを使用することで前述の!isInstanceOf[SkipCsrfCheck]という条件にあてはまらなくなるなるので、
CSRF対策チェックは実行されません。


次回は、CSRFトークンも保存されているセッションおよびクッキーについて掘り下げたいと思います。

[Xitrumことはじめ][基本編] 7. リクエストとスコープ: FullHttpRequest

Xitrumことはじめ (基本編)

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

目次はこちら

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

7. リクエストとスコープ:

今回はリクエストに直接アクセスする方法を取り上げたいと思います。

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

7-3. FullHttpRequest

7-3-1. リクエストに直接アクセスする

作成したActionクラスのexecuteメソッドが呼ばれた時、生のリクエストFullHttpRequest
requestという変数に、リクエストボディはrequestContentStringという変数で文字列として取得することができます。

RequestRawExample.scala
@GET("/requestraw")
class RequestRawIndex extends Action {
  def execute() {
    val whaleRequest = request
    log.debug("Request:" + whaleRequest.toString)
    respondText(
s"""
Request:${whaleRequest.toString}
"""
    )
  }
}
 
@GET("/requestbody")
class RequestBodyIndex extends Action {
  def execute() {
    val body = requestContentString
    log.debug("body:" + requestContentString)
    respondText(
s"""
body:${requestContentString}
"""
    )
  }
}

これらのURLにアクセスすると以下のような結果となります。

curl -X GET http://localhost:8000/requestraw\?query\=Hello -H "X-MyHeader:World" -d "message=xxx"
 
Request:DefaultFullHttpRequest(decodeResult: success)
GET /requestraw?query=Hello HTTP/1.1
User-Agent: curl/7.32.0
Host: localhost:8000
Accept: */*
X-MyHeader: World
Content-Length: 11
Content-Type: application/x-www-form-urlencoded
 
 
curl -X GET http://localhost:8000/requestbody\?query\=Hello -H "X-MyHeader:World" -d "message=xxx"
 
body:message=xxx

7-3-2. リクエストヘッダーにアクセスする

HTTP Headerにアクセスするにはparamparamoは使用することができません。
直接リクエストから取得する必要があります。

RequestHeaderExample.scala
@GET("/requestheader")
class RequestHeaderIndex extends Action {
  def execute() {
    val headers = request.headers
    log.debug("Header:" + headers.toString)
 
    val entries = headers.entries
    log.debug("Entries:" + entries.toString)
 
    val myHeader = headers.get("X-MyHeader")
    log.debug("X-MyHeader:" + myHeader.toString)
 
 
    respondText(
s"""
Header:${headers.toString}
Entries:${entries.toString}
X-MyHeader:${myHeader.toString}
"""
    )
  }
}

結果は以下のようになります。

curl -X GET http://localhost:8000/requestheader\?query\=Hello -H "X-MyHeader:World" -d "message=xxx"
 
Header:io.netty.handler.codec.http.DefaultHttpHeaders@2f1bc1e1
Entries:[User-Agent=curl/7.32.0, Host=localhost:8000, Accept=*/*, X-MyHeader=World, Content-Length=11, Content-Type=application/x-www-form-urlencoded]
myHeader:World

ヘッダーパラメーターがparamparamoの対象外である理由としては、
通常のActionからはヘッダーパラメータにアクセスする機会が少ないからであるといえます。
ヘッダーパラメータをアプリケーションが利用するユースケースとして、認証処理などが考えられます。
たとえば、X-APP_TOKENなどといった形式でアプリケーションのトークンを全てのリクエストのヘッダーに含めて認証を行うアプリの場合、
認証処理は全てのActionに共通であるため、通常はフィルターが使用されます。
リクエストへの直接のアクセスはフィルターに限定して、通常のActionはそのActionのためのリクエスト(クエリー、パス、ボディ)のみを参照する設計がよいと考えられます。
フィルターを使ったサンプルはまた次の機会にやってみます。

[Xitrumことはじめ][基本編] 7. リクエストとスコープ: リクエストパラメーター

Xitrumことはじめ (基本編)

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

目次はこちら

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

7. リクエストとスコープ:

今回はリクエストパラメーターの扱い方を取り上げたいと思います。

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

7-2. リクエストパラメーター

7-2-1. リクエストパラメータにアクセスする

XitrumのActionで扱えるリクエストパラメータは以下の通りです。

リクエストパラメーターには2種類あります:

テキストパラメータ
ファイルアップロードパラメーター(バイナリー)
テキストパラメーターは scala.collection.mutable.Map[String, List[String]] の型をとる3種類があります:

queryParams: URL内の?以降で指定されたパラメーター 例: http://example.com/blah?x=1&y=2
bodyTextParams: POSTリクエストのbodyで指定されたパラメーター
pathParams: URL内に含まれるパラメーター 例: GET("articles/:id/:title")
これらのパラメーターは上記の順番で、 textParams としてマージされます。 (後からマージされるパラメーターは上書きとなります。)

bodyFileParams は scala.collection.mutable.Map[String, List[ FileUpload ]] の型をとります。

今回は、テキストパラメータについてのみ試します。
テキストパラメータは、paramおよび、paramoメソッドで取得することができます。

paramで指定したキーが存在しない場合、Xitrumが自動で400 Bad Requestをレスポンスします。
paramoは指定したキーをOption型として取得し、存在しない場合はNoneとなります

また、bodyTextParamsについては、POST、PUT,PATCHメソッドの場合のみ取得されます。

RequestParamExample.scala
@GET("/requestparam/:path1/:path2")
@POST("/requestparam/:path1/:path2")
class RequestParamIndex extends Action with SkipCsrfCheck {
  def execute() {
 
    // From path param
    val path1  = param("path1")
    val path2  = param("path2")
    log.debug("path1"+path1)
    log.debug("path2"+path2)
 
    // From query param
    val query1  = param("query1")
    val query2  = param("query2")
    log.debug("query1"+query1)
    log.debug("query2"+query2)
 
    // From body param when HTTP method is POST, PUT, PATCH
    val body1   = param("body1")
    val body2   = param("body2")
    log.debug("body1"+body1)
    log.debug("body2"+body2)
 
    respondText(
s"""
textParams:${textParams}
queryParams:${queryParams}
bodyTextParams:${bodyTextParams}
pathParams:${pathParams}
path1:${path1}
path2:${path2}
query1:${query1}
query2:${query2}
"""
    )
  }
}
 
@GET("/requestparamoption/:path1/:path2")
@POST("/requestparamoption/:path1/:path2")
class RequestParamOptionIndex extends Action with SkipCsrfCheck {
  def execute() {
 
    // From path param
    val path1  = paramo("path1")
    val path2  = paramo("path2")
    log.debug("path1"+path1)
    log.debug("path2"+path2)
 
    // From query param
    val query1  = paramo("query1")
    val query2  = paramo("query2")
    log.debug("query1"+query1)
    log.debug("query2"+query2)
 
    // From query param
    val body1   = paramo("body1")
    val body2   = paramo("body2")
    log.debug("body1"+body1)
    log.debug("body2"+body2)
 
    respondText(
s"""
textParams:${textParams}
queryParams:${queryParams}
bodyTextParams:${bodyTextParams}
pathParams:${pathParams}
path1:${path1}
path2:${path2}
query1:${query1}
query2:${query2}
body1:${body1}
body2:${body2}
"""
    )
  }
}

それぞれのURLにGETとPOSTでアクセスすると以下の様な結果となります。

curl -X GET http://localhost:8000/requestparam/x/y\?query1\=q1\&query2\=q2 -H "X-MyHeader:World" -d "body1=b1" -d "body2=b2"
Missing param: body1
 
 
curl -X POST http://localhost:8000/requestparam/x/y\?query1\=q1\&query2\=q2 -H "X-MyHeader:World" -d "body1=b1" -d "body2=b2"
 
textParams:Map(path2 -> List(y), path1 -> List(x), body2 -> List(b2), query2 -> List(q2), body1 -> List(b1), query1 -> List(q1))
queryParams:Map(query2 -> List(q2), query1 -> List(q1))
bodyTextParams:Map(body2 -> List(b2), body1 -> List(b1))
pathParams:Map(path2 -> List(y), path1 -> List(x))
path1:x
path2:y
query1:q1
query2:q2
 
 
curl -X GET http://localhost:8000/requestparamoption/x/y\?query1\=q1\&query2\=q2 -H "X-MyHeader:World" -d "body1=b1" -d "body2=b2"
 
textParams:Map(path2 -> List(y), path1 -> List(x), query2 -> List(q2), query1 -> List(q1))
queryParams:Map(query2 -> List(q2), query1 -> List(q1))
bodyTextParams:Map()
pathParams:Map(path2 -> List(y), path1 -> List(x))
path1:Some(x)
path2:Some(y)
query1:Some(q1)
query2:Some(q2)
body1:None
body2:None
 
 
curl -X POST http://localhost:8000/requestparamoption/x/y\?query1\=q1\&query2\=q2 -H "X-MyHeader:World" -d "body1=b1" -d "body2=b2"
 
textParams:Map(path2 -> List(y), path1 -> List(x), body2 -> List(b2), query2 -> List(q2), body1 -> List(b1), query1 -> List(q1))
queryParams:Map(query2 -> List(q2), query1 -> List(q1))
bodyTextParams:Map(body2 -> List(b2), body1 -> List(b1))
pathParams:Map(path2 -> List(y), path1 -> List(x))
path1:Some(x)
path2:Some(y)
query1:Some(q1)
query2:Some(q2)
body1:Some(b1)
body2:Some(b2)

7-2-2 型を指定してパラメータを取得する

リクエストパラメータのデフォルトはString型です。
paramおよび、paramoに型を指定することで任意の型でパラメータを取得することができます。
デフォルトのコンバータは以下の用に定義されており、独自の型に変換する場合は、Action内でconvertTextParamをオーバーライドします。
なおconvertTextParamをオーバーライドする際に、Scala 2.10ではTypeの代わりにManifestを使用することに注意してください。

RequestParamConvertExample.scala
@GET("/requestparamconvert/:one")
class RequestConvertIndex extends Action {
  def execute() {
 
      val one_as_String   = param[String]("one")
      val one_as_Char     = param[Char]("one")
      val one_as_Byte     = param[Byte]("one")
      val one_as_Short    = param[Short]("one")
      val one_as_Int      = param[Int]("one")
      val one_as_Long     = param[Long]("one")
      val one_as_Float    = param[Float]("one")
      val one_as_Double   = param[Double]("one")
      val one_as_Implicit = param("one")
 
 
    respondText(
s"""
one_as_String =>   Class:${one_as_String.getClass.toString}, Value:${one_as_String}
one_as_Char =>     Class:${one_as_Char.getClass.toString},   Value:${one_as_Char}
one_as_Byte =>     Class:${one_as_Byte.getClass.toString},   Value:${one_as_Byte}
one_as_Short =>    Class:${one_as_Short.getClass.toString},  Value:${one_as_Short}
one_as_Int =>      Class:${one_as_Int.getClass.toString},    Value:${one_as_Int}
one_as_Long =>     Class:${one_as_Long.getClass.toString},   Value:${one_as_Long}
one_as_Float =>    Class:${one_as_Float.getClass.toString},  Value:${one_as_Float}
one_as_Double =>   Class:${one_as_Double.getClass.toString}, Value:${one_as_Double}
one_as_Implicit => Class:${one_as_Implicit.getClass.toString}, Value:${one_as_Implicit}
"""
    )
  }
}
 
case class MyClass(value:String)
 
@GET("/requestparamconvertcustome/:one")
class RequestConvertCustomeIndex extends Action {
  override  def convertTextParam[T: TypeTag](value: String): T = {
    val t = typeOf[T]
    val any: Any =
           if (t <:< typeOf[String])  value
      else if (t <:< typeOf[MyClass]) MyClass(value)
      else if (t <:< typeOf[Int])    value.toInt
      else throw new Exception("convertTextParam cannot covert " + value + " to " + t)
    any.asInstanceOf[T]
  }
 
  def execute() {
 
      val one_as_MyClass  = param[MyClass]("one")
      val one_as_String   = param[String]("one")
      val one_as_Int      = param[Int]("one")
 
 
    respondText(
s"""
one_as_String =>   Class:${one_as_String.getClass.toString},  Value:${one_as_String}
one_as_MyClass=>   Class:${one_as_MyClass.getClass.toString}, Value:${one_as_MyClass}
one_as_Int =>      Class:${one_as_Int.getClass.toString},     Value:${one_as_Int}
"""
    )
  }
}

実行結果は以下のようになります。

oshidatakeharu@oshida [~] curl -X GET http://localhost:8000/requestparamconvert/1
 
one_as_String =>   Class:class java.lang.String, Value:1
one_as_Char =>     Class:char,   Value:1
one_as_Byte =>     Class:byte,   Value:1
one_as_Short =>    Class:short,  Value:1
one_as_Int =>      Class:int,    Value:1
one_as_Long =>     Class:long,   Value:1
one_as_Float =>    Class:float,  Value:1.0
one_as_Double =>   Class:double, Value:1.0
one_as_Implicit => Class:class java.lang.String, Value:1
 
 
oshidatakeharu@oshida [~] curl -X GET http://localhost:8000/requestparamconvertcustome/1
 
one_as_String =>   Class:class java.lang.String,  Value:1
one_as_MyClass=>   Class:class quickstart.action.MyClass, Value:MyClass(1)
one_as_Int =>      Class:int,     Value:1

7-2-3. リクエストボディをJSONとして扱う

Restful APIサーバなど、クライアントとのインターフェイスをJSONで定義したアプリケーションの場合など、
JSONをそのままMapとして扱えると便利です。Xitrumでは以下の方法で実現することができます。

RequestJSONExample.scala
@GET("/requestbodyjson")
class RequestBodyJsonIndex extends Action {
  def execute() {
    val bodyJson = requestContentJson[Map[String, Any]]
    log.debug("body as Json:" + bodyJson)
 
    bodyJson match {
      case Some(v) => log.debug("Successfully parsed")
      case None =>    log.debug("Failed to parse")
    }
    respondText(
s"""
body as Json:${bodyJson}
"""
    )
  }
}

requestContentJsonはパースに失敗した場合はNoneを返します。

curl -X GET http://localhost:8000/requestbodyjson\?query\=Hello -H "X-MyHeader:World" -d "{\"message\":\"xxx\",\"code\":1,\"list\":[1,2,3],\"bool\":true}"
 
body as Json:Some(Map(message -> xxx, code -> 1, list -> List(1, 2, 3), bool -> true))
 
 
curl -X GET http://localhost:8000/requestbodyjson\?query\=Hello -H "X-MyHeader:World" -d "{\"json\":{\"nest\":1}}"
 
body as Json:Some(Map(json -> Map(nest -> 1)))
 
 
curl -X GET http://localhost:8000/requestbodyjson\?query\=Hello -H "X-MyHeader:World" -d "invalidjson"
 
body as Json:None

次回は生のリクエストにアクセスする方法をやります。

[Xitrumことはじめ][基本編] 7. リクエストとスコープ: チャネルパイプライン

Xitrumことはじめ (基本編)

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

目次はこちら

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

7. リクエストとスコープ:

今回からはリクエストおよびスコープについて勉強します。
クライアントからのリクエストがどのように処理されるか、リクエストパラメーターの扱い方などを取り上げたいと思います。

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

7-1. チャネルパイプラインとActionのライフサイクル

アプリケーションのロジックでリクエストを処理するうえで、Xitrumの処理の流れの概要を掴んでおきたいと思います。
Xitrumアプリケーションを起動時に

xitrum.Server.start()

と呼び出す必要があります。xitrum.Serverは、
NettyのChannelPipelineを構築し、Nettyサーバを起動しています。

Xitrumがデフォルトで構築するチャネルパイプラインは、
次のようなイメージです

                                        +~~~~~~~~~~~~~~~~~~~~~~+
                                ------->|         Action       |
                                |       +~~~~~~~~~~~~~~~~~~~~~~+
                                |                   |
+-------------------------------|-------------------+---------------+
| ChannelPipeline               |                   |               |
|                               |                   |               |
|    +---------------------+    |                   |               |
|    |  BadClientSilencer  |    |                   |               |
|    +----------+----------+    |                   |               |
|              /|\              |                   |               |
|               |          _____|                  \|/     OutBound |
|    +--------------------/+            +-----------+----------+    |
|    |      Dispatcher     |            |     XSendResource    |    |
|    +----------+----------+            +-----------+----------+    |
|              /|\                                  |               |
|               |                                  \|/              |
|    +---------------------+            +-----------+----------+    |
|    |   MethodOverrider   |            |       XSendFile      |    |
|    +----------+----------+            +-----------+----------+    |
|              /|\                                  |               |
|               |                                  \|/              |
|    +----------+----------+            +-----------+----------+    |
|    |      UriParser      |            |   FixiOS6SafariPOST  |    |
|    +----------+----------+            +-----------+----------+    |
|              /|\                                  .               |
|               |                                  \|/              |
|    +----------+----------+            +-----------+----------+    |
|    |    WebJarsServer    |            |    OPTIONSResponse   |    |
|    +----------+----------+            +-----------+----------+    |
|              /|\                                  |               |
|               |                                  \|/              |
|    +----------+----------+            +-----------+----------+    |
|    |   PublicFileServer  |            |        SetCORS       |    |
|    +----------+----------+            +-----------+----------+    |
|              /|\                                  |               |
|               |                                  \|/              |
|    +----------+----------+            +-----------+----------+    |
|    |    BaseUrlRemover   |            |     Env2Response     |    |
|    +----------+----------+            +-----------+----------+    |
|              /|\                                  |               |
|               |                                  \|/              |
|    +----------+----------+            +-----------+----------+    |
|    |     Request2Env     |            | ChunkedWriteHandler  |    |
|    +----------+----------+            +-----------+----------+    |
|              /|\                                  |               |
|               |                                  \|/              |
|    +----------+----------+            +-----------+----------+    |
|    | HttpRequestDecoder  |            | HttpResponseEncoder  |    |
|    +----------+----------+            +-----------+----------+    |
| InBound      /|\                                  |               |
+---------------+-----------------------------------+---------------+
                |                                  \|/
+---------------+-----------------------------------+---------------+
|               |                                   |               |
|       [ Socket.read() ]                    [ Socket.write() ]     |
|                                                                   |
|  Netty Internal I/O Threads (Transport Implementation)            |
+-------------------------------------------------------------------+

クライアントからのリクエストに対して、NettyのI/Oスレッド上でそれぞれパイプラインが生成・実行されます。
リクエストはInboundハンドラーを経て、DispacherにてルーティングにマッチしたActionへディスパッチされます。
このパイプラインを経ることで、Actionが実行される段階では生のリクエストはアプリケーション開発者にとって利用しやすい形になっています。
ActionはrespondViewなどを実行した段階で処理を終え、処理の流れはOutBoundへと進みます。

クライアントからのリクエスト毎に割り当てられるこのスレッド上でActionクラスのインスタンスは生成されます。
一般にアプリケーション開発者が意識すべきActionのライフサイクルは
Actionのexecuteメソッド内に限られており、
アノテーションで指定されたルーティングにマッチするリクエストを受け付けた時に、
Actionインスタンスは生成され、executeメソッドが実行されます。
executeメソッドの最後にrespondXXXを実行することoutBoundハンドラーへの電文を作成しActionは役目を終えます。

WebSocketおよびSockJSクライアントのように、接続を持続するリクエストに対しては、
Action以降の不要なハンドラーが削除されます

なお、XitrumにはActionの実行をFutureやAkkaのスレッドで実行する仕組みも用意されています。
その話はまた今度。

クライアントからのリクエストがActionに届く流れがは大体こんな感じなので、
次回は実際にHTTPリクエストをAction内で扱うための便利機能について使ってみたいと思います。

[Xitrumことはじめ][基本編] 6. レスポンスとビュー: ポストバック、リダイレクト、フォーワード

Xitrumことはじめ (基本編)

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

目次はこちら

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

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

今回は、Actionによるレスポンスのパターンとして、
ポストバック、リダイレクト、フォーワードを取り上げます。

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

6-6-1. ポストバック

Xitrumのポストバックは、Nitrogenという、Erlangライブラリを参考に実装されています。
参考:Nitrogenのポストバックチュートリアル

ポストリクエストに対してインタラクティブにページを生成するための機能と言えます。
Xitrumが提供するポストバックは、Formのポスト処理と、ポスト処理に対するサーバサイドのレスポンスを
クライアントサイドのxitrum.jsがポストバックイベントとしてハンドリングすることで実現されています。

ただし、AngularやBackboneなどのJSフレームワークとJSONレスポンスを用いることでより柔軟なページ生成ができることが
多いため、この機能を使う機会は少ないかもしれません。

Postback.scala

@GET("postback")
class PostbackIndex extends DefaultLayout {
  def execute() {
    respondInlineView(
      <form data-postback="submit" action={url[PostbackAction]}>
        <label>Title</label>
        <input type="text" name="title" class="required" /><br />
 
        <label>Body</label>
        <textarea name="body" class="required"></textarea><br />
 
        <input type="submit" value="Postback" />
      </form>
      <hr>
      <form data-postback="submit" action={url[PostbackAction2]}>
        <label>Title</label>
        <input type="text" name="title" class="required" /><br />
 
        <label>Body</label>
        <textarea name="body" class="required"></textarea><br />
 
        <input type="submit" value="Postback2" />
      </form>    )
  }
}
 
@POST("postback")
class PostbackAction extends DefaultLayout {
  def execute() {
    val title   = param("title")
    val body    = param("body")
    flash("Posted.")
    jsRedirectTo[PostbackIndex]()
  }
}
 
@POST("postback2")
class PostbackAction2 extends DefaultLayout {
  def execute() {
    val title   = param("title")
    val body    = param("body")
    jsRespond(s"""$$('body').append('title:$title\\nbody:$body')""")
  }
}

この例では、
Postbackボタン、Postback2ボタン共にポストバックリクエストを行います。
違いは、レスポンスにあり1の場合はjsRedirectToを通じてクライアントから再びPostbackIndexへリクエストが行われます。
Postしてもとのページヘバックすることになります。
2の場合はレスポンスしたjavascriptがクライアント側で実行されます。

6-6-2. リダイレクト

リダイレクトを行う場合、redirectToを使用します。
クライアントには、HTTPステータス 302が返却され、レスポンスヘッダーには次のリダイレクト先URLが追加されます。

Redirect.scala

@GET("/redirect")
class RedirectIndex extends Action {
  def execute() {
    log.debug("RedirectIndex")
    log.debug(textParams.toString)
    redirectTo[RedirectedPage]()
  }
}
 
@GET("/redirected")
class RedirectedPage extends Action {
  def execute() {
    log.debug("RedirectedPage")
    log.debug(textParams.toString)
    respondText(getClass)
  }
}

上記のActionに対してcurlクライアントからリクエストを行った場合、
以下のような結果となります。

curl -v http://localhost:8000/redirect\?x\=1
* About to connect() to localhost port 8000 (#0)
*   Trying ::1...
* Adding handle: conn: 0x7fb089003000
* Adding handle: send: 0
* Adding handle: recv: 0
* Curl_addHandleToPipeline: length: 1
* - Conn 0 (0x7fb089003000) send_pipe: 1, recv_pipe: 0
* Connected to localhost (::1) port 8000 (#0)
> GET /redirect?x=1 HTTP/1.1
> User-Agent: curl/7.32.0
> Host: localhost:8000
> Accept: */*
>
< HTTP/1.1 302 Found
< Connection: keep-alive
< Location: /redirected
< Access-Control-Allow-Origin: *
< Access-Control-Allow-Credentials: true
< Access-Control-Allow-Methods: OPTIONS, GET, HEAD
< Content-Length: 0
<
* Connection #0 to host localhost left intact 

XitrumログにはRedirectIndexへのアクセスのみ表示されます。

[DEBUG] RedirectIndex
[DEBUG] Map(x -> List(1))
[INFO] 0:0:0:0:0:0:0:1 GET /redirect?x=1 -> quickstart.action.RedirectIndex, queryParams: {x: 1} -> 302, 1 [ms]

ブラウザから上記のURLを表示した場合、ブラウザがリダイレクト用のレスポンスヘッダーを解釈して次のURLへリクエストを自動で作成します。
この時、RedirectIndexへのリクエストとは全く別のリクエストがRedirectedPageへ行われるため、リクエストパラメータは引き継がれません。

Xitrumログは以下のように成ります。

[DEBUG] RedirectIndex
[DEBUG] Map(x -> List(1))
[INFO] 0:0:0:0:0:0:0:1 GET /redirect?x=1 -> quickstart.action.RedirectIndex, queryParams: {x: 1} -> 302, 86 [ms]
[DEBUG] RedirectedPage
[DEBUG] Map()
[INFO] 0:0:0:0:0:0:0:1 GET /redirected -> quickstart.action.RedirectedPage -> 200, 43 [ms]

6-6-3. フォーワード

フォーワードの場合は、処理の流れはクライアントにレスポンスは戻らずに、直接次のアクションへ処理が移ります。
リクエストパラメータはそのままフォーワード先のアクションへと引き継がれます。

Forward.scala

@GET("/forward")
class ForwardIndex extends Action {
  def execute() {
    log.debug("ForwardIndex")
    log.debug(textParams.toString)
    forwardTo[ForwardedPage]()
  }
}
 
@GET("/forwarded")
class ForwardedPage extends Action {
  def execute() {
    log.debug("ForwardedPage")
    log.debug(textParams.toString)
    respondText(getClass)
  }
}

Forwardの場合、クライアントがcurlでもブラウザでも同じ結果となります。

curl -v http://localhost:8000/forward\?x\=1
* About to connect() to localhost port 8000 (#0)
*   Trying ::1...
* Adding handle: conn: 0x7fdf82006e00
* Adding handle: send: 0
* Adding handle: recv: 0
* Curl_addHandleToPipeline: length: 1
* - Conn 0 (0x7fdf82006e00) send_pipe: 1, recv_pipe: 0
* Connected to localhost (::1) port 8000 (#0)
> GET /forward?x=1 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: 37
< ETag: "Bz-NYDptSQMU_PgZeFPu0w"
<
* Connection #0 to host localhost left intact 
class quickstart.action.ForwardedPage%
[DEBUG] ForwardIndex
[DEBUG] Map(x -> List(1))
[DEBUG] ForwardedPage
[DEBUG] Map(x -> List(1))
[INFO] 0:0:0:0:0:0:0:1 GET /forward?x=1 -> quickstart.action.ForwardedPage, queryParams: {x: 1} -> 200, 2 [ms]


今回はこれまで。
レスポンスのパターンはだいたい把握できたので、
次回からは、リクエストパラメーターやスコープについて勉強します。

[Xitrumことはじめ][基本編] 6. レスポンスとビュー: JavaScriptとCSS

Xitrumことはじめ (基本編)

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

目次はこちら

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

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

Viewに関連する項目として今回は、webJarsを利用したフロントエンドのライブラリ活用についてやります。

Scala(Java)プロジェクトにおけるフロントエンドライブラリ管理の手法としてWebJarという仕組みがあります。
Xitrumもこの仕組をサポートしています。

また、Xitrumのコア自体もwebJarを利用していくつかのフロントエンドライブラリを使用しています。

build.sbt

libraryDependencies += "org.webjars" % "jquery" % "2.1.1"
 
libraryDependencies += "org.webjars" % "jquery-validation" % "1.13.0"
 
libraryDependencies += "org.webjars" % "sockjs-client" % "0.3.4"
 
libraryDependencies += "org.webjars" % "swagger-ui" % "2.0.22"
 
libraryDependencies += "org.webjars" % "d3js" % "3.4.11"

webJarを利用することで、
bowerやyoeman、npmなどScala以外のツールの導入なしでプロジェクトのリソース管理を全て
sbtにまとめることができます。

もちろんbowerなどのツールを利用することも、CDNなどを利用することもできますが、
今回はこのwebJarsを利用してみようと思います。

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

6-5. JavaScriptとCSS

webJarを利用するには、使用したいライブラリが公開されている必要があります。
http://www.webjars.org/ から該当のライブラリを探します。

JavaScriptライブラリとしてUnderscore.jsを導入してみます。
またxitrum-newをscaffoldとして使用した場合、CSSライブラリとしてbootstrapが既に導入されています。

プロジェクトのbuild.sbtに該当のライブラリをlibraryDependenciesとして追加します。

build.sbt

libraryDependencies += "org.webjars" % "bootstrap" % "3.2.0"
 
libraryDependencies += "org.webjars" % "underscorejs" % "1.6.0-3"

build.sbtを更新したら、sbtを使用してライブラリをダウンロードします。

sbt/sbt update

以下のように

[info] Resolving org.webjars#underscorejs;1.6.0-3 ...
[info] downloading http://repo1.maven.org/maven2/org/webjars/underscorejs/1.6.0-3/underscorejs-1.6.0-3.jar ...
[info]     [SUCCESSFUL ] org.webjars#underscorejs;1.6.0-3!underscorejs.jar (751ms)
[info] Done updating.

該当のライブラリがダウンロードされます。

これらはjar形式でプロジェクト起動時のクラスパスに含まれる事になります。

アプリケーションのViewから該当のリソースを取得するには、ActionwebJarsUrlを使用します。
テンプレートエンジンにxitrum-scalateを使用している場合、webJarsUrlはテンプレートファイル内でそのまま使えます。

DefaultLayout.jade

link(type="text/css" rel="stylesheet" media="all" href={webJarsUrl("bootstrap/3.2.0/css", "bootstrap.css", "bootstrap.min.css")})
 
script(src={webJarsUrl("underscorejs/1.6.0", "underscore.js", "underscore-min.js")})

webJarsUrlには、

  • 第1引数として該当のjarファイルのパス(META-INF/resources/webjars以下)を指定します。
    jar内のファイル群がどういう構成かは、http://www.webjars.org/ のFilesボタンを押せば分かります。

  • 第2引数には該当のファイルを指定します。

  • 第3引数にはプロダクション環境用のファイルを指定します。通常は圧縮版のファイルを指定します。該当のファイルの圧縮版が存在しない場合は第2引数と同じものを指定します。

ビルドしてDefaultLayoutを使用しているSiteIndexにアクセスしてみます。

http://localhost:8000/

webJarsUrlは以下のリンクに展開されています。

<link type="text/css" rel="stylesheet" media="all" href="/webjars/bootstrap/3.2.0/css/bootstrap.css?4pWKTr6RZtuqbFkxGygQIQ">
 
<script src="/webjars/underscorejs/1.6.0/underscore.js?3ZZjvppx81cLw18O26KHEg"></script>

ブラウザはこれらのタグを解析して、GETリクエストとしてXitrumサーバに送信する事になります。
Xitrumサーバは受け付けたリクエストをどのようにルーティングしているのでしょうか。

少し内部の難しい話になりますが、
XitrumのChannelPipelineにはinboundハンドラーとして
WebJarsServer.scalaがあります。
このハンドラーがwebjarasで始まるURLの場合に、該当のファイルをjarファイルの中から見つけてレスポンスしてくれるという事になります。
該当のリソースが見つからない場合404エラーフラグをセットして次のハンドラーへ処理が移ります。

一方でユーザーが書くアプリケーションのActionへのルーティングは、inboundハンドラーの最後にあります。
そのため、該当のリソースがwebJarsに見つからなければユーザーが書いたルーティングで処理されうるということです。

実験してみましょう。

WebJarsNoRoute.scala

@GET("/webjars/underscorejs/1.6.0/underscore.js", "/webjars/underscorejs/1.7.0/underscore.js")
class WebJarsNoRoute extends Action {
  def execute() {
    respondText("underscorejs-1.7.0 is not found")
  }
}

curl http://localhost:8000/webjars/underscorejs/1.6.0/underscore.js

curl http://localhost:8000/webjars/underscorejs/1.7.0/underscore.js

1つめのリクエストは正しいwebJarsリソースが存在するので、underscore.jsがレスポンスされて、
2つめのリクエストはWebJarsNoRouteが実行されます。


今回はこれまで。
次回は、レスポンスおよびルーティングに関わるトピックの最後として
リダイレクトとフォーワード、ポストバックをやってみます。

[Xitrumことはじめ][基本編] 6. レスポンスとビュー: Viewに関するAPI

Xitrumことはじめ (基本編)

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

目次はこちら

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

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

Xitrum-scalateをテンプレートエンジンに使用することで、
Actionの各メソッドや、現在のActionのインスタンスをViewから使用することができるようになります。
もちろんAction内部でインラインでViewを構成する場合にもAPIが使用できます。
今回はXitrumが提供するViewに関連するAPIを勉強します。

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

6-4. Viewに関するAPI

xitrum.Actionクラスは、
多くのtraitをwithで継承しており、それらはXitrum内部で使用するもの以外にも、
ユーザーアプリケーションから使用することができるAPIとして提供されています。

具体的なAPIの使用例として
ScaffoldProjectのDefaultLayout.jadeが参考になります。

DefaultLayout.jade

- import quickstart.action._
 
!!! 5
html
  head
    != antiCsrfMeta
    != xitrumCss
 
    meta(content="text/html; charset=utf-8" http-equiv="content-type")
    title My new Xitrum project
 
    link(rel="shortcut icon" href={publicUrl("favicon.ico")})
 
    link(type="text/css" rel="stylesheet" media="all" href={webJarsUrl("bootstrap/3.2.0/css", "bootstrap.css", "bootstrap.min.css")})
    link(type="text/css" rel="stylesheet" media="all" href={publicUrl("app.css")})
 
  body
    .container
      h1
        a(href={url[SiteIndex]}) My new Xitrum project
 
      #flash
        !~ jsRenderFlash()
      != renderedView
 
    != jsDefaults
    != jsForView

ここでは以下のAPIが使用されています。

  • antiCsrfMeta
  • xitrumCss
  • publicUrl
  • webJarsUrl
  • url
  • jsRenderFlash
  • renderedView
  • jsDefaults
  • jsForView

    実際に出力されるHTMLは以下の通りです。

    <!DOCTYPE html>
    <html>
     <head>
       <meta name="csrf-token" content="7c8da946-1b53-47de-8a62-b2d361e96510"/>
       <link href="/webjars/xitrum/3.16/xitrum.css?mhIAFrxv3tBMQXtHcoYT7w" type="text/css" rel="stylesheet" media="all"/>
       <meta content="text/html; charset=utf-8" http-equiv="content-type"/>
       <title>My new Xitrum project</title>
       <link rel="shortcut icon" href="/favicon.ico?BjK0shXmVIuSRS0IsYBdHA"/>
       <link type="text/css" rel="stylesheet" media="all" href="/webjars/bootstrap/3.2.0/css/bootstrap.css?4pWKTr6RZtuqbFkxGygQIQ"/>
       <link type="text/css" rel="stylesheet" media="all" href="/app.css?V0CGnmnzXFV6l7a-UkY_7w"/>
     </head>
     <body>
       <div class="container">
         <h1>
           <a href="/">My new Xitrum project</a>
         </h1>
         <div id="flash">
         </div>
          <!-- DefaultLayoutを継承した各ActionのrenderedViewの結果がここに出力される -->
       </div>
             <script type="text/javascript" src="/webjars/jquery/2.1.1/jquery.js?dAMGCVD0oTvjs9_eBJDuBQ"></script>
             <script type="text/javascript" src="/webjars/jquery-validation/1.12.0/jquery.validate.js?MoZJHtxFQR8TR6gNokHx2w"></script>
             <script type="text/javascript" src="/webjars/jquery-validation/1.12.0/additional-methods.js?VMrHLE7MT-YZGBg3T6jSGA"></script>
             <script type="text/javascript" src="/webjars/sockjs-client/0.3.4/sockjs.js?G6ezG627D2WKnJ3F55SoNQ"></script>
             <script type="text/javascript" src="/xitrum/xitrum.js?BMfHCVrVdosDpgtIlbqZWw"></script>
     </body>
    </html>

まず、antiCsrfMetacsrf-tokenというメタタグに展開されています。
このタグはxitrum.js内の処理によって、ajaxリクエスト実行時にリクエストヘッダーに自動で含められることになります。
xitrumはcsrf対策として、put/post/deleteリクエストを受け付けた場合デフォルトでcsrfトークンの妥当性をチェックします。

このtokenは、antiCsrfInputまたはantiCsrfTokenとして取得することも可能です。
後程のサンプルで実際に使ってみます。

なお、Actionでcsrfトークンチェックを無効にしたい場合は、xitrum.SkipCsrfChecktraitを
継承します。

次に、xitrumCssです。
こちらは、単純なCSSファイルへのlinkタグに展開されます。
xitrum.jsを使用している場合に、使われるクラスが定義されています。アプリケーションで不要なら必ずしも記載する必要はありません。

つづいて、urlpublicUrlwebjarsです。
これらはそれぞれ、ActionへのURL,publicディレクトリへのURL、webJarへのURLへと展開されます。
また、publicUrlwebjarsは各リソースのETagに応じてクエリストリングを自動で付加してくれます。
このほか、@sockjsアノテーションに対応したActionのURLを取得するsockJsUrl
@websocketアノテーションに対応したActionのURLを取得する、urlwebSocketAbsUrlなどがあります。
(websocketの場合、ws/wssスキーマも含めたフルパスが取得できます。)

<div id="flash">および、jsRenderFlashはxitrum.jsと組み合わせて、
ちょっとしたアラートを出すための記述になります。
Action内でflashメソッドで追加した文字列が表示されます。
具体的な使用例はのちほど。

jsDefaultsはjQuery、jQuery-validation、sockJS、xitrum.jsが展開されます。
xitrum.js は具体的には
https://github.com/xitrum-framework/xitrum/blob/master/src/main/scala/xitrum/js.scala
が該当します。
xitrum.jsは前述のajaxリクエストに対するcsrf攻撃対策トークンを含めることの他に、
jqueryValidationの実行、postback関連のクライアント処理、flashメッセージ出力処理などが記載されています。
主にクライアントサイドとサーバーサイドの連携、バリデーションを目的とした機能となります。
アプリケーションに応じてこれらの処理が不要であれば、jsDefaultsは必ずしも記載する必要はありません。

jsForViewにはAction内でjsAddToViewで追加したjavaScript文字列が展開されます。

そのほか、ActionからViewへパラメータを渡すatや、i18n文字列を生成するtなどのView用APIがあります。
それではatを使ったサンプルを作ってみたいと思います。

ViewAPIExample.scala

case class Person(name:String,age:Int)
 
@GET("/viewapi/at")
class AtExample extends Action {
  def execute() {
 
    at("key1") = "value"
 
    at("taro") = new Person("Taro", 10)
 
    at("jiro") = new Person("Jiro", 20)
 
    at("serializable") = Map("key" -> "val")
 
    respondView()
  }
}

この例ではリクエストを受け付けた際に、atメソッドを使用して"key1"、"key2"という名前でそれぞれ、
文字列とオブジェクトを保存しています。

AtExample.jade

- import quickstart.action.Person
 
- val key1Value = at("key1").asInstanceOf[String]
- val person = at("taro").asInstanceOf[Person]
- val person2 = at[Person]("jiro")
 
div
  p "key1"
  p = key1Value
 
  p "taro"
  p = person.name
  p = person.age
 
  p "jiro"
  p = person2.name
  p = person2.age
 
  p "toJson"
  p = at.toJson("serializable")

Viewの方では、Scalaコードでatからキーと型を指定して値を取得します。
atの実態は実はHashMap[String, Any]です。(https://github.com/xitrum-framework/xitrum/blob/46f330ac6c360688417406dcc1539ebb8704b721/src/main/scala/xitrum/scope/request/At.scala)
そのため値を取得する際は型を指定する必要があります。

次はJavaScriptに関連するAPIとして、flashjsAddToViewおよびjsForViewメソッドを使ってみます。

ViewAPIExample.scala

@GET("/viewapi/js")
class JsExample extends DefaultLayout {
  def execute() {
 
    flash("Hello World")
 
    jsAddToView("""console.log("Hello js Add To View")""")
 
    jsAddToView("""console.log("Hello js Add To View Again!")""")
 
    respondView()
  }
}

JsExample.jade

p This is a "JsExample" See web console

この例では、リクエストを受け付けると、flashメソッドに"Hello World"という文字列を登録、
その後jsAddToViewメソッドを2回呼び出してそれぞれjavascriptのコードを登録しています。
実際に http://localhost:8000/viweapi/js にアクセスして返されるhtmlには、
DefaultLayout内のjsForViewが以下のように展開されています。

    <script type="text/javascript">
        //<![CDATA[
        $(function() {
        console.log("Hello js Add To View");
        console.log("Hello js Add To View Again!");
        xitrum.flash("Hello World");
        });
        //]]>
    </script>

また、画面は以下の用になります。

と表示されます。
これは最後のxitrum.flashというxitrum.js内のjavascript関数によって生成されたDOMエレメントとなります。

xitrum.flash = function (msg) {
  var div =
    '<div class="flash">' +
      '<a class="flash_close" href="javascript:">X</a>' +
      '<div class="flash_msg">' + msg + '</div>' +
    '</div>';
  $('#flash').append($(div).hide().fadeIn(1000));
}

flashのスタイルはデフォルトのxitrum.cssを使用していますが
アプリケーションに応じてxitrum.flashやクラスのスタイルをオーバーライドすることも可能です。

jsAddToView("""xitrum.flash = function(msg){alert("This is custome flash:" + msg);}""")

この他にもViewに関するAPI(i18nなど)はいくつかありますが、今回はここまで。
次回は、JavaScript、CSSについてもう少し詳しく見ていきます。

[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について掘り下げます。

[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の表示について確認します。