0

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>
2
  • I think you're trying to run before you can walk. This is an ambitious project to attempt and judging from your difficulty in specifying the task, I suspect it's beyond your skill level. I'm afraid I'm not going to try and help, because I fear I would end up writing and debugging the code myself. Commented Jan 12, 2022 at 8:36
  • Thank you @MichaelKay for the reply. Let me try to see for any solution. Commented Jan 12, 2022 at 10:59

1 Answer 1

1

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>
Sign up to request clarification or add additional context in comments.

2 Comments

Hi @MartinHonnen The first approach where creating two data elements works fine. Regarding the second approach where we need to infer which indices are left out and creating them, could you please help to provide some code fragments so that I make further progress.
@Nijith, I have added a code sample, a bit to convoluted at the moment, but it at least manages to insert stuff in the wanted space by generating the "missing" elements from your path data.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.