GroovyのMarkupBuilderで再起的な構造のXMLを生成する
はじめに
発端はid:kyon_mmさんが
groovyでパラメータでわたってきたkey,valueをそのままxmlのタグ名とバリューにしたいときってどうやるのがいいんだろう。DOMを使うのがいいのかな? #groovy
とつぶやいていたところから。
(2011-03-19追記)
id:kyon_mmさんの方でTogetterしてくれました。そちらで流れを追ってからこの記事を読むと分かりやすいと思います。
ひとまずサンプルコードをgistにあげて貰ってからちょっといじってみました。
サンプルコード by id:kyon_mmさん
やりたいことを要約すると、
[ new KeyValue("key1","value1"), new KeyValue("key2","value2"), new KeyValue("key3", [ new KeyValue("key3-1","value3-1"), new KeyValue("key3-2","value3-2") ]) ]
のようなList
<langs type="current"> <key1>value1</key1> <key2>value2</key2> <key3> <key3-1>value3-1</key3-1> <key3-2>value3-2</key3-2> </key3> </langs>
のようなXMLを作りたいということですね。
処理戦略は至ってシンプルで、listをeachしてMarkupBuilderでXMLの構造を作っていこうというもの。ただし、前述のように構造が再起的であるため、単純にeachしただけではうまくいきません。
このはじめのサンプルの流れを大きく変えずにいじっていきたいと思います。
改造その1:力技で再起処理を実装
convert関数をちょっといじって、builderを引数で受け取って再起的に処理を行えるようにしたバージョンがこちらです。
改造の本体はこのconvertメソッドで引数で受け取ったbuilderを使って、「builder.タグ名(内容)」の形でMarkupBuilderを活用しています。でもfirstとかカッコわるい…
def convert(def builder, List list, boolean first = true) { if (first) { builder.langs(type: 'current') { convert(builder, list, false) } } else { list.each { kv -> if (kv.value instanceof List) builder."${kv.key}" { convert(builder, kv.value, false) } else builder."${kv.key}"(kv.value) } } }
このようにしたことで、呼び出し側も以下のようになりました。
convert(xml, list)
改造その2:List自身にMarkupBuilderを処理する能力を付加
処理する主体はList
List.metaClass.convert = { def builder, boolean first = true -> def me = delegate if (first) { builder.langs(type: 'current') { me.convert(builder, false) } } else { me.each { kv -> if (kv.value instanceof List) builder."${kv.key}" { kv.value.convert(builder, false) } else builder."${kv.key}"(kv.value) } } }
処理自体は引数からListが減ったくらいで、基本的には改造その1と同じです。meとかでdelegateの参照を保持しているのはbuilderのクロージャに入ってしまうと、delegateの参照がbuilderのクロージャの方のものになってしまうからです。ここで欲しいのはあくまで最初のList
で、このバージョンでは呼び出しがさらにすっきりしました。こんな感じです。
list.convert(xml)
改造その3:List自身にやらせるのは再起処理の部分だけに限定する
その2ではMarkupBuilderの持つ自己表現能力*2が落ちてしまっていましたので、List自身にやらせるのは再起処理の部分だけに限定し、最初のバージョンにより近づけてみました。いちおうこれが完成版となります。
List.metaClassに追加しているメソッドは以下のようにシンプルになりました。builderを受け取り、もし、valueがListだった場合は自己再起、そうでなければkey, valueのタグを出力するという流れです。
List.metaClass.eachKeyValueRecurse = { builder -> delegate.each { kv -> if (kv.value instanceof List) builder."${kv.key}" { kv.value.eachKeyValueRecurse(builder) } else builder."${kv.key}"(kv.value) } }
呼び出しも分かりやすくこんな感じに。
xml.langs(type: 'current') { list.eachKeyValueRecurse(xml) }
そういえば
ちょっと気になるのはGroovyの場合再起って末尾再起にしても最適化とかされなかったような気がする*3のであまり深いネストをされるとOutOfMemoryErrorが出るかもしれないです。