Jenkins(+Nginx) - Hipchat - Hubot 環境構築メモ

タイトルの通りの環境を作ってみたのでメモを残しておきます。

(昨日やったことを思い出しながら書いているので参考までに)

この環境でできることは

1.HipchatからHubotに命令する。
2.HubotがJenkinsのJOB実行APIを叩く
3.JenkinsがJOBを実行する
4.JenkinsがHipchatに結果を通知する

ができるようになりました。

0.目次

  • 1.Hipchatの設定
  • 2.EC2インスタンスの準備
  • 3.Jenkinsのインストール
  • 4.Jenkinsの初期設定とプラグインのインストール
  • 5.Hubotのインストール
  • 6.HipchatからHubot経由でJenkinsのJOBを実行する

1.Hipchatの設定

参考にしたのはこちらの記事
-> Jenkins+HipChat+Hubotをチーム開発に導入してお手軽CI

やることは以下の2つです。

  • Hubot用のJenkinsユーザー作成をGroupAdminから作成します。
  • 通知用のAPIトークンを生成しておく。

あとでこのAPIや、作成したユーザーのJabber IDが必要になります。
ユーザー登録時にEメール設定が必要で、gmailの拡張エイリアスを使って <メールアドレス>+jenkins@gmail.comとしておいた。
作成したユーザーをhubotを呼びたいルームに招待しておく。

2.EC2インスタンスの準備

参考にしたのはこの記事
-> EC2にJenkinsによるCI環境を作成する

Amazon Linux AMI (64bit)をM3.Mediumで作成。

必要そうなモジュールのインストールと初期起動設定

インスタンスができたらEC2-userでログインして、yumで必要そうなモジュール追加
実際の順序は異なったり、平行してRuby環境も作ってたのでJenkinsには不要なものもあるかもしれないけど
Node.jsのビルドや、Hubotの実行やらなんやらで以下のものがhisoryコマンドの結果にありました。

sudo yum update
sudo yum -y install git
sudo yum -y install build-essential
sudo yum -y install make glibc-devel gcc
sudo yum -y install openssl
sudo yum -y install openssl-devel
sudo yum -y install libicu-devel
sudo yum -y install nginx
sudo yum -y install httpd-tools
sudo yum --enablerepo=epel install redis
 
sudo /etc/init.d/redis start
sudo /sbin/chkconfig --levels 235 redis on
 
sudo /etc/init.d/nginx start
sudo  /sbin/chkconfig --levels 235 nginx on

3.Jenkinsのインストール

まずはJavaのインストール。

今回はJava8にしてみた。Oracleからダウンロードするには以下のコマンドでOK

wget --no-check-certificate --no-cookies --header "Cookie: oraclelicense=accept-securebackup-cookie" http://download.oracle.com/otn-pub/java/jdk/8u25-b17/jdk-8u25-linux-x64.rpm -q -O /tmp/jdk-8u25-linux-x64.rpm
sudo yum localinstall /tmp/jdk-8u25-linux-x64.rpm
sudo update-alternatives --install /usr/bin/java java /usr/java/default/bin/java 200000
ll /usr/java/jdk1.8.0_25/bin/
sudo update-alternatives --install "/usr/bin/java" "java" "/usr/java/jdk1.8.0_25/bin/java" 1

Jenkinsはyumでインストール

sudo wget -O /etc/yum.repos.d/jenkins.repo http://pkg.jenkins-ci.org/redhat/jenkins.repo
sudo rpm --import http://pkg.jenkins-ci.org/redhat/jenkins-ci.org.key
sudo yum install jenkins
sudo service jenkins start
sudo  /sbin/chkconfig --levels 235 jenkins on

これでとりあえずJenkinsは動く。

4.Jenkinsの初期設定とプラグインのインストール

初期ユーザー設定

あとで少し変更するがEC2にJenkinsによるCI環境を作成するにそって、アドミン権限的なユーザーを作成しておく。

プラグインのインストール

Manage Jenkins > Manage Plugins から必要そうなのと使いそうなプラグインを選んでインストールして再起動
いま確認したら以下のプラグインが有効になってた。

  • Ant Plugin 1.2
  • Build With Parameters 1.1
  • Config File Provider Plugin 2.7.5
  • Confluence Publisher 1.8
  • Credentials Plugin 1.18
  • External Monitor Job Type Plugin 1.2
  • GIT client plugin 1.11.0
  • GIT plugin 2.2.7
  • GitBucket Plugin 0.5.1
  • Gradle plugin 1.24
  • HipChat Plugin 0.1.8
  • Ivy Plugin 1.24
  • Javadoc Plugin 1.2
  • JDK Parameter Plugin 1.0
  • JUnit Plugin 1.1
  • Mailer Plugin 1.11
  • Matrix Authorization Strategy Plugin 1.2
  • Matrix Project Plugin 1.4
  • Maven Integration plugin 2.7
  • NodeJS Plugin 0.2.1
  • OWASP Markup Formatter Plugin 1.2
  • PAM Authentication plugin 1.2
  • Rake plugin 1.8.0
  • rbenv plugin 0.0.16
  • ruby-runtime 0.12
  • RubyMetrics plugin for Jenkins 1.6.2
  • sbt plugin 1.4
  • Scala JUnit Name Decoder 1.0
  • SCM API Plugin 0.2
  • SSH Credentials Plugin 1.10
  • SSH Slaves plugin 1.8
  • TestFairy 1.0
  • Testflight Plugin 1.3.9
  • Token Macro Plugin 1.10
  • Translation Assistance plugin 1.11
  • Windows Slaves Plugin 1.0
  • Xcode integration 1.4.2

プラグインの設定

インストールしたプラグインの設定をManage Jenkins > Configure System から行う。
ビルドツール系のパスの設定などを行う。
Hipchatプラグインの設定はこちらの記事のキャプチャにあるとおり
-> Jenkinsのビルド結果をHipChatで通知する方法

適当なビルドJOBを作成して、
[HipChat Notifications]のところでルーム名を指定すればビルド開始や完了時にHipchatに通知が届くようになりました。

5.Hubotのインストール

次はHubotを使ってHipchatからJenkinsへのリクエストを行えるようにします。
参考にしたのは公式doc
-> Getting Started With Hubot

Node.jsはnodebrewを使って準備

wget -O git.io/nodebrew
perl nodebrew setup
echo 'export PATH=$HOME/.nodebrew/current/bin:$PATH"' >> ~/.bash_profile
source ~/.bashrc
nodebrew install v0.10.32
nodebrew alias default v0.10.32
nodebrew use default
npm install -g hubot coffee-script

Hubotのワークディレクトリの作成

次のReadMEを参考に
-> hipchat/hubot-hipchat

ただし、この記事はHerokuで動かす前提で書かれているのでProcfileとかは不要です。

hubot --create jenkins
npm install
npm install --save hubot-hipchat
git add .
git commit -m "Initial commit"

hubotに渡す変数は全て環境変数経由のようなので、bin/hubotを修正します。
あと、PORT番号をJenkinsとぶつからないように変更します。

#!/bin/sh 
 
set -e
 
npm install
export PATH="node_modules/.bin:node_modules/hubot/node_modules/.bin:$PATH"
 
export HUBOT_HIPCHAT_JID="<作成したHipchatユーザーのJobberID>"
export HUBOT_HIPCHAT_PASSWORD="<作成したHipchatユーザーのログインパスワード>"
 
# ここはあとで設定する 
# export HUBOT_JENKINS_URL="<JenkinsのURL>" 
# export HUBOT_JENKINS_AUTH="<Jenkinsのベーシック認証ユーザ>:<Jenkinsのベーシック認証パスワード>" 
 
 
export PORT="8081"
 
exec node_modules/.bin/hubot "$@" --adapter hipchat

この状態でHubotを起動(start-stopデーモンがうまく動かなかったのでとりあえずnohupでバックグランド起動)すると、

nohup bin/hubot &

以下のようなログがでていればOK

[Mon Oct 20 2014 09:49:46 GMT+0000 (UTC)] INFO Connected to hipchat.com as @<Hubotユーザー名>
[Mon Oct 20 2014 09:49:46 GMT+0000 (UTC)] INFO Data for hubot brain retrieved from Redis
[Mon Oct 20 2014 09:49:47 GMT+0000 (UTC)] INFO Joining <ルームID>@conf.hipchat.com

hipchatのhubotユーザーがルームに入ってきます。

@<Hubotユーザー名> hi
@<Hubotユーザー名> translate me こんにちは

みたいなhubotのデフォルトのスクリプトに反応してくれるようになります。

6.HipchatからHubot経由でJenkinsのJOBを実行する

jenkins用hubotコマンドの入手

さて、最後はHubotに命令することでJenkinsのJOBが実行できるようにします。
Jenkinsを起動するスクリプトはこれ
github/hubot-scriptsにはほかにもたくさん便利コマンドがありそうなので、いろいろ試してみたい。

使いたいコマンドをscriptsディレクトリに保存します。

wget -O https://raw.githubusercontent.com/github/hubot-scripts/master/src/scripts/jenkins.coffee

jenkins.coffeeのヘッダーコメントにあるように、このコマンドに必要な引数を
bin/hubot内で環境変数にセットします。

export HUBOT_JENKINS_URL="<JenkinsのURL>"
export HUBOT_JENKINS_AUTH="<Jenkinsのベーシック認証ユーザ>:<Jenkinsのベーシック認証パスワード>"

一度hubotを止めて、bin/hubotで再度起動します。

Jenkinsのベーシック認証設定

Jenkinsに対してリモートからHTTPリクエストを投げる場合は、Jenkinsの認証はベーシック認証で行う必要があるようです。
ググるとJenkins単体でベーシック認証をするのではなく、ApacheやNginxで行うのが一般的なようでした。

参考にしたのはこの記事
-> JenkinsサーバのSSL対応とBasic認証

今回はSSL化は行いませんでしたが、SSL設定はJenkinsはあまり関係なく、keyとcertファイルを用意してNginxだけでやれば良さそうです。
Nginxでリバースプロキシします。
今回EC2のセキュリティグループで8080しかポートを開けていなかったのでNginxで8080をListenしてJenkinsは別のポートを使うように
/etc/sysconfig/jenkinsを修正する必要がありました。

sudo su - root
cd /etc/nginx/conf.d
cp virtual.conf jenkins.conf
vi jenkins.conf
htpasswd -c /etc/nginx/.htpasswd jenkins_admin
cp -rp /etc/sysconfig/jenkins /etc/sysconfig/jenkins.bak
vi /etc/sysconfig/jenkins
service nginx stop
service jenkins restart
service nginx start

/etc/sysconfig/jenkinsでPORTを8090に変更して、
jenkins.confは以下の通りにしました。

server {
    listen       8080;
    server_name  jenkins;
 
    location / {
      proxy_pass http://127.0.0.1:8090/;
      auth_basic "jenkins server";
      auth_basic_user_file /etc/nginx/.htpasswd;
      proxy_set_header Authorization "";
    }
}

これでHipchatからhubotに対してjenkins build ジョブ名と語りかけると指定したジョブが実行されます。
ビルド結果はJenkinsから直接Hipchatに対して通知されます。


今後は、JIRA,bitbucket連携を進めていきたいです。

[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]


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

  • Sep
  • 7
  • 2014

IT

ScalaMatsuri2014で「Xitrum Live Coding」というタイトルで発表をしました。

ScalaMatsuri2014 で「Xitrum Live Coding」というタイトルで発表をしました。


ビデオはこちら
http://live.nicovideo.jp/watch/lv191315534

デモコードはこちら
https://github.com/xitrum-framework/matsuri14


コンパイルエラーとか失敗したりしたけど楽しかったです。ありがとうございました。
ScalaでWebフレームワークというと圧倒的にPlayFrameworkが人気かと思いますが、
Xitrumの知名度が少しでも上がったかなと思います。
また、デモアプリのチャットにはAkkaを使用していますが、
弊社モビルスのKonnect内部ではAkkaと合わせてHazelcastをもっと応用しています。

SparkやDatabricksなど、Konnectのサービスの解析に使えそうなので
勉強したいと思いました。

最後に、モビルスでもScalaエンジニアを募集しています。
興味があるかたはTwitter(@george_osd)などでご連絡いただければと思います。

[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に向けた機能なのでまた後程。