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/3] 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/3] 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/3] 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