3

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

4
  • Table? Column? They are not standard XML words. You are translating to what? XHTML? Commented Apr 20, 2009 at 15:17
  • @bortzmeyer: I think an (X)HTML table is what the OP is after. Commented Apr 20, 2009 at 16:01
  • I think you should not accept my answer just yet. I am sure if you leave the question open, better alternatives will show up. Commented Apr 20, 2009 at 16:05
  • See the solution I posted: it uses no recursion and hi-lights a few useful XSLT techniques such as Muenchian grouping, keys, finding maximum and iterating without recursion. Commented Apr 20, 2009 at 17:03

2 Answers 2

3

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">
    <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.

Sign up to request clarification or add additional context in comments.

6 Comments

You solution does not currently produce the empty table cells as they are required to make a valid HTML table. :)
@Tomalak Thank you for this observation. Corrected now!
There is still a small error. Now your solution produces surplus and incorrectly nested "<td>"s.
SOrry, I have been doing thousands of other things these days. Hope that now there are no more problems.
+1 for the now working solution. I've again leaned a few things. :)
|
1

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.

1 Comment

Nevertheless you should wait with accepting the solution. Leave the question open overnight to attract some more responses.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.