Search code examples
xmlxsltxsdxslt-2.0xslt-3.0

Adding XML nodes to an existing XML using XPATH


I have a requirement to add new XML nodes to an existing XML using XPATH.

Existing XML:

<create>
   <article>
      <identifier>Test</identifier>
   </article>
</create>

XSLT Used (Got if from another thread in stackoverflow):

<xsl:stylesheet version="2.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
 xmlns:xs="http://www.w3.org/2001/XMLSchema"
 xmlns:my="my:my">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>

 <xsl:variable name="vPop" as="element()*">
    <item path="/create/article[1]/id">1</item>
    <item path="/create/article[1]/description">bar</item>
    <item path="/create/article[1]/name[1]">foo</item>
    <item path="/create/article[1]/price[1]/amount">00.00</item>
    <item path="/create/article[1]/price[1]/currency">USD</item>
    <item path="/create/article[1]/price[2]/amount">11.11</item>
    <item path="/create/article[1]/price[2]/currency">AUD</item>
    <item path="/create/article[2]/id">2</item>
    <item path="/create/article[2]/description">some name</item>
    <item path="/create/article[2]/name[1]">some description</item>
    <item path="/create/article[2]/price[1]/amount">00.01</item>
    <item path="/create/article[2]/price[1]/currency">USD</item>
 </xsl:variable>

 <xsl:template match="/">
  <xsl:sequence select="my:subTree($vPop/@path/concat(.,'/',string(..)))"/>
 </xsl:template>

 <xsl:function name="my:subTree" as="node()*">
  <xsl:param name="pPaths" as="xs:string*"/>

  <xsl:for-each-group select="$pPaths"
    group-adjacent=
        "substring-before(substring-after(concat(., '/'), '/'), '/')">
    <xsl:if test="current-grouping-key()">
     <xsl:choose>
       <xsl:when test=
          "substring-after(current-group()[1], current-grouping-key())">
         <xsl:element name=
           "{substring-before(concat(current-grouping-key(), '['), '[')}">

          <xsl:sequence select=
            "my:subTree(for $s in current-group()
                         return
                            concat('/',substring-after(substring($s, 2),'/'))
                             )
            "/>
        </xsl:element>
       </xsl:when>
       <xsl:otherwise>
        <xsl:value-of select="current-grouping-key()"/>
       </xsl:otherwise>
     </xsl:choose>
     </xsl:if>
  </xsl:for-each-group>
 </xsl:function>
</xsl:stylesheet>

Using above XSLT the XPATHs are converted to XML nodes. But the issue is existing XML nodes (<identifier>Test</identifier>) are lost. Kindly help to fix this issue. This is a sample XML and the actual XML contains many existing XML nodes.

Actual results:

<create>
   <article>
      <id>1</id>
      <description>bar</description>
      <name>foo</name>
      <price>
         <amount>00.00</amount>
         <currency>USD</currency>
      </price>
      <price>
         <amount>11.11</amount>
         <currency>AUD</currency>
      </price>
   </article>
   <article>
      <id>2</id>
      <description>some name</description>
      <name>some description</name>
      <price>
         <amount>00.01</amount>
         <currency>USD</currency>
      </price>
   </article>
</create>

Expected result:

<create>
   <article>
      <identifier>Test</identifier>
      <id>1</id>
      <description>bar</description>
      <name>foo</name>
      <price>
         <amount>00.00</amount>
         <currency>USD</currency>
      </price>
      <price>
         <amount>11.11</amount>
         <currency>AUD</currency>
      </price>
   </article>
   <article>
      <id>2</id>
      <description>some name</description>
      <name>some description</name>
      <price>
         <amount>00.01</amount>
         <currency>USD</currency>
      </price>
   </article>
</create>

Solution

  • This is a rough outline of the create the new nodes based on your existing function and merge them with the input document approach:

    <?xml version="1.0" encoding="utf-8"?>
    <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
      version="3.0"
      xmlns:xs="http://www.w3.org/2001/XMLSchema"
      exclude-result-prefixes="#all"
      xmlns:my="http://example.com/my-functions"
      expand-text="yes">
      
    <xsl:variable name="vPop" as="element()*">
        <item path="/create/article[1]/id">1</item>
        <item path="/create/article[1]/description">bar</item>
        <item path="/create/article[1]/name[1]">foo</item>
        <item path="/create/article[1]/price[1]/amount">00.00</item>
        <item path="/create/article[1]/price[1]/currency">USD</item>
        <item path="/create/article[1]/price[2]/amount">11.11</item>
        <item path="/create/article[1]/price[2]/currency">AUD</item>
        <item path="/create/article[2]/id">2</item>
        <item path="/create/article[2]/description">some name</item>
        <item path="/create/article[2]/name[1]">some description</item>
        <item path="/create/article[2]/price[1]/amount">00.01</item>
        <item path="/create/article[2]/price[1]/currency">USD</item>
     </xsl:variable>
     
     <xsl:variable name="new-nodes">
       <xsl:sequence select="my:subTree($vPop/@path/concat(.,'/',string(..)))"/>
     </xsl:variable>
    
      <xsl:output method="xml" indent="yes"/>
      <xsl:strip-space elements="*"/>
        
      <xsl:template match="/" name="xsl:initial-template">
        <xsl:sequence select="my:merge(*, $new-nodes/*)"/>
      </xsl:template>
    
    
     <xsl:function name="my:merge" as="node()*">
       <xsl:param name="node1" as="node()*"/>
       <xsl:param name="node2" as="node()*"/>
       <xsl:for-each-group select="$node1, $node2" group-by="path()">
         <xsl:copy>
           <xsl:sequence select="my:merge(@*, current-group()[2]/@*)"/>
           <xsl:sequence select="my:merge(node(), current-group()[2]/node())"/>
         </xsl:copy>
       </xsl:for-each-group>
     </xsl:function>
    
     <xsl:function name="my:subTree" as="node()*">
       
       
      <xsl:param name="pPaths" as="xs:string*"/>
    
      <xsl:for-each-group select="$pPaths"
        group-adjacent=
            "substring-before(substring-after(concat(., '/'), '/'), '/')">
        <xsl:if test="current-grouping-key()">
         <xsl:choose>
           <xsl:when test=
              "substring-after(current-group()[1], current-grouping-key())">
             <xsl:element name=
               "{substring-before(concat(current-grouping-key(), '['), '[')}">
    
              <xsl:sequence select=
                "my:subTree(for $s in current-group()
                             return
                                concat('/',substring-after(substring($s, 2),'/'))
                                 )
                "/>
            </xsl:element>
           </xsl:when>
           <xsl:otherwise>
            <xsl:value-of select="current-grouping-key()"/>
           </xsl:otherwise>
         </xsl:choose>
         </xsl:if>
      </xsl:for-each-group>
     </xsl:function>
    </xsl:stylesheet>
    

    But most merging requirements are way more complex than that simple my:merge I have shown there, so you would need to refine that function.