Skip to content

Add excel provider and improve csv #432

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,16 @@
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>jackson2-api</artifactId>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>5.4.1</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.4.1</version>
</dependency>

<!-- Workflow dependencies -->
<dependency>
Expand Down Expand Up @@ -164,6 +174,25 @@
<groupId>org.jenkins-ci.tools</groupId>
<artifactId>maven-hpi-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<executions>
<execution>
<id>prepare-agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>generate-report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Binary file removed src/.DS_Store
Binary file not shown.
Binary file removed src/main/.DS_Store
Binary file not shown.
18 changes: 14 additions & 4 deletions src/main/java/io/jenkins/plugins/reporter/model/Item.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -72,8 +73,11 @@
return result;
}

return getItems()
.stream()
if (items == null || items.isEmpty()) {

Check warning on line 76 in src/main/java/io/jenkins/plugins/reporter/model/Item.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 76 is only partially covered, 3 branches are missing
return new LinkedHashMap<>();
}

return items.stream()

Check warning on line 80 in src/main/java/io/jenkins/plugins/reporter/model/Item.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 80 is not covered by tests
.map(Item::getResult)
.flatMap(map -> map.entrySet().stream())
.collect(Collectors.groupingBy(Map.Entry::getKey, LinkedHashMap::new, Collectors.summingInt(Map.Entry::getValue)));
Expand Down Expand Up @@ -101,19 +105,25 @@
this.result = result;
}

public List<Item> getItems() {

Check notice

Code scanning / CodeQL

Exposing internal representation Note

getItems exposes the internal representation stored in field items. The value may be modified
after this call to getItems
.
if (items == null) {

Check warning on line 109 in src/main/java/io/jenkins/plugins/reporter/model/Item.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 109 is only partially covered, one branch is missing
items = new ArrayList<>();

Check warning on line 110 in src/main/java/io/jenkins/plugins/reporter/model/Item.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 110 is not covered by tests
}
return items;
}

public boolean hasItems() {
return !Objects.isNull(items) && !items.isEmpty();
return items != null && !items.isEmpty();

Check warning on line 116 in src/main/java/io/jenkins/plugins/reporter/model/Item.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 116 is only partially covered, one branch is missing
}

public void setItems(List<Item> items) {
this.items = items;
}

public void addItem(Item item) {
this.items.add(item);
if (items == null) {
items = new ArrayList<>();
}
items.add(item);

Check warning on line 127 in src/main/java/io/jenkins/plugins/reporter/model/Item.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 124-127 are not covered by tests
}
}
Original file line number Diff line number Diff line change
@@ -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<String> 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;

Check warning on line 62 in src/main/java/io/jenkins/plugins/reporter/provider/AbstractExcelProvider.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 62 is only partially covered, one branch is missing

int nonEmptyConsecutiveCells = 0;
int firstNonEmptyCellIndex = -1;

// Check the cells in the row
for (int colIndex = 0; colIndex < 100; colIndex++) { // Arbitrary limit of 100 columns

Check warning on line 68 in src/main/java/io/jenkins/plugins/reporter/provider/AbstractExcelProvider.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 68 is only partially covered, one branch is missing
Cell cell = row.getCell(colIndex, Row.MissingCellPolicy.RETURN_BLANK_AS_NULL);
if (cell != null) {
if (firstNonEmptyCellIndex == -1) {
firstNonEmptyCellIndex = colIndex;
}
nonEmptyConsecutiveCells++;
} else if (firstNonEmptyCellIndex != -1) {

Check warning on line 75 in src/main/java/io/jenkins/plugins/reporter/provider/AbstractExcelProvider.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 75 is only partially covered, one branch is missing
// 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) {

Check warning on line 82 in src/main/java/io/jenkins/plugins/reporter/provider/AbstractExcelProvider.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 82 is only partially covered, one branch is missing
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;

Check warning on line 111 in src/main/java/io/jenkins/plugins/reporter/provider/AbstractExcelProvider.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 103-111 are not covered by tests
}

/** Extracts data from an Excel sheet */
protected TabularData extractSheetData(Sheet sheet, List<String> 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<String> 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) {

Check warning on line 133 in src/main/java/io/jenkins/plugins/reporter/provider/AbstractExcelProvider.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 133 is only partially covered, one branch is missing
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) {

Check warning on line 142 in src/main/java/io/jenkins/plugins/reporter/provider/AbstractExcelProvider.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 142 is only partially covered, one branch is missing
parserMessages.add(String.format("Skipped sheet '%s' - Header has less than 2 columns", sheet.getSheetName()));
return null;

Check warning on line 144 in src/main/java/io/jenkins/plugins/reporter/provider/AbstractExcelProvider.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 143-144 are not covered by tests
}

// 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<List<String>> 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) {

Check warning on line 161 in src/main/java/io/jenkins/plugins/reporter/provider/AbstractExcelProvider.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 161 is only partially covered, one branch is missing
continue;

Check warning on line 162 in src/main/java/io/jenkins/plugins/reporter/provider/AbstractExcelProvider.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 162 is not covered by tests
}

List<String> 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()) {

Check warning on line 173 in src/main/java/io/jenkins/plugins/reporter/provider/AbstractExcelProvider.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 173 is only partially covered, 2 branches are missing
hasData = true;
}
}

// Only add rows that have at least one non-empty cell
if (hasData) {

Check warning on line 179 in src/main/java/io/jenkins/plugins/reporter/provider/AbstractExcelProvider.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 179 is only partially covered, one branch is missing
rows.add(rowData);
}
}

// Only return data if we found any rows
return rows.isEmpty() ? null : new TabularData(id, header, rows);

Check warning on line 185 in src/main/java/io/jenkins/plugins/reporter/provider/AbstractExcelProvider.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 185 is only partially covered, one branch is missing
}

/** Converts an Excel cell value to a string */
protected String getCellValueAsString(Cell cell) {
if (cell == null) {

Check warning on line 190 in src/main/java/io/jenkins/plugins/reporter/provider/AbstractExcelProvider.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 190 is only partially covered, one branch is missing
return "";

Check warning on line 191 in src/main/java/io/jenkins/plugins/reporter/provider/AbstractExcelProvider.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 191 is not covered by tests
}

switch (cell.getCellType()) {

Check warning on line 194 in src/main/java/io/jenkins/plugins/reporter/provider/AbstractExcelProvider.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 194 is only partially covered, 3 branches are missing
case STRING:
return cell.getStringCellValue();
case NUMERIC:
if (DateUtil.isCellDateFormatted(cell)) {

Check warning on line 198 in src/main/java/io/jenkins/plugins/reporter/provider/AbstractExcelProvider.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 198 is only partially covered, one branch is missing
return cell.getDateCellValue().toString();

Check warning on line 199 in src/main/java/io/jenkins/plugins/reporter/provider/AbstractExcelProvider.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 199 is not covered by tests
} else {
// To avoid scientific notation display
double value = cell.getNumericCellValue();
if (value == Math.floor(value)) {

Check warning on line 203 in src/main/java/io/jenkins/plugins/reporter/provider/AbstractExcelProvider.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 203 is only partially covered, one branch is missing
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 "";

Check warning on line 222 in src/main/java/io/jenkins/plugins/reporter/provider/AbstractExcelProvider.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 206-222 are not covered by tests
}
}

/** 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;
}
}
}
}

Loading
Loading