From 579fabc6c10694c5585b9dd547ce113fabbe12ff Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Fri, 17 Jul 2026 12:04:00 +0100 Subject: [PATCH 1/2] 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 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 getDataValidations() { return new ArrayList<>(); } + + @Override + public DataValidationHelper getDataValidationHelper() { return null; } + + @Override + public CellRange removeArrayFormula(Cell cell) { return null; } + + @Override + public CellRange 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 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 getNames(String name) { return new ArrayList<>(); } + + @Override + public List 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/2] 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;