Question

I have a requirement to transform a sequential XML node list into a hierarchical, but I run into some XSLT specific knowledge gap. The input XML contains articles, colors and sizes. In the sample below 'Record1' is an article, 'Record2' represents a color and 'Record3' are the sizes. The number of colors and sizes (record2 and record3) elements can vary.

<root>
 <Record1>...</Record1>
 <Record2>...</Record2>
 <Record3>...</Record3>
 <Record3>...</Record3>
 <Record2>...</Record2>
 <Record3>...</Record3>
 <Record3>...</Record3>
 <Record3>...</Record3>
 <Record3>...</Record3>
 <Record1>...</Record1>
 <Record2>...</Record2>
 <Record3>...</Record3>
 <Record3>...</Record3>
 <Record2>...</Record2>
 <Record3>...</Record3>
 <Record3>...</Record3>
 <Record3>...</Record3>
 <Record3>...</Record3>
</root> 

All fields are on the same hierarchical level, but still I have to create this structure as output:

<root>
 <article>              -> Record1
  <color>               -> Record2
   <size>...</size>     -> Record3
   <size>...</size>     -> Record3
  </color>
  <color>               -> Record2
   <size>...</size>     -> Record3
   <size>...</size>     -> Record3
   <size>...</size>     -> Record3
   <size>...</size>     -> Record3
  </color>
 </article>
 <article>              -> Record1
  <color>               -> Record2
   <size>...</size>     -> Record3
   <size>...</size>     -> Record3
  </color>
  <color>               -> Record2
   <size>...</size>     -> Record3
   <size>...</size>     -> Record3
   <size>...</size>     -> Record3
   <size>...</size>     -> Record3
  </color>
 </article>
</root>

I've tried to iterate the nodes sequentially but for example the 'article' (=record1) node tag needs to remain unclosed while 'color' (=record2) nodes are processed. The same counts for processing 'size' (=record3) having 'color' unclosed, but that is not allowed by XSLT. My next idea was to call a template for every article, color and size level, but I don't know how to select for example all 'record3' nodes between the current 'record2' and the next article represented by 'record1'.

I've also got a limitation on the XSLT version because I need this transformation in BizTalk Server which only supports XSLT 1.0

Can someone push me in the right direction?

Was it helpful?

Solution

Here's one XSLT 1.0 option. I'm not sure what you wanted to do with the values of Record1 and Record2, so I put them in a val attribute.

XML Input

<root>
    <Record1>article1</Record1>
    <Record2>color1</Record2>
    <Record3>size1</Record3>
    <Record3>size2</Record3>
    <Record2>color2</Record2>
    <Record3>size3</Record3>
    <Record3>size4</Record3>
    <Record3>size5</Record3>
    <Record3>size6</Record3>
    <Record1>article2</Record1>
    <Record2>color3</Record2>
    <Record3>size7</Record3>
    <Record3>size8</Record3>
    <Record2>color4</Record2>
    <Record3>size9</Record3>
    <Record3>size10</Record3>
    <Record3>size11</Record3>
    <Record3>size12</Record3>
</root>

XSLT 1.0

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output indent="yes"/>
    <xsl:strip-space elements="*"/>

    <xsl:template match="/*">
        <xsl:copy>
            <xsl:apply-templates select="Record1"/>
        </xsl:copy>
    </xsl:template>

    <xsl:template match="Record1">
        <article val="{.}">
            <xsl:apply-templates select="following-sibling::Record2[generate-id(preceding-sibling::Record1[1])=generate-id(current())]"/>
        </article>
    </xsl:template>

    <xsl:template match="Record2">
        <color val="{.}">
            <xsl:apply-templates select="following-sibling::Record3[generate-id(preceding-sibling::Record2[1])=generate-id(current())]"/>
        </color>
    </xsl:template>

    <xsl:template match="Record3">
        <size>
            <xsl:value-of select="."/>
        </size>
    </xsl:template>

</xsl:stylesheet>

XML Output

<root>
   <article val="article1">
      <color val="color1">
         <size>size1</size>
         <size>size2</size>
      </color>
      <color val="color2">
         <size>size3</size>
         <size>size4</size>
         <size>size5</size>
         <size>size6</size>
      </color>
   </article>
   <article val="article2">
      <color val="color3">
         <size>size7</size>
         <size>size8</size>
      </color>
      <color val="color4">
         <size>size9</size>
         <size>size10</size>
         <size>size11</size>
         <size>size12</size>
      </color>
   </article>
</root>

OTHER TIPS

The solutions from DevNull and Novatchev are both viable. A third solution, and the one that seems to me most elegant, is to use "sibling recursion". I won't give the code in full, but you start off with a template rule for the root like this:

<xsl:template match="root">
  <xsl:apply-templates select="*[1]"/>
</xsl:template>

and then have template rules for the different "levels" that look something like this:

<xsl:template match="record1">
  <section1>
    <xsl:apply-templates select="following-sibling::*[1][self::record2]"/>
  </section1>
  <xsl:apply-templates select="following-sibling::record1[1]"/>
</xsl:template>

and similarly for each level. This solution is probably faster and more concise than the others, though it can be a bit "mind-bending" until you get used to it. The key to understanding it is that at each level, you first process the first "logical child" - that is, the next element in the flat sequence, provided it's at a deeper level than the current element; and then you process the next "logical sibling" - the next element in the flat sequence that's at the same level as the current one. The solution can be adapted, of course, for cases where there may be missing levels or where the level number is indicated by an attribute rather than by the element name. It can even be adapted for the case where the number of levels is not known in advance: you simply use a single rule in which the selection of "first logical child" and "next logical sibling" are suitably parameterized.

Here is a simpler and likely more efficient transformation, using keys:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>
 <xsl:strip-space elements="*"/>

 <xsl:key name="kColors" match="Record2"
          use="generate-id(preceding-sibling::Record1[1])"/>

 <xsl:key name="kSizes" match="Record3"
          use="generate-id(preceding-sibling::Record2[1])"/>

 <xsl:template match="/*">
  <root>
    <xsl:apply-templates select="Record1"/>
  </root>
 </xsl:template>

 <xsl:template match="Record1">
   <article>
     <xsl:apply-templates select="key('kColors', generate-id())"/>
   </article>
 </xsl:template>

 <xsl:template match="Record2">
  <color>
     <xsl:apply-templates select="key('kSizes', generate-id())"/>
  </color>
 </xsl:template>

 <xsl:template match="Record3">
  <size><xsl:apply-templates/></size>
 </xsl:template>
</xsl:stylesheet>

When this transformation is applied on the provided XML document:

<root>
    <Record1>article1</Record1>
    <Record2>color1</Record2>
    <Record3>size1</Record3>
    <Record3>size2</Record3>
    <Record2>color2</Record2>
    <Record3>size3</Record3>
    <Record3>size4</Record3>
    <Record3>size5</Record3>
    <Record3>size6</Record3>
    <Record1>article2</Record1>
    <Record2>color3</Record2>
    <Record3>size7</Record3>
    <Record3>size8</Record3>
    <Record2>color4</Record2>
    <Record3>size9</Record3>
    <Record3>size10</Record3>
    <Record3>size11</Record3>
    <Record3>size12</Record3>
</root>

the wanted, correct result is produced:

<root>
   <article>
      <color>
         <size>size1</size>
         <size>size2</size>
      </color>
      <color>
         <size>size3</size>
         <size>size4</size>
         <size>size5</size>
         <size>size6</size>
      </color>
   </article>
   <article>
      <color>
         <size>size7</size>
         <size>size8</size>
      </color>
      <color>
         <size>size9</size>
         <size>size10</size>
         <size>size11</size>
         <size>size12</size>
      </color>
   </article>
</root>

Explanation:

  1. The key named kColors expresses the set of Record2 (color) elements as a function of their Record1 (article) logical parent.

  2. Similarly, the key named kSizes expresses the set of Record3 (size) elements as a function of their Record2 (color) logical parent.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top