diff --git a/src/main/java/org/springframework/data/convert/CustomConversions.java b/src/main/java/org/springframework/data/convert/CustomConversions.java index 2683379b6..77ef6344a 100644 --- a/src/main/java/org/springframework/data/convert/CustomConversions.java +++ b/src/main/java/org/springframework/data/convert/CustomConversions.java @@ -16,7 +16,16 @@ package org.springframework.data.convert; import java.lang.annotation.Annotation; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; import java.util.function.Predicate; @@ -33,7 +42,6 @@ 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.convert.PropertyValueConverter.ValueConversionContext; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.model.SimpleTypeHolder; import org.springframework.data.util.Predicates; @@ -98,7 +106,7 @@ public class CustomConversions { private final Function> getRawWriteTarget = convertiblePair -> getCustomTarget( convertiblePair.getSourceType(), null, writingPairs); - private PropertyValueConversions propertyValueConversions; + private @Nullable PropertyValueConversions propertyValueConversions; /** * @param converterConfiguration the {@link ConverterConfiguration} to apply. @@ -182,7 +190,7 @@ public class CustomConversions { * @param property must not be {@literal null}. * @return {@literal true} if a specific {@link PropertyValueConverter} is available. * @see PropertyValueConversions#hasValueConverter(PersistentProperty) - * @since ? + * @since 2.7 */ public boolean hasPropertyValueConverter(PersistentProperty property) { return propertyValueConversions != null ? propertyValueConversions.hasValueConverter(property) : false; @@ -197,11 +205,11 @@ public class CustomConversions { * @param conversion context type * @return the suitable {@link PropertyValueConverter} or {@literal null} if none available. * @see PropertyValueConversions#getValueConverter(PersistentProperty) - * @since ? + * @since 2.7 */ @Nullable - public > PropertyValueConverter getPropertyValueConverter( - PersistentProperty property) { + public , D extends ValueConversionContext> PropertyValueConverter getPropertyValueConverter( + C property) { return propertyValueConversions != null ? propertyValueConversions.getValueConverter(property) : null; } @@ -326,8 +334,8 @@ public class CustomConversions { registrationIntent.getSourceType(), registrationIntent.getTargetType(), registrationIntent.isReading() ? "reading" : "writing")); } else { - logger.debug(String.format(SKIP_CONVERTER, registrationIntent.getSourceType(), registrationIntent.getTargetType(), - registrationIntent.isReading() ? "reading" : "writing", + logger.debug(String.format(SKIP_CONVERTER, registrationIntent.getSourceType(), + registrationIntent.getTargetType(), registrationIntent.isReading() ? "reading" : "writing", registrationIntent.isReading() ? registrationIntent.getSourceType() : registrationIntent.getTargetType())); } } @@ -926,8 +934,22 @@ public class CustomConversions { this(storeConversions, userConverters, converterRegistrationFilter, new SimplePropertyValueConversions()); } + /** + * Create a new ConverterConfiguration holding the given {@link StoreConversions} and user defined converters as + * well as a {@link Collection} of {@link ConvertiblePair} for which to skip the registration of default converters. + *
+ * This allows store implementations to modify default converter registration based on specific needs and + * configurations. User defined converters will are never subject of filtering. + * + * @param storeConversions must not be {@literal null}. + * @param userConverters must not be {@literal null} use {@link Collections#emptyList()} instead. + * @param converterRegistrationFilter must not be {@literal null}. + * @param propertyValueConversions can be {@literal null}. + * @since 2.7 + */ public ConverterConfiguration(StoreConversions storeConversions, List userConverters, - Predicate converterRegistrationFilter, @Nullable PropertyValueConversions propertyValueConversions) { + Predicate converterRegistrationFilter, + @Nullable PropertyValueConversions propertyValueConversions) { this.storeConversions = storeConversions; this.userConverters = new ArrayList<>(userConverters); @@ -958,7 +980,7 @@ public class CustomConversions { /** * @return the configured {@link PropertyValueConversions} if set, {@literal null} otherwise. - * @since ? + * @since 2.7 */ @Nullable public PropertyValueConversions getPropertyValueConversions() { diff --git a/src/main/java/org/springframework/data/convert/PropertyValueConversions.java b/src/main/java/org/springframework/data/convert/PropertyValueConversions.java index e69b14164..1c1b09fcf 100644 --- a/src/main/java/org/springframework/data/convert/PropertyValueConversions.java +++ b/src/main/java/org/springframework/data/convert/PropertyValueConversions.java @@ -15,18 +15,19 @@ */ package org.springframework.data.convert; -import org.springframework.data.convert.PropertyValueConverter.ValueConversionContext; +import java.util.function.Consumer; + 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}. + * registered in {@link CustomConversions}, the property based variants accept and allow returning {@literal null} + * values and provide access to a store specific {@link ValueConversionContext conversion context}. * * @author Christoph Strobl - * @since ? + * @since 2.7 * @currentBook The Desert Prince - Peter V. Brett */ public interface PropertyValueConversions { @@ -38,7 +39,7 @@ public interface PropertyValueConversions { * @return {@literal true} if a specific {@link PropertyValueConverter} is available. */ default boolean hasValueConverter(PersistentProperty property) { - return getValueConverter(property) != null; + return getValueConverter((PersistentProperty) property) != null; } /** @@ -50,6 +51,20 @@ public interface PropertyValueConversions { * @return the suitable {@link PropertyValueConverter} or {@literal null} if none available. */ @Nullable - > PropertyValueConverter getValueConverter( - PersistentProperty property); + , D extends ValueConversionContext> PropertyValueConverter getValueConverter( + C property); + + /** + * Helper that allows to create {@link PropertyValueConversions} instance with the configured + * {@link PropertyValueConverter converters} provided via the given callback. + */ + static

> PropertyValueConversions simple( + Consumer> config) { + + SimplePropertyValueConversions conversions = new SimplePropertyValueConversions(); + PropertyValueConverterRegistrar registrar = new PropertyValueConverterRegistrar(); + config.accept(registrar); + conversions.setValueConverterRegistry(registrar.buildRegistry()); + return conversions; + } } diff --git a/src/main/java/org/springframework/data/convert/PropertyValueConverter.java b/src/main/java/org/springframework/data/convert/PropertyValueConverter.java index e249931c8..7c0ad8834 100644 --- a/src/main/java/org/springframework/data/convert/PropertyValueConverter.java +++ b/src/main/java/org/springframework/data/convert/PropertyValueConverter.java @@ -15,10 +15,9 @@ */ package org.springframework.data.convert; -import org.springframework.data.convert.PropertyValueConverter.ValueConversionContext; +import java.util.function.BiFunction; + import org.springframework.data.mapping.PersistentProperty; -import org.springframework.data.util.ClassTypeInformation; -import org.springframework.data.util.TypeInformation; import org.springframework.lang.Nullable; /** @@ -29,8 +28,9 @@ import org.springframework.lang.Nullable; * 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 domain specific type. + * @param store native type. + * @param the store specific {@link ValueConversionContext conversion context}. * @since 2.7 */ public interface PropertyValueConverter>> { @@ -39,107 +39,23 @@ public interface PropertyValueConverter> { - - /** - * 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 null; - } - - /** - * 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 T write(@Nullable Object value, Class target) { - return write(value, ClassTypeInformation.from(target)); - } - - /** - * 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 T write(@Nullable Object value, TypeInformation target) { - return null; - } - - /** - * Reads the value into the type of the current property. - * - * @param value can be {@literal null}. - * @return can be {@literal null}. - */ - @Nullable - default Object read(@Nullable Object value) { - return read(value, getProperty().getTypeInformation()); - } - - /** - * Reads the value as the given type. - * - * @param value can be {@literal null}. - * @param target must not be {@literal null}. - * @return can be {@literal null}. - */ - @Nullable - default T read(@Nullable Object value, Class target) { - return null; - } - - /** - * Reads the value as the given type. - * - * @param value can be {@literal null}. - * @param target must not be {@literal null}. - * @return can be {@literal null}. - */ - @Nullable - default T read(@Nullable Object value, TypeInformation target) { - return null; - } - } + B write(@Nullable A value, C context); /** * NoOp {@link PropertyValueConverter} implementation. @@ -152,13 +68,42 @@ public interface PropertyValueConverter> + implements PropertyValueConverter> { + + private final BiFunction, B> writer; + private final BiFunction, A> reader; + + public FunctionPropertyValueConverter(BiFunction, B> writer, + BiFunction, A> reader) { + + this.writer = writer; + this.reader = reader; + } + + @Override + public B write(A value, ValueConversionContext

context) { + return writer.apply(value, context); + } + + @Override + public A read(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 index 540521824..dac34508d 100644 --- a/src/main/java/org/springframework/data/convert/PropertyValueConverterFactories.java +++ b/src/main/java/org/springframework/data/convert/PropertyValueConverterFactories.java @@ -16,17 +16,18 @@ 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.convert.PropertyValueConverter.ValueConversionContext; import org.springframework.data.mapping.PersistentProperty; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -34,28 +35,31 @@ 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 ChainedPropertyValueConverterFactory chained} and the created {@link PropertyValueConverter converter} * {@link CachingPropertyValueConverterFactory cached}. * * @author Christoph Strobl - * @since ? + * @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 ? + * @since 2.7 */ - static class CompositePropertyValueConverterFactory implements PropertyValueConverterFactory { + static class ChainedPropertyValueConverterFactory implements PropertyValueConverterFactory { private List delegates; - CompositePropertyValueConverterFactory(PropertyValueConverterFactory... delegates) { + ChainedPropertyValueConverterFactory(PropertyValueConverterFactory... delegates) { this(Arrays.asList(delegates)); } - CompositePropertyValueConverterFactory(List delegates) { - this.delegates = delegates; + ChainedPropertyValueConverterFactory(List delegates) { + this.delegates = Collections.unmodifiableList(delegates); } @Nullable @@ -72,13 +76,18 @@ final class PropertyValueConverterFactories { return delegates.stream().filter(it -> it.getConverter(converterType) != null).findFirst() .map(it -> it.getConverter(converterType)).orElse(null); } + + public List converterFactories() { + return delegates; + } } /** - * Trivial implementation of {@link PropertyValueConverter}. + * Trivial implementation of {@link PropertyValueConverterFactory} capable of instantiating a + * {@link PropertyValueConverter} via default constructor or in case of {@link Enum} returning the first value. * * @author Christoph Strobl - * @since ? + * @since 2.7 */ static class SimplePropertyConverterFactory implements PropertyValueConverterFactory { @@ -100,13 +109,13 @@ final class PropertyValueConverterFactories { * {@link PropertyValueConverter}. This allows the {@link PropertyValueConverter} to make use of DI. * * @author Christoph Strobl - * @since ? + * @since 2.7 */ static class BeanFactoryAwarePropertyValueConverterFactory implements PropertyValueConverterFactory { private final BeanFactory beanFactory; - public BeanFactoryAwarePropertyValueConverterFactory(BeanFactory beanFactory) { + BeanFactoryAwarePropertyValueConverterFactory(BeanFactory beanFactory) { this.beanFactory = beanFactory; } @@ -131,24 +140,27 @@ final class PropertyValueConverterFactories { } /** + * {@link PropertyValueConverterFactory} implementation that serves {@link PropertyValueConverter} from a given + * {@link ValueConverterRegistry registry}. + * * @author Christoph Strobl - * @since ? + * @since 2.7 */ static class ConfiguredInstanceServingValueConverterFactory implements PropertyValueConverterFactory { - private final PropertyValueConverterRegistrar conversionsRegistrar; + private final ValueConverterRegistry converterRegistry; - public ConfiguredInstanceServingValueConverterFactory(PropertyValueConverterRegistrar conversionsRegistrar) { + ConfiguredInstanceServingValueConverterFactory(ValueConverterRegistry converterRegistry) { - Assert.notNull(conversionsRegistrar, "ConversionsRegistrar must not be null!"); - this.conversionsRegistrar = conversionsRegistrar; + Assert.notNull(converterRegistry, "ConversionsRegistrar must not be null!"); + this.converterRegistry = converterRegistry; } @Nullable @Override public > PropertyValueConverter getConverter( PersistentProperty property) { - return (PropertyValueConverter) conversionsRegistrar.getConverter(property.getOwner().getType(), + return (PropertyValueConverter) converterRegistry.getConverter(property.getOwner().getType(), property.getName()); } @@ -160,17 +172,18 @@ final class PropertyValueConverterFactories { } /** - * TODO: This would be easier if we'd get rid of {@link PropertyValueConverterFactory#getConverter(Class)}. - * + * {@link PropertyValueConverterFactory} implementation that caches converters provided by an underlying + * {@link PropertyValueConverterFactory factory}. + * * @author Christoph Strobl - * @since ? + * @since 2.7 */ static class CachingPropertyValueConverterFactory implements PropertyValueConverterFactory { private final PropertyValueConverterFactory delegate; private final Cache cache = new Cache(); - public CachingPropertyValueConverterFactory(PropertyValueConverterFactory delegate) { + CachingPropertyValueConverterFactory(PropertyValueConverterFactory delegate) { Assert.notNull(delegate, "Delegate must not be null!"); this.delegate = delegate; @@ -181,10 +194,9 @@ final class PropertyValueConverterFactories { public > PropertyValueConverter getConverter( PersistentProperty property) { - PropertyValueConverter converter = cache.get(property); + Optional>> converter = cache.get(property); - return converter != null - ? converter + return converter != null ? (PropertyValueConverter) converter.orElse(null) : cache.cache(property, delegate.getConverter(property)); } @@ -192,36 +204,35 @@ final class PropertyValueConverterFactories { public > PropertyValueConverter getConverter( Class> converterType) { - PropertyValueConverter converter = cache.get(converterType); + Optional>> converter = cache.get(converterType); - return converter != null - ? converter + return converter != null ? (PropertyValueConverter) converter.orElse(null) : cache.cache(converterType, delegate.getConverter(converterType)); } static class Cache { - Map, PropertyValueConverter>> perPropertyCache = new HashMap<>(); - Map, PropertyValueConverter>> typeCache = new HashMap<>(); + Map, Optional>>> perPropertyCache = new HashMap<>(); + Map, Optional>>> typeCache = new HashMap<>(); - PropertyValueConverter> get(PersistentProperty property) { + Optional>> get(PersistentProperty property) { return perPropertyCache.get(property); } - PropertyValueConverter> get(Class type) { + Optional>> get(Class type) { return typeCache.get(type); } > PropertyValueConverter cache(PersistentProperty property, - PropertyValueConverter converter) { - perPropertyCache.putIfAbsent(property, converter); + @Nullable PropertyValueConverter converter) { + perPropertyCache.putIfAbsent(property, Optional.ofNullable(converter)); cache(property.getValueConverterType(), converter); return converter; } > PropertyValueConverter cache(Class type, - PropertyValueConverter converter) { - typeCache.putIfAbsent(type, converter); + @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 index f3df7a182..7a40cf3d5 100644 --- a/src/main/java/org/springframework/data/convert/PropertyValueConverterFactory.java +++ b/src/main/java/org/springframework/data/convert/PropertyValueConverterFactory.java @@ -19,10 +19,9 @@ import java.util.Arrays; import java.util.List; import org.springframework.beans.factory.BeanFactory; -import org.springframework.data.convert.PropertyValueConverter.ValueConversionContext; 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.ChainedPropertyValueConverterFactory; import org.springframework.data.convert.PropertyValueConverterFactories.ConfiguredInstanceServingValueConverterFactory; import org.springframework.data.convert.PropertyValueConverterFactories.SimplePropertyConverterFactory; import org.springframework.data.mapping.PersistentProperty; @@ -37,7 +36,7 @@ import org.springframework.util.Assert; * {@link #caching(PropertyValueConverterFactory) cached}. * * @author Christoph Strobl - * @since ? + * @since 2.7 */ public interface PropertyValueConverterFactory { @@ -45,8 +44,9 @@ 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 domain specific type. + * @param store native type. + * @param value conversion context to use. * @return can be {@literal null}. */ @Nullable @@ -57,12 +57,22 @@ public interface PropertyValueConverterFactory { if (!property.hasValueConverter()) { return null; } + return getConverter((Class>) property.getValueConverterType()); } + /** + * Get the converter by its type. + * + * @param converterType must not be {@literal null}. + * @param domain specific type. + * @param store native type. + * @param value conversion context to use. + * @return + */ @Nullable - > PropertyValueConverter getConverter( - Class> converterType); + > PropertyValueConverter getConverter( + Class> converterType); /** * Obtain a simple {@link PropertyValueConverterFactory} capable of instantiating {@link PropertyValueConverter} @@ -93,7 +103,7 @@ public interface PropertyValueConverterFactory { * @param registrar must not be {@literal null}. * @return new instance of {@link PropertyValueConverterFactory}. */ - static PropertyValueConverterFactory configuredInstance(PropertyValueConverterRegistrar registrar) { + static PropertyValueConverterFactory configuredInstance(ValueConverterRegistry registrar) { return new ConfiguredInstanceServingValueConverterFactory(registrar); } @@ -123,15 +133,15 @@ public interface PropertyValueConverterFactory { return factoryList.iterator().next(); } - return new CompositePropertyValueConverterFactory(factoryList); + return new ChainedPropertyValueConverterFactory(factoryList); } /** * Obtain a {@link PropertyValueConverterFactory} that will cache {@link PropertyValueConverter} instances per * {@link PersistentProperty}. * - * @param factory - * @return + * @param factory must not be {@literal null}. + * @return new instance of {@link PropertyValueConverterFactory}. */ 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 index 73d588836..b7fb46364 100644 --- a/src/main/java/org/springframework/data/convert/PropertyValueConverterRegistrar.java +++ b/src/main/java/org/springframework/data/convert/PropertyValueConverterRegistrar.java @@ -15,85 +15,177 @@ */ package org.springframework.data.convert; -import java.util.Collection; -import java.util.LinkedHashMap; -import java.util.Map; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; -import org.springframework.lang.Nullable; -import org.springframework.util.ObjectUtils; +import org.springframework.data.convert.PropertyValueConverter.FunctionPropertyValueConverter; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.util.MethodInvocationRecorder; +import org.springframework.util.Assert; /** + * Configuration class to register a {@link PropertyValueConverter} with a {@link SimplePropertyValueConverterRegistry} + * that can be used with {@link PropertyValueConversions}. + *

+ * It is possible to register type safe converters via {@link #registerConverter(Class, Function)} + * + *

+ * registrar.registerConverter(Person.class, Person::getName) //
+ * 		.writing(StringConverter::encrypt) //
+ * 		.reading(StringConverter::decrypt);
+ * 
+ * * @author Christoph Strobl - * @since ? + * @author Oliver Drotbohm + * @since 2.7 */ -public class PropertyValueConverterRegistrar { +public class PropertyValueConverterRegistrar

> { - private final Map> converterRegistrationMap = new LinkedHashMap<>(); + private final SimplePropertyValueConverterRegistry

registry = new SimplePropertyValueConverterRegistry<>(); - boolean hasConverterFor(Class type, String path) { - return converterRegistrationMap.containsKey(new Key(type, path)); + /** + * Starts a converter registration by pointing to a property of a domain type. + * + * @param the domain type + * @param 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 WritingConverterRegistrationBuilder registerConverter(Class type, Function property) { + + String propertyName = MethodInvocationRecorder.forProxyOf(type).record(property).getPropertyPath() + .orElseThrow(() -> new IllegalArgumentException("Cannot obtain property name!")); + + return new WritingConverterRegistrationBuilder(type, propertyName, this); } - @Nullable - public PropertyValueConverter getConverter(Class type, String path) { - return converterRegistrationMap.get(new Key(type, path)); + /** + * Starts a converter registration by pointing to a property of a domain type. + * + * @param the domain type + * @param 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 WritingConverterRegistrationBuilder registerConverter(Class type, String propertyName, + Class propertyType) { + return new WritingConverterRegistrationBuilder(type, propertyName, this); } - public int size() { - return converterRegistrationMap.size(); - } + /** + * 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 registerConverter(Class type, String path, + PropertyValueConverter> converter) { - public boolean isEmpty() { - return converterRegistrationMap.isEmpty(); - } - - public PropertyValueConverterRegistrar register(Class type, String path, - PropertyValueConverter converter) { - - converterRegistrationMap.put(new Key(type, path), converter); + registry.registerConverter(type, path, + (PropertyValueConverter>) converter); return this; } - public Collection> converters() { - return converterRegistrationMap.values(); + /** + * Obtain the {@link SimplePropertyValueConverterRegistry}. + * + * @return new instance of {@link SimplePropertyValueConverterRegistry}. + */ + public ValueConverterRegistry

buildRegistry() { + return new SimplePropertyValueConverterRegistry<>(registry); } - public PropertyValueConverterRegistrar registerIfAbsent(Class type, String path, - PropertyValueConverter converter) { - converterRegistrationMap.putIfAbsent(new Key(type, path), converter); - return this; + /** + * 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()); + }); } - static class Key { + /** + * Helper to build up a fluent registration API starting on + * + * @author Oliver Drotbohm + */ + static class WritingConverterRegistrationBuilder> { - Class type; - String path; + private final Consumer registration; + private final PropertyValueConverterRegistrar config; - public Key(Class type, String path) { - this.type = type; - this.path = path; + public WritingConverterRegistrationBuilder(Class type, String property, PropertyValueConverterRegistrar config) { + + this.config = config; + this.registration = (converter) -> config.registerConverter(type, property, converter); } - @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); + ReadingConverterRegistrationBuilder writingAsIs() { + return writing((source, context) -> source); } - @Override - public int hashCode() { - int result = ObjectUtils.nullSafeHashCode(type); - result = 31 * result + ObjectUtils.nullSafeHashCode(path); - return result; + ReadingConverterRegistrationBuilder writing(Function writer) { + return writing((source, context) -> writer.apply(source)); + } + + /** + * Describes how to convert the domain property value into the database native property. + * + * @param the type to be written to the database + * @param writer the property conversion to extract a value written to the database + * @return will never be {@literal null}. + */ + ReadingConverterRegistrationBuilder writing(BiFunction, 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 + */ + static class ReadingConverterRegistrationBuilder> { + + private WritingConverterRegistrationBuilder origin; + private BiFunction, R> writer; + + public ReadingConverterRegistrationBuilder(WritingConverterRegistrationBuilder origin, + BiFunction, R> writer) { + this.origin = origin; + this.writer = writer; + } + + PropertyValueConverterRegistrar

readingAsIs() { + return reading((source, context) -> (S) source); + } + + PropertyValueConverterRegistrar

reading(Function reader) { + return reading((source, context) -> reader.apply(source)); + } + + /** + * Describes how to read a database value into a domain object's property value. + * + * @param reader must not be {@literal null}. + * @return + */ + PropertyValueConverterRegistrar

reading(BiFunction, S> reader) { + + origin.registration.accept(new FunctionPropertyValueConverter(writer, reader)); + + return origin.config; } } } diff --git a/src/main/java/org/springframework/data/convert/SimplePropertyValueConversions.java b/src/main/java/org/springframework/data/convert/SimplePropertyValueConversions.java index 0740fb82f..b93f3e660 100644 --- a/src/main/java/org/springframework/data/convert/SimplePropertyValueConversions.java +++ b/src/main/java/org/springframework/data/convert/SimplePropertyValueConversions.java @@ -20,36 +20,79 @@ import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import org.springframework.beans.factory.InitializingBean; -import org.springframework.data.convert.PropertyValueConverter.ValueConversionContext; +import org.springframework.data.convert.PropertyValueConverterFactories.ChainedPropertyValueConverterFactory; import org.springframework.data.mapping.PersistentProperty; import org.springframework.lang.Nullable; /** + * {@link PropertyValueConversions} implementation that allows to pick a {@link PropertyValueConverterFactory} serving + * {@link PropertyValueConverter converters}. Activating {@link #setConverterCacheEnabled(boolean) cahing} allows to + * reuse converters. Providing a {@link SimplePropertyValueConverterRegistry} adds path configured converter instances. + *

+ * Should be {@link #afterPropertiesSet() initialized}. If not, {@link #init()} will be called of fist attempt of + * {@link PropertyValueConverter converter} retrieval. + * * @author Christoph Strobl - * @since ? + * @since 2.7 */ public class SimplePropertyValueConversions implements PropertyValueConversions, InitializingBean { private @Nullable PropertyValueConverterFactory converterFactory; - private @Nullable PropertyValueConverterRegistrar converterRegistrar; + 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; } - public void setConverterRegistrar(PropertyValueConverterRegistrar converterRegistrar) { - this.converterRegistrar = converterRegistrar; + @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 > PropertyValueConverter getValueConverter( - PersistentProperty property) { + public , D extends ValueConversionContext> PropertyValueConverter getValueConverter( + C property) { if (!initialized.get()) { init(); @@ -58,9 +101,13 @@ public class SimplePropertyValueConversions implements PropertyValueConversions, return this.converterFactory.getConverter(property); } + /** + * May be called just once to initialize the underlying factory with its values. + */ public void init() { if (initialized.compareAndSet(false, true)) { + List factoryList = new ArrayList<>(3); if (converterFactory != null) { @@ -69,8 +116,8 @@ public class SimplePropertyValueConversions implements PropertyValueConversions, factoryList.add(PropertyValueConverterFactory.simple()); } - if ((converterRegistrar != null) && !converterRegistrar.isEmpty()) { - factoryList.add(PropertyValueConverterFactory.configuredInstance(converterRegistrar)); + if ((valueConverterRegistry != null) && !valueConverterRegistry.isEmpty()) { + factoryList.add(PropertyValueConverterFactory.configuredInstance(valueConverterRegistry)); } PropertyValueConverterFactory targetFactory = factoryList.size() > 1 @@ -84,7 +131,6 @@ public class SimplePropertyValueConversions implements PropertyValueConversions, @Override public void afterPropertiesSet() throws Exception { - init(); } } diff --git a/src/main/java/org/springframework/data/convert/SimplePropertyValueConverterRegistry.java b/src/main/java/org/springframework/data/convert/SimplePropertyValueConverterRegistry.java new file mode 100644 index 000000000..6b336eb45 --- /dev/null +++ b/src/main/java/org/springframework/data/convert/SimplePropertyValueConverterRegistry.java @@ -0,0 +1,126 @@ +/* + * 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.LinkedHashMap; +import java.util.Map; + +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.util.ObjectUtils; + +/** + * A registry of property specific {@link PropertyValueConverter value convertes} that may be used to convert only + * specific properties/values of an object. + * + * @author Christoph Strobl + * @since 2.7 + */ +public class SimplePropertyValueConverterRegistry

> implements ValueConverterRegistry

{ + + private final Map>> converterRegistrationMap = new LinkedHashMap<>(); + + public SimplePropertyValueConverterRegistry() {} + + SimplePropertyValueConverterRegistry(SimplePropertyValueConverterRegistry

source) { + this.converterRegistrationMap.putAll(source.converterRegistrationMap); + } + + @Override + public void registerConverter(Class type, String path, + PropertyValueConverter> 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> 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 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>> getConverterRegistrationMap() { + return converterRegistrationMap; + } + + 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/ValueConversionContext.java b/src/main/java/org/springframework/data/convert/ValueConversionContext.java new file mode 100644 index 000000000..cf5fec26b --- /dev/null +++ b/src/main/java/org/springframework/data/convert/ValueConversionContext.java @@ -0,0 +1,113 @@ +/* + * 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.data.util.ClassTypeInformation; +import org.springframework.data.util.TypeInformation; +import org.springframework.lang.Nullable; + +/** + * The {@link ValueConversionContext} provides access to the store specific {@link PersistentProperty} and allows to + * call the store default conversion via the {@literal read}/{@literal write} methods. + *

+ * 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 T write(@Nullable Object value, Class target) { + return write(value, ClassTypeInformation.from(target)); + } + + /** + * 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 T write(@Nullable Object value, TypeInformation target) { + throw new IllegalStateException(String.format( + "%s does not provide write function that allows value conversion to target type (%s).", getClass(), target)); + } + + /** + * Reads the value into the type of the current property. + * + * @param value can be {@literal null}. + * @return can be {@literal null}. + */ + @Nullable + default Object read(@Nullable Object value) { + return read(value, getProperty().getTypeInformation()); + } + + /** + * Reads the value as the given type. + * + * @param value can be {@literal null}. + * @param target must not be {@literal null}. + * @return can be {@literal null}. + */ + @Nullable + default T read(@Nullable Object value, Class target) { + return read(value, ClassTypeInformation.from(target)); + } + + /** + * Reads the value as the given type. + * + * @param value can be {@literal null}. + * @param target must not be {@literal null}. + * @return can be {@literal null}. + */ + @Nullable + default T read(@Nullable Object value, TypeInformation target) { + throw new IllegalStateException(String.format( + "%s does not provide write function that allows value conversion to target type (%s).", getClass(), target)); + } +} diff --git a/src/main/java/org/springframework/data/convert/PropertyConverter.java b/src/main/java/org/springframework/data/convert/ValueConverter.java similarity index 78% rename from src/main/java/org/springframework/data/convert/PropertyConverter.java rename to src/main/java/org/springframework/data/convert/ValueConverter.java index 482e4f318..bdcf7933a 100644 --- a/src/main/java/org/springframework/data/convert/PropertyConverter.java +++ b/src/main/java/org/springframework/data/convert/ValueConverter.java @@ -25,17 +25,23 @@ 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. + * Annotation to define usage of a {@link PropertyValueConverter} to read/write the property.
+ * May be used as meta annotation utilizing {@link org.springframework.core.annotation.AliasFor}. + *

+ * 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 ? + * @since 2.7 + * @see PropertyValueConverter + * @see PropertyValueConverterFactory */ -@Target(FIELD) +@Target({ FIELD, ANNOTATION_TYPE }) @Documented @Retention(RetentionPolicy.RUNTIME) -public @interface PropertyConverter { +public @interface ValueConverter { /** * The {@link PropertyValueConverter} type handling the value conversion of the annotated property. 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 000000000..2631a57c2 --- /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 convertes} that may be used 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> 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 + * @param + * @return {@literal null} if no converter present for the given type/path combination. + */ + @Nullable + 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 there a converters registered. + */ + boolean isEmpty(); + + /** + * Obtain a simple {@link ValueConverterRegistry}. + * + * @param + * @return new instance of {@link ValueConverterRegistry}. + */ + static > ValueConverterRegistry simple() { + return new SimplePropertyValueConverterRegistry<>(); + } +} diff --git a/src/main/java/org/springframework/data/mapping/PersistentProperty.java b/src/main/java/org/springframework/data/mapping/PersistentProperty.java index ddd863d28..c557cddd7 100644 --- a/src/main/java/org/springframework/data/mapping/PersistentProperty.java +++ b/src/main/java/org/springframework/data/mapping/PersistentProperty.java @@ -23,9 +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.convert.ValueConversionContext; +import org.springframework.data.convert.ValueConverter; import org.springframework.data.util.TypeInformation; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -36,6 +36,7 @@ import org.springframework.util.Assert; * @author Oliver Gierke * @author Mark Paluch * @author Jens Schauder + * @author Christoph Strobl */ public interface PersistentProperty

> { @@ -68,9 +69,9 @@ public interface PersistentProperty

> { TypeInformation getTypeInformation(); /** - * 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 @@ -426,16 +427,32 @@ public interface PersistentProperty

> { return getOwner().getPropertyAccessor(owner); } + /** + * Obtain the the {@link PropertyValueConverter converter type} to be used for read-/writing the properties value. By + * default looks for the {@link ValueConverter} annotation and extracts its {@link ValueConverter#value() value} + * attribute. + *

+ * 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>> getValueConverterType() { + default Class>>> getValueConverterType() { - PropertyConverter annotation = findAnnotation(PropertyConverter.class); + ValueConverter annotation = findAnnotation(ValueConverter.class); return annotation == null ? null - : (Class>>) annotation.value(); + : (Class>>>) annotation + .value(); } + /** + * @return by default return {@literal true} if {@link ValueConverter} annotation is present. + * @since 2.7 + */ default boolean hasValueConverter() { - return isAnnotationPresent(PropertyConverter.class); + 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 921bc8fe4..75d76bf2a 100644 --- a/src/main/java/org/springframework/data/mapping/model/AnnotationBasedPersistentProperty.java +++ b/src/main/java/org/springframework/data/mapping/model/AnnotationBasedPersistentProperty.java @@ -33,9 +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.ValueConverter; import org.springframework.data.convert.PropertyValueConverter; -import org.springframework.data.convert.PropertyValueConverter.ValueConversionContext; +import org.springframework.data.convert.ValueConversionContext; import org.springframework.data.mapping.Association; import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.PersistentEntity; @@ -288,10 +288,10 @@ public abstract class AnnotationBasedPersistentProperty

>> getValueConverterType() { + public Class>>> getValueConverterType() { - return doFindAnnotation(PropertyConverter.class) // - .map(PropertyConverter::value) // + return doFindAnnotation(ValueConverter.class) // + .map(ValueConverter::value) // .map(Class.class::cast) // .orElse(null); } diff --git a/src/test/java/org/springframework/data/convert/PropertyValueConverterFactoryUnitTests.java b/src/test/java/org/springframework/data/convert/PropertyValueConverterFactoryUnitTests.java index b502145b1..7a84aed72 100644 --- a/src/test/java/org/springframework/data/convert/PropertyValueConverterFactoryUnitTests.java +++ b/src/test/java/org/springframework/data/convert/PropertyValueConverterFactoryUnitTests.java @@ -21,10 +21,11 @@ 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.convert.PropertyValueConverter.ValueConversionContext; +import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.context.SamplePersistentProperty; import org.springframework.lang.Nullable; @@ -108,7 +109,7 @@ public class PropertyValueConverterFactoryUnitTests { } @Test // GH-1484 - void compositeConverterFactoryIteratesFactories() { + void chainedConverterFactoryIteratesFactories() { PropertyValueConverter expected = mock(PropertyValueConverter.class); @@ -132,7 +133,7 @@ public class PropertyValueConverterFactoryUnitTests { } @Test // GH-1484 - void compositeConverterFactoryFailsOnException() { + void chainedConverterFactoryFailsOnException() { PropertyValueConverterFactory factory = PropertyValueConverterFactory.chained(new PropertyValueConverterFactory() { @Nullable @@ -163,6 +164,27 @@ public class PropertyValueConverterFactoryUnitTests { .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() { @@ -182,14 +204,14 @@ public class PropertyValueConverterFactoryUnitTests { @Nullable @Override - public String nativeToDomain(@Nullable UUID nativeValue, ValueConversionContext context) { - return nativeValue.toString(); + public String read(@Nullable UUID value, ValueConversionContext context) { + return value.toString(); } @Nullable @Override - public UUID domainToNative(@Nullable String domainValue, ValueConversionContext context) { - return UUID.fromString(domainValue); + public UUID write(@Nullable String value, ValueConversionContext context) { + return UUID.fromString(value); } } @@ -199,14 +221,14 @@ public class PropertyValueConverterFactoryUnitTests { @Nullable @Override - public String nativeToDomain(@Nullable UUID nativeValue, ValueConversionContext context) { - return nativeValue.toString(); + public String read(@Nullable UUID value, ValueConversionContext context) { + return value.toString(); } @Nullable @Override - public UUID domainToNative(@Nullable String domainValue, ValueConversionContext context) { - return UUID.fromString(domainValue); + public UUID write(@Nullable String value, ValueConversionContext context) { + return UUID.fromString(value); } } @@ -221,22 +243,44 @@ public class PropertyValueConverterFactoryUnitTests { @Nullable @Override - public String nativeToDomain(@Nullable UUID nativeValue, ValueConversionContext context) { + public String read(@Nullable UUID value, ValueConversionContext context) { assertThat(someDependency).isNotNull(); - return nativeValue.toString(); + return value.toString(); } @Nullable @Override - public UUID domainToNative(@Nullable String domainValue, ValueConversionContext context) { + public UUID write(@Nullable String value, ValueConversionContext context) { assertThat(someDependency).isNotNull(); - return UUID.fromString(domainValue); + return UUID.fromString(value); } } static class SomeDependency { } + + static class Person { + String name; + Address address; + + public String getName() { + return name; + } + + public Address getAddress() { + return address; + } + } + + static class Address { + String street; + ZipCode zipCode; + } + + static class ZipCode { + + } } diff --git a/src/test/java/org/springframework/data/convert/PropertyValueConverterRegistrarUnitTests.java b/src/test/java/org/springframework/data/convert/PropertyValueConverterRegistrarUnitTests.java new file mode 100644 index 000000000..b15cb5fdb --- /dev/null +++ b/src/test/java/org/springframework/data/convert/PropertyValueConverterRegistrarUnitTests.java @@ -0,0 +1,107 @@ +/* + * 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 org.junit.jupiter.api.Test; +import org.springframework.data.convert.PropertyValueConverterFactoryUnitTests.Person; +import org.springframework.data.mapping.context.SamplePersistentProperty; +import org.springframework.lang.Nullable; + +/** + * @author Christoph Strobl + */ +public class PropertyValueConverterRegistrarUnitTests { + + @Test // GH-1484 + void buildsRegistryCorrectly() { + + ValueConverterRegistry registry = new PropertyValueConverterRegistrar<>() // + .registerConverter(Person.class, "name", new ReversingPropertyValueConverter()) // + .buildRegistry(); // + + assertThat(registry.containsConverterFor(Person.class, "name")).isTrue(); + assertThat(registry.containsConverterFor(Person.class, "not-a-property")).isFalse(); + } + + @Test // GH-1484 + void registersConvertersInRegistryCorrectly() { + + ValueConverterRegistry registry = ValueConverterRegistry.simple(); + + new PropertyValueConverterRegistrar<>() // + .registerConverter(Person.class, "name", new ReversingPropertyValueConverter()) // + .registerConvertersIn(registry); // + + assertThat(registry.containsConverterFor(Person.class, "name")).isTrue(); + assertThat(registry.containsConverterFor(Person.class, "not-a-property")).isFalse(); + } + + @Test // GH-1484 + void allowsTypeSafeConverterRegistration() { + + PropertyValueConverterRegistrar registrar = new PropertyValueConverterRegistrar<>(); + registrar.registerConverter(Person.class, "name", String.class) // + .writing(PropertyValueConverterRegistrarUnitTests::reverse) // + .readingAsIs(); // + + PropertyValueConverter> name = registrar + .buildRegistry().getConverter(Person.class, "name"); + assertThat(name.write("foo", null)).isEqualTo("oof"); + assertThat(name.read("off", null)).isEqualTo("off"); + } + + @Test // GH-1484 + void allowsTypeSafeConverterRegistrationViaRecordedProperty() { + + PropertyValueConverterRegistrar registrar = new PropertyValueConverterRegistrar<>(); + registrar.registerConverter(Person.class, Person::getName) // + .writing(PropertyValueConverterRegistrarUnitTests::reverse) // + .readingAsIs(); + + PropertyValueConverter> name = registrar + .buildRegistry().getConverter(Person.class, "name"); + assertThat(name.write("foo", null)).isEqualTo("oof"); + assertThat(name.read("мир", null)).isEqualTo("мир"); + } + + static class ReversingPropertyValueConverter + implements PropertyValueConverter> { + + @Nullable + @Override + public String read(@Nullable String value, ValueConversionContext context) { + return PropertyValueConverterRegistrarUnitTests.reverse(value); + } + + @Nullable + @Override + public String write(@Nullable String value, ValueConversionContext context) { + return PropertyValueConverterRegistrarUnitTests.reverse(value); + } + } + + @Nullable + static String reverse(@Nullable String source) { + + if (source == null) { + return null; + } + + return new StringBuilder(source).reverse().toString(); + } +} diff --git a/src/test/java/org/springframework/data/convert/SimplePropertyValueConversionsUnitTests.java b/src/test/java/org/springframework/data/convert/SimplePropertyValueConversionsUnitTests.java new file mode 100644 index 000000000..047d2dd76 --- /dev/null +++ b/src/test/java/org/springframework/data/convert/SimplePropertyValueConversionsUnitTests.java @@ -0,0 +1,69 @@ +/* + * 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 org.junit.jupiter.api.Test; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.data.convert.PropertyValueConverterFactories.CachingPropertyValueConverterFactory; +import org.springframework.data.convert.PropertyValueConverterFactories.ChainedPropertyValueConverterFactory; + +/** + * @author Christoph Strobl + */ +class SimplePropertyValueConversionsUnitTests { + + @Test // GH-1484 + void decoratesTargetFactoryWithCacheWhenCachingIsEnabled() { + + SimplePropertyValueConversions conversions = new SimplePropertyValueConversions(); + conversions.setConverterFactory(PropertyValueConverterFactory.beanFactoryAware(mock(BeanFactory.class))); + conversions.setConverterCacheEnabled(true); + + conversions.init(); + assertThat(conversions.getConverterFactory()).isInstanceOf(CachingPropertyValueConverterFactory.class); + } + + @Test // GH-1484 + void doesNotDecorateTargetFactoryWithCacheWhenCachingIsDisabled() { + + PropertyValueConverterFactory factory = PropertyValueConverterFactory.beanFactoryAware(mock(BeanFactory.class)); + + SimplePropertyValueConversions conversions = new SimplePropertyValueConversions(); + conversions.setConverterFactory(factory); + conversions.setConverterCacheEnabled(false); + + conversions.init(); + assertThat(conversions.getConverterFactory()).isSameAs(factory); + } + + @Test // GH-1484 + void chainsFactoriesIfConverterRegistryPresent() { + + ValueConverterRegistry registry = mock(ValueConverterRegistry.class); + PropertyValueConverterFactory factory = PropertyValueConverterFactory.beanFactoryAware(mock(BeanFactory.class)); + + SimplePropertyValueConversions conversions = new SimplePropertyValueConversions(); + conversions.setConverterFactory(factory); + conversions.setValueConverterRegistry(registry); + conversions.setConverterCacheEnabled(false); + + conversions.init(); + assertThat(conversions.getConverterFactory()).isInstanceOf(ChainedPropertyValueConverterFactory.class); + } +} diff --git a/src/test/java/org/springframework/data/convert/SimplePropertyValueConverterRegistryUnitTests.java b/src/test/java/org/springframework/data/convert/SimplePropertyValueConverterRegistryUnitTests.java new file mode 100644 index 000000000..5eea00e75 --- /dev/null +++ b/src/test/java/org/springframework/data/convert/SimplePropertyValueConverterRegistryUnitTests.java @@ -0,0 +1,88 @@ +/* + * 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 org.junit.jupiter.api.Test; +import org.springframework.data.mapping.Person; + +/** + * @author Christoph Strobl + */ +class SimplePropertyValueConverterRegistryUnitTests { + + @Test // GH-1484 + void emptyRegistryDoesNotServeConverters() { + + SimplePropertyValueConverterRegistry registry = new SimplePropertyValueConverterRegistry(); + + assertThat(registry.isEmpty()).isTrue(); + assertThat(registry.size()).isZero(); + assertThat(registry.containsConverterFor(Person.class, "name")).isFalse(); + assertThat(registry.getConverter(Person.class, "name")).isNull(); + } + + @Test // GH-1484 + void registryCopiesOverConverters() { + + SimplePropertyValueConverterRegistry sourceRegistry = new SimplePropertyValueConverterRegistry(); + sourceRegistry.registerConverter(Person.class, "name", mock(PropertyValueConverter.class)); + + SimplePropertyValueConverterRegistry targetRegistry = new SimplePropertyValueConverterRegistry(sourceRegistry); + assertThat(targetRegistry.size()).isOne(); + + sourceRegistry.registerConverter(Address.class, "street", mock(PropertyValueConverter.class)); + assertThat(sourceRegistry.size()).isEqualTo(2); + assertThat(targetRegistry.size()).isOne(); + } + + @Test // GH-1484 + void registryServesMatchingConverter() { + + SimplePropertyValueConverterRegistry registry = new SimplePropertyValueConverterRegistry(); + registry.registerConverter(Person.class, "name", mock(PropertyValueConverter.class)); + + assertThat(registry.isEmpty()).isFalse(); + assertThat(registry.size()).isOne(); + + assertThat(registry.containsConverterFor(Person.class, "name")).isTrue(); + assertThat(registry.getConverter(Person.class, "name")).isNotNull(); + + assertThat(registry.containsConverterFor(Person.class, "age")).isFalse(); + assertThat(registry.getConverter(Person.class, "age")).isNull(); + + assertThat(registry.getConverter(Address.class, "name")).isNull(); + } + + @Test // GH-1484 + void registryMayHoldConvertersForDifferentPropertiesOfSameType() { + + PropertyValueConverter nameConverter = mock(PropertyValueConverter.class); + PropertyValueConverter ageConverter = mock(PropertyValueConverter.class); + + SimplePropertyValueConverterRegistry registry = new SimplePropertyValueConverterRegistry(); + registry.registerConverter(Person.class, "name", nameConverter); + registry.registerConverter(Person.class, "age", ageConverter); + + assertThat(registry.getConverter(Person.class, "name")).isSameAs(nameConverter); + assertThat(registry.getConverter(Person.class, "age")).isSameAs(ageConverter); + } + + static class Address {} + +} diff --git a/src/test/java/org/springframework/data/convert/WhatWeWant.java b/src/test/java/org/springframework/data/convert/WhatWeWant.java index 7ca616af4..2f4a14a1e 100644 --- a/src/test/java/org/springframework/data/convert/WhatWeWant.java +++ b/src/test/java/org/springframework/data/convert/WhatWeWant.java @@ -1,19 +1,3 @@ -/* - * 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. * @@ -21,7 +5,7 @@ * 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 + * 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, @@ -39,7 +23,6 @@ import java.util.function.Function; import java.util.function.Predicate; import org.junit.jupiter.api.Test; -import org.springframework.data.convert.PropertyValueConverter.ValueConversionContext; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.PropertyPath; import org.springframework.data.mapping.context.SamplePersistentProperty; @@ -87,13 +70,13 @@ public class WhatWeWant { @Nullable @Override - public Object nativeToDomain(@Nullable Object nativeValue, ValueConversionContext context) { + public Object read(@Nullable Object value, ValueConversionContext context) { return null; } @Nullable @Override - public Object domainToNative(@Nullable Object domainValue, ValueConversionContext context) { + public Object write(@Nullable Object value, ValueConversionContext context) { return null; } }); @@ -238,13 +221,13 @@ public class WhatWeWant { } @Override - public B domainToNative(A domainValue, ValueConversionContext

context) { - return writer.apply(domainValue, context); + public B write(A value, ValueConversionContext

context) { + return writer.apply(value, context); } @Override - public A nativeToDomain(B nativeValue, ValueConversionContext

context) { - return reader.apply(nativeValue, context); + public A read(B value, ValueConversionContext

context) { + return reader.apply(value, context); } } } 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 7d4c84fb2..e108da8da 100755 --- a/src/test/java/org/springframework/data/mapping/model/AbstractPersistentPropertyUnitTests.java +++ b/src/test/java/org/springframework/data/mapping/model/AbstractPersistentPropertyUnitTests.java @@ -44,7 +44,6 @@ 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; /** 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 e9f2c5cd0..818980cb8 100755 --- a/src/test/java/org/springframework/data/mapping/model/AnnotationBasedPersistentPropertyUnitTests.java +++ b/src/test/java/org/springframework/data/mapping/model/AnnotationBasedPersistentPropertyUnitTests.java @@ -36,7 +36,6 @@ 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; @@ -45,9 +44,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.convert.ValueConversionContext; +import org.springframework.data.convert.ValueConverter; import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.context.SampleMappingContext; @@ -342,6 +341,15 @@ public class AnnotationBasedPersistentPropertyUnitTests

, Annotation> getAnnotationCache(SamplePersistentProperty property) { return (Map, Annotation>) ReflectionTestUtils.getField(property, "annotationCache"); @@ -536,48 +544,21 @@ public class AnnotationBasedPersistentPropertyUnitTests

> { @Override - public Object nativeToDomain(Object value, ValueConversionContext context) { + public Object read(Object value, ValueConversionContext context) { return null; } @Override - public Object domainToNative(Object value, ValueConversionContext context) { + public Object write(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 { - - } }