GrailsのView機能では、デフォルトではSiteMeshによりレイアウトおよびGSPのレンダリングを行います。
しかしHTMLではなく、ナマのバイナリデータを出力したい時や、あるいは完全に独自の出力を特定のアクションでだけ行いたい、などで、Grailsのデフォルトビューを無効化したい場合もあります。そのような場合の対処方法、逆に言えばきちんと対処しておかないと「なんでこんな現象が!?」と目を白黒させてドツボにハマりますので、その辺を本記事では紹介していきます。
画像やバイナリデータの直接出力を実装したい場合は、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 } }
同じ要領で他のデータ、あるいはバイナリにも対応できます。しかし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が発生する理由ですが、ざっと調べたところ、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>
※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の内部構造が良く分かってないので、もしかしたら実はこんな方法が・・・というのも将来見つかるかもしれません。