From 579fabc6c10694c5585b9dd547ce113fabbe12ff Mon Sep 17 00:00:00 2001
From: Arthit Suriyawongkul
Date: Fri, 17 Jul 2026 12:04:00 +0100
Subject: [PATCH 1/5] Add OpenDocument Spreadsheet (ODS) support
Using SODS library https://github.com/miachm/SODS
Signed-off-by: Arthit Suriyawongkul
---
README.md | 3 +-
pom.xml | 5 +
.../spreadsheetstore/SpdxSpreadsheet.java | 36 +-
.../spreadsheetstore/SpreadsheetStore.java | 2 +-
.../spdx/spreadsheetstore/ods/OdsCell.java | 248 +++++++++++
.../spreadsheetstore/ods/OdsCellStyle.java | 354 +++++++++++++++
.../ods/OdsCreationHelper.java | 66 +++
.../spreadsheetstore/ods/OdsDataFormat.java | 44 ++
.../spdx/spreadsheetstore/ods/OdsFont.java | 143 ++++++
.../org/spdx/spreadsheetstore/ods/OdsRow.java | 175 ++++++++
.../spdx/spreadsheetstore/ods/OdsSheet.java | 409 ++++++++++++++++++
.../spreadsheetstore/ods/OdsWorkbook.java | 354 +++++++++++++++
.../spreadsheetstore/ods/package-info.java | 32 ++
.../SpreadsheetStoreTest.java | 99 +++++
14 files changed, 1967 insertions(+), 3 deletions(-)
create mode 100644 src/main/java/org/spdx/spreadsheetstore/ods/OdsCell.java
create mode 100644 src/main/java/org/spdx/spreadsheetstore/ods/OdsCellStyle.java
create mode 100644 src/main/java/org/spdx/spreadsheetstore/ods/OdsCreationHelper.java
create mode 100644 src/main/java/org/spdx/spreadsheetstore/ods/OdsDataFormat.java
create mode 100644 src/main/java/org/spdx/spreadsheetstore/ods/OdsFont.java
create mode 100644 src/main/java/org/spdx/spreadsheetstore/ods/OdsRow.java
create mode 100644 src/main/java/org/spdx/spreadsheetstore/ods/OdsSheet.java
create mode 100644 src/main/java/org/spdx/spreadsheetstore/ods/OdsWorkbook.java
create mode 100644 src/main/java/org/spdx/spreadsheetstore/ods/package-info.java
diff --git a/README.md b/README.md
index e10ec6d..5eee350 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/pom.xml b/pom.xml
index cb6c364..82af6e8 100644
--- a/pom.xml
+++ b/pom.xml
@@ -161,6 +161,11 @@
poi-ooxml
5.4.1
+
+ com.github.miachm.sods
+ SODS
+ 1.10.1
+
diff --git a/src/main/java/org/spdx/spreadsheetstore/SpdxSpreadsheet.java b/src/main/java/org/spdx/spreadsheetstore/SpdxSpreadsheet.java
index a5436c2..685a73f 100644
--- a/src/main/java/org/spdx/spreadsheetstore/SpdxSpreadsheet.java
+++ b/src/main/java/org/spdx/spreadsheetstore/SpdxSpreadsheet.java
@@ -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
@@ -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);
@@ -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);
}
diff --git a/src/main/java/org/spdx/spreadsheetstore/SpreadsheetStore.java b/src/main/java/org/spdx/spreadsheetstore/SpreadsheetStore.java
index cbc8cf4..ceeafc5 100644
--- a/src/main/java/org/spdx/spreadsheetstore/SpreadsheetStore.java
+++ b/src/main/java/org/spdx/spreadsheetstore/SpreadsheetStore.java
@@ -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;
diff --git a/src/main/java/org/spdx/spreadsheetstore/ods/OdsCell.java b/src/main/java/org/spdx/spreadsheetstore/ods/OdsCell.java
new file mode 100644
index 0000000..dc67fc3
--- /dev/null
+++ b/src/main/java/org/spdx/spreadsheetstore/ods/OdsCell.java
@@ -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; }
+}
diff --git a/src/main/java/org/spdx/spreadsheetstore/ods/OdsCellStyle.java b/src/main/java/org/spdx/spreadsheetstore/ods/OdsCellStyle.java
new file mode 100644
index 0000000..6009ce2
--- /dev/null
+++ b/src/main/java/org/spdx/spreadsheetstore/ods/OdsCellStyle.java
@@ -0,0 +1,354 @@
+/*
+ * SPDX-FileContributor: Arthit Suriyawongkul
+ * SPDX-FileCopyrightText: 2026 SPDX Contributors
+ * SPDX-FileType: SOURCE
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package org.spdx.spreadsheetstore.ods;
+
+import org.apache.poi.ss.usermodel.*;
+
+/**
+ * Adapter for Apache POI {@link CellStyle} over the SODS {@link com.github.miachm.sods.Style}.
+ * Manages borders, fonts, alignments, and backgrounds mapping for ODS cells.
+ */
+public class OdsCellStyle implements CellStyle {
+
+ private final com.github.miachm.sods.Style style = new com.github.miachm.sods.Style();
+ private boolean wrapText = false;
+ private int fontIndex = -1;
+ private short fillForegroundColor = 0;
+ private FillPatternType fillPattern = FillPatternType.NO_FILL;
+ private HorizontalAlignment horizontalAlignment = HorizontalAlignment.GENERAL;
+ private VerticalAlignment verticalAlignment = VerticalAlignment.BOTTOM;
+ private short dataFormat = 0;
+ private short index = 0;
+
+ private BorderStyle borderBottom = BorderStyle.NONE;
+ private BorderStyle borderLeft = BorderStyle.NONE;
+ private BorderStyle borderRight = BorderStyle.NONE;
+ private BorderStyle borderTop = BorderStyle.NONE;
+
+ private final OdsWorkbook workbook;
+
+ public OdsCellStyle(OdsWorkbook workbook) {
+ this.workbook = workbook;
+ }
+
+ public com.github.miachm.sods.Style getSodsStyle() {
+ return style;
+ }
+
+ @Override
+ public void setAlignment(HorizontalAlignment align) {
+ this.horizontalAlignment = align;
+ if (align == HorizontalAlignment.CENTER || align == HorizontalAlignment.CENTER_SELECTION) {
+ style.setTextAligment(com.github.miachm.sods.Style.TEXT_ALIGMENT.Center);
+ } else if (align == HorizontalAlignment.RIGHT) {
+ style.setTextAligment(com.github.miachm.sods.Style.TEXT_ALIGMENT.Right);
+ } else if (align == HorizontalAlignment.LEFT) {
+ style.setTextAligment(com.github.miachm.sods.Style.TEXT_ALIGMENT.Left);
+ }
+ }
+
+ @Override
+ public HorizontalAlignment getAlignment() {
+ return horizontalAlignment;
+ }
+
+ @Override
+ public void setVerticalAlignment(VerticalAlignment align) {
+ this.verticalAlignment = align;
+ if (align == VerticalAlignment.TOP) {
+ style.setVerticalTextAligment(com.github.miachm.sods.Style.VERTICAL_TEXT_ALIGMENT.Top);
+ } else if (align == VerticalAlignment.CENTER) {
+ style.setVerticalTextAligment(com.github.miachm.sods.Style.VERTICAL_TEXT_ALIGMENT.Middle);
+ } else if (align == VerticalAlignment.BOTTOM) {
+ style.setVerticalTextAligment(com.github.miachm.sods.Style.VERTICAL_TEXT_ALIGMENT.Bottom);
+ }
+ }
+
+ @Override
+ public VerticalAlignment getVerticalAlignment() {
+ return verticalAlignment;
+ }
+
+ @Override
+ public void setBorderBottom(BorderStyle border) {
+ this.borderBottom = border;
+ updateBorders();
+ }
+
+ @Override
+ public BorderStyle getBorderBottom() {
+ return borderBottom;
+ }
+
+ @Override
+ public void setBorderLeft(BorderStyle border) {
+ this.borderLeft = border;
+ updateBorders();
+ }
+
+ @Override
+ public BorderStyle getBorderLeft() {
+ return borderLeft;
+ }
+
+ @Override
+ public void setBorderRight(BorderStyle border) {
+ this.borderRight = border;
+ updateBorders();
+ }
+
+ @Override
+ public BorderStyle getBorderRight() {
+ return borderRight;
+ }
+
+ @Override
+ public void setBorderTop(BorderStyle border) {
+ this.borderTop = border;
+ updateBorders();
+ }
+
+ @Override
+ public BorderStyle getBorderTop() {
+ return borderTop;
+ }
+
+ private void updateBorders() {
+ com.github.miachm.sods.Borders borders = new com.github.miachm.sods.Borders();
+
+ boolean hasBottom = borderBottom != BorderStyle.NONE;
+ borders.setBorderBottom(hasBottom);
+ if (hasBottom) {
+ borders.setBorderBottomProperties(getBorderProperties(borderBottom));
+ }
+
+ boolean hasLeft = borderLeft != BorderStyle.NONE;
+ borders.setBorderLeft(hasLeft);
+ if (hasLeft) {
+ borders.setBorderLeftProperties(getBorderProperties(borderLeft));
+ }
+
+ boolean hasRight = borderRight != BorderStyle.NONE;
+ borders.setBorderRight(hasRight);
+ if (hasRight) {
+ borders.setBorderRightProperties(getBorderProperties(borderRight));
+ }
+
+ boolean hasTop = borderTop != BorderStyle.NONE;
+ borders.setBorderTop(hasTop);
+ if (hasTop) {
+ borders.setBorderTopProperties(getBorderProperties(borderTop));
+ }
+
+ style.setBorders(borders);
+ }
+
+ private String getBorderProperties(BorderStyle borderStyle) {
+ if (borderStyle == BorderStyle.NONE) {
+ return null;
+ }
+ switch (borderStyle) {
+ case THICK:
+ return "0.07cm solid #000000";
+ case MEDIUM:
+ return "0.05cm solid #000000";
+ case DOUBLE:
+ return "0.05cm double #000000";
+ case DASHED:
+ case MEDIUM_DASHED:
+ return "0.035cm dashed #000000";
+ case DOTTED:
+ return "0.035cm dotted #000000";
+ default:
+ return "0.035cm solid #000000";
+ }
+ }
+
+ @Override
+ public void setFillForegroundColor(short bg) {
+ this.fillForegroundColor = bg;
+ com.github.miachm.sods.Color color = getSodsColor(bg);
+ if (color != null) {
+ style.setBackgroundColor(color);
+ }
+ }
+
+ private com.github.miachm.sods.Color getSodsColor(short indexedColor) {
+ // Map IndexedColors to standard RGB
+ if (indexedColor == 42 || indexedColor == 57) { // LIGHT_GREEN
+ return new com.github.miachm.sods.Color(204, 255, 204);
+ }
+ if (indexedColor == 43 || indexedColor == 34) { // LIGHT_YELLOW
+ return new com.github.miachm.sods.Color(255, 255, 153);
+ }
+ if (indexedColor == 10) { // RED
+ return new com.github.miachm.sods.Color(255, 199, 206);
+ }
+ if (indexedColor == 22) { // GREY_25_PERCENT
+ return new com.github.miachm.sods.Color(224, 224, 224);
+ }
+ return null;
+ }
+
+ @Override
+ public short getFillForegroundColor() {
+ return fillForegroundColor;
+ }
+
+ @Override
+ public void setFillPattern(FillPatternType fp) {
+ this.fillPattern = fp;
+ }
+
+ @Override
+ public FillPatternType getFillPattern() {
+ return fillPattern;
+ }
+
+ @Override
+ public void setFont(Font font) {
+ if (font instanceof OdsFont) {
+ OdsFont odsFont = (OdsFont) font;
+ this.fontIndex = odsFont.getIndex();
+ style.setFontFamily(odsFont.getFontName());
+ style.setFontSize((int) odsFont.getFontHeightInPoints());
+ style.setBold(odsFont.getBold());
+ style.setItalic(odsFont.getItalic());
+ style.setUnderline(odsFont.getUnderline() != org.apache.poi.ss.usermodel.Font.U_NONE);
+ style.setLineThrough(odsFont.getStrikeout());
+ // Map font color if we want
+ short fontColorIdx = odsFont.getColor();
+ com.github.miachm.sods.Color fontColor = getSodsColor(fontColorIdx);
+ if (fontColor != null) {
+ style.setFontColor(fontColor);
+ }
+ }
+ }
+
+ /**
+ * Sets the data format for the cell.
+ *
+ * Due to limitations in the underlying SODS library, exact POI data format patterns
+ * are not fully supported. If the provided format index corresponds to a recognized
+ * Date format, it will be mapped to the ISO date style ("YYYY-MM-DD") to ensure
+ * dates are properly displayed. Other formats are currently stored but may not
+ * affect the final ODS cell rendering.
+ *
+ *
+ * @param fmt the data format index
+ */
+ @Override
+ public void setDataFormat(short fmt) {
+ this.dataFormat = fmt;
+ if (workbook != null) {
+ String formatStr = workbook.createDataFormat().getFormat(fmt);
+ if (DateUtil.isADateFormat(fmt, formatStr)) {
+ style.setDataStyle("YYYY-MM-DD");
+ }
+ }
+ }
+
+ @Override
+ public short getDataFormat() {
+ return dataFormat;
+ }
+
+ @Override
+ public void setWrapText(boolean wrapped) {
+ this.wrapText = wrapped;
+ style.setWrap(wrapped);
+ }
+
+ @Override
+ public boolean getWrapText() {
+ return wrapText;
+ }
+
+ @Override
+ public int getFontIndex() {
+ return fontIndex;
+ }
+
+ @Override
+ public int getFontIndexAsInt() {
+ return fontIndex;
+ }
+
+ @Override
+ public short getIndex() {
+ return index;
+ }
+
+ @Override
+ public void cloneStyleFrom(CellStyle source) {}
+ @Override
+ public void setBottomBorderColor(short color) {}
+ @Override
+ public short getBottomBorderColor() { return 0; }
+ @Override
+ public void setLeftBorderColor(short color) {}
+ @Override
+ public short getLeftBorderColor() { return 0; }
+ @Override
+ public void setRightBorderColor(short color) {}
+ @Override
+ public short getRightBorderColor() { return 0; }
+ @Override
+ public void setTopBorderColor(short color) {}
+ @Override
+ public short getTopBorderColor() { return 0; }
+ @Override
+ public void setFillBackgroundColor(short bg) {}
+ @Override
+ public short getFillBackgroundColor() { return 0; }
+ @Override
+ public void setHidden(boolean hidden) {}
+ @Override
+ public boolean getHidden() { return false; }
+ @Override
+ public void setLocked(boolean locked) {}
+ @Override
+ public boolean getLocked() { return false; }
+ @Override
+ public void setQuotePrefixed(boolean quotePrefix) {}
+ @Override
+ public boolean getQuotePrefixed() { return false; }
+ @Override
+ public void setIndention(short indent) {}
+ @Override
+ public short getIndention() { return 0; }
+ @Override
+ public void setRotation(short rotation) {}
+ @Override
+ public short getRotation() { return 0; }
+ @Override
+ public String getDataFormatString() { return ""; }
+ @Override
+ public void setShrinkToFit(boolean shrinkToFit) {}
+ @Override
+ public boolean getShrinkToFit() { return false; }
+
+ @Override
+ public void invalidateCachedProperties() {}
+
+ @Override
+ public java.util.EnumMap getFormatProperties() {
+ return new java.util.EnumMap<>(org.apache.poi.ss.usermodel.CellPropertyType.class);
+ }
+
+ @Override
+ public Color getFillForegroundColorColor() { return null; }
+
+ @Override
+ public Color getFillBackgroundColorColor() { return null; }
+
+ @Override
+ public void setFillForegroundColor(Color color) {}
+
+ @Override
+ public void setFillBackgroundColor(Color color) {}
+}
diff --git a/src/main/java/org/spdx/spreadsheetstore/ods/OdsCreationHelper.java b/src/main/java/org/spdx/spreadsheetstore/ods/OdsCreationHelper.java
new file mode 100644
index 0000000..20bcd39
--- /dev/null
+++ b/src/main/java/org/spdx/spreadsheetstore/ods/OdsCreationHelper.java
@@ -0,0 +1,66 @@
+/*
+ * SPDX-FileContributor: Arthit Suriyawongkul
+ * SPDX-FileCopyrightText: 2026 SPDX Contributors
+ * SPDX-FileType: SOURCE
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package org.spdx.spreadsheetstore.ods;
+
+import org.apache.poi.common.usermodel.HyperlinkType;
+import org.apache.poi.ss.usermodel.*;
+import org.apache.poi.ss.util.AreaReference;
+import org.apache.poi.ss.util.CellReference;
+
+/**
+ * Adapter for Apache POI {@link CreationHelper} for ODS documents.
+ * Provides factories for formula evaluators, formats, and hyperlinks.
+ */
+public class OdsCreationHelper implements CreationHelper {
+
+ private final OdsWorkbook workbook;
+ private final OdsDataFormat dataFormat = new OdsDataFormat();
+
+ public OdsCreationHelper(OdsWorkbook workbook) {
+ this.workbook = workbook;
+ }
+
+ @Override
+ public FormulaEvaluator createFormulaEvaluator() {
+ return null;
+ }
+
+ @Override
+ public OdsDataFormat createDataFormat() {
+ return dataFormat;
+ }
+
+ @Override
+ public Hyperlink createHyperlink(HyperlinkType type) {
+ return null;
+ }
+
+ @Override
+ public ExtendedColor createExtendedColor() {
+ return null;
+ }
+
+ @Override
+ public ClientAnchor createClientAnchor() {
+ return null;
+ }
+
+ @Override
+ public AreaReference createAreaReference(String reference) {
+ return new AreaReference(reference, workbook.getSpreadsheetVersion());
+ }
+
+ @Override
+ public AreaReference createAreaReference(CellReference topLeft, CellReference bottomRight) {
+ return new AreaReference(topLeft, bottomRight, workbook.getSpreadsheetVersion());
+ }
+
+ @Override
+ public RichTextString createRichTextString(String text) {
+ return new org.apache.poi.xssf.usermodel.XSSFRichTextString(text);
+ }
+}
diff --git a/src/main/java/org/spdx/spreadsheetstore/ods/OdsDataFormat.java b/src/main/java/org/spdx/spreadsheetstore/ods/OdsDataFormat.java
new file mode 100644
index 0000000..d902cd3
--- /dev/null
+++ b/src/main/java/org/spdx/spreadsheetstore/ods/OdsDataFormat.java
@@ -0,0 +1,44 @@
+/*
+ * SPDX-FileContributor: Arthit Suriyawongkul
+ * SPDX-FileCopyrightText: 2026 SPDX Contributors
+ * SPDX-FileType: SOURCE
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package org.spdx.spreadsheetstore.ods;
+
+import org.apache.poi.ss.usermodel.DataFormat;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Adapter for Apache POI {@link DataFormat} for ODS documents.
+ *
+ * Note: The SODS library has limited support for custom data formats.
+ * It natively only supports plain text ("@") and an ISO date format
+ * ("YYYY-MM-DD"). As a result, exact POI format patterns are not matched in
+ * the ODS output. Any date formats will map to "YYYY-MM-DD" internally,
+ * while other formats remain unstyled.
+ *
+ */
+public class OdsDataFormat implements DataFormat {
+
+ private final List formats = new ArrayList<>();
+
+ @Override
+ public short getFormat(String format) {
+ int index = formats.indexOf(format);
+ if (index == -1) {
+ index = formats.size();
+ formats.add(format);
+ }
+ return (short) index;
+ }
+
+ @Override
+ public String getFormat(short index) {
+ if (index >= 0 && index < formats.size()) {
+ return formats.get(index);
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/org/spdx/spreadsheetstore/ods/OdsFont.java b/src/main/java/org/spdx/spreadsheetstore/ods/OdsFont.java
new file mode 100644
index 0000000..f55e24f
--- /dev/null
+++ b/src/main/java/org/spdx/spreadsheetstore/ods/OdsFont.java
@@ -0,0 +1,143 @@
+/*
+ * SPDX-FileContributor: Arthit Suriyawongkul
+ * SPDX-FileCopyrightText: 2026 SPDX Contributors
+ * SPDX-FileType: SOURCE
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package org.spdx.spreadsheetstore.ods;
+
+import org.apache.poi.ss.usermodel.Font;
+
+/**
+ * Adapter for Apache POI {@link Font} over SODS styles.
+ * Holds font configurations (size, bold, italic, underline, strikeout) for an ODS cell.
+ */
+public class OdsFont implements Font {
+
+ private final short index;
+ private String name = "Arial";
+ private short height = 200;
+ private boolean bold = false;
+ private short color = 0;
+ private boolean italic = false;
+ private boolean strikeout = false;
+ private short typeOffset = 0;
+ private byte underline = 0;
+
+ public OdsFont(short index) {
+ this.index = index;
+ }
+
+ @Override
+ public void setFontName(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public String getFontName() {
+ return this.name;
+ }
+
+ @Override
+ public void setFontHeight(short height) {
+ this.height = height;
+ }
+
+ @Override
+ public void setFontHeightInPoints(short height) {
+ this.height = (short) (height * 20);
+ }
+
+ @Override
+ public short getFontHeight() {
+ return this.height;
+ }
+
+ @Override
+ public short getFontHeightInPoints() {
+ return (short) (this.height / 20);
+ }
+
+ @Override
+ public void setItalic(boolean italic) {
+ this.italic = italic;
+ }
+
+ @Override
+ public boolean getItalic() {
+ return this.italic;
+ }
+
+ @Override
+ public void setStrikeout(boolean strikeout) {
+ this.strikeout = strikeout;
+ }
+
+ @Override
+ public boolean getStrikeout() {
+ return this.strikeout;
+ }
+
+ @Override
+ public void setColor(short color) {
+ this.color = color;
+ }
+
+ @Override
+ public short getColor() {
+ return this.color;
+ }
+
+ @Override
+ public void setTypeOffset(short offset) {
+ this.typeOffset = offset;
+ }
+
+ @Override
+ public short getTypeOffset() {
+ return this.typeOffset;
+ }
+
+ @Override
+ public void setUnderline(byte underline) {
+ this.underline = underline;
+ }
+
+ @Override
+ public byte getUnderline() {
+ return this.underline;
+ }
+
+ @Override
+ public int getCharSet() {
+ return 0;
+ }
+
+ @Override
+ public void setCharSet(byte charset) {
+ }
+
+ @Override
+ public void setCharSet(int charset) {
+ }
+
+ @Override
+ public void setBold(boolean bold) {
+ this.bold = bold;
+ }
+
+ @Override
+ public boolean getBold() {
+ return this.bold;
+ }
+
+ @Override
+ public int getIndex() {
+ return this.index;
+ }
+
+ @Override
+ public int getIndexAsInt() {
+ return this.index;
+ }
+}
diff --git a/src/main/java/org/spdx/spreadsheetstore/ods/OdsRow.java b/src/main/java/org/spdx/spreadsheetstore/ods/OdsRow.java
new file mode 100644
index 0000000..3a0bcbb
--- /dev/null
+++ b/src/main/java/org/spdx/spreadsheetstore/ods/OdsRow.java
@@ -0,0 +1,175 @@
+/*
+ * 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.*;
+import org.apache.poi.ss.usermodel.*;
+
+/**
+ * Adapter for Apache POI {@link Row} managing a specific row in a SODS {@link com.github.miachm.sods.Sheet}.
+ */
+public class OdsRow implements Row {
+ private final OdsSheet sheet;
+ private final int rowNum;
+ private final Map cells = new TreeMap<>();
+
+ public OdsRow(OdsSheet sheet, int rowNum) {
+ this.sheet = sheet;
+ this.rowNum = rowNum;
+ }
+
+ public void clear() {
+ for (OdsCell cell : cells.values()) {
+ cell.getSodsRange().setValue(null);
+ }
+ cells.clear();
+ }
+
+ @Override
+ public Cell createCell(int column) {
+ return createCell(column, CellType.BLANK);
+ }
+
+ @Override
+ public Cell createCell(int column, CellType type) {
+ com.github.miachm.sods.Sheet sodsSheet = sheet.getSodsSheet();
+ int currentCols = sodsSheet.getMaxColumns();
+ if (column >= currentCols) {
+ for (int i = currentCols; i <= column; i++) {
+ sodsSheet.appendColumn();
+ }
+ }
+ com.github.miachm.sods.Range range = sodsSheet.getRange(rowNum, column);
+ OdsCell cell = new OdsCell(this, column, range);
+ cells.put(column, cell);
+ return cell;
+ }
+
+ @Override
+ public Cell getCell(int cellnum) {
+ // If it exists in cells map, return it
+ if (cells.containsKey(cellnum)) {
+ return cells.get(cellnum);
+ }
+ // If it exists in the underlying SODS sheet but not yet wrapped, wrap and return
+ com.github.miachm.sods.Sheet sodsSheet = sheet.getSodsSheet();
+ if (cellnum >= 0 && cellnum < sodsSheet.getMaxColumns()) {
+ com.github.miachm.sods.Range range = sodsSheet.getRange(rowNum, cellnum);
+ Object val = range.getValue();
+ if (val != null) {
+ if (val instanceof String && ((String) val).trim().isEmpty()) {
+ return null;
+ }
+ OdsCell cell = new OdsCell(this, cellnum, range);
+ cells.put(cellnum, cell);
+ return cell;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public short getFirstCellNum() {
+ com.github.miachm.sods.Sheet sodsSheet = sheet.getSodsSheet();
+ int maxCols = sodsSheet.getMaxColumns();
+ for (int col = 0; col < maxCols; col++) {
+ if (cells.containsKey(col)) {
+ return (short) col;
+ }
+ Object val = sodsSheet.getRange(rowNum, col).getValue();
+ if (val != null) {
+ return (short) col;
+ }
+ }
+ return -1;
+ }
+
+ @Override
+ public short getLastCellNum() {
+ com.github.miachm.sods.Sheet sodsSheet = sheet.getSodsSheet();
+ int maxCols = sodsSheet.getMaxColumns();
+ for (int col = maxCols - 1; col >= 0; col--) {
+ if (cells.containsKey(col)) {
+ return (short) (col + 1);
+ }
+ Object val = sodsSheet.getRange(rowNum, col).getValue();
+ if (val != null) {
+ return (short) (col + 1);
+ }
+ }
+ return -1;
+ }
+
+ @Override
+ public int getRowNum() {
+ return rowNum;
+ }
+
+ @Override
+ public void setHeight(short height) {}
+ @Override
+ public void setHeightInPoints(float height) {}
+ @Override
+ public short getHeight() { return 255; }
+ @Override
+ public float getHeightInPoints() { return 15.0f; }
+ @Override
+ public Sheet getSheet() {
+ return sheet;
+ }
+ @Override
+ public Iterator cellIterator() {
+ List list = new ArrayList<>(cells.values());
+ return list.iterator();
+ }
+ @Override
+ public Iterator iterator() {
+ return cellIterator();
+ }
+ @Override
+ public void removeCell(Cell cell) {
+ if (cell instanceof OdsCell) {
+ int col = cell.getColumnIndex();
+ ((OdsCell) cell).getSodsRange().setValue(null);
+ cells.remove(col);
+ }
+ }
+ @Override
+ public void setRowNum(int rowNum) {}
+ @Override
+ public int getPhysicalNumberOfCells() { return cells.size(); }
+ @Override
+ public boolean isFormatted() { return false; }
+ @Override
+ public CellStyle getRowStyle() { return null; }
+ @Override
+ public void setRowStyle(CellStyle style) {}
+
+ @Override
+ public void shiftCellsLeft(int firstShiftColumnIndex, int lastShiftColumnIndex, int step) {}
+
+ @Override
+ public void shiftCellsRight(int firstShiftColumnIndex, int lastShiftColumnIndex, int step) {}
+
+ @Override
+ public int getOutlineLevel() { return 0; }
+
+ @Override
+ public boolean getZeroHeight() { return false; }
+
+ @Override
+ public void setZeroHeight(boolean zHeight) {}
+
+ @Override
+ public Cell getCell(int cellnum, MissingCellPolicy policy) {
+ Cell cell = getCell(cellnum);
+ if (cell == null && policy == MissingCellPolicy.CREATE_NULL_AS_BLANK) {
+ return createCell(cellnum);
+ }
+ return cell;
+ }
+}
diff --git a/src/main/java/org/spdx/spreadsheetstore/ods/OdsSheet.java b/src/main/java/org/spdx/spreadsheetstore/ods/OdsSheet.java
new file mode 100644
index 0000000..49e4f12
--- /dev/null
+++ b/src/main/java/org/spdx/spreadsheetstore/ods/OdsSheet.java
@@ -0,0 +1,409 @@
+/*
+ * 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.*;
+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 Sheet} over a SODS {@link com.github.miachm.sods.Sheet}.
+ * Manages rows, columns, and sheet-level properties for an ODS document.
+ */
+public class OdsSheet implements Sheet {
+
+ private final OdsWorkbook workbook;
+ private final com.github.miachm.sods.Sheet sodsSheet;
+ private final NavigableMap rows = new TreeMap<>();
+
+ public OdsSheet(OdsWorkbook workbook, com.github.miachm.sods.Sheet sodsSheet) {
+ this.workbook = workbook;
+ this.sodsSheet = sodsSheet;
+ for (int r = 0; r < sodsSheet.getMaxRows(); r++) {
+ rows.put(r, new OdsRow(this, r));
+ }
+ }
+
+ public com.github.miachm.sods.Sheet getSodsSheet() {
+ return this.sodsSheet;
+ }
+
+ @Override
+ public Row createRow(int rownum) {
+ int currentRows = sodsSheet.getMaxRows();
+ if (rownum >= currentRows) {
+ for (int i = currentRows; i <= rownum; i++) {
+ sodsSheet.appendRow();
+ }
+ }
+ OdsRow row = new OdsRow(this, rownum);
+ rows.put(rownum, row);
+ return row;
+ }
+
+ @Override
+ public Row getRow(int rownum) {
+ return rows.get(rownum);
+ }
+
+ @Override
+ public void removeRow(Row row) {
+ if (row instanceof OdsRow) {
+ int rowNum = row.getRowNum();
+ OdsRow odsRow = (OdsRow) row;
+ odsRow.clear();
+ rows.remove(rowNum);
+ }
+ }
+
+ @Override
+ public int getFirstRowNum() {
+ return rows.isEmpty() ? 0 : rows.keySet().iterator().next();
+ }
+
+ @Override
+ public int getLastRowNum() {
+ return rows.isEmpty() ? 0 : rows.lastKey();
+ }
+
+ @Override
+ public void setColumnWidth(int columnIndex, int width) {
+ }
+
+ @Override
+ public int getColumnWidth(int columnIndex) {
+ return 2048;
+ }
+
+ @Override
+ public void autoSizeColumn(int columnIndex) {
+ }
+
+ @Override
+ public Workbook getWorkbook() {
+ return workbook;
+ }
+
+ @Override
+ public String getSheetName() {
+ return sodsSheet.getName();
+ }
+
+ @Override
+ public double getMargin(PageMargin margin) { return 0; }
+ @Override
+ public void setMargin(PageMargin margin, double size) {}
+ @Override
+ public boolean isPrintGridlines() { return false; }
+ @Override
+ public void setPrintGridlines(boolean show) {}
+ @Override
+ public boolean isPrintRowAndColumnHeadings() { return false; }
+ @Override
+ public void setPrintRowAndColumnHeadings(boolean show) {}
+ @Override
+ public void setActiveCell(CellAddress address) {}
+ @Override
+ public void removeMergedRegion(int index) {}
+ @Override
+ public void removeMergedRegions(Collection indices) {}
+ @Override
+ public int getNumMergedRegions() { return 0; }
+ @Override
+ public CellRangeAddress getMergedRegion(int index) { return null; }
+ @Override
+ public List getMergedRegions() { return new ArrayList<>(); }
+ @Override
+ public Iterator rowIterator() {
+ List list = new ArrayList<>(rows.values());
+ return list.iterator();
+ }
+ @Override
+ public Iterator iterator() {
+ return rowIterator();
+ }
+ @Override
+ public void setForceFormulaRecalculation(boolean value) {}
+ @Override
+ public boolean getForceFormulaRecalculation() { return false; }
+ @Override
+ public void setAutobreaks(boolean value) {}
+ @Override
+ public boolean getAutobreaks() { return false; }
+ @Override
+ public void setDisplayGuts(boolean value) {}
+ @Override
+ public boolean getDisplayGuts() { return false; }
+ @Override
+ public void setDisplayRowColHeadings(boolean value) {}
+ @Override
+ public void setDisplayFormulas(boolean value) {}
+ @Override
+ public void setDisplayGridlines(boolean value) {}
+ @Override
+ public boolean isDisplayGridlines() { return false; }
+ @Override
+ public void setRowSumsBelow(boolean value) {}
+ @Override
+ public boolean getRowSumsBelow() { return false; }
+ @Override
+ public void setRowSumsRight(boolean value) {}
+ @Override
+ public boolean getRowSumsRight() { return false; }
+ @Override
+ public int getPhysicalNumberOfRows() { return rows.size(); }
+ @Override
+ public int addMergedRegion(CellRangeAddress region) { return 0; }
+ @Override
+ public int addMergedRegionUnsafe(CellRangeAddress region) { return 0; }
+
+ @Override
+ public CellAddress getActiveCell() { return null; }
+
+ @Override
+ public List extends Hyperlink> getHyperlinkList() { return new ArrayList<>(); }
+
+ @Override
+ public Hyperlink getHyperlink(CellAddress address) { return null; }
+
+ @Override
+ public Hyperlink getHyperlink(int row, int column) { return null; }
+
+ @Override
+ public int getColumnOutlineLevel(int columnIndex) { return 0; }
+
+ @Override
+ public void setRepeatingColumns(CellRangeAddress repeatingColumnsToIndex) {}
+
+ @Override
+ public void setRepeatingRows(CellRangeAddress repeatingRowsToIndex) {}
+
+ @Override
+ public CellRangeAddress getRepeatingColumns() { return null; }
+
+ @Override
+ public CellRangeAddress getRepeatingRows() { return null; }
+
+ @Override
+ public SheetConditionalFormatting getSheetConditionalFormatting() { return null; }
+
+ @Override
+ public AutoFilter setAutoFilter(CellRangeAddress range) { return null; }
+
+ @Override
+ public void addValidationData(DataValidation dataValidation) {}
+
+ @Override
+ public List extends DataValidation> getDataValidations() { return new ArrayList<>(); }
+
+ @Override
+ public DataValidationHelper getDataValidationHelper() { return null; }
+
+ @Override
+ public CellRange extends Cell> removeArrayFormula(Cell cell) { return null; }
+
+ @Override
+ public CellRange extends Cell> setArrayFormula(String formula, CellRangeAddress range) { return null; }
+
+ @Override
+ public boolean isSelected() { return false; }
+
+ @Override
+ public Drawing> createDrawingPatriarch() { return null; }
+
+ @Override
+ public Drawing> getDrawingPatriarch() { return null; }
+
+ @Override
+ public Map getCellComments() {
+ return new HashMap<>();
+ }
+
+ @Override
+ public Comment getCellComment(CellAddress address) { return null; }
+
+ @Override
+ public void autoSizeColumn(int columnIndex, boolean useMergedCells) {}
+
+ @Override
+ public void setDefaultColumnStyle(int column, CellStyle style) {}
+
+ @Override
+ public void setRowGroupCollapsed(int row, boolean collapse) {}
+
+ @Override
+ public void groupRow(int startRow, int endRow) {}
+
+ @Override
+ public void ungroupRow(int startRow, int endRow) {}
+
+ @Override
+ public void groupColumn(int startColumn, int endColumn) {}
+
+ @Override
+ public void ungroupColumn(int startColumn, int endColumn) {}
+
+ @Override
+ public void setColumnGroupCollapsed(int columnNumber, boolean collapsed) {}
+
+ @Override
+ public void setRowBreak(int row) {}
+
+ @Override
+ public void removeRowBreak(int row) {}
+
+ @Override
+ public boolean isRowBroken(int row) { return false; }
+
+ @Override
+ public int[] getRowBreaks() { return new int[0]; }
+
+ @Override
+ public void setColumnBreak(int column) {}
+
+ @Override
+ public void removeColumnBreak(int column) {}
+
+ @Override
+ public boolean isColumnBroken(int column) { return false; }
+
+ @Override
+ public int[] getColumnBreaks() { return new int[0]; }
+
+ @Override
+ public boolean isDisplayRowColHeadings() { return true; }
+
+ @Override
+ public boolean isDisplayFormulas() { return false; }
+
+ @Override
+ public org.apache.poi.ss.util.PaneInformation getPaneInformation() { return null; }
+
+ @Override
+ public void createFreezePane(int colSplit, int rowSplit, int leftmostColumn, int topRow) {}
+
+ @Override
+ public void createFreezePane(int colSplit, int rowSplit) {}
+
+ @Override
+ public void createSplitPane(int xSplit, int ySplit, int leftmostColumn, int topRow, PaneType activePane) {}
+
+ @Override
+ public void createSplitPane(int xSplit, int ySplit, int leftmostColumn, int topRow, int activePane) {}
+
+ @Override
+ public void shiftColumns(int startColumn, int endColumn, int n) {}
+
+ @Override
+ public void shiftRows(int startRow, int endRow, int n, boolean copyRowHeight, boolean resetOriginalRowHeight) {}
+
+ @Override
+ public void shiftRows(int startRow, int endRow, int n) {}
+
+ @Override
+ public void showInPane(int topRow, int leftmostColumn) {}
+
+ @Override
+ public short getLeftCol() { return 0; }
+
+ @Override
+ public short getTopRow() { return 0; }
+
+ @Override
+ public void setZoom(int scale) {}
+
+ @Override
+ public boolean getScenarioProtect() { return false; }
+
+ @Override
+ public void protectSheet(String password) {}
+
+ @Override
+ public boolean getProtect() { return false; }
+
+ @Override
+ public double getMargin(short margin) { return 0; }
+
+ @Override
+ public void setMargin(short margin, double size) {}
+
+ @Override
+ public void setSelected(boolean sel) {}
+
+ @Override
+ public Header getHeader() { return null; }
+
+ @Override
+ public Footer getFooter() { return null; }
+
+ @Override
+ public PrintSetup getPrintSetup() { return null; }
+
+ @Override
+ public boolean getFitToPage() { return false; }
+
+ @Override
+ public void setFitToPage(boolean value) {}
+
+ @Override
+ public boolean isDisplayZeros() { return true; }
+
+ @Override
+ public void setDisplayZeros(boolean value) {}
+
+ @Override
+ public boolean getHorizontallyCenter() { return false; }
+
+ @Override
+ public void setHorizontallyCenter(boolean value) {}
+
+ @Override
+ public boolean getVerticallyCenter() { return false; }
+
+ @Override
+ public void setVerticallyCenter(boolean value) {}
+
+ @Override
+ public void validateMergedRegions() {}
+
+ @Override
+ public CellStyle getColumnStyle(int column) { return null; }
+
+ @Override
+ public short getDefaultRowHeight() { return 300; }
+
+ @Override
+ public void setDefaultRowHeight(short height) {}
+
+ @Override
+ public float getDefaultRowHeightInPoints() { return 15.0f; }
+
+ @Override
+ public void setDefaultRowHeightInPoints(float height) {}
+
+ @Override
+ public int getDefaultColumnWidth() { return 8; }
+
+ @Override
+ public void setDefaultColumnWidth(int width) {}
+
+ @Override
+ public float getColumnWidthInPixels(int columnIndex) { return 8.0f * 8; }
+
+ @Override
+ public boolean isRightToLeft() { return false; }
+
+ @Override
+ public void setRightToLeft(boolean value) {}
+
+ @Override
+ public boolean isColumnHidden(int columnIndex) { return false; }
+
+ @Override
+ public void setColumnHidden(int columnIndex, boolean hidden) {}
+}
diff --git a/src/main/java/org/spdx/spreadsheetstore/ods/OdsWorkbook.java b/src/main/java/org/spdx/spreadsheetstore/ods/OdsWorkbook.java
new file mode 100644
index 0000000..df6caaf
--- /dev/null
+++ b/src/main/java/org/spdx/spreadsheetstore/ods/OdsWorkbook.java
@@ -0,0 +1,354 @@
+/*
+ * 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.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import com.github.miachm.sods.SpreadSheet;
+import org.apache.poi.ss.SpreadsheetVersion;
+import org.apache.poi.ss.usermodel.*;
+
+/**
+ * Adapter for Apache POI {@link Workbook} over a SODS {@link com.github.miachm.sods.SpreadSheet}.
+ * Serves as the primary entry point for ODS read/write operations within the POI interface layer.
+ */
+public class OdsWorkbook implements Workbook {
+
+ private final SpreadSheet spreadSheet;
+ private final List sheets = new ArrayList<>();
+ private final List fonts = new ArrayList<>();
+ private final List styles = new ArrayList<>();
+ private final OdsCreationHelper creationHelper = new OdsCreationHelper(this);
+
+ public OdsWorkbook() {
+ this.spreadSheet = new SpreadSheet();
+ }
+
+ public OdsWorkbook(InputStream is) throws IOException {
+ this.spreadSheet = new SpreadSheet(is);
+ for (com.github.miachm.sods.Sheet sodsSheet : spreadSheet.getSheets()) {
+ sheets.add(new OdsSheet(this, sodsSheet));
+ }
+ }
+
+ public SpreadSheet getSpreadSheet() {
+ return this.spreadSheet;
+ }
+
+ @Override
+ public int getActiveSheetIndex() {
+ return 0;
+ }
+
+ @Override
+ public void setActiveSheet(int sheetIndex) {
+ }
+
+ @Override
+ public int getFirstVisibleTab() {
+ return 0;
+ }
+
+ @Override
+ public void setFirstVisibleTab(int sheetIndex) {
+ }
+
+ @Override
+ public void setSheetOrder(String sheetname, int pos) {
+ }
+
+ @Override
+ public void setSelectedTab(int index) {
+ }
+
+ @Override
+ public void setSheetName(int sheet, String name) {
+ if (sheet >= 0 && sheet < sheets.size()) {
+ sheets.get(sheet).getSodsSheet().setName(name);
+ }
+ }
+
+ @Override
+ public String getSheetName(int sheet) {
+ if (sheet >= 0 && sheet < sheets.size()) {
+ return sheets.get(sheet).getSheetName();
+ }
+ return null;
+ }
+
+ @Override
+ public int getSheetIndex(String name) {
+ for (int i = 0; i < sheets.size(); i++) {
+ if (sheets.get(i).getSheetName().equalsIgnoreCase(name)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ @Override
+ public int getSheetIndex(Sheet sheet) {
+ return sheets.indexOf(sheet);
+ }
+
+ @Override
+ public Sheet createSheet() {
+ return createSheet("Sheet" + sheets.size());
+ }
+
+ @Override
+ public Sheet createSheet(String sheetname) {
+ com.github.miachm.sods.Sheet sodsSheet = new com.github.miachm.sods.Sheet(sheetname);
+ spreadSheet.appendSheet(sodsSheet);
+ OdsSheet newSheet = new OdsSheet(this, sodsSheet);
+ sheets.add(newSheet);
+ return newSheet;
+ }
+
+ @Override
+ public Sheet cloneSheet(int sheetNum) {
+ throw new UnsupportedOperationException("OdsWorkbook: cloneSheet not implemented");
+ }
+
+ @Override
+ public Iterator sheetIterator() {
+ List list = new ArrayList<>(sheets);
+ return list.iterator();
+ }
+
+ @Override
+ public int getNumberOfSheets() {
+ return sheets.size();
+ }
+
+ @Override
+ public Sheet getSheetAt(int index) {
+ if (index >= 0 && index < sheets.size()) {
+ return sheets.get(index);
+ }
+ return null;
+ }
+
+ @Override
+ public Sheet getSheet(String name) {
+ int index = getSheetIndex(name);
+ return index != -1 ? getSheetAt(index) : null;
+ }
+
+ @Override
+ public void removeSheetAt(int index) {
+ if (index >= 0 && index < sheets.size()) {
+ spreadSheet.deleteSheet(index);
+ sheets.remove(index);
+ }
+ }
+
+ @Override
+ public Font createFont() {
+ OdsFont font = new OdsFont((short) fonts.size());
+ fonts.add(font);
+ return font;
+ }
+
+ @Override
+ public Font findFont(boolean bold, short color, short fontHeight, String name, boolean italic, boolean strikeout, short typeOffset, byte underline) {
+ for (OdsFont font : fonts) {
+ if (font.getBold() == bold && font.getColor() == color && font.getFontHeight() == fontHeight && font.getFontName().equals(name)) {
+ return font;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public int getNumberOfFonts() {
+ return fonts.size();
+ }
+
+ @Override
+ public Font getFontAt(int idx) {
+ if (idx >= 0 && idx < fonts.size()) {
+ return fonts.get(idx);
+ }
+ return null;
+ }
+
+ @Override
+ public DataFormat createDataFormat() {
+ return creationHelper.createDataFormat();
+ }
+
+ @Override
+ public CellStyle createCellStyle() {
+ OdsCellStyle style = new OdsCellStyle(this);
+ styles.add(style);
+ return style;
+ }
+
+ @Override
+ public int getNumCellStyles() {
+ return styles.size();
+ }
+
+ @Override
+ public CellStyle getCellStyleAt(int idx) {
+ if (idx >= 0 && idx < styles.size()) {
+ return styles.get(idx);
+ }
+ return null;
+ }
+
+ @Override
+ public void write(OutputStream stream) throws IOException {
+ spreadSheet.save(stream);
+ }
+
+ @Override
+ public void close() throws IOException {
+ }
+
+ @Override
+ public int addPicture(byte[] pictureData, int format) {
+ return 0;
+ }
+
+ @Override
+ public List extends PictureData> getAllPictures() {
+ return new ArrayList<>();
+ }
+
+ @Override
+ public CreationHelper getCreationHelper() {
+ return creationHelper;
+ }
+
+ @Override
+ public boolean isHidden() {
+ return false;
+ }
+
+ @Override
+ public void setHidden(boolean hidden) {
+ }
+
+ @Override
+ public boolean isSheetHidden(int sheetNum) {
+ return false;
+ }
+
+ @Override
+ public boolean isSheetVeryHidden(int sheetNum) {
+ return false;
+ }
+
+ @Override
+ public void setSheetHidden(int sheetNum, boolean hidden) {
+ }
+
+ @Override
+ public void setSheetVisibility(int sheetNum, SheetVisibility visibility) {
+ }
+
+ @Override
+ public SheetVisibility getSheetVisibility(int sheetNum) {
+ return SheetVisibility.VISIBLE;
+ }
+
+ @Override
+ public void addToolPack(org.apache.poi.ss.formula.udf.UDFFinder toopack) {
+ }
+
+ @Override
+ public void setForceFormulaRecalculation(boolean value) {
+ }
+
+ @Override
+ public boolean getForceFormulaRecalculation() {
+ return false;
+ }
+
+ @Override
+ public SpreadsheetVersion getSpreadsheetVersion() {
+ return SpreadsheetVersion.EXCEL2007;
+ }
+
+ @Override
+ public Iterator iterator() {
+ return sheetIterator();
+ }
+
+ @Override
+ public void setCellReferenceType(org.apache.poi.ss.usermodel.CellReferenceType cellReferenceType) {
+ }
+
+ @Override
+ public org.apache.poi.ss.usermodel.CellReferenceType getCellReferenceType() {
+ return org.apache.poi.ss.usermodel.CellReferenceType.A1;
+ }
+
+ @Override
+ public int addOlePackage(byte[] oleData, String label, String fileName, String command) throws IOException {
+ return 0;
+ }
+
+ @Override
+ public org.apache.poi.ss.formula.EvaluationWorkbook createEvaluationWorkbook() {
+ return null;
+ }
+
+ @Override
+ public void setMissingCellPolicy(Row.MissingCellPolicy missingCellPolicy) {}
+
+ @Override
+ public Row.MissingCellPolicy getMissingCellPolicy() {
+ return Row.MissingCellPolicy.RETURN_NULL_AND_BLANK;
+ }
+
+ @Override
+ public void removePrintArea(int sheetIndex) {}
+
+ @Override
+ public void setPrintArea(int sheetIndex, String reference) {}
+
+ @Override
+ public void setPrintArea(int sheetIndex, int startColumn, int endColumn, int startRow, int endRow) {}
+
+ @Override
+ public String getPrintArea(int sheetIndex) { return null; }
+
+ @Override
+ public int linkExternalWorkbook(String name, Workbook workbook) {
+ return 0;
+ }
+
+ @Override
+ public int getNumberOfNames() { return 0; }
+
+ @Override
+ public Name getName(String name) { return null; }
+
+ @Override
+ public List extends Name> getNames(String name) { return new ArrayList<>(); }
+
+ @Override
+ public List extends Name> getAllNames() { return new ArrayList<>(); }
+
+ @Override
+ public Name createName() { return null; }
+
+ @Override
+ public void removeName(Name name) {}
+
+ @Override
+ public int getNumberOfFontsAsInt() {
+ return fonts.size();
+ }
+}
diff --git a/src/main/java/org/spdx/spreadsheetstore/ods/package-info.java b/src/main/java/org/spdx/spreadsheetstore/ods/package-info.java
new file mode 100644
index 0000000..92d86f4
--- /dev/null
+++ b/src/main/java/org/spdx/spreadsheetstore/ods/package-info.java
@@ -0,0 +1,32 @@
+/*
+ * SPDX-FileContributor: Arthit Suriyawongkul
+ * SPDX-FileCopyrightText: 2026 SPDX Contributors
+ * SPDX-FileType: SOURCE
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/**
+ * Custom Apache POI adapter layer using the SODS library to provide
+ * OpenDocument Spreadsheet (ODS) support.
+ *
+ * This package maps the Apache POI {@code org.apache.poi.ss.usermodel}
+ * interfaces to the underlying {@code com.github.miachm.sods} library.
+ *
+ *
+ * Standard spreadsheet styling elements (colors, fonts, alignments, text
+ * wrapping, and cell borders) are fully supported.
+ *
+ *
+ * Limitations
+ *
+ * Custom cell data formatting (e.g., specific number formats and custom date
+ * patterns) has limited support due to the underlying SODS library. SODS only
+ * natively supports plain text ("@") or an ISO date format ("YYYY-MM-DD").
+ *
+ *
+ * Consequently, any target cell format representing a date is mapped to the
+ * standard {@code "YYYY-MM-DD"} format, while custom numerical and string
+ * format patterns remain unstyled.
+ *
+ */
+package org.spdx.spreadsheetstore.ods;
diff --git a/src/test/java/org/spdx/spreadsheetstore/SpreadsheetStoreTest.java b/src/test/java/org/spdx/spreadsheetstore/SpreadsheetStoreTest.java
index 45fcae3..ffe94bb 100644
--- a/src/test/java/org/spdx/spreadsheetstore/SpreadsheetStoreTest.java
+++ b/src/test/java/org/spdx/spreadsheetstore/SpreadsheetStoreTest.java
@@ -1,5 +1,6 @@
/*
* SPDX-FileContributor: Gary O'Neall
+ * SPDX-FileContributor: Arthit Suriyawongkul
* SPDX-FileCopyrightText: Copyright (c) 2020 Source Auditor Inc.
* SPDX-FileType: SOURCE
* SPDX-License-Identifier: Apache-2.0
@@ -67,6 +68,7 @@
import org.spdx.library.model.v2.license.SpdxNoAssertionLicense;
import org.spdx.library.model.v3_0_1.SpdxModelInfoV3_0;
import org.spdx.library.referencetype.ListedReferenceTypes;
+import org.spdx.spreadsheetstore.SpreadsheetStore.SpreadsheetFormatType;
import org.spdx.storage.compatv2.CompatibleModelStoreWrapper;
import org.spdx.storage.simple.InMemSpdxStore;
import org.spdx.utility.compare.SpdxCompareException;
@@ -495,6 +497,103 @@ public void testSerialize() throws InvalidSPDXAnalysisException, IOException, Sp
}
+ /**
+ * Test method for
+ * {@link org.spdx.spreadsheetstore.SpreadsheetStore#serialize(java.io.OutputStream)}
+ * using ODS format.
+ *
+ * @throws InvalidSPDXAnalysisException
+ * @throws IOException
+ * @throws SpdxCompareException
+ */
+ @SuppressWarnings("unchecked")
+ public void testSerializeOds() throws InvalidSPDXAnalysisException, IOException, SpdxCompareException {
+ SpreadsheetStore sst = new SpreadsheetStore(new InMemSpdxStore(), SpreadsheetFormatType.ODS);
+ String documentUri = "http://newdoc/uri";
+ ModelCopyManager copyManager = new ModelCopyManager();
+ compareStore.getAllItems(compareDocument.getDocumentUri(), SpdxConstantsCompatV2.CLASS_EXTERNAL_DOC_REF).forEach(tv -> {
+ try {
+ copyManager.copy(sst, compareStore, tv.getObjectUri(),
+ CompatibleModelStoreWrapper.LATEST_SPDX_2X_VERSION, documentUri + "#");
+ } catch (InvalidSPDXAnalysisException e) {
+ throw new RuntimeException(e);
+ }
+ });
+ compareStore.getAllItems(compareDocument.getDocumentUri(), null).forEach(tv -> {
+ try {
+ if (!SpdxConstantsCompatV2.CLASS_EXTERNAL_DOC_REF.equals(tv.getType())) {
+ copyManager.copy(sst, compareStore, tv.getObjectUri(),
+ CompatibleModelStoreWrapper.LATEST_SPDX_2X_VERSION, documentUri + "#");
+ }
+ } catch (InvalidSPDXAnalysisException e) {
+ throw new RuntimeException(e);
+ }
+ });
+
+ Path tempFilePath = Files.createTempFile("temp", ".ods");
+ try {
+ try (FileOutputStream out = new FileOutputStream(tempFilePath.toFile())) {
+ sst.serialize(out);
+ }
+ SpreadsheetStore resultStore = new SpreadsheetStore(new InMemSpdxStore());
+ try (FileInputStream stream = new FileInputStream(tempFilePath.toFile())) {
+ resultStore.deSerialize(stream, false);
+ }
+ List allDocs = (List)SpdxModelFactory.getSpdxObjects(resultStore, copyManager,
+ SpdxConstantsCompatV2.CLASS_SPDX_DOCUMENT, null, null).collect(Collectors.toList());
+ assertEquals(1, allDocs.size());
+ String resultDocUri = allDocs.get(0).getDocumentUri();
+ assertEquals(documentUri, resultDocUri);
+ ModelCopyManager cm = new ModelCopyManager();
+ SpdxDocument doc = new SpdxDocument(resultStore, resultDocUri, cm, false);
+ // Document fields and extracted license infos
+ assertDocFields(doc, "SPDX-2.3");
+ SpdxComparer comparer = new SpdxComparer();
+ SpdxPackage comparePkg = new SpdxPackage(compareDocument.getModelStore(), compareDocument.getDocumentUri(), "SPDXRef-Package", null, false);
+ SpdxPackage resultPkg = new SpdxPackage(resultStore, resultDocUri, "SPDXRef-Package", null, false);
+ assertTrue(resultPkg.equivalent(comparePkg));
+ comparer.compare(compareDocument, doc);
+ assertFalse(comparer.isDifferenceFound());
+ // Files
+ try(Stream elementStream = (Stream)SpdxModelFactory.getSpdxObjects(resultStore, copyManager,
+ SpdxConstantsCompatV2.CLASS_SPDX_FILE, documentUri, documentUri + "#")) {
+ elementStream.forEach(element -> {
+ try {
+ assertTrue(((SpdxElement)element).equivalent(compareFiles.get(((SpdxElement)element).getId())));
+ } catch (InvalidSPDXAnalysisException e) {
+ fail("Exception: "+e.getMessage());
+ }
+ });
+ }
+
+ // Packages
+ try(Stream elementStream = (Stream)SpdxModelFactory.getSpdxObjects(resultStore, copyManager,
+ SpdxConstantsCompatV2.CLASS_SPDX_PACKAGE, documentUri, documentUri + "#")) {
+ elementStream.forEach(element -> {
+ try {
+ assertTrue(((SpdxElement)element).equivalent(comparePackages.get(((SpdxElement)element).getId())));
+ } catch (InvalidSPDXAnalysisException e) {
+ fail("Exception: "+e.getMessage());
+ }
+ });
+ }
+
+ // Snippets
+ try(Stream elementStream = (Stream)SpdxModelFactory.getSpdxObjects(resultStore, copyManager,
+ SpdxConstantsCompatV2.CLASS_SPDX_SNIPPET, documentUri, documentUri + "#")) {
+ elementStream.forEach(element -> {
+ try {
+ assertTrue(((SpdxElement)element).equivalent(compareSnippets.get(((SpdxElement)element).getId())));
+ } catch (InvalidSPDXAnalysisException e) {
+ fail("Exception: "+e.getMessage());
+ }
+ });
+ }
+ } finally {
+ tempFilePath.toFile().delete();
+ }
+ }
+
/**
* Test method for {@link org.spdx.spreadsheetstore.SpreadsheetStore#deSerialize(java.io.InputStream, boolean)}.
* @throws IOException
From 4658f81d5e157e6ac40d90394fcedf02382191c4 Mon Sep 17 00:00:00 2001
From: Arthit Suriyawongkul
Date: Sat, 18 Jul 2026 13:24:27 +0100
Subject: [PATCH 2/5] Add comment on actual datatime precision being stored
Signed-off-by: Arthit Suriyawongkul
---
.../java/org/spdx/spreadsheetstore/ods/OdsCellStyle.java | 6 ++++++
.../java/org/spdx/spreadsheetstore/ods/package-info.java | 7 +++++++
2 files changed, 13 insertions(+)
diff --git a/src/main/java/org/spdx/spreadsheetstore/ods/OdsCellStyle.java b/src/main/java/org/spdx/spreadsheetstore/ods/OdsCellStyle.java
index 6009ce2..76f1517 100644
--- a/src/main/java/org/spdx/spreadsheetstore/ods/OdsCellStyle.java
+++ b/src/main/java/org/spdx/spreadsheetstore/ods/OdsCellStyle.java
@@ -238,6 +238,12 @@ public void setFont(Font font) {
* dates are properly displayed. Other formats are currently stored but may not
* affect the final ODS cell rendering.
* | | |
+ *
+ * This is display-only: the full {@code HH:mm:ss} timestamp is still stored
+ * in {@code office:date-value} and returned unchanged by
+ * {@link Cell#getDateCellValue()}, even though time-inclusive formats
+ * render as date-only in spreadsheet applications.
+ *
*
* @param fmt the data format index
*/
diff --git a/src/main/java/org/spdx/spreadsheetstore/ods/package-info.java b/src/main/java/org/spdx/spreadsheetstore/ods/package-info.java
index 92d86f4..9ffd603 100644
--- a/src/main/java/org/spdx/spreadsheetstore/ods/package-info.java
+++ b/src/main/java/org/spdx/spreadsheetstore/ods/package-info.java
@@ -28,5 +28,12 @@
* standard {@code "YYYY-MM-DD"} format, while custom numerical and string
* format patterns remain unstyled.
*
+ *
+ * This is a display-only limitation. Date/time values are still stored with
+ * full {@code HH:mm:ss} precision in the cell's underlying
+ * {@code office:date-value} attribute; only the visual rendering in
+ * spreadsheet applications (e.g. LibreOffice, Excel) is truncated to the
+ * date portion.
+ *
*/
package org.spdx.spreadsheetstore.ods;
From c0cf6490706abf435ab2c197e528ffc06029f4f4 Mon Sep 17 00:00:00 2001
From: Arthit Suriyawongkul
Date: Sun, 2 Aug 2026 09:00:21 +0100
Subject: [PATCH 3/5] Use proper SODS API
Co-Authored-By: Miguel Chacon
Signed-off-by: Arthit Suriyawongkul
---
pom.xml | 4 +-
.../spreadsheetstore/SpdxSpreadsheet.java | 15 +-
.../spreadsheetstore/SpreadsheetStore.java | 20 +-
.../spdx/spreadsheetstore/ods/OdsCell.java | 159 ++++++-
.../spreadsheetstore/ods/OdsCellStyle.java | 77 ++--
.../spdx/spreadsheetstore/ods/OdsComment.java | 127 +++++
.../ods/OdsCreationHelper.java | 15 +-
.../spreadsheetstore/ods/OdsDataFormat.java | 12 +-
.../ods/OdsRichTextString.java | 67 +++
.../org/spdx/spreadsheetstore/ods/OdsRow.java | 205 +++++++--
.../spdx/spreadsheetstore/ods/OdsSheet.java | 433 ++++++++++++++++--
.../spreadsheetstore/ods/OdsWorkbook.java | 116 ++++-
.../spreadsheetstore/ods/OdsWorkbookTest.java | 277 +++++++++++
13 files changed, 1354 insertions(+), 173 deletions(-)
create mode 100644 src/main/java/org/spdx/spreadsheetstore/ods/OdsComment.java
create mode 100644 src/main/java/org/spdx/spreadsheetstore/ods/OdsRichTextString.java
create mode 100644 src/test/java/org/spdx/spreadsheetstore/ods/OdsWorkbookTest.java
diff --git a/pom.xml b/pom.xml
index 82af6e8..cb76477 100644
--- a/pom.xml
+++ b/pom.xml
@@ -8,7 +8,7 @@
spdx-spreadsheet-store
http://github.com/spdx/spdx-spreadsheet-store
- Stores SPDX documents in Microsoft Excel formats. Supports both XLS and XLSX file types.
+ Stores SPDX documents in Microsoft Excel and OpenDocument Spreadsheet formats. Supports ODS, XLS, and XLSX file types.
https://github.com/spdx/spdx-java-spreadsheet-store
scm:git:ssh://git@github.com:spdx/spdx-java-spreadsheet-store
@@ -159,7 +159,7 @@
org.apache.poi
poi-ooxml
- 5.4.1
+ 5.5.1
com.github.miachm.sods
diff --git a/src/main/java/org/spdx/spreadsheetstore/SpdxSpreadsheet.java b/src/main/java/org/spdx/spreadsheetstore/SpdxSpreadsheet.java
index 685a73f..d1588c1 100644
--- a/src/main/java/org/spdx/spreadsheetstore/SpdxSpreadsheet.java
+++ b/src/main/java/org/spdx/spreadsheetstore/SpdxSpreadsheet.java
@@ -45,7 +45,7 @@
*
* @author Gary O'Neall
*/
-public class SpdxSpreadsheet {
+public class SpdxSpreadsheet implements AutoCloseable {
static final Logger logger = LoggerFactory.getLogger(SpdxSpreadsheet.class);
@@ -94,8 +94,8 @@ public class SpdxSpreadsheet {
// 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];
+ stream.mark(4096);
+ byte[] header = new byte[2048];
int bytesRead = 0;
while (bytesRead < header.length) {
int r = stream.read(header, bytesRead, header.length - bytesRead);
@@ -143,6 +143,9 @@ public SpdxSpreadsheet(InputStream stream, IModelStore modelStore, ModelCopyMana
} catch (IOException e) {
logger.error("I/O error reading SPDX Spreadsheet", e);
throw new SpreadsheetException("I/O error reading SPDX Spreadsheet", e);
+ } catch (Exception e) {
+ logger.error("Error reading SPDX Spreadsheet", e);
+ throw new SpreadsheetException("Error reading SPDX Spreadsheet", e);
}
this.version = readVersion(this.workbook, DOCUMENT_INFO_NAME);
if (this.version.equals(UNKNOWN_VERSION)) {
@@ -460,4 +463,10 @@ public void write(OutputStream stream) throws IOException {
this.workbook.write(stream);
}
+ @Override
+ public void close() throws IOException {
+ if (this.workbook != null) {
+ this.workbook.close();
+ }
+ }
}
diff --git a/src/main/java/org/spdx/spreadsheetstore/SpreadsheetStore.java b/src/main/java/org/spdx/spreadsheetstore/SpreadsheetStore.java
index ceeafc5..b7b8afe 100644
--- a/src/main/java/org/spdx/spreadsheetstore/SpreadsheetStore.java
+++ b/src/main/java/org/spdx/spreadsheetstore/SpreadsheetStore.java
@@ -34,6 +34,7 @@
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
+import java.util.TimeZone;
import java.util.Optional;
import java.util.TreeMap;
import java.util.stream.Collectors;
@@ -70,7 +71,7 @@
/**
* SPDX Java Library store which serializes and deserializes to Microsoft Excel
- * Workbooks
+ * and OpenDocument Spreadsheet (ODS) Workbooks
*
* @author Gary O'Neall
*/
@@ -85,19 +86,18 @@ public enum SpreadsheetFormatType {ODS, XLS, XLSX};
private SpreadsheetFormatType spreadsheetFormat;
- private static final ThreadLocal FORMAT = new ThreadLocal() {
- @Override
- protected DateFormat initialValue() {
- return new SimpleDateFormat(SpdxConstantsCompatV2.SPDX_DATE_FORMAT);
- }
- };
+ private static final ThreadLocal FORMAT = ThreadLocal.withInitial(() -> {
+ DateFormat df = new SimpleDateFormat(SpdxConstantsCompatV2.SPDX_DATE_FORMAT);
+ df.setTimeZone(TimeZone.getTimeZone("UTC"));
+ return df;
+ });
/**
* Constructs an SPDX model store which serializes and deserializes to
- * Microsoft Excel Workbooks in a specified format
+ * spreadsheets in a specified format (XLS, XLSX, or ODS)
*
* @param baseStore SPDX model store for deserialization/serialization
- * @param spreadsheetFormat format type XLS or XLSX
+ * @param spreadsheetFormat format type XLS, XLSX, or ODS
*/
public SpreadsheetStore(IModelStore baseStore, SpreadsheetFormatType spreadsheetFormat) {
super(baseStore);
@@ -468,7 +468,7 @@ private void copyAnyMissingFileContains(PerFileSheet perFileSheet,
pkg.addFile(file);
}
} else {
- logger.warn("Can not add file "+file.getName()+" to package "+pkgId);
+ logger.warn("Can not add file "+(file != null ? file.getName() : fileId)+" to package "+pkgId);
}
}
}
diff --git a/src/main/java/org/spdx/spreadsheetstore/ods/OdsCell.java b/src/main/java/org/spdx/spreadsheetstore/ods/OdsCell.java
index dc67fc3..cfb1b91 100644
--- a/src/main/java/org/spdx/spreadsheetstore/ods/OdsCell.java
+++ b/src/main/java/org/spdx/spreadsheetstore/ods/OdsCell.java
@@ -6,8 +6,20 @@
*/
package org.spdx.spreadsheetstore.ods;
+import java.util.Calendar;
import java.util.Date;
-import org.apache.poi.ss.usermodel.*;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+import org.apache.poi.ss.usermodel.Cell;
+import org.apache.poi.ss.usermodel.CellStyle;
+import org.apache.poi.ss.usermodel.CellType;
+import org.apache.poi.ss.usermodel.Comment;
+import org.apache.poi.ss.usermodel.DateUtil;
+import org.apache.poi.ss.usermodel.Hyperlink;
+import org.apache.poi.ss.usermodel.RichTextString;
+import org.apache.poi.ss.usermodel.Row;
+import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.util.CellAddress;
import org.apache.poi.ss.util.CellRangeAddress;
@@ -21,38 +33,62 @@ public class OdsCell implements Cell {
private final com.github.miachm.sods.Range range;
private CellStyle cellStyle;
+ /**
+ * Creates an ODS cell wrapper.
+ *
+ * @param row Parent ODS row adapter.
+ * @param columnIndex Zero-based column index.
+ * @param range Underlying SODS Range representing this cell.
+ */
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() {
+ /**
+ * Returns the underlying SODS Range instance.
+ *
+ * @return The underlying SODS Range.
+ */
+ com.github.miachm.sods.Range getSodsRange() {
return range;
}
@Override
public void setCellValue(String value) {
+ range.setFormula(null);
range.setValue(value);
}
@Override
public void setCellValue(double value) {
+ range.setFormula(null);
range.setValue(value);
}
@Override
public void setCellValue(Date value) {
+ range.setFormula(null);
if (value == null) {
range.setValue(null);
} else {
- java.time.LocalDateTime ldt = java.time.LocalDateTime.ofInstant(value.toInstant(), java.time.ZoneId.systemDefault());
+ java.time.LocalDateTime ldt = java.time.LocalDateTime.ofInstant(value.toInstant(), java.time.ZoneOffset.UTC);
range.setValue(ldt);
}
}
@Override
public void setCellType(CellType cellType) {
+ if (cellType == CellType.BLANK) {
+ range.setValue(null);
+ range.setFormula(null);
+ } else if (cellType == CellType.STRING) {
+ Object val = range.getValue();
+ if (val != null) {
+ range.setValue(val.toString());
+ }
+ }
}
@Override
@@ -63,10 +99,31 @@ public String getStringCellValue() {
@Override
public double getNumericCellValue() {
+ CellType type = getCellType();
+ if (type != CellType.NUMERIC && type != CellType.FORMULA) {
+ throw new IllegalStateException("Cannot get a NUMERIC value from a " + type + " cell");
+ }
Object val = range.getValue();
if (val instanceof Number) {
return ((Number) val).doubleValue();
}
+ if (val instanceof com.github.miachm.sods.OfficeCurrency) {
+ Double d = ((com.github.miachm.sods.OfficeCurrency) val).getValue();
+ return d != null ? d : 0.0;
+ }
+ if (val instanceof com.github.miachm.sods.OfficePercentage) {
+ Double d = ((com.github.miachm.sods.OfficePercentage) val).getValue();
+ return d != null ? d : 0.0;
+ }
+ if (val instanceof java.util.Date) {
+ return DateUtil.getExcelDate((java.util.Date) val);
+ }
+ if (val instanceof java.time.LocalDateTime) {
+ return DateUtil.getExcelDate(java.sql.Timestamp.valueOf((java.time.LocalDateTime) val));
+ }
+ if (val instanceof java.time.LocalDate) {
+ return DateUtil.getExcelDate(java.sql.Date.valueOf((java.time.LocalDate) val));
+ }
return 0.0;
}
@@ -74,13 +131,13 @@ public double getNumericCellValue() {
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 LocalDateTime) {
+ LocalDateTime ldt = (LocalDateTime) value;
+ return Date.from(ldt.atZone(java.time.ZoneOffset.UTC).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 LocalDate) {
+ LocalDate ld = (LocalDate) value;
+ return Date.from(ld.atStartOfDay(java.time.ZoneOffset.UTC).toInstant());
}
if (value instanceof java.util.Date) {
return (Date) value;
@@ -88,15 +145,37 @@ public Date getDateCellValue() {
if (value instanceof Number) {
return org.apache.poi.ss.usermodel.DateUtil.getJavaDate(((Number) value).doubleValue());
}
+ if (value instanceof String) {
+ String s = ((String) value).trim();
+ if (s.isEmpty()) return null;
+ try {
+ if (s.endsWith("Z") || s.contains("+")) {
+ java.time.Instant instant = java.time.Instant.parse(s);
+ return Date.from(instant);
+ } else if (s.contains("T")) {
+ LocalDateTime ldt = LocalDateTime.parse(s, java.time.format.DateTimeFormatter.ISO_LOCAL_DATE_TIME);
+ return Date.from(ldt.atZone(java.time.ZoneOffset.UTC).toInstant());
+ } else {
+ LocalDate ld = LocalDate.parse(s, java.time.format.DateTimeFormatter.ISO_LOCAL_DATE);
+ return Date.from(ld.atStartOfDay(java.time.ZoneOffset.UTC).toInstant());
+ }
+ } catch (Exception e) {
+ // Not an ISO date string
+ }
+ }
return null;
}
@Override
public CellType getCellType() {
+ String formula = range.getFormula();
+ if (formula != null && !formula.isEmpty()) {
+ return CellType.FORMULA;
+ }
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 Number || val instanceof com.github.miachm.sods.OfficeCurrency || val instanceof com.github.miachm.sods.OfficePercentage) 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;
@@ -140,15 +219,18 @@ public Row getRow() {
@Override
public void setBlank() {
range.setValue(null);
+ range.setFormula(null);
}
@Override
public void setCellFormula(String formula) {
+ range.setFormula(formula);
}
@Override
public String getCellFormula() {
- return "";
+ String formula = range.getFormula();
+ return formula == null ? "" : formula;
}
@Override
@@ -177,28 +259,55 @@ public CellAddress getAddress() {
public void setCellValue(java.util.Calendar value) {
if (value != null) {
setCellValue(value.getTime());
+ } else {
+ setBlank();
}
}
@Override
public void setCellValue(RichTextString value) {
if (value != null) {
setCellValue(value.getString());
+ } else {
+ setBlank();
}
}
@Override
public void setCellValue(boolean value) {
+ range.setFormula(null);
range.setValue(value);
}
@Override
public RichTextString getRichStringCellValue() {
- return new org.apache.poi.xssf.usermodel.XSSFRichTextString(getStringCellValue());
+ return new OdsRichTextString(getStringCellValue());
}
@Override
- public void removeCellComment() {}
+ public void removeCellComment() {
+ range.setAnnotation(null);
+ }
+
@Override
- public Comment getCellComment() { return null; }
+ public Comment getCellComment() {
+ com.github.miachm.sods.OfficeAnnotation annotation = range.getAnnotation();
+ if (annotation != null) {
+ OdsComment comment = new OdsComment(annotation);
+ comment.setAddress(getAddress());
+ return comment;
+ }
+ return null;
+ }
+
@Override
- public void setCellComment(Comment comment) {}
+ public void setCellComment(Comment comment) {
+ if (comment instanceof OdsComment) {
+ range.setAnnotation(((OdsComment) comment).getAnnotation());
+ } else if (comment != null) {
+ RichTextString rts = comment.getString();
+ String text = rts != null ? rts.getString() : "";
+ range.setAnnotation(new com.github.miachm.sods.OfficeAnnotation(text, java.time.LocalDateTime.now()));
+ } else {
+ range.setAnnotation(null);
+ }
+ }
@Override
public Hyperlink getHyperlink() { return null; }
@Override
@@ -221,11 +330,11 @@ public java.time.LocalDateTime getLocalDateTimeCellValue() {
return ((java.time.LocalDate) value).atStartOfDay();
}
if (value instanceof Date) {
- return java.time.LocalDateTime.ofInstant(((Date) value).toInstant(), java.time.ZoneId.systemDefault());
+ return java.time.LocalDateTime.ofInstant(((Date) value).toInstant(), java.time.ZoneOffset.UTC);
}
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 java.time.LocalDateTime.ofInstant(date.toInstant(), java.time.ZoneOffset.UTC);
}
return null;
}
@@ -241,8 +350,20 @@ public void setCellValue(java.time.LocalDate value) {
}
@Override
- public void removeFormula() {}
+ public void removeFormula() {
+ range.setFormula(null);
+ }
@Override
- public CellType getCachedFormulaResultType() { return CellType.BLANK; }
+ public CellType getCachedFormulaResultType() {
+ Object val = range.getValue();
+ if (val == null) return CellType.BLANK;
+ if (val instanceof String) return CellType.STRING;
+ if (val instanceof Number || val instanceof com.github.miachm.sods.OfficeCurrency || val instanceof com.github.miachm.sods.OfficePercentage) 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;
+ }
}
diff --git a/src/main/java/org/spdx/spreadsheetstore/ods/OdsCellStyle.java b/src/main/java/org/spdx/spreadsheetstore/ods/OdsCellStyle.java
index 76f1517..ab6436a 100644
--- a/src/main/java/org/spdx/spreadsheetstore/ods/OdsCellStyle.java
+++ b/src/main/java/org/spdx/spreadsheetstore/ods/OdsCellStyle.java
@@ -6,7 +6,15 @@
*/
package org.spdx.spreadsheetstore.ods;
-import org.apache.poi.ss.usermodel.*;
+import org.apache.poi.hssf.util.HSSFColor;
+import org.apache.poi.ss.usermodel.BorderStyle;
+import org.apache.poi.ss.usermodel.CellStyle;
+import org.apache.poi.ss.usermodel.Color;
+import org.apache.poi.ss.usermodel.DateUtil;
+import org.apache.poi.ss.usermodel.FillPatternType;
+import org.apache.poi.ss.usermodel.Font;
+import org.apache.poi.ss.usermodel.HorizontalAlignment;
+import org.apache.poi.ss.usermodel.VerticalAlignment;
/**
* Adapter for Apache POI {@link CellStyle} over the SODS {@link com.github.miachm.sods.Style}.
@@ -15,6 +23,7 @@
public class OdsCellStyle implements CellStyle {
private final com.github.miachm.sods.Style style = new com.github.miachm.sods.Style();
+ private final com.github.miachm.sods.Borders borders = new com.github.miachm.sods.Borders();
private boolean wrapText = false;
private int fontIndex = -1;
private short fillForegroundColor = 0;
@@ -31,11 +40,21 @@ public class OdsCellStyle implements CellStyle {
private final OdsWorkbook workbook;
+ /**
+ * Creates an ODS cell style adapter.
+ *
+ * @param workbook Parent ODS workbook.
+ */
public OdsCellStyle(OdsWorkbook workbook) {
this.workbook = workbook;
}
- public com.github.miachm.sods.Style getSodsStyle() {
+ /**
+ * Returns the underlying SODS Style instance.
+ *
+ * @return The underlying SODS Style.
+ */
+ com.github.miachm.sods.Style getSodsStyle() {
return style;
}
@@ -118,27 +137,25 @@ public BorderStyle getBorderTop() {
}
private void updateBorders() {
- com.github.miachm.sods.Borders borders = new com.github.miachm.sods.Borders();
-
- boolean hasBottom = borderBottom != BorderStyle.NONE;
+ boolean hasBottom = borderBottom != null && borderBottom != BorderStyle.NONE;
borders.setBorderBottom(hasBottom);
if (hasBottom) {
borders.setBorderBottomProperties(getBorderProperties(borderBottom));
}
- boolean hasLeft = borderLeft != BorderStyle.NONE;
+ boolean hasLeft = borderLeft != null && borderLeft != BorderStyle.NONE;
borders.setBorderLeft(hasLeft);
if (hasLeft) {
borders.setBorderLeftProperties(getBorderProperties(borderLeft));
}
- boolean hasRight = borderRight != BorderStyle.NONE;
+ boolean hasRight = borderRight != null && borderRight != BorderStyle.NONE;
borders.setBorderRight(hasRight);
if (hasRight) {
borders.setBorderRightProperties(getBorderProperties(borderRight));
}
- boolean hasTop = borderTop != BorderStyle.NONE;
+ boolean hasTop = borderTop != null && borderTop != BorderStyle.NONE;
borders.setBorderTop(hasTop);
if (hasTop) {
borders.setBorderTopProperties(getBorderProperties(borderTop));
@@ -178,18 +195,11 @@ public void setFillForegroundColor(short bg) {
}
private com.github.miachm.sods.Color getSodsColor(short indexedColor) {
- // Map IndexedColors to standard RGB
- if (indexedColor == 42 || indexedColor == 57) { // LIGHT_GREEN
- return new com.github.miachm.sods.Color(204, 255, 204);
- }
- if (indexedColor == 43 || indexedColor == 34) { // LIGHT_YELLOW
- return new com.github.miachm.sods.Color(255, 255, 153);
- }
- if (indexedColor == 10) { // RED
- return new com.github.miachm.sods.Color(255, 199, 206);
- }
- if (indexedColor == 22) { // GREY_25_PERCENT
- return new com.github.miachm.sods.Color(224, 224, 224);
+ java.util.Map map = org.apache.poi.hssf.util.HSSFColor.getIndexHash();
+ org.apache.poi.hssf.util.HSSFColor hc = map.get((int) indexedColor);
+ if (hc != null && hc.getTriplet() != null) {
+ short[] rgb = hc.getTriplet();
+ return new com.github.miachm.sods.Color(rgb[0], rgb[1], rgb[2]);
}
return null;
}
@@ -211,21 +221,18 @@ public FillPatternType getFillPattern() {
@Override
public void setFont(Font font) {
- if (font instanceof OdsFont) {
- OdsFont odsFont = (OdsFont) font;
- this.fontIndex = odsFont.getIndex();
- style.setFontFamily(odsFont.getFontName());
- style.setFontSize((int) odsFont.getFontHeightInPoints());
- style.setBold(odsFont.getBold());
- style.setItalic(odsFont.getItalic());
- style.setUnderline(odsFont.getUnderline() != org.apache.poi.ss.usermodel.Font.U_NONE);
- style.setLineThrough(odsFont.getStrikeout());
- // Map font color if we want
- short fontColorIdx = odsFont.getColor();
- com.github.miachm.sods.Color fontColor = getSodsColor(fontColorIdx);
- if (fontColor != null) {
- style.setFontColor(fontColor);
- }
+ if (font == null) return;
+ this.fontIndex = font.getIndex();
+ style.setFontFamily(font.getFontName());
+ style.setFontSize((int) Math.round((double) font.getFontHeight() / 20.0));
+ style.setBold(font.getBold());
+ style.setItalic(font.getItalic());
+ style.setUnderline(font.getUnderline() != org.apache.poi.ss.usermodel.Font.U_NONE);
+ style.setLineThrough(font.getStrikeout());
+ short fontColorIdx = font.getColor();
+ com.github.miachm.sods.Color fontColor = getSodsColor(fontColorIdx);
+ if (fontColor != null) {
+ style.setFontColor(fontColor);
}
}
diff --git a/src/main/java/org/spdx/spreadsheetstore/ods/OdsComment.java b/src/main/java/org/spdx/spreadsheetstore/ods/OdsComment.java
new file mode 100644
index 0000000..3524f77
--- /dev/null
+++ b/src/main/java/org/spdx/spreadsheetstore/ods/OdsComment.java
@@ -0,0 +1,127 @@
+/*
+ * 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.time.LocalDateTime;
+import com.github.miachm.sods.OfficeAnnotation;
+import org.apache.poi.ss.usermodel.ClientAnchor;
+import org.apache.poi.ss.usermodel.Comment;
+import org.apache.poi.ss.usermodel.RichTextString;
+import org.apache.poi.ss.util.CellAddress;
+
+/**
+ * Adapter for Apache POI {@link Comment} over SODS {@link OfficeAnnotation}.
+ */
+public class OdsComment implements Comment {
+
+ private OfficeAnnotation annotation;
+ private String author = "";
+ private CellAddress address;
+
+ /**
+ * Creates an ODS comment wrapping an existing SODS OfficeAnnotation.
+ *
+ * @param annotation Underlying SODS OfficeAnnotation instance.
+ */
+ public OdsComment(OfficeAnnotation annotation) {
+ this.annotation = annotation;
+ }
+
+ /**
+ * Creates an ODS comment with specified text content.
+ *
+ * @param text Comment text.
+ */
+ public OdsComment(String text) {
+ this.annotation = new OfficeAnnotation(text, LocalDateTime.now());
+ }
+
+ /**
+ * Returns the underlying SODS OfficeAnnotation.
+ *
+ * @return The underlying SODS OfficeAnnotation.
+ */
+ OfficeAnnotation getAnnotation() {
+ return annotation;
+ }
+
+ @Override
+ public void setVisible(boolean visible) {
+ }
+
+ @Override
+ public boolean isVisible() {
+ return true;
+ }
+
+ @Override
+ public CellAddress getAddress() {
+ return address;
+ }
+
+ @Override
+ public void setAddress(CellAddress address) {
+ this.address = address;
+ }
+
+ @Override
+ public void setAddress(int row, int col) {
+ this.address = new CellAddress(row, col);
+ }
+
+ @Override
+ public int getRow() {
+ return address != null ? address.getRow() : 0;
+ }
+
+ @Override
+ public void setRow(int row) {
+ int col = getColumn();
+ this.address = new CellAddress(row, col);
+ }
+
+ @Override
+ public int getColumn() {
+ return address != null ? address.getColumn() : 0;
+ }
+
+ @Override
+ public void setColumn(int col) {
+ int row = getRow();
+ this.address = new CellAddress(row, col);
+ }
+
+ @Override
+ public String getAuthor() {
+ return author;
+ }
+
+ @Override
+ public void setAuthor(String author) {
+ this.author = author;
+ }
+
+ @Override
+ public RichTextString getString() {
+ return new OdsRichTextString(annotation != null ? annotation.getMsg() : "");
+ }
+
+ @Override
+ public void setString(RichTextString string) {
+ String text = string != null ? string.getString() : "";
+ this.annotation = new OfficeAnnotation(text, LocalDateTime.now());
+ }
+
+ public void setString(String string) {
+ this.annotation = new OfficeAnnotation(string != null ? string : "", LocalDateTime.now());
+ }
+
+ @Override
+ public ClientAnchor getClientAnchor() {
+ return null;
+ }
+}
diff --git a/src/main/java/org/spdx/spreadsheetstore/ods/OdsCreationHelper.java b/src/main/java/org/spdx/spreadsheetstore/ods/OdsCreationHelper.java
index 20bcd39..69139c2 100644
--- a/src/main/java/org/spdx/spreadsheetstore/ods/OdsCreationHelper.java
+++ b/src/main/java/org/spdx/spreadsheetstore/ods/OdsCreationHelper.java
@@ -7,7 +7,13 @@
package org.spdx.spreadsheetstore.ods;
import org.apache.poi.common.usermodel.HyperlinkType;
-import org.apache.poi.ss.usermodel.*;
+import org.apache.poi.ss.usermodel.ClientAnchor;
+import org.apache.poi.ss.usermodel.CreationHelper;
+import org.apache.poi.ss.usermodel.DataFormat;
+import org.apache.poi.ss.usermodel.ExtendedColor;
+import org.apache.poi.ss.usermodel.FormulaEvaluator;
+import org.apache.poi.ss.usermodel.Hyperlink;
+import org.apache.poi.ss.usermodel.RichTextString;
import org.apache.poi.ss.util.AreaReference;
import org.apache.poi.ss.util.CellReference;
@@ -20,6 +26,11 @@ public class OdsCreationHelper implements CreationHelper {
private final OdsWorkbook workbook;
private final OdsDataFormat dataFormat = new OdsDataFormat();
+ /**
+ * Creates an ODS creation helper adapter.
+ *
+ * @param workbook Parent ODS workbook.
+ */
public OdsCreationHelper(OdsWorkbook workbook) {
this.workbook = workbook;
}
@@ -61,6 +72,6 @@ public AreaReference createAreaReference(CellReference topLeft, CellReference bo
@Override
public RichTextString createRichTextString(String text) {
- return new org.apache.poi.xssf.usermodel.XSSFRichTextString(text);
+ return new OdsRichTextString(text);
}
}
diff --git a/src/main/java/org/spdx/spreadsheetstore/ods/OdsDataFormat.java b/src/main/java/org/spdx/spreadsheetstore/ods/OdsDataFormat.java
index d902cd3..d1ec22a 100644
--- a/src/main/java/org/spdx/spreadsheetstore/ods/OdsDataFormat.java
+++ b/src/main/java/org/spdx/spreadsheetstore/ods/OdsDataFormat.java
@@ -6,6 +6,7 @@
*/
package org.spdx.spreadsheetstore.ods;
+import org.apache.poi.ss.usermodel.BuiltinFormats;
import org.apache.poi.ss.usermodel.DataFormat;
import java.util.ArrayList;
import java.util.List;
@@ -28,7 +29,11 @@ public class OdsDataFormat implements DataFormat {
public short getFormat(String format) {
int index = formats.indexOf(format);
if (index == -1) {
- index = formats.size();
+ int builtinIndex = BuiltinFormats.getBuiltinFormat(format);
+ if (builtinIndex != -1) {
+ return (short) builtinIndex;
+ }
+ index = formats.size() + 165;
formats.add(format);
}
return (short) index;
@@ -36,9 +41,12 @@ public short getFormat(String format) {
@Override
public String getFormat(short index) {
+ if (index >= 165 && (index - 165) < formats.size()) {
+ return formats.get(index - 165);
+ }
if (index >= 0 && index < formats.size()) {
return formats.get(index);
}
- return null;
+ return BuiltinFormats.getBuiltinFormat(index);
}
}
diff --git a/src/main/java/org/spdx/spreadsheetstore/ods/OdsRichTextString.java b/src/main/java/org/spdx/spreadsheetstore/ods/OdsRichTextString.java
new file mode 100644
index 0000000..b73026d
--- /dev/null
+++ b/src/main/java/org/spdx/spreadsheetstore/ods/OdsRichTextString.java
@@ -0,0 +1,67 @@
+/*
+ * SPDX-FileContributor: Arthit Suriyawongkul
+ * SPDX-FileCopyrightText: 2026 SPDX Contributors
+ * SPDX-FileType: SOURCE
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package org.spdx.spreadsheetstore.ods;
+
+import org.apache.poi.ss.usermodel.Font;
+import org.apache.poi.ss.usermodel.RichTextString;
+
+/**
+ * Adapter for Apache POI {@link RichTextString} for ODS documents.
+ */
+public class OdsRichTextString implements RichTextString {
+
+ private String text;
+
+ public OdsRichTextString(String text) {
+ this.text = text != null ? text : "";
+ }
+
+ @Override
+ public void applyFont(int startIndex, int endIndex, short fontIndex) {
+ }
+
+ @Override
+ public void applyFont(int startIndex, int endIndex, Font font) {
+ }
+
+ @Override
+ public void applyFont(Font font) {
+ }
+
+ @Override
+ public void clearFormatting() {
+ }
+
+ @Override
+ public String getString() {
+ return text;
+ }
+
+ @Override
+ public int length() {
+ return text.length();
+ }
+
+ @Override
+ public int numFormattingRuns() {
+ return 0;
+ }
+
+ @Override
+ public int getIndexOfFormattingRun(int index) {
+ return 0;
+ }
+
+ @Override
+ public void applyFont(short fontIndex) {
+ }
+
+ @Override
+ public String toString() {
+ return text;
+ }
+}
diff --git a/src/main/java/org/spdx/spreadsheetstore/ods/OdsRow.java b/src/main/java/org/spdx/spreadsheetstore/ods/OdsRow.java
index 3a0bcbb..260c29d 100644
--- a/src/main/java/org/spdx/spreadsheetstore/ods/OdsRow.java
+++ b/src/main/java/org/spdx/spreadsheetstore/ods/OdsRow.java
@@ -6,8 +6,16 @@
*/
package org.spdx.spreadsheetstore.ods;
-import java.util.*;
-import org.apache.poi.ss.usermodel.*;
+import java.util.Iterator;
+import java.util.NavigableMap;
+import java.util.NoSuchElementException;
+import java.util.TreeMap;
+
+import org.apache.poi.ss.usermodel.Cell;
+import org.apache.poi.ss.usermodel.CellStyle;
+import org.apache.poi.ss.usermodel.CellType;
+import org.apache.poi.ss.usermodel.Row;
+import org.apache.poi.ss.usermodel.Sheet;
/**
* Adapter for Apache POI {@link Row} managing a specific row in a SODS {@link com.github.miachm.sods.Sheet}.
@@ -15,13 +23,22 @@
public class OdsRow implements Row {
private final OdsSheet sheet;
private final int rowNum;
- private final Map cells = new TreeMap<>();
+ private final NavigableMap cells = new TreeMap<>();
+ /**
+ * Creates an ODS row wrapper around a specific row index in a sheet.
+ *
+ * @param sheet Parent ODS sheet adapter.
+ * @param rowNum Zero-based row index.
+ */
public OdsRow(OdsSheet sheet, int rowNum) {
this.sheet = sheet;
this.rowNum = rowNum;
}
+ /**
+ * Clears all cells in this row, setting their SODS range values to null.
+ */
public void clear() {
for (OdsCell cell : cells.values()) {
cell.getSodsRange().setValue(null);
@@ -29,41 +46,47 @@ public void clear() {
cells.clear();
}
+ private static final double MM_PER_PT = 25.4 / 72.0;
+
@Override
public Cell createCell(int column) {
return createCell(column, CellType.BLANK);
}
@Override
- public Cell createCell(int column, CellType type) {
+ public synchronized Cell createCell(int column, CellType type) {
+ if (column < 0) {
+ throw new IllegalArgumentException("Column index must be >= 0");
+ }
com.github.miachm.sods.Sheet sodsSheet = sheet.getSodsSheet();
+ int currentRows = sodsSheet.getMaxRows();
+ if (rowNum >= currentRows) {
+ sodsSheet.appendRows(rowNum - currentRows + 1);
+ }
int currentCols = sodsSheet.getMaxColumns();
if (column >= currentCols) {
- for (int i = currentCols; i <= column; i++) {
- sodsSheet.appendColumn();
- }
+ sodsSheet.appendColumns(column - currentCols + 1);
}
com.github.miachm.sods.Range range = sodsSheet.getRange(rowNum, column);
OdsCell cell = new OdsCell(this, column, range);
+ if (type != null) {
+ cell.setCellType(type);
+ }
cells.put(column, cell);
return cell;
}
@Override
- public Cell getCell(int cellnum) {
- // If it exists in cells map, return it
- if (cells.containsKey(cellnum)) {
- return cells.get(cellnum);
+ public synchronized Cell getCell(int cellnum) {
+ OdsCell cached = cells.get(cellnum);
+ if (cached != null) {
+ return cached;
}
- // If it exists in the underlying SODS sheet but not yet wrapped, wrap and return
com.github.miachm.sods.Sheet sodsSheet = sheet.getSodsSheet();
- if (cellnum >= 0 && cellnum < sodsSheet.getMaxColumns()) {
+ if (cellnum >= 0 && cellnum < sodsSheet.getMaxColumns() && rowNum >= 0 && rowNum < sodsSheet.getMaxRows()) {
com.github.miachm.sods.Range range = sodsSheet.getRange(rowNum, cellnum);
- Object val = range.getValue();
- if (val != null) {
- if (val instanceof String && ((String) val).trim().isEmpty()) {
- return null;
- }
+ if (range.getValue() != null || range.getFormula() != null || range.getAnnotation() != null
+ || (range.getStyle() != null && !range.getStyle().equals(new com.github.miachm.sods.Style()))) {
OdsCell cell = new OdsCell(this, cellnum, range);
cells.put(cellnum, cell);
return cell;
@@ -74,32 +97,34 @@ public Cell getCell(int cellnum) {
@Override
public short getFirstCellNum() {
- com.github.miachm.sods.Sheet sodsSheet = sheet.getSodsSheet();
- int maxCols = sodsSheet.getMaxColumns();
- for (int col = 0; col < maxCols; col++) {
- if (cells.containsKey(col)) {
- return (short) col;
- }
- Object val = sodsSheet.getRange(rowNum, col).getValue();
- if (val != null) {
- return (short) col;
+ com.github.miachm.sods.Range dataRange = sheet.getSodsSheet().getDataRange();
+ if (!cells.isEmpty()) {
+ int col = cells.firstKey();
+ if (dataRange != null && rowNum >= dataRange.getRow() && rowNum <= dataRange.getLastRow()) {
+ col = Math.min(col, dataRange.getColumn());
}
+ return col > Short.MAX_VALUE ? Short.MAX_VALUE : (short) col;
+ }
+ if (dataRange != null && rowNum >= dataRange.getRow() && rowNum <= dataRange.getLastRow()) {
+ int col = dataRange.getColumn();
+ return col > Short.MAX_VALUE ? Short.MAX_VALUE : (short) col;
}
return -1;
}
@Override
public short getLastCellNum() {
- com.github.miachm.sods.Sheet sodsSheet = sheet.getSodsSheet();
- int maxCols = sodsSheet.getMaxColumns();
- for (int col = maxCols - 1; col >= 0; col--) {
- if (cells.containsKey(col)) {
- return (short) (col + 1);
- }
- Object val = sodsSheet.getRange(rowNum, col).getValue();
- if (val != null) {
- return (short) (col + 1);
+ com.github.miachm.sods.Range dataRange = sheet.getSodsSheet().getDataRange();
+ if (!cells.isEmpty()) {
+ int nextCol = cells.lastKey() + 1;
+ if (dataRange != null && rowNum >= dataRange.getRow() && rowNum <= dataRange.getLastRow()) {
+ nextCol = Math.max(nextCol, dataRange.getLastColumn() + 1);
}
+ return nextCol > Short.MAX_VALUE ? Short.MAX_VALUE : (short) nextCol;
+ }
+ if (dataRange != null && rowNum >= dataRange.getRow() && rowNum <= dataRange.getLastRow()) {
+ int nextCol = dataRange.getLastColumn() + 1;
+ return nextCol > Short.MAX_VALUE ? Short.MAX_VALUE : (short) nextCol;
}
return -1;
}
@@ -110,21 +135,82 @@ public int getRowNum() {
}
@Override
- public void setHeight(short height) {}
+ public void setHeightInPoints(float height) {
+ int currentRows = sheet.getSodsSheet().getMaxRows();
+ if (rowNum >= currentRows) {
+ sheet.getSodsSheet().appendRows(rowNum - currentRows + 1);
+ }
+ // Convert POI points to SODS millimeters (1 pt = 25.4 mm / 72 pt).
+ sheet.getSodsSheet().setRowHeight(rowNum, height * MM_PER_PT);
+ }
+
@Override
- public void setHeightInPoints(float height) {}
+ public void setHeight(short height) {
+ setHeightInPoints(height / 20.0f);
+ }
+
@Override
- public short getHeight() { return 255; }
+ public float getHeightInPoints() {
+ Double heightMm = sheet.getSodsSheet().getRowHeight(rowNum);
+ if (heightMm != null) {
+ // Convert SODS millimeters back to POI points.
+ return (float) (heightMm / MM_PER_PT);
+ }
+ return 15.0f;
+ }
+
@Override
- public float getHeightInPoints() { return 15.0f; }
+ public short getHeight() {
+ return (short) Math.round(getHeightInPoints() * 20.0f);
+ }
@Override
public Sheet getSheet() {
return sheet;
}
@Override
public Iterator cellIterator() {
- List list = new ArrayList<>(cells.values());
- return list.iterator();
+ return new Iterator() {
+ private int curCol = getFirstCellNum();
+ private final int lastCol = getLastCellNum();
+ private Cell nextCell = null;
+
+ private void advance() {
+ nextCell = null;
+ if (curCol < 0) return;
+ while (curCol >= 0 && curCol < lastCol) {
+ Cell c = getCell(curCol);
+ curCol++;
+ if (c != null) {
+ nextCell = c;
+ break;
+ }
+ }
+ }
+
+ {
+ advance();
+ }
+
+ @Override
+ public boolean hasNext() {
+ return nextCell != null;
+ }
+
+ @Override
+ public Cell next() {
+ if (!hasNext()) {
+ throw new NoSuchElementException();
+ }
+ Cell res = nextCell;
+ advance();
+ return res;
+ }
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException("Remove not supported on cell iterator");
+ }
+ };
}
@Override
public Iterator iterator() {
@@ -159,16 +245,43 @@ public void shiftCellsRight(int firstShiftColumnIndex, int lastShiftColumnIndex,
public int getOutlineLevel() { return 0; }
@Override
- public boolean getZeroHeight() { return false; }
+ public boolean getZeroHeight() {
+ return sheet.getSodsSheet().rowIsHidden(rowNum);
+ }
@Override
- public void setZeroHeight(boolean zHeight) {}
+ public void setZeroHeight(boolean zHeight) {
+ int currentRows = sheet.getSodsSheet().getMaxRows();
+ if (rowNum >= currentRows) {
+ sheet.getSodsSheet().appendRows(rowNum - currentRows + 1);
+ }
+ if (zHeight) {
+ sheet.getSodsSheet().hideRow(rowNum);
+ } else {
+ sheet.getSodsSheet().showRow(rowNum);
+ }
+ }
@Override
public Cell getCell(int cellnum, MissingCellPolicy policy) {
+ if (policy == null) {
+ policy = sheet.getWorkbook().getMissingCellPolicy();
+ }
Cell cell = getCell(cellnum);
- if (cell == null && policy == MissingCellPolicy.CREATE_NULL_AS_BLANK) {
- return createCell(cellnum);
+ if (policy == MissingCellPolicy.RETURN_NULL_AND_BLANK) {
+ return cell;
+ }
+ if (policy == MissingCellPolicy.RETURN_BLANK_AS_NULL) {
+ if (cell == null || cell.getCellType() == CellType.BLANK) {
+ return null;
+ }
+ return cell;
+ }
+ if (policy == MissingCellPolicy.CREATE_NULL_AS_BLANK) {
+ if (cell == null) {
+ return createCell(cellnum, CellType.BLANK);
+ }
+ return cell;
}
return cell;
}
diff --git a/src/main/java/org/spdx/spreadsheetstore/ods/OdsSheet.java b/src/main/java/org/spdx/spreadsheetstore/ods/OdsSheet.java
index 49e4f12..421ddf0 100644
--- a/src/main/java/org/spdx/spreadsheetstore/ods/OdsSheet.java
+++ b/src/main/java/org/spdx/spreadsheetstore/ods/OdsSheet.java
@@ -6,8 +6,34 @@
*/
package org.spdx.spreadsheetstore.ods;
-import java.util.*;
-import org.apache.poi.ss.usermodel.*;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableMap;
+import java.util.TreeMap;
+
+import org.apache.poi.ss.usermodel.AutoFilter;
+import org.apache.poi.ss.usermodel.Cell;
+import org.apache.poi.ss.usermodel.CellRange;
+import org.apache.poi.ss.usermodel.CellStyle;
+import org.apache.poi.ss.usermodel.Comment;
+import org.apache.poi.ss.usermodel.DataValidation;
+import org.apache.poi.ss.usermodel.DataValidationHelper;
+import org.apache.poi.ss.usermodel.Drawing;
+import org.apache.poi.ss.usermodel.Footer;
+import org.apache.poi.ss.usermodel.Header;
+import org.apache.poi.ss.usermodel.Hyperlink;
+import org.apache.poi.ss.usermodel.PageMargin;
+import org.apache.poi.ss.usermodel.PaneType;
+import org.apache.poi.ss.usermodel.PrintSetup;
+import org.apache.poi.ss.usermodel.Row;
+import org.apache.poi.ss.usermodel.Sheet;
+import org.apache.poi.ss.usermodel.SheetConditionalFormatting;
+import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.util.CellAddress;
import org.apache.poi.ss.util.CellRangeAddress;
@@ -21,25 +47,36 @@ public class OdsSheet implements Sheet {
private final com.github.miachm.sods.Sheet sodsSheet;
private final NavigableMap rows = new TreeMap<>();
+ private final List mergedRegions = new ArrayList<>();
+
+ /**
+ * Creates an ODS sheet wrapper around a SODS sheet.
+ *
+ * @param workbook Parent ODS workbook adapter.
+ * @param sodsSheet Underlying SODS sheet instance.
+ */
public OdsSheet(OdsWorkbook workbook, com.github.miachm.sods.Sheet sodsSheet) {
this.workbook = workbook;
this.sodsSheet = sodsSheet;
- for (int r = 0; r < sodsSheet.getMaxRows(); r++) {
- rows.put(r, new OdsRow(this, r));
- }
}
- public com.github.miachm.sods.Sheet getSodsSheet() {
+ /**
+ * Returns the underlying SODS sheet instance.
+ *
+ * @return The underlying SODS sheet.
+ */
+ com.github.miachm.sods.Sheet getSodsSheet() {
return this.sodsSheet;
}
@Override
- public Row createRow(int rownum) {
+ public synchronized Row createRow(int rownum) {
+ if (rownum < 0) {
+ throw new IllegalArgumentException("Row number must be >= 0");
+ }
int currentRows = sodsSheet.getMaxRows();
if (rownum >= currentRows) {
- for (int i = currentRows; i <= rownum; i++) {
- sodsSheet.appendRow();
- }
+ sodsSheet.appendRows(rownum - currentRows + 1);
}
OdsRow row = new OdsRow(this, rownum);
rows.put(rownum, row);
@@ -47,8 +84,17 @@ public Row createRow(int rownum) {
}
@Override
- public Row getRow(int rownum) {
- return rows.get(rownum);
+ public synchronized Row getRow(int rownum) {
+ OdsRow row = rows.get(rownum);
+ if (row != null) {
+ return row;
+ }
+ if (rownum >= 0 && rownum < sodsSheet.getMaxRows()) {
+ OdsRow newRow = new OdsRow(this, rownum);
+ rows.put(rownum, newRow);
+ return newRow;
+ }
+ return null;
}
@Override
@@ -61,23 +107,82 @@ public void removeRow(Row row) {
}
}
+ private boolean hasData() {
+ if (!rows.isEmpty()) {
+ return true;
+ }
+ com.github.miachm.sods.Range dataRange = sodsSheet.getDataRange();
+ if (dataRange == null) {
+ return false;
+ }
+ for (int r = dataRange.getRow(); r <= dataRange.getLastRow(); r++) {
+ for (int c = dataRange.getColumn(); c <= dataRange.getLastColumn(); c++) {
+ com.github.miachm.sods.Range range = sodsSheet.getRange(r, c);
+ if (range.getValue() != null || range.getFormula() != null || range.getAnnotation() != null) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
@Override
public int getFirstRowNum() {
- return rows.isEmpty() ? 0 : rows.keySet().iterator().next();
+ if (!hasData()) {
+ return -1;
+ }
+ com.github.miachm.sods.Range dataRange = sodsSheet.getDataRange();
+ if (!rows.isEmpty()) {
+ int firstRow = rows.firstKey();
+ if (dataRange != null) {
+ return Math.min(firstRow, dataRange.getRow());
+ }
+ return firstRow;
+ }
+ return dataRange.getRow();
}
@Override
public int getLastRowNum() {
- return rows.isEmpty() ? 0 : rows.lastKey();
+ if (!hasData()) {
+ return -1;
+ }
+ com.github.miachm.sods.Range dataRange = sodsSheet.getDataRange();
+ if (!rows.isEmpty()) {
+ int lastRow = rows.lastKey();
+ if (dataRange != null) {
+ return Math.max(lastRow, dataRange.getLastRow());
+ }
+ return lastRow;
+ }
+ return dataRange.getLastRow();
}
@Override
public void setColumnWidth(int columnIndex, int width) {
+ if (columnIndex < 0) {
+ throw new IllegalArgumentException("Column index must be >= 0");
+ }
+ int currentCols = sodsSheet.getMaxColumns();
+ if (columnIndex >= currentCols) {
+ sodsSheet.appendColumns(columnIndex - currentCols + 1);
+ }
+ // Convert POI 1/256th character units to SODS millimeters.
+ // 1 character width (e.g. 10pt Arial '0') = 5.25 pt = 5.25 * (25.4/72) mm ≈ 1.852 mm.
+ double widthMm = (width / 256.0) * 1.852;
+ sodsSheet.setColumnWidth(columnIndex, widthMm);
}
@Override
public int getColumnWidth(int columnIndex) {
- return 2048;
+ if (columnIndex >= 0 && columnIndex < sodsSheet.getMaxColumns()) {
+ double widthMm = sodsSheet.getColumnWidth(columnIndex);
+ if (widthMm > 0) {
+ // Convert SODS millimeters back to POI 1/256th character units.
+ return (int) Math.round((widthMm / 1.852) * 256.0);
+ }
+ }
+ return 2048; // default width
}
@Override
@@ -109,19 +214,98 @@ public void setPrintRowAndColumnHeadings(boolean show) {}
@Override
public void setActiveCell(CellAddress address) {}
@Override
- public void removeMergedRegion(int index) {}
+ public void removeMergedRegion(int index) {
+ if (index < 0 || index >= mergedRegions.size()) {
+ throw new IllegalArgumentException("Invalid merged region index: " + index);
+ }
+ CellRangeAddress region = mergedRegions.remove(index);
+ int firstRow = region.getFirstRow();
+ int firstCol = region.getFirstColumn();
+ int numRows = region.getLastRow() - firstRow + 1;
+ int numCols = region.getLastColumn() - firstCol + 1;
+ com.github.miachm.sods.Range range = sodsSheet.getRange(firstRow, firstCol, numRows, numCols);
+ range.split();
+ }
+
@Override
- public void removeMergedRegions(Collection indices) {}
+ public void removeMergedRegions(Collection indices) {
+ if (indices != null) {
+ List sorted = new ArrayList<>();
+ for (Integer idx : indices) {
+ if (idx != null) {
+ sorted.add(idx);
+ }
+ }
+ Collections.sort(sorted, Collections.reverseOrder());
+ for (int idx : sorted) {
+ if (idx >= 0 && idx < mergedRegions.size()) {
+ removeMergedRegion(idx);
+ }
+ }
+ }
+ }
+
@Override
- public int getNumMergedRegions() { return 0; }
+ public int getNumMergedRegions() {
+ return mergedRegions.size();
+ }
+
@Override
- public CellRangeAddress getMergedRegion(int index) { return null; }
+ public CellRangeAddress getMergedRegion(int index) {
+ if (index < 0 || index >= mergedRegions.size()) {
+ throw new IllegalArgumentException("Invalid merged region index: " + index);
+ }
+ return mergedRegions.get(index);
+ }
+
@Override
- public List getMergedRegions() { return new ArrayList<>(); }
+ public List getMergedRegions() {
+ return new ArrayList<>(mergedRegions);
+ }
@Override
public Iterator rowIterator() {
- List list = new ArrayList<>(rows.values());
- return list.iterator();
+ return new Iterator() {
+ private int curRow = getFirstRowNum();
+ private final int lastRow = getLastRowNum();
+ private Row nextRow = null;
+
+ private void advance() {
+ nextRow = null;
+ if (curRow < 0) return;
+ while (curRow <= lastRow) {
+ Row r = getRow(curRow);
+ curRow++;
+ if (r != null) {
+ nextRow = r;
+ break;
+ }
+ }
+ }
+
+ {
+ advance();
+ }
+
+ @Override
+ public boolean hasNext() {
+ return nextRow != null;
+ }
+
+ @Override
+ public Row next() {
+ if (!hasNext()) {
+ throw new java.util.NoSuchElementException();
+ }
+ Row res = nextRow;
+ advance();
+ return res;
+ }
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException("Remove not supported on row iterator");
+ }
+ };
}
@Override
public Iterator iterator() {
@@ -158,9 +342,32 @@ public void setRowSumsRight(boolean value) {}
@Override
public int getPhysicalNumberOfRows() { return rows.size(); }
@Override
- public int addMergedRegion(CellRangeAddress region) { return 0; }
+ public int addMergedRegion(CellRangeAddress region) {
+ if (region == null) {
+ throw new IllegalArgumentException("Merged region cannot be null");
+ }
+ int firstRow = region.getFirstRow();
+ int lastRow = region.getLastRow();
+ int firstCol = region.getFirstColumn();
+ int lastCol = region.getLastColumn();
+ if (lastRow >= sodsSheet.getMaxRows()) {
+ sodsSheet.appendRows(lastRow - sodsSheet.getMaxRows() + 1);
+ }
+ if (lastCol >= sodsSheet.getMaxColumns()) {
+ sodsSheet.appendColumns(lastCol - sodsSheet.getMaxColumns() + 1);
+ }
+ int numRows = lastRow - firstRow + 1;
+ int numCols = lastCol - firstCol + 1;
+ com.github.miachm.sods.Range range = sodsSheet.getRange(firstRow, firstCol, numRows, numCols);
+ range.merge();
+ mergedRegions.add(region);
+ return mergedRegions.size() - 1;
+ }
+
@Override
- public int addMergedRegionUnsafe(CellRangeAddress region) { return 0; }
+ public int addMergedRegionUnsafe(CellRangeAddress region) {
+ return addMergedRegion(region);
+ }
@Override
public CellAddress getActiveCell() { return null; }
@@ -221,11 +428,44 @@ public void addValidationData(DataValidation dataValidation) {}
@Override
public Map getCellComments() {
- return new HashMap<>();
+ Map map = new HashMap<>();
+ com.github.miachm.sods.Range dataRange = sodsSheet.getDataRange();
+ if (dataRange == null) {
+ return map;
+ }
+ int startRow = dataRange.getRow();
+ int endRow = dataRange.getLastRow();
+ int startCol = dataRange.getColumn();
+ int endCol = dataRange.getLastColumn();
+ for (int r = startRow; r <= endRow; r++) {
+ for (int c = startCol; c <= endCol; c++) {
+ com.github.miachm.sods.Range range = sodsSheet.getRange(r, c);
+ if (range.getAnnotation() != null) {
+ CellAddress addr = new CellAddress(r, c);
+ OdsComment comment = new OdsComment(range.getAnnotation());
+ comment.setAddress(addr);
+ map.put(addr, comment);
+ }
+ }
+ }
+ return map;
}
@Override
- public Comment getCellComment(CellAddress address) { return null; }
+ public Comment getCellComment(CellAddress address) {
+ if (address == null) return null;
+ int r = address.getRow();
+ int c = address.getColumn();
+ if (r >= 0 && r < sodsSheet.getMaxRows() && c >= 0 && c < sodsSheet.getMaxColumns()) {
+ com.github.miachm.sods.Range range = sodsSheet.getRange(r, c);
+ if (range.getAnnotation() != null) {
+ OdsComment comment = new OdsComment(range.getAnnotation());
+ comment.setAddress(address);
+ return comment;
+ }
+ }
+ return null;
+ }
@Override
public void autoSizeColumn(int columnIndex, boolean useMergedCells) {}
@@ -278,17 +518,38 @@ public void removeColumnBreak(int column) {}
@Override
public boolean isDisplayRowColHeadings() { return true; }
+ private boolean protectedState = false;
+ private final Map marginMap = new HashMap<>();
+ private int freezeColSplit = 0;
+ private int freezeRowSplit = 0;
+
@Override
public boolean isDisplayFormulas() { return false; }
@Override
- public org.apache.poi.ss.util.PaneInformation getPaneInformation() { return null; }
+ public org.apache.poi.ss.util.PaneInformation getPaneInformation() {
+ if (freezeColSplit > 0 || freezeRowSplit > 0) {
+ return new org.apache.poi.ss.util.PaneInformation((short) freezeColSplit, (short) freezeRowSplit, (short) freezeRowSplit, (short) freezeColSplit, (byte) 0, true);
+ }
+ return null;
+ }
@Override
- public void createFreezePane(int colSplit, int rowSplit, int leftmostColumn, int topRow) {}
+ public void createFreezePane(int colSplit, int rowSplit, int leftmostColumn, int topRow) {
+ createFreezePane(colSplit, rowSplit);
+ }
@Override
- public void createFreezePane(int colSplit, int rowSplit) {}
+ public void createFreezePane(int colSplit, int rowSplit) {
+ this.freezeColSplit = colSplit;
+ this.freezeRowSplit = rowSplit;
+ if (rowSplit > 0) {
+ sodsSheet.freezeRows(rowSplit);
+ }
+ if (colSplit > 0) {
+ sodsSheet.freezeColumns(colSplit);
+ }
+ }
@Override
public void createSplitPane(int xSplit, int ySplit, int leftmostColumn, int topRow, PaneType activePane) {}
@@ -321,28 +582,103 @@ public void setZoom(int scale) {}
public boolean getScenarioProtect() { return false; }
@Override
- public void protectSheet(String password) {}
+ public void protectSheet(String password) {
+ this.protectedState = true;
+ if (password != null && !password.isEmpty()) {
+ try {
+ sodsSheet.setPassword(password);
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to set sheet password", e);
+ }
+ }
+ }
@Override
- public boolean getProtect() { return false; }
+ public boolean getProtect() {
+ return protectedState || sodsSheet.isProtected();
+ }
@Override
- public double getMargin(short margin) { return 0; }
+ public double getMargin(short margin) {
+ Double val = marginMap.get(margin);
+ return val != null ? val : 0.75;
+ }
@Override
- public void setMargin(short margin, double size) {}
+ public void setMargin(short margin, double size) {
+ marginMap.put(margin, size);
+ }
@Override
public void setSelected(boolean sel) {}
- @Override
- public Header getHeader() { return null; }
+ private static class OdsHeaderFooterStub implements Header, Footer {
+ private String left = "";
+ private String center = "";
+ private String right = "";
+
+ @Override public String getLeft() { return left; }
+ @Override public void setLeft(String newLeft) { this.left = newLeft != null ? newLeft : ""; }
+ @Override public String getCenter() { return center; }
+ @Override public void setCenter(String newCenter) { this.center = newCenter != null ? newCenter : ""; }
+ @Override public String getRight() { return right; }
+ @Override public void setRight(String newRight) { this.right = newRight != null ? newRight : ""; }
+ }
- @Override
- public Footer getFooter() { return null; }
+ private final OdsHeaderFooterStub headerStub = new OdsHeaderFooterStub();
+ private final OdsHeaderFooterStub footerStub = new OdsHeaderFooterStub();
+
+ @Override
+ public Header getHeader() { return headerStub; }
+
+ @Override
+ public Footer getFooter() { return footerStub; }
+
+ private static class OdsPrintSetupStub implements PrintSetup {
+ private boolean landscape = false;
+ private short paperSize = LETTER_PAPERSIZE;
+ @Override public void setPaperSize(short size) { this.paperSize = size; }
+ @Override public void setScale(short scale) {}
+ @Override public void setPageStart(short start) {}
+ @Override public void setFitWidth(short width) {}
+ @Override public void setFitHeight(short height) {}
+ @Override public void setLeftToRight(boolean ltr) {}
+ @Override public void setLandscape(boolean ls) { this.landscape = ls; }
+ @Override public void setValidSettings(boolean valid) {}
+ @Override public void setNoColor(boolean mono) {}
+ @Override public void setDraft(boolean draft) {}
+ @Override public void setNotes(boolean printNotes) {}
+ @Override public void setNoOrientation(boolean orientation) {}
+ @Override public void setUsePage(boolean page) {}
+ @Override public void setHResolution(short resolution) {}
+ @Override public void setVResolution(short resolution) {}
+ @Override public void setCopies(short copies) {}
+ @Override public short getPaperSize() { return paperSize; }
+ @Override public short getScale() { return 100; }
+ @Override public short getPageStart() { return 1; }
+ @Override public short getFitWidth() { return 1; }
+ @Override public short getFitHeight() { return 1; }
+ @Override public boolean getLeftToRight() { return false; }
+ @Override public boolean getLandscape() { return landscape; }
+ @Override public boolean getValidSettings() { return true; }
+ @Override public boolean getNoColor() { return false; }
+ @Override public boolean getDraft() { return false; }
+ @Override public boolean getNotes() { return false; }
+ @Override public boolean getNoOrientation() { return false; }
+ @Override public boolean getUsePage() { return false; }
+ @Override public short getHResolution() { return 300; }
+ @Override public short getVResolution() { return 300; }
+ @Override public void setHeaderMargin(double headmargin) {}
+ @Override public void setFooterMargin(double footmargin) {}
+ @Override public double getHeaderMargin() { return 0.5; }
+ @Override public double getFooterMargin() { return 0.5; }
+ @Override public short getCopies() { return 1; }
+ }
+
+ private final OdsPrintSetupStub printSetupStub = new OdsPrintSetupStub();
@Override
- public PrintSetup getPrintSetup() { return null; }
+ public PrintSetup getPrintSetup() { return printSetupStub; }
@Override
public boolean getFitToPage() { return false; }
@@ -402,8 +738,25 @@ public void setDefaultColumnWidth(int width) {}
public void setRightToLeft(boolean value) {}
@Override
- public boolean isColumnHidden(int columnIndex) { return false; }
+ public boolean isColumnHidden(int columnIndex) {
+ if (columnIndex >= 0 && columnIndex < sodsSheet.getMaxColumns()) {
+ return sodsSheet.columnIsHidden(columnIndex);
+ }
+ return false;
+ }
@Override
- public void setColumnHidden(int columnIndex, boolean hidden) {}
+ public void setColumnHidden(int columnIndex, boolean hidden) {
+ if (columnIndex >= 0) {
+ int currentCols = sodsSheet.getMaxColumns();
+ if (columnIndex >= currentCols) {
+ sodsSheet.appendColumns(columnIndex - currentCols + 1);
+ }
+ if (hidden) {
+ sodsSheet.hideColumn(columnIndex);
+ } else {
+ sodsSheet.showColumn(columnIndex);
+ }
+ }
+ }
}
diff --git a/src/main/java/org/spdx/spreadsheetstore/ods/OdsWorkbook.java b/src/main/java/org/spdx/spreadsheetstore/ods/OdsWorkbook.java
index df6caaf..8b0b48e 100644
--- a/src/main/java/org/spdx/spreadsheetstore/ods/OdsWorkbook.java
+++ b/src/main/java/org/spdx/spreadsheetstore/ods/OdsWorkbook.java
@@ -14,7 +14,16 @@
import java.util.List;
import com.github.miachm.sods.SpreadSheet;
import org.apache.poi.ss.SpreadsheetVersion;
-import org.apache.poi.ss.usermodel.*;
+import org.apache.poi.ss.usermodel.CellStyle;
+import org.apache.poi.ss.usermodel.CreationHelper;
+import org.apache.poi.ss.usermodel.DataFormat;
+import org.apache.poi.ss.usermodel.Font;
+import org.apache.poi.ss.usermodel.Name;
+import org.apache.poi.ss.usermodel.PictureData;
+import org.apache.poi.ss.usermodel.Row;
+import org.apache.poi.ss.usermodel.Sheet;
+import org.apache.poi.ss.usermodel.SheetVisibility;
+import org.apache.poi.ss.usermodel.Workbook;
/**
* Adapter for Apache POI {@link Workbook} over a SODS {@link com.github.miachm.sods.SpreadSheet}.
@@ -27,11 +36,21 @@ public class OdsWorkbook implements Workbook {
private final List fonts = new ArrayList<>();
private final List styles = new ArrayList<>();
private final OdsCreationHelper creationHelper = new OdsCreationHelper(this);
+ private Row.MissingCellPolicy missingCellPolicy = Row.MissingCellPolicy.RETURN_NULL_AND_BLANK;
+ /**
+ * Creates an empty ODS workbook.
+ */
public OdsWorkbook() {
this.spreadSheet = new SpreadSheet();
}
+ /**
+ * Reads an ODS workbook from an input stream.
+ *
+ * @param is Input stream containing the ODS document.
+ * @throws IOException If an I/O error occurs while parsing.
+ */
public OdsWorkbook(InputStream is) throws IOException {
this.spreadSheet = new SpreadSheet(is);
for (com.github.miachm.sods.Sheet sodsSheet : spreadSheet.getSheets()) {
@@ -39,7 +58,12 @@ public OdsWorkbook(InputStream is) throws IOException {
}
}
- public SpreadSheet getSpreadSheet() {
+ /**
+ * Returns the underlying SODS SpreadSheet instance.
+ *
+ * @return The underlying SODS SpreadSheet.
+ */
+ SpreadSheet getSpreadSheet() {
return this.spreadSheet;
}
@@ -63,6 +87,14 @@ public void setFirstVisibleTab(int sheetIndex) {
@Override
public void setSheetOrder(String sheetname, int pos) {
+ int currentIndex = getSheetIndex(sheetname);
+ if (currentIndex >= 0 && pos >= 0 && pos < sheets.size()) {
+ OdsSheet sheet = sheets.remove(currentIndex);
+ sheets.add(pos, sheet);
+ com.github.miachm.sods.Sheet sodsSheet = sheet.getSodsSheet();
+ spreadSheet.deleteSheet(currentIndex);
+ spreadSheet.addSheet(sodsSheet, pos);
+ }
}
@Override
@@ -86,8 +118,11 @@ public String getSheetName(int sheet) {
@Override
public int getSheetIndex(String name) {
+ if (name == null) {
+ return -1;
+ }
for (int i = 0; i < sheets.size(); i++) {
- if (sheets.get(i).getSheetName().equalsIgnoreCase(name)) {
+ if (name.equalsIgnoreCase(sheets.get(i).getSheetName())) {
return i;
}
}
@@ -115,7 +150,27 @@ public Sheet createSheet(String sheetname) {
@Override
public Sheet cloneSheet(int sheetNum) {
- throw new UnsupportedOperationException("OdsWorkbook: cloneSheet not implemented");
+ if (sheetNum < 0 || sheetNum >= sheets.size()) {
+ throw new IllegalArgumentException("Invalid sheet index: " + sheetNum);
+ }
+ OdsSheet srcSheet = sheets.get(sheetNum);
+ try {
+ com.github.miachm.sods.Sheet clonedSods = (com.github.miachm.sods.Sheet) srcSheet.getSodsSheet().clone();
+ String baseName = srcSheet.getSheetName();
+ String clonedName = baseName + " (2)";
+ int count = 2;
+ while (getSheetIndex(clonedName) != -1) {
+ count++;
+ clonedName = baseName + " (" + count + ")";
+ }
+ clonedSods.setName(clonedName);
+ spreadSheet.appendSheet(clonedSods);
+ OdsSheet newSheet = new OdsSheet(this, clonedSods);
+ sheets.add(newSheet);
+ return newSheet;
+ } catch (CloneNotSupportedException e) {
+ throw new RuntimeException("Failed to clone sheet", e);
+ }
}
@Override
@@ -131,24 +186,25 @@ public int getNumberOfSheets() {
@Override
public Sheet getSheetAt(int index) {
- if (index >= 0 && index < sheets.size()) {
- return sheets.get(index);
+ if (index < 0 || index >= sheets.size()) {
+ throw new IllegalArgumentException("Sheet index (" + index + ") is out of range (0.." + (sheets.size() - 1) + ")");
}
- return null;
+ return sheets.get(index);
}
@Override
public Sheet getSheet(String name) {
int index = getSheetIndex(name);
- return index != -1 ? getSheetAt(index) : null;
+ return index != -1 ? sheets.get(index) : null;
}
@Override
public void removeSheetAt(int index) {
- if (index >= 0 && index < sheets.size()) {
- spreadSheet.deleteSheet(index);
- sheets.remove(index);
+ if (index < 0 || index >= sheets.size()) {
+ throw new IllegalArgumentException("Sheet index (" + index + ") is out of range (0.." + (sheets.size() - 1) + ")");
}
+ spreadSheet.deleteSheet(index);
+ sheets.remove(index);
}
@Override
@@ -161,7 +217,10 @@ public Font createFont() {
@Override
public Font findFont(boolean bold, short color, short fontHeight, String name, boolean italic, boolean strikeout, short typeOffset, byte underline) {
for (OdsFont font : fonts) {
- if (font.getBold() == bold && font.getColor() == color && font.getFontHeight() == fontHeight && font.getFontName().equals(name)) {
+ if (font.getBold() == bold && font.getColor() == color && font.getFontHeight() == fontHeight
+ && font.getFontName().equals(name) && font.getItalic() == italic
+ && font.getStrikeout() == strikeout && font.getTypeOffset() == typeOffset
+ && font.getUnderline() == underline) {
return font;
}
}
@@ -213,6 +272,9 @@ public void write(OutputStream stream) throws IOException {
@Override
public void close() throws IOException {
+ sheets.clear();
+ fonts.clear();
+ styles.clear();
}
@Override
@@ -241,6 +303,9 @@ public void setHidden(boolean hidden) {
@Override
public boolean isSheetHidden(int sheetNum) {
+ if (sheetNum >= 0 && sheetNum < sheets.size()) {
+ return sheets.get(sheetNum).getSodsSheet().isHidden();
+ }
return false;
}
@@ -251,14 +316,35 @@ public boolean isSheetVeryHidden(int sheetNum) {
@Override
public void setSheetHidden(int sheetNum, boolean hidden) {
+ if (sheetNum >= 0 && sheetNum < sheets.size()) {
+ com.github.miachm.sods.Sheet sodsSheet = sheets.get(sheetNum).getSodsSheet();
+ if (hidden) {
+ sodsSheet.hideSheet();
+ } else {
+ sodsSheet.showSheet();
+ }
+ }
}
@Override
public void setSheetVisibility(int sheetNum, SheetVisibility visibility) {
+ if (sheetNum >= 0 && sheetNum < sheets.size()) {
+ com.github.miachm.sods.Sheet sodsSheet = sheets.get(sheetNum).getSodsSheet();
+ if (visibility == SheetVisibility.HIDDEN || visibility == SheetVisibility.VERY_HIDDEN) {
+ sodsSheet.hideSheet();
+ } else {
+ sodsSheet.showSheet();
+ }
+ }
}
@Override
public SheetVisibility getSheetVisibility(int sheetNum) {
+ if (sheetNum >= 0 && sheetNum < sheets.size()) {
+ if (sheets.get(sheetNum).getSodsSheet().isHidden()) {
+ return SheetVisibility.HIDDEN;
+ }
+ }
return SheetVisibility.VISIBLE;
}
@@ -305,11 +391,13 @@ public org.apache.poi.ss.formula.EvaluationWorkbook createEvaluationWorkbook() {
}
@Override
- public void setMissingCellPolicy(Row.MissingCellPolicy missingCellPolicy) {}
+ public void setMissingCellPolicy(Row.MissingCellPolicy missingCellPolicy) {
+ this.missingCellPolicy = missingCellPolicy;
+ }
@Override
public Row.MissingCellPolicy getMissingCellPolicy() {
- return Row.MissingCellPolicy.RETURN_NULL_AND_BLANK;
+ return missingCellPolicy;
}
@Override
diff --git a/src/test/java/org/spdx/spreadsheetstore/ods/OdsWorkbookTest.java b/src/test/java/org/spdx/spreadsheetstore/ods/OdsWorkbookTest.java
new file mode 100644
index 0000000..e28fe24
--- /dev/null
+++ b/src/test/java/org/spdx/spreadsheetstore/ods/OdsWorkbookTest.java
@@ -0,0 +1,277 @@
+/*
+ * SPDX-FileContributor: Arthit Suriyawongkul
+ * SPDX-FileCopyrightText: Copyright (c) 2026 Source Auditor Inc.
+ * SPDX-FileType: SOURCE
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package org.spdx.spreadsheetstore.ods;
+
+import org.apache.poi.ss.usermodel.Cell;
+import org.apache.poi.ss.usermodel.CellType;
+import org.apache.poi.ss.usermodel.Row;
+import org.apache.poi.ss.usermodel.Sheet;
+import org.apache.poi.ss.util.CellRangeAddress;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Unit tests for OdsWorkbook, OdsSheet, OdsRow, and OdsCell implementations.
+ */
+public class OdsWorkbookTest {
+
+ @Test
+ public void testCreateSheetAndRowsAndCells() {
+ OdsWorkbook workbook = new OdsWorkbook();
+ Sheet sheet = workbook.createSheet("TestSheet");
+ assertNotNull(sheet);
+ assertEquals("TestSheet", sheet.getSheetName());
+
+ Row row = sheet.createRow(5);
+ assertNotNull(row);
+ assertEquals(5, row.getRowNum());
+
+ Cell cell = row.createCell(10);
+ assertNotNull(cell);
+ assertEquals(10, cell.getColumnIndex());
+ }
+
+ @Test
+ public void testCloneSheet() {
+ OdsWorkbook workbook = new OdsWorkbook();
+ Sheet sheet1 = workbook.createSheet("Original");
+ Row row = sheet1.createRow(0);
+ Cell cell = row.createCell(0);
+ cell.setCellValue("Hello World");
+
+ Sheet sheet2 = workbook.cloneSheet(0);
+ assertNotNull(sheet2);
+ assertEquals(2, workbook.getNumberOfSheets());
+ }
+
+ @Test
+ public void testSheetHiddenState() {
+ OdsWorkbook workbook = new OdsWorkbook();
+ Sheet sheet = workbook.createSheet("Sheet1");
+ assertFalse(workbook.isSheetHidden(0));
+
+ workbook.setSheetHidden(0, true);
+ assertTrue(workbook.isSheetHidden(0));
+
+ workbook.setSheetHidden(0, false);
+ assertFalse(workbook.isSheetHidden(0));
+ }
+
+ @Test
+ public void testMergedRegions() {
+ OdsWorkbook workbook = new OdsWorkbook();
+ Sheet sheet = workbook.createSheet("MergedSheet");
+ sheet.createRow(0).createCell(0);
+ sheet.createRow(2).createCell(3);
+
+ CellRangeAddress region = new CellRangeAddress(0, 2, 0, 3);
+ sheet.addMergedRegion(region);
+
+ assertEquals(1, sheet.getNumMergedRegions());
+ CellRangeAddress retrieved = sheet.getMergedRegion(0);
+ assertNotNull(retrieved);
+ assertEquals(0, retrieved.getFirstRow());
+ assertEquals(2, retrieved.getLastRow());
+ assertEquals(0, retrieved.getFirstColumn());
+ assertEquals(3, retrieved.getLastColumn());
+ }
+
+ @Test
+ public void testColumnAndRowDimensions() {
+ OdsWorkbook workbook = new OdsWorkbook();
+ Sheet sheet = workbook.createSheet("DimSheet");
+ Row row = sheet.createRow(0);
+
+ sheet.setColumnWidth(2, 5000);
+ assertTrue(sheet.getColumnWidth(2) > 0);
+
+ row.setHeightInPoints(25.0f);
+ assertTrue(row.getHeightInPoints() > 0);
+ }
+
+ @Test
+ public void testColumnAndRowVisibility() {
+ OdsWorkbook workbook = new OdsWorkbook();
+ Sheet sheet = workbook.createSheet("VisSheet");
+ Row row = sheet.createRow(1);
+
+ assertFalse(sheet.isColumnHidden(1));
+ sheet.setColumnHidden(1, true);
+ assertTrue(sheet.isColumnHidden(1));
+
+ assertFalse(row.getZeroHeight());
+ row.setZeroHeight(true);
+ assertTrue(row.getZeroHeight());
+ }
+
+ @Test
+ public void testFormulas() {
+ OdsWorkbook workbook = new OdsWorkbook();
+ Sheet sheet = workbook.createSheet("FormulaSheet");
+ Row row = sheet.createRow(0);
+ Cell cell = row.createCell(0);
+
+ cell.setCellFormula("SUM(A1:A5)");
+ assertEquals("SUM(A1:A5)", cell.getCellFormula());
+ assertEquals(CellType.FORMULA, cell.getCellType());
+ }
+
+ @Test
+ public void testFreezePanesAndProtection() {
+ OdsWorkbook workbook = new OdsWorkbook();
+ Sheet sheet = workbook.createSheet("ProtectSheet");
+
+ sheet.createFreezePane(1, 2);
+ assertFalse(sheet.getProtect());
+
+ sheet.protectSheet("secret");
+ assertTrue(sheet.getProtect());
+ }
+
+ @Test
+ public void testSheetOrdering() {
+ OdsWorkbook workbook = new OdsWorkbook();
+ workbook.createSheet("First");
+ workbook.createSheet("Second");
+
+ assertEquals(0, workbook.getSheetIndex("First"));
+ assertEquals(1, workbook.getSheetIndex("Second"));
+
+ workbook.setSheetOrder("Second", 0);
+ assertEquals(0, workbook.getSheetIndex("Second"));
+ assertEquals(1, workbook.getSheetIndex("First"));
+ }
+
+ @Test
+ public void testCellComments() {
+ OdsWorkbook workbook = new OdsWorkbook();
+ Sheet sheet = workbook.createSheet("CommentSheet");
+ Row row = sheet.createRow(0);
+ Cell cell = row.createCell(0);
+
+ OdsComment comment = new OdsComment("This is a comment");
+ cell.setCellComment(comment);
+
+ assertNotNull(cell.getCellComment());
+ assertEquals("This is a comment", cell.getCellComment().getString().getString());
+
+ cell.removeCellComment();
+ org.junit.Assert.assertNull(cell.getCellComment());
+ }
+
+ @Test
+ public void testCellIteratorNoNulls() {
+ OdsWorkbook workbook = new OdsWorkbook();
+ Sheet sheet = workbook.createSheet("SparseSheet");
+ Row row = sheet.createRow(0);
+ row.createCell(0).setCellValue("A");
+ row.createCell(5).setCellValue("F"); // Gap columns 1-4
+
+ int cellCount = 0;
+ for (Cell cell : row) {
+ assertNotNull("Cell iterator must not return null elements", cell);
+ cellCount++;
+ }
+ assertEquals(2, cellCount);
+ }
+
+ @Test
+ public void testRowIteratorEmptySheet() {
+ OdsWorkbook workbook = new OdsWorkbook();
+ Sheet sheet = workbook.createSheet("EmptySheet");
+ assertEquals(-1, sheet.getFirstRowNum());
+ assertEquals(-1, sheet.getLastRowNum());
+
+ int rowCount = 0;
+ for (Row row : sheet) {
+ rowCount++;
+ }
+ assertEquals(0, rowCount);
+ assertEquals(-1, sheet.getFirstRowNum()); // No synthetic row created
+ }
+
+ @Test
+ public void testNullBorderHandling() {
+ OdsWorkbook workbook = new OdsWorkbook();
+ OdsCellStyle style = (OdsCellStyle) workbook.createCellStyle();
+ style.setBorderBottom(null);
+ org.junit.Assert.assertNull(style.getBorderBottom());
+ }
+
+ @Test
+ public void testRemoveFormulaAndClear() {
+ OdsWorkbook workbook = new OdsWorkbook();
+ Sheet sheet = workbook.createSheet("FormulaClear");
+ Row row = sheet.createRow(0);
+ Cell cell = row.createCell(0);
+
+ cell.setCellFormula("SUM(A1:A5)");
+ assertEquals(CellType.FORMULA, cell.getCellType());
+
+ cell.removeFormula();
+ assertFalse(cell.getCellType() == CellType.FORMULA);
+
+ cell.setCellFormula("A1+A2");
+ cell.setBlank();
+ assertEquals(CellType.BLANK, cell.getCellType());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testOutOfBoundsSheetIndexException() {
+ OdsWorkbook workbook = new OdsWorkbook();
+ workbook.getSheetAt(99);
+ }
+
+ @Test
+ public void testBuiltinDataFormatFallback() {
+ OdsWorkbook workbook = new OdsWorkbook();
+ org.apache.poi.ss.usermodel.DataFormat df = workbook.createDataFormat();
+ assertEquals("m/d/yy", df.getFormat((short) 14));
+ }
+
+ @Test
+ public void testIsoDateStringWithZ() {
+ OdsWorkbook workbook = new OdsWorkbook();
+ Sheet sheet = workbook.createSheet("Dates");
+ Row row = sheet.createRow(0);
+ Cell cell = row.createCell(0);
+ cell.setCellValue("2026-08-02T08:34:56Z");
+ java.util.Date date = cell.getDateCellValue();
+ org.junit.Assert.assertNotNull(date);
+ }
+
+ @Test
+ public void testHeaderFooterAndPrintSetupStubs() {
+ OdsWorkbook workbook = new OdsWorkbook();
+ Sheet sheet = workbook.createSheet("Stubs");
+ org.junit.Assert.assertNotNull(sheet.getHeader());
+ org.junit.Assert.assertNotNull(sheet.getFooter());
+ org.junit.Assert.assertNotNull(sheet.getPrintSetup());
+ sheet.protectSheet(null);
+ org.junit.Assert.assertTrue(sheet.getProtect());
+ }
+
+ @Test
+ public void testDoubleRowHeightPrecision() {
+ OdsWorkbook workbook = new OdsWorkbook();
+ Sheet sheet = workbook.createSheet("Heights");
+ Row row = sheet.createRow(0);
+ row.setHeightInPoints(20.5f);
+ assertEquals(20.5f, row.getHeightInPoints(), 0.1f);
+ }
+
+ @Test
+ public void testFindFontNullName() {
+ OdsWorkbook workbook = new OdsWorkbook();
+ org.apache.poi.ss.usermodel.Font font = workbook.findFont(false, (short) 0, (short) 200, null, false, false, (short) 0, (byte) 0);
+ // Should return without NPE
+ }
+}
From 06267d02d1546c11635ea78fce8a02d49a81afab Mon Sep 17 00:00:00 2001
From: Arthit Suriyawongkul
Date: Mon, 3 Aug 2026 13:24:10 +0100
Subject: [PATCH 4/5] Add comment on empty addValidationData
Signed-off-by: Arthit Suriyawongkul
---
.../java/org/spdx/spreadsheetstore/ods/OdsCell.java | 1 -
.../org/spdx/spreadsheetstore/ods/OdsCellStyle.java | 1 -
.../spdx/spreadsheetstore/ods/OdsCreationHelper.java | 1 -
.../java/org/spdx/spreadsheetstore/ods/OdsSheet.java | 10 ++++++++++
.../org/spdx/spreadsheetstore/ods/package-info.java | 5 +++--
5 files changed, 13 insertions(+), 5 deletions(-)
diff --git a/src/main/java/org/spdx/spreadsheetstore/ods/OdsCell.java b/src/main/java/org/spdx/spreadsheetstore/ods/OdsCell.java
index cfb1b91..aebfae5 100644
--- a/src/main/java/org/spdx/spreadsheetstore/ods/OdsCell.java
+++ b/src/main/java/org/spdx/spreadsheetstore/ods/OdsCell.java
@@ -6,7 +6,6 @@
*/
package org.spdx.spreadsheetstore.ods;
-import java.util.Calendar;
import java.util.Date;
import java.time.LocalDate;
import java.time.LocalDateTime;
diff --git a/src/main/java/org/spdx/spreadsheetstore/ods/OdsCellStyle.java b/src/main/java/org/spdx/spreadsheetstore/ods/OdsCellStyle.java
index ab6436a..8c6e4d2 100644
--- a/src/main/java/org/spdx/spreadsheetstore/ods/OdsCellStyle.java
+++ b/src/main/java/org/spdx/spreadsheetstore/ods/OdsCellStyle.java
@@ -6,7 +6,6 @@
*/
package org.spdx.spreadsheetstore.ods;
-import org.apache.poi.hssf.util.HSSFColor;
import org.apache.poi.ss.usermodel.BorderStyle;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.Color;
diff --git a/src/main/java/org/spdx/spreadsheetstore/ods/OdsCreationHelper.java b/src/main/java/org/spdx/spreadsheetstore/ods/OdsCreationHelper.java
index 69139c2..2321e9c 100644
--- a/src/main/java/org/spdx/spreadsheetstore/ods/OdsCreationHelper.java
+++ b/src/main/java/org/spdx/spreadsheetstore/ods/OdsCreationHelper.java
@@ -9,7 +9,6 @@
import org.apache.poi.common.usermodel.HyperlinkType;
import org.apache.poi.ss.usermodel.ClientAnchor;
import org.apache.poi.ss.usermodel.CreationHelper;
-import org.apache.poi.ss.usermodel.DataFormat;
import org.apache.poi.ss.usermodel.ExtendedColor;
import org.apache.poi.ss.usermodel.FormulaEvaluator;
import org.apache.poi.ss.usermodel.Hyperlink;
diff --git a/src/main/java/org/spdx/spreadsheetstore/ods/OdsSheet.java b/src/main/java/org/spdx/spreadsheetstore/ods/OdsSheet.java
index 421ddf0..fbffe7f 100644
--- a/src/main/java/org/spdx/spreadsheetstore/ods/OdsSheet.java
+++ b/src/main/java/org/spdx/spreadsheetstore/ods/OdsSheet.java
@@ -402,12 +402,22 @@ public void setRepeatingRows(CellRangeAddress repeatingRowsToIndex) {}
@Override
public AutoFilter setAutoFilter(CellRangeAddress range) { return null; }
+ /**
+ * Data validation is not currently supported by the underlying
+ * SODS 1.10 library.
+ * This method acts as a no-op to maintain POI interface compatibility.
+ */
@Override
public void addValidationData(DataValidation dataValidation) {}
@Override
public List extends DataValidation> getDataValidations() { return new ArrayList<>(); }
+ /**
+ * Data validation helpers are not currently supported by the underlying
+ * SODS 1.10 library.
+ * Returns {@code null} to maintain POI interface compatibility.
+ */
@Override
public DataValidationHelper getDataValidationHelper() { return null; }
diff --git a/src/main/java/org/spdx/spreadsheetstore/ods/package-info.java b/src/main/java/org/spdx/spreadsheetstore/ods/package-info.java
index 9ffd603..ef526ac 100644
--- a/src/main/java/org/spdx/spreadsheetstore/ods/package-info.java
+++ b/src/main/java/org/spdx/spreadsheetstore/ods/package-info.java
@@ -20,8 +20,9 @@
* Limitations
*
* Custom cell data formatting (e.g., specific number formats and custom date
- * patterns) has limited support due to the underlying SODS library. SODS only
- * natively supports plain text ("@") or an ISO date format ("YYYY-MM-DD").
+ * patterns) has limited support due to the underlying SODS 1.10 library.
+ * SODS only natively supports plain text ("@") or an ISO date format
+ * ("YYYY-MM-DD").
*
*
* Consequently, any target cell format representing a date is mapped to the
From e02b883990d593afdb6723c2f768bd80ce3df732 Mon Sep 17 00:00:00 2001
From: Arthit Suriyawongkul
Date: Mon, 3 Aug 2026 13:33:47 +0100
Subject: [PATCH 5/5] Add comment on empty addPicture
Signed-off-by: Arthit Suriyawongkul
---
.../java/org/spdx/spreadsheetstore/ods/OdsWorkbook.java | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/src/main/java/org/spdx/spreadsheetstore/ods/OdsWorkbook.java b/src/main/java/org/spdx/spreadsheetstore/ods/OdsWorkbook.java
index 8b0b48e..caa0a67 100644
--- a/src/main/java/org/spdx/spreadsheetstore/ods/OdsWorkbook.java
+++ b/src/main/java/org/spdx/spreadsheetstore/ods/OdsWorkbook.java
@@ -277,11 +277,20 @@ public void close() throws IOException {
styles.clear();
}
+ /**
+ * Stub returning 0 for POI interface parity.
+ * SODS attaches images per sheet;
+ * pictures are not used in SPDX serializations.
+ */
@Override
public int addPicture(byte[] pictureData, int format) {
return 0;
}
+ /**
+ * Stub returning an empty list for POI interface parity;
+ * pictures are not used in SPDX serializations.
+ */
@Override
public List extends PictureData> getAllPictures() {
return new ArrayList<>();
| | | |