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 @@ -26,15 +26,19 @@
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;
import lombok.Setter;
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;
Expand Down Expand Up @@ -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<ConverterKey> 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);
}
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<StringWriteData> dataList() {
List<StringWriteData> 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<BooleanReadData> 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<BooleanReadData> 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<Boolean> {

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<BooleanReadData> 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<IndexStringReadData> 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<String> {

@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<IndexStringReadData> {

@Override
public void invoke(IndexStringReadData data, AnalysisContext context) {}

@Override
public void doAfterAllAnalysed(AnalysisContext context) {}
}

public static class AlwaysFalseReadConverter implements Converter<Boolean> {

@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<BooleanReadData> {

@Override
public void invoke(BooleanReadData data, AnalysisContext context) {}

@Override
public void doAfterAllAnalysed(AnalysisContext context) {}
}
}
12 changes: 12 additions & 0 deletions website/docs/sheet/read/converter.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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`),而已经被显式注册的转换器占用的键则保持不变 — 同样是显式注册优先。

**一句话总结:** 一次通配注册即可覆盖所有格式与所有单元格类型;显式注册永远优先于通配注册;用户注册永远优先于内置转换器。
Loading