Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
Expand Down Expand Up @@ -239,23 +240,10 @@ private void doFill(

if (analysisCell.getOnlyOneVariable()) {
String variable = analysisCell.getVariableList().get(0);
Object value = null;
if (dataKeySet.contains(variable)) {
value = dataMap.get(variable);
}
ExcelContentProperty excelContentProperty = ClassUtils.declaredExcelContentProperty(
dataMap,
writeContext
.currentWriteHolder()
.excelWriteHeadProperty()
.getHeadClazz(),
variable,
writeContext.currentWriteHolder());
cellWriteHandlerContext.setExcelContentProperty(excelContentProperty);
Object value = resolveFillProperty(dataMap, dataKeySet, variable, cellWriteHandlerContext);

createCell(analysisCell, fillConfig, cellWriteHandlerContext, rowWriteHandlerContext);
cellWriteHandlerContext.setOriginalValue(value);
cellWriteHandlerContext.setOriginalFieldClass(FieldUtils.getFieldClass(dataMap, variable, value));

converterAndSet(cellWriteHandlerContext);
WriteCellData<?> cellData = cellWriteHandlerContext.getFirstCellData();
Expand All @@ -279,21 +267,8 @@ private void doFill(

for (String variable : analysisCell.getVariableList()) {
cellValueBuild.append(analysisCell.getPrepareDataList().get(index++));
Object value = null;
if (dataKeySet.contains(variable)) {
value = dataMap.get(variable);
}
ExcelContentProperty excelContentProperty = ClassUtils.declaredExcelContentProperty(
dataMap,
writeContext
.currentWriteHolder()
.excelWriteHeadProperty()
.getHeadClazz(),
variable,
writeContext.currentWriteHolder());
Object value = resolveFillProperty(dataMap, dataKeySet, variable, cellWriteHandlerContext);
cellWriteHandlerContext.setOriginalValue(value);
cellWriteHandlerContext.setOriginalFieldClass(FieldUtils.getFieldClass(dataMap, variable, value));
cellWriteHandlerContext.setExcelContentProperty(excelContentProperty);
cellWriteHandlerContext.setTargetCellDataType(CellDataTypeEnum.STRING);

WriteCellData<?> cellData = convert(cellWriteHandlerContext);
Expand Down Expand Up @@ -339,6 +314,37 @@ private void doFill(
}
}

private Object resolveFillProperty(
Map dataMap, Set<String> dataKeySet, String variable, CellWriteHandlerContext context) {
Map propertyMap = dataMap;
String fieldName = variable;
// A literal Map key (including a null value) takes precedence over a nested path.
if (!dataKeySet.contains(variable)) {
int separator = fieldName.indexOf('.');
while (separator >= 0) {
Object parent = propertyMap.get(fieldName.substring(0, separator));
fieldName = fieldName.substring(separator + 1);
if (parent == null) {
propertyMap = Collections.emptyMap();
break;
}
propertyMap = parent instanceof Map ? (Map) parent : BeanMapUtils.create(parent);
if (propertyMap.containsKey(fieldName)) {
break;
}
separator = fieldName.indexOf('.');
}
}
Object value = propertyMap.get(fieldName);
Class<?> headClass = propertyMap == dataMap
? writeContext.currentWriteHolder().excelWriteHeadProperty().getHeadClazz()
: null;
context.setExcelContentProperty(ClassUtils.declaredExcelContentProperty(
propertyMap, headClass, fieldName, writeContext.currentWriteHolder()));
context.setOriginalFieldClass(FieldUtils.getFieldClass(propertyMap, fieldName, value));
return value;
}

private Integer getRelativeRowIndex() {
Integer relativeRowIndex = relativeRowIndexMap.get(currentUniqueDataFlag);
if (relativeRowIndex == null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.fesod.sheet.fill;

import java.io.File;
import java.io.FileOutputStream;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.apache.fesod.sheet.FesodSheet;
import org.apache.fesod.sheet.annotation.ExcelProperty;
import org.apache.fesod.sheet.annotation.format.DateTimeFormat;
import org.apache.fesod.sheet.converters.Converter;
import org.apache.fesod.sheet.enums.CellDataTypeEnum;
import org.apache.fesod.sheet.enums.WriteDirectionEnum;
import org.apache.fesod.sheet.metadata.GlobalConfiguration;
import org.apache.fesod.sheet.metadata.data.WriteCellData;
import org.apache.fesod.sheet.metadata.property.ExcelContentProperty;
import org.apache.fesod.sheet.testkit.Tags;
import org.apache.fesod.sheet.testkit.base.AbstractExcelTest;
import org.apache.fesod.sheet.testkit.enums.ExcelFormat;
import org.apache.fesod.sheet.testkit.params.ExcelFormatSource;
import org.apache.fesod.sheet.testkit.params.FormatScope;
import org.apache.fesod.sheet.write.metadata.fill.FillConfig;
import org.apache.fesod.sheet.write.metadata.fill.FillWrapper;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.WorkbookFactory;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.params.ParameterizedTest;

@Tag(Tags.ROUND_TRIP)
class NestedObjectFillTest extends AbstractExcelTest {

@ParameterizedTest
@ExcelFormatSource(FormatScope.BINARY)
void fillsNestedBeansAndRetainsLeafAnnotations(ExcelFormat format) throws Exception {
File template = template(format, "{.child.name}", "Value {.child.name}", "{.child.date}");
File output = createTempFile("nested-beans", format);
FesodSheet.write(output)
.withTemplate(template)
.sheet()
.doFill(Arrays.asList(new Record(new Child("alpha")), new Record(new Child("beta"))));

try (Workbook workbook = WorkbookFactory.create(output)) {
Sheet sheet = workbook.getSheetAt(0);
Assertions.assertEquals("ALPHA", sheet.getRow(0).getCell(0).getStringCellValue());
Assertions.assertEquals("Value ALPHA", sheet.getRow(0).getCell(1).getStringCellValue());
Assertions.assertEquals("BETA", sheet.getRow(1).getCell(0).getStringCellValue());
Assertions.assertEquals(
"yyyy/MM/dd", sheet.getRow(0).getCell(2).getCellStyle().getDataFormatString());
}
}

@ParameterizedTest
@ExcelFormatSource(FormatScope.BINARY)
void fillsNamedNestedMapsAndKeepsExactKeysFirst(ExcelFormat format) throws Exception {
File template = template(format, "{items.child.name}", "Value {items.child.name}", "{items.child.meta.code}");
File output = createTempFile("nested-maps", format);
Map<String, Object> row = new HashMap<>();
Map<String, Object> child = new HashMap<>();
child.put("name", "nested");
child.put("meta", Collections.singletonMap("code", 7));
row.put("child", child);
row.put("child.name", "literal");

FesodSheet.write(output)
.withTemplate(template)
.sheet()
.doFill(new FillWrapper("items", Collections.singletonList(row)));

try (Workbook workbook = WorkbookFactory.create(output)) {
Row filled = workbook.getSheetAt(0).getRow(0);
Assertions.assertEquals("literal", filled.getCell(0).getStringCellValue());
Assertions.assertEquals("Value literal", filled.getCell(1).getStringCellValue());
Assertions.assertEquals(7, filled.getCell(2).getNumericCellValue());
}
}

@ParameterizedTest
@ExcelFormatSource(FormatScope.BINARY)
void clearsMissingAndNullNestedProperties(ExcelFormat format) throws Exception {
File template = template(format, "{.child.name}", "Value {.child.name}", "{.child.missing}");
File output = createTempFile("nested-nulls", format);
Map<String, Object> exactNull = new HashMap<>();
exactNull.put("child", new Child("must-not-be-used"));
exactNull.put("child.name", null);
FesodSheet.write(output)
.withTemplate(template)
.sheet()
.doFill(Arrays.asList(new Record(null), Collections.emptyMap(), exactNull));

try (Workbook workbook = WorkbookFactory.create(output)) {
for (int row = 0; row < 3; row++) {
Row filled = workbook.getSheetAt(0).getRow(row);
Assertions.assertEquals("", filled.getCell(0).getStringCellValue());
Assertions.assertEquals("Value ", filled.getCell(1).getStringCellValue());
Assertions.assertEquals("", filled.getCell(2).getStringCellValue());
}
}
}

@ParameterizedTest
@ExcelFormatSource(FormatScope.BINARY)
void fillsMixedMapAndBeanPathsHorizontally(ExcelFormat format) throws Exception {
File template = template(format, "{.child.name}");
File output = createTempFile("nested-horizontal", format);
FillConfig config =
FillConfig.builder().direction(WriteDirectionEnum.HORIZONTAL).build();
FesodSheet.write(output)
.withTemplate(template)
.sheet()
.doFill(
Arrays.asList(
Collections.singletonMap("child", new Child("alpha")), new Record(new Child("beta"))),
config);

try (Workbook workbook = WorkbookFactory.create(output)) {
Row filled = workbook.getSheetAt(0).getRow(0);
Assertions.assertEquals("ALPHA", filled.getCell(0).getStringCellValue());
Assertions.assertEquals("BETA", filled.getCell(1).getStringCellValue());
}
}

private File template(ExcelFormat format, String... placeholders) throws Exception {
File file = createTempFile("nested-template", format);
try (Workbook workbook = format == ExcelFormat.XLS ? new HSSFWorkbook() : new XSSFWorkbook();
FileOutputStream output = new FileOutputStream(file)) {
Row row = workbook.createSheet().createRow(0);
for (int i = 0; i < placeholders.length; i++) {
row.createCell(i).setCellValue(placeholders[i]);
}
workbook.write(output);
}
return file;
}

@Getter
@AllArgsConstructor
public static class Record {
private final Child child;
}

@Getter
public static class Child {
@ExcelProperty(converter = UpperCaseConverter.class)
private final String name;

@DateTimeFormat("yyyy/MM/dd")
private final LocalDate date = LocalDate.of(2026, 10, 20);

public Child(String name) {
this.name = name;
}
}

public static class UpperCaseConverter implements Converter<String> {
@Override
public Class<?> supportJavaTypeKey() {
return String.class;
}

@Override
public CellDataTypeEnum supportExcelTypeKey() {
return CellDataTypeEnum.STRING;
}

@Override
public WriteCellData<?> convertToExcelData(
String value, ExcelContentProperty property, GlobalConfiguration configuration) {
return new WriteCellData<>(value.toUpperCase(Locale.ROOT));
}
}
}
9 changes: 9 additions & 0 deletions website/docs/sheet/fill/fill.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,21 @@ the cell is filled:
| `{name}` | a single variable | `doFill(object)`, `doFill(map)` |
| `{.name}` | the `name` property of every item of a list | `doFill(list)`, `fill(list, ...)` |
| `{data1.name}` | the same, for the list named `data1` | `fill(new FillWrapper("data1", list), ...)` |
| `{.child.name}` | a nested property of each list item | `doFill(list)` |
| `{data1.child.name}` | a nested property of each item in the list named `data1` | `fill(new FillWrapper("data1", list), ...)` |
| `\{name\}` | escaped with `\`, never parsed | nothing |

The leading `.` is what makes a cell repeat once per item - downwards by default, or across the
columns with `FillConfig.builder().direction(WriteDirectionEnum.HORIZONTAL)`. The text before the
`.` names which list the items come from, so one template can hold several lists side by side.

After the list prefix, additional dots traverse JavaBean properties or Map entries. For example,
`{.child.name}` reads `item.getChild().getName()`. A missing property or null intermediate value is
filled as empty. A Map entry matching the complete remaining path, such as `"child.name"`, takes
precedence over traversal, including when its value is null. Format annotations and converters on
the final JavaBean field are applied as usual. The first dot still identifies a list: use a
`FillWrapper` for named lists rather than `{child.name}` for a single object.

A cell may mix several placeholders with ordinary text, as in `{name} is {number} years old this
year`. A placeholder the fill does not supply is cleared rather than left in the sheet: filling a
list against `{name}`, or an object against `{.name}`, empties the cell and keeps only the text
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,18 @@ title: '填充'
| `{name}` | 单个变量 | `doFill(object)`、`doFill(map)` |
| `{.name}` | 列表中每一项的 `name` 属性 | `doFill(list)`、`fill(list, ...)` |
| `{data1.name}` | 同上,但取自名为 `data1` 的列表 | `fill(new FillWrapper("data1", list), ...)` |
| `{.child.name}` | 列表中每一项的嵌套属性 | `doFill(list)` |
| `{data1.child.name}` | 名为 `data1` 的列表中每一项的嵌套属性 | `fill(new FillWrapper("data1", list), ...)` |
| `\{name\}` | 用 `\` 转义,不会被解析 | 不填充 |

开头的 `.` 决定了单元格会按列表的每一项重复:默认向下重复,也可以通过 `FillConfig.builder().direction(WriteDirectionEnum.HORIZONTAL)` 向右重复。`.` 之前的文字指明数据来自哪个列表,因此一个模板中可以并存多个列表。

列表前缀之后的点号用于逐层读取 JavaBean 属性或 Map 条目。例如,`{.child.name}` 对应
`item.getChild().getName()`。属性不存在或中间对象为 null 时填充为空。Map 中如果存在完整的
剩余路径键(如 `"child.name"`),则优先使用该键,包括值为 null 的情况。最终 JavaBean 字段的
格式注解和转换器仍然生效。第一个点号仍用于区分列表,因此不要用 `{child.name}` 填充单个对象;
命名列表请使用 `FillWrapper`。

一个单元格里可以混合多个占位符和普通文本,例如 `{name}今年{number}岁了`。填充时没有提供的占位符会被清空,而不是原样留在表格里:用列表填充 `{name}`,或者用单个对象填充 `{.name}`,都会清空该单元格,只保留周围的文本。

转义只是让大括号不被解析,而 `\` 本身只有在同一个单元格里还存在真实占位符时才会被去掉。如果单元格里没有别的内容,`\{name\}` 会原样写出,包括反斜杠。
Expand Down
Loading