2010/03/19

Anakia:#set()の挙動について

このブログ記事や最近作成したIronPythonについての文書はAnakiaで生成するようにしています。 最近ではJSPとの比較でVelocityが語られる事はむしろ少なくなってきたのかもしれませんが、テンプレート言語として習得の容易さやスピードの面で優れていると思います。

ただ、しばらく使ってみてAnakiaで作成する原稿からは事前に設定された、主にXML文書のリソース、にアクセスできるだけで柔軟性に欠ける部分があると感じるようになりました。 つまり動的にマクロの内部から任意のXMLファイルへ、あるいはネットワーク越しにRSSなどのXML形式の外部データソースにアクセスする事はできません。

これはシンプルにするための割り切りなのかもしれませんが、プロセス上のフレームワークの作り手とドキュメントを作成するライターの役割分担を行なうことを意識しているようにも思えます。

いずれにしても本当に不満であれば、AnakiaTask.java辺りを書き換えれば簡単に拡張できそうです。 例えば$dateで参照可能なjava.util.Dateオブジェクトは次のようなコードで実装されています。

org/apache/anakia/AnakiaTask.javaより抜粋

...
context.put ("date", new java.util.Date() );
...

本題:Velocity #set()マクロの挙動について

さてさて、本題ですが、*.vslマクロファイルを書いている時に気がついたコーディングパターンがあります。 それは#setマクロでgetAttributeValue()メソッドを使った時に変数が初期化されないという点です。 結局のところ次のように空文字列で初期化をする1行が必要になりました。

最終的に作成したマクロ

#macro (find_nextpage $value)##
  #set($name = "")
  #set($name = $root.getChild("next").getAttributeValue("name"))##
  ...

このgetAttributeValue()メソッドを呼び出す前に$name変数を空文字列で初期化しているところが問題です。 こうしないと該当するAttributeが存在しない場合に、前のループでセットした$nameの値がそのまま維持されてしまうところです。 $nameの値はnullでもなく変化しません。

velocity.propertiesの設定でvelocimacro.context.localscope=falseとしているために、全ての変数が大域変数と同じ扱いになるため以前の呼び出しで設定された$nameの値を覚えているのは、しかたのないことです。 しかし代入しているのに、左辺値が変化しないのはちょっと感覚とずれているように感じます。

左辺値が変化しないのはgetAttributeValue()に限らず、#set()マクロの基本的な挙動のようです。 コードをざっとみたところleftReferenceがcontextから削除されているので、未定義なのかと思いきや以前の値が復活しているという少し謎な動きです。

getAttributeValue()を少し追ってみる

私がコーディングしたコードではgetChildren("name")のようなメソッドは、getName()で事前にタグが"name"だと判定しているので戻り値があると分かっていて呼ぶ場合がほとんどでした。 その反面getAttributeValue()はオプション扱いのattributeの存在を確認するために呼ぶ場合がほとんどだったため、戻り値が空文字列になると仮定して書いたコードは、とってもバギーになってしまいました。

そこでgetAttributeValue()周りのコードをシンプルにできないか少しコードを追ってみました。

もともとgetAttributeValue()メソッドはAnakiaが準備しているものではなく、 org.jdom.Elementで定義されているjdom由来のメソッドです。 この処理は次のように定義されています。

引数が1つの場合(org/jdom/Element.javaより抜粋)

    public String getAttributeValue(final String name) {
        return getAttributeValue(name, Namespace.NO_NAMESPACE);
    }

さらにNamespaceが指定されている場合に呼ばれるメソッドは次のようになります。

引数が2つでNamespaceを取る場合(org/jdom/Element.javaより抜粋)

    public String getAttributeValue(final String name, final Namespace ns) {
        return getAttributeValue(name, ns, null);
    }

最終的には次のように該当するattributeがない場合には、defに該当するnullが返されます。

該当するattributeがない場合に第3引数が返される

    public String getAttributeValue(final String name, final Namespace ns, final String def) {
        final Attribute attribute = (Attribute) attributes.get(name, ns);
        if (attribute == null) {
            return def;
        }

        return attribute.getValue();
    }

しかし.getAttributeValue("name", "", "")をようなマクロを作成しても呼ばれるのはGetAttributeValue("name")のままで、defにはnullが入ってしまうため回避する方法はなさそうです。

別のメソッドを呼び出す方法がないか考えましたが、良い方法は思い付きません。 最初の例を別の方法で書き直すなら、次のような方法でしょうか。

別の解決策

  #if($root.getChild("next").getAttributeValue("name"))##
    #set($name = $root.getChild("next").getAttributeValue("name"))##
  #else##
    #set($name = "")##
  #end##

いずれにしても良い方法ではないので、繰り返し呼ぶマクロ内部では変数の初期化とgetAttributeValue()の呼び出しは対で必要そうです。

0 件のコメント: