iterator() {
+ return rowIterator();
+ }
+}
diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/ods/OdsWorkbook.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/ods/OdsWorkbook.java
new file mode 100644
index 000000000..b9d442ec2
--- /dev/null
+++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/ods/OdsWorkbook.java
@@ -0,0 +1,441 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.fesod.sheet.metadata.ods;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.fesod.sheet.exception.ExcelGenerateException;
+import org.apache.poi.ss.SpreadsheetVersion;
+import org.apache.poi.ss.formula.EvaluationWorkbook;
+import org.apache.poi.ss.formula.udf.UDFFinder;
+import org.apache.poi.ss.usermodel.CellReferenceType;
+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.MissingCellPolicy;
+import org.apache.poi.ss.usermodel.Sheet;
+import org.apache.poi.ss.usermodel.SheetVisibility;
+import org.apache.poi.ss.usermodel.Workbook;
+import org.odftoolkit.odfdom.doc.OdfSpreadsheetDocument;
+
+/**
+ * ODS workbook implementation for writing ODS files.
+ *
+ */
+@Getter
+@Setter
+@EqualsAndHashCode
+@Slf4j
+public class OdsWorkbook implements Workbook {
+
+ /**
+ * ODF Spreadsheet Document
+ */
+ private OdfSpreadsheetDocument odfDocument;
+
+ /**
+ * true if date uses 1904 windowing, or false if using 1900 date windowing.
+ *
+ * default is false
+ */
+ private Boolean use1904windowing;
+
+ /**
+ * locale
+ */
+ private Locale locale;
+
+ /**
+ * Whether to use scientific Format.
+ *
+ * default is false
+ */
+ private Boolean useScientificFormat;
+
+ /**
+ * data format
+ */
+ private OdsDataFormat odsDataFormat;
+
+ /**
+ * sheets
+ */
+ private List odsSheetList;
+
+ /**
+ * cell styles
+ */
+ private List odsCellStyleList;
+
+ public OdsWorkbook(Locale locale, Boolean use1904windowing, Boolean useScientificFormat) {
+ this.locale = locale;
+ this.use1904windowing = use1904windowing;
+ this.useScientificFormat = useScientificFormat;
+ this.odsSheetList = new ArrayList<>();
+ this.odsCellStyleList = new ArrayList<>();
+
+ // ODS format uses ISO 8601 date standard and does not support Excel's 1904 date windowing
+ if (Boolean.TRUE.equals(use1904windowing)) {
+ log.warn(
+ "ODS format does not support 1904 date windowing. The 'use1904windowing' parameter will be ignored. "
+ + "ODS uses the standard 1900 date system (ISO 8601).");
+ }
+
+ try {
+ this.odfDocument = OdfSpreadsheetDocument.newSpreadsheetDocument();
+ // Remove the default sheet that OdfToolkit creates
+ if (odfDocument.getTableList().size() > 0) {
+ odfDocument.getTableList().get(0).remove();
+ }
+ } catch (Exception e) {
+ throw new ExcelGenerateException("Failed to create ODS document", e);
+ }
+ }
+
+ @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) {}
+
+ @Override
+ public String getSheetName(int sheet) {
+ if (sheet < 0 || sheet >= odsSheetList.size()) {
+ return null;
+ }
+ return odsSheetList.get(sheet).getSheetName();
+ }
+
+ @Override
+ public int getSheetIndex(String name) {
+ for (int i = 0; i < odsSheetList.size(); i++) {
+ if (name.equals(odsSheetList.get(i).getSheetName())) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ @Override
+ public int getSheetIndex(Sheet sheet) {
+ return odsSheetList.indexOf(sheet);
+ }
+
+ @Override
+ public Sheet createSheet() {
+ return createSheet("Sheet" + (odsSheetList.size() + 1));
+ }
+
+ @Override
+ public Sheet createSheet(String sheetname) {
+ OdsSheet odsSheet = new OdsSheet(this, sheetname);
+ odsSheetList.add(odsSheet);
+ return odsSheet;
+ }
+
+ @Override
+ public Sheet cloneSheet(int sheetNum) {
+ return null;
+ }
+
+ @Override
+ public Iterator sheetIterator() {
+ return (Iterator) (Iterator extends Sheet>) odsSheetList.iterator();
+ }
+
+ @Override
+ public int getNumberOfSheets() {
+ return odsSheetList.size();
+ }
+
+ @Override
+ public Sheet getSheetAt(int index) {
+ if (index < 0 || index >= odsSheetList.size()) {
+ return null;
+ }
+ return odsSheetList.get(index);
+ }
+
+ @Override
+ public Sheet getSheet(String name) {
+ int index = getSheetIndex(name);
+ if (index >= 0) {
+ return odsSheetList.get(index);
+ }
+ return null;
+ }
+
+ @Override
+ public void removeSheetAt(int index) {
+ if (index >= 0 && index < odsSheetList.size()) {
+ odsSheetList.remove(index);
+ }
+ }
+
+ @Override
+ public Font createFont() {
+ return null;
+ }
+
+ @Override
+ public Font findFont(
+ boolean bold,
+ short color,
+ short fontHeight,
+ String name,
+ boolean italic,
+ boolean strikeout,
+ short typeOffset,
+ byte underline) {
+ return null;
+ }
+
+ @Override
+ public int getNumberOfFonts() {
+ return 0;
+ }
+
+ @Override
+ public int getNumberOfFontsAsInt() {
+ return 0;
+ }
+
+ @Override
+ public Font getFontAt(int idx) {
+ return null;
+ }
+
+ @Override
+ public CellStyle createCellStyle() {
+ OdsCellStyle odsCellStyle = new OdsCellStyle((short) odsCellStyleList.size());
+ odsCellStyleList.add(odsCellStyle);
+ return odsCellStyle;
+ }
+
+ @Override
+ public int getNumCellStyles() {
+ return odsCellStyleList.size();
+ }
+
+ @Override
+ public CellStyle getCellStyleAt(int idx) {
+ if (idx < 0 || idx >= odsCellStyleList.size()) {
+ return null;
+ }
+ return odsCellStyleList.get(idx);
+ }
+
+ @Override
+ public void write(OutputStream stream) throws IOException {
+ try {
+ // Flush all sheets data to the ODF document
+ for (OdsSheet sheet : odsSheetList) {
+ sheet.flushToDocument();
+ }
+ odfDocument.save(stream);
+ } catch (Exception e) {
+ throw new IOException("Failed to write ODS document", e);
+ }
+ }
+
+ @Override
+ public void close() throws IOException {
+ if (odfDocument != null) {
+ odfDocument.close();
+ }
+ }
+
+ @Override
+ public int getNumberOfNames() {
+ return 0;
+ }
+
+ @Override
+ public Name getName(String name) {
+ return null;
+ }
+
+ @Override
+ public List extends Name> getNames(String name) {
+ return null;
+ }
+
+ @Override
+ public List extends Name> getAllNames() {
+ return null;
+ }
+
+ @Override
+ public Name createName() {
+ return null;
+ }
+
+ @Override
+ public void removeName(Name name) {}
+
+ @Override
+ public int linkExternalWorkbook(String name, Workbook workbook) {
+ return 0;
+ }
+
+ @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 void removePrintArea(int sheetIndex) {}
+
+ @Override
+ public MissingCellPolicy getMissingCellPolicy() {
+ return null;
+ }
+
+ @Override
+ public void setMissingCellPolicy(MissingCellPolicy missingCellPolicy) {}
+
+ @Override
+ public DataFormat createDataFormat() {
+ if (odsDataFormat != null) {
+ return odsDataFormat;
+ }
+ odsDataFormat = new OdsDataFormat(locale);
+ return odsDataFormat;
+ }
+
+ @Override
+ public int addPicture(byte[] pictureData, int format) {
+ return 0;
+ }
+
+ @Override
+ public List extends PictureData> getAllPictures() {
+ return null;
+ }
+
+ @Override
+ public CreationHelper getCreationHelper() {
+ return null;
+ }
+
+ @Override
+ public boolean isHidden() {
+ return false;
+ }
+
+ @Override
+ public void setHidden(boolean hiddenFlag) {}
+
+ @Override
+ public boolean isSheetHidden(int sheetIx) {
+ return false;
+ }
+
+ @Override
+ public boolean isSheetVeryHidden(int sheetIx) {
+ return false;
+ }
+
+ @Override
+ public void setSheetHidden(int sheetIx, boolean hidden) {}
+
+ @Override
+ public SheetVisibility getSheetVisibility(int sheetIx) {
+ return null;
+ }
+
+ @Override
+ public void setSheetVisibility(int sheetIx, SheetVisibility visibility) {}
+
+ @Override
+ public void addToolPack(UDFFinder toolpack) {}
+
+ @Override
+ public void setForceFormulaRecalculation(boolean value) {}
+
+ @Override
+ public boolean getForceFormulaRecalculation() {
+ return false;
+ }
+
+ @Override
+ public SpreadsheetVersion getSpreadsheetVersion() {
+ return null;
+ }
+
+ @Override
+ public int addOlePackage(byte[] oleData, String label, String fileName, String command) {
+ return 0;
+ }
+
+ @Override
+ public EvaluationWorkbook createEvaluationWorkbook() {
+ return null;
+ }
+
+ @Override
+ public CellReferenceType getCellReferenceType() {
+ return null;
+ }
+
+ @Override
+ public void setCellReferenceType(CellReferenceType cellReferenceType) {}
+
+ @Override
+ public Iterator iterator() {
+ return sheetIterator();
+ }
+}
diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/read/metadata/holder/ods/OdsReadSheetHolder.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/read/metadata/holder/ods/OdsReadSheetHolder.java
new file mode 100644
index 000000000..3df34cebb
--- /dev/null
+++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/read/metadata/holder/ods/OdsReadSheetHolder.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.fesod.sheet.read.metadata.holder.ods;
+
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.Setter;
+import org.apache.fesod.sheet.read.metadata.ReadSheet;
+import org.apache.fesod.sheet.read.metadata.holder.ReadSheetHolder;
+import org.apache.fesod.sheet.read.metadata.holder.ReadWorkbookHolder;
+
+/**
+ * ODS sheet holder
+ *
+ */
+@Getter
+@Setter
+@EqualsAndHashCode
+public class OdsReadSheetHolder extends ReadSheetHolder {
+
+ public OdsReadSheetHolder(ReadSheet readSheet, ReadWorkbookHolder readWorkbookHolder) {
+ super(readSheet, readWorkbookHolder);
+ }
+}
diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/read/metadata/holder/ods/OdsReadWorkbookHolder.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/read/metadata/holder/ods/OdsReadWorkbookHolder.java
new file mode 100644
index 000000000..19049e976
--- /dev/null
+++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/read/metadata/holder/ods/OdsReadWorkbookHolder.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.fesod.sheet.read.metadata.holder.ods;
+
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.Setter;
+import org.apache.fesod.sheet.read.metadata.ReadWorkbook;
+import org.apache.fesod.sheet.read.metadata.holder.ReadWorkbookHolder;
+import org.apache.fesod.sheet.support.ExcelTypeEnum;
+import org.odftoolkit.odfdom.doc.OdfSpreadsheetDocument;
+
+/**
+ * ODS Workbook holder
+ *
+ */
+@Getter
+@Setter
+@EqualsAndHashCode
+public class OdsReadWorkbookHolder extends ReadWorkbookHolder {
+
+ /**
+ * ODF Spreadsheet Document
+ */
+ private OdfSpreadsheetDocument odfSpreadsheetDocument;
+
+ public OdsReadWorkbookHolder(ReadWorkbook readWorkbook) {
+ super(readWorkbook);
+ setExcelType(ExcelTypeEnum.ODS);
+ }
+}
diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/support/ExcelTypeEnum.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/support/ExcelTypeEnum.java
index c795d2c17..e2a751b6d 100644
--- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/support/ExcelTypeEnum.java
+++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/support/ExcelTypeEnum.java
@@ -20,9 +20,12 @@
package org.apache.fesod.sheet.support;
import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
import lombok.Getter;
import org.apache.fesod.sheet.exception.ExcelAnalysisException;
import org.apache.fesod.sheet.exception.ExcelCommonException;
@@ -50,7 +53,12 @@ public enum ExcelTypeEnum {
/**
* xlsx
*/
- XLSX(".xlsx", new byte[] {80, 75, 3, 4});
+ XLSX(".xlsx", new byte[] {80, 75, 3, 4}),
+
+ /**
+ * ods (OpenDocument Spreadsheet)
+ */
+ ODS(".ods", new byte[] {80, 75, 3, 4});
final String value;
final byte[] magic;
@@ -100,6 +108,8 @@ public static ExcelTypeEnum valueOf(ReadWorkbook readWorkbook) {
return XLS;
} else if (fileName.endsWith(CSV.getValue())) {
return CSV;
+ } else if (fileName.endsWith(ODS.getValue())) {
+ return ODS;
}
try (BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream(file))) {
return recognitionExcelType(bufferedInputStream);
@@ -128,7 +138,9 @@ private static ExcelTypeEnum recognitionExcelType(InputStream inputStream) throw
// Grab the first bytes of this stream
byte[] data = IOUtils.peekFirstNBytes(inputStream, MAX_PATTERN_LENGTH);
if (findMagic(XLSX.magic, data)) {
- return XLSX;
+ // Both XLSX and ODS are ZIP files with the same magic bytes {80, 75, 3, 4}
+ // Need to check internal structure to distinguish them
+ return distinguishZipBasedFormat(inputStream);
} else if (findMagic(XLS.magic, data)) {
return XLS;
}
@@ -136,6 +148,65 @@ private static ExcelTypeEnum recognitionExcelType(InputStream inputStream) throw
return CSV;
}
+ /**
+ * Distinguish between XLSX and ODS formats by checking ZIP internal structure.
+ * ODS files contain a 'mimetype' file with content 'application/vnd.oasis.opendocument.spreadsheet'.
+ * XLSX files contain '[Content_Types].xml' or 'xl/' directory.
+ *
+ * @param inputStream the input stream (must support mark/reset)
+ * @return ODS if it's an ODS file, XLSX otherwise (default for ZIP-based spreadsheets)
+ */
+ private static ExcelTypeEnum distinguishZipBasedFormat(InputStream inputStream) throws Exception {
+ // Read enough bytes to check the ZIP structure
+ // Most ZIP files have the first entry within the first 4KB
+ final int BUFFER_SIZE = 4096;
+ if (!inputStream.markSupported()) {
+ // If mark is not supported, default to XLSX
+ return XLSX;
+ }
+
+ inputStream.mark(BUFFER_SIZE);
+ try {
+ byte[] buffer = new byte[BUFFER_SIZE];
+ int bytesRead = inputStream.read(buffer);
+ if (bytesRead <= 0) {
+ return XLSX; // Default to XLSX for empty files
+ }
+
+ try (ZipInputStream zipInputStream = new ZipInputStream(new ByteArrayInputStream(buffer, 0, bytesRead))) {
+ ZipEntry entry;
+ while ((entry = zipInputStream.getNextEntry()) != null) {
+ String entryName = entry.getName();
+ // ODS files have 'mimetype' as the first file (usually)
+ if ("mimetype".equals(entryName)) {
+ // Verify it's ODS by reading the mimetype content
+ byte[] mimeBytes = new byte[64];
+ int len = zipInputStream.read(mimeBytes);
+ if (len > 0) {
+ String mimeType = new String(mimeBytes, 0, len).trim();
+ if (mimeType.contains("opendocument.spreadsheet")) {
+ return ODS;
+ }
+ }
+ }
+ // XLSX files typically have these entries
+ if (entryName.equals("[Content_Types].xml") || entryName.startsWith("xl/")) {
+ return XLSX;
+ }
+ // ODS files also have content.xml
+ if ("content.xml".equals(entryName) || entryName.equals("META-INF/manifest.xml")) {
+ return ODS;
+ }
+ zipInputStream.closeEntry();
+ }
+ }
+ } finally {
+ inputStream.reset();
+ }
+ // Default to XLSX for unrecognized ZIP-based format
+ return XLSX;
+ }
+
private static boolean findMagic(byte[] expected, byte[] actual) {
int i = 0;
for (byte expectedByte : expected) {
diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/WorkBookUtil.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/WorkBookUtil.java
index 7c17a30a4..6391ec649 100644
--- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/WorkBookUtil.java
+++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/WorkBookUtil.java
@@ -25,6 +25,7 @@
import org.apache.fesod.sheet.metadata.csv.CsvWorkbook;
import org.apache.fesod.sheet.metadata.data.DataFormatData;
import org.apache.fesod.sheet.metadata.data.WriteCellData;
+import org.apache.fesod.sheet.metadata.ods.OdsWorkbook;
import org.apache.fesod.sheet.write.metadata.holder.WriteWorkbookHolder;
import org.apache.fesod.sheet.write.metadata.style.WriteCellStyle;
import org.apache.poi.hssf.record.crypto.Biff8EncryptionKey;
@@ -106,6 +107,14 @@ public static void createWorkBook(WriteWorkbookHolder writeWorkbookHolder) throw
writeWorkbookHolder.setCachedWorkbook(csvWorkbook);
writeWorkbookHolder.setWorkbook(csvWorkbook);
return;
+ case ODS:
+ OdsWorkbook odsWorkbook = new OdsWorkbook(
+ writeWorkbookHolder.getGlobalConfiguration().getLocale(),
+ writeWorkbookHolder.getGlobalConfiguration().getUse1904windowing(),
+ writeWorkbookHolder.getGlobalConfiguration().getUseScientificFormat());
+ writeWorkbookHolder.setCachedWorkbook(odsWorkbook);
+ writeWorkbookHolder.setWorkbook(odsWorkbook);
+ return;
default:
throw new UnsupportedOperationException("Wrong excel type.");
}
diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/handler/DefaultWriteHandlerLoader.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/handler/DefaultWriteHandlerLoader.java
index 4f1ba50ce..944e81469 100644
--- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/handler/DefaultWriteHandlerLoader.java
+++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/handler/DefaultWriteHandlerLoader.java
@@ -72,6 +72,10 @@ public static List loadDefaultHandler(Boolean useDefaultStyle, Exc
handlerList.add(new DefaultRowWriteHandler());
handlerList.add(new FillStyleCellWriteHandler());
break;
+ case ODS:
+ handlerList.add(new DefaultRowWriteHandler());
+ handlerList.add(new FillStyleCellWriteHandler());
+ break;
default:
break;
}
diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/ods/OdsReadWriteTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/ods/OdsReadWriteTest.java
new file mode 100644
index 000000000..1939b7c1c
--- /dev/null
+++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/ods/OdsReadWriteTest.java
@@ -0,0 +1,468 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.fesod.sheet.ods;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.Setter;
+import org.apache.fesod.sheet.ExcelWriter;
+import org.apache.fesod.sheet.FesodSheet;
+import org.apache.fesod.sheet.annotation.ExcelProperty;
+import org.apache.fesod.sheet.context.AnalysisContext;
+import org.apache.fesod.sheet.read.listener.ReadListener;
+import org.apache.fesod.sheet.support.ExcelTypeEnum;
+import org.apache.fesod.sheet.util.FileUtils;
+import org.apache.fesod.sheet.util.TestFileUtil;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test class for ODS (OpenDocument Spreadsheet) read and write functionality.
+ */
+public class OdsReadWriteTest {
+
+ private File tempDir;
+
+ @BeforeEach
+ public void setUp() {
+ tempDir = new File(System.getProperty("java.io.tmpdir"), "fesod-ods-test");
+ tempDir.mkdirs();
+ }
+
+ @AfterEach
+ public void tearDown() {
+ if (tempDir != null && tempDir.exists()) {
+ FileUtils.delete(tempDir);
+ }
+ }
+
+ /**
+ * Test writing ODS file.
+ */
+ @Test
+ public void testWriteOds() {
+ String fileName = new File(tempDir, "test-write.ods").getAbsolutePath();
+
+ List dataList = generateTestData(10);
+
+ FesodSheet.write(fileName, OdsTestData.class)
+ .excelType(ExcelTypeEnum.ODS)
+ .sheet("TestSheet")
+ .doWrite(dataList);
+
+ File outputFile = new File(fileName);
+ assertTrue(outputFile.exists(), "ODS file should be created");
+ assertTrue(outputFile.length() > 0, "ODS file should not be empty");
+ }
+
+ /**
+ * Test reading ODS file.
+ */
+ @Test
+ public void testReadOds() {
+ // First write the file
+ String fileName = new File(tempDir, "test-read.ods").getAbsolutePath();
+ List writeData = generateTestData(5);
+
+ FesodSheet.write(fileName, OdsTestData.class)
+ .excelType(ExcelTypeEnum.ODS)
+ .sheet("TestSheet")
+ .doWrite(writeData);
+
+ // Then read it back
+ List readData = new ArrayList<>();
+ FesodSheet.read(fileName, OdsTestData.class, new ReadListener() {
+ @Override
+ public void invoke(OdsTestData data, AnalysisContext context) {
+ readData.add(data);
+ }
+
+ @Override
+ public void doAfterAllAnalysed(AnalysisContext context) {
+ // Reading complete
+ }
+ })
+ .sheet()
+ .doRead();
+
+ // Verify
+ assertNotNull(readData, "Read data should not be null");
+ assertEquals(5, readData.size(), "Should read 5 rows");
+
+ // Verify first row data
+ OdsTestData firstRow = readData.get(0);
+ assertNotNull(firstRow.getString(), "String value should not be null");
+ assertTrue(firstRow.getString().startsWith("String"), "String value should start with 'String'");
+ }
+
+ /**
+ * Test write and read round-trip.
+ */
+ @Test
+ public void testWriteReadRoundTrip() {
+ String fileName = new File(tempDir, "test-roundtrip.ods").getAbsolutePath();
+
+ // Prepare test data
+ List originalData = generateTestData(3);
+
+ // Write
+ FesodSheet.write(fileName, OdsTestData.class)
+ .excelType(ExcelTypeEnum.ODS)
+ .sheet("RoundTrip")
+ .doWrite(originalData);
+
+ // Read
+ List readBackData = new ArrayList<>();
+ FesodSheet.read(fileName, OdsTestData.class, new ReadListener() {
+ @Override
+ public void invoke(OdsTestData data, AnalysisContext context) {
+ readBackData.add(data);
+ }
+
+ @Override
+ public void doAfterAllAnalysed(AnalysisContext context) {}
+ })
+ .sheet()
+ .doRead();
+
+ // Verify count
+ assertEquals(originalData.size(), readBackData.size(), "Data count should match");
+
+ // Verify values
+ for (int i = 0; i < originalData.size(); i++) {
+ OdsTestData original = originalData.get(i);
+ OdsTestData readBack = readBackData.get(i);
+ assertEquals(original.getString(), readBack.getString(), "String values should match at row " + i);
+ assertEquals(original.getDoubleData(), readBack.getDoubleData(), "Double values should match at row " + i);
+ }
+ }
+
+ /**
+ * Test reading ODS file from InputStream (verifies magic number conflict fix).
+ */
+ @Test
+ public void testReadOdsFromInputStream() {
+ // First write the file
+ String fileName = new File(tempDir, "test-inputstream.ods").getAbsolutePath();
+ List writeData = generateTestData(3);
+
+ FesodSheet.write(fileName, OdsTestData.class)
+ .excelType(ExcelTypeEnum.ODS)
+ .sheet("TestSheet")
+ .doWrite(writeData);
+
+ // Read from InputStream without explicit type - should auto-detect as ODS
+ List readData = new ArrayList<>();
+ try (InputStream inputStream = new FileInputStream(fileName)) {
+ FesodSheet.read(inputStream, OdsTestData.class, new ReadListener() {
+ @Override
+ public void invoke(OdsTestData data, AnalysisContext context) {
+ readData.add(data);
+ }
+
+ @Override
+ public void doAfterAllAnalysed(AnalysisContext context) {}
+ })
+ .sheet()
+ .doRead();
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to read ODS from InputStream", e);
+ }
+
+ // Verify
+ assertNotNull(readData, "Read data should not be null");
+ assertEquals(3, readData.size(), "Should read 3 rows");
+ }
+
+ /**
+ * Test multiple sheets support.
+ */
+ @Test
+ public void testMultipleSheets() {
+ String fileName = new File(tempDir, "test-multi-sheet.ods").getAbsolutePath();
+ List dataList1 = generateTestData(5);
+ List dataList2 = generateTestData(3);
+
+ // Write multiple sheets using ExcelWriter
+ try (ExcelWriter excelWriter = FesodSheet.write(fileName, OdsTestData.class)
+ .excelType(ExcelTypeEnum.ODS)
+ .build()) {
+ excelWriter.write(dataList1, FesodSheet.writerSheet("Sheet1").build());
+ excelWriter.write(dataList2, FesodSheet.writerSheet("Sheet2").build());
+ }
+
+ // Read first sheet
+ List readData1 = new ArrayList<>();
+ FesodSheet.read(fileName, OdsTestData.class, new ReadListener() {
+ @Override
+ public void invoke(OdsTestData data, AnalysisContext context) {
+ readData1.add(data);
+ }
+
+ @Override
+ public void doAfterAllAnalysed(AnalysisContext context) {}
+ })
+ .sheet(0)
+ .doRead();
+
+ assertEquals(5, readData1.size(), "First sheet should have 5 rows");
+
+ // Read second sheet
+ List readData2 = new ArrayList<>();
+ FesodSheet.read(fileName, OdsTestData.class, new ReadListener() {
+ @Override
+ public void invoke(OdsTestData data, AnalysisContext context) {
+ readData2.add(data);
+ }
+
+ @Override
+ public void doAfterAllAnalysed(AnalysisContext context) {}
+ })
+ .sheet(1)
+ .doRead();
+
+ assertEquals(3, readData2.size(), "Second sheet should have 3 rows");
+ }
+
+ /**
+ * Test various data types.
+ */
+ @Test
+ public void testVariousDataTypes() {
+ String fileName = new File(tempDir, "test-datatypes.ods").getAbsolutePath();
+ List dataList = new ArrayList<>();
+
+ OdsExtendedTestData data1 = new OdsExtendedTestData();
+ data1.setString("Test String");
+ data1.setInteger(42);
+ data1.setLongValue(123456789L);
+ data1.setBooleanValue(true);
+ data1.setBigDecimal(new BigDecimal("123.456"));
+ data1.setDoubleValue(3.14159);
+ dataList.add(data1);
+
+ OdsExtendedTestData data2 = new OdsExtendedTestData();
+ data2.setString("Another String");
+ data2.setInteger(100);
+ data2.setLongValue(987654321L);
+ data2.setBooleanValue(false);
+ data2.setBigDecimal(new BigDecimal("999.999"));
+ data2.setDoubleValue(2.71828);
+ dataList.add(data2);
+
+ // Write
+ FesodSheet.write(fileName, OdsExtendedTestData.class)
+ .excelType(ExcelTypeEnum.ODS)
+ .sheet("DataTypes")
+ .doWrite(dataList);
+
+ // Read
+ List readData = new ArrayList<>();
+ FesodSheet.read(fileName, OdsExtendedTestData.class, new ReadListener() {
+ @Override
+ public void invoke(OdsExtendedTestData data, AnalysisContext context) {
+ readData.add(data);
+ }
+
+ @Override
+ public void doAfterAllAnalysed(AnalysisContext context) {}
+ })
+ .sheet()
+ .doRead();
+
+ assertEquals(2, readData.size(), "Should read 2 rows");
+ assertEquals("Test String", readData.get(0).getString());
+ assertEquals(Integer.valueOf(42), readData.get(0).getInteger());
+ assertEquals(Long.valueOf(123456789L), readData.get(0).getLongValue());
+ assertEquals(Boolean.TRUE, readData.get(0).getBooleanValue());
+ }
+
+ /**
+ * Test empty data list.
+ */
+ @Test
+ public void testEmptyData() {
+ String fileName = new File(tempDir, "test-empty.ods").getAbsolutePath();
+ List emptyList = new ArrayList<>();
+
+ FesodSheet.write(fileName, OdsTestData.class)
+ .excelType(ExcelTypeEnum.ODS)
+ .sheet("EmptySheet")
+ .doWrite(emptyList);
+
+ File outputFile = new File(fileName);
+ assertTrue(outputFile.exists(), "ODS file should be created even with empty data");
+
+ // Read empty file
+ List readData = new ArrayList<>();
+ FesodSheet.read(fileName, OdsTestData.class, new ReadListener() {
+ @Override
+ public void invoke(OdsTestData data, AnalysisContext context) {
+ readData.add(data);
+ }
+
+ @Override
+ public void doAfterAllAnalysed(AnalysisContext context) {}
+ })
+ .sheet()
+ .doRead();
+
+ assertEquals(0, readData.size(), "Should read 0 rows from empty file");
+ }
+
+ /**
+ * Test writing to OutputStream.
+ */
+ @Test
+ public void testWriteToOutputStream() throws Exception {
+ String fileName = new File(tempDir, "test-outputstream.ods").getAbsolutePath();
+ List dataList = generateTestData(5);
+
+ try (FileOutputStream outputStream = new FileOutputStream(fileName)) {
+ FesodSheet.write(outputStream, OdsTestData.class)
+ .excelType(ExcelTypeEnum.ODS)
+ .sheet("OutputStream")
+ .doWrite(dataList);
+ }
+
+ File outputFile = new File(fileName);
+ assertTrue(outputFile.exists(), "ODS file should be created");
+ assertTrue(outputFile.length() > 0, "ODS file should not be empty");
+ }
+
+ /**
+ * Test reading from real ODS file with multiple sheets.
+ * The test file has:
+ * - First sheet: 3 rows of data
+ * - Second sheet: 2 rows of data
+ */
+ @Test
+ public void testReadRealOdsFile() {
+ File resourceFile = TestFileUtil.readFile("ods/ods.ods");
+ assertTrue(resourceFile.exists(), "Test ODS file should exist");
+
+ // Read first sheet (should have 3 rows)
+ List readData1 = new ArrayList<>();
+ FesodSheet.read(resourceFile.getAbsolutePath(), OdsTestData.class, new ReadListener() {
+ @Override
+ public void invoke(OdsTestData data, AnalysisContext context) {
+ readData1.add(data);
+ }
+
+ @Override
+ public void doAfterAllAnalysed(AnalysisContext context) {}
+ })
+ .excelType(ExcelTypeEnum.ODS)
+ .sheet(0)
+ .doRead();
+
+ assertEquals(3, readData1.size(), "First sheet should have 3 rows");
+
+ // Read second sheet (should have 2 rows)
+ List readData2 = new ArrayList<>();
+ FesodSheet.read(resourceFile.getAbsolutePath(), OdsTestData.class, new ReadListener() {
+ @Override
+ public void invoke(OdsTestData data, AnalysisContext context) {
+ readData2.add(data);
+ }
+
+ @Override
+ public void doAfterAllAnalysed(AnalysisContext context) {}
+ })
+ .excelType(ExcelTypeEnum.ODS)
+ .sheet(1)
+ .doRead();
+
+ assertEquals(2, readData2.size(), "Second sheet should have 2 rows");
+
+ // Verify total rows across all sheets
+ assertEquals(5, readData1.size() + readData2.size(), "Total rows should be 5");
+ }
+
+ /**
+ * Generate test data.
+ */
+ private List generateTestData(int count) {
+ List list = new ArrayList<>();
+ for (int i = 0; i < count; i++) {
+ OdsTestData data = new OdsTestData();
+ data.setString("String" + i);
+ data.setDate(new Date());
+ data.setDoubleData(0.56 + i);
+ list.add(data);
+ }
+ return list;
+ }
+
+ /**
+ * Test data model for ODS tests.
+ */
+ @Getter
+ @Setter
+ @EqualsAndHashCode
+ public static class OdsTestData {
+ @ExcelProperty("String Title")
+ private String string;
+
+ @ExcelProperty("Date Title")
+ private Date date;
+
+ @ExcelProperty("Number Title")
+ private Double doubleData;
+ }
+
+ /**
+ * Extended test data model with various data types.
+ */
+ @Getter
+ @Setter
+ @EqualsAndHashCode
+ public static class OdsExtendedTestData {
+ @ExcelProperty("String")
+ private String string;
+
+ @ExcelProperty("Integer")
+ private Integer integer;
+
+ @ExcelProperty("Long")
+ private Long longValue;
+
+ @ExcelProperty("Boolean")
+ private Boolean booleanValue;
+
+ @ExcelProperty("BigDecimal")
+ private BigDecimal bigDecimal;
+
+ @ExcelProperty("Double")
+ private Double doubleValue;
+ }
+}
diff --git a/fesod-sheet/src/test/resources/ods/ods.ods b/fesod-sheet/src/test/resources/ods/ods.ods
new file mode 100644
index 000000000..487b1105c
Binary files /dev/null and b/fesod-sheet/src/test/resources/ods/ods.ods differ
diff --git a/pom.xml b/pom.xml
index 5b9d407b4..a0b5f18fe 100644
--- a/pom.xml
+++ b/pom.xml
@@ -64,6 +64,7 @@
3.5.4
true
true
+ 0.9.0
4.5.0
1.14.1
3.18.0
@@ -141,9 +142,14 @@
${commons-csv.version}