Class TemplateMarkers

java.lang.Object
io.keikai.axyra.sheets.template.TemplateMarkers

public final class TemplateMarkers extends Object
Data binding via template markers — a lightweight reporting layer built entirely on the public read/write API.

A marker is ${...} inside a text cell:

  • Scalar / nested path — ${name} or ${a.b.c}. Each hop resolves through a Map key, a record component, a JavaBean getter, or a public field; a null anywhere along the chain renders as a blank cell. A cell that is exactly one marker keeps the value's type (number, boolean, text; LocalDate, LocalDateTime and Date become date serials); a marker embedded in other text yields text.
  • List — ${people.name} marks a template row bound to the list data.get("people") (a List of maps, records, or beans). The row is repeated once per record, inserting rows as needed, each record's fields substituted. Static text/values and formulas on the template row are copied to every record row. Formula cells get Excel fill-down semantics: row-relative references shift by the record offset, including sheet-qualified ones (Sheet2!A1); row-absolute references (A$1, $A$1), string literals and structured references stay untouched. An array formula confined to the template row is re-entered as an array formula on each record row.
  • Aggregate — ${people.age:sum} (also :avg, :count, :min, :max) evaluates over the whole list and may sit in any non-template-row cell. :count counts non-null values of any type; the numeric aggregates render blank when the field has no numeric values.
  • Grouping — ${people.dept:group} on a template row groups the records by that field (stable first-appearance key order, original record order inside each group). An optional subtotal row immediately below the template row — recognized by ${people.age:subtotal-sum}-style markers (also -avg/-count/-min/-max) or ${people.dept:groupname} (the group's key) — is emitted once per group with that group's aggregate.
  • Image — ${image:logo} (or ${image:people.photo} on a template row) resolves to a byte[] that is inserted as a picture anchored at the cell (one-cell anchor, default 1×0.75 inch); the cell itself is blanked. Non-byte[] values just blank the cell.

Unknown markers resolve to empty. One list binds per template row (the first ${list.field} marker whose leading segment is a List wins). A subtotal row is recognized only under a grouped template row. Every output row takes the cell formats of the template row it comes from.


 // A1: "Name"   B1: "Age"
 // A2: "${people.name}"   B2: "${people.age}"
 // A3: "Total"  B3: "${people.age:sum}"
 Map<String, Object> data = Map.of("people", List.of(
         Map.of("name", "Ann", "age", 31),
         Map.of("name", "Bob", "age", 42)));
 TemplateMarkers.process(sheet, data);   // rows 2-3 hold Ann and Bob, the total moves to row 4
 
  • Method Details

    • process

      public static void process(Sheet sheet, Map<String,Object> data)
      Replaces every ${...} marker in the used range of sheet with values from data, in place. List template rows expand first, bottom to top, by inserting rows below each template row, so content and references below shift down. Then the remaining markers resolve against data. A list with no records keeps its template row and blanks its marker cells. Markers inside formula cells are not processed. A sole-marker cell keeps the value's type; a LocalDate, LocalDateTime or Date (read in the system time zone) becomes a date serial in the workbook's date system, with a date format only when the cell has the General format. A List value renders blank. A top-level key in data that contains dots is matched literally before path resolution. Each cell is written with its own call, so the cost grows with the number of output cells.
      Parameters:
      sheet - the sheet to fill; it is modified in place
      data - the values markers resolve against; a list marker needs a List value
      Throws:
      NullPointerException - when data is null and the sheet has a marker