tags:

views:

996

answers:

2

How would I take this xml and create a table with a column for each "section" element and then display all the "document" elements in that column using xslt?

<Documents>
  <Section>
  <SectionName>Green</SectionName>
    <Document>
      <FileName>Tier 1 Schedules</FileName>     
    </Document>
    <Document>
      <FileName>Tier 3 Schedules</FileName>      
    </Document>
    <Document>
      <FileName>Setback Schedule</FileName>    
    </Document>
    <Document>
      <FileName>Tier 2 Governance</FileName>    
    </Document>
 </Section>
 <Section>
 <SectionName>MRO/Refurb</SectionName>
   <Document>
     <FileName>Tier 2 Governance</FileName>    
   </Document>
 </Section>

Thanks, Al

+1  A: 

This is one possible solution:

<xsl:variable name="vCountRows">
  <xsl:apply-templates select="Documents/Section[1]" mode="findmax" />
</xsl:variable>

<xsl:variable name="vCountCols" select="count(Documents/Section)" />

<xsl:template match="/Documents">
  <table r="{$vCountRows}" s="{$vCountCols}">
    <thead>
      <xsl:call-template name="create-thead" />
    </thead>
    <tbody>
      <xsl:call-template name="create-tr" />
    </tbody>
  </table>
</xsl:template>

<xsl:template name="create-thead">
   <tr>
    <xsl:apply-templates select="Section" />
  </tr>    
</xsl:template>

<xsl:template match="Section">
  <th><xsl:value-of select="SectionName" /></th>
</xsl:template>

<xsl:template name="create-tr">
  <xsl:param name="row" select="1" />

  <tr>
    <xsl:call-template name="create-td">
      <xsl:with-param name="row" select="$row" />
    </xsl:call-template>
  </tr>

  <xsl:if test="$row &lt; $vCountRows">
    <xsl:call-template name="create-tr">
      <xsl:with-param name="row" select="$row + 1" />
    </xsl:call-template>
  </xsl:if>

</xsl:template>

<xsl:template name="create-td">
  <xsl:param name="col" select="1" />
  <xsl:param name="row" select="1" />

  <td>
    <xsl:value-of select="Section[$col]/Document[$row]/FileName" />
  </td>

  <xsl:if test="$col &lt; $vCountCols">
    <xsl:call-template name="create-td">
      <xsl:with-param name="col" select="$col + 1" />
      <xsl:with-param name="row" select="$row" />
    </xsl:call-template>
  </xsl:if>
</xsl:template>

<xsl:template match="Section" mode="findmax">
  <xsl:variable name="c" select="count(Document)" />
  <xsl:variable name="next" select="following-sibling::Section[count(Document) &gt; $c][1]" />

  <xsl:choose>    
    <xsl:when test="$next">
      <xsl:apply-templates select="$next" mode="findmax" />
    </xsl:when>
    <xsl:otherwise>
      <xsl:value-of select="$c" />
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>

With your input it produces:

<table>
  <thead>
    <tr>
      <td>Green</td>
      <td>MRO/Refurb</td>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Tier 1 Schedules</td>
      <td>Tier 2 Governance</td>
    </tr>
    <tr>
      <td>Tier 3 Schedules</td>
      <td></td>
    </tr>
    <tr>
      <td>Setback Schedule</td>
      <td></td>
    </tr>
    <tr>
      <td>Tier 2 Governance</td>
      <td></td>
    </tr>
  </tbody>
</table>

The general apporach goes like this:

  1. Find out how many rows we are going to get (this happens in <xsl:template match="Section" mode="findmax">, which recursively finds the section with the maximum number of <Document> nodes
  2. Find out how many columns we are going to get (by counting the number of <Section>s)
  3. calling a templates that creates a <tr>, and keeps calling itself until all necessary rows have been created
  4. in this template, a second template is called, this one creates the <td>s. It keeps calling itself until it reaches the max number of columns (from step 2)

The algorithm:

  • uses incrementing column- and row-numbers as indexes
  • uses recursion to achieve one-by-one index increments (since actually incrementing a variable is impossible in XSLT)
  • creates the right number of empty cells for sections that have less documents

A more efficient version (probably using <xsl:key>s) exists, I'll look into optimizing mine a little more.

Tomalak
worked perfectly thanks
Nevertheless you should wait with accepting the solution. Leave the question open overnight to attract some more responses.
Tomalak
I posted my solution -- no recursion used.
Dimitre Novatchev
Thank you for this observation. Corrected now!
Dimitre Novatchev
Corrected after your second comment. Sorry, I've been doing thousands of things lately and it is difficult to focus on minor details in such situation.
Dimitre Novatchev
+2  A: 

This solution uses no recursion and hi-lights a few useful XSLT techniques such as Muenchian grouping, keys, finding maximum and iterating without recursion.

This transformation:

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;
    <xsl:output omit-xml-declaration="yes" indent="yes"/>

    <xsl:strip-space elements="*"/>

    <xsl:key name="kSectsByValue" match="SectionName"
         use="."/>

    <xsl:key name="kDocBySect" match="Document"
         use="../SectionName"/>

    <xsl:variable name="vCols" select=
       "/*/*/SectionName
                [generate-id()
                =          
                 generate-id(key('kSectsByValue',.)[1])
                 ]"/>

    <xsl:variable name="vMaxRows">
             <xsl:for-each select="$vCols">
               <xsl:sort data-type="number" order="descending"
                    select="count(key('kDocBySect', .))"      />
               <xsl:if test="position() = 1">
                 <xsl:value-of select="count(key('kDocBySect', .))"/>
               </xsl:if>
             </xsl:for-each>
    </xsl:variable>

    <xsl:template match="/">
             <table>
               <tr>
                 <xsl:apply-templates select="$vCols"/>
               </tr>

               <xsl:for-each select=
                 "(/*/*/Document)[not(position() > $vMaxRows)]">                   
                 <tr>

                   <xsl:variable name="vPos" select="position()"/>

                   <xsl:for-each select="$vCols">
                     <td>
                       <xsl:value-of select=
                           "../Document[$vPos]/FileName"/>
                     </td>
                   </xsl:for-each>

                 </tr>
              </xsl:for-each>
            </table>

    </xsl:template>

    <xsl:template match="SectionName">
            <td>
              <xsl:value-of select="." />
            </td>   
    </xsl:template>
</xsl:stylesheet>

when applied on the original XML document (corrected to be well-formed):

<Documents>
    <Section>
     <SectionName>Green</SectionName>
     <Document>
      <FileName>Tier 1 Schedules</FileName>
     </Document>
     <Document>
      <FileName>Tier 3 Schedules</FileName>
     </Document>
     <Document>
      <FileName>Setback Schedule</FileName>
     </Document>
     <Document>
      <FileName>Tier 2 Governance</FileName>
     </Document>
    </Section>
    <Section>
     <SectionName>MRO/Refurb</SectionName>
     <Document>
      <FileName>Tier 2 Governance</FileName>
     </Document>
    </Section>
</Documents>

produces the desired result:

<table>
   <tr>
      <td>Green</td>
      <td>MRO/Refurb</td>
   </tr>
   <tr>
      <td>Tier 1 Schedules</td>
      <td>Tier 2 Governance</td>
   </tr>
   <tr>
      <td>Tier 3 Schedules</td>
      <td/>
   </tr>
   <tr>
      <td>Setback Schedule</td>
      <td/>
   </tr>
   <tr>
      <td>Tier 2 Governance</td>
      <td/>
   </tr>
</table>

Do note:

  1. We use the Muenchian method for grouping in order to find all different column names, not relying that in the XML document they will be unique.

  2. Keys are used both for the Muenchian grouping and for finding all items belonging to a column.

  3. The maximum number of rows is found and kept in the variable $vMaxRows

  4. We iterate N times to produce the N rows of the table -- not using recursion!

  5. The N-th row is output by applying templates to all column items that have position N in their column.

Dimitre Novatchev
You solution does not currently produce the empty table cells as they are required to make a valid HTML table. :)
Tomalak
@Tomalak Thank you for this observation. Corrected now!
Dimitre Novatchev
There is still a small error. Now your solution produces surplus and incorrectly nested "<td>"s.
Tomalak
SOrry, I have been doing thousands of other things these days. Hope that now there are no more problems.
Dimitre Novatchev
+1 for the now working solution. I've again leaned a few things. :)
Tomalak
What is the reason to use "not(position() > $vMaxRows)" instead of "position() <= $vMaxRows"? Personal preference? Aesthetics? Legibility? Or something else?
Tomalak
@Tomalak Personal preference for readability
Dimitre Novatchev