diff --git a/hutool-poi/src/main/java/org/dromara/hutool/poi/excel/cell/CellUtil.java b/hutool-poi/src/main/java/org/dromara/hutool/poi/excel/cell/CellUtil.java index aebb6a086..feac78ff6 100644 --- a/hutool-poi/src/main/java/org/dromara/hutool/poi/excel/cell/CellUtil.java +++ b/hutool-poi/src/main/java/org/dromara/hutool/poi/excel/cell/CellUtil.java @@ -39,6 +39,7 @@ import org.dromara.hutool.poi.excel.style.StyleSet; public class CellUtil { // region ----- getCellValue + /** * 获取单元格值 * @@ -103,6 +104,7 @@ public class CellUtil { // endregion // region ----- setCellValue + /** * 设置单元格值
* 根据传入的styleSet自动匹配样式
@@ -169,8 +171,8 @@ public class CellUtil { * 根据传入的styleSet自动匹配样式
* 当为头部样式时默认赋值头部样式,但是头部中如果有数字、日期等类型,将按照数字、日期样式设置 * - * @param cell 单元格 - * @param value 值或{@link CellSetter} + * @param cell 单元格 + * @param value 值或{@link CellSetter} * @since 5.6.4 */ public static void setCellValue(final Cell cell, final Object value) { @@ -191,10 +193,11 @@ public class CellUtil { // endregion // region ----- getCell + /** * 获取指定坐标单元格,如果isCreateIfNotExist为false,则在单元格不存在时返回{@code null} * - * @param sheet {@link Sheet} + * @param sheet {@link Sheet} * @param x X坐标,从0计数,即列号 * @param y Y坐标,从0计数,即行号 * @param isCreateIfNotExist 单元格不存在时是否创建 @@ -249,6 +252,7 @@ public class CellUtil { // endregion // region ----- merging 合并单元格 + /** * 判断指定的单元格是否是合并单元格 * @@ -287,7 +291,7 @@ public class CellUtil { for (int i = 0; i < sheetMergeCount; i++) { ca = sheet.getMergedRegion(i); if (y >= ca.getFirstRow() && y <= ca.getLastRow() - && x >= ca.getFirstColumn() && x <= ca.getLastColumn()) { + && x >= ca.getFirstColumn() && x <= ca.getLastColumn()) { return true; } } @@ -297,7 +301,7 @@ public class CellUtil { /** * 合并单元格,可以根据设置的值来合并行和列 * - * @param sheet 表对象 + * @param sheet 表对象 * @param cellRangeAddress 合并单元格范围,定义了起始行列和结束行列 * @return 合并后的单元格号 */ @@ -308,9 +312,9 @@ public class CellUtil { /** * 合并单元格,可以根据设置的值来合并行和列 * - * @param sheet 表对象 + * @param sheet 表对象 * @param cellRangeAddress 合并单元格范围,定义了起始行列和结束行列 - * @param cellStyle 单元格样式,只提取边框样式,null表示无样式 + * @param cellStyle 单元格样式,只提取边框样式,null表示无样式 * @return 合并后的单元格号 */ public static int mergingCells(final Sheet sheet, final CellRangeAddress cellRangeAddress, final CellStyle cellStyle) { @@ -360,8 +364,8 @@ public class CellUtil { return null; } return ObjUtil.defaultIfNull( - getCellIfMergedRegion(cell.getSheet(), cell.getColumnIndex(), cell.getRowIndex()), - cell); + getCellIfMergedRegion(cell.getSheet(), cell.getColumnIndex(), cell.getRowIndex()), + cell); } /** @@ -376,8 +380,8 @@ public class CellUtil { */ public static Cell getMergedRegionCell(final Sheet sheet, final int x, final int y) { return ObjUtil.defaultIfNull( - getCellIfMergedRegion(sheet, x, y), - () -> SheetUtil.getCell(sheet, y, x)); + getCellIfMergedRegion(sheet, x, y), + () -> SheetUtil.getCell(sheet, y, x)); } // endregion @@ -420,13 +424,25 @@ public class CellUtil { // 修正在XSSFCell中未设置地址导致错位问题 comment.setAddress(cell.getAddress()); comment.setString(factory.createRichTextString(commentText)); - if(null != commentAuthor){ + if (null != commentAuthor) { comment.setAuthor(commentAuthor); } cell.setCellComment(comment); } + /** + * 移除指定单元格 + * + * @param cell 单元格 + */ + public static void remove(final Cell cell) { + if (null != cell) { + cell.getRow().removeCell(cell); + } + } + // -------------------------------------------------------------------------------------------------------------- Private method start + /** * 获取合并单元格,非合并单元格返回{@code null}
* 传入的x,y坐标(列行数)可以是合并单元格范围内的任意一个单元格 diff --git a/hutool-poi/src/main/java/org/dromara/hutool/poi/excel/cell/VirtualCell.java b/hutool-poi/src/main/java/org/dromara/hutool/poi/excel/cell/VirtualCell.java new file mode 100644 index 000000000..0d5c570c1 --- /dev/null +++ b/hutool-poi/src/main/java/org/dromara/hutool/poi/excel/cell/VirtualCell.java @@ -0,0 +1,269 @@ +/* + * Copyright (c) 2024 Hutool Team and hutool.cn + * + * Licensed 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.dromara.hutool.poi.excel.cell; + +import org.apache.poi.ss.SpreadsheetVersion; +import org.apache.poi.ss.usermodel.*; +import org.apache.poi.ss.util.CellRangeAddress; +import org.dromara.hutool.poi.excel.cell.values.FormulaCellValue; + +import java.time.LocalDateTime; +import java.util.Calendar; +import java.util.Date; + +/** + * 虚拟单元格,表示一个单元格的位置、值和样式,但是并非实际创建的单元格
+ * 注意:虚拟单元格设置值和样式均不会在实际工作簿中生效 + * + * @author Looly + * @since 6.0.0 + */ +public class VirtualCell extends CellBase { + + private final Row row; + private final int columnIndex; + private final int rowIndex; + + private CellType cellType; + private Object value; + private CellStyle style; + + /** + * 构造 + * + * @param cell 参照单元格 + * @param x 新的列号,从0开始 + * @param y 新的行号,从0开始 + */ + public VirtualCell(final Cell cell, final int x, final int y) { + this(cell.getRow(), x, y); + this.cellType = cell.getCellType(); + this.value = CellUtil.getCellValue(cell); + this.style = cell.getCellStyle(); + this.comment = cell.getCellComment(); + } + + private Comment comment; + + /** + * 构造 + * + * @param row 行 + * @param y 行号,从0开始 + * @param x 列号,从0开始 + */ + public VirtualCell(final Row row, final int x, final int y) { + this.row = row; + this.rowIndex = y; + this.columnIndex = x; + } + + @Override + protected void setCellTypeImpl(final CellType cellType) { + this.cellType = cellType; + } + + @Override + protected void setCellFormulaImpl(final String formula) { + this.value = new FormulaCellValue(formula); + } + + @Override + protected void removeFormulaImpl() { + if (this.value instanceof FormulaCellValue) { + this.value = null; + } + } + + @Override + protected void setCellValueImpl(final double value) { + this.value = value; + } + + @Override + protected void setCellValueImpl(final Date value) { + this.value = value; + } + + @Override + protected void setCellValueImpl(final LocalDateTime value) { + this.value = value; + } + + @Override + protected void setCellValueImpl(final Calendar value) { + this.value = value; + } + + @Override + protected void setCellValueImpl(final String value) { + this.value = value; + } + + @Override + protected void setCellValueImpl(final RichTextString value) { + this.value = value; + } + + @Override + protected SpreadsheetVersion getSpreadsheetVersion() { + return SpreadsheetVersion.EXCEL2007; + } + + @Override + public int getColumnIndex() { + return this.columnIndex; + } + + @Override + public int getRowIndex() { + return this.rowIndex; + } + + @Override + public Sheet getSheet() { + return this.row.getSheet(); + } + + @Override + public Row getRow() { + return this.row; + } + + @Override + public CellType getCellType() { + return this.cellType; + } + + @Override + public CellType getCachedFormulaResultType() { + if (this.value instanceof FormulaCellValue) { + return ((FormulaCellValue) this.value).getResultType(); + } + return null; + } + + @Override + public String getCellFormula() { + if (this.value instanceof FormulaCellValue) { + return ((FormulaCellValue) this.value).getValue(); + } + return null; + } + + @Override + public double getNumericCellValue() { + return (double) this.value; + } + + @Override + public Date getDateCellValue() { + return (Date) this.value; + } + + @Override + public LocalDateTime getLocalDateTimeCellValue() { + return (LocalDateTime) this.value; + } + + @Override + public RichTextString getRichStringCellValue() { + return (RichTextString) this.value; + } + + @Override + public String getStringCellValue() { + return (String) this.value; + } + + @Override + public void setCellValue(final boolean value) { + this.value = value; + } + + @Override + public void setCellErrorValue(final byte value) { + this.value = value; + } + + @Override + public boolean getBooleanCellValue() { + return (boolean) this.value; + } + + @Override + public byte getErrorCellValue() { + return (byte) this.value; + } + + @Override + public void setCellStyle(final CellStyle style) { + this.style = style; + } + + @Override + public CellStyle getCellStyle() { + return this.style; + } + + @Override + public void setAsActiveCell() { + throw new UnsupportedOperationException("Virtual cell cannot be set as active cell"); + } + + @Override + public void setCellComment(final Comment comment) { + this.comment = comment; + } + + @Override + public Comment getCellComment() { + return this.comment; + } + + @Override + public void removeCellComment() { + this.comment = null; + } + + @Override + public Hyperlink getHyperlink() { + return (Hyperlink) this.value; + } + + @Override + public void setHyperlink(final Hyperlink link) { + this.value = link; + } + + @Override + public void removeHyperlink() { + if (this.value instanceof Hyperlink) { + this.value = null; + } + } + + @Override + public CellRangeAddress getArrayFormulaRange() { + return null; + } + + @Override + public boolean isPartOfArrayFormulaGroup() { + return false; + } +} diff --git a/hutool-poi/src/main/java/org/dromara/hutool/poi/excel/cell/values/FormulaCellValue.java b/hutool-poi/src/main/java/org/dromara/hutool/poi/excel/cell/values/FormulaCellValue.java index 64d1d6e12..66d1a582f 100644 --- a/hutool-poi/src/main/java/org/dromara/hutool/poi/excel/cell/values/FormulaCellValue.java +++ b/hutool-poi/src/main/java/org/dromara/hutool/poi/excel/cell/values/FormulaCellValue.java @@ -16,6 +16,7 @@ package org.dromara.hutool.poi.excel.cell.values; +import org.apache.poi.ss.usermodel.CellType; import org.dromara.hutool.poi.excel.cell.setters.CellSetter; import org.apache.poi.ss.usermodel.Cell; @@ -40,6 +41,7 @@ public class FormulaCellValue implements CellValue, CellSetter { * 结果,使用ExcelWriter时可以不用 */ private final Object result; + private final CellType resultType; /** * 构造 @@ -57,8 +59,20 @@ public class FormulaCellValue implements CellValue, CellSetter { * @param result 结果 */ public FormulaCellValue(final String formula, final Object result) { + this(formula, result, null); + } + + /** + * 构造 + * + * @param formula 公式 + * @param result 结果 + * @param resultType 结果类型 + */ + public FormulaCellValue(final String formula, final Object result, final CellType resultType) { this.formula = formula; this.result = result; + this.resultType = resultType; } @Override @@ -73,12 +87,22 @@ public class FormulaCellValue implements CellValue, CellSetter { /** * 获取结果 + * * @return 结果 */ public Object getResult() { return this.result; } + /** + * 获取结果类型 + * + * @return 结果类型,{@code null}表示未明确 + */ + public CellType getResultType() { + return this.resultType; + } + @Override public String toString() { return getResult().toString(); diff --git a/hutool-poi/src/main/java/org/dromara/hutool/poi/excel/writer/ExcelWriter.java b/hutool-poi/src/main/java/org/dromara/hutool/poi/excel/writer/ExcelWriter.java index c37c342e2..e86b4d1cc 100644 --- a/hutool-poi/src/main/java/org/dromara/hutool/poi/excel/writer/ExcelWriter.java +++ b/hutool-poi/src/main/java/org/dromara/hutool/poi/excel/writer/ExcelWriter.java @@ -91,7 +91,7 @@ public class ExcelWriter extends ExcelBase { /** * 构造
* 此构造不传入写出的Excel文件路径,只能调用{@link #flush(OutputStream)}方法写出到流
- * 若写出到文件,需要调用{@link #flush(File)} 写出到文件 + * 若写出到文件,需要调用{@link #flush(File, boolean)} 写出到文件 * * @param isXlsx 是否为xlsx格式 * @since 3.2.1 @@ -112,7 +112,7 @@ public class ExcelWriter extends ExcelBase { /** * 构造
* 此构造不传入写出的Excel文件路径,只能调用{@link #flush(OutputStream)}方法写出到流
- * 若写出到文件,需要调用{@link #flush(File)} 写出到文件 + * 若写出到文件,需要调用{@link #flush(File, boolean)} 写出到文件 * * @param isXlsx 是否为xlsx格式 * @param sheetName sheet名,第一个sheet名并写出到此sheet,例如sheet1 @@ -152,7 +152,7 @@ public class ExcelWriter extends ExcelBase { if (!FileUtil.exists(targetFile)) { this.targetFile = targetFile; - } else{ + } else { // 如果是已经存在的文件,则作为模板加载,此时不能写出到模板文件 // 初始化模板 this.templateContext = new TemplateContext(this.sheet); @@ -549,6 +549,7 @@ public class ExcelWriter extends ExcelBase { } // region ----- merge + /** * 合并当前行的单元格 * @@ -640,6 +641,7 @@ public class ExcelWriter extends ExcelBase { // endregion // region ----- write + /** * 写出数据,本方法只是将数据写入Workbook中的Sheet,并不写出到文件
* 写出的起始行为当前行号,可使用{@link #getCurrentRow()}方法调用,根据写出的的行数,当前行号自动增加 @@ -1010,21 +1012,16 @@ public class ExcelWriter extends ExcelBase { // region ----- fill - public ExcelWriter fillRow(final Map rowMap){ - rowMap.forEach((key, value)->{ - - }); + /** + * 填充模板行 + * + * @param rowMap 行数据 + * @return this + */ + public ExcelWriter fillRow(final Map rowMap) { + rowMap.forEach((key, value) -> this.templateContext.fillAndPointToNext(StrUtil.toStringOrNull(key), rowMap)); return this; } - - public ExcelWriter fillCell(final String name, final Object value){ - final Cell cell = this.templateContext.getCell(name); - if(null != cell){ - CellUtil.setCellValue(cell, value, this.config.getCellEditor()); - } - return this; - } - // endregion // region ----- writeCol @@ -1282,21 +1279,39 @@ public class ExcelWriter extends ExcelBase { * @throws IORuntimeException IO异常 */ public ExcelWriter flush() throws IORuntimeException { - return flush(this.targetFile); + return flush(false); + } + + /** + * 将Excel Workbook刷出到预定义的文件
+ * 如果用户未自定义输出的文件,将抛出{@link NullPointerException}
+ * 预定义文件可以通过{@link #setTargetFile(File)} 方法预定义,或者通过构造定义 + * + * @param override 是否覆盖已有文件 + * @return this + * @throws IORuntimeException IO异常 + */ + public ExcelWriter flush(final boolean override) throws IORuntimeException { + Assert.notNull(this.targetFile, "[targetFile] is null, and you must call setTargetFile(File) first."); + return flush(this.targetFile, override); } /** * 将Excel Workbook刷出到文件
* 如果用户未自定义输出的文件,将抛出{@link NullPointerException} * - * @param destFile 写出到的文件 + * @param targetFile 写出到的文件 + * @param override 是否覆盖已有文件 * @return this * @throws IORuntimeException IO异常 * @since 4.0.6 */ - public ExcelWriter flush(final File destFile) throws IORuntimeException { - Assert.notNull(destFile, "[destFile] is null, and you must call setDestFile(File) first or call flush(OutputStream)."); - return flush(FileUtil.getOutputStream(destFile), true); + public ExcelWriter flush(final File targetFile, final boolean override) throws IORuntimeException { + Assert.notNull(targetFile, "targetFile is null!"); + if (FileUtil.exists(targetFile) && !override) { + throw new IORuntimeException("File to write exist: " + targetFile); + } + return flush(FileUtil.getOutputStream(targetFile), true); } /** @@ -1321,6 +1336,7 @@ public class ExcelWriter extends ExcelBase { */ public ExcelWriter flush(final OutputStream out, final boolean isCloseOut) throws IORuntimeException { Assert.isFalse(this.isClosed, "ExcelWriter has been closed!"); + try { this.workbook.write(out); out.flush(); diff --git a/hutool-poi/src/main/java/org/dromara/hutool/poi/excel/writer/TemplateContext.java b/hutool-poi/src/main/java/org/dromara/hutool/poi/excel/writer/TemplateContext.java index 066d022cf..676d78f98 100644 --- a/hutool-poi/src/main/java/org/dromara/hutool/poi/excel/writer/TemplateContext.java +++ b/hutool-poi/src/main/java/org/dromara/hutool/poi/excel/writer/TemplateContext.java @@ -20,11 +20,14 @@ import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.CellType; import org.apache.poi.ss.usermodel.Sheet; import org.dromara.hutool.core.collection.CollUtil; +import org.dromara.hutool.core.lang.Assert; import org.dromara.hutool.core.regex.ReUtil; import org.dromara.hutool.core.text.StrUtil; import org.dromara.hutool.poi.excel.SheetUtil; +import org.dromara.hutool.poi.excel.cell.CellUtil; +import org.dromara.hutool.poi.excel.cell.VirtualCell; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.regex.Pattern; @@ -50,7 +53,7 @@ public class TemplateContext { private static final Pattern ESCAPE_VAR_PATTERN = Pattern.compile("\\\\\\{([.$_a-zA-Z]+\\d*[.$_a-zA-Z]*)\\\\}"); // 存储变量对应单元格的映射 - private final Map varMap = new HashMap<>(); + private final Map varMap = new LinkedHashMap<>(); /** * 构造 @@ -62,7 +65,7 @@ public class TemplateContext { } /** - * 获取变量对应的单元格,列表变量以.开头 + * 获取变量对应的当前单元格,列表变量以开头 * * @param varName 变量名 * @return 单元格 @@ -71,6 +74,45 @@ public class TemplateContext { return varMap.get(varName); } + /** + * 填充变量名name指向的单元格,并将变量指向下一列 + * + * @param name 变量名 + * @param rowData 一行数据的键值对 + * @since 6.0.0 + */ + public void fillAndPointToNext(final String name, final Map rowData) { + Cell cell = varMap.get(name); + if (null == cell) { + // 没有对应变量占位 + return; + } + + final String templateStr = cell.getStringCellValue(); + + // 指向下一列的单元格 + final Cell next = new VirtualCell(cell, cell.getColumnIndex(), cell.getRowIndex() + 1); + next.setCellValue(templateStr); + varMap.put(name, next); + + if(cell instanceof VirtualCell){ + // 虚拟单元格,转换为实际单元格 + final Cell newCell = CellUtil.getCell(cell.getSheet(), cell.getColumnIndex(), cell.getRowIndex(), true); + Assert.notNull(newCell, "Can not get or create cell at {},{}", cell.getColumnIndex(), cell.getRowIndex()); + newCell.setCellStyle(cell.getCellStyle()); + cell = newCell; + } + + // 模板替换 + if(StrUtil.equals(name, StrUtil.unWrap(templateStr, "{", "}"))){ + // 一个单元格只有一个变量 + CellUtil.setCellValue(cell, rowData.get(name)); + } else { + // 模板中存在多个变量或模板填充 + CellUtil.setCellValue(cell, StrUtil.format(templateStr, rowData)); + } + } + /** * 初始化,提取变量及位置,并将转义的变量回填 * diff --git a/hutool-poi/src/test/java/org/dromara/hutool/poi/excel/writer/ExcelWriteTest.java b/hutool-poi/src/test/java/org/dromara/hutool/poi/excel/writer/ExcelWriteTest.java index 61d0e9f7f..3667c6ade 100644 --- a/hutool-poi/src/test/java/org/dromara/hutool/poi/excel/writer/ExcelWriteTest.java +++ b/hutool-poi/src/test/java/org/dromara/hutool/poi/excel/writer/ExcelWriteTest.java @@ -143,7 +143,7 @@ public class ExcelWriteTest { writer.writeRow(row2); // 生成文件或导出Excel - writer.flush(FileUtil.file("d:/test/writeWithSheetTest.xlsx")); + writer.flush(FileUtil.file("d:/test/writeWithSheetTest.xlsx"), true); writer.close(); } @@ -868,7 +868,7 @@ public class ExcelWriteTest { writer.writeImg(file, 0, 0, 5, 10); - writer.flush(new File("C:\\Users\\zsz\\Desktop\\2.xlsx")); + writer.flush(new File("C:\\Users\\zsz\\Desktop\\2.xlsx"), true); writer.close(); } diff --git a/hutool-poi/src/test/java/org/dromara/hutool/poi/excel/writer/TemplateContextTest.java b/hutool-poi/src/test/java/org/dromara/hutool/poi/excel/writer/TemplateContextTest.java index def486b64..95c45e216 100644 --- a/hutool-poi/src/test/java/org/dromara/hutool/poi/excel/writer/TemplateContextTest.java +++ b/hutool-poi/src/test/java/org/dromara/hutool/poi/excel/writer/TemplateContextTest.java @@ -22,6 +22,6 @@ public class TemplateContextTest { final ExcelWriter writer = ExcelUtil.getWriter("template.xlsx"); final TemplateContext templateContext = new TemplateContext(writer.getSheet()); Assertions.assertNotNull(templateContext.getCell("date")); - Assertions.assertNotNull(templateContext.getCell(".month")); + Assertions.assertNotNull(templateContext.getCell("month")); } } diff --git a/hutool-poi/src/test/java/org/dromara/hutool/poi/excel/writer/TemplateWriterTest.java b/hutool-poi/src/test/java/org/dromara/hutool/poi/excel/writer/TemplateWriterTest.java new file mode 100644 index 000000000..fe653e2fc --- /dev/null +++ b/hutool-poi/src/test/java/org/dromara/hutool/poi/excel/writer/TemplateWriterTest.java @@ -0,0 +1,34 @@ +package org.dromara.hutool.poi.excel.writer; + +import org.dromara.hutool.core.io.file.FileUtil; +import org.dromara.hutool.core.map.MapUtil; +import org.dromara.hutool.poi.excel.ExcelUtil; +import org.junit.jupiter.api.Test; + +public class TemplateWriterTest { + @Test + void writeRowTest() { + final ExcelWriter writer = ExcelUtil.getWriter("d:/test/template.xlsx"); + + // 单个替换的变量 + writer.fillRow(MapUtil + .builder("date", (Object)"2024-01-01") + .build()); + + // 列表替换 + for (int i = 0; i < 10; i++) { + writer.fillRow(MapUtil + .builder("user.name", (Object)"张三") + .put("user.age", 18) + .put("year", 2024) + .put("month", 8) + .put("day", 24) + .put("day", 24) + .put("user.area123", "某某市") + .put("invalid", "不替换") + .build()); + } + + writer.flush(FileUtil.file("d:/test/templateResult.xlsx"), true); + } +} diff --git a/hutool-poi/src/test/resources/template.xlsx b/hutool-poi/src/test/resources/template.xlsx index b78abfddc..357afbdd1 100644 Binary files a/hutool-poi/src/test/resources/template.xlsx and b/hutool-poi/src/test/resources/template.xlsx differ