> PropertyValueConversions simple(
+ Consumer
+ * A {@link PropertyValueConverter} is, other than a {@link ReadingConverter} or {@link WritingConverter}, only applied
+ * to special annotated fields which allows a fine-grained conversion of certain values within a specific context.
+ *
+ * @author Christoph Strobl
+ * @param domain specific type.
+ * @param store native type.
+ * @param context) {
+ return writer.apply(value, context);
+ }
+
+ @Nullable
+ @Override
+ public A read(@Nullable B value, ValueConversionContext context) {
+ return reader.apply(value, context);
+ }
+ }
+}
diff --git a/src/main/java/org/springframework/data/convert/PropertyValueConverterFactories.java b/src/main/java/org/springframework/data/convert/PropertyValueConverterFactories.java
new file mode 100644
index 0000000000..dac34508d5
--- /dev/null
+++ b/src/main/java/org/springframework/data/convert/PropertyValueConverterFactories.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright 2022 the original author or authors.
+ *
+ * Licensed 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
+ *
+ * https://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.springframework.data.convert;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.BeanFactory;
+import org.springframework.beans.factory.NoSuchBeanDefinitionException;
+import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
+import org.springframework.data.mapping.PersistentProperty;
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
+
+/**
+ * {@link PropertyValueConverterFactories} provides a collection of predefined {@link PropertyValueConverterFactory}
+ * implementations. Depending on the applications need {@link PropertyValueConverterFactory factories} can be
+ * {@link ChainedPropertyValueConverterFactory chained} and the created {@link PropertyValueConverter converter}
+ * {@link CachingPropertyValueConverterFactory cached}.
+ *
+ * @author Christoph Strobl
+ * @since 2.7
+ */
+final class PropertyValueConverterFactories {
+
+ /**
+ * {@link PropertyValueConverterFactory} implementation that returns the first no null {@link PropertyValueConverter}
+ * by asking given {@link PropertyValueConverterFactory factories} one by one.
+ *
+ * @author Christoph Strobl
+ * @since 2.7
+ */
+ static class ChainedPropertyValueConverterFactory implements PropertyValueConverterFactory {
+
+ private List
+ * Depending on the applications need {@link PropertyValueConverterFactory factories} can be {@link #chained(List)
+ * chained} and the resulting {@link PropertyValueConverter converter} may be
+ * {@link #caching(PropertyValueConverterFactory) cached}.
+ *
+ * @author Christoph Strobl
+ * @since 2.7
+ */
+public interface PropertyValueConverterFactory {
+
+ /**
+ * Get the {@link PropertyValueConverter} applicable for the given {@link PersistentProperty}.
+ *
+ * @param property must not be {@literal null}.
+ * @param domain specific type.
+ * @param store native type.
+ * @param
+ * It is possible to register type safe converters via {@link #registerConverter(Class, Function)}
+ *
+ * > {
+
+ private final SimplePropertyValueConverterRegistry registry = new SimplePropertyValueConverterRegistry<>();
+
+ /**
+ * Starts a converter registration by pointing to a property of a domain type.
+ *
+ * @param registerConverter(Class> type, String path,
+ PropertyValueConverter, ?, ? extends ValueConversionContext>> converter) {
+
+ registry.registerConverter(type, path,
+ (PropertyValueConverter, ?, ? extends ValueConversionContext >) converter);
+ return this;
+ }
+
+ /**
+ * Obtain the {@link SimplePropertyValueConverterRegistry}.
+ *
+ * @return new instance of {@link SimplePropertyValueConverterRegistry}.
+ */
+ public ValueConverterRegistry buildRegistry() {
+ return new SimplePropertyValueConverterRegistry<>(registry);
+ }
+
+ /**
+ * Register collected {@link PropertyValueConverter converters} within the given {@link ValueConverterRegistry
+ * registry}.
+ *
+ * @return new instance of {@link SimplePropertyValueConverterRegistry}.
+ */
+ public void registerConvertersIn(ValueConverterRegistry target) {
+
+ Assert.notNull(target, "Target registry must not be null!");
+
+ registry.getConverterRegistrationMap().entrySet().forEach(entry -> {
+ target.registerConverter(entry.getKey().type, entry.getKey().path, entry.getValue());
+ });
+ }
+
+ /**
+ * Helper to build up a fluent registration API starting on
+ *
+ * @author Oliver Drotbohm
+ */
+ public static class WritingConverterRegistrationBuilder config;
+
+ WritingConverterRegistrationBuilder(Class config) {
+
+ this.config = config;
+ this.registration = (converter) -> config.registerConverter(type, property, converter);
+ }
+
+ public ReadingConverterRegistrationBuilder readingAsIs() {
+ return reading((source, context) -> (S) source);
+ }
+
+ public PropertyValueConverterRegistrar reading(Function reading(BiFunction
+ * Should be {@link #afterPropertiesSet() initialized}. If not, {@link #init()} will be called of fist attempt of
+ * {@link PropertyValueConverter converter} retrieval.
+ *
+ * @author Christoph Strobl
+ * @since 2.7
+ */
+public class SimplePropertyValueConversions implements PropertyValueConversions, InitializingBean {
+
+ private @Nullable PropertyValueConverterFactory converterFactory;
+ private @Nullable ValueConverterRegistry> valueConverterRegistry;
+ private boolean converterCacheEnabled = true;
+ private AtomicBoolean initialized = new AtomicBoolean(false);
+
+ /**
+ * Set the {@link PropertyValueConverterFactory factory} responsible for creating the actual
+ * {@link PropertyValueConverter converter}.
+ *
+ * @param converterFactory must not be {@literal null}.
+ */
+ public void setConverterFactory(PropertyValueConverterFactory converterFactory) {
+ this.converterFactory = converterFactory;
+ }
+
+ @Nullable
+ public PropertyValueConverterFactory getConverterFactory() {
+ return converterFactory;
+ }
+
+ /**
+ * Set the {@link ValueConverterRegistry converter registry} for path configured converters. This is short for adding
+ * a
+ * {@link org.springframework.data.convert.PropertyValueConverterFactories.ConfiguredInstanceServingValueConverterFactory}
+ * at the end of a {@link ChainedPropertyValueConverterFactory}.
+ *
+ * @param valueConverterRegistry must not be {@literal null}.
+ */
+ public void setValueConverterRegistry(ValueConverterRegistry> valueConverterRegistry) {
+ this.valueConverterRegistry = valueConverterRegistry;
+ }
+
+ /**
+ * Get the {@link ValueConverterRegistry} used for path configured converters.
+ *
+ * @return can be {@literal null}.
+ */
+ @Nullable
+ public ValueConverterRegistry> getValueConverterRegistry() {
+ return valueConverterRegistry;
+ }
+
+ /**
+ * Dis-/Enable caching. Enabled by default.
+ *
+ * @param converterCacheEnabled set to {@literal true} to enable caching of {@link PropertyValueConverter converter}
+ * instances.
+ */
+ public void setConverterCacheEnabled(boolean converterCacheEnabled) {
+ this.converterCacheEnabled = converterCacheEnabled;
+ }
+
+ @Nullable
+ @Override
+ public , D extends ValueConversionContext > implements ValueConverterRegistry {
+
+ private final Map source) {
+ this.converterRegistrationMap.putAll(source.converterRegistrationMap);
+ }
+
+ @Override
+ public void registerConverter(Class> type, String path,
+ PropertyValueConverter, ?, ? extends ValueConversionContext > converter) {
+
+ converterRegistrationMap.put(new Key(type, path), converter);
+ }
+
+ /**
+ * Register the {@link PropertyValueConverter} for the property of the given type if none had been registered before.
+ *
+ * @param type the target type.
+ * @param path the property name.
+ * @param converter the converter to register.
+ */
+ public void registerConverterIfAbsent(Class> type, String path,
+ PropertyValueConverter, ?, ? extends ValueConversionContext > converter) {
+ converterRegistrationMap.putIfAbsent(new Key(type, path), converter);
+ }
+
+ @Override
+ public boolean containsConverterFor(Class> type, String path) {
+ return converterRegistrationMap.containsKey(new Key(type, path));
+ }
+
+ @Override
+ public
+ * Store implementations should provide their own flavor of {@link ValueConversionContext} enhancing the existing API,
+ * implementing delegates for {@link #read(Object, TypeInformation)}, {@link #write(Object, TypeInformation)}.
+ *
+ * @author Christoph Strobl
+ * @author Oliver Drotbohm
+ */
+public interface ValueConversionContext > {
+
+ /**
+ * Return the {@link PersistentProperty} to be handled.
+ *
+ * @return will never be {@literal null}.
+ */
+ P getProperty();
+
+ /**
+ * Write to whatever type is considered best for the given source.
+ *
+ * @param value
+ * @return
+ */
+ @Nullable
+ default Object write(@Nullable Object value) {
+ return write(value, getProperty().getTypeInformation());
+ }
+
+ /**
+ * Write as the given type.
+ *
+ * @param value can be {@literal null}.
+ * @param target must not be {@literal null}.
+ * @return can be {@literal null}.
+ */
+ @Nullable
+ default
+ * The target {@link PropertyValueConverter} is typically provided via a {@link PropertyValueConverterFactory converter
+ * factory}.
+ *
+ * Consult the store-specific documentation for details and support notes.
+ *
+ * @author Christoph Strobl
+ * @since 2.7
+ * @see PropertyValueConverter
+ * @see PropertyValueConverterFactory
+ */
+@Target({ FIELD, ANNOTATION_TYPE })
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+public @interface ValueConverter {
+
+ /**
+ * The {@link PropertyValueConverter} type handling the value conversion of the annotated property.
+ *
+ * @return the configured {@link PropertyValueConverter}. {@link ObjectToObjectPropertyValueConverter} by default.
+ */
+ Class extends PropertyValueConverter> value() default ObjectToObjectPropertyValueConverter.class;
+
+}
diff --git a/src/main/java/org/springframework/data/convert/ValueConverterRegistry.java b/src/main/java/org/springframework/data/convert/ValueConverterRegistry.java
new file mode 100644
index 0000000000..2ad9c7a2e7
--- /dev/null
+++ b/src/main/java/org/springframework/data/convert/ValueConverterRegistry.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2022 the original author or authors.
+ *
+ * Licensed 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
+ *
+ * https//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.springframework.data.convert;
+
+import org.springframework.data.mapping.PersistentProperty;
+import org.springframework.lang.Nullable;
+
+/**
+ * A registry of property-specific {@link PropertyValueConverter value converters} to convert only specific
+ * properties/values of an object.
+ *
+ * @author Christoph Strobl
+ * @since 2.7
+ */
+public interface ValueConverterRegistry > {
+
+ /**
+ * Register the {@link PropertyValueConverter} for the property of the given type.
+ *
+ * @param type the target type. Must not be {@literal null}.
+ * @param path the property name. Must not be {@literal null}.
+ * @param converter the converter to register. Must not be {@literal null}.
+ */
+ void registerConverter(Class> type, String path,
+ PropertyValueConverter, ?, ? extends ValueConversionContext > converter);
+
+ /**
+ * Obtain the converter registered for the given type, path combination or {@literal null} if none defined.
+ *
+ * @param type the target type. Must not be {@literal null}.
+ * @param path the property name. Must not be {@literal null}.
+ * @param > {
@@ -76,9 +80,9 @@ public interface PersistentProperty > {
Iterable extends TypeInformation>> getPersistentEntityTypes();
/**
- * Returns the detected {@link TypeInformation TypeInformations} if the property references a {@link PersistentEntity}.
- * Will return an {@literal empty} {@link Iterable} in case it refers to a simple type. Will return the {@link Collection}'s
- * component types or the {@link Map}'s value type transparently.
+ * Returns the detected {@link TypeInformation TypeInformations} if the property references a
+ * {@link PersistentEntity}. Will return an {@literal empty} {@link Iterable} in case it refers to a simple type. Will
+ * return the {@link Collection}'s component types or the {@link Map}'s value type transparently.
*
* @return never {@literal null}.
* @since 2.6
@@ -433,4 +437,33 @@ default
+ * Store implementations may override the default and provide a more specific type.
+ *
+ * @return {@literal null} if none defined. Check {@link #hasValueConverter()} to check if the annotation is present
+ * at all.
+ * @since 2.7
+ */
+ @Nullable
+ default Class extends PropertyValueConverter, ?, ? extends ValueConversionContext extends PersistentProperty>>>> getValueConverterType() {
+
+ ValueConverter annotation = findAnnotation(ValueConverter.class);
+
+ return annotation == null ? null
+ : (Class extends PropertyValueConverter, ?, ? extends ValueConversionContext extends PersistentProperty>>>>) annotation
+ .value();
+ }
+
+ /**
+ * @return by default return {@literal true} if {@link ValueConverter} annotation is present.
+ * @since 2.7
+ */
+ default boolean hasValueConverter() {
+ return isAnnotationPresent(ValueConverter.class);
+ }
}
diff --git a/src/main/java/org/springframework/data/mapping/model/AnnotationBasedPersistentProperty.java b/src/main/java/org/springframework/data/mapping/model/AnnotationBasedPersistentProperty.java
index f44fd5d48e..1db387a152 100644
--- a/src/main/java/org/springframework/data/mapping/model/AnnotationBasedPersistentProperty.java
+++ b/src/main/java/org/springframework/data/mapping/model/AnnotationBasedPersistentProperty.java
@@ -33,6 +33,9 @@
import org.springframework.data.annotation.Reference;
import org.springframework.data.annotation.Transient;
import org.springframework.data.annotation.Version;
+import org.springframework.data.convert.ValueConverter;
+import org.springframework.data.convert.PropertyValueConverter;
+import org.springframework.data.convert.ValueConversionContext;
import org.springframework.data.mapping.Association;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.PersistentEntity;
@@ -132,8 +135,7 @@ private void populateAnnotationCache(Property property) {
+ "multiple times on accessor methods of property %s in class %s!",
annotationType.getSimpleName(), getName(), getOwner().getType().getSimpleName());
- annotationCache.put(annotationType,
- Optional.of(mergedAnnotation));
+ annotationCache.put(annotationType, Optional.of(mergedAnnotation));
}
});
@@ -148,8 +150,7 @@ private void populateAnnotationCache(Property property) {
"Ambiguous mapping! Annotation %s configured " + "on field %s and one of its accessor methods in class %s!",
annotationType.getSimpleName(), it.getName(), getOwner().getType().getSimpleName());
- annotationCache.put(annotationType,
- Optional.of(mergedAnnotation));
+ annotationCache.put(annotationType, Optional.of(mergedAnnotation));
}
});
}
@@ -308,6 +309,17 @@ public TypeInformation> getAssociationTargetTypeInformation() {
return associationTargetType.getNullable();
}
+ @Nullable
+ @Override
+ @SuppressWarnings("unchecked")
+ public Class extends PropertyValueConverter, ?, ? extends ValueConversionContext extends PersistentProperty>>>> getValueConverterType() {
+
+ return doFindAnnotation(ValueConverter.class) //
+ .map(ValueConverter::value) //
+ .map(Class.class::cast) //
+ .orElse(null);
+ }
+
/*
* (non-Javadoc)
* @see org.springframework.data.mapping.model.AbstractPersistentProperty#toString()
@@ -343,8 +355,7 @@ private Stream extends AnnotatedElement> getAccessors() {
@SuppressWarnings("unchecked")
private static Class extends Annotation> loadIdentityType() {
- return (Class extends Annotation>) ReflectionUtils.loadIfPresent(
- "org.jmolecules.ddd.annotation.Identity",
+ return (Class extends Annotation>) ReflectionUtils.loadIfPresent("org.jmolecules.ddd.annotation.Identity",
AbstractPersistentProperty.class.getClassLoader());
}
}
diff --git a/src/test/java/org/springframework/data/convert/CustomConversionsUnitTests.java b/src/test/java/org/springframework/data/convert/CustomConversionsUnitTests.java
index c034094497..8722594f09 100644
--- a/src/test/java/org/springframework/data/convert/CustomConversionsUnitTests.java
+++ b/src/test/java/org/springframework/data/convert/CustomConversionsUnitTests.java
@@ -45,6 +45,7 @@
import org.springframework.data.convert.Jsr310Converters.LocalDateTimeToDateConverter;
import org.springframework.data.convert.ThreeTenBackPortConverters.LocalDateTimeToJavaTimeInstantConverter;
import org.springframework.data.geo.Point;
+import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.model.SimpleTypeHolder;
import org.threeten.bp.LocalDateTime;
@@ -303,6 +304,25 @@ void registersVavrConverters() {
assertThat(conversionService.canConvert(List.class, io.vavr.collection.List.class)).isTrue();
}
+ @Test // GH-1484
+ void allowsToRegisterPropertyConversions() {
+
+ PropertyValueConversions propertyValueConversions = mock(PropertyValueConversions.class);
+ when(propertyValueConversions.getValueConverter(any())).thenReturn(mock(PropertyValueConverter.class));
+
+ CustomConversions conversions = new CustomConversions(new ConverterConfiguration(StoreConversions.NONE,
+ Collections.emptyList(), (it) -> true, propertyValueConversions));
+ assertThat(conversions.getPropertyValueConverter(mock(PersistentProperty.class))).isNotNull();
+ }
+
+ @Test // GH-1484
+ void doesNotFailIfPropertiesConversionIsNull() {
+
+ CustomConversions conversions = new CustomConversions(new ConverterConfiguration(StoreConversions.NONE,
+ Collections.emptyList(), (it) -> true, null));
+ assertThat(conversions.getPropertyValueConverter(mock(PersistentProperty.class))).isNull();
+ }
+
private static Class> createProxyTypeFor(Class> type) {
ProxyFactory factory = new ProxyFactory();
diff --git a/src/test/java/org/springframework/data/convert/PropertyValueConverterFactoryUnitTests.java b/src/test/java/org/springframework/data/convert/PropertyValueConverterFactoryUnitTests.java
new file mode 100644
index 0000000000..7a84aed72c
--- /dev/null
+++ b/src/test/java/org/springframework/data/convert/PropertyValueConverterFactoryUnitTests.java
@@ -0,0 +1,286 @@
+/*
+ * Copyright 2022 the original author or authors.
+ *
+ * Licensed 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
+ *
+ * https://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.springframework.data.convert;
+
+import static org.assertj.core.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import java.util.UUID;
+
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.support.BeanDefinitionBuilder;
+import org.springframework.beans.factory.support.DefaultListableBeanFactory;
+import org.springframework.data.mapping.PersistentEntity;
+import org.springframework.data.mapping.PersistentProperty;
+import org.springframework.data.mapping.context.SamplePersistentProperty;
+import org.springframework.lang.Nullable;
+
+/**
+ * @author Christoph Strobl
+ */
+public class PropertyValueConverterFactoryUnitTests {
+
+ @Test // GH-1484
+ void simpleConverterFactoryCanInstantiateFactoryWithDefaultCtor() {
+
+ assertThat(PropertyValueConverterFactory.simple().getConverter(ConverterWithDefaultCtor.class))
+ .isInstanceOf(ConverterWithDefaultCtor.class);
+ }
+
+ @Test // GH-1484
+ void simpleConverterFactoryReadsConverterFromAnnotation() {
+
+ PersistentProperty property = mock(PersistentProperty.class);
+ when(property.hasValueConverter()).thenReturn(true);
+ when(property.getValueConverterType()).thenReturn(ConverterWithDefaultCtor.class);
+
+ assertThat(PropertyValueConverterFactory.simple().getConverter(property))
+ .isInstanceOf(ConverterWithDefaultCtor.class);
+ }
+
+ @Test // GH-1484
+ void simpleConverterFactoryErrorsOnNullType() {
+
+ assertThatIllegalArgumentException()
+ .isThrownBy(() -> PropertyValueConverterFactory.simple().getConverter((Class) null));
+ }
+
+ @Test // GH-1484
+ void simpleConverterFactoryCanExtractFactoryEnumInstance() {
+
+ assertThat(PropertyValueConverterFactory.simple().getConverter(ConverterEnum.class))
+ .isInstanceOf(ConverterEnum.class);
+ }
+
+ @Test // GH-1484
+ void simpleConverterFactoryCannotInstantiateFactoryWithDependency() {
+
+ assertThatExceptionOfType(RuntimeException.class)
+ .isThrownBy(() -> PropertyValueConverterFactory.simple().getConverter(ConverterWithDependency.class));
+ }
+
+ @Test // GH-1484
+ void beanFactoryAwareConverterFactoryCanInstantiateFactoryWithDefaultCtor() {
+
+ DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
+
+ assertThat(PropertyValueConverterFactory.beanFactoryAware(beanFactory).getConverter(ConverterWithDefaultCtor.class))
+ .isInstanceOf(ConverterWithDefaultCtor.class);
+ }
+
+ @Test // GH-1484
+ void beanFactoryAwareConverterFactoryCanInstantiateFactoryWithBeanReference() {
+
+ DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
+ beanFactory.registerBeanDefinition("someDependency",
+ BeanDefinitionBuilder.rootBeanDefinition(SomeDependency.class).getBeanDefinition());
+
+ assertThat(PropertyValueConverterFactory.beanFactoryAware(beanFactory).getConverter(ConverterWithDependency.class))
+ .isInstanceOf(ConverterWithDependency.class);
+ }
+
+ @Test // GH-1484
+ void beanFactoryAwareConverterFactoryCanLookupExistingBean() {
+
+ DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
+ beanFactory.registerBeanDefinition("someDependency",
+ BeanDefinitionBuilder.rootBeanDefinition(SomeDependency.class).getBeanDefinition());
+ beanFactory.registerBeanDefinition("theMightyConverter",
+ BeanDefinitionBuilder.rootBeanDefinition(ConverterWithDependency.class)
+ .addConstructorArgReference("someDependency").getBeanDefinition());
+
+ assertThat(PropertyValueConverterFactory.beanFactoryAware(beanFactory).getConverter(ConverterWithDependency.class))
+ .isSameAs(beanFactory.getBean("theMightyConverter"));
+ }
+
+ @Test // GH-1484
+ void chainedConverterFactoryIteratesFactories() {
+
+ PropertyValueConverter expected = mock(PropertyValueConverter.class);
+
+ PropertyValueConverterFactory factory = PropertyValueConverterFactory.chained(new PropertyValueConverterFactory() {
+ @Nullable
+ @Override
+ public > PropertyValueConverter getConverter(
+ Class extends PropertyValueConverter> converterType) {
+ return delegates.stream().filter(it -> it.getConverter(converterType) != null).findFirst()
+ .map(it -> it.getConverter(converterType)).orElse(null);
+ }
+
+ public List> PropertyValueConverter getConverter(
+ Class extends PropertyValueConverter> converterType) {
+
+ Assert.notNull(converterType, "ConverterType must not be null!");
+
+ if (converterType.isEnum()) {
+ return (PropertyValueConverter) EnumSet.allOf((Class) converterType).iterator().next();
+ }
+ return BeanUtils.instantiateClass(converterType);
+ }
+ }
+
+ /**
+ * {@link PropertyValueConverterFactory} implementation that leverages the {@link BeanFactory} to create the desired
+ * {@link PropertyValueConverter}. This allows the {@link PropertyValueConverter} to make use of DI.
+ *
+ * @author Christoph Strobl
+ * @since 2.7
+ */
+ static class BeanFactoryAwarePropertyValueConverterFactory implements PropertyValueConverterFactory {
+
+ private final BeanFactory beanFactory;
+
+ BeanFactoryAwarePropertyValueConverterFactory(BeanFactory beanFactory) {
+ this.beanFactory = beanFactory;
+ }
+
+ @Override
+ public > PropertyValueConverter getConverter(
+ Class extends PropertyValueConverter> converterType) {
+
+ Assert.state(beanFactory != null, "BeanFactory must not be null. Did you forget to set it!");
+ Assert.notNull(converterType, "ConverterType must not be null!");
+
+ try {
+ return beanFactory.getBean(converterType);
+ } catch (NoSuchBeanDefinitionException exception) {
+
+ if (beanFactory instanceof AutowireCapableBeanFactory) {
+ return (PropertyValueConverter) ((AutowireCapableBeanFactory) beanFactory).createBean(converterType,
+ AutowireCapableBeanFactory.AUTOWIRE_CONSTRUCTOR, false);
+ }
+ }
+ return null;
+ }
+ }
+
+ /**
+ * {@link PropertyValueConverterFactory} implementation that serves {@link PropertyValueConverter} from a given
+ * {@link ValueConverterRegistry registry}.
+ *
+ * @author Christoph Strobl
+ * @since 2.7
+ */
+ static class ConfiguredInstanceServingValueConverterFactory implements PropertyValueConverterFactory {
+
+ private final ValueConverterRegistry> converterRegistry;
+
+ ConfiguredInstanceServingValueConverterFactory(ValueConverterRegistry> converterRegistry) {
+
+ Assert.notNull(converterRegistry, "ConversionsRegistrar must not be null!");
+ this.converterRegistry = converterRegistry;
+ }
+
+ @Nullable
+ @Override
+ public > PropertyValueConverter getConverter(
+ PersistentProperty> property) {
+ return (PropertyValueConverter) converterRegistry.getConverter(property.getOwner().getType(),
+ property.getName());
+ }
+
+ @Override
+ public > PropertyValueConverter getConverter(
+ Class extends PropertyValueConverter> converterType) {
+ return null;
+ }
+ }
+
+ /**
+ * {@link PropertyValueConverterFactory} implementation that caches converters provided by an underlying
+ * {@link PropertyValueConverterFactory factory}.
+ *
+ * @author Christoph Strobl
+ * @since 2.7
+ */
+ static class CachingPropertyValueConverterFactory implements PropertyValueConverterFactory {
+
+ private final PropertyValueConverterFactory delegate;
+ private final Cache cache = new Cache();
+
+ CachingPropertyValueConverterFactory(PropertyValueConverterFactory delegate) {
+
+ Assert.notNull(delegate, "Delegate must not be null!");
+ this.delegate = delegate;
+ }
+
+ @Nullable
+ @Override
+ public > PropertyValueConverter getConverter(
+ PersistentProperty> property) {
+
+ Optional> PropertyValueConverter getConverter(
+ Class extends PropertyValueConverter> converterType) {
+
+ Optional> PropertyValueConverter cache(PersistentProperty> property,
+ @Nullable PropertyValueConverter converter) {
+ perPropertyCache.putIfAbsent(property, Optional.ofNullable(converter));
+ cache(property.getValueConverterType(), converter);
+ return converter;
+ }
+
+ > PropertyValueConverter cache(Class> type,
+ @Nullable PropertyValueConverter converter) {
+ typeCache.putIfAbsent(type, Optional.ofNullable(converter));
+ return converter;
+ }
+ }
+ }
+}
diff --git a/src/main/java/org/springframework/data/convert/PropertyValueConverterFactory.java b/src/main/java/org/springframework/data/convert/PropertyValueConverterFactory.java
new file mode 100644
index 0000000000..7a40cf3d5a
--- /dev/null
+++ b/src/main/java/org/springframework/data/convert/PropertyValueConverterFactory.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2022 the original author or authors.
+ *
+ * Licensed 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
+ *
+ * https://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.springframework.data.convert;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.springframework.beans.factory.BeanFactory;
+import org.springframework.data.convert.PropertyValueConverterFactories.BeanFactoryAwarePropertyValueConverterFactory;
+import org.springframework.data.convert.PropertyValueConverterFactories.CachingPropertyValueConverterFactory;
+import org.springframework.data.convert.PropertyValueConverterFactories.ChainedPropertyValueConverterFactory;
+import org.springframework.data.convert.PropertyValueConverterFactories.ConfiguredInstanceServingValueConverterFactory;
+import org.springframework.data.convert.PropertyValueConverterFactories.SimplePropertyConverterFactory;
+import org.springframework.data.mapping.PersistentProperty;
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
+
+/**
+ * A factory that provides {@link PropertyValueConverter value converters}.
+ *
+ * registrar.registerConverter(Person.class, Person::getName) //
+ * .writing(StringConverter::encrypt) //
+ * .reading(StringConverter::decrypt);
+ *
+ *
+ * @author Christoph Strobl
+ * @author Oliver Drotbohm
+ * @since 2.7
+ */
+public class PropertyValueConverterRegistrar the property type
+ * @param type the domain type to obtain the property from
+ * @param property a function to describe the property to be referenced. Usually a method handle to a getter.
+ * @return will never be {@literal null}.
+ */
+ public the property type
+ * @param type the domain type to obtain the property from
+ * @param propertyName a function to describe the property to be referenced. Usually a method handle to a getter.
+ * @return will never be {@literal null}.
+ */
+ public propertyType) {
+ return new WritingConverterRegistrationBuilder<>(type, propertyName, this);
+ }
+
+ /**
+ * Register the given converter for the types property identified via its name.
+ *
+ * @param type the domain type to obtain the property from
+ * @param path the property name.
+ * @param converter the converter to apply.
+ * @return this.
+ */
+ public PropertyValueConverterRegistrar writer) {
+ return writing((source, context) -> writer.apply(source));
+ }
+
+ /**
+ * Describes how to convert the domain property value into the database native property.
+ *
+ * @param , R> writer) {
+ return new ReadingConverterRegistrationBuilder<>(this, writer);
+ }
+ }
+
+ /**
+ * A helper to build a fluent API to register how to read a database value into a domain object property.
+ *
+ * @author Oliver Drotbohm
+ */
+ public static class ReadingConverterRegistrationBuilder, R> writer;
+
+ ReadingConverterRegistrationBuilder(WritingConverterRegistrationBuilder, R> writer) {
+ this.origin = origin;
+ this.writer = writer;
+ }
+
+ public PropertyValueConverterRegistrar PropertyValueConverter> getConverter(Class> type,
+ String path) {
+
+ return (PropertyValueConverter>) converterRegistrationMap
+ .get(new Key(type, path));
+ }
+
+ /**
+ * @return the number of registered converters.
+ */
+ public int size() {
+ return converterRegistrationMap.size();
+ }
+
+ public boolean isEmpty() {
+ return converterRegistrationMap.isEmpty();
+ }
+
+ /**
+ * Obtain the underlying (mutable) map of converters.
+ *
+ * @return never {@literal null}.
+ */
+ Map
+ * Can be used as meta annotation utilizing {@link org.springframework.core.annotation.AliasFor}.
+ *
+ * @param PropertyValueConverter> getConverter(Class> type, String path);
+
+ /**
+ * Check if a converter is registered for the given type, path combination.
+ *
+ * @param type the target type. Must not be {@literal null}.
+ * @param path the property name. Must not be {@literal null}.
+ * @return {@literal false} if no converter present for the given type/path combination.
+ */
+ default boolean containsConverterFor(Class> type, String path) {
+ return getConverter(type, path) != null;
+ }
+
+ /**
+ * Check if converters are registered.
+ */
+ boolean isEmpty();
+
+ /**
+ * Obtain a simple {@link ValueConverterRegistry}.
+ *
+ * @param > PropertyValueConverter getConverter(
+ Class extends PropertyValueConverter> converterType) {
+ return null;
+ }
+ }, new PropertyValueConverterFactory() {
+ @Nullable
+ @Override
+ public > PropertyValueConverter getConverter(
+ Class extends PropertyValueConverter> converterType) {
+ return expected;
+ }
+ });
+
+ assertThat(factory.getConverter(ConverterWithDefaultCtor.class)).isSameAs(expected);
+ }
+
+ @Test // GH-1484
+ void chainedConverterFactoryFailsOnException() {
+
+ PropertyValueConverterFactory factory = PropertyValueConverterFactory.chained(new PropertyValueConverterFactory() {
+ @Nullable
+ @Override
+ public > PropertyValueConverter getConverter(
+ Class extends PropertyValueConverter> converterType) {
+ return null;
+ }
+ }, new PropertyValueConverterFactory() {
+ @Nullable
+ @Override
+ public > PropertyValueConverter getConverter(
+ Class extends PropertyValueConverter> converterType) {
+ throw new RuntimeException("can't touch this!");
+ }
+ });
+
+ assertThatExceptionOfType(RuntimeException.class)
+ .isThrownBy(() -> factory.getConverter(ConverterWithDefaultCtor.class));
+ }
+
+ @Test // GH-1484
+ void cachingConverterFactoryServesCachedInstance() {
+
+ PropertyValueConverterFactory factory = PropertyValueConverterFactory
+ .caching(PropertyValueConverterFactory.simple());
+ assertThat(factory.getConverter(ConverterWithDefaultCtor.class))
+ .isSameAs(factory.getConverter(ConverterWithDefaultCtor.class));
+ }
+
+ @Test // GH-1484
+ void cachingConverterFactoryAlsoCachesAbsenceOfConverter() {
+
+ PropertyValueConverterFactory source = Mockito.spy(PropertyValueConverterFactory.simple());
+ PropertyValueConverterFactory factory = PropertyValueConverterFactory.caching(source);
+
+ PersistentEntity entity = mock(PersistentEntity.class);
+ PersistentProperty property = mock(PersistentProperty.class);
+ when(property.getOwner()).thenReturn(entity);
+ when(entity.getType()).thenReturn(Person.class);
+ when(property.getName()).thenReturn("firstname");
+
+ // fill the cache
+ assertThat(factory.getConverter(property)).isNull();
+ verify(source).getConverter(any(PersistentProperty.class));
+
+ // now get the cached null value
+ assertThat(factory.getConverter(property)).isNull();
+ verify(source).getConverter(any(PersistentProperty.class));
+ }
+
+ @Test // GH-1484
+ void cachingConverterFactoryServesCachedInstanceForProperty() {
+
+ PersistentProperty property = mock(PersistentProperty.class);
+ when(property.hasValueConverter()).thenReturn(true);
+ when(property.getValueConverterType()).thenReturn(ConverterWithDefaultCtor.class);
+
+ PropertyValueConverterFactory factory = PropertyValueConverterFactory
+ .caching(PropertyValueConverterFactory.simple());
+ assertThat(factory.getConverter(property)) //
+ .isSameAs(factory.getConverter(property)) //
+ .isSameAs(factory.getConverter(ConverterWithDefaultCtor.class)); // TODO: is this a valid assumption?
+ }
+
+ static class ConverterWithDefaultCtor
+ implements PropertyValueConverter