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)