Boot.scalaで、
LiftRules.encodeJSessionIdInUrl_? = trueしてやればOK。
Liftで携帯サイト作ってる人とかはアップデートの際に要注意!
ScalaとかObjective-Cとかforce.comとかで開発してます。
LiftRules.encodeJSessionIdInUrl_? = trueしてやればOK。
Note that when using an InputStream generator, chuncked encoding will be used with no Content-Length header
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を指定
val httpmime = "org.apache.httpcomponents" % "httpmime" % "4.1-beta1"
| ログイン画面 | 承認画面 | 
|---|---|
| 特に問題なし! | 自動でリダイレクトはされないが、リンクを手動でクリックすれば動作はOK! | 
| ログイン画面 | 承認画面 | 
|---|---|
| 文字化けがひどいが、ID・パスワード欄っぽいところに入れて1つ目のボタン(ログイン)を押せば承認画面に飛ぶ。 | 自動でリダイレクトはされないが、リンクを手動でクリックすれば動作はOK! | 
| ログイン画面 | 承認画面 | 
|---|---|
| 両方とも送信ボタン…。1つ目を押せば承認画面に飛ぶ。 | 文字化けがひどいが、リンクを手動でクリックすれば動作はOK! | 
| ログイン画面 | 承認画面 | 
|---|---|
| 日本語文字化け… | 文字化け&Denyが押せないが、動作はOK! | 
| ログイン画面 | 承認画面 | 
|---|---|
| デザインは崩れるが、動作はOK! | デザインは崩れるが、動作はOK! | 
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
  }
}
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>
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  
}
| app | ave | max | min | 
|---|---|---|---|
| lift-blank *1 | 6829 | 7338 | 5938 | 
| nomapper *2 | 6681 | 7105 | 6035 | 
| nomapper/nojson *3 | 6531 | 7124 | 5705 | 
| min *4 | 6481 | 7240 | 5957 | 
INFO - Service request (GET) / took 3056 Milliseconds次はこれを調べてみることにする。
<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>
<security-constraint>
  <web-resource-collection>
    <url-pattern>/stats/*</url-pattern>
  </web-resource-collection>
  <auth-constraint>
    <role-name>admin</role-name>
  </auth-constraint>
</security-constraint>
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())
  }
}
LiftRules.statelessDispatchTable.append {
  case Req("cron" :: "sessionCleaner" :: Nil, _, _) => () => SessionCleaner.execute()
}
// 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))
}
<lift:StatefulTest.liftForm form="POST"> <e:instance/> <e:input/> <e:submit/> </lift:StatefulTest.liftForm>
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)
)
}
<lift:StatefulTest.myForm>
  
  <form action="mySayHello" method="POST"> 
    <e:instance/>
    <e:key/>
    <e:input/>
    <e:submit/>
  </form>
</lift:StatefulTest.myForm>
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の問題がありますが…)val $x = e match {case p => (x1, . . . , xn)}
val x1 = $x._1
. . .
val xn = $x._n
※The Scala Language Specification(PDF)のp.36参照(タプルが{}になっているのは古い仕様?なので↑では()に直しています)。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
// 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()
// (省略)
}
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
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
// StatefulSnippet.scala
def link(to: String, func: () => Any, body: NodeSeq): Elem = SHtml.link(to, () => {registerThisSnippet(); func()}, body)
// 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>)(_ % _))
}
<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は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)
}
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> 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
できることは分かったけど、きっと使わないなぁ…。
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を呼ぶ時とかに使えそう。
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">
// 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))
//...後略...
// net/liftweb/http/LiftResponse.scala(l.415)
InMemoryResponse(ret.getBytes("UTF-8"), headers, cookies, code)
// 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"
}
// 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 _ => ""
}
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
  }
}
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)
same as BufferedSource.fromInputStream(is, "utf-8", Source.DefaultBufSize)
codec (implicit) a scala.io.Codec specifying behavior (defaults to Codec.default)
def default = apply(Charset.defaultCharset)としているだけ(Charsetはjava.nio.charset.Charset)なので、JVMのシステムプロパティで-Dfile.encodingを設定してやればOKそうです。Mavenでjettyを起動しているので、MVN_OPTSに-Dfile.encoding=utf-8を追加したら問題なく動きました。
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)
        }
      )
    }
  }
}
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)