tentackle-fx-rdc-poi — Spreadsheet (XLS) Export for RDC Tables¶
Overview and Motivation¶
Every rich desktop client table and tree-table
already offers a context-menu action to export its rows to a spreadsheet file. Out of the box
that action lives in TableUtilities
and writes CSV (RFC 4180) — a format every spreadsheet application can open, with no extra
dependencies.
tentackle-fx-rdc-poi upgrades that same action to write a real Excel .xls workbook using
Apache POI. It does so without any change to application code: the module
contributes a single service that replaces the default TableUtilities. Drop the module on the
path and the table popup's Export to spreadsheet command produces a styled .xls file instead of
CSV.
How it plugs in¶
The module ships one public type,
PoiTableUtilities, which extends
TableUtilities and is registered through the service API:
@Service(TableUtilities.class)
@ClasspathFirst(TableUtilities.class) // this module is no JPMS module (POI requires a filename-based automatic module), but fx-rdc is
public class PoiTableUtilities extends TableUtilities {
...
}
@Service(TableUtilities.class)makesServiceFinderreturnPoiTableUtilitieswherever the RDC asks for the singletonTableUtilities.getInstance()— including the Export / Export selected rows items wired up inTablePopup.@ClasspathFirst(TableUtilities.class)is required because this module cannot be a real JPMS module (see the caveat below). Without it, the service from the real moduletentackle-fx-rdcwould always win over the one from the classpath.
Two methods are overridden:
selectSpreadsheetFile(...)— opens the file dialog with the.xlsextension and remembers the last chosen file per table. Like every file selection of the RDC, it uses the JDK'sjava.util.prefs.Preferences.userRoot(), node<application name>/lastXlsNames/<table name>, keypath. A file path only makes sense on the machine it was chosen on, so it is deliberately not kept in the database-backed persisted preferences.toSpreadsheet(columnConfigurations, file, items)— builds the workbook from the table's visibleTableColumnConfigurations.
A subclass can additionally override the protected assertFitsIntoSheet(columnCount, rowCount),
which checks the sheet limits.
What the export produces¶
The exporter walks each visible column's binding and maps the model value to a typed cell:
| Model value | Cell rendering |
|---|---|
Boolean |
boolean cell |
BMoney |
numeric cell with a #,##0[.0…] format matching the money scale |
any other Number |
numeric cell |
java.sql.Timestamp (e.g. Tentackle's Timestamp) |
date cell with the built-in m/d/yy h:mm format |
java.sql.Time (e.g. Tentackle's Time) |
numeric cell holding the fraction of a day, format h:mm:ss |
any other Date / Calendar / LocalDate |
date cell with the built-in m/d/yy format |
LocalDateTime |
date cell with the built-in m/d/yy h:mm format |
OffsetDateTime / ZonedDateTime |
as LocalDateTime: a spreadsheet has no notion of a time zone |
Instant |
as LocalDateTime, resolved in the JVM's default zone |
LocalTime / OffsetTime |
numeric cell holding the fraction of a day, format h:mm:ss |
anything else (!= null) |
text cell holding the text displayed in the table |
Column headers are written as a bold, italic, centered first row. Number formats are cached and reused per scale, so a large export does not create one cell style per row.
The java.sql types are checked first: they extend java.util.Date, but a date-only format would
hide the time of a timestamp and show a time as January 1, 1970. Null values and columns without a
binding leave the cell empty. The text cells show the same text as the table and the CSV export,
including case conversion and the formatting of the column's cell type. Note that a spreadsheet time
has no sub-second precision.
Sheet limits¶
The .xls (BIFF8) format holds at most 65,536 rows (the header included) and 256 columns.
toSpreadsheet(...) checks both up front and throws an RdcRuntimeException naming the limit,
rather than letting POI fail with an IllegalArgumentException that names nothing but the offending
index. Invisible columns don't count. Exports that big need one of the OOXML formats
(XSSFWorkbook, or SXSSFWorkbook for streaming), which this module does not currently use.
JPMS / POI caveat¶
Apache POI 5.x ships a module-info (org.apache.poi.poi), but it requires commons-math3, which
is still a filename-based automatic module. A module depending on POI therefore must not be published
as a real JPMS module, so this one deliberately has no module-info.java. Its manifest declares
the Automatic-Module-Name org.tentackle.fx.rdc.poi instead, and the service relies on
@ClasspathFirst.
If you build a distribution image with the
jlink/jpackage plugin, keep
the POI artifact on the classpath — moving it to the module path (even via modulePathOnly)
fails, because an automatic module referencing it uses the wrong class loader and throws
ClassNotFoundException:
<plugin>
<groupId>org.tentackle</groupId>
<artifactId>tentackle-jlink-maven-plugin</artifactId>
<configuration>
<classpathDependencies>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
</dependency>
</classpathDependencies>
</configuration>
</plugin>
Once POI and all its transitive dependencies are fully modularized, the module can declare a normal
module-info.java and this special handling can be dropped.
Unit testing the export¶
The module's own tests (src/test/java) run without a JavaFX toolkit. That is possible because
the export only touches the table configuration, never a Control:
- A
DefaultTableConfigurationis filled with columns and bound to a plain row class with@Bindablegetters (config.getBinder().bind()), which yields the very sameFxTableBindings the RDC uses at runtime. - Column visibility is toggled through
getTableColumn()/getTreeTableColumn()— the column configuration creates those lazily, and aTableColumnis not aControl. - The workbook is written to a temporary file and read back with POI, so cell types, values, number
formats and the reuse of cell styles are verified on the real
.xlsoutput. - The displayed text of a column comes from its
TableCellType, which needs no toolkit either.
selectSpreadsheetFile(...) would open a FileChooser, so the tests register a
TestRdcUtilities via @Service(RdcUtilities.class) that records the arguments instead
(hence the test-analyze goal in the module's pom.xml). The same mechanism verifies that
TableUtilities.getInstance() really resolves to PoiTableUtilities.