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 000000000..dbb4b27fc --- /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 000000000..466d942a6 --- /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 affb88631..d88c2dff8 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 f17d9b62c..56370d61b 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 @@ -86,6 +86,7 @@ */ public class DefaultConverterLoader { private static Map> defaultWriteConverter; + private static Map> defaultWriteStringConverter; private static Map> allConverter; static { @@ -171,8 +172,10 @@ private static void initDefaultWriteConverter() { putWriteConverter(new ByteArrayImageConverter()); putWriteConverter(new BoxingByteArrayImageConverter()); putWriteConverter(new UrlImageConverter()); + 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()); @@ -187,7 +190,7 @@ private static void initDefaultWriteConverter() { putWriteStringConverter(new LongStringConverter()); putWriteStringConverter(new ShortStringConverter()); putWriteStringConverter(new StringStringConverter()); - defaultWriteConverter = Collections.unmodifiableMap(defaultWriteConverter); + defaultWriteStringConverter = Collections.unmodifiableMap(defaultWriteStringConverter); } /** @@ -208,13 +211,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 000000000..18e2039f6 --- /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 000000000..c1659e419 --- /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 35b63aff4..8bcc4330e 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,13 +60,8 @@ public abstract class AbstractHolder implements ConfigurationHolder { * Some global variables */ private GlobalConfiguration globalConfiguration; - /** - *

- * Read key: - *

- * Write key: - */ - private Map> converterMap; + + private CellDataConverterRegistry converterRegistry; public AbstractHolder(BasicParameter basicParameter, AbstractHolder prentAbstractHolder) { this.newInitialization = Boolean.TRUE; @@ -125,11 +120,32 @@ public AbstractHolder(BasicParameter basicParameter, AbstractHolder prentAbstrac } else { globalConfiguration.setFiledCacheLocation(basicParameter.getFiledCacheLocation()); } + + if (prentAbstractHolder == null) { + this.converterRegistry = new CellDataConverterRegistry(); + } else { + this.converterRegistry = new CellDataConverterRegistry(prentAbstractHolder.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 de25c3c08..440db28ad 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 19a2bece0..be878aaae 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 79d19a05a..dc6c3a376 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 d1e32586d..295de65cd 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 d6a4f19f6..2a0f1b37e 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 dacebf880..acf775752 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 f7b58be88..83d9fc16d 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 35cc5c1d2..28df3c658 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,10 @@ public class CsvReadWorkbookHolder extends ReadWorkbookHolder { public CsvReadWorkbookHolder(ReadWorkbook readWorkbook) { super(readWorkbook); setExcelType(ExcelTypeEnum.CSV); + + // init default converters + 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 ea97a906c..608add446 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,10 @@ public XlsReadWorkbookHolder(ReadWorkbook readWorkbook) { this.boundSheetRecordList = new ArrayList(); this.needReadSheet = Boolean.TRUE; setExcelType(ExcelTypeEnum.XLS); + + // init default converters + 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 d33766ee9..5bab74ee3 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,10 @@ public XlsxReadWorkbookHolder(ReadWorkbook readWorkbook) { super(readWorkbook); this.saxParserFactoryName = readWorkbook.getXlsxSAXParserFactoryName(); setExcelType(ExcelTypeEnum.XLSX); + + // init default converters + 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 007c3e6a8..a1942ef30 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 012e5b3ae..2cc816f07 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 48b0fc596..ccebf42a5 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 5dd959e71..a4fee0cf4 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 e658b5d8e..d22057297 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 021dd8523..e28e269f3 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,9 @@ public WriteWorkbookHolder(WriteWorkbook writeWorkbook) { this.excelType = writeWorkbook.getExcelType(); } + // init default converters + 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 000000000..295b5dc81 --- /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 f838c7c5f..02c5e73ca 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; @@ -39,7 +39,6 @@ import org.apache.fesod.sheet.testkit.base.AbstractExcelTest; import org.apache.fesod.sheet.testkit.builders.TestDataBuilder; import org.apache.fesod.sheet.write.builder.ExcelWriterSheetBuilder; -import org.apache.fesod.sheet.write.metadata.holder.WriteSheetHolder; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; @@ -52,18 +51,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(toStringConverter, timestampStringConverter); + Assertions.assertEquals(toNumberConverter, timestampNumberConverter); + } } @Test @@ -81,29 +86,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"); @@ -130,14 +112,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/TimestampNumberConverter.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/converter/TimestampNumberConverter.java index d8134cf67..84a49dff7 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 3688ead82..e829c8f07 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 000000000..059698a86 --- /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 a30708a52..a21d17ec7 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 @@ -62,9 +62,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))); } private static void assertLoadIsImmutableAndCopyIsMutable( 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 03cb3fc82..5ff3e3235 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 a4c30bc42..3322b09e7 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; @@ -66,15 +67,15 @@ class ConverterUtilsTest { @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 +86,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 +107,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 +154,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 +171,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 +194,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 +214,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 +234,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 +259,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 +276,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 +285,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());