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

2011年3月22日火曜日

[Scala] sbt jetty-runでhttpsを使用したい

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

方法1: カスタムjetty.xmlを使う

sbt標準だとjetty.xmlを1から書けばOK。
override def jettyConfiguration = new CustomJettyConfiguration {
  override def jettyConfigurationXML = List("/path/to/jetty.xml")
}

この方法だとjetty.xmlにConnectorとかHandlerとか全部自分で書かないといけないので面倒…
参考:jetty.xmlのリファレンス

方法2: sbtにちょっと手を入れてjetty設定用のhookを使う

sbtのソースをcheckoutしてhookが設定できるようにする


参考:Build - simple-build-tool -

sbtのソースをcheckout
$ git clone git://github.com/harrah/xsbt.git
$ cd xsbt

変更したのは下の2行だけ。jetty-runが呼ばれてデフォルトのhttpポートのConnectorの設定直後にcallbackで設定した関数が呼ばれるようにする。
diff --git a/sbt/src/main/scala/sbt/WebApp.scala b/sbt/src/main/scala/sbt/WebApp.scala
index f5a3d09..997294e 100644
--- a/sbt/src/main/scala/sbt/WebApp.scala
+++ b/sbt/src/main/scala/sbt/WebApp.scala
@@ -105,6 +105,8 @@ trait DefaultJettyConfiguration extends JettyConfiguration
        def parentLoader: ClassLoader
        def jettyEnv: Option[File]
        def webDefaultXml: Option[File]
+
+       def callback: Option[AnyRef => Any] = None
 }
 abstract class CustomJettyConfiguration extends JettyConfiguration
 {
diff --git a/sbt/src/main/scala/sbt/jetty/LazyJettyRun.scala.templ b/sbt/src/main/scala/sbt/jetty/LazyJettyRun.scala.templ
index 263afa6..4ca4b0e 100644
--- a/sbt/src/main/scala/sbt/jetty/LazyJettyRun.scala.templ
+++ b/sbt/src/main/scala/sbt/jetty/LazyJettyRun.scala.templ
@@ -54,6 +54,7 @@ private object LazyJettyRun${jetty.version} extends JettyRun
                                case c: DefaultJettyConfiguration =>
                                        import c._
                                        configureDefaultConnector(server, port)
+                                       c.callback.foreach(_(server))
                                        val webapp = new WebAppContext(war.absolutePath, contextPath)
                                        webDefaultXml.foreach{webDefaultXml:File => webapp.setDefaultsDescriptor(webDefaultXml.toString)}

sbtのビルド

$ sbt update generate-loader-compat proguard "project Simple Build Tool" publish-local
※generate-loader-compatしないとjetty6用のクラスが一部生成されなかった。(jettyは6->7でパッケージ名が変わったので、sbtのソースではテンプレート化して6,7,7.2用のscalaソースが吐かれるようになってる

ssl用証明書の準備

JDKのkeytoolを使用。とりあえずオレオレ証明書で。
$ keytool -keystore src/test/resources/keystore -alias jetty -genkey -keyalg RSA
$ keytool -selfcert -validity 1024 -keystore src/test/resources/keystore -alias jetty
※keystoreのパスワードを聞かれるので入力します

sbt プロジェクト側の準備

build.propertiesの sbt.version=0.7.5.RC0 にしてsbt実行、先に標準の0.7.5.RC0を入れておく。

ビルドしたsbt/target/scala_2.7.7/sbt_2.7.7-0.7.5.RC0.jar をプロジェクトのproject/boot/scala-2.7.7/org.scala-tools.sbt/sbt/0.7.5.RC0/sbt_2.7.7-0.7.5.RC0.jar に上書き。

project/build/MyProject.scala の設定。callbackでjettyのServerインスタンスにSslSocketConnectorを追加。ClassLoaderが違うので普通に書くとClassCastExceptionが発生するのでリフレクションで。
  override def jettyConfiguration: JettyConfiguration =
    new DefaultJettyConfiguration {
      def classpath = jettyRunClasspath
      def jettyClasspath = MyProject.this.jettyClasspath
      def war = jettyWebappPath
      def contextPath = jettyContextPath
      def classpathName = "test"
      def parentLoader = buildScalaInstance.loader
      def scanDirectories = Path.getFiles(MyProject.this.scanDirectories).toSeq
      def scanInterval = MyProject.this.scanInterval
      def port = jettyPort
      def log = MyProject.this.log
      def jettyEnv = jettyEnvXml
      def webDefaultXml = jettyWebDefaultXml
      override def callback = Some((server: AnyRef) =>  {
        val cl = server.getClass.getClassLoader
        val ssl = cl.loadClass("org.mortbay.jetty.security.SslSocketConnector").newInstance.asInstanceOf[{
          def setPort(p: Int): Unit
          def setMaxIdleTime(t: Int): Unit
          def setKeystore(s: String): Unit
          def setPassword(s: String): Unit
          def setKeyPassword(s: String): Unit
          def setTruststore(s: String): Unit
          def setTrustPassword(s: String): Unit
        }]
        val addConnector: java.lang.reflect.Method = server.getClass.getMethod("addConnector", cl.loadClass("org.mortbay.jetty.Connector"))

        ssl.setPort(8443)
        ssl.setMaxIdleTime(30000)
        ssl.setKeystore(info.projectPath + "/src/test/resources/keystore")
        ssl.setPassword("password")
        ssl.setKeyPassword("password")
        ssl.setTruststore(info.projectPath + "/src/test/resources/keystore")
        ssl.setTrustPassword("password")
        addConnector.invoke(server, ssl)
      })
  }
※上記はjetty6の場合で、jetty7の場合はパッケージ・クラス名がちょっと違います。

これで、jetty-runで8080でhttp、8443でhttpsが起動します。

2011年2月9日水曜日

[Scala][sbt]DefaultWebProjectでwarではなくjarも作りたい

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
DefaultWebProjectだとsbt packageタスクでwarが作られますが、jarも作りたい時があった場合、こんな感じで。
lazy val jar = packageTask(packagePaths, jarPath, packageOptions).dependsOn(compile) describedAs "Creates a jar file."
sbt jarでjarファイルが作られます。

2011年1月1日土曜日

[Scala] sbt-appengineなプロジェクトをIntelliJ IDEAでデバッグ

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
@teaplanetさんにヒントいただいてIDEAのsbt consoleでdev-appserver-startできるようになったので、IDEA上でブレークポイントで止めたりできるように設定してみた。

class MyProject(info: ProjectInfo) extends AppengineProject(info) {
  //... 中略
  //APPENGINE_SDK_HOME設定の代わり
  override val appengineSdkPath = Path.fromFile("/Users/pomu0325/dev/appengine-java-sdk-1.4.0")  
  
  //dev-appserver-startの際のJVMオプション
  override val devAppserverJvmOptions = List("-Xdebug", "-Xrunjdwp:transport=dt_socket,server=y,address=2011,suspend=y") ++ super.devAppserverJvmOptions 
}

この設定でsbt consoleからdev-appserver-startするとリモートデバッガで接続できる状態で起動するので、後はIDEAからデバッガ接続する。

IDEAのメニューからRun -> Edit Configurations -> [+] -> Remote を選んで、Nameは適当に、PortにdevAppserverJvmOptionsのaddress=xxxxで指定したポートを入れる(↑の例だと2011。適当に開いてるポート設定すればOK)。

設定したら、Run -> Debug... で選んでデバッグ開始するとdev-appserverのJVMにアタッチされてブレークポイントで止まってくれる。

※Before launchの"Run sbt action"でdev-appserver-startが指定できればsbt consoleからいちいちdev-appserver-startしなくても一発で起動できそうだけど一覧に出てこないし、直接指定してもダメでした…

参考:sbt-appengineの基本的な設定等は"sbtでGAEをする場合の注意点など色々。"が参考になります。

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年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月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月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月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月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)