No Programming, No Life

プログラミング関連の話題や雑記

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のvalue側にまた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を拡張してconvertメソッドを付加してみました。無駄に@Newifyを使ってるのはご愛嬌で。*1

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のdelegateですからね。

で、このバージョンでは呼び出しがさらにすっきりしました。こんな感じです。

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が出るかもしれないです。

おわりに

ネタを提供してくれたid:kyon_mmさん、ありがとうございました。大変勉強になりました。


Enjoy your code more groovier!!

*1:使いたかったんだ!理由はそれだけ。

*2:見たまんまがそのままXMLなどになること。

*3:Groovy++だったらされるのか?あれ?それともv1.8からされるのか?