Text Extraction & Apache Tika

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 in development and has not been released), Java 21 and later, Spring Boot 4.x on Spring Framework 7 and Jakarta EE 11, and Apache Tika 4.1.0 (the latest release on Maven Central on that date). Many programs do not need to build a Workbook or an XWPFDocument at all: they only need the text of an uploaded file, for a search index, a preview, a spam check or a "does this file contain the string we expect" validation. POI ships a family of text extractors for exactly that. This page explains how they are chosen (ExtractorFactory), what each one does, how to keep memory low, how to reach embedded documents, and where POI stops and Apache Tika is the better tool. The examples use the Bookshelf scenario described in the section landing page: books-inventory.xlsx, books-inventory.xls, loan-letter.docx and loan-letter.doc.

The component map is in Introduction & Architecture, the jars in Setup & Dependencies, and the two container families (OOXML and OLE2) in File Formats: OOXML & OLE2. The extractors sit on top of the same readers described in Spreadsheets with the usermodel API, Streaming Reads: Event and SAX APIs, Word Documents with XWPF and Legacy .doc with HWPF.

What a text extractor is, and when to use one

A text extractor turns a file into a single String (plus, for some formats, a few more targeted accessors) and exposes the document metadata. It is a read-only convenience layer: it opens the file with the normal POI readers, walks the content in document order and concatenates the text. Compared with writing the traversal yourself:

  • It is a few lines of code and the same lines work for every supported format.

  • It follows the quirks of each format (shared strings, text boxes, headers and footers, cell comments) so you do not have to.

  • It returns text, not structure. Fonts, colours, number formats, merged regions, table geometry and cell addresses are gone. If you need to know that a value came from cell C7, use the usermodel or the event API instead.

The official POI position is explicit: for turnkey extraction "including the latest support, styles etc." the project strongly advises Apache Tika, which builds on POI. The POI extractors are the right choice when you want something simple and stand-alone, when you must modify the behaviour heavily, or when you are already inside a POI-only application and one more dependency is not welcome.

Need POI extractor Better tool

Text of one known format, no extra dependencies

XSSFExcelExtractor, XWPFWordExtractor, WordExtractor, ExcelExtractor

-

Text of an upload whose format is not known in advance

ExtractorFactory (Office files only)

Tika (any format, content-based detection)

Low-memory text of a large .xlsx

XSSFEventBasedExcelExtractor

The event API directly when you need cell addresses (see Streaming Reads)

Text plus structure (headings, tables, cell values with types)

Not an extractor job

The usermodel API (XWPFDocument, Workbook)

PDF, HTML, email, archives, images with OCR

Not supported by POI

Tika

Rich output (XHTML with structure) for indexing

Not supported by the POI extractors

Tika

The extractor family

All POI extractors implement org.apache.poi.extractor.POITextExtractor, an interface in the poi core jar. In POI 5.x it is a Closeable; the common surface is small.

Member Meaning

String getText()

The text of the whole document. For many uses this is all you need.

POITextExtractor getMetadataTextExtractor()

A second extractor whose getText() returns the document properties (title, author, dates, application) as key = value lines.

Object getDocument()

The underlying POI object (XSSFWorkbook, XWPFDocument, HWPFDocument …​). Useful to call the full API after a quick look at the text.

close(), setCloseFilesystem(boolean), isCloseFilesystem()

Releases the file. By default closing the extractor also closes the underlying package or file system, so always use try-with-resources.

Two sub-interfaces add the metadata of the two container families:

  • org.apache.poi.extractor.POIOLE2TextExtractor for OLE2 files (.xls, .doc, .ppt, .pub, .vsd): adds getSummaryInformation() and getDocSummaryInformation() (HPSF).

  • org.apache.poi.ooxml.extractor.POIXMLTextExtractor for OOXML files: adds getCoreProperties(), getExtendedProperties(), getCustomProperties() and getPackage().

In POI 4.x these types were abstract classes (POIOLE2TextExtractor and POIXMLTextExtractor in org.apache.poi and org.apache.poi.ooxml.extractor); in 5.5.1 all three are interfaces. Code written for 4.x that does extends POITextExtractor has to change to implements. The properties themselves are the subject of a later page; here they are only a side effect of extraction.

The concrete classes, by format and jar:

Format Class Jar Notes

.xlsx .xlsm

org.apache.poi.xssf.extractor.XSSFExcelExtractor

poi-ooxml

Loads an XSSFWorkbook. Options for sheet names, formulas, comments, headers and footers and text boxes.

.xlsx .xlsm

org.apache.poi.xssf.extractor.XSSFEventBasedExcelExtractor

poi-ooxml

SAX-based, no workbook object model, lower memory. Has the same option setters.

.xls

org.apache.poi.hssf.extractor.ExcelExtractor

poi

Loads an HSSFWorkbook. Also has setIncludeBlankCells.

.xls

org.apache.poi.hssf.extractor.EventBasedExcelExtractor

poi

Based on the HSSF event user model; low memory, but complex and shared formulas may not be printed correctly.

.docx .docm

org.apache.poi.xwpf.extractor.XWPFWordExtractor

poi-ooxml

Paragraphs, tables, headers, footers, footnotes. setFetchHyperlinks adds link targets.

.doc (Word 97 to 2003)

org.apache.poi.hwpf.extractor.WordExtractor

poi-scratchpad

Paragraph array, header text, footer text, footnotes, endnotes, comments, text boxes.

.doc (Word 6 and Word 95)

org.apache.poi.hwpf.extractor.Word6Extractor

poi-scratchpad

Simple text of the older formats.

.ppt .pptx

org.apache.poi.sl.extractor.SlideShowExtractor

poi-scratchpad (.ppt), poi-ooxml (.pptx)

Slides, notes, or both. PowerPoint is outside this section’s scope.

.pub, .vsd

PublisherExtractor, VisioTextExtractor

poi-scratchpad

Mentioned for completeness.

The two Excel extractors for each family implement the small interface org.apache.poi.ss.extractor.ExcelExtractor (setIncludeSheetNames, setFormulasNotResults, setIncludeHeadersFooters, setIncludeCellComments, getText), so code that only toggles those options does not care which family it received.

ExtractorFactory: let POI pick the extractor

org.apache.poi.extractor.ExtractorFactory does for extraction what WorkbookFactory does for workbooks. You pass a File, an InputStream or a POIFSFileSystem; it inspects the first bytes of the content (it does not look at the file name) with FileMagic, decides whether the stream is OLE2 or OOXML, and returns the matching extractor as a POITextExtractor.

import java.io.IOException;
import java.nio.file.Path;
import org.apache.poi.EmptyFileException;
import org.apache.poi.extractor.ExtractorFactory;
import org.apache.poi.extractor.POITextExtractor;

public class ExtractAnyFile {

    public static void main(String[] args) throws IOException {
        for (String name : args) {
            Path path = Path.of(name);
            try (POITextExtractor extractor = ExtractorFactory.createExtractor(path.toFile())) {
                String text = extractor.getText();
                System.out.printf("%s -> %s, %d characters%n", path.getFileName(),
                        extractor.getClass().getSimpleName(), text.length());
                System.out.println(text.strip());
                System.out.println("--- metadata ---");
                System.out.println(extractor.getMetadataTextExtractor().getText().strip());
            } catch (EmptyFileException e) {
                System.out.println(path.getFileName() + " -> empty file");
            } catch (IOException e) {
                System.out.println(path.getFileName() + " -> not extractable: " + e.getMessage());
            }
            System.out.println("==========");
        }
    }
}

Running it against the Bookshelf files (abridged; the locale of the machine decides the decimal separator, see below):

books-inventory.xlsx -> XSSFExcelExtractor, 90 characters
Books
Bookshelf inventory
Title	Price	Copies
Dune	12.5	3
Emma	9.9	5
Total copies	8
Empty
--- metadata ---
Created = Fri Oct 09 20:15:37 UTC 2026
Creator = Apache POI
...
books-inventory.xls -> ExcelExtractor, 28 characters
...
loan-letter.docx -> XWPFWordExtractor, 82 characters
Dear Ada Reader,
The book "Dune" is due on 2026-10-30.
Title	Due
Dune	2026-10-30
...
loan-letter.doc -> WordExtractor, 54 characters
...
notes.txt -> not extractable: Can't create extractor - unsupported file type: UNKNOWN

Things to know about the factory:

  • Detection is by content, and limited to what POI supports. A file that is neither OLE2 nor OOXML (plain text, PDF, an HTML page renamed .xlsx) ends in an IOException with the message "unsupported file type". An empty file throws EmptyFileException. An OOXML file that is not a Word, Excel or PowerPoint package is rejected as well.

  • Providers come from the jars on the class path. The factory delegates to providers: MainExtractorFactory (in the core jar, .xls), POIXMLExtractorFactory (in poi-ooxml, OOXML) and OLE2ScratchpadExtractorFactory (in poi-scratchpad, .doc, .ppt, .pub, .vsd). If a jar is missing, its formats are simply not recognised, and the error message names the providers that were found. With poi-scratchpad removed, the example above reports the .doc as "not extractable", and with poi-ooxml removed it does the same for the .xlsx. addProvider and removeProvider let an application register its own.

  • Return type is the interface. Use instanceof (pattern matching in Java 21) to reach format-specific methods such as WordExtractor.getHeaderText().

  • Password-protected files. There are overloads taking a password (createExtractor(File, String), createExtractor(InputStream, String)); an encrypted file without a password fails with EncryptedDocumentException. Encryption is covered by a later page.

  • An OPCPackage overload no longer exists on ExtractorFactory in 5.5.1. The POI text-extraction page still says the factory accepts "a OOXML Package". In 5.5.1 the overloads are File, InputStream, POIFSFileSystem and DirectoryNode (plus the password variants). To start from an already opened package, call POIXMLExtractorFactory.create(OPCPackage) (a class in org.apache.poi.ooxml.extractor) or construct the specific extractor directly.

  • Prefer File over InputStream when the file is on disk: for OOXML, POI can then open the zip lazily from the file instead of buffering the stream, the same advice as for WorkbookFactory.

Excel extraction in detail

Both OOXML Excel extractors write one line per row, with cells separated by a tab, and return the sheets in workbook order. The options are the same:

Setter Default Effect

setIncludeSheetNames(boolean)

true

Prints the sheet name before its rows.

setFormulasNotResults(boolean)

false

Prints the formula text (SUM(C2:C3)) instead of the cached result.

setIncludeCellComments(boolean)

false

Appends the comment text and its author after the commented cell (Comment by author: text).

setIncludeHeadersFooters(boolean)

true

Prints the sheet header and footer (left, centre and right parts).

setIncludeTextBoxes(boolean)

true

Prints the text of text boxes in the drawing layer.

setLocale(Locale)

not set (JVM default)

Locale used when formatting numbers and dates as the user would see them.

setConcatenatePhoneticRuns(boolean) (event extractor only)

see Javadoc

Appends the phonetic (furigana) text of Japanese strings.

Numbers and dates are formatted the way Excel would display them (through DataFormatter, see Dates, Numbers & DataFormatter), so a date cell comes out as the text its number format produces rather than as a serial number.

import java.nio.file.Path;
import java.util.Locale;
import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.openxml4j.opc.PackageAccess;
import org.apache.poi.xssf.extractor.XSSFEventBasedExcelExtractor;
import org.apache.poi.xssf.extractor.XSSFExcelExtractor;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;

public class ExcelOptions {

    public static void main(String[] args) throws Exception {
        Path file = Path.of(args[0]);

        // DOM-based: loads the whole workbook as XSSFWorkbook
        try (XSSFWorkbook workbook = new XSSFWorkbook(OPCPackage.open(file.toFile(), PackageAccess.READ));
             XSSFExcelExtractor extractor = new XSSFExcelExtractor(workbook)) {
            extractor.setIncludeSheetNames(true);
            extractor.setIncludeCellComments(true);
            extractor.setIncludeHeadersFooters(true);
            extractor.setFormulasNotResults(false);
            extractor.setLocale(Locale.ROOT);
            System.out.println("[DOM, results]");
            System.out.println(extractor.getText());
        }

        // SAX-based: streams the sheets, no workbook object model
        try (OPCPackage pkg = OPCPackage.open(file.toFile(), PackageAccess.READ);
             XSSFEventBasedExcelExtractor extractor = new XSSFEventBasedExcelExtractor(pkg)) {
            extractor.setIncludeSheetNames(false);
            extractor.setFormulasNotResults(true);
            System.out.println("[SAX, formulas]");
            System.out.println(extractor.getText());
        }
    }
}

Output for books-inventory.xlsx (a comment on the cell with "Dune", and a SUM formula in the last row):

[DOM, results]
Books
Bookshelf inventory
Title	Price	Copies
Dune Comment by : Check the second copy	12.5	3
Emma	9.9	5
Total copies	8
Empty

[SAX, formulas]
Bookshelf inventory
Title	Price	Copies
Dune	12,5	3
Emma	9,9	5
Total copies	SUM(C2:C3)

The output shows several behaviours worth knowing:

  • The comment’s author was empty in this fixture, hence "Comment by :". Comments are off by default.

  • In the DOM run the sheet names are printed (Books, Empty) and the header text "Bookshelf inventory" is printed under the sheet name; in the SAX run both were requested off or left at their defaults.

  • The SAX run used the machine’s default locale (a comma as decimal separator on the machine that produced this output); the DOM run was told Locale.ROOT. Always set the locale explicitly if the extracted text is compared, indexed or stored.

  • The formula result is the cached value stored in the file. POI extractors never recalculate. A workbook written by POI without calling the formula evaluator has no cached result, and the formula cell extracts as 0 or as an empty string (see Formulas & Evaluation). Use setFormulasNotResults(true) to get the formula text instead.

DOM or SAX?

Aspect XSSFExcelExtractor XSSFEventBasedExcelExtractor

Memory

Whole workbook object model (several times the size of the XML)

Streams each sheet; shared strings and styles are still loaded

Speed

Slower for large files

Faster for large files

Fidelity

Closest to what the usermodel shows

Same text for normal cells; fewer special cases

Typical use

Small and medium uploads, tools that already have an XSSFWorkbook

Indexing, validation of large uploads in a server

The memory behaviour is the one described in Streaming Reads: Event and SAX APIs: the event extractor avoids the sheet XML tree but still reads the shared string table. For .xls the equivalent pair is ExcelExtractor and EventBasedExcelExtractor. The POI documentation warns that the event-based .xls extractor "will have problems correctly outputting more complex formulas" because it sees records one at a time and has no access to shared and complex formula records.

Choosing the event extractors globally: setThreadPrefersEventExtractors

ExtractorFactory normally returns the DOM extractor for .xlsx. A server that extracts text from untrusted uploads may prefer the event-based one. Instead of constructing it by hand, you can change the factory’s choice:

Method Effect

setThreadPrefersEventExtractors(boolean)

Affects only the calling thread. Backed by a ThreadLocal.

removeThreadPrefersEventExtractorsSetting()

Clears the thread-local value for the calling thread (important with pooled threads).

setAllThreadsPreferEventExtractors(Boolean)

Global override for every thread. null removes the override again.

getPreferEventExtractor()

The effective value for the calling thread.

getThreadPrefersEventExtractors(), getAllThreadsPreferEventExtractors()

The two settings individually.

The global value, when not null, wins over the per-thread value. The same static methods exist on POIXMLExtractorFactory, which is where the choice is applied.

import java.io.IOException;
import java.nio.file.Path;
import org.apache.poi.extractor.ExtractorFactory;
import org.apache.poi.extractor.POITextExtractor;

public class PreferEvent {

    static String extract(Path file) throws IOException {
        try (POITextExtractor extractor = ExtractorFactory.createExtractor(file.toFile())) {
            return extractor.getClass().getSimpleName();
        }
    }

    public static void main(String[] args) throws Exception {
        Path xlsx = Path.of(args[0]);
        System.out.println("default            : " + extract(xlsx));

        ExtractorFactory.setThreadPrefersEventExtractors(true);
        try {
            System.out.println("this thread, event : " + extract(xlsx));
            Thread other = new Thread(() -> {
                try {
                    System.out.println("other thread       : " + extract(xlsx));
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            });
            other.start();
            other.join();
        } finally {
            ExtractorFactory.removeThreadPrefersEventExtractorsSetting();
        }

        ExtractorFactory.setAllThreadsPreferEventExtractors(true);
        try {
            Thread other = new Thread(() -> {
                try {
                    System.out.println("all threads, other : " + extract(xlsx));
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            });
            other.start();
            other.join();
            System.out.println("getPreferEventExtractor: " + ExtractorFactory.getPreferEventExtractor());
        } finally {
            ExtractorFactory.setAllThreadsPreferEventExtractors(null);
        }
        System.out.println("after reset        : " + extract(xlsx));
    }
}

Output:

default            : XSSFExcelExtractor
this thread, event : XSSFEventBasedExcelExtractor
other thread       : XSSFExcelExtractor
all threads, other : XSSFEventBasedExcelExtractor
getPreferEventExtractor: true
after reset        : XSSFExcelExtractor

Two practical consequences. First, because the setting is a thread-local, a request filter that sets it must clear it in finally, or the next request served by that pooled thread inherits the choice. Second, the preference only affects the extractor class that ExtractorFactory returns for .xlsx; code that casts the result to XSSFExcelExtractor will fail with a ClassCastException when the event variant is returned, so program against POITextExtractor or the ss.extractor.ExcelExtractor interface. Constructing the extractor you want directly is always an alternative that needs no global state.

Word extraction in detail

XWPFWordExtractor walks the body in document order (paragraphs and tables interleaved, which is why the table text in the output above comes after the two paragraphs), then the headers and footers, footnotes and endnotes. Table cells are separated by a tab. setFetchHyperlinks(true) appends the target of each hyperlink after its text, and setConcatenatePhoneticRuns controls the phonetic text of East Asian runs. WordExtractor for .doc returns the same information in pieces:

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import org.apache.poi.hwpf.HWPFDocument;
import org.apache.poi.hwpf.extractor.WordExtractor;
import org.apache.poi.xwpf.extractor.XWPFWordExtractor;
import org.apache.poi.xwpf.usermodel.XWPFDocument;

public class WordExtraction {

    public static void main(String[] args) throws IOException {
        Path docx = Path.of(args[0]);
        Path doc = Path.of(args[1]);

        try (InputStream in = Files.newInputStream(docx);
             XWPFDocument document = new XWPFDocument(in);
             XWPFWordExtractor extractor = new XWPFWordExtractor(document)) {
            extractor.setFetchHyperlinks(true);
            System.out.println("[docx]");
            System.out.println(extractor.getText());
        }

        try (InputStream in = Files.newInputStream(doc);
             HWPFDocument document = new HWPFDocument(in);
             WordExtractor extractor = new WordExtractor(document)) {
            System.out.println("[doc]");
            for (String paragraph : extractor.getParagraphText()) {
                System.out.print(paragraph);
            }
            System.out.println("header: [" + extractor.getHeaderText().strip() + "]");
            System.out.println("footer: [" + extractor.getFooterText().strip() + "]");
        }
    }
}

Output for loan-letter.docx and loan-letter.doc:

[docx]
Dear Ada Reader,
The book "Dune" is due on 2026-10-30.
Title	Due
Dune	2026-10-30

[doc]
Dear Ada Reader,
The book Dune is due on 2026-10-30.
header: []
footer: []

The WordExtractor accessors are getParagraphText() (one array element per paragraph, each ending with the paragraph mark), getHeaderText(), getFooterText(), getFootnoteText(), getEndnoteText(), getCommentsText(), getMainTextboxText() and getTextFromPieces() (the raw text of the file’s text pieces, including field codes). getText() returns the document text as one string. The static WordExtractor.stripFields removes Word field codes from a string. HWPF is in the orphaned, thread-unsafe poi-scratchpad jar (see Legacy .doc with HWPF), and .doc extraction is exactly where untrusted input is most likely to fail in odd ways; treat any RuntimeException from a legacy extractor as "not extractable" and not as a bug in your code.

What none of the Word extractors give you: formatting, heading levels, list numbering as typed in Word, images or any layout. They also do not evaluate field results beyond what is stored in the document. For anything structural, read the document with XWPFDocument (see Word Documents with XWPF).

Embedded documents

Office files can contain other Office files: a Word table whose data lives in an embedded spreadsheet, a worksheet with an embedded Word contract, a PowerPoint with an embedded chart workbook. A search index that skips them misses text, and a malware or data-leak filter that skips them misses payloads. How you reach them depends on the container.

  • OLE2 container (.xls, .doc, .ppt). The POI documentation shows the pattern: create the outer extractor, then ask ExtractorFactory.getEmbeddedDocsTextExtractors(POIOLE2TextExtractor) for an array of extractors, one per embedded Excel, Word, PowerPoint or Visio object. The method does not exist for OOXML extractors, and it only returns objects POI can open (an embedded PDF or a picture is ignored). The POI documentation page still shows the old, misspelled name getEmbededDocsTextExtractors; 5.5.1 has only getEmbeddedDocsTextExtractors.

  • OOXML container. Embedded files are ordinary package parts, typically under /xl/embeddings/, /word/embeddings/ or /ppt/embeddings/: an embedded .docx or .xlsx is stored as itself, and a non-Office file or an OLE object is stored as oleObject1.bin. Iterate the parts through the package and hand each stream to ExtractorFactory.

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
import java.util.regex.Pattern;
import org.apache.poi.extractor.ExtractorFactory;
import org.apache.poi.extractor.POIOLE2TextExtractor;
import org.apache.poi.extractor.POITextExtractor;
import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.openxml4j.opc.PackageAccess;
import org.apache.poi.openxml4j.opc.PackagePart;

public class EmbeddedDocuments {

    /** OOXML container: the embedded files are ordinary package parts. */
    static void ooxml(Path file) throws Exception {
        try (OPCPackage pkg = OPCPackage.open(file.toFile(), PackageAccess.READ)) {
            for (PackagePart part : pkg.getPartsByName(Pattern.compile("/(xl|word|ppt)/embeddings/.*"))) {
                System.out.println("embedded part " + part.getPartName() + " (" + part.getContentType() + ")");
                try (InputStream in = part.getInputStream();
                     POITextExtractor embedded = ExtractorFactory.createExtractor(in)) {
                    System.out.println(embedded.getClass().getSimpleName() + ": " + embedded.getText().strip());
                } catch (IOException e) {
                    System.out.println("  not an Office document: " + e.getMessage());
                }
            }
        }
    }

    /** OLE2 container (.xls, .doc, .ppt): ExtractorFactory finds Excel, Word, PowerPoint and Visio objects. */
    static void ole2(Path file) throws IOException {
        try (POITextExtractor outer = ExtractorFactory.createExtractor(file.toFile())) {
            if (outer instanceof POIOLE2TextExtractor ole2) {
                POITextExtractor[] embedded = ExtractorFactory.getEmbeddedDocsTextExtractors(ole2);
                for (POITextExtractor extractor : embedded) {
                    try (extractor) {
                        System.out.println("embedded " + extractor.getClass().getSimpleName()
                                + ": " + extractor.getText().strip());
                    }
                }
                System.out.println(embedded.length + " embedded document(s)");
            }
        }
    }

    public static void main(String[] args) throws Exception {
        ooxml(Path.of(args[0]));
        ole2(Path.of(args[1]));
    }
}

Run against a books-inventory.xlsx that has a loan-letter.docx embedded (the fixture added the document as a package part with a relationship) and a books-inventory.xls without embedded objects:

embedded part /xl/embeddings/Microsoft_Word_Document1.docx (application/vnd.openxmlformats-officedocument.wordprocessingml.document)
XWPFWordExtractor: Dear Ada Reader,
The book "Dune" is due on 2026-10-30.
Title	Due
Dune	2026-10-30
0 embedded document(s)

The ole2 branch was compiled and run on a file without embedded objects; the file with embedded OLE2 documents was not available for the test. The code is the pattern from the POI documentation, adapted to the 5.x method name and to try-with-resources.

Embedded content is also where hostile files hide recursion: an archive whose embedded file contains the same archive. Cap the depth you follow, count the extractors you create, and count the characters you collect.

Memory, performance and limits

Concern What to do

Large .xlsx

Use XSSFEventBasedExcelExtractor (directly, or through setThreadPrefersEventExtractors). The DOM extractor builds an XSSFWorkbook, which is heavy for big sheets. The shared string table is loaded by both.

Large .xls, large .doc

There is no streaming Word extractor. EventBasedExcelExtractor exists for .xls. Both legacy formats are read fully into memory by POIFS unless you open them from a File.

Inputs on disk

Pass a File to ExtractorFactory or open the OPCPackage from a File, not from an InputStream.

Text size

ZipSecureFile.setMaxTextSize caps the characters an OOXML extractor will return; POIXMLTextExtractor checks it while building the text and fails instead of returning gigabytes. Related limits are ZipSecureFile.setMinInflateRatio and setMaxEntrySize, and IOUtils.setByteArrayMaxOverride for OLE2 records.

Time

POI has no timeout. Run extraction in a worker with a deadline and a thread you can abandon, or in a separate process.

Decimal and date format

Pass an explicit Locale.

Thread safety

One extractor per thread; do not share an extractor or its document.

The limits are described together, with the recommended values, in the Security & Untrusted Files page of this section and in the official POI security guidance. The most important rule for extraction is the same as for any other parsing: an uploaded file is hostile input, extraction can throw any Throwable (including OutOfMemoryError and StackOverflowError), and a server should run it with bounded memory, a deadline and, for the highest-risk deployments, in a separate process. Since 5.1.0 POI also refuses OOXML parts that contain a DOCTYPE, which blocks the classic XML external entity attacks at the parser.

Extraction also copies document content into your logs and index. Do not log whole extracted texts, and apply the same access rules to the index as to the original file.

Locale and number formatting

Because the extractors format values like the application does, the same file extracts differently on two machines with different default locales (12.5 or 12,5; 10/9/2026 or 09.10.2026). Call setLocale where the extractor offers it, and set the default locale of the extraction worker (Locale.setDefault is global; prefer the setter) so the text of a file is reproducible. For a search index it is often better to normalise afterwards than to depend on a display format at all; if you need exact numbers and dates, read the cells with the usermodel API.

Apache Tika

For a complete guide to Tika itself (detection, parsing, metadata, tika-server and Tika Pipes) see the Apache Tika section; this section covers only what a reader of Office files needs.

Apache Tika (https://tika.apache.org/) is the Apache project for detecting and extracting content from many file formats with one API. It is a separate project with its own release cycle, and its Microsoft Office parsers are built on Apache POI. POI’s own documentation recommends it for turnkey extraction. On 2026-10-09 the latest release on Maven Central was 4.1.0 (published 2026-09-29), which requires Java 17; the 3.3.x line remains supported and continues to receive bug fixes. Tika 4.0 is a major release with breaking changes, so check the migration guide before moving an existing integration from the 2.x or 3.x API.

What Tika adds over the POI extractors

Capability POI extractors Tika

Formats

OOXML and OLE2 Office files (Word, Excel, PowerPoint, Publisher, Visio)

Hundreds: Office, OpenDocument, PDF, HTML, XML, e-mail, archives, images, audio and video metadata, source code

Detection

FileMagic for OLE2 or OOXML only

Content-based media type detection (magic bytes, container inspection, file name hint, declared type) with tika.detect and the Detector API

Auto selection of the parser

ExtractorFactory, Office only

AutoDetectParser

Output

String

SAX events: plain text, XHTML (with structure) or other handlers; a Metadata object

Metadata

Document properties as text

Normalised keys (dc:title, dcterms:created, Content-Type …​) for all formats

Embedded documents

OLE2: getEmbeddedDocsTextExtractors. OOXML: manual

Recursive parsing of embedded files and attachments through a ParseContext

Encrypted files

Password overloads on the factory

Password via a PasswordProvider in the ParseContext

OCR

No

Optional through an OCR module (Tesseract)

Failure isolation

In-process

Tika 4 moves parsing into crash-isolated forked processes in tika-server, tika-app and tika-grpc

Dependencies

POI jars only

A large dependency tree when the standard parser package is used

When to use Tika, and when not

Use Tika when:

  • the upload endpoint accepts more than Word and Excel (PDF, HTML, plain text, ODF), or the file type is not trusted and must be detected rather than inferred from the extension. Tika’s detection does not rely on the extension alone;

  • the goal is search indexing (Apache Lucene, Solr, OpenSearch or Elasticsearch ingest pipelines): Tika’s output and metadata keys are the usual input of these systems, and its parsers keep up with new Office variants;

  • you need text from embedded documents without writing the recursion yourself;

  • you need a process boundary: tika-server and the 4.x forked parsing exist because parsing of untrusted content is the riskiest part of a document pipeline.

Stay with the POI extractors when:

  • the application already depends on POI and handles only .xlsx, .docx, .xls and .doc, and the added dependency weight (Tika’s standard parser package pulls in dozens of artifacts) is not justified;

  • you want the text and then the structure of the same file with one open (getDocument() returns the XWPFDocument or XSSFWorkbook you can keep using);

  • you need full control over what is extracted and how.

Move to the usermodel or event APIs, not to Tika, when you need values with their types and cell addresses: Tika produces text and metadata, not a spreadsheet model. For a validation of an Excel import, the pipeline described in the Bookshelf import pages reads cells; text extraction is only a pre-check.

Using Tika from Maven

Tika is split into a small core and parser modules. The tika-parsers-standard-package artifact is a POM that pulls the standard parser modules (Microsoft, PDF, text, HTML, XML, packaging and more); it has packaging pom, so declare it with <type>pom</type>. In a project that already declares POI, keep a single POI version: Tika’s Microsoft module brings its own POI dependency, and the rule from Setup & Dependencies (never mix POI jars of different versions) still applies. Check with mvn dependency:tree that every org.apache.poi artifact has the same version.

<dependency>
    <groupId>org.apache.tika</groupId>
    <artifactId>tika-core</artifactId>
    <version>4.1.0</version>
</dependency>
<dependency>
    <groupId>org.apache.tika</groupId>
    <artifactId>tika-parsers-standard-package</artifactId>
    <version>4.1.0</version>
    <type>pom</type>
</dependency>

Detection and extraction with Tika

The Tika facade class detects the type and returns the text in one call; AutoDetectParser gives control over the handler and the metadata. In Tika 4.x the parser methods take a TikaInputStream (not a plain InputStream), which is Tika’s own stream wrapper that can spool to disk and reopen the content for detection and parsing.

import java.nio.file.Path;
import org.apache.tika.Tika;
import org.apache.tika.io.TikaInputStream;
import org.apache.tika.metadata.HttpHeaders;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.metadata.TikaCoreProperties;
import org.apache.tika.parser.AutoDetectParser;
import org.apache.tika.parser.ParseContext;
import org.apache.tika.sax.BodyContentHandler;

public class TikaExtraction {

    public static void main(String[] args) throws Exception {
        Path file = Path.of(args[0]);

        // Facade: detection and plain-text extraction in two calls
        Tika tika = new Tika();
        System.out.println("detected: " + tika.detect(file));
        tika.setMaxStringLength(100_000);
        System.out.println(tika.parseToString(file).strip());

        // Parser API: text and metadata, with a write limit against huge documents
        AutoDetectParser parser = new AutoDetectParser();
        BodyContentHandler handler = new BodyContentHandler(100_000);
        Metadata metadata = new Metadata();
        try (TikaInputStream in = TikaInputStream.get(file)) {
            parser.parse(in, handler, metadata, new ParseContext());
        }
        System.out.println("Content-Type: " + metadata.get(HttpHeaders.CONTENT_TYPE));
        System.out.println("title: " + metadata.get(TikaCoreProperties.TITLE));
        System.out.println(handler.toString().strip());
    }
}

Output for loan-letter.doc (the .xlsx run printed the same detection of the spreadsheet media type, followed by the sheets, the comment, and the text of the embedded loan-letter.docx without any extra code):

detected: application/msword
Dear Ada Reader,

The book Dune is due on 2026-10-30.
Content-Type: application/msword
title: null
Dear Ada Reader,

The book Dune is due on 2026-10-30.

Points that matter in production:

  • BodyContentHandler(100_000) and Tika.setMaxStringLength cap the extracted characters; the limit in the example is an arbitrary value, so choose one that fits the largest document you are willing to index. Always set an explicit cap for uploads.

  • The facade creates a parser (and loads its configuration) per Tika instance. Create one Tika or AutoDetectParser and reuse it; parsers are thread-safe, handlers and Metadata objects are not.

  • Tika detects the type from content, then parses. A file with an .xlsx extension but PDF content is parsed as a PDF. If your application accepts only Excel, check the detected media type against an allow-list (see the formats table in File Formats: OOXML & OLE2) before accepting the file.

  • Treat Tika like any other parser of untrusted data. The 4.x line recommends and supports out-of-process parsing for that reason. A parser crash, an endless loop or an out-of-memory error should not take the application down.

Summary

  • All POI text extractors implement POITextExtractor (getText, getMetadataTextExtractor, close); OLE2 extractors add HPSF metadata, OOXML extractors add the package properties.

  • ExtractorFactory.createExtractor picks the extractor from the content, using the providers of the jars on the class path: poi for .xls, poi-ooxml for OOXML, poi-scratchpad for .doc and the other legacy formats.

  • Excel: XSSFExcelExtractor (DOM) or XSSFEventBasedExcelExtractor (SAX, low memory) for .xlsx; ExcelExtractor or EventBasedExcelExtractor for .xls. Formula results are the cached values; set the Locale explicitly.

  • setThreadPrefersEventExtractors and setAllThreadsPreferEventExtractors make the factory return the event-based .xlsx extractor; remove the thread setting in finally.

  • Word: XWPFWordExtractor for .docx; WordExtractor for .doc. Text only, no structure, no layout.

  • Embedded documents: getEmbeddedDocsTextExtractors for OLE2 containers, package parts under embeddings/ for OOXML.

  • Treat extraction as parsing of hostile input: size, time and memory limits, a worker you can abandon, a separate process when the risk is high.

  • For many formats, content-based detection, search indexing, recursive embedded parsing and process isolation, use Apache Tika, which builds on POI. For typed cell values and structure, use the usermodel or event APIs instead.

References