From d3752b7648354186f514661acb7ccc070fa96418 Mon Sep 17 00:00:00 2001 From: Looly Date: Sun, 25 Aug 2024 12:17:27 +0800 Subject: [PATCH] add fill template --- .../dromara/hutool/core/bean/DynaBean.java | 23 +++++ .../core/text/placeholder/StrTemplate.java | 6 +- .../template/NamedPlaceholderStrTemplate.java | 20 +---- .../dromara/hutool/poi/excel/SheetUtil.java | 22 +++-- .../poi/excel/writer/ExcelWriteConfig.java | 16 ++++ .../hutool/poi/excel/writer/ExcelWriter.java | 70 +++++++++++---- .../poi/excel/writer/TemplateContext.java | 87 +++++++++++++++---- .../poi/excel/writer/TemplateWriterTest.java | 61 ++++++++++--- 8 files changed, 233 insertions(+), 72 deletions(-) diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/bean/DynaBean.java b/hutool-core/src/main/java/org/dromara/hutool/core/bean/DynaBean.java index b501316ed..a29f20712 100644 --- a/hutool-core/src/main/java/org/dromara/hutool/core/bean/DynaBean.java +++ b/hutool-core/src/main/java/org/dromara/hutool/core/bean/DynaBean.java @@ -93,6 +93,17 @@ public class DynaBean implements Cloneable, Serializable { } } + /** + * 获得path表达式对应的值 + * + * @param expression path表达式 + * @param 属性值类型 + * @return 值 + */ + public T getProperty(final String expression) { + return BeanUtil.getProperty(bean, expression); + } + /** * 获得字段对应值 * @@ -161,6 +172,18 @@ public class DynaBean implements Cloneable, Serializable { } } + /** + * 设置属性值 + * + * @param expression path表达式 + * @param value 值 + * @return this + */ + public DynaBean setProperty(final String expression, final Object value) { + BeanUtil.setProperty(bean, expression, value); + return this; + } + /** * 设置字段值 * diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/text/placeholder/StrTemplate.java b/hutool-core/src/main/java/org/dromara/hutool/core/text/placeholder/StrTemplate.java index de1435160..63aa58615 100644 --- a/hutool-core/src/main/java/org/dromara/hutool/core/text/placeholder/StrTemplate.java +++ b/hutool-core/src/main/java/org/dromara/hutool/core/text/placeholder/StrTemplate.java @@ -346,11 +346,7 @@ public abstract class StrTemplate { // 根据 占位符 返回 需要序列化的值 final Object value = valueSupplier.apply(segment); if (value != null) { - if (value instanceof String) { - return (String) value; - } else { - return StrUtil.utf8Str(value); - } + return StrUtil.utf8Str(value); } else { // 处理null值 return formatNullValue(segment); diff --git a/hutool-core/src/main/java/org/dromara/hutool/core/text/placeholder/template/NamedPlaceholderStrTemplate.java b/hutool-core/src/main/java/org/dromara/hutool/core/text/placeholder/template/NamedPlaceholderStrTemplate.java index aff16bfb0..faf6d12ea 100644 --- a/hutool-core/src/main/java/org/dromara/hutool/core/text/placeholder/template/NamedPlaceholderStrTemplate.java +++ b/hutool-core/src/main/java/org/dromara/hutool/core/text/placeholder/template/NamedPlaceholderStrTemplate.java @@ -17,19 +17,16 @@ package org.dromara.hutool.core.text.placeholder.template; import org.dromara.hutool.core.array.ArrayUtil; -import org.dromara.hutool.core.bean.BeanDesc; import org.dromara.hutool.core.bean.BeanUtil; import org.dromara.hutool.core.collection.CollUtil; import org.dromara.hutool.core.collection.ListUtil; import org.dromara.hutool.core.exception.HutoolException; -import org.dromara.hutool.core.func.LambdaUtil; import org.dromara.hutool.core.lang.Assert; import org.dromara.hutool.core.math.NumberUtil; import org.dromara.hutool.core.text.StrPool; import org.dromara.hutool.core.text.placeholder.StrTemplate; import org.dromara.hutool.core.text.placeholder.segment.*; -import java.lang.reflect.Method; import java.util.*; import java.util.function.*; @@ -345,15 +342,6 @@ public class NamedPlaceholderStrTemplate extends StrTemplate { } if (beanOrMap instanceof Map) { return format((Map) beanOrMap); - } else if (BeanUtil.isReadableBean(beanOrMap.getClass())) { - final BeanDesc beanDesc = BeanUtil.getBeanDesc(beanOrMap.getClass()); - return format(fieldName -> { - final Method getterMethod = beanDesc.getGetter(fieldName); - if (getterMethod == null) { - return null; - } - return LambdaUtil.buildGetter(getterMethod).apply(beanOrMap); - }); } return format(fieldName -> BeanUtil.getProperty(beanOrMap, fieldName)); } @@ -374,14 +362,14 @@ public class NamedPlaceholderStrTemplate extends StrTemplate { /** * 使用 占位变量名称 从 valueSupplier 中查询值来 替换 占位符 * - * @param valueSupplier 根据 占位变量名称 返回 值 + * @param valueProvider 根据 占位变量名称 返回 值 * @return 格式化字符串 */ - public String format(final Function valueSupplier) { - if (valueSupplier == null) { + public String format(final Function valueProvider) { + if (valueProvider == null) { return getTemplate(); } - return formatBySegment(segment -> valueSupplier.apply(segment.getPlaceholder())); + return formatBySegment(segment -> valueProvider.apply(segment.getPlaceholder())); } /** diff --git a/hutool-poi/src/main/java/org/dromara/hutool/poi/excel/SheetUtil.java b/hutool-poi/src/main/java/org/dromara/hutool/poi/excel/SheetUtil.java index 5501f9271..84c9c4586 100644 --- a/hutool-poi/src/main/java/org/dromara/hutool/poi/excel/SheetUtil.java +++ b/hutool-poi/src/main/java/org/dromara/hutool/poi/excel/SheetUtil.java @@ -25,7 +25,6 @@ import org.apache.poi.ss.util.cellwalk.CellWalk; import org.apache.poi.xssf.streaming.SXSSFSheet; import org.apache.poi.xssf.usermodel.XSSFSheet; import org.dromara.hutool.core.reflect.FieldUtil; -import org.dromara.hutool.core.text.StrUtil; /** * {@link Sheet}相关工具类 @@ -40,18 +39,27 @@ public class SheetUtil { * 如果sheet表在Workbook中已经存在,则获取之,否则创建之 * * @param book 工作簿{@link Workbook} - * @param sheetName 工作表名 + * @param sheetName 工作表名,{@code null}表示默认 * @return 工作表{@link Sheet} * @since 4.0.2 */ - public static Sheet getOrCreateSheet(final Workbook book, String sheetName) { + public static Sheet getOrCreateSheet(final Workbook book, final String sheetName) { if (null == book) { return null; } - sheetName = StrUtil.isBlank(sheetName) ? "sheet1" : sheetName; - Sheet sheet = book.getSheet(sheetName); - if (null == sheet) { - sheet = book.createSheet(sheetName); + + Sheet sheet; + if (null == sheetName) { + sheet = book.getSheetAt(0); + if (null == sheet) { + // 工作簿中无sheet,创建默认 + sheet = book.createSheet(); + } + } else { + sheet = book.getSheet(sheetName); + if (null == sheet) { + sheet = book.createSheet(sheetName); + } } return sheet; } diff --git a/hutool-poi/src/main/java/org/dromara/hutool/poi/excel/writer/ExcelWriteConfig.java b/hutool-poi/src/main/java/org/dromara/hutool/poi/excel/writer/ExcelWriteConfig.java index 31fb3964a..c5fa173af 100644 --- a/hutool-poi/src/main/java/org/dromara/hutool/poi/excel/writer/ExcelWriteConfig.java +++ b/hutool-poi/src/main/java/org/dromara/hutool/poi/excel/writer/ExcelWriteConfig.java @@ -41,6 +41,11 @@ public class ExcelWriteConfig extends ExcelConfig { * 是否只保留别名对应的字段 */ protected boolean onlyAlias; + /** + * 是否强制插入行
+ * 如果为{@code true},则写入行以下的已存在行下移,{@code false}则利用填充已有行,不存在再创建行 + */ + protected boolean insertRow; /** * 标题顺序比较器 */ @@ -76,6 +81,17 @@ public class ExcelWriteConfig extends ExcelConfig { return this; } + /** + * 设置是否插入行,如果为true,则写入行以下的已存在行下移,false则利用填充已有行,不存在时创建行 + * + * @param insertRow 是否插入行 + * @return this + */ + public ExcelWriteConfig setInsertRow(final boolean insertRow) { + this.insertRow = insertRow; + return this; + } + /** * 获取单例的别名比较器,比较器的顺序为别名加入的顺序 * 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 e86b4d1cc..da7d3af92 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 @@ -847,12 +847,15 @@ public class ExcelWriter extends ExcelBase { public ExcelWriter writeHeadRow(final Iterable rowData) { Assert.isFalse(this.isClosed, "ExcelWriter has been closed!"); this.headLocationCache = new SafeConcurrentHashMap<>(); - final Row row = this.sheet.createRow(this.currentRow.getAndIncrement()); + + final int rowNum = this.currentRow.getAndIncrement(); + final Row row = this.config.insertRow ? this.sheet.createRow(rowNum) : RowUtil.getOrCreateRow(this.sheet, rowNum); + final CellEditor cellEditor = this.config.getCellEditor(); int i = 0; Cell cell; for (final Object value : rowData) { - cell = row.createCell(i); + cell = CellUtil.getOrCreateCell(row, i); CellUtil.setCellValue(cell, value, this.styleSet, true, cellEditor); this.headLocationCache.put(StrUtil.toString(value), i); i++; @@ -945,6 +948,17 @@ public class ExcelWriter extends ExcelBase { return writeRow(rowMap, isWriteKeyAsHead); } + /** + * 填充非列表模板变量(一次性变量) + * + * @param rowMap 行数据 + * @return this + */ + public ExcelWriter fillOnce(final Map rowMap) { + rowMap.forEach((key, value) -> this.templateContext.fill(StrUtil.toStringOrNull(key), rowMap, false)); + return this; + } + /** * 将一个Map写入到Excel,isWriteKeyAsHead为true写出两行,Map的keys做为一行,values做为第二行,否则只写出一行values
* 如果rowMap为空(包括null),则写出空行 @@ -961,11 +975,17 @@ public class ExcelWriter extends ExcelBase { return passCurrentRow(); } + // 模板写出 + if (null != this.templateContext) { + fillRow(rowMap, this.config.insertRow); + return this; + } + final Table aliasTable = this.config.aliasTable(rowMap); if (isWriteKeyAsHead) { // 写出标题行,并记录标题别名和列号的关系 writeHeadRow(aliasTable.columnKeys()); - // 记录原数据key对应列号 + // 记录原数据key和别名对应列号 int i = 0; for (final Object key : aliasTable.rowKeySet()) { this.headLocationCache.putIfAbsent(StrUtil.toString(key), i); @@ -1005,21 +1025,10 @@ public class ExcelWriter extends ExcelBase { */ public ExcelWriter writeRow(final Iterable rowData) { Assert.isFalse(this.isClosed, "ExcelWriter has been closed!"); - RowUtil.writeRow(this.sheet.createRow(this.currentRow.getAndIncrement()), rowData, this.styleSet, false, this.config.getCellEditor()); - return this; - } - // endregion - // region ----- fill - - /** - * 填充模板行 - * - * @param rowMap 行数据 - * @return this - */ - public ExcelWriter fillRow(final Map rowMap) { - rowMap.forEach((key, value) -> this.templateContext.fillAndPointToNext(StrUtil.toStringOrNull(key), rowMap)); + final int rowNum = this.currentRow.getAndIncrement(); + final Row row = this.config.insertRow ? this.sheet.createRow(rowNum) : RowUtil.getOrCreateRow(this.sheet, rowNum); + RowUtil.writeRow(row, rowData, this.styleSet, false, this.config.getCellEditor()); return this; } // endregion @@ -1374,4 +1383,31 @@ public class ExcelWriter extends ExcelBase { // 清空对象 this.styleSet = null; } + + /** + * 填充模板行,用于列表填充 + * + * @param rowMap 行数据 + * @param insertRow 是否插入行,如果为{@code true},则已有行下移,否则利用已有行 + */ + private void fillRow(final Map rowMap, final boolean insertRow) { + if(insertRow){ + // 当前填充行的模板行以下全部下移 + final int bottomRowIndex = this.templateContext.getBottomRowIndex(rowMap); + if(bottomRowIndex < 0){ + // 无可填充行 + return; + } + if(bottomRowIndex != 0){ + final int lastRowNum = this.sheet.getLastRowNum(); + if(bottomRowIndex <= lastRowNum){ + // 填充行底部需有数据,无数据跳过 + // 虚拟行的行号就是需要填充的行,这行的已有数据整体下移 + this.sheet.shiftRows(bottomRowIndex, this.sheet.getLastRowNum(), 1); + } + } + } + + rowMap.forEach((key, value) -> this.templateContext.fill(StrUtil.toStringOrNull(key), rowMap, true)); + } } 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 676d78f98..b85d4c0c7 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 @@ -22,6 +22,7 @@ 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.StrPool; import org.dromara.hutool.core.text.StrUtil; import org.dromara.hutool.poi.excel.SheetUtil; import org.dromara.hutool.poi.excel.cell.CellUtil; @@ -40,6 +41,9 @@ import java.util.regex.Pattern; */ public class TemplateContext { + private static final String VAR_PREFIX = StrPool.DELIM_START; + private static final String VAR_SUFFIX = StrPool.DELIM_END; + /** * 变量正则 *
    @@ -75,14 +79,43 @@ public class TemplateContext { } /** - * 填充变量名name指向的单元格,并将变量指向下一列 + * 获取当前替换的数据行对应变量的底部索引
    + * 此方法用户获取填充行,以便下移填充行后的行
    + *
      + *
    • 如果为实体单元格,直接填充,无需下移,返回0
    • + *
    • 如果为{@link VirtualCell},返回最底部虚拟单元格各行号
    • + *
    * - * @param name 变量名 - * @param rowData 一行数据的键值对 + * @param rowData 填充数据 + * @return 最大行索引,-1表示无数据填充,0表示无需下移 + */ + public int getBottomRowIndex(final Map rowData) { + int bottomRowIndex = -1; + Cell cell; + for (final Object key : rowData.keySet()) { + cell = this.varMap.get(StrUtil.toStringOrNull(key)); + if (null != cell) { + if(cell instanceof VirtualCell){ + bottomRowIndex = Math.max(bottomRowIndex, cell.getRowIndex()); + } else{ + // 实体单元格,直接填充,无需下移 + bottomRowIndex = 0; + } + } + } + return bottomRowIndex; + } + + /** + * 填充变量名name指向的单元格 + * + * @param name 变量名 + * @param rowData 一行数据的键值对 + * @param isListVar 是否为列表填充,列表填充会自动指向下一列,否则填充结束后删除变量 * @since 6.0.0 */ - public void fillAndPointToNext(final String name, final Map rowData) { - Cell cell = varMap.get(name); + public void fill(final String name, final Map rowData, final boolean isListVar) { + final Cell cell = varMap.get(name); if (null == cell) { // 没有对应变量占位 return; @@ -90,27 +123,49 @@ public class TemplateContext { final String templateStr = cell.getStringCellValue(); - // 指向下一列的单元格 - final Cell next = new VirtualCell(cell, cell.getColumnIndex(), cell.getRowIndex() + 1); - next.setCellValue(templateStr); - varMap.put(name, next); + if (isListVar) { + // 指向下一列的单元格 + final Cell next = new VirtualCell(cell, cell.getColumnIndex(), cell.getRowIndex() + 1); + next.setCellValue(templateStr); + varMap.put(name, next); + } else { + // 非列表,一次性填充,即变量填充后,和此单元格去掉关联 + varMap.remove(name); + } - if(cell instanceof VirtualCell){ + fill(cell, name, templateStr, rowData); + } + + /** + * 填充数据 + * + * @param cell 单元格,非模板中变量所在单元格则为{@link VirtualCell} + * @param name 变量名 + * @param templateStr 模板字符串 + * @param rowData 填充的数据 + */ + private void fill(Cell cell, final String name, final String templateStr, final Map rowData) { + if (cell instanceof VirtualCell) { // 虚拟单元格,转换为实际单元格 - final Cell newCell = CellUtil.getCell(cell.getSheet(), cell.getColumnIndex(), cell.getRowIndex(), true); + final Cell newCell; + + 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; } + final Object cellValue; // 模板替换 - if(StrUtil.equals(name, StrUtil.unWrap(templateStr, "{", "}"))){ + if (StrUtil.equals(name, StrUtil.unWrap(templateStr, VAR_PREFIX, VAR_SUFFIX))) { // 一个单元格只有一个变量 - CellUtil.setCellValue(cell, rowData.get(name)); + cellValue = rowData.get(name); } else { - // 模板中存在多个变量或模板填充 - CellUtil.setCellValue(cell, StrUtil.format(templateStr, rowData)); + // 模板中存在多个变量或模板填充,直接赋值为String + cellValue = StrUtil.format(templateStr, rowData); } + CellUtil.setCellValue(cell, cellValue); } /** @@ -134,7 +189,7 @@ public class TemplateContext { } // 替换转义的变量 - final String str = ReUtil.replaceAll(cellValue, ESCAPE_VAR_PATTERN, (matcher) -> "{" + matcher.group(1) + "}"); + final String str = ReUtil.replaceAll(cellValue, ESCAPE_VAR_PATTERN, (matcher) -> VAR_PREFIX + matcher.group(1) + VAR_SUFFIX); if (!StrUtil.equals(cellValue, str)) { cell.setCellValue(str); } 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 index fe653e2fc..6abfcb124 100644 --- 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 @@ -5,30 +5,69 @@ import org.dromara.hutool.core.map.MapUtil; import org.dromara.hutool.poi.excel.ExcelUtil; import org.junit.jupiter.api.Test; +import java.util.Map; + public class TemplateWriterTest { + + @Test + void insertTest() { + final ExcelWriter writer = ExcelUtil.getWriter("d:/test/template.xlsx"); + writer.getConfig().setInsertRow(true); + writer.setCurrentRow(3); + writer.getSheet().shiftRows(4, 4, 10); + + writer.flush(FileUtil.file("d:/test/templateInsertResult.xlsx"), true); + writer.close(); + } + @Test void writeRowTest() { final ExcelWriter writer = ExcelUtil.getWriter("d:/test/template.xlsx"); + writer.getConfig().setInsertRow(true); // 单个替换的变量 - writer.fillRow(MapUtil + writer.fillOnce(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.writeRow(createRow(), false); } writer.flush(FileUtil.file("d:/test/templateResult.xlsx"), true); + writer.close(); + } + + @Test + void writeRowWithFooterTest() { + final ExcelWriter writer = ExcelUtil.getWriter("d:/test/templateWithFooter.xlsx"); + writer.getConfig().setInsertRow(true); + + // 单个替换的变量 + writer.fillOnce(MapUtil + .builder("date", (Object)"2024-01-01") + .build()); + + // 列表替换 + for (int i = 0; i < 10; i++) { + writer.writeRow(createRow(), false); + } + + writer.flush(FileUtil.file("d:/test/templateWithFooterResult.xlsx"), true); + writer.close(); + } + + private static Map createRow(){ + return 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(); } }