2010年12月31日金曜日

[Lift] Lift2.2でCookieが無い場合にjsessionidがリンクに付加されない

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
#658 Supressing URL rewriting for jesessionid from Lift の修正により、デフォルトでjsessionidが付かないように動作が変わっています。

Boot.scalaで、
LiftRules.encodeJSessionIdInUrl_? = true
してやればOK。

Liftで携帯サイト作ってる人とかはアップデートの際に要注意!

2010年12月30日木曜日

[Scala] dispatchでContent-Length付きのmultipartなPOSTする方法

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
とある写真投稿サービスのAPIにdispatch(0.7.8)を使って実装しようとした時のこと。APIのレスポンスでContent-Lengthが無いって怒られました。

dispatch-mimeを使うとmultipart/form-data形式でPOSTができるのですが、普通に実装するとContent-Lengthを送ってくれないようです。

ソースコメントにも書いてあるし。
Note that when using an InputStream generator, chuncked encoding will be used with no Content-Length header

Fileが引数の方を使えば良さげだけどGAEからだとFile使えません。dispatchのソースとapache-httpclientのjavadoc眺めて工夫したらできました。

import dispatch._
import dispatch.Http._
import dispatch.mime.Mime._
import org.apache.http.entity.mime.content.ByteArrayBody

var req = ... //普通にdispatchのRequest作る
req = req next req.add(paramName, new ByteArrayBody(data, contentType, filename)) //ByteArrayBodyを指定

※ただし、ByteArrayBodyはhttpclient 4.1(現在BETA1)から追加されるクラスなので4.1のjarを使わないとダメ(dispatch 0.7.8のdependencyはhttpclient 4.0になってた)。
sbtならこんな感じで。
val httpmime = "org.apache.httpcomponents" % "httpmime" % "4.1-beta1"

2010年9月6日月曜日

[Lift][GAE] Scala座01でLTしてきました

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
第1回Scala座に参加してきました。実はスタッフとして少々お手伝いもしたのですが大した事ないので(ry

収穫は…
  • いろいろと勉強不足だなぁと刺激を受けた(FP的な意味で)
  • Twitter等で気になっていた人々にリアルで会えた
  • ワラビモチ!←マイブーム
といったとこでしょうか。

「Lift on GAE/J」ネタでLTさせてもらったのですが、先にまとめてしまうと、「LiftはGoogle AppEngineには向いてない」てことです。詳しくは「Lift on GAE/J」のスライド見てください。

LTやろうと思ったきっかけは、自分でLiftも(Scalaも)GAEもよく分かってないうちにLift+GAEという組み合わせを選んでしまったのですが、いろいろと苦労したので… 最近TwitterでLiftをGAEで動かしてみるとかのtweetを時々見かけ、危険だよー、ということを広めたかっただけです。

ただ、勘違いしないで欲しいのは、Liftがダメと言っている訳ではなく、LiftはフルスタックのWebフレームワークとして、慣れれば生産性よくAJAX/Cometを使ったリッチなWebアプリを作れる素晴らしいフレームワークだと思っています。クラウドという性質上、GAEには様々な制限があるので、GAE上で動かしたいのであれば他のもう少し軽量なフレームワークが向いていると思います(まだあまり試せていませんが…)。

Scala座の企画をして下さった@keisuke_nさん、Scalaを勉強していく上で参考にさせていただいたり助けていただいたりで本当にいろいろとお世話になっている@yuroyoroさん、@kmizuさん、@takedasoftさん、会場・懇親会でお話させていただいたみなさま、本当にありがとうございました。ぜひ第2回でもよろしくお願いします!

2010年5月15日土曜日

[OAuth] 携帯でTwitterのOAuthを無理やりやってみた

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
2011-07-04追記: 今はTwitterのOAuth画面もガラケー対応されてます。自動でリダイレクトはされないのでユーザによるクリックは相変わらず必要ですが。

モバイルフォースクエアの開発初期に、OAuthでTwitterの認証をやろうとしていた時に試した結果です。手元にあったAU,docomoの端末で確認しています。

AU(W61CA)でTwitterのOAuthをやってみた

ログイン画面 承認画面
特に問題なし! 自動でリダイレクトはされないが、リンクを手動でクリックすれば動作はOK!

docomo Cookie対応機種(N-07A)でTwitterのOAuthをやってみた

ログイン画面 承認画面
文字化けがひどいが、ID・パスワード欄っぽいところに入れて1つ目のボタン(ログイン)を押せば承認画面に飛ぶ。 自動でリダイレクトはされないが、リンクを手動でクリックすれば動作はOK!

docomo Cookie非対応機種(SH704i)でTwitterのOAuthをやってみた

ログイン画面 承認画面
両方とも送信ボタン…。1つ目を押せば承認画面に飛ぶ。 文字化けがひどいが、リンクを手動でクリックすれば動作はOK!

まとめ

 docomoだと文字化けがひどいが、OAuthの機能的には動作する。ただし最後のリダイレクトを手動でクリックしないといけないのでユーザにこれを強いるのは難しい。
※実際、OAuth開始前の画面に注意書きを入れたにもかかわらず、最後のリダイレクトのクリックが分からず、トークンが取得できていないユーザ多数いた
 PIN入力方式にすればよいが、PC・スマートフォンでも同じコンシューマキーを使いたいので、PC・スマートフォンでのユーザビリティが落ちるので却下。PCでOAuth認証だけ先にさせるのも携帯のみのユーザのことを考えると却下。
→結果、xAuthを使うことにした。

[OAuth] 携帯でfoursquareのOAuthを無理やりやってみた

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
モバイルフォースクエアの開発初期に、OAuthでfoursquareの認証をやろうとしていた時に試した結果です。手元にあったAU,docomoの端末で確認しています。

AU(W61CA)でfoursquareのOAuthをやってみた

ログイン画面 承認画面
日本語文字化け… 文字化け&Denyが押せないが、動作はOK!

docomo Cookie対応機種(N-07A)でfoursquareのOAuthをやってみた

ログイン画面 承認画面


デザインは崩れるが、動作はOK! デザインは崩れるが、動作はOK!

docomo Cookie非対応機種(SH704i)でfoursquareのOAuthをやってみた

ログイン画面→表示が崩れ、ログインボタンを押しても同じ画面が繰り返し表示されるだけ…。
※当時はログインできていた記憶が…、サーバ側の実装が変わったかもしれないです


まとめ

Cookie対応機なら文字化けしたりデザインは崩れるが、OAuthの処理自体は正常。文字化けがひどい状態ではサービス提供できないので、foursquareの中の人に日本の携帯ブラウザ事情を説明し、Auth Exchange採用。

2010年4月27日火曜日

[Scala][GAE] dispatchのOAuthをGAEで使う

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
dispatchが内部でcommons-httpclientを使っていて、GAEでの禁止クラスを使っているのでそのままだとエラーになった。ので無理やりimplicit defでURLFetchのHTTPRequestに変換することで回避してます。こんな感じ。

import dispatch._
import com.google.appengine.api.urlfetch._
import java.net.URL

object DispatchHelper {
  /**
   * convert dispatch.Request -> String
   */
  implicit def r2s(r: Request) = {
    r.host.get + r.req.getRequestLine.getUri
  }

  /**
   * convert dispatch.Request -> com.google.appengine.api.urlfetch.HTTPRequest
   */
  implicit def dispatch2gae(r: Request): HTTPRequest = {
   val isPost = r.req.getMethod == "POST"
   val g = new HTTPRequest(new URL(r), if (isPost) HTTPMethod.POST else HTTPMethod.GET, FetchOptions.Builder.withDeadline(10))
   r.req.getAllHeaders.foreach(h => {g.addHeader(h); println(h)})
   r.req match {
    case p: org.apache.http.client.methods.HttpPost =>
     val payload = new java.io.ByteArrayOutputStream
     p.getEntity.writeTo(payload)
     g.setPayload(payload.toByteArray)
     println(p.getEntity.getContentType)
    case _ =>
   }
   g
  }
}

こんな感じでimplicit defを宣言してimport DispatchHelper._すれば、dispatchでリクエストを作って、URLFetchService.fetch()の引数にそのまま渡せばOK。

dispatchの解説はこちら

2010-06-26追記

2chで質問受けてコードが一部足りなかったので追記しました。

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

2010年2月22日月曜日

[Lift] "?"を含むURLがStatefulSnippet.linkメソッドでStatefulにならない

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
StatefulSnippetで、同じインスタンスのStatefulSnippetを呼び出すようなリンク(<a>タグ)を出力する際には、StatefulSnippet.linkメソッドを使用します。が、StatefulSnippet.linkを使ってもリンク後に別のインスタンスのSnippetが作られてしまう場合があり、困ったのでStatefulになる仕組みを追ってみました。

仕組み

  • StatefulSnippet.linkでリンク用URLを出力すると、ランダムなキーでコールバック関数がセッションに保持され、http://localhost/search?F869982852207GSN=_ の様にキーがクエリ文字列として付加されます。
  • このリンクをクリックすると、LiftSession.runParamsでこのキーで登録されているコールバック関数が実行されます。
  • コールバック関数内で、registerThisSnippet()が呼ばれ、ステートが維持される仕組みになっています。

    ソースを追ってみると、StatefulSnippet.linkはコールバック時に第2引数で指定した関数の前に、registerThisSnippet()を呼ぶようなブロックに差し替えて、SHtml.linkと同じ処理をしているだけです。
    // StatefulSnippet.scala
    def link(to: String, func: () => Any, body: NodeSeq): Elem = SHtml.link(to, () => {registerThisSnippet(); func()}, body)
    

    SHtml.linkは何をやっているかというと、fmapFuncでコールバック関数を登録し、そのキーをクエリ文字列としてURLに付加しています。
    // SHtml.scala
    def link(to: String, func: () => Any, body: NodeSeq,
             attrs: (String, String)*): Elem = {
      fmapFunc((a: List[String]) => {func(); true})(key =>
              attrs.foldLeft(<a href={to + (if (to.indexOf("?") >= 0) "&" else "?") + key + "=_"}>{body}</a>)(_ % _))
    }
    

    ここで、既にクエリ文字列がある(="?"が含まれる)場合には"&"でつなげてることに気づき、問題解決。

    問題解決

    問題となったは、AU携帯で位置情報を付加してリクエストを送るための、以下の様なaタグを出力しようとした部分。
    <a href="device:location?url=http://www.mb4sq.jp/search"/>
    

    def search(in: NodeSeq): NodeSeq =
      bind("f", in, "link" -> link("device:location?url=http://localhost/search", ()=>"", Text("search")))
    
    こんなコードを書いてみたところ、本来欲しいURLは
    device:location?url=http://localhost/search?F869982852207GSN=_ の様なものなんですが、元のURLに"?"が入ってしまっているので、"?"の代わりに"&"が付加されてしまい、
    device:location?url=http://localhost/search&F869982852207GSN=_ になってしまってるわけですね…。これではクエリ文字列として取得できず、ステートを維持するためのregisterThiSnippet()のコールバックが呼ばれずに、ステートが切れてしまっていたと。

    結局、StatefulSnippet.linkをオーバーライドして、GPSのリンクの場合はハードコードで"?"を付けるようにして回避しました。
    override def link(to: String, func: () => Any, body: NodeSeq): Elem = {
      def insert(s: String, i: String, p: Int) = List(s take p, i, s drop p).mkString
      if (to.startsWith("device:")) 
        S.fmapFunc((a: List[String]) => {registerThisSnippet(); func()})(key => {
        val u = if (to.indexOf("&ver=1") > 0) insert(to, ("?" + key + "=_"), to.indexOf("&ver=1"))
            else to + ("?" + key + "=_")
          <a href={u}>{body}</a>
        })
       else super.link(to, func, body)
    }
    
  • 2010年2月20日土曜日

    [Scala] Stringの指定位置にStringを挿入する

    このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
    ありそうでなさそうなので作ってみた。RichStringがSeqなのでtakeとかdropとか使えて便利。
    scala> def insert(s: String, i: String, p: Int) = List(s take p, i, s drop p).mkString
    insert: (String,String,Int)String
    
    scala> insert("abcd", "123", 2)
    res1: String = ab123cd
    

    [Scala] untilループ

    このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
    Scala Hack-a-thonで、Scalaにはuntilループが無いけど作れる、って@yuroyoroさんが言っていたので、作ってみた。
    scala> def until(b: =>Boolean)(f: =>Any) = while(!b)f
    until: (=> Boolean)(=> Any)Unit
    
    ポイントは=>で引数を名前渡しにするとこか。

    使用例

    scala> var i = 0
    i: Int = 0
    
    scala> until (i==10) {println(i);i=i+1}
    0
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    できることは分かったけど、きっと使わないなぁ…。

    2010年2月18日木曜日

    [Scala][GAE] URLFetchのリトライ

    このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
    Scala勉強会@東北で取り上げていたquerulousRetryingQueryにインスパイアされて、GAEのURLFetchをリトライするコードを書いてみた。

    URLFetchがIOExceptionを投げるか、ステータス500が返ってきた場合にリトライ。
    import com.google.appengine.api.urlfetch._
    
    val UFS = URLFetchServiceFactory.getURLFetchService()
    def retryingURLFetch[A](r: HTTPRequest, retry: Int)(f: HTTPResponse => A): A = {
      try {
        val res = UFS.fetch(r)
        res.getResponseCode match {
          case 500 =>
            if (retry <= 0) f(res)
            else retryingURLFetch(r, retry - 1)(f)
          case _ => f(res)
        }
      } catch {
        case e: java.io.IOException =>
          if (retry <= 0) throw e
          retryingURLFetch(r, retry - 1)(f)
      }
    }
    

    使用例

    import java.net.URL
    import com.google.appengine.api.urlfetch._
    
    retryingURLFetch(new HTTPRequest(new URL("http://pomu0325.blogspot.com")), 2) {
      res => println(new String(res.getContent, "utf-8"))
    }
    
    不安定なAPIを呼ぶ時とかに使えそう。

    2010年2月16日火曜日

    [Joomla] mod_feedでrssの日付を表示する

    このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
    Joomla!のRSS表示モジュール(mod_feed)で、日付を表示する設定がなかったので、デフォルトテンプレートを書き換えてみた。modules/mod_feed/tmpl/default.phpの、65行目が追加した行。
    for ($j = 0; $j < $totalItems; $j ++)
    {
      $currItem = & $feed->items[$j];
      // item title
      ?>
      <li>
      <span style="padding-right:10px;"><?php echo $currItem->get_date('Y年n月d日'); ?></span>
      <?php
      if ( !is_null( $currItem->get_link() ) ) {
      ?>
        <a href="<?php echo $currItem->get_link(); ?>" target="_blank">
    

    2010年2月13日土曜日

    [Lift] Liftのテンプレートでutf-8以外のレスポンスを返す

    このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
    Liftでテンプレートを使用したページを作ると、必ずutf-8で返してくれるようです。携帯向けサイトとかで、どうしてもShift_JISで返したい時に、これだと文字化けしてしまいます。

    例のごとくドキュメントが少ないので、Liftのソースを追ってみます。通常、テンプレートを使用したページは、XhtmlResponseとしてNodeが保持され、LiftServletの最後にtoResponseでInMemoryResponseに変換(*1)してから送られます。
    // net/liftweb/http/LiftServlet.scala(l.183)
    resp match {
      case Full(cresp) =>
        val resp = cresp.toResponse // *1 LiftResponse.toResponseはInMemoryResponseを返す
         logIfDump(req, resp)
         sendResponse(resp, response, Full(req))
    //...後略...
    

    問題1:文字コード

    XhtmlResponseのtoResponseで何をやっているのか見てみると、mixinしているNodeResponse.toResponseで、utf-8とハードコードされています…。
    // net/liftweb/http/LiftResponse.scala(l.415)
    InMemoryResponse(ret.getBytes("UTF-8"), headers, cookies, code)
    

    問題2:Content-Type

    次、Content-Typeにもcharset=utf-8が勝手に出てしまいます。どこでやっているかと調べると、LiftServlet.sendResponseでContent-Typeをセットしています。determineContentTypeはここでもまたutf-8がハードコードです。ただし、ここでは明示的にContent-Typeを指定しておけば、上書きはしないようです。
    // LiftServlet.scala(l.482)
    // insure that certain header fields are set
    val header = insureField(fixHeaders(resp.headers), List(("Content-Type", // *2
            LiftRules.determineContentType(pairFromRequest(request))),
      ("Content-Length", len.toString)))
    
    // LiftRules.scala(l.168)
    @volatile var determineContentType: PartialFunction[(Box[Req], Box[String]), String] = {
      case (_, Full(accept)) if this.useXhtmlMimeType && accept.toLowerCase.contains("application/xhtml+xml") =>
        "application/xhtml+xml; charset=utf-8"
      case _ => "text/html; charset=utf-8"
    }
    
    1. insureFieldはnet.liftweb.util.HttpHelpersのメソッド。第1引数のList[Pair]に第2引数のList[Pair]の_1が含まれてない場合に、追加する。

    問題3:XML宣言

    さらに、<?xml version="1.0" encoding="UTF-8"?>のXML宣言も必ず付けてくれるようです。が、S.skipXmlHeaderをtrueにしておけば、何もしないようです。
    // net.liftweb.http.LiftRules.scala(l.442)
    @volatile var calculateXmlHeader: (NodeResponse, Node, Box[String]) => String = {
      case _ if S.skipXmlHeader => ""
      case (_, up: Unparsed, _) => ""
    
      case (_, _, Empty) | (_, _, Failure(_, _, _)) =>
        "\n"
    
      case (_, _, Full(s)) if (s.toLowerCase.startsWith("text/html")) =>
        "\n"
    
      case (_, _, Full(s)) if (s.toLowerCase.startsWith("text/xml") ||
          s.toLowerCase.startsWith("text/xhtml") ||
          s.toLowerCase.startsWith("application/xml") ||
          s.toLowerCase.startsWith("application/xhtml+xml")) =>
        "\n"
    
      case _ => ""
    }
    

    対策

    LiftRulesに、responseTransformersというものがあり、ここでLiftResponseが送られる前に書き換えることができます。最終的に、Boot.scalaで以下の様なことをしてやると、好みの文字コードでレスポンスを返すことができました。
    def boot {
      //...省略...
      LiftRules.responseTransformers.append(conv2sjis)
    }
    
    private def conv2sjis(org: LiftResponse): LiftResponse = {
      org match {
        case x: XhtmlResponse =>
          S.skipXmlHeader = true // *1
          val m = x.toResponse // *2
          val h = x.headers ::: ("Content-Type", "text/html; charset=Shift_JIS") :: Nil // *3
          InMemoryResponse(new String(m.data, "utf-8").getBytes("Shift_JIS"), h, m.cookies, m.code) // *4
        case _ => org
      }
    }
    
    1. XML宣言を付けないようにS.skipXmlHeader=trueにセット。
    2. 一度、InMemoryResponseに変換
    3. Content-TypeヘッダをListに追加。
    4. SJISに変換し、新たなInMemoryResponseを返す。(LiftResponseはケースクラスなので書き換えられない)

    2010年2月12日金曜日

    [Lift] Lift2.0-scala280で日本語テンプレートが使えない

    このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク

    2010-02-25追記

    Lift 2.0-M3で治るそうです(google-group)。テンプレートは必ずutf-8で!

    元記事

    先日、せっかくだから最新を使おうと、Lift2.0-scala280(beta)に手を出してみました。API周りの修正はarchetype-basicで生成しなおしたプロジェクトを元にマージし、ビルドできるようになったと思いきや、Lift1.0.2では動いてたページが、以下のエラーで動作しなくなりました。
    Message: java.nio.charset.UnmappableCharacterException: Input length = 2
           java.nio.charset.CoderResult.throwException(CoderResult.java:261)
           sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:319)
           sun.nio.cs.StreamDecoder.read(StreamDecoder.java:158)
           java.io.InputStreamReader.read(InputStreamReader.java:167)
           java.io.BufferedReader.fill(BufferedReader.java:136)
           java.io.BufferedReader.read(BufferedReader.java:157)
           scala.io.BufferedSource$$anonfun$1$$anonfun$apply$1.apply(BufferedSource.scala:29)
           scala.io.BufferedSource$$anonfun$1$$anonfun$apply$1.apply(BufferedSource.scala:29)
           scala.io.Codec.wrap(Codec.scala:65)
           scala.io.BufferedSource$$anonfun$1.apply(BufferedSource.scala:29)
           scala.io.BufferedSource$$anonfun$1.apply(BufferedSource.scala:29)
           scala.collection.Iterator$$anon$13.next(Iterator.scala:145)
           scala.collection.Iterator$$anon$24.hasNext(Iterator.scala:435)
           scala.collection.Iterator$$anon$19.hasNext(Iterator.scala:326)
           scala.io.Source.hasNext(Source.scala:209)
           net.liftweb.util.PCDataXmlParser$$anonfun$apply$2$$anonfun$apply$4.apply(PCDataMarkupParser.scala:184)
    

    スタックトレースの示すPCDataMarkupParser.scala:184近辺を見てみたところ、scala.io.Source.fromInputStream(in)が使われていますが、Scala2.7.7と2.8.0で、エンコードを指定しなかった場合の動作が変わっています。2.7.7では、utf-8が使われます(*1)が、2.8.0だとSource.fromInputStreamは新しく導入されたCodecを引数に取るようになり、省略した場合はCodec.defaultが使用されます(*2)。

    1. 2.7.7のscaladoc: same as BufferedSource.fromInputStream(is, "utf-8", Source.DefaultBufSize)
    2. 2.8.0-Betaのscaladoc: codec (implicit) a scala.io.Codec specifying behavior (defaults to Codec.default)

    上記APIの仕様変更により、日本語Windowsなうちの環境だとutf-8のテンプレートをMS932で読もうとして失敗しているわけですね。

    Scala2.8.0のライブラリの、Codec.defaultのソースを見てみると、
    def default = apply(Charset.defaultCharset)
    
    としているだけ(Charsetはjava.nio.charset.Charset)なので、JVMのシステムプロパティで-Dfile.encodingを設定してやればOKそうです。Mavenでjettyを起動しているので、MVN_OPTSに-Dfile.encoding=utf-8を追加したら問題なく動きました。

    関連Tweet 2010-02-02 20:06:36~

    運用環境だとそう簡単にJVMのシステムプロパティなど変えられないと思うので、どこかでテンプレートのエンコーディングを指定するか、もう必ずutf-8で読んでくれるかしれくれないと困りますね。LiftのMLに投げました。

    追記

    MVN_OPTSで-Dfile.encoding=utf-8にしてNetBeansからrun:jettyで起動すると、コンソールの日本語が化けます。おそらくNetBeansも自身のJVMのfile.encoding(この場合はMS932)を使用してコンソールに出力しているためと思われます。

    2010年2月11日木曜日

    [Scala] ScalaでOAuth

    このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
    lift-oauthはサーバ側(サービスプロバイダ)とのことなので、Scalaで使えるOAuthライブラリを探してみた。dispatchに含まれてることが分かったので、早速試してみるが、演算子オーバーロード(*1)やらimplicit defやら使いまくりで非常に難解…。(*1: Scalaでは識別子に記号が使えるので演算子オーバーロードってのはホントは正しくない)

    Specsのテストコード(dispatch/oauth/src/test/scala/OAuthSpec.scala)が一番良いサンプルだったので抜粋してコード読解。
    import org.specs._
    
    object OAuthSpec extends Specification {
      import dispatch._
      import oauth._
      import OAuth._
      
      val svc = :/("term.ie") / "oauth" / "example" // *1, *2
      val consumer = Consumer("key", "secret") // *3
      
      "OAuth test host" should {
        "echo parameters from protected service" in {
          val h = new Http
          val request_token = h(svc / "request_token.php" <@ consumer as_token) // *4, *5, *6
          val access_token = h(svc / "access_token" <@ (consumer, request_token) as_token)
          val payload = Map("identité" -> "caché", "identity" -> "hidden", "アイデンティティー" -> "秘密", 
            "pita" -> "-._~")
          h(
            svc / "echo_api.php" <<? payload <@ (consumer, access_token) >% { { // *7
              _ must_== (payload)
            }
          )
        }
      }
    }
    

    1. :/ はobject。:/() == :/.apply() メソッドで、dispatch.Requestを返す。(http://っぽいから:/にしたと思われ)
    2. / はRequestクラスのメソッド。パスをつなげて新しいRequestを返す。
    3. OAuthのコンシューマ・キー、コンシューマ・シークレットを保持するケースクラス。
    4. Http.apply()メソッドにRequestインスタンスを渡してHTTPリクエストを投げている。
    5. <@ はRequestSignerクラスのメソッド。Requestクラスをコンシューマ・シークレットで署名したりする。Request → RequestSignerはimplicit defされているのでそのまま呼べる。
    6. as_token はHttp.apply()の第2引数として渡されるハンドラ。レスポンスのbodyに含まれるトークンをTokenインスタンスにして返す。
    7. <<? はRequestクラスのメソッドで、クエリ文字列を付加した新しいRequestを返す。

    参考文献など:

    [Scala] ListをMapに変換するコード

    このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
    List("a", "1", "b", "2")をMap("a" -> "1", "b" -> "2")にする必要があって、ListをMapに変換するコードを再帰で書いてみた。
    def l2m[T](l: List[T]): Map[T,T] = l match {case x::x2::xs => Map(x -> x2) ++ l2m(xs) case _ => Map()}
    
    実行例:
    scala> val l = List("a", "1", "b", "2")
    l: List[java.lang.String] = List(a, 1, b, 2)
    
    scala> def l2m[T](l: List[T]): Map[T,T] = l match {case x::x2::xs => Map(x -> x2) ++ l2m(xs) case _ => Map()}
    l2m: [T](List[T])Map[T,T]
    
    scala> l2m(l)
    res0: Map[java.lang.String,java.lang.String] = Map(a -> 1, b -> 2)