add template support

This commit is contained in:
Looly 2024-08-24 22:17:05 +08:00
parent 01e81de806
commit e8ceffa98d
9 changed files with 440 additions and 39 deletions

View File

@ -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
/**
* 设置单元格值<br>
* 根据传入的styleSet自动匹配样式<br>
@ -169,8 +171,8 @@ public class CellUtil {
* 根据传入的styleSet自动匹配样式<br>
* 当为头部样式时默认赋值头部样式但是头部中如果有数字日期等类型将按照数字日期样式设置
*
* @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}<br>
* 传入的x,y坐标列行数可以是合并单元格范围内的任意一个单元格

View File

@ -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;
/**
* 虚拟单元格表示一个单元格的位置值和样式但是并非实际创建的单元格<br>
* 注意虚拟单元格设置值和样式均不会在实际工作簿中生效
*
* @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;
}
}

View File

@ -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<String>, CellSetter {
* 结果使用ExcelWriter时可以不用
*/
private final Object result;
private final CellType resultType;
/**
* 构造
@ -57,8 +59,20 @@ public class FormulaCellValue implements CellValue<String>, 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<String>, CellSetter {
/**
* 获取结果
*
* @return 结果
*/
public Object getResult() {
return this.result;
}
/**
* 获取结果类型
*
* @return 结果类型{@code null}表示未明确
*/
public CellType getResultType() {
return this.resultType;
}
@Override
public String toString() {
return getResult().toString();

View File

@ -91,7 +91,7 @@ public class ExcelWriter extends ExcelBase<ExcelWriter, ExcelWriteConfig> {
/**
* 构造<br>
* 此构造不传入写出的Excel文件路径只能调用{@link #flush(OutputStream)}方法写出到流<br>
* 若写出到文件需要调用{@link #flush(File)} 写出到文件
* 若写出到文件需要调用{@link #flush(File, boolean)} 写出到文件
*
* @param isXlsx 是否为xlsx格式
* @since 3.2.1
@ -112,7 +112,7 @@ public class ExcelWriter extends ExcelBase<ExcelWriter, ExcelWriteConfig> {
/**
* 构造<br>
* 此构造不传入写出的Excel文件路径只能调用{@link #flush(OutputStream)}方法写出到流<br>
* 若写出到文件需要调用{@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<ExcelWriter, ExcelWriteConfig> {
if (!FileUtil.exists(targetFile)) {
this.targetFile = targetFile;
} else{
} else {
// 如果是已经存在的文件则作为模板加载此时不能写出到模板文件
// 初始化模板
this.templateContext = new TemplateContext(this.sheet);
@ -549,6 +549,7 @@ public class ExcelWriter extends ExcelBase<ExcelWriter, ExcelWriteConfig> {
}
// region ----- merge
/**
* 合并当前行的单元格
*
@ -640,6 +641,7 @@ public class ExcelWriter extends ExcelBase<ExcelWriter, ExcelWriteConfig> {
// endregion
// region ----- write
/**
* 写出数据本方法只是将数据写入Workbook中的Sheet并不写出到文件<br>
* 写出的起始行为当前行号可使用{@link #getCurrentRow()}方法调用根据写出的的行数当前行号自动增加
@ -1010,21 +1012,16 @@ public class ExcelWriter extends ExcelBase<ExcelWriter, ExcelWriteConfig> {
// 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<ExcelWriter, ExcelWriteConfig> {
* @throws IORuntimeException IO异常
*/
public ExcelWriter flush() throws IORuntimeException {
return flush(this.targetFile);
return flush(false);
}
/**
* 将Excel Workbook刷出到预定义的文件<br>
* 如果用户未自定义输出的文件将抛出{@link NullPointerException}<br>
* 预定义文件可以通过{@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刷出到文件<br>
* 如果用户未自定义输出的文件将抛出{@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<ExcelWriter, ExcelWriteConfig> {
*/
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();

View File

@ -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<String, Cell> varMap = new HashMap<>();
private final Map<String, Cell> 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));
}
}
/**
* 初始化提取变量及位置并将转义的变量回填
*

View File

@ -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();
}

View File

@ -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"));
}
}

View File

@ -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);
}
}