Home > Blockchain >  Clone nodes while expanding flat to hierarchical XML using Microsoft XSLT 1.0
Clone nodes while expanding flat to hierarchical XML using Microsoft XSLT 1.0

Time:05-14

TL;DR: Here is my Fiddle: https://xsltfiddle.liberty-development.net/6qtiBn6/2

Expanding on the answer to my previous XSLT 1.0 (Microsoft vendor) problem, I actually also need to create some repeated fields out of one single node whose values are separated by delimiter "|". I do receive a parameter field count to tell me the number of duplicates of that one node to make. The node is one_two.

   <?xml version="1.0" encoding="UTF-8"?>
    <data>
        <settings>
            <field style="element" level="count" target="param">3</field>
            <field style="element" level="one" target="one"></field>
            <field style="attribute" level="one.quality" target="one.quality">high</field>
            <field style="attribute" level="one.weight" target="one.weight">10 kg</field>
            <field style="element" level="one_two" target="two"></field>
            <field style="attribute" level="one_two.type" target="two.type">a|b|c</field>
            <field style="element" level="one_two_ten" target="ten">alpha|beta|gamma</field>
            <field style="attribute" level="one_two_ten.type" target="ten.type">apple|ball|NULL</field>
            <field style="attribute" level="one_two_ten.age" target="ten.age">baby|young|old</field>
            <field style="element" level="one_three" target="three"></field>
            <field style="attribute" level="one_three.color" target="three.color">black</field>
            <field style="element" level="one_three_four" target="four" >B</field>
            <field style="attribute" level="one_three_four.length" target="four.length">12 cm</field>
            <field style="attribute" level="one_three_four.width" target="four.width"> 7 cm</field>
            <field style="element" level="one_three_five" target="five" >C</field>
            <field style="element" level="one_six" target="six" ></field>
            <field style="attribute" level="one_six.size" target="six.size" >large</field>
            <field style="element" level="one_six_seven" target="seven" ></field>
            <field style="element" level="one_six_seven_eight" target="eight">D</field>
            <field style="element" level="one_nine" target="nine">E</field>
        </settings>
    </data>

Here is the XSLT 1.0 code that I have:

<xsl:stylesheet version="1.0" 
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns="http://www.example.org/standards/template/1" >
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
    
    <xsl:key name="elem" match="field[@style='element']" use="substring-before(@level, @target)" />
    <xsl:key name="attr" match="field[@style='attribute']" use="substring-before(@level, '.')" />
    
    <xsl:variable name="maxcount" select="//field[@level = 'count']"/>

    <xsl:template match="/">
        <template xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.example.org/standards/template.xsd"           xmlns:ac="http://www.example.org/Standards/abc/1" Version="2022-01">
            <xsl:apply-templates select="key('elem', '')" />
        </template>
    </xsl:template>
    
    <xsl:template match="field[@style='element']">
      <xsl:choose>
        <xsl:when test="@level = 'one_two'">
          <xsl:call-template name="multiply">
              <xsl:with-param name="maxCount" select="$maxcount" />
              <xsl:with-param name="target" select="@target" />
              <xsl:with-param name="level" select="@level" />
          </xsl:call-template>
        </xsl:when>
        <xsl:when test="@target != 'param'">
          <xsl:element name="{@target}">
              <xsl:apply-templates select="key('attr', @level)" />
              <xsl:value-of select="." />
              <xsl:apply-templates select="key('elem', concat(@level, '_'))" />
          </xsl:element>
        </xsl:when>
        <xsl:otherwise />
      </xsl:choose>
    </xsl:template>
    
    <xsl:template match="field[@style='attribute']">
        <xsl:attribute name="{substring-after(@target, '.')}">
            <xsl:value-of select="." />
        </xsl:attribute>
    </xsl:template>
    
    <xsl:template name="multiply">
        <xsl:param name="maxCount" />
        <xsl:param name="target" />
        <xsl:param name="level" />
        <xsl:param name="i" select="1" />

        <xsl:choose>
            <xsl:when test="$i &lt;= $maxCount">
                <xsl:element name="{$target}">
                  <xsl:apply-templates select="key('attr', @level)" />
                  <xsl:value-of select="." />
                  <xsl:apply-templates select="key('elem', concat(@level, '_'))" />
                </xsl:element>

                <xsl:call-template name="multiply">
                    <xsl:with-param name="maxCount" select="$maxCount" />
                    <xsl:with-param name="target" select="@target" />
                    <xsl:with-param name="level" select="@level" />
                    <xsl:with-param name="i" select="$i 1" />
                </xsl:call-template>
            </xsl:when>
            <xsl:otherwise />
        </xsl:choose>
    </xsl:template>
</xsl:stylesheet>

And here is the result that I am getting from the above:

<?xml version="1.0" encoding="UTF-8"?>
<template xmlns="http://www.example.org/standards/template/1"
          xmlns:ac="http://www.example.org/Standards/abc/1"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://www.example.org/standards/template.xsd"
          Version="2022-01">
   <one quality="high" weight="10 kg">
      <two type="a|b|c">
         <ten type="apple|ball|NULL" age="baby|young|old">alpha|beta|gamma</ten>
      </two>
      <two type="a|b|c">
         <ten type="apple|ball|NULL" age="baby|young|old">alpha|beta|gamma</ten>
      </two>
      <two type="a|b|c">
         <ten type="apple|ball|NULL" age="baby|young|old">alpha|beta|gamma</ten>
      </two>
      <three color="black">
         <four length="12 cm" width=" 7 cm">B</four>
         <five>C</five>
      </three>
      <six size="large">
         <seven>
            <eight>D</eight>
         </seven>
      </six>
      <nine>E</nine>
   </one>
</template>

I am not totally there yet. i.e. this is what I need: Notice how one_two has expanded into 3 elements i.e. count) and the children take corresponding values split out from the delimited list. Also, the NULL keyword means the attribute for the second duplicate will be skipped (so I will probably want to apply a replacement there - TBD).

<?xml version="1.0" encoding="UTF-8"?>
<template xmlns="http://www.example.org/standards/template/1"
          xmlns:ac="http://www.example.org/Standards/abc/1"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://www.example.org/standards/template.xsd"
          Version="2022-01">
   <one quality="high" weight="10 kg">
      <two type="a">
         <ten type="apple" age="baby">alpha</ten>
      </two>
      <two type="b">
         <ten type="ball" age="young">beta</ten>
      </two>
      <two type="c">
         <ten age="old">gamma</ten>
      </two>
      <three color="black">
         <four length="12 cm" width=" 7 cm">B</four>
         <five>C</five>
      </three>
      <six size="large">
         <seven>
            <eight>D</eight>
         </seven>
      </six>
      <nine>E</nine>
   </one>
</template>

I need only two more things to be done here: (1) I need to modify the multiply template so that each multiplied element picks up the relative part of the substring for the counter $i. i.e. when $1 = 1, we pick the first part, etc. Thinking of how to apply a choose within the loop for the counter i, and then choosing sub-strings of the corresponding item. (2) Note how the ten element (under the repeated element <two type="c">) has no type, because it was NULL in the supplied string. I figure I need a sort of IF statement there.

CodePudding user response:

The format of your input is very inconvenient.

Moreover, there is something very strange in your added requirement: according to your description, the element:

<field style="element" level="count" target="param">3</field>

indicates that some element in the input needs to be replicated 3 times in the output - but it doesn't specify which one. You tell us that the instruction refers to:

<field style="element" level="one_two" target="two"></field>

and its descendant nodes - but this information is not contained in the input itself (perhaps it could be derived from the fact that the string-values of the descendant nodes are delimited strings with 3 tokens each, but that would require a significantly different process).

Anyway, taking advantage of this external knowledge, I think you could adapt the answer from your previous question to:

XSLT 1.0

<xsl:stylesheet version="1.0" 
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns="http://www.example.org/standards/template/1" >
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>

<xsl:key name="elem" match="field[@style='element'][@level!='count']" use="substring-before(@level, @target)" />
<xsl:key name="attr" match="field[@style='attribute']" use="substring-before(@level, '.')" />

<xsl:template match="/">
    <template xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.example.org/standards/template.xsd" xmlns:ac="http://www.example.org/Standards/abc/1" Version="2022-01">
        <xsl:apply-templates select="key('elem', '')" />
    </template>
</xsl:template>

<!-- create element -->
<xsl:template match="field[@style='element']">
    <xsl:param name="N" select="1"/>
    <xsl:element name="{@target}">
        <!-- add attributes -->
        <xsl:apply-templates select="key('attr', @level)" >
            <xsl:with-param name="N" select="$N"/>
        </xsl:apply-templates>
        <!-- add text node -->
        <xsl:call-template name="get-Nth-token">
            <xsl:with-param name="N" select="$N"/>
            <xsl:with-param name="list" select="."/>
        </xsl:call-template>
        <!-- add child elements -->
        <xsl:apply-templates select="key('elem', concat(@level, '_'))" >
            <xsl:with-param name="N" select="$N"/>
        </xsl:apply-templates>
    </xsl:element>
</xsl:template>

<!-- create attribute -->
<xsl:template match="field[@style='attribute']">
    <xsl:param name="N" select="1"/>
    <xsl:attribute name="{substring-after(@target, '.')}">
        <!-- string-value  -->
        <xsl:call-template name="get-Nth-token">
            <xsl:with-param name="N" select="$N"/>
            <xsl:with-param name="list" select="."/>
        </xsl:call-template>
    </xsl:attribute>
</xsl:template>

<!-- handle "special" element  -->
<xsl:template match="field[@level='one_two']" name="special" priority="1">
    <xsl:param name="count" select="../field[@level='count']"/>
    <xsl:if test="$count > 0">
        <xsl:call-template name="special">
            <xsl:with-param name="count" select="$count - 1"/>
        </xsl:call-template>
        <two>
            <xsl:apply-templates select="key('attr', @level) | key('elem', concat(@level, '_'))" >
                <xsl:with-param name="N" select="$count"/>
            </xsl:apply-templates>
        </two>
    </xsl:if>
</xsl:template>

<!-- get Nth token -->
<xsl:template name="get-Nth-token">
    <xsl:param name="list"/>
    <xsl:param name="N"/>
    <xsl:param name="delimiter" select="'|'"/>
    <xsl:choose>
        <xsl:when test="$N = 1">
            <xsl:value-of select="substring-before(concat($list, $delimiter), $delimiter)"/>
        </xsl:when>
        <xsl:when test="contains($list, $delimiter) and $N > 1">
            <!-- recursive call -->
            <xsl:call-template name="get-Nth-token">
                <xsl:with-param name="list" select="substring-after($list, $delimiter)"/>
                <xsl:with-param name="N" select="$N - 1"/>
                <xsl:with-param name="delimiter" select="$delimiter"/>
            </xsl:call-template>
        </xsl:when>
    </xsl:choose>
</xsl:template>

</xsl:stylesheet>

Applied to your input example, this will produce:

Result

<?xml version="1.0" encoding="UTF-8"?>
<template xmlns="http://www.example.org/standards/template/1"
          xmlns:ac="http://www.example.org/Standards/abc/1"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://www.example.org/standards/template.xsd"
          Version="2022-01">
   <one quality="high" weight="10 kg">
      <two type="a">
         <ten type="apple" age="baby">alpha</ten>
      </two>
      <two type="b">
         <ten type="ball" age="young">beta</ten>
      </two>
      <two type="c">
         <ten type="NULL" age="old">gamma</ten>
      </two>
      <three color="black">
         <four length="12 cm" width=" 7 cm">B</four>
         <five>C</five>
      </three>
      <six size="large">
         <seven>
            <eight>D</eight>
         </seven>
      </six>
      <nine>E</nine>
   </one>
</template>

which I believe is the expected result, with one minor exception.

To avoid creating attributes or elements with the string-value of "NULL", place the returned token in a variable first; then create the node only if $token != 'NULL'.

  • Related