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)