From 9f2782d0e8fe33a567079cda920c759bd53f9831 Mon Sep 17 00:00:00 2001 From: Mikkey-f <178465103+Mikkey-f@users.noreply.github.com> Date: Mon, 7 Sep 2026 13:27:29 +0800 Subject: [PATCH] fix: honor wildcard-key custom converters when writing CSV (#1045) (#1056) --- .../fesod/sheet/converters/Converter.java | 8 +- .../metadata/holder/AbstractWriteHolder.java | 42 +++- .../converter/WildcardConverterWriteTest.java | 220 ++++++++++++++++++ website/docs/sheet/write/converter.md | 25 ++ .../current/sheet/write/converter.md | 25 ++ 5 files changed, 309 insertions(+), 11 deletions(-) create mode 100644 fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/WildcardConverterWriteTest.java diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/Converter.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/Converter.java index affb88631..5c2e772ab 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/Converter.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/Converter.java @@ -46,7 +46,13 @@ default Class> supportJavaTypeKey() { } /** - * Back to object enum in excel + * Excel data type this converter supports. + * + *
Returning {@code null} declares the wildcard key {@code (JavaType, null)}: the converter matches + * every target cell data type. The xlsx write path looks converters up with a {@code null} target type, + * and a custom converter declaring {@code null} is additionally registered under the + * {@link CellDataTypeEnum#STRING} key so that it is honored by the CSV write path, which forces the + * lookup key to {@code (JavaType, STRING)}. * * @return Support for {@link CellDataTypeEnum} */ diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/metadata/holder/AbstractWriteHolder.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/metadata/holder/AbstractWriteHolder.java index e658b5d8e..10e9e4a1c 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/metadata/holder/AbstractWriteHolder.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/metadata/holder/AbstractWriteHolder.java @@ -42,6 +42,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.HeadKindEnum; import org.apache.fesod.sheet.enums.HeaderMergeStrategy; import org.apache.fesod.sheet.event.NotRepeatExecutor; @@ -273,11 +274,7 @@ public AbstractWriteHolder(WriteBasicParameter writeBasicParameter, AbstractWrit setConverterMap(new HashMap<>(parentAbstractWriteHolder.getConverterMap())); if (CollectionUtils.isNotEmpty(parentAbstractWriteHolder.getCustomConverterList())) { for (Converter> converter : parentAbstractWriteHolder.getCustomConverterList()) { - getConverterMap() - .put( - ConverterKeyBuild.buildKey( - converter.supportJavaTypeKey(), converter.supportExcelTypeKey()), - converter); + putCustomConverter(converter); } } } @@ -285,11 +282,36 @@ public AbstractWriteHolder(WriteBasicParameter writeBasicParameter, AbstractWrit && !writeBasicParameter.getCustomConverterList().isEmpty()) { this.customConverterList = writeBasicParameter.getCustomConverterList(); for (Converter> converter : writeBasicParameter.getCustomConverterList()) { - getConverterMap() - .put( - ConverterKeyBuild.buildKey( - converter.supportJavaTypeKey(), converter.supportExcelTypeKey()), - converter); + putCustomConverter(converter); + } + } + } + + /** + * Registers a custom converter under the key it declares. + * + *
A custom converter declaring a {@code null} excel type key is additionally registered under the + * {@link CellDataTypeEnum#STRING} key. The xlsx write path looks converters up with a {@code null} + * target cell data type, while the CSV write path forces the target to {@code STRING}; registering + * only the wildcard key would make the converter silently shadowed by the built-in string converter + * in CSV mode. See issues #1045 and #1056. + * + *
The {@code STRING} registration only takes over the slot when it is still held by the built-in + * default converter, so an explicitly registered converter for the same {@code STRING} key is never + * displaced, regardless of registration order. + */ + private void putCustomConverter(Converter> converter) { + Class> javaTypeKey = converter.supportJavaTypeKey(); + CellDataTypeEnum excelTypeKey = converter.supportExcelTypeKey(); + getConverterMap().put(ConverterKeyBuild.buildKey(javaTypeKey, excelTypeKey), converter); + if (excelTypeKey == null) { + ConverterKeyBuild.ConverterKey stringKey = ConverterKeyBuild.buildKey(javaTypeKey, CellDataTypeEnum.STRING); + Converter> occupant = getConverterMap().get(stringKey); + if (occupant == null + || occupant + == DefaultConverterLoader.loadDefaultWriteConverter() + .get(stringKey)) { + getConverterMap().put(stringKey, converter); } } } diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/WildcardConverterWriteTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/WildcardConverterWriteTest.java new file mode 100644 index 000000000..c9a7c1a6e --- /dev/null +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/WildcardConverterWriteTest.java @@ -0,0 +1,220 @@ +/* + * 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.converter; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import org.apache.fesod.sheet.ExcelWriter; +import org.apache.fesod.sheet.FesodSheet; +import org.apache.fesod.sheet.converters.Converter; +import org.apache.fesod.sheet.converters.ConverterKeyBuild; +import org.apache.fesod.sheet.enums.CellDataTypeEnum; +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.support.ExcelTypeEnum; +import org.apache.fesod.sheet.testkit.base.AbstractExcelTest; +import org.apache.fesod.sheet.write.builder.ExcelWriterSheetBuilder; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.usermodel.WorkbookFactory; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * Tests for custom converters registered with {@code supportExcelTypeKey() == null} (the wildcard key). + * + *
The wildcard key {@code (JavaType, null)} is only reachable from the xlsx lookup, which uses
+ * {@code null} as the target cell data type. CSV mode forces the target to {@link CellDataTypeEnum#STRING},
+ * so a wildcard-only registration is silently shadowed by the built-in string converter. See issues
+ * #1045 and #1056.
+ */
+public class WildcardConverterWriteTest extends AbstractExcelTest {
+
+ @Test
+ void xlsxWildcardConverterIsApplied() throws Exception {
+ File file = new File(tempDir, "wildcard-converter.xlsx");
+ FesodSheet.write(file)
+ .excelType(ExcelTypeEnum.XLSX)
+ .registerConverter(new BooleanYesNoConverter())
+ .sheet()
+ .doWrite(booleans(Boolean.TRUE, Boolean.FALSE));
+
+ try (Workbook workbook = WorkbookFactory.create(file)) {
+ Assertions.assertEquals(
+ "YES", workbook.getSheetAt(0).getRow(0).getCell(0).getStringCellValue());
+ Assertions.assertEquals(
+ "NO", workbook.getSheetAt(0).getRow(1).getCell(0).getStringCellValue());
+ }
+ }
+
+ @Test
+ void csvWildcardConverterIsApplied() throws Exception {
+ File file = new File(tempDir, "wildcard-converter.csv");
+ FesodSheet.write(file)
+ .excelType(ExcelTypeEnum.CSV)
+ .charset(StandardCharsets.UTF_8)
+ .registerConverter(new BooleanYesNoConverter())
+ .sheet()
+ .doWrite(booleans(Boolean.TRUE, Boolean.FALSE));
+
+ String csvContent = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8);
+ Assertions.assertTrue(
+ csvContent.contains("YES"), "CSV should apply the custom converter, but was: " + csvContent);
+ Assertions.assertTrue(
+ csvContent.contains("NO"), "CSV should apply the custom converter, but was: " + csvContent);
+ }
+
+ @Test
+ void csvWithoutCustomConvertersIsUnchanged() throws Exception {
+ File file = new File(tempDir, "wildcard-converter-default.csv");
+ FesodSheet.write(file)
+ .excelType(ExcelTypeEnum.CSV)
+ .charset(StandardCharsets.UTF_8)
+ .sheet()
+ .doWrite(booleans(Boolean.TRUE, Boolean.FALSE));
+
+ String csvContent = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8);
+ Assertions.assertTrue(
+ csvContent.contains("true"), "Default CSV output should be unchanged, but was: " + csvContent);
+ Assertions.assertTrue(
+ csvContent.contains("false"), "Default CSV output should be unchanged, but was: " + csvContent);
+ }
+
+ @Test
+ void csvExplicitStringKeyConverterIsUnaffected() throws Exception {
+ File file = new File(tempDir, "wildcard-converter-explicit.csv");
+ FesodSheet.write(file)
+ .excelType(ExcelTypeEnum.CSV)
+ .charset(StandardCharsets.UTF_8)
+ .registerConverter(new BooleanExplicitStringConverter())
+ .sheet()
+ .doWrite(booleans(Boolean.TRUE));
+
+ String csvContent = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8);
+ Assertions.assertTrue(
+ csvContent.contains("explicit-yes"),
+ "Explicit STRING-key converter should win, but was: " + csvContent);
+ }
+
+ @Test
+ void csvExplicitStringKeyRegisteredBeforeWildcardStillWins() throws Exception {
+ File file = new File(tempDir, "wildcard-converter-explicit-first.csv");
+ FesodSheet.write(file)
+ .excelType(ExcelTypeEnum.CSV)
+ .charset(StandardCharsets.UTF_8)
+ .registerConverter(new BooleanExplicitStringConverter())
+ .registerConverter(new BooleanYesNoConverter())
+ .sheet()
+ .doWrite(booleans(Boolean.TRUE, Boolean.FALSE));
+
+ String csvContent = new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8);
+ Assertions.assertTrue(
+ csvContent.contains("explicit-yes"),
+ "Explicit STRING-key converter registered before the wildcard should still win, but was: "
+ + csvContent);
+ }
+
+ @Test
+ void wildcardConverterIsRegisteredUnderBothKeysInWorkbookAndSheetHolders() throws Exception {
+ File file = new File(tempDir, "wildcard-converter-map.csv");
+ BooleanYesNoConverter converter = new BooleanYesNoConverter();
+ try (ExcelWriter excelWriter = FesodSheet.write(file)
+ .excelType(ExcelTypeEnum.CSV)
+ .registerConverter(converter)
+ .build()) {
+ excelWriter.write(
+ booleans(Boolean.TRUE),
+ new ExcelWriterSheetBuilder().sheetNo(0).build());
+ Map> booleans(Boolean... values) {
+ List
> rows = new java.util.ArrayList<>();
+ for (Boolean value : values) {
+ rows.add(Collections.singletonList(value));
+ }
+ return rows;
+ }
+
+ /** Custom converter returning {@code null} from {@code supportExcelTypeKey()} (the wildcard key). */
+ public static class BooleanYesNoConverter implements Converter