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..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 @@ -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,7 +36,9 @@ 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; import org.apache.fesod.sheet.metadata.AbstractHolder; import org.apache.fesod.sheet.read.listener.ModelBuildEventListener; @@ -126,12 +130,35 @@ 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()) { - getConverterMap() - .put( - ConverterKeyBuild.buildKey( - converter.supportJavaTypeKey(), converter.supportExcelTypeKey()), - converter); + ConverterKey explicitKey = + ConverterKeyBuild.buildKey(converter.supportJavaTypeKey(), converter.supportExcelTypeKey()); + getConverterMap().put(explicitKey, converter); + explicitKeys.add(explicitKey); + } + // 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. 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; + } + 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 new file mode 100644 index 000000000..a3e847883 --- /dev/null +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/read/WildcardConverterReadTest.java @@ -0,0 +1,237 @@ +/* + * 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); + } + } + + @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()); + } + + @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 + 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 { + + @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) {} + } +}