September 26, 2024

IIQCommon overview: fluent, type-safe HTML tables

This article is part of a blog series providing an overview of the many SailPoint IIQ utilities and tools available in Instrumental ID’s open-source iiq-common-public library. (For an overview of the entire library, see this earlier post.)

This post will be about the Table class and its various auxiliary classes, a part of IIQCommon’s work item utilities. This class provides an API to generate fluent, type-safe HTML tables in IIQ scripts. We use this API widely in our customer installations for UI elements, email contents, and other

The problem: HTML in Beanshell in XML

When writing a custom SailPoint IIQ workflow, it’s often useful to include text-only fields in user-facing forms. For example, you may want to provide either feedback or instructions to the person interacting with your work item. IIQ allows these text-only fields to be formatted using standard HTML, vastly increasing the usability and appearance of your forms.

A screenshot of an IIQ work item, showing instructions in blue and red.

Unfortunately, constructing nicely formatted HTML within a script quickly becomes tedious. A particularly large table may result in repetitive, difficult-to-maintain code like this:

<Script>
<Source><![CDATA[
StringBuilder bldr = new StringBuilder();

bldr.append("<table  style='border: 1px solid black; width:100%; margin-left:auto; margin-right:0px;'> <tr > <th style='border: 1px solid black; text-align:center; '>Application</th><th style='border: 1px solid black; text-align:center;'>Address</th></tr> ");

for(int i = 0; i < links.size(); i++) {
  if (links.get(i).getApplicationName().equalsIgnoreCase("HR Employees")) {
    if (links.get(i).getAttribute("address") != null) {
      bldr.append("<tr>")
        .append("<td style='border: 1px solid black; padding-left:3px; padding-right:3px; height:30px; text-align:center;' >" + links.get(i).getApplicationName() + "</td>")
        .append("<td style='border: 1px solid black;  padding-left:3px; padding-right:3px; text-align:center;'>" + links.get(i).getAttribute("address") + "</br> " + links.get(i).getAttribute("city") + ", "  + links.get(i).getAttribute("state") + " " + links.get(i).getAttribute("zip"))
        .append("</tr>);

    }
  }
}

// and so forth, for hundreds of lines of code :(

So much repetition. We could use variables to store those repeated styles, but that just moves the ugliness elsewhere.

Making things worse, this is HTML embedded in Beanshell embedded in an IIQ XML object. Unless your script enclosed within a CDATA tag, and you remember to keep it that way while moving between environments, the XML parser is going to reject each one of those < characters.

Also, did we remember to close all of our HTML tags? No, but it’s not obvious.

The Table class

IIQCommon supplies a class called Table, intended to solve all of these problems. It provides a fluent API for constructing and formatting HTML tables and their contents.

The simplest use of the Table API is straightforward and readable:

Table table = new Table();

table
  // Add a header row with two cells
  .row()
  .header()
  .cell("Application")
  .cell("Address")
  // Start a new row
  .row()
  // Apply these options to each cell in the row
  .withCellOptions(CellOptions.style("border: 1px solid black"))
  // First cell
  .cell(
    link.getApplicationName(), 
    CellOptions.addCssClasses("customAppNameStyling")
  )
  // Second cell, which contains HTML
  .htmlCell(
    link.getAttribute("address") + "<br/> " + 
    link.getAttribute("city") + ", " + 
    link.getAttribute("state") + " " + 
    link.getAttribute("zip") + "<br/> "
  );

// Renders to HTML
return table.render();

Apart from those cases where a cell itself explicitly contains HTML, you never need to worry about HTML tags.

Other features

Apply formatting to every cell in the entire table using table.withCellClasses("cssClass1", "cssClass2").

Apply formatting to the current level – the whole table, just a row, or just a cell – using withCellOptions, as illustrated in the example above. Available CellOptions include style, header, rowspan, colspan, html, and addCssClasses. You can also write your own Java or Beanshell implementations of CellOption for your own common cases.

CellOptions can also be passed to individual cell() calls:

// Re-use these
CellOption option = CellOptions.style("word-break: break-word; overflow-wrap: break-word");
CellOption nativeIdentityStyle = CellOptions.style("word-break: break-word; overflow-wrap: break-word; width: 20%");
	                              		
for(Link account : identityAccounts) {
  String trimmedNativeIdentity = Util.truncate(account.getNativeIdentity(), 40);

  identityTable
    .row()
    // Applies to the row, here
    .withStyle("width: 100%;")
    .cell(account.getApplicationName(), option)
    .cell(trimmedNativeIdentity, nativeIdentityStyle)
    .cell(account.getAttribute(Constants.LINK_ATTR_UNIQUE_ID), option)
    .cell(account.getAttribute(Constants.LINK_ATTR_USERNAME), option)
    .cell(account.getAttribute(Constants.LINK_ATTR_SECRET), option)
    .cell(account.getAttribute(Constants.LINK_ATTR_FIRST_NAME), option)
    .cell(account.getAttribute(Constants.LINK_ATTR_LAST_NAME), option);
}

Rows can be constructed in bulk using Table.row(), passing in a List of contents for each cell (horizontally), plus any number of CellOption objects to format them. If you need even more bulk, you can use Table.rows(), passing in a List<List> structure.

// Add the header row
table.row(List.of("Name", "Native Identity"), CellOptions.header());

List rowData = new ArrayList();

for(Link link : identity.getLinks()) {
  List linkRow = new ArrayList();
  linkRow.add(link.getApplicationName());
  linkRow.add(link.getNativeIdentity());

  rowData.add(linkRow);
}

table
  // Applies to every future cell in the table
  .withCellClasses("yourCssClass");
  // Add all the rows at once
  .rows(rowData);

If you use withCellClasses, it applies to all future cells. If you want to reset to the default state, use clearCellClasses.

There are a handful of non-fluent methods for applying options to a specific row. These will generally be invoked after you’ve populated your row data, such as in the bulk example above.

  • Use setCssClasses(List<String> classes) to apply one or more CSS classes at the table level. Commonly, this is IIQ’s built-in spBlueTable.
  • Use setRowStyle(int row, String style) to style a particular numbered row in the table.
  • Use setRowCellOptions(int row, CellOption... options) to style a particular numbered row in the table with the given cell options.
  • Use setColumnCellOptions(int column, CellOption... options) to add cell options to a certain column (i.e., the Nth item in each row).
  • Use setColumnStyle(int column, String style) to apply the given style to a certain column.
  • Use setExtraColumnStyle(int column, String style) to append the given style to a certain column, adding it to any style already on the cell.
  • Use withHeaderRow(CellOption... options) to retroactively style the first row in the table as a header with the given options.

Complex example: Combined with Velocity

At one customer, we have combined this function with Velocity for a powerful rendering engine. This is an implementation of a custom attribute displayed by our UI Enhancer plugin. The goal is to display a summary of authoritative source data for those IIQ users (mostly IT Support) who aren’t authorized to view the raw source data in the Link objects.

Note that this also uses the Utilities.velocityRender() method from IIQCommon.

We created a Custom object configured with Velocity templates for each type of account, distilled for this example to “Contractors” (a system for affiliates) and “HR Employees” (an ERP system for true employees). In the real implementation, the Velocity templates are much more complex, and there are over a dozen of them.

<Custom name="Link Templates">
  <Attributes>
    <Map>

      <entry key="Contractors">
        <value><String>
          $link.attributes["JobDescription"], $link.attributes["OrgUnitDescription"], $link.attributes["OrgParentDescription"], $link.attributes["RelationshipToOrganization"]
        </String></value>
      </entry>

      <entry key="HR Employees">
        <value><String>
          #if ($link.Attributes["Is_Retired"] == "1" and $link.Attributes["Is_Terminated"] == "1")
            Retired
          #end
          !$link.Attributes["JobTitle"]
        </String></value>
      </entry>

    </Map>
  </Attributes
</Custom>

With this configuration, plus Table, we can render this custom attribute as an HTML table.

// A utility method to render the Velocity template for a given Link
String getLinkDescription(Link link) {
  Custom config = context.getObject(Custom.class, "Link Description Templates");
  String velocityTemplate = config.get(link.getApplicationName());
    	
  if (Util.isNotNullOrEmpty(velocityTemplate)) {
    Map velocityInput = new HashMap();
    velocityInput.put("link", link);
    return Utilities.velocityRender(velocityTemplate, velocityInput); 
  }

  return null;
}

// Create the table
Table table = new Table();

// Header row
table.row().header().cell("Application").cell("Description");

// One row per Link with a template
for(Link link : links) {
  String desc = getLinkDescription(link);
  if (Util.isNotNullOrEmpty(desc)) {
    table.row().cell(link.getApplicationName()).htmlCell(desc);
  }
}

// All done!
return table.render();