月別アーカイブ: 2012年5月

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);
}

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