Add an attribute to an element in an existing XML using Xquery

4.2k views Asked by At

I need to add an attribute to an element of my response XML using XQuery. Take the below XML as input,

<xyz:RootNode xmlns:abc="url1" xmlns:xyz="url2">
  <abc:OtherNodes/>
  <abc:messageHeader att1="val1" att2="val2">
       <abc:childNodes/>  
  </abc:messageHeader>
  <abc:OtherNodes/>
</xyz:RootNode>

Need an Xquery that add one more attribute newAtt with value newVal and give the result as,

<xyz:RootNode xmlns:abc="url1" xmlns:xyz="url2">
   <abc:OtherNodes/>
   <abc:messageHeader att1="val1" att2="val2" newAtt="newVal">
       <abc:childNodes/>  
  </abc:messageHeader>
  <abc:OtherNodes>
</xyz:RootNode>

Each time the number of attributes of message header may change. So the query should add a new attribute along with all the existing attributes and return the whole document.

2

There are 2 answers

2
Loren Cahlander On BEST ANSWER

Try the following:

xquery version "3.0";

module namespace foo="http://exist-db.org/apps/ns/foo";

declare function foo:process-node($node as node()?, $model as map()) {
    if ($node) then 
    typeswitch($node) 
        case text() return $node
        case element(messageHeader) return foo:messageHeader($node, $model)
        default return element { $node/name() } 
                               { $node/@*, foo:recurse($node, $model) }

    else () 
};

declare function foo:recurse($node as node()?, $model as map()) as item()* {
    if ($node) 
    then 
        for $cnode in $node/node() 
        return foo:process-node($cnode, $model) 
    else ()
};

declare function foo:messageHeader($node as node(), $model as map()) {
element { $node/name() } 
        { $node/@*, 
          attribute { 'newAtt' } { 'newVal' },
          foo:recurse($node, $model)
        }
};

You then call foo:process-node on the RootNode

0
ScalaWilliam On

You can always re-use the wheel that is there for such things, XSLT. Especially if you want to minimise risk in your code.

eXist supports XSL Transformations and here is an example of how to run an XSLT transformation that does the job that you want:

xquery version "3.0";

declare function local:add-attribute($input as node()?, $attributeName as xs:string, $attributeValue as xs:string?) {
    let $xslt := <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
    <xsl:template xmlns:abc="url1" match="abc:messageHeader">
        <xsl:copy>
            <xsl:apply-templates select="@*"/>
            <xsl:attribute name="{$attributeName}">{$attributeValue}</xsl:attribute>
            <xsl:apply-templates select="node()"/>
        </xsl:copy>
    </xsl:template>
    <xsl:template match="@*|node()">
        <xsl:copy>
            <xsl:apply-templates select="@*|node()"/>
        </xsl:copy>
    </xsl:template>
</xsl:stylesheet>
return transform:transform($input, $xslt, ())
};

let $input := <xyz:RootNode xmlns:abc="url1" xmlns:xyz="url2">
  <abc:OtherNodes/>
  <abc:messageHeader att1="val1" att2="val2">
       <abc:childNodes/>  
  </abc:messageHeader>
  <abc:OtherNodes/>
</xyz:RootNode>

return local:add-attribute($input, "hey", "bam")

You can also make use of the $parameters attribute if you want to bring your XSLT out into its own file. Will make things even more testable and modular.