9

I have the following groovy code:

def xml = '''<note>
<to>Tove</to>
<from>Jani</from>
<heading>Reminder</heading>
<body>Don't forget me this weekend!</body>
<foot>
    <email>m@m.com</email>
    <sig>hello world</sig>
</foot>
</note>'''

def records = new XmlSlurper().parseText(xml)

How do I get records to return a map looks like the following:

["to":"Tove","from":"Jani","heading":"Reminder","body":"Don't forget me this weekend!","foot":["email":"m@m.com","sig":"hello world"]]

Thanks.

dmahapatro
  • 49,365
  • 7
  • 88
  • 117
nzsquall
  • 395
  • 1
  • 10
  • 27

2 Answers2

13

You can swing the recursion weapon. ;)

def xml = '''<note>
<to>Tove</to>
<from>Jani</from>
<heading>Reminder</heading>
<body>Don't forget me this weekend!</body>
<foot>
    <email>m@m.com</email>
    <sig>hello world</sig>
</foot>
</note>'''

def slurper = new XmlSlurper().parseText( xml )

def convertToMap(nodes) {
    nodes.children().collectEntries { 
        [ it.name(), it.childNodes() ? convertToMap(it) : it.text() ] 
    }
}

assert convertToMap( slurper ) == [
    'to':'Tove', 
    'from':'Jani', 
    'heading':'Reminder', 
    'body':"Don't forget me this weekend!", 
    'foot': ['email':'m@m.com', 'sig':'hello world']
]
dmahapatro
  • 49,365
  • 7
  • 88
  • 117
  • 1
    why collect().sum() and not collectEntries() ? – cfrick Nov 12 '14 at 15:55
  • 3
    In one line for a laugh: `def map = new XmlSlurper().parseText(xml).children().collectEntries {n->[(n.name()):{->n.children()?.collectEntries(owner)?:n.text()}()]}` – tim_yates Nov 12 '14 at 17:24
  • @tim_yates Cool. This is the second time I have seen you playing swiftly with `owner` here in SO. ;-) Both of them involved recursion. :P – dmahapatro Nov 12 '14 at 17:29
  • 1
    @dmahapatro ...wracking my brains thinking of another valid use for `owner`... ;-) – tim_yates Nov 12 '14 at 17:36
  • Thanks guys :), but I ran your solution and saw the following exception: Caught: groovy.lang.MissingMethodException: No signature of method: groovy.util.slurpersupport.NodeChildren.collectEntries() is applicable for argument types: (CopyOftest$_run_closure1) values: [CopyOftest$_run_closure1@5c0ad75b] groovy.lang.MissingMethodException: No signature of method: groovy.util.slurpersupport.NodeChildren.collectEntries() is applicable for argument types: (CopyOftest$_run_closure1) values: [CopyOftest$_run_closure1@5c0ad75b] at CopyOftest.run(CopyOftest.groovy:23) – nzsquall Nov 12 '14 at 20:03
  • How old is your version of groovy? – tim_yates Nov 12 '14 at 20:07
  • When I ran it in Eclipse (groovy compiler 2.1), it complaints with the above error. When I run it from command line (groovy compiler 2.3), it is working fine.... – nzsquall Nov 12 '14 at 20:21
  • I used it in my code, but it seems to ignore multiple children and just adds one of them (looks like the second one of 2) – Ruth Aug 21 '18 at 06:30
  • @RuthiRuth How does the xml look like? – dmahapatro Aug 21 '18 at 12:53
  • @RuthiRuth I believe I have the idea what you are facing here. This is expected. A map cannot have duplicate keys. If you have notes which are repeated in XML then you have come up with a plan to represent them in a list. – dmahapatro Aug 21 '18 at 13:02
  • yes I was thinking this could be the case, but wanted to point out that this is not a general solution - more a solution to a very specific problem (my question was marked as a duplicate - https://stackoverflow.com/questions/51932850/transform-xml-into-a-map-using-groovy – Ruth Aug 22 '18 at 14:16
0

The posted solution works if you have unique tags among children of a node. The problem with collectEntries is that it only allows unique keys. If you have <to>Jani</to> <to>Bani</to> the last tag will overwrite the previous one in the resulting Map. To avoid this you can group the children by their name first using groupBy.

Map xmlToMap(node) {
    if( !node.childNodes() ){
        [(node.name()): node.text()]
    }else{
        Map groups = node.children().groupBy { it.name() }
        [(node.name()): groups.collectEntries { k, v ->
            v.size() == 1 ? xmlToMap(v.first()) : [k, v.collectMany { xmlToMap(it).values() }]
        }]
    }
}

def xml = '''<note>
<to>Tove</to>
<to>Jani</to> 
<to>Bani</to>
<from>Jani</from>
<heading>Reminder</heading>
<body>Don't forget me this weekend!</body>
<foot>
    <email>m@m.com</email>
    <email>m2@m.com</email>
    <sig>hello world</sig>
</foot>
</note>'''

def slurper = new XmlSlurper().parseText(xml)
assert xmlToMap(slurper).note == [to:['Tove', 'Jani', 'Bani'], from:'Jani', heading:'Reminder', body:"Don't forget me this weekend!", foot:[email:['m@m.com', 'm2@m.com'], sig:'hello world']]
IWilms
  • 500
  • 3
  • 10