XSLT – Adding Elements dynamically

elementxslt

I've a need to display certain XML content in tabular form (XSL-FO for pdf reports),
and not all the columns to displayed are present in source XML. So, I was wondering
if there is a way to transform source XML by embedding additional columns based on
certain element values, and then process the resulting XML to display content?

As an example, for source data:

<projectteam>
  <member>
    <name>John Doe</name>
    <role>dev</role>
 <hrs>100</hrs>
  </member>
  <member>
    <name>Paul Coder</name>
    <role>dev</role>
 <hrs>40</hrs>
  </member>
  <member>
    <name>Henry Tester</name>
    <role>qa</role>
 <hrs>80</hrs>
  </member>
  <member>
    <name>Peter Tester</name>
    <role>qa</role>
 <hrs>40</hrs>
  </member>
</projectteam>

I'd like the data to be displayed as :

Name         Role  Dev   QA
---------------------------
John Doe     dev   100  
Paul Coder   dev    40
Henry Tester qa          80
Peter Tester qa          40
---------------------------
Role Totals:       140  120
---------------------------

I would like to know if I can use something like:

    <xsl:element name="{role}">
      <xsl:value-of select="member/hrs"/>
    </xsl:element>

So that I can embed elements <dev>100</dev> and so on at run time during
first pass, and then use the resulting XML to display data for new columsn 'dev'
and 'qa', that way, calculating totals for each role type will be much simpler
(for eg. "sum(preceding-sibling::member/dev)" for dev column), and the data for each
cell in 'dev' and 'qa' colums could simply be the value-of these tags respectively.

It got the desired results the hard way using following stylesheet (page formatting
details omitted to keep it brief), but am not convinced that this is the apt solution.

    ...
              <fo:table-body>
      <!-- fills table rows -->
                <xsl:apply-templates select="member"/>

      <!-- dislpay totals for each role -->
      <fo:table-row height="12pt" border-bottom="1pt solid black">
      <fo:table-cell number-columns-spanned="2">
        <fo:block>Role Totals:</fo:block>
      </fo:table-cell>

      <fo:table-cell text-align="right">
        <xsl:call-template name="RoleTotals">
          <xsl:with-param name="node" select="//member[1]"/>
         <xsl:with-param name="roleName" select="'dev'"/>
        </xsl:call-template>
      </fo:table-cell>
      <fo:table-cell text-align="right">
        <xsl:call-template name="RoleTotals">
          <xsl:with-param name="node" select="//member[1]"/>
         <xsl:with-param name="roleName" select="'qa'"/>
        </xsl:call-template>
      </fo:table-cell>
      </fo:table-row>
              </fo:table-body>
    ...
    </fo:root>
  </xsl:template>

  <xsl:template match="member">
  <fo:table-row border-bottom="1pt solid black">
      <fo:table-cell> <fo:block> <xsl:value-of select="name"/></fo:block></fo:table-cell>
      <fo:table-cell> <fo:block> <xsl:value-of select="role"/></fo:block></fo:table-cell>
      <fo:table-cell text-align="right">
        <fo:block> 
      <xsl:if test="role = 'dev'"><xsl:value-of select="hrs"/></xsl:if>
  </fo:block>
      </fo:table-cell>
      <fo:table-cell text-align="right">
        <fo:block> 
      <xsl:if test="role = 'qa'"><xsl:value-of select="hrs"/></xsl:if>
  </fo:block>
      </fo:table-cell>
    </fo:table-row>
  </xsl:template>

  <xsl:template name="RoleTotals">
    <xsl:param name="node"/>
    <xsl:param name="roleName"/>
    <xsl:param name="RT" select="0"/>
    <xsl:variable name="newRT">
    <xsl:choose>
      <xsl:when test="$node/role = $roleName">
         <xsl:value-of select="$RT + $node/hrs"/>
      </xsl:when>
      <xsl:otherwise><xsl:value-of select="$RT"/></xsl:otherwise>
    </xsl:choose>
    </xsl:variable>
   <xsl:choose>
     <xsl:when test="$node/following-sibling::member">
      <xsl:call-template name="RoleTotals">
        <xsl:with-param name="node" select="$node/following-sibling::member[1]"/>
        <xsl:with-param name="roleName" select="$roleName"/>
        <xsl:with-param name="RT" select="$newRT"/>
      </xsl:call-template>
     </xsl:when>
     <xsl:otherwise>
      <fo:block><xsl:value-of select="$newRT"/></fo:block>
     </xsl:otherwise>
   </xsl:choose> 
  </xsl:template>

Best Answer

What would happen if more job roles, other than Dev and QA got added? Would your stylesheet be able to cope? Maybe you can make use of Muenchian Grouping to get all possible roles in the stylesheet, and then generate columns for each possible role dynamically?

<xsl:key name="roles" match="role" use="."/>

<xsl:template match="/projectteam">
 <table border="1">
  <tr>
   <td>Name</td>
   <td>Role</td>
   <xsl:for-each select="member[generate-id(role) = generate-id(key('roles', role)[1])]">
    <td>
     <xsl:value-of select="role"/>
    </td>
   </xsl:for-each>
  </tr>
  <xsl:apply-templates select="member"/>
 </table>
</xsl:template>

<xsl:template match="member">
 <xsl:variable name="currentrole" select="role"/>
 <xsl:variable name="currenthrs" select="hrs"/>
 <tr>
  <td>
   <xsl:value-of select="name"/>
  </td>
  <td>
   <xsl:value-of select="role"/>
  </td>
  <xsl:for-each select="/projectteam/member[generate-id(role) = generate-id(key('roles', role)[1])]">
   <td>
    <xsl:choose>
     <xsl:when test="$currentrole = role">
      <xsl:value-of select="$currenthrs"/>
     </xsl:when>
    </xsl:choose>
   </td>
  </xsl:for-each>
 </tr>
</xsl:template>

I've outputted as HTML, not XSL-FO, but maybe this gives you the general idea.