by Matthias Laux

Complex HTML tables made easy with Apache Velocity

feature
Jan 3, 200827 mins

Try a Java package that takes the pain out of HTML table creation and handling when using Velocity templates

The Apache Velocity template engine simplifies creation of textual content by merging data contained in a Java object model with Velocity templates. But creating complex HTML tables with cells that span multiple columns and rows can lead to elaborate nested hierarchies of Velocity directives, which quickly render the Velocity template difficult to read and maintain. In this article Matthias Laux introduces a Java package that simplifies handling of even highly complex HTML tables, using a small number of directives while retaining all the benefits of Velocity for HTML generation. The package is independent of Apache Velocity and can potentially be used with other rendering schemes, such as JavaServer Pages.

I’ve worked on several projects that required generated HTML pages to display large amounts of data, typically available in a database or as XML files. To apply the well-established Model-View-Controller paradigm, I used Apache Velocity as the template engine to generate all the pages. The controller application’s task was to read the data, fill in some Java data structures based on the data model for a given use case, and merge the data objects with Velocity templates.

Most of the data I worked with required extensive use of HTML tables on the Velocity side. The data contained hierarchical information, so the tables became increasingly complex because cells had to be merged using rowspan or colspan attributes to reflect the hierarchies. Correct output of required HTML markup elements such as <tr> and <td>, and the use of rowspan and colspan attributes, had to be controlled by Velocity directives (such as #if ... #end), which eventually overwhelmed the template’s HTML code. This recurring pattern led me to implement a general solution for handling complex tables in this context. Beyond basic functionality, I wanted the solution to include some convenience features, such as:

  • The ability to grow a table dynamically after instantiation
  • Flexible behavior at the boundaries: clipping and autogrowth during cell insertion
  • Compacting of tables
  • Cloning of tables

This article introduces my solution: a Java package that takes the complexity out of HTML table generation with Velocity. First I’ll demonstrate how complex tables lead to overly complex Velocity templates. Then I’ll show you how you can use my solution to generate a complex table with minimal Java code and a few Velocity directives. Finally, with the help of a real-world example, you’ll learn how to take advantage of the package’s convenience features.

Complex tables, cluttered templates

Table 1 is an (abridged) example of a fairly complex table. Several of its cells are combined via rowspan attributes.

 

MainTopic  # Topic  # SubTopic  #
DOC 31 Developer Guide 17 Fact Sheet 4
General 8
HowTo 1
Module Whitepaper 3
Tutorial 1
General 7 Concept Paper 2
IDN 1
Release Documentation 3
User Guide 1
Training Material 7 eHF Conference 7
INF 28 Deployment Infrastructure 1 Performance 1
Development Environment 12 Generator 12
Test Infrastructure 14 Audit 1
Authentication 1
Document 1
Test Client 10
User Management 1
Tools for eHF Users 1 Security Workbench 1
 Total Count 59  Total Count 59  Total Count 59

Table 1. A fairly complex table

The values of the rowspan attributes in Table 1 must be assigned dynamically at page-creation time, because the number of hierarchy elements (MainTopic, Topic, and SubTopic), and the string values, aren’t known until then. For this reason, the Velocity template must handle the required HTML output, causing the template to be cluttered with directives to handle all possible cases. As you can see in Listing 1, the template contains several references to the data model objects (such as $categoryData).

Listing 1. Portion of the Velocity template for Table 1

<table class="legacyTable" cellpadding="2" cellspacing="1" border="1" style="empty-cells:show">

  <tr bgcolor="$headerBackgroundColor">
    <td align="center"> $headerFontOn MainTopic $headerFontOff
    <td align="center"> $headerFontOn # $headerFontOff
    <td align="center"> $headerFontOn Topic $headerFontOff
    <td align="center"> $headerFontOn # $headerFontOff
    <td align="center"> $headerFontOn SubTopic $headerFontOff
    <td align="center"> $headerFontOn # $headerFontOff
  </tr>

  #foreach ( $mainTopic_Name in $categoryData.backlogElements.keySet() )
  
    #set ( $mainTopic_Category                     = $categoryData.categories.get($mainTopic_Name) )
    #set ( $mainTopic_Category_BacklogElementCount = $categoryData.backlogElements.get($mainTopic_Name).size() )
    #set ( $topic_Category_Count                   = $mainTopic_Category.backlogElements.size() )
  
    #set ( $rowCount1 = 0 )
    #foreach ( $topic_Name in $mainTopic_Category.backlogElements.keySet() )
      #set ( $topic_Category = $mainTopic_Category.categories.get($topic_Name) )
      #foreach ( $subTopic_Name in $topic_Category.backlogElements.keySet() )
        #set ( $rowCount1 = $rowCount1 + 1 )
      #end
    #end

    #set ( $first1 = 1 )

    #foreach ( $topic_Name in $mainTopic_Category.backlogElements.keySet() )
  
      #set ( $topic_Category                     = $mainTopic_Category.categories.get($topic_Name) )
      #set ( $topic_Category_BacklogElementCount = $mainTopic_Category.backlogElements.get($topic_Name).size() )
      #set ( $subTopic_Category_Count            = $topic_Category.backlogElements.size() )
  
      #set ( $rowCount2 = 0 )
      #foreach ( $subTopic_Name in $topic_Category.backlogElements.keySet() )
        #set ( $rowCount2 = $rowCount2 + 1 )
      #end

      #set ( $first2 = 1 )

      #foreach ( $subTopic_Name in $topic_Category.backlogElements.keySet() )
    
        #set ( $subTopic_Category_BacklogElementCount = $topic_Category.backlogElements.get($subTopic_Name).size() )
    
        <tr>

          #if ( $first1 == 1 )
            <td rowspan="$rowCount1" valign="top"> 
              $plainFontOn 
              <a href="#$formatter.getAnchorName($mainTopic_Name)">
              $mainTopic_Name 
              </a>

              $plainFontOff
            </td>
            <td rowspan="$rowCount1" valign="top"
                bgcolor="$colorScheme.getNextColor($mainTopic_Category_BacklogElementCount, $maxCount)"> 
              $plainFontOn 
              $mainTopic_Category_BacklogElementCount
              $plainFontOff
            </td>
            #set ( $first1 = 0 )
          #end
    
          #if ( $first2 == 1 )
            <td rowspan="$rowCount2" valign="top">
              <a href="#$formatter.getAnchorName($mainTopic_Name, $topic_Name)">

              $plainFontOn $topic_Name $plainFontOff
              </a>
            </td>
            <td rowspan="$rowCount2" valign="top" 
                bgcolor="$colorScheme.getNextColor($topic_Category_BacklogElementCount, $maxCount)"> 
              $plainFontOn $topic_Category_BacklogElementCount $plainFontOff
            </td>
            #set ( $first2 = 0 )
          #end
    
          <td valign="top"> 
            <a href="#$formatter.getAnchorName($mainTopic_Name, $topic_Name, $subTopic_Name)">

            $plainFontOn $subTopic_Name $plainFontOff
            </a>
          </td>
          <td valign="top" bgcolor="$colorScheme.getNextColor($subTopic_Category_BacklogElementCount, $maxCount)"> 
            $plainFontOn $subTopic_Category_BacklogElementCount $plainFontOff
          </td>
        </tr>

      #end
    
    #end
    
  #end

  <tr bgcolor="$subHeaderBackgroundColor">
    <td> $plainFontOn <b> Total Count </b> $plainFontOff
    <td> $plainFontOn <b> $cnt1       </b> $plainFontOff
    <td> $plainFontOn <b> Total Count </b> $plainFontOff
    <td> $plainFontOn <b> $cnt2       </b> $plainFontOff
    <td> $plainFontOn <b> Total Count </b> $plainFontOff
    <td> $plainFontOn <b> $cnt3       </b> $plainFontOff
  </tr>

</table>

Clearly, the Velocity directives in Listing 1 predominate over the actual formatting of the output, making the HTML code that controls the page’s appearance difficult to read and maintain.

Surprising though it may be, Table 1 is a relatively simple example, compared to project requirements I’ve encountered. Take a look at another example (Table 2).

 

Box 1                     Box 5  
           

Box 2

             
                         
                         
  Box 3                        
                                 
          Box 4       Box 6    
                   
                   
                             

Table 2. A more-complex table

Table 2 is admittedly a somewhat artificial example, but it should be clear by now that inserting its <tr> and <td> tags in the right places using Velocity macros — especially if the colored cells’ number, size, and location aren’t known in advance — is no trivial task.

This is where the HTML Table package I’m introducing in this article comes to the rescue. I’ll show you how to create Table 2 with just a few lines of Java and Velocity-template code.

The Table and Cell classes

The solution’s basic approach is to represent the HTML table information by a Java model consisting of a Table class and a Cell class.

First, you create a Table instance, such as:

int   rowNumber = 10;
int   colNumber = 5;
Table table     = new Table(rowNumber, colNumber);

Then you create cells and add them to the table. The setCell() method is the central method for adding cells. This code adds a cell of width 1 column and height 1 row to the table at row 3 and column 2:

Cell cell = new Cell("A first cell");
table.setCell(cell, 3, 2);

The approach gets more interesting (and really starts to make sense) when cells span multiple rows and/or columns. In this case, the cell spans three rows and four columns:

Cell cell = new Cell("A more complex cell", 3, 4);
table.setCell(cell, 3, 2);

To see how this is reflected in the Table class’s internal structure, take a look at some of the internal data that Table holds:

private Cell[][]    cells   = null;
private boolean[][] visible = null;
private boolean[][] def     = null;

Once the row and column numbers are known, these two-dimensional arrays are created with one array element for every cell of the table. The boolean visible and def values are then set to true initially, and the cells array is initialized with a default cell:

private static Cell defaultCell = new Cell("", 1, 1);

If a Cell instance spanning more than one row and/or column is added to the table, it’s stored in the corresponding location in the cells array. This Cell instance covers other cells array elements in its vicinity, so the visible value for those cells array elements is set to false because they are now hidden. A reference to the newly added Cell instance is also stored in the now-hidden cells array elements. You can use this information to check for conflicts should more Cell instances be added that overlap with the previously added Cell instance. (HTML cannot properly display table cells that overlap, so such conflicts must be detected in the Table class.)

The initial setup for a table with four rows and six columns is:

Table table = new Table(4, 6);

Table 3 shows the internal data structures for this table:

 

cells   = default

def     = true

visible = true

cells   = default

def     = true

visible = true

cells   = default

def     = true

visible = true

cells   = default

def     = true

visible = true

cells   = default

def     = true

visible = true

cells   = default

def     = true

visible = true

cells   = default

def     = true

visible = true

cells   = default

def     = true

visible = true

cells   = default

def     = true

visible = true

cells   = default

def     = true

visible = true

cells   = default

def     = true

visible = true

cells   = default

def     = true

visible = true

cells   = default

def     = true

visible = true

cells   = default

def     = true

visible = true

cells   = default

def     = true

visible = true

cells   = default

def     = true

visible = true

cells   = default

def     = true

visible = true

cells   = default

def     = true

visible = true

cells   = default

def     = true

visible = true

cells   = default

def     = true

visible = true

cells   = default

def     = true

visible = true

cells   = default

def     = true

visible = true

cells   = default

def     = true

visible = true

cells   = default

def     = true

visible = true

Table 3. Internal data structures for a four-row, six-column table

You use this code to add a cell with two rows and three columns:

Cell cell = new Cell("A cell", 2, 3);
table.setCell(cell, 1, 2);              // Rows and columns start at 0

The internal data structures then look like Table 4, and the corresponding HTML table looks like Table 5.

 

cells   = default

def     = true

visible = true

cells   = default

def     = true

visible = true

cells   = default

def     = true

visible = true

cells   = default

def     = true

visible = true

cells   = default

def     = true

visible = true

cells   = default

def     = true

visible = true

cells   = default

def     = true

visible = true

cells   = default

def     = true

visible = true

cells   = cell

def     = false

visible = true

cells   = cell

def     = false

visible =

false

cells   = cell

def     = false

visible =

false

cells   = default

def     = true

visible = true

cells   = default

def     = true

visible = true

cells   = default

def     = true

visible = true

cells   = cell

def     = false

visible =

false

cells   = cell

def     = false

visible =

false

cells   = cell

def     = false

visible =

false

cells   = default

def     = true

visible = true

cells   = default

def     = true

visible = true

cells   = default

def     = true

visible = true

cells   = default

def     = true

visible = true

cells   = default

def     = true

visible = true

cells   = default

def     = true

visible = true

cells   = default

def     = true

visible = true

Table 4. Internal data structures for a four-row, six-column table

 

           
       
     
           

Table 5. Four-row, six-column table with a two-row, three-column cell

Initially assigning an instance of the default cell — defaultCell — to each element of the cells array helps prevent null pointer exceptions from occurring when the table is used in a template. Internally, the def array is used as a helper to determine if a cell contains this default cell.

Merging the Table instance with the Velocity template

Effectively you now have a simple data structure that holds the structure of the HTML table to be created, plus all the cells you’ve added. Merging this structure with a template is standard Velocity usage, and you just need to add the Table instance to the VelocityContext:

velocityContext.put("table", table);

A simple template to process the table structure could look like Listing 2:

Listing 2. Template for processing Table 5’s structure

<html>

<body>
<table class="legacyTable" border="1" cellspacing="0" cellpadding="0">
  #foreach ( $row in [$table.row0..$table.rowEnd])
    <tr>
    #foreach ( $col in [$table.col0..$table.colEnd])
      #if ( $table.isVisible($row, $col) )
        #if ( $table.isDefaultCell($row, $col) )
          <td> </td>
        #else
          #set ( $cell = $table.getCell($row, $col) )
          <td rowspan="$cell.rowSpan"
              colspan="$cell.colSpan"
              align="center"> $cell.name
          </td>

        #end
      #end
    #end
  #end
</table>
</body>
</html>

The important pieces in Listing 2 are:

  • The $table.isVisible() check, which ensures that cells covered by other cells are not displayed.
  • The $table.isDefaultCell() check, which handles the default cells (i.e., the ones that are neither set explicitly nor covered by cells set explicitly). It’s still important to include the <td> tag here (even though the tag contains no content) to ensure correct formatting of the table.
  • Visible cells that are not default cells are added with the predefined rowspan and colspan attribute values. As HTML cell content, you simply use the cell name here. Of course, the Cell class supports more-complex approaches to manage the data it contains (as you’ll learn as you read on).

Back to Table 2

Listing 3 shows the Java code required to set up the table structure for Table 2:

Listing 3. Java code for setting up Table 2’s structure

Table demoTable = new Table(10, 20);

Cell box1 = new Cell("Box 1", 1, 5);
Cell box2 = new Cell("Box 2", 3, 3);
Cell box3 = new Cell("Box 3", 6, 3);
Cell box4 = new Cell("Box 4", 3, 5);
Cell box5 = new Cell("Box 5", 5, 4);
Cell box6 = new Cell("Box 6", 4, 2);

box1.setProperty("backgroundColor", "#33FF33");
box2.setProperty("backgroundColor", "#FF6666");
box3.setProperty("backgroundColor", "#FFFF00");
box4.setProperty("backgroundColor", "#6600CC");
box5.setProperty("backgroundColor", "#C0C0C0");
box6.setProperty("backgroundColor", "#FFCC33");
     
demoTable.setCell(box1, 0, 0);
demoTable.setCell(box2, 1, 6);
demoTable.setCell(box3, 4, 1);
demoTable.setCell(box4, 6, 8);
demoTable.setCell(box5, 0, 15);
demoTable.setCell(box6, 6, 16);

The code in Listing 3 can be combined with the Velocity template in Listing 4 to obtain the result shown in Table 2:

Listing 4. Velocity template for Table 2

<table class="legacyTable" width="50%" valign="middle" border="1"
       cellpadding="2" cellspacing="2" style="empty-cells:show">
  #foreach ( $row in [$demoTable.row0..$demoTable.rowEnd])
    <tr>
    #foreach ( $col in [$demoTable.col0..$demoTable.colEnd])
      #if ( $demoTable.isVisible($row, $col) )
        #if ( $demoTable.isDefaultCell($row, $col) )
          <td> <br> </td>

        #else
          #set ( $cell = $demoTable.getCell($row, $col) )
          <td rowspan="$cell.rowSpan"
              colspan="$cell.colSpan"
              align="center"
              bgcolor="$cell.getProperty("backgroundColor")">
            $cell.name
          </td>
        #end
      #end
    #end
  #end
</table>

Notice that in Listing 3 you explicitly set a backgroundColor property for the Cell instances that’s then queried in the template (Listing 4) and used to set the cell’s background color. This is part of a more general consideration: how to control the appearance of the table and the cells in the template, and how to transport cell data from the Java code to the Velocity template.

Controlling cell appearance and content

At first glance, you’d expect that an HTML table’s appearance is controlled entirely by the Velocity template, because this is a view aspect. On the other hand, for dynamically created tables, a not uncommon scenario is that it’s easier or even necessary to control certain aspects of the cell’s appearance in the Java code rather than the template. For this reason, the Cell class supports both approaches. (Both approaches can also be mixed when necessary.)

To delegate control over a cell’s display aspects to the template, the Cell class supports the concept of cell types:

public void setType(String type);
public boolean isType(String type);
public Set<String> getTypes();

A cell can be assigned any number of type strings, which can then be queried in the template to control display aspects, as in this example:

#if ( $cell.isType("mainTopic") )
  <td bgcolor="blue"> $cell.name </td>
#else
  <td bgcolor="red"< $cell.name </td>

#end

Alternatively, for simple cases you can also use the cell name for the same purpose (but of course there’s only one cell name).

To support the second option of controlling certain aspects in the Java code, the Cell class offers these two methods:

public void setProperty(String key, String value);
public String getProperty(String key);

A cell can be assigned any number of properties, which can be used for typical HTML table cell attributes such as bgcolor (as in the preceding example), for alignment control (align or valign), or for tooltips.

The Java code for setting a tooltips property looks like this:

cell.setProperty("toolTip", "Start date: Sept. 19, 2007");

And the template tooltip property code looks like this:

<td title="$cell.getProperty("toolTip")"> $cell.name </td>

This tooltip example strongly favors the approach of controlling certain HTML aspects from within the application, because you wouldn’t want to manage such dates in the template itself. Of course, in a general approach the actual date would not be hardcoded in the Java sources but would be retrieved from the outside, for example from XML configuration data or from a persistence layer.

Cell content too is an important consideration for dynamically generated tables, because the table cells typically contain data such as text or images. This information must be transported into the template. The Cell class offers several ways to achieve this.

In the simplest case, the cell name can be used to transport plain text. Technically, this string value could be used to transport any amount of string data, but that’s not what it’s intended for. Keeping cell names short and descriptive is much cleaner, especially if you use the cell name to control display aspects. To transport actual data, the preferred approach is to use these two methods, which can handle any kind of data:

public void setContent(String key, Object value);
public Object getContent(String key);

Using a key identifier lets you differentiate between different content values per cell. The template needs to be aware of the data a cell contains and react accordingly with the generated HTML markup. This is where complementary concepts such as cell types and properties can be nicely combined with the content-handling methods to ensure the template knows exactly what to do with a given cell.

Logical indexing

The Table class supports logical indexing: row and column indexes that don’t necessarily range from 0 to the number of rows/columns minus one. Logical indexes can start at any integer, so the upper left corner of the table can have nonzero logical index values of row0 and col0. Consider these two constructors for tables:

public Table(int row0, int col0, int rowNumber, int colNumber);    
public Table(int rowNumber, int colNumber);

The first constructor sets the logical starting row and column indexes to the given values (row0 and col0), whereas the second one uses (0, 0) as for its starting indexes. So an invocation of the form Table table = new Table(5, 6, 10, 20); creates a table with logical row indexes ranging from 5 to 14 and column indexes ranging from 6 to 25. Setter and getter methods are then constrained to these logical indexes rather than the indexes of the actual underlying Java arrays inside the Table class, which in this case still range from 0 to 9 (10 rows) and 0 to 19 (20 columns), respectively.

Boundary conditions, clipping, and grow/autogrow

When you add cells to the table using the setCell() method, the call can simply succeed, or else several things can go wrong:

  • Another cell could already be in the same location.
  • The new cell could overlap with a previously added cell.
  • The cell could extend beyond the table’s boundaries.

All of these contingencies would lead to an inconsistent state if not handled properly. The Table class implements several checks to help you avoid creating such inconsistent states: it checks for overlapping cells and checks the table’s four boundaries for cells that would extend beyond the limits of the underlying table.

The behavior at the boundaries can be handled in a more flexible way, though. Not only does setCell() throw an exception when the table’s limits are exceeded, but some helper enumerations and interfaces (shown in Listing 5) also let you:

  • Automatically clip the cell to be added. (I.e., excess rows and/or columns are truncated.)
  • Implement autogrow behavior for the table itself. (I.e., additional rows and/or columns are added as necessary to allow the cell to fit.)

Listing 5. Enumerations and interfaces for clipping and autogrow

//.... The different boundary conditions

public enum BoundaryCondition {
  FIXED,
  CLIPPING,
  GROW;
}


//.... The interfaces and enums to identify the various locations
 
public interface Location {
  ;
}
 
public interface BoundaryLocation extends Location {
  ;
}
 
public enum ColumnLocation implements BoundaryLocation {
  LEFT,
  RIGHT;
}
 
public enum RowLocation implements BoundaryLocation {
  TOP,
  BOTTOM;
}

In addition, the Table class maintains the state of the boundaries at the four locations in a HashMap:

private Map<BoundaryLocation, BoundaryCondition> boundaryConditions
  = new HashMap<BoundaryLocation, BoundaryCondition>();

Some API methods also maintain the boundaries’ state:

public BoundaryCondition getBoundaryCondition(BoundaryLocation boundaryLocation);
public void setBoundaryCondition(BoundaryLocation boundaryLocation, BoundaryCondition boundaryCondition);

The default boundary condition is FIXED, but the setCell() method always checks at each boundary for the boundary condition in effect and reacts accordingly (throws an exception, clips, or autogrows). The Cell instance passed as argument into setCell() can be modified if clipping is enabled. This is required because, as you saw earlier, a reference to that cell is stored in the Table instance and is used for the formatting process within Velocity; so this Cell instance must reflect the actual (potentially clipped) values for row and column numbers.

To support the autogrowth behavior associated with BoundaryCondition.GROW, the package also provides several API methods (shown in Listing 6) that you can invoke directly to grow the table manually where necessary:

Listing 6. Methods for growing a table manually

//.... Add count columns at the given column location

public void addColumns(ColumnLocation location, int count); 

//.... Add 1 column at the given column location

public void addColumn(ColumnLocation location);             

//.... Add count rows at the given row location

public void addRows(RowLocation location, int count);       

//.... Add 1 row at the given row location

public void addRow(RowLocation);

The setCell() method contains the logic to handle all the cell-insertion cases along with the boundary conditions that are possible at the table’s four boundaries. The setCell() method returns an instance of SetResult, a simple bean-like class holding five values:

  • int row – The logical row index where the insertion of the cell occurred. It can be different from the original value provided as argument if clipping is enabled.
  • int col – The logical column index where the insertion of the cell occurred. It can be different from the original value provided as argument if clipping is enabled.
  • int rowEnd – The logical end-row index of the cell, which can also be different from the original expectation (i.e., logical start index plus number of rows minus 1) if clipping is enabled.
  • int colEnd – The logical end-column index of the cell, which can also be different from the original expectation (i.e., logical start index plus number of columns minus 1) if clipping is enabled.
  • boolean modified – This is true if the cell has been clipped during addition of the cell to the table.

This information can be useful to the invoking method to determine what happened during addition of a cell to the table and to react accordingly.

A clipping/autogrow example

The clipping and autogrow capabilities are very helpful when the required table dimensions are not known at table-creation time — for example, when a table is filled with the results of a database query. An example from one of my projects that leverages these capabilities is a tabular product roadmap. Its columns represent days of the year, and the rows contain information on product features that are to be implemented over time. By enabling clipping, I can slide the start and end dates of the table that I want to display over the actual data like a moving window that shows only part of the data. I don’t need to implement any complex logic to filter out data for cells that lie outside the selected time range. See Table 6 to get an idea of what this table looks like.

This table was created using a project-specific EventTable helper class. EventTable extends Table and simplifies management of timelines by using logical indexes for an absolute integer ordering of the days of the year. EventTable also provides additional methods that add cells representing months and years to table rows with just one call (which is how the timeline rows at the top and the bottom were created).

In this example, the actual data (such as the feature clusters, features, and their timelines) that is being fed into the table through the addition of cells covers the years between 2007 and 2009. By selecting the start column index and column number to span the duration of one year, it is easily possible to create separate tables for 2007, 2008, and 2009 while using the same input data. The underlying Table instance automatically discards all cells outside of the given range and also ensures that cells sitting on the edges of the table are properly clipped. Other features of the Table class that this example uses are:

  • The leading column with the row descriptions is added by invoking addColumn(ColumnLocation.LEFT) after all data has been added to the table. The row descriptions do not have any timeline attached to them, so that information is not associated with a particular column (all of which correspond to days of the year here). So, after invoking addColumn(), I can just put that row information data into the column index returned by the Table class’s getCol0() method.
  • At the lower end of the table, I use more rows to overlay the product-roadmap information with timelines for other products that my company makes. This allows for simple visual control over how the different timelines are related.
  • Cell appearance is controlled through a combination of cell types and properties via a hierarchy of statements in the Velocity template, as shown in Listing 7:

Listing 7. Velocity template statements for cell appearance in Table 6

...

#elseif ( $cell.isType("yearHeader") )

  <td rowspan="$cell.rowSpan" colspan="$cell.colSpan" align="center" bgcolor="#339999">
      $plainFontOn <b> $cell.name </b> $plainFontOff
  </td>

#elseif ( $cell.isType("monthHeader") )

  <td rowspan="$cell.rowSpan" colspan="$cell.colSpan" align="center" bgcolor="#66FF99">
      $plainFontOn <b> $cell.name </b> $plainFontOff
  </td>

#elseif ( $cell.isType("left") )

  <td rowspan="$cell.rowSpan" colspan="$cell.colSpan" align="left" bgcolor="#FFFF66">

      $plainFontOn <b> $cell.name </b> $plainFontOff
    </td>

#elseif ( $cell.isType("event") )

  <td rowspan="$cell.rowSpan" colspan="$cell.colSpan" align="center"
      title="$cell.getProperty("toolTip")" bgcolor="#FFCC66">
      $plainFontOn <b> $cell.name </b> $plainFontOff
  </td>

...

Listing 7 uses hardcoded colors, depending on the cell type assigned in the Java code. Tooltips are provided through cell properties. The rest of the display logic (for example to put the <tr> and <td> tags in the right places) is exactly the same as in the Table 2 example. The entire Velocity template for this table takes fewer than 80 lines of HTML code with Velocity macros.

The HTML tables I manage this way contain up to several tens of thousands of cells and can still be easily processed on a standard laptop in a few seconds.

Compacting, cloning, and dumping tables

The Table class offers some additional capabilities that can come in handy: compacting, cloning, and dumping.

Tables can be compacted along different dimensions. This is, entire rows and columns containing only the default cells can be eliminated automatically. This functionality is — to a certain extent — the counterpart to the autogrow capability I described previously. It goes beyond that capability, though, because it can also eliminate rows and columns in the table’s interior.

Another enumeration is introduced first to control this elimination process in the table’s interior:

public enum InternalLocation implements Location {
  ROW,
  COLUMN;
}

You use this and the other enumerations implementing the Location interface along with corresponding API methods:

public boolean compact(Location... location);
public boolean compact();

The compact(Location... location) method takes as arguments one or more of the six available instances of Location and compacts the table accordingly. It returns true if any row or column was removed in the process. The compact() method is a convenience method that compacts the table along the four boundaries.

Another use case that’s come up in my projects is table cloning. I had cases where several tables shared a large amount of the data, and only a small percentage of the data was different. In such cases, its much simpler to:

  1. Set up one Table instance with the common data for all of these tables
  2. Create the required number of clones using the clone() method.
  3. Fill each clone with the remaining table-specific data as necessary.

Note that the clone() method returns a shallow copy — that is, the Cell instances referenced are not cloned (which is in line with the use case described here: simplified handling of shared data).

Finally, the dump() method comes in quite handy for debugging purposes. It sends a simple HTML text fragment to STDOUT that can then be displayed in a browser. This gives you quick visual control over the internal table structure without needing to go through the template-merging process with Velocity first.

Summary and outlook

The HTML table package I’ve described in this article contains useful features that greatly simplify creation and handling of complex HTML tables in conjunction with the Apache Velocity template engine. The package is used in real-life projects and has been proven to meet customer demands.

I have a few ideas for potential future developments:

  • A flip() method that would let you flip an entire table along horizontal, vertical, and diagonal axes. This feature would be helpful for playing with different ways to display tabular data without needing to change the entire Java coding should it turn out at some late stage in the project, for example, that the rows should have been the columns to begin with and vice versa. Implementing this functionality is not trivial, though, because the table data is asymmetric if cells span more than one row and/or column.
  • A coalescing method that would automatically combine cells containing only the default cell into one cell after all data has been added to the table. This would simplify the resulting HTML table and speed up processing.
  • Using the classes described here with other rendering technologies. One immediate thought is to create custom tags that would render tables in JavaServer Pages.

Acknowledgment

I’d like to thank my colleague Richard Golden for inspiring this work by asking simple yet profound questions such as “Can I have a timeline attached to that table?” After numerous nightshifts and cans of caffeinated soda, the answer is “Yes, you can.”

Dr. Matthias Laux heads the product-management organization for Basis Technology at InterComponentWare AG, a provider of personal health record software and health care integration technologies. He’s a Sun certified Java Programmer and Java Enterprise Architect and has published several articles on various aspects of Java and other technologies. His background also includes high-performance computing and parallel programming, performance analysis and tuning, and platform and infrastructure topics. He has worked in the past for Sun Microsystems, IBM, and NASA.