読者です 読者をやめる 読者になる 読者になる

Webエンジニア susumuis の技術ブログ

このブログの内容は個人の見解であり、所属する組織の公式見解ではありません

Mayaaでm:idの解決の仕方を自分好みにカスタマイズする方法

このブログで何度か触れましたが、僕の勤務先の会社では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);
}

これをEngineごとに差し替える機能が提供されているので、単なる多段化を実現するならこれを実装するだけで十分です。ちなみに標準はこのようになっています。

    public Specification getParentSpecification(Specification spec) {
        if (spec instanceof Page) {
            return ProviderUtil.getEngine();
        } else if (spec instanceof Template) {
            return ((Template) spec).getPage();
        }
        return null;
    }

ふむふむ。テンプレートも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するだけです。

この記事は、自ブログよりも、勤務先の技術ブログに書いたほうが適切なので、折を見てそちらに転載しようと思います。