#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|