2

I've got this Groovy code working to delete nodes using xpath strings, but I'm having problems deleting nodes where the xpath results in multiple node instances.

Sample XML...

<root>
  <element1>foo</element1>
  <element2>bar</element2>
  <items>
     <item>
       <name>a</name>
       <desc>b</desc>
     <item>
     <item>
        <name>c</name>
        <desc>x</desc>
     </item>
  </items>
</root>

Code to delete nodes...

def resource = XmlSlurper().parseText(xml)
def xpathsToDelete = ['/root/element1','/root/items/item/name']
 xpathsToDelete.each {
     def pathTokens = it.path.tokenize '/'
     def currentNode = resource
     if ( currentNode.name() == pathTokens.first() ) { 
       def xpath = pathTokens.tail().join '/'
       currentNode = currentNode."${xpath}"
       currentNode.replaceNode{}
     }
}

The above code removes the node element1 using xpath /root/element1, which evaluates to a single node, but does not work for /root/items/name which evaluates to multiple nodes.

Opal
  • 81,889
  • 28
  • 189
  • 210
raffian
  • 31,267
  • 26
  • 103
  • 174

2 Answers2

2

This is a tricky one. It is related to this question, which is vital to my answer.

Here is a solution:

import groovy.util.* 
import groovy.xml.* 

def xml = """<root>
  <element1>foo</element1>
  <element2>bar</element2>
  <items>
     <item>
       <name>a</name>
       <desc>b</desc>
     </item>
     <item>
        <name>c</name>
        <desc>x</desc>
     </item>
  </items>
</root>"""

def removeNodes = { doc, path ->
    def nodes = doc
    path.split("\\.").each { nodes = nodes."${it}" }
    nodes.each { it.replaceNode{} }    
}

def resource = new XmlSlurper().parseText(xml)
def xpathsToDelete = ['/root/element1','/root/items/item/name']

xpathsToDelete.each { xpath ->
    def trimXPath = xpath.replaceFirst( "/root/", "").replace("/",".")
    removeNodes(resource, trimXPath)
}

println XmlUtil.serialize(new StreamingMarkupBuilder().bind {
    mkp.yield resource
})
Community
  • 1
  • 1
Michael Easter
  • 23,733
  • 7
  • 76
  • 107
  • Is `xpath.replace("/root/", "")` necessary? I'm working with different xml documents, the root element name will change, but it's easy for me to get the element name, so perhaps I can set it using a variable instead of hardcoded `"/root/"` – raffian Oct 15 '12 at 16:21
  • Good point. That code is only there because I was trying to reproduce your exact scenario. If you change the values in the xpathsToDelete array to ["element1", "items/item/name"], the code can be simplified. – Michael Easter Oct 15 '12 at 18:23
2

This seems to work as well:

import groovy.xml.*

def xml = '''<root>
            |  <element1>foo</element1>
            |  <element2>bar</element2>
            |  <items>
            |     <item>
            |       <name>a</name>
            |       <desc>b</desc>
            |     </item>
            |     <item>
            |        <name>c</name>
            |        <desc>x</desc>
            |     </item>
            |  </items>
            |</root>'''.stripMargin()

def newxml = new XmlSlurper().parseText( xml ).with { x ->
  [ '/root/element1', '/root/items/item/name' ].each { path ->
    def s = path.split( '/' ).drop( 2 ).inject( x ) { element, p ->
      element."$p"
    }?.replaceNode {}
  }
  x
}

println XmlUtil.serialize(new StreamingMarkupBuilder().bind {
    mkp.yield newxml
})
tim_yates
  • 167,322
  • 27
  • 342
  • 338
  • This looks short and sweet, will test it now, what does `mkp.yield` do? – raffian Oct 15 '12 at 13:56
  • That just passes the `newxml` structure into the StreamingMarkupBuilder as-is, which is then passed to `XmlUtil.serialize` to make it pretty for printing – tim_yates Oct 15 '12 at 14:03