Search code examples
xmlxslt

Dot separated attribute values to nested XML elements


Given a source XML instance as follows:

source.xml

<?xml version="1.0" encoding="UTF-8"?>
<data>
  <elements>
    <element path="a." name="Aname"/>
    <element path="a.b." parent="a." name="Bname"/>
    <element path="a.c." parent="a." name="Cname"/>
    <element path="a.c.d." parent="a.c." name="Dname"/>
    <element path="a.c.d.e." parent="a.c.d." name="Ename"/>
    <element path="a.f." parent="a." name="Fname"/>
    <element path="a.f.g." parent="a.f." name="Gname"/>
    <element path="a.f.g.h." parent="a.f.g." name="Hname"/>
    <element path="a.f.g.h.i." parent="a.f.g.h." name="Iname"/>
    <element path="a.f.g.h.i.j." parent="a.f.g.h.i." name="Jname"/>
    <element path="a.f.g.k." parent="a.f.g." name="Kname"/>
    <element path="a.f.g.l." parent="a.f.g." name="Lname"/>
    <element path="a.f.m." parent="a.f." name="Mname"/>
  </elements>
</data>

How can the following resultant XML be achieved via an XSLT transformation:

result.xml

<?xml version="1.0" encoding="UTF-8"?>
<map>
  <node TEXT="Aname">
    <node TEXT="Bname"/>
    <node TEXT="Cname">
      <node TEXT="Dname">
        <node TEXT="Ename"/>
      </node>
    </node>
    <node TEXT="Fname">
      <node TEXT="Gname">
        <node TEXT="Hname">
          <node TEXT="Iname">
            <node TEXT="Jname"/>
          </node>
        </node>
        <node TEXT="Kname"/>
        <node TEXT="Lname"/>
      </node>
      <node TEXT="Mname"/>
    </node>
  </node>
</map>

As you can see in the source XML every element XML element has an associated path attribute with a dot separated string value. Every element in the source XML also has a parent attribute with a dot separated string value.

Can the values of both these attributes (path and parent) somehow be utilized to achieve the desired nesting of node elements as shown in the resultant XML ?

Below is just one example of my many failed XSLT attempts. All attempts I've tried fail to achieve the desired nesting of node elements - the resultant XML always remains flat.

XSLT

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    exclude-result-prefixes="xs"
    version="2.0">

  <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
  <xsl:strip-space elements="*"/>

  <xsl:template match="data">
    <map>
      <xsl:apply-templates select="@*|node()"/>
    </map>
  </xsl:template>

  <xsl:template match="element">
    <xsl:for-each select=".">
      <node>
        <xsl:attribute name="TEXT" select="@name"/>
      </node>
    </xsl:for-each>
  </xsl:template>

</xsl:stylesheet>

Solution

  • This can be very easy it you use the correct tool for resolving cross-references:

    XSLT 1.0

    <xsl:stylesheet version="1.0" 
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
    
    <xsl:key name="child" match="element" use="@parent" />
    
    <xsl:template match="/data">
        <map>
            <xsl:apply-templates select="elements/element[not(@parent)]"/>
        </map>
    </xsl:template>
    
    <xsl:template match="element">
        <node TEXT="{@name}">
            <xsl:apply-templates select="key('child', @path)"/>
        </node>
    </xsl:template>
    
    </xsl:stylesheet>