Home > Software engineering >  Using XSLT to expand a flattened XML with path delimiters
Using XSLT to expand a flattened XML with path delimiters

Time:05-11

(Edit) As per advice from @michael.hor257k I have also included the verbose form of the input XML.

I have an input XML that I can't control, and need to transform it in XSLT. Please note that unlike many "related answers" on SO, in this case, the "depth" level is not known as an attribute of each item, but needs to be calculated from the path.

Here is an XSLT fiddle of my problem: http://xsltransform.net/6rexjhn/3

Here is the a simplified form of the input (compact form):

<?xml version="1.0" encoding="UTF-8"?>
<data>
    <settings>
        <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">A</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="attribute" level="one_six.size" target="six.size" >large</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>

And here is the verbose form:

<?xml version="1.0" encoding="UTF-8"?>
<data>
    <settings>
        <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">A</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>

The flattening is such that (1) an underscore represents a child element of the target, (2) a period is for the attribute, (3) Max depth isn't known but should be reasonable and (4) Elements that have children have only children elements, no stand-alone values. This is what I would like to get:

<?xml version="1.0" encoding="UTF-8"?>
<template xmlns="http://www.example.org/standards/template/1"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xmlns:ac="http://www.example.org/Standards/abc/1"
          xsi:schemaLocation="http://www.example.org/standards/template.xsd"
          Version="2022-01">
  <one quality="high" weight="10 kg">
    <two>A</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>

Based on this SO answer that also doesn't have a depth attribute, here is what I have tried (using XSLT 1.0). A lot of the data is missing, and I can't figure out how to handle the attributes.

<?xml version="1.0" encoding="UTF-8" ?>
<xsl:transform xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="xml" doctype-public="XSLT-compat" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:strip-space elements="*"/>

<xsl:key name="siblings" match="*[not(self::field)]" use="generate-id(preceding-sibling::field[1])" />
<xsl:key name="nextlevel" match="field" use="generate-id(preceding-sibling::field[@level][starts-with(current(), concat(., '_'))][1])" />

<xsl:template match="/">
    <template xmlns="http://www.example.org/standards/template/1" 
                    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                    xmlns:ac="http://www.example.org/Standards/abc/1"
                    xsi:schemaLocation="http://www.example.org/standards/template.xsd"
                    Version="2022-01">
        <one quality="{//field[@target='one.quality']}" weight="{//field[@target='one.weight']}">
            <xsl:apply-templates select="//field[@level='one']" />
        </one>
    </template>
</xsl:template>

<!-- Fetch elements -->
<xsl:template match="//field[@style='element']">
     <xsl:element name="{@target}">
        <xsl:apply-templates select="key('siblings', generate-id())" />
       <xsl:apply-templates select="key('nextlevel', generate-id())" />
    </xsl:element>
</xsl:template>

<!-- identity copy transform -->
<xsl:template match="@*|node()">
    <xsl:copy>
        <xsl:apply-templates select="@*|node()"/>
    </xsl:copy>
</xsl:template>

</xsl:transform>

This is what I am getting using the XSLT above:

<?xml version="1.0" encoding="UTF-8"?>
<template xmlns="http://www.example.org/standards/template/1"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xmlns:ac="http://www.example.org/Standards/abc/1"
          xsi:schemaLocation="http://www.example.org/standards/template.xsd"
          Version="2022-01">
   <one quality="high" weight="10 kg"/>
</template>

CodePudding user response:

It seems to me that given your "verbose" input, you could achieve the expected result quite easily using:

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']" 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>

<xsl:template match="field[@style='element']">
    <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:template>

<xsl:template match="field[@style='attribute']">
    <xsl:attribute name="{substring-after(@target, '.')}">
        <xsl:value-of select="." />
    </xsl:attribute>
</xsl:template>

</xsl:stylesheet>

Note that there is no hard-coding of any node names, and the hierarchy depth is unlimited.

  • Related