Skip to content
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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ Create an instance of a store which implements the [SPDX Java Library Storage In

Create an instance of `SpreadsheetStore(IModelStore baseStore, SpreadsheetFormatType spreadsheetFormat)` passing in the instance of a store created above along with the format. The format is one of the following:

- `ODS` - OpenDocument Spreadsheet format
- `XLS` - Microsoft Excel 97 to 2003 Workbook format
- `XLSX` - Microsoft Excel workbook format
- `XLSX` - Microsoft Excel Workbook format

## Serializing and Deserializing

Expand Down
5 changes: 5 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,11 @@
<artifactId>poi-ooxml</artifactId>
<version>5.4.1</version>
</dependency>
<dependency>
<groupId>com.github.miachm.sods</groupId>
<artifactId>SODS</artifactId>
<version>1.10.1</version>
</dependency>
</dependencies>
<build>
<testResources>
Expand Down
36 changes: 35 additions & 1 deletion src/main/java/org/spdx/spreadsheetstore/SpdxSpreadsheet.java
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,33 @@ public class SpdxSpreadsheet {

private ModelCopyManager copyManager;

// Detects if the input stream contains an OpenDocument Spreadsheet file.
private static boolean isOdsStream(InputStream stream) {
try {
stream.mark(512);
byte[] header = new byte[256];
int bytesRead = 0;
while (bytesRead < header.length) {
int r = stream.read(header, bytesRead, header.length - bytesRead);
if (r < 0) {
break;
}
bytesRead += r;
}
stream.reset();
if (bytesRead < 4) {
return false;
}
if (header[0] != 0x50 || header[1] != 0x4B || header[2] != 0x03 || header[3] != 0x04) {
return false;
}
String s = new String(header, 0, bytesRead, java.nio.charset.StandardCharsets.US_ASCII);
return s.contains("application/vnd.oasis.opendocument.spreadsheet");
} catch (IOException e) {
return false;
}
}

/**
* Open an existing SPDX spreadsheet from an input stream
* @param stream
Expand All @@ -103,8 +130,13 @@ public SpdxSpreadsheet(InputStream stream, IModelStore modelStore, ModelCopyMana
Objects.requireNonNull(copyManager, "Missing required model copy manager");
this.modelStore = modelStore;
this.copyManager = copyManager;
InputStream bis = stream.markSupported() ? stream : new java.io.BufferedInputStream(stream);
try {
workbook = WorkbookFactory.create(stream);
if (isOdsStream(bis)) {
workbook = new org.spdx.spreadsheetstore.ods.OdsWorkbook(bis);
} else {
workbook = WorkbookFactory.create(bis);
}
} catch (EncryptedDocumentException e) {
logger.error("Unable to read encrypted SPDX Spreadsheet", e);
throw new SpreadsheetException("Unable to read encrypted SPDX Spreadsheet", e);
Expand Down Expand Up @@ -156,6 +188,8 @@ public SpdxSpreadsheet(IModelStore modelStore, ModelCopyManager copyManager, Str
workbook = new XSSFWorkbook();
} else if (SpreadsheetFormatType.XLS.equals(spreadsheetFormat)) {
workbook = new HSSFWorkbook();
} else if (SpreadsheetFormatType.ODS.equals(spreadsheetFormat)) {
workbook = new org.spdx.spreadsheetstore.ods.OdsWorkbook();
} else {
throw new SpreadsheetException("Unsupported spreadsheet format: "+spreadsheetFormat);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ public class SpreadsheetStore extends ExtendedSpdxStore implements ISerializable
/**
* Enum for the spreadsheet format type
*/
public enum SpreadsheetFormatType {XLS, XLSX};
public enum SpreadsheetFormatType {ODS, XLS, XLSX};

private SpreadsheetFormatType spreadsheetFormat;

Expand Down
248 changes: 248 additions & 0 deletions src/main/java/org/spdx/spreadsheetstore/ods/OdsCell.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
/*
* SPDX-FileContributor: Arthit Suriyawongkul
* SPDX-FileCopyrightText: 2026 SPDX Contributors
* SPDX-FileType: SOURCE
* SPDX-License-Identifier: Apache-2.0
*/
package org.spdx.spreadsheetstore.ods;

import java.util.Date;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellAddress;
import org.apache.poi.ss.util.CellRangeAddress;

/**
* Adapter for Apache POI {@link Cell} over the SODS {@link com.github.miachm.sods.Range}.
* Provides mechanisms to read and write cell data, respecting SODS limitations.
*/
public class OdsCell implements Cell {
private final OdsRow row;
private final int columnIndex;
private final com.github.miachm.sods.Range range;
private CellStyle cellStyle;

public OdsCell(OdsRow row, int columnIndex, com.github.miachm.sods.Range range) {
this.row = row;
this.columnIndex = columnIndex;
this.range = range;
}

public com.github.miachm.sods.Range getSodsRange() {
return range;
}

@Override
public void setCellValue(String value) {
range.setValue(value);
}

@Override
public void setCellValue(double value) {
range.setValue(value);
}

@Override
public void setCellValue(Date value) {
if (value == null) {
range.setValue(null);
} else {
java.time.LocalDateTime ldt = java.time.LocalDateTime.ofInstant(value.toInstant(), java.time.ZoneId.systemDefault());
range.setValue(ldt);
}
}

@Override
public void setCellType(CellType cellType) {
}

@Override
public String getStringCellValue() {
Object val = range.getValue();
return val == null ? "" : val.toString();
}

@Override
public double getNumericCellValue() {
Object val = range.getValue();
if (val instanceof Number) {
return ((Number) val).doubleValue();
}
return 0.0;
}

@Override
public Date getDateCellValue() {
Object value = range.getValue();
if (value == null) return null;
if (value instanceof java.time.LocalDateTime) {
java.time.LocalDateTime ldt = (java.time.LocalDateTime) value;
return java.util.Date.from(ldt.atZone(java.time.ZoneId.systemDefault()).toInstant());
}
if (value instanceof java.time.LocalDate) {
java.time.LocalDate ld = (java.time.LocalDate) value;
return java.util.Date.from(ld.atStartOfDay(java.time.ZoneId.systemDefault()).toInstant());
}
if (value instanceof java.util.Date) {
return (Date) value;
}
if (value instanceof Number) {
return org.apache.poi.ss.usermodel.DateUtil.getJavaDate(((Number) value).doubleValue());
}
return null;
}

@Override
public CellType getCellType() {
Object val = range.getValue();
if (val == null) return CellType.BLANK;
if (val instanceof String) return CellType.STRING;
if (val instanceof Number) return CellType.NUMERIC;
if (val instanceof Boolean) return CellType.BOOLEAN;
if (val instanceof java.time.LocalDateTime || val instanceof java.time.LocalDate || val instanceof java.util.Date) {
return CellType.NUMERIC;
}
return CellType.STRING;
}

@Override
public void setCellStyle(CellStyle style) {
this.cellStyle = style;
if (style instanceof OdsCellStyle) {
range.setStyle(((OdsCellStyle) style).getSodsStyle());
}
}

@Override
public CellStyle getCellStyle() {
return cellStyle;
}

@Override
public int getColumnIndex() {
return columnIndex;
}

@Override
public int getRowIndex() {
return row.getRowNum();
}

@Override
public Sheet getSheet() {
return row.getSheet();
}

@Override
public Row getRow() {
return row;
}

@Override
public void setBlank() {
range.setValue(null);
}

@Override
public void setCellFormula(String formula) {
}

@Override
public String getCellFormula() {
return "";
}

@Override
public boolean getBooleanCellValue() {
Object val = range.getValue();
if (val instanceof Boolean) {
return (Boolean) val;
}
return false;
}

@Override
public byte getErrorCellValue() {
return 0;
}

@Override
public void setCellErrorValue(byte value) {}
@Override
public void setAsActiveCell() {}
@Override
public CellAddress getAddress() {
return new CellAddress(getRowIndex(), getColumnIndex());
}
@Override
public void setCellValue(java.util.Calendar value) {
if (value != null) {
setCellValue(value.getTime());
}
}
@Override
public void setCellValue(RichTextString value) {
if (value != null) {
setCellValue(value.getString());
}
}
@Override
public void setCellValue(boolean value) {
range.setValue(value);
}
@Override
public RichTextString getRichStringCellValue() {
return new org.apache.poi.xssf.usermodel.XSSFRichTextString(getStringCellValue());
}
@Override
public void removeCellComment() {}
@Override
public Comment getCellComment() { return null; }
@Override
public void setCellComment(Comment comment) {}
@Override
public Hyperlink getHyperlink() { return null; }
@Override
public void setHyperlink(Hyperlink hyperlink) {}
@Override
public CellRangeAddress getArrayFormulaRange() { return null; }
@Override
public boolean isPartOfArrayFormulaGroup() { return false; }

@Override
public void removeHyperlink() {}

@Override
public java.time.LocalDateTime getLocalDateTimeCellValue() {
Object value = range.getValue();
if (value instanceof java.time.LocalDateTime) {
return (java.time.LocalDateTime) value;
}
if (value instanceof java.time.LocalDate) {
return ((java.time.LocalDate) value).atStartOfDay();
}
if (value instanceof Date) {
return java.time.LocalDateTime.ofInstant(((Date) value).toInstant(), java.time.ZoneId.systemDefault());
}
if (value instanceof Number) {
Date date = org.apache.poi.ss.usermodel.DateUtil.getJavaDate(((Number) value).doubleValue());
return java.time.LocalDateTime.ofInstant(date.toInstant(), java.time.ZoneId.systemDefault());
}
return null;
}

@Override
public void setCellValue(java.time.LocalDateTime value) {
range.setValue(value);
}

@Override
public void setCellValue(java.time.LocalDate value) {
range.setValue(value);
}

@Override
public void removeFormula() {}

@Override
public CellType getCachedFormulaResultType() { return CellType.BLANK; }
}
Loading
Loading