diff --git a/src/main/java/com/example/parser/FlowableExcelMapper.java b/src/main/java/com/example/parser/FlowableExcelMapper.java index b9743c5..12bc072 100644 --- a/src/main/java/com/example/parser/FlowableExcelMapper.java +++ b/src/main/java/com/example/parser/FlowableExcelMapper.java @@ -2,7 +2,6 @@ package com.example.parser; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; @@ -21,12 +20,37 @@ public class FlowableExcelMapper { protected static FlowableExcelParser flowableExcelParser = new FlowableExcelParser(); public FlowableExcelMapper() { + // Enable ObjectMapper for handling Instant objectMapper.registerModule(javaTimeModule); - objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); } - public ArrayNode excelToJsonNode(String excelPath) { - ArrayList> rows = flowableExcelParser.parseExcel(excelPath); + // excelPath defines the location of the Excel file + // FlowableExcelParser is used to create an ArrayList from the Excel file + // The first row of every sheet contains the path of the variables to be created in the JsonNode + // Delimiter is "." for an ObjectNode and "*" for an ArrayNode + // The 2nd row contains tht type of the variable. Supported types are: + // - String + // - Boolean + // - Integer + // - Double + // - Date + // All sheets are added to an ArrayNode + // All rows are added to an ArrayNode in the sheets ArrayNode + // All cell values are added to an ObjectNode in the rows ArrayNode + public ArrayNode excelBookToJsonNode(String excelPath) { + ArrayList>> book = flowableExcelParser.parseExcelBook(excelPath); + return bookToJsonNode(book); + } + + protected ArrayNode bookToJsonNode(ArrayList>> book) { + ArrayNode bookNode = objectMapper.createArrayNode(); + for (ArrayList> sheet : book) { + bookNode.add(sheetToJsonNode(sheet)); + } + return bookNode; + } + + protected ArrayNode sheetToJsonNode(ArrayList> rows) { ArrayList paths = new ArrayList<>(); ArrayList types = new ArrayList<>(); ArrayNode arrayNode = objectMapper.createArrayNode(); @@ -39,31 +63,35 @@ public class FlowableExcelMapper { types.addAll(rows.get(rowNum)); } else { // All other rows contain the values of the variables - JsonNode jsonNode = null; - ArrayList row = rows.get(rowNum); - for (int cellNum = 0; cellNum < row.size(); cellNum++) { - Object content = row.get(cellNum); - String type = types.get(cellNum).toString(); - switch (type) { - case "String": content = content.toString(); break; - case "Boolean": content = Boolean.parseBoolean(content.toString()); break; - case "Integer": content = (int) Double.parseDouble(content.toString()); break; - case "Double": content = Double.parseDouble(content.toString()); break; - case "Date": - // Excel considers dates as local dates, but we want them as UTC - Date dateTime = DateUtil.getJavaDate(Double.parseDouble(content.toString())); - content = dateTime.toInstant().plusSeconds(ZonedDateTime.now().getOffset().getTotalSeconds()); - break; - default: break; - } - jsonNode = addNode(jsonNode, paths.get(cellNum).toString(), content); - } - arrayNode.add(jsonNode); + arrayNode.add(rowToJsonNode(types, paths, rows.get(rowNum), null)); } } return arrayNode; } + protected JsonNode rowToJsonNode(ArrayList types, ArrayList paths, ArrayList row, JsonNode jsonNode) { + for (int cellNum = 0; cellNum < row.size(); cellNum++) { + Object content = row.get(cellNum); + if (content != null) { + String type = types.get(cellNum).toString(); + switch (type) { + case "String": content = content.toString(); break; + case "Boolean": content = Boolean.parseBoolean(content.toString()); break; + case "Integer": content = (int) Double.parseDouble(content.toString()); break; + case "Double": content = Double.parseDouble(content.toString()); break; + case "Date": + // Excel considers dates as local dates, but we want them as UTC + Date dateTime = DateUtil.getJavaDate(Double.parseDouble(content.toString())); + content = dateTime.toInstant().plusSeconds(ZonedDateTime.now().getOffset().getTotalSeconds()); + break; + default: break; + } + } + jsonNode = addNode(jsonNode, paths.get(cellNum).toString(), content); + } + return jsonNode; + } + protected JsonNode addNode(JsonNode parent, String path, Object value) { if (parent == null) { parent = objectMapper.createObjectNode(); @@ -77,7 +105,8 @@ public class FlowableExcelMapper { } else { // Index of a value in an ArrayNode int index = Integer.parseInt(path); - for (int i = 0; parent.size() <= index; i++) { + while (parent.size() <= index) { + // Create empty entries to parent ArrayNode ((ArrayNode) parent).add(objectMapper.createObjectNode()); } ((ArrayNode) parent).set(index, objectMapper.valueToTree(value)); @@ -112,8 +141,8 @@ public class FlowableExcelMapper { // Take the existing ArrayNode as child child = node; } - // Create empty entries to parent ArrayNode - for (int i = 0; parent.size() <= index; i++) { + while (parent.size() <= index) { + // Create empty entries to parent ArrayNode ((ArrayNode) parent).add(objectMapper.createObjectNode()); } ((ArrayNode) parent).set(index, addNode(child, newPath, value)); diff --git a/src/main/java/com/example/parser/FlowableExcelParser.java b/src/main/java/com/example/parser/FlowableExcelParser.java index 0ba5da8..2db2707 100644 --- a/src/main/java/com/example/parser/FlowableExcelParser.java +++ b/src/main/java/com/example/parser/FlowableExcelParser.java @@ -9,62 +9,104 @@ import java.io.FileInputStream; import java.io.IOException; import java.util.ArrayList; + @Component public class FlowableExcelParser { - // resourcePath defines the location of the Excel file - // Return all cells in the 1st sheet which have values as objects (String, boolean or double) - // The 1st row defines the amount of cells per row - // Parsing the sheet stops after the 1st empty row - public ArrayList> parseExcel(String resourcePath) { - ArrayList> rows = new ArrayList<>(); - try (FileInputStream fileInputStream = new FileInputStream(resourcePath)) { - Workbook workbook = new XSSFWorkbook(fileInputStream); - Sheet sheet = workbook.getSheetAt(0); - int maxCells = 0; - for (int rowNum = 0; rowNum < sheet.getLastRowNum(); rowNum++) { - Row row = sheet.getRow(rowNum); - if (row == null) { - break; - } - if (maxCells == 0) { - maxCells = row.getLastCellNum(); - } - boolean allCellsNull = true; - ArrayList cells = new ArrayList<>(); - for (int cellNum = 0; cellNum < maxCells; cellNum++) { - Cell cell = row.getCell(cellNum); - if(rowNum == 0) { - if (cell == null || cell.getCellType() == CellType.BLANK || cell.getCellType() == CellType._NONE) { - // Count cells of 1st row until the first empty cell - maxCells = cellNum; - break; - } - } - CellValue cellValue = workbook.getCreationHelper().createFormulaEvaluator().evaluate(cell); - Object content = null; - if (cellValue != null) { - CellType cellType = cell.getCellType(); - if (cellType != CellType.BLANK && cellType != CellType._NONE) { - switch (cellValue.getCellType()) { - case STRING : content = cellValue.getStringValue(); break; - case BOOLEAN : content = cellValue.getBooleanValue(); break; - case NUMERIC : content = cellValue.getNumberValue(); break; - } - allCellsNull = false; - } - } - cells.add(content); - } - if (allCellsNull) { - // Take all rows until the first empty row - return rows; - } - rows.add(cells); - } + // excelPath defines the location of the Excel file + // Return all cells in all sheets with the following restrictions: + // - Max columns per sheet is defined in the first row + // - Max column count starts with the 1st cell and ends with the 1st empty cell + // - Parsing the sheet stops after the 1st empty row + // All sheets are added to an ArrayList + // All rows are added to an ArrayList in the sheets ArrayList + // All cell values are added to an ArrayList in the rows ArrayList + // Formulas in cells are evaluated + // Excel values can have only 3 types: String, boolean, double + public ArrayList>> parseExcelBook(String excelPath) { + try (FileInputStream fileInputStream = new FileInputStream(excelPath)) { + Workbook workBook = new XSSFWorkbook(fileInputStream); + return parseBook(workBook); } catch (IOException e) { throw new RuntimeException(e); } - return rows; + } + + protected ArrayList>> parseBook(Workbook workBook ) { + ArrayList>> book = new ArrayList<>(); + FormulaEvaluator formulaEvaluator = workBook.getCreationHelper().createFormulaEvaluator(); + for (Sheet workSheet : workBook) { + book.add(parseSheet(workSheet, formulaEvaluator)); + } + return book; + } + + protected ArrayList> parseSheet(Sheet workSheet, FormulaEvaluator formulaEvaluator) { + ArrayList> sheet = new ArrayList<>(); + // Count columns of first row + int maxCols = getMaxCols(workSheet.getRow(0)); + int lastRowNum = workSheet.getLastRowNum(); + for (int rowIndex = 0; rowIndex < lastRowNum; rowIndex++) { + Row workRow = workSheet.getRow(rowIndex); + ArrayList row = parseRow(workRow, maxCols, formulaEvaluator); + if (row == null) { + // Take all rows until the first empty row + break; + } else { + sheet.add(row); + } + } + return sheet; + } + + protected ArrayList parseRow(Row workRow, int maxCols, FormulaEvaluator formulaEvaluator) { + if (workRow == null) { + // Row is empty + return null; + } + ArrayList row = new ArrayList<>(); + boolean allCellsNull = true; + for (int colIndex = 0; colIndex < maxCols; colIndex++) { + Object content = null; + Cell workCell = workRow.getCell(colIndex); + if (workCell != null) { + CellType workCellType = workCell.getCellType(); + if (workCellType != CellType.BLANK && workCellType != CellType._NONE) { + allCellsNull = false; + switch (workCellType) { + case STRING : content = workCell.getStringCellValue(); break; + case BOOLEAN : content = workCell.getBooleanCellValue(); break; + case NUMERIC : content = workCell.getNumericCellValue(); break; + case FORMULA : + CellValue evaluatedValue = formulaEvaluator.evaluate(workCell); + switch (evaluatedValue.getCellType()) { + case STRING : content = evaluatedValue.getStringValue(); break; + case BOOLEAN : content = evaluatedValue.getBooleanValue(); break; + case NUMERIC : content = evaluatedValue.getNumberValue(); break; + } + break; + } + } + } + row.add(content); + } + if (allCellsNull) { + // Row is empty + return null; + } else { + return row; + } + } + + protected int getMaxCols(Row workRow) { + int maxCol = workRow.getLastCellNum(); + for (int colIndex = 0; colIndex < maxCol; colIndex++) { + Cell workCell = workRow.getCell(colIndex); + if (workCell == null || workCell.getCellType() == CellType.BLANK || workCell.getCellType() == CellType._NONE) { + // Count cells until the first empty cell + return colIndex; + } + } + return maxCol; } } diff --git a/src/main/java/com/example/parser/ParserApp.java b/src/main/java/com/example/parser/ParserApp.java index 414987a..0a740ca 100644 --- a/src/main/java/com/example/parser/ParserApp.java +++ b/src/main/java/com/example/parser/ParserApp.java @@ -31,10 +31,10 @@ public class ParserApp { URL resourceUrl = ParserApp.class.getClassLoader().getResource("flowableExcelParserExampleData.xlsx"); assert resourceUrl != null; - logger.info("parsed excel={}", flowableExcelParser.parseExcel(resourceUrl.getPath())); + logger.info("parsed excel={}", flowableExcelParser.parseExcelBook(resourceUrl.getPath())); resourceUrl = ParserApp.class.getClassLoader().getResource("flowableExcelMapperExampleData.xlsx"); assert resourceUrl != null; - logger.info("mapped excel={}", flowableExcelMapper.excelToJsonNode(resourceUrl.getPath())); + logger.info("mapped excel={}", flowableExcelMapper.excelBookToJsonNode(resourceUrl.getPath())); } } diff --git a/src/test/java/com/example/parser/FlowableExcelMapperTest.java b/src/test/java/com/example/parser/FlowableExcelMapperTest.java index 6227178..deca194 100644 --- a/src/test/java/com/example/parser/FlowableExcelMapperTest.java +++ b/src/test/java/com/example/parser/FlowableExcelMapperTest.java @@ -3,7 +3,6 @@ package com.example.parser; import com.fasterxml.jackson.databind.JsonNode; import org.assertj.core.api.Assertions; import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -17,6 +16,7 @@ import java.net.URL; import java.util.ArrayList; import java.util.stream.Stream; + @TestInstance(TestInstance.Lifecycle.PER_CLASS) @SpringBootTest class FlowableExcelMapperTest { @@ -34,25 +34,24 @@ class FlowableExcelMapperTest { return resourceUrl.getPath(); } - @Test - public void excelToJsonNodeTest() { - JsonNode node = flowableExcelMapper.excelToJsonNode(getResourcePath("flowableExcelMapperTestData.xlsx")); - logger.info("node2={}{}", System.lineSeparator(), node.toPrettyString()); - } - - @NotNull - private Stream excelToJsonNodeParametrizedTestData() { + protected Stream getArgumentsFromExcel(String path) { ArrayList argumentList = new ArrayList<>(); - JsonNode nodes = flowableExcelMapper.excelToJsonNode(getResourcePath("flowableExcelMapperTestData.xlsx")); - for (JsonNode node : nodes) { - argumentList.add(Arguments.of(node)); + JsonNode book = flowableExcelMapper.excelBookToJsonNode(getResourcePath(path)); + for (JsonNode sheet : book) { + for (JsonNode row : sheet) { + argumentList.add(Arguments.of(row)); + } } return argumentList.stream(); } + @NotNull + protected Stream excelMapperTestData() { + return getArgumentsFromExcel("flowableExcelMapperTestData.xlsx"); + } @ParameterizedTest - @MethodSource("excelToJsonNodeParametrizedTestData") - public void excelToJsonNodeParametrizedTest(JsonNode node) { - logger.info("node2={}{}", System.lineSeparator(), node.toPrettyString()); + @MethodSource("excelMapperTestData") + public void excelMapperTest(JsonNode sheet) { + logger.info("node2={}{}", System.lineSeparator(), sheet.toPrettyString()); } } diff --git a/src/test/java/com/example/parser/FlowableExcelParserTest.java b/src/test/java/com/example/parser/FlowableExcelParserTest.java index 21d3352..c641cf2 100644 --- a/src/test/java/com/example/parser/FlowableExcelParserTest.java +++ b/src/test/java/com/example/parser/FlowableExcelParserTest.java @@ -13,6 +13,7 @@ import java.net.URL; import java.util.*; import java.util.stream.Stream; + @TestInstance(TestInstance.Lifecycle.PER_CLASS) @SpringBootTest class FlowableExcelParserTest { @@ -27,22 +28,23 @@ class FlowableExcelParserTest { return resourceUrl.getPath(); } - protected Stream GetArgumentsFromExcel(String path) { + protected Stream getArgumentsFromExcel(String path) { + ArrayList>> book = flowableExcelParser.parseExcelBook(getResourcePath(path)); ArrayList argumentList = new ArrayList<>(); - ArrayList> rowList = flowableExcelParser.parseExcel(getResourcePath(path)); - for (ArrayList row : rowList) { - argumentList.add(Arguments.of(row)); + for (ArrayList> sheet : book) { + for (ArrayList row : sheet) { + argumentList.add(Arguments.of(row)); + } } return argumentList.stream(); } @NotNull - private Stream booleanExcelParserTestDataProvider() { - return GetArgumentsFromExcel("flowableExcelParserTestData1.xlsx"); + protected Stream booleanExcelParserTestData() { + return getArgumentsFromExcel("flowableExcelParserTestData1.xlsx"); } - @ParameterizedTest - @MethodSource("booleanExcelParserTestDataProvider") + @MethodSource("booleanExcelParserTestData") void booleanExcelParserTest(ArrayList row) { Boolean valOr = (Boolean) row.get(0) || (Boolean) row.get(1) || (Boolean) row.get(2); Assertions.assertThat(row.get(3)).isEqualTo(valOr); @@ -52,12 +54,11 @@ class FlowableExcelParserTest { } @NotNull - private Stream stringExcelParserTestDataProvider() { - return GetArgumentsFromExcel("flowableExcelParserTestData2.xlsx"); + protected Stream stringExcelParserTestData() { + return getArgumentsFromExcel("flowableExcelParserTestData2.xlsx"); } - @ParameterizedTest - @MethodSource("stringExcelParserTestDataProvider") + @MethodSource("stringExcelParserTestData") void stringExcelParserTest(ArrayList row) { String val12 = row.get(0).toString() + row.get(1).toString(); Assertions.assertThat(row.get(3)).isEqualTo(val12); @@ -70,12 +71,11 @@ class FlowableExcelParserTest { } @NotNull - private Stream numberExcelParserTestDataProvider() { - return GetArgumentsFromExcel("flowableExcelParserTestData3.xlsx"); + protected Stream numberExcelParserTestData() { + return getArgumentsFromExcel("flowableExcelParserTestData3.xlsx"); } - @ParameterizedTest - @MethodSource("numberExcelParserTestDataProvider") + @MethodSource("numberExcelParserTestData") void numberExcelParserTest(ArrayList row) { Double val12 = (Double) row.get(0) + (Double) row.get(1); Assertions.assertThat(row.get(3)).isEqualTo(val12); diff --git a/src/test/resources/flowableExcelMapperTestData.xlsx b/src/test/resources/flowableExcelMapperTestData.xlsx index effac21..8986492 100644 Binary files a/src/test/resources/flowableExcelMapperTestData.xlsx and b/src/test/resources/flowableExcelMapperTestData.xlsx differ diff --git a/target/classes/com/example/parser/FlowableExcelParser$1.class b/target/classes/com/example/parser/FlowableExcelParser$1.class index 83a8f4b..3070480 100644 Binary files a/target/classes/com/example/parser/FlowableExcelParser$1.class and b/target/classes/com/example/parser/FlowableExcelParser$1.class differ diff --git a/target/classes/com/example/parser/FlowableExcelParser.class b/target/classes/com/example/parser/FlowableExcelParser.class index b8e4d16..316f7a3 100644 Binary files a/target/classes/com/example/parser/FlowableExcelParser.class and b/target/classes/com/example/parser/FlowableExcelParser.class differ diff --git a/target/classes/com/example/parser/ParserApp.class b/target/classes/com/example/parser/ParserApp.class index 0399618..3e73505 100644 Binary files a/target/classes/com/example/parser/ParserApp.class and b/target/classes/com/example/parser/ParserApp.class differ diff --git a/target/test-classes/com/example/parser/FlowableExcelParserTest.class b/target/test-classes/com/example/parser/FlowableExcelParserTest.class index 8249fde..d73d4c5 100644 Binary files a/target/test-classes/com/example/parser/FlowableExcelParserTest.class and b/target/test-classes/com/example/parser/FlowableExcelParserTest.class differ