From cae86145fb65fbcfc51ee2dddce6f0e9ad08b97e Mon Sep 17 00:00:00 2001 From: totocaca Date: Sun, 30 Mar 2025 16:42:37 +0200 Subject: [PATCH 1/5] Add excel provider and improve csv --- pom.xml | 10 + .../plugins/reporter/provider/Csv.java | 162 ++------- .../plugins/reporter/provider/Excel.java | 311 ++++++++++++++++++ .../plugins/reporter/util/TabularData.java | 175 ++++++++++ .../reporter/provider/CreateExcelSample.java | 64 ++++ .../provider/ExcelTestFileGenerator.java | 130 ++++++++ src/test/resources/test-excel-mixed.xlsx | Bin 0 -> 3400 bytes src/test/resources/test-excel-normal.xlsx | Bin 0 -> 3406 bytes src/test/resources/test-excel-offset.xlsx | Bin 0 -> 3409 bytes src/test/resources/test-excel.xlsx | 1 + 10 files changed, 716 insertions(+), 137 deletions(-) create mode 100644 src/main/java/io/jenkins/plugins/reporter/provider/Excel.java create mode 100644 src/main/java/io/jenkins/plugins/reporter/util/TabularData.java create mode 100644 src/test/java/io/jenkins/plugins/reporter/provider/CreateExcelSample.java create mode 100644 src/test/java/io/jenkins/plugins/reporter/provider/ExcelTestFileGenerator.java create mode 100644 src/test/resources/test-excel-mixed.xlsx create mode 100644 src/test/resources/test-excel-normal.xlsx create mode 100644 src/test/resources/test-excel-offset.xlsx create mode 100644 src/test/resources/test-excel.xlsx diff --git a/pom.xml b/pom.xml index 634f806e..5bcb03f5 100644 --- a/pom.xml +++ b/pom.xml @@ -117,6 +117,16 @@ jackson-dataformat-csv ${jackson-dataformat.version} + + org.apache.poi + poi + 5.4.0 + + + org.apache.poi + poi-ooxml + 5.4.0 + diff --git a/src/main/java/io/jenkins/plugins/reporter/provider/Csv.java b/src/main/java/io/jenkins/plugins/reporter/provider/Csv.java index 4417152d..272f6742 100644 --- a/src/main/java/io/jenkins/plugins/reporter/provider/Csv.java +++ b/src/main/java/io/jenkins/plugins/reporter/provider/Csv.java @@ -7,12 +7,11 @@ import hudson.Extension; import io.jenkins.plugins.reporter.Messages; -import io.jenkins.plugins.reporter.model.Item; import io.jenkins.plugins.reporter.model.Provider; import io.jenkins.plugins.reporter.model.ReportDto; import io.jenkins.plugins.reporter.model.ReportParser; +import io.jenkins.plugins.reporter.util.TabularData; import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.math.NumberUtils; import org.jenkinsci.Symbol; import org.kohsuke.stapler.DataBoundConstructor; @@ -24,18 +23,23 @@ import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; +/** + * Provider for CSV files + */ public class Csv extends Provider { private static final long serialVersionUID = 9141170397250309265L; private static final String ID = "csv"; + /** Default constructor */ @DataBoundConstructor public Csv() { super(); // empty constructor required for stapler } + /** Creates a CSV parser */ @Override public ReportParser createParser() { if (getActualId().equals(getDescriptor().getId())) { @@ -45,41 +49,45 @@ public ReportParser createParser() { return new CsvCustomParser(getActualId()); } - /** Descriptor for this provider. */ + /** Descriptor for this provider */ @Symbol("csv") @Extension - public static class Descriptor extends Provider.ProviderDescriptor { - /** Creates the descriptor instance. */ + public static class Descriptor extends ProviderDescriptor { + /** Creates a descriptor instance */ public Descriptor() { super(ID); } } + /** + * Parser for CSV files + */ public static class CsvCustomParser extends ReportParser { private static final long serialVersionUID = -8689695008930386640L; private final String id; - private List parserMessages; + /** Constructor */ public CsvCustomParser(String id) { super(); this.id = id; this.parserMessages = new ArrayList(); } + /** Returns the parser identifier */ public String getId() { return id; } - + /** Detects the delimiter used in the CSV file */ private char detectDelimiter(File file) throws IOException { // List of possible delimiters char[] delimiters = { ',', ';', '\t', '|' }; int[] delimiterCounts = new int[delimiters.length]; - // Read the lines of the file to detect the delimiter + // Read the file lines to detect the delimiter try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8))) { int linesToCheck = 5; // Number of lines to check int linesChecked = 0; @@ -105,13 +113,14 @@ private char detectDelimiter(File file) throws IOException { return detectedDelimiter; } - + /** Parses a CSV file and creates a ReportDto */ @Override public ReportDto parse(File file) throws IOException { - // Get delimiter + // Delimiter detection char delimiter = detectDelimiter(file); + // CSV parser configuration final CsvMapper mapper = new CsvMapper(); final CsvSchema schema = mapper.schemaFor(String[].class).withColumnSeparator(delimiter); @@ -121,139 +130,18 @@ public ReportDto parse(File file) throws IOException { mapper.enable(CsvParser.Feature.INSERT_NULLS_FOR_MISSING_COLUMNS); mapper.enable(CsvParser.Feature.TRIM_SPACES); + // Read CSV data final MappingIterator> it = mapper.readerForListOf(String.class) .with(schema) .readValues(file); - ReportDto report = new ReportDto(); - report.setId(getId()); - report.setItems(new ArrayList<>()); - + // Extract headers and rows final List header = it.next(); final List> rows = it.readAll(); - int rowCount = 0; - final int headerColumnCount = header.size(); - int colIdxValueStart = 0; - - if (headerColumnCount >= 2) { - rowCount = rows.size(); - } else { - parserMessages.add(String.format("skipped file - First line has %d elements", headerColumnCount + 1)); - } - - /** Parse all data rows */ - for (int rowIdx = 0; rowIdx < rowCount; rowIdx++) { - String parentId = "report"; - List row = rows.get(rowIdx); - Item last = null; - boolean lastItemAdded = false; - LinkedHashMap result = new LinkedHashMap<>(); - boolean emptyFieldFound = false; - int rowSize = row.size(); - - /** Parse untill first data line is found to get data and value field */ - if (colIdxValueStart == 0) { - /** Col 0 is assumed to be string */ - for (int colIdx = rowSize - 1; colIdx > 1; colIdx--) { - String value = row.get(colIdx); - - if (NumberUtils.isCreatable(value)) { - colIdxValueStart = colIdx; - } else { - if (colIdxValueStart > 0) { - parserMessages - .add(String.format("Found data - fields number = %d - numeric fields = %d", - colIdxValueStart, rowSize - colIdxValueStart)); - } - break; - } - } - } - - String valueId = ""; - /** Parse line if first data line is OK and line has more element than header */ - if ((colIdxValueStart > 0) && (rowSize >= headerColumnCount)) { - /** Check line and header size matching */ - for (int colIdx = 0; colIdx < headerColumnCount; colIdx++) { - String id = header.get(colIdx); - String value = row.get(colIdx); - - /** Check value fields */ - if ((colIdx < colIdxValueStart)) { - /** Test if text item is a value or empty */ - if ((NumberUtils.isCreatable(value)) || (StringUtils.isBlank(value))) { - /** Empty field found - message */ - if (colIdx == 0) { - parserMessages - .add(String.format("skipped line %d - First column item empty - col = %d ", - rowIdx + 2, colIdx + 1)); - break; - } else { - emptyFieldFound = true; - /** Continue next column parsing */ - continue; - } - } else { - /** Check if field values are present after empty cells */ - if (emptyFieldFound) { - parserMessages.add(String.format("skipped line %d Empty field in col = %d ", - rowIdx + 2, colIdx + 1)); - break; - } - } - valueId += value; - Optional parent = report.findItem(parentId, report.getItems()); - Item item = new Item(); - lastItemAdded = false; - item.setId(valueId); - item.setName(value); - String finalValueId = valueId; - if (parent.isPresent()) { - Item p = parent.get(); - if (!p.hasItems()) { - p.setItems(new ArrayList<>()); - } - if (p.getItems().stream().noneMatch(i -> i.getId().equals(finalValueId))) { - p.addItem(item); - lastItemAdded = true; - } - } else { - if (report.getItems().stream().noneMatch(i -> i.getId().equals(finalValueId))) { - report.getItems().add(item); - lastItemAdded = true; - } - } - parentId = valueId; - last = item; - } else { - Number val = 0; - if (NumberUtils.isCreatable(value)) { - val = NumberUtils.createNumber(value); - } - result.put(id, val.intValue()); - } - } - } else { - /** Skip file if first data line has no value field */ - if (colIdxValueStart == 0) { - parserMessages.add(String.format("skipped line %d - First data row not found", rowIdx + 2)); - continue; - } else { - parserMessages - .add(String.format("skipped line %d - line has fewer element than title", rowIdx + 2)); - continue; - } - } - /** If last item was created, it will be added to report */ - if (lastItemAdded) { - last.setResult(result); - } else { - parserMessages.add(String.format("ignored line %d - Same fields already exists", rowIdx + 2)); - } - } - // report.setParserLog(parserMessages); - return report; + // Create TabularData object and process it + TabularData tabularData = new TabularData(id, header, rows); + return tabularData.processData(parserMessages); } } -} \ No newline at end of file +} diff --git a/src/main/java/io/jenkins/plugins/reporter/provider/Excel.java b/src/main/java/io/jenkins/plugins/reporter/provider/Excel.java new file mode 100644 index 00000000..1639d451 --- /dev/null +++ b/src/main/java/io/jenkins/plugins/reporter/provider/Excel.java @@ -0,0 +1,311 @@ +package io.jenkins.plugins.reporter.provider; + +import hudson.Extension; +import io.jenkins.plugins.reporter.Messages; +import io.jenkins.plugins.reporter.model.Provider; +import io.jenkins.plugins.reporter.model.ReportDto; +import io.jenkins.plugins.reporter.model.ReportParser; +import io.jenkins.plugins.reporter.util.TabularData; +import org.apache.poi.ss.usermodel.*; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.jenkinsci.Symbol; +import org.kohsuke.stapler.DataBoundConstructor; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Provider for Excel (XLSX) files + */ +public class Excel extends Provider { + + private static final long serialVersionUID = 9141170397250309265L; + + private static final String ID = "excel"; + + /** Default constructor */ + @DataBoundConstructor + public Excel() { + super(); + // empty constructor required for stapler + } + + /** Creates an Excel parser */ + @Override + public ReportParser createParser() { + if (getActualId().equals(getDescriptor().getId())) { + throw new IllegalArgumentException(Messages.Provider_Error()); + } + + return new ExcelParser(getActualId()); + } + + /** Descriptor for this provider */ + @Symbol("excel") + @Extension + public static class Descriptor extends ProviderDescriptor { + /** Creates a descriptor instance */ + public Descriptor() { + super(ID); + } + } + + /** + * Parser for Excel files + */ + public static class ExcelParser extends ReportParser { + + private static final long serialVersionUID = -8689695008930386641L; + private final String id; + private List parserMessages; + + /** Constructor */ + public ExcelParser(String id) { + super(); + this.id = id; + this.parserMessages = new ArrayList(); + } + + /** Returns the parser identifier */ + public String getId() { + return id; + } + + /** Detects the table position in an Excel sheet */ + private TablePosition detectTablePosition(Sheet sheet) { + int startRow = -1; + int startCol = -1; + int maxNonEmptyConsecutiveCells = 0; + int headerRowIndex = -1; + + // Check the first 20 rows to find the table start + int maxRowsToCheck = Math.min(20, sheet.getLastRowNum() + 1); + + for (int rowIndex = 0; rowIndex < maxRowsToCheck; rowIndex++) { + Row row = sheet.getRow(rowIndex); + if (row == null) continue; + + int nonEmptyConsecutiveCells = 0; + int firstNonEmptyCellIndex = -1; + + // Check the cells in the row + for (int colIndex = 0; colIndex < 100; colIndex++) { // Arbitrary limit of 100 columns + Cell cell = row.getCell(colIndex, Row.MissingCellPolicy.RETURN_BLANK_AS_NULL); + if (cell != null) { + if (firstNonEmptyCellIndex == -1) { + firstNonEmptyCellIndex = colIndex; + } + nonEmptyConsecutiveCells++; + } else if (firstNonEmptyCellIndex != -1) { + // Found an empty cell after non-empty cells + break; + } + } + + // If we found a row with more consecutive non-empty cells than before + if (nonEmptyConsecutiveCells > maxNonEmptyConsecutiveCells && nonEmptyConsecutiveCells >= 2) { + maxNonEmptyConsecutiveCells = nonEmptyConsecutiveCells; + headerRowIndex = rowIndex; + startCol = firstNonEmptyCellIndex; + } + } + + // If we found a potential header + if (headerRowIndex != -1) { + startRow = headerRowIndex; + } else { + // Default to the first row + startRow = 0; + startCol = 0; + } + + return new TablePosition(startRow, startCol); + } + + /** Checks if a row is empty */ + private boolean isRowEmpty(Row row, int startCol, int columnCount) { + if (row == null) return true; + + for (int i = startCol; i < startCol + columnCount; i++) { + Cell cell = row.getCell(i, Row.MissingCellPolicy.RETURN_BLANK_AS_NULL); + if (cell != null) { + return false; + } + } + return true; + } + + /** Extracts data from an Excel sheet */ + private TabularData extractSheetData(Sheet sheet, List referenceHeader) { + // Detect table position + TablePosition tablePos = detectTablePosition(sheet); + int startRow = tablePos.getStartRow(); + int startCol = tablePos.getStartCol(); + + // Get the header row + Row headerRow = sheet.getRow(startRow); + if (headerRow == null) { + parserMessages.add(String.format("Skipped sheet '%s' - No header row found", sheet.getSheetName())); + return null; + } + + // Extract headers + List header = new ArrayList<>(); + int lastCol = headerRow.getLastCellNum(); + for (int colIdx = startCol; colIdx < lastCol; colIdx++) { + Cell cell = headerRow.getCell(colIdx, Row.MissingCellPolicy.RETURN_BLANK_AS_NULL); + if (cell != null) { + header.add(getCellValueAsString(cell)); + } else { + // If we find an empty cell in the header, stop + break; + } + } + + // Check that the header has at least 2 columns + if (header.size() < 2) { + parserMessages.add(String.format("Skipped sheet '%s' - Header has less than 2 columns", sheet.getSheetName())); + return null; + } + + // If a reference header is provided, check that it matches + if (referenceHeader != null && !header.equals(referenceHeader)) { + parserMessages.add(String.format("Skipped sheet '%s' - Header does not match reference header", sheet.getSheetName())); + return null; + } + + // Extract data rows + List> rows = new ArrayList<>(); + int headerColumnCount = header.size(); + + for (int rowIdx = startRow + 1; rowIdx <= sheet.getLastRowNum(); rowIdx++) { + Row dataRow = sheet.getRow(rowIdx); + + // If the row is empty, skip to the next one + if (isRowEmpty(dataRow, startCol, headerColumnCount)) { + continue; + } + + List rowData = new ArrayList<>(); + boolean rowComplete = true; + + // Extract data from the row + for (int colIdx = startCol; colIdx < startCol + headerColumnCount; colIdx++) { + Cell cell = dataRow.getCell(colIdx, Row.MissingCellPolicy.RETURN_BLANK_AS_NULL); + if (cell != null) { + rowData.add(getCellValueAsString(cell)); + } else { + // Empty cell, add an empty string + rowData.add(""); + } + } + + // If the row has the correct number of columns, add it + if (rowComplete) { + rows.add(rowData); + } + } + + return new TabularData(id, header, rows); + } + + /** Converts an Excel cell value to a string */ + private String getCellValueAsString(Cell cell) { + if (cell == null) { + return ""; + } + + switch (cell.getCellType()) { + case STRING: + return cell.getStringCellValue(); + case NUMERIC: + if (DateUtil.isCellDateFormatted(cell)) { + return cell.getDateCellValue().toString(); + } else { + // To avoid scientific notation display + double value = cell.getNumericCellValue(); + if (value == Math.floor(value)) { + return String.format("%.0f", value); + } else { + return String.valueOf(value); + } + } + case BOOLEAN: + return String.valueOf(cell.getBooleanCellValue()); + case FORMULA: + try { + return String.valueOf(cell.getNumericCellValue()); + } catch (IllegalStateException e) { + try { + return String.valueOf(cell.getStringCellValue()); + } catch (IllegalStateException e2) { + return "#ERROR"; + } + } + default: + return ""; + } + } + + /** Parses an Excel file and creates a ReportDto */ + @Override + public ReportDto parse(File file) throws IOException { + try (FileInputStream fis = new FileInputStream(file); + Workbook workbook = new XSSFWorkbook(fis)) { + + TabularData result = null; + List referenceHeader = null; + + // Process all sheets in the workbook + for (int i = 0; i < workbook.getNumberOfSheets(); i++) { + Sheet sheet = workbook.getSheetAt(i); + + // Extract data from the sheet + TabularData sheetData = extractSheetData(sheet, referenceHeader); + + // If the sheet contains valid data + if (sheetData != null) { + // If it's the first valid sheet, use it as reference + if (result == null) { + result = sheetData; + referenceHeader = sheetData.getHeader(); + } else { + // Otherwise, add the sheet rows to the result + result.getRows().addAll(sheetData.getRows()); + } + } + } + + // If no sheet contains valid data + if (result == null) { + throw new IOException("No valid data found in Excel file"); + } + + // Process tabular data + return result.processData(parserMessages); + } + } + + /** Internal class to store the position of a table in an Excel sheet */ + private static class TablePosition { + private final int startRow; + private final int startCol; + + public TablePosition(int startRow, int startCol) { + this.startRow = startRow; + this.startCol = startCol; + } + + public int getStartRow() { + return startRow; + } + + public int getStartCol() { + return startCol; + } + } + } +} diff --git a/src/main/java/io/jenkins/plugins/reporter/util/TabularData.java b/src/main/java/io/jenkins/plugins/reporter/util/TabularData.java new file mode 100644 index 00000000..8fab5776 --- /dev/null +++ b/src/main/java/io/jenkins/plugins/reporter/util/TabularData.java @@ -0,0 +1,175 @@ +package io.jenkins.plugins.reporter.util; + +import io.jenkins.plugins.reporter.model.Item; +import io.jenkins.plugins.reporter.model.ReportDto; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.math.NumberUtils; + +import java.io.Serializable; +import java.util.*; + +/** + * Utility class for tabular data processing + * Combines data storage and processing functionality + */ +public class TabularData implements Serializable { + + private static final long serialVersionUID = 1L; + + private final String sourceId; + private final List header; + private final List> rows; + + /** Constructor */ + public TabularData(String sourceId, List header, List> rows) { + this.sourceId = sourceId; + this.header = header; + this.rows = rows; + } + + /** Returns the list of column headers */ + public List getHeader() { + return header; + } + + /** Returns the list of data rows */ + public List> getRows() { + return rows; + } + + /** + * Process tabular data to create a ReportDto + * @param reportId Report identifier + * @param parserMessages List to store parser messages + * @return ReportDto containing the processed data + */ + public ReportDto processData(List parserMessages) { + // Create the report + ReportDto report = new ReportDto(); + report.setId(sourceId); + report.setItems(new ArrayList<>()); + + int rowCount = 0; + final int headerColumnCount = header.size(); + int colIdxValueStart = 0; + + if (headerColumnCount >= 2) { + rowCount = rows.size(); + } else { + parserMessages.add(String.format("skipped file - First line has %d elements", headerColumnCount + 1)); + } + + /** Parse all data rows */ + for (int rowIdx = 0; rowIdx < rowCount; rowIdx++) { + String parentId = "report"; + List row = rows.get(rowIdx); + Item last = null; + boolean lastItemAdded = false; + LinkedHashMap result = new LinkedHashMap<>(); + boolean emptyFieldFound = false; + int rowSize = row.size(); + + /** Parse until first data line is found to get data and value field */ + if (colIdxValueStart == 0) { + /** Col 0 is assumed to be string */ + for (int colIdx = rowSize - 1; colIdx > 1; colIdx--) { + String value = row.get(colIdx); + + if (NumberUtils.isCreatable(value)) { + colIdxValueStart = colIdx; + } else { + if (colIdxValueStart > 0) { + parserMessages + .add(String.format("Found data - fields number = %d - numeric fields = %d", + colIdxValueStart, rowSize - colIdxValueStart)); + } + break; + } + } + } + + String valueId = ""; + /** Parse line if first data line is OK and line has more element than header */ + if ((colIdxValueStart > 0) && (rowSize >= headerColumnCount)) { + /** Check line and header size matching */ + for (int colIdx = 0; colIdx < headerColumnCount; colIdx++) { + String colId = header.get(colIdx); + String value = row.get(colIdx); + + /** Check value fields */ + if ((colIdx < colIdxValueStart)) { + /** Test if text item is a value or empty */ + if ((NumberUtils.isCreatable(value)) || (StringUtils.isBlank(value))) { + /** Empty field found - message */ + if (colIdx == 0) { + parserMessages + .add(String.format("skipped line %d - First column item empty - col = %d ", + rowIdx + 2, colIdx + 1)); + break; + } else { + emptyFieldFound = true; + /** Continue next column parsing */ + continue; + } + } else { + /** Check if field values are present after empty cells */ + if (emptyFieldFound) { + parserMessages.add(String.format("skipped line %d Empty field in col = %d ", + rowIdx + 2, colIdx + 1)); + break; + } + } + valueId += value; + Optional parent = report.findItem(parentId, report.getItems()); + Item item = new Item(); + lastItemAdded = false; + item.setId(valueId); + item.setName(value); + String finalValueId = valueId; + if (parent.isPresent()) { + Item p = parent.get(); + if (!p.hasItems()) { + p.setItems(new ArrayList<>()); + } + if (p.getItems().stream().noneMatch(i -> i.getId().equals(finalValueId))) { + p.addItem(item); + lastItemAdded = true; + } + } else { + if (report.getItems().stream().noneMatch(i -> i.getId().equals(finalValueId))) { + report.getItems().add(item); + lastItemAdded = true; + } + } + parentId = valueId; + last = item; + } else { + Number val = 0; + if (NumberUtils.isCreatable(value)) { + val = NumberUtils.createNumber(value); + } + result.put(colId, val.intValue()); + } + } + } else { + /** Skip file if first data line has no value field */ + if (colIdxValueStart == 0) { + parserMessages.add(String.format("skipped line %d - First data row not found", rowIdx + 2)); + continue; + } else { + parserMessages + .add(String.format("skipped line %d - line has fewer element than title", rowIdx + 2)); + continue; + } + } + /** If last item was created, it will be added to report */ + if (lastItemAdded) { + last.setResult(result); + } else { + parserMessages.add(String.format("ignored line %d - Same fields already exists", rowIdx + 2)); + } + } + // report.setParserLog(parserMessages); + return report; + } +} diff --git a/src/test/java/io/jenkins/plugins/reporter/provider/CreateExcelSample.java b/src/test/java/io/jenkins/plugins/reporter/provider/CreateExcelSample.java new file mode 100644 index 00000000..255d8cac --- /dev/null +++ b/src/test/java/io/jenkins/plugins/reporter/provider/CreateExcelSample.java @@ -0,0 +1,64 @@ +package io.jenkins.plugins.reporter.provider; + +import org.apache.poi.ss.usermodel.*; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; + +import java.io.FileOutputStream; +import java.io.IOException; + +/** + * Utilitaire pour créer un fichier Excel de test dans le répertoire etc/ + */ +public class CreateExcelSample { + + public static void main(String[] args) { + String filePath = "/home/ubuntu/workspace/nested-data-reporting-plugin/etc/report.xlsx"; + + try { + // Créer un nouveau classeur Excel + Workbook workbook = new XSSFWorkbook(); + + // Créer une feuille + Sheet sheet = workbook.createSheet("Sample Data"); + + // Créer l'en-tête + Row headerRow = sheet.createRow(0); + headerRow.createCell(0).setCellValue("Category"); + headerRow.createCell(1).setCellValue("Subcategory"); + headerRow.createCell(2).setCellValue("Value1"); + headerRow.createCell(3).setCellValue("Value2"); + + // Créer les données + Row row1 = sheet.createRow(1); + row1.createCell(0).setCellValue("Category A"); + row1.createCell(1).setCellValue(""); + row1.createCell(2).setCellValue(10); + row1.createCell(3).setCellValue(20); + + Row row2 = sheet.createRow(2); + row2.createCell(0).setCellValue("Category B"); + row2.createCell(1).setCellValue(""); + row2.createCell(2).setCellValue(30); + row2.createCell(3).setCellValue(40); + + Row row3 = sheet.createRow(3); + row3.createCell(0).setCellValue("Category C"); + row3.createCell(1).setCellValue(""); + row3.createCell(2).setCellValue(50); + row3.createCell(3).setCellValue(60); + + // Écrire dans le fichier + try (FileOutputStream fileOut = new FileOutputStream(filePath)) { + workbook.write(fileOut); + } + + // Fermer le classeur + workbook.close(); + + System.out.println("Fichier Excel créé avec succès à " + filePath); + + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/src/test/java/io/jenkins/plugins/reporter/provider/ExcelTestFileGenerator.java b/src/test/java/io/jenkins/plugins/reporter/provider/ExcelTestFileGenerator.java new file mode 100644 index 00000000..a9e12634 --- /dev/null +++ b/src/test/java/io/jenkins/plugins/reporter/provider/ExcelTestFileGenerator.java @@ -0,0 +1,130 @@ +package io.jenkins.plugins.reporter.provider; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import org.apache.poi.ss.usermodel.*; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; + +/** + * Utility class to generate test Excel files for testing the Excel parser functionality. + */ +public class ExcelTestFileGenerator { + + public static void main(String[] args) throws IOException { + // Create test files in the resources directory + String resourcesDir = "src/test/resources"; + + // Create normal Excel file + createNormalExcelFile(new File(resourcesDir, "test-excel-normal.xlsx")); + + // Create Excel file with offset header + createOffsetHeaderExcelFile(new File(resourcesDir, "test-excel-offset.xlsx")); + + // Create Excel file with mixed data + createMixedDataExcelFile(new File(resourcesDir, "test-excel-mixed.xlsx")); + + System.out.println("Test Excel files created successfully in " + resourcesDir); + } + + /** + * Creates a normal Excel file with header in the first row and data below. + */ + public static void createNormalExcelFile(File file) throws IOException { + Workbook workbook = new XSSFWorkbook(); + Sheet sheet = workbook.createSheet("Test Sheet"); + + // Create header row + Row headerRow = sheet.createRow(0); + headerRow.createCell(0).setCellValue("Category"); + headerRow.createCell(1).setCellValue("Subcategory"); + headerRow.createCell(2).setCellValue("Value1"); + headerRow.createCell(3).setCellValue("Value2"); + + // Create data rows + Row dataRow1 = sheet.createRow(1); + dataRow1.createCell(0).setCellValue("Category A"); + dataRow1.createCell(1).setCellValue(""); + dataRow1.createCell(2).setCellValue(10); + dataRow1.createCell(3).setCellValue(20); + + Row dataRow2 = sheet.createRow(2); + dataRow2.createCell(0).setCellValue("Category B"); + dataRow2.createCell(1).setCellValue(""); + dataRow2.createCell(2).setCellValue(30); + dataRow2.createCell(3).setCellValue(40); + + // Write to file + try (FileOutputStream fileOut = new FileOutputStream(file)) { + workbook.write(fileOut); + } + workbook.close(); + } + + /** + * Creates an Excel file with header not in the first row. + */ + public static void createOffsetHeaderExcelFile(File file) throws IOException { + Workbook workbook = new XSSFWorkbook(); + Sheet sheet = workbook.createSheet("Test Sheet"); + + // Add some empty rows and cells + sheet.createRow(0).createCell(0).setCellValue("This is not the header"); + sheet.createRow(1); // Empty row + + // Create header row at position 3 + Row headerRow = sheet.createRow(3); + headerRow.createCell(0).setCellValue("Category"); + headerRow.createCell(1).setCellValue("Subcategory"); + headerRow.createCell(2).setCellValue("Value1"); + headerRow.createCell(3).setCellValue("Value2"); + + // Create data rows + Row dataRow1 = sheet.createRow(4); + dataRow1.createCell(0).setCellValue("Category A"); + dataRow1.createCell(1).setCellValue(""); + dataRow1.createCell(2).setCellValue(10); + dataRow1.createCell(3).setCellValue(20); + + // Write to file + try (FileOutputStream fileOut = new FileOutputStream(file)) { + workbook.write(fileOut); + } + workbook.close(); + } + + /** + * Creates an Excel file with mixed data types and nested structure. + */ + public static void createMixedDataExcelFile(File file) throws IOException { + Workbook workbook = new XSSFWorkbook(); + Sheet sheet = workbook.createSheet("Test Sheet"); + + // Create header row + Row headerRow = sheet.createRow(0); + headerRow.createCell(0).setCellValue("Parent"); + headerRow.createCell(1).setCellValue("Child"); + headerRow.createCell(2).setCellValue("Value1"); + headerRow.createCell(3).setCellValue("Value2"); + + // Create data rows with nested structure + Row dataRow1 = sheet.createRow(1); + dataRow1.createCell(0).setCellValue("Parent"); + dataRow1.createCell(1).setCellValue("Child"); + dataRow1.createCell(2).setCellValue(30); + dataRow1.createCell(3).setCellValue(40); + + // Add some rows with different data types + Row dataRow2 = sheet.createRow(2); + dataRow2.createCell(0).setCellValue("Parent"); + dataRow2.createCell(1).setCellValue("Child2"); + dataRow2.createCell(2).setCellValue("Not a number"); // String instead of number + dataRow2.createCell(3).setCellValue(50); + + // Write to file + try (FileOutputStream fileOut = new FileOutputStream(file)) { + workbook.write(fileOut); + } + workbook.close(); + } +} diff --git a/src/test/resources/test-excel-mixed.xlsx b/src/test/resources/test-excel-mixed.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..8a522ac17c12cf215ede9703d22290fb0b5d8bc5 GIT binary patch literal 3400 zcmaJ@2{@E%8y*HTLuGBwbW*k~sSFW=lrR{Kp(sm6V@$?2GnN)aSsKT_3_}vf7A=%4 zWs51~Alu0vM?_J!jGdgB`v1f4KXuRb&2nAe^UinQ@BKc{ZDtJQ5&?k0V8FXH9~;0H z2(!N1>XJ#`1d_MCg%5?`Wv6h}-L2$I6X}Jbz*=HMx4bIT%yM_2uGlN+JtpwHAI)k> z`ccNrq8$j4a5{|F;|uhO5MWgwe zZcPjl75*SDohxoD)eqd|GMX2nYu@?$r{v#~p9K21l$k7BY6ywwr3r@zWiLw)h8E_< z4jq1}Q`s<^XB!F0uHP@N*;JSWVfvXelLJC~tY(8Lb)r757?TVxSgfn*jk8nPv$=1p zb!-Btk1Nxzd%za1_wtK}hl)YC_=T5ndcHRj#>S?+-U z2B_om$xu|4xk6f2;H5u*6cU=G8Rs@&PWr@`PU&!rb{X|QD3^%kFN zxc#@o^P^()sv)BVbM#$q_CTa{!}DZ7)#oS6mp$CXl)KR;=?Gi?M|cQ}&qW}ak<7aT{S zuuX}MX7I91sW%<8A>c`V>MIbZk|Q1@>(|B)9`E8wK^vYO|8Se;o4RLg*5c_3vQmlg zOI~6{c^u~-)b{qG#F*6_jYd}2HN`|@TB`oRO_XSK`_ zMRd!sdQXypLyy{P4hkV#@RfW(;OPe^>g%juB6SwqMY3U}bKDzMLFvI?S(^(2tCE5x zXspM5#=3sI*@9aM;mMu^HY4}g0i#+)0e$9Zsd)+IGSena6fG3uogZR^WkL%=4 zCHEM}<5Z7mOIQ{zyh)&FOvxDc^vpIWM%;IhIf&zyr&SZ)KA+=!qo{LU=TOI^lCk@B zLsx(G%g!jP7fIcrbSz~xq+kqcu9N!#S=h$#;dJe>m0B1QiiaKP`j}LCseh&5YkkqD zqb*-2)~0J>rn2O6_@#@F5gt zezH6Txq#1kQIIJXy5eknWopb6>?w3k^kvxz z%A-@SQ`dkBce)R9`gXJlI?bxWwX^%IcRD*wYaebdXmCMUV@6jVuJf>!sWZLjHo)TT z@Kb00`dyjt!)vQP-v?P7+La}e_C|QSzefiHmPLZ&+dg%z1Mr7Dh?0AZS3g##SG?@f z25B-6M9%o}7Y_!s(0pD=O3{J~oEhTW(Na5iVHYuKix>2?kSCQgfDS#Ru2zIk{>cMT z)+WeUgyH>Vh7APb1mEeDT|QpcjQv!!Lv@uh1MIWxAMEvPxgq`M=gi;K2Jp z)bk71+D|uS-y15|xVRQWUk%u?)bP$-zk}~`#f{)9(ISnA_K}e8M@x6V=2WkDUZbyDQvu~ipIDjh`R`G0MjE}7i6{s2ahQ(a@r>=o*y2gu0Ab5MR`W5R#iEW>ov?fxp zq5yI+afP40Sa)2<7O9;8MNVd(?(80{nK7%;FBM){E9bl;-Qwj*8*eU& zy)6!()fMz>Jh3Vd!)F(}r=|qS@%^`nLk@6Pz)dEArc{tP(H+uINsTrNU94fE{y-We zl8{J8_zmcYS+~$%m!?q$d#SWMqQ(UoL5Li_a=y#?21+REGX@x(+S#Dzlsb)`v#GT@ zB%k81!&k;RaL5CojD9#fK%7%BeV{0u9eKo1J2)&e~=OWC|6PQ#9Tc#ynw``W7JIDsT#@ji0 zpj~tysC#GpV&coN)VghTgy|-E{uwvjmRJ$jyEy60O}4PuNs}8fnlGdFbL;nkiy+QU>EfKaX<6?m9DZ;@i0@w__TO%1e z^!_jYB?50>U~6t+C$~*pWBuip9Jjr2Yd&DdnoT6LjI=%EY_Hxr8QB?P6V5D_|5X1s zQEV^YI%(MBa})WjZ2E7*bo&ZhdnJ1!Z6cNX`wIWer|qR%n+dy_ZbB6Ft#oVa*j~K# yw6XimCRD-Siht03wy(3*6zsFPiPMmO9^(H_s+lo25Bp9C>vfW~O>?~LUw;Eh3{-po literal 0 HcmV?d00001 diff --git a/src/test/resources/test-excel-normal.xlsx b/src/test/resources/test-excel-normal.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..0f3b6a598914955c9fbbe04e88ba330058905a4c GIT binary patch literal 3406 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 literal 0 HcmV?d00001 diff --git a/src/test/resources/test-excel-offset.xlsx b/src/test/resources/test-excel-offset.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..635ac393ba51d7076ff706a8e6101cad07fe6b4a GIT binary patch literal 3409 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 JZL`sS{RQ|wSa1LU literal 0 HcmV?d00001 diff --git a/src/test/resources/test-excel.xlsx b/src/test/resources/test-excel.xlsx new file mode 100644 index 00000000..76873415 --- /dev/null +++ b/src/test/resources/test-excel.xlsx @@ -0,0 +1 @@ +PK From 3702bb5ba0067703c8ceff916f6a4c98c3613c5a Mon Sep 17 00:00:00 2001 From: totocaca Date: Sun, 30 Mar 2025 16:50:31 +0200 Subject: [PATCH 2/5] correct javadoc problem --- src/main/java/io/jenkins/plugins/reporter/util/TabularData.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/io/jenkins/plugins/reporter/util/TabularData.java b/src/main/java/io/jenkins/plugins/reporter/util/TabularData.java index 8fab5776..c2025727 100644 --- a/src/main/java/io/jenkins/plugins/reporter/util/TabularData.java +++ b/src/main/java/io/jenkins/plugins/reporter/util/TabularData.java @@ -39,7 +39,6 @@ public List> getRows() { /** * Process tabular data to create a ReportDto - * @param reportId Report identifier * @param parserMessages List to store parser messages * @return ReportDto containing the processed data */ From d657153951b76c4416dd6e99c00d30b036316f08 Mon Sep 17 00:00:00 2001 From: totocaca123 <42418580+totocaca123@users.noreply.github.com> Date: Sun, 30 Mar 2025 16:54:38 +0200 Subject: [PATCH 3/5] Potential fix for code scanning alert no. 14: Exposing internal representation Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .../java/io/jenkins/plugins/reporter/util/TabularData.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/io/jenkins/plugins/reporter/util/TabularData.java b/src/main/java/io/jenkins/plugins/reporter/util/TabularData.java index c2025727..b17eb2c0 100644 --- a/src/main/java/io/jenkins/plugins/reporter/util/TabularData.java +++ b/src/main/java/io/jenkins/plugins/reporter/util/TabularData.java @@ -34,7 +34,11 @@ public List getHeader() { /** Returns the list of data rows */ public List> getRows() { - return rows; + List> rowsCopy = new ArrayList<>(); + for (List row : rows) { + rowsCopy.add(new ArrayList<>(row)); + } + return rowsCopy; } /** From 9d5a8f961e3876efb9e48203c134fdab7dac247c Mon Sep 17 00:00:00 2001 From: totocaca Date: Sun, 4 May 2025 22:23:02 +0200 Subject: [PATCH 4/5] update and clean --- pom.xml | 60 +++++++++++++++++++++++++------------- src/.DS_Store | Bin 6148 -> 0 bytes src/main/.DS_Store | Bin 6148 -> 0 bytes src/main/webapp/.DS_Store | Bin 6148 -> 0 bytes 4 files changed, 40 insertions(+), 20 deletions(-) delete mode 100644 src/.DS_Store delete mode 100644 src/main/.DS_Store delete mode 100644 src/main/webapp/.DS_Store diff --git a/pom.xml b/pom.xml index e196f58f..41a38207 100644 --- a/pom.xml +++ b/pom.xml @@ -1,23 +1,25 @@ - + 4.0.0 - + org.jenkins-ci.plugins plugin 5.13 - + - 999999-SNAPSHOT + 1.0.0-SNAPSHOT jenkinsci/nested-data-reporting-plugin 2.492 ${jenkins.baseline}.1 - 2.18.3 + HEAD - + + io.jenkins.plugins nested-data-reporting ${changelist} @@ -25,9 +27,9 @@ Nested Data Reporting Jenkins plugin to report data from nested as pie-charts, trend-charts and data tables. - + https://github.com/jenkinsci/${project.artifactId}-plugin - + MIT License @@ -42,14 +44,14 @@ post@simon-symhoven.de - + scm:git:https://github.com/${gitHubRepo}.git scm:git:git@github.com:${gitHubRepo}.git https://github.com/${gitHubRepo} ${scmTag} - + repo.jenkins-ci.org @@ -62,7 +64,7 @@ https://repo.jenkins-ci.org/public/ - + @@ -72,9 +74,15 @@ import pom + + commons-codec + commons-codec + 1.18.0 + - + + io.jenkins.plugins @@ -119,12 +127,12 @@ org.apache.poi poi - 5.4.0 + 5.4.1 org.apache.poi poi-ooxml - 5.4.0 + 5.4.1 @@ -171,13 +179,25 @@ - org.jenkins-ci.tools - maven-hpi-plugin - true - - 0.3 - + org.jacoco + jacoco-maven-plugin + + + prepare-agent + + prepare-agent + + + + generate-report + test + + report + + + + diff --git a/src/.DS_Store b/src/.DS_Store deleted file mode 100644 index 91a3d37cf473bc7acb161167f97a1d03379972ab..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHK%SyvQ6rE|KO({Ya3SADkE!g@h;wHrU14eYAQWH`%m}W|onnfvOtv}?K_&we` zGZBk*D`M}3nRA~rnFE;zV~l&VsK=PY7_*@va#U6bx>ts3CK-|A7-5l3!bFB(znR!y z2mE%6jak5AmVW*IaFWDX-tE5gTHV^%Y}gIEW#9RavhedDpJiS!y+P|z$|S7xAiRo4 z#n9P4lW88r>1e77;%EdZx7TqR$-%&J*YoU8(?q5Z;H&bhJVIiC7$63Sf%Rp; zoC$VoeJP-o69dG+4-DY`AfO?-21|`<>wpfg<B5Q9#GH1fnqL8Z0$J1cd8SKwZks z6NBq=@C%dY8Z0&Ha>munFpinIdc1HoJNSi4XWZ3DJuyHGEHhBori17IIs7s!ANk8A zWDx_zz&~SvTLXXK!J^FB`fYi5)(U9%&`>b1LS5Z-NTn^J@v6nYGJE!g^3#7l_v1&ruHr6we3FlIxOnnNk%tS{t~_&m<+ zZVthKHxWAnyWi~m>}Ed5{xHV4vkWGTnT#<58X`xfLD1ajYUyA^F6T&D>K9q+$HH%z z=r5Y^+qfit2r*V|!p;KgbSGv1tEmo-9$+s3P&uHAd|VSau|K1KR1V~)pR-;Sx1xO^~hSC z9*jXhJXx>%#_rz!+2#B>eo4feB9a5=Qnoc#@D56?rdM~7#3FeFdzDkg5)uQ%05L!e zY#sypEHDO}S2|Tp3=ji9Fo64m1r5>GSSplT2XuISMt=tp1$2B%AX*x2jio~HfN+%x zs8YFpVsMoXc1y?E8cT&LopHG`%%fMX9xq(34t7h1Gj1!So){no>I}5i(8Tlq0)Cms zM}9qpM#KOy@Xr|FotZmxpeTK|ek%{pS_|3(G!%?0Q2_zHb_sw1?ju9#RDKI}h_f}8 V3UL;+%XC1x2uMPxBL;qffiGWOOep{W diff --git a/src/main/webapp/.DS_Store b/src/main/webapp/.DS_Store deleted file mode 100644 index a9ed938a7f9014345ceb5ebdf5300d529c341006..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHK%}T>S5Z<+|O({YS3Oz1(E!g^3#7l_v1&ruHr6#6mFlI}WnnNk%tS{t~_&m<+ zZlJ~BQN+%`?l(I>yO|HNKa4T%&cZ%pHe<|!hR9K=5Hwf1HcT)gS97F6o=$>T1{Kr% zO%r~5gGKCtNtUqX@BauUag=7g-Y4Iv)f<~lt7)~ZJMT&6Ugl@B)b*z~IJ%TF4ock* zuA*^1vbWD97D+Mb(DlMcjYVzQp;2ue(BbtN{dGhX(D5ySC=5CV3yt6b;W`yi zr*iYe;5r@b!o)cS3ynIRakVncV^%I7FI=q-cA>%@)>{Q`A}a|{+5 UaTc_zbU?ZYC_< Date: Mon, 26 May 2025 08:41:50 +0200 Subject: [PATCH 5/5] correct test --- .../jenkins/plugins/reporter/model/Item.java | 18 +- .../provider/AbstractExcelProvider.java | 246 ++++++++++++ .../plugins/reporter/provider/Excel.java | 212 +---------- .../plugins/reporter/provider/ExcelMulti.java | 150 ++++++++ .../plugins/reporter/util/TabularData.java | 210 +++++----- .../provider/CreateExcelMultiSample.java | 233 ++++++++++++ .../reporter/provider/ExcelMultiTest.java | 358 ++++++++++++++++++ .../provider/RegenerateTestFiles.java | 31 ++ .../test-excel-multi-consistent.xlsx | Bin 0 -> 4407 bytes .../test-excel-multi-inconsistent.xlsx | Bin 0 -> 4374 bytes .../resources/test-excel-multi-mixed.xlsx | Bin 0 -> 4748 bytes 11 files changed, 1147 insertions(+), 311 deletions(-) create mode 100644 src/main/java/io/jenkins/plugins/reporter/provider/AbstractExcelProvider.java create mode 100644 src/main/java/io/jenkins/plugins/reporter/provider/ExcelMulti.java create mode 100644 src/test/java/io/jenkins/plugins/reporter/provider/CreateExcelMultiSample.java create mode 100644 src/test/java/io/jenkins/plugins/reporter/provider/ExcelMultiTest.java create mode 100644 src/test/java/io/jenkins/plugins/reporter/provider/RegenerateTestFiles.java create mode 100644 src/test/resources/test-excel-multi-consistent.xlsx create mode 100644 src/test/resources/test-excel-multi-inconsistent.xlsx create mode 100644 src/test/resources/test-excel-multi-mixed.xlsx diff --git a/src/main/java/io/jenkins/plugins/reporter/model/Item.java b/src/main/java/io/jenkins/plugins/reporter/model/Item.java index d5ebe13d..ff03a542 100644 --- a/src/main/java/io/jenkins/plugins/reporter/model/Item.java +++ b/src/main/java/io/jenkins/plugins/reporter/model/Item.java @@ -9,6 +9,7 @@ import java.io.Serializable; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; +import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -72,8 +73,11 @@ public LinkedHashMap getResult() { return result; } - return getItems() - .stream() + if (items == null || items.isEmpty()) { + return new LinkedHashMap<>(); + } + + return items.stream() .map(Item::getResult) .flatMap(map -> map.entrySet().stream()) .collect(Collectors.groupingBy(Map.Entry::getKey, LinkedHashMap::new, Collectors.summingInt(Map.Entry::getValue))); @@ -102,11 +106,14 @@ public void setResult(LinkedHashMap result) { } public List getItems() { + if (items == null) { + items = new ArrayList<>(); + } return items; } public boolean hasItems() { - return !Objects.isNull(items) && !items.isEmpty(); + return items != null && !items.isEmpty(); } public void setItems(List items) { @@ -114,6 +121,9 @@ public void setItems(List items) { } public void addItem(Item item) { - this.items.add(item); + if (items == null) { + items = new ArrayList<>(); + } + items.add(item); } } \ No newline at end of file diff --git a/src/main/java/io/jenkins/plugins/reporter/provider/AbstractExcelProvider.java b/src/main/java/io/jenkins/plugins/reporter/provider/AbstractExcelProvider.java new file mode 100644 index 00000000..638fb0da --- /dev/null +++ b/src/main/java/io/jenkins/plugins/reporter/provider/AbstractExcelProvider.java @@ -0,0 +1,246 @@ +package io.jenkins.plugins.reporter.provider; + +import io.jenkins.plugins.reporter.model.Provider; +import io.jenkins.plugins.reporter.model.ReportDto; +import io.jenkins.plugins.reporter.model.ReportParser; +import io.jenkins.plugins.reporter.util.TabularData; +import org.apache.poi.ss.usermodel.*; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Abstract base class for Excel-based providers + * Contains common functionality for Excel file parsing + */ +public abstract class AbstractExcelProvider extends Provider { + + private static final long serialVersionUID = 5463781055792899347L; + + /** Default constructor */ + protected AbstractExcelProvider() { + super(); + } + + /** + * Base class for Excel parsers + */ + public abstract static class AbstractExcelParser extends ReportParser { + + private static final long serialVersionUID = -8689695008930386641L; + protected final String id; + protected List parserMessages; + + /** Constructor */ + protected AbstractExcelParser(String id) { + super(); + this.id = id; + this.parserMessages = new ArrayList<>(); + } + + /** Returns the parser identifier */ + public String getId() { + return id; + } + + /** Detects the table position in an Excel sheet */ + protected TablePosition detectTablePosition(Sheet sheet) { + int startRow = -1; + int startCol = -1; + int maxNonEmptyConsecutiveCells = 0; + int headerRowIndex = -1; + + // Check the first 20 rows to find the table start + int maxRowsToCheck = Math.min(20, sheet.getLastRowNum() + 1); + + for (int rowIndex = 0; rowIndex < maxRowsToCheck; rowIndex++) { + Row row = sheet.getRow(rowIndex); + if (row == null) continue; + + int nonEmptyConsecutiveCells = 0; + int firstNonEmptyCellIndex = -1; + + // Check the cells in the row + for (int colIndex = 0; colIndex < 100; colIndex++) { // Arbitrary limit of 100 columns + Cell cell = row.getCell(colIndex, Row.MissingCellPolicy.RETURN_BLANK_AS_NULL); + if (cell != null) { + if (firstNonEmptyCellIndex == -1) { + firstNonEmptyCellIndex = colIndex; + } + nonEmptyConsecutiveCells++; + } else if (firstNonEmptyCellIndex != -1) { + // Found an empty cell after non-empty cells + break; + } + } + + // If we found a row with more consecutive non-empty cells than before + if (nonEmptyConsecutiveCells > maxNonEmptyConsecutiveCells && nonEmptyConsecutiveCells >= 2) { + maxNonEmptyConsecutiveCells = nonEmptyConsecutiveCells; + headerRowIndex = rowIndex; + startCol = firstNonEmptyCellIndex; + } + } + + // If we found a potential header + if (headerRowIndex != -1) { + startRow = headerRowIndex; + } else { + // Default to the first row + startRow = 0; + startCol = 0; + } + + return new TablePosition(startRow, startCol); + } + + /** Checks if a row is empty */ + protected boolean isRowEmpty(Row row, int startCol, int columnCount) { + if (row == null) return true; + + for (int i = startCol; i < startCol + columnCount; i++) { + Cell cell = row.getCell(i, Row.MissingCellPolicy.RETURN_BLANK_AS_NULL); + if (cell != null && !getCellValueAsString(cell).trim().isEmpty()) { + return false; + } + } + return true; + } + + /** Extracts data from an Excel sheet */ + protected TabularData extractSheetData(Sheet sheet, List referenceHeader) { + // Detect table position + TablePosition tablePos = detectTablePosition(sheet); + int startRow = tablePos.getStartRow(); + int startCol = tablePos.getStartCol(); + + // Get the header row + Row headerRow = sheet.getRow(startRow); + if (headerRow == null) { + parserMessages.add(String.format("Skipped sheet '%s' - No header row found", sheet.getSheetName())); + return null; + } + + // Extract headers + List header = new ArrayList<>(); + int lastCol = headerRow.getLastCellNum(); + for (int colIdx = startCol; colIdx < lastCol; colIdx++) { + Cell cell = headerRow.getCell(colIdx, Row.MissingCellPolicy.RETURN_BLANK_AS_NULL); + if (cell != null) { + header.add(getCellValueAsString(cell)); + } else { + // If we find an empty cell in the header, stop + break; + } + } + + // Check that the header has at least 2 columns + if (header.size() < 2) { + parserMessages.add(String.format("Skipped sheet '%s' - Header has less than 2 columns", sheet.getSheetName())); + return null; + } + + // If a reference header is provided, check that it matches + if (referenceHeader != null && !header.equals(referenceHeader)) { + parserMessages.add(String.format("Skipped sheet '%s' - Header does not match reference header", sheet.getSheetName())); + return null; + } + + // Extract data rows + List> rows = new ArrayList<>(); + int headerColumnCount = header.size(); + + for (int rowIdx = startRow + 1; rowIdx <= sheet.getLastRowNum(); rowIdx++) { + Row dataRow = sheet.getRow(rowIdx); + + // Skip if row is null + if (dataRow == null) { + continue; + } + + List rowData = new ArrayList<>(); + boolean hasData = false; + + // Extract data from the row + for (int colIdx = startCol; colIdx < startCol + headerColumnCount; colIdx++) { + Cell cell = dataRow.getCell(colIdx, Row.MissingCellPolicy.RETURN_BLANK_AS_NULL); + String value = getCellValueAsString(cell); + rowData.add(value); + if (cell != null && !value.trim().isEmpty()) { + hasData = true; + } + } + + // Only add rows that have at least one non-empty cell + if (hasData) { + rows.add(rowData); + } + } + + // Only return data if we found any rows + return rows.isEmpty() ? null : new TabularData(id, header, rows); + } + + /** Converts an Excel cell value to a string */ + protected String getCellValueAsString(Cell cell) { + if (cell == null) { + return ""; + } + + switch (cell.getCellType()) { + case STRING: + return cell.getStringCellValue(); + case NUMERIC: + if (DateUtil.isCellDateFormatted(cell)) { + return cell.getDateCellValue().toString(); + } else { + // To avoid scientific notation display + double value = cell.getNumericCellValue(); + if (value == Math.floor(value)) { + return String.format("%.0f", value); + } else { + return String.valueOf(value); + } + } + case BOOLEAN: + return String.valueOf(cell.getBooleanCellValue()); + case FORMULA: + try { + return String.valueOf(cell.getNumericCellValue()); + } catch (IllegalStateException e) { + try { + return String.valueOf(cell.getStringCellValue()); + } catch (IllegalStateException e2) { + return "#ERROR"; + } + } + default: + return ""; + } + } + + /** Internal class to store the position of a table in an Excel sheet */ + protected static class TablePosition { + private final int startRow; + private final int startCol; + + public TablePosition(int startRow, int startCol) { + this.startRow = startRow; + this.startCol = startCol; + } + + public int getStartRow() { + return startRow; + } + + public int getStartCol() { + return startCol; + } + } + } +} + diff --git a/src/main/java/io/jenkins/plugins/reporter/provider/Excel.java b/src/main/java/io/jenkins/plugins/reporter/provider/Excel.java index 1639d451..ed86c667 100644 --- a/src/main/java/io/jenkins/plugins/reporter/provider/Excel.java +++ b/src/main/java/io/jenkins/plugins/reporter/provider/Excel.java @@ -2,7 +2,6 @@ import hudson.Extension; import io.jenkins.plugins.reporter.Messages; -import io.jenkins.plugins.reporter.model.Provider; import io.jenkins.plugins.reporter.model.ReportDto; import io.jenkins.plugins.reporter.model.ReportParser; import io.jenkins.plugins.reporter.util.TabularData; @@ -14,13 +13,12 @@ import java.io.File; import java.io.FileInputStream; import java.io.IOException; -import java.util.ArrayList; import java.util.List; /** * Provider for Excel (XLSX) files */ -public class Excel extends Provider { +public class Excel extends AbstractExcelProvider { private static final long serialVersionUID = 9141170397250309265L; @@ -56,198 +54,13 @@ public Descriptor() { /** * Parser for Excel files */ - public static class ExcelParser extends ReportParser { + public static class ExcelParser extends AbstractExcelParser { private static final long serialVersionUID = -8689695008930386641L; - private final String id; - private List parserMessages; /** Constructor */ public ExcelParser(String id) { - super(); - this.id = id; - this.parserMessages = new ArrayList(); - } - - /** Returns the parser identifier */ - public String getId() { - return id; - } - - /** Detects the table position in an Excel sheet */ - private TablePosition detectTablePosition(Sheet sheet) { - int startRow = -1; - int startCol = -1; - int maxNonEmptyConsecutiveCells = 0; - int headerRowIndex = -1; - - // Check the first 20 rows to find the table start - int maxRowsToCheck = Math.min(20, sheet.getLastRowNum() + 1); - - for (int rowIndex = 0; rowIndex < maxRowsToCheck; rowIndex++) { - Row row = sheet.getRow(rowIndex); - if (row == null) continue; - - int nonEmptyConsecutiveCells = 0; - int firstNonEmptyCellIndex = -1; - - // Check the cells in the row - for (int colIndex = 0; colIndex < 100; colIndex++) { // Arbitrary limit of 100 columns - Cell cell = row.getCell(colIndex, Row.MissingCellPolicy.RETURN_BLANK_AS_NULL); - if (cell != null) { - if (firstNonEmptyCellIndex == -1) { - firstNonEmptyCellIndex = colIndex; - } - nonEmptyConsecutiveCells++; - } else if (firstNonEmptyCellIndex != -1) { - // Found an empty cell after non-empty cells - break; - } - } - - // If we found a row with more consecutive non-empty cells than before - if (nonEmptyConsecutiveCells > maxNonEmptyConsecutiveCells && nonEmptyConsecutiveCells >= 2) { - maxNonEmptyConsecutiveCells = nonEmptyConsecutiveCells; - headerRowIndex = rowIndex; - startCol = firstNonEmptyCellIndex; - } - } - - // If we found a potential header - if (headerRowIndex != -1) { - startRow = headerRowIndex; - } else { - // Default to the first row - startRow = 0; - startCol = 0; - } - - return new TablePosition(startRow, startCol); - } - - /** Checks if a row is empty */ - private boolean isRowEmpty(Row row, int startCol, int columnCount) { - if (row == null) return true; - - for (int i = startCol; i < startCol + columnCount; i++) { - Cell cell = row.getCell(i, Row.MissingCellPolicy.RETURN_BLANK_AS_NULL); - if (cell != null) { - return false; - } - } - return true; - } - - /** Extracts data from an Excel sheet */ - private TabularData extractSheetData(Sheet sheet, List referenceHeader) { - // Detect table position - TablePosition tablePos = detectTablePosition(sheet); - int startRow = tablePos.getStartRow(); - int startCol = tablePos.getStartCol(); - - // Get the header row - Row headerRow = sheet.getRow(startRow); - if (headerRow == null) { - parserMessages.add(String.format("Skipped sheet '%s' - No header row found", sheet.getSheetName())); - return null; - } - - // Extract headers - List header = new ArrayList<>(); - int lastCol = headerRow.getLastCellNum(); - for (int colIdx = startCol; colIdx < lastCol; colIdx++) { - Cell cell = headerRow.getCell(colIdx, Row.MissingCellPolicy.RETURN_BLANK_AS_NULL); - if (cell != null) { - header.add(getCellValueAsString(cell)); - } else { - // If we find an empty cell in the header, stop - break; - } - } - - // Check that the header has at least 2 columns - if (header.size() < 2) { - parserMessages.add(String.format("Skipped sheet '%s' - Header has less than 2 columns", sheet.getSheetName())); - return null; - } - - // If a reference header is provided, check that it matches - if (referenceHeader != null && !header.equals(referenceHeader)) { - parserMessages.add(String.format("Skipped sheet '%s' - Header does not match reference header", sheet.getSheetName())); - return null; - } - - // Extract data rows - List> rows = new ArrayList<>(); - int headerColumnCount = header.size(); - - for (int rowIdx = startRow + 1; rowIdx <= sheet.getLastRowNum(); rowIdx++) { - Row dataRow = sheet.getRow(rowIdx); - - // If the row is empty, skip to the next one - if (isRowEmpty(dataRow, startCol, headerColumnCount)) { - continue; - } - - List rowData = new ArrayList<>(); - boolean rowComplete = true; - - // Extract data from the row - for (int colIdx = startCol; colIdx < startCol + headerColumnCount; colIdx++) { - Cell cell = dataRow.getCell(colIdx, Row.MissingCellPolicy.RETURN_BLANK_AS_NULL); - if (cell != null) { - rowData.add(getCellValueAsString(cell)); - } else { - // Empty cell, add an empty string - rowData.add(""); - } - } - - // If the row has the correct number of columns, add it - if (rowComplete) { - rows.add(rowData); - } - } - - return new TabularData(id, header, rows); - } - - /** Converts an Excel cell value to a string */ - private String getCellValueAsString(Cell cell) { - if (cell == null) { - return ""; - } - - switch (cell.getCellType()) { - case STRING: - return cell.getStringCellValue(); - case NUMERIC: - if (DateUtil.isCellDateFormatted(cell)) { - return cell.getDateCellValue().toString(); - } else { - // To avoid scientific notation display - double value = cell.getNumericCellValue(); - if (value == Math.floor(value)) { - return String.format("%.0f", value); - } else { - return String.valueOf(value); - } - } - case BOOLEAN: - return String.valueOf(cell.getBooleanCellValue()); - case FORMULA: - try { - return String.valueOf(cell.getNumericCellValue()); - } catch (IllegalStateException e) { - try { - return String.valueOf(cell.getStringCellValue()); - } catch (IllegalStateException e2) { - return "#ERROR"; - } - } - default: - return ""; - } + super(id); } /** Parses an Excel file and creates a ReportDto */ @@ -288,24 +101,5 @@ public ReportDto parse(File file) throws IOException { return result.processData(parserMessages); } } - - /** Internal class to store the position of a table in an Excel sheet */ - private static class TablePosition { - private final int startRow; - private final int startCol; - - public TablePosition(int startRow, int startCol) { - this.startRow = startRow; - this.startCol = startCol; - } - - public int getStartRow() { - return startRow; - } - - public int getStartCol() { - return startCol; - } - } } } diff --git a/src/main/java/io/jenkins/plugins/reporter/provider/ExcelMulti.java b/src/main/java/io/jenkins/plugins/reporter/provider/ExcelMulti.java new file mode 100644 index 00000000..88b65edd --- /dev/null +++ b/src/main/java/io/jenkins/plugins/reporter/provider/ExcelMulti.java @@ -0,0 +1,150 @@ +package io.jenkins.plugins.reporter.provider; + +import hudson.Extension; +import io.jenkins.plugins.reporter.Messages; +import io.jenkins.plugins.reporter.model.ReportDto; +import io.jenkins.plugins.reporter.model.ReportParser; +import io.jenkins.plugins.reporter.util.TabularData; +import org.apache.poi.ss.usermodel.*; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.jenkinsci.Symbol; +import org.kohsuke.stapler.DataBoundConstructor; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Provider for multi-sheet Excel (XLSX) files with header consistency enforcement + * This provider parses all sheets in an Excel file, enforcing header consistency across sheets. + * Only sheets with identical headers (compared to the first valid sheet) will be processed. + */ +public class ExcelMulti extends AbstractExcelProvider { + + private static final long serialVersionUID = 1845129735392309267L; + + private static final String ID = "excelMulti"; + + /** Default constructor */ + @DataBoundConstructor + public ExcelMulti() { + super(); + // empty constructor required for stapler + } + + /** Creates an ExcelMulti parser */ + @Override + public ReportParser createParser() { + if (getActualId().equals(getDescriptor().getId())) { + throw new IllegalArgumentException(Messages.Provider_Error()); + } + + return new ExcelMultiParser(getActualId()); + } + + /** Descriptor for this provider */ + @Symbol("excelMulti") + @Extension + public static class Descriptor extends ProviderDescriptor { + /** Creates a descriptor instance */ + public Descriptor() { + super(ID); + } + } + + /** + * Parser for multi-sheet Excel files with header consistency enforcement + */ + public static class ExcelMultiParser extends AbstractExcelParser { + + private static final long serialVersionUID = 3724789235028726491L; + + /** Constructor */ + public ExcelMultiParser(String id) { + super(id); + } + + /** + * Parses an Excel file with multiple sheets and creates a ReportDto + * Only processes sheets with consistent headers (identical to the first valid sheet) + */ + @Override + public ReportDto parse(File file) throws IOException { + try (FileInputStream fis = new FileInputStream(file); + Workbook workbook = new XSSFWorkbook(fis)) { + + TabularData result = null; + List referenceHeader = null; + int totalSheets = workbook.getNumberOfSheets(); + int processedSheets = 0; + int skippedSheets = 0; + List processedSheetNames = new ArrayList<>(); + List skippedSheetNames = new ArrayList<>(); + List> allRows = new ArrayList<>(); + + // Add an initial parser message about number of sheets + parserMessages.add(String.format("Excel file contains %d sheets", totalSheets)); + + // First pass: validate sheets and collect all rows + for (int i = 0; i < totalSheets; i++) { + Sheet sheet = workbook.getSheetAt(i); + String sheetName = sheet.getSheetName(); + + // Extract data from the sheet + TabularData sheetData = extractSheetData(sheet, referenceHeader); + + // If the sheet contains valid data + if (sheetData != null) { + // If it's the first valid sheet, use it as reference + if (referenceHeader == null) { + referenceHeader = sheetData.getHeader(); + allRows.addAll(sheetData.getRows()); + processedSheets++; + processedSheetNames.add(sheetName); + parserMessages.add(String.format("Processing sheet '%s' as reference sheet with %d columns: %s", + sheetName, referenceHeader.size(), String.join(", ", referenceHeader))); + } else { + // For subsequent sheets, add rows to the collection + allRows.addAll(sheetData.getRows()); + processedSheets++; + processedSheetNames.add(sheetName); + parserMessages.add(String.format("Processing sheet '%s' - adding %d rows", + sheetName, sheetData.getRows().size())); + } + } else { + skippedSheets++; + skippedSheetNames.add(sheetName); + parserMessages.add(String.format("Skipped sheet '%s' - Invalid format or no data", sheetName)); + } + } + + // Create final TabularData with all rows + if (referenceHeader != null && !allRows.isEmpty()) { + result = new TabularData(getId(), referenceHeader, allRows); + } + + // Add summary information to parser messages + parserMessages.add(String.format("Processed %d sheets: %s", + processedSheets, String.join(", ", processedSheetNames))); + + if (skippedSheets > 0) { + parserMessages.add(String.format("Skipped %d sheets: %s", + skippedSheets, String.join(", ", skippedSheetNames))); + } + + // If no sheet contains valid data + if (result == null) { + throw new IOException("No valid data found in Excel file. All sheets were skipped."); + } + + parserMessages.add(String.format("Total rows collected: %d", allRows.size())); + + // Process and return the final tabular data + return result.processData(parserMessages); + } + } + } +} + diff --git a/src/main/java/io/jenkins/plugins/reporter/util/TabularData.java b/src/main/java/io/jenkins/plugins/reporter/util/TabularData.java index b17eb2c0..f1817c21 100644 --- a/src/main/java/io/jenkins/plugins/reporter/util/TabularData.java +++ b/src/main/java/io/jenkins/plugins/reporter/util/TabularData.java @@ -7,6 +7,7 @@ import java.io.Serializable; import java.util.*; +import java.util.stream.Collectors; /** * Utility class for tabular data processing @@ -54,125 +55,138 @@ public ReportDto processData(List parserMessages) { int rowCount = 0; final int headerColumnCount = header.size(); - int colIdxValueStart = 0; + + // First two columns are always category columns, rest are value columns + final int categoryColumns = 2; + final int valueColumns = headerColumnCount - categoryColumns; if (headerColumnCount >= 2) { rowCount = rows.size(); + parserMessages.add(String.format("Processing data with %d rows, %d category columns, %d value columns", + rowCount, categoryColumns, valueColumns)); + parserMessages.add(String.format("Headers: %s", String.join(", ", header))); } else { - parserMessages.add(String.format("skipped file - First line has %d elements", headerColumnCount + 1)); + parserMessages.add(String.format("skipped file - First line has %d elements", headerColumnCount)); + return report; } /** Parse all data rows */ for (int rowIdx = 0; rowIdx < rowCount; rowIdx++) { - String parentId = "report"; List row = rows.get(rowIdx); - Item last = null; - boolean lastItemAdded = false; - LinkedHashMap result = new LinkedHashMap<>(); - boolean emptyFieldFound = false; - int rowSize = row.size(); + + // Add debug info for first and last row + if (rowIdx == 0 || rowIdx == rows.size() - 1) { // Only log first and last row for brevity + parserMessages.add(String.format("Processing row %d: %s", + rowIdx + 1, + String.join(", ", row.subList(0, Math.min(row.size(), 5))) + + (row.size() > 5 ? "..." : ""))); + } + + if (row.size() < headerColumnCount) { + parserMessages.add(String.format("skipped line %d - line has fewer elements than header", rowIdx + 2)); + continue; + } - /** Parse until first data line is found to get data and value field */ - if (colIdxValueStart == 0) { - /** Col 0 is assumed to be string */ - for (int colIdx = rowSize - 1; colIdx > 1; colIdx--) { - String value = row.get(colIdx); + // Process category columns + String[] categories = new String[categoryColumns]; + boolean validRow = true; + for (int i = 0; i < categoryColumns; i++) { + categories[i] = row.get(i).trim(); + if (categories[i].isEmpty()) { + validRow = false; + break; + } + } + + if (!validRow) { + parserMessages.add(String.format("skipped line %d - empty category", rowIdx + 2)); + continue; + } - if (NumberUtils.isCreatable(value)) { - colIdxValueStart = colIdx; + // Process each category level + Item currentItem = null; + String parentId = "report"; + + for (int level = 0; level < categories.length; level++) { + String categoryName = categories[level]; + String categoryId = level == 0 ? categoryName : parentId + categoryName; + + if (level == 0) { + // Find or create root level item + Optional existing = report.getItems().stream() + .filter(i -> i.getName().equals(categoryName)) + .findFirst(); + + if (existing.isPresent()) { + currentItem = existing.get(); + } else { + currentItem = new Item(); + currentItem.setId(categoryId); + currentItem.setName(categoryName); + report.getItems().add(currentItem); + parserMessages.add(String.format("Created new root item: %s", categoryName)); + } + } else { + // Find or create sub-item + if (!currentItem.hasItems()) { + currentItem.setItems(new ArrayList<>()); + } + + Optional existing = currentItem.getItems().stream() + .filter(i -> i.getName().equals(categoryName)) + .findFirst(); + + if (existing.isPresent()) { + currentItem = existing.get(); } else { - if (colIdxValueStart > 0) { - parserMessages - .add(String.format("Found data - fields number = %d - numeric fields = %d", - colIdxValueStart, rowSize - colIdxValueStart)); - } - break; + Item newItem = new Item(); + newItem.setId(categoryId); + newItem.setName(categoryName); + currentItem.getItems().add(newItem); + currentItem = newItem; + parserMessages.add(String.format("Created new sub-item: %s under %s", + categoryName, categories[level-1])); } } + parentId = categoryId; } - String valueId = ""; - /** Parse line if first data line is OK and line has more element than header */ - if ((colIdxValueStart > 0) && (rowSize >= headerColumnCount)) { - /** Check line and header size matching */ - for (int colIdx = 0; colIdx < headerColumnCount; colIdx++) { - String colId = header.get(colIdx); - String value = row.get(colIdx); - - /** Check value fields */ - if ((colIdx < colIdxValueStart)) { - /** Test if text item is a value or empty */ - if ((NumberUtils.isCreatable(value)) || (StringUtils.isBlank(value))) { - /** Empty field found - message */ - if (colIdx == 0) { - parserMessages - .add(String.format("skipped line %d - First column item empty - col = %d ", - rowIdx + 2, colIdx + 1)); - break; - } else { - emptyFieldFound = true; - /** Continue next column parsing */ - continue; - } - } else { - /** Check if field values are present after empty cells */ - if (emptyFieldFound) { - parserMessages.add(String.format("skipped line %d Empty field in col = %d ", - rowIdx + 2, colIdx + 1)); - break; - } - } - valueId += value; - Optional parent = report.findItem(parentId, report.getItems()); - Item item = new Item(); - lastItemAdded = false; - item.setId(valueId); - item.setName(value); - String finalValueId = valueId; - if (parent.isPresent()) { - Item p = parent.get(); - if (!p.hasItems()) { - p.setItems(new ArrayList<>()); - } - if (p.getItems().stream().noneMatch(i -> i.getId().equals(finalValueId))) { - p.addItem(item); - lastItemAdded = true; - } - } else { - if (report.getItems().stream().noneMatch(i -> i.getId().equals(finalValueId))) { - report.getItems().add(item); - lastItemAdded = true; - } - } - parentId = valueId; - last = item; - } else { - Number val = 0; - if (NumberUtils.isCreatable(value)) { - val = NumberUtils.createNumber(value); - } - result.put(colId, val.intValue()); - } + // Process value columns + if (currentItem != null) { + LinkedHashMap values = new LinkedHashMap<>(); + for (int i = categoryColumns; i < headerColumnCount && i < row.size(); i++) { + String headerName = header.get(i); + String value = row.get(i); + int numericValue = NumberUtils.isCreatable(value) ? + NumberUtils.createNumber(value).intValue() : 0; + values.put(headerName, numericValue); } - } else { - /** Skip file if first data line has no value field */ - if (colIdxValueStart == 0) { - parserMessages.add(String.format("skipped line %d - First data row not found", rowIdx + 2)); - continue; + + if (currentItem.getResult() == null) { + currentItem.setResult(values); } else { - parserMessages - .add(String.format("skipped line %d - line has fewer element than title", rowIdx + 2)); - continue; + // Merge values with existing results + LinkedHashMap existing = currentItem.getResult(); + for (Map.Entry entry : values.entrySet()) { + existing.merge(entry.getKey(), entry.getValue(), Integer::sum); + } } + + parserMessages.add(String.format("Processed %s: %s", currentItem.getName(), + values.entrySet().stream() + .map(e -> e.getKey() + "=" + e.getValue()) + .collect(Collectors.joining(", ")))); } - /** If last item was created, it will be added to report */ - if (lastItemAdded) { - last.setResult(result); - } else { - parserMessages.add(String.format("ignored line %d - Same fields already exists", rowIdx + 2)); - } } - // report.setParserLog(parserMessages); + + // Add debug info about final report structure + parserMessages.add(String.format("Final report contains %d root items", report.getItems().size())); + for (Item item : report.getItems()) { + parserMessages.add(String.format("Root item: %s with %d subitems", + item.getName(), + item.hasItems() ? item.getItems().size() : 0)); + } + return report; } } diff --git a/src/test/java/io/jenkins/plugins/reporter/provider/CreateExcelMultiSample.java b/src/test/java/io/jenkins/plugins/reporter/provider/CreateExcelMultiSample.java new file mode 100644 index 00000000..32a67181 --- /dev/null +++ b/src/test/java/io/jenkins/plugins/reporter/provider/CreateExcelMultiSample.java @@ -0,0 +1,233 @@ +package io.jenkins.plugins.reporter.provider; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import org.apache.poi.ss.usermodel.*; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; + +/** + * Utility class to generate multi-tab Excel test files for testing the ExcelMulti provider functionality. + */ +public class CreateExcelMultiSample { + + public static void main(String[] args) throws IOException { + // Create test files in the resources directory + String resourcesDir = "src/test/resources"; + + // Create multi-sheet Excel file with consistent headers + createConsistentHeadersExcelFile(new File(resourcesDir, "test-excel-multi-consistent.xlsx")); + + // Create multi-sheet Excel file with inconsistent headers + createInconsistentHeadersExcelFile(new File(resourcesDir, "test-excel-multi-inconsistent.xlsx")); + + // Create multi-sheet Excel file with mixed validity + createMixedValidityExcelFile(new File(resourcesDir, "test-excel-multi-mixed.xlsx")); + + System.out.println("Multi-tab test Excel files created successfully in " + resourcesDir); + } + + /** + * Creates a multi-sheet Excel file with consistent headers across all sheets. + */ + public static void createConsistentHeadersExcelFile(File file) throws IOException { + Workbook workbook = new XSSFWorkbook(); + + // Create first sheet + Sheet sheet1 = workbook.createSheet("Sheet 1"); + + // Create header row + Row headerRow1 = sheet1.createRow(0); + headerRow1.createCell(0).setCellValue("Category"); + headerRow1.createCell(1).setCellValue("Subcategory"); + headerRow1.createCell(2).setCellValue("Value1"); + headerRow1.createCell(3).setCellValue("Value2"); + + // Create data rows + Row dataRow1 = sheet1.createRow(1); + dataRow1.createCell(0).setCellValue("Category A"); + dataRow1.createCell(1).setCellValue("Subcat A1"); + dataRow1.createCell(2).setCellValue(10); + dataRow1.createCell(3).setCellValue(20); + + Row dataRow2 = sheet1.createRow(2); + dataRow2.createCell(0).setCellValue("Category A"); + dataRow2.createCell(1).setCellValue("Subcat A2"); + dataRow2.createCell(2).setCellValue(15); + dataRow2.createCell(3).setCellValue(25); + + // Create second sheet with the same headers + Sheet sheet2 = workbook.createSheet("Sheet 2"); + + // Create identical header row + Row headerRow2 = sheet2.createRow(0); + headerRow2.createCell(0).setCellValue("Category"); + headerRow2.createCell(1).setCellValue("Subcategory"); + headerRow2.createCell(2).setCellValue("Value1"); + headerRow2.createCell(3).setCellValue("Value2"); + + // Create data rows with different data + Row dataRow3 = sheet2.createRow(1); + dataRow3.createCell(0).setCellValue("Category B"); + dataRow3.createCell(1).setCellValue("Subcat B1"); + dataRow3.createCell(2).setCellValue(30); + dataRow3.createCell(3).setCellValue(40); + + Row dataRow4 = sheet2.createRow(2); + dataRow4.createCell(0).setCellValue("Category B"); + dataRow4.createCell(1).setCellValue("Subcat B2"); + dataRow4.createCell(2).setCellValue(35); + dataRow4.createCell(3).setCellValue(45); + + // Create third sheet with the same headers + Sheet sheet3 = workbook.createSheet("Sheet 3"); + + // Create identical header row + Row headerRow3 = sheet3.createRow(0); + headerRow3.createCell(0).setCellValue("Category"); + headerRow3.createCell(1).setCellValue("Subcategory"); + headerRow3.createCell(2).setCellValue("Value1"); + headerRow3.createCell(3).setCellValue("Value2"); + + // Create data rows with different data + Row dataRow5 = sheet3.createRow(1); + dataRow5.createCell(0).setCellValue("Category C"); + dataRow5.createCell(1).setCellValue("Subcat C1"); + dataRow5.createCell(2).setCellValue(50); + dataRow5.createCell(3).setCellValue(60); + + // Write to file + try (FileOutputStream fileOut = new FileOutputStream(file)) { + workbook.write(fileOut); + } + workbook.close(); + } + + /** + * Creates a multi-sheet Excel file with inconsistent headers across sheets. + */ + public static void createInconsistentHeadersExcelFile(File file) throws IOException { + Workbook workbook = new XSSFWorkbook(); + + // Create first sheet + Sheet sheet1 = workbook.createSheet("Sheet 1"); + + // Create header row + Row headerRow1 = sheet1.createRow(0); + headerRow1.createCell(0).setCellValue("Category"); + headerRow1.createCell(1).setCellValue("Subcategory"); + headerRow1.createCell(2).setCellValue("Value1"); + headerRow1.createCell(3).setCellValue("Value2"); + + // Create data rows + Row dataRow1 = sheet1.createRow(1); + dataRow1.createCell(0).setCellValue("Category A"); + dataRow1.createCell(1).setCellValue("Subcat A1"); + dataRow1.createCell(2).setCellValue(10); + dataRow1.createCell(3).setCellValue(20); + + // Create second sheet with different headers + Sheet sheet2 = workbook.createSheet("Sheet 2"); + + // Create different header row (different column order) + Row headerRow2 = sheet2.createRow(0); + headerRow2.createCell(0).setCellValue("Category"); + headerRow2.createCell(1).setCellValue("Value1"); // Swapped order + headerRow2.createCell(2).setCellValue("Subcategory"); + headerRow2.createCell(3).setCellValue("Value2"); + + // Create data rows + Row dataRow2 = sheet2.createRow(1); + dataRow2.createCell(0).setCellValue("Category B"); + dataRow2.createCell(1).setCellValue(30); + dataRow2.createCell(2).setCellValue("Subcat B1"); + dataRow2.createCell(3).setCellValue(40); + + // Create third sheet with different headers + Sheet sheet3 = workbook.createSheet("Sheet 3"); + + // Create different header row (different column names) + Row headerRow3 = sheet3.createRow(0); + headerRow3.createCell(0).setCellValue("Group"); // Different name + headerRow3.createCell(1).setCellValue("Subcategory"); + headerRow3.createCell(2).setCellValue("Score1"); // Different name + headerRow3.createCell(3).setCellValue("Score2"); // Different name + + // Create data rows + Row dataRow3 = sheet3.createRow(1); + dataRow3.createCell(0).setCellValue("Category C"); + dataRow3.createCell(1).setCellValue("Subcat C1"); + dataRow3.createCell(2).setCellValue(50); + dataRow3.createCell(3).setCellValue(60); + + // Write to file + try (FileOutputStream fileOut = new FileOutputStream(file)) { + workbook.write(fileOut); + } + workbook.close(); + } + + /** + * Creates a multi-sheet Excel file with mixed validity (some valid sheets, some invalid). + */ + public static void createMixedValidityExcelFile(File file) throws IOException { + Workbook workbook = new XSSFWorkbook(); + + // Create first sheet (valid) + Sheet sheet1 = workbook.createSheet("Valid Sheet 1"); + + // Create header row + Row headerRow1 = sheet1.createRow(0); + headerRow1.createCell(0).setCellValue("Category"); + headerRow1.createCell(1).setCellValue("Subcategory"); + headerRow1.createCell(2).setCellValue("Value1"); + headerRow1.createCell(3).setCellValue("Value2"); + + // Create data rows + Row dataRow1 = sheet1.createRow(1); + dataRow1.createCell(0).setCellValue("Category A"); + dataRow1.createCell(1).setCellValue("Subcat A1"); + dataRow1.createCell(2).setCellValue(10); + dataRow1.createCell(3).setCellValue(20); + + // Create second sheet (empty - invalid) + workbook.createSheet("Empty Sheet"); + + // Create third sheet (valid - same header) + Sheet sheet3 = workbook.createSheet("Valid Sheet 2"); + + // Create header row (same as first sheet) + Row headerRow3 = sheet3.createRow(0); + headerRow3.createCell(0).setCellValue("Category"); + headerRow3.createCell(1).setCellValue("Subcategory"); + headerRow3.createCell(2).setCellValue("Value1"); + headerRow3.createCell(3).setCellValue("Value2"); + + // Create data rows + Row dataRow3 = sheet3.createRow(1); + dataRow3.createCell(0).setCellValue("Category C"); + dataRow3.createCell(1).setCellValue("Subcat C1"); + dataRow3.createCell(2).setCellValue(50); + dataRow3.createCell(3).setCellValue(60); + + // Create fourth sheet (invalid - fewer columns) + Sheet sheet4 = workbook.createSheet("Invalid Sheet - Fewer Columns"); + + // Create header row with fewer columns + Row headerRow4 = sheet4.createRow(0); + headerRow4.createCell(0).setCellValue("Category"); + headerRow4.createCell(1).setCellValue("Value1"); + + // Create data row + Row dataRow4 = sheet4.createRow(1); + dataRow4.createCell(0).setCellValue("Category D"); + dataRow4.createCell(1).setCellValue(70); + + // Write to file + try (FileOutputStream fileOut = new FileOutputStream(file)) { + workbook.write(fileOut); + } + workbook.close(); + } +} + diff --git a/src/test/java/io/jenkins/plugins/reporter/provider/ExcelMultiTest.java b/src/test/java/io/jenkins/plugins/reporter/provider/ExcelMultiTest.java new file mode 100644 index 00000000..c406f578 --- /dev/null +++ b/src/test/java/io/jenkins/plugins/reporter/provider/ExcelMultiTest.java @@ -0,0 +1,358 @@ +package io.jenkins.plugins.reporter.provider; + +import io.jenkins.plugins.reporter.model.Item; +import io.jenkins.plugins.reporter.model.ReportDto; +import io.jenkins.plugins.reporter.model.ReportParser; +import org.apache.poi.ss.usermodel.*; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; + +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.Optional; + +import static org.junit.Assert.*; + +/** + * Tests for the ExcelMulti provider class. + * This test suite validates the functionality of the ExcelMulti provider, + * which handles multi-sheet Excel files with header consistency enforcement. + */ +public class ExcelMultiTest { + + @Rule + public JenkinsRule jenkinsRule = new JenkinsRule(); + + private File consistentHeadersFile; + private File inconsistentHeadersFile; + private File mixedValidityFile; + private ExcelMulti provider; + + /** + * Set up the test files and provider before each test. + */ + @Before + public void setUp() throws IOException { + // Generate the test files if they don't exist + String resourcesDir = "src/test/resources"; + consistentHeadersFile = new File(resourcesDir, "test-excel-multi-consistent.xlsx"); + inconsistentHeadersFile = new File(resourcesDir, "test-excel-multi-inconsistent.xlsx"); + mixedValidityFile = new File(resourcesDir, "test-excel-multi-mixed.xlsx"); + + if (!consistentHeadersFile.exists()) { + CreateExcelMultiSample.createConsistentHeadersExcelFile(consistentHeadersFile); + } + + if (!inconsistentHeadersFile.exists()) { + CreateExcelMultiSample.createInconsistentHeadersExcelFile(inconsistentHeadersFile); + } + + if (!mixedValidityFile.exists()) { + CreateExcelMultiSample.createMixedValidityExcelFile(mixedValidityFile); + } + + // Create and configure the provider + provider = new ExcelMulti(); + provider.setId("test-excel-multi"); + + // Add debug logging in setUp + if (consistentHeadersFile.exists()) { + System.out.println("Test file exists: " + consistentHeadersFile.getAbsolutePath()); + System.out.println("File size: " + consistentHeadersFile.length() + " bytes"); + } else { + System.out.println("Test file does not exist: " + consistentHeadersFile.getAbsolutePath()); + } + + if (mixedValidityFile.exists()) { + System.out.println("Test file exists: " + mixedValidityFile.getAbsolutePath()); + System.out.println("File size: " + mixedValidityFile.length() + " bytes"); + } else { + System.out.println("Test file does not exist: " + mixedValidityFile.getAbsolutePath()); + } + + // Debug Excel structure + try (Workbook workbook = WorkbookFactory.create(consistentHeadersFile)) { + System.out.println("=== Excel Structure Debug (consistentHeadersFile) ==="); + System.out.println("Number of sheets: " + workbook.getNumberOfSheets()); + for (int i = 0; i < workbook.getNumberOfSheets(); i++) { + Sheet sheet = workbook.getSheetAt(i); + System.out.println("Sheet " + i + ": " + sheet.getSheetName()); + Row headerRow = sheet.getRow(0); + if (headerRow != null) { + System.out.println(" Headers:"); + for (Cell cell : headerRow) { + System.out.println(" - " + cell.getStringCellValue()); + } + } + } + System.out.println("========================="); + } catch (Exception e) { + System.out.println("Error reading Excel structure: " + e.getMessage()); + } + + try (Workbook workbook = WorkbookFactory.create(mixedValidityFile)) { + System.out.println("=== Excel Structure Debug (mixedValidityFile) ==="); + System.out.println("Number of sheets: " + workbook.getNumberOfSheets()); + for (int i = 0; i < workbook.getNumberOfSheets(); i++) { + Sheet sheet = workbook.getSheetAt(i); + System.out.println("Sheet " + i + ": " + sheet.getSheetName()); + Row headerRow = sheet.getRow(0); + if (headerRow != null) { + System.out.println(" Headers:"); + for (Cell cell : headerRow) { + System.out.println(" - " + cell.getStringCellValue()); + } + } else { + System.out.println(" No header row found"); + } + } + System.out.println("========================="); + } catch (Exception e) { + System.out.println("Error reading Excel structure: " + e.getMessage()); + } + } + + /** + * Test the basic functionality of the ExcelMulti provider. + * This test verifies that the provider can parse a simple multi-sheet Excel file + * with consistent headers across all sheets. + */ + @Test + public void testBasicFunctionality() throws IOException { + // Create the parser + ReportParser parser = provider.createParser(); + assertNotNull("Parser should not be null", parser); + + // Parse the file with consistent headers + ReportDto report = parser.parse(consistentHeadersFile); + + // Verify the report is correctly parsed + assertNotNull("Report should not be null", report); + assertEquals("Report ID should match provider ID", "test-excel-multi", report.getId()); + + // Check that we have items + List items = report.getItems(); + assertNotNull("Items list should not be null", items); + assertFalse("Items list should not be empty", items.isEmpty()); + } + + /** + * Test that data from all sheets with consistent headers is aggregated. + * This test verifies that the ExcelMulti provider correctly aggregates data + * from all sheets that have consistent headers. + */ + @Test + public void testDataAggregation() throws IOException { + // Create the parser + ReportParser parser = provider.createParser(); + + // Parse the file with consistent headers + ReportDto report = parser.parse(consistentHeadersFile); + + // Verify data from all sheets is included + List items = report.getItems(); + + // Debug logging + System.out.println("=== Data Aggregation Test Debug ==="); + System.out.println("Total items found: " + items.size()); + for (Item item : items) { + System.out.println("Found category: " + item.getName()); + if (item.getItems() != null) { + for (Item subItem : item.getItems()) { + System.out.println(" - Subcategory: " + subItem.getName()); + } + } + } + System.out.println("================================"); + + // Find items from different sheets + Optional categoryA = items.stream() + .filter(item -> item.getName().equals("Category A")) + .findFirst(); + + Optional categoryB = items.stream() + .filter(item -> item.getName().equals("Category B")) + .findFirst(); + + Optional categoryC = items.stream() + .filter(item -> item.getName().equals("Category C")) + .findFirst(); + + // Verify all categories are present (from different sheets) + assertTrue("Category A should be present", categoryA.isPresent()); + assertTrue("Category B should be present", categoryB.isPresent()); + assertTrue("Category C should be present", categoryC.isPresent()); + + // Verify subcategories for Category A + if (categoryA.isPresent()) { + List subcategoriesA = categoryA.get().getItems(); + assertNotNull("Subcategories for Category A should not be null", subcategoriesA); + assertEquals("Category A should have 2 subcategories", 2, subcategoriesA.size()); + + // Check subcategory names + assertEquals("First subcategory of A should be Subcat A1", + "Subcat A1", subcategoriesA.get(0).getName()); + assertEquals("Second subcategory of A should be Subcat A2", + "Subcat A2", subcategoriesA.get(1).getName()); + } + + // Verify subcategories for Category B + if (categoryB.isPresent()) { + List subcategoriesB = categoryB.get().getItems(); + assertNotNull("Subcategories for Category B should not be null", subcategoriesB); + assertEquals("Category B should have 2 subcategories", 2, subcategoriesB.size()); + + // Check subcategory names + assertEquals("First subcategory of B should be Subcat B1", + "Subcat B1", subcategoriesB.get(0).getName()); + assertEquals("Second subcategory of B should be Subcat B2", + "Subcat B2", subcategoriesB.get(1).getName()); + } + } + + /** + * Test that the ExcelMulti provider correctly enforces header consistency. + * This test verifies that only sheets with consistent headers are processed. + */ + @Test + public void testHeaderConsistencyValidation() throws IOException { + // Create the parser and make it an instance of ExcelMultiParser to access parser messages + ExcelMulti.ExcelMultiParser parser = (ExcelMulti.ExcelMultiParser) provider.createParser(); + + // Parse the file with inconsistent headers + ReportDto report = parser.parse(inconsistentHeadersFile); + + // Verify only data from the first sheet is included (as other sheets have inconsistent headers) + List items = report.getItems(); + + // There should only be Category A (from first sheet) + Optional categoryA = items.stream() + .filter(item -> item.getName().equals("Category A")) + .findFirst(); + + Optional categoryB = items.stream() + .filter(item -> item.getName().equals("Category B")) + .findFirst(); + + Optional categoryC = items.stream() + .filter(item -> item.getName().equals("Category C")) + .findFirst(); + + // Verify only Category A is present (as other sheets have inconsistent headers) + assertTrue("Category A should be present", categoryA.isPresent()); + assertFalse("Category B should not be present", categoryB.isPresent()); + assertFalse("Category C should not be present", categoryC.isPresent()); + } + + /** + * Test that the ExcelMulti provider correctly handles invalid sheets. + * This test verifies that invalid sheets are skipped and valid sheets are processed. + */ + @Test + public void testHandlingInvalidSheets() throws IOException { + // Create the parser + ReportParser parser = provider.createParser(); + + // Parse the file with mixed validity + ReportDto report = parser.parse(mixedValidityFile); + + // Verify only data from valid sheets is included + List items = report.getItems(); + + // Debug logging + System.out.println("=== Invalid Sheets Test Debug ==="); + System.out.println("Total items found: " + items.size()); + for (Item item : items) { + System.out.println("Found category: " + item.getName()); + if (item.getItems() != null) { + for (Item subItem : item.getItems()) { + System.out.println(" - Subcategory: " + subItem.getName()); + } + } + } + System.out.println("================================"); + + // There should be Categories A and C (from valid sheets) + Optional categoryA = items.stream() + .filter(item -> item.getName().equals("Category A")) + .findFirst(); + + Optional categoryC = items.stream() + .filter(item -> item.getName().equals("Category C")) + .findFirst(); + + Optional categoryD = items.stream() + .filter(item -> item.getName().equals("Category D")) + .findFirst(); + + // Verify only Categories A and C are present (from valid sheets) + assertTrue("Category A should be present", categoryA.isPresent()); + assertTrue("Category C should be present", categoryC.isPresent()); + assertFalse("Category D should not be present", categoryD.isPresent()); + } + + /** + * Test that the ExcelMulti provider generates appropriate parser messages. + * This test verifies that detailed messages are generated for skipped sheets. + */ + @Test + public void testParserMessages() throws IOException { + // Create the parser and make it an instance of ExcelMultiParser to access parser messages + ExcelMulti.ExcelMultiParser parser = (ExcelMulti.ExcelMultiParser) provider.createParser(); + + // Parse the file with mixed validity + parser.parse(mixedValidityFile); + + // Get parser messages + List messages = parser.parserMessages; + + // Verify that parser messages were generated + assertFalse("Parser messages should not be empty", messages.isEmpty()); + + // Check for expected messages + boolean foundInitialMessage = false; + boolean foundSkippedEmptySheet = false; + boolean foundSkippedFewerColumnsSheet = false; + boolean foundSummaryMessage = false; + + for (String message : messages) { + if (message.contains("Excel file contains")) { + foundInitialMessage = true; + } + if (message.contains("Empty Sheet")) { + foundSkippedEmptySheet = true; + } + if (message.contains("Invalid Sheet - Fewer Columns")) { + foundSkippedFewerColumnsSheet = true; + } + if (message.contains("Skipped") && message.contains("sheets:")) { + foundSummaryMessage = true; + } + } + + assertTrue("Should have initial message about sheet count", foundInitialMessage); + assertTrue("Should have message about skipped empty sheet", foundSkippedEmptySheet); + assertTrue("Should have message about skipped sheet with fewer columns", foundSkippedFewerColumnsSheet); + assertTrue("Should have summary message about skipped sheets", foundSummaryMessage); + } + + /** + * Test that the ExcelMulti provider correctly handles error cases. + * This test verifies that appropriate exceptions are thrown for invalid files. + */ + @Test(expected = IOException.class) + public void testErrorHandling() throws IOException { + // Create the parser + ReportParser parser = provider.createParser(); + + // Try to parse a non-existent file + File nonExistentFile = new File("non-existent-file.xlsx"); + parser.parse(nonExistentFile); + + // Should throw IOException + } +} + diff --git a/src/test/java/io/jenkins/plugins/reporter/provider/RegenerateTestFiles.java b/src/test/java/io/jenkins/plugins/reporter/provider/RegenerateTestFiles.java new file mode 100644 index 00000000..81bb00f1 --- /dev/null +++ b/src/test/java/io/jenkins/plugins/reporter/provider/RegenerateTestFiles.java @@ -0,0 +1,31 @@ +package io.jenkins.plugins.reporter.provider; + +import java.io.File; +import java.io.IOException; + +/** + * Temporary utility class to regenerate test Excel files. + * Run this class to update test files before running tests. + */ +public class RegenerateTestFiles { + + public static void main(String[] args) throws IOException { + // Temporary file to regenerate test data + File resourcesDir = new File("src/test/resources"); + if (!resourcesDir.exists()) { + resourcesDir.mkdirs(); + } + + System.out.println("Regenerating test Excel files..."); + + CreateExcelMultiSample.createConsistentHeadersExcelFile( + new File(resourcesDir, "test-excel-multi-consistent.xlsx")); + CreateExcelMultiSample.createInconsistentHeadersExcelFile( + new File(resourcesDir, "test-excel-multi-inconsistent.xlsx")); + CreateExcelMultiSample.createMixedValidityExcelFile( + new File(resourcesDir, "test-excel-multi-mixed.xlsx")); + + System.out.println("Test Excel files regenerated successfully!"); + } +} + diff --git a/src/test/resources/test-excel-multi-consistent.xlsx b/src/test/resources/test-excel-multi-consistent.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..8ca5d849a3c429a0b4d5ec16e5de813134f77039 GIT binary patch literal 4407 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 W%nT{1h^M9_yaos*bK@-W*Z%-AcH>U~ literal 0 HcmV?d00001 diff --git a/src/test/resources/test-excel-multi-inconsistent.xlsx b/src/test/resources/test-excel-multi-inconsistent.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..e4113032346b32cc70f4bfdcf93349ae163762e5 GIT binary patch literal 4374 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 literal 0 HcmV?d00001 diff --git a/src/test/resources/test-excel-multi-mixed.xlsx b/src/test/resources/test-excel-multi-mixed.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..01d877b3d93cc0c5d8904ed27effb068ff22a114 GIT binary patch literal 4748 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 literal 0 HcmV?d00001