From 8279a73c0dd233d680d9da35e77bdefca284a05b Mon Sep 17 00:00:00 2001 From: Andreas Isler Date: Fri, 7 Nov 2025 19:38:42 +0100 Subject: [PATCH] Added ExcelMapper --- .gitignore | 1 + .idea/misc.xml | 3 +- pom.xml | 11 +- .../example/parser/FlowableExcelMapper.java | 100 ++++++++++++++++++ .../example/parser/FlowableExcelParser.java | 29 +++-- .../java/com/example/parser/ParserApp.java | 3 +- .../parser/FlowableExcelMapperTest.java | 50 +++++++++ .../parser/FlowableExcelParserTest.java | 10 +- .../flowableExcelMapperTestData1.xlsx | Bin 0 -> 48230 bytes 9 files changed, 185 insertions(+), 22 deletions(-) create mode 100644 src/main/java/com/example/parser/FlowableExcelMapper.java create mode 100644 src/test/java/com/example/parser/FlowableExcelMapperTest.java create mode 100644 src/test/resources/flowableExcelMapperTestData1.xlsx diff --git a/.gitignore b/.gitignore index 549e00a..e92c632 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,4 @@ build/ ### VS Code ### .vscode/ +/target/ diff --git a/.idea/misc.xml b/.idea/misc.xml index b268cf8..b169464 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,3 +1,4 @@ + @@ -73,7 +74,7 @@ - + \ No newline at end of file diff --git a/pom.xml b/pom.xml index c85fa9d..2a38a9a 100644 --- a/pom.xml +++ b/pom.xml @@ -1,11 +1,12 @@ - 4.0.0 org.springframework.boot spring-boot-starter-parent - 3.3.4 + 3.5.7 com.example @@ -27,7 +28,7 @@ - 17 + 11 @@ -54,12 +55,12 @@ org.apache.poi poi - 5.2.5 + 5.4.1 org.apache.poi poi-ooxml - 5.2.5 + 5.4.1 org.jetbrains diff --git a/src/main/java/com/example/parser/FlowableExcelMapper.java b/src/main/java/com/example/parser/FlowableExcelMapper.java new file mode 100644 index 0000000..16c0dd0 --- /dev/null +++ b/src/main/java/com/example/parser/FlowableExcelMapper.java @@ -0,0 +1,100 @@ +package com.example.parser; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.springframework.stereotype.Component; + +import java.util.*; + +@Component +public class FlowableExcelMapper { + + protected static ObjectMapper objectMapper = new ObjectMapper(); + protected static FlowableExcelParser flowableExcelParser = new FlowableExcelParser(); + + public ArrayNode excelToJsonNode(String excelPath) { + ArrayList> rowList = flowableExcelParser.parseExcel(excelPath); + ArrayNode arrayNode = objectMapper.createArrayNode(); + for (LinkedHashMap row : rowList) { + JsonNode jsonNode = null; + Set> entries = row.entrySet(); + for (Map.Entry entry : entries) { + jsonNode = addNode(jsonNode, entry.getKey(), entry.getValue()); + } + arrayNode.add(jsonNode); + } + return arrayNode; + } + + public JsonNode addNode(JsonNode parent, String path, Object value) { + objectMapper = new ObjectMapper(); + + if (parent == null) { + parent = objectMapper.createObjectNode(); + } + + int delimiterPosition = indexOfFirstDelimiter(path, ".*"); + if (delimiterPosition <= 0) { + if (parent instanceof ObjectNode) { + // Key of a value in an ObjectNode + ((ObjectNode) parent).set(path, objectMapper.valueToTree(value)); + } else { + // Index of a value in an ArrayNode + int index = Integer.parseInt(path); + for (int i = 0; parent.size() <= index; i++) { + ((ArrayNode) parent).add(objectMapper.createObjectNode()); + } + ((ArrayNode) parent).set(index, objectMapper.valueToTree(value)); + } + return parent; + } + + String key = path.substring(0, delimiterPosition); + String newPath = path.substring(delimiterPosition + 1); + String delimiter = path.substring(delimiterPosition, delimiterPosition + 1); + JsonNode child = null; + if (delimiter.equals(".")) { + child = objectMapper.createObjectNode(); + } else if (delimiter.equals("*")) { + child = objectMapper.createArrayNode(); + } + + JsonNode node; + if (parent instanceof ObjectNode) { + // Key of a JsonNode in an ObjectNode + node = parent.get(key); + if (node != null) { + // Take the existing ObjectNode as child + child = node; + } + ((ObjectNode) parent).set(key, addNode(child, newPath, value)); + } else { + // Index of a JsonNode in an ArrayNode + int index = Integer.parseInt(key); + node = parent.get(index); + if (node != null) { + // Take the existing ArrayNode as child + child = node; + } + // Create empty entries to parent ArrayNode + for (int i = 0; parent.size() <= index; i++) { + ((ArrayNode) parent).add(objectMapper.createObjectNode()); + } + ((ArrayNode) parent).set(index, addNode(child, newPath, value)); + } + return parent; + } + + public static int indexOfFirstDelimiter(String str, String delimiters) { + for (int i = 0; i < str.length(); i++) { + for (char c : delimiters.toCharArray()) { + if (str.charAt(i) == c) { + return i; + } + } + } + return -1; + } +} diff --git a/src/main/java/com/example/parser/FlowableExcelParser.java b/src/main/java/com/example/parser/FlowableExcelParser.java index 1efdc27..8ff5a7a 100644 --- a/src/main/java/com/example/parser/FlowableExcelParser.java +++ b/src/main/java/com/example/parser/FlowableExcelParser.java @@ -9,11 +9,11 @@ import java.io.FileInputStream; import java.io.IOException; import java.util.ArrayList; import java.util.LinkedHashMap; -import java.util.List; @Component public class FlowableExcelParser { - public ArrayList> parseExcel(String resourcePath, List colNumberList) { + + public ArrayList> parseExcel(String resourcePath) { ArrayList> rowList = new ArrayList<>(); try (FileInputStream fileInputStream = new FileInputStream(resourcePath)) { Workbook workbook = new XSSFWorkbook(fileInputStream); @@ -22,25 +22,36 @@ public class FlowableExcelParser { for (Row row : sheet) { if (row.getRowNum() == 0) { // Header row - for (Integer colNumber : colNumberList) { - colHeaders.add(row.getCell(colNumber).getStringCellValue()); + for (int colNum = 0; colNum < row.getLastCellNum(); colNum++) { + Cell cell = row.getCell(colNum); + if (cell == null || cell.getCellType() == CellType.BLANK || cell.getCellType() == CellType._NONE) { + // Take all columns until the first empty header cell + break; + } else { + colHeaders.add(cell.getStringCellValue()); + } } continue; } + boolean allCellsNull = true; LinkedHashMap rowMap = new LinkedHashMap<>(); - for (int i = 0; i < colNumberList.size(); i++) { - Integer colNumber = colNumberList.get(i); + for (int colNum = 0; colNum < colHeaders.size(); colNum++) { Object content = null; - Cell cell = row.getCell(colNumber); + Cell cell = row.getCell(colNum); CellValue cellValue = workbook.getCreationHelper().createFormulaEvaluator().evaluate(cell); - if (cellValue != null) { + if (cellValue != null && cell.getCellType() != CellType.BLANK && cell.getCellType() != 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; } - rowMap.put(colHeaders.get(i), content); + rowMap.put(colHeaders.get(colNum), content); + } + if (allCellsNull) { + // Take all rows until the first empty row + return rowList; } rowList.add(rowMap); } diff --git a/src/main/java/com/example/parser/ParserApp.java b/src/main/java/com/example/parser/ParserApp.java index 2dc04e3..74ed2aa 100644 --- a/src/main/java/com/example/parser/ParserApp.java +++ b/src/main/java/com/example/parser/ParserApp.java @@ -9,7 +9,6 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; import java.io.IOException; import java.io.InputStream; import java.net.URL; -import java.util.List; import java.util.Map; @SpringBootApplication @@ -32,6 +31,6 @@ public class ParserApp { ClassLoader classLoader = ParserApp.class.getClassLoader(); URL resourceUrl = classLoader.getResource("flowableExcelParserExampleData.xlsx"); assert resourceUrl != null; - logger.info("parsed excel={}", flowableExcelParser.parseExcel(resourceUrl.getPath(), List.of(0,1,2,4,5,6))); + logger.info("parsed excel={}", flowableExcelParser.parseExcel(resourceUrl.getPath())); } } diff --git a/src/test/java/com/example/parser/FlowableExcelMapperTest.java b/src/test/java/com/example/parser/FlowableExcelMapperTest.java new file mode 100644 index 0000000..e25becc --- /dev/null +++ b/src/test/java/com/example/parser/FlowableExcelMapperTest.java @@ -0,0 +1,50 @@ +package com.example.parser; + +import com.fasterxml.jackson.databind.JsonNode; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.net.URL; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@SpringBootTest +class FlowableExcelMapperTest { + + @Autowired + private FlowableExcelMapper flowableExcelMapper; + + private static final Logger logger = LoggerFactory.getLogger(ParserApp.class); + + + @Test + public void addNodeTest() { + JsonNode node = null; + node = flowableExcelMapper.addNode(node, "3*0*2", "String1"); + node = flowableExcelMapper.addNode(node, "3*0*3", "String1"); + node = flowableExcelMapper.addNode(node, "3*1.1", 234); + node = flowableExcelMapper.addNode(node, "3*1.2", 234); + node = flowableExcelMapper.addNode(node, "3*2", false); + node = flowableExcelMapper.addNode(node, "4.5", "String1"); + node = flowableExcelMapper.addNode(node, "4.6", 234); + node = flowableExcelMapper.addNode(node, "4.7", false); + logger.info("node2={}{}", System.lineSeparator(), node.toPrettyString()); + } + + protected String getResourcePath(String path) { + ClassLoader classLoader = FlowableExcelMapperTest.class.getClassLoader(); + URL resourceUrl = classLoader.getResource(path); + Assertions.assertThat(resourceUrl).isNotNull(); + return resourceUrl.getPath(); + } + + @Test + public void excelToJsonNodeTest() { + JsonNode node = flowableExcelMapper.excelToJsonNode(getResourcePath("flowableExcelMapperTestData1.xlsx")); + logger.info("node2={}{}", System.lineSeparator(), node.toPrettyString()); + } +} diff --git a/src/test/java/com/example/parser/FlowableExcelParserTest.java b/src/test/java/com/example/parser/FlowableExcelParserTest.java index d16e72c..ed4f4de 100644 --- a/src/test/java/com/example/parser/FlowableExcelParserTest.java +++ b/src/test/java/com/example/parser/FlowableExcelParserTest.java @@ -20,12 +20,12 @@ class FlowableExcelParserTest { @Autowired private FlowableExcelParser flowableExcelParser; - private Stream GetArgumentsFromExcel(String path, List colNumberList) { + private Stream GetArgumentsFromExcel(String path) { ArrayList argumentList = new ArrayList<>(); ClassLoader classLoader = FlowableExcelParserTest.class.getClassLoader(); URL resourceUrl = classLoader.getResource(path); Assertions.assertThat(resourceUrl).isNotNull(); - ArrayList> rowList = flowableExcelParser.parseExcel(resourceUrl.getPath(), colNumberList); + ArrayList> rowList = flowableExcelParser.parseExcel(resourceUrl.getPath()); Assertions.assertThat(rowList).isNotNull(); for (LinkedHashMap row : rowList) { LinkedHashMap inputMap = new LinkedHashMap<>(); @@ -46,7 +46,7 @@ class FlowableExcelParserTest { @NotNull private Stream booleanExcelParserTestDataProvider() { - return GetArgumentsFromExcel("flowableExcelParserTestData1.xlsx", List.of(0,1,2,4,5)); + return GetArgumentsFromExcel("flowableExcelParserTestData1.xlsx"); } @ParameterizedTest @@ -67,7 +67,7 @@ class FlowableExcelParserTest { @NotNull private Stream stringExcelParserTestDataProvider() { - return GetArgumentsFromExcel("flowableExcelParserTestData2.xlsx", List.of(0,1,2,4,5,6)); + return GetArgumentsFromExcel("flowableExcelParserTestData2.xlsx"); } @ParameterizedTest @@ -92,7 +92,7 @@ class FlowableExcelParserTest { @NotNull private Stream numberExcelParserTestDataProvider() { - return GetArgumentsFromExcel("flowableExcelParserTestData3.xlsx", List.of(0,1,2,4,5,6)); + return GetArgumentsFromExcel("flowableExcelParserTestData3.xlsx"); } @ParameterizedTest diff --git a/src/test/resources/flowableExcelMapperTestData1.xlsx b/src/test/resources/flowableExcelMapperTestData1.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..60d14066e1f06e10c9caff3521250c81780522f7 GIT binary patch literal 48230 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

_OY@LZsaK;;6x1UNSsB%ry$2m#Lx#tBqz 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 NF@LWYhJN+E_g}2xT-pEt literal 0 HcmV?d00001