Skip to content

Commit 215a3dd

Browse files
committed
feat: add support for ODS file format with read/write capabilities
1 parent dbbc7e6 commit 215a3dd

File tree

22 files changed

+3119
-1
lines changed

22 files changed

+3119
-1
lines changed

fesod/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@
4141
<artifactId>commons-csv</artifactId>
4242
</dependency>
4343

44+
<dependency>
45+
<groupId>org.odftoolkit</groupId>
46+
<artifactId>odfdom-java</artifactId>
47+
</dependency>
48+
4449
<dependency>
4550
<groupId>org.apache.poi</groupId>
4651
<artifactId>poi</artifactId>

fesod/src/main/java/org/apache/fesod/sheet/analysis/ExcelAnalyserImpl.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,14 @@
2424
import lombok.extern.slf4j.Slf4j;
2525
import org.apache.commons.collections4.CollectionUtils;
2626
import org.apache.fesod.sheet.analysis.csv.CsvExcelReadExecutor;
27+
import org.apache.fesod.sheet.analysis.ods.OdsExcelReadExecutor;
2728
import org.apache.fesod.sheet.analysis.v03.XlsSaxAnalyser;
2829
import org.apache.fesod.sheet.analysis.v07.XlsxSaxAnalyser;
2930
import org.apache.fesod.sheet.context.AnalysisContext;
3031
import org.apache.fesod.sheet.context.csv.CsvReadContext;
3132
import org.apache.fesod.sheet.context.csv.DefaultCsvReadContext;
33+
import org.apache.fesod.sheet.context.ods.DefaultOdsReadContext;
34+
import org.apache.fesod.sheet.context.ods.OdsReadContext;
3235
import org.apache.fesod.sheet.context.xls.DefaultXlsReadContext;
3336
import org.apache.fesod.sheet.context.xls.XlsReadContext;
3437
import org.apache.fesod.sheet.context.xlsx.DefaultXlsxReadContext;
@@ -39,6 +42,7 @@
3942
import org.apache.fesod.sheet.read.metadata.ReadWorkbook;
4043
import org.apache.fesod.sheet.read.metadata.holder.ReadWorkbookHolder;
4144
import org.apache.fesod.sheet.read.metadata.holder.csv.CsvReadWorkbookHolder;
45+
import org.apache.fesod.sheet.read.metadata.holder.ods.OdsReadWorkbookHolder;
4246
import org.apache.fesod.sheet.read.metadata.holder.xls.XlsReadWorkbookHolder;
4347
import org.apache.fesod.sheet.read.metadata.holder.xlsx.XlsxReadWorkbookHolder;
4448
import org.apache.fesod.sheet.support.ExcelTypeEnum;
@@ -165,6 +169,12 @@ private void chooseExcelExecutor(ReadWorkbook readWorkbook) throws Exception {
165169
analysisContext = csvReadContext;
166170
excelReadExecutor = new CsvExcelReadExecutor(csvReadContext);
167171
break;
172+
case ODS:
173+
// Create a context and executor for processing ODS files
174+
OdsReadContext odsReadContext = new DefaultOdsReadContext(readWorkbook, ExcelTypeEnum.ODS);
175+
analysisContext = odsReadContext;
176+
excelReadExecutor = new OdsExcelReadExecutor(odsReadContext);
177+
break;
168178
default:
169179
// Reserved branch for handling potential future Excel types
170180
break;
@@ -260,6 +270,16 @@ public void finish() {
260270
throwable = t;
261271
}
262272

273+
// close ods.
274+
try {
275+
if ((readWorkbookHolder instanceof OdsReadWorkbookHolder)
276+
&& ((OdsReadWorkbookHolder) readWorkbookHolder).getOdfSpreadsheetDocument() != null) {
277+
((OdsReadWorkbookHolder) readWorkbookHolder).getOdfSpreadsheetDocument().close();
278+
}
279+
} catch (Throwable t) {
280+
throwable = t;
281+
}
282+
263283
try {
264284
if (analysisContext.readWorkbookHolder().getAutoCloseStream()
265285
&& readWorkbookHolder.getInputStream() != null) {
Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.apache.fesod.sheet.analysis.ods;
21+
22+
import java.util.ArrayList;
23+
import java.util.LinkedHashMap;
24+
import java.util.List;
25+
import java.util.Map;
26+
import lombok.extern.slf4j.Slf4j;
27+
import org.apache.commons.collections4.MapUtils;
28+
import org.apache.fesod.sheet.analysis.ExcelReadExecutor;
29+
import org.apache.fesod.sheet.context.ods.OdsReadContext;
30+
import org.apache.fesod.sheet.enums.CellDataTypeEnum;
31+
import org.apache.fesod.sheet.enums.RowTypeEnum;
32+
import org.apache.fesod.sheet.exception.ExcelAnalysisException;
33+
import org.apache.fesod.sheet.exception.ExcelAnalysisStopSheetException;
34+
import org.apache.fesod.sheet.metadata.Cell;
35+
import org.apache.fesod.sheet.metadata.data.ReadCellData;
36+
import org.apache.fesod.sheet.read.metadata.ReadSheet;
37+
import org.apache.fesod.sheet.read.metadata.holder.ReadRowHolder;
38+
import org.apache.fesod.sheet.read.metadata.holder.ods.OdsReadWorkbookHolder;
39+
import org.apache.fesod.sheet.util.SheetUtils;
40+
import org.apache.fesod.sheet.util.StringUtils;
41+
import org.odftoolkit.odfdom.doc.OdfSpreadsheetDocument;
42+
import org.odftoolkit.odfdom.doc.table.OdfTable;
43+
import org.odftoolkit.odfdom.doc.table.OdfTableCell;
44+
import org.odftoolkit.odfdom.doc.table.OdfTableRow;
45+
46+
/**
47+
* ODS Excel Read Executor, responsible for reading and processing ODS (OpenDocument Spreadsheet) files.
48+
*/
49+
@Slf4j
50+
public class OdsExcelReadExecutor implements ExcelReadExecutor {
51+
52+
// List of sheets to be read
53+
private final List<ReadSheet> sheetList;
54+
// Context for ODS reading operation
55+
private final OdsReadContext odsReadContext;
56+
// ODF Spreadsheet Document
57+
private OdfSpreadsheetDocument odfDocument;
58+
59+
public OdsExcelReadExecutor(OdsReadContext odsReadContext) {
60+
this.odsReadContext = odsReadContext;
61+
this.sheetList = new ArrayList<>();
62+
initSheetList();
63+
}
64+
65+
/**
66+
* Initialize the sheet list from the ODS document.
67+
*/
68+
private void initSheetList() {
69+
try {
70+
OdsReadWorkbookHolder workbookHolder = odsReadContext.odsReadWorkbookHolder();
71+
if (workbookHolder.getFile() != null) {
72+
odfDocument = OdfSpreadsheetDocument.loadDocument(workbookHolder.getFile());
73+
} else if (workbookHolder.getInputStream() != null) {
74+
odfDocument = OdfSpreadsheetDocument.loadDocument(workbookHolder.getInputStream());
75+
} else {
76+
throw new ExcelAnalysisException("File and inputStream must be a non-null.");
77+
}
78+
workbookHolder.setOdfSpreadsheetDocument(odfDocument);
79+
80+
List<OdfTable> tables = odfDocument.getTableList();
81+
for (int i = 0; i < tables.size(); i++) {
82+
OdfTable table = tables.get(i);
83+
ReadSheet readSheet = new ReadSheet();
84+
readSheet.setSheetNo(i);
85+
readSheet.setSheetName(table.getTableName());
86+
sheetList.add(readSheet);
87+
}
88+
} catch (Exception e) {
89+
throw new ExcelAnalysisException("Failed to load ODS document", e);
90+
}
91+
}
92+
93+
@Override
94+
public List<ReadSheet> sheetList() {
95+
return sheetList;
96+
}
97+
98+
/**
99+
* Execute the reading process for all sheets.
100+
*/
101+
@Override
102+
public void execute() {
103+
List<OdfTable> tables = odfDocument.getTableList();
104+
105+
for (ReadSheet readSheet : sheetList) {
106+
readSheet = SheetUtils.match(readSheet, odsReadContext);
107+
if (readSheet == null) {
108+
continue;
109+
}
110+
111+
try {
112+
odsReadContext.currentSheet(readSheet);
113+
114+
OdfTable table = tables.get(readSheet.getSheetNo());
115+
int rowCount = table.getRowCount();
116+
117+
for (int rowIndex = 0; rowIndex < rowCount; rowIndex++) {
118+
OdfTableRow row = table.getRowByIndex(rowIndex);
119+
if (row == null) {
120+
continue;
121+
}
122+
dealRow(table, row, rowIndex);
123+
}
124+
} catch (ExcelAnalysisStopSheetException e) {
125+
if (log.isDebugEnabled()) {
126+
log.debug("Custom stop!", e);
127+
}
128+
}
129+
130+
odsReadContext.analysisEventProcessor().endSheet(odsReadContext);
131+
}
132+
}
133+
134+
/**
135+
* Process a single row from the ODS table.
136+
*
137+
* @param table The ODF table
138+
* @param row The ODF table row
139+
* @param rowIndex The index of the current row
140+
*/
141+
private void dealRow(OdfTable table, OdfTableRow row, int rowIndex) {
142+
Map<Integer, Cell> cellMap = new LinkedHashMap<>();
143+
int cellCount = row.getCellCount();
144+
Boolean autoTrim = odsReadContext.odsReadWorkbookHolder().globalConfiguration().getAutoTrim();
145+
Boolean autoStrip = odsReadContext.odsReadWorkbookHolder().globalConfiguration().getAutoStrip();
146+
147+
for (int columnIndex = 0; columnIndex < cellCount; columnIndex++) {
148+
OdfTableCell odfCell = row.getCellByIndex(columnIndex);
149+
if (odfCell == null) {
150+
continue;
151+
}
152+
153+
ReadCellData<String> readCellData = new ReadCellData<>();
154+
readCellData.setRowIndex(rowIndex);
155+
readCellData.setColumnIndex(columnIndex);
156+
157+
String cellValue = getCellValue(odfCell);
158+
159+
if (StringUtils.isNotBlank(cellValue)) {
160+
readCellData.setType(determineCellType(odfCell));
161+
if (autoStrip) {
162+
readCellData.setStringValue(StringUtils.strip(cellValue));
163+
} else if (autoTrim) {
164+
readCellData.setStringValue(cellValue.trim());
165+
} else {
166+
readCellData.setStringValue(cellValue);
167+
}
168+
169+
// Handle numeric values
170+
if (readCellData.getType() == CellDataTypeEnum.NUMBER) {
171+
try {
172+
Double numericValue = odfCell.getDoubleValue();
173+
if (numericValue != null) {
174+
readCellData.setNumberValue(new java.math.BigDecimal(numericValue.toString()));
175+
}
176+
} catch (Exception e) {
177+
// Keep as string if parsing fails
178+
readCellData.setType(CellDataTypeEnum.STRING);
179+
}
180+
}
181+
182+
// Handle boolean values
183+
if (readCellData.getType() == CellDataTypeEnum.BOOLEAN) {
184+
try {
185+
Boolean boolValue = odfCell.getBooleanValue();
186+
if (boolValue != null) {
187+
readCellData.setBooleanValue(boolValue);
188+
}
189+
} catch (Exception e) {
190+
readCellData.setType(CellDataTypeEnum.STRING);
191+
}
192+
}
193+
} else {
194+
readCellData.setType(CellDataTypeEnum.EMPTY);
195+
}
196+
197+
cellMap.put(columnIndex, readCellData);
198+
}
199+
200+
RowTypeEnum rowType = MapUtils.isEmpty(cellMap) ? RowTypeEnum.EMPTY : RowTypeEnum.DATA;
201+
ReadRowHolder readRowHolder = new ReadRowHolder(
202+
rowIndex, rowType, odsReadContext.readWorkbookHolder().getGlobalConfiguration(), cellMap);
203+
odsReadContext.readRowHolder(readRowHolder);
204+
205+
odsReadContext.odsReadSheetHolder().setCellMap(cellMap);
206+
odsReadContext.odsReadSheetHolder().setRowIndex(rowIndex);
207+
odsReadContext.analysisEventProcessor().endRow(odsReadContext);
208+
}
209+
210+
/**
211+
* Get the string value from an ODF cell.
212+
*
213+
* @param cell The ODF table cell
214+
* @return The cell value as a string
215+
*/
216+
private String getCellValue(OdfTableCell cell) {
217+
if (cell == null) {
218+
return null;
219+
}
220+
221+
String valueType = cell.getValueType();
222+
if (valueType == null) {
223+
return cell.getDisplayText();
224+
}
225+
226+
switch (valueType) {
227+
case "float":
228+
case "currency":
229+
case "percentage":
230+
Double doubleValue = cell.getDoubleValue();
231+
if (doubleValue != null) {
232+
// Remove trailing zeros for display
233+
if (doubleValue == Math.floor(doubleValue) && !Double.isInfinite(doubleValue)) {
234+
return String.valueOf(doubleValue.longValue());
235+
}
236+
return doubleValue.toString();
237+
}
238+
return null;
239+
case "date":
240+
case "time":
241+
return cell.getDisplayText();
242+
case "boolean":
243+
Boolean boolValue = cell.getBooleanValue();
244+
return boolValue != null ? boolValue.toString() : null;
245+
case "string":
246+
default:
247+
return cell.getStringValue();
248+
}
249+
}
250+
251+
/**
252+
* Determine the cell data type based on ODF cell type.
253+
*
254+
* @param cell The ODF table cell
255+
* @return The corresponding CellDataTypeEnum
256+
*/
257+
private CellDataTypeEnum determineCellType(OdfTableCell cell) {
258+
if (cell == null) {
259+
return CellDataTypeEnum.EMPTY;
260+
}
261+
262+
String valueType = cell.getValueType();
263+
if (valueType == null) {
264+
return CellDataTypeEnum.STRING;
265+
}
266+
267+
switch (valueType) {
268+
case "float":
269+
case "currency":
270+
case "percentage":
271+
return CellDataTypeEnum.NUMBER;
272+
case "date":
273+
case "time":
274+
return CellDataTypeEnum.STRING; // Dates are returned as formatted strings
275+
case "boolean":
276+
return CellDataTypeEnum.BOOLEAN;
277+
case "string":
278+
default:
279+
return CellDataTypeEnum.STRING;
280+
}
281+
}
282+
}
283+

fesod/src/main/java/org/apache/fesod/sheet/context/AnalysisContextImpl.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
import org.apache.fesod.sheet.read.metadata.holder.ReadWorkbookHolder;
3232
import org.apache.fesod.sheet.read.metadata.holder.csv.CsvReadSheetHolder;
3333
import org.apache.fesod.sheet.read.metadata.holder.csv.CsvReadWorkbookHolder;
34+
import org.apache.fesod.sheet.read.metadata.holder.ods.OdsReadSheetHolder;
35+
import org.apache.fesod.sheet.read.metadata.holder.ods.OdsReadWorkbookHolder;
3436
import org.apache.fesod.sheet.read.metadata.holder.xls.XlsReadSheetHolder;
3537
import org.apache.fesod.sheet.read.metadata.holder.xls.XlsReadWorkbookHolder;
3638
import org.apache.fesod.sheet.read.metadata.holder.xlsx.XlsxReadSheetHolder;
@@ -79,6 +81,9 @@ public AnalysisContextImpl(ReadWorkbook readWorkbook, ExcelTypeEnum actualExcelT
7981
case CSV:
8082
readWorkbookHolder = new CsvReadWorkbookHolder(readWorkbook);
8183
break;
84+
case ODS:
85+
readWorkbookHolder = new OdsReadWorkbookHolder(readWorkbook);
86+
break;
8287
default:
8388
break;
8489
}
@@ -101,6 +106,9 @@ public void currentSheet(ReadSheet readSheet) {
101106
case CSV:
102107
readSheetHolder = new CsvReadSheetHolder(readSheet, readWorkbookHolder);
103108
break;
109+
case ODS:
110+
readSheetHolder = new OdsReadSheetHolder(readSheet, readWorkbookHolder);
111+
break;
104112
default:
105113
break;
106114
}

0 commit comments

Comments
 (0)