Skip to content

Commit 6335a69

Browse files
authored
Merge pull request #143 from spring-projects/more-flexible-configuration
Customization of Apache POI helpers
2 parents 41cbb93 + 3955606 commit 6335a69

16 files changed

+201
-229
lines changed

spring-batch-excel/README.adoc

+8-5
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ The `PoiItemReader` has the most features but is also the most memory intensive
66

77
To reduce the memory footprint the `StreamingXlsxItemReader` can be used, this will only keep the current row in memory and discard it afterward. Not everything is supported while streaming the XLSX file. It can be that formulas don't get evaluated or lead to an error.
88

9-
NOTE: The `ItemReader` classess are **not threadsafe**. The API from https://poi.apache.org/help/faq.html#20[Apache POI] itself isn't threadsafe as well as the https://docs.spring.io/spring-batch/docs/current/api/org/springframework/batch/item/support/AbstractItemCountingItemStreamItemReader.html[`AbstractItemCountingItemStreamItemReader`] used as a base class for the `ItemReader` classes. Reading from multiple threads is therefore not supported. Using a multi-threaded processor/writer should work as long as you use a single thread for reading.
9+
WARNING: The `ItemReader` classess are **not threadsafe**. The API from https://poi.apache.org/help/faq.html#20[Apache POI] itself isn't threadsafe as well as the https://docs.spring.io/spring-batch/docs/current/api/org/springframework/batch/item/support/AbstractItemCountingItemStreamItemReader.html[`AbstractItemCountingItemStreamItemReader`] used as a base class for the `ItemReader` classes. Reading from multiple threads is therefore not supported. Using a multi-threaded processor/writer should work as long as you use a single thread for reading.
1010

11-
*Compatibility:* Spring Batch Excel is compatible with Spring Batch 4.3 and 5.0/5.1.
11+
*Compatibility:* Spring Batch Excel is compatible with Spring Batch 5.x.
1212

1313
== Configuration of `PoiItemReader`
1414

@@ -34,10 +34,10 @@ Configuration of can be done in XML or Java Config.
3434
----
3535
@Bean
3636
@StepScope
37-
public PoiItemReader excelReader() {
37+
public PoiItemReader excelReader(RowMapper rowMapper) {
3838
PoiItemReader reader = new PoiItemReader();
3939
reader.setResource(new FileSystemResource("/path/to/your/excel/file"));
40-
reader.setRowMapper(rowMapper());
40+
reader.setRowMapper(rowMapper);
4141
return reader;
4242
}
4343
@@ -82,7 +82,6 @@ public RowMapper rowMapper() {
8282
}
8383
----
8484

85-
8685
== Configuration properties
8786
[cols="1,1,1,4"]
8887
.Properties for item readers
@@ -99,8 +98,12 @@ public RowMapper rowMapper() {
9998
| `strict` | no | `true` | This controls wether or not an exception is thrown if the file doesn't exists or isn't readable, by default an exception will be thrown.
10099
| `datesAsIso` | no | `false` | Controls if dates need to be parsed as ISO or to use the format as specified in the excel sheet.
101100
| `userLocale` | no | `null` | Set the `java.util.Locale` to use when formatting dates when there is no explicit format set in the Excel document.
101+
| `dataFormatterCustomizer` | no | `DataFormatterCustomizer.DEFAULT` | To additionally configure the https://poi.apache.org/apidocs/dev/org/apache/poi/ss/usermodel/DataFormatter.html[`DataFormatter`] in use to format the data. The default will set the `useCachedValuesForFormulaCells` property to `true` to use cached values instead of evaluating the formulas.
102+
| `formulaEvaluatorFactory` | no | `FormulaEvaluatorFactory.NOOP` | A factory approach to create a `FormulaEvaluator` used by Apache POI to evaluate the formulas in the, the default implementation will return `null` as the default is to use the cached values.
102103
|===
103104

105+
== ColumnNameExtractors
106+
104107
- `StaticColumnNameExtractor` uses a preset list of column names.
105108
- `RowNumberColumnNameExtractor` (**the default**) reads a given row (default 0) to determine the column names of the current sheet
106109

spring-batch-excel/src/main/java/org/springframework/batch/extensions/excel/AbstractExcelItemReader.java

+32
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ public abstract class AbstractExcelItemReader<T> extends AbstractItemCountingIte
7474

7575
private DataFormatter dataFormatter;
7676

77+
private DataFormatterCustomizer dataFormatterCustomizer = DataFormatterCustomizer.DEFAULT;
78+
79+
private FormulaEvaluatorFactory formulaEvaluatorFactory = FormulaEvaluatorFactory.NOOP;
80+
7781
public AbstractExcelItemReader() {
7882
super();
7983
this.setName(ClassUtils.getShortName(this.getClass()));
@@ -222,12 +226,15 @@ public void setResource(final Resource resource) {
222226

223227
public void afterPropertiesSet() {
224228
Assert.notNull(this.rowMapper, "RowMapper must be set");
229+
Assert.notNull(this.dataFormatterCustomizer, "DataFormatterCustomizer must be set");
230+
Assert.notNull(this.formulaEvaluatorFactory, "FormulaEvaluatorFactory must be set");
225231
if (this.datesAsIso) {
226232
this.dataFormatter = (this.userLocale != null) ? new IsoFormattingDateDataFormatter(this.userLocale) : new IsoFormattingDateDataFormatter();
227233
}
228234
else {
229235
this.dataFormatter = (this.userLocale != null) ? new DataFormatter(this.userLocale) : new DataFormatter();
230236
}
237+
this.dataFormatterCustomizer.customize(this.dataFormatter);
231238
}
232239

233240
protected DataFormatter getDataFormatter() {
@@ -330,4 +337,29 @@ public void setDatesAsIso(boolean datesAsIso) {
330337
public void setUserLocale(Locale userLocale) {
331338
this.userLocale = userLocale;
332339
}
340+
341+
/**
342+
* The {@code DataFormatterCustomizer} to use to configure the {@code DataFormatter} used to format/read data.
343+
* The default used is the {@code DataFormatterCustomizer.DEFAULT} which will disable formula evaluating and return
344+
* the cached value for a cell.
345+
* @param dataFormatterCustomizer the {@code DataFormatterCustomizer} never {@code null}.
346+
*/
347+
public void setDataFormatterCustomizer(DataFormatterCustomizer dataFormatterCustomizer) {
348+
this.dataFormatterCustomizer = dataFormatterCustomizer;
349+
}
350+
351+
/**
352+
* The {@code FormulaEvaluatorFactory} to use when a {@code FormulaEvaluator} is needed. The default used will
353+
* return {@code null} as the evaluator to use, this as by default the {@code DataFormatter} is configured to use
354+
* the cached value anyway.
355+
* @param formulaEvaluatorFactory the {@code FormulaEvaluatorFactory} to use, never {@code null}
356+
* @see FormulaEvaluatorFactory
357+
*/
358+
public void setFormulaEvaluatorFactory(FormulaEvaluatorFactory formulaEvaluatorFactory) {
359+
this.formulaEvaluatorFactory = formulaEvaluatorFactory;
360+
}
361+
362+
protected FormulaEvaluatorFactory getFormulaEvaluatorFactory() {
363+
return this.formulaEvaluatorFactory;
364+
}
333365
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright 2011-2024 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;
18+
19+
import org.apache.poi.ss.usermodel.DataFormatter;
20+
21+
/**
22+
* Callback for customizing a given {@code DataFormatter}. Designed for use with a lambda expression or method reference.
23+
* @author Marten Deinum
24+
* @since 0.2.0
25+
*
26+
*/
27+
@FunctionalInterface
28+
public interface DataFormatterCustomizer {
29+
30+
/** Noop {@code DataFormatterCustomizer}. **/
31+
DataFormatterCustomizer NOOP = (df) -> { };
32+
33+
/** The default {@code DataFormatterCustomizer}, setting the use of cached values. **/
34+
DataFormatterCustomizer DEFAULT = (df) -> df.setUseCachedValuesForFormulaCells(true);
35+
36+
void customize(DataFormatter dataFormatter);
37+
38+
39+
}
40+
41+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright 2011-2024 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;
18+
19+
import org.apache.poi.ss.usermodel.FormulaEvaluator;
20+
import org.apache.poi.ss.usermodel.Workbook;
21+
22+
import org.springframework.lang.Nullable;
23+
24+
/**
25+
* Factory interface for creating a {@code FormulaEvaluator}, the default will return {@code null} as to re-use the
26+
* cached formula result in the workbooks.
27+
*
28+
* @author Marten Deinum
29+
* @since 0.2.0
30+
*
31+
*/
32+
@FunctionalInterface
33+
public interface FormulaEvaluatorFactory {
34+
35+
/** Return {@code null} for the {@code FormulaEvaluator}, used by default. **/
36+
FormulaEvaluatorFactory NOOP = (wb) -> null;
37+
38+
/** Delegate the creation of the {@code FormulaEvaluator} to the workbook. **/
39+
FormulaEvaluatorFactory SIMPLE = (wb) -> wb.getCreationHelper().createFormulaEvaluator();
40+
41+
/**
42+
* Create the {@code FormulaEvaluator} for the given {@code Workbook}.
43+
* @param workbook the workbook
44+
* @return the {@code FormulaEvaluator}, can be {@code null}.
45+
*/
46+
@Nullable
47+
FormulaEvaluator create(Workbook workbook);
48+
}

spring-batch-excel/src/main/java/org/springframework/batch/extensions/excel/poi/PoiItemReader.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public class PoiItemReader<T> extends AbstractExcelItemReader<T> {
4646

4747
@Override
4848
protected Sheet getSheet(final int sheet) {
49-
return new PoiSheet(this.workbook.getSheetAt(sheet), getDataFormatter());
49+
return new PoiSheet(this.workbook.getSheetAt(sheet), getDataFormatter(), getFormulaEvaluatorFactory());
5050
}
5151

5252
@Override

spring-batch-excel/src/main/java/org/springframework/batch/extensions/excel/poi/PoiSheet.java

+8-3
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.apache.poi.ss.usermodel.FormulaEvaluator;
2727
import org.apache.poi.ss.usermodel.Row;
2828

29+
import org.springframework.batch.extensions.excel.FormulaEvaluatorFactory;
2930
import org.springframework.batch.extensions.excel.Sheet;
3031
import org.springframework.lang.Nullable;
3132

@@ -41,20 +42,23 @@ class PoiSheet implements Sheet {
4142
private final org.apache.poi.ss.usermodel.Sheet delegate;
4243
private final int numberOfRows;
4344
private final String name;
45+
private final FormulaEvaluatorFactory formulaEvaluatorFactory;
4446

4547
private FormulaEvaluator evaluator;
4648

4749
/**
4850
* Constructor which takes the delegate sheet.
4951
* @param delegate the apache POI sheet
5052
* @param dataFormatter the {@code DataFormatter} to use.
53+
* @param formulaEvaluatorFactory the {@code FormulaEvaluatorFactory} to use.
5154
*/
52-
PoiSheet(final org.apache.poi.ss.usermodel.Sheet delegate, DataFormatter dataFormatter) {
55+
PoiSheet(final org.apache.poi.ss.usermodel.Sheet delegate, DataFormatter dataFormatter, FormulaEvaluatorFactory formulaEvaluatorFactory) {
5356
super();
5457
this.delegate = delegate;
5558
this.numberOfRows = this.delegate.getLastRowNum() + 1;
5659
this.name = this.delegate.getSheetName();
5760
this.dataFormatter = dataFormatter;
61+
this.formulaEvaluatorFactory = formulaEvaluatorFactory;
5862
}
5963

6064
/**
@@ -108,17 +112,18 @@ private String[] map(Row row) {
108112
* Lazy getter for the {@code FormulaEvaluator}. Takes some time to create an
109113
* instance, so if not necessary don't create it.
110114
* @return the {@code FormulaEvaluator}
115+
* @see FormulaEvaluatorFactory
111116
*/
112117
private FormulaEvaluator getFormulaEvaluator() {
113118
if (this.evaluator == null) {
114-
this.evaluator = this.delegate.getWorkbook().getCreationHelper().createFormulaEvaluator();
119+
this.evaluator = this.formulaEvaluatorFactory.create(this.delegate.getWorkbook());
115120
}
116121
return this.evaluator;
117122
}
118123

119124
@Override
120125
public Iterator<String[]> iterator() {
121-
return new Iterator<String[]>() {
126+
return new Iterator<>() {
122127
private final Iterator<Row> delegateIter = PoiSheet.this.delegate.iterator();
123128

124129
@Override

spring-batch-excel/src/main/java/org/springframework/batch/extensions/excel/streaming/StreamingSheet.java

+4-3
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818

1919
import java.io.InputStream;
2020
import java.util.Arrays;
21-
import java.util.HashMap;
2221
import java.util.Iterator;
2322
import java.util.Map;
2423

@@ -38,6 +37,7 @@
3837
import org.xml.sax.Attributes;
3938

4039
import org.springframework.batch.extensions.excel.Sheet;
40+
import org.springframework.util.CollectionUtils;
4141
import org.springframework.util.StringUtils;
4242
import org.springframework.util.xml.StaxUtils;
4343

@@ -161,7 +161,7 @@ public void close() throws Exception {
161161

162162
@Override
163163
public Iterator<String[]> iterator() {
164-
return new Iterator<String[]>() {
164+
return new Iterator<>() {
165165

166166
private String[] currentRow;
167167

@@ -233,9 +233,10 @@ String[] getValues() {
233233
*/
234234
private static final class AttributesAdapter implements Attributes {
235235

236-
private final Map<String, String> attributes = new HashMap<>();
236+
private final Map<String, String> attributes;
237237

238238
private AttributesAdapter(XMLStreamReader delegate) {
239+
this.attributes = CollectionUtils.newHashMap(delegate.getAttributeCount());
239240
for (int i = 0; i < delegate.getAttributeCount(); i++) {
240241
String name = delegate.getAttributeLocalName(i);
241242
String value = delegate.getAttributeValue(i);

spring-batch-excel/src/main/java/org/springframework/batch/extensions/excel/streaming/StreamingXlsxItemReader.java

+4-1
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,10 @@ private void initSheets(XSSFReader reader, OPCPackage pkg) throws IOException, I
9797

9898
@Override
9999
protected void doClose() throws Exception {
100-
this.pkg.revert();
100+
101+
if (this.pkg != null) {
102+
this.pkg.revert();
103+
}
101104

102105
for (StreamingSheet sheet : this.sheets) {
103106
sheet.close();

0 commit comments

Comments
 (0)