Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<ConverterKeyBuild.ConverterKey, Converter<?>> TO_STRING_CONVERTERS =
DefaultConverterLoader.loadDefaultWriteStringConverter();

private final Map<ConverterKeyBuild.ConverterKey, Deque<WriteConverter<?>>> customWriteConverters =
new LinkedHashMap<>();
private final Map<ConverterKeyBuild.ConverterKey, Deque<ReadConverter<?>>> customReadConverters =
new LinkedHashMap<>();

private final Map<ConverterKeyBuild.ConverterKey, Converter<?>> defaultConverters = new LinkedHashMap<>();

private final Map<WriteCacheKey, ConverterHolder<WriteConverter<?>>> writeConvertersCache =
new ConcurrentHashMap<>(40);
private final Map<ReadCacheKey, ConverterHolder<ReadConverter<?>>> readConvertersCache =
new ConcurrentHashMap<>(40);

public CellDataConverterRegistry() {
this(null);
}

public CellDataConverterRegistry(CellDataConverterRegistry parent) {
this.parent = parent;
}

/**
* Registers the default converters.
*/
public void addDefaultConverters(Map<ConverterKeyBuild.ConverterKey, Converter<?>> 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<WriteConverter<?>> 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.
*
* <p>
* Priority: current registry (column‑bound converters take precedence over global converters),
* then parent registry, and finally default converters.
* </p>
*
* @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<ReadConverter<?>> 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.
*
* <p>
* Priority: current registry (column‑bound converters take precedence over global converters),
* then parent registry, and finally default converters.
* </p>
*
* @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.
*
* <p>
* Priority: column‑bound converter take precedence over global converter.
* </p>
*/
private <T> T selectConverter(Deque<T> 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<Integer> 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<T> {
private final T converter;

ConverterHolder(T converter) {
this.converter = converter;
}

boolean isNull() {
return converter == null;
}
}
}
Original file line number Diff line number Diff line change
@@ -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).
* <p>
* 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.
* </p>
* <b>WARNING:</b> Providing no valid column index ({@code null} or an empty set) renders this converter
* completely ineffective, it will match no column and will <b>not</b> 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<Integer> columnIndexes() {
Integer result = columnIndex();
return result == null ? Collections.emptySet() : Collections.singleton(result);
}
}
Loading
Loading