diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/CellDataConverterRegistry.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/CellDataConverterRegistry.java new file mode 100644 index 0000000000..dbb4b27fc0 --- /dev/null +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/CellDataConverterRegistry.java @@ -0,0 +1,316 @@ +/* + * 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.converters; + +import java.util.Deque; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.Validate; +import org.apache.fesod.sheet.enums.CellDataTypeEnum; + +/** + * Registry for managing converters that transform between Java objects and spreadsheet cell data. + */ +public class CellDataConverterRegistry { + + /** + * The parent converter registry, or {@code null} if this is the root registry. + */ + private final CellDataConverterRegistry parent; + + private static final Map> TO_STRING_CONVERTERS = + DefaultConverterLoader.loadDefaultWriteStringConverter(); + + private final Map>> customWriteConverters = + new LinkedHashMap<>(); + private final Map>> customReadConverters = + new LinkedHashMap<>(); + + private final Map> defaultConverters = new LinkedHashMap<>(); + + private final Map>> writeConvertersCache = + new ConcurrentHashMap<>(40); + private final Map>> readConvertersCache = + new ConcurrentHashMap<>(40); + + public CellDataConverterRegistry() { + this(null); + } + + public CellDataConverterRegistry(CellDataConverterRegistry parent) { + this.parent = parent; + } + + /** + * Registers the default converters. + */ + public void addDefaultConverters(Map> map) { + defaultConverters.putAll(map); + } + + /** + * Registers a custom write converter. When multiple converters of the same type exist, + * the converter registered later takes precedence in matching. + */ + public void addCustomWriteConverter(WriteConverter converter) { + Validate.notNull(converter, "WriteConverter must not be null"); + Validate.notNull( + converter.supportJavaTypeKey(), + "WriteConverter [" + converter.getClass().getName() + "] must explicitly specify supportJavaTypeKey()"); + + ConverterKeyBuild.ConverterKey key = ConverterKeyBuild.buildKey(converter.supportJavaTypeKey()); + customWriteConverters.computeIfAbsent(key, k -> new LinkedList<>()).addFirst(converter); + + writeConvertersCache.clear(); + } + + /** + * Registers a custom read converter. When multiple converters of the same type exist, + * the converter registered later takes precedence in matching. + */ + public void addCustomReadConverter(ReadConverter converter) { + Validate.notNull(converter, "ReadConverter must not be null"); + Validate.notNull( + converter.supportJavaTypeKey(), + "ReadConverter [" + converter.getClass().getName() + "] must explicitly specify supportJavaTypeKey()"); + Validate.notNull( + converter.supportExcelTypeKey(), + "ReadConverter [" + converter.getClass().getName() + "] must explicitly specify supportExcelTypeKey()"); + + ConverterKeyBuild.ConverterKey key = + ConverterKeyBuild.buildKey(converter.supportJavaTypeKey(), converter.supportExcelTypeKey()); + customReadConverters.computeIfAbsent(key, k -> new LinkedList<>()).addFirst(converter); + + readConvertersCache.clear(); + } + + /** + * Retrieves the global write converter for the specified Java type. + * + * @param sourceType the source Java type + * @return the matched write converter or {@code null} if not found + */ + public WriteConverter findWriteConverter(Class sourceType) { + return findWriteConverter(sourceType, null); + } + + /** + * Retrieves the write converter for the specified Java type and target column index. + * + * @param sourceType the source Java type + * @param column optional target cell column index (0-based) + * @return the matched write converter or {@code null} if not found + */ + public WriteConverter findWriteConverter(Class sourceType, Integer column) { + return findWriteConverter(sourceType, column, false); + } + + /** + * Retrieves the write converter for the specified Java type and target column index, and default fallback strategy. + * + * @param sourceType the source Java type + * @param column optional target cell column index (0-based) + * @param defaultToString {@code true} to default to the string converter set when no custom converter matches + * @return the matched write converter or {@code null} if not found + */ + public WriteConverter findWriteConverter(Class sourceType, Integer column, boolean defaultToString) { + if (sourceType == null) { + return null; + } + + WriteCacheKey cacheKey = new WriteCacheKey(sourceType, column, defaultToString); + ConverterHolder> cachedHolder = writeConvertersCache.get(cacheKey); + if (cachedHolder != null) { + return cachedHolder.isNull() ? null : cachedHolder.converter; + } + + WriteConverter result = resolveWriteConverter(sourceType, column, defaultToString); + writeConvertersCache.put(cacheKey, new ConverterHolder<>(result)); + return result; + } + + /** + * Retrieves the write converter. + * + *

+ * Priority: current registry (column‑bound converters take precedence over global converters), + * then parent registry, and finally default converters. + *

+ * + * @param sourceType the source Java type + * @param column optional target cell column index (0-based) + * @param defaultToString {@code true} to default to the string converter set when no custom converter matches + * @return the matched write converter or {@code null} if not found + */ + private WriteConverter resolveWriteConverter(Class sourceType, Integer column, boolean defaultToString) { + ConverterKeyBuild.ConverterKey key = ConverterKeyBuild.buildKey(sourceType); + + WriteConverter localSelected = selectConverter(customWriteConverters.get(key), column); + + if (localSelected != null) { + return localSelected; + } + + if (parent != null) { + return parent.findWriteConverter(sourceType, column, defaultToString); + } + return defaultToString ? TO_STRING_CONVERTERS.get(key) : defaultConverters.get(key); + } + + /** + * Retrieves the read converter for the specified source cell type and target Java type. + * + * @param targetType the target Java type + * @param sourceType the source spreadsheet cell data type + * @return the matched read converter or {@code null} if not found + */ + public ReadConverter findReadConverter(Class targetType, CellDataTypeEnum sourceType) { + return findReadConverter(targetType, sourceType, null); + } + + /** + * Retrieves the read converter for the specified source cell type, target Java type and target column index. + * + * @param targetType the target Java type + * @param sourceType the source spreadsheet cell data type + * @param column optional target cell column index (0-based) + * @return the matched read converter or {@code null} if not found + */ + public ReadConverter findReadConverter(Class targetType, CellDataTypeEnum sourceType, Integer column) { + if (targetType == null || sourceType == null) { + return null; + } + + ReadCacheKey cacheKey = new ReadCacheKey(targetType, sourceType, column); + ConverterHolder> cachedHolder = readConvertersCache.get(cacheKey); + if (cachedHolder != null) { + return cachedHolder.isNull() ? null : cachedHolder.converter; + } + + ReadConverter result = resolveReadConverter(targetType, sourceType, column); + readConvertersCache.put(cacheKey, new ConverterHolder<>(result)); + return result; + } + + /** + * Retrieves the read converter. + * + *

+ * Priority: current registry (column‑bound converters take precedence over global converters), + * then parent registry, and finally default converters. + *

+ * + * @param targetType the target Java type + * @param sourceType the source spreadsheet cell data type + * @param column optional target cell column index (0-based) + * @return the matched read converter or {@code null} if not found + */ + private ReadConverter resolveReadConverter(Class targetType, CellDataTypeEnum sourceType, Integer column) { + ConverterKeyBuild.ConverterKey key = ConverterKeyBuild.buildKey(targetType, sourceType); + + ReadConverter localSelected = selectConverter(customReadConverters.get(key), column); + + if (localSelected != null) { + return localSelected; + } + + if (parent != null) { + return parent.findReadConverter(targetType, sourceType, column); + } + return defaultConverters.get(key); + } + + /** + * Selects the best-matching (read or write) converter from a candidates. + * + *

+ * Priority: column‑bound converter take precedence over global converter. + *

+ */ + private T selectConverter(Deque candidates, Integer column) { + if (CollectionUtils.isEmpty(candidates)) { + return null; + } + + T globalMatched = null; + for (T candidate : candidates) { + if (candidate instanceof ColumnBinding) { + if (isColumnMatched((ColumnBinding) candidate, column)) { + return candidate; + } + continue; + } + + if (globalMatched == null) { + if (column == null) { + return candidate; + } + globalMatched = candidate; + } + } + return globalMatched; + } + + /** + * Determines whether the given column binding matches the specified column index. + */ + private boolean isColumnMatched(ColumnBinding columnBinding, Integer column) { + if (column == null) { + return false; + } + Set columnIndexes = columnBinding.columnIndexes(); + return CollectionUtils.isNotEmpty(columnIndexes) && columnIndexes.contains(column); + } + + @EqualsAndHashCode + @AllArgsConstructor + private static class WriteCacheKey { + private final Class javaType; + private final Integer column; + private final boolean defaultToString; + } + + @EqualsAndHashCode + @AllArgsConstructor + private static class ReadCacheKey { + private final Class javaType; + private final CellDataTypeEnum cellDataType; + private final Integer column; + } + + @EqualsAndHashCode + private static class ConverterHolder { + private final T converter; + + ConverterHolder(T converter) { + this.converter = converter; + } + + boolean isNull() { + return converter == null; + } + } +} diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/ColumnBinding.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/ColumnBinding.java new file mode 100644 index 0000000000..466d942a6a --- /dev/null +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/ColumnBinding.java @@ -0,0 +1,52 @@ +/* + * 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.converters; + +import java.util.Collections; +import java.util.Set; + +/** + * Column binding contract for converters that apply only to specific spreadsheet column(s). + *

+ * A converter that does not implement this interface is considered a global converter. + * Implementations typically need to override either {@link #columnIndex()} for a single column + * or {@link #columnIndexes()} for multiple columns. + *

+ * WARNING: Providing no valid column index ({@code null} or an empty set) renders this converter + * completely ineffective, it will match no column and will not act as a global fallback. + * Do not implement this interface if a global converter is intended. + */ +public interface ColumnBinding { + + /** + * The column index (0-based) to which this converter is bound. + */ + default Integer columnIndex() { + return null; + } + + /** + * The column indexes (0-based) to which this converter is bound. + */ + default Set columnIndexes() { + Integer result = columnIndex(); + return result == null ? Collections.emptySet() : Collections.singleton(result); + } +} 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 affb88631e..d88c2dff8a 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 @@ -26,88 +26,22 @@ package org.apache.fesod.sheet.converters; import org.apache.fesod.sheet.enums.CellDataTypeEnum; -import org.apache.fesod.sheet.metadata.GlobalConfiguration; -import org.apache.fesod.sheet.metadata.data.ReadCellData; -import org.apache.fesod.sheet.metadata.data.WriteCellData; -import org.apache.fesod.sheet.metadata.property.ExcelContentProperty; /** - * Convert between Java objects and excel objects + * Converter combining both reading and writing of spreadsheet cell data. + * + * @see ReadConverter + * @see WriteConverter */ -public interface Converter { +public interface Converter extends ReadConverter, WriteConverter { - /** - * Back to object types in Java - * - * @return Support for Java class - */ + @Override default Class supportJavaTypeKey() { throw new UnsupportedOperationException("The current operation is not supported by the current converter."); } - /** - * Back to object enum in excel - * - * @return Support for {@link CellDataTypeEnum} - */ + @Override default CellDataTypeEnum supportExcelTypeKey() { throw new UnsupportedOperationException("The current operation is not supported by the current converter."); } - - /** - * Convert excel objects to Java objects - * - * @param cellData Excel cell data.NotNull. - * @param contentProperty Content property.Nullable. - * @param globalConfiguration Global configuration.NotNull. - * @return Data to put into a Java object - * @throws Exception Exception. - */ - default T convertToJavaData( - ReadCellData cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) - throws Exception { - throw new UnsupportedOperationException("The current operation is not supported by the current converter."); - } - - /** - * Convert excel objects to Java objects - * - * @param context read converter context - * @return Data to put into a Java object - * @throws Exception Exception. - */ - default T convertToJavaData(ReadConverterContext context) throws Exception { - return convertToJavaData( - context.getReadCellData(), - context.getContentProperty(), - context.getAnalysisContext().currentReadHolder().globalConfiguration()); - } - - /** - * Convert Java objects to excel objects - * - * @param value Java Data.NotNull. - * @param contentProperty Content property.Nullable. - * @param globalConfiguration Global configuration.NotNull. - * @return Data to put into a Excel - * @throws Exception Exception. - */ - default WriteCellData convertToExcelData( - T value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception { - throw new UnsupportedOperationException("The current operation is not supported by the current converter."); - } - - /** - * Convert Java objects to excel objects - * - * @param context write context - * @return Data to put into a Excel - * @throws Exception Exception. - */ - default WriteCellData convertToExcelData(WriteConverterContext context) throws Exception { - return convertToExcelData( - context.getValue(), - context.getContentProperty(), - context.getWriteContext().currentWriteHolder().globalConfiguration()); - } } diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/DefaultConverterLoader.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/DefaultConverterLoader.java index 95a247442f..f337284224 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/DefaultConverterLoader.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/DefaultConverterLoader.java @@ -99,6 +99,7 @@ */ public class DefaultConverterLoader { private static Map> defaultWriteConverter; + private static Map> defaultWriteStringConverter; private static Map> allConverter; static { @@ -205,8 +206,10 @@ private static void initDefaultWriteConverter() { putWriteConverter(new BoxingByteArrayImageConverter()); putWriteConverter(new UrlImageConverter()); putWriteConverter(new UUIDStringConverter()); + defaultWriteConverter = Collections.unmodifiableMap(defaultWriteConverter); // In some cases, it must be converted to string + defaultWriteStringConverter = MapUtils.newHashMapWithExpectedSize(40); putWriteStringConverter(new BigDecimalStringConverter()); putWriteStringConverter(new BigIntegerStringConverter()); putWriteStringConverter(new BooleanStringConverter()); @@ -228,7 +231,7 @@ private static void initDefaultWriteConverter() { putWriteStringConverter(new StringStringConverter()); putWriteStringConverter(new UUIDStringConverter()); putWriteStringConverter(new YearStringConverter()); - defaultWriteConverter = Collections.unmodifiableMap(defaultWriteConverter); + defaultWriteStringConverter = Collections.unmodifiableMap(defaultWriteStringConverter); } /** @@ -249,13 +252,19 @@ public static Map> copyDefaultWriteConverter() { return new HashMap<>(loadDefaultWriteConverter()); } + /** + * Returns default write converter for string (without cellDataTypeEnum). + */ + public static Map> loadDefaultWriteStringConverter() { + return defaultWriteStringConverter; + } + private static void putWriteConverter(Converter converter) { defaultWriteConverter.put(ConverterKeyBuild.buildKey(converter.supportJavaTypeKey()), converter); } private static void putWriteStringConverter(Converter converter) { - defaultWriteConverter.put( - ConverterKeyBuild.buildKey(converter.supportJavaTypeKey(), converter.supportExcelTypeKey()), converter); + defaultWriteStringConverter.put(ConverterKeyBuild.buildKey(converter.supportJavaTypeKey()), converter); } /** diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/ReadConverter.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/ReadConverter.java new file mode 100644 index 0000000000..18e2039f6c --- /dev/null +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/ReadConverter.java @@ -0,0 +1,72 @@ +/* + * 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.converters; + +import org.apache.fesod.sheet.enums.CellDataTypeEnum; +import org.apache.fesod.sheet.metadata.GlobalConfiguration; +import org.apache.fesod.sheet.metadata.data.ReadCellData; +import org.apache.fesod.sheet.metadata.property.ExcelContentProperty; + +/** + * Converter for transforming spreadsheet cell data into Java objects. + * + * @param the target Java type + */ +public interface ReadConverter { + + /** + * The Java type supported by this converter (non-null). + */ + Class supportJavaTypeKey(); + + /** + * The spreadsheet cell type supported by this converter (non-null). + */ + CellDataTypeEnum supportExcelTypeKey(); + + /** + * Convert the given spreadsheet cell data into a Java value. + * + * @param cellData the source cell data (non-null) + * @param contentProperty optional content property (nullable) + * @param globalConfiguration global configuration (non-null) + * @return the converted Java value + * @throws Exception if conversion fails + */ + default T convertToJavaData( + ReadCellData cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) + throws Exception { + throw new UnsupportedOperationException("The current operation is not supported by the current converter."); + } + + /** + * Convert the spreadsheet cell data into a Java value using the given context. + * + * @param context the read conversion context + * @return the converted Java value + * @throws Exception if conversion fails + */ + default T convertToJavaData(ReadConverterContext context) throws Exception { + return convertToJavaData( + context.getReadCellData(), + context.getContentProperty(), + context.getAnalysisContext().currentReadHolder().globalConfiguration()); + } +} diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/WriteConverter.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/WriteConverter.java new file mode 100644 index 0000000000..c1659e4190 --- /dev/null +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/converters/WriteConverter.java @@ -0,0 +1,65 @@ +/* + * 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.converters; + +import org.apache.fesod.sheet.metadata.GlobalConfiguration; +import org.apache.fesod.sheet.metadata.data.WriteCellData; +import org.apache.fesod.sheet.metadata.property.ExcelContentProperty; + +/** + * Converter for transforming Java objects into spreadsheet cell data. + * + * @param the source Java type + */ +public interface WriteConverter { + + /** + * The Java type supported by this converter (non-null). + */ + Class supportJavaTypeKey(); + + /** + * Convert the given Java value into spreadsheet cell data. + * + * @param value the source Java value (non-null) + * @param contentProperty optional content property (nullable) + * @param globalConfiguration global configuration (non-null) + * @return the converted spreadsheet cell data + * @throws Exception if conversion fails + */ + default WriteCellData convertToExcelData( + T value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception { + throw new UnsupportedOperationException("The current operation is not supported by the current converter."); + } + + /** + * Convert the Java value into spreadsheet cell data using the given context. + * + * @param context the write conversion context + * @return the converted spreadsheet cell data + * @throws Exception if conversion fails + */ + default WriteCellData convertToExcelData(WriteConverterContext context) throws Exception { + return convertToExcelData( + context.getValue(), + context.getContentProperty(), + context.getWriteContext().currentWriteHolder().globalConfiguration()); + } +} diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/AbstractHolder.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/AbstractHolder.java index 35b63aff41..dd501b7abd 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/AbstractHolder.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/AbstractHolder.java @@ -26,13 +26,13 @@ package org.apache.fesod.sheet.metadata; import java.util.List; -import java.util.Map; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; -import org.apache.fesod.sheet.converters.Converter; -import org.apache.fesod.sheet.converters.ConverterKeyBuild; +import org.apache.fesod.sheet.converters.CellDataConverterRegistry; +import org.apache.fesod.sheet.converters.DefaultConverterLoader; +import org.apache.fesod.sheet.support.ExcelTypeEnum; /** * Write/read holder @@ -60,76 +60,92 @@ public abstract class AbstractHolder implements ConfigurationHolder { * Some global variables */ private GlobalConfiguration globalConfiguration; - /** - *

- * Read key: - *

- * Write key: - */ - private Map> converterMap; - public AbstractHolder(BasicParameter basicParameter, AbstractHolder prentAbstractHolder) { + private CellDataConverterRegistry converterRegistry; + + public AbstractHolder(BasicParameter basicParameter, AbstractHolder parentAbstractHolder) { this.newInitialization = Boolean.TRUE; - if (basicParameter.getHead() == null && basicParameter.getClazz() == null && prentAbstractHolder != null) { - this.head = prentAbstractHolder.getHead(); + if (basicParameter.getHead() == null && basicParameter.getClazz() == null && parentAbstractHolder != null) { + this.head = parentAbstractHolder.getHead(); } else { this.head = basicParameter.getHead(); } - if (basicParameter.getHead() == null && basicParameter.getClazz() == null && prentAbstractHolder != null) { - this.clazz = prentAbstractHolder.getClazz(); + if (basicParameter.getHead() == null && basicParameter.getClazz() == null && parentAbstractHolder != null) { + this.clazz = parentAbstractHolder.getClazz(); } else { this.clazz = basicParameter.getClazz(); } this.globalConfiguration = new GlobalConfiguration(); if (basicParameter.getAutoTrim() == null) { - if (prentAbstractHolder != null) { + if (parentAbstractHolder != null) { globalConfiguration.setAutoTrim( - prentAbstractHolder.getGlobalConfiguration().getAutoTrim()); + parentAbstractHolder.getGlobalConfiguration().getAutoTrim()); } } else { globalConfiguration.setAutoTrim(basicParameter.getAutoTrim()); } if (basicParameter.getAutoStrip() == null) { - if (prentAbstractHolder != null) { + if (parentAbstractHolder != null) { globalConfiguration.setAutoStrip( - prentAbstractHolder.getGlobalConfiguration().getAutoStrip()); + parentAbstractHolder.getGlobalConfiguration().getAutoStrip()); } } else { globalConfiguration.setAutoStrip(basicParameter.getAutoStrip()); } if (basicParameter.getUse1904windowing() == null) { - if (prentAbstractHolder != null) { + if (parentAbstractHolder != null) { globalConfiguration.setUse1904windowing( - prentAbstractHolder.getGlobalConfiguration().getUse1904windowing()); + parentAbstractHolder.getGlobalConfiguration().getUse1904windowing()); } } else { globalConfiguration.setUse1904windowing(basicParameter.getUse1904windowing()); } if (basicParameter.getLocale() == null) { - if (prentAbstractHolder != null) { + if (parentAbstractHolder != null) { globalConfiguration.setLocale( - prentAbstractHolder.getGlobalConfiguration().getLocale()); + parentAbstractHolder.getGlobalConfiguration().getLocale()); } } else { globalConfiguration.setLocale(basicParameter.getLocale()); } if (basicParameter.getFiledCacheLocation() == null) { - if (prentAbstractHolder != null) { + if (parentAbstractHolder != null) { globalConfiguration.setFiledCacheLocation( - prentAbstractHolder.getGlobalConfiguration().getFiledCacheLocation()); + parentAbstractHolder.getGlobalConfiguration().getFiledCacheLocation()); } } else { globalConfiguration.setFiledCacheLocation(basicParameter.getFiledCacheLocation()); } + + if (parentAbstractHolder == null) { + this.converterRegistry = new CellDataConverterRegistry(); + } else { + this.converterRegistry = new CellDataConverterRegistry(parentAbstractHolder.getConverterRegistry()); + } + } + + /** + * register default converters + */ + protected void initDefaultConverters(ExcelTypeEnum excelTypeEnum, boolean readable) { + if (readable) { + getConverterRegistry().addDefaultConverters(DefaultConverterLoader.loadDefaultReadConverter()); + return; + } + if (ExcelTypeEnum.CSV.equals(excelTypeEnum)) { + getConverterRegistry().addDefaultConverters(DefaultConverterLoader.loadDefaultWriteStringConverter()); + } else { + getConverterRegistry().addDefaultConverters(DefaultConverterLoader.loadDefaultWriteConverter()); + } } @Override - public Map> converterMap() { - return getConverterMap(); + public CellDataConverterRegistry converterRegistry() { + return getConverterRegistry(); } @Override diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/AbstractParameterBuilder.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/AbstractParameterBuilder.java index de25c3c088..440db28ada 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/AbstractParameterBuilder.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/AbstractParameterBuilder.java @@ -30,7 +30,6 @@ import java.util.Locale; import java.util.Objects; import java.util.function.Consumer; -import org.apache.fesod.common.util.ListUtils; import org.apache.fesod.sheet.converters.Converter; import org.apache.fesod.sheet.enums.CacheLocationEnum; @@ -119,13 +118,7 @@ public T headIfNotNull(Class clazz) { * @param converter * @return */ - public T registerConverter(Converter converter) { - if (parameter().getCustomConverterList() == null) { - parameter().setCustomConverterList(ListUtils.newArrayList()); - } - parameter().getCustomConverterList().add(converter); - return self(); - } + public abstract T registerConverter(Converter converter); /** * true if date uses 1904 windowing, or false if using 1900 date windowing. diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/BasicParameter.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/BasicParameter.java index 19a2bece08..be878aaaec 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/BasicParameter.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/BasicParameter.java @@ -30,7 +30,6 @@ import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; -import org.apache.fesod.sheet.converters.Converter; import org.apache.fesod.sheet.enums.CacheLocationEnum; /** @@ -50,10 +49,6 @@ public class BasicParameter { * You can only choose one of the {@link BasicParameter#head} and {@link BasicParameter#clazz} */ private Class clazz; - /** - * Custom type conversions override the default - */ - private List> customConverterList; /** * Automatic trim includes sheet name and content */ diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/ConfigurationHolder.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/ConfigurationHolder.java index 79d19a05a3..dc6c3a3762 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/ConfigurationHolder.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/metadata/ConfigurationHolder.java @@ -25,9 +25,7 @@ package org.apache.fesod.sheet.metadata; -import java.util.Map; -import org.apache.fesod.sheet.converters.Converter; -import org.apache.fesod.sheet.converters.ConverterKeyBuild; +import org.apache.fesod.sheet.converters.CellDataConverterRegistry; /** * Get the corresponding holder @@ -50,10 +48,5 @@ public interface ConfigurationHolder extends Holder { */ GlobalConfiguration globalConfiguration(); - /** - * What converter does the currently operated cell need to execute - * - * @return Converter - */ - Map> converterMap(); + CellDataConverterRegistry converterRegistry(); } diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/read/builder/AbstractExcelReaderParameterBuilder.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/read/builder/AbstractExcelReaderParameterBuilder.java index d1e32586de..295de65cd4 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/read/builder/AbstractExcelReaderParameterBuilder.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/read/builder/AbstractExcelReaderParameterBuilder.java @@ -27,6 +27,8 @@ import java.util.Objects; import org.apache.fesod.common.util.ListUtils; +import org.apache.fesod.sheet.converters.Converter; +import org.apache.fesod.sheet.converters.ReadConverter; import org.apache.fesod.sheet.metadata.AbstractParameterBuilder; import org.apache.fesod.sheet.read.listener.ReadListener; import org.apache.fesod.sheet.read.metadata.ReadBasicParameter; @@ -93,4 +95,25 @@ public T registerReadListenerIfNotNull(ReadListener readListener) { } return self(); } + + /** + * Registers a custom converter. + * + * @see #registerReadConverter(ReadConverter) + */ + @Override + public T registerConverter(Converter converter) { + return registerReadConverter(converter); + } + + /** + * Registers a custom read converter. + */ + public T registerReadConverter(ReadConverter converter) { + if (parameter().getCustomConverterList() == null) { + parameter().setCustomConverterList(ListUtils.newArrayList()); + } + parameter().getCustomConverterList().add(converter); + return self(); + } } diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/read/listener/ModelBuildEventListener.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/read/listener/ModelBuildEventListener.java index d6a4f19f67..2a0f1b37e5 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/read/listener/ModelBuildEventListener.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/read/listener/ModelBuildEventListener.java @@ -83,7 +83,7 @@ private Object buildNoModel( cellData, null, null, - readSheetHolder.converterMap(), + readSheetHolder.converterRegistry(), context, context.readRowHolder().getRowIndex(), key)); @@ -143,7 +143,7 @@ private ReadCellData convertReadCellData( ReadCellData.class, classGeneric, null, - readSheetHolder.converterMap(), + readSheetHolder.converterRegistry(), context, context.readRowHolder().getRowIndex(), columnIndex); @@ -193,7 +193,7 @@ private Object buildUserModel( readSheetHolder.excelReadHeadProperty().getHeadClazz(), fieldName, readSheetHolder), - readSheetHolder.converterMap(), + readSheetHolder.converterRegistry(), context, context.readRowHolder().getRowIndex(), index); diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/read/metadata/ReadBasicParameter.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/read/metadata/ReadBasicParameter.java index dacebf8805..acf775752c 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/read/metadata/ReadBasicParameter.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/read/metadata/ReadBasicParameter.java @@ -30,6 +30,7 @@ import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; +import org.apache.fesod.sheet.converters.ReadConverter; import org.apache.fesod.sheet.metadata.BasicParameter; import org.apache.fesod.sheet.read.listener.ReadListener; @@ -57,8 +58,13 @@ public class ReadBasicParameter extends BasicParameter { * Custom type listener run after default */ private List> customReadListenerList; + /** + * Custom type converters for reading. + */ + private List> customConverterList; public ReadBasicParameter() { customReadListenerList = new ArrayList<>(); + customConverterList = new ArrayList<>(); } } 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 f7b58be882..83d9fc16d5 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 @@ -25,16 +25,13 @@ package org.apache.fesod.sheet.read.metadata.holder; -import java.util.HashMap; import java.util.List; 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.DefaultConverterLoader; +import org.apache.fesod.sheet.converters.ReadConverter; import org.apache.fesod.sheet.enums.HolderEnum; import org.apache.fesod.sheet.metadata.AbstractHolder; import org.apache.fesod.sheet.read.listener.ModelBuildEventListener; @@ -119,19 +116,11 @@ public AbstractReadHolder(ReadBasicParameter readBasicParameter, AbstractReadHol this.readListenerList.addAll(readBasicParameter.getCustomReadListenerList()); } - if (parentAbstractReadHolder == null) { - setConverterMap(DefaultConverterLoader.copyDefaultReadConverter()); - } else { - setConverterMap(new HashMap<>(parentAbstractReadHolder.getConverterMap())); - } + // register converters (custom) if (readBasicParameter.getCustomConverterList() != null && !readBasicParameter.getCustomConverterList().isEmpty()) { - for (Converter converter : readBasicParameter.getCustomConverterList()) { - getConverterMap() - .put( - ConverterKeyBuild.buildKey( - converter.supportJavaTypeKey(), converter.supportExcelTypeKey()), - converter); + for (ReadConverter converter : readBasicParameter.getCustomConverterList()) { + getConverterRegistry().addCustomReadConverter(converter); } } } diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/read/metadata/holder/csv/CsvReadWorkbookHolder.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/read/metadata/holder/csv/CsvReadWorkbookHolder.java index 35cc5c1d2c..424acb6ebc 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/read/metadata/holder/csv/CsvReadWorkbookHolder.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/read/metadata/holder/csv/CsvReadWorkbookHolder.java @@ -50,6 +50,9 @@ public class CsvReadWorkbookHolder extends ReadWorkbookHolder { public CsvReadWorkbookHolder(ReadWorkbook readWorkbook) { super(readWorkbook); setExcelType(ExcelTypeEnum.CSV); + + initDefaultConverters(ExcelTypeEnum.CSV, true); + this.csvFormat = readWorkbook.getCsvFormat() == null ? CSVFormat.DEFAULT : readWorkbook.getCsvFormat(); } } diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/read/metadata/holder/xls/XlsReadWorkbookHolder.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/read/metadata/holder/xls/XlsReadWorkbookHolder.java index ea97a906c8..f1616b4ee5 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/read/metadata/holder/xls/XlsReadWorkbookHolder.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/read/metadata/holder/xls/XlsReadWorkbookHolder.java @@ -88,6 +88,9 @@ public XlsReadWorkbookHolder(ReadWorkbook readWorkbook) { this.boundSheetRecordList = new ArrayList(); this.needReadSheet = Boolean.TRUE; setExcelType(ExcelTypeEnum.XLS); + + initDefaultConverters(ExcelTypeEnum.XLS, true); + if (getGlobalConfiguration().getUse1904windowing() == null) { getGlobalConfiguration().setUse1904windowing(Boolean.FALSE); } diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/read/metadata/holder/xlsx/XlsxReadWorkbookHolder.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/read/metadata/holder/xlsx/XlsxReadWorkbookHolder.java index d33766ee9d..d8fba8adf1 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/read/metadata/holder/xlsx/XlsxReadWorkbookHolder.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/read/metadata/holder/xlsx/XlsxReadWorkbookHolder.java @@ -83,6 +83,9 @@ public XlsxReadWorkbookHolder(ReadWorkbook readWorkbook) { super(readWorkbook); this.saxParserFactoryName = readWorkbook.getXlsxSAXParserFactoryName(); setExcelType(ExcelTypeEnum.XLSX); + + initDefaultConverters(ExcelTypeEnum.XLSX, true); + dataFormatDataCache = MapUtils.newHashMap(); } diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/ConverterUtils.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/ConverterUtils.java index 007c3e6a8b..a1942ef309 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/ConverterUtils.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/ConverterUtils.java @@ -31,10 +31,9 @@ import java.util.Map; import org.apache.fesod.common.util.MapUtils; import org.apache.fesod.sheet.context.AnalysisContext; -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.CellDataConverterRegistry; import org.apache.fesod.sheet.converters.NullableObjectConverter; +import org.apache.fesod.sheet.converters.ReadConverter; import org.apache.fesod.sheet.converters.ReadConverterContext; import org.apache.fesod.sheet.enums.CellDataTypeEnum; import org.apache.fesod.sheet.exception.ExcelDataConvertException; @@ -77,8 +76,8 @@ public static Map convertToStringMap( stringMap.put(key, null); continue; } - Converter converter = - readSheetHolder.converterMap().get(ConverterKeyBuild.buildKey(String.class, cellData.getType())); + ReadConverter converter = + readSheetHolder.converterRegistry().findReadConverter(String.class, cellData.getType(), key); if (converter == null) { throw new ExcelDataConvertException( context.readRowHolder().getRowIndex(), @@ -109,7 +108,7 @@ public static Map convertToStringMap( * @param cellData * @param field * @param contentProperty - * @param converterMap + * @param converterRegistry * @param context * @param rowIndex * @param columnIndex @@ -119,12 +118,12 @@ public static Object convertToJavaObject( ReadCellData cellData, Field field, ExcelContentProperty contentProperty, - Map> converterMap, + CellDataConverterRegistry converterRegistry, AnalysisContext context, Integer rowIndex, Integer columnIndex) { return convertToJavaObject( - cellData, field, null, null, contentProperty, converterMap, context, rowIndex, columnIndex); + cellData, field, null, null, contentProperty, converterRegistry, context, rowIndex, columnIndex); } /** @@ -134,7 +133,7 @@ public static Object convertToJavaObject( * @param field * @param clazz * @param contentProperty - * @param converterMap + * @param converterRegistry * @param context * @param rowIndex * @param columnIndex @@ -146,7 +145,7 @@ public static Object convertToJavaObject( Class clazz, Class classGeneric, ExcelContentProperty contentProperty, - Map> converterMap, + CellDataConverterRegistry converterRegistry, AnalysisContext context, Integer rowIndex, Integer columnIndex) { @@ -163,13 +162,14 @@ public static Object convertToJavaObject( cellData, getClassGeneric(field, classGeneric), contentProperty, - converterMap, + converterRegistry, context, rowIndex, columnIndex)); return cellDataReturn; } - return doConvertToJavaObject(cellData, clazz, contentProperty, converterMap, context, rowIndex, columnIndex); + return doConvertToJavaObject( + cellData, clazz, contentProperty, converterRegistry, context, rowIndex, columnIndex); } private static Class getClassGeneric(Field field, Class classGeneric) { @@ -199,7 +199,7 @@ private static Class getClassGeneric(Field field, Class classGeneric) { * @param cellData * @param clazz * @param contentProperty - * @param converterMap + * @param converterRegistry * @param context * @param rowIndex * @param columnIndex @@ -209,11 +209,11 @@ private static Object doConvertToJavaObject( ReadCellData cellData, Class clazz, ExcelContentProperty contentProperty, - Map> converterMap, + CellDataConverterRegistry converterRegistry, AnalysisContext context, Integer rowIndex, Integer columnIndex) { - Converter converter = null; + ReadConverter converter = null; if (contentProperty != null) { converter = contentProperty.getConverter(); } @@ -225,7 +225,7 @@ private static Object doConvertToJavaObject( } if (converter == null) { - converter = converterMap.get(ConverterKeyBuild.buildKey(clazz, cellData.getType())); + converter = converterRegistry.findReadConverter(clazz, cellData.getType(), columnIndex); } if (converter == null) { throw new ExcelDataConvertException( diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/builder/AbstractExcelWriterParameterBuilder.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/builder/AbstractExcelWriterParameterBuilder.java index 012e5b3ae5..2cc816f07e 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/builder/AbstractExcelWriterParameterBuilder.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/builder/AbstractExcelWriterParameterBuilder.java @@ -27,6 +27,9 @@ import java.util.ArrayList; import java.util.Collection; +import org.apache.fesod.common.util.ListUtils; +import org.apache.fesod.sheet.converters.Converter; +import org.apache.fesod.sheet.converters.WriteConverter; import org.apache.fesod.sheet.enums.HeaderMergeStrategy; import org.apache.fesod.sheet.metadata.AbstractParameterBuilder; import org.apache.fesod.sheet.write.handler.WriteHandler; @@ -73,6 +76,27 @@ public T registerWriteHandler(WriteHandler writeHandler) { return self(); } + /** + * Registers a custom converter. + * + * @see #registerWriteConverter(WriteConverter) + */ + @Override + public T registerConverter(Converter converter) { + return registerWriteConverter(converter); + } + + /** + * Registers a custom write converter. + */ + public T registerWriteConverter(WriteConverter converter) { + if (parameter().getCustomConverterList() == null) { + parameter().setCustomConverterList(ListUtils.newArrayList()); + } + parameter().getCustomConverterList().add(converter); + return self(); + } + /** * Use the default style.Default is true. * diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/executor/AbstractExcelWriteExecutor.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/executor/AbstractExcelWriteExecutor.java index 48b0fc5968..ccebf42a57 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/executor/AbstractExcelWriteExecutor.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/executor/AbstractExcelWriteExecutor.java @@ -29,9 +29,8 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.fesod.common.util.ListUtils; import org.apache.fesod.sheet.context.WriteContext; -import org.apache.fesod.sheet.converters.Converter; -import org.apache.fesod.sheet.converters.ConverterKeyBuild; import org.apache.fesod.sheet.converters.NullableObjectConverter; +import org.apache.fesod.sheet.converters.WriteConverter; import org.apache.fesod.sheet.converters.WriteConverterContext; import org.apache.fesod.sheet.enums.CellDataTypeEnum; import org.apache.fesod.sheet.exception.ExcelWriteDataConvertException; @@ -335,24 +334,22 @@ private void fillProperty(WriteCellData cellDataValue, ExcelContentProperty e } } + @SuppressWarnings("unchecked") private WriteCellData doConvert(CellWriteHandlerContext cellWriteHandlerContext) { ExcelContentProperty excelContentProperty = cellWriteHandlerContext.getExcelContentProperty(); - Converter converter = null; + WriteConverter converter = null; if (excelContentProperty != null) { converter = excelContentProperty.getConverter(); } if (converter == null) { - // csv is converted to string by default - if (writeContext.writeWorkbookHolder().getExcelType() == ExcelTypeEnum.CSV) { - cellWriteHandlerContext.setTargetCellDataType(CellDataTypeEnum.STRING); - } converter = writeContext .currentWriteHolder() - .converterMap() - .get(ConverterKeyBuild.buildKey( + .converterRegistry() + .findWriteConverter( cellWriteHandlerContext.getOriginalFieldClass(), - cellWriteHandlerContext.getTargetCellDataType())); + cellWriteHandlerContext.getColumnIndex(), + CellDataTypeEnum.STRING.equals(cellWriteHandlerContext.getTargetCellDataType())); } if (cellWriteHandlerContext.getOriginalValue() == null && !(converter instanceof NullableObjectConverter)) { return new WriteCellData<>(CellDataTypeEnum.EMPTY); @@ -365,7 +362,7 @@ private WriteCellData doConvert(CellWriteHandlerContext cellWriteHandlerConte } WriteCellData cellData; try { - cellData = ((Converter) converter) + cellData = ((WriteConverter) converter) .convertToExcelData(new WriteConverterContext<>( cellWriteHandlerContext.getOriginalValue(), excelContentProperty, writeContext)); } catch (Exception e) { diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/metadata/WriteBasicParameter.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/metadata/WriteBasicParameter.java index 5dd959e714..a4fee0cf4b 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/metadata/WriteBasicParameter.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/metadata/WriteBasicParameter.java @@ -31,6 +31,7 @@ import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; +import org.apache.fesod.sheet.converters.WriteConverter; import org.apache.fesod.sheet.enums.HeaderMergeStrategy; import org.apache.fesod.sheet.metadata.BasicParameter; import org.apache.fesod.sheet.write.handler.WriteHandler; @@ -56,6 +57,10 @@ public class WriteBasicParameter extends BasicParameter { * Custom type handler override the default */ private List customWriteHandlerList = new ArrayList(); + /** + * Custom type converters for writing. + */ + private List> customConverterList = new ArrayList<>(); /** * Use the default style.Default is true. */ 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 e658b5d8e7..d220572973 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 @@ -27,7 +27,6 @@ import java.util.ArrayList; import java.util.Collection; -import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -39,9 +38,7 @@ import lombok.Setter; import org.apache.commons.collections4.CollectionUtils; import org.apache.fesod.sheet.constant.OrderConstant; -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.converters.WriteConverter; import org.apache.fesod.sheet.enums.HeadKindEnum; import org.apache.fesod.sheet.enums.HeaderMergeStrategy; import org.apache.fesod.sheet.event.NotRepeatExecutor; @@ -131,11 +128,6 @@ public abstract class AbstractWriteHolder extends AbstractHolder implements Writ */ private Boolean orderByIncludeColumn; - /** - * Custom converters for this holder - */ - private List> customConverterList; - /** * Write handler */ @@ -266,30 +258,11 @@ public AbstractWriteHolder(WriteBasicParameter writeBasicParameter, AbstractWrit // Initialization property this.excelWriteHeadProperty = new ExcelWriteHeadProperty(this, getClazz(), getHead()); - // Set converterMap - if (parentAbstractWriteHolder == null) { - setConverterMap(DefaultConverterLoader.copyDefaultWriteConverter()); - } else { - setConverterMap(new HashMap<>(parentAbstractWriteHolder.getConverterMap())); - if (CollectionUtils.isNotEmpty(parentAbstractWriteHolder.getCustomConverterList())) { - for (Converter converter : parentAbstractWriteHolder.getCustomConverterList()) { - getConverterMap() - .put( - ConverterKeyBuild.buildKey( - converter.supportJavaTypeKey(), converter.supportExcelTypeKey()), - converter); - } - } - } + // register converters (custom) if (writeBasicParameter.getCustomConverterList() != null && !writeBasicParameter.getCustomConverterList().isEmpty()) { - this.customConverterList = writeBasicParameter.getCustomConverterList(); - for (Converter converter : writeBasicParameter.getCustomConverterList()) { - getConverterMap() - .put( - ConverterKeyBuild.buildKey( - converter.supportJavaTypeKey(), converter.supportExcelTypeKey()), - converter); + for (WriteConverter converter : writeBasicParameter.getCustomConverterList()) { + getConverterRegistry().addCustomWriteConverter(converter); } } } diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/metadata/holder/WriteWorkbookHolder.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/metadata/holder/WriteWorkbookHolder.java index 021dd8523c..4872d5c6b8 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/metadata/holder/WriteWorkbookHolder.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/metadata/holder/WriteWorkbookHolder.java @@ -239,6 +239,8 @@ public WriteWorkbookHolder(WriteWorkbook writeWorkbook) { this.excelType = writeWorkbook.getExcelType(); } + initDefaultConverters(this.excelType, false); + // init handler try { initHandler(writeWorkbook, null); diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/CustomConverterRoundTripTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/CustomConverterRoundTripTest.java new file mode 100644 index 0000000000..295b5dc811 --- /dev/null +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/CustomConverterRoundTripTest.java @@ -0,0 +1,165 @@ +/* + * 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.util.Collections; +import java.util.List; +import org.apache.fesod.sheet.FesodSheet; +import org.apache.fesod.sheet.annotation.ExcelProperty; +import org.apache.fesod.sheet.converters.ColumnBinding; +import org.apache.fesod.sheet.converters.ReadConverter; +import org.apache.fesod.sheet.converters.WriteConverter; +import org.apache.fesod.sheet.enums.CellDataTypeEnum; +import org.apache.fesod.sheet.metadata.GlobalConfiguration; +import org.apache.fesod.sheet.metadata.data.ReadCellData; +import org.apache.fesod.sheet.metadata.data.WriteCellData; +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.apache.fesod.sheet.testkit.helpers.RoundTripHelper; +import org.apache.fesod.sheet.testkit.listeners.CollectingReadListener; +import org.apache.fesod.sheet.testkit.params.ExcelFormatSource; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.params.ParameterizedTest; + +/** + * Verifies custom converters registered through the top-level builder API are actually + * invoked by the write/read pipeline (not just stored in the registry). + */ +@Tag(Tags.ROUND_TRIP) +class CustomConverterRoundTripTest extends AbstractExcelTest { + + @ParameterizedTest + @ExcelFormatSource + void shouldApplyRegisteredReadConverterWhenReadingModel(ExcelFormat format) throws Exception { + // full read chain: builder -> ReadBasicParameter -> holder registry -> column-aware lookup in ConverterUtils + File file = createTempFile(format); + RoundTripHelper.write(file, TwoColumnData.class, data()); + + CollectingReadListener listener = new CollectingReadListener<>(); + FesodSheet.read(file, TwoColumnData.class, listener) + .registerReadConverter(new PrefixReadConverter("read:")) + .sheet() + .doRead(); + + List rows = listener.getRows(); + Assertions.assertEquals(1, rows.size()); + Assertions.assertEquals("read:apple", rows.get(0).getFirst()); + Assertions.assertEquals("read:banana", rows.get(0).getSecond()); + } + + @ParameterizedTest + @ExcelFormatSource + void shouldApplyColumnBoundWriteConverterOnlyToItsBoundColumn(ExcelFormat format) throws Exception { + // the write executor must pass the column index so the bound converter applies to column 0 only + File file = createTempFile(format); + FesodSheet.write(file, TwoColumnData.class) + .registerWriteConverter(new ColumnPrefixWriteConverter()) + .sheet() + .doWrite(data()); + + List rows = RoundTripHelper.read(file, TwoColumnData.class); + Assertions.assertEquals(1, rows.size()); + Assertions.assertEquals("col0:apple", rows.get(0).getFirst()); + // unbound column keeps the default String converter + Assertions.assertEquals("banana", rows.get(0).getSecond()); + } + + private static List data() { + TwoColumnData row = new TwoColumnData(); + row.setFirst("apple"); + row.setSecond("banana"); + return Collections.singletonList(row); + } + + private static final class PrefixReadConverter implements ReadConverter { + + private final String prefix; + + PrefixReadConverter(String prefix) { + this.prefix = prefix; + } + + @Override + public Class supportJavaTypeKey() { + return String.class; + } + + @Override + public CellDataTypeEnum supportExcelTypeKey() { + return CellDataTypeEnum.STRING; + } + + @Override + public String convertToJavaData( + ReadCellData cellData, + ExcelContentProperty contentProperty, + GlobalConfiguration globalConfiguration) { + return prefix + cellData.getStringValue(); + } + } + + private static final class ColumnPrefixWriteConverter implements WriteConverter, ColumnBinding { + + @Override + public Integer columnIndex() { + return 0; + } + + @Override + public Class supportJavaTypeKey() { + return String.class; + } + + @Override + public WriteCellData convertToExcelData( + String value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { + return new WriteCellData<>("col0:" + value); + } + } + + public static class TwoColumnData { + + @ExcelProperty(value = "first", index = 0) + private String first; + + @ExcelProperty(value = "second", index = 1) + private String second; + + public String getFirst() { + return first; + } + + public void setFirst(String first) { + this.first = first; + } + + public String getSecond() { + return second; + } + + public void setSecond(String second) { + this.second = second; + } + } +} diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/CustomConverterTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/CustomConverterTest.java index 03ec70f503..0d36357b13 100644 --- a/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/CustomConverterTest.java +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/CustomConverterTest.java @@ -25,12 +25,12 @@ import java.sql.Timestamp; import java.util.ArrayList; 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.annotation.ExcelProperty; +import org.apache.fesod.sheet.converters.CellDataConverterRegistry; import org.apache.fesod.sheet.converters.Converter; -import org.apache.fesod.sheet.converters.ConverterKeyBuild; +import org.apache.fesod.sheet.converters.WriteConverter; import org.apache.fesod.sheet.enums.CellDataTypeEnum; import org.apache.fesod.sheet.metadata.GlobalConfiguration; import org.apache.fesod.sheet.metadata.data.WriteCellData; @@ -42,7 +42,6 @@ import org.apache.fesod.sheet.testkit.params.ExcelFormatSource; import org.apache.fesod.sheet.testkit.params.FormatScope; import org.apache.fesod.sheet.write.builder.ExcelWriterSheetBuilder; -import org.apache.fesod.sheet.write.metadata.holder.WriteSheetHolder; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Workbook; import org.apache.poi.ss.usermodel.WorkbookFactory; @@ -59,18 +58,24 @@ void converterMapTest() { File converterCsvFile10 = new File(tempDir, "converter10.csv"); TimestampStringConverter timestampStringConverter = new TimestampStringConverter(); TimestampNumberConverter timestampNumberConverter = new TimestampNumberConverter(); - ExcelWriter excelWriter = FesodSheet.write(converterCsvFile10) + + try (ExcelWriter excelWriter = FesodSheet.write(converterCsvFile10) .registerConverter(timestampStringConverter) .registerConverter(timestampNumberConverter) - .build(); - Map> converterMap = - excelWriter.writeContext().currentWriteHolder().converterMap(); - excelWriter.write(data(), new ExcelWriterSheetBuilder().sheetNo(0).build()); - excelWriter.finish(); - Assertions.assertTrue(converterMap.containsKey(ConverterKeyBuild.buildKey( - timestampStringConverter.supportJavaTypeKey(), timestampStringConverter.supportExcelTypeKey()))); - Assertions.assertTrue(converterMap.containsKey(ConverterKeyBuild.buildKey( - timestampNumberConverter.supportJavaTypeKey(), timestampNumberConverter.supportExcelTypeKey()))); + .build()) { + + CellDataConverterRegistry converterRegistry = + excelWriter.writeContext().currentWriteHolder().converterRegistry(); + + excelWriter.write(data(), new ExcelWriterSheetBuilder().sheetNo(0).build()); + excelWriter.finish(); + + WriteConverter toStringConverter = converterRegistry.findWriteConverter(Timestamp.class, 0); + WriteConverter toNumberConverter = converterRegistry.findWriteConverter(Timestamp.class, 1); + + Assertions.assertEquals(timestampStringConverter, toStringConverter); + Assertions.assertEquals(timestampNumberConverter, toNumberConverter); + } } @Test @@ -88,29 +93,6 @@ void writeXlsx() throws Exception { writeFile(new File(tempDir, "converter12.xlsx")); } - @Test - void globalConverterInSheetHolder() { - File converterExcelFile13 = new File(tempDir, "converter13.xlsx"); - TimestampStringConverter timestampStringConverter = new TimestampStringConverter(); - ExcelWriter excelWriter = FesodSheet.write(converterExcelFile13) - .registerConverter(timestampStringConverter) - .build(); - excelWriter.write(data(), new ExcelWriterSheetBuilder().sheetNo(0).build()); - WriteSheetHolder sheetHolder = excelWriter.writeContext().writeSheetHolder(); - Map> sheetConverterMap = sheetHolder.converterMap(); - excelWriter.finish(); - Assertions.assertTrue(sheetConverterMap.containsKey(ConverterKeyBuild.buildKey( - timestampStringConverter.supportJavaTypeKey(), timestampStringConverter.supportExcelTypeKey()))); - } - - @Test - void globalConverterWriteWithoutFieldLevelConverter() throws Exception { - FesodSheet.write(new File(tempDir, "converter14.csv")) - .registerConverter(new TimestampStringConverter()) - .sheet() - .doWrite(globalData()); - } - @Test void fieldLevelConverterTakesPrecedenceOverRegisteredConverter() throws Exception { File converterCsvFile15 = new File(tempDir, "converter15.csv"); @@ -159,14 +141,6 @@ private void writeFile(File file) { .doWrite(data()); } - private List globalData() { - List list = new ArrayList<>(); - GlobalConverterWriteData writeData = new GlobalConverterWriteData(); - writeData.setTimestampData(Timestamp.valueOf("2020-01-01 01:00:00")); - list.add(writeData); - return list; - } - private List data() { return TestDataBuilder.customConverterWriteData(); } diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/GlobalConverterWriteData.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/GlobalConverterWriteData.java deleted file mode 100644 index 7bbbf4828e..0000000000 --- a/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/GlobalConverterWriteData.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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.sql.Timestamp; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.Setter; -import org.apache.fesod.sheet.annotation.ExcelProperty; - -@Getter -@Setter -@EqualsAndHashCode -public class GlobalConverterWriteData { - @ExcelProperty("TimestampString") - private Timestamp timestampData; -} diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/TimestampNumberConverter.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/TimestampNumberConverter.java index d8134cf672..84a49dff75 100644 --- a/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/TimestampNumberConverter.java +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/TimestampNumberConverter.java @@ -21,23 +21,24 @@ import java.math.BigDecimal; import java.sql.Timestamp; +import org.apache.fesod.sheet.converters.ColumnBinding; import org.apache.fesod.sheet.converters.Converter; -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.util.DateUtils; import org.apache.poi.ss.usermodel.DateUtil; -public class TimestampNumberConverter implements Converter { +public class TimestampNumberConverter implements Converter, ColumnBinding { + @Override - public Class supportJavaTypeKey() { - return Timestamp.class; + public Integer columnIndex() { + return 1; } @Override - public CellDataTypeEnum supportExcelTypeKey() { - return CellDataTypeEnum.NUMBER; + public Class supportJavaTypeKey() { + return Timestamp.class; } @Override diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/TimestampStringConverter.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/TimestampStringConverter.java index 3688ead825..e829c8f076 100644 --- a/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/TimestampStringConverter.java +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/TimestampStringConverter.java @@ -20,6 +20,7 @@ package org.apache.fesod.sheet.converter; import java.sql.Timestamp; +import org.apache.fesod.sheet.converters.ColumnBinding; import org.apache.fesod.sheet.converters.Converter; import org.apache.fesod.sheet.enums.CellDataTypeEnum; import org.apache.fesod.sheet.metadata.GlobalConfiguration; @@ -27,15 +28,16 @@ import org.apache.fesod.sheet.metadata.property.ExcelContentProperty; import org.apache.fesod.sheet.util.DateUtils; -public class TimestampStringConverter implements Converter { +public class TimestampStringConverter implements Converter, ColumnBinding { + @Override - public Class supportJavaTypeKey() { - return Timestamp.class; + public Integer columnIndex() { + return 0; } @Override - public CellDataTypeEnum supportExcelTypeKey() { - return CellDataTypeEnum.STRING; + public Class supportJavaTypeKey() { + return Timestamp.class; } @Override diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/converters/CellDataConverterRegistryTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converters/CellDataConverterRegistryTest.java new file mode 100644 index 0000000000..059698a865 --- /dev/null +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converters/CellDataConverterRegistryTest.java @@ -0,0 +1,254 @@ +/* + * 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.converters; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import org.apache.fesod.sheet.converters.string.StringStringConverter; +import org.apache.fesod.sheet.enums.CellDataTypeEnum; +import org.apache.fesod.sheet.testkit.Tags; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +/** + * Tests {@link CellDataConverterRegistry}. + */ +@Tag(Tags.UNIT) +class CellDataConverterRegistryTest { + + @Test + void shouldResolveCustomWriteConverterBeforeParentCustomBeforeDefault() { + // resolution order: local custom > parent custom > default, for write lookups + StubConverter defaultWrite = new StubConverter("default", String.class); + CellDataConverterRegistry root = new CellDataConverterRegistry(); + addDefaultConverterTo(root, String.class, null, defaultWrite); + + StubConverter parentWrite = new StubConverter("parent", String.class); + root.addCustomWriteConverter(parentWrite); + CellDataConverterRegistry child = new CellDataConverterRegistry(root); + + Assertions.assertSame(parentWrite, root.findWriteConverter(String.class)); + Assertions.assertSame(parentWrite, child.findWriteConverter(String.class)); + + StubConverter localWrite = new StubConverter("local", String.class); + child.addCustomWriteConverter(localWrite); + Assertions.assertSame(localWrite, child.findWriteConverter(String.class)); + } + + @Test + void shouldResolveCustomReadConverterBeforeParentCustomBeforeDefault() { + // resolution order: local custom > parent custom > default, for read lookups + StubConverter defaultRead = new StubConverter("default", String.class, CellDataTypeEnum.NUMBER); + CellDataConverterRegistry root = new CellDataConverterRegistry(); + addDefaultConverterTo(root, String.class, CellDataTypeEnum.NUMBER, defaultRead); + + StubConverter parentRead = new StubConverter("parent", String.class, CellDataTypeEnum.NUMBER); + root.addCustomReadConverter(parentRead); + CellDataConverterRegistry child = new CellDataConverterRegistry(root); + + Assertions.assertSame(parentRead, root.findReadConverter(String.class, CellDataTypeEnum.NUMBER)); + Assertions.assertSame(parentRead, child.findReadConverter(String.class, CellDataTypeEnum.NUMBER)); + + StubConverter localRead = new StubConverter("local", String.class, CellDataTypeEnum.NUMBER); + child.addCustomReadConverter(localRead); + Assertions.assertSame(localRead, child.findReadConverter(String.class, CellDataTypeEnum.NUMBER)); + } + + @Test + void shouldResolveCustomReadConverterOnlyForItsDeclaredExcelType() { + // read converters are keyed by (javaType, excelType): the NUMBER custom must not shadow the STRING default + StubConverter numberCustom = new StubConverter("number", String.class, CellDataTypeEnum.NUMBER); + StubConverter stringDefault = new StubConverter("string", String.class, CellDataTypeEnum.STRING); + CellDataConverterRegistry registry = new CellDataConverterRegistry(); + addDefaultConverterTo(registry, String.class, CellDataTypeEnum.STRING, stringDefault); + registry.addCustomReadConverter(numberCustom); + + Assertions.assertSame(numberCustom, registry.findReadConverter(String.class, CellDataTypeEnum.NUMBER)); + Assertions.assertSame(stringDefault, registry.findReadConverter(String.class, CellDataTypeEnum.STRING)); + } + + @Test + void shouldPreferMostRecentlyRegisteredConverterForSameKey() { + // for the same key, the converter registered later takes precedence + CellDataConverterRegistry registry = new CellDataConverterRegistry(); + registry.addCustomWriteConverter(new StubConverter("first", String.class)); + StubConverter second = new StubConverter("second", String.class); + registry.addCustomWriteConverter(second); + + Assertions.assertSame(second, registry.findWriteConverter(String.class)); + } + + @Test + void shouldResolveCustomWriteConverterEvenWhenDefaultingToString() { + // the to-string default tier must never shadow a custom converter (no-bean fill invariant) + CellDataConverterRegistry registry = new CellDataConverterRegistry(); + StubConverter custom = new StubConverter("custom", String.class); + registry.addCustomWriteConverter(custom); + + Assertions.assertSame(custom, registry.findWriteConverter(String.class, null, true)); + } + + @Test + void shouldReturnNullInsteadOfNaturalDefaultWhenDefaultingToString() { + // types without a to-string sibling resolve to null rather than falling back to the natural default + StubConverter natural = new StubConverter("natural", Object.class); + CellDataConverterRegistry registry = new CellDataConverterRegistry(); + addDefaultConverterTo(registry, Object.class, null, natural); + + Assertions.assertSame(natural, registry.findWriteConverter(Object.class, null, false)); + Assertions.assertNull(registry.findWriteConverter(Object.class, null, true)); + } + + @Test + void shouldDefaultToBuiltInStringConverterFamily() { + CellDataConverterRegistry registry = new CellDataConverterRegistry(); + Assertions.assertInstanceOf(StringStringConverter.class, registry.findWriteConverter(String.class, 0, true)); + } + + @Test + void shouldPreferColumnBoundConverterOverGlobalOnlyForItsBoundColumn() { + CellDataConverterRegistry registry = new CellDataConverterRegistry(); + StubConverter global = new StubConverter("global", String.class); + StubColumnConverter bound = new StubColumnConverter("bound", 1); + registry.addCustomWriteConverter(global); + registry.addCustomWriteConverter(bound); + + Assertions.assertSame(bound, registry.findWriteConverter(String.class, 1)); + Assertions.assertSame(global, registry.findWriteConverter(String.class, 2)); + // a column-less lookup must never pick up a column-bound converter + Assertions.assertSame(global, registry.findWriteConverter(String.class)); + } + + @Test + void shouldNotTreatColumnBoundConverterWithoutIndexesAsGlobalFallback() { + CellDataConverterRegistry registry = new CellDataConverterRegistry(); + registry.addCustomWriteConverter(new StubColumnConverter("orphan")); + + Assertions.assertNull(registry.findWriteConverter(String.class, 1)); + Assertions.assertNull(registry.findWriteConverter(String.class)); + } + + @Test + void shouldRefreshCachedWriteLookupsAfterConverterRegistration() { + CellDataConverterRegistry registry = new CellDataConverterRegistry(); + Assertions.assertNull(registry.findWriteConverter(String.class, 3)); + + StubConverter late = new StubConverter("late", String.class); + registry.addCustomWriteConverter(late); + Assertions.assertSame(late, registry.findWriteConverter(String.class, 3)); + + StubConverter later = new StubConverter("later", String.class); + registry.addCustomWriteConverter(later); + Assertions.assertSame(later, registry.findWriteConverter(String.class, 3)); + } + + @Test + void shouldRefreshCachedReadLookupsAfterConverterRegistration() { + CellDataConverterRegistry registry = new CellDataConverterRegistry(); + Assertions.assertNull(registry.findReadConverter(String.class, CellDataTypeEnum.NUMBER, 5)); + + StubConverter late = new StubConverter("late", String.class, CellDataTypeEnum.NUMBER); + registry.addCustomReadConverter(late); + Assertions.assertSame(late, registry.findReadConverter(String.class, CellDataTypeEnum.NUMBER, 5)); + } + + @Test + void shouldMatchPrimitiveAndBoxedKeysInterchangeably() { + CellDataConverterRegistry registry = new CellDataConverterRegistry(); + StubConverter intWrite = new StubConverter("int", int.class); + registry.addCustomWriteConverter(intWrite); + Assertions.assertSame(intWrite, registry.findWriteConverter(int.class)); + Assertions.assertSame(intWrite, registry.findWriteConverter(Integer.class)); + + StubConverter longRead = new StubConverter("long", long.class, CellDataTypeEnum.NUMBER); + registry.addCustomReadConverter(longRead); + Assertions.assertSame(longRead, registry.findReadConverter(Long.class, CellDataTypeEnum.NUMBER)); + } + + @Test + void shouldRejectConverterMissingRequiredSupportKeys() { + CellDataConverterRegistry registry = new CellDataConverterRegistry(); + + Assertions.assertThrows(NullPointerException.class, () -> registry.addCustomWriteConverter(null)); + Assertions.assertThrows( + NullPointerException.class, () -> registry.addCustomWriteConverter(new StubConverter("noKey", null))); + Assertions.assertThrows(NullPointerException.class, () -> registry.addCustomReadConverter(null)); + Assertions.assertThrows( + NullPointerException.class, + () -> registry.addCustomReadConverter(new StubConverter("noExcelType", String.class, null))); + } + + private static void addDefaultConverterTo( + CellDataConverterRegistry targetRegistry, + Class javaType, + CellDataTypeEnum cellDataType, + Converter converter) { + targetRegistry.addDefaultConverters( + Collections.singletonMap(ConverterKeyBuild.buildKey(javaType, cellDataType), converter)); + } + + private static class StubConverter implements Converter { + private final String name; + private final Class javaTypeKey; + private final CellDataTypeEnum excelTypeKey; + + StubConverter(String name, Class javaTypeKey) { + this(name, javaTypeKey, null); + } + + StubConverter(String name, Class javaTypeKey, CellDataTypeEnum excelTypeKey) { + this.name = name; + this.javaTypeKey = javaTypeKey; + this.excelTypeKey = excelTypeKey; + } + + @Override + public Class supportJavaTypeKey() { + return javaTypeKey; + } + + @Override + public CellDataTypeEnum supportExcelTypeKey() { + return excelTypeKey; + } + + @Override + public String toString() { + return name; + } + } + + private static final class StubColumnConverter extends StubConverter implements ColumnBinding { + private final Set columnIndexes; + + StubColumnConverter(String name, Integer... columnIndexes) { + super(name, String.class); + this.columnIndexes = new HashSet<>(Arrays.asList(columnIndexes)); + } + + @Override + public Set columnIndexes() { + return columnIndexes; + } + } +} diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/converters/DefaultConverterLoaderTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converters/DefaultConverterLoaderTest.java index bd5f19d417..852ba0e278 100644 --- a/fesod-sheet/src/test/java/org/apache/fesod/sheet/converters/DefaultConverterLoaderTest.java +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converters/DefaultConverterLoaderTest.java @@ -74,9 +74,10 @@ void loadConvertersRegistersLocalTimeFamily() { Map> writeConverter = DefaultConverterLoader.loadDefaultWriteConverter(); Assertions.assertInstanceOf( LocalTimeDateConverter.class, writeConverter.get(ConverterKeyBuild.buildKey(LocalTime.class))); + + Map> writeStringConverter = DefaultConverterLoader.loadDefaultWriteStringConverter(); Assertions.assertInstanceOf( - LocalTimeStringConverter.class, - writeConverter.get(ConverterKeyBuild.buildKey(LocalTime.class, CellDataTypeEnum.STRING))); + LocalTimeStringConverter.class, writeStringConverter.get(ConverterKeyBuild.buildKey(LocalTime.class))); } @Test @@ -89,9 +90,10 @@ void loadConvertersRegistersInstantStringConverter() { Map> writeConverter = DefaultConverterLoader.loadDefaultWriteConverter(); Assertions.assertInstanceOf( InstantStringConverter.class, writeConverter.get(ConverterKeyBuild.buildKey(Instant.class))); + + Map> writeStringConverter = DefaultConverterLoader.loadDefaultWriteStringConverter(); Assertions.assertInstanceOf( - InstantStringConverter.class, - writeConverter.get(ConverterKeyBuild.buildKey(Instant.class, CellDataTypeEnum.STRING))); + InstantStringConverter.class, writeStringConverter.get(ConverterKeyBuild.buildKey(Instant.class))); } @Test @@ -100,12 +102,14 @@ void loadConvertersRegistersUUIDFamily() { UUIDStringConverter.class, DefaultConverterLoader.loadDefaultReadConverter() .get(ConverterKeyBuild.buildKey(UUID.class, CellDataTypeEnum.STRING))); + Map> writeConverter = DefaultConverterLoader.loadDefaultWriteConverter(); Assertions.assertInstanceOf( UUIDStringConverter.class, writeConverter.get(ConverterKeyBuild.buildKey(UUID.class))); + + Map> writeStringConverter = DefaultConverterLoader.loadDefaultWriteStringConverter(); Assertions.assertInstanceOf( - UUIDStringConverter.class, - writeConverter.get(ConverterKeyBuild.buildKey(UUID.class, CellDataTypeEnum.STRING))); + UUIDStringConverter.class, writeStringConverter.get(ConverterKeyBuild.buildKey(UUID.class))); } @Test @@ -120,9 +124,9 @@ void loadConvertersRegistersSqlTimeFamily() { Map> writeConverter = DefaultConverterLoader.loadDefaultWriteConverter(); Assertions.assertInstanceOf( SqlTimeDateConverter.class, writeConverter.get(ConverterKeyBuild.buildKey(Time.class))); + Map> writeStringConverter = DefaultConverterLoader.loadDefaultWriteStringConverter(); Assertions.assertInstanceOf( - SqlTimeStringConverter.class, - writeConverter.get(ConverterKeyBuild.buildKey(Time.class, CellDataTypeEnum.STRING))); + SqlTimeStringConverter.class, writeStringConverter.get(ConverterKeyBuild.buildKey(Time.class))); } @Test @@ -138,9 +142,9 @@ void loadConvertersRegistersTimestampFamily() { Map> writeConverter = DefaultConverterLoader.loadDefaultWriteConverter(); Assertions.assertInstanceOf( TimestampDateConverter.class, writeConverter.get(ConverterKeyBuild.buildKey(Timestamp.class))); + Map> writeStringConverter = DefaultConverterLoader.loadDefaultWriteStringConverter(); Assertions.assertInstanceOf( - TimestampStringConverter.class, - writeConverter.get(ConverterKeyBuild.buildKey(Timestamp.class, CellDataTypeEnum.STRING))); + TimestampStringConverter.class, writeStringConverter.get(ConverterKeyBuild.buildKey(Timestamp.class))); } private static void assertLoadIsImmutableAndCopyIsMutable( diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/converters/timestamp/TimestampConverterTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converters/timestamp/TimestampConverterTest.java index c58c15d087..23d4650045 100644 --- a/fesod-sheet/src/test/java/org/apache/fesod/sheet/converters/timestamp/TimestampConverterTest.java +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converters/timestamp/TimestampConverterTest.java @@ -204,7 +204,7 @@ void defaultConverterLoaderRegistersTimestampConverters() { Assertions.assertTrue(DefaultConverterLoader.loadDefaultWriteConverter() .containsKey(ConverterKeyBuild.buildKey(Timestamp.class))); - Assertions.assertTrue(DefaultConverterLoader.loadDefaultWriteConverter() - .containsKey(ConverterKeyBuild.buildKey(Timestamp.class, CellDataTypeEnum.STRING))); + Assertions.assertTrue(DefaultConverterLoader.loadDefaultWriteStringConverter() + .containsKey(ConverterKeyBuild.buildKey(Timestamp.class))); } } diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/style/FillDataTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/style/FillDataTest.java index 03cb3fc822..5ff3e32358 100644 --- a/fesod-sheet/src/test/java/org/apache/fesod/sheet/style/FillDataTest.java +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/style/FillDataTest.java @@ -26,6 +26,11 @@ package org.apache.fesod.sheet.style; import java.io.File; +import java.math.BigDecimal; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -40,6 +45,7 @@ import org.apache.fesod.sheet.testkit.params.ExcelFormatSource; import org.apache.fesod.sheet.testkit.params.FormatCapability; import org.apache.fesod.sheet.testkit.params.FormatScope; +import org.apache.fesod.sheet.util.DateUtils; import org.apache.fesod.sheet.write.merge.LoopMergeStrategy; import org.apache.fesod.sheet.write.metadata.WriteSheet; import org.apache.fesod.sheet.write.metadata.fill.FillConfig; @@ -100,6 +106,29 @@ void compositeFill(ExcelFormat format) throws Exception { compositeFillImpl(file, template); } + @ParameterizedTest + @ExcelFormatSource(value = FormatScope.BINARY, requires = FormatCapability.TEMPLATES) + void shouldFormatVariablesInCompositeCellViaStringConverters(ExcelFormat format) throws Exception { + File template = createTempFile("fillToStringTemplate", format); + List> head = new ArrayList<>(); + head.add(Collections.singletonList("line")); + List> templateRow = new ArrayList<>(); + templateRow.add(Collections.singletonList("date: {date} amount: {amount}")); + FesodSheet.write(template).head(head).sheet().doWrite(templateRow); + + File file = createTempFile("fillToString", format); + Map data = new HashMap<>(); + Date date = Date.from(Instant.now()); + data.put("date", date); + data.put("amount", new BigDecimal("1234.56")); + FesodSheet.write(file).withTemplate(template).sheet().doFill(data); + + List> rows = + FesodSheet.read(file).sheet().headRowNumber(0).doReadSync(); + Map filledRow = rows.get(1); + Assertions.assertEquals("date: " + DateUtils.format(date) + " amount: 1234.56", filledRow.get(0)); + } + private void byNameFillImpl(File file, File template) { FillData fillData = new FillData(); fillData.setName("Zhang San"); diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/util/ConverterUtilsTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/util/ConverterUtilsTest.java index a4c30bc42c..8f3f04adff 100644 --- a/fesod-sheet/src/test/java/org/apache/fesod/sheet/util/ConverterUtilsTest.java +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/util/ConverterUtilsTest.java @@ -25,9 +25,10 @@ import java.util.Map; import java.util.TreeMap; import org.apache.fesod.sheet.context.AnalysisContext; +import org.apache.fesod.sheet.converters.CellDataConverterRegistry; import org.apache.fesod.sheet.converters.Converter; -import org.apache.fesod.sheet.converters.ConverterKeyBuild; import org.apache.fesod.sheet.converters.NullableObjectConverter; +import org.apache.fesod.sheet.converters.ReadConverter; import org.apache.fesod.sheet.enums.CellDataTypeEnum; import org.apache.fesod.sheet.exception.ExcelDataConvertException; import org.apache.fesod.sheet.metadata.data.ReadCellData; @@ -63,18 +64,15 @@ class ConverterUtilsTest { @Mock private Converter stringConverter; - @Mock - private Converter integerConverter; - - private Map> converterMap; + private CellDataConverterRegistry converterRegistry; @BeforeEach void setUp() { - converterMap = new HashMap<>(); + converterRegistry = new CellDataConverterRegistry(); Mockito.lenient().when(context.readSheetHolder()).thenReturn(readSheetHolder); Mockito.lenient().when(context.readRowHolder()).thenReturn(readRowHolder); - Mockito.lenient().when(readSheetHolder.converterMap()).thenReturn(converterMap); + Mockito.lenient().when(readSheetHolder.converterRegistry()).thenReturn(converterRegistry); Mockito.lenient().when(readRowHolder.getRowIndex()).thenReturn(1); } @@ -85,9 +83,12 @@ void test_convertToStringMap_normal() throws Exception { cellDataMap.put(0, new ReadCellData<>("A")); cellDataMap.put(1, new ReadCellData<>("B")); - ConverterKeyBuild.ConverterKey key = ConverterKeyBuild.buildKey(String.class, CellDataTypeEnum.STRING); - converterMap.put(key, stringConverter); - Mockito.when(stringConverter.convertToJavaData(Mockito.any())).thenReturn("A", "B"); + ReadConverter readConverter = Mockito.mock(ReadConverter.class); + Mockito.doReturn(String.class).when(readConverter).supportJavaTypeKey(); + Mockito.doReturn(CellDataTypeEnum.STRING).when(readConverter).supportExcelTypeKey(); + Mockito.when(readConverter.convertToJavaData(Mockito.any())).thenReturn("A", "B"); + + converterRegistry.addCustomReadConverter(readConverter); Map result = ConverterUtils.convertToStringMap(cellDataMap, context); @@ -103,9 +104,12 @@ void test_convertToStringMap_withGap() throws Exception { cellDataMap.put(0, new ReadCellData<>("A")); cellDataMap.put(2, new ReadCellData<>("C")); - ConverterKeyBuild.ConverterKey key = ConverterKeyBuild.buildKey(String.class, CellDataTypeEnum.STRING); - converterMap.put(key, stringConverter); - Mockito.when(stringConverter.convertToJavaData(Mockito.any())).thenReturn("A", "C"); + ReadConverter readConverter = Mockito.mock(ReadConverter.class); + Mockito.doReturn(String.class).when(readConverter).supportJavaTypeKey(); + Mockito.doReturn(CellDataTypeEnum.STRING).when(readConverter).supportExcelTypeKey(); + Mockito.when(readConverter.convertToJavaData(Mockito.any())).thenReturn("A", "C"); + + converterRegistry.addCustomReadConverter(readConverter); Map result = ConverterUtils.convertToStringMap(cellDataMap, context); @@ -147,12 +151,15 @@ class DemoData { void test_convertToJavaData_simpleConversion() throws Exception { ReadCellData cellData = new ReadCellData<>(new BigDecimal("123")); - ConverterKeyBuild.ConverterKey key = ConverterKeyBuild.buildKey(Integer.class, CellDataTypeEnum.NUMBER); - converterMap.put(key, integerConverter); - Mockito.when(integerConverter.convertToJavaData(Mockito.any())).thenReturn(123); + ReadConverter readConverter = Mockito.mock(ReadConverter.class); + Mockito.doReturn(Integer.class).when(readConverter).supportJavaTypeKey(); + Mockito.doReturn(CellDataTypeEnum.NUMBER).when(readConverter).supportExcelTypeKey(); + Mockito.when(readConverter.convertToJavaData(Mockito.any())).thenReturn(123); + + converterRegistry.addCustomReadConverter(readConverter); Object result = ConverterUtils.convertToJavaObject( - cellData, null, Integer.class, null, null, converterMap, context, 1, 0); + cellData, null, Integer.class, null, null, converterRegistry, context, 1, 0); Assertions.assertEquals(123, result); } @@ -161,16 +168,19 @@ void test_convertToJavaData_simpleConversion() throws Exception { void test_convertToJavaData() throws Exception { ReadCellData cellData = new ReadCellData<>("123"); - ConverterKeyBuild.ConverterKey key = ConverterKeyBuild.buildKey(String.class, CellDataTypeEnum.STRING); - converterMap.put(key, stringConverter); - Mockito.when(stringConverter.convertToJavaData(Mockito.any())).thenReturn("123"); + ReadConverter readConverter = Mockito.mock(ReadConverter.class); + Mockito.doReturn(String.class).when(readConverter).supportJavaTypeKey(); + Mockito.doReturn(CellDataTypeEnum.STRING).when(readConverter).supportExcelTypeKey(); + Mockito.when(readConverter.convertToJavaData(Mockito.any())).thenReturn("123"); + + converterRegistry.addCustomReadConverter(readConverter); Object result1 = - ConverterUtils.convertToJavaObject(cellData, null, null, null, null, converterMap, context, 1, 0); + ConverterUtils.convertToJavaObject(cellData, null, null, null, null, converterRegistry, context, 1, 0); Field field = DemoData.class.getDeclaredField("stringField"); Object result2 = - ConverterUtils.convertToJavaObject(cellData, field, null, null, null, converterMap, context, 1, 0); + ConverterUtils.convertToJavaObject(cellData, field, null, null, null, converterRegistry, context, 1, 0); Assertions.assertEquals("123", result1); Assertions.assertEquals("123", result2); @@ -181,12 +191,15 @@ void test_ReadCellData_generic_inference() throws Exception, NoSuchFieldExceptio ReadCellData cellData = new ReadCellData<>(new BigDecimal("100")); Field field = DemoData.class.getDeclaredField("cellDataIntField"); - ConverterKeyBuild.ConverterKey key = ConverterKeyBuild.buildKey(Integer.class, CellDataTypeEnum.NUMBER); - converterMap.put(key, integerConverter); - Mockito.when(integerConverter.convertToJavaData(Mockito.any())).thenReturn(100); + ReadConverter readConverter = Mockito.mock(ReadConverter.class); + Mockito.doReturn(Integer.class).when(readConverter).supportJavaTypeKey(); + Mockito.doReturn(CellDataTypeEnum.NUMBER).when(readConverter).supportExcelTypeKey(); + Mockito.when(readConverter.convertToJavaData(Mockito.any())).thenReturn(100); + + converterRegistry.addCustomReadConverter(readConverter); Object result = ConverterUtils.convertToJavaObject( - cellData, field, ReadCellData.class, null, null, converterMap, context, 1, 0); + cellData, field, ReadCellData.class, null, null, converterRegistry, context, 1, 0); Assertions.assertInstanceOf(ReadCellData.class, result); ReadCellData resultData = (ReadCellData) result; @@ -198,15 +211,18 @@ void test_ReadCellData_raw_defaultString() throws Exception, NoSuchFieldExceptio ReadCellData cellData = new ReadCellData<>("test"); Field field = DemoData.class.getDeclaredField("rawCellDataField"); - ConverterKeyBuild.ConverterKey key = ConverterKeyBuild.buildKey(String.class, CellDataTypeEnum.STRING); - converterMap.put(key, stringConverter); - Mockito.when(stringConverter.convertToJavaData(Mockito.any())).thenReturn("test"); + ReadConverter readConverter = Mockito.mock(ReadConverter.class); + Mockito.doReturn(String.class).when(readConverter).supportJavaTypeKey(); + Mockito.doReturn(CellDataTypeEnum.STRING).when(readConverter).supportExcelTypeKey(); + Mockito.when(readConverter.convertToJavaData(Mockito.any())).thenReturn("test"); + + converterRegistry.addCustomReadConverter(readConverter); Object result = ConverterUtils.convertToJavaObject( - cellData, field, ReadCellData.class, null, null, converterMap, context, 1, 0); + cellData, field, ReadCellData.class, null, null, converterRegistry, context, 1, 0); Assertions.assertInstanceOf(ReadCellData.class, result); - Mockito.verify(stringConverter).convertToJavaData(Mockito.any()); + Mockito.verify(readConverter).convertToJavaData(Mockito.any()); } @Test @@ -215,14 +231,18 @@ void test_Priority_ContentProperty() throws Exception { Converter globalConverter = Mockito.mock(Converter.class); Converter customConverter = Mockito.mock(Converter.class); - converterMap.put(ConverterKeyBuild.buildKey(String.class, CellDataTypeEnum.STRING), globalConverter); + + Mockito.doReturn(String.class).when(globalConverter).supportJavaTypeKey(); + Mockito.doReturn(CellDataTypeEnum.STRING).when(globalConverter).supportExcelTypeKey(); + + converterRegistry.addCustomReadConverter(globalConverter); ExcelContentProperty property = Mockito.mock(ExcelContentProperty.class); Mockito.when(property.getConverter()).thenReturn(customConverter); Mockito.when(customConverter.convertToJavaData(Mockito.any())).thenReturn("Custom"); Object result = ConverterUtils.convertToJavaObject( - cellData, null, String.class, null, property, converterMap, context, 1, 0); + cellData, null, String.class, null, property, converterRegistry, context, 1, 0); Assertions.assertEquals("Custom", result); Mockito.verify(customConverter).convertToJavaData(Mockito.any()); @@ -236,7 +256,7 @@ void test_EmptyCell_NormalConverter() throws Exception, NoSuchFieldException { Mockito.when(property.getConverter()).thenReturn(stringConverter); Object result = ConverterUtils.convertToJavaObject( - cellData, null, String.class, null, property, converterMap, context, 1, 0); + cellData, null, String.class, null, property, converterRegistry, context, 1, 0); Assertions.assertNull(result); Mockito.verify(stringConverter, Mockito.never()).convertToJavaData(Mockito.any()); @@ -253,7 +273,7 @@ void test_EmptyCell_NullableConverter() throws Exception { Mockito.when(nullableConverter.convertToJavaData(Mockito.any())).thenReturn("HandledNull"); Object result = ConverterUtils.convertToJavaObject( - cellData, null, String.class, null, property, converterMap, context, 1, 0); + cellData, null, String.class, null, property, converterRegistry, context, 1, 0); Assertions.assertEquals("HandledNull", result); Mockito.verify(nullableConverter).convertToJavaData(Mockito.any()); @@ -262,13 +282,18 @@ void test_EmptyCell_NullableConverter() throws Exception { @Test void test_exception_wrapping() throws Exception { ReadCellData cellData = new ReadCellData<>("ErrorData"); - ConverterKeyBuild.ConverterKey key = ConverterKeyBuild.buildKey(String.class, CellDataTypeEnum.STRING); - converterMap.put(key, stringConverter); - Mockito.when(stringConverter.convertToJavaData(Mockito.any())).thenThrow(new RuntimeException("Inner error")); + ReadConverter readConverter = Mockito.mock(ReadConverter.class); + Mockito.doReturn(String.class).when(readConverter).supportJavaTypeKey(); + Mockito.doReturn(CellDataTypeEnum.STRING).when(readConverter).supportExcelTypeKey(); + + converterRegistry.addCustomReadConverter(readConverter); + + Mockito.when(readConverter.convertToJavaData(Mockito.any())).thenThrow(new RuntimeException("Inner error")); ExcelDataConvertException ex = Assertions.assertThrows(ExcelDataConvertException.class, () -> { - ConverterUtils.convertToJavaObject(cellData, null, String.class, null, null, converterMap, context, 99, 88); + ConverterUtils.convertToJavaObject( + cellData, null, String.class, null, null, converterRegistry, context, 99, 88); }); Assertions.assertEquals(99, ex.getRowIndex()); diff --git a/website/docs/sheet/advanced/custom-converter.md b/website/docs/sheet/advanced/custom-converter.md index 699bc90251..d971e3cc26 100644 --- a/website/docs/sheet/advanced/custom-converter.md +++ b/website/docs/sheet/advanced/custom-converter.md @@ -26,11 +26,19 @@ This chapter introduces how to create and register custom converters for both re ## Overview -Fesod provides a `Converter` interface that allows you to define custom data transformation logic. Converters can be registered per-field (via annotation) or globally (via builder), and work for both reading and writing. +Fesod splits conversion into two directions, each with its own interface: + +- `ReadConverter` — converts spreadsheet cell data into Java objects when reading. +- `WriteConverter` — converts Java objects into spreadsheet cell data when writing. +- `Converter` — combines both interfaces for two-way conversion. + +Converters can be registered per-field (via annotation), globally (via builder), or bound to specific columns (via `ColumnBinding`). ## Creating a Custom Converter -### Converter Implementation +### Read & Write: `Converter` + +Implement `Converter` when the same converter handles both reading and writing. ```java public class CustomStringStringConverter implements Converter { @@ -56,62 +64,94 @@ public class CustomStringStringConverter implements Converter { } ``` ---- +### Read-Only: `ReadConverter` -## Per-Field vs. Global Registration - -| Approach | Scope | How | -|:---|:---|:---| -| Per-field | Single field only | `@ExcelProperty(converter = MyConverter.class)` | -| Global | All fields matching Java type + Excel type | `.registerConverter(new MyConverter())` on the builder | - -### Converter Resolution Priority +Implement `ReadConverter` when only reading requires customization. -1. Field-level converter (`@ExcelProperty(converter = ...)`) — highest priority -2. Builder-level converter (`.registerConverter(...)`) with a matching lookup key -3. Built-in default converter — lowest priority +```java +public class CustomStringReadConverter implements ReadConverter { + @Override + public Class supportJavaTypeKey() { + return String.class; + } ---- + @Override + public CellDataTypeEnum supportExcelTypeKey() { + return CellDataTypeEnum.STRING; + } -## Global Registration Example + @Override + public String convertToJavaData(ReadConverterContext context) { + return "Custom: " + context.getReadCellData().getStringValue(); + } +} +``` -### Write with Global Converter +### Write-Only: `WriteConverter` -For ordinary XLSX/XLS writes, global converter lookup uses the Java type and a `null` Excel type key. Use the following variant of the converter above. The returned `WriteCellData` still determines the output cell type; returning `null` here only changes the lookup key. +Implement `WriteConverter` when only writing requires customization. ```java -public class GlobalStringWriteConverter extends CustomStringStringConverter { +public class CustomStringWriteConverter implements WriteConverter { @Override - public CellDataTypeEnum supportExcelTypeKey() { - return null; + public Class supportJavaTypeKey() { + return String.class; + } + + @Override + public WriteCellData convertToExcelData(WriteConverterContext context) { + return new WriteCellData<>("Custom: " + context.getValue()); } } ``` -For CSV writing and reading string cells, register the original `CustomStringStringConverter`, which returns `CellDataTypeEnum.STRING`. +--- -```java -@Test -public void customConverterWrite() { - String fileName = "customConverterWrite" + System.currentTimeMillis() + ".xlsx"; - - FesodSheet.write(fileName, DemoData.class) - .registerConverter(new GlobalStringWriteConverter()) - .sheet() - .doWrite(data()); -} -``` +## Column-Bound Converters (`ColumnBinding`) -### Read with Global Converter +By default, a globally registered converter applies to all eligible columns across the entire sheet. Implement `ColumnBinding` to restrict a converter (read or write) to specific column indices (0-based). + +This is especially useful in **no-bean** scenarios where you cannot use field annotations: ```java -@Test -public void customConverterRead() { - String fileName = "path/to/demo.xlsx"; - - FesodSheet.read(fileName, DemoData.class, new DemoDataListener()) - .registerConverter(new CustomStringStringConverter()) - .sheet() - .doRead(); +public class ColumnZeroStringWriteConverter implements WriteConverter, ColumnBinding { + @Override + public Integer columnIndex() { + return 0; // Binds exclusively to Column 0 + } + + @Override + public Class supportJavaTypeKey() { + return String.class; + } + + @Override + public WriteCellData convertToExcelData(WriteConverterContext context) { + return new WriteCellData<>("Col0: " + context.getValue()); + } } ``` + +- Override `columnIndex()` for a single column. +- Override `columnIndexes()` if you need to match multiple columns (e.g. `Set.of(1, 3)`). + +:::warning +If a converter implements `ColumnBinding` but returns `null` or an empty set, it will **match no columns at all** and will **not** fall back as a global converter. Do not implement `ColumnBinding` if you intend to create a global converter. +::: + +## Registration Scope & Precedence + +| Approach | Scope | How to Register | +|:-----------------|:-------------------|:----------------------------------------------------------------------------------------------| +| **Per-Field** | Single field | `@ExcelProperty(converter = MyConverter.class)` | +| **Column-Bound** | Target column(s) | Implement `ColumnBinding` + register on builder | +| **Global** | All matching types | `.registerReadConverter(...)`, `.registerWriteConverter(...)`,
`.registerConverter(...)` | + +### Converter Resolution Priority + +1. **Field-level** (`@ExcelProperty(converter = ...)`) +2. **Column-bound custom converter** (`ColumnBinding` matching target column index) +3. **Global custom converter** (if multiple match the same type, **the last one registered wins**) +4. **Built-in default converter** + +*Scope Rule: Registrations at the sheet-level take precedence over workbook-level registrations.* diff --git a/website/docs/sheet/read/converter.md b/website/docs/sheet/read/converter.md index b6ff7541ce..6d7fa72363 100644 --- a/website/docs/sheet/read/converter.md +++ b/website/docs/sheet/read/converter.md @@ -20,16 +20,13 @@ title: 'Converter' - limitations under the License. --> -# Format Conversion +# Read Data Conversion -Fesod supports date, number, and custom format conversions. +Fesod supports format conversion during data reading, allowing users to define custom data conversion rules to meet various business requirements. -## Overview +## Per-Field Converter -During usage, we may need to convert read or written data into specific formats. Fesod provides a flexible converter -mechanism that allows users to define custom data conversion rules to meet various business requirements. - -## Example +Apply a converter to a specific field using the `@ExcelProperty(converter = ...)` annotation. The annotation requires the combined `Converter` interface due to backward compatibility. ### POJO Class @@ -37,7 +34,7 @@ mechanism that allows users to define custom data conversion rules to meet vario @Getter @Setter @EqualsAndHashCode -public class ConverterData { +public class DemoData { @ExcelProperty(converter = CustomStringStringConverter.class) private String string; @@ -70,7 +67,7 @@ public class CustomStringStringConverter implements Converter { @Override public WriteCellData convertToExcelData(WriteConverterContext context) { - return new WriteCellData<>(context.getValue()); + return new WriteCellData<>("Custom: " + context.getValue()); } } ``` @@ -80,10 +77,111 @@ public class CustomStringStringConverter implements Converter { ```java @Test public void converterRead() { - String fileName = "path/to/demo.xlsx"; + String fileName = "path/from/demo.xlsx"; + + FesodSheet.read(fileName, DemoData.class, new DemoDataListener()) + .sheet() + .doRead(); +} +``` + +--- + +## Global Converter Registration + +Register a converter on the builder to apply it across all fields matching the Java type and Spreadsheet cell type. + +For read-only, implementing `ReadConverter` is recommended. + +### POJO Class + +```java +@Getter +@Setter +@EqualsAndHashCode +public class DemoData { + @ExcelProperty("String Title") + private String string; +} +``` + +### Converter + +```java +public class CustomStringReadConverter implements ReadConverter { + @Override + public Class supportJavaTypeKey() { + return String.class; + } + + @Override + public CellDataTypeEnum supportExcelTypeKey() { + return CellDataTypeEnum.STRING; + } + + @Override + public String convertToJavaData(ReadConverterContext context) { + return "Custom: " + context.getReadCellData().getStringValue(); + } +} +``` + +### Code Example + +```java +@Test +public void globalConverterRead() { + String fileName = "path/from/demo.xlsx"; + + FesodSheet.read(fileName, DemoData.class, new DemoDataListener()) + .registerReadConverter(new CustomStringReadConverter()) + .sheet() + .doRead(); +} +``` + +--- + +## Column-Bound Converter + +In no-bean scenarios where annotations cannot be used, implement `ColumnBinding` to restrict a read converter to specific columns (0-based): + +### Converter + +```java +public class ColumnZeroStringReadConverter implements ReadConverter, ColumnBinding { + @Override + public Integer columnIndex() { + return 0; // Only applies to column 0 + } + + @Override + public Class supportJavaTypeKey() { + return String.class; + } + + @Override + public CellDataTypeEnum supportExcelTypeKey() { + return CellDataTypeEnum.STRING; + } + + @Override + public String convertToJavaData(ReadConverterContext context) { + return "Col0: " + context.getReadCellData().getStringValue(); + } +} +``` + +### Code Example + +```java +@Test +public void columnBoundRead() { + String fileName = "path/from/demo.xlsx"; - FesodSheet.read(fileName, ConverterData.class, new DemoDataListener()) - .registerConverter(new CustomStringStringConverter()) // Register custom converter + FesodSheet.read(fileName, new NoModelDataListener()) + .head(HeadBuilder.forSimple("Custom String Title", "String Title")) + .registerReadConverter(new ColumnZeroStringReadConverter()) .sheet() .doRead(); } diff --git a/website/docs/sheet/write/converter.md b/website/docs/sheet/write/converter.md index 11204b0cf1..560d6eafa5 100644 --- a/website/docs/sheet/write/converter.md +++ b/website/docs/sheet/write/converter.md @@ -20,19 +20,33 @@ title: 'Converter' - limitations under the License. --> -# Converter +# Write Data Conversion -This chapter introduces how to use custom converters when writing data. +Fesod supports format conversion during data writing, allowing users to define custom data conversion rules to meet various business requirements. -## Overview +## Per-Field Converter -Fesod supports custom data converters for write operations. You can transform Java field values before they are written to Excel cells, enabling custom formatting, encryption, or any data transformation logic. +Apply a converter to a specific field using the `@ExcelProperty(converter = ...)` annotation. The annotation requires the combined `Converter` interface due to backward compatibility. -## Per-Field Converter +### POJO Class -### Overview +```java +@Getter +@Setter +@EqualsAndHashCode +public class DemoData { + @ExcelProperty(value = "String Title", converter = CustomStringStringConverter.class) + private String string; -Apply a converter to a specific field using the `@ExcelProperty(converter = ...)` annotation. + @DateTimeFormat("yyyy-MM-dd HH:mm:ss") + @ExcelProperty("Date Title") + private Date date; + + @NumberFormat("#.##%") + @ExcelProperty("Number Title") + private Double doubleData; +} +``` ### Converter @@ -60,33 +74,14 @@ public class CustomStringStringConverter implements Converter { } ``` -### POJO Class - -```java -@Getter -@Setter -@EqualsAndHashCode -public class ConverterData { - @ExcelProperty(value = "String Title", converter = CustomStringStringConverter.class) - private String string; - - @DateTimeFormat("yyyy-MM-dd HH:mm:ss") - @ExcelProperty("Date Title") - private Date date; - - @NumberFormat("#.##%") - @ExcelProperty("Number Title") - private Double doubleData; -} -``` - ### Code Example ```java @Test public void converterWrite() { - String fileName = "converterWrite" + System.currentTimeMillis() + ".xlsx"; - FesodSheet.write(fileName, ConverterData.class) + String fileName = "path/to/demo.xlsx"; + + FesodSheet.write(fileName, DemoData.class) .sheet() .doWrite(data()); } @@ -96,31 +91,47 @@ public void converterWrite() { ## Global Converter Registration -### Overview +Register a `WriteConverter` on the builder to apply it globally to all fields of matching Java type. -Register a converter at the builder level to apply it to ALL fields matching the Java type and Excel type. This is useful when you want the same transformation applied globally without annotating each field. +For write-only, implementing `WriteConverter` is recommended. -For ordinary XLSX/XLS writes, global converter lookup uses the Java type and a `null` Excel type key. Use the following variant of the converter above. The returned `WriteCellData` still determines the output cell type; returning `null` here only changes the lookup key. +### POJO Class ```java -public class GlobalStringWriteConverter extends CustomStringStringConverter { +@Getter +@Setter +@EqualsAndHashCode +public class DemoData { + @ExcelProperty("String Title") + private String string; +} +``` + +### Converter + +```java +public class CustomStringWriteConverter implements WriteConverter { @Override - public CellDataTypeEnum supportExcelTypeKey() { - return null; + public Class supportJavaTypeKey() { + return String.class; + } + + @Override + public WriteCellData convertToExcelData(WriteConverterContext context) { + return new WriteCellData<>("Custom: " + context.getValue()); } } ``` -For CSV writing and reading string cells, register the original `CustomStringStringConverter`, which returns `CellDataTypeEnum.STRING`. - ### Code Example ```java @Test public void globalConverterWrite() { - String fileName = "globalConverterWrite" + System.currentTimeMillis() + ".xlsx"; + String fileName = "path/to/demo.xlsx"; + FesodSheet.write(fileName, DemoData.class) - .registerConverter(new GlobalStringWriteConverter()) + .registerWriteConverter(new CustomStringWriteConverter()) .sheet() .doWrite(data()); } @@ -128,10 +139,42 @@ public void globalConverterWrite() { --- -## Converter Resolution Priority +## Column-Bound Converter -When multiple converters could apply to a field, Fesod resolves them in this order: +In no-bean scenarios where annotations cannot be used, implement `ColumnBinding` to restrict a write converter to specific columns (0-based): -1. Field-level converter (`@ExcelProperty(converter = ...)`) — highest priority -2. Builder-level converter (`.registerConverter(...)`) with a matching lookup key -3. Built-in default converter — lowest priority +### Converter + +```java +public class ColumnZeroStringWriteConverter implements WriteConverter, ColumnBinding { + @Override + public Integer columnIndex() { + return 0; // Only applies to column 0 + } + + @Override + public Class supportJavaTypeKey() { + return String.class; + } + + @Override + public WriteCellData convertToExcelData(WriteConverterContext context) { + return new WriteCellData<>("Col0: " + context.getValue()); + } +} +``` + +### Code Example + +```java +@Test +public void columnBoundWrite() { + String fileName = "path/to/demo.xlsx"; + + FesodSheet.write(fileName) + .head(HeadBuilder.forSimple("Custom String Title", "String Title")) + .registerWriteConverter(new ColumnZeroStringWriteConverter()) + .sheet() + .doWrite(data()); +} +``` diff --git a/website/i18n/zh-cn/docusaurus-plugin-content-docs/current/sheet/advanced/custom-converter.md b/website/i18n/zh-cn/docusaurus-plugin-content-docs/current/sheet/advanced/custom-converter.md index abe3247f1c..7535280c8b 100644 --- a/website/i18n/zh-cn/docusaurus-plugin-content-docs/current/sheet/advanced/custom-converter.md +++ b/website/i18n/zh-cn/docusaurus-plugin-content-docs/current/sheet/advanced/custom-converter.md @@ -26,11 +26,19 @@ title: '自定义转换器' ## 概述 -Fesod 提供了 `Converter` 接口,允许您定义自定义数据转换逻辑。转换器可以按字段注册(通过注解)或全局注册(通过构建器),同时适用于读取和写入。 +Fesod 将转换拆分为两个方向,各自拥有独立的接口: + +- `ReadConverter` —— 读取时将电子表格单元格数据转换为 Java 对象。 +- `WriteConverter` —— 写入时将 Java 对象转换为电子表格单元格数据。 +- `Converter` —— 组合以上两个接口,实现双向转换。 + +转换器可以按字段注册(通过注解)、全局注册(通过构建器),或绑定到特定列(通过 `ColumnBinding`)。 ## 创建自定义转换器 -### 转换器实现 +### 读写双向:`Converter` + +当同一个转换器需要同时处理读取和写入时,实现 `Converter`。 ```java public class CustomStringStringConverter implements Converter { @@ -56,62 +64,94 @@ public class CustomStringStringConverter implements Converter { } ``` ---- +### 仅读取:`ReadConverter` -## 字段级与全局注册 - -| 方式 | 作用范围 | 使用方法 | -|:---|:---|:---| -| 字段级 | 仅单个字段 | `@ExcelProperty(converter = MyConverter.class)` | -| 全局 | 所有匹配 Java 类型 + Excel 类型的字段 | 在构建器上使用 `.registerConverter(new MyConverter())` | - -### 转换器解析优先级 +当只需要自定义读取逻辑时,实现 `ReadConverter`。 -1. 字段级转换器(`@ExcelProperty(converter = ...)`)— 最高优先级 -2. 查找键匹配的构建器级转换器(`.registerConverter(...)`) -3. 内置默认转换器 — 最低优先级 +```java +public class CustomStringReadConverter implements ReadConverter { + @Override + public Class supportJavaTypeKey() { + return String.class; + } ---- + @Override + public CellDataTypeEnum supportExcelTypeKey() { + return CellDataTypeEnum.STRING; + } -## 全局注册示例 + @Override + public String convertToJavaData(ReadConverterContext context) { + return "自定义:" + context.getReadCellData().getStringValue(); + } +} +``` -### 使用全局转换器写入 +### 仅写入:`WriteConverter` -普通 XLSX/XLS 写入按 Java 类型和 `null` Excel 类型键查找全局转换器,因此需要使用上述转换器的以下变体。输出单元格类型仍由返回的 `WriteCellData` 决定;此处返回 `null` 仅改变查找键。 +当只需要自定义写入逻辑时,实现 `WriteConverter`。 ```java -public class GlobalStringWriteConverter extends CustomStringStringConverter { +public class CustomStringWriteConverter implements WriteConverter { @Override - public CellDataTypeEnum supportExcelTypeKey() { - return null; + public Class supportJavaTypeKey() { + return String.class; + } + + @Override + public WriteCellData convertToExcelData(WriteConverterContext context) { + return new WriteCellData<>("自定义:" + context.getValue()); } } ``` -写入 CSV 或读取字符串单元格时,请注册返回 `CellDataTypeEnum.STRING` 的原始 `CustomStringStringConverter`。 +--- -```java -@Test -public void customConverterWrite() { - String fileName = "customConverterWrite" + System.currentTimeMillis() + ".xlsx"; - - FesodSheet.write(fileName, DemoData.class) - .registerConverter(new GlobalStringWriteConverter()) - .sheet() - .doWrite(data()); -} -``` +## 列绑定转换器(`ColumnBinding`) -### 使用全局转换器读取 +默认情况下,全局注册的转换器会作用于整个工作表中所有符合条件的列。实现 `ColumnBinding` 可以将转换器(读取或写入)限定到特定的列索引(从 0 开始)。 + +在无法使用字段注解的**无实体类**场景下尤其有用: ```java -@Test -public void customConverterRead() { - String fileName = "path/to/demo.xlsx"; - - FesodSheet.read(fileName, DemoData.class, new DemoDataListener()) - .registerConverter(new CustomStringStringConverter()) - .sheet() - .doRead(); +public class ColumnZeroStringWriteConverter implements WriteConverter, ColumnBinding { + @Override + public Integer columnIndex() { + return 0; // 仅绑定到第 0 列 + } + + @Override + public Class supportJavaTypeKey() { + return String.class; + } + + @Override + public WriteCellData convertToExcelData(WriteConverterContext context) { + return new WriteCellData<>("第0列:" + context.getValue()); + } } ``` + +- 绑定单个列时覆写 `columnIndex()`。 +- 需要匹配多个列时覆写 `columnIndexes()`(例如 `Set.of(1, 3)`)。 + +:::warning +如果转换器实现了 `ColumnBinding` 但返回 `null` 或空集合,它将**匹配不到任何列**,也**不会**退化为全局转换器。如果要创建全局转换器,请不要实现 `ColumnBinding`。 +::: + +## 注册范围与优先级 + +| 方式 | 作用范围 | 注册方法 | +|:-----------|:---------------|:-----------------------------------------------------------------------------------------------| +| **字段级** | 单个字段 | `@ExcelProperty(converter = MyConverter.class)` | +| **列绑定** | 目标列 | 实现 `ColumnBinding` 并在构建器上注册 | +| **全局** | 所有匹配的类型 | `.registerReadConverter(...)`、`.registerWriteConverter(...)`、
`.registerConverter(...)` | + +### 转换器解析优先级 + +1. **字段级**(`@ExcelProperty(converter = ...)`) +2. **列绑定自定义转换器**(匹配目标列索引的 `ColumnBinding`) +3. **全局自定义转换器**(同一类型匹配多个转换器时,**后注册的优先**) +4. **内置默认转换器** + +*范围规则:工作表(sheet)级注册优先于工作簿(workbook)级注册。* 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 19e7748302..dd3bb70d70 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 @@ -1,6 +1,6 @@ --- id: 'converter' -title: '格式转换' +title: '读取数据转换' --- -# 格式转换 +# 读取数据转换 -Fesod 支持日期、数字、自定义格式转换。 +Fesod 支持读取数据过程中的格式转换,允许用户自定义数据转换规则,以满足各种业务需求。 -## 概述 +## 字段级转换器 -在使用过程中,我们可能需要对读取或写入的数据进行特定格式的转换。Fesod 提供了灵活的转换器机制,允许用户自定义数据转换规则,以满足各种业务需求。 - -## 示例 +通过 `@ExcelProperty(converter = ...)` 注解对特定字段应用转换器。出于向后兼容的考虑,该注解要求实现组合的 `Converter` 接口。 ### POJO 类 @@ -36,11 +34,11 @@ Fesod 支持日期、数字、自定义格式转换。 @Getter @Setter @EqualsAndHashCode -public class ConverterData { +public class DemoData { @ExcelProperty(converter = CustomStringStringConverter.class) private String string; - @DateTimeFormat("yyyy年MM月dd日HH时mm分ss秒") + @DateTimeFormat("yyyyMMddHHmmss") private String date; @NumberFormat("#.##%") @@ -69,7 +67,7 @@ public class CustomStringStringConverter implements Converter { @Override public WriteCellData convertToExcelData(WriteConverterContext context) { - return new WriteCellData<>(context.getValue()); + return new WriteCellData<>("自定义:" + context.getValue()); } } ``` @@ -79,10 +77,111 @@ public class CustomStringStringConverter implements Converter { ```java @Test public void converterRead() { - String fileName = "path/to/demo.xlsx"; + String fileName = "path/from/demo.xlsx"; + + FesodSheet.read(fileName, DemoData.class, new DemoDataListener()) + .sheet() + .doRead(); +} +``` + +--- + +## 全局转换器注册 + +在构建器上注册转换器,使其应用于所有匹配 Java 类型和 Spreadsheet 单元格类型的字段。 + +仅读取场景推荐实现 `ReadConverter`。 + +### POJO 类 + +```java +@Getter +@Setter +@EqualsAndHashCode +public class DemoData { + @ExcelProperty("字符串标题") + private String string; +} +``` + +### 转换器 + +```java +public class CustomStringReadConverter implements ReadConverter { + @Override + public Class supportJavaTypeKey() { + return String.class; + } + + @Override + public CellDataTypeEnum supportExcelTypeKey() { + return CellDataTypeEnum.STRING; + } + + @Override + public String convertToJavaData(ReadConverterContext context) { + return "自定义:" + context.getReadCellData().getStringValue(); + } +} +``` + +### 代码示例 + +```java +@Test +public void globalConverterRead() { + String fileName = "path/from/demo.xlsx"; + + FesodSheet.read(fileName, DemoData.class, new DemoDataListener()) + .registerReadConverter(new CustomStringReadConverter()) + .sheet() + .doRead(); +} +``` + +--- + +## 列绑定转换器 + +在无法使用字段注解的无实体类场景下,实现 `ColumnBinding` 可以将读取转换器限定到特定列(从 0 开始): + +### 转换器 + +```java +public class ColumnZeroStringReadConverter implements ReadConverter, ColumnBinding { + @Override + public Integer columnIndex() { + return 0; // 仅应用于第 0 列 + } + + @Override + public Class supportJavaTypeKey() { + return String.class; + } + + @Override + public CellDataTypeEnum supportExcelTypeKey() { + return CellDataTypeEnum.STRING; + } + + @Override + public String convertToJavaData(ReadConverterContext context) { + return "第0列:" + context.getReadCellData().getStringValue(); + } +} +``` + +### 代码示例 + +```java +@Test +public void columnBoundRead() { + String fileName = "path/from/demo.xlsx"; - FesodSheet.read(fileName, ConverterData.class, new DemoDataListener()) - .registerConverter(new CustomStringStringConverter()) // 注册自定义转换器 + FesodSheet.read(fileName, new NoModelDataListener()) + .head(HeadBuilder.forSimple("自定义字符串标题", "字符串标题")) + .registerReadConverter(new ColumnZeroStringReadConverter()) .sheet() .doRead(); } diff --git a/website/i18n/zh-cn/docusaurus-plugin-content-docs/current/sheet/write/converter.md b/website/i18n/zh-cn/docusaurus-plugin-content-docs/current/sheet/write/converter.md index 146ef552cd..44f193f741 100644 --- a/website/i18n/zh-cn/docusaurus-plugin-content-docs/current/sheet/write/converter.md +++ b/website/i18n/zh-cn/docusaurus-plugin-content-docs/current/sheet/write/converter.md @@ -1,6 +1,6 @@ --- id: 'converter' -title: '转换器' +title: '写入数据转换' --- -# 转换器 +# 写入数据转换 -本章节介绍写入数据时如何使用自定义转换器。 +Fesod 支持写入数据过程中的格式转换,允许用户自定义数据转换规则,以满足各种业务需求。 -## 概述 +## 字段级转换器 -Fesod 支持写入操作的自定义数据转换器。您可以在 Java 字段值写入 Excel 单元格之前进行转换,实现自定义格式化、加密或任何数据转换逻辑。 +通过 `@ExcelProperty(converter = ...)` 注解对特定字段应用转换器。出于向后兼容的考虑,该注解要求实现组合的 `Converter` 接口。 -## 字段级转换器 +### POJO 类 -### 概述 +```java +@Getter +@Setter +@EqualsAndHashCode +public class DemoData { + @ExcelProperty(value = "字符串标题", converter = CustomStringStringConverter.class) + private String string; -通过 `@ExcelProperty(converter = ...)` 注解对特定字段应用转换器。 + @DateTimeFormat("yyyy-MM-dd HH:mm:ss") + @ExcelProperty("日期标题") + private Date date; + + @NumberFormat("#.##%") + @ExcelProperty("数字标题") + private Double doubleData; +} +``` ### 转换器 @@ -60,33 +74,14 @@ public class CustomStringStringConverter implements Converter { } ``` -### POJO 类 - -```java -@Getter -@Setter -@EqualsAndHashCode -public class ConverterData { - @ExcelProperty(value = "字符串标题", converter = CustomStringStringConverter.class) - private String string; - - @DateTimeFormat("yyyy年MM月dd日HH时mm分ss秒") - @ExcelProperty("日期标题") - private Date date; - - @NumberFormat("#.##%") - @ExcelProperty("数字标题") - private Double doubleData; -} -``` - ### 代码示例 ```java @Test public void converterWrite() { - String fileName = "converterWrite" + System.currentTimeMillis() + ".xlsx"; - FesodSheet.write(fileName, ConverterData.class) + String fileName = "path/to/demo.xlsx"; + + FesodSheet.write(fileName, DemoData.class) .sheet() .doWrite(data()); } @@ -96,31 +91,47 @@ public void converterWrite() { ## 全局转换器注册 -### 概述 +在构建器上注册 `WriteConverter`,使其全局应用于所有匹配 Java 类型的字段。 -在构建器级别注册转换器,将其应用于所有匹配 Java 类型和 Excel 类型的字段。当您希望对所有字段统一应用相同的转换逻辑时非常有用,无需逐一注解。 +仅写入场景推荐实现 `WriteConverter`。 -普通 XLSX/XLS 写入按 Java 类型和 `null` Excel 类型键查找全局转换器,因此需要使用上述转换器的以下变体。输出单元格类型仍由返回的 `WriteCellData` 决定;此处返回 `null` 仅改变查找键。 +### POJO 类 ```java -public class GlobalStringWriteConverter extends CustomStringStringConverter { +@Getter +@Setter +@EqualsAndHashCode +public class DemoData { + @ExcelProperty("字符串标题") + private String string; +} +``` + +### 转换器 + +```java +public class CustomStringWriteConverter implements WriteConverter { @Override - public CellDataTypeEnum supportExcelTypeKey() { - return null; + public Class supportJavaTypeKey() { + return String.class; + } + + @Override + public WriteCellData convertToExcelData(WriteConverterContext context) { + return new WriteCellData<>("自定义:" + context.getValue()); } } ``` -写入 CSV 或读取字符串单元格时,请注册返回 `CellDataTypeEnum.STRING` 的原始 `CustomStringStringConverter`。 - ### 代码示例 ```java @Test public void globalConverterWrite() { - String fileName = "globalConverterWrite" + System.currentTimeMillis() + ".xlsx"; + String fileName = "path/to/demo.xlsx"; + FesodSheet.write(fileName, DemoData.class) - .registerConverter(new GlobalStringWriteConverter()) + .registerWriteConverter(new CustomStringWriteConverter()) .sheet() .doWrite(data()); } @@ -128,10 +139,42 @@ public void globalConverterWrite() { --- -## 转换器解析优先级 +## 列绑定转换器 + +在无法使用字段注解的无实体类场景下,实现 `ColumnBinding` 可以将写入转换器限定到特定列(从 0 开始): + +### 转换器 + +```java +public class ColumnZeroStringWriteConverter implements WriteConverter, ColumnBinding { + @Override + public Integer columnIndex() { + return 0; // 仅应用于第 0 列 + } + + @Override + public Class supportJavaTypeKey() { + return String.class; + } + + @Override + public WriteCellData convertToExcelData(WriteConverterContext context) { + return new WriteCellData<>("第0列:" + context.getValue()); + } +} +``` -当多个转换器可能应用于某个字段时,Fesod 按以下顺序进行解析: +### 代码示例 -1. 字段级转换器(`@ExcelProperty(converter = ...)`)— 最高优先级 -2. 查找键匹配的构建器级转换器(`.registerConverter(...)`) -3. 内置默认转换器 — 最低优先级 +```java +@Test +public void columnBoundWrite() { + String fileName = "path/to/demo.xlsx"; + + FesodSheet.write(fileName) + .head(HeadBuilder.forSimple("自定义字符串标题", "字符串标题")) + .registerWriteConverter(new ColumnZeroStringWriteConverter()) + .sheet() + .doWrite(data()); +} +```