このブログで何度か触れましたが、僕の勤務先の会社ではMayaaを使っています。まだMayaaを使っていますし、これからも使っていくと思います。しかしさすがにMayaa長いこと使用していると、次の悩みが発生しました。
- default.mayaaファイルが巨大化しすぎた
- 利用当初のノウハウが無かった頃に作ったm:id体系を改めて新しく作り直したい!
しかしながら、
- 既存の資産を捨てる訳にはいかない
というビジネス上の事情もあり、このような手段を取ることにしました。
- 今までのID体系をm:id属性として提供し、新しいID体系をe:idとして提供する(eは弊社の製品のイニシャルがeであるためです)
- e:idは全てdefault.mayaaのように全ページで使えるようにし、ファイルが肥大化しないように分割できるようにする
- Mayaaファイル内の記述が冗長にならないよう、よく使うプロセッサーをショートカット出来るような新しいプロセッサーを作る
これらについての実現方法を今日は紹介したいと思います。ボリュームが多いので複数回に分けようと思います。
Not only m:id, but also e:id
テンプレートのid、またはm:id属性と、mayaaファイルのプロセッサーとのマッピングは、EqualsIDInjectionResolverによって行われています。他に、XPathMatchesInjectionResolverなど、複数のInjectionResolverが存在しそれらを登録することで、柔軟にプロセッサーの解決ルールを定義することができます。なんて柔軟な作りなのでしょう!このおかげで独自のInjectionResolverを実装して登録することによって、独自のルールでプロセッサーを解決することが出来るのです!この時点で勝利が決定したようなものです。
では、InjectionResolverの実装はどのようにすればいいでしょうか?実際はEqualsIDInjectionResolverのコードを熟読したわけですが(美しいコードで読みやすかったです!)、今回はEqualsIDInjectionResolverを継承することにしました。修正部分はこの部分です。
まず、IDをm:idではなく独自のnamespaceの属性で取れるようにします。
// ここにURIを定義、実際は所属組織のURIなどを記述
public static final URI URI_EXAMPLE_COM = URIImpl.getInstance("http://hogehoge.example.com");
@Override
protected NodeAttribute getAttribute(SpecificationNode node) {
NodeAttribute attr = node.getAttribute(QNameImpl.getInstance(URI_EXAMPLE_COM, "id"));
if (attr != null) {
return attr;
}
return null;
}
これで、テンプレートに
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:m="http://mayaa.seasar.org" xmlns:e="http://hogehoge.example.com" xml:lang="ja" lang="ja">
と記述するだけで、m:idではなくe:idでマッピングすることができます。さらに、デフォルトのページ名.mayaaやdefault.mayaaを見に行かず別のルーティングでmayaaファイルを見つけに行くようにしましょう。これにはどうしたらよいでしょ?ヒントはページのmayaaファイルの次にdefault.mayaaを読みに行く機構です。親のMayaaファイルを探しに行く機構として、ParentSpecificationResolverというインターフェースが提供されています。
public interface ParentSpecificationResolver extends ParameterAware {
/**
* 指定した{@link Specification}の親を取得する。
* <p>
* 標準の実装では、テンプレートファイルの場合は対応するMayaaファイル、
* Mayaaファイルの場合はdefault.mayaaファイルの{@link Specification}を返す。
* default.mayaaの親はないので{@code null}を返す。
* </p>
* @param spec 親を探す起点となる{@link Specification}。見つからない場合は{@code null}。
*/
Specification getParentSpecification(Specification spec);
}
ふむふむ。テンプレートもSpecificationであって、Mayaaファイルに当たるものはPageらしいです。そして、Pageの親はEngine(これはdefault.mayaaに相当)なのですね!
今回は、一つのInjectionResolverにだけ別の親解決ロジックを組み込みたかったので、残念ながらこの機構は使用出来ませんでした。それではどうするかというと、少々強引ですが、メソッド一個をコピペして書き換えました。
@Override
public SpecificationNode getNode(SpecificationNode original,
InjectionChain chain) {
if (original == null || chain == null) {
throw new IllegalArgumentException();
}
String id = getID(original);
if (StringUtil.hasValue(id)) {
Specification spec = SpecificationUtil.findSpecification(original);
SpecificationNode injected = null;
while (spec != null) {
SpecificationNode mayaa = SpecificationUtil.getMayaaNode(spec);
if (mayaa != null) {
List injectNodes = new ArrayList();
getEqualsIDNodes(mayaa, id, injectNodes);
if (injectNodes.size() > 0) {
injected = (SpecificationNode) injectNodes.get(0);
if (isReportDuplicatedID() && injectNodes.size() > 1) {
logWarnning(id, original, 2);
}
break;
}
}
// ここを標準と差し替える。 by ishigami
// spec = EngineUtil.getParentSpecification(spec);
spec = MyMayaaEngineUtil.getNextEIDSpecification(spec);
// ここを標準と差し替える。 by ishigami end
}
if (injected != null) {
if (QM_IGNORE.equals(injected.getQName())) {
return chain.getNode(original);
}
return injected.copyTo(getCopyToFilter());
}
if (isReportResolvedID()) {
logWarnning(id, original, 1);
}
}
return chain.getNode(original);
}
願うことならこの部分がprotected以上のメソッドとして提供されていたらOverrideできたので、是非ともそのようになって欲しいですね。
さて、MyMayaaEngineUtil.getNextEIDSpecification(spec);についてですが、
これは、ディレクトリのリストを取得して、アルファベット順に次のmayaaファイルを返し、次のファイルが無くなったらEngineを返すようにしています。普通のコードなのでここでは割愛します。
さあ、これで、任意のディレクトリに置いたmayaaファイルを順に読みこんでくれるようになったので、ディレクトリにmayaaファイルを分割して配置することが出来るようになりました!全部ロードするのが不都合になったらのちのちimport機構を作ればいいでしょう。
次にMayaaにこのInjectionResolverを登録しましょう。これは他の設定と同様で、src/META-INFなどの直下にorg.seasar.mayaa.provider.ServiceProviderというファイルを作成し、標準の設定ファイルから、次の部分を抜粋して書き換えます。
<provider>
<templateBuilder
class="org.seasar.mayaa.impl.builder.TemplateBuilderImpl">
<resolver class="org.seasar.mayaa.impl.builder.injection.MetaValuesSetter"/>
<resolver class="org.seasar.mayaa.impl.builder.injection.ReplaceSetter"/>
<resolver class="org.seasar.mayaa.impl.builder.injection.RenderedSetter"/>
<resolver class="org.seasar.mayaa.impl.builder.injection.InsertSetter"/>
<resolver class="org.seasar.mayaa.impl.builder.injection.InjectAttributeInjectionResolver"/>
<resolver class="org.seasar.mayaa.impl.builder.injection.EqualsIDInjectionResolver">
<parameter name="reportUnresolvedID" value="true"/>
<parameter name="reportDuplicatedID" value="true"/>
<parameter name="addAttribute"
value="{http://www.w3.org/TR/html4}id"/>
<parameter name="addAttribute"
value="{http://www.w3.org/1999/xhtml}id"/>
</resolver>
<!-- EID対応のため独自のものを加えている -->
<resolver class="com.example.hogehoge.MyEqualsEIDInjectionResolver">
<parameter name="reportUnresolvedID" value="true"/>
<parameter name="reportDuplicatedID" value="true"/>
</resolver>
<resolver class="org.seasar.mayaa.impl.builder.injection.XPathMatchesInjectionResolver"/>
<parameter name="outputTemplateWhitespace" value="true"/>
<parameter name="outputMayaaWhitespace" value="false"/>
<parameter name="optimize" value="true"/>
</templateBuilder>
</provider>
この状態でWebアプリを立ち上げると、思った通りにe:idと記述した時、標準とは違う任意のファイル解決ルールでプロセッサーをひもづけることができました。しかし、まだ問題があります。今のままではmayaaファイルを変更・追加しても、Webアプリケーションを再起動するまで反映してくれません。これを対応するためには、SourceDescriptorの実装が必要ですが、長くなるので次回以降にしたいと思います。概要だけ説明すると、getTimestampをOverrideするだけです。