MarkupBuilder を斜め読みする

Yokohama.groovy #7 で気になった MarkupBuilder のソースを斜め読みしてみました。

参照したソース

MarkupBuilder.java
BuilderSupport.java

MarkupBuilder クラス

BuilderSupport クラスを継承。
このクラスには invokeMethod メソッドも methodMissing メソッドもない。

BuilderSupport クラス

invokeMethod メソッドが実装されており、存在しないメソッドが呼ばれたときの処理の定義をしている。
MarkupBuilder で XML の要素名のメソッドが呼び出せるのはこれのおかげ。

ソース抜粋
    public Object invokeMethod(String methodName) {
        return invokeMethod(methodName, null);
    }

    public Object invokeMethod(String methodName, Object args) {
        Object name = getName(methodName);
        return doInvokeMethod(methodName, name, args);
    }

    protected Object doInvokeMethod(String methodName, Object name, Object args) {
        Object node = null;
        Closure closure = null;
        List list = InvokerHelper.asList(args);

        //System.out.println("Called invokeMethod with name: " + name + " arguments: " + list);

        switch (list.size()) {
            case 0:
                node = proxyBuilder.createNode(name);
                break;
            case 1: {
                Object object = list.get(0);
                if (object instanceof Map) {
                    node = proxyBuilder.createNode(name, (Map) object);
                } else if (object instanceof Closure) {
                    closure = (Closure) object;
                    node = proxyBuilder.createNode(name);
                } else {
                    node = proxyBuilder.createNode(name, object);
                }
            }
            break;
            case 2: {
                Object object1 = list.get(0);
                Object object2 = list.get(1);
                if (object1 instanceof Map) {
                    if (object2 instanceof Closure) {
                        closure = (Closure) object2;
                        node = proxyBuilder.createNode(name, (Map) object1);
                    } else {
                        node = proxyBuilder.createNode(name, (Map) object1, object2);
                    }
                } else {
                    if (object2 instanceof Closure) {
                        closure = (Closure) object2;
                        node = proxyBuilder.createNode(name, object1);
                    } else if (object2 instanceof Map) {
                        node = proxyBuilder.createNode(name, (Map) object2, object1);
                    } else {
                        throw new MissingMethodException(name.toString(), getClass(), list.toArray(), false);
                    }
                }
            }
            break;
            case 3: {
                Object arg0 = list.get(0);
                Object arg1 = list.get(1);
                Object arg2 = list.get(2);
                if (arg0 instanceof Map && arg2 instanceof Closure) {
                    closure = (Closure) arg2;
                    node = proxyBuilder.createNode(name, (Map) arg0, arg1);
                } else if (arg1 instanceof Map && arg2 instanceof Closure) {
                    closure = (Closure) arg2;
                    node = proxyBuilder.createNode(name, (Map) arg1, arg0);
                } else {
                    throw new MissingMethodException(name.toString(), getClass(), list.toArray(), false);
                }
            }
            break;
            default: {
                throw new MissingMethodException(name.toString(), getClass(), list.toArray(), false);
            }

        }

        if (current != null) {
            proxyBuilder.setParent(current, node);
        }

        if (closure != null) {
            // push new node on stack
            Object oldCurrent = getCurrent();
            setCurrent(node);
            // let's register the builder as the delegate
            setClosureDelegate(closure, node);
            closure.call();
            setCurrent(oldCurrent);
        }

        proxyBuilder.nodeCompleted(current, node);
        return proxyBuilder.postNodeCompletion(current, node);
    }

doInvokeMethod メソッド内で、引数 args の数によって処理を振り分けて、引数にクロージャがあれば呼び出します。
これにより入れ子になった要素名メソッドを順次実行、ノードを作成していきます。
うまくできてますねー。

あと本筋とは関係ないですが、なぜ引数が 0 から 3 に限定できるのかわかりませんでした。
見た感じ InvokerHelper.asList(args) が怪しいので確認してみました。

import org.codehaus.groovy.runtime.InvokerHelper;
class Hoge {
    Object invokeMethod(String name, Object args) {
        List list = InvokerHelper.asList(args);
        println list.size()
        println list
    }
}
Hoge h = new Hoge()

h.Products() {
    Product(type:'regular'){
        Name('Instant Noodle')
        Price(147)
    }
}

h.Products(attribute1:'1','contents',attribute2:'2') {
  Product(type:'regular'){
    Name('Instant Noodle')
    Price(147)
  }
}
1
[ConsoleScript9$_run_closure1@33afbbe3]
3
[[attribute1:1, attribute2:2], contents, ConsoleScript9$_run_closure2@7885bf5f]

2 つ目の呼び出しでは意図的に引数を属性 2、内容とクロージャ各 1 の合計 4 つ渡した上、属性の並びも不連続にしています。
結果は size() が 3 、属性が map にまとめられています。

どうやら、タグの内容(数値 or 文字列)、属性(map)、クロージャに分けているようです。
XML の構成要素は、<要素名 属性="値">内容 であり、ビルダーでは要素名がメソッド名になるので、引数としては属性と内容、入れ子として処理するためのクロージャの 3 種類になるんですね。

ざっくり斜め読みしただけですが、ちょっとすっきりした気がします。
今後も機会を見つけて少しずつソースコードリーディングしようと思います。