diff --git a/src/main/java/org/springframework/data/convert/CustomConversions.java b/src/main/java/org/springframework/data/convert/CustomConversions.java index 59a787f72..faa169c5e 100644 --- a/src/main/java/org/springframework/data/convert/CustomConversions.java +++ b/src/main/java/org/springframework/data/convert/CustomConversions.java @@ -43,10 +43,12 @@ import org.springframework.core.convert.converter.GenericConverter; import org.springframework.core.convert.converter.GenericConverter.ConvertiblePair; import org.springframework.core.convert.support.GenericConversionService; import org.springframework.data.convert.ConverterBuilder.ConverterAware; +import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.model.SimpleTypeHolder; import org.springframework.data.util.Predicates; import org.springframework.data.util.Streamable; import org.springframework.data.util.VavrCollectionConverters; +import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -67,13 +69,13 @@ import org.springframework.util.ObjectUtils; public class CustomConversions { private static final Log logger = LogFactory.getLog(CustomConversions.class); - private static final String READ_CONVERTER_NOT_SIMPLE = "Registering converter from %s to %s as reading converter although it doesn't convert from a store-supported type! You might want to check your annotation setup at the converter implementation."; - private static final String WRITE_CONVERTER_NOT_SIMPLE = "Registering converter from %s to %s as writing converter although it doesn't convert to a store-supported type! You might want to check your annotation setup at the converter implementation."; - private static final String NOT_A_CONVERTER = "Converter %s is neither a Spring Converter, GenericConverter or ConverterFactory!"; + private static final String READ_CONVERTER_NOT_SIMPLE = "Registering converter from %s to %s as reading converter although it doesn't convert from a store-supported type; You might want to check your annotation setup at the converter implementation."; + private static final String WRITE_CONVERTER_NOT_SIMPLE = "Registering converter from %s to %s as writing converter although it doesn't convert to a store-supported type; You might want to check your annotation setup at the converter implementation."; + private static final String NOT_A_CONVERTER = "Converter %s is neither a Spring Converter, GenericConverter or ConverterFactory"; private static final String CONVERTER_FILTER = "converter from %s to %s as %s converter."; private static final String ADD_CONVERTER = "Adding %s" + CONVERTER_FILTER; private static final String SKIP_CONVERTER = "Skipping " + CONVERTER_FILTER - + " %s is not a store supported simple type!"; + + " %s is not a store supported simple type"; private static final List DEFAULT_CONVERTERS; static { @@ -106,23 +108,24 @@ public class CustomConversions { private final Function> getRawWriteTarget = convertiblePair -> getCustomTarget( convertiblePair.getSourceType(), null, writingPairs); - private @Nullable PropertyValueConversions propertyValueConversions; + @Nullable + private final PropertyValueConversions propertyValueConversions; /** * @param converterConfiguration the {@link ConverterConfiguration} to apply. * @since 2.3 */ - public CustomConversions(ConverterConfiguration converterConfiguration) { + public CustomConversions(@NonNull ConverterConfiguration converterConfiguration) { this.converterConfiguration = converterConfiguration; List registeredConverters = collectPotentialConverterRegistrations( - converterConfiguration.getStoreConversions(), converterConfiguration.getUserConverters()).stream() // - .filter(this::isSupportedConverter) // - .filter(this::shouldRegister) // - .map(ConverterRegistrationIntent::getConverterRegistration) // - .map(this::register) // - .distinct() // + converterConfiguration.getStoreConversions(), converterConfiguration.getUserConverters()).stream() + .filter(this::isSupportedConverter) + .filter(this::shouldRegister) + .map(ConverterRegistrationIntent::getConverterRegistration) + .map(this::register) + .distinct() .collect(Collectors.toList()); Collections.reverse(registeredConverters); @@ -142,30 +145,60 @@ public class CustomConversions { * @param storeConversions must not be {@literal null}. * @param converters must not be {@literal null}. */ - public CustomConversions(StoreConversions storeConversions, Collection converters) { + public CustomConversions(@NonNull StoreConversions storeConversions, @NonNull Collection converters) { this(new ConverterConfiguration(storeConversions, new ArrayList<>(converters))); } /** * Returns the underlying {@link SimpleTypeHolder}. * - * @return + * @return the underlying {@link SimpleTypeHolder}. + * @see SimpleTypeHolder */ - public SimpleTypeHolder getSimpleTypeHolder() { + public @NonNull SimpleTypeHolder getSimpleTypeHolder() { return simpleTypeHolder; } + /** + * Determines whether the given, required {@link PersistentProperty property} has a value-specific converter + * registered. Returns {@literal false} if no {@link PropertyValueConversions} have been configured for the + * underlying store. + *

+ * This method protects against {@literal null} when not {@link PropertyValueConversions} have been configured for + * the underlying data store, and is a shortcut for: + * + * + * customConversions.getPropertyValueConversions().hasValueConverter(property); + * + * + * @param property {@link PersistentProperty} to evaluate; must not be {@literal null}. + * @return a boolean value indicating whether {@link PropertyValueConverter} has been configured and registered + * for the {@link PersistentProperty property}. + * @see PropertyValueConversions#hasValueConverter(PersistentProperty) + * @see #getPropertyValueConversions() + * @see PersistentProperty + */ + public boolean hasValueConverter(@NonNull PersistentProperty property) { + + PropertyValueConversions propertyValueConversions = getPropertyValueConversions(); + + return propertyValueConversions != null && propertyValueConversions.hasValueConverter(property); + } + /** * Returns whether the given type is considered to be simple. That means it's either a general simple type or we have * a writing {@link Converter} registered for a particular type. * * @see SimpleTypeHolder#isSimpleType(Class) - * @param type - * @return + * @param type {@link Class} to evaluate as a simple type, such as a primitive type. + * @return a boolean value indicating whether the given, required {@link Class type} is simple. */ - public boolean isSimpleType(Class type) { + // TODO: Technically, an 'isXyz(..)' method (returning a boolean to answer a user's question should not throw an Exception). + // Rather, a null Class type argument should simply return false to indicate it is clearly not a "simple type". + // How much data store specific code relies on the existing behavior? + public boolean isSimpleType(@NonNull Class type) { - Assert.notNull(type, "Type must not be null!"); + Assert.notNull(type, "Type must not be null"); return simpleTypeHolder.isSimpleType(type); } @@ -173,16 +206,25 @@ public class CustomConversions { /** * Populates the given {@link GenericConversionService} with the converters registered. * - * @param conversionService + * @param conversionService {@link ConverterRegistry} to populate; must not be {@literal null}. + * @see ConverterRegistry */ - public void registerConvertersIn(ConverterRegistry conversionService) { + public void registerConvertersIn(@NonNull ConverterRegistry conversionService) { - Assert.notNull(conversionService, "ConversionService must not be null!"); + Assert.notNull(conversionService, "ConversionService must not be null"); converters.forEach(it -> registerConverterIn(it, conversionService)); VavrCollectionConverters.getConvertersToRegister().forEach(it -> registerConverterIn(it, conversionService)); } + /** + * Gets a reference to the configured {@link PropertyValueConversions} if property value conversions + * are supported by the underlying data store. + * + * @return a reference to the configured {@link PropertyValueConversions}; may be {@literal null} + * if the underlying data store does not support property value conversions. + * @see PropertyValueConversions + */ @Nullable public PropertyValueConversions getPropertyValueConversions() { return propertyValueConversions; @@ -191,32 +233,33 @@ public class CustomConversions { /** * Get all converters and add origin information * - * @param storeConversions - * @param converters - * @return + * @param storeConversions collection of store-base conversions; must not be {@literal null}. + * @param converters collections of custom, user-based converters; must not be {@literal null}. + * @return a {@link List} of intended {@link ConverterRegistration ConverterRegistrations}. + * @see ConverterRegistration * @since 2.3 */ - private List collectPotentialConverterRegistrations(StoreConversions storeConversions, - Collection converters) { + private List collectPotentialConverterRegistrations(@ + NonNull StoreConversions storeConversions, @NonNull Collection converters) { List converterRegistrations = new ArrayList<>(); - converters.stream() // - .map(storeConversions::getRegistrationsFor) // - .flatMap(Streamable::stream) // - .map(ConverterRegistrationIntent::userConverters) // + converters.stream() + .map(storeConversions::getRegistrationsFor) + .flatMap(Streamable::stream) + .map(ConverterRegistrationIntent::userConverters) .forEach(converterRegistrations::add); - storeConversions.getStoreConverters().stream() // - .map(storeConversions::getRegistrationsFor) // - .flatMap(Streamable::stream) // - .map(ConverterRegistrationIntent::storeConverters) // + storeConversions.getStoreConverters().stream() + .map(storeConversions::getRegistrationsFor) + .flatMap(Streamable::stream) + .map(ConverterRegistrationIntent::storeConverters) .forEach(converterRegistrations::add); - DEFAULT_CONVERTERS.stream() // - .map(storeConversions::getRegistrationsFor) // - .flatMap(Streamable::stream) // - .map(ConverterRegistrationIntent::defaultConverters) // + DEFAULT_CONVERTERS.stream() + .map(storeConversions::getRegistrationsFor) + .flatMap(Streamable::stream) + .map(ConverterRegistrationIntent::defaultConverters) .forEach(converterRegistrations::add); return converterRegistrations; @@ -230,38 +273,29 @@ public class CustomConversions { */ private void registerConverterIn(Object candidate, ConverterRegistry conversionService) { - if (candidate instanceof Converter) { - conversionService.addConverter(Converter.class.cast(candidate)); - return; - } - - if (candidate instanceof ConverterFactory) { - conversionService.addConverterFactory(ConverterFactory.class.cast(candidate)); - return; + if (candidate instanceof Converter converter) { + conversionService.addConverter(converter); + } else if (candidate instanceof ConverterFactory converterFactory) { + conversionService.addConverterFactory(converterFactory); + } else if (candidate instanceof GenericConverter genericConverter) { + conversionService.addConverter(genericConverter); + } else if (candidate instanceof ConverterAware converterAware) { + converterAware.getConverters().forEach(it -> registerConverterIn(it, conversionService)); + } else { + throw new IllegalArgumentException(String.format(NOT_A_CONVERTER, candidate)); } - - if (candidate instanceof GenericConverter) { - conversionService.addConverter(GenericConverter.class.cast(candidate)); - return; - } - - if (candidate instanceof ConverterAware) { - ConverterAware.class.cast(candidate).getConverters().forEach(it -> registerConverterIn(it, conversionService)); - return; - } - - throw new IllegalArgumentException(String.format(NOT_A_CONVERTER, candidate)); } /** * Registers the given {@link ConvertiblePair} as reading or writing pair depending on the type sides being basic * types. * - * @param converterRegistration + * @param converterRegistration {@link ConverterRegistration} to register; must not be {@literal null}. + * @see ConverterRegistration */ - private Object register(ConverterRegistration converterRegistration) { + private Object register(@NonNull ConverterRegistration converterRegistration) { - Assert.notNull(converterRegistration, "Converter registration must not be null!"); + Assert.notNull(converterRegistration, "Converter registration must not be null"); ConvertiblePair pair = converterRegistration.getConvertiblePair(); @@ -288,15 +322,15 @@ public class CustomConversions { } /** - * Validate a given {@link ConverterRegistration} in a specific setup.
+ * Validate a given {@link ConverterRegistration} in a specific setup.
* Non {@link ReadingConverter reading} and user defined {@link Converter converters} are only considered supported if * the {@link ConverterRegistrationIntent#isSimpleTargetType() target type} is considered to be a store simple type. * - * @param registrationIntent + * @param registrationIntent {@link ConverterRegistrationIntent} to validate; must not be {@literal null}. * @return {@literal true} if supported. * @since 2.3 */ - private boolean isSupportedConverter(ConverterRegistrationIntent registrationIntent) { + private boolean isSupportedConverter(@NonNull ConverterRegistrationIntent registrationIntent) { boolean register = registrationIntent.isUserConverter() || registrationIntent.isStoreConverter() || (registrationIntent.isReading() && registrationIntent.isSimpleSourceType()) @@ -323,7 +357,7 @@ public class CustomConversions { * @return {@literal false} if the given {@link ConverterRegistration} shall be skipped. * @since 2.3 */ - private boolean shouldRegister(ConverterRegistrationIntent intent) { + private boolean shouldRegister(@NonNull ConverterRegistrationIntent intent) { return !intent.isDefaultConverter() || converterConfiguration.shouldRegister(intent.getConverterRegistration().getConvertiblePair()); } @@ -333,11 +367,12 @@ public class CustomConversions { * type into a store native one. * * @param sourceType must not be {@literal null} - * @return + * @return the target type to convert to in case we have a custom conversion registered to convert the given source + * type into a store native one. */ - public Optional> getCustomWriteTarget(Class sourceType) { + public Optional> getCustomWriteTarget(@NonNull Class sourceType) { - Assert.notNull(sourceType, "Source type must not be null!"); + Assert.notNull(sourceType, "Source type must not be null"); Class target = customWriteTargetTypes.computeIfAbsent(sourceType, getRawWriteTarget); @@ -351,12 +386,12 @@ public class CustomConversions { * * @param sourceType must not be {@literal null} * @param requestedTargetType must not be {@literal null}. - * @return + * @return the target type we can read an inject of the given source type to. */ - public Optional> getCustomWriteTarget(Class sourceType, Class requestedTargetType) { + public Optional> getCustomWriteTarget(@NonNull Class sourceType, @NonNull Class requestedTargetType) { - Assert.notNull(sourceType, "Source type must not be null!"); - Assert.notNull(requestedTargetType, "Target type must not be null!"); + Assert.notNull(sourceType, "Source type must not be null"); + Assert.notNull(requestedTargetType, "Target type must not be null"); Class target = customWriteTargetTypes.computeIfAbsent(sourceType, requestedTargetType, getWriteTarget); @@ -368,11 +403,11 @@ public class CustomConversions { * type might be a subclass of the given expected type though. * * @param sourceType must not be {@literal null} - * @return + * @return whether we have a custom conversion registered to read {@code sourceType} into a native type. */ - public boolean hasCustomWriteTarget(Class sourceType) { + public boolean hasCustomWriteTarget(@NonNull Class sourceType) { - Assert.notNull(sourceType, "Source type must not be null!"); + Assert.notNull(sourceType, "Source type must not be null"); return getCustomWriteTarget(sourceType).isPresent(); } @@ -383,12 +418,13 @@ public class CustomConversions { * * @param sourceType must not be {@literal null}. * @param targetType must not be {@literal null}. - * @return + * @return whether we have a custom conversion registered to read an object of the given source type into an object + * of the given native target type. */ - public boolean hasCustomWriteTarget(Class sourceType, Class targetType) { + public boolean hasCustomWriteTarget(@NonNull Class sourceType, @NonNull Class targetType) { - Assert.notNull(sourceType, "Source type must not be null!"); - Assert.notNull(targetType, "Target type must not be null!"); + Assert.notNull(sourceType, "Source type must not be null"); + Assert.notNull(targetType, "Target type must not be null"); return getCustomWriteTarget(sourceType, targetType).isPresent(); } @@ -398,12 +434,12 @@ public class CustomConversions { * * @param sourceType must not be {@literal null} * @param targetType must not be {@literal null} - * @return + * @return whether we have a custom conversion registered to read the given source into the given target type. */ - public boolean hasCustomReadTarget(Class sourceType, Class targetType) { + public boolean hasCustomReadTarget(@NonNull Class sourceType, @NonNull Class targetType) { - Assert.notNull(sourceType, "Source type must not be null!"); - Assert.notNull(targetType, "Target type must not be null!"); + Assert.notNull(sourceType, "Source type must not be null"); + Assert.notNull(targetType, "Target type must not be null"); return getCustomReadTarget(sourceType, targetType) != null; } @@ -414,24 +450,24 @@ public class CustomConversions { * * @param sourceType must not be {@literal null}. * @param targetType must not be {@literal null}. - * @return + * @return the actual target type for the given {@code sourceType} and {@code targetType}. */ @Nullable - private Class getCustomReadTarget(Class sourceType, Class targetType) { + private Class getCustomReadTarget(@NonNull Class sourceType, @NonNull Class targetType) { return customReadTargetTypes.computeIfAbsent(sourceType, targetType, getReadTarget); } /** * Inspects the given {@link ConvertiblePair ConvertiblePairs} for ones that have a source compatible type as source. - * Additionally checks assignability of the target type if one is given. + * Additionally, checks assignability of the target type if one is given. * * @param sourceType must not be {@literal null}. * @param targetType can be {@literal null}. * @param pairs must not be {@literal null}. - * @return + * @return the base {@link Class type} for the (requested) {@link Class target type} if present. */ @Nullable - private Class getCustomTarget(Class sourceType, @Nullable Class targetType, + private Class getCustomTarget(@NonNull Class sourceType, @Nullable Class targetType, Collection pairs) { if (targetType != null && pairs.contains(new ConvertiblePair(sourceType, targetType))) { @@ -456,11 +492,13 @@ public class CustomConversions { return null; } - private static boolean hasAssignableSourceType(ConvertiblePair pair, Class sourceType) { + private static boolean hasAssignableSourceType(@NonNull ConvertiblePair pair, @NonNull Class sourceType) { return pair.getSourceType().isAssignableFrom(sourceType); } - private static boolean requestedTargetTypeIsAssignable(@Nullable Class requestedTargetType, Class targetType) { + private static boolean requestedTargetTypeIsAssignable(@Nullable Class requestedTargetType, + @NonNull Class targetType) { + return requestedTargetType == null || targetType.isAssignableFrom(requestedTargetType); } @@ -736,8 +774,8 @@ public class CustomConversions { */ public static StoreConversions of(SimpleTypeHolder storeTypeHolder, Object... converters) { - Assert.notNull(storeTypeHolder, "SimpleTypeHolder must not be null!"); - Assert.notNull(converters, "Converters must not be null!"); + Assert.notNull(storeTypeHolder, "SimpleTypeHolder must not be null"); + Assert.notNull(converters, "Converters must not be null"); return new StoreConversions(storeTypeHolder, Arrays.asList(converters)); } @@ -752,8 +790,8 @@ public class CustomConversions { */ public static StoreConversions of(SimpleTypeHolder storeTypeHolder, Collection converters) { - Assert.notNull(storeTypeHolder, "SimpleTypeHolder must not be null!"); - Assert.notNull(converters, "Converters must not be null!"); + Assert.notNull(storeTypeHolder, "SimpleTypeHolder must not be null"); + Assert.notNull(converters, "Converters must not be null"); return new StoreConversions(storeTypeHolder, converters); } @@ -766,23 +804,23 @@ public class CustomConversions { */ public Streamable getRegistrationsFor(Object converter) { - Assert.notNull(converter, "Converter must not be null!"); + Assert.notNull(converter, "Converter must not be null"); Class type = converter.getClass(); boolean isWriting = isAnnotatedWith(type, WritingConverter.class); boolean isReading = isAnnotatedWith(type, ReadingConverter.class); - if (converter instanceof ConverterAware) { + if (converter instanceof ConverterAware converterAware) { - return Streamable.of(() -> ConverterAware.class.cast(converter).getConverters().stream()// + return Streamable.of(() -> converterAware.getConverters().stream() .flatMap(it -> getRegistrationsFor(it).stream())); - } else if (converter instanceof GenericConverter) { + } else if (converter instanceof GenericConverter genericConverter) { - Set convertibleTypes = GenericConverter.class.cast(converter).getConvertibleTypes(); + Set convertibleTypes = genericConverter.getConvertibleTypes(); - return convertibleTypes == null // - ? Streamable.empty() // + return convertibleTypes == null + ? Streamable.empty() : Streamable.of(convertibleTypes).map(it -> register(converter, it, isReading, isWriting)); } else if (converter instanceof ConverterFactory) { @@ -794,7 +832,7 @@ public class CustomConversions { return getRegistrationFor(converter, Converter.class, isReading, isWriting); } else { - throw new IllegalArgumentException(String.format("Unsupported converter type %s!", converter)); + throw new IllegalArgumentException(String.format("Unsupported converter type %s", converter)); } } @@ -809,7 +847,7 @@ public class CustomConversions { Class[] arguments = GenericTypeResolver.resolveTypeArguments(converterType, type); if (arguments == null) { - throw new IllegalStateException(String.format("Couldn't resolve type arguments for %s!", converterType)); + throw new IllegalStateException(String.format("Couldn't resolve type arguments for %s", converterType)); } return Streamable.of(register(converter, arguments[0], arguments[1], isReading, isWriting)); @@ -922,8 +960,8 @@ public class CustomConversions { * @param propertyValueConversions can be {@literal null}. * @since 2.7 */ - public ConverterConfiguration(StoreConversions storeConversions, List userConverters, - Predicate converterRegistrationFilter, + public ConverterConfiguration(@NonNull StoreConversions storeConversions, @NonNull List userConverters, + @NonNull Predicate converterRegistrationFilter, @Nullable PropertyValueConversions propertyValueConversions) { this.storeConversions = storeConversions; diff --git a/src/test/java/org/springframework/data/convert/CustomConversionsUnitTests.java b/src/test/java/org/springframework/data/convert/CustomConversionsUnitTests.java index 041123222..72bed2da9 100644 --- a/src/test/java/org/springframework/data/convert/CustomConversionsUnitTests.java +++ b/src/test/java/org/springframework/data/convert/CustomConversionsUnitTests.java @@ -42,6 +42,7 @@ import org.springframework.data.convert.CustomConversions.ConverterConfiguration import org.springframework.data.convert.CustomConversions.StoreConversions; import org.springframework.data.convert.Jsr310Converters.LocalDateTimeToDateConverter; import org.springframework.data.geo.Point; +import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.model.SimpleTypeHolder; /** @@ -59,7 +60,7 @@ class CustomConversionsUnitTests { @Override public boolean isSimpleType(Class type) { - return type.getName().startsWith("java.time") ? false : super.isSimpleType(type); + return !type.getName().startsWith("java.time") && super.isSimpleType(type); } }; @@ -103,7 +104,7 @@ class CustomConversionsUnitTests { GenericConversionService conversionService = new DefaultConversionService(); var conversions = new CustomConversions(StoreConversions.NONE, - Arrays.asList(StringToFormatConverter.INSTANCE)); + Collections.singletonList(StringToFormatConverter.INSTANCE)); conversions.registerConvertersIn(conversionService); assertThat(conversionService.canConvert(String.class, Format.class)).isTrue(); @@ -113,7 +114,7 @@ class CustomConversionsUnitTests { void doesNotConsiderTypeSimpleIfOnlyReadConverterIsRegistered() { var conversions = new CustomConversions(StoreConversions.NONE, - Arrays.asList(StringToFormatConverter.INSTANCE)); + Collections.singletonList(StringToFormatConverter.INSTANCE)); assertThat(conversions.isSimpleType(Format.class)).isFalse(); } @@ -121,7 +122,7 @@ class CustomConversionsUnitTests { void discoversConvertersForSubtypesOfMongoTypes() { var conversions = new CustomConversions(StoreConversions.NONE, - Arrays.asList(StringToIntegerConverter.INSTANCE)); + Collections.singletonList(StringToIntegerConverter.INSTANCE)); assertThat(conversions.hasCustomReadTarget(String.class, Integer.class)).isTrue(); assertThat(conversions.hasCustomWriteTarget(String.class, Integer.class)).isTrue(); } @@ -130,7 +131,7 @@ class CustomConversionsUnitTests { void shouldSelectPropertCustomWriteTargetForCglibProxiedType() { var conversions = new CustomConversions(StoreConversions.NONE, - Arrays.asList(FormatToStringConverter.INSTANCE)); + Collections.singletonList(FormatToStringConverter.INSTANCE)); assertThat(conversions.getCustomWriteTarget(createProxyTypeFor(Format.class))).hasValue(String.class); } @@ -138,7 +139,7 @@ class CustomConversionsUnitTests { void shouldSelectPropertCustomReadTargetForCglibProxiedType() { var conversions = new CustomConversions(StoreConversions.NONE, - Arrays.asList(CustomTypeToStringConverter.INSTANCE)); + Collections.singletonList(CustomTypeToStringConverter.INSTANCE)); assertThat(conversions.hasCustomReadTarget(createProxyTypeFor(CustomType.class), String.class)).isTrue(); } @@ -202,7 +203,7 @@ class CustomConversionsUnitTests { Collections.emptyList()); conversions.registerConvertersIn(registry); - assertThat(conversions.isSimpleType(Point.class)); + assertThat(conversions.isSimpleType(Point.class)).isTrue(); // Point is a custom simple type verify(registry).addConverter(any(PointToMapConverter.class)); } @@ -279,6 +280,44 @@ class CustomConversionsUnitTests { new ConverterConfiguration(StoreConversions.NONE, Collections.emptyList(), (it) -> true, null)); } + @Test + void hasValueConverterReturnsFalseWhenNoPropertyValueConversionsAreConfigured() { + + ConverterConfiguration configuration = new ConverterConfiguration(StoreConversions.NONE, + Collections.emptyList(), it -> true, null); + + CustomConversions conversions = new CustomConversions(configuration); + + PersistentProperty mockProperty = mock(PersistentProperty.class); + + assertThat(conversions.getPropertyValueConversions()).isNull(); + assertThat(conversions.hasValueConverter(mockProperty)).isFalse(); + + verifyNoInteractions(mockProperty); + } + + @Test + public void hasValueConverterReturnsTrueWhenConverterRegisteredForProperty() { + + PersistentProperty mockProperty = mock(PersistentProperty.class); + + PropertyValueConversions mockPropertyValueConversions = mock(PropertyValueConversions.class); + + doReturn(true).when(mockPropertyValueConversions).hasValueConverter(eq(mockProperty)); + + ConverterConfiguration configuration = new ConverterConfiguration(StoreConversions.NONE, + Collections.emptyList(), it -> true, mockPropertyValueConversions); + + CustomConversions conversions = new CustomConversions(configuration); + + assertThat(conversions.getPropertyValueConversions()).isSameAs(mockPropertyValueConversions); + assertThat(conversions.hasValueConverter(mockProperty)).isTrue(); + + verify(mockPropertyValueConversions, times(1)).hasValueConverter(eq(mockProperty)); + verifyNoMoreInteractions(mockPropertyValueConversions); + verifyNoInteractions(mockProperty); + } + private static Class createProxyTypeFor(Class type) { var factory = new ProxyFactory();