diff --git a/README.md b/README.md index 1306fed..d97ba22 100644 --- a/README.md +++ b/README.md @@ -66,24 +66,53 @@ Export/Writing Here is an export example: ```java Windmill - .export(Arrays.asList(bean1, bean2, bean3)) - .withHeaderMapping( - new ExportHeaderMapping() - .add("Name", Bean::getName) - .add("User login", bean -> bean.getUser().getLogin()) - ) - .asExcel() - .writeTo(new FileOutputStream("Export.xlsx")); + .exporter() + .withHeaders() + .column("Name", Bean::getName) + .column("User login", bean -> bean.getUser().getLogin()) + .asExcel() + .writeRows(Arrays.asList(bean1, bean2, bean3)) + .writeInto(new FileOutputStream("Export.xlsx")); +``` +And an import example: +```java +Windmill + .importer() + .source(FileSource.of(new FileInputStream("Export.xlsx"))) + .withHeaders() + .stream() + .map(row -> new Bean(row.cell("Name").asString(), new User(row.cell(1).asString()))) + .collect(Collectors.toList()); ``` Options can be passed to the exporter, for example with CSV files, it is possible to specify multiple parameters like the separator character or the escape character: ```java Windmill - .export(Arrays.asList(bean1, bean2, bean3)) - .withNoHeaderMapping(Bean::getName, bean -> bean.getUser().getLogin()) - .asCsv(ExportCsvConfig.builder().separator(';').escapeChar('"').build()); - .toByteArray(); + .exporter() + .withoutHeaders() + .column(Bean::getName) + .column(bean -> bean.getUser().getLogin()) + .asCsv(ExportCsvConfig.builder() + .separator(';') + .escapeChar('"') + .build()) + .writeRows(Arrays.asList(bean1, bean2, bean3)) + .toByteArray(); +``` +```java +Windmill + .importer() + .source(FileSource.of(bytes)) + .parser(Parsers.csv(CsvParserConfig.builder() + .separator(';') + .escapeChar('"') + .quoteChar('\'') + .build())) + .withoutHeaders() + .stream() + .map(row -> new Bean(row.cell(0).asString(), new User(row.cell(1).asString()))) + .collect(Collectors.toList()); ``` It is also possible to export multiple tabs in one Excel workbook: @@ -91,16 +120,47 @@ It is also possible to export multiple tabs in one Excel workbook: Workbook xlsxFile = new XSSFWorkbook(); Windmill - .export(Arrays.asList(bean1, bean2, bean3)) - .withNoHeaderMapping(Bean::getName, bean -> bean.getUser().getLogin()) - .asExcel(ExportExcelConfig.fromWorkbook(xlsxFile).build("First tab")) - .write(); + .exporter() + .withoutHeaders() + .column(Bean::getName) + .column(bean -> bean.getUser().getLogin()) + .asExcel(ExportExcelConfig.fromWorkbook(xlsxFile) + .build("First tab")) + .writeRows(Arrays.asList(bean1, bean2, bean3)); Windmill - .export(Arrays.asList(film1, film2)) - .withNoHeaderMapping(Film::getTitle, Film::getReleaseDate) - .asExcel(ExportExcelConfig.fromWorkbook(xlsxFile).build("Second tab with films")) - .write(); + .exporter() + .withoutHeaders() + .column(Film::getTitle) + .column(Film::getReleaseDate) + .asExcel(ExportExcelConfig.fromWorkbook(xlsxFile) + .build("Second tab with films")) + .writeRow(film1) + .writeRow(film2); xlsxFile.write(new FileOutputStream("Export.xlsx")); ``` +```java +Windmill + .importer() + .source(FileSource.of(new FileInputStream("Export.xlsx"))) + .parser(Parsers.xlsx("First tab")) + .withoutHeaders() + .stream() + .map(row -> new Bean(row.cell(0).asString(), new User(row.cell(1).asString()))) + .collect(Collectors.toList()); + +Windmill + .importer() + .source(FileSource.of(new FileInputStream("Export.xlsx"))) + .parser(Parsers.xlsx("Second tab with films")) + .withoutHeaders() + .stream() + .map(row -> { + String title = row.cell(0).asString(); + // TODO: create convenient method for custom types + Date releaseDate = DateUtil.getJavaDate(row.cell(1).asDouble().value()); + return new Film(title, releaseDate); + }) + .collect(Collectors.toList()); +``` diff --git a/pom.xml b/pom.xml index 90884f1..bec85ee 100644 --- a/pom.xml +++ b/pom.xml @@ -178,6 +178,12 @@ 3.8.0 test + + commons-io + commons-io + 2.6 + test + diff --git a/src/main/java/com/coreoz/windmill/Exporter.java b/src/main/java/com/coreoz/windmill/Exporter.java new file mode 100644 index 0000000..aeb2be5 --- /dev/null +++ b/src/main/java/com/coreoz/windmill/Exporter.java @@ -0,0 +1,145 @@ +package com.coreoz.windmill; + +import com.coreoz.windmill.exports.exporters.csv.CsvExporter; +import com.coreoz.windmill.exports.exporters.csv.ExportCsvConfig; +import com.coreoz.windmill.exports.exporters.excel.ExcelExporter; +import com.coreoz.windmill.exports.exporters.excel.ExportExcelConfig; +import com.coreoz.windmill.exports.mapping.ExportHeaderMapping; +import com.coreoz.windmill.exports.mapping.ExportMapping; +import com.coreoz.windmill.exports.mapping.NoHeaderDecorator; +import org.apache.commons.collections4.map.LinkedMap; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.Collection; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Function; + +public interface Exporter extends Consumer { + + Exporter writeRow(T row); + Exporter writeRows(Iterable rows); + + default void accept(T row) { + writeRow(row); + } + + /** + * Write the exported state into an existing {@link OutputStream}. + * + * This {@link OutputStream} will not be closed automatically: + * it should be closed manually after this method is called. + * + * @throws IOException if anything can't be written. + */ + Exporter writeInto(OutputStream outputStream); + + /** + * @throws IOException if anything can't be written. + */ + byte[] toByteArray(); + + interface InitialState { + NamedValueMapperStage withHeaders(); + ValueMapperStage withoutHeaders(); + PresentationState withExportMapping(ExportMapping mapping); + } + + interface ValueMapperStage extends PresentationState { + ValueMapperStage column(Function applier); + PresentationState columns(Collection> appliers); + } + + interface NamedValueMapperStage extends PresentationState { + NamedValueMapperStage column(String name, Function applier); + PresentationState columns(Map> appliers); + } + + interface PresentationState { + CsvExporter asCsv(); + CsvExporter asCsv(ExportCsvConfig config); + ExcelExporter asExcel(); + ExcelExporter asExcel(ExportExcelConfig config); + } + + class Builder implements InitialState, ValueMapperStage, NamedValueMapperStage, PresentationState { + + private final LinkedMap> toValues; + + private ExportMapping headerMapping; + + public Builder(LinkedMap> toValues) { + this.toValues = toValues; + } + + @Override + public NamedValueMapperStage withHeaders() { + this.headerMapping = new ExportHeaderMapping<>(toValues); + return this; + } + + @Override + public ValueMapperStage withoutHeaders() { + this.headerMapping = new NoHeaderDecorator<>(new ExportHeaderMapping<>(toValues)); + return this; + } + + @Override + public ValueMapperStage column(Function applier) { + toValues.put(applier.toString(), applier); + return this; + } + + @Override + public PresentationState columns(Collection> appliers) { + for (Function applier : appliers) { + column(applier); + } + + return this; + } + + @Override + public NamedValueMapperStage column(String name, Function applier) { + toValues.put(name, applier); + return this; + } + + @Override + public PresentationState columns(Map> appliers) { + toValues.putAll(appliers); + return this; + } + + @Override + public PresentationState withExportMapping(ExportMapping mapping) { + this.headerMapping = mapping; + return this; + } + + @Override + public CsvExporter asCsv() { + return asCsv(ExportCsvConfig.builder().build()); + } + + @Override + public CsvExporter asCsv(ExportCsvConfig config) { + return new CsvExporter<>(headerMapping, config); + } + + @Override + public ExcelExporter asExcel() { + return asExcel(ExportExcelConfig.newXlsxFile().build()); + } + + @Override + public ExcelExporter asExcel(ExportExcelConfig config) { + return new ExcelExporter<>(headerMapping, config); + } + } + + static InitialState builder() { + return new Builder<>(new LinkedMap<>()); + } +} diff --git a/src/main/java/com/coreoz/windmill/Importer.java b/src/main/java/com/coreoz/windmill/Importer.java new file mode 100644 index 0000000..e86df80 --- /dev/null +++ b/src/main/java/com/coreoz/windmill/Importer.java @@ -0,0 +1,60 @@ +package com.coreoz.windmill; + +import com.coreoz.windmill.files.FileSource; +import com.coreoz.windmill.files.FileTypeGuesser; +import com.coreoz.windmill.imports.FileParser; +import com.coreoz.windmill.imports.Parsers; +import com.coreoz.windmill.imports.Row; + +import java.util.stream.Stream; + +public interface Importer { + + Stream stream(); + + interface InitialState { + ParserState source(FileSource fileSource); + } + + interface ParserState extends HeaderState { + HeaderState parser(FileParser fileParser); + } + + interface HeaderState { + Importer withHeaders(); + Importer withoutHeaders(); + } + + class Builder implements InitialState, ParserState, HeaderState { + + private FileParser parser; + private FileSource fileSource; + + @Override + public ParserState source(FileSource fileSource) { + this.fileSource = fileSource; + this.parser = Parsers.forType(FileTypeGuesser.guess(fileSource)); + return this; + } + + @Override + public HeaderState parser(FileParser fileParser) { + this.parser = fileParser; + return this; + } + + @Override + public Importer withHeaders() { + return () -> parser.parse(fileSource).skip(1); + } + + @Override + public Importer withoutHeaders() { + return () -> parser.parse(fileSource); + } + } + + static InitialState builder() { + return new Builder(); + } +} diff --git a/src/main/java/com/coreoz/windmill/Windmill.java b/src/main/java/com/coreoz/windmill/Windmill.java index 20533a3..58b9bce 100644 --- a/src/main/java/com/coreoz/windmill/Windmill.java +++ b/src/main/java/com/coreoz/windmill/Windmill.java @@ -1,14 +1,13 @@ package com.coreoz.windmill; -import java.io.IOException; -import java.util.stream.Stream; - -import com.coreoz.windmill.exports.config.ExportConfig; import com.coreoz.windmill.files.FileSource; import com.coreoz.windmill.files.FileTypeGuesser; import com.coreoz.windmill.imports.FileParser; -import com.coreoz.windmill.imports.Row; import com.coreoz.windmill.imports.Parsers; +import com.coreoz.windmill.imports.Row; + +import java.io.IOException; +import java.util.stream.Stream; /** * Entry point to parse/export CSV and Excel files @@ -20,24 +19,33 @@ public final class Windmill { * to the file type determined by the {@link FileTypeGuesser} * @throws IOException */ + @Deprecated public static Stream parse(FileSource fileSource) { - return parse(fileSource, Parsers.forType(FileTypeGuesser.guess(fileSource))); + return Importer.builder() + .source(fileSource) + .withoutHeaders() + .stream(); } /** * Parse a file with a dedicated {@link FileParser}. See {@link Parsers} for a list of all parsers * @throws IOException */ + @Deprecated public static Stream parse(FileSource fileSource, FileParser fileParser) { - return fileParser.parse(fileSource); + return Importer.builder() + .source(fileSource) + .parser(fileParser) + .withoutHeaders() + .stream(); } - /** - * Prepare a file export - * @param rows The rows that will be exported to a file - */ - public static ExportConfig export(Iterable rows) { - return new ExportConfig<>(rows); + public static Importer.InitialState importer() { + return Importer.builder(); + } + + public static Exporter.InitialState exporter() { + return Exporter.builder(); } } diff --git a/src/main/java/com/coreoz/windmill/exports/config/ExportColumn.java b/src/main/java/com/coreoz/windmill/exports/config/ExportColumn.java deleted file mode 100644 index b9c59cf..0000000 --- a/src/main/java/com/coreoz/windmill/exports/config/ExportColumn.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.coreoz.windmill.exports.config; - -import java.util.function.Function; - -import lombok.Value; - -@Value(staticConstructor = "of") -public class ExportColumn { - - private final String name; - private final Function toValue; - -} diff --git a/src/main/java/com/coreoz/windmill/exports/config/ExportConfig.java b/src/main/java/com/coreoz/windmill/exports/config/ExportConfig.java deleted file mode 100644 index 260b891..0000000 --- a/src/main/java/com/coreoz/windmill/exports/config/ExportConfig.java +++ /dev/null @@ -1,84 +0,0 @@ -package com.coreoz.windmill.exports.config; - -import java.util.List; -import java.util.function.Function; - -/** - * A builder that contains rows to export in a file - * - * @param The type of rows to export - */ -public class ExportConfig { - - private final Iterable rows; - - public ExportConfig(Iterable rows) { - this.rows = rows; - } - - /** - * Prepare an export that will NOT contains a header row with the column names. - * A usage example: - *
-	 * 
-	 * Windmill
-	 *   .export(Arrays.asList(bean1, bean2, bean3))
-	 *   .withNoHeaderMapping(
-	 *     Arrays.asList(
-	 *       Bean::getName,
-	 *       bean -> bean.getUser().getLogin()
-	 *     )
-	 *   )
-	 * 
-	 * 
- * - * @param valuesMapping A list of mapping that will fetch a value for each row. - */ - public ExportRowsConfig withNoHeaderMapping(List> valuesMapping) { - return new ExportRowsConfig<>(rows, new ExportNoHeaderMapping<>(valuesMapping)); - } - - /** - * Prepare an export that will NOT contains a header row with the column names. - * A usage example: - *
-	 * 
-	 * Windmill
-	 *   .export(Arrays.asList(bean1, bean2, bean3))
-	 *   .withNoHeaderMapping(
-	 *     Bean::getName,
-	 *     bean -> bean.getUser().getLogin()
-	 *   )
-	 * 
-	 * 
- * - * @param rowValueExtractor A mapping that will fetch a value for each row. - */ - @SafeVarargs - public final ExportRowsConfig withNoHeaderMapping(Function ...rowValueExtractor) { - return new ExportRowsConfig<>(rows, new ExportNoHeaderMapping<>(rowValueExtractor)); - } - - /** - * Prepare an export that will contains a header row with the column names. - * A usage example: - *
-	 * 
-	 * Windmill
-	 *   .export(Arrays.asList(bean1, bean2, bean3))
-	 *   .withHeaderMapping(
-	 *     new ExportHeaderMapping<Bean>()
-	 *       .add("Name", Bean::getName)
-	 *       .add("User login", bean -> bean.getUser().getLogin())
-	 *   )
-	 * 
-	 * 
- * - * @param mapping A mapping that will associate a name and a function to extract a row value - * for each columns of the export file - */ - public ExportRowsConfig withHeaderMapping(ExportHeaderMapping mapping) { - return new ExportRowsConfig<>(rows, mapping); - } - -} diff --git a/src/main/java/com/coreoz/windmill/exports/config/ExportHeaderMapping.java b/src/main/java/com/coreoz/windmill/exports/config/ExportHeaderMapping.java deleted file mode 100644 index 5582ea2..0000000 --- a/src/main/java/com/coreoz/windmill/exports/config/ExportHeaderMapping.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.coreoz.windmill.exports.config; - -import java.util.ArrayList; -import java.util.List; -import java.util.function.Function; -import java.util.stream.Collectors; - -public class ExportHeaderMapping implements ExportMapping { - - private final List> columns; - - public ExportHeaderMapping(List> columns) { - this.columns = columns; - } - - public ExportHeaderMapping() { - this(new ArrayList<>()); - } - - public ExportHeaderMapping add(String name, Function toValue) { - this.columns.add(ExportColumn.of(name, toValue)); - return this; - } - - @Override - public List headerColumns() { - return columns - .stream() - .map(ExportColumn::getName) - .collect(Collectors.toList()); - } - - @Override - public int columnsCount() { - return columns.size(); - } - - @Override - public Object cellValue(int columnIndex, T row) { - return columns.get(columnIndex).getToValue().apply(row); - } - -} diff --git a/src/main/java/com/coreoz/windmill/exports/config/ExportNoHeaderMapping.java b/src/main/java/com/coreoz/windmill/exports/config/ExportNoHeaderMapping.java deleted file mode 100644 index 077c722..0000000 --- a/src/main/java/com/coreoz/windmill/exports/config/ExportNoHeaderMapping.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.coreoz.windmill.exports.config; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.function.Function; - -public class ExportNoHeaderMapping implements ExportMapping { - - private final List> toValues; - - public ExportNoHeaderMapping(List> toValues) { - this.toValues = toValues; - } - - @SafeVarargs - public ExportNoHeaderMapping(Function ...toValue) { - this.toValues = Arrays.asList(toValue); - } - - @Override - public List headerColumns() { - return Collections.emptyList(); - } - - @Override - public int columnsCount() { - return toValues.size(); - } - - @Override - public Object cellValue(int columnIndex, T row) { - return toValues.get(columnIndex).apply(row); - } - -} diff --git a/src/main/java/com/coreoz/windmill/exports/config/ExportRowsConfig.java b/src/main/java/com/coreoz/windmill/exports/config/ExportRowsConfig.java deleted file mode 100644 index ca81c50..0000000 --- a/src/main/java/com/coreoz/windmill/exports/config/ExportRowsConfig.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.coreoz.windmill.exports.config; - -import com.coreoz.windmill.exports.exporters.csv.CsvExporter; -import com.coreoz.windmill.exports.exporters.csv.ExportCsvConfig; -import com.coreoz.windmill.exports.exporters.excel.ExcelExporter; -import com.coreoz.windmill.exports.exporters.excel.ExportExcelConfig; - -public class ExportRowsConfig { - - private final Iterable rows; - private final ExportMapping mapping; - - public ExportRowsConfig(Iterable rows, ExportMapping mapping) { - this.mapping = mapping; - this.rows = rows; - } - - public CsvExporter asCsv() { - return asCsv(ExportCsvConfig.builder().build()); - } - - public CsvExporter asCsv(ExportCsvConfig config) { - return new CsvExporter<>(rows, mapping, config); - } - - public ExcelExporter asExcel() { - return asExcel(ExportExcelConfig.newXlsxFile().build()); - } - - public ExcelExporter asExcel(ExportExcelConfig config) { - return new ExcelExporter<>(rows, mapping, config); - } - -} diff --git a/src/main/java/com/coreoz/windmill/exports/exporters/csv/CsvExporter.java b/src/main/java/com/coreoz/windmill/exports/exporters/csv/CsvExporter.java index f34c219..10f8c19 100644 --- a/src/main/java/com/coreoz/windmill/exports/exporters/csv/CsvExporter.java +++ b/src/main/java/com/coreoz/windmill/exports/exporters/csv/CsvExporter.java @@ -1,95 +1,107 @@ package com.coreoz.windmill.exports.exporters.csv; +import com.coreoz.windmill.Exporter; +import com.coreoz.windmill.exports.mapping.ExportMapping; +import com.opencsv.CSVWriter; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import org.apache.commons.lang3.ObjectUtils; + import java.io.ByteArrayOutputStream; -import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.util.List; -import com.coreoz.windmill.exports.config.ExportMapping; -import com.opencsv.CSVWriter; - -import lombok.SneakyThrows; - -public class CsvExporter { +@RequiredArgsConstructor +public class CsvExporter implements Exporter { - private final Iterable rows; private final ExportMapping mapping; private final ExportCsvConfig exportConfig; - private CSVWriter csvWriter; - public CsvExporter(Iterable rows, ExportMapping mapping, ExportCsvConfig exportConfig) { - this.rows = rows; - this.mapping = mapping; - this.exportConfig = exportConfig; - } + private CSVWriter csvWriter; + private ByteArrayOutputStream intermediate; + private boolean isHeaderInitialized; - /** - * Write the export file in an existing {@link OutputStream}. - * - * This {@link OutputStream} will not be closed automatically: - * it should be closed manually after this method is called. - * - * @throws IOException if anything can't be written. - */ @SneakyThrows - public OutputStream writeTo(OutputStream outputStream) { - csvWriter = new CSVWriter( - new OutputStreamWriter(outputStream, exportConfig.getCharset()), - exportConfig.getSeparator(), - exportConfig.getQuoteChar(), - exportConfig.getEscapeChar(), - exportConfig.getLineEnd() - ); - writeRows(); - return outputStream; - } + public CsvExporter writeRow(T row) { + writeHeaderRowIfRequired(); - /** - * @throws IOException if anything can't be written. - */ - public byte[] toByteArray() { - ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream(); - writeTo(byteOutputStream); - return byteOutputStream.toByteArray(); - } + String[] csvRowValues = new String[mapping.columnsCount()]; + for (int i = 0; i < mapping.columnsCount(); i++) { + Object value = ObjectUtils.defaultIfNull(mapping.cellValue(i, row), ""); + csvRowValues[i] = String.valueOf(value); + } - // internals + getWriter().writeNext(csvRowValues, exportConfig.isApplyQuotesToAll()); + getWriter().flush(); - private void writeRows() { - writeHeaderRow(); + return this; + } + @Override + public CsvExporter writeRows(Iterable rows) { for(T row : rows) { writeRow(row); } - } - private void writeHeaderRow() { - List headerColumn = mapping.headerColumns(); - if(!headerColumn.isEmpty()) { - String[] csvRowValues = new String[headerColumn.size()]; - for (int i = 0; i < headerColumn.size(); i++) { - csvRowValues[i] = stringValue(headerColumn.get(i)); - } - csvWriter.writeNext(csvRowValues,exportConfig.isApplyQuotesToAll()); - } + return this; } + @Override @SneakyThrows - private void writeRow(T row) { - String[] csvRowValues = new String[mapping.columnsCount()]; - for (int i = 0; i < mapping.columnsCount(); i++) { - csvRowValues[i] = stringValue(mapping.cellValue(i, row)); + public CsvExporter writeInto(OutputStream outputStream) { + if (csvWriter == null) { + this.csvWriter = initializeWriter(outputStream); + } else { + outputStream.write(intermediate.toByteArray()); } - csvWriter.writeNext(csvRowValues, exportConfig.isApplyQuotesToAll()); - csvWriter.flush(); + + return this; + } + + @Override + public byte[] toByteArray() { + if (intermediate == null) { + throw new IllegalStateException("The state has already been flushed to another output stream"); + } + + return intermediate.toByteArray(); } - private String stringValue(final Object object) { - if (object == null) { - return ""; + private CSVWriter initializeWriter(OutputStream outputStream) { + return new CSVWriter( + new OutputStreamWriter(outputStream, exportConfig.getCharset()), + exportConfig.getSeparator(), + exportConfig.getQuoteChar(), + exportConfig.getEscapeChar(), + exportConfig.getLineEnd() + ); + } + + private void writeHeaderRowIfRequired() { + if (!isHeaderInitialized) { + List headerColumn = mapping.headerColumns(); + if (!headerColumn.isEmpty()) { + String[] csvRowValues = new String[headerColumn.size()]; + for (int i = 0; i < headerColumn.size(); i++) { + String value = ObjectUtils.defaultIfNull(headerColumn.get(i), ""); + csvRowValues[i] = value; + } + + getWriter().writeNext(csvRowValues, exportConfig.isApplyQuotesToAll()); + } + + isHeaderInitialized = true; } - return object.toString(); } + private CSVWriter getWriter() { + if (csvWriter == null) { + // initialize intermediate buffer for written rows + intermediate = new ByteArrayOutputStream(); + csvWriter = initializeWriter(intermediate); + } + + return csvWriter; + } } diff --git a/src/main/java/com/coreoz/windmill/exports/exporters/excel/ExcelExporter.java b/src/main/java/com/coreoz/windmill/exports/exporters/excel/ExcelExporter.java index 9c390fe..b54fb78 100644 --- a/src/main/java/com/coreoz/windmill/exports/exporters/excel/ExcelExporter.java +++ b/src/main/java/com/coreoz/windmill/exports/exporters/excel/ExcelExporter.java @@ -1,88 +1,81 @@ package com.coreoz.windmill.exports.exporters.excel; +import com.coreoz.windmill.Exporter; +import com.coreoz.windmill.exports.mapping.ExportMapping; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Row.MissingCellPolicy; +import org.apache.poi.ss.usermodel.Workbook; + import java.io.ByteArrayOutputStream; -import java.io.IOException; import java.io.OutputStream; import java.math.BigDecimal; import java.util.Calendar; import java.util.Date; import java.util.List; -import org.apache.poi.ss.usermodel.Cell; -import org.apache.poi.ss.usermodel.Row; -import org.apache.poi.ss.usermodel.Row.MissingCellPolicy; -import org.apache.poi.ss.usermodel.Workbook; +@RequiredArgsConstructor +public class ExcelExporter implements Exporter { -import com.coreoz.windmill.exports.config.ExportMapping; - -import lombok.SneakyThrows; - -public class ExcelExporter { - - private final Iterable rows; private final ExportMapping mapping; private final ExportExcelConfig sheetConfig; - private Row currentExcelRow; - public ExcelExporter(Iterable rows, ExportMapping mapping, ExportExcelConfig sheetConfig) { - this.rows = rows; - this.mapping = mapping; - this.sheetConfig = sheetConfig; - this.currentExcelRow = null; - } + private Row currentExcelRow; + private boolean isHeaderInitialized; - /** - * Write the export file in the {@link Workbook} - * - * @return The {@link Workbook} in which the export has been written - */ - public Workbook write() { - writeRows(); + public Workbook workbook() { return sheetConfig.sheet().getWorkbook(); } - /** - * Write the export file in an existing {@link OutputStream}. - * - * This {@link OutputStream} will not be closed automatically: - * it should be closed manually after this method is called. - * - * @throws IOException if anything can't be written. - */ - @SneakyThrows - public OutputStream writeTo(OutputStream outputStream) { - write().write(outputStream); - return outputStream; + public ExcelExporter writeRow(T row) { + writeHeaderRowIfRequired(); + + initializeExcelRow(); + for (int i = 0; i < mapping.columnsCount(); i++) { + setCellValue(mapping.cellValue(i, row), i); + } + + return this; } - /** - * @throws IOException if anything can't be written. - */ + @Override public byte[] toByteArray() { ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream(); - writeTo(byteOutputStream); + writeInto(byteOutputStream); return byteOutputStream.toByteArray(); } - // internals - - private void writeRows() { - writeHeaderRow(); - + @Override + public ExcelExporter writeRows(Iterable rows) { for(T row : rows) { writeRow(row); } setAutoSizeColumns(); + return this; + } + + @Override + @SneakyThrows + public ExcelExporter writeInto(OutputStream outputStream) { + Workbook workbook = sheetConfig.sheet().getWorkbook(); + workbook.write(outputStream); + return this; } - private void writeHeaderRow() { - List headerColumn = mapping.headerColumns(); - if(!headerColumn.isEmpty()) { - initializeExcelRow(); - for (int i = 0; i < headerColumn.size(); i++) { - setCellValue(headerColumn.get(i), i); + private void writeHeaderRowIfRequired() { + if (!isHeaderInitialized) { + List headerColumn = mapping.headerColumns(); + if (!headerColumn.isEmpty()) { + initializeExcelRow(); + for (int i = 0; i < headerColumn.size(); i++) { + setCellValue(headerColumn.get(i), i); + } } + + isHeaderInitialized = true; } } @@ -92,13 +85,6 @@ private void setAutoSizeColumns() { } } - private void writeRow(T row) { - initializeExcelRow(); - for (int i = 0; i < mapping.columnsCount(); i++) { - setCellValue(mapping.cellValue(i, row), i); - } - } - private void initializeExcelRow() { int rowIndex = currentExcelRow == null ? sheetConfig.rowOrigin() : currentExcelRow.getRowNum() + 1; currentExcelRow = sheetConfig.sheet().getRow(rowIndex); @@ -120,11 +106,11 @@ private void setCellValue(final Object value, final int columnIndex) { // numbers if (value instanceof Integer) { - cell.setCellValue(Double.valueOf(((Integer) value).intValue())); + cell.setCellValue((double) (Integer) value); } else if (value instanceof Long) { - cell.setCellValue(Double.valueOf(((Long) value).longValue())); + cell.setCellValue((double) (Long) value); } else if (value instanceof Float) { - cell.setCellValue(Double.valueOf(((Float) value).floatValue())); + cell.setCellValue((double) (Float) value); } else if (value instanceof BigDecimal) { cell.setCellValue(((BigDecimal) value).doubleValue()); } else if (value instanceof Double) { @@ -132,7 +118,7 @@ private void setCellValue(final Object value, final int columnIndex) { } // other types else if (value instanceof Boolean) { - cell.setCellValue(((Boolean) value).booleanValue()); + cell.setCellValue((Boolean) value); } else if (value instanceof Calendar) { cell.setCellValue((Calendar) value); } else if (value instanceof Date) { @@ -145,5 +131,4 @@ else if (value instanceof Boolean) { cell.setCellValue(value.toString()); } } - } diff --git a/src/main/java/com/coreoz/windmill/exports/mapping/ExportHeaderMapping.java b/src/main/java/com/coreoz/windmill/exports/mapping/ExportHeaderMapping.java new file mode 100644 index 0000000..105d3c4 --- /dev/null +++ b/src/main/java/com/coreoz/windmill/exports/mapping/ExportHeaderMapping.java @@ -0,0 +1,35 @@ +package com.coreoz.windmill.exports.mapping; + +import lombok.RequiredArgsConstructor; +import org.apache.commons.collections4.map.LinkedMap; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +@RequiredArgsConstructor +public class ExportHeaderMapping implements ExportMapping { + + private final LinkedMap> toValues; + + public ExportHeaderMapping(Map> toValues) { + this(new LinkedMap<>(toValues)); + } + + @Override + public List headerColumns() { + return new ArrayList<>(toValues.keySet()); + } + + @Override + public int columnsCount() { + return toValues.size(); + } + + @Override + public Object cellValue(int columnIndex, T row) { + return toValues.getValue(columnIndex).apply(row); + } + +} diff --git a/src/main/java/com/coreoz/windmill/exports/config/ExportMapping.java b/src/main/java/com/coreoz/windmill/exports/mapping/ExportMapping.java similarity index 83% rename from src/main/java/com/coreoz/windmill/exports/config/ExportMapping.java rename to src/main/java/com/coreoz/windmill/exports/mapping/ExportMapping.java index 720a8ba..ce8af46 100644 --- a/src/main/java/com/coreoz/windmill/exports/config/ExportMapping.java +++ b/src/main/java/com/coreoz/windmill/exports/mapping/ExportMapping.java @@ -1,4 +1,4 @@ -package com.coreoz.windmill.exports.config; +package com.coreoz.windmill.exports.mapping; import java.util.List; diff --git a/src/main/java/com/coreoz/windmill/exports/mapping/NoHeaderDecorator.java b/src/main/java/com/coreoz/windmill/exports/mapping/NoHeaderDecorator.java new file mode 100644 index 0000000..fc99948 --- /dev/null +++ b/src/main/java/com/coreoz/windmill/exports/mapping/NoHeaderDecorator.java @@ -0,0 +1,27 @@ +package com.coreoz.windmill.exports.mapping; + +import lombok.RequiredArgsConstructor; + +import java.util.Collections; +import java.util.List; + +@RequiredArgsConstructor +public class NoHeaderDecorator implements ExportMapping { + + private final ExportMapping delegate; + + @Override + public List headerColumns() { + return Collections.emptyList(); + } + + @Override + public int columnsCount() { + return delegate.columnsCount(); + } + + @Override + public Object cellValue(int columnIndex, T row) { + return delegate.cellValue(columnIndex, row); + } +} diff --git a/src/main/java/com/coreoz/windmill/imports/parsers/excel/ExcelCell.java b/src/main/java/com/coreoz/windmill/imports/parsers/excel/ExcelCell.java index 6e39189..c4722ac 100644 --- a/src/main/java/com/coreoz/windmill/imports/parsers/excel/ExcelCell.java +++ b/src/main/java/com/coreoz/windmill/imports/parsers/excel/ExcelCell.java @@ -1,12 +1,11 @@ package com.coreoz.windmill.imports.parsers.excel; -import java.util.function.Function; - -import org.apache.poi.ss.usermodel.CellType; - import com.coreoz.windmill.imports.Cell; import com.coreoz.windmill.imports.NumberValue; import com.coreoz.windmill.imports.NumberValueWithDefault; +import org.apache.poi.ss.usermodel.CellType; + +import java.util.function.Function; class ExcelCell implements Cell { @@ -33,6 +32,7 @@ public String asString() { } if (excelCell.getCellTypeEnum() == CellType.NUMERIC || excelCell.getCellTypeEnum() == CellType.FORMULA) { + // todo: side effect can influence on tryGetValue method excelCell.setCellType(CellType.STRING); } return emptyToNullTrimmed(excelCell.getRichStringCellValue().getString(), trimValue); diff --git a/src/test/java/com/coreoz/windmill/Import.java b/src/test/java/com/coreoz/windmill/Import.java new file mode 100644 index 0000000..2086c61 --- /dev/null +++ b/src/test/java/com/coreoz/windmill/Import.java @@ -0,0 +1,18 @@ +package com.coreoz.windmill; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor(staticName = "of") +public class Import { + private String a; + private String b; + private String c; + private Integer integerNumber; + private Double doubleNumber; +} diff --git a/src/test/java/com/coreoz/windmill/ReadmeExamples.java b/src/test/java/com/coreoz/windmill/ReadmeExamples.java new file mode 100644 index 0000000..715adae --- /dev/null +++ b/src/test/java/com/coreoz/windmill/ReadmeExamples.java @@ -0,0 +1,189 @@ +package com.coreoz.windmill; + +import com.coreoz.windmill.exports.exporters.csv.ExportCsvConfig; +import com.coreoz.windmill.exports.exporters.excel.ExportExcelConfig; +import com.coreoz.windmill.files.FileSource; +import com.coreoz.windmill.imports.Parsers; +import com.coreoz.windmill.imports.parsers.csv.CsvParserConfig; +import lombok.Builder; +import lombok.Data; +import org.apache.poi.ss.usermodel.DateUtil; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.Test; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ReadmeExamples { + + private static final Bean bean1 = Bean.builder() + .name("first name") + .user(new User("first login")) + .build(); + + private static final Bean bean2 = Bean.builder() + .name("second name") + .user(new User("second login")) + .build(); + + private static final Bean bean3 = Bean.builder() + .name("third name") + .user(new User("third login")) + .build(); + + private static final Film film1 = Film.builder() + .title("first title") + .releaseDate(new Date()) + .build(); + + private static final Film film2 = Film.builder() + .title("second title") + .releaseDate(new Date()) + .build(); + + @Data + @Builder + public static class Bean { + private String name; + private User user; + } + + @Data + @Builder + public static class User { + private String login; + } + + @Data + @Builder + public static class Film { + private String title; + private Date releaseDate; + } + + @AfterClass + public static void tearDown() throws Exception { + Assert.assertTrue(new File("Export.xlsx").delete()); + } + + @Test + public void should_write_into_xls_file() throws IOException { + // example start + Windmill + .exporter() + .withHeaders() + .column("Name", Bean::getName) + .column("User login", bean -> bean.getUser().getLogin()) + .asExcel() + .writeRows(Arrays.asList(bean1, bean2, bean3)) + .writeInto(new FileOutputStream("Export.xlsx")); + // example end + + List actual = Windmill + .importer() + .source(FileSource.of(new FileInputStream("Export.xlsx"))) + .withHeaders() + .stream() + .map(row -> new Bean(row.cell("Name").asString(), new User(row.cell(1).asString()))) + .collect(Collectors.toList()); + + assertThat(actual).containsExactlyElementsOf(Arrays.asList(bean1, bean2, bean3)); + } + + @Test + public void should_write_into_csv_file() throws IOException { + // example start + byte[] bytes = Windmill + .exporter() + .withoutHeaders() + .column(Bean::getName) + .column(bean -> bean.getUser().getLogin()) + .asCsv(ExportCsvConfig.builder() + .separator(';') + .escapeChar('"') + .build()) + .writeRows(Arrays.asList(bean1, bean2, bean3)) + .toByteArray(); + // example end + + List actual = Windmill + .importer() + .source(FileSource.of(bytes)) + .parser(Parsers.csv(CsvParserConfig.builder() + .separator(';') + .escapeChar('"') + .quoteChar('\'') + .build())) + .withoutHeaders() + .stream() + .map(row -> new Bean(row.cell(0).asString(), new User(row.cell(1).asString()))) + .collect(Collectors.toList()); + + assertThat(actual).containsExactlyElementsOf(Arrays.asList(bean1, bean2, bean3)); + } + + @Test + public void should_write_into_single_xls_workbook() throws IOException { + // example start + Workbook xlsxFile = new XSSFWorkbook(); + + Windmill + .exporter() + .withoutHeaders() + .column(Bean::getName) + .column(bean -> bean.getUser().getLogin()) + .asExcel(ExportExcelConfig.fromWorkbook(xlsxFile) + .build("First tab")) + .writeRows(Arrays.asList(bean1, bean2, bean3)); + + Windmill + .exporter() + .withoutHeaders() + .column(Film::getTitle) + .column(Film::getReleaseDate) + .asExcel(ExportExcelConfig.fromWorkbook(xlsxFile) + .build("Second tab with films")) + .writeRow(film1) + .writeRow(film2); + + xlsxFile.write(new FileOutputStream("Export.xlsx")); + // example end + + List actualBeans = Windmill + .importer() + .source(FileSource.of(new FileInputStream("Export.xlsx"))) + .parser(Parsers.xlsx("First tab")) + .withoutHeaders() + .stream() + .map(row -> new Bean(row.cell(0).asString(), new User(row.cell(1).asString()))) + .collect(Collectors.toList()); + + assertThat(actualBeans).containsExactlyElementsOf(Arrays.asList(bean1, bean2, bean3)); + + List actualFilms = Windmill + .importer() + .source(FileSource.of(new FileInputStream("Export.xlsx"))) + .parser(Parsers.xlsx("Second tab with films")) + .withoutHeaders() + .stream() + .map(row -> { + String title = row.cell(0).asString(); + Date releaseDate = DateUtil.getJavaDate(row.cell(1).asDouble().value()); + return new Film(title, releaseDate); + }) + .collect(Collectors.toList()); + + assertThat(actualFilms).containsExactlyElementsOf(Arrays.asList(film1, film2)); + } +} diff --git a/src/test/java/com/coreoz/windmill/StreamingExamples.java b/src/test/java/com/coreoz/windmill/StreamingExamples.java new file mode 100644 index 0000000..48f93d3 --- /dev/null +++ b/src/test/java/com/coreoz/windmill/StreamingExamples.java @@ -0,0 +1,128 @@ +package com.coreoz.windmill; + +import com.coreoz.windmill.exports.exporters.csv.CsvExporter; +import com.coreoz.windmill.exports.exporters.excel.ExcelExporter; +import com.coreoz.windmill.exports.exporters.excel.ExportExcelConfig; +import org.apache.commons.io.output.CountingOutputStream; +import org.apache.commons.io.output.NullOutputStream; +import org.apache.poi.xssf.streaming.SXSSFWorkbook; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.junit.Test; + +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import static org.apache.commons.io.IOUtils.closeQuietly; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +public class StreamingExamples { + + @Test + public void should_stream_csv_without_intermediate_state_buffer() { + CountingOutputStream outputStream = new CountingOutputStream(new NullOutputStream()); + CsvExporter exporter = initial().asCsv() + .writeInto(outputStream); + + generate(100_000).forEach(exporter); + assertThat(outputStream.getByteCount()).isGreaterThan(4_000_000); + + try { + exporter.toByteArray(); + fail("unreachable"); + } catch (Exception e) { + assertThat(e).hasNoCause() + .isInstanceOf(IllegalStateException.class) + .hasMessage("The state has already been flushed to another output stream"); + } + } + + @Test + public void should_stream_csv_with_intermediate_state_buffer() { + CsvExporter exporter = initial().asCsv(); + + generate(10_000).forEach(exporter); + assertThat(exporter.toByteArray().length).isGreaterThan(300_000); + } + + @Test + public void should_stream_xls_with_intermediate_state_buffer() { + CountingOutputStream outputStream = new CountingOutputStream(new NullOutputStream()); + ExcelExporter exporter = initial().asExcel() + .writeInto(outputStream); + + generate(1_000).forEach(exporter); + + // by default workbook doesn't support async flushing of internal state + assertThat(outputStream.getByteCount()).isLessThan(10_000); + exporter.writeInto(outputStream); + assertThat(outputStream.getByteCount()).isGreaterThan(30_000); + } + + @Test + public void should_stream_xls_without_inmemory_intermediate_state_buffer() { + SXSSFWorkbook workbook = new SXSSFWorkbook(new XSSFWorkbook(), 100); + ExcelExporter exporter = initial() + .asExcel(ExportExcelConfig.fromWorkbook(workbook).build()); + + generate(100_000).forEach(exporter); + + CountingOutputStream outputStream = new CountingOutputStream(new NullOutputStream()); + exporter.writeInto(outputStream); + assertThat(outputStream.getByteCount()).isGreaterThan(2_500_000); + workbook.dispose(); + closeQuietly(workbook); + } + + @Test + public void should_fail_on_processing_after_first_flushing() { + CountingOutputStream outputStream = new CountingOutputStream(new NullOutputStream()); + SXSSFWorkbook workbook = new SXSSFWorkbook(new XSSFWorkbook(), 100); + ExcelExporter exporter = initial() + .asExcel(ExportExcelConfig.fromWorkbook(workbook).build()) + .writeInto(outputStream); + + try { + generate(1_000).forEach(exporter); + } catch (Exception e) { + assertThat(e).hasCauseInstanceOf(IOException.class) + .isInstanceOf(RuntimeException.class) + .hasMessageContaining("Stream closed"); + } finally { + workbook.dispose(); + closeQuietly(workbook); + } + } + + private Exporter.PresentationState initial() { + return Windmill.exporter() + .withHeaders() + .columns(getAppliers()); + } + + private Map> getAppliers() { + Map> map = new LinkedHashMap<>(); + map.put("a", Import::getA); + map.put("b", Import::getB); + map.put("c", Import::getC); + map.put("d", Import::getDoubleNumber); + map.put("e", Import::getIntegerNumber); + + return map; + } + + private Stream generate(int limit) { + return IntStream.range(0, limit) + .mapToObj(i -> Import.builder() + .a("a" + i) + .b("b" + i) + .c("c" + i) + .integerNumber(i) + .doubleNumber(((double) i * 3) / 2) + .build()); + } +} diff --git a/src/test/java/com/coreoz/windmill/WindmillTest.java b/src/test/java/com/coreoz/windmill/WindmillTest.java index c7b2139..608886c 100644 --- a/src/test/java/com/coreoz/windmill/WindmillTest.java +++ b/src/test/java/com/coreoz/windmill/WindmillTest.java @@ -1,25 +1,26 @@ package com.coreoz.windmill; -import static org.assertj.core.api.Assertions.assertThat; - -import java.util.Arrays; -import java.util.List; -import java.util.function.Function; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import org.junit.Test; - -import com.coreoz.windmill.exports.config.ExportConfig; -import com.coreoz.windmill.exports.config.ExportHeaderMapping; +import com.coreoz.windmill.exports.exporters.excel.ExcelCellStyler; +import com.coreoz.windmill.exports.exporters.excel.ExcelExporter; import com.coreoz.windmill.exports.exporters.excel.ExportExcelConfig; +import com.coreoz.windmill.exports.mapping.ExportHeaderMapping; import com.coreoz.windmill.files.FileSource; import com.coreoz.windmill.files.ParserGuesserTest; import com.coreoz.windmill.imports.Cell; +import com.coreoz.windmill.imports.FileParser; import com.coreoz.windmill.imports.Parsers; import com.coreoz.windmill.imports.Row; +import org.junit.Test; -import lombok.Value; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; /** * Integration tests for Windmill @@ -28,9 +29,10 @@ public class WindmillTest { @Test public void should_export_as_xlsx_with_header() { - byte[] xlsxExport = exportBase() - .withHeaderMapping(exportHeaderMapping()) + byte[] xlsxExport = Exporter.builder() + .withExportMapping(exportHeaderMapping()) .asExcel() + .writeRows(data()) .toByteArray(); tryParseHeaderFile(FileSource.of(xlsxExport)); @@ -38,41 +40,100 @@ public void should_export_as_xlsx_with_header() { @Test public void should_export_as_csv_with_header() { - byte[] csvExport = exportBase() - .withHeaderMapping(exportHeaderMapping()) - .asCsv() - .toByteArray(); + byte[] csvExport = Exporter.builder() + .withExportMapping(exportHeaderMapping()) + .asCsv() + .writeRows(data()) + .toByteArray(); tryParseHeaderFile(FileSource.of(csvExport)); } + @Test + public void should_export_as_csv_with_header_by_properties() { + List data = Arrays.asList( + Import.builder().a("a1").b("b1").build(), + Import.builder().a("a2").b("b2").build() + ); + + byte[] csvExport = Windmill.exporter() + .withHeaders() + .column("a", Import::getA) + .column("b", Import::getB) + .asCsv() + .writeRows(data) + .toByteArray(); + + List result = Windmill.importer() + .source(FileSource.of(csvExport)) + .withHeaders() + .stream() + .map(row -> Import.builder() + .a(row.cell(0).asString()) + .b(row.cell(1).asString()) + .build()) + .collect(Collectors.toList()); + + assertThat(result).containsExactlyElementsOf(data); + } + @Test public void should_export_as_xlsx_no_header() { - byte[] xlsxExport = exportBase() - .withNoHeaderMapping(exportNoHeaderMapping()) - .asExcel() - .toByteArray(); + byte[] xlsxExport = Exporter.builder() + .withoutHeaders() + .columns(exportNoHeaderMapping()) + .asExcel(ExportExcelConfig.newXlsxFile() + .build("Sheet1")) + .writeRows(data()) + .toByteArray(); tryParseNoHeaderFile(FileSource.of(xlsxExport)); + tryParseNoHeaderFile(FileSource.of(xlsxExport), Parsers.xlsx()); + tryParseNoHeaderFile(FileSource.of(xlsxExport), Parsers.xlsx(0)); + tryParseNoHeaderFile(FileSource.of(xlsxExport), Parsers.xlsx("Sheet1")); + } + + @Test + public void should_export_as_xls_no_header() { + ExcelExporter exporter = Exporter.builder() + .withoutHeaders() + .columns(exportNoHeaderMapping()) + .asExcel(ExportExcelConfig.newXlsFile() + .build("Sheet1") + .withCellStyler(ExcelCellStyler.bordersStyle())) + .writeRows(data()); + + assertThat(exporter.workbook()).isNotNull(); + byte[] xlsExport = exporter.toByteArray(); + + tryParseNoHeaderFile(FileSource.of(xlsExport)); + tryParseNoHeaderFile(FileSource.of(xlsExport), Parsers.xls()); + tryParseNoHeaderFile(FileSource.of(xlsExport), Parsers.xls(0)); + tryParseNoHeaderFile(FileSource.of(xlsExport), Parsers.xls("Sheet1")); } @Test public void should_export_as_csv_with_no_header() { - byte[] csvExport = exportBase() - .withNoHeaderMapping(exportNoHeaderMapping()) - .asCsv() - .toByteArray(); + byte[] csvExport = Exporter.builder() + .withoutHeaders() + .columns(exportNoHeaderMapping()) + .asCsv() + .writeRows(data()) + .toByteArray(); tryParseNoHeaderFile(FileSource.of(csvExport)); + tryParseNoHeaderFile(FileSource.of(csvExport), Parsers.csv()); } @Test public void should_export_excel_data_starting_from_a_non_origin_point() { - byte[] xlsxExport = exportBase() - .withNoHeaderMapping(exportNoHeaderMapping()) + byte[] xlsxExport = Exporter.builder() + .withoutHeaders() + .columns(exportNoHeaderMapping()) // we are using an existing file to write the new data, // else POI will simply ignore the first empty rows and the test will be biased .asExcel(ExportExcelConfig.fromWorkbook(loadFile("/import.xlsx")).build("Feuil1").withOrigin(6, 3)) + .writeRows(data()) .toByteArray(); // check that the first rows are not modified @@ -159,17 +220,15 @@ private List data() { ); } - private ExportConfig exportBase() { - return Windmill.export(data()); - } - private ExportHeaderMapping exportHeaderMapping() { - return new ExportHeaderMapping() - .add("a", Import::getA) - .add("b", Import::getB) - .add("c", Import::getC) - .add("Integer number", Import::getIntegerNumber) - .add("Double number", Import::getDoubleNumber); + Map> toValues = new LinkedHashMap<>(); + toValues.put("a", Import::getA); + toValues.put("b", Import::getB); + toValues.put("c", Import::getC); + toValues.put("Integer number", Import::getIntegerNumber); + toValues.put("Double number", Import::getDoubleNumber); + + return new ExportHeaderMapping<>(toValues); } private List> exportNoHeaderMapping() { @@ -220,6 +279,15 @@ private void tryParseNoHeaderFile(FileSource fileSource) { } } + private void tryParseNoHeaderFile(FileSource fileSource, FileParser parser) { + try (Stream rowStream = Windmill.parse(fileSource, parser)) { + List result = rowStream.map(this::parsingFunction) + .collect(Collectors.toList()); + + assertThat(result).containsExactlyElementsOf(data()); + } + } + private Import parsingFunction(Row row) { return Import.of( row.cell(0).asString(), @@ -259,14 +327,4 @@ private void checkInexistantCell(String fileName) { assertThat(inexistantCellByIndex.asString()).isNull(); assertThat(inexistantCellByIndex.asLong().isNull()).isTrue(); } - - @Value(staticConstructor = "of") - private static class Import { - private String a; - private String b; - private String c; - private Integer integerNumber; - private Double doubleNumber; - } - } diff --git a/src/test/java/com/coreoz/windmill/exports/exporters/excel/ExportExcelConfigTest.java b/src/test/java/com/coreoz/windmill/exports/exporters/excel/ExportExcelConfigTest.java index 7bd7daa..9e16e63 100644 --- a/src/test/java/com/coreoz/windmill/exports/exporters/excel/ExportExcelConfigTest.java +++ b/src/test/java/com/coreoz/windmill/exports/exporters/excel/ExportExcelConfigTest.java @@ -1,11 +1,10 @@ package com.coreoz.windmill.exports.exporters.excel; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; - +import com.coreoz.windmill.files.FileSource; import org.junit.Test; -import com.coreoz.windmill.files.FileSource; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; public class ExportExcelConfigTest { diff --git a/src/test/java/com/coreoz/windmill/imports/parsers/csv/CsvRowCellTest.java b/src/test/java/com/coreoz/windmill/imports/parsers/csv/CsvRowCellTest.java new file mode 100644 index 0000000..22851e5 --- /dev/null +++ b/src/test/java/com/coreoz/windmill/imports/parsers/csv/CsvRowCellTest.java @@ -0,0 +1,79 @@ +package com.coreoz.windmill.imports.parsers.csv; + +import com.coreoz.windmill.imports.Cell; +import com.coreoz.windmill.imports.FileSchema; +import org.junit.Test; + +import java.util.Arrays; + +import static org.assertj.core.api.Assertions.assertThat; + +public class CsvRowCellTest { + + private static final CsvRow ROW = new CsvRow( + 3, + new FileSchema(Arrays.asList( + new CsvCell(0, "name"), + new CsvCell(1, "integer"), + new CsvCell(2, "fractional"))), + new String[]{"foobar", "42", "0.001"}); + + @Test + public void check_for_correct_row_index() { + assertThat(ROW.rowIndex()).isEqualTo(3); + } + + @Test + public void iterator_should_return_correct_amount_of_entries() { + assertThat(ROW.iterator()).hasSize(3); + } + + @Test + public void should_discover_columns() { + assertThat(ROW.columnExists("name")).isTrue(); + assertThat(ROW.columnExists("integer")).isTrue(); + assertThat(ROW.columnExists("foobar")).isFalse(); + } + + @Test + public void should_discover_integer_number() { + Cell cell = ROW.cell(1); + + assertThat(cell.asInteger().isNull()).isFalse(); + assertThat(cell.asInteger().value()).isEqualTo(42); + assertThat(cell.asInteger().safeValue()).isEqualTo(42); + + assertThat(cell.asLong().isNull()).isFalse(); + assertThat(cell.asLong().value()).isEqualTo(42); + assertThat(cell.asLong().safeValue()).isEqualTo(42); + } + + @Test + public void should_discover_fractional_number() { + Cell cell = ROW.cell("fractional"); + + assertThat(cell.asString()).isEqualTo("0.001"); + + assertThat(cell.asDouble().isNull()).isFalse(); + assertThat(cell.asDouble().value()).isEqualTo(0.001d); + assertThat(cell.asDouble().safeValue()).isEqualTo(0.001d); + + assertThat(cell.asFloat().isNull()).isFalse(); + assertThat(cell.asFloat().value()).isEqualTo(0.001f); + assertThat(cell.asFloat().safeValue()).isEqualTo(0.001f); + + assertThat(cell.asInteger().isNull()).isFalse(); + assertThat(cell.asInteger().safeValue()).isNull(); + + assertThat(cell.asLong().isNull()).isFalse(); + assertThat(cell.asLong().safeValue()).isNull(); + } + + @Test(expected = NumberFormatException.class) + public void should_throw_exception_on_unsafe_operation() { + Cell cell = ROW.cell(2); + assertThat(cell.columnIndex()).isEqualTo(2); + + cell.asInteger().value(); + } +} \ No newline at end of file diff --git a/src/test/java/com/coreoz/windmill/imports/parsers/excel/ExcelRowCellTest.java b/src/test/java/com/coreoz/windmill/imports/parsers/excel/ExcelRowCellTest.java new file mode 100644 index 0000000..527def2 --- /dev/null +++ b/src/test/java/com/coreoz/windmill/imports/parsers/excel/ExcelRowCellTest.java @@ -0,0 +1,108 @@ +package com.coreoz.windmill.imports.parsers.excel; + +import com.coreoz.windmill.imports.Cell; +import com.coreoz.windmill.imports.FileSchema; +import com.coreoz.windmill.utils.IteratorStreams; +import org.apache.poi.hssf.usermodel.HSSFRow; +import org.apache.poi.hssf.usermodel.HSSFSheet; +import org.apache.poi.hssf.usermodel.HSSFWorkbook; +import org.apache.poi.ss.usermodel.CellType; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.List; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ExcelRowCellTest { + + private static ExcelRow ROW; + + @BeforeClass + public static void initializeRow() { + HSSFWorkbook workbook = new HSSFWorkbook(); + HSSFSheet sheet = workbook.createSheet(); + + HSSFRow headerRow = sheet.createRow(0); + headerRow.createCell(0, CellType.STRING).setCellValue("name"); + headerRow.createCell(1, CellType.STRING).setCellValue("integer"); + headerRow.createCell(2, CellType.STRING).setCellValue("formula"); + + HSSFRow dataRow = sheet.createRow(1); + dataRow.createCell(0, CellType.STRING).setCellValue("foobar"); + dataRow.createCell(1, CellType.FORMULA).setCellFormula("SQRT(3 * 3)"); + dataRow.createCell(2, CellType.FORMULA).setCellFormula("SQRT(0.001 * 0.001)"); + + workbook.getCreationHelper().createFormulaEvaluator().evaluateAll(); + + List headerCells = IteratorStreams.stream(headerRow.cellIterator()) + .map(c -> new ExcelCell(c.getColumnIndex(), c, false)) + .collect(Collectors.toList()); + + ROW = new ExcelRow(sheet.getRow(1), new FileSchema(headerCells), false); + } + + @Test + public void check_for_correct_row_index() { + assertThat(ROW.rowIndex()).isEqualTo(1); + } + + @Test + public void iterator_should_return_correct_amount_of_entries() { + assertThat(ROW.iterator()).hasSize(3); + } + + @Test + public void should_discover_columns() { + assertThat(ROW.columnExists("name")).isTrue(); + assertThat(ROW.columnExists("integer")).isTrue(); + assertThat(ROW.columnExists("foobar")).isFalse(); + } + + @Test + public void should_discover_integer_number() { + Cell cell = ROW.cell(1); + + assertThat(cell.asString()).isEqualTo("3"); + + assertThat(cell.asInteger().isNull()).isFalse(); + assertThat(cell.asInteger().value()).isEqualTo(3); + assertThat(cell.asInteger().safeValue()).isEqualTo(3); + + assertThat(cell.asLong().isNull()).isFalse(); + assertThat(cell.asLong().value()).isEqualTo(3); + assertThat(cell.asLong().safeValue()).isEqualTo(3); + } + + @Test + public void should_discover_fractional_number() { + Cell cell = ROW.cell("formula"); + + assertThat(cell.asString()).isEqualTo("0.001"); + assertThat(cell.toString()).isEqualTo("0.001"); + + assertThat(cell.asDouble().isNull()).isFalse(); + assertThat(cell.asDouble().value()).isEqualTo(0.001d); + assertThat(cell.asDouble().safeValue()).isEqualTo(0.001d); + + assertThat(cell.asFloat().isNull()).isFalse(); + assertThat(cell.asFloat().value()).isEqualTo(0.001f); + assertThat(cell.asFloat().safeValue()).isEqualTo(0.001f); + + assertThat(cell.asInteger().isNull()).isFalse(); + assertThat(cell.asInteger().safeValue()).isNull(); + + assertThat(cell.asLong().isNull()).isFalse(); + assertThat(cell.asLong().safeValue()).isNull(); + } + + @Test + public void weird_behaviour_compared_with_csv_cell() { + Cell cell = ROW.cell(2); + assertThat(cell.columnIndex()).isEqualTo(2); + + Integer value = cell.asInteger().value(); + assertThat(value).isEqualTo(0); + } +} \ No newline at end of file