Search code examples
xmlxsltxpathxslt-2.0xslt-3.0

Creating XML nodes from XPATH and merging that to existing XML


I have a requirement to create XML nodes from XPATH and merge that to the existing XML. I am facing an issue where even if I am specifying the newly generated xml nodes from xpath should be in a specific position of an array it is still coming as the top element of that array. Kindly help to address this issue.

Input XML (here data is an array):

<?xml version="1.0" encoding="UTF-8"?>
<header>
  <identifier>12345</identifier>
</header>
<data>
  <txCtry>SG</txCtry>
</data>
<data>
  <txCtry>TH</txCtry>
</data>
<data>
  <txCtry>MY</txCtry>
</data>

XSLT:

<?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:output omit-xml-declaration="yes" />
<xsl:variable name="vPop" as="element()*">
<item path="/data[2]/txCurr">MYD</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-by=
        "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>

output XML (Here txCurr as per the XSLT should have been created and added to 2nd position of data array but got added in 0th position):

<header>
   <identifier>12345</identifier>
</header>
<data>
   <txCtry>SG</txCtry>
   <txCurr>MYD</txCurr>
</data>
<data>
   <txCtry>TH</txCtry>
</data>
<data>
   <txCtry>MY</txCtry>
</data>

Solution

  • What the code does, is, it first creates some XML fragment from that VPop variable and the result from that, for your given sample data, is simply <data<txCurr>MYD</txCurr</data>, i.e. a single data element with a single txtCurr element. The next step then merges the XML fragment with the input fragment, based on the XPaths the XPath 3.1 path function gives. So the information that you might have wanted the second data element is already gone after that first step, somehow it expects you to ensure your "input" paths specify and therefore create two data elements (e.g. <xsl:variable name="vPop" as="element()*"><item path="/data[1]"/><item path="/data[2]/txCurr">MYD</item></xsl:variable>), otherwise the whole approach can't work.

    Or the first step would need to be rewritten not only to break up and create elements based on names but also to try to infer which indices are left out/missing and also create them, something that is (even more) complex than the current approach; here is a basic, and admittedly, currently rather convoluted 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"
      xmlns:map="http://www.w3.org/2005/xpath-functions/map"
      xmlns:array="http://www.w3.org/2005/xpath-functions/array" 
      exclude-result-prefixes="#all"
      xmlns:mf="http://example.com/mf"
      expand-text="yes">
    
      <xsl:variable name="vPop" as="element()*">
       <item path="/data[2]/txCurr">MYD</item>
     </xsl:variable>
     
     <xsl:variable name="new-nodes">
       <xsl:sequence select="mf:generate-nodes($vPop ! map:entry(@path!string(), string(.)))"/>
     </xsl:variable>
    
      <xsl:output method="xml" indent="yes"/>
      <xsl:strip-space elements="*"/>
        
      <xsl:template match="/" name="xsl:initial-template">
        <xsl:sequence select="mf:merge($main-input/*, $new-nodes/*)"/>
        <xsl:comment>Run with {system-property('xsl:product-name')} {system-property('xsl:product-version')} {system-property('Q{http://saxon.sf.net/}platform')}</xsl:comment>
      </xsl:template>
    
    
     <xsl:function name="mf: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="mf:merge(@*, current-group()[2]/@*)"/>
           <xsl:sequence select="mf:merge(node(), current-group()[2]/node())"/>
         </xsl:copy>
       </xsl:for-each-group>
     </xsl:function>  
    
    
      <xsl:output method="xml" indent="yes" />
    
      <xsl:mode on-no-match="shallow-copy"/>
    
      <xsl:param name="main-input" as="document-node()" select="parse-xml-fragment($main-fragment)"/>
      
      <xsl:param name="main-fragment" as="xs:string"><![CDATA[<header>
      <identifier>12345</identifier>
    </header>
    <data>
      <txCtry>SG</txCtry>
    </data>
    <data>
      <txCtry>TH</txCtry>
    </data>
    <data>
      <txCtry>MY</txCtry>
    </data>]]></xsl:param>
    
      <xsl:function name="mf:generate-nodes" as="node()*" visibility="public">
            <xsl:param name="xpath-values" as="map(xs:string, item()*)*"/>
            <xsl:for-each-group select="$xpath-values" group-adjacent="
                    let $first-step := replace(map:keys(.), '^/?([^/]+)(.*$)', '$1'),
                        $exp := replace($first-step, '\[[0-9]+\]$', '')
                    return
                        $exp">
                <xsl:choose>
                    <xsl:when test="current-grouping-key() = ''">
                        <xsl:choose>
                            <xsl:when test="?* instance of node()+">
                                <xsl:sequence select="?*"/>
                            </xsl:when>
                            <xsl:otherwise>
                                <xsl:value-of select="?*"/>
                            </xsl:otherwise>
                        </xsl:choose>
                    </xsl:when>
                    <xsl:otherwise>
                        <xsl:iterate select="
                                1 to max(current-group() !
                                (let $key := map:keys(.),
                                    $first-step := replace($key, '^/?([^/]+)(.*$)', '$1'),
                                    $pos := if (not(ends-with($first-step, ']'))) then
                                        1
                                    else
                                        replace($first-step, '^[^\[]+(\[([0-9]+)\])$', '$2') ! xs:integer(.)
                                return
                                    $pos))">
                            <xsl:variable name="exp" select="current-grouping-key()"/>
                            <xsl:variable name="step" as="xs:string*" select="
                                    if (. eq 1) then
                                        current-grouping-key()
                                    else
                                        (), current-grouping-key() || '[' || . || ']'"/>                      
                            <xsl:variable name="current-grouping-steps"
                                select="current-group()[map:keys(.) ! tokenize(., '/')[normalize-space()][1][. = $step]]"/>
                            <xsl:choose>
                                <xsl:when test="not($exp = '') and not(exists($current-grouping-steps))">                               
                                    <xsl:choose>
                                        <xsl:when test="starts-with($exp, 'comment()')">
                                            <xsl:comment/>
                                        </xsl:when>
                                        <xsl:when test="starts-with($exp, 'processing-instruction(')">
                                            <xsl:processing-instruction name="{replace($exp, '^processing-instruction\(([''&quot;]?)([^''&quot;]+)[&quot;'']?\)$', '$2')}"/>
                                        </xsl:when>
                                        <xsl:when test="starts-with($exp, '@')">
                                            <xsl:attribute name="{substring($exp, 2)}"/>
                                        </xsl:when>
                                        <xsl:when test="$exp">
                                            <xsl:element name="{$exp}"/>
                                        </xsl:when>
                                    </xsl:choose>
                                </xsl:when>
                                <xsl:otherwise>
                                    <xsl:for-each-group select="$current-grouping-steps"
                                        group-by="replace(map:keys(.), '^/?([^/]+)(.*$)', '$1')">
                                        <xsl:variable name="name" as="xs:string"
                                            select="replace(current-grouping-key(), '\[[0-9]+\]$', '')"/>
                                        <xsl:choose>
                                            <xsl:when test="starts-with($name, 'comment()')">
                                                <xsl:comment select="?*"/>
                                            </xsl:when>
                                            <xsl:when
                                                test="starts-with($name, 'processing-instruction(')">
                                                <xsl:processing-instruction name="{replace($name, '^processing-instruction\(([''&quot;]?)([^''&quot;]+)[&quot;'']?\)$', '$2')}" select="?*"/>
                                            </xsl:when>
                                            <xsl:when test="starts-with($name, '@')">
                                                <xsl:attribute name="{substring($name, 2)}" select="?*"
                                                />
                                            </xsl:when>
                                            <xsl:when test="$name">
                                                <xsl:element name="{$name}">
                                                    <xsl:sequence
                                                      select="mf:generate-nodes(current-group() ! map:entry(map:keys(.) ! replace(., '^/?[^/]+(.*)', '$1'), ?*))"
                                                    />
                                                </xsl:element>
                                            </xsl:when>
                                            <xsl:otherwise>
                                                <xsl:choose>
                                                    <xsl:when test="?* instance of node()+">
                                                      <xsl:sequence select="?*"/>
                                                    </xsl:when>
                                                    <xsl:otherwise>
                                                      <xsl:value-of select="?*"/>
                                                    </xsl:otherwise>
                                                </xsl:choose>
                                            </xsl:otherwise>
                                        </xsl:choose>
                                    </xsl:for-each-group>
                                </xsl:otherwise>
                            </xsl:choose>
                        </xsl:iterate>
                    </xsl:otherwise>
                </xsl:choose>
            </xsl:for-each-group>
        </xsl:function>
    </xsl:stylesheet>