diff --git a/fesod-sheet/pom.xml b/fesod-sheet/pom.xml index 5c26670ef..7ca619430 100644 --- a/fesod-sheet/pom.xml +++ b/fesod-sheet/pom.xml @@ -58,6 +58,10 @@ fesod-shaded ${project.version} + + org.odftoolkit + odfdom-java + org.apache.poi poi diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/analysis/ExcelAnalyserImpl.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/analysis/ExcelAnalyserImpl.java index 464efdec3..f446c4345 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/analysis/ExcelAnalyserImpl.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/analysis/ExcelAnalyserImpl.java @@ -24,11 +24,14 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; import org.apache.fesod.sheet.analysis.csv.CsvExcelReadExecutor; +import org.apache.fesod.sheet.analysis.ods.OdsExcelReadExecutor; import org.apache.fesod.sheet.analysis.v03.XlsSaxAnalyser; import org.apache.fesod.sheet.analysis.v07.XlsxSaxAnalyser; import org.apache.fesod.sheet.context.AnalysisContext; import org.apache.fesod.sheet.context.csv.CsvReadContext; import org.apache.fesod.sheet.context.csv.DefaultCsvReadContext; +import org.apache.fesod.sheet.context.ods.DefaultOdsReadContext; +import org.apache.fesod.sheet.context.ods.OdsReadContext; import org.apache.fesod.sheet.context.xls.DefaultXlsReadContext; import org.apache.fesod.sheet.context.xls.XlsReadContext; import org.apache.fesod.sheet.context.xlsx.DefaultXlsxReadContext; @@ -39,6 +42,7 @@ import org.apache.fesod.sheet.read.metadata.ReadWorkbook; import org.apache.fesod.sheet.read.metadata.holder.ReadWorkbookHolder; import org.apache.fesod.sheet.read.metadata.holder.csv.CsvReadWorkbookHolder; +import org.apache.fesod.sheet.read.metadata.holder.ods.OdsReadWorkbookHolder; import org.apache.fesod.sheet.read.metadata.holder.xls.XlsReadWorkbookHolder; import org.apache.fesod.sheet.read.metadata.holder.xlsx.XlsxReadWorkbookHolder; import org.apache.fesod.sheet.support.ExcelTypeEnum; @@ -165,6 +169,12 @@ private void chooseExcelExecutor(ReadWorkbook readWorkbook) throws Exception { analysisContext = csvReadContext; excelReadExecutor = new CsvExcelReadExecutor(csvReadContext); break; + case ODS: + // Create a context and executor for processing ODS files + OdsReadContext odsReadContext = new DefaultOdsReadContext(readWorkbook, ExcelTypeEnum.ODS); + analysisContext = odsReadContext; + excelReadExecutor = new OdsExcelReadExecutor(odsReadContext); + break; default: // Reserved branch for handling potential future Excel types break; @@ -260,6 +270,18 @@ public void finish() { throwable = t; } + // close ods. + try { + if ((readWorkbookHolder instanceof OdsReadWorkbookHolder) + && ((OdsReadWorkbookHolder) readWorkbookHolder).getOdfSpreadsheetDocument() != null) { + ((OdsReadWorkbookHolder) readWorkbookHolder) + .getOdfSpreadsheetDocument() + .close(); + } + } catch (Throwable t) { + throwable = t; + } + try { if (analysisContext.readWorkbookHolder().getAutoCloseStream() && readWorkbookHolder.getInputStream() != null) { diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/analysis/ods/OdsExcelReadExecutor.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/analysis/ods/OdsExcelReadExecutor.java new file mode 100644 index 000000000..c562ba532 --- /dev/null +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/analysis/ods/OdsExcelReadExecutor.java @@ -0,0 +1,384 @@ +/* + * 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.analysis.ods; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.MapUtils; +import org.apache.fesod.sheet.analysis.ExcelReadExecutor; +import org.apache.fesod.sheet.context.ods.OdsReadContext; +import org.apache.fesod.sheet.enums.CellDataTypeEnum; +import org.apache.fesod.sheet.enums.RowTypeEnum; +import org.apache.fesod.sheet.exception.ExcelAnalysisException; +import org.apache.fesod.sheet.exception.ExcelAnalysisStopSheetException; +import org.apache.fesod.sheet.metadata.Cell; +import org.apache.fesod.sheet.metadata.data.ReadCellData; +import org.apache.fesod.sheet.read.metadata.ReadSheet; +import org.apache.fesod.sheet.read.metadata.holder.ReadRowHolder; +import org.apache.fesod.sheet.read.metadata.holder.ods.OdsReadWorkbookHolder; +import org.apache.fesod.sheet.util.SheetUtils; +import org.apache.fesod.sheet.util.StringUtils; +import org.odftoolkit.odfdom.doc.OdfSpreadsheetDocument; +import org.odftoolkit.odfdom.doc.table.OdfTable; +import org.odftoolkit.odfdom.doc.table.OdfTableCell; +import org.odftoolkit.odfdom.doc.table.OdfTableRow; +import org.odftoolkit.odfdom.dom.element.table.TableTableCellElementBase; +import org.w3c.dom.Node; + +/** + * ODS Excel Read Executor, responsible for reading and processing ODS (OpenDocument Spreadsheet) files. + */ +@Slf4j +public class OdsExcelReadExecutor implements ExcelReadExecutor { + + // List of sheets to be read + private final List sheetList; + // Context for ODS reading operation + private final OdsReadContext odsReadContext; + // ODF Spreadsheet Document + private OdfSpreadsheetDocument odfDocument; + + public OdsExcelReadExecutor(OdsReadContext odsReadContext) { + this.odsReadContext = odsReadContext; + this.sheetList = new ArrayList<>(); + initSheetList(); + } + + /** + * Initialize the sheet list from the ODS document. + */ + private void initSheetList() { + try { + OdsReadWorkbookHolder workbookHolder = odsReadContext.odsReadWorkbookHolder(); + if (workbookHolder.getFile() != null) { + odfDocument = OdfSpreadsheetDocument.loadDocument(workbookHolder.getFile()); + } else if (workbookHolder.getInputStream() != null) { + odfDocument = OdfSpreadsheetDocument.loadDocument(workbookHolder.getInputStream()); + } else { + throw new ExcelAnalysisException("File and inputStream must be a non-null."); + } + workbookHolder.setOdfSpreadsheetDocument(odfDocument); + + List tables = odfDocument.getTableList(); + for (int i = 0; i < tables.size(); i++) { + OdfTable table = tables.get(i); + ReadSheet readSheet = new ReadSheet(); + readSheet.setSheetNo(i); + readSheet.setSheetName(table.getTableName()); + sheetList.add(readSheet); + } + } catch (Exception e) { + throw new ExcelAnalysisException("Failed to load ODS document", e); + } + } + + @Override + public List sheetList() { + return sheetList; + } + + /** + * Execute the reading process for all sheets. + */ + @Override + public void execute() { + List tables = odfDocument.getTableList(); + + for (ReadSheet readSheet : sheetList) { + readSheet = SheetUtils.match(readSheet, odsReadContext); + if (readSheet == null) { + continue; + } + + try { + odsReadContext.currentSheet(readSheet); + + OdfTable table = tables.get(readSheet.getSheetNo()); + int rowCount = table.getRowCount(); + + for (int rowIndex = 0; rowIndex < rowCount; rowIndex++) { + OdfTableRow row = table.getRowByIndex(rowIndex); + if (row == null) { + continue; + } + dealRow(table, row, rowIndex); + } + } catch (ExcelAnalysisStopSheetException e) { + if (log.isDebugEnabled()) { + log.debug("Custom stop!", e); + } + } + + odsReadContext.analysisEventProcessor().endSheet(odsReadContext); + } + } + + /** + * Process a single row from the ODS table. + * Uses DOM traversal to avoid performance issues with getCellByIndex() and getCellCount(). + * + * @param table The ODF table + * @param row The ODF table row + * @param rowIndex The index of the current row + */ + private void dealRow(OdfTable table, OdfTableRow row, int rowIndex) { + Map cellMap = new LinkedHashMap<>(); + Boolean autoTrim = + odsReadContext.odsReadWorkbookHolder().globalConfiguration().getAutoTrim(); + Boolean autoStrip = + odsReadContext.odsReadWorkbookHolder().globalConfiguration().getAutoStrip(); + + // Use DOM traversal to iterate through cells directly + // This avoids performance issues with getCellByIndex() which can trigger column expansion + int columnIndex = 0; + Node cellNode = row.getOdfElement().getFirstChild(); + + while (cellNode != null) { + if (cellNode instanceof TableTableCellElementBase) { + TableTableCellElementBase cellElement = (TableTableCellElementBase) cellNode; + + // Handle repeated cells + int repeatCount = 1; + Integer columnsRepeated = cellElement.getTableNumberColumnsRepeatedAttribute(); + if (columnsRepeated != null && columnsRepeated > 1) { + repeatCount = columnsRepeated; + } + + // Process cell data directly from the DOM element + processCellDataFromElement(cellMap, cellElement, rowIndex, columnIndex, autoTrim, autoStrip); + + // For repeated cells, advance the column index accordingly + columnIndex += repeatCount; + } + cellNode = cellNode.getNextSibling(); + } + + RowTypeEnum rowType = MapUtils.isEmpty(cellMap) ? RowTypeEnum.EMPTY : RowTypeEnum.DATA; + ReadRowHolder readRowHolder = new ReadRowHolder( + rowIndex, rowType, odsReadContext.readWorkbookHolder().getGlobalConfiguration(), cellMap); + odsReadContext.readRowHolder(readRowHolder); + + odsReadContext.odsReadSheetHolder().setCellMap(cellMap); + odsReadContext.odsReadSheetHolder().setRowIndex(rowIndex); + odsReadContext.analysisEventProcessor().endRow(odsReadContext); + } + + /** + * Process cell data directly from DOM element and add to the cell map. + */ + private void processCellDataFromElement( + Map cellMap, + TableTableCellElementBase cellElement, + int rowIndex, + int columnIndex, + Boolean autoTrim, + Boolean autoStrip) { + ReadCellData readCellData = new ReadCellData<>(); + readCellData.setRowIndex(rowIndex); + readCellData.setColumnIndex(columnIndex); + + String cellValue = getCellValueFromElement(cellElement); + String valueType = cellElement.getOfficeValueTypeAttribute(); + + if (StringUtils.isNotBlank(cellValue)) { + readCellData.setType(determineCellTypeFromElement(valueType)); + if (autoStrip) { + readCellData.setStringValue(StringUtils.strip(cellValue)); + } else if (autoTrim) { + readCellData.setStringValue(cellValue.trim()); + } else { + readCellData.setStringValue(cellValue); + } + + // Handle numeric values + if (readCellData.getType() == CellDataTypeEnum.NUMBER) { + try { + Double numericValue = cellElement.getOfficeValueAttribute(); + if (numericValue != null) { + readCellData.setNumberValue(new java.math.BigDecimal(numericValue.toString())); + } + } catch (Exception e) { + // Keep as string if parsing fails + readCellData.setType(CellDataTypeEnum.STRING); + } + } + + // Handle boolean values + if (readCellData.getType() == CellDataTypeEnum.BOOLEAN) { + try { + Boolean boolValue = cellElement.getOfficeBooleanValueAttribute(); + if (boolValue != null) { + readCellData.setBooleanValue(boolValue); + } + } catch (Exception e) { + readCellData.setType(CellDataTypeEnum.STRING); + } + } + + cellMap.put(columnIndex, readCellData); + } else { + readCellData.setType(CellDataTypeEnum.EMPTY); + // Don't add empty cells to the map to save memory + } + } + + /** + * Get cell value directly from DOM element. + */ + private String getCellValueFromElement(TableTableCellElementBase cellElement) { + if (cellElement == null) { + return null; + } + + String valueType = cellElement.getOfficeValueTypeAttribute(); + if (valueType == null) { + // Try to get text content + return cellElement.getTextContent(); + } + + switch (valueType) { + case "float": + case "currency": + case "percentage": + Double doubleValue = cellElement.getOfficeValueAttribute(); + if (doubleValue != null) { + // Remove trailing zeros for display + if (doubleValue == Math.floor(doubleValue) && !Double.isInfinite(doubleValue)) { + return String.valueOf(doubleValue.longValue()); + } + return doubleValue.toString(); + } + return cellElement.getTextContent(); + case "date": + case "time": + return cellElement.getTextContent(); + case "boolean": + Boolean boolValue = cellElement.getOfficeBooleanValueAttribute(); + return boolValue != null ? boolValue.toString() : cellElement.getTextContent(); + case "string": + default: + String stringValue = cellElement.getOfficeStringValueAttribute(); + if (stringValue != null) { + return stringValue; + } + return cellElement.getTextContent(); + } + } + + /** + * Determine cell type from value type string. + */ + private CellDataTypeEnum determineCellTypeFromElement(String valueType) { + if (valueType == null) { + return CellDataTypeEnum.STRING; + } + + switch (valueType) { + case "float": + case "currency": + case "percentage": + return CellDataTypeEnum.NUMBER; + case "date": + case "time": + return CellDataTypeEnum.STRING; + case "boolean": + return CellDataTypeEnum.BOOLEAN; + case "string": + default: + return CellDataTypeEnum.STRING; + } + } + + /** + * Get the string value from an ODF cell. + * + * @param cell The ODF table cell + * @return The cell value as a string + */ + private String getCellValue(OdfTableCell cell) { + if (cell == null) { + return null; + } + + String valueType = cell.getValueType(); + if (valueType == null) { + return cell.getDisplayText(); + } + + switch (valueType) { + case "float": + case "currency": + case "percentage": + Double doubleValue = cell.getDoubleValue(); + if (doubleValue != null) { + // Remove trailing zeros for display + if (doubleValue == Math.floor(doubleValue) && !Double.isInfinite(doubleValue)) { + return String.valueOf(doubleValue.longValue()); + } + return doubleValue.toString(); + } + return null; + case "date": + case "time": + return cell.getDisplayText(); + case "boolean": + Boolean boolValue = cell.getBooleanValue(); + return boolValue != null ? boolValue.toString() : null; + case "string": + default: + return cell.getStringValue(); + } + } + + /** + * Determine the cell data type based on ODF cell type. + * + * @param cell The ODF table cell + * @return The corresponding CellDataTypeEnum + */ + private CellDataTypeEnum determineCellType(OdfTableCell cell) { + if (cell == null) { + return CellDataTypeEnum.EMPTY; + } + + String valueType = cell.getValueType(); + if (valueType == null) { + return CellDataTypeEnum.STRING; + } + + switch (valueType) { + case "float": + case "currency": + case "percentage": + return CellDataTypeEnum.NUMBER; + case "date": + case "time": + return CellDataTypeEnum.STRING; // Dates are returned as formatted strings + case "boolean": + return CellDataTypeEnum.BOOLEAN; + case "string": + default: + return CellDataTypeEnum.STRING; + } + } +} diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/AnalysisContextImpl.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/AnalysisContextImpl.java index 6617ba1ba..4c4886203 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/AnalysisContextImpl.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/AnalysisContextImpl.java @@ -31,6 +31,8 @@ import org.apache.fesod.sheet.read.metadata.holder.ReadWorkbookHolder; import org.apache.fesod.sheet.read.metadata.holder.csv.CsvReadSheetHolder; import org.apache.fesod.sheet.read.metadata.holder.csv.CsvReadWorkbookHolder; +import org.apache.fesod.sheet.read.metadata.holder.ods.OdsReadSheetHolder; +import org.apache.fesod.sheet.read.metadata.holder.ods.OdsReadWorkbookHolder; import org.apache.fesod.sheet.read.metadata.holder.xls.XlsReadSheetHolder; import org.apache.fesod.sheet.read.metadata.holder.xls.XlsReadWorkbookHolder; import org.apache.fesod.sheet.read.metadata.holder.xlsx.XlsxReadSheetHolder; @@ -79,6 +81,9 @@ public AnalysisContextImpl(ReadWorkbook readWorkbook, ExcelTypeEnum actualExcelT case CSV: readWorkbookHolder = new CsvReadWorkbookHolder(readWorkbook); break; + case ODS: + readWorkbookHolder = new OdsReadWorkbookHolder(readWorkbook); + break; default: break; } @@ -101,6 +106,9 @@ public void currentSheet(ReadSheet readSheet) { case CSV: readSheetHolder = new CsvReadSheetHolder(readSheet, readWorkbookHolder); break; + case ODS: + readSheetHolder = new OdsReadSheetHolder(readSheet, readWorkbookHolder); + break; default: break; } diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/ods/DefaultOdsReadContext.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/ods/DefaultOdsReadContext.java new file mode 100644 index 000000000..6d20079c9 --- /dev/null +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/ods/DefaultOdsReadContext.java @@ -0,0 +1,47 @@ +/* + * 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.context.ods; + +import org.apache.fesod.sheet.context.AnalysisContextImpl; +import org.apache.fesod.sheet.read.metadata.ReadWorkbook; +import org.apache.fesod.sheet.read.metadata.holder.ods.OdsReadSheetHolder; +import org.apache.fesod.sheet.read.metadata.holder.ods.OdsReadWorkbookHolder; +import org.apache.fesod.sheet.support.ExcelTypeEnum; + +/** + * A context is the main anchorage point of an ODS reader. + * + */ +public class DefaultOdsReadContext extends AnalysisContextImpl implements OdsReadContext { + + public DefaultOdsReadContext(ReadWorkbook readWorkbook, ExcelTypeEnum actualExcelType) { + super(readWorkbook, actualExcelType); + } + + @Override + public OdsReadWorkbookHolder odsReadWorkbookHolder() { + return (OdsReadWorkbookHolder) readWorkbookHolder(); + } + + @Override + public OdsReadSheetHolder odsReadSheetHolder() { + return (OdsReadSheetHolder) readSheetHolder(); + } +} diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/ods/OdsReadContext.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/ods/OdsReadContext.java new file mode 100644 index 000000000..a937cbc43 --- /dev/null +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/ods/OdsReadContext.java @@ -0,0 +1,44 @@ +/* + * 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.context.ods; + +import org.apache.fesod.sheet.context.AnalysisContext; +import org.apache.fesod.sheet.read.metadata.holder.ods.OdsReadSheetHolder; +import org.apache.fesod.sheet.read.metadata.holder.ods.OdsReadWorkbookHolder; + +/** + * A context is the main anchorage point of an ODS reader. + * + */ +public interface OdsReadContext extends AnalysisContext { + /** + * All information about the workbook you are currently working on. + * + * @return Current workbook holder + */ + OdsReadWorkbookHolder odsReadWorkbookHolder(); + + /** + * All information about the sheet you are currently working on. + * + * @return Current sheet holder + */ + OdsReadSheetHolder odsReadSheetHolder(); +} diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/ods/OdsCell.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/ods/OdsCell.java new file mode 100644 index 000000000..eaf322056 --- /dev/null +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/ods/OdsCell.java @@ -0,0 +1,359 @@ +/* + * 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.math.BigDecimal; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Calendar; +import java.util.Date; +import lombok.AccessLevel; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import org.apache.fesod.sheet.enums.NumericCellTypeEnum; +import org.apache.fesod.sheet.metadata.data.FormulaData; +import org.apache.poi.ss.SpreadsheetVersion; +import org.apache.poi.ss.usermodel.CellBase; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.CellType; +import org.apache.poi.ss.usermodel.Comment; +import org.apache.poi.ss.usermodel.Hyperlink; +import org.apache.poi.ss.usermodel.RichTextString; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.util.CellRangeAddress; + +/** + * ODS cell implementation for writing ODS files. + * + */ +@Getter +@Setter +@EqualsAndHashCode +public class OdsCell extends CellBase { + + /** + * column index + */ + @Getter(value = AccessLevel.NONE) + @Setter(value = AccessLevel.NONE) + private Integer columnIndex; + + /** + * cell type + */ + @Getter(value = AccessLevel.NONE) + @Setter(value = AccessLevel.NONE) + private CellType cellType; + + /** + * numeric cell type + */ + private NumericCellTypeEnum numericCellType; + + /** + * workbook + */ + private final OdsWorkbook odsWorkbook; + + /** + * sheet + */ + private final OdsSheet odsSheet; + + /** + * row + */ + private final OdsRow odsRow; + + /** + * {@link CellType#NUMERIC} + */ + private BigDecimal numberValue; + + /** + * {@link CellType#STRING} and {@link CellType#ERROR} {@link CellType#FORMULA} + */ + private String stringValue; + + /** + * {@link CellType#BOOLEAN} + */ + private Boolean booleanValue; + + /** + * {@link CellType#NUMERIC} + */ + private LocalDateTime dateValue; + + /** + * formula + */ + private FormulaData formulaData; + + /** + * rich text string + */ + private RichTextString richTextString; + + /** + * style + */ + private CellStyle cellStyle; + + public OdsCell(OdsWorkbook odsWorkbook, OdsSheet odsSheet, OdsRow odsRow, Integer columnIndex, CellType cellType) { + this.odsWorkbook = odsWorkbook; + this.odsSheet = odsSheet; + this.odsRow = odsRow; + this.columnIndex = columnIndex; + this.cellType = cellType; + if (this.cellType == null) { + this.cellType = CellType._NONE; + } + } + + @Override + protected void setCellTypeImpl(CellType cellType) { + this.cellType = cellType; + } + + @Override + protected void setCellFormulaImpl(String formula) { + FormulaData formulaData = new FormulaData(); + formulaData.setFormulaValue(formula); + this.formulaData = formulaData; + this.cellType = CellType.FORMULA; + } + + @Override + protected void removeFormulaImpl() { + this.formulaData = null; + } + + @Override + protected void setCellValueImpl(double value) { + numberValue = BigDecimal.valueOf(value); + this.cellType = CellType.NUMERIC; + } + + @Override + protected void setCellValueImpl(Date value) { + if (value == null) { + return; + } + this.dateValue = LocalDateTime.ofInstant(value.toInstant(), ZoneId.systemDefault()); + this.cellType = CellType.NUMERIC; + this.numericCellType = NumericCellTypeEnum.DATE; + } + + @Override + protected void setCellValueImpl(LocalDateTime value) { + this.dateValue = value; + this.cellType = CellType.NUMERIC; + this.numericCellType = NumericCellTypeEnum.DATE; + } + + @Override + protected void setCellValueImpl(Calendar value) { + if (value == null) { + return; + } + this.dateValue = LocalDateTime.ofInstant(value.toInstant(), ZoneId.systemDefault()); + this.cellType = CellType.NUMERIC; + } + + @Override + protected void setCellValueImpl(String value) { + this.stringValue = value; + this.cellType = CellType.STRING; + } + + @Override + protected void setCellValueImpl(RichTextString value) { + richTextString = value; + this.cellType = CellType.STRING; + } + + @Override + public void setCellValue(String value) { + if (value == null) { + setBlank(); + return; + } + setCellValueImpl(value); + } + + @Override + public void setCellValue(RichTextString value) { + if (value == null || value.getString() == null) { + setBlank(); + return; + } + setCellValueImpl(value); + } + + @Override + protected SpreadsheetVersion getSpreadsheetVersion() { + return null; + } + + @Override + public int getColumnIndex() { + return columnIndex; + } + + @Override + public int getRowIndex() { + return odsRow.getRowNum(); + } + + @Override + public Sheet getSheet() { + return odsRow.getSheet(); + } + + @Override + public Row getRow() { + return odsRow; + } + + @Override + public CellType getCellType() { + return cellType; + } + + @Override + public CellType getCachedFormulaResultType() { + return getCellType(); + } + + @Override + public String getCellFormula() { + if (formulaData == null) { + return null; + } + return formulaData.getFormulaValue(); + } + + @Override + public double getNumericCellValue() { + if (numberValue == null) { + return 0; + } + return numberValue.doubleValue(); + } + + @Override + public Date getDateCellValue() { + if (dateValue == null) { + return null; + } + return Date.from(dateValue.atZone(ZoneId.systemDefault()).toInstant()); + } + + @Override + public LocalDateTime getLocalDateTimeCellValue() { + return dateValue; + } + + @Override + public RichTextString getRichStringCellValue() { + return richTextString; + } + + @Override + public String getStringCellValue() { + return stringValue; + } + + @Override + public void setCellValue(boolean value) { + this.booleanValue = value; + this.cellType = CellType.BOOLEAN; + } + + @Override + public void setCellErrorValue(byte value) { + this.numberValue = BigDecimal.valueOf(value); + this.cellType = CellType.ERROR; + } + + @Override + public boolean getBooleanCellValue() { + if (booleanValue == null) { + return false; + } + return booleanValue; + } + + @Override + public byte getErrorCellValue() { + if (numberValue == null) { + return 0; + } + return numberValue.byteValue(); + } + + @Override + public void setCellStyle(CellStyle style) { + this.cellStyle = style; + } + + @Override + public CellStyle getCellStyle() { + return cellStyle; + } + + @Override + public void setAsActiveCell() {} + + @Override + public void setCellComment(Comment comment) {} + + @Override + public Comment getCellComment() { + return null; + } + + @Override + public void removeCellComment() {} + + @Override + public Hyperlink getHyperlink() { + return null; + } + + @Override + public void setHyperlink(Hyperlink link) {} + + @Override + public void removeHyperlink() {} + + @Override + public CellRangeAddress getArrayFormulaRange() { + return null; + } + + @Override + public boolean isPartOfArrayFormulaGroup() { + return false; + } +} diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/ods/OdsCellStyle.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/ods/OdsCellStyle.java new file mode 100644 index 000000000..7785ce473 --- /dev/null +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/ods/OdsCellStyle.java @@ -0,0 +1,292 @@ +/* + * 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.util.EnumMap; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import org.apache.fesod.sheet.metadata.data.DataFormatData; +import org.apache.poi.ss.usermodel.BorderStyle; +import org.apache.poi.ss.usermodel.CellPropertyType; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.Color; +import org.apache.poi.ss.usermodel.FillPatternType; +import org.apache.poi.ss.usermodel.Font; +import org.apache.poi.ss.usermodel.HorizontalAlignment; +import org.apache.poi.ss.usermodel.VerticalAlignment; +import org.apache.poi.ss.util.CellUtil; + +/** + * ODS cell style implementation for writing ODS files. + * + */ +@Getter +@Setter +@EqualsAndHashCode +public class OdsCellStyle implements CellStyle { + + /** + * data format + */ + private DataFormatData dataFormatData; + + /** + * index + */ + private Short index; + + public OdsCellStyle(Short index) { + this.index = index; + } + + @Override + public short getIndex() { + return index; + } + + @Override + public void setDataFormat(short fmt) { + initDataFormatData(); + dataFormatData.setIndex(fmt); + } + + private void initDataFormatData() { + if (dataFormatData == null) { + dataFormatData = new DataFormatData(); + } + } + + @Override + public short getDataFormat() { + if (dataFormatData == null) { + return 0; + } + return dataFormatData.getIndex(); + } + + @Override + public String getDataFormatString() { + if (dataFormatData == null) { + return null; + } + return dataFormatData.getFormat(); + } + + @Override + public void setFont(Font font) {} + + @Override + public int getFontIndex() { + return 0; + } + + @Override + public int getFontIndexAsInt() { + 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 setAlignment(HorizontalAlignment align) {} + + @Override + public HorizontalAlignment getAlignment() { + return null; + } + + @Override + public void setWrapText(boolean wrapped) {} + + @Override + public boolean getWrapText() { + return false; + } + + @Override + public void setVerticalAlignment(VerticalAlignment align) {} + + @Override + public VerticalAlignment getVerticalAlignment() { + return null; + } + + @Override + public void setRotation(short rotation) {} + + @Override + public short getRotation() { + return 0; + } + + @Override + public void setIndention(short indent) {} + + @Override + public short getIndention() { + return 0; + } + + @Override + public void setBorderLeft(BorderStyle border) {} + + @Override + public BorderStyle getBorderLeft() { + return null; + } + + @Override + public void setBorderRight(BorderStyle border) {} + + @Override + public BorderStyle getBorderRight() { + return null; + } + + @Override + public void setBorderTop(BorderStyle border) {} + + @Override + public BorderStyle getBorderTop() { + return null; + } + + @Override + public void setBorderBottom(BorderStyle border) {} + + @Override + public BorderStyle getBorderBottom() { + return null; + } + + @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 setBottomBorderColor(short color) {} + + @Override + public short getBottomBorderColor() { + return 0; + } + + @Override + public void setFillPattern(FillPatternType fp) {} + + @Override + public FillPatternType getFillPattern() { + return null; + } + + @Override + public void setFillBackgroundColor(short bg) {} + + @Override + public void setFillBackgroundColor(Color color) {} + + @Override + public short getFillBackgroundColor() { + return 0; + } + + @Override + public Color getFillBackgroundColorColor() { + return null; + } + + @Override + public void setFillForegroundColor(short bg) {} + + @Override + public void setFillForegroundColor(Color color) {} + + @Override + public short getFillForegroundColor() { + return 0; + } + + @Override + public Color getFillForegroundColorColor() { + return null; + } + + @Override + public void cloneStyleFrom(CellStyle source) {} + + @Override + public void setShrinkToFit(boolean shrinkToFit) {} + + @Override + public boolean getShrinkToFit() { + return false; + } + + @Override + public EnumMap getFormatProperties() { + return CellUtil.getFormatProperties(this); + } + + @Override + public void invalidateCachedProperties() {} +} diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/ods/OdsDataFormat.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/ods/OdsDataFormat.java new file mode 100644 index 000000000..47188f5d2 --- /dev/null +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/ods/OdsDataFormat.java @@ -0,0 +1,84 @@ +/* + * 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.util.List; +import java.util.Locale; +import java.util.Map; +import org.apache.fesod.sheet.constant.BuiltinFormats; +import org.apache.fesod.sheet.util.ListUtils; +import org.apache.fesod.sheet.util.MapUtils; +import org.apache.poi.ss.usermodel.DataFormat; + +/** + * ODS data format implementation for writing ODS files. + * + */ +public class OdsDataFormat implements DataFormat { + /** + * It is stored in both map and list for easy retrieval + */ + private final Map formatMap; + + private final List formatList; + + /** + * Excel's built-in format conversion. + */ + private final Map builtinFormatsMap; + + private final String[] builtinFormats; + + public OdsDataFormat(Locale locale) { + formatMap = MapUtils.newHashMap(); + formatList = ListUtils.newArrayList(); + builtinFormatsMap = BuiltinFormats.switchBuiltinFormatsMap(locale); + builtinFormats = BuiltinFormats.switchBuiltinFormats(locale); + } + + @Override + public short getFormat(String format) { + Short index = builtinFormatsMap.get(format); + if (index != null) { + return index; + } + index = formatMap.get(format); + if (index != null) { + return index; + } + short indexPrimitive = (short) (formatList.size() + BuiltinFormats.MIN_CUSTOM_DATA_FORMAT_INDEX); + index = indexPrimitive; + formatList.add(format); + formatMap.put(format, index); + return indexPrimitive; + } + + @Override + public String getFormat(short index) { + if (index < BuiltinFormats.MIN_CUSTOM_DATA_FORMAT_INDEX) { + return builtinFormats[index]; + } + int actualIndex = index - BuiltinFormats.MIN_CUSTOM_DATA_FORMAT_INDEX; + if (actualIndex < formatList.size()) { + return formatList.get(actualIndex); + } + return null; + } +} diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/ods/OdsRow.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/ods/OdsRow.java new file mode 100644 index 000000000..5034d940d --- /dev/null +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/ods/OdsRow.java @@ -0,0 +1,211 @@ +/* + * 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.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.CellType; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; + +/** + * ODS row implementation for writing ODS files. + * + */ +@Getter +@Setter +@EqualsAndHashCode +public class OdsRow implements Row { + + /** + * cell list + */ + private final List cellList; + + /** + * workbook + */ + private final OdsWorkbook odsWorkbook; + + /** + * sheet + */ + private final OdsSheet odsSheet; + + /** + * row index + */ + private Integer rowIndex; + + /** + * style + */ + private CellStyle cellStyle; + + public OdsRow(OdsWorkbook odsWorkbook, OdsSheet odsSheet, Integer rowIndex) { + this.cellList = new ArrayList<>(); + this.odsWorkbook = odsWorkbook; + this.odsSheet = odsSheet; + this.rowIndex = rowIndex; + } + + @Override + public Cell createCell(int column) { + OdsCell cell = new OdsCell(odsWorkbook, odsSheet, this, column, null); + cellList.add(cell); + return cell; + } + + @Override + public Cell createCell(int column, CellType type) { + OdsCell cell = new OdsCell(odsWorkbook, odsSheet, this, column, type); + cellList.add(cell); + return cell; + } + + @Override + public void removeCell(Cell cell) { + cellList.remove(cell); + } + + @Override + public void setRowNum(int rowNum) { + this.rowIndex = rowNum; + } + + @Override + public int getRowNum() { + return rowIndex; + } + + @Override + public Cell getCell(int cellnum) { + for (OdsCell cell : cellList) { + if (cell.getColumnIndex() == cellnum) { + return cell; + } + } + return null; + } + + @Override + public Cell getCell(int cellnum, MissingCellPolicy policy) { + return getCell(cellnum); + } + + @Override + public short getFirstCellNum() { + if (CollectionUtils.isEmpty(cellList)) { + return -1; + } + return 0; + } + + @Override + public short getLastCellNum() { + if (CollectionUtils.isEmpty(cellList)) { + return -1; + } + int maxIndex = 0; + for (OdsCell cell : cellList) { + if (cell.getColumnIndex() > maxIndex) { + maxIndex = cell.getColumnIndex(); + } + } + return (short) (maxIndex + 1); + } + + @Override + public int getPhysicalNumberOfCells() { + return cellList.size(); + } + + @Override + public void setHeight(short height) {} + + @Override + public void setZeroHeight(boolean zHeight) {} + + @Override + public boolean getZeroHeight() { + return false; + } + + @Override + public void setHeightInPoints(float height) {} + + @Override + public short getHeight() { + return 0; + } + + @Override + public float getHeightInPoints() { + return 0; + } + + @Override + public boolean isFormatted() { + return false; + } + + @Override + public CellStyle getRowStyle() { + return cellStyle; + } + + @Override + public void setRowStyle(CellStyle style) { + this.cellStyle = style; + } + + @Override + public Iterator cellIterator() { + return (Iterator) (Iterator) cellList.iterator(); + } + + @Override + public Sheet getSheet() { + return odsSheet; + } + + @Override + public int getOutlineLevel() { + return 0; + } + + @Override + public void shiftCellsRight(int firstShiftColumnIndex, int lastShiftColumnIndex, int step) {} + + @Override + public void shiftCellsLeft(int firstShiftColumnIndex, int lastShiftColumnIndex, int step) {} + + @Override + public Iterator iterator() { + return cellIterator(); + } +} diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/ods/OdsSheet.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/ods/OdsSheet.java new file mode 100644 index 000000000..e3c47af0c --- /dev/null +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/ods/OdsSheet.java @@ -0,0 +1,694 @@ +/* + * 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.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import org.apache.fesod.sheet.exception.ExcelGenerateException; +import org.apache.poi.ss.usermodel.AutoFilter; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellRange; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.Comment; +import org.apache.poi.ss.usermodel.DataValidation; +import org.apache.poi.ss.usermodel.DataValidationHelper; +import org.apache.poi.ss.usermodel.Drawing; +import org.apache.poi.ss.usermodel.Footer; +import org.apache.poi.ss.usermodel.Header; +import org.apache.poi.ss.usermodel.Hyperlink; +import org.apache.poi.ss.usermodel.PageMargin; +import org.apache.poi.ss.usermodel.PaneType; +import org.apache.poi.ss.usermodel.PrintSetup; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.SheetConditionalFormatting; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.util.CellAddress; +import org.apache.poi.ss.util.CellRangeAddress; +import org.apache.poi.ss.util.PaneInformation; +import org.odftoolkit.odfdom.doc.table.OdfTable; +import org.odftoolkit.odfdom.doc.table.OdfTableCell; +import org.odftoolkit.odfdom.doc.table.OdfTableRow; + +/** + * ODS sheet implementation for writing ODS files. + * + */ +@Getter +@Setter +@EqualsAndHashCode +public class OdsSheet implements Sheet { + + /** + * workbook + */ + private OdsWorkbook odsWorkbook; + + /** + * sheet name + */ + private String sheetName; + + /** + * row list + */ + private List rowList; + + /** + * last row index + */ + private Integer lastRowIndex; + + /** + * ODF Table (created when flushing) + */ + private OdfTable odfTable; + + public OdsSheet(OdsWorkbook odsWorkbook, String sheetName) { + this.odsWorkbook = odsWorkbook; + this.sheetName = sheetName; + this.rowList = new ArrayList<>(); + this.lastRowIndex = -1; + } + + /** + * Flush all data to the ODF document. + */ + public void flushToDocument() { + try { + odfTable = OdfTable.newTable(odsWorkbook.getOdfDocument(), rowList.size(), getMaxColumnCount()); + odfTable.setTableName(sheetName); + + for (OdsRow odsRow : rowList) { + int rowIndex = odsRow.getRowIndex(); + OdfTableRow odfRow = odfTable.getRowByIndex(rowIndex); + + Iterator cellIterator = odsRow.cellIterator(); + while (cellIterator.hasNext()) { + OdsCell odsCell = (OdsCell) cellIterator.next(); + int colIndex = odsCell.getColumnIndex(); + OdfTableCell odfCell = odfRow.getCellByIndex(colIndex); + + writeCellValue(odfCell, odsCell); + } + } + } catch (Exception e) { + throw new ExcelGenerateException("Failed to flush ODS sheet data", e); + } + } + + /** + * Get the maximum column count across all rows. + */ + private int getMaxColumnCount() { + int max = 1; + for (OdsRow row : rowList) { + if (row.getLastCellNum() > max) { + max = row.getLastCellNum(); + } + } + return max; + } + + /** + * Write cell value to ODF cell. + */ + private void writeCellValue(OdfTableCell odfCell, OdsCell odsCell) { + if (odsCell == null || odfCell == null) { + return; + } + + switch (odsCell.getCellType()) { + case STRING: + String stringValue = odsCell.getStringCellValue(); + if (stringValue != null) { + odfCell.setStringValue(stringValue); + } + break; + case NUMERIC: + if (odsCell.getDateValue() != null) { + // Handle date + odfCell.setDateValue(java.util.Calendar.getInstance()); + java.util.Calendar cal = java.util.Calendar.getInstance(); + cal.set( + odsCell.getDateValue().getYear(), + odsCell.getDateValue().getMonthValue() - 1, + odsCell.getDateValue().getDayOfMonth(), + odsCell.getDateValue().getHour(), + odsCell.getDateValue().getMinute(), + odsCell.getDateValue().getSecond()); + odfCell.setDateValue(cal); + } else if (odsCell.getNumberValue() != null) { + odfCell.setDoubleValue(odsCell.getNumberValue().doubleValue()); + } + break; + case BOOLEAN: + odfCell.setBooleanValue(odsCell.getBooleanCellValue()); + break; + case FORMULA: + String formula = odsCell.getCellFormula(); + if (formula != null) { + odfCell.setFormula(formula); + } + break; + case BLANK: + case _NONE: + default: + // Leave cell empty + break; + } + } + + @Override + public Row createRow(int rownum) { + lastRowIndex++; + OdsRow odsRow = new OdsRow(odsWorkbook, this, rownum); + rowList.add(odsRow); + return odsRow; + } + + @Override + public void removeRow(Row row) { + rowList.remove(row); + } + + @Override + public Row getRow(int rownum) { + for (OdsRow row : rowList) { + if (row.getRowIndex() == rownum) { + return row; + } + } + return null; + } + + @Override + public int getPhysicalNumberOfRows() { + return rowList.size(); + } + + @Override + public int getFirstRowNum() { + if (rowList.isEmpty()) { + return -1; + } + return 0; + } + + @Override + public int getLastRowNum() { + return lastRowIndex; + } + + @Override + public void setColumnHidden(int columnIndex, boolean hidden) {} + + @Override + public boolean isColumnHidden(int columnIndex) { + return false; + } + + @Override + public void setRightToLeft(boolean value) {} + + @Override + public boolean isRightToLeft() { + return false; + } + + @Override + public void setColumnWidth(int columnIndex, int width) {} + + @Override + public int getColumnWidth(int columnIndex) { + return 0; + } + + @Override + public float getColumnWidthInPixels(int columnIndex) { + return 0; + } + + @Override + public void setDefaultColumnWidth(int width) {} + + @Override + public int getDefaultColumnWidth() { + return 0; + } + + @Override + public short getDefaultRowHeight() { + return 0; + } + + @Override + public float getDefaultRowHeightInPoints() { + return 0; + } + + @Override + public void setDefaultRowHeight(short height) {} + + @Override + public void setDefaultRowHeightInPoints(float height) {} + + @Override + public CellStyle getColumnStyle(int column) { + return null; + } + + @Override + public int addMergedRegion(CellRangeAddress region) { + return 0; + } + + @Override + public int addMergedRegionUnsafe(CellRangeAddress region) { + return 0; + } + + @Override + public void validateMergedRegions() {} + + @Override + public void setVerticallyCenter(boolean value) {} + + @Override + public void setHorizontallyCenter(boolean value) {} + + @Override + public boolean getHorizontallyCenter() { + return false; + } + + @Override + public boolean getVerticallyCenter() { + return false; + } + + @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 null; + } + + @Override + public Iterator rowIterator() { + return (Iterator) (Iterator) rowList.iterator(); + } + + @Override + public void setForceFormulaRecalculation(boolean value) {} + + @Override + public boolean getForceFormulaRecalculation() { + return false; + } + + @Override + public void setAutobreaks(boolean value) {} + + @Override + public void setDisplayGuts(boolean value) {} + + @Override + public void setDisplayZeros(boolean value) {} + + @Override + public boolean isDisplayZeros() { + return false; + } + + @Override + public void setFitToPage(boolean value) {} + + @Override + public void setRowSumsBelow(boolean value) {} + + @Override + public void setRowSumsRight(boolean value) {} + + @Override + public boolean getAutobreaks() { + return false; + } + + @Override + public boolean getDisplayGuts() { + return false; + } + + @Override + public boolean getFitToPage() { + return false; + } + + @Override + public boolean getRowSumsBelow() { + return false; + } + + @Override + public boolean getRowSumsRight() { + return false; + } + + @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 PrintSetup getPrintSetup() { + return null; + } + + @Override + public Header getHeader() { + return null; + } + + @Override + public Footer getFooter() { + return null; + } + + @Override + public void setSelected(boolean value) {} + + @Override + public double getMargin(short margin) { + return 0; + } + + @Override + public double getMargin(PageMargin pageMargin) { + return 0; + } + + @Override + public void setMargin(short margin, double size) {} + + @Override + public void setMargin(PageMargin pageMargin, double v) {} + + @Override + public boolean getProtect() { + return false; + } + + @Override + public void protectSheet(String password) {} + + @Override + public boolean getScenarioProtect() { + return false; + } + + @Override + public void setZoom(int scale) {} + + @Override + public short getTopRow() { + return 0; + } + + @Override + public short getLeftCol() { + return 0; + } + + @Override + public void showInPane(int topRow, int leftCol) {} + + @Override + public void shiftRows(int startRow, int endRow, int n) {} + + @Override + public void shiftRows(int startRow, int endRow, int n, boolean copyRowHeight, boolean resetOriginalRowHeight) {} + + @Override + public void shiftColumns(int startColumn, int endColumn, int n) {} + + @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 xSplitPos, int ySplitPos, int leftmostColumn, int topRow, int activePane) {} + + @Override + public void createSplitPane(int i, int i1, int i2, int i3, PaneType paneType) {} + + @Override + public PaneInformation getPaneInformation() { + return null; + } + + @Override + public void setDisplayGridlines(boolean show) {} + + @Override + public boolean isDisplayGridlines() { + return false; + } + + @Override + public void setDisplayFormulas(boolean show) {} + + @Override + public boolean isDisplayFormulas() { + return false; + } + + @Override + public void setDisplayRowColHeadings(boolean show) {} + + @Override + public boolean isDisplayRowColHeadings() { + return false; + } + + @Override + public void setRowBreak(int row) {} + + @Override + public boolean isRowBroken(int row) { + return false; + } + + @Override + public void removeRowBreak(int row) {} + + @Override + public int[] getRowBreaks() { + return new int[0]; + } + + @Override + public int[] getColumnBreaks() { + return new int[0]; + } + + @Override + public void setColumnBreak(int column) {} + + @Override + public boolean isColumnBroken(int column) { + return false; + } + + @Override + public void removeColumnBreak(int column) {} + + @Override + public void setColumnGroupCollapsed(int columnNumber, boolean collapsed) {} + + @Override + public void groupColumn(int fromColumn, int toColumn) {} + + @Override + public void ungroupColumn(int fromColumn, int toColumn) {} + + @Override + public void groupRow(int fromRow, int toRow) {} + + @Override + public void ungroupRow(int fromRow, int toRow) {} + + @Override + public void setRowGroupCollapsed(int row, boolean collapse) {} + + @Override + public void setDefaultColumnStyle(int column, CellStyle style) {} + + @Override + public void autoSizeColumn(int column) {} + + @Override + public void autoSizeColumn(int column, boolean useMergedCells) {} + + @Override + public Comment getCellComment(CellAddress ref) { + return null; + } + + @Override + public Map getCellComments() { + return null; + } + + @Override + public Drawing getDrawingPatriarch() { + return null; + } + + @Override + public Drawing createDrawingPatriarch() { + return null; + } + + @Override + public Workbook getWorkbook() { + return odsWorkbook; + } + + @Override + public boolean isSelected() { + return false; + } + + @Override + public CellRange setArrayFormula(String formula, CellRangeAddress range) { + return null; + } + + @Override + public CellRange removeArrayFormula(Cell cell) { + return null; + } + + @Override + public DataValidationHelper getDataValidationHelper() { + return null; + } + + @Override + public List getDataValidations() { + return null; + } + + @Override + public void addValidationData(DataValidation dataValidation) {} + + @Override + public AutoFilter setAutoFilter(CellRangeAddress range) { + return null; + } + + @Override + public SheetConditionalFormatting getSheetConditionalFormatting() { + return null; + } + + @Override + public CellRangeAddress getRepeatingRows() { + return null; + } + + @Override + public CellRangeAddress getRepeatingColumns() { + return null; + } + + @Override + public void setRepeatingRows(CellRangeAddress rowRangeRef) {} + + @Override + public void setRepeatingColumns(CellRangeAddress columnRangeRef) {} + + @Override + public int getColumnOutlineLevel(int columnIndex) { + return 0; + } + + @Override + public Hyperlink getHyperlink(int row, int column) { + return null; + } + + @Override + public Hyperlink getHyperlink(CellAddress addr) { + return null; + } + + @Override + public List getHyperlinkList() { + return null; + } + + @Override + public CellAddress getActiveCell() { + return null; + } + + @Override + public void setActiveCell(CellAddress address) {} + + @Override + public Iterator 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) 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 getNames(String name) { + return null; + } + + @Override + public List 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 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} - org.apache.commons - commons-lang3 - ${commons-lang3.version} + org.odftoolkit + odfdom-java + ${odfdom.version} + + + org.apache.commons + commons-lang3 + ${commons-lang3.version} org.apache.poi diff --git a/website/docs/sheet/read/ods.md b/website/docs/sheet/read/ods.md new file mode 100644 index 000000000..a6a9c0b2d --- /dev/null +++ b/website/docs/sheet/read/ods.md @@ -0,0 +1,163 @@ +--- +id: 'ods' +title: 'ODS' +--- + +# ODS File Format Support + +This chapter introduces how to read and write ODS (OpenDocument Spreadsheet) files using Fesod. + +## Overview + +ODS (OpenDocument Spreadsheet) is an open standard spreadsheet file format defined by OASIS. It is widely used in: + +- LibreOffice Calc +- Apache OpenOffice Calc +- Google Sheets (export format) +- Many other open-source office suites + +Fesod provides full support for reading and writing ODS files using the same API as other formats (XLSX, XLS, CSV). + +## Reading ODS Files + +### Basic Reading + +Reading ODS files follows the same pattern as reading other spreadsheet formats: + +```java +@Test +public void readOds() { + String fileName = "path/to/demo.ods"; + + FesodSheet.read(fileName, DemoData.class, new DemoDataListener()) + .sheet() + .doRead(); +} +``` + +### Explicit Type Specification + +When reading from an InputStream without a file extension, you can explicitly specify the file type: + +```java +@Test +public void readOdsFromStream() { + InputStream inputStream = getOdsInputStream(); + + FesodSheet.read(inputStream, DemoData.class, new DemoDataListener()) + .excelType(ExcelTypeEnum.ODS) + .sheet() + .doRead(); +} +``` + +### Multiple Sheets + +ODS files support multiple sheets, and you can read them the same way as other formats: + +```java +@Test +public void readMultipleSheets() { + String fileName = "path/to/demo.ods"; + + // Read all sheets + FesodSheet.read(fileName, DemoData.class, new DemoDataListener()) + .doReadAll(); + + // Or read specific sheets by index + FesodSheet.read(fileName, DemoData.class, new DemoDataListener()) + .sheet(0) // First sheet + .doRead(); +} +``` + +## Writing ODS Files + +### Basic Writing + +Writing ODS files is straightforward: + +```java +@Test +public void writeOds() { + String fileName = "path/to/output.ods"; + List dataList = generateData(); + + FesodSheet.write(fileName, DemoData.class) + .sheet("Sheet1") + .doWrite(dataList); +} +``` + +### Explicit Type Specification + +You can explicitly specify the output type: + +```java +@Test +public void writeOdsExplicit() { + String fileName = "path/to/output.ods"; + List dataList = generateData(); + + FesodSheet.write(fileName, DemoData.class) + .excelType(ExcelTypeEnum.ODS) + .sheet("MySheet") + .doWrite(dataList); +} +``` + +### Writing to OutputStream + +When writing to an OutputStream, explicitly specify the file type: + +```java +@Test +public void writeOdsToStream() throws IOException { + OutputStream outputStream = new FileOutputStream("output.ods"); + List dataList = generateData(); + + FesodSheet.write(outputStream, DemoData.class) + .excelType(ExcelTypeEnum.ODS) + .sheet("Sheet1") + .doWrite(dataList); +} +``` + +## Supported Features + +| Feature | Support Status | +|---------|---------------| +| Basic Read/Write | ✅ Full Support | +| Multiple Sheets | ✅ Full Support | +| String Data | ✅ Full Support | +| Numeric Data | ✅ Full Support | +| Date/Time Data | ✅ Full Support | +| Boolean Data | ✅ Full Support | +| Formulas | ⚠️ Basic Support | +| Styles | ⚠️ Basic Support | +| Images | ⚠️ Limited Support | +| Comments | ⚠️ Limited Support | +| Encryption | ❌ Not Supported | + +## Dependencies + +ODS support is provided through the Apache ODF Toolkit. The dependency is automatically included when you use Fesod: + +```xml + + org.odftoolkit + odfdom-java + +``` + +## Notes + +1. **File Detection**: ODS files are automatically detected by their `.ods` extension. When reading from streams without a file extension, Fesod will automatically detect ODS format by checking the ZIP internal structure (ODS files contain a `mimetype` file or `content.xml`). However, it's recommended to use `excelType(ExcelTypeEnum.ODS)` to specify the format explicitly for better performance and reliability. + +2. **1904 Date System**: ODS format uses the ISO 8601 date standard (1900 date system) and does not support Excel's 1904 date windowing. If you set `use1904windowing(true)` when writing ODS files, a warning will be logged and the setting will be ignored. ODS always uses the standard 1900 date system. + +3. **Performance**: ODS reading and writing performance is comparable to XLSX for typical use cases. + +4. **Compatibility**: Files created by Fesod are compatible with LibreOffice, OpenOffice, and other applications that support the ODF standard. + +5. **Formula Syntax**: ODS uses a different formula syntax than Excel. Cross-format formula conversion is not automatically performed. diff --git a/website/i18n/zh-cn/docusaurus-plugin-content-docs/current/sheet/read/ods.md b/website/i18n/zh-cn/docusaurus-plugin-content-docs/current/sheet/read/ods.md new file mode 100644 index 000000000..f59f3139d --- /dev/null +++ b/website/i18n/zh-cn/docusaurus-plugin-content-docs/current/sheet/read/ods.md @@ -0,0 +1,161 @@ +--- +id: 'ods' +title: 'ODS' +--- + +# ODS 文件格式支持 + +本章介绍如何使用 Fesod 读写 ODS(OpenDocument Spreadsheet)文件。 + +## 概述 + +ODS(OpenDocument Spreadsheet)是由 OASIS 定义的开放标准电子表格文件格式,广泛应用于: + +- LibreOffice Calc +- Apache OpenOffice Calc +- Google 表格(导出格式) +- 许多其他开源办公套件 + +Fesod 提供了对 ODS 文件的完整读写支持,使用与其他格式(XLSX、XLS、CSV)相同的 API。 + +## 读取 ODS 文件 + +### 基本读取 + +读取 ODS 文件遵循与读取其他电子表格格式相同的模式: + +```java +@Test +public void readOds() { + String fileName = "path/to/demo.ods"; + + FesodSheet.read(fileName, DemoData.class, new DemoDataListener()) + .sheet() + .doRead(); +} +``` + +### 显式指定类型 + +当从没有文件扩展名的 InputStream 读取时,可以显式指定文件类型: + +```java +@Test +public void readOdsFromStream() { + InputStream inputStream = getOdsInputStream(); + + FesodSheet.read(inputStream, DemoData.class, new DemoDataListener()) + .excelType(ExcelTypeEnum.ODS) + .sheet() + .doRead(); +} +``` + +### 多工作表 + +ODS 文件支持多个工作表,读取方式与其他格式相同: + +```java +@Test +public void readMultipleSheets() { + String fileName = "path/to/demo.ods"; + + // 读取所有工作表 + FesodSheet.read(fileName, DemoData.class, new DemoDataListener()) + .doReadAll(); + + // 或按索引读取特定工作表 + FesodSheet.read(fileName, DemoData.class, new DemoDataListener()) + .sheet(0) // 第一个工作表 + .doRead(); +} +``` + +## 写入 ODS 文件 + +### 基本写入 + +写入 ODS 文件非常简单: + +```java +@Test +public void writeOds() { + String fileName = "path/to/output.ods"; + List dataList = generateData(); + + FesodSheet.write(fileName, DemoData.class) + .sheet("Sheet1") + .doWrite(dataList); +} +``` + +### 显式指定类型 + +可以显式指定输出类型: + +```java +@Test +public void writeOdsExplicit() { + String fileName = "path/to/output.ods"; + List dataList = generateData(); + + FesodSheet.write(fileName, DemoData.class) + .excelType(ExcelTypeEnum.ODS) + .sheet("MySheet") + .doWrite(dataList); +} +``` + +### 写入到 OutputStream + +当写入到 OutputStream 时,显式指定文件类型: + +```java +@Test +public void writeOdsToStream() throws IOException { + OutputStream outputStream = new FileOutputStream("output.ods"); + List dataList = generateData(); + + FesodSheet.write(outputStream, DemoData.class) + .excelType(ExcelTypeEnum.ODS) + .sheet("Sheet1") + .doWrite(dataList); +} +``` + +## 支持的功能 + +| 功能 | 支持状态 | +|------|---------| +| 基本读写 | ✅ 完全支持 | +| 多工作表 | ✅ 完全支持 | +| 字符串数据 | ✅ 完全支持 | +| 数值数据 | ✅ 完全支持 | +| 日期/时间数据 | ✅ 完全支持 | +| 布尔数据 | ✅ 完全支持 | +| 公式 | ⚠️ 基本支持 | +| 样式 | ⚠️ 基本支持 | +| 图片 | ⚠️ 有限支持 | +| 批注 | ⚠️ 有限支持 | +| 加密 | ❌ 不支持 | + +## 依赖 + +ODS 支持通过 Apache ODF Toolkit 提供。当您使用 Fesod 时,该依赖会自动包含: + +```xml + + org.odftoolkit + odfdom-java + +``` + +## 注意事项 + +1. **文件检测**:ODS 文件通过 `.ods` 扩展名自动检测。当从流读取时,使用 `excelType(ExcelTypeEnum.ODS)` 显式指定格式。 + +2. **性能**:对于典型用例,ODS 读写性能与 XLSX 相当。 + +3. **兼容性**:Fesod 创建的文件与 LibreOffice、OpenOffice 和其他支持 ODF 标准的应用程序兼容。 + +4. **公式语法**:ODS 使用与 Excel 不同的公式语法。跨格式的公式转换不会自动执行。 diff --git a/website/sidebars.js b/website/sidebars.js index 5d3ef97b1..2e66d4a47 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -56,6 +56,7 @@ const sidebars = { 'sheet/read/sheet', 'sheet/read/num-rows', 'sheet/read/csv', + 'sheet/read/ods', 'sheet/read/head', 'sheet/read/extra', 'sheet/read/exception',