xmlgoxpathsiblings

How to access sibling element using Go with etree XML package?


Given an XML document such as this:

<MasterXML>
    <Processes>
        <Params>
            <ParamName>today</ParamName>
            <ParamType>1</ParamType>
            <ParamValue/>
        </Params>
       <Params>
            <ParamName>today</ParamName>
            <ParamType>2</ParamType>
            <ParamValue/>
        </Params>
       <Params>
            <ParamName>today</ParamName>
            <ParamType>3</ParamType>
            <ParamValue/>
        </Params>
    </Processes>
</MasterXML>

Using the beevik/etree for Go package, how can I access <ParamValue/> for each instance of <Params> in the document to populate it with a value when <ParamName> has a particular, specific value.

In the given example, I'd want to populate all <ParamValue/> nodes in all the <Params> nodes with 05/02/2024 when <ParamName> holds the value today.

This code only works on the first instance of the <Params> node in a document containing many instances where <ParamName>== today , although the loop should seemingly access every instance of <Params>:

    for _, elem1 := range doc.FindElements(".//Processes//Params//ParamName") {
    
            if elem1 == nil {
                log.Fatal("Check XPath)
            }
    
            s := elem1.Text()
    
            if s == "today" {
                elem2 := elem1.FindElement("//ParamValue")
                elem2.SetText("05/02/2024")
            }
    }

How can I do this? Why doesn't range doc.FindElements(".//Processes//Params//ParamName") find every instance of <Params>? Should I be using a different approach?


Solution

  • Your current code does behave in the way you described, because by using the double slash in the XPath you always search the XML tree recursively, meaning that you always find the first occurence from the top.

    This also means that FindElements has always worked the way you thought (which can be validated easily by uncommenting the println statement below, or by running the code through your debugger).

    In order to actually produce the desired behaviour, your XPath statement should be adjusted:

    therefore the correct XPath would be ../ParamValue".

    Here is the fixed sample:

    package main
    
    import (
        "log"
        "os"
        "strings"
    
        "github.com/beevik/etree"
    )
    
    func main() {
        doc := etree.NewDocument()
        if err := doc.ReadFromFile("master.xml"); err != nil {
            panic(err)
        }
    
        for _, elem1 := range doc.FindElements(".//Processes//Params//ParamName") {
    
            if elem1 == nil {
                log.Fatal("Check XPath")
            }
    
            s := elem1.Text()
            //println(s)
    
            if strings.TrimSpace(s) == "today" {
                elem2 := elem1.FindElement("../ParamValue")
                elem2.SetText("05/02/2024")
            }
        }
    
        doc.WriteTo(os.Stdout)
    }
    

    This will require master.xml in the same directory as your .go file.

    I went with this:

    <MasterXML>
        <Processes>
            <Params>
                <ParamName> today </ParamName>
                <ParamType>1</ParamType>
                <ParamValue/>
            </Params>
           <Params>
                <ParamName> tomorrow </ParamName>
                <ParamType>2</ParamType>
                <ParamValue/>
            </Params>
           <Params>
                <ParamName> today </ParamName>
                <ParamType>3</ParamType>
                <ParamValue/>
            </Params>
        </Processes>
    </MasterXML>
    

    which produces the desired output:

    <MasterXML>
        <Processes>
            <Params>
                <ParamName> today </ParamName>
                <ParamType>1</ParamType>
                <ParamValue>05/02/2024</ParamValue>
            </Params>
           <Params>
                <ParamName> tomorrow </ParamName>
                <ParamType>2</ParamType>
                <ParamValue/>
            </Params>
           <Params>
                <ParamName> today </ParamName>
                <ParamType>3</ParamType>
                <ParamValue>05/02/2024</ParamValue>
            </Params>
        </Processes>
    </MasterXML>