From caf49ad739c63ad919cfcefe3c8255d6037a2d42 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 10 Dec 2021 12:18:54 +0100 Subject: [PATCH] Add support for property-specific converters. Closes #1484 Original pull request: #2566. --- .../data/convert/CustomConversions.java | 51 ++++ .../data/convert/PropertyConverter.java | 47 ++++ .../convert/PropertyValueConversions.java | 54 ++++ .../data/convert/PropertyValueConverter.java | 85 +++++++ .../PropertyValueConverterFactories.java | 228 +++++++++++++++++ .../PropertyValueConverterFactory.java | 137 ++++++++++ .../PropertyValueConverterRegistrar.java | 99 ++++++++ .../SimplePropertyValueConversions.java | 89 +++++++ .../data/mapping/PersistentProperty.java | 13 + .../AnnotationBasedPersistentProperty.java | 21 +- .../convert/CustomConversionsUnitTests.java | 20 ++ ...ropertyValueConverterFactoryUnitTests.java | 239 ++++++++++++++++++ .../data/convert/WhatWeWant.java | 90 +++++++ .../AbstractPersistentPropertyUnitTests.java | 6 +- ...ationBasedPersistentPropertyUnitTests.java | 49 ++++ 15 files changed, 1220 insertions(+), 8 deletions(-) create mode 100644 src/main/java/org/springframework/data/convert/PropertyConverter.java create mode 100644 src/main/java/org/springframework/data/convert/PropertyValueConversions.java create mode 100644 src/main/java/org/springframework/data/convert/PropertyValueConverter.java create mode 100644 src/main/java/org/springframework/data/convert/PropertyValueConverterFactories.java create mode 100644 src/main/java/org/springframework/data/convert/PropertyValueConverterFactory.java create mode 100644 src/main/java/org/springframework/data/convert/PropertyValueConverterRegistrar.java create mode 100644 src/main/java/org/springframework/data/convert/SimplePropertyValueConversions.java create mode 100644 src/test/java/org/springframework/data/convert/PropertyValueConverterFactoryUnitTests.java create mode 100644 src/test/java/org/springframework/data/convert/WhatWeWant.java diff --git a/src/main/java/org/springframework/data/convert/CustomConversions.java b/src/main/java/org/springframework/data/convert/CustomConversions.java index e9a121063..de29695fa 100644 --- a/src/main/java/org/springframework/data/convert/CustomConversions.java +++ b/src/main/java/org/springframework/data/convert/CustomConversions.java @@ -33,6 +33,7 @@ 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; @@ -96,6 +97,8 @@ public class CustomConversions { private final Function> getRawWriteTarget = convertiblePair -> getCustomTarget( convertiblePair.getSourceType(), null, writingPairs); + private PropertyValueConversions propertyValueConversions; + /** * @param converterConfiguration the {@link ConverterConfiguration} to apply. * @since 2.3 @@ -118,6 +121,7 @@ public class CustomConversions { this.converters = Collections.unmodifiableList(registeredConverters); this.simpleTypeHolder = new SimpleTypeHolder(customSimpleTypes, converterConfiguration.getStoreConversions().getStoreTypeHolder()); + this.propertyValueConversions = converterConfiguration.getPropertyValueConversions(); } /** @@ -170,6 +174,36 @@ public class CustomConversions { VavrCollectionConverters.getConvertersToRegister().forEach(it -> registerConverterIn(it, conversionService)); } + /** + * Delegate check if a {@link PropertyValueConverter} for the given {@literal property} is present via + * {@link PropertyValueConversions}. + * + * @param property must not be {@literal null}. + * @return {@literal true} if a specific {@link PropertyValueConverter} is available. + * @see PropertyValueConversions#hasValueConverter(PersistentProperty) + * @since ? + */ + public boolean hasPropertyValueConverter(PersistentProperty property) { + return propertyValueConversions != null ? propertyValueConversions.hasValueConverter(property) : false; + } + + /** + * Delegate to obtain the {@link PropertyValueConverter} for the given {@literal property} from + * {@link PropertyValueConversions}. + * + * @param property must not be {@literal null}. param domain specific type + * @param store native type + * @param conversion context type + * @return the suitable {@link PropertyValueConverter} or {@literal null} if none available. + * @see PropertyValueConversions#getValueConverter(PersistentProperty) + * @since ? + */ + @Nullable + public PropertyValueConverter getPropertyValueConverter( + PersistentProperty property) { + return propertyValueConversions != null ? propertyValueConversions.getValueConverter(property) : null; + } + /** * Get all converters and add origin information * @@ -862,6 +896,7 @@ public class CustomConversions { private final StoreConversions storeConversions; private final List userConverters; private final Predicate converterRegistrationFilter; + private final PropertyValueConversions propertyValueConversions; /** * Create a new ConverterConfiguration holding the given {@link StoreConversions} and user defined converters. @@ -887,9 +922,16 @@ public class CustomConversions { public ConverterConfiguration(StoreConversions storeConversions, List userConverters, Predicate converterRegistrationFilter) { + this(storeConversions, userConverters, converterRegistrationFilter, new SimplePropertyValueConversions()); + } + + public ConverterConfiguration(StoreConversions storeConversions, List userConverters, + Predicate converterRegistrationFilter, @Nullable PropertyValueConversions propertyValueConversions) { + this.storeConversions = storeConversions; this.userConverters = new ArrayList<>(userConverters); this.converterRegistrationFilter = converterRegistrationFilter; + this.propertyValueConversions = propertyValueConversions; } /** @@ -912,5 +954,14 @@ public class CustomConversions { boolean shouldRegister(ConvertiblePair candidate) { return this.converterRegistrationFilter.test(candidate); } + + /** + * @return the configured {@link PropertyValueConversions} if set, {@literal null} otherwise. + * @since ? + */ + @Nullable + public PropertyValueConversions getPropertyValueConversions() { + return this.propertyValueConversions; + } } } diff --git a/src/main/java/org/springframework/data/convert/PropertyConverter.java b/src/main/java/org/springframework/data/convert/PropertyConverter.java new file mode 100644 index 000000000..43fc76d35 --- /dev/null +++ b/src/main/java/org/springframework/data/convert/PropertyConverter.java @@ -0,0 +1,47 @@ +/* + * 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 java.lang.annotation.ElementType.*; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.data.convert.PropertyValueConverter.ObjectToObjectPropertyValueConverter; + +/** + * Annotation to define usage of a {@link PropertyValueConverter} to read/write the property. + *

+ * 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 + * @since 2.7 + */ +public interface PropertyValueConverter { + + /** + * Convert the given store specific value into it's domain value representation. Typically a {@literal read} + * operation. + * + * @param nativeValue can be {@literal null}. + * @param context never {@literal null}. + * @return the converted value. Can be {@literal null}. + */ + @Nullable + A /*read*/ nativeToDomain(@Nullable B nativeValue, C context); + + /** + * Convert the given domain specific value into it's native store representation. Typically a {@literal write} + * operation. + * + * @param domainValue can be {@literal null}. + * @param context never {@literal null}. + * @return the converted value. Can be {@literal null}. + */ + @Nullable + B /*write*/ domainToNative(@Nullable A domainValue, C context); + + /** + * @author Christoph Strobl + */ + interface ValueConversionContext { + + PersistentProperty getProperty(); + } + + /** + * NoOp {@link PropertyValueConverter} implementation. + * + * @author Christoph Strobl + */ + enum ObjectToObjectPropertyValueConverter implements PropertyValueConverter { + + INSTANCE; + + @Override + public Object nativeToDomain(Object value, ValueConversionContext context) { + return value; + } + + @Override + public Object domainToNative(Object value, ValueConversionContext context) { + return value; + } + } + +} 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 000000000..1843e263c --- /dev/null +++ b/src/main/java/org/springframework/data/convert/PropertyValueConverterFactories.java @@ -0,0 +1,228 @@ +/* + * 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.EnumSet; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +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 CompositePropertyValueConverterFactory chained} and the created {@link PropertyValueConverter converter} + * {@link CachingPropertyValueConverterFactory cached}. + * + * @author Christoph Strobl + * @since ? + */ +final class PropertyValueConverterFactories { + + /** + * @author Christoph Strobl + * @since ? + */ + static class CompositePropertyValueConverterFactory implements PropertyValueConverterFactory { + + private List delegates; + + CompositePropertyValueConverterFactory(PropertyValueConverterFactory... delegates) { + this(Arrays.asList(delegates)); + } + + CompositePropertyValueConverterFactory(List delegates) { + this.delegates = delegates; + } + + @Nullable + @Override + public PropertyValueConverter getConverter( + PersistentProperty property) { + return delegates.stream().map(it -> (PropertyValueConverter) it.getConverter(property)) + .filter(Objects::nonNull).findFirst().orElse(null); + } + + @Override + public PropertyValueConverter getConverter( + Class> converterType) { + return delegates.stream().filter(it -> it.getConverter(converterType) != null).findFirst() + .map(it -> it.getConverter(converterType)).orElse(null); + } + } + + /** + * Trivial implementation of {@link PropertyValueConverter}. + * + * @author Christoph Strobl + * @since ? + */ + static class SimplePropertyConverterFactory implements PropertyValueConverterFactory { + + @Override + public PropertyValueConverter getConverter( + Class> 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 ? + */ + static class BeanFactoryAwarePropertyValueConverterFactory implements PropertyValueConverterFactory { + + private final BeanFactory beanFactory; + + public BeanFactoryAwarePropertyValueConverterFactory(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + @Override + public PropertyValueConverter getConverter( + Class> 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; + } + } + + /** + * @author Christoph Strobl + * @since ? + */ + static class ConfiguredInstanceServingValueConverterFactory implements PropertyValueConverterFactory { + + private final PropertyValueConverterRegistrar conversionsRegistrar; + + public ConfiguredInstanceServingValueConverterFactory(PropertyValueConverterRegistrar conversionsRegistrar) { + + Assert.notNull(conversionsRegistrar, "ConversionsRegistrar must not be null!"); + this.conversionsRegistrar = conversionsRegistrar; + } + + @Nullable + @Override + public PropertyValueConverter getConverter( + PersistentProperty property) { + return (PropertyValueConverter) conversionsRegistrar.getConverter(property.getOwner().getType(), + property.getName()); + } + + @Override + public PropertyValueConverter getConverter( + Class> converterType) { + return null; + } + } + + /** + * TODO: This would be easier if we'd get rid of {@link PropertyValueConverterFactory#getConverter(Class)}. + * + * @author Christoph Strobl + * @since ? + */ + static class CachingPropertyValueConverterFactory implements PropertyValueConverterFactory { + + private final PropertyValueConverterFactory delegate; + private final Cache cache = new Cache(); + + public CachingPropertyValueConverterFactory(PropertyValueConverterFactory delegate) { + + Assert.notNull(delegate, "Delegate must not be null!"); + this.delegate = delegate; + } + + @Nullable + @Override + public PropertyValueConverter getConverter( + PersistentProperty property) { + + PropertyValueConverter converter = cache.get(property); + if (converter != null) { + return converter; + } + return cache.cache(property, delegate.getConverter(property)); + } + + @Override + public PropertyValueConverter getConverter( + Class> converterType) { + + PropertyValueConverter converter = cache.get(converterType); + if (converter != null) { + return converter; + } + return cache.cache(converterType, delegate.getConverter(converterType)); + } + + static class Cache { + + Map, PropertyValueConverter> perPropertyCache = new HashMap<>(); + Map, PropertyValueConverter> typeCache = new HashMap<>(); + + PropertyValueConverter get(PersistentProperty property) { + return perPropertyCache.get(property); + } + + PropertyValueConverter get(Class type) { + return typeCache.get(type); + } + + PropertyValueConverter cache(PersistentProperty property, + PropertyValueConverter converter) { + perPropertyCache.putIfAbsent(property, converter); + cache(property.getValueConverterType(), converter); + return converter; + } + + PropertyValueConverter cache(Class type, + PropertyValueConverter converter) { + typeCache.putIfAbsent(type, 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 000000000..bb5cf47a8 --- /dev/null +++ b/src/main/java/org/springframework/data/convert/PropertyValueConverterFactory.java @@ -0,0 +1,137 @@ +/* + * 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.CompositePropertyValueConverterFactory; +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}. + *

+ * 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 ? + */ +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 + * @return can be {@literal null}. + */ + @Nullable + default PropertyValueConverter getConverter( + PersistentProperty property) { + + if (!property.hasValueConverter()) { + return null; + } + return getConverter((Class>) property.getValueConverterType()); + } + + @Nullable + PropertyValueConverter getConverter( + Class> converterType); + + /** + * Obtain a simple {@link PropertyValueConverterFactory} capable of instantiating {@link PropertyValueConverter} + * implementations via their default {@link java.lang.reflect.Constructor} or in case of an {@link Enum} accessing the + * first enum value. + * + * @return new instance of {@link PropertyValueConverterFactory}. + */ + static PropertyValueConverterFactory simple() { + return new SimplePropertyConverterFactory(); + } + + /** + * Obtain a {@link PropertyValueConverterFactory} capable of looking up/creating the {@link PropertyValueConverter} + * via the given {@link BeanFactory}. + * + * @param beanFactory must not be {@literal null}. + * @return new instance of {@link PropertyValueConverterFactory}. + */ + static PropertyValueConverterFactory beanFactoryAware(BeanFactory beanFactory) { + return new BeanFactoryAwarePropertyValueConverterFactory(beanFactory); + } + + /** + * Obtain a {@link PropertyValueConverterFactory} capable of looking up the {@link PropertyValueConverter} in the + * given {@link PropertyValueConverterRegistrar}. + * + * @param registrar must not be {@literal null}. + * @return new instance of {@link PropertyValueConverterFactory}. + */ + static PropertyValueConverterFactory configuredInstance(PropertyValueConverterRegistrar registrar) { + return new ConfiguredInstanceServingValueConverterFactory(registrar); + } + + /** + * Obtain a {@link PropertyValueConverterFactory} that will try to obtain a {@link PropertyValueConverter} from the + * given array of {@link PropertyValueConverterFactory factories} by returning the first non {@literal null} one. + * + * @param factories must not be {@literal null} nor contain {@literal null} values. + * @return new instance of {@link PropertyValueConverterFactory}. + */ + static PropertyValueConverterFactory chained(PropertyValueConverterFactory... factories) { + return chained(Arrays.asList(factories)); + } + + /** + * Obtain a {@link PropertyValueConverterFactory} that will try to obtain a {@link PropertyValueConverter} from the + * given list of {@link PropertyValueConverterFactory factories} by returning the first non {@literal null} one. + * + * @param factoryList must not be {@literal null} nor contain {@literal null} values. + * @return new instance of {@link PropertyValueConverterFactory}. + */ + static PropertyValueConverterFactory chained(List factoryList) { + + Assert.noNullElements(factoryList, "FactoryList must not contain null elements."); + + if (factoryList.size() == 1) { + return factoryList.iterator().next(); + } + + return new CompositePropertyValueConverterFactory(factoryList); + } + + /** + * Obtain a {@link PropertyValueConverterFactory} that will cache {@link PropertyValueConverter} instances per + * {@link PersistentProperty}. + * + * @param factory + * @return + */ + static PropertyValueConverterFactory caching(PropertyValueConverterFactory factory) { + return new CachingPropertyValueConverterFactory(factory); + } +} diff --git a/src/main/java/org/springframework/data/convert/PropertyValueConverterRegistrar.java b/src/main/java/org/springframework/data/convert/PropertyValueConverterRegistrar.java new file mode 100644 index 000000000..73d588836 --- /dev/null +++ b/src/main/java/org/springframework/data/convert/PropertyValueConverterRegistrar.java @@ -0,0 +1,99 @@ +/* + * 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.Collection; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.lang.Nullable; +import org.springframework.util.ObjectUtils; + +/** + * @author Christoph Strobl + * @since ? + */ +public class PropertyValueConverterRegistrar { + + private final Map> converterRegistrationMap = new LinkedHashMap<>(); + + boolean hasConverterFor(Class type, String path) { + return converterRegistrationMap.containsKey(new Key(type, path)); + } + + @Nullable + public PropertyValueConverter getConverter(Class type, String path) { + return converterRegistrationMap.get(new Key(type, path)); + } + + public int size() { + return converterRegistrationMap.size(); + } + + public boolean isEmpty() { + return converterRegistrationMap.isEmpty(); + } + + public PropertyValueConverterRegistrar register(Class type, String path, + PropertyValueConverter converter) { + + converterRegistrationMap.put(new Key(type, path), converter); + return this; + } + + public Collection> converters() { + return converterRegistrationMap.values(); + } + + public PropertyValueConverterRegistrar registerIfAbsent(Class type, String path, + PropertyValueConverter converter) { + converterRegistrationMap.putIfAbsent(new Key(type, path), converter); + return this; + } + + static class Key { + + Class type; + String path; + + public Key(Class type, String path) { + this.type = type; + this.path = path; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + Key key = (Key) o; + + if (!ObjectUtils.nullSafeEquals(type, key.type)) { + return false; + } + return ObjectUtils.nullSafeEquals(path, key.path); + } + + @Override + public int hashCode() { + int result = ObjectUtils.nullSafeHashCode(type); + result = 31 * result + ObjectUtils.nullSafeHashCode(path); + return result; + } + } +} diff --git a/src/main/java/org/springframework/data/convert/SimplePropertyValueConversions.java b/src/main/java/org/springframework/data/convert/SimplePropertyValueConversions.java new file mode 100644 index 000000000..2e13dd7d0 --- /dev/null +++ b/src/main/java/org/springframework/data/convert/SimplePropertyValueConversions.java @@ -0,0 +1,89 @@ +/* + * 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.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.lang.Nullable; + +/** + * @author Christoph Strobl + * @since ? + */ +public class SimplePropertyValueConversions implements PropertyValueConversions, InitializingBean { + + private @Nullable PropertyValueConverterFactory converterFactory; + private @Nullable PropertyValueConverterRegistrar converterRegistrar; + private boolean converterCacheEnabled = true; + private AtomicBoolean initialized = new AtomicBoolean(false); + + public void setConverterFactory(PropertyValueConverterFactory converterFactory) { + this.converterFactory = converterFactory; + } + + public void setConverterRegistrar(PropertyValueConverterRegistrar converterRegistrar) { + this.converterRegistrar = converterRegistrar; + } + + public void setConverterCacheEnabled(boolean converterCacheEnabled) { + this.converterCacheEnabled = converterCacheEnabled; + } + + @Override + public PropertyValueConverter getValueConverter( + PersistentProperty property) { + + if (!initialized.get()) { + init(); + } + + return this.converterFactory.getConverter(property); + } + + public void init() { + + if (initialized.compareAndSet(false, true)) { + List factoryList = new ArrayList<>(3); + + if (converterFactory != null) { + factoryList.add(converterFactory); + } else { + factoryList.add(PropertyValueConverterFactory.simple()); + } + + if (converterRegistrar != null && !converterRegistrar.isEmpty()) { + factoryList.add(PropertyValueConverterFactory.configuredInstance(converterRegistrar)); + } + + PropertyValueConverterFactory targetFactory = factoryList.size() > 1 + ? PropertyValueConverterFactory.chained(factoryList) + : factoryList.iterator().next(); + + this.converterFactory = converterCacheEnabled ? PropertyValueConverterFactory.caching(targetFactory) + : targetFactory; + } + } + + @Override + public void afterPropertiesSet() throws Exception { + + init(); + } +} diff --git a/src/main/java/org/springframework/data/mapping/PersistentProperty.java b/src/main/java/org/springframework/data/mapping/PersistentProperty.java index 6e420d474..30fc41e80 100644 --- a/src/main/java/org/springframework/data/mapping/PersistentProperty.java +++ b/src/main/java/org/springframework/data/mapping/PersistentProperty.java @@ -23,6 +23,9 @@ import java.util.Map; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.data.convert.PropertyConverter; +import org.springframework.data.convert.PropertyValueConverter; +import org.springframework.data.convert.PropertyValueConverter.ValueConversionContext; import org.springframework.data.util.TypeInformation; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -422,4 +425,14 @@ public interface PersistentProperty

> { return getOwner().getPropertyAccessor(owner); } + + @Nullable + default Class> getValueConverterType() { + PropertyConverter annotation = findAnnotation(PropertyConverter.class); + return annotation == null ? null : annotation.value(); + } + + default boolean hasValueConverter() { + return isAnnotationPresent(PropertyConverter.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 76b96d9cf..318241eb8 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.ReadOnlyProperty; import org.springframework.data.annotation.Reference; import org.springframework.data.annotation.Transient; import org.springframework.data.annotation.Version; +import org.springframework.data.convert.PropertyConverter; +import org.springframework.data.convert.PropertyValueConverter; +import org.springframework.data.convert.PropertyValueConverter.ValueConversionContext; import org.springframework.data.mapping.Association; import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.PersistentEntity; @@ -132,8 +135,7 @@ public abstract class AnnotationBasedPersistentProperty

> getValueConverterType() { + + return doFindAnnotation(PropertyConverter.class) // + .map(PropertyConverter::value) // + .orElse(null); + } + @Override public String toString() { @@ -315,8 +325,7 @@ public abstract class AnnotationBasedPersistentProperty

loadIdentityType() { - return (Class) ReflectionUtils.loadIfPresent( - "org.jmolecules.ddd.annotation.Identity", + return (Class) 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 ab4a34cf8..9a337381a 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; /** @@ -273,6 +274,25 @@ class CustomConversionsUnitTests { 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) { var 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 000000000..15426b866 --- /dev/null +++ b/src/test/java/org/springframework/data/convert/PropertyValueConverterFactoryUnitTests.java @@ -0,0 +1,239 @@ +/* + * 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.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.data.convert.PropertyValueConverter.ValueConversionContext; +import org.springframework.data.mapping.PersistentProperty; +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 compositeConverterFactoryIteratesFactories() { + + PropertyValueConverter expected = mock(PropertyValueConverter.class); + + PropertyValueConverterFactory factory = PropertyValueConverterFactory.chained(new PropertyValueConverterFactory() { + @Nullable + @Override + public PropertyValueConverter getConverter( + Class> converterType) { + return null; + } + }, new PropertyValueConverterFactory() { + @Nullable + @Override + public PropertyValueConverter getConverter( + Class> converterType) { + return expected; + } + }); + + assertThat(factory.getConverter(ConverterWithDefaultCtor.class)).isSameAs(expected); + } + + @Test // GH-1484 + void compositeConverterFactoryFailsOnException() { + + PropertyValueConverterFactory factory = PropertyValueConverterFactory.chained(new PropertyValueConverterFactory() { + @Nullable + @Override + public PropertyValueConverter getConverter( + Class> converterType) { + return null; + } + }, new PropertyValueConverterFactory() { + @Nullable + @Override + public PropertyValueConverter getConverter( + Class> 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 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 { + + @Nullable + @Override + public String nativeToDomain(@Nullable UUID nativeValue, ValueConversionContext context) { + return nativeValue.toString(); + } + + @Nullable + @Override + public UUID domainToNative(@Nullable String domainValue, ValueConversionContext context) { + return UUID.fromString(domainValue); + } + } + + enum ConverterEnum implements PropertyValueConverter { + + INSTANCE; + + @Nullable + @Override + public String nativeToDomain(@Nullable UUID nativeValue, ValueConversionContext context) { + return nativeValue.toString(); + } + + @Nullable + @Override + public UUID domainToNative(@Nullable String domainValue, ValueConversionContext context) { + return UUID.fromString(domainValue); + } + } + + static class ConverterWithDependency implements PropertyValueConverter { + + private final SomeDependency someDependency; + + public ConverterWithDependency(@Autowired SomeDependency someDependency) { + this.someDependency = someDependency; + } + + @Nullable + @Override + public String nativeToDomain(@Nullable UUID nativeValue, ValueConversionContext context) { + + assertThat(someDependency).isNotNull(); + return nativeValue.toString(); + } + + @Nullable + @Override + public UUID domainToNative(@Nullable String domainValue, ValueConversionContext context) { + + assertThat(someDependency).isNotNull(); + return UUID.fromString(domainValue); + } + } + + static class SomeDependency { + + } +} diff --git a/src/test/java/org/springframework/data/convert/WhatWeWant.java b/src/test/java/org/springframework/data/convert/WhatWeWant.java new file mode 100644 index 000000000..9092b40e1 --- /dev/null +++ b/src/test/java/org/springframework/data/convert/WhatWeWant.java @@ -0,0 +1,90 @@ +/* + * 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 + * + * 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. + */ + +/* + * 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 + * + * 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.springframework.data.convert; + +import java.util.function.Predicate; + +import org.junit.jupiter.api.Test; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mapping.PropertyPath; +import org.springframework.lang.Nullable; + +/** + * @author Christoph Strobl + * @since 2022/01 + */ +public class WhatWeWant { + + @Test + void converterConfig() { + + ConverterConfig converterConfig = null; + + converterConfig.registerConverter(Foo.class, "value", new PropertyValueConverter() { + + @Nullable + @Override + public Object nativeToDomain(@Nullable Object nativeValue, ValueConversionContext context) { + return null; + } + + @Nullable + @Override + public Object domainToNative(@Nullable Object domainValue, ValueConversionContext context) { + return null; + } + }); + + } + + static class ConverterConfig { + + ConverterConfig registerConverter(Predicate> filter, PropertyValueConverter converter) { + return this; + } + + ConverterConfig registerConverter(Class type, String property, PropertyValueConverter converter) { + PropertyPath.from(property, type); + return this; + } + } + + static class Foo { + String value; + } + + interface SpecificValueConversionContext extends PropertyValueConverter.ValueConversionContext { + + } + + interface SpecificPropertyValueConverter extends PropertyValueConverter {} +} diff --git a/src/test/java/org/springframework/data/mapping/model/AbstractPersistentPropertyUnitTests.java b/src/test/java/org/springframework/data/mapping/model/AbstractPersistentPropertyUnitTests.java index fa9554067..7d4c84fb2 100755 --- a/src/test/java/org/springframework/data/mapping/model/AbstractPersistentPropertyUnitTests.java +++ b/src/test/java/org/springframework/data/mapping/model/AbstractPersistentPropertyUnitTests.java @@ -36,6 +36,7 @@ import org.jmolecules.ddd.types.Identifier; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.data.convert.PropertyValueConverter; import org.springframework.data.mapping.Association; import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.PersistentProperty; @@ -43,6 +44,7 @@ import org.springframework.data.mapping.Person; import org.springframework.data.util.ClassTypeInformation; import org.springframework.data.util.Optionals; import org.springframework.data.util.TypeInformation; +import org.springframework.lang.Nullable; import org.springframework.util.ReflectionUtils; /** @@ -233,8 +235,7 @@ public class AbstractPersistentPropertyUnitTests { assertThat(property.isAssociation()).isTrue(); assertThat(property.getAssociationTargetType()).isEqualTo(JMoleculesAggregate.class); - assertThat(property.getPersistentEntityTypeInformation()) - .extracting(it -> it.getType()) + assertThat(property.getPersistentEntityTypeInformation()).extracting(it -> it.getType()) .containsExactly((Class) JMoleculesAggregate.class); } @@ -385,6 +386,7 @@ public class AbstractPersistentPropertyUnitTests { public A findPropertyOrOwnerAnnotation(Class annotationType) { return null; } + } static class Sample { diff --git a/src/test/java/org/springframework/data/mapping/model/AnnotationBasedPersistentPropertyUnitTests.java b/src/test/java/org/springframework/data/mapping/model/AnnotationBasedPersistentPropertyUnitTests.java index 909ce1fbc..9ac5b96e3 100755 --- a/src/test/java/org/springframework/data/mapping/model/AnnotationBasedPersistentPropertyUnitTests.java +++ b/src/test/java/org/springframework/data/mapping/model/AnnotationBasedPersistentPropertyUnitTests.java @@ -36,6 +36,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.annotation.AliasFor; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.data.annotation.AccessType; @@ -44,6 +45,9 @@ import org.springframework.data.annotation.Id; import org.springframework.data.annotation.ReadOnlyProperty; import org.springframework.data.annotation.Reference; import org.springframework.data.annotation.Transient; +import org.springframework.data.convert.PropertyConverter; +import org.springframework.data.convert.PropertyValueConverter; +import org.springframework.data.convert.PropertyValueConverter.ValueConversionContext; import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.context.SampleMappingContext; @@ -529,4 +533,49 @@ public class AnnotationBasedPersistentPropertyUnitTests

{} + + static class WithPropertyConverter { + + @PropertyConverter(MyPropertyConverter.class) + String value; + + @PropertyConverter(MyPropertyConverterThatRequiresComponents.class) + String value2; + } + + static class MyPropertyConverter implements PropertyValueConverter { + + @Override + public Object nativeToDomain(Object value, ValueConversionContext context) { + return null; + } + + @Override + public Object domainToNative(Object value, ValueConversionContext context) { + return null; + } + } + + static class MyPropertyConverterThatRequiresComponents implements PropertyValueConverter { + + private final SomeDependency someDependency; + + public MyPropertyConverterThatRequiresComponents(@Autowired SomeDependency someDependency) { + this.someDependency = someDependency; + } + + @Override + public Object nativeToDomain(Object value, ValueConversionContext context) { + return null; + } + + @Override + public Object domainToNative(Object value, ValueConversionContext context) { + return null; + } + } + + static class SomeDependency { + + } }

+ * Consult the store specific documentation for details and support notes. + * + * @author Christoph Strobl + * @since ? + */ +@Target(FIELD) +@Documented +@Retention(RetentionPolicy.RUNTIME) +public @interface PropertyConverter { + + /** + * The {@link PropertyValueConverter} type handling the value conversion of the annotated property. + * + * @return the configured {@link PropertyValueConverter}. {@link ObjectToObjectPropertyValueConverter} by default. + */ + Class> value() default ObjectToObjectPropertyValueConverter.class; + +} diff --git a/src/main/java/org/springframework/data/convert/PropertyValueConversions.java b/src/main/java/org/springframework/data/convert/PropertyValueConversions.java new file mode 100644 index 000000000..8a8a42056 --- /dev/null +++ b/src/main/java/org/springframework/data/convert/PropertyValueConversions.java @@ -0,0 +1,54 @@ +/* + * 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; + +/** + * {@link PropertyValueConversions} provides access to {@link PropertyValueConverter converters} that may only be + * applied to a specific property. Other than {@link org.springframework.core.convert.converter.Converter converters} + * registered in {@link CustomConversions} the property based variants accept and allow returning {@literal null} values + * and provide access to a store specific {@link PropertyValueConverter.ValueConversionContext conversion context}. + * + * @author Christoph Strobl + * @since ? + * @currentBook The Desert Prince - Peter V. Brett + */ +public interface PropertyValueConversions { + + /** + * Check if a {@link PropertyValueConverter} is present for the given {@literal property}. + * + * @param property must not be {@literal null}. + * @return {@literal true} if a specific {@link PropertyValueConverter} is available. + */ + default boolean hasValueConverter(PersistentProperty property) { + return getValueConverter(property) != null; + } + + /** + * Get the {@link PropertyValueConverter} for the given {@literal property} if present. + * + * @param property must not be {@literal null}. param domain specific type + * @param store native type + * @param conversion context type + * @return the suitable {@link PropertyValueConverter} or {@literal null} if none available. + */ + @Nullable + PropertyValueConverter getValueConverter( + PersistentProperty property); +} diff --git a/src/main/java/org/springframework/data/convert/PropertyValueConverter.java b/src/main/java/org/springframework/data/convert/PropertyValueConverter.java new file mode 100644 index 000000000..ce9d8140f --- /dev/null +++ b/src/main/java/org/springframework/data/convert/PropertyValueConverter.java @@ -0,0 +1,85 @@ +/* + * Copyright 2021 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; + +/** + * {@link PropertyValueConverter} provides a symmetric way of converting certain properties from domain to store + * specific values. + *