val ImgPtn = """(.*\.(?:png|gif|jpg));jsessionid=.+""".r
LiftRules.urlDecorate.append {
case ImgPtn(url) => url
}
※Lift 2.2からデフォルトではjsessionid付かなくなったのはここに書いてある通りです。
ScalaとかObjective-Cとかforce.comとかで開発してます。
val ImgPtn = """(.*\.(?:png|gif|jpg));jsessionid=.+""".r
LiftRules.urlDecorate.append {
case ImgPtn(url) => url
}
<filter-mapping> <filter-name>LiftFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
LiftRules.passNotFoundToChain = true
LiftRules.liftRequest.append {
case Req("_ah" :: _, _, _) => false
case Req("remote_api" :: _, _, _) => false
}
LiftRules.encodeJSessionIdInUrl_? = trueしてやればOK。
| 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次はこれを調べてみることにする。
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
// 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)
}
// 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を追加したら問題なく動きました。