Skip to content

Commit 4ec1110

Browse files
committed
Add streaming ItemReader for legacy .xls files
Signed-off-by: Kebba Manneh <kebbaman@gmail.com>
1 parent 00196a1 commit 4ec1110

5 files changed

Lines changed: 698 additions & 0 deletions

File tree

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
/*
2+
* Copyright 2006-2026 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.batch.extensions.excel.streaming;
18+
19+
import java.io.IOException;
20+
import java.util.ArrayList;
21+
import java.util.HashMap;
22+
import java.util.List;
23+
import java.util.Map;
24+
25+
import org.apache.poi.hssf.record.BoundSheetRecord;
26+
import org.apache.poi.hssf.record.EOFRecord;
27+
import org.apache.poi.hssf.record.ExtendedFormatRecord;
28+
import org.apache.poi.hssf.record.FormatRecord;
29+
import org.apache.poi.hssf.record.Record;
30+
import org.apache.poi.hssf.record.RecordFactoryInputStream;
31+
import org.apache.poi.hssf.record.SSTRecord;
32+
import org.apache.poi.poifs.filesystem.DocumentInputStream;
33+
import org.apache.poi.poifs.filesystem.POIFSFileSystem;
34+
35+
import org.springframework.batch.extensions.excel.AbstractExcelItemReader;
36+
import org.springframework.batch.extensions.excel.Sheet;
37+
import org.springframework.core.io.Resource;
38+
39+
/**
40+
* Low-memory streaming {@code ItemReader} for legacy binary {@code .xls} (BIFF8 / HSSF)
41+
* workbooks. It mirrors {@link StreamingXlsxItemReader} but reads the OLE2
42+
* {@code "Workbook"} stream record-by-record using Apache POI's pull primitive
43+
* {@link RecordFactoryInputStream}, so rows are produced lazily with a bounded memory
44+
* footprint instead of materialising the whole workbook object graph like the
45+
* {@code PoiItemReader}.
46+
* <p>
47+
* The shared, immutable workbook globals (the {@link SSTRecord shared-string table} and
48+
* the number/date format table) are read once when the file is opened and handed,
49+
* read-only, to every {@link XlsSheet}. Each sheet then opens its own stream positioned
50+
* at its {@code BOF} offset, matching the way {@code StreamingXlsxItemReader} gives every
51+
* sheet its own {@code InputStream}.
52+
* <p>
53+
* Encrypted {@code .xls} files are not supported by this reader (consistent with
54+
* {@code StreamingXlsxItemReader}); a configured password is ignored.
55+
*
56+
* @param <T> the type
57+
* @author Kebba Manneh
58+
* @since 0.3.0
59+
*/
60+
public class StreamingXlsItemReader<T> extends AbstractExcelItemReader<T> {
61+
62+
/**
63+
* Candidate names of the workbook stream inside the OLE2 container, newest first.
64+
*/
65+
private static final String[] WORKBOOK_ENTRY_NAMES = {"Workbook", "Book", "WORKBOOK", "BOOK", "workbook", "book"};
66+
67+
private final List<XlsSheet> sheets = new ArrayList<>();
68+
69+
private POIFSFileSystem poifs;
70+
71+
@Override
72+
protected Sheet getSheet(int sheet) {
73+
return this.sheets.get(sheet);
74+
}
75+
76+
@Override
77+
protected int getNumberOfSheets() {
78+
return this.sheets.size();
79+
}
80+
81+
@Override
82+
protected void openExcelFile(Resource resource, String password) throws Exception {
83+
this.poifs = resource.isFile() ? new POIFSFileSystem(resource.getFile())
84+
: new POIFSFileSystem(resource.getInputStream());
85+
String workbookEntry = resolveWorkbookEntry(this.poifs);
86+
initSheets(workbookEntry);
87+
}
88+
89+
// Read the globals substream once to capture the shared, immutable state (shared
90+
// strings + format table + the bound sheets), then create one lazy XlsSheet per
91+
// worksheet sharing that state.
92+
private void initSheets(String workbookEntry) throws IOException {
93+
SSTRecord sst = null;
94+
List<ExtendedFormatRecord> extendedFormats = new ArrayList<>();
95+
Map<Integer, String> formats = new HashMap<>();
96+
List<BoundSheetRecord> boundSheets = new ArrayList<>();
97+
98+
try (DocumentInputStream dis = this.poifs.createDocumentInputStream(workbookEntry)) {
99+
RecordFactoryInputStream rfis = new RecordFactoryInputStream(dis, false);
100+
Record record;
101+
while ((record = rfis.nextRecord()) != null) {
102+
if (record instanceof EOFRecord) {
103+
// End of the globals substream, everything shared has been read.
104+
break;
105+
}
106+
else if (record instanceof SSTRecord sstRecord) {
107+
sst = sstRecord;
108+
}
109+
else if (record instanceof ExtendedFormatRecord extendedFormat) {
110+
extendedFormats.add(extendedFormat);
111+
}
112+
else if (record instanceof FormatRecord format) {
113+
formats.put(format.getIndexCode(), format.getFormatString());
114+
}
115+
else if (record instanceof BoundSheetRecord boundSheet) {
116+
boundSheets.add(boundSheet);
117+
}
118+
}
119+
}
120+
121+
for (BoundSheetRecord boundSheet : BoundSheetRecord.orderByBofPosition(boundSheets)) {
122+
this.sheets.add(new XlsSheet(boundSheet.getSheetname(), this.poifs, workbookEntry,
123+
boundSheet.getPositionOfBof(), sst, extendedFormats, formats, getDataFormatter()));
124+
}
125+
126+
if (this.logger.isTraceEnabled()) {
127+
this.logger.trace("Prepared " + this.sheets.size() + " sheets.");
128+
}
129+
}
130+
131+
private static String resolveWorkbookEntry(POIFSFileSystem poifs) {
132+
for (String name : WORKBOOK_ENTRY_NAMES) {
133+
if (poifs.getRoot().hasEntry(name)) {
134+
return name;
135+
}
136+
}
137+
throw new IllegalStateException("Unable to locate a workbook stream in the OLE2 container.");
138+
}
139+
140+
@Override
141+
protected void doClose() throws Exception {
142+
for (XlsSheet sheet : this.sheets) {
143+
sheet.close();
144+
}
145+
this.sheets.clear();
146+
147+
if (this.poifs != null) {
148+
this.poifs.close();
149+
this.poifs = null;
150+
}
151+
super.doClose();
152+
}
153+
154+
}

0 commit comments

Comments
 (0)