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はケースクラスなので書き換えられない)

0 件のコメント:

コメントを投稿