diff --git a/spring-batch-excel/README.adoc b/spring-batch-excel/README.adoc index 8f366ed..e6fecc1 100644 --- a/spring-batch-excel/README.adoc +++ b/spring-batch-excel/README.adoc @@ -1,4 +1,4 @@ -# spring-batch-excel += spring-batch-excel Spring Batch extension containing an `ItemReader` implementation for Excel based on https://poi.apache.org[Apache POI]. It supports reading both XLS and XLSX files. For the latter, there is also (experimental) streaming support. @@ -8,26 +8,28 @@ To reduce the memory footprint the `StreamingXlsxItemReader` can be used, this w 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. -## Configuration of `PoiItemReader` +== Configuration of `PoiItemReader` Next to the https://docs.spring.io/spring-batch/reference/html/configureJob.html[configuration of Spring Batch] one needs to configure the `PoiItemReader`. Configuration of can be done in XML or Java Config. -### XML +=== XML -```xml +[source,xml] +---- -``` +---- -### Java Config +=== Java Config -```java +[source,java] +---- @Bean @StepScope public PoiItemReader excelReader() { @@ -41,32 +43,34 @@ public PoiItemReader excelReader() { public RowMapper rowMapper() { return new PassThroughRowMapper(); } -``` +---- -## Configuration of `StreamingXlsxItemReader` +== Configuration of `StreamingXlsxItemReader` Configuration can be done in XML or Java Config. -### XML +=== XML -```xml +[source,xml] +---- -``` +---- -### Java Config +=== Java Config -```java +[source,java] +---- @Bean @StepScope -public StreamingXlsxItemReader excelReader() { +public StreamingXlsxItemReader excelReader(RowMapper rowMapper) { StreamingXlsxItemReader reader = new StreamingXlsxItemReader(); reader.setResource(new FileSystemResource("/path/to/your/excel/file")); - reader.setRowMapper(rowMapper()); + reader.setRowMapper(rowMapper); return reader; } @@ -74,10 +78,10 @@ public StreamingXlsxItemReader excelReader() { public RowMapper rowMapper() { return new PassThroughRowMapper(); } -``` +---- -## Configuration properties +== Configuration properties [cols="1,1,1,4"] .Properties for item readers |=== @@ -91,28 +95,30 @@ public RowMapper rowMapper() { | `rowSetFactory` | no | `DefaultRowSetFactory` | For reading rows a `RowSet` abstraction is used. To construct a `RowSet` for the current `Sheet` a `RowSetFactory` is needed. The `DefaultRowSetFactory` constructs a `DefaultRowSet` and `DefaultRowSetMetaData`. For construction of the latter a `ColumnNameExtractor` is needed. At the moment there are 2 implementations | `skippedRowsCallback` | no | `null` | When rows are skipped an optional `RowCallbackHandler` is called with the skipped row. This comes in handy when one needs to write the skipped rows to another file or create some logging. | `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. -| `datesAsIso` | no | `false` | Controls if dates need to be parsed as ISO or to use the format as specified in the excel sheet. *NOTE:* Only for the `PoiItemReader` **not** the `StreamingXlsxReader`! +| `datesAsIso` | no | `false` | Controls if dates need to be parsed as ISO or to use the format as specified in the excel sheet. +| `userLocale` | no | `null` | Set the `java.util.Locale` to use when formatting dates when there is no explicit format set in the Excel document. |=== - - `StaticColumnNameExtractor` uses a preset list of column names. +- `StaticColumnNameExtractor` uses a preset list of column names. - `RowNumberColumnNameExtractor` (**the default**) reads a given row (default 0) to determine the column names of the current sheet -## RowMappers +== RowMappers To map a read row a `RowMapper` is needed. Out-of-the-box there are 2 implementations. The `PassThroughRowMapper` and `BeanWrapperRowMapper`. -### PassThroughRowMapper +=== PassThroughRowMapper Transforms the read row from excel into a `String[]`. -### BeanWrapperRowMapper +=== BeanWrapperRowMapper Uses a `BeanWrapper` to convert a given row into an object. Uses the column names of the given `RowSet` to map column to properties of the `targetType` or prototype bean. -```java +[source,xml] +---- - + -``` +---- diff --git a/spring-batch-excel/src/main/java/org/springframework/batch/extensions/excel/AbstractExcelItemReader.java b/spring-batch-excel/src/main/java/org/springframework/batch/extensions/excel/AbstractExcelItemReader.java index c39411f..0d6ad2b 100644 --- a/spring-batch-excel/src/main/java/org/springframework/batch/extensions/excel/AbstractExcelItemReader.java +++ b/spring-batch-excel/src/main/java/org/springframework/batch/extensions/excel/AbstractExcelItemReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2006-2021 the original author or authors. + * Copyright 2006-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,11 @@ package org.springframework.batch.extensions.excel; +import java.util.Locale; + import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.apache.poi.ss.usermodel.DataFormatter; import org.springframework.batch.extensions.excel.support.rowset.DefaultRowSetFactory; import org.springframework.batch.extensions.excel.support.rowset.RowSet; @@ -65,6 +68,12 @@ public abstract class AbstractExcelItemReader extends AbstractItemCountingIte private String password; + private boolean datesAsIso = false; + + private Locale userLocale; + + private DataFormatter dataFormatter; + public AbstractExcelItemReader() { super(); this.setName(ClassUtils.getShortName(this.getClass())); @@ -213,6 +222,16 @@ public abstract class AbstractExcelItemReader extends AbstractItemCountingIte public void afterPropertiesSet() throws Exception { Assert.notNull(this.rowMapper, "RowMapper must be set"); + if (this.datesAsIso) { + this.dataFormatter = (this.userLocale != null) ? new IsoFormattingDateDataFormatter(this.userLocale) : new IsoFormattingDateDataFormatter(); + } + else { + this.dataFormatter = (this.userLocale != null) ? new DataFormatter(this.userLocale) : new DataFormatter(); + } + } + + protected DataFormatter getDataFormatter() { + return this.dataFormatter; } /** @@ -295,4 +314,20 @@ public abstract class AbstractExcelItemReader extends AbstractItemCountingIte this.password = password; } + /** + * Instead of using the format defined in the Excel sheet, read the date/time fields as an ISO formatted + * string instead. This is by default {@code false} to leave the original behavior. + * @param datesAsIso default {@code false} + */ + public void setDatesAsIso(boolean datesAsIso) { + this.datesAsIso = datesAsIso; + } + + /** + * The {@code Locale} to use when reading sheets. Defaults to the platform default as set by Java. + * @param userLocale the {@code Locale} to use, default {@code null} + */ + public void setUserLocale(Locale userLocale) { + this.userLocale = userLocale; + } } diff --git a/spring-batch-excel/src/main/java/org/springframework/batch/extensions/excel/IsoFormattingDateDataFormatter.java b/spring-batch-excel/src/main/java/org/springframework/batch/extensions/excel/IsoFormattingDateDataFormatter.java new file mode 100644 index 0000000..3a83e4c --- /dev/null +++ b/spring-batch-excel/src/main/java/org/springframework/batch/extensions/excel/IsoFormattingDateDataFormatter.java @@ -0,0 +1,67 @@ +/* + * Copyright 2011-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.batch.extensions.excel; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Locale; + +import org.apache.poi.ss.formula.ConditionalFormattingEvaluator; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellType; +import org.apache.poi.ss.usermodel.DataFormatter; +import org.apache.poi.ss.usermodel.DateUtil; +import org.apache.poi.ss.usermodel.FormulaEvaluator; + +/** + * Specialized subclass for additionally formatting the date into an ISO date/time. + * + * @author Marten Deinum + * + * @see DateTimeFormatter#ISO_OFFSET_DATE_TIME + */ +public class IsoFormattingDateDataFormatter extends DataFormatter { + + public IsoFormattingDateDataFormatter() { + super(); + } + + public IsoFormattingDateDataFormatter(Locale locale) { + super(locale); + } + + @Override + public String formatCellValue(Cell cell, FormulaEvaluator evaluator, ConditionalFormattingEvaluator cfEvaluator) { + if (cell == null) { + return ""; + } + + CellType cellType = cell.getCellType(); + if (cellType == CellType.FORMULA) { + if (evaluator == null) { + return cell.getCellFormula(); + } + cellType = evaluator.evaluateFormulaCell(cell); + } + + if (cellType == CellType.NUMERIC && DateUtil.isCellDateFormatted(cell, cfEvaluator)) { + LocalDateTime value = cell.getLocalDateTimeCellValue(); + return (value != null) ? value.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME) : ""; + } + return super.formatCellValue(cell, evaluator, cfEvaluator); + } +} diff --git a/spring-batch-excel/src/main/java/org/springframework/batch/extensions/excel/poi/PoiItemReader.java b/spring-batch-excel/src/main/java/org/springframework/batch/extensions/excel/poi/PoiItemReader.java index cf1c200..3ddb8ba 100644 --- a/spring-batch-excel/src/main/java/org/springframework/batch/extensions/excel/poi/PoiItemReader.java +++ b/spring-batch-excel/src/main/java/org/springframework/batch/extensions/excel/poi/PoiItemReader.java @@ -44,11 +44,9 @@ public class PoiItemReader extends AbstractExcelItemReader { private InputStream inputStream; - private boolean datesAsIso = false; - @Override protected Sheet getSheet(final int sheet) { - return new PoiSheet(this.workbook.getSheetAt(sheet), this.datesAsIso); + return new PoiSheet(this.workbook.getSheetAt(sheet), getDataFormatter()); } @Override @@ -92,12 +90,4 @@ public class PoiItemReader extends AbstractExcelItemReader { this.workbook.setMissingCellPolicy(Row.MissingCellPolicy.CREATE_NULL_AS_BLANK); } - /** - * Instead of using the format defined in the Excel sheet, read the date/time fields as an ISO formatted - * string instead. This is by default {@code false} to leave the original behavior. - * @param datesAsIso default {@code false} - */ - public void setDatesAsIso(boolean datesAsIso) { - this.datesAsIso = datesAsIso; - } } diff --git a/spring-batch-excel/src/main/java/org/springframework/batch/extensions/excel/poi/PoiSheet.java b/spring-batch-excel/src/main/java/org/springframework/batch/extensions/excel/poi/PoiSheet.java index 02e0cb1..a28a725 100644 --- a/spring-batch-excel/src/main/java/org/springframework/batch/extensions/excel/poi/PoiSheet.java +++ b/spring-batch-excel/src/main/java/org/springframework/batch/extensions/excel/poi/PoiSheet.java @@ -16,17 +16,13 @@ package org.springframework.batch.extensions.excel.poi; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; import java.util.Iterator; import java.util.LinkedList; import java.util.List; -import org.apache.poi.ss.formula.ConditionalFormattingEvaluator; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.CellType; import org.apache.poi.ss.usermodel.DataFormatter; -import org.apache.poi.ss.usermodel.DateUtil; import org.apache.poi.ss.usermodel.FormulaEvaluator; import org.apache.poi.ss.usermodel.Row; @@ -42,12 +38,8 @@ import org.springframework.lang.Nullable; class PoiSheet implements Sheet { private final DataFormatter dataFormatter; - private final org.apache.poi.ss.usermodel.Sheet delegate; - private final boolean datesAsIso; - private final int numberOfRows; - private final String name; private FormulaEvaluator evaluator; @@ -55,15 +47,14 @@ class PoiSheet implements Sheet { /** * Constructor which takes the delegate sheet. * @param delegate the apache POI sheet - * @param datesAsIso should we format the dates as ISO or use the Excel formatting instead + * @param dataFormatter the {@code DataFormatter} to use. */ - PoiSheet(final org.apache.poi.ss.usermodel.Sheet delegate, boolean datesAsIso) { + PoiSheet(final org.apache.poi.ss.usermodel.Sheet delegate, DataFormatter dataFormatter) { super(); this.delegate = delegate; - this.datesAsIso = datesAsIso; this.numberOfRows = this.delegate.getLastRowNum() + 1; this.name = this.delegate.getSheetName(); - this.dataFormatter = this.datesAsIso ? new IsoFormattingDateDataFormatter() : new DataFormatter(); + this.dataFormatter = dataFormatter; } /** @@ -142,33 +133,4 @@ class PoiSheet implements Sheet { }; } - /** - * Specialized subclass for additionally formatting the date into an ISO date/time. - * - * @author Marten Deinum - * @see DateTimeFormatter#ISO_OFFSET_DATE_TIME - */ - private static class IsoFormattingDateDataFormatter extends DataFormatter { - - @Override - public String formatCellValue(Cell cell, FormulaEvaluator evaluator, ConditionalFormattingEvaluator cfEvaluator) { - if (cell == null) { - return ""; - } - - CellType cellType = cell.getCellType(); - if (cellType == CellType.FORMULA) { - if (evaluator == null) { - return cell.getCellFormula(); - } - cellType = evaluator.evaluateFormulaCell(cell); - } - - if (cellType == CellType.NUMERIC && DateUtil.isCellDateFormatted(cell, cfEvaluator)) { - LocalDateTime value = cell.getLocalDateTimeCellValue(); - return (value != null) ? value.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME) : ""; - } - return super.formatCellValue(cell, evaluator, cfEvaluator); - } - } } diff --git a/spring-batch-excel/src/main/java/org/springframework/batch/extensions/excel/streaming/StreamingSheet.java b/spring-batch-excel/src/main/java/org/springframework/batch/extensions/excel/streaming/StreamingSheet.java index a4d87b5..7653f2f 100644 --- a/spring-batch-excel/src/main/java/org/springframework/batch/extensions/excel/streaming/StreamingSheet.java +++ b/spring-batch-excel/src/main/java/org/springframework/batch/extensions/excel/streaming/StreamingSheet.java @@ -28,6 +28,7 @@ import javax.xml.stream.XMLStreamReader; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.apache.poi.ss.usermodel.DataFormatter; import org.apache.poi.ss.util.CellRangeAddress; import org.apache.poi.ss.util.CellReference; import org.apache.poi.xssf.eventusermodel.XSSFSheetXMLHandler; @@ -64,11 +65,11 @@ class StreamingSheet implements Sheet { private int colCount; - StreamingSheet(String name, InputStream is, SharedStrings sharedStrings, Styles styles) { + StreamingSheet(String name, InputStream is, SharedStrings sharedStrings, Styles styles, DataFormatter dataFormatter) { this.name = name; this.is = is; this.contentHandler = new ValueRetrievingContentsHandler(); - this.sheetHandler = new XSSFSheetXMLHandler(styles, sharedStrings, this.contentHandler, false); + this.sheetHandler = new XSSFSheetXMLHandler(styles, sharedStrings, this.contentHandler, dataFormatter, false); try { this.reader = StaxUtils.createDefensiveInputFactory().createXMLStreamReader(is); diff --git a/spring-batch-excel/src/main/java/org/springframework/batch/extensions/excel/streaming/StreamingXlsxItemReader.java b/spring-batch-excel/src/main/java/org/springframework/batch/extensions/excel/streaming/StreamingXlsxItemReader.java index 6441c7a..105e432 100644 --- a/spring-batch-excel/src/main/java/org/springframework/batch/extensions/excel/streaming/StreamingXlsxItemReader.java +++ b/spring-batch-excel/src/main/java/org/springframework/batch/extensions/excel/streaming/StreamingXlsxItemReader.java @@ -87,7 +87,7 @@ public class StreamingXlsxItemReader extends AbstractExcelItemReader { while (iter.hasNext()) { InputStream is = iter.next(); String name = iter.getSheetName(); - this.sheets.add(new StreamingSheet(name, is, sharedStrings, styles)); + this.sheets.add(new StreamingSheet(name, is, sharedStrings, styles, getDataFormatter())); } if (this.logger.isTraceEnabled()) {