MarkupBuilder を斜め読みする
Yokohama.groovy #7 で気になった MarkupBuilder のソースを斜め読みしてみました。
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 種類になるんですね。
ざっくり斜め読みしただけですが、ちょっとすっきりした気がします。
今後も機会を見つけて少しずつソースコードリーディングしようと思います。