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

ためしに、リクエストのロケールに対応するGSPビューを出力するプラグインを作ってみる。やりたいことは、

  1. リクエストのロケールがjaの場合でviews/hogehoge/list_ja.gspが存在する場合、これを出力する。
  2. リクエストのロケールがenの場合でviews/hogehoge/list_en.gspが存在しない場合、list.gspを出力する。

と、ロケールによって出力するビューそのものを変えるようなプラグインを作成する。以下の方法は、いろいろとやってみてこれで動いたみたい!!な内容なので、Grailsの仕様とかにあっているかは保証しません。あしからず。
まずは、適当なディレクトリでプラグインプロジェクトを作成する。

grails create-plugin locale-view-switcher

次に、作成したプロジェクトディレクトリ配下の、src/java/jp/ne/hatena/d/noryksj/grails/pluginsに以下のようなソースを作成。

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

import java.util.Locale;
import grails.util.GrailsUtil;
import org.codehaus.groovy.grails.web.servlet.view.GrailsViewResolver;
import org.springframework.context.ApplicationContext;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.web.servlet.View;

/**
 * A plug-in that switches gsp views according to locale of request.
 * @author noryksj
 */
public class LocaledViewResolver extends GrailsViewResolver {
	private static final String GSP_SUFFIX = ".gsp";
	private static final String GROOVY_PAGE_RESOURCE_LOADER = "groovyPageResourceLoader";
	private ResourceLoader resourceLoader;
    
	/** {@inheritDoc} */
	public void setResourceLoader(ResourceLoader resourceLoader) {
		this.resourceLoader = resourceLoader;
		super.setResourceLoader(resourceLoader);
	}
	
	/**
	 * Copied from GrailsViewResolver
	 * @return ResourceLoader
	 */
	private ResourceLoader establishResourceLoader() {
		ApplicationContext ctx = getApplicationContext();
		if(ctx.containsBean(GROOVY_PAGE_RESOURCE_LOADER)
				&& GrailsUtil.isDevelopmentEnv()) {
			return (ResourceLoader)ctx.getBean(GROOVY_PAGE_RESOURCE_LOADER);
		}
		return this.resourceLoader;
	}

	/** {@inheritDoc} */
	protected View createView(String viewName, Locale locale) throws Exception {
		ResourceLoader resourceLoader = establishResourceLoader();
		String prefix = getPrefix();
		String path = prefix + viewName + "_" + locale.getLanguage() + GSP_SUFFIX;
		Resource res = resourceLoader.getResource(path);
		if (res != null && res.exists()) {
			viewName = path.substring(prefix.length(), path.length() - GSP_SUFFIX.length());
		}
		return loadView(viewName, locale);
	}
	
	/** {@inheritDoc} */
	protected Object getCacheKey(String viewName, Locale locale) {
		return viewName + "_" + locale.getLanguage();
	}
}

作成するのは、GrailsViewResolverを継承して作成する。なぜかは、デバッガでいろいろとみてみたところ、これが良さそうかなーと思ったからだ。GrailsViewResolverでは、使用しているResourceLoaderその他サフィックスなど(GSP_SUFFIXとか、GROOVY_PAGE_RESOURCE_LOADERのことね)が全てprivateなので、これをコピーして同等な機能をまず作成する。protectedとかだったら良かったのに。
やりたいことは、以下の箇所だ。とりあえず、ロケールの言語のみの対応で。

/** {@inheritDoc} */
protected View createView(String viewName, Locale locale) throws Exception {
	ResourceLoader resourceLoader = establishResourceLoader();
	String prefix = getPrefix();
	String path = prefix + viewName + "_" + locale.getLanguage() + GSP_SUFFIX;
	Resource res = resourceLoader.getResource(path);
	if (res != null && res.exists()) {
		viewName = path.substring(prefix.length(), path.length() - GSP_SUFFIX.length());
	}
	return loadView(viewName, locale);
}
	
/** {@inheritDoc} */
protected Object getCacheKey(String viewName, Locale locale) {
	return viewName + "_" + locale.getLanguage();
}

GrailsJavaDocをみていただければわかるのだが、GrailsViewResolverは、org.springframework.web.servlet.view.AbstractCachingViewResolverの子孫クラスとなっている。実際、production環境で起動すると結果をキャッシュする。これが、ResourceLoaderではなくGraisViewResolverを継承することとした理由でもある。
createView(String,Locale)では、ビューが存在するかをチェックし存在する場合は、viewNameを変更して結果を返却するようにしている。また、getCacheKey(String, Locale)では、上記createView(String,Locale)の結果と合うように結果を返却する。このメソッドをオーバライドしてあげないとキャッシュされた結果がおかしくなる。というのは、org.springframework.web.servlet.view.UrlBasedViewResolver#getCacheKey(String, Locale)では単にパラメタを返却するだけの動作にオーバライドされているからだ。
次に、プロジェクトディレクトリに作成されたLocaleViewSwitcherGrailsPlugin.groovyファイルを以下のように修正する。

import org.codehaus.groovy.grails.web.servlet.GrailsApplicationAttributes;
import jp.ne.hatena.d.noryksj.grails.plugins.LocaledViewResolver;

/**
 * A plug-in that switches gsp views according to locale of request.
 *
 * @author noryksj
 */
class LocaleViewSwitcherGrailsPlugin {
	def version = 0.1
	def dependsOn = [:]
	
	def doWithSpring = {
		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
			}
		}
	}   
	def doWithApplicationContext = { applicationContext ->
		// TODO Implement post initialization spring config (optional)		
	}
	def doWithWebDescriptor = {
		// TODO Implement additions to web.xml (optional)
	}	                                      
	def onChange = { event ->
		// TODO Implement code that is executed when this class plugin class is changed  
		// the event contains: event.application and event.applicationContext objects
	}                                                                                  
	def onApplicationChange = { event ->
		// TODO Implement code that is executed when any class in a GrailsApplication changes
		// the event contain: event.source, event.application and event.applicationContext objects
	}
}

修正したのは、コメント、import、doWithSpringクロージャのみ。doWithSpringクロージャの中身はsrc/groovy/org/codehaus/groovy/grails/plugins/web/ControllersGrailsPlugin.groovyから抜粋して変更した(パクッたとも言う)。これで、使用するViewResolverをがっつりと変更だ!
最後に、

grails package-plugin

として、他のプロジェクトでgrails install-pluginとして使用してみた。当然、ビューを事前に作っておかなければならない(grails create-viewsとかして)。その後、list.gspとかをコピーしてlist_ja.gspを作成して起動する。
ただ、index.gsp(トップページ)とかがダメみたい orz。多分、ResourceLoaderとかに手を入れないといけないような気が...。これはまた今度...