001/*
002 *  Copyright (c) 2023-2026, Agents-Flex (fuhai999@gmail.com).
003 *  <p>
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *  <p>
008 *  http://www.apache.org/licenses/LICENSE-2.0
009 *  <p>
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package com.agentsflex.core.file2text.extractor.impl;
017
018import com.agentsflex.core.file2text.extractor.FileExtractor;
019import com.agentsflex.core.file2text.source.DocumentSource;
020import org.apache.poi.hssf.usermodel.HSSFWorkbook;
021import org.apache.poi.ss.usermodel.*;
022import org.apache.poi.xssf.usermodel.XSSFWorkbook;
023
024import java.io.IOException;
025import java.io.InputStream;
026import java.util.Collections;
027import java.util.HashSet;
028import java.util.Set;
029
030/**
031 * Excel 文档提取器(.xlsx, .xlsm, .xls, .xltx)
032 * 输出格式:Markdown 表格,支持多 Sheet、公式计算、特殊字符转义
033 */
034public class ExcelExtractor implements FileExtractor {
035
036    private static final Set<String> KNOWN_MIME_TYPES;
037    private static final String MIME_PREFIX = "application/vnd.openxmlformats-officedocument.spreadsheetml";
038    private static final Set<String> SUPPORTED_EXTENSIONS;
039
040    static {
041        Set<String> mimeTypes = new HashSet<>();
042        mimeTypes.add("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
043        mimeTypes.add("application/vnd.openxmlformats-officedocument.spreadsheetml.template");
044        mimeTypes.add("application/vnd.ms-excel");
045        mimeTypes.add("application/vnd.ms-excel.sheet.macroEnabled.12");
046        KNOWN_MIME_TYPES = Collections.unmodifiableSet(mimeTypes);
047
048        Set<String> extensions = new HashSet<>();
049        extensions.add("xlsx");
050        extensions.add("xlsm");
051        extensions.add("xls");
052        extensions.add("xltx");
053        SUPPORTED_EXTENSIONS = Collections.unmodifiableSet(extensions);
054    }
055
056    @Override
057    public boolean supports(DocumentSource source) {
058        String mimeType = source.getMimeType();
059        String fileName = source.getFileName();
060
061        if (mimeType != null && KNOWN_MIME_TYPES.contains(mimeType)) {
062            return true;
063        }
064        if (mimeType != null && mimeType.startsWith(MIME_PREFIX)) {
065            return true;
066        }
067        if (fileName != null) {
068            String ext = getExtension(fileName);
069            if (ext != null && SUPPORTED_EXTENSIONS.contains(ext.toLowerCase())) {
070                return true;
071            }
072        }
073        return false;
074    }
075
076    @Override
077    public String extractText(DocumentSource source) throws IOException {
078        StringBuilder text = new StringBuilder();
079
080        try (InputStream is = source.openStream();
081             Workbook workbook = openWorkbook(is, source.getFileName())) {
082
083            FormulaEvaluator evaluator = workbook.getCreationHelper().createFormulaEvaluator();
084            int sheetCount = workbook.getNumberOfSheets();
085
086            for (int i = 0; i < sheetCount; i++) {
087                Sheet sheet = workbook.getSheetAt(i);
088                String sheetName = sheet.getSheetName();
089
090                // Sheet 标题(Markdown 三级标题)
091                text.append("\n### ").append(escapeMarkdown(sheetName)).append("\n\n");
092
093                // 收集所有非空行数据,用于计算最大列数
094                java.util.List<java.util.List<String>> rowsData = new java.util.ArrayList<>();
095                int maxColumns = 0;
096
097                for (Row row : sheet) {
098                    if (isRowEmpty(row)) {
099                        continue;
100                    }
101                    java.util.List<String> rowData = new java.util.ArrayList<>();
102                    short firstCellNum = row.getFirstCellNum();
103                    short lastCellNum = row.getLastCellNum();
104
105                    for (short j = firstCellNum; j < lastCellNum; j++) {
106                        Cell cell = row.getCell(j);
107                        String cellValue = getCellValue(cell, evaluator);
108                        rowData.add(cellValue != null ? cellValue : "");
109                    }
110                    if (!rowData.isEmpty()) {
111                        rowsData.add(rowData);
112                        maxColumns = Math.max(maxColumns, rowData.size());
113                    }
114                }
115
116                if (rowsData.isEmpty()) {
117                    text.append("*Empty Sheet*\n");
118                    continue;
119                }
120
121                // 输出 Markdown 表格
122                outputMarkdownTable(text, rowsData, maxColumns);
123            }
124
125        } catch (Exception e) {
126            throw new IOException("Failed to extract Excel: " + e.getMessage(), e);
127        }
128
129        return text.toString().trim();
130    }
131
132    /**
133     * 输出 Markdown 格式表格
134     */
135    private void outputMarkdownTable(StringBuilder output,
136                                     java.util.List<java.util.List<String>> rowsData,
137                                     int maxColumns) {
138        // 输出表头行(第一行作为表头)
139        java.util.List<String> headerRow = rowsData.get(0);
140        output.append("| ");
141        for (int i = 0; i < maxColumns; i++) {
142            String cell = i < headerRow.size() ? escapeMarkdownCell(headerRow.get(i)) : "";
143            output.append(cell).append(" | ");
144        }
145        output.append("\n| ");
146
147        // 输出分隔行(默认左对齐 :---)
148        for (int i = 0; i < maxColumns; i++) {
149            output.append(":--- | ");
150        }
151        output.append("\n");
152
153        // 输出数据行
154        for (int r = 1; r < rowsData.size(); r++) {
155            java.util.List<String> rowData = rowsData.get(r);
156            output.append("| ");
157            for (int i = 0; i < maxColumns; i++) {
158                String cell = i < rowData.size() ? escapeMarkdownCell(rowData.get(i)) : "";
159                output.append(cell).append(" | ");
160            }
161            output.append("\n");
162        }
163        output.append("\n");
164    }
165
166    /**
167     * 转义单元格内容中的 Markdown 特殊字符
168     * 主要处理:| \ ` * _ [ ] < > 等,避免破坏表格结构
169     */
170    private String escapeMarkdownCell(String content) {
171        if (content == null || content.isEmpty()) {
172            return "";
173        }
174        // 先转义反斜杠,避免重复转义
175        String escaped = content.replace("\\", "\\\\");
176        // 转义管道符(表格分隔符)
177        escaped = escaped.replace("|", "\\|");
178        // 转义其他可能的 Markdown 语法字符(可选,根据需求开启)
179        // escaped = escaped.replace("*", "\\*").replace("_", "\\_").replace("`", "\\`");
180        // 移除换行符,避免破坏表格行结构
181        escaped = escaped.replace("\n", " ").replace("\r", " ");
182        return escaped.trim();
183    }
184
185    /**
186     * 转义 Sheet 名称中的 Markdown 特殊字符(用于标题)
187     */
188    private String escapeMarkdown(String text) {
189        if (text == null) return "";
190        return text.replace("#", "\\#").replace("*", "\\*");
191    }
192
193    private Workbook openWorkbook(InputStream inputStream, String fileName) throws IOException {
194        String ext = fileName != null ? getExtension(fileName) : null;
195
196        if ("xls".equalsIgnoreCase(ext)) {
197            return new HSSFWorkbook(inputStream);
198        } else if ("xlsx".equalsIgnoreCase(ext) || "xlsm".equalsIgnoreCase(ext) || "xltx".equalsIgnoreCase(ext)) {
199            return new XSSFWorkbook(inputStream);
200        }
201
202        try {
203            return new XSSFWorkbook(inputStream);
204        } catch (Exception e) {
205            throw new IOException("Unable to determine Excel format. Please ensure file extension is correct.", e);
206        }
207    }
208
209    private String getCellValue(Cell cell, FormulaEvaluator evaluator) {
210        if (cell == null) {
211            return "";
212        }
213        switch (cell.getCellType()) {
214            case STRING:
215                return cell.getStringCellValue();
216            case NUMERIC:
217                if (DateUtil.isCellDateFormatted(cell)) {
218                    return cell.getDateCellValue().toString();
219                }
220                double numericValue = cell.getNumericCellValue();
221                if (numericValue == Math.floor(numericValue) && !Double.isInfinite(numericValue)) {
222                    return String.valueOf((long) numericValue);
223                }
224                return String.valueOf(numericValue);
225            case BOOLEAN:
226                return String.valueOf(cell.getBooleanCellValue());
227            case FORMULA:
228                try {
229                    CellValue evaluated = evaluator.evaluate(cell);
230                    return formatEvaluatedValue(evaluated);
231                } catch (Exception e) {
232                    return cell.getCellFormula();
233                }
234            case BLANK:
235            case _NONE:
236            default:
237                return "";
238        }
239    }
240
241    private String formatEvaluatedValue(CellValue evaluated) {
242        if (evaluated == null) {
243            return "";
244        }
245        switch (evaluated.getCellType()) {
246            case STRING:
247                return evaluated.getStringValue();
248            case NUMERIC:
249                double num = evaluated.getNumberValue();
250                if (num == Math.floor(num) && !Double.isInfinite(num)) {
251                    return String.valueOf((long) num);
252                }
253                return String.valueOf(num);
254            case BOOLEAN:
255                return String.valueOf(evaluated.getBooleanValue());
256            case ERROR:
257                return "#ERROR:" + evaluated.getErrorValue();
258            default:
259                return "";
260        }
261    }
262
263    private boolean isRowEmpty(Row row) {
264        if (row == null) return true;
265        for (Cell cell : row) {
266            if (cell != null && cell.getCellType() != CellType.BLANK) {
267                // 空行判断使用简化逻辑,不传 evaluator
268                String value;
269                if (cell.getCellType() == CellType.STRING) {
270                    value = cell.getStringCellValue();
271                } else {
272                    value = String.valueOf(cell);
273                }
274                if (value != null && !value.trim().isEmpty()) {
275                    return false;
276                }
277            }
278        }
279        return true;
280    }
281
282    @Override
283    public int getOrder() {
284        return 10;
285    }
286
287    private String getExtension(String fileName) {
288        if (fileName == null || !fileName.contains(".")) return null;
289        int lastDot = fileName.lastIndexOf('.');
290        return fileName.substring(lastDot + 1);
291    }
292}