ContentTypeのCharasetテスト(Grails 0.5)

GSPファイルにContentTypeを設定して、どうなるかをチェックしてみた。まあ、Grailsというよりもsitemeshのテストになるのかな。sitemeshってあまりよく知らないので。
アプリケーションとしては、昨日のサンプルを使用。Grailsは0.5にpageEncodingディレクティブのパッチを当てたものに、ロケールによりビューを変えるプラグインをインストールしたものを使用する。まっさらなGrailsだと以下のとおりにやっても動かないので注意。
最初に、新規にコントローラを作成する。

grails create-controller hello

HelloControllerができた。これはこのままで。
次に、grails-app/views/hello/index.gspを以下の内容で作成して、UTF-8で保存する。

<%@ page pageEncoding="utf-8" contentType="text/html;charset=utf-8" %>
<html>
    <head>
         <meta name="layout" content="hello" />
         <title>こんにちは</title>
    </head>
    <body>
        <div class="body">
        よく来たな、まあ座れや。
        </div>
    </body>
</html>

次はレイアウトファイルだ。grails-app/views/layouts/main.gspを同じディレクトリのhello.gspとしてコピーし、<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>を追加する。次にgrails-app/views/layouts/hello_ja.gspを以下の内容で作成してMS932で保存する。main.gspにちょっと手を入れただけだ。

<%@ page contentType="text/html;charset=windows-31j" pageEncoding="windows-31j" %>
<html>
	<head>
		<title><g:layoutTitle default="Grails" />(日本語レイアウト)</title>
        <meta http-equiv="Content-Type" content="text/html;charset=Windows-31j"/>
		<link rel="stylesheet" href="${createLinkTo(dir:'css',file:'main.css')}"></link>
		<g:layoutHead />
		<g:javascript library="application" />				
	</head>
	<body>
		<div id="spinner" class="spinner" style="display:none;">
			<img src="${createLinkTo(dir:'images',file:'spinner.gif')}" alt="Spinner" />
		</div>	
        <div class="logo"><img src="${createLinkTo(dir:'images',file:'grails_logo.jpg')}" alt="Grails" /></div>	(日本語レイアウト)
		<g:layoutBody />		
	</body>	
</html>

ブラウザの言語が日本語の場合には、日本語のレイアウトでMS932で出力され、ブラウザの言語がenとかの場合には、utf-8で出力される。
レイアウトのcharsetと、ビューのcharsetが異なる場合(hello_ja.gspはMS932で、index.gspはutf-8だ)、レイアウトのcharsetとなるみたいだ。ここで気になるのはmetaタグのContent-Typeだ。レイアウトのcharsetが有効となるならば、metaタグはレイアウトに入れるのが妥当な気がする(そんなわけでレイアウトファイルに入れてある)。
「ビューのcharsetが言語ごとに変わってもフォームのパラメタはどうすんのよ?」というアナタ。アナタは正しい。さあ、どうしましょう?

GSPにpageEncodingディレクティブを作ってみた![追記]

pageEncodingを作ってみたところで、Parseクラスのソースを見ていたところ、こんなところが。

    public InputStream parse() {

        StringWriter sw = new StringWriter();
        out = new GSPWriter(sw,this);
        page();
        finalPass = true;
        scan.reset();
        page();
//		if (DEBUG) System.out.println(buf);
        InputStream in = new ByteArrayInputStream(sw.toString().getBytes());
        scan = null;
        return in;
    } // parse()

new ByteArrayInputStream(sw.toString().getBytes())としているので、GSPファイルを出力した結果が結局、デフォルト文字コードでバイト列に直されてストリームになってしまうみたいだ。ぐは!!
とりあえず、テストしてみよう。先日のJUnitテストケースをちょっと修正。

 	public void testParseWithPageEncoding() throws Exception {
		String output = parseCodeBytes("myTest", (
				"<%@ page pageEncoding=\"utf-8\" import=\"some.test.package.*\"%>" +
				"<div>\u3053\u3093\u306b\u3061\u306f\ud840\udca2[Japanese]</div>"
				).getBytes("utf-8"));
		String expected = 
			"import org.codehaus.groovy.grails.web.pages.GroovyPage\n" +
			"import org.codehaus.groovy.grails.web.taglib.*\n"+
			"import some.test.package.*\n" +
			"\n"+
			"class myTest extends GroovyPage {\n"+
			"public Object run() {\n"+
			"out.print('<div>\u3053\u3093\u306b\u3061\u306f\ud840\udca2[Japanese]</div>')\n"+
			"}\n"+
			"}";

//		System.out.println(output);
		assertEquals(expected, trimAndRemoveCR(output));
		
		String output2 = parseCodeBytes("myTest", (
				"<%@ page import=\"some.test.package.*\"%><%@ page pageEncoding=\"utf-8\"%>" +
				"<div>\u3053\u3093\u306b\u3061\u306f\ud840\udca2[Japanese]</div>"
				).getBytes("utf-8"));
		
//		System.out.println(output2);
		assertEquals(expected, trimAndRemoveCR(output2));
 	}

\ud840\udca2を入れてみた。これは、𠂢という文字(表示されていないかもしれない)で、MS932(テストマシンの環境:日本語WindowsXP)では多分変換できない。
実行してみたところ、やっぱりエラーだ。うは。
まずは、テストケースから直す必要があるようだ。

    public static void send(InputStream in, Writer out) throws IOException {
        try {
            Reader reader = new InputStreamReader(in);
            char[] buf = new char[8192];
            for (;;) {
                int read = reader.read(buf);
                if (read <= 0) break;
                out.write(buf, 0, read);
            }
        } finally {
            out.close();
            in.close();
        }
    } // writeInputStreamToResponse()

これを、

    public static void send(InputStream in, Writer out) throws IOException {
    	send(in, null, out);
    }
    
    public static void send(InputStream in, String charset, Writer out) throws IOException {
        try {
            Reader reader = (charset == null)
                ? new InputStreamReader(in)
                : new InputStreamReader(in, charset);
            char[] buf = new char[8192];
            for (;;) {
                int read = reader.read(buf);
                if (read <= 0) break;
                out.write(buf, 0, read);
            }
        } finally {
            out.close();
            in.close();
        }
    } // writeInputStreamToResponse()

こんなふうにして、UTF-8で出力結果をチェックするようにして、呼び出し側も修正。

	public String parseCodeBytes(String uri, byte[] gsp) throws IOException {
		StringWriter sw = new StringWriter();
		PrintWriter pw = new PrintWriter(sw);
		InputStream gspIn = new ByteArrayInputStream(gsp);
        Parse parse = new Parse(uri, gspIn);
        InputStream in = parse.parse();
        send(in, "utf-8", pw);

		return sw.toString();
	}
	

次に、Parse#parse()を以下のように修正。

    public InputStream parse() {

        StringWriter sw = new StringWriter();
        out = new GSPWriter(sw,this);
        page();
        finalPass = true;
        scan.reset();
        page();
//		if (DEBUG) System.out.println(buf);
        try {
            InputStream in = (pageEncoding == null)
                ? new ByteArrayInputStream(sw.toString().getBytes())
                : new ByteArrayInputStream(sw.toString().getBytes("utf-8"));
            scan = null;
            return in;
        }
        catch (UnsupportedEncodingException neverHappen) {
        	throw	new GrailsRuntimeException(neverHappen);
        }
    } // parse()

これで、JUnitテストは通るようになった。問題は、これでGSPファイルが正しくコンパイルされるのかどうか、だ。というか、今までデフォルトエンコーディングでできていたのだから、できっこない。
前の日記でちょっと追っかけたところをみると、GroovyPagesTemplateEngine#createPageMetaInfo()か、compileGroovyPage()あたりだろうか。
GroovyPagesTemplateEngine#createPageMetaInfo()のソースは以下のとおり。

    private GroovyPageMetaInfo createPageMetaInfo(Parse parse, long lastModified, InputStream in) {
        GroovyPageMetaInfo pageMeta = new GroovyPageMetaInfo();
        pageMeta.setContentType(parse.getContentType());
        pageMeta.setLineNumbers(parse.getLineNumberMatrix());
        pageMeta.setLastModified(lastModified);
            // just return groovy and don't compile if asked
        if (GrailsUtil.isDevelopmentEnv()) {
            pageMeta.setGroovySource(in);
        }

        return pageMeta;
    }

InputStreamは使っていない。ならば、GroovyPagesTemplateEngine#compileGroovyPage()か。ソースは以下のとおり。

    private Class compileGroovyPage(InputStream in, String name) {
        // Compile the script into an object
        Class scriptClass;
        try {
            scriptClass =
                this.classLoader.parseClass(in, name);
        } catch (CompilationFailedException e) {
        	LOG.error("Compilation error compiling GSP ["+name+"]:" + e.getMessage(), e);
            throw new GroovyPagesException("Could not parse script [" + name + "]: " + e.getMessage(), e);
        }
        return scriptClass;
    }

ここで使ってるようだ。さて、GroovyClassLoader#parseClass(InputStream,String)ってInputStreamをどう扱うのかな。おそらくデフォルト文字コードとして扱うのだとおもう。
ちなみにGroovyClassLoaderのソースはここでみれる(リビジョンは今日の時点での最新)。それによると、こんな感じになっている。

    public Class parseClass(final InputStream in, final String fileName) throws CompilationFailedException {
        // For generic input streams, provide a catch-all codebase of
        // GroovyScript
        // Security for these classes can be administered via policy grants with
        // a codebase of file:groovy.script
        GroovyCodeSource gcs = (GroovyCodeSource) AccessController.doPrivileged(new PrivilegedAction() {
            public Object run() {
                return new GroovyCodeSource(in, fileName, "/groovy/script");
            }
        });
        return parseClass(gcs);
    }

これだけでは、文字コードをどう扱うかはわからない。でも、こんなコードがある。

    public Class parseClass(String text) throws CompilationFailedException {
        return parseClass(new ByteArrayInputStream(text.getBytes()), "script" + System.currentTimeMillis() + ".groovy");
    }

文字列として与えられたスクリプトは、デフォルトの文字コードとして処理されるようだ。そういう動作をストリームでもするのではなかろうか。うわ、八方塞がりの香りがぷんぷんしてきた。
これ以降は、Groovyの方のソースを読まないとわからないなあ、何か良い方法があればよいのだけれど。とりあえず、先日のパッチではダメだということはわかった。orz
Groovy自体は -e とかでエンコーディングの指定ができたはずだ。ということはどこかに設定が隠されているはず。で、GroovyClassLoaderのソースを見ているとこんな箇所が。

    /**
     * this cache contains the loaded classes or PARSING, if the class is currently parsed 
     */
    protected Map classCache = new HashMap();
    protected Map sourceCache = new HashMap();
    private CompilerConfiguration config;
    private Boolean recompile = null;
    // use 1000000 as offset to avoid conflicts with names form the GroovyShell 
    private static int scriptNameCounter = 1000000;
    

CompilerConfigurationですって、奥様!!ここらへんに設定があるに違いありませんわ!!で、ソースはここ
ソースを見ると、

    /**
     * Sets the encoding to be used when reading source files.
     */
    public void setSourceEncoding(String encoding) {
        this.sourceEncoding = encoding;
    }

をを、あるある。これをいじればよいのかな。GroovyTemplatesEngineのソースを見ると、

public class GroovyPagesTemplateEngine  extends ResourceAwareTemplateEngine implements ApplicationContextAware, ServletContextAware {


    private static final Log LOG = LogFactory.getLog(GroovyPagesTemplateEngine.class);
    private static Map pageCache = Collections.synchronizedMap(new HashMap());
    private GroovyClassLoader classLoader = new GroovyClassLoader();
    private int scriptNameCount;
    private ResourceLoader resourceLoader;
    [...]    

となっているが、classLoaderに関してはsetterが存在している。プラグインを作ってみた際に、がっつりと上書きしたところにclassLoaderの指定があったはずだ。

def doWithSpring = {
	groovyPagesTemplateEngine(LocaledTemplateEngine) {
		classLoader = ref("classLoader")
		if(grails.util.GrailsUtil.isDevelopmentEnv()) {
			resourceLoader = groovyPageResourceLoader
		}
	}
	[...]
}

classLoaderを設定しているのは、CoreGrailsPlugin.groovyだ。ソースは、以下のとおり。

	def doWithSpring = {
		classLoader(MethodInvokingFactoryBean) {
			targetObject = ref("grailsApplication", true)
			targetMethod = "getClassLoader"
		}
		classEditor(ClassEditor) {
			classLoader = classLoader
		}
		customEditors(CustomEditorConfigurer) {
			customEditors = [(java.lang.Class.class):classEditor]
		}
	}

となっている。このクラスローダを親のクラスローダにして、文字コードを設定したクラスローダをclassLoaderに設定してやれば、万事うまく行くような気もする。でも、プラグインをインストールしないと動かないパッチっていうのもなぁ。
pageEncodingを廃止して、環境変数とかをいじらずにすむプラグインとかなら簡単にできそうな気がしなくもない。それじゃあ、あまり意味がない?
ふと思ったのだが、CompilerConfigurationでソースのエンコーディングの指定ができるのだとしたら、以下のソース

    public Class parseClass(String text) throws CompilationFailedException {
        return parseClass(new ByteArrayInputStream(text.getBytes()), "script" + System.currentTimeMillis() + ".groovy");
    }

って違くね?

GSPにpageEncodingディレクティブを作ってみた!

結局Parseクラスのパッチを作ってみた。最初にScanクラスでトークンを読み出してみて、pageEncodingディレクティブがあったらそれで再度Scanクラスのインスタンスを生成することにした。あんまり効率よくなさそう...
注)以下のパッチには不備があることが判明しています。詳細はここ

Index: Parse.java
===================================================================
--- Parse.java	(revision 4321)
+++ Parse.java	(working copy)
@@ -36,6 +36,7 @@
  *
  * Date: Jan 10, 2004
  *
+ * Added by pageEncoding directive by Norihiro Seto
  */
 public class Parse implements Tokens {
     public static final Log LOG = LogFactory.getLog(Parse.class);
@@ -60,6 +61,8 @@
     private String contentType = "text/html;charset=UTF-8";
     private boolean doNextScan = true;
     private int state;
+    private ByteArrayOutputStream streamContent;
+    private String pageEncoding;
 
 
     public String getContentType() {
@@ -80,6 +83,7 @@
 
     public Parse(String name, InputStream in) throws IOException {
         scan = new Scan(readStream(in));
+        scanPageEncoding();
         makeName(name);
     } // Parse()
 
@@ -129,6 +133,7 @@
             String value = mat.group(2);
             if (name.equals("import")) pageImport(value);
             if (name.equals("contentType")) contentType(value);
+            if (name.equals("pageEncoding")) pageEncoding(value);
             ix = mat.end();
         }
     } // directPage()
@@ -208,6 +213,50 @@
         return 0;
     } // match()
 
+    private void pageEncoding(String value) {
+    	this.pageEncoding = value;
+    }
+
+    private void scanPageEncoding() throws IOException {
+        if (LOG.isDebugEnabled()) LOG.debug("parse: scanPageEncoding");
+        int stateBkup = state;
+        boolean doNextScanBkup = doNextScan;
+        out = new GSPWriter(new StringWriter(), this);
+        try {
+	        loop: for (;;) {
+	            if(doNextScan)
+	                state = scan.nextToken();
+	            else
+	                doNextScan = true;
+	
+	            switch (state) {
+	                case EOF: break loop;
+	                case JDIRECT: direct(); break;
+	                case GDIRECT: direct(); break;
+	            }
+		        if (pageEncoding != null) {
+		            if (LOG.isDebugEnabled()) {
+		            	LOG.debug("parse: scanPageEncoding encoding="
+		            			+ pageEncoding);
+		            }
+		        	scan = new Scan(streamContent.toString(pageEncoding));
+		        	break;
+		        }
+	        }
+        }
+        finally {
+        	state = stateBkup;
+        	doNextScan = doNextScanBkup;
+        	streamContent = null;
+        	try {
+        		out.close();
+        	}
+        	catch (Exception ignore) {}
+        	out = null;
+        	scan.reset();
+        }
+    } // scanPageEncoding()
+    
     private void page() {
         if (LOG.isDebugEnabled()) LOG.debug("parse: page");
         if (finalPass) {
@@ -450,6 +499,7 @@
                 if (read <= 0) break;
                 out.write(buf, 0, read);
             }
+            streamContent = out;
             return out.toString();
         } finally {
             out.close();

テストクラスのパッチは、これ

Index: ParseTests.java
===================================================================
--- ParseTests.java	(revision 4321)
+++ ParseTests.java	(working copy)
@@ -82,6 +82,45 @@
  		assertEquals(trimAndRemoveCR(expected), trimAndRemoveCR(output));
  	}
 
+	public String parseCodeBytes(String uri, byte[] gsp) throws IOException {
+		StringWriter sw = new StringWriter();
+		PrintWriter pw = new PrintWriter(sw);
+		InputStream gspIn = new ByteArrayInputStream(gsp);
+        Parse parse = new Parse(uri, gspIn);
+        InputStream in = parse.parse();
+        send(in, pw);
+
+		return sw.toString();
+	}
+	
+ 	public void testParseWithPageEncoding() throws Exception {
+		String output = parseCodeBytes("myTest", (
+				"<%@ page pageEncoding=\"utf-8\" import=\"some.test.package.*\"%>" +
+				"<div>\u3053\u3093\u306b\u3061\u306f[Japanese]</div>"
+				).getBytes("utf-8"));
+		String expected = 
+			"import org.codehaus.groovy.grails.web.pages.GroovyPage\n" +
+			"import org.codehaus.groovy.grails.web.taglib.*\n"+
+			"import some.test.package.*\n" +
+			"\n"+
+			"class myTest extends GroovyPage {\n"+
+			"public Object run() {\n"+
+			"out.print('<div>\u3053\u3093\u306b\u3061\u306f[Japanese]</div>')\n"+
+			"}\n"+
+			"}";
+
+//		System.out.println(output);
+		assertEquals(expected, trimAndRemoveCR(output));
+		
+		String output2 = parseCodeBytes("myTest", (
+				"<%@ page import=\"some.test.package.*\"%><%@ page pageEncoding=\"utf-8\"%>" +
+				"<div>\u3053\u3093\u306b\u3061\u306f[Japanese]</div>"
+				).getBytes("utf-8"));
+		
+//		System.out.println(output2);
+		assertEquals(expected, trimAndRemoveCR(output2));
+ 	}
+
     /**
      * Copy all of input to output.
      * @param in

単体テストは成功することは確認した。実際にアプリケーションを動作してのテストはしていない。

ロケール対応プラグインを作ってみる(Grails 0.5)

前のページで「先日のロケール対応処理が2重に実行されそうな気がしなくもない」って書いてあったところをチェックしてみた。ええ、そりゃあもう2重に呼び出されていましたとも(威張るところではないけど)。
getResourceForUriっていう名前のとおりにロケールだとかをURIにつけちゃいけないんだろうけど、このメソッドは、org.codehaus.groovy.grails.web.pages#doPage()から呼び出されているので、ここしかないんだと思う(プラグインからGSPサーブレットのクラスを変えるとかはできないと思われ)。このメソッドのソースは、

    public void doPage(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    	request.setAttribute(GrailsApplicationAttributes.REQUEST_SCOPE_ID, grailsAttributes);

        GroovyPagesTemplateEngine engine = grailsAttributes.getPagesTemplateEngine();
        String pageName = (String)request.getAttribute(GrailsApplicationAttributes.GSP_TO_RENDER);
        if(StringUtils.isBlank(pageName)) {
            pageName = engine.getCurrentRequestUri(request);
        }

        Resource page = engine.getResourceForUri(pageName);
        if (page == null) {
            context.log("GroovyPagesServlet:  \"" + pageName + "\" not found");
            response.sendError(404, "\"" + pageName + "\" not found.");
            return;
        }

        renderPageWithEngine(engine, request, response, page);
    }

となっているので、フィルタか何かをねじ込んでGrailsApplicationAttributes.GSP_TO_RENDER属性にURIをセットすればよさそうに思えたんだけど、URIが"/"のときとかにうまく行かない...
仕方ないので、

public class LocaledViewResolver extends GrailsViewResolver {
	/** Atrribute for marking LocaledViewResolver checked resource */
	public static final String LOCALESQITCHER_CHECKED =
						"jp.ne.hatena.d.noryksj.grails.LocaleSwitchChecked";
	[...]
	/** {@inheritDoc} */
	protected View createView(String viewName, Locale locale) throws Exception {
		String prefix = getPrefix();
		String path = prefix + viewName + "_" + locale.getLanguage() + GSP_SUFFIX;
		RequestAttributes reqattrs = RequestContextHolder.getRequestAttributes();
		reqattrs.setAttribute(LOCALESQITCHER_CHECKED + "." + path, Boolean.TRUE,
						RequestAttributes.SCOPE_REQUEST);
		ResourceLoader resourceLoader = establishResourceLoader();
		Resource res = resourceLoader.getResource(path);
		if (res != null && res.exists()) {
			viewName = path.substring(prefix.length(), path.length() - GSP_SUFFIX.length());
		}
		return loadView(viewName, locale);
	}
	[...]
}

として、

public class LocaledTemplateEngine extends GroovyPagesTemplateEngine {
	/** {@inheritDoc} */
	public Resource getResourceForUri(String uri) {
		RequestAttributes reqattrs = RequestContextHolder.getRequestAttributes();
		Object done = reqattrs.getAttribute(LocaledViewResolver.LOCALESQITCHER_CHECKED + "." + uri,
						RequestAttributes.SCOPE_REQUEST);
		if (done != null) {
			return 	super.getResourceForUri(uri);
		}
		[...]
	}
}

とすることにした。これで2重チェックは防げるかな。

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に相当するものを全部自前で作らないとダメっぽい。
どーする、俺!?

pageEncodingディレクティブってないの? (Grails 0.5)

昨日書いたGSPファイルの文字コードについて、http://www.nabble.com/Re%3A-template-encoding-p8507832.html とかをみると、JVMのオプションに -Dfile.encoding=UTF-8 とかを指定して対応するみたいだ。まあ、解決策としてはありかもしれないけど、JSPみたいにpageEncodingディレクティブ欲しいなあ。

ロケール対応プラグインを作ってみる[追記](Grails 0.5)

トップページ(index.gsp)のロケール対応もやってみたので追記!なお、以下の処理に対しての追記があるので注意。
プロジェクトディレクトリ配下の、src/java/jp/ne/hatena/d/noryksj/grails/pluginsに以下のようなソースを作成。

package jp.ne.hatena.d.noryksj.grails.plugins;

import java.util.Locale;

import org.codehaus.groovy.grails.web.pages.GroovyPagesTemplateEngine;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.core.io.Resource;

/**
 * Lcale aware GroovyPagesTemplateEngine
 * @author noryksj
 */
public class LocaledTemplateEngine extends GroovyPagesTemplateEngine {
	/** {@inheritDoc} */
	public Resource getResourceForUri(String uri) {
		int lastSlash = uri.lastIndexOf('/');
		int lastDot = uri.lastIndexOf('.');
    	
		if (lastSlash < 0) {
			lastSlash = 0;
		}
		if (lastDot > lastSlash) {
			Locale locale = LocaleContextHolder.getLocale();
			String checkUri = uri.substring(0, lastDot) + "_"
					+ locale.getLanguage() + uri.substring(lastDot);
			Resource res = super.getResourceForUri(checkUri);
			if (res != null && res.exists()) {
				return	res;
			}
		}
		return	super.getResourceForUri(uri);
	}
}

LocaleContextHolder.getLocale()でロケールを取るのは苦肉の策で。で、LocaleViewSwitcherGrailsPlugin.groovyファイルのdoWithSpringクロージャを以下のように修正。

def doWithSpring = {
	groovyPagesTemplateEngine(LocaledTemplateEngine) {
		classLoader = ref("classLoader")
		if(grails.util.GrailsUtil.isDevelopmentEnv()) {
			resourceLoader = groovyPageResourceLoader
		}
	}
	jspViewResolver(LocaledViewResolver) {
		viewClass = org.springframework.web.servlet.view.JstlView.class
		prefix = GrailsApplicationAttributes.PATH_TO_VIEWS
		suffix = ".jsp"
		templateEngine = groovyPagesTemplateEngine
		if(grails.util.GrailsUtil.isDevelopmentEnv()) {
			resourceLoader = groovyPageResourceLoader
		}
	}
}   

再度、プラグインをパッケージして、別のGrailsプロジェクトに再度インストールする。その際、grails cleanを実行してから、plugins/LocaleViewSwitcher-0.1をディレクトリごと削除してから行う。でないと、実行時にZIPファイルが読めないとかわけのわからないエラーが出る(というか出た)。
index_ja.gspを作成して実行すると、できた!!
他にも手を入れないといけない箇所があるやも。それは気が付いたときにすることにする。本当にやりたいことにはまだまだ足りないのだ。
ここにいたって疑問がひとつ。index_ja.gspとかを作ってプロパティファイルとか関係なしにガンガン日本語を書けるのだけれど、index_ja.gspの文字コードは何で作ればいいんだろう?どうもデフォルトのキャラクタセットでGSPファイルを読んでいるようだけど、UTF-8とかで書きたいなぁ。でないと、Windowsで開発したアプリをLinuxで実行すると・・・怖いことになりそう。