#navi_header|Groovy|
GrailsのView機能では、デフォルトではSiteMeshによりレイアウトおよびGSPのレンダリングを行います。
しかしHTMLではなく、ナマのバイナリデータを出力したい時や、あるいは完全に独自の出力を特定のアクションでだけ行いたい、などで、Grailsのデフォルトビューを無効化したい場合もあります。そのような場合の対処方法、逆に言えばきちんと対処しておかないと「なんでこんな現象が!?」と目を白黒させてドツボにハマりますので、その辺を本記事では紹介していきます。
#more||
#outline||
----
* 画像を出力する位なら簡単なんだけど・・・(Content-Type: image/jpeg)
画像やバイナリデータの直接出力を実装したい場合は、Controllerからアクセスできるresponse(HttpServletResponse)を使ってOutputStreamにバイナリデータを出力します。
http://grails.org/doc/latest/ref/Servlet%20API/response.html より:
#pre||>
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エラー画面が表示されます。
#pre||>
class SampleHtmlDownloadController {
def index() {
response.contentType = "text/html"
response.outputStream << "
test"
}
}
->
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でした。
#pre||>
||<
中身はよくわかりませんが、雰囲気的に、"text/html"でcharsetがiso-8859とutf8の場合はGrailsHTMLPageParserが発動してしまう、そんな感じがします。
となりますと、一番単純な回避策としては"text/html"のContentTypeを使わない、となりますが、そもそもHTMLのデータを出力したい場合は回避しようがありません。"text/html"にしないとブラウザ側で正常に表示できません(多分)。
ということで、なんとかして"text/html"のままでSiteMeshを回避する方法ですが、ちゃんと存在します。
1. sitemesh.xmlの""タグの下に以下の一行を書き足します。
2. "/WEB-INF/sitemesh-excludes.xml" を以下の内容で作成します。
(URLパスパターンマッチ文字列)
/foo
/bar/action1
/baz/**/download
今回は例示した SampleImageController, SampleHtmlDownloadController のコントローラの各アクションを除外設定にしてみます:
#pre||>
/sampleImage/*
/sampleHtmlDownload/*
||<
sitemesh.xml関連の設定を追加したら、Grailsの再起動が必要でした。(clean/compileまでは不要)
これで無事、本来outputStreamに出力したデータがGrailsからのHTTPレスポンスに出力されました!
#pre||>
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
test
||<
* 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関連:
- sitemesh (SiteMesh)
-- https://github.com/sitemesh
- OpenSymphony, RIP (2000 - 2011)
-- http://www.opensymphony.com/sitemesh/dm.html
- Home - SiteMesh 2 - SiteMesh Wiki
-- http://wiki.sitemesh.org/display/sitemesh/Home
当初はOpenSymphony上で他のプロダクトと一緒に開発されていたようですが、それぞれがOSSとして成熟していったことにより、OpenSymphonyは解散となりそれぞれ独立していったようです。SiteMeshのサイトも、現在は http://wiki.sitemesh.org/ に移動しています。2012年現在、Grails 2.1.1ではSiteMesh 2.4が使用されています。
GrailsでSiteMeshを何とかして無効化しようと力の限り足掻いた人たち:
- How to prevent Grails from rendering the default view? - Stack Overflow
-- http://stackoverflow.com/questions/5708654/how-to-prevent-grails-from-rendering-the-default-view
- Grails - user - How to avoid sitemesh
-- http://grails.1312388.n4.nabble.com/How-to-avoid-sitemesh-td3464471.html
- [#GRAILS-1223] setting response.contentType to text/html causes exception - Grails JIRA
-- http://jira.grails.org/browse/GRAILS-1223
- [#GRAILS-5770] Make it possible to by pass GSP Sitemesh preprocessing for a GSP file with a page directive - Grails JIRA
-- http://jira.grails.org/browse/GRAILS-5770
- [#GRAILS-5773] Add support for excluding uri's from Sitemesh processing in a Grails way (in UrlMappings etc) - Grails JIRA
-- http://jira.grails.org/browse/GRAILS-5773
GrailsPageResponseWrapper.deactivateSiteMesh()というメソッドが存在した・・・らしい、のですが、少なくともGrails 2.1.1 では存在しないようです。
今回紹介したのはsitemesh.xml経由でSiteMeshの除外URLを設定する方式でした。ControllerのActionからプログラムでON/OFF出来る確実な方法が調べきれなかったんです。まだGrailsの内部構造が良く分かってないので、もしかしたら実はこんな方法が・・・というのも将来見つかるかもしれません。
#navi_footer|Groovy|