ラベル Lift の投稿を表示しています。 すべての投稿を表示
ラベル Lift の投稿を表示しています。 すべての投稿を表示

2011年3月29日火曜日

[Lift] 画像ファイルにjsessionidを付けない

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
takedasoftさんがUrl Rewrite Filterを使った方法を書いてましたが、Boot.scalaでLiftRule.urlDecorateにルール追加するだけで簡単にできたのでメモ。

val ImgPtn = """(.*\.(?:png|gif|jpg));jsessionid=.+""".r
LiftRules.urlDecorate.append {
  case ImgPtn(url) => url
}

※Lift 2.2からデフォルトではjsessionid付かなくなったのはここに書いてある通りです。

2011年1月12日水曜日

[Lift] Liftで処理したくないリクエストの設定

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
web.xmlで全てのリクエストがLiftで処理されるようになっているので、Lift以外のサーブレット等で処理したい場合に困ります…
<filter-mapping>
  <filter-name>LiftFilter</filter-name>
  <url-pattern>/*</url-pattern>
</filter-mapping>

Boot.scalaのdef bootにこんな感じで書けばOK。
LiftRules.passNotFoundToChain = true
LiftRules.liftRequest.append {
  case Req("_ah" :: _, _, _) => false
  case Req("remote_api" :: _, _, _) => false
}

※filter-mappingで除外ルールが書ければいいんだけど…

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年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年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日水曜日

[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年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月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)を使用してコンソールに出力しているためと思われます。