OleDogNewTricks
OleDogNewTricks

Reputation: 11

XSLT 2.0: Issues creating HTML table with dynamic rows & columns

I'm creating an HTML table that is based on dynamic columns(Hostname) and rows(VLAN). I'm running into an issue after the first position data(1 row for all hosts) in the is written to the table; selects the 2nd position data just fine, but the $vCol variable takes it back to the first line of the $vCols variable.

Appreciate any direction you can offer. Thanks

XSLT-2.0 code:

<?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:key name="kHostNameByValue" match="Hostname" use="."/>
  <xsl:key name="kVLAN" match="Hostname" use="."/>
  <xsl:variable name="vCols" select="/*/*/Hostname[generate-id()=generate-id(key('kHostNameByValue',.)[1])]"/> 

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

    <xsl:template match="DocumentRoot">
      <table border="1">
        <!--  Print out column headings by Hostname    -->
       <tr>
            <xsl:apply-templates select="$vCols"/>
       </tr>

          <!--  Print out VLANs by Hostname    -->
          <xsl:for-each-group select="(//Row)[not(position() > $vMaxRows)]" group-by="VLAN">
              <tr>     
                 <xsl:variable name="vPos" select="position()"/>
                  <!-- Issue on 2nd position when $vCols goes back to 1st hostname at line 3 -->
                  <xsl:for-each select="$vCols"> 
                    <td>  
                        <xsl:value-of select="..[$vPos]/VLAN"/>
                    </td>    
                 </xsl:for-each> 
              </tr>
            </xsl:for-each-group>
      </table>
    </xsl:template>

    <xsl:template match="Hostname">
        <td>
            <b>
                <xsl:value-of select="." />
            </b>
        </td>   
    </xsl:template>

    <xsl:template match="text()"/>
</xsl:stylesheet>

Here's the sample XML data.

<?xml version="1.0" encoding="UTF-8"?>
<DocumentRoot>
    <Row>
        <Hostname>switch-1</Hostname>
        <HostIP>10.29.178.102</HostIP>
        <VLAN>10</VLAN>
        <VLANName>VLAN-10</VLANName>
    </Row>
    <Row>
        <Hostname>switch-1</Hostname>
        <HostIP>10.29.178.102</HostIP>
        <VLAN>500</VLAN>
        <VLANName>VLAN-500</VLANName>
    </Row>
    <Row>
        <Hostname>switch-2</Hostname>
        <HostIP>10.29.178.103</HostIP>
        <VLAN>11</VLAN>
        <VLANName>VLAN-11</VLANName>
    </Row>
    <Row>
        <Hostname>switch-2</Hostname>
        <HostIP>10.29.178.103</HostIP>
        <VLAN>501</VLAN>
        <VLANName>VLAN-500</VLANName>
    </Row>
    <Row>
        <Hostname>switch-3</Hostname>
        <HostIP>10.29.170.1</HostIP>
        <VLAN>15</VLAN>
        <VLANName>VLAN-15</VLANName>
    </Row>
    <Row>
        <Hostname>switch-3</Hostname>
        <HostIP>10.29.170.1</HostIP>
        <VLAN>25</VLAN>
        <VLANName>VLAN-25</VLANName>
    </Row>
    <Row>
        <Hostname>switch-3</Hostname>
        <HostIP>10.29.170.1</HostIP>
        <VLAN>35</VLAN>
        <VLANName>VLAN-35</VLANName>
    </Row>
    <Row>
        <Hostname>switch-3</Hostname>
        <HostIP>10.29.170.1</HostIP>
        <VLAN>45</VLAN>
        <VLANName>VLAN-45</VLANName>
    </Row>
    <Row>
        <Hostname>switch-3</Hostname>
        <HostIP>10.29.170.1</HostIP>
        <VLAN>55</VLAN>
        <VLANName>VLAN-55</VLANName>
    </Row>
</DocumentRoot>

Output (Actual and desired):

Output

Upvotes: 1

Views: 376

Answers (3)

Martin Honnen
Martin Honnen

Reputation: 167571

I don't think in XSLT 2 or 3, once you use for-each-group, you need any of the keys, you can just store the grouping result and then process it, for instance to store the grouping result as XML in XSLT 2 or 3 you could use

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    exclude-result-prefixes="#all"
    expand-text="yes"
    version="3.0">

  <xsl:output method="html" indent="yes" html-version="5"/>

  <xsl:template match="/">
    <html>
      <head>
        <title>.NET XSLT Fiddle Example</title>
      </head>
      <body>
        <xsl:apply-templates/>
      </body>
    </html>
  </xsl:template>

  <xsl:template match="DocumentRoot">
      <table>
          <xsl:variable name="cols" as="element(col)*">
              <xsl:for-each-group select="Row" group-by="Hostname">
                  <col name="{current-grouping-key()}">
                      <xsl:sequence select="current-group()"/>
                  </col>
              </xsl:for-each-group>
          </xsl:variable>
          <thead>
              <tr>
                  <xsl:for-each select="$cols">
                      <th>{@name}</th>
                  </xsl:for-each>
              </tr>
          </thead>
          <tbody>
              <xsl:variable name="rows" select="max($cols!count(Row))"/>
              <xsl:for-each select="1 to $rows">
                  <xsl:variable name="row" select="."/>
                  <tr>
                      <xsl:for-each select="$cols">
                          <td>{Row[$row]/VLAN}</td>
                      </xsl:for-each>
                  </tr>
              </xsl:for-each>
          </tbody>
      </table>
  </xsl:template>

</xsl:stylesheet>

https://xsltfiddle.liberty-development.net/94rmq6Q/3 is XSLT 3 with the ! map operator and the text value templates but https://xsltfiddle.liberty-development.net/94rmq6Q/4 rewrites that as XSLT 2 with value-of instead.

Or in XSLT 3 you could store the grouping result in a sequence of arrays:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    xmlns:array="http://www.w3.org/2005/xpath-functions/array"
    exclude-result-prefixes="#all"
    expand-text="yes"
    version="3.0">

  <xsl:output method="html" indent="yes" html-version="5"/>

  <xsl:template match="/">
    <html>
      <head>
        <title>.NET XSLT Fiddle Example</title>
      </head>
      <body>
        <xsl:apply-templates/>
      </body>
    </html>
  </xsl:template>

  <xsl:template match="DocumentRoot">
      <table>
          <xsl:variable name="cols" as="array(element(Row))*">
              <xsl:for-each-group select="Row" group-by="Hostname">
                  <xsl:sequence select="array{ current-group() }"/>
              </xsl:for-each-group>
          </xsl:variable>
          <thead>
              <tr>
                  <xsl:for-each select="$cols">
                      <th>{?1/Hostname}</th>
                  </xsl:for-each>
              </tr>
          </thead>
          <tbody>
              <xsl:variable name="rows" select="max($cols!array:size(.))"/>
              <xsl:for-each select="1 to $rows">
                  <xsl:variable name="row" select="."/>
                  <tr>
                      <xsl:for-each select="$cols">
                          <td>{if ($row le array:size(.)) 
                               then .($row)/VLAN 
                               else ()}</td>
                      </xsl:for-each>
                  </tr>
              </xsl:for-each>
          </tbody>
      </table>
  </xsl:template>

</xsl:stylesheet>

https://xsltfiddle.liberty-development.net/94rmq6Q/2

Upvotes: 1

Zotta
Zotta

Reputation: 2603

I think using a group by might make this more complicated than it needs to be. Basically for each row you need to iterate over all columns and output the cell, if it exists, or an empty cell otherwise. This means you should iterate over an index rather then row elements:

<xsl:for-each select="1 to $numRows">
    <xsl:variable name="rowIndex" select="position()" />
    <tr>
    <xsl:for-each select="$vCols">
        <xsl:variable name="cell" select="//Row[string(Hostname) = .][position() = $rowIndex]" />
        <xsl:apply-templates select="$cell">
        <xsl:if test="not($cell)">
            <td></td>
        </xsl:if>
    </xsl:for-each>
    <tr>
</xsl:for.each>

Upvotes: 1

Tim C
Tim C

Reputation: 70618

Firstly, I think your maxRows variable can be simplified to this

<xsl:variable name="maxRows" select="max(//Row/count(key(hostNameByValue, Hostname)))" />

Where I have defined hostNameByValue key like so:

<xsl:key name="hostNameByValue" match="Row" use="Hostname"/>

You can also use distinct-values to get the distinct column names

<xsl:variable name="cols" select="distinct-values(//Row/Hostname)" />

So, assuming $rowNum is the current number (within a <xsl:for-each select="1 to $maxRows"> block, the code to get the current cell value would be this

<xsl:for-each select="$cols">
  <th><xsl:value-of select="key('hostNameByValue', ., $doc)[position() = $rowNum]/VLAN"/></th>
</xsl:for-each>

(Where $doc is a reference to the initial XML document, because within the xsl:for-each is now a sequence of atomic values)

Try this XSLT

<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" indent="yes" />

  <xsl:key name="hostNameByValue" match="Row" use="Hostname"/>

  <xsl:variable name="cols" select="distinct-values(//Row/Hostname)" />
  <xsl:variable name="maxRows" select="max(//Row/count(key('hostNameByValue', Hostname)))" />
  <xsl:variable name="doc" select="/" />

  <xsl:template match="DocumentRoot">
    <table>
    <tr>
      <xsl:for-each select="$cols">
        <th><xsl:value-of select="."/></th>
      </xsl:for-each>
      </tr>
      <xsl:for-each select="1 to $maxRows">
        <xsl:variable name="rowNum" select="position()"/>
        <tr>
          <xsl:for-each select="$cols">
            <th><xsl:value-of select="key('hostNameByValue', ., $doc)[position() = $rowNum]/VLAN"/></th>
          </xsl:for-each>
        </tr>
      </xsl:for-each>
    </table>
  </xsl:template>
</xsl:stylesheet>

See it in action at http://xsltfiddle.liberty-development.net/6r5Gh3N

Upvotes: 1

Related Questions