From 8f0ab7a042f7a1060b3730393eb7fa6fdee9418e Mon Sep 17 00:00:00 2001 From: BigDataDZ <76271875+BigDataDZ@users.noreply.github.com> Date: Wed, 9 Sep 2026 00:20:06 +0800 Subject: [PATCH 1/4] feat: honor wildcard custom converters when reading Signed-off-by: BigDataDZ <76271875+BigDataDZ@users.noreply.github.com> --- .../metadata/holder/AbstractReadHolder.java | 25 ++- .../sheet/read/WildcardConverterReadTest.java | 142 ++++++++++++++++++ 2 files changed, 162 insertions(+), 5 deletions(-) create mode 100644 fesod-sheet/src/test/java/org/apache/fesod/sheet/read/WildcardConverterReadTest.java diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/read/metadata/holder/AbstractReadHolder.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/read/metadata/holder/AbstractReadHolder.java index f7b58be88..bd942a7b0 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/read/metadata/holder/AbstractReadHolder.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/read/metadata/holder/AbstractReadHolder.java @@ -35,6 +35,7 @@ import org.apache.fesod.sheet.converters.Converter; import org.apache.fesod.sheet.converters.ConverterKeyBuild; import org.apache.fesod.sheet.converters.DefaultConverterLoader; +import org.apache.fesod.sheet.enums.CellDataTypeEnum; import org.apache.fesod.sheet.enums.HolderEnum; import org.apache.fesod.sheet.metadata.AbstractHolder; import org.apache.fesod.sheet.read.listener.ModelBuildEventListener; @@ -127,11 +128,25 @@ public AbstractReadHolder(ReadBasicParameter readBasicParameter, AbstractReadHol if (readBasicParameter.getCustomConverterList() != null && !readBasicParameter.getCustomConverterList().isEmpty()) { for (Converter converter : readBasicParameter.getCustomConverterList()) { - getConverterMap() - .put( - ConverterKeyBuild.buildKey( - converter.supportJavaTypeKey(), converter.supportExcelTypeKey()), - converter); + registerCustomConverter(converter); + } + } + } + + private void registerCustomConverter(Converter converter) { + getConverterMap() + .put( + ConverterKeyBuild.buildKey(converter.supportJavaTypeKey(), converter.supportExcelTypeKey()), + converter); + if (converter.supportExcelTypeKey() == null) { + // Read lookups use the concrete cell type as key (see ConverterUtils), so a converter + // registered with supportExcelTypeKey() == null must also be present under each + // concrete key to honor its "matches every cell data type" contract. + for (CellDataTypeEnum cellDataType : CellDataTypeEnum.values()) { + if (cellDataType != CellDataTypeEnum.EMPTY) { + getConverterMap() + .put(ConverterKeyBuild.buildKey(converter.supportJavaTypeKey(), cellDataType), converter); + } } } } diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/read/WildcardConverterReadTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/read/WildcardConverterReadTest.java new file mode 100644 index 000000000..54dc5c828 --- /dev/null +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/read/WildcardConverterReadTest.java @@ -0,0 +1,142 @@ +/* + * 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.read; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.apache.fesod.sheet.FesodSheet; +import org.apache.fesod.sheet.annotation.ExcelProperty; +import org.apache.fesod.sheet.context.AnalysisContext; +import org.apache.fesod.sheet.converters.Converter; +import org.apache.fesod.sheet.enums.CellDataTypeEnum; +import org.apache.fesod.sheet.event.AnalysisEventListener; +import org.apache.fesod.sheet.metadata.GlobalConfiguration; +import org.apache.fesod.sheet.metadata.data.ReadCellData; +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.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +@Tag(Tags.ROUND_TRIP) +@Slf4j +public class WildcardConverterReadTest extends AbstractExcelTest { + + private List dataList() { + List dataList = new ArrayList<>(); + StringWriteData yesRow = new StringWriteData(); + yesRow.setFlag("yes"); + StringWriteData noRow = new StringWriteData(); + noRow.setFlag("no"); + dataList.add(yesRow); + dataList.add(noRow); + return dataList; + } + + @Test + public void testWildcardConverterAppliesOnRead() throws Exception { + File file = createTempFile(ExcelFormat.XLSX); + FesodSheet.write(file, StringWriteData.class).sheet().doWrite(dataList()); + + List rows = FesodSheet.read(file, BooleanReadData.class, new BooleanReadListener()) + .registerConverter(new BooleanYesNoReadConverter(null)) + .sheet() + .doReadSync(); + + Assertions.assertEquals(2, rows.size()); + Assertions.assertEquals(Boolean.TRUE, rows.get(0).getFlag()); + Assertions.assertEquals(Boolean.FALSE, rows.get(1).getFlag()); + } + + @Test + public void testExplicitStringKeyConverterAppliesOnRead() throws Exception { + File file = createTempFile(ExcelFormat.XLSX); + FesodSheet.write(file, StringWriteData.class).sheet().doWrite(dataList()); + + List rows = FesodSheet.read(file, BooleanReadData.class, new BooleanReadListener()) + .registerConverter(new BooleanYesNoReadConverter(CellDataTypeEnum.STRING)) + .sheet() + .doReadSync(); + + Assertions.assertEquals(2, rows.size()); + Assertions.assertEquals(Boolean.TRUE, rows.get(0).getFlag()); + Assertions.assertEquals(Boolean.FALSE, rows.get(1).getFlag()); + } + + public static class BooleanYesNoReadConverter implements Converter { + + private final CellDataTypeEnum excelTypeKey; + + public BooleanYesNoReadConverter(CellDataTypeEnum excelTypeKey) { + this.excelTypeKey = excelTypeKey; + } + + @Override + public Class supportJavaTypeKey() { + return Boolean.class; + } + + @Override + public CellDataTypeEnum supportExcelTypeKey() { + // null means the converter matches every cell type + return excelTypeKey; + } + + @Override + public Boolean convertToJavaData( + ReadCellData cellData, + ExcelContentProperty contentProperty, + GlobalConfiguration globalConfiguration) { + String value = cellData.getStringValue(); + return "yes".equalsIgnoreCase(value); + } + } + + @Getter + @Setter + public static class StringWriteData { + + @ExcelProperty("flag") + private String flag; + } + + @Getter + @Setter + public static class BooleanReadData { + + @ExcelProperty("flag") + private Boolean flag; + } + + public static class BooleanReadListener extends AnalysisEventListener { + + @Override + public void invoke(BooleanReadData data, AnalysisContext context) {} + + @Override + public void doAfterAllAnalysed(AnalysisContext context) {} + } +} From d318eea0ec3dd727fdbd21da41d54914342db0b0 Mon Sep 17 00:00:00 2001 From: BigDataDZ <76271875+BigDataDZ@users.noreply.github.com> Date: Sat, 12 Sep 2026 13:04:02 +0800 Subject: [PATCH 2/4] fix: preserve explicit converter priority over wildcard expansion Signed-off-by: BigDataDZ <76271875+BigDataDZ@users.noreply.github.com> --- .../metadata/holder/AbstractReadHolder.java | 43 +++++++++++-------- .../sheet/read/WildcardConverterReadTest.java | 39 +++++++++++++++++ 2 files changed, 65 insertions(+), 17 deletions(-) diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/read/metadata/holder/AbstractReadHolder.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/read/metadata/holder/AbstractReadHolder.java index bd942a7b0..e90088b27 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/read/metadata/holder/AbstractReadHolder.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/read/metadata/holder/AbstractReadHolder.java @@ -26,7 +26,9 @@ package org.apache.fesod.sheet.read.metadata.holder; import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Set; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; @@ -34,6 +36,7 @@ import org.apache.fesod.common.util.ListUtils; import org.apache.fesod.sheet.converters.Converter; import org.apache.fesod.sheet.converters.ConverterKeyBuild; +import org.apache.fesod.sheet.converters.ConverterKeyBuild.ConverterKey; import org.apache.fesod.sheet.converters.DefaultConverterLoader; import org.apache.fesod.sheet.enums.CellDataTypeEnum; import org.apache.fesod.sheet.enums.HolderEnum; @@ -127,25 +130,31 @@ public AbstractReadHolder(ReadBasicParameter readBasicParameter, AbstractReadHol } if (readBasicParameter.getCustomConverterList() != null && !readBasicParameter.getCustomConverterList().isEmpty()) { + // Register explicit (JavaType, CellDataType) keys first so that wildcard expansion + // below cannot shadow them, regardless of registration order. + Set explicitKeys = new HashSet<>(); for (Converter converter : readBasicParameter.getCustomConverterList()) { - registerCustomConverter(converter); + ConverterKey explicitKey = + ConverterKeyBuild.buildKey(converter.supportJavaTypeKey(), converter.supportExcelTypeKey()); + getConverterMap().put(explicitKey, converter); + explicitKeys.add(explicitKey); } - } - } - - private void registerCustomConverter(Converter converter) { - getConverterMap() - .put( - ConverterKeyBuild.buildKey(converter.supportJavaTypeKey(), converter.supportExcelTypeKey()), - converter); - if (converter.supportExcelTypeKey() == null) { - // Read lookups use the concrete cell type as key (see ConverterUtils), so a converter - // registered with supportExcelTypeKey() == null must also be present under each - // concrete key to honor its "matches every cell data type" contract. - for (CellDataTypeEnum cellDataType : CellDataTypeEnum.values()) { - if (cellDataType != CellDataTypeEnum.EMPTY) { - getConverterMap() - .put(ConverterKeyBuild.buildKey(converter.supportJavaTypeKey(), cellDataType), converter); + // A converter registered with supportExcelTypeKey() == null matches every cell type, + // and read lookups use the concrete cell type as key (see ConverterUtils), so expand + // each wildcard registration under every concrete key — overwriting built-in defaults + // while keeping the explicit registrations above in priority. + for (Converter converter : readBasicParameter.getCustomConverterList()) { + if (converter.supportExcelTypeKey() != null) { + continue; + } + for (CellDataTypeEnum cellDataType : CellDataTypeEnum.values()) { + if (cellDataType == CellDataTypeEnum.EMPTY) { + continue; + } + ConverterKey expandedKey = ConverterKeyBuild.buildKey(converter.supportJavaTypeKey(), cellDataType); + if (!explicitKeys.contains(expandedKey)) { + getConverterMap().put(expandedKey, converter); + } } } } diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/read/WildcardConverterReadTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/read/WildcardConverterReadTest.java index 54dc5c828..9879017a6 100644 --- a/fesod-sheet/src/test/java/org/apache/fesod/sheet/read/WildcardConverterReadTest.java +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/read/WildcardConverterReadTest.java @@ -115,6 +115,45 @@ public Boolean convertToJavaData( } } + @Test + void testExplicitStringKeyConverterWinsOverLaterWildcardRegistration() throws Exception { + File file = createTempFile(ExcelFormat.XLSX); + FesodSheet.write(file, StringWriteData.class).sheet().doWrite(dataList()); + + List rows = FesodSheet.read(file, BooleanReadData.class, new BooleanReadListener()) + // explicit STRING key first, wildcard second: the explicit registration must keep + // handling STRING cells regardless of registration order + .registerConverter(new BooleanYesNoReadConverter(CellDataTypeEnum.STRING)) + .registerConverter(new AlwaysFalseReadConverter()) + .sheet() + .doReadSync(); + + Assertions.assertEquals(2, rows.size()); + Assertions.assertEquals(Boolean.TRUE, rows.get(0).getFlag()); + Assertions.assertEquals(Boolean.FALSE, rows.get(1).getFlag()); + } + + public static class AlwaysFalseReadConverter implements Converter { + + @Override + public Class supportJavaTypeKey() { + return Boolean.class; + } + + @Override + public CellDataTypeEnum supportExcelTypeKey() { + return null; + } + + @Override + public Boolean convertToJavaData( + ReadCellData cellData, + ExcelContentProperty contentProperty, + GlobalConfiguration globalConfiguration) { + return Boolean.FALSE; + } + } + @Getter @Setter public static class StringWriteData { From 8dd46dabc8f720bf0b440237a2422153e5328ad6 Mon Sep 17 00:00:00 2001 From: BigDataDZ <76271875+BigDataDZ@users.noreply.github.com> Date: Sat, 12 Sep 2026 22:28:03 +0800 Subject: [PATCH 3/4] test: cover wildcard String converters and document expansion asymmetry Signed-off-by: BigDataDZ <76271875+BigDataDZ@users.noreply.github.com> --- .../metadata/holder/AbstractReadHolder.java | 5 +- .../sheet/read/WildcardConverterReadTest.java | 56 +++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/read/metadata/holder/AbstractReadHolder.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/read/metadata/holder/AbstractReadHolder.java index e90088b27..661f49c89 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/read/metadata/holder/AbstractReadHolder.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/read/metadata/holder/AbstractReadHolder.java @@ -142,7 +142,10 @@ public AbstractReadHolder(ReadBasicParameter readBasicParameter, AbstractReadHol // A converter registered with supportExcelTypeKey() == null matches every cell type, // and read lookups use the concrete cell type as key (see ConverterUtils), so expand // each wildcard registration under every concrete key — overwriting built-in defaults - // while keeping the explicit registrations above in priority. + // while keeping the explicit registrations above in priority. Note this deliberately + // differs from the write side (#1069 expands to STRING only): write lookups see null + // targets for xlsx and force STRING only on the CSV/fill paths, while read lookups hit + // the actual cell type of every cell. for (Converter converter : readBasicParameter.getCustomConverterList()) { if (converter.supportExcelTypeKey() != null) { continue; diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/read/WildcardConverterReadTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/read/WildcardConverterReadTest.java index 9879017a6..a3e847883 100644 --- a/fesod-sheet/src/test/java/org/apache/fesod/sheet/read/WildcardConverterReadTest.java +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/read/WildcardConverterReadTest.java @@ -133,6 +133,62 @@ void testExplicitStringKeyConverterWinsOverLaterWildcardRegistration() throws Ex Assertions.assertEquals(Boolean.FALSE, rows.get(1).getFlag()); } + @Test + void testWildcardStringConverterAppliesToDataCells() throws Exception { + File file = createTempFile(ExcelFormat.XLSX); + FesodSheet.write(file, StringWriteData.class).sheet().doWrite(dataList()); + + // Index-based matching keeps the header path out of the equation here; the header-cell + // interaction of wildcard String converters is tracked separately in #1098. + List rows = FesodSheet.read(file, IndexStringReadData.class, new IndexStringReadListener()) + .registerConverter(new UpperCaseStringConverter()) + .sheet() + .doReadSync(); + + Assertions.assertEquals(2, rows.size()); + Assertions.assertEquals("YES", rows.get(0).getFlag()); + Assertions.assertEquals("NO", rows.get(1).getFlag()); + } + + public static class UpperCaseStringConverter implements Converter { + + @Override + public Class supportJavaTypeKey() { + return String.class; + } + + @Override + public CellDataTypeEnum supportExcelTypeKey() { + return null; + } + + @Override + public String convertToJavaData( + ReadCellData cellData, + ExcelContentProperty contentProperty, + GlobalConfiguration globalConfiguration) { + String value = cellData.getStringValue(); + return value == null ? null : value.toUpperCase(); + } + } + + @Getter + @Setter + public static class IndexStringReadData { + + @ExcelProperty(index = 0) + private String flag; + } + + public static class IndexStringReadListener extends AnalysisEventListener { + + @Override + public void invoke(IndexStringReadData data, AnalysisContext context) {} + + @Override + public void doAfterAllAnalysed(AnalysisContext context) {} + } + public static class AlwaysFalseReadConverter implements Converter { @Override From e717b5d6115c3d5a9f62d169b5095a22d2e354ea Mon Sep 17 00:00:00 2001 From: BigDataDZ <76271875+BigDataDZ@users.noreply.github.com> Date: Sun, 11 Oct 2026 19:58:50 +0800 Subject: [PATCH 4/4] docs(read): document converter resolution priority across read and write Add the shared "Converter resolution across read and write" paragraph to the read-side converter docs (EN and zh-CN), as agreed in #1069: how the (JavaType, CellDataType) lookup key is built per direction, why explicit registrations beat wildcard ones, and how CSV's forced STRING write type interacts with the wildcard registration. The header-row note is intentionally left out until #1098 lands. Generated-by: ZCode --- website/docs/sheet/read/converter.md | 12 ++++++++++++ .../current/sheet/read/converter.md | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/website/docs/sheet/read/converter.md b/website/docs/sheet/read/converter.md index b6ff7541c..395552ca5 100644 --- a/website/docs/sheet/read/converter.md +++ b/website/docs/sheet/read/converter.md @@ -88,3 +88,15 @@ public void converterRead() { .doRead(); } ``` + +## Converter Resolution Priority + +### Converter resolution across read and write + +Registered converters are looked up by the key `(JavaType, CellDataType)`, but the two directions build that key differently, and both give priority to explicit registrations over wildcard ones. + +**Write.** The target cell type is `null` for xlsx/xls (the converter decides the cell type it produces) and forced to `STRING` for CSV. A converter registered with `supportExcelTypeKey() == null` (the wildcard key `(JavaType, null)`) therefore wins on xlsx/xls writes directly, and is additionally registered under `(JavaType, STRING)` so CSV honors it too. That extra registration only takes over the `STRING` slot when it is still held by the built-in default, so an explicitly registered converter is never displaced, regardless of registration order. + +**Read.** Cells carry a concrete type from the file, so every format resolves `(JavaType, cellData.getType())` - e.g. `(Boolean, STRING)` for a text cell. A wildcard read registration is expanded under every concrete cell-type key (overwriting built-in defaults, skipping `EMPTY`), while keys already held by an explicitly registered converter are left untouched - again, explicit wins. + +**In short:** one wildcard registration covers every format and every cell type, an explicit registration always beats the wildcard, and user registrations always beat built-ins. diff --git a/website/i18n/zh-cn/docusaurus-plugin-content-docs/current/sheet/read/converter.md b/website/i18n/zh-cn/docusaurus-plugin-content-docs/current/sheet/read/converter.md index 19e774830..2d8a6ae7a 100644 --- a/website/i18n/zh-cn/docusaurus-plugin-content-docs/current/sheet/read/converter.md +++ b/website/i18n/zh-cn/docusaurus-plugin-content-docs/current/sheet/read/converter.md @@ -87,3 +87,15 @@ public void converterRead() { .doRead(); } ``` + +## 转换器解析优先级 + +### 跨读取与写入的转换器解析 + +注册的转换器通过 `(JavaType, CellDataType)` 键进行查找,但读取与写入两个方向构建该键的方式不同,且二者都优先匹配显式注册、其次才是通配注册。 + +**写入。** 对于 xlsx/xls,目标单元格类型为 `null`(由转换器自行决定产出的单元格类型);对于 CSV 则强制为 `STRING`。因此,以 `supportExcelTypeKey() == null`(通配键 `(JavaType, null)`)注册的转换器会直接在 xlsx/xls 写入时生效,并且会额外注册到 `(JavaType, STRING)` 键下,使 CSV 写入同样生效。这一额外注册只会在 `STRING` 槽位仍由内置默认转换器持有时才接管该槽位,因此无论注册顺序如何,显式注册的转换器都不会被顶掉。 + +**读取。** 单元格本身带有来自文件的具体类型,因此所有格式都会解析 `(JavaType, cellData.getType())` — 例如文本单元格对应 `(Boolean, STRING)`。通配的读取注册会展开到每一个具体单元格类型键下(覆盖内置默认值、跳过 `EMPTY`),而已经被显式注册的转换器占用的键则保持不变 — 同样是显式注册优先。 + +**一句话总结:** 一次通配注册即可覆盖所有格式与所有单元格类型;显式注册永远优先于通配注册;用户注册永远优先于内置转换器。