GSPにpageEncodingディレクティブを!(Grails 0.5)

昨日のpageEncodingディレクティブを自分で作ってみよう。ということで、取っ掛かりは先日の日記のGroovyPagesTemplateEngineクラスあたりから。プラグインのdoWithSpringクロージャをみるとjspViewResolverもプロパティとして持っているし、getResourceForUri(String)をオーバライドするとちゃんとビューが変わるのでここから後の処理であると予想する。
GroovyPagesTemplateEngineクラスのソースによると、getResourceForUri(String)を呼び出しているのは、calculateLineNumbersForPage(ServletContext,String)とcreateTemplate(String)だけだ。また、GrailsViewResolverの場合も、GrailsViewResolver#loadView()→GroovyPageView#renderMergedOutputModel()→GroovyPageView#renderWithTemplateEngine()→GroovyPagesTemplateEngine#createTemplate()と最終的にGroovyPagesTemplateEngine#createTemplate(String)で処理が行われているっぽい。
GroovyPagesTemplateEngine#createTemplate(String)のソースは以下のとおり。

    public Template createTemplate(String uri)  {
        return createTemplate(getResourceForUri(uri));
    }

むう、先日のロケール対応処理が2重に実行されそうな気がしなくもない。今度チェックしよう。で、GroovyPagesTemplateEngine#createTemplate(Resource)のソースは以下のとおり。

    public Template createTemplate(Resource resource) {
        if(resource == null) {
            GrailsWebRequest webRequest = getWebRequest();
            throw new GroovyPagesException("No Groovy page found for URI: " + getCurrentRequestUri(webRequest.getCurrentRequest()));
        }
        String name = establishPageName(resource, null);
        if(pageCache.containsKey(name)) {
            GroovyPageMetaInfo meta = (GroovyPageMetaInfo)pageCache.get(name);

            if(isGroovyPageReloadable(resource, meta)) {
                try {
                    return createTemplateWithResource(resource);
                } catch (IOException e) {
                    throw new GroovyPagesException("I/O error reading stream for resource ["+resource+"]: " + e.getMessage(),e);
                }
            }
            else {
                return new GroovyPageTemplate(meta);
            }
        }
        else {
            try {
                return createTemplateWithResource(resource);
            } catch (IOException e) {
                throw new GroovyPagesException("I/O error reading stream for resource ["+resource+"]: " + e.getMessage(),e);
            }
        }
    }

createTemplateWithResource(Resource)かな?

    private Template createTemplateWithResource(Resource resource) throws IOException {
        InputStream in = resource.getInputStream();
        try {
            return createTemplate(in, resource, null);
        }
        finally {
            in.close();
        }
    }

リソースをオープンしているな。さらに奥に。

    protected Template createTemplate(InputStream inputStream, Resource resource, String pageName) {
        GroovyPageMetaInfo metaInfo = buildPageMetaInfo(inputStream, resource, pageName);
        return new GroovyPageTemplate(metaInfo);
    }

ストリームを使用しているのはbuildPageMetaInfo()だ。次。

    protected GroovyPageMetaInfo buildPageMetaInfo(InputStream inputStream, Resource res, String pageName) {
        String name = establishPageName(res, pageName);

        long lastModified = establishLastModified(res);

        Parse parse;
        try {
            parse = new Parse(name, inputStream);
        } catch (IOException e) {
            throw new GroovyPagesException("I/O parsing Groovy page ["+(res != null ? res.getDescription() : name)+"]: " + e.getMessage(),e);
        }
        InputStream in = parse.parse();

        // Make a new metaInfo
        GroovyPageMetaInfo metaInfo = createPageMetaInfo(parse, lastModified, in);
        metaInfo.setPageClass( compileGroovyPage(in, name) );

        pageCache.put(name, metaInfo);

        return metaInfo;
    }

ストリームを使用しているのはParseクラスしかない。名前どおりこのクラスでGSPファイルを解析しているのだろう。org.codehaus.groovy.grails.web.pages.Parseクラスも見る必要があるな。で、このクラスのコンストラクタは、

    public Parse(String name, InputStream in) throws IOException {
        scan = new Scan(readStream(in));
        makeName(name);
    } // Parse()

Scanクラスというのが出てきた。と、その前にreadStream()でストリームの内容を読み込んでいる?この処理はというと、

    private String readStream(InputStream in) throws IOException {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        try {
            byte[] buf = new byte[8192];
            for (;;) {
                int read = in.read(buf);
                if (read <= 0) break;
                out.write(buf, 0, read);
            }
            return out.toString();
        } finally {
            out.close();
            in.close();
        }
    } // readStream()

なるほど、ストリームから読み出して全部文字列にしてるのね。最後の

            return out.toString();

でストリームの内容はデフォルトエンコーディングとして扱われているというわけだ。ここを

            return out.toString("UTF-8");

とかにすれば、ファイルは全てUTF-8として扱われることになるんだろう。今回は、pageEncodingディレクティブで指定したいので、固定でUTF-8とか指定できない。さあ、どうしようか。
Parseクラスのサブクラスを作っていじろうにも、ParseクラスのコンストラクタでreadStream()が呼ばれちゃうんだよなorz。さらによくよく見てみると、Scanクラスも、GroovyPageMetaInfoもpackage privateになってる!!protected GroovyPageMetaInfo buildPageMetaInfo()ってなってるのにぃ。一体どうしろというのか。
ストリームの内容を自前で読み込んで、デフォルトエンコーディングに変換してから渡すとかすればよさそうだけど、デフォルトエンコーディングに変換できない文字がGSPファイルに存在していたらダメだよなぁ。GroovyPageMetaInfo、Scan、Parseに相当するものを全部自前で作らないとダメっぽい。
どーする、俺!?