diff --git a/src/main/java/org/springframework/data/convert/CustomConversions.java b/src/main/java/org/springframework/data/convert/CustomConversions.java index f1bbd2dc3..fdf7ced74 100644 --- a/src/main/java/org/springframework/data/convert/CustomConversions.java +++ b/src/main/java/org/springframework/data/convert/CustomConversions.java @@ -22,7 +22,16 @@ import lombok.RequiredArgsConstructor; import lombok.Value; import lombok.extern.slf4j.Slf4j; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; @@ -70,25 +79,23 @@ public class CustomConversions { DEFAULT_CONVERTERS = Collections.unmodifiableList(defaults); } - private final Set readingPairs = new LinkedHashSet<>(); - private final Set writingPairs = new LinkedHashSet<>(); - private final Set> customSimpleTypes; private final SimpleTypeHolder simpleTypeHolder; - private final List converters; - private final Map>> customReadTargetTypes; - private final Map>> customWriteTargetTypes; - private final Map, Optional>> rawWriteTargetTypes; + private final Set readingPairs = new LinkedHashSet<>(); + private final Set writingPairs = new LinkedHashSet<>(); + private final Set> customSimpleTypes = new HashSet<>(); + private final ConversionTargetsCache customReadTargetTypes = new ConversionTargetsCache(); + private final ConversionTargetsCache customWriteTargetTypes = new ConversionTargetsCache(); - private final Function>> getCustomReadTarget = it -> getCustomTarget( - it.getSourceType(), Optional.of(it.getTargetType()), readingPairs); + private final Function>> getReadTarget = convertiblePair -> getCustomTarget( + convertiblePair.getSourceType(), Optional.of(convertiblePair.getTargetType()), readingPairs); - private final Function, Optional>> getCustomWriteTargetForSource = it -> getCustomTarget(it, - Optional.empty(), writingPairs); + private Function>> getWriteTarget = convertiblePair -> getCustomTarget( + convertiblePair.getSourceType(), Optional.of(convertiblePair.getTargetType()), writingPairs); - private final Function>> getCustomWriteTarget = it -> getCustomTarget( - it.getSourceType(), Optional.of(it.getTargetType()), writingPairs); + private Function>> getRawWriteTarget = convertiblePair -> getCustomTarget( + convertiblePair.getSourceType(), Optional.empty(), writingPairs); /** * Creates a new {@link CustomConversions} instance registering the given converters. @@ -101,12 +108,7 @@ public class CustomConversions { Assert.notNull(storeConversions, "StoreConversions must not be null!"); Assert.notNull(converters, "List of converters must not be null!"); - this.customSimpleTypes = new HashSet<>(); - this.customReadTargetTypes = new ConcurrentHashMap<>(); - this.customWriteTargetTypes = new ConcurrentHashMap<>(); - this.rawWriteTargetTypes = new ConcurrentHashMap<>(); - - List toRegister = new ArrayList(); + List toRegister = new ArrayList<>(); // Add user provided converters to make sure they can override the defaults toRegister.addAll(converters); @@ -237,7 +239,7 @@ public class CustomConversions { Assert.notNull(sourceType, "Source type must not be null!"); - return rawWriteTargetTypes.computeIfAbsent(sourceType, getCustomWriteTargetForSource); + return customWriteTargetTypes.computeIfAbsent(sourceType, getRawWriteTarget); } /** @@ -254,8 +256,7 @@ public class CustomConversions { Assert.notNull(sourceType, "Source type must not be null!"); Assert.notNull(requestedTargetType, "Target type must not be null!"); - return customWriteTargetTypes.computeIfAbsent(new ConvertiblePair(sourceType, requestedTargetType), - getCustomWriteTarget); + return customWriteTargetTypes.computeIfAbsent(sourceType, requestedTargetType, getWriteTarget); } /** @@ -313,8 +314,7 @@ public class CustomConversions { * @return */ private Optional> getCustomReadTarget(Class sourceType, Class targetType) { - - return customReadTargetTypes.computeIfAbsent(new ConvertiblePair(sourceType, targetType), getCustomReadTarget); + return customReadTargetTypes.computeIfAbsent(sourceType, targetType, getReadTarget); } /** @@ -349,7 +349,82 @@ public class CustomConversions { return !requestedTargetType.isPresent() // ? true // - : requestedTargetType.map(it -> targetType.isAssignableFrom(it)).orElse(false); + : requestedTargetType.map(targetType::isAssignableFrom).orElse(false); + } + + /** + * Value object to cache custom conversion targets. + * + * @author Mark Paluch + */ + static class ConversionTargetsCache { + + private final Map, TargetTypes> customReadTargetTypes = new ConcurrentHashMap<>(); + + /** + * Get or compute a target type given its {@code sourceType}. Returns a cached {@link Optional} if the value + * (present/absent target) was computed once. Otherwise, uses a {@link Function mappingFunction} to determine a + * possibly existing target type. + * + * @param sourceType must not be {@literal null}. + * @param mappingFunction must not be {@literal null}. + * @return the optional target type. + */ + public Optional> computeIfAbsent(Class sourceType, + Function>> mappingFunction) { + return computeIfAbsent(sourceType, Object.class, mappingFunction); + } + + /** + * Get or compute a target type given its {@code sourceType} and {@code targetType}. Returns a cached + * {@link Optional} if the value (present/absent target) was computed once. Otherwise, uses a {@link Function + * mappingFunction} to determine a possibly existing target type. + * + * @param sourceType must not be {@literal null}. + * @param targetType must not be {@literal null}. + * @param mappingFunction must not be {@literal null}. + * @return the optional target type. + */ + public Optional> computeIfAbsent(Class sourceType, Class targetType, + Function>> mappingFunction) { + + TargetTypes targetTypes = customReadTargetTypes.computeIfAbsent(sourceType, TargetTypes::new); + return targetTypes.computeIfAbsent(targetType, mappingFunction); + } + } + + /** + * Value object for a specific {@code Class source type} to determine possible target conversion types. + * + * @author Mark Paluch + */ + @RequiredArgsConstructor + static class TargetTypes { + + private final @NonNull Class sourceType; + private final Map, Optional>> conversionTargets = new ConcurrentHashMap<>(); + + /** + * Get or compute a target type given its {@code targetType}. Returns a cached {@link Optional} if the value + * (present/absent target) was computed once. Otherwise, uses a {@link Function mappingFunction} to determine a + * possibly existing target type. + * + * @param targetType must not be {@literal null}. + * @param mappingFunction must not be {@literal null}. + * @return the optional target type. + */ + public Optional> computeIfAbsent(Class targetType, + Function>> mappingFunction) { + + Optional> optionalTarget = conversionTargets.get(targetType); + + if (optionalTarget == null) { + optionalTarget = mappingFunction.apply(new ConvertiblePair(sourceType, targetType)); + conversionTargets.put(targetType, optionalTarget); + } + + return optionalTarget; + } } /** @@ -359,7 +434,7 @@ public class CustomConversions { * @author Mark Paluch */ @RequiredArgsConstructor(access = AccessLevel.PRIVATE) - private static class ConverterRegistration { + static class ConverterRegistration { private final @NonNull ConvertiblePair convertiblePair; private final @NonNull StoreConversions storeConversions;