Streaming Reads: Event & SAX APIs
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
Baseline date: 2026-10-09. This page was written against Apache POI 5.5.1 (the latest release on that date; the 6.0
line is announced but has not been released), Java 21 and later, Spring Boot 4.x on Spring Framework 7 and Jakarta EE
11. It is the reading counterpart of Streaming Writes with
SXSSF: where SXSSF keeps only a window of rows in memory while writing, the event APIs described here keep only the
current row in memory while reading. They are what you reach for when a catalogue upload such as the Bookshelf
loans-report.xlsx has hundreds of thousands of rows and WorkbookFactory.create would exhaust the heap.
The usermodel API (Spreadsheet Usermodel Basics) parses the
whole file into an object tree: every Row, Cell, style and string is a Java object, and all of them stay reachable
until the workbook is closed. The event APIs do the opposite. They tokenise the file and call you back for each piece
they recognise, then forget it. Nothing is kept unless your own code keeps it. The price is a lower-level, read-only,
forward-only API with no random access, no formula evaluation and no editing.
When to leave the usermodel
-
Size. The usermodel needs a multiple of the file size in heap, because the XML is expanded into objects. A few tens of megabytes of
.xlsxcan already need hundreds of megabytes of heap. There is no fixed ratio: it depends on the number of cells, on how many strings are unique and on styles. Measure your own files (see Memory and time: how to measure). -
Throughput. A SAX pass over a sheet avoids building the object model, so it is normally faster as well as smaller; measure it on your own data.
-
Read-only extraction. Import jobs, search indexing and data migration only need the values once. That is the ideal use of an event API.
-
Not when you need to edit. To change a workbook you need the usermodel (or its streaming writer SXSSF, which cannot read an existing file either). Reading with events and writing a new file with SXSSF is the usual large-file conversion pipeline.
The three event APIs
POI has one event API per file family. They share a philosophy, not a class hierarchy.
| Format | Entry classes | Callback interface | Jar |
|---|---|---|---|
|
|
|
|
|
|
The same |
|
|
|
|
|
The XLSX and XLSB handlers deliberately reuse one callback interface, so code that consumes cells (a mapper to a
Book record, for instance) works for both formats. The HSSF API is older, closer to the binary records and has a
different shape.
How SAX reading of an .xlsx works
An .xlsx file is a ZIP package (see File Formats: OOXML &
OLE2). A sheet part (/xl/worksheets/sheet1.xml) does not contain text: string cells hold an index into
/xl/sharedStrings.xml, and numeric cells hold a number whose meaning (integer, percentage, date) depends on the
style index stored in /xl/styles.xml. To turn a <c> element back into the text a person sees, the parser needs
all three parts. The classes map onto that structure:
| Class | Responsibility |
|---|---|
|
Opens the ZIP and its relationships. Open it with |
|
Locates the parts through the package relationships: |
|
A compact reader for |
|
Gives |
|
A SAX |
|
Your code. Receives rows and cells. |
The sequence of calls for one sheet is shown below. Only the parts inside the loop run per row and per cell.
Three consequences follow from the diagram:
-
The sheet is a stream you can consume only once. A second pass needs a second
getSheetsData()call. -
cell(...)is called only for cells that exist in the XML. Empty cells and entirely empty rows are skipped, so your handler has to compute gaps from the cell reference. -
The value you receive is already a formatted string, because
XSSFSheetXMLHandlerappliesDataFormatter. The raw number or date is not delivered unless you override the handler (see Pitfalls).
A complete .xlsx reader
The example reads loans-report.xlsx, the large export produced in
Streaming Writes with SXSSF, and counts the loans of
every member. Its columns are Loan, ISBN, Member, Loaned on and Fee, so column C holds the member name; row 0 is the
header. Only the sheet named
Loans is parsed.
package bookshelf;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.LinkedHashMap;
import java.util.Map;
import javax.xml.parsers.ParserConfigurationException;
import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
import org.apache.poi.openxml4j.exceptions.OpenXML4JException;
import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.openxml4j.opc.PackageAccess;
import org.apache.poi.ss.usermodel.DataFormatter;
import org.apache.poi.ss.util.CellReference;
import org.apache.poi.util.XMLHelper;
import org.apache.poi.xssf.eventusermodel.ReadOnlySharedStringsTable;
import org.apache.poi.xssf.eventusermodel.XSSFReader;
import org.apache.poi.xssf.eventusermodel.XSSFSheetXMLHandler;
import org.apache.poi.xssf.eventusermodel.XSSFSheetXMLHandler.SheetContentsHandler;
import org.apache.poi.xssf.model.StylesTable;
import org.apache.poi.xssf.usermodel.XSSFComment;
import org.xml.sax.ContentHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
/** Counts the loans of every member in loans-report.xlsx without loading the workbook into memory. */
public class LoansReportReader {
/** Collects column C (member) of every data row. */
static class LoanCounter implements SheetContentsHandler {
final Map<String, Integer> loansPerMember = new LinkedHashMap<>();
private int currentRow = -1;
private String member;
@Override
public void startRow(int rowNum) {
currentRow = rowNum;
member = null;
}
@Override
public void cell(String cellReference, String formattedValue, XSSFComment comment) {
if (cellReference == null) {
return; // some writers omit the r attribute; fall back to counting cells yourself
}
if (new CellReference(cellReference).getCol() == 2) {
member = formattedValue; // ignore the other columns
}
}
@Override
public void endRow(int rowNum) {
if (rowNum == 0 || member == null || member.isBlank()) {
return; // header row or empty row
}
loansPerMember.merge(member, 1, Integer::sum);
}
}
public static Map<String, Integer> loansPerMember(File file)
throws IOException, OpenXML4JException, SAXException, ParserConfigurationException {
try (OPCPackage pkg = OPCPackage.open(file, PackageAccess.READ)) {
XSSFReader reader = new XSSFReader(pkg);
ReadOnlySharedStringsTable strings = new ReadOnlySharedStringsTable(pkg);
StylesTable styles = reader.getStylesTable();
LoanCounter counter = new LoanCounter();
XSSFReader.SheetIterator sheets = (XSSFReader.SheetIterator) reader.getSheetsData();
while (sheets.hasNext()) {
try (InputStream sheet = sheets.next()) {
if (!"Loans".equals(sheets.getSheetName())) {
continue;
}
ContentHandler handler = new XSSFSheetXMLHandler(
styles, strings, counter, new DataFormatter(), false);
XMLReader parser = XMLHelper.newXMLReader();
parser.setContentHandler(handler);
parser.parse(new InputSource(sheet));
}
}
return counter.loansPerMember;
}
}
public static void main(String[] args) throws Exception {
loansPerMember(new File(args[0])).forEach((m, n) -> System.out.println(m + " -> " + n));
}
}
Source: XLSX2CSV in poi-examples
and the XSSFSheetXMLHandler Javadoc.
Running it against the 300,000-row loans-report.xlsx of the SXSSF page prints one line per member
(member-1 -> 375, member-2 -> 375, …). Points worth reading again in the
code:
-
OPCPackage.open(file, PackageAccess.READ)takes aFile, not anInputStream: with aFilePOI reads the ZIP entries on demand, while anInputStreamhas to be buffered completely into memory first. The same rule as forXSSFWorkbook. -
XMLHelper.newXMLReader()returns a SAX parser with external entities and DTDs disabled. Do not create aSAXParserFactoryyourself: an unsafe parser makes an uploaded file an XXE vector (see Introduction & Architecture for the security model). -
SheetIterator.getSheetName()is only valid afternext()and only for the sheet just returned. Skipping a sheet still means closing its stream, which the try-with-resources block does. -
new DataFormatter()uses the JVM default locale. On a server, pass an explicitLocaleso that1234.5is not1.234,5on one host and1,234.5on another. -
The last argument of the handler constructor,
formulasNotResults, selects what a formula cell reports:falsethe cached result (the default for data extraction),truethe formula text such asSUM(D2:D400).
Dense rows: a reusable adapter
Real import code rarely wants callbacks. It wants a List<String> per row, with empty cells as empty strings so the
column positions stay stable. The adapter below turns the three callbacks into a callback per row and fills the gaps
the parser leaves.
package bookshelf;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiConsumer;
import org.apache.poi.ss.util.CellReference;
import org.apache.poi.xssf.eventusermodel.XSSFSheetXMLHandler.SheetContentsHandler;
import org.apache.poi.xssf.usermodel.XSSFComment;
/** Turns SAX callbacks into one dense List per row: skipped (empty) cells become empty strings. */
public class RowCollector implements SheetContentsHandler {
private final BiConsumer<Integer, List<String>> rowConsumer;
private final List<String> current = new ArrayList<>();
public RowCollector(BiConsumer<Integer, List<String>> rowConsumer) {
this.rowConsumer = rowConsumer;
}
@Override
public void startRow(int rowNum) {
current.clear();
}
@Override
public void cell(String cellReference, String formattedValue, XSSFComment comment) {
int column = cellReference == null ? current.size() : new CellReference(cellReference).getCol();
while (current.size() < column) {
current.add(""); // the parser never reports empty cells, so fill the gap here
}
current.add(formattedValue == null ? "" : formattedValue);
}
@Override
public void endRow(int rowNum) {
rowConsumer.accept(rowNum, List.copyOf(current)); // 0-based row index, as in the usermodel
}
}
Source: XLSX2CSV in poi-examples, which fills the
same gaps with commas when it writes CSV.
Use it in place of LoanCounter in the previous example:
ContentHandler handler = new XSSFSheetXMLHandler(styles, strings,
new RowCollector((rowNum, cells) -> System.out.println(rowNum + " " + cells)),
new DataFormatter(Locale.ROOT), false);
For a sheet with a header row, a blank fifth column and a numeric sixth column the output looks like
1 [Ana, Dune, 2026-01-01, OPEN, , 12.5]. Mapping those lists to a Book or Loan record, validating them and
reporting errors by row number is the subject of
the section landing page’s import path; the event API only supplies the rows.
Handler callbacks and what they carry
| Callback | What it gives you |
|---|---|
|
The zero-based row index. Rows that contain no cells may never be reported, so row numbers are not consecutive. |
|
The A1 reference ( |
|
Called after the last cell of the row. This is where a row-level consumer flushes its state. |
|
Default method that ignores headers and footers. Override it to receive the sheet’s header and footer text
( |
The constructors of XSSFSheetXMLHandler have two shapes: with and without a Comments argument. Pass the comments
table (XSSFReader.SheetIterator.getSheetComments()) when you need the cell comments; otherwise the comment
parameter is always null. Comments are read into memory, one table per sheet.
Options of the event extractor
XSSFEventBasedExcelExtractor is the ready-made consumer of this API: it opens the package, runs the handler and
returns the sheet text. ExtractorFactory returns it for an .xlsx only when you call
ExtractorFactory.setThreadPrefersEventExtractors(true) (or setAllThreadsPreferEventExtractors(true)): the default is
false, which gives the usermodel-based XSSFExcelExtractor.
import java.io.File;
import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.openxml4j.opc.PackageAccess;
import org.apache.poi.xssf.extractor.XSSFEventBasedExcelExtractor;
public class BookshelfTextDump {
public static void main(String[] args) throws Exception {
try (OPCPackage pkg = OPCPackage.open(new File(args[0]), PackageAccess.READ);
XSSFEventBasedExcelExtractor extractor = new XSSFEventBasedExcelExtractor(pkg)) {
extractor.setIncludeSheetNames(true);
extractor.setFormulasNotResults(false); // cached results, not formula text
System.out.println(extractor.getText());
}
}
}
Source: XSSFEventBasedExcelExtractor Javadoc
and POI text extraction.
| Setting | Effect |
|---|---|
|
Prints each sheet name before its rows (the default). |
|
|
|
Appends the comment of each cell. |
|
Includes sheet headers and footers. |
|
Includes text in drawing text boxes. |
|
Locale for number and date formatting. |
The extractor returns the whole text as one String, so it is only as memory-friendly as the text it produces. For
millions of rows write your own SheetContentsHandler and process each row as it arrives.
Reading .xlsb with XSSFBReader
Excel Binary Workbook (.xlsb) is an OPC package like .xlsx, but its parts hold binary records instead of XML. POI
reads it through the org.apache.poi.xssf.binary package and the XSSFBReader, and it reuses the
SheetContentsHandler interface, so the consuming code does not change. As of 5.5.1 the support is limited to reading
cell text, shared strings, the styles needed for number formatting and comments:
package bookshelf;
import java.io.File;
import java.io.InputStream;
import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.openxml4j.opc.PackageAccess;
import org.apache.poi.ss.usermodel.DataFormatter;
import org.apache.poi.xssf.binary.XSSFBSharedStringsTable;
import org.apache.poi.xssf.binary.XSSFBSheetHandler;
import org.apache.poi.xssf.binary.XSSFBStylesTable;
import org.apache.poi.xssf.eventusermodel.XSSFBReader;
import org.apache.poi.xssf.eventusermodel.XSSFSheetXMLHandler.SheetContentsHandler;
import org.apache.poi.xssf.usermodel.XSSFComment;
/** Prints every cell of every sheet of an .xlsb file. */
public class XlsbLoansReader {
public static void main(String[] args) throws Exception {
try (OPCPackage pkg = OPCPackage.open(new File(args[0]), PackageAccess.READ)) {
XSSFBReader reader = new XSSFBReader(pkg);
XSSFBSharedStringsTable strings = new XSSFBSharedStringsTable(pkg);
XSSFBStylesTable styles = reader.getXSSFBStylesTable();
SheetContentsHandler printer = new SheetContentsHandler() {
@Override public void startRow(int rowNum) { System.out.println("row " + rowNum); }
@Override public void endRow(int rowNum) { }
@Override public void cell(String ref, String value, XSSFComment comment) {
System.out.println(" " + ref + " = " + value);
}
};
XSSFBReader.SheetIterator sheets = (XSSFBReader.SheetIterator) reader.getSheetsData();
while (sheets.hasNext()) {
try (InputStream sheet = sheets.next()) {
System.out.println("sheet " + sheets.getSheetName());
new XSSFBSheetHandler(sheet, styles, sheets.getXSSFBSheetComments(), strings,
printer, new DataFormatter(), false).parse();
}
}
}
}
}
Source: XSSFBReader Javadoc
and the POI source of
XSSFBEventBasedExcelExtractor,
which drives XSSFBReader and XSSFBSheetHandler the same way. The example compiles against POI 5.5.1;
no .xlsb sample file was available to run it, so verify it against one of your own files.
What POI does not provide for .xlsb: there is no usermodel (XSSFWorkbook cannot open it), no writer and no formula
evaluation. XSSFBReader only reads. To produce an .xlsb or to edit one you need Excel or another tool; the usual
server-side answer is to ask users for .xlsx and to convert once with a desktop tool if only .xlsb is available.
The HSSF event model for .xls
The binary .xls format is a stream of records (BOFRecord, BoundSheetRecord, SSTRecord, LabelSSTRecord,
NumberRecord, FormulaRecord, EOFRecord and about two hundred more). HSSFEventFactory reads that stream and hands
each record to every HSSFListener registered in an HSSFRequest. The low-level API is faithful to the file but
awkward: a listener sees records exactly as they were written, so it must reassemble strings, track the current sheet
and notice rows with missing cells. POI ships two decorators for that:
| Class | What it adds |
|---|---|
|
Wraps your listener and inserts dummy records: |
|
Tracks the |
Decorators are chained: request.addListenerForAllRecords(formatListener) where formatListener wraps
MissingRecordAwareHSSFListener, which wraps your own listener. The Bookshelf example reads a legacy
books-inventory.xls and prints one pipe-separated line per row:
package bookshelf;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.apache.poi.hssf.eventusermodel.FormatTrackingHSSFListener;
import org.apache.poi.hssf.eventusermodel.HSSFEventFactory;
import org.apache.poi.hssf.eventusermodel.HSSFListener;
import org.apache.poi.hssf.eventusermodel.HSSFRequest;
import org.apache.poi.hssf.eventusermodel.MissingRecordAwareHSSFListener;
import org.apache.poi.hssf.eventusermodel.dummyrecord.LastCellOfRowDummyRecord;
import org.apache.poi.hssf.eventusermodel.dummyrecord.MissingCellDummyRecord;
import org.apache.poi.hssf.record.BoundSheetRecord;
import org.apache.poi.hssf.record.LabelSSTRecord;
import org.apache.poi.hssf.record.NumberRecord;
import org.apache.poi.hssf.record.Record;
import org.apache.poi.hssf.record.SSTRecord;
import org.apache.poi.poifs.filesystem.POIFSFileSystem;
/** Reads the title and price columns of a legacy books-inventory.xls using the HSSF event model. */
public class HssfLoansListener implements HSSFListener {
private final List<String> row = new ArrayList<>();
private SSTRecord sst;
private FormatTrackingHSSFListener formatListener;
@Override
public void processRecord(Record record) {
if (record instanceof BoundSheetRecord sheet) {
System.out.println("sheet: " + sheet.getSheetname());
} else if (record instanceof SSTRecord table) {
sst = table;
} else if (record instanceof LabelSSTRecord label) {
put(label.getColumn(), sst.getString(label.getSSTIndex()).getString());
} else if (record instanceof NumberRecord number) {
put(number.getColumn(), formatListener.formatNumberDateCell(number));
} else if (record instanceof MissingCellDummyRecord gap) {
put(gap.getColumn(), "");
} else if (record instanceof LastCellOfRowDummyRecord) {
System.out.println(String.join(" | ", row));
row.clear();
}
}
private void put(int column, String text) {
while (row.size() <= column) {
row.add("");
}
row.set(column, text);
}
public void process(File file) throws IOException {
try (POIFSFileSystem fs = new POIFSFileSystem(file, true)) {
MissingRecordAwareHSSFListener missing = new MissingRecordAwareHSSFListener(this);
formatListener = new FormatTrackingHSSFListener(missing);
HSSFRequest request = new HSSFRequest();
request.addListenerForAllRecords(formatListener);
new HSSFEventFactory().processWorkbookEvents(request, fs);
}
}
public static void main(String[] args) throws IOException {
new HssfLoansListener().process(new File(args[0]));
}
}
Notes for the HSSF listener:
-
The
POIFSFileSystem(File, boolean readOnly)constructor withtrueopens the file read-only and reads blocks from it on demand instead of copying the whole file into the heap. -
A real listener has to handle more record types than the example does:
FormulaRecord(its string result arrives in a followingStringRecord),LabelRecord(inline strings),BoolErrRecord,RKRecord(compressed numbers, written by older producers) andMulRKRecord.XLS2CSVmrain the official examples shows each of them. -
The sheet is identified by order of appearance of the
BOFRecord(type worksheet), and its name comes from the matchingBoundSheetRecord, read earlier in the stream. Sheets that are hidden are still reported. -
The
.xlsformat caps a sheet at 65,536 rows, so the memory argument for events is much weaker than for.xlsx. The event model remains useful when a very wide or very old file would otherwise be loaded fully for one pass. -
HSSFEventFactoryand the record classes are in thepoijar, not inpoi-ooxml.
Usermodel versus event APIs
| Aspect | Usermodel (XSSFWorkbook, HSSFWorkbook) |
Event / SAX (XSSFReader, HSSFEventFactory) |
|---|---|---|
Memory |
Whole file as objects; grows with file size and with the number of distinct styles and strings |
Current row plus the shared strings table (all unique strings stay in memory) and the styles |
Access |
Random: |
Forward only, one pass, callbacks in file order |
Reads / writes |
Both |
Read only |
Empty cells |
|
Never reported; you derive gaps from references |
Values |
|
Formatted |
Formulas |
|
Cached result or formula text only; no evaluation |
Styles, merged regions, validations |
Accessible through |
Not delivered by the cell callbacks; parse the relevant XML elements yourself if you need them |
Code size |
Short and readable |
Longer; state machines and manual gap handling |
Best for |
Files up to a few tens of MB, editing, templates, random access |
Large uploads, one-pass extraction, indexing, migrations |
Memory and time: how to measure
POI does not publish a memory-per-cell figure, and any number depends on content (a million short numbers is not a million long distinct strings), so the honest advice is to measure with your own files. A reliable way:
-
Run the read in a separate JVM with a fixed heap, for example with the
-Xmx512moption on thejavacommand line. If the usermodel version fails withOutOfMemoryErrorat-Xmx512mand the event version passes, the event API is justified. This is the cheapest and most telling experiment. -
Watch the heap, not the total: enable GC logging (
-Xlog:gc*) and note the live set after a full GC while the file is being read. For the event API the live set is dominated by the shared strings table. -
Record the time of the parse alone, with a warmed-up JVM, on a file the size of your biggest expected upload.
-
Repeat with a file with many unique strings (names, ISBNs) and one with few (statuses): the shared strings table is the main variable in the event approach.
-
Use a heap dump (
jcmd <pid> GC.heap_dump) or Java Flight Recorder to see which classes hold the memory before guessing.
Pitfalls
| Pitfall | What to do |
|---|---|
Memory still grows with the shared strings table |
|
Dates arrive formatted, not as dates |
The value is the displayed text ( |
Locale-dependent output |
|
Empty cells and rows are missing |
Compute gaps from the column of |
Stream consumed once |
Open the sheet again for a second pass, or collect what you need on the first one. |
Not closing the package or streams |
Use try-with-resources for |
Unsafe XML parsers |
Use |
No Log4j provider |
POI 5.x logs through the Log4j 2 API. Without a provider on the classpath, as in a bare example, an
|
Mixing the two models |
Do not open the same large file with both |
What POI does not provide here
-
No random access or backtracking. Looking up a cell in another sheet while reading a formula needs a separate pass or a lookup structure you build.
-
No formula evaluation. Only cached results or formula text are available.
-
No object mapping or validation. There is no
@Columnmapping; the section’s import pages build the mapper on top of rows like the ones above. -
No event API for Word. The
.docxand.docfiles have usermodel APIs only; Word extraction for large files is done with the extractors, covered later in the section. -
No official event-model guide for
.xlsb. Support is limited to the Javadoc and the example classes. -
Internal details are only in the Javadoc and the examples. The official spreadsheet guide documents the event API with a short how-to section; the behaviour of
XSSFSheetXMLHandlerinternals (for instance how inline strings, rich text andt="str"cells are mapped) has to be read from the source.
Changes between versions
| Version | Change relevant to event reading |
|---|---|
4.x → 5.x |
POI 5.x logs through the Log4j 2 API instead of its own logger, and XML parsing goes through the hardened |
5.5.1 (as of 2026-10-09) |
The latest release. The event API for |
6.0 (planned, not released as of 2026-10-09) |
The announced 6.0 line removes APIs deprecated in 5.x and changes |
For the history of the whole section, see the migration page, covered later in the section.
Related pages
-
Spreadsheet Usermodel Basics for the in-memory API.
-
Streaming Writes with SXSSF for the matching writer.
-
Dates, Numbers & DataFormatter for the formatting the handler applies.
-
Legacy .xls with HSSF for the usermodel of old files.
-
File Formats: OOXML & OLE2 for the package structure.
References
-
Spreadsheet How-To, event API: https://poi.apache.org/components/spreadsheet/how-to.html#event_api
-
Spreadsheet components: https://poi.apache.org/components/spreadsheet/
-
Text extraction: https://poi.apache.org/text-extraction.html
-
Javadoc,
XSSFReader: https://poi.apache.org/apidocs/dev/org/apache/poi/xssf/eventusermodel/XSSFReader.html -
Javadoc,
XSSFSheetXMLHandler: https://poi.apache.org/apidocs/dev/org/apache/poi/xssf/eventusermodel/XSSFSheetXMLHandler.html -
Javadoc,
ReadOnlySharedStringsTable: https://poi.apache.org/apidocs/dev/org/apache/poi/xssf/eventusermodel/ReadOnlySharedStringsTable.html -
Javadoc,
XSSFBReader: https://poi.apache.org/apidocs/dev/org/apache/poi/xssf/eventusermodel/XSSFBReader.html -
Javadoc,
HSSFEventFactory: https://poi.apache.org/apidocs/dev/org/apache/poi/hssf/eventusermodel/HSSFEventFactory.html -
Javadoc,
MissingRecordAwareHSSFListener: https://poi.apache.org/apidocs/dev/org/apache/poi/hssf/eventusermodel/MissingRecordAwareHSSFListener.html -
Javadoc,
FormatTrackingHSSFListener: https://poi.apache.org/apidocs/dev/org/apache/poi/hssf/eventusermodel/FormatTrackingHSSFListener.html -
Javadoc,
XMLHelper: https://poi.apache.org/apidocs/dev/org/apache/poi/util/XMLHelper.html -
Official examples,
xssf/eventusermodel: https://github.com/apache/poi/tree/trunk/poi-examples/src/main/java/org/apache/poi/examples/xssf/eventusermodel -
Official examples,
hssf/eventusermodel: https://github.com/apache/poi/tree/trunk/poi-examples/src/main/java/org/apache/poi/examples/hssf/eventusermodel -
Security: https://poi.apache.org/security.html
-
Release changes: https://poi.apache.org/changes.html