2010年3月25日木曜日

[Scala] Elemのlabel(要素名)を書き換える

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
こんな感じか?
def changeTagName[T >: Node](from: String, to: String)(in: T): T = in match {
  case Elem(p, l, a, ns, child @ _*) => 
    val label = if (l == from) to else l
    Elem(p, label, a, ns, child.map(changeTagName(from, to)(_)): _*)
  case x => x
}

使用例

scala> val hoge2moge = changeTagName("hoge", "moge") _
scala> hoge2moge: (scala.xml.Node) => scala.xml.Node = <function>

scala> hoge2moge(<pre:hoge attr="value">parentText<child:hoge>childtext</child:hoge></pre:hoge>)
scala> res17: scala.xml.Node = <pre:moge attr="value">parentText<child:moge>childtext</child:moge></pre:moge>

追記:さらに汎用化してみた

高階関数化して、引数に名前を変更するための関数を渡せるようにした。
def changeTagName[T >: Node](changeFunc: (String) => String)(in: T): T = in match {  
  case Elem(p, l, a, ns, child @ _*) =>   
    Elem(p, changeFunc(l), a, ns, child.map(changeTagName(changeFunc)(_)): _*)  
  case x => x  
}

使用例

お題はこちら
scala> val c = changeTagName(s => if (List("moge", "koge").contains(s)) "bege" else s) _
c: (scala.xml.Node) => scala.xml.Node = <function>

scala> c(x)
res6: scala.xml.Node =
<lage>
         <hoge>
           <bege></bege>
           <bege></bege>
         </hoge>
         <hoge>
           <bege></bege>
           <bege></bege>
         </hoge>
       </lage>

23:52 さらにちょっと修正

flatMap使えば別にNodeSeq渡せることに気がついた。
def changeTagName(changeFunc: (String) => String)(in: NodeSeq): NodeSeq = in match {  
  case Elem(p, l, a, ns, child @ _*) =>   
    Elem(p, changeFunc(l), a, ns, child.flatMap(changeTagName(changeFunc)(_)): _*)  
  case x => x  
}

2010年3月22日月曜日

[Lift][GAE] spin-up時間を測定してみる

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
@yuroyoroさんがGAE上のScalaのスピンアップ時間を測定していたので、倣ってLiftのspin-up時間を測定してみた。前は数回適当に起動した時間を見ていて7-8秒って感じだったのを、もうちょっと正確に。

測定したのはarchetype:blankで生成される、LiftテンプレートとSnippetを使用したHelloWorld。また、不要なjarを減らしたら早くなるのかも気になったのでいくつかパターンを用意してみた。

結果

appavemaxmin
lift-blank *1682973385938
nomapper *2668171056035
nomapper/nojson *3653171245705
min *4648172405957
  1. Lift2.0-M3のarchetype-blankをそのままGAEに載せたもの
  2. GAE上ではあまり使わないと思われるlift-mapperのjarを取り除いたもの
  3. ↑からさらにlift-jsonのjarを取り除いたもの
  4. ↑からさらにHelloWorldには不要なjar(javax.mail, commons-codec)を全て取り除いたもの

考察

  • 簡単なLiftテンプレート+Snippetだと、Lift単品では7秒弱。
  • maxとminが前後しているのは気になるが、平均で見ると不要なjarを取り除くと若干早くなるっぽい。
    ※前測って7-8秒だったのは、Lift以外のライブラリのjarがあったのでさらにオーバーヘッドがかかったのか?

ほか気づいたこと

Liftがリクエストの処理にかかった時間をログに出してくれるのだが、初回がやたら遅くて3秒ほどかかっているのでこれがspin-upの半分を占めている。2回目のリクエストからは数ms…。
INFO - Service request (GET) / took 3056 Milliseconds
次はこれを調べてみることにする。

2010年3月17日水曜日

[GAE] JavaでAppstatsを使ってみる

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
3/26追記:やっと公式にドキュメントが追加されました

GAE SDK 1.3.2より、JavaでもAppstatsが使えるようになりました。1.3.2-preの段階ではまだlabsのjarに入っているので、appengine-api-labs-1.3.2.jarをWEB-INF/libに含めて、web.xmlに↓こんな感じで設定するだけ。※この設定だとstats自身もフィルタ対象になってしまうので除外する方法調査中。
<filter>
  <filter-name>AppstatsFilter</filter-name>
  <display-name>Appstats Filter</display-name>
  <description>Appstats Filter</description>
  <filter-class>com.google.appengine.tools.appstats.AppstatsFilter</filter-class>
</filter>

<filter-mapping>
  <filter-name>AppstatsFilter</filter-name>
  <url-pattern>/*</url-pattern>
</filter-mapping>

<servlet>
  <servlet-name>AppstatsServlet</servlet-name>
  <servlet-class>
    com.google.appengine.tools.appstats.AppstatsServlet
  </servlet-class>
</servlet>

<servlet-mapping>
  <servlet-name>AppstatsServlet</servlet-name>
  <url-pattern>/stats/*</url-pattern>
</servlet-mapping>

ブラウザで/stats/でを開くと、↓こんな統計が見れます。
※/stats て最後のスラッシュを抜かすとなぜかリダイレクトループしてしまいます…
※IE8だと表示が崩れてました。

これだけだとURLが分かれば誰でも見えてしまうので、管理者のみアクセスできるような設定もした方がよいでしょう。
<security-constraint>
  <web-resource-collection>
    <url-pattern>/stats/*</url-pattern>
  </web-resource-collection>
  <auth-constraint>
    <role-name>admin</role-name>
  </auth-constraint>
</security-constraint>

[Lift][GAE] DataStoreに溜まったsessionをcronで削除する

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
↓こんなobjectを作っておき、cronを設定した。だいたい550件程度削除すると30秒経過のタイムアウトでエラーになるけど別に気にしない。
import net.liftweb.http._
import net.liftweb.common._
import com.google.appengine.api.datastore._

object SessionCleaner {
  // Iteratorのimplicit conversionは定義されていないようなので自前で定義。
  implicit def j2s[A](j: java.util.Iterator[A]) = 
    new scala.collection.jcl.MutableIterator.Wrapper[A](j)
  private lazy val DSS = DatastoreServiceFactory.getDatastoreService

  def execute(): Box[LiftResponse] = {
    var count = 0
    try {
      val q = new Query("_ah_SESSION")
      q.addFilter("_expires", Query.FilterOperator.LESS_THAN_OR_EQUAL, System.currentTimeMillis)
      DSS.prepare(q).asIterator.foreach(e => {DSS.delete(e.getKey); count = count + 1})
    } finally {
      println(count + " sessions deleted.")
    }
    Full(OkResponse())
  }
}

Boot.scalaのdef boot内にはこんな感じ。
LiftRules.statelessDispatchTable.append {
  case Req("cron" :: "sessionCleaner" :: Nil, _, _) => () => SessionCleaner.execute()
}

ソース全体はgithubに置きました

2010年3月16日火曜日

[Lift] 無駄に新規セッションを生成させずに条件によってリダイレクトする方法

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
リクエストヘッダの条件によってリダイレクトを行うには、snippet内でS.redirectToを使ってリダイレクトするのが一番簡単な方法ですが、Liftでsnippetを使うと必ずセッションを生成してくれます。検索エンジンのクローラbotとかからアクセスがある度に無駄にセッションが生成されてしまってサーバリソースを無駄に使っていたので、こんな感じで不要な場合はセッションが生成される前にリダイレクトさせてみました。
// Boot.scala の def boot内
LiftRules.statelessDispatchTable.prepend {  // *1
  case MyReq("venue" :: vid :: _, r) if r.header("User-Agent") == Full("何か") => // *2
    () => Full(RedirectResponse("http://foursquare.com/venue/" + vid))
}

object MyReq {  // *3
  def unapply(in: Req): Option[(List[String], HTTPRequest)] = 
    Some((in.path.partPath, in.request))
}
  1. LiftRules.statelessDispatchTableにPartialFunction[Req, () => Box[LiftResponse]]を追加しておくと、セッション生成前に条件にマッチした場合は戻り値のLiftResponseをクライアントに返してくれます。
  2. 通常は、Req(パス, 拡張子, リクエストタイプ)でパターンマッチさせますが、ここではリクエストの内容によって判断したいため、*3で独自の抽出子(extractor)を定義し、抽出したHTTPRequestの中身(上記例ではリクエストヘッダ)をパターンガード条件として使用しています。
  3. unapplyメソッドを持つobjectが抽出子です。参照:コップ本第24章(p.440)
抽出子とパターンガードを使うとパターンマッチが益々便利です。

2010年3月8日月曜日

[Lift] GAEがスケールアウトするとLiftセッションが切れる…

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
LiftはServletSessionとは別に独自にセッションを実装しているので、StatefulSnippetはGAEのスケールアウトにうまく対応できないようです…。当然、StatefulSnippetじゃなくてもコールバックをクロージャで実装していると、スケールアウトにタイミング悪くもぶち当たるとアウトなはず。

以下がスケールアウトしたときのログ。

ロードバランサがCookie見てセッション同じなら同じサーバインスタンスに振り分けてくれればうれしいのに…。というわけでLift on GAEな人は投票を

2010年3月5日金曜日

[Lift] LiftFormを使わずにStatefulSnippetをステートフルに使う

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
LiftFormは大変便利なのですが、Lift流にsubmit時のコールバックをクロージャで書いてしまうと、セッションが切れた時にコールバックが動きません。Google App Engine上で動かしたりなんかすると、3分程度でスピンダウンしてしまうので、すぐ切れてしまってフォーム入力に時間がかかるようなアプリだと、セッション切れてまともに動きません…。(※実際は、ajax_requestが定期的にコールされるので、そんなにスピンダウンしない?と思うけど今作ってるのが携帯向けサイトでajax_requestをOFFにしてるので問題が顕在化)

そこで、クラシックな通常のHTMLフォームを使い、なおかつStatefulSnippetとして同じインスタンスが呼ばれるようなコードを書いてみました。

ソースを見る

まずはLift流に

普通にLift流に簡単なフォームを書くとこんな感じです。
<lift:StatefulTest.liftForm form="POST">
  <e:instance/>
  <e:input/>
  <e:submit/>
</lift:StatefulTest.liftForm>

Snippetはこんな感じ。
def liftForm(in: NodeSeq): NodeSeq = {
var name = ""
def sayHello() = {
  S.notice("Hello, " + name + ". I'm " + this)
  redirectTo("/liftSayHello")
}
bind("e", in,
  "instance" -> Text("I'm " + this),
  "input" -> SHtml.text(name, i => name = i),
  "submit" -> SHtml.submit("say hello", sayHello)
)
}

無理やり普通のformで

これと同様なフォームを、無理やり書くとこんな感じです。
<lift:StatefulTest.myForm>
  
  <form action="mySayHello" method="POST"> 
    <e:instance/>
    <e:key/>
    <e:input/>
    <e:submit/>
  </form>
</lift:StatefulTest.myForm>

ポイントは、registerThisSnippet()を呼ぶ関数ブロックを、S.fmapFuncで自分で関数マップに登録してやり、そのキーをhiddenで埋め込んでやること(*1)。
def myForm(in: NodeSeq): NodeSeq = {
S.fmapFunc((a: List[String]) => {registerThisSnippet()})(key => {
  bind("e", in,
    "key" -> <input type="hidden" name={key} value="_"/>, // *1
    "instance" -> Text("I'm " + this),
    "input" -> <input type="text" name="name"/>,
    "submit" -> <input type="submit" value="say hello"/>
  )
})
}
このやり方だと、セッションが続いていれば同じインスタンスのStatefulSnippetが呼ばれ、続いていない場合でも、少なくとも新しいインスタンスのStatefulSnippetで処理は行えます。(CSRFの問題がありますが…)

ソースはgithubに置きました。http://github.com/pomu0325/lift_sandbox

2010年3月3日水曜日

[Scala] valの左辺にパターンマッチが使える

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
今日の第4回Scala言語仕様輪読会での収穫。

pがパターンの時、val p = eは以下の様に展開される。
val $x = e match {case p => (x1, . . . , xn)}
val x1 = $x._1
. . .
val xn = $x._n
The Scala Language Specification(PDF)のp.36参照(タプルが{}になっているのは古い仕様?なので↑では()に直しています)。

簡単な例だと、タプルを使って同時にvalに代入できる。
val (x, y) = (1, 2)
// ↑は↓に展開され
val tmp = (1, 2) match {case (a, b) => (a, b)}
val x = tmp._1
val y = tmp._2
// 結局↓と同じ
val x = 1
val y = 2

Liftで↓なコードがあって、意味が分からなかったのだが、やっと理解できた。
// net/liftweb/http/Req.scala l.275
case class ParamCalcInfo(paramNames: List[String],
            params: Map[String, List[String]],
            uploadedFiles: List[FileParamHolder],
            body: Box[Array[Byte]])
// l.284
class Req(val path: ParsePath,
          val contextPath: String,
          val requestType: RequestType,
          val contentType: Box[String],
          val request: HTTPRequest,
          val nanoStart: Long,
          val nanoEnd: Long,
          private[http] val paramCalculator: () => ParamCalcInfo, // *2
          private[http] val addlParams: Map[String, String]) extends HasParams
{
// (省略)
// l.342
  lazy val ParamCalcInfo(paramNames: List[String],  // *1
            _params: Map[String, List[String]],
            uploadedFiles: List[FileParamHolder],
            body: Box[Array[Byte]]) = paramCalculator()
// (省略)
}
  1. なぜ突然valの後にcase classが…? と思ったらこれがパターンマッチ。コンストラクタで指定した関数paramCalculator(*2)がParamCalcInfoを返すので、ParamCalcInfoの各valがそれぞれparamNames, _params, uploadedFiles, bodyに束縛され、Req.body等として使える。つまり、↓の様に展開されているということと理解。
    val tmp = paramCalculator() match {
      case ParamCalcInfo(a, b, c, d) => (a, b, c, d)
    }
    val paramNames = tmp._1
    val _params = tmp._2
    val uploadedFiles = tmp._3
    val body = tmp._4
    

2010年3月2日火曜日

[Scala] 文字列に含まれるURLをaタグにしてNodeSeqで返す

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
Liftでbindする時にNodeSeqが必要だったので書いてみた。はじめ、Regex.findAllInとfor式のyieldでできるんじゃないかと試行錯誤してたが、再帰を使った方が簡単なことに気づいてこんな感じになった。

import scala.xml._
implicit def c2s(c: CharSequence): String = c.toString
val URLPATTERN = """http://[\d\w\-\./%?=#]+""".r
def linkURL(s: String): NodeSeq = URLPATTERN.findFirstMatchIn(s) match {
 case None => Text(s)
 case Some(m) =>
  <xml:group>{Text(m.before)}<a href={m.matched}>{m.matched}</a>{linkURL(m.after)}</xml:group>
}

使用例

scala> linkURL("aaaa http://pomu0325.blogspot.com/ bbbb http://twitter.com/pomu0325 cccc")
res21: scala.xml.NodeSeq = aaaa <a href="http://pomu0325.blogspot.com/">http://pomu0325.blogspot.com
/</a> bbbb <a href="http://twitter.com/pomu0325">http://twitter.com/pomu0325</a> cccc