home ホーム search 検索 -  login ログイン  | reload edit datainfo version cmd icon diff delete  | help ヘルプ

Groovy/Grails/HttpServletResponseを使う + SiteMeshは無効化する。

Groovy/Grails/HttpServletResponseを使う + SiteMeshは無効化する。

Groovy / Grails / HttpServletResponseを使う + SiteMeshは無効化する。
id: 1114 所有者: msakamoto-sf    作成日: 2012-10-13 16:58:33
カテゴリ: Grails Groovy 

GrailsのView機能では、デフォルトではSiteMeshによりレイアウトおよびGSPのレンダリングを行います。
しかしHTMLではなく、ナマのバイナリデータを出力したい時や、あるいは完全に独自の出力を特定のアクションでだけ行いたい、などで、Grailsのデフォルトビューを無効化したい場合もあります。そのような場合の対処方法、逆に言えばきちんと対処しておかないと「なんでこんな現象が!?」と目を白黒させてドツボにハマりますので、その辺を本記事では紹介していきます。


画像を出力する位なら簡単なんだけど・・・(Content-Type: image/jpeg)

画像やバイナリデータの直接出力を実装したい場合は、Controllerからアクセスできるresponse(HttpServletResponse)を使ってOutputStreamにバイナリデータを出力します。
http://grails.org/doc/latest/ref/Servlet%20API/response.html より:

class BookController {
    def downloadFile() {
        byte[] bytes = // read bytes
        response.outputStream << bytes
    }
}

ContentTypeまで指定してみます。JPEG画像を直接出力したい場合ですと、次のようになります。

class SampleImageController {
    def index() {
        def input = servletContext.getResourceAsStream("/WEB-INF/sample.jpg")
        response.contentType = "image/jpeg"
        response.outputStream << input
    }
}

"Content-Type: text/html" で response.outputStream を使うと何故か404になってしまう

同じ要領で他のデータ、あるいはバイナリにも対応できます。しかしContentTypeに"text/html"を指定して、outputStream経由で出力しようとすると、なぜかHTTPステータスコード404が返されてしまい、Tomcatのデフォルトの404エラー画面が表示されます。

class SampleHtmlDownloadController {
    def index() {
        response.contentType = "text/html"
        response.outputStream << "<html><body>test</body></html>"
    }
}

->
HTTP Status 404 - .../WEB-INF/grails-app/views/sampleHtmlDownload/index.jsp

404ではなくIllegalStateExceptionが発生しGrailsのエラー画面が表示された場合もありました。職場で発生していたのでエラー画面とかはお見せできないのですが、どうも、Controller内でHttpServletResponseのOutputStreamに出力し終わっている、にも関わらず、SiteMesh側でgetWriter()経由で出力しようとしたためのIllegalStateExceptionのようでした。
・・・が、こちらの現象は家では再現しなかったため、厳密にどの条件が当てはまると404になる or IllegalStateExceptionが発生するかまではわかりませんでした。いずれにせよ、どちらも後述の対応で回避できます。

ちなみに、"/views/.../index.gsp" を空っぽで作成してみると、outputStreamに出力したデータはごっそり無視されてしまい、空っぽのindex.gspの中身が表示されておしまいになります。勘弁してくれよと言いたくなります。
(´・ω・`)

404の原因とSiteMeshの無効化

さて404が発生する理由ですが、ざっと調べたところ、GrailsのSiteMeshのデフォルト設定では"text/html"のContent-Typeが指定されたら、自動的にSiteMeshによるGSPレンダリング処理が発生するようです。そのため、SiteMeshがデフォルトのビューファイルを見に行ったが見つからない、ということで404が発生した・・・多分、そんな経緯だと思います。Grailsの内部コードまでは調査していませんので断言できません(;´∀`)

Grails 2.1.1の場合、"(GrailsApp)/web-app/WEB-INF/sitemesh.xml" というのがあり、デフォルトでは以下の様なXMLでした。

<sitemesh>
    <page-parsers>
        <parser content-type="text/html"
            class="org.codehaus.groovy.grails.web.sitemesh.GrailsHTMLPageParser" />
        <parser content-type="text/html;charset=ISO-8859-1"
            class="org.codehaus.groovy.grails.web.sitemesh.GrailsHTMLPageParser" />
        <parser content-type="text/html;charset=UTF-8"
            class="org.codehaus.groovy.grails.web.sitemesh.GrailsHTMLPageParser" />
    </page-parsers>

    <decorator-mappers>
        <mapper class="org.codehaus.groovy.grails.web.sitemesh.GrailsLayoutDecoratorMapper" />
    </decorator-mappers>
</sitemesh>

中身はよくわかりませんが、雰囲気的に、"text/html"でcharsetがiso-8859とutf8の場合はGrailsHTMLPageParserが発動してしまう、そんな感じがします。

となりますと、一番単純な回避策としては"text/html"のContentTypeを使わない、となりますが、そもそもHTMLのデータを出力したい場合は回避しようがありません。"text/html"にしないとブラウザ側で正常に表示できません(多分)。
ということで、なんとかして"text/html"のままでSiteMeshを回避する方法ですが、ちゃんと存在します。

1. sitemesh.xmlの"<sitemesh>"タグの下に以下の一行を書き足します。

<excludes file="/WEB-INF/sitemesh-excludes.xml" />

2. "/WEB-INF/sitemesh-excludes.xml" を以下の内容で作成します。

<?xml version="1.0" encoding="UTF-8"?>
<sitemesh-excludes>
  <excludes>
    <pattern>(URLパスパターンマッチ文字列)</pattern>
    <!-- 複数設定可能 -->
    <pattern>/foo</pattern>
    <pattern>/bar/action1</pattern>
    <pattern>/baz/**/download</pattern>
  </excludes>
</sitemesh-excludes>

今回は例示した SampleImageController, SampleHtmlDownloadController のコントローラの各アクションを除外設定にしてみます:

<?xml version="1.0" encoding="UTF-8"?>
<sitemesh-excludes>
  <excludes>
    <pattern>/sampleImage/*</pattern>
    <pattern>/sampleHtmlDownload/*</pattern>
  </excludes>
</sitemesh-excludes>

sitemesh.xml関連の設定を追加したら、Grailsの再起動が必要でした。(clean/compileまでは不要)

これで無事、本来outputStreamに出力したデータがGrailsからのHTTPレスポンスに出力されました!

HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Content-Type: text/html
Date: Sat, 13 Oct 2012 07:39:05 GMT
Content-Length: 30

<html><body>test</body></html>

Controllerからアクセスできる"response"の実体は?

※Grails 2.1.1 の場合です。

Controllerからアクセスできる"response"の実体ですが、SiteMeshの有効・無効で切り替わります。

SiteMeshが有効なControllerの場合、"response"はorg.codehaus.groovy.grails.web.sitemesh.GrailsContentBufferingResponse のインスタンスです。試しに "println response.dump()" などを入れてみるとすぐに確認できます。

sitemesh.xmlなどにより無効化された場合、"response"は org.apache.catalina.connector.ResponseFacade のインスタンスでした。これは"run-app"したことでTomcatのコンテナが起動したため、HttpServletResponseのTomcatによる実装であると想像されます。

参考資料

SiteMesh関連:

当初はOpenSymphony上で他のプロダクトと一緒に開発されていたようですが、それぞれがOSSとして成熟していったことにより、OpenSymphonyは解散となりそれぞれ独立していったようです。SiteMeshのサイトも、現在は http://wiki.sitemesh.org/ に移動しています。2012年現在、Grails 2.1.1ではSiteMesh 2.4が使用されています。

GrailsでSiteMeshを何とかして無効化しようと力の限り足掻いた人たち:

GrailsPageResponseWrapper.deactivateSiteMesh()というメソッドが存在した・・・らしい、のですが、少なくともGrails 2.1.1 では存在しないようです。

今回紹介したのはsitemesh.xml経由でSiteMeshの除外URLを設定する方式でした。ControllerのActionからプログラムでON/OFF出来る確実な方法が調べきれなかったんです。まだGrailsの内部構造が良く分かってないので、もしかしたら実はこんな方法が・・・というのも将来見つかるかもしれません。



プレーンテキスト形式でダウンロード
現在のバージョン : 1
更新者: msakamoto-sf
更新日: 2012-10-13 17:00:11
md5:e52288941b37cc8e3a33660152e91439
sha1:3fb0a11220bf5e405564d4bc7952446a569876e5
コメント
コメントを投稿するにはログインして下さい。