diff --git a/pom.xml b/pom.xml index 8c94b24960..6ddf2a9fc3 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-commons - 2.7.0-SNAPSHOT + 2.7.0-GH-1484-SNAPSHOT Spring Data Core diff --git a/src/main/java/org/springframework/data/convert/CustomConversions.java b/src/main/java/org/springframework/data/convert/CustomConversions.java index 6a97eb0cb9..271b1b0601 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,6 +42,7 @@ 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; @@ -98,6 +108,8 @@ public class CustomConversions { private final Function> getRawWriteTarget = convertiblePair -> getCustomTarget( convertiblePair.getSourceType(), null, writingPairs); + private @Nullable PropertyValueConversions propertyValueConversions; + /** * @param converterConfiguration the {@link ConverterConfiguration} to apply. * @since 2.3 @@ -120,6 +132,7 @@ public CustomConversions(ConverterConfiguration converterConfiguration) { this.converters = Collections.unmodifiableList(registeredConverters); this.simpleTypeHolder = new SimpleTypeHolder(customSimpleTypes, converterConfiguration.getStoreConversions().getStoreTypeHolder()); + this.propertyValueConversions = converterConfiguration.getPropertyValueConversions(); } /** @@ -172,6 +185,36 @@ public void registerConvertersIn(ConverterRegistry conversionService) { 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 2.7 + */ + 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 2.7 + */ + @Nullable + public , D extends ValueConversionContext> PropertyValueConverter getPropertyValueConverter( + C property) { + return propertyValueConversions != null ? propertyValueConversions.getValueConverter(property) : null; + } + /** * Get all converters and add origin information * @@ -293,8 +336,8 @@ private boolean isSupportedConverter(ConverterRegistrationIntent registrationInt 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())); } } @@ -877,6 +920,7 @@ protected static class ConverterConfiguration { 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. @@ -902,9 +946,30 @@ public ConverterConfiguration(StoreConversions storeConversions, List userCon public ConverterConfiguration(StoreConversions storeConversions, List userConverters, Predicate converterRegistrationFilter) { + 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) { + this.storeConversions = storeConversions; this.userConverters = new ArrayList<>(userConverters); this.converterRegistrationFilter = converterRegistrationFilter; + this.propertyValueConversions = propertyValueConversions; } /** @@ -927,5 +992,14 @@ List getUserConverters() { boolean shouldRegister(ConvertiblePair candidate) { return this.converterRegistrationFilter.test(candidate); } + + /** + * @return the configured {@link PropertyValueConversions} if set, {@literal null} otherwise. + * @since 2.7 + */ + @Nullable + public PropertyValueConversions getPropertyValueConversions() { + return this.propertyValueConversions; + } } } 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 0000000000..1c1b09fcf2 --- /dev/null +++ b/src/main/java/org/springframework/data/convert/PropertyValueConversions.java @@ -0,0 +1,70 @@ +/* + * 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.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 ValueConversionContext conversion context}. + * + * @author Christoph Strobl + * @since 2.7 + * @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((PersistentProperty) 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 + , 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 new file mode 100644 index 0000000000..dd8d5d610d --- /dev/null +++ b/src/main/java/org/springframework/data/convert/PropertyValueConverter.java @@ -0,0 +1,113 @@ +/* + * 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 java.util.function.BiFunction; + +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. + *

context) { + return writer.apply(value, context); + } + + @Nullable + @Override + public A read(@Nullable B value, ValueConversionContext

context) { + return reader.apply(value, context); + } + } +} diff --git a/src/main/java/org/springframework/data/convert/PropertyValueConverterFactories.java b/src/main/java/org/springframework/data/convert/PropertyValueConverterFactories.java new file mode 100644 index 0000000000..dac34508d5 --- /dev/null +++ b/src/main/java/org/springframework/data/convert/PropertyValueConverterFactories.java @@ -0,0 +1,240 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.convert; + +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * {@link PropertyValueConverterFactories} provides a collection of predefined {@link PropertyValueConverterFactory} + * implementations. Depending on the applications need {@link PropertyValueConverterFactory factories} can be + * {@link ChainedPropertyValueConverterFactory chained} and the created {@link PropertyValueConverter converter} + * {@link CachingPropertyValueConverterFactory cached}. + * + * @author Christoph Strobl + * @since 2.7 + */ +final class PropertyValueConverterFactories { + + /** + * {@link PropertyValueConverterFactory} implementation that returns the first no null {@link PropertyValueConverter} + * by asking given {@link PropertyValueConverterFactory factories} one by one. + * + * @author Christoph Strobl + * @since 2.7 + */ + static class ChainedPropertyValueConverterFactory implements PropertyValueConverterFactory { + + private List delegates; + + ChainedPropertyValueConverterFactory(PropertyValueConverterFactory... delegates) { + this(Arrays.asList(delegates)); + } + + ChainedPropertyValueConverterFactory(List delegates) { + this.delegates = Collections.unmodifiableList(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); + } + + public List converterFactories() { + return delegates; + } + } + + /** + * 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 2.7 + */ + 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 2.7 + */ + static class BeanFactoryAwarePropertyValueConverterFactory implements PropertyValueConverterFactory { + + private final BeanFactory beanFactory; + + 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; + } + } + + /** + * {@link PropertyValueConverterFactory} implementation that serves {@link PropertyValueConverter} from a given + * {@link ValueConverterRegistry registry}. + * + * @author Christoph Strobl + * @since 2.7 + */ + static class ConfiguredInstanceServingValueConverterFactory implements PropertyValueConverterFactory { + + private final ValueConverterRegistry converterRegistry; + + ConfiguredInstanceServingValueConverterFactory(ValueConverterRegistry converterRegistry) { + + Assert.notNull(converterRegistry, "ConversionsRegistrar must not be null!"); + this.converterRegistry = converterRegistry; + } + + @Nullable + @Override + public > PropertyValueConverter getConverter( + PersistentProperty property) { + return (PropertyValueConverter) converterRegistry.getConverter(property.getOwner().getType(), + property.getName()); + } + + @Override + public > PropertyValueConverter getConverter( + Class> converterType) { + return null; + } + } + + /** + * {@link PropertyValueConverterFactory} implementation that caches converters provided by an underlying + * {@link PropertyValueConverterFactory factory}. + * + * @author Christoph Strobl + * @since 2.7 + */ + static class CachingPropertyValueConverterFactory implements PropertyValueConverterFactory { + + private final PropertyValueConverterFactory delegate; + private final Cache cache = new Cache(); + + CachingPropertyValueConverterFactory(PropertyValueConverterFactory delegate) { + + Assert.notNull(delegate, "Delegate must not be null!"); + this.delegate = delegate; + } + + @Nullable + @Override + public > PropertyValueConverter getConverter( + PersistentProperty property) { + + Optional>> converter = cache.get(property); + + return converter != null ? (PropertyValueConverter) converter.orElse(null) + : cache.cache(property, delegate.getConverter(property)); + } + + @Override + public > PropertyValueConverter getConverter( + Class> converterType) { + + Optional>> converter = cache.get(converterType); + + return converter != null ? (PropertyValueConverter) converter.orElse(null) + : cache.cache(converterType, delegate.getConverter(converterType)); + } + + static class Cache { + + Map, Optional>>> perPropertyCache = new HashMap<>(); + Map, Optional>>> typeCache = new HashMap<>(); + + Optional>> get(PersistentProperty property) { + return perPropertyCache.get(property); + } + + Optional>> get(Class type) { + return typeCache.get(type); + } + + > PropertyValueConverter cache(PersistentProperty property, + @Nullable PropertyValueConverter converter) { + perPropertyCache.putIfAbsent(property, Optional.ofNullable(converter)); + cache(property.getValueConverterType(), converter); + return converter; + } + + > PropertyValueConverter cache(Class type, + @Nullable PropertyValueConverter converter) { + typeCache.putIfAbsent(type, Optional.ofNullable(converter)); + return converter; + } + } + } +} diff --git a/src/main/java/org/springframework/data/convert/PropertyValueConverterFactory.java b/src/main/java/org/springframework/data/convert/PropertyValueConverterFactory.java new file mode 100644 index 0000000000..7a40cf3d5a --- /dev/null +++ b/src/main/java/org/springframework/data/convert/PropertyValueConverterFactory.java @@ -0,0 +1,149 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.convert; + +import java.util.Arrays; +import java.util.List; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.data.convert.PropertyValueConverterFactories.BeanFactoryAwarePropertyValueConverterFactory; +import org.springframework.data.convert.PropertyValueConverterFactories.CachingPropertyValueConverterFactory; +import org.springframework.data.convert.PropertyValueConverterFactories.ChainedPropertyValueConverterFactory; +import org.springframework.data.convert.PropertyValueConverterFactories.ConfiguredInstanceServingValueConverterFactory; +import org.springframework.data.convert.PropertyValueConverterFactories.SimplePropertyConverterFactory; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * A factory that provides {@link PropertyValueConverter value converters}. + *

+ * Depending on the applications need {@link PropertyValueConverterFactory factories} can be {@link #chained(List) + * chained} and the resulting {@link PropertyValueConverter converter} may be + * {@link #caching(PropertyValueConverterFactory) cached}. + * + * @author Christoph Strobl + * @since 2.7 + */ +public interface PropertyValueConverterFactory { + + /** + * Get the {@link PropertyValueConverter} applicable for the given {@link PersistentProperty}. + * + * @param property must not be {@literal null}. + * @param domain specific type. + * @param store native type. + * @param value conversion context to use. + * @return can be {@literal null}. + */ + @Nullable + @SuppressWarnings("unchecked") + default > PropertyValueConverter getConverter( + PersistentProperty property) { + + 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); + + /** + * 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(ValueConverterRegistry 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 ChainedPropertyValueConverterFactory(factoryList); + } + + /** + * Obtain a {@link PropertyValueConverterFactory} that will cache {@link PropertyValueConverter} instances per + * {@link PersistentProperty}. + * + * @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 new file mode 100644 index 0000000000..d4207d4131 --- /dev/null +++ b/src/main/java/org/springframework/data/convert/PropertyValueConverterRegistrar.java @@ -0,0 +1,192 @@ +/* + * 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.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; + +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 + * @author Oliver Drotbohm + * @since 2.7 + */ +public class PropertyValueConverterRegistrar

> { + + private final SimplePropertyValueConverterRegistry

registry = new SimplePropertyValueConverterRegistry<>(); + + /** + * 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); + } + + /** + * 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, + @SuppressWarnings("unused") Class propertyType) { + return new WritingConverterRegistrationBuilder<>(type, propertyName, this); + } + + /** + * Register the given converter for the types property identified via its name. + * + * @param type the domain type to obtain the property from + * @param path the property name. + * @param converter the converter to apply. + * @return this. + */ + public PropertyValueConverterRegistrar

registerConverter(Class type, String path, + PropertyValueConverter> converter) { + + registry.registerConverter(type, path, + (PropertyValueConverter>) converter); + return this; + } + + /** + * Obtain the {@link SimplePropertyValueConverterRegistry}. + * + * @return new instance of {@link SimplePropertyValueConverterRegistry}. + */ + public ValueConverterRegistry

buildRegistry() { + return new SimplePropertyValueConverterRegistry<>(registry); + } + + /** + * Register collected {@link PropertyValueConverter converters} within the given {@link ValueConverterRegistry + * registry}. + * + * @return new instance of {@link SimplePropertyValueConverterRegistry}. + */ + public void registerConvertersIn(ValueConverterRegistry

target) { + + Assert.notNull(target, "Target registry must not be null!"); + + registry.getConverterRegistrationMap().entrySet().forEach(entry -> { + target.registerConverter(entry.getKey().type, entry.getKey().path, entry.getValue()); + }); + } + + /** + * Helper to build up a fluent registration API starting on + * + * @author Oliver Drotbohm + */ + public static class WritingConverterRegistrationBuilder> { + + private final Consumer>> registration; + private final PropertyValueConverterRegistrar

config; + + WritingConverterRegistrationBuilder(Class type, String property, PropertyValueConverterRegistrar

config) { + + this.config = config; + this.registration = (converter) -> config.registerConverter(type, property, converter); + } + + public ReadingConverterRegistrationBuilder writingAsIs() { + return writing((source, context) -> source); + } + + public 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}. + */ + public 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 + */ + public static class ReadingConverterRegistrationBuilder> { + + private final WritingConverterRegistrationBuilder origin; + private final BiFunction, R> writer; + + ReadingConverterRegistrationBuilder(WritingConverterRegistrationBuilder origin, + BiFunction, R> writer) { + this.origin = origin; + this.writer = writer; + } + + public PropertyValueConverterRegistrar

readingAsIs() { + return reading((source, context) -> (S) source); + } + + public 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 + */ + public 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 new file mode 100644 index 0000000000..b93f3e660d --- /dev/null +++ b/src/main/java/org/springframework/data/convert/SimplePropertyValueConversions.java @@ -0,0 +1,136 @@ +/* + * 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.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 2.7 + */ +public class SimplePropertyValueConversions implements PropertyValueConversions, InitializingBean { + + private @Nullable PropertyValueConverterFactory converterFactory; + private @Nullable ValueConverterRegistry valueConverterRegistry; + private boolean converterCacheEnabled = true; + private AtomicBoolean initialized = new AtomicBoolean(false); + + /** + * Set the {@link PropertyValueConverterFactory factory} responsible for creating the actual + * {@link PropertyValueConverter converter}. + * + * @param converterFactory must not be {@literal null}. + */ + public void setConverterFactory(PropertyValueConverterFactory converterFactory) { + this.converterFactory = converterFactory; + } + + @Nullable + public PropertyValueConverterFactory getConverterFactory() { + return converterFactory; + } + + /** + * Set the {@link ValueConverterRegistry converter registry} for path configured converters. This is short for adding + * a + * {@link org.springframework.data.convert.PropertyValueConverterFactories.ConfiguredInstanceServingValueConverterFactory} + * at the end of a {@link ChainedPropertyValueConverterFactory}. + * + * @param valueConverterRegistry must not be {@literal null}. + */ + public void setValueConverterRegistry(ValueConverterRegistry valueConverterRegistry) { + this.valueConverterRegistry = valueConverterRegistry; + } + + /** + * Get the {@link ValueConverterRegistry} used for path configured converters. + * + * @return can be {@literal null}. + */ + @Nullable + public ValueConverterRegistry getValueConverterRegistry() { + return valueConverterRegistry; + } + + /** + * Dis-/Enable caching. Enabled by default. + * + * @param converterCacheEnabled set to {@literal true} to enable caching of {@link PropertyValueConverter converter} + * instances. + */ + public void setConverterCacheEnabled(boolean converterCacheEnabled) { + this.converterCacheEnabled = converterCacheEnabled; + } + + @Nullable + @Override + public , D extends ValueConversionContext> PropertyValueConverter getValueConverter( + C property) { + + if (!initialized.get()) { + init(); + } + + 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) { + factoryList.add(converterFactory); + } else { + factoryList.add(PropertyValueConverterFactory.simple()); + } + + if ((valueConverterRegistry != null) && !valueConverterRegistry.isEmpty()) { + factoryList.add(PropertyValueConverterFactory.configuredInstance(valueConverterRegistry)); + } + + 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/convert/SimplePropertyValueConverterRegistry.java b/src/main/java/org/springframework/data/convert/SimplePropertyValueConverterRegistry.java new file mode 100644 index 0000000000..9a26a195d4 --- /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 { + + final Class type; + final 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 0000000000..049485af08 --- /dev/null +++ b/src/main/java/org/springframework/data/convert/ValueConversionContext.java @@ -0,0 +1,123 @@ +/* + * 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 through 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) { + + if (value == null || target.getType().isInstance(value)) { + return target.getType().cast(value); + } + + 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) { + + if (value == null || target.getType().isInstance(value)) { + return target.getType().cast(value); + } + + 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/ValueConverter.java b/src/main/java/org/springframework/data/convert/ValueConverter.java new file mode 100644 index 0000000000..f345ea7912 --- /dev/null +++ b/src/main/java/org/springframework/data/convert/ValueConverter.java @@ -0,0 +1,53 @@ +/* + * 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.
+ * Can 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 2.7 + * @see PropertyValueConverter + * @see PropertyValueConverterFactory + */ +@Target({ FIELD, ANNOTATION_TYPE }) +@Documented +@Retention(RetentionPolicy.RUNTIME) +public @interface ValueConverter { + + /** + * The {@link PropertyValueConverter} type handling the value conversion of the annotated property. + * + * @return the configured {@link PropertyValueConverter}. {@link ObjectToObjectPropertyValueConverter} by default. + */ + Class value() default ObjectToObjectPropertyValueConverter.class; + +} diff --git a/src/main/java/org/springframework/data/convert/ValueConverterRegistry.java b/src/main/java/org/springframework/data/convert/ValueConverterRegistry.java new file mode 100644 index 0000000000..2ad9c7a2e7 --- /dev/null +++ b/src/main/java/org/springframework/data/convert/ValueConverterRegistry.java @@ -0,0 +1,77 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https//www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.convert; + +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.lang.Nullable; + +/** + * A registry of property-specific {@link PropertyValueConverter value converters} to convert only specific + * properties/values of an object. + * + * @author Christoph Strobl + * @since 2.7 + */ +public interface ValueConverterRegistry

> { + + /** + * Register the {@link PropertyValueConverter} for the property of the given type. + * + * @param type the target type. Must not be {@literal null}. + * @param path the property name. Must not be {@literal null}. + * @param converter the converter to register. Must not be {@literal null}. + */ + void registerConverter(Class type, String path, + PropertyValueConverter> 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 converters are 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 69ac12a9eb..5ce731e21a 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 org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.data.convert.PropertyValueConverter; +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; @@ -33,6 +36,7 @@ * @author Oliver Gierke * @author Mark Paluch * @author Jens Schauder + * @author Christoph Strobl */ public interface PersistentProperty

> { @@ -76,9 +80,9 @@ public interface PersistentProperty

> { Iterable> getPersistentEntityTypes(); /** - * Returns the detected {@link TypeInformation TypeInformations} if the property references a {@link PersistentEntity}. - * Will return an {@literal empty} {@link Iterable} in case it refers to a simple type. Will return the {@link Collection}'s - * component types or the {@link Map}'s value type transparently. + * Returns the detected {@link TypeInformation TypeInformations} if the property references a + * {@link PersistentEntity}. Will return an {@literal empty} {@link Iterable} in case it refers to a simple type. Will + * return the {@link Collection}'s component types or the {@link Map}'s value type transparently. * * @return never {@literal null}. * @since 2.6 @@ -433,4 +437,33 @@ default PersistentPropertyAccessor getAccessorForOwner(T owner) { 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() { + + ValueConverter annotation = findAnnotation(ValueConverter.class); + + return annotation == null ? null + : (Class>>>) annotation + .value(); + } + + /** + * @return by default return {@literal true} if {@link ValueConverter} annotation is present. + * @since 2.7 + */ + default boolean hasValueConverter() { + return isAnnotationPresent(ValueConverter.class); + } } diff --git a/src/main/java/org/springframework/data/mapping/model/AnnotationBasedPersistentProperty.java b/src/main/java/org/springframework/data/mapping/model/AnnotationBasedPersistentProperty.java index f44fd5d48e..1db387a152 100644 --- a/src/main/java/org/springframework/data/mapping/model/AnnotationBasedPersistentProperty.java +++ b/src/main/java/org/springframework/data/mapping/model/AnnotationBasedPersistentProperty.java @@ -33,6 +33,9 @@ import org.springframework.data.annotation.Reference; import org.springframework.data.annotation.Transient; import org.springframework.data.annotation.Version; +import org.springframework.data.convert.ValueConverter; +import org.springframework.data.convert.PropertyValueConverter; +import org.springframework.data.convert.ValueConversionContext; import org.springframework.data.mapping.Association; import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.PersistentEntity; @@ -132,8 +135,7 @@ private void populateAnnotationCache(Property property) { + "multiple times on accessor methods of property %s in class %s!", annotationType.getSimpleName(), getName(), getOwner().getType().getSimpleName()); - annotationCache.put(annotationType, - Optional.of(mergedAnnotation)); + annotationCache.put(annotationType, Optional.of(mergedAnnotation)); } }); @@ -148,8 +150,7 @@ private void populateAnnotationCache(Property property) { "Ambiguous mapping! Annotation %s configured " + "on field %s and one of its accessor methods in class %s!", annotationType.getSimpleName(), it.getName(), getOwner().getType().getSimpleName()); - annotationCache.put(annotationType, - Optional.of(mergedAnnotation)); + annotationCache.put(annotationType, Optional.of(mergedAnnotation)); } }); } @@ -308,6 +309,17 @@ public TypeInformation getAssociationTargetTypeInformation() { return associationTargetType.getNullable(); } + @Nullable + @Override + @SuppressWarnings("unchecked") + public Class>>> getValueConverterType() { + + return doFindAnnotation(ValueConverter.class) // + .map(ValueConverter::value) // + .map(Class.class::cast) // + .orElse(null); + } + /* * (non-Javadoc) * @see org.springframework.data.mapping.model.AbstractPersistentProperty#toString() @@ -343,8 +355,7 @@ private Stream getAccessors() { @SuppressWarnings("unchecked") private static Class 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 c034094497..8722594f09 100644 --- a/src/test/java/org/springframework/data/convert/CustomConversionsUnitTests.java +++ b/src/test/java/org/springframework/data/convert/CustomConversionsUnitTests.java @@ -45,6 +45,7 @@ import org.springframework.data.convert.Jsr310Converters.LocalDateTimeToDateConverter; import org.springframework.data.convert.ThreeTenBackPortConverters.LocalDateTimeToJavaTimeInstantConverter; import org.springframework.data.geo.Point; +import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.model.SimpleTypeHolder; import org.threeten.bp.LocalDateTime; @@ -303,6 +304,25 @@ void registersVavrConverters() { assertThat(conversionService.canConvert(List.class, io.vavr.collection.List.class)).isTrue(); } + @Test // GH-1484 + void allowsToRegisterPropertyConversions() { + + PropertyValueConversions propertyValueConversions = mock(PropertyValueConversions.class); + when(propertyValueConversions.getValueConverter(any())).thenReturn(mock(PropertyValueConverter.class)); + + CustomConversions conversions = new CustomConversions(new ConverterConfiguration(StoreConversions.NONE, + Collections.emptyList(), (it) -> true, propertyValueConversions)); + assertThat(conversions.getPropertyValueConverter(mock(PersistentProperty.class))).isNotNull(); + } + + @Test // GH-1484 + void doesNotFailIfPropertiesConversionIsNull() { + + CustomConversions conversions = new CustomConversions(new ConverterConfiguration(StoreConversions.NONE, + Collections.emptyList(), (it) -> true, null)); + assertThat(conversions.getPropertyValueConverter(mock(PersistentProperty.class))).isNull(); + } + private static Class createProxyTypeFor(Class type) { ProxyFactory factory = new ProxyFactory(); diff --git a/src/test/java/org/springframework/data/convert/PropertyValueConverterFactoryUnitTests.java b/src/test/java/org/springframework/data/convert/PropertyValueConverterFactoryUnitTests.java new file mode 100644 index 0000000000..7a84aed72c --- /dev/null +++ b/src/test/java/org/springframework/data/convert/PropertyValueConverterFactoryUnitTests.java @@ -0,0 +1,286 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.convert; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.UUID; + +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mapping.context.SamplePersistentProperty; +import org.springframework.lang.Nullable; + +/** + * @author Christoph Strobl + */ +public class PropertyValueConverterFactoryUnitTests { + + @Test // GH-1484 + void simpleConverterFactoryCanInstantiateFactoryWithDefaultCtor() { + + assertThat(PropertyValueConverterFactory.simple().getConverter(ConverterWithDefaultCtor.class)) + .isInstanceOf(ConverterWithDefaultCtor.class); + } + + @Test // GH-1484 + void simpleConverterFactoryReadsConverterFromAnnotation() { + + PersistentProperty property = mock(PersistentProperty.class); + when(property.hasValueConverter()).thenReturn(true); + when(property.getValueConverterType()).thenReturn(ConverterWithDefaultCtor.class); + + assertThat(PropertyValueConverterFactory.simple().getConverter(property)) + .isInstanceOf(ConverterWithDefaultCtor.class); + } + + @Test // GH-1484 + void simpleConverterFactoryErrorsOnNullType() { + + assertThatIllegalArgumentException() + .isThrownBy(() -> PropertyValueConverterFactory.simple().getConverter((Class) null)); + } + + @Test // GH-1484 + void simpleConverterFactoryCanExtractFactoryEnumInstance() { + + assertThat(PropertyValueConverterFactory.simple().getConverter(ConverterEnum.class)) + .isInstanceOf(ConverterEnum.class); + } + + @Test // GH-1484 + void simpleConverterFactoryCannotInstantiateFactoryWithDependency() { + + assertThatExceptionOfType(RuntimeException.class) + .isThrownBy(() -> PropertyValueConverterFactory.simple().getConverter(ConverterWithDependency.class)); + } + + @Test // GH-1484 + void beanFactoryAwareConverterFactoryCanInstantiateFactoryWithDefaultCtor() { + + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + + assertThat(PropertyValueConverterFactory.beanFactoryAware(beanFactory).getConverter(ConverterWithDefaultCtor.class)) + .isInstanceOf(ConverterWithDefaultCtor.class); + } + + @Test // GH-1484 + void beanFactoryAwareConverterFactoryCanInstantiateFactoryWithBeanReference() { + + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + beanFactory.registerBeanDefinition("someDependency", + BeanDefinitionBuilder.rootBeanDefinition(SomeDependency.class).getBeanDefinition()); + + assertThat(PropertyValueConverterFactory.beanFactoryAware(beanFactory).getConverter(ConverterWithDependency.class)) + .isInstanceOf(ConverterWithDependency.class); + } + + @Test // GH-1484 + void beanFactoryAwareConverterFactoryCanLookupExistingBean() { + + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + beanFactory.registerBeanDefinition("someDependency", + BeanDefinitionBuilder.rootBeanDefinition(SomeDependency.class).getBeanDefinition()); + beanFactory.registerBeanDefinition("theMightyConverter", + BeanDefinitionBuilder.rootBeanDefinition(ConverterWithDependency.class) + .addConstructorArgReference("someDependency").getBeanDefinition()); + + assertThat(PropertyValueConverterFactory.beanFactoryAware(beanFactory).getConverter(ConverterWithDependency.class)) + .isSameAs(beanFactory.getBean("theMightyConverter")); + } + + @Test // GH-1484 + void chainedConverterFactoryIteratesFactories() { + + PropertyValueConverter expected = mock(PropertyValueConverter.class); + + PropertyValueConverterFactory factory = PropertyValueConverterFactory.chained(new PropertyValueConverterFactory() { + @Nullable + @Override + public > PropertyValueConverter getConverter( + Class> 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 chainedConverterFactoryFailsOnException() { + + 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 cachingConverterFactoryAlsoCachesAbsenceOfConverter() { + + PropertyValueConverterFactory source = Mockito.spy(PropertyValueConverterFactory.simple()); + PropertyValueConverterFactory factory = PropertyValueConverterFactory.caching(source); + + PersistentEntity entity = mock(PersistentEntity.class); + PersistentProperty property = mock(PersistentProperty.class); + when(property.getOwner()).thenReturn(entity); + when(entity.getType()).thenReturn(Person.class); + when(property.getName()).thenReturn("firstname"); + + // fill the cache + assertThat(factory.getConverter(property)).isNull(); + verify(source).getConverter(any(PersistentProperty.class)); + + // now get the cached null value + assertThat(factory.getConverter(property)).isNull(); + verify(source).getConverter(any(PersistentProperty.class)); + } + + @Test // GH-1484 + void cachingConverterFactoryServesCachedInstanceForProperty() { + + PersistentProperty property = mock(PersistentProperty.class); + when(property.hasValueConverter()).thenReturn(true); + when(property.getValueConverterType()).thenReturn(ConverterWithDefaultCtor.class); + + PropertyValueConverterFactory factory = PropertyValueConverterFactory + .caching(PropertyValueConverterFactory.simple()); + assertThat(factory.getConverter(property)) // + .isSameAs(factory.getConverter(property)) // + .isSameAs(factory.getConverter(ConverterWithDefaultCtor.class)); // TODO: is this a valid assumption? + } + + static class ConverterWithDefaultCtor + implements PropertyValueConverter> { + + @Nullable + @Override + public String read(@Nullable UUID value, ValueConversionContext context) { + return value.toString(); + } + + @Nullable + @Override + public UUID write(@Nullable String value, ValueConversionContext context) { + return UUID.fromString(value); + } + } + + enum ConverterEnum implements PropertyValueConverter> { + + INSTANCE; + + @Nullable + @Override + public String read(@Nullable UUID value, ValueConversionContext context) { + return value.toString(); + } + + @Nullable + @Override + public UUID write(@Nullable String value, ValueConversionContext context) { + return UUID.fromString(value); + } + } + + static class ConverterWithDependency + implements PropertyValueConverter> { + + private final SomeDependency someDependency; + + public ConverterWithDependency(@Autowired SomeDependency someDependency) { + this.someDependency = someDependency; + } + + @Nullable + @Override + public String read(@Nullable UUID value, ValueConversionContext context) { + + assertThat(someDependency).isNotNull(); + return value.toString(); + } + + @Nullable + @Override + public UUID write(@Nullable String value, ValueConversionContext context) { + + assertThat(someDependency).isNotNull(); + 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 0000000000..b15cb5fdb0 --- /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 0000000000..047d2dd76a --- /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 0000000000..5eea00e75d --- /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 new file mode 100644 index 0000000000..2f4a14a1e3 --- /dev/null +++ b/src/test/java/org/springframework/data/convert/WhatWeWant.java @@ -0,0 +1,264 @@ +/* + * 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.HashMap; +import java.util.Map; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; +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.data.mapping.context.SamplePersistentProperty; +import org.springframework.data.util.MethodInvocationRecorder; +import org.springframework.lang.Nullable; + +/** + * @author Christoph Strobl + * @since 2022/01 + */ +public class WhatWeWant { + + @Test + void converterConfig() { + + ConverterConfig converterConfig = new ConverterConfig(); + + // Arbitrary translations + converterConfig.registerConverter(Foo.class, Foo::getValue) + .writing((source, context) -> WhatWeWant.reverse(source)) // store reversed + .reading((source, context) -> WhatWeWant.reverse(source)); // restore order + + converterConfig.registerConverter(it -> it.findAnnotation(Deprecated.class) != null) + .writing((source, context) -> WhatWeWant.reverse(source.toString())) + .reading((source, context) -> { + return context.read(WhatWeWant.reverse(source)); + }); + + // Clean up on read + converterConfig.registerConverter(Foo.class, Foo::getValue) + .writingAsIs() + .reading((source, context) -> source.trim()); + + converterConfig.registerConverter(Foo.class, Foo::getAddress) + .writing((source, context) -> { + + Map map = new HashMap<>(); + map.put("STREET", source.street.toUpperCase()); + map.put("ZIP", context.write(source.zipCode)); + + return map; + }); + + converterConfig.registerConverter(Foo.class, "value", new PropertyValueConverter() { + + @Nullable + @Override + public Object read(@Nullable Object value, ValueConversionContext context) { + return null; + } + + @Nullable + @Override + public Object write(@Nullable Object value, ValueConversionContext context) { + return null; + } + }); + } + + static String reverse(String source) { + return new StringBuilder(source).reverse().toString(); + } + + 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; + } + + /** + * 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}. + */ + 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); + } + + WritingConverterRegistrationBuilder registerConverter(Predicate

predicate) { + return new WritingConverterRegistrationBuilder(predicate, this); + } + + /** + * Helper to build up a fluent registration API starting on + * {@link ConverterConfig#registerConverter(Class, Function)}. + * + * @author Oliver Drotbohm + */ + static class WritingConverterRegistrationBuilder> { + + private final Consumer registration; + private final ConverterConfig config; + + public WritingConverterRegistrationBuilder(Class type, String property, ConverterConfig config) { + + this.config = config; + this.registration = (converter) -> config.registerConverter(type, property, converter); + } + + public WritingConverterRegistrationBuilder(Predicate

predicate, ConverterConfig config) { + + this.config = config; + this.registration = (converter) -> config.registerConverter(predicate, converter); + } + + ReadingConverterRegistrationBuilder writingAsIs() { + return writing((source, context) -> source); + } + + 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; + } + + ConverterConfig

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

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 + */ + ConverterConfig

reading(BiFunction, S> reader) { + + origin.registration.accept(new FunctionPropertyValueConverter(writer, reader)); + + return origin.config; + } + + /** + * A {@link PropertyValueConverter} that delegates conversion to the given {@link BiFunction}s. + * + * @author Oliver Drotbohm + */ + static class FunctionPropertyValueConverter> + 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); + } + } + } + } + + static class Foo { + String value; + Address address; + + public String getValue() { + return value; + } + + public Address getAddress() { + return address; + } + } + + static class Address { + String street; + ZipCode zipCode; + } + + static class ZipCode { + + } + + interface SpecificValueConversionContext

> extends ValueConversionContext

{ + + } + + interface SpecificPropertyValueConverter + extends PropertyValueConverter> {} +} 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 0185b2780b..c2acacfb1f 100755 --- a/src/test/java/org/springframework/data/mapping/model/AnnotationBasedPersistentPropertyUnitTests.java +++ b/src/test/java/org/springframework/data/mapping/model/AnnotationBasedPersistentPropertyUnitTests.java @@ -44,6 +44,9 @@ import org.springframework.data.annotation.ReadOnlyProperty; import org.springframework.data.annotation.Reference; import org.springframework.data.annotation.Transient; +import org.springframework.data.convert.PropertyValueConverter; +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; @@ -338,6 +341,15 @@ void detectsJMoleculesIdentity() { assertThat(property.isIdProperty()).isTrue(); } + @Test // GH-1484 + void detectsValueConverter() { + + SamplePersistentProperty property = getProperty(WithPropertyConverter.class, "value"); + + assertThat(property.hasValueConverter()).isTrue(); + assertThat(property.getValueConverterType()).isEqualTo(MyPropertyConverter.class); + } + @SuppressWarnings("unchecked") private Map, Annotation> getAnnotationCache(SamplePersistentProperty property) { return (Map, Annotation>) ReflectionTestUtils.getField(property, "annotationCache"); @@ -529,4 +541,24 @@ static class JMolecules { } interface JMoleculesAggregate extends AggregateRoot {} + + static class WithPropertyConverter { + + @ValueConverter(MyPropertyConverter.class) + String value; + } + + static class MyPropertyConverter + implements PropertyValueConverter> { + + @Override + public Object read(Object value, ValueConversionContext context) { + return null; + } + + @Override + public Object write(Object value, ValueConversionContext context) { + return null; + } + } }

+ * A {@link PropertyValueConverter} is, other than a {@link ReadingConverter} or {@link WritingConverter}, only applied + * to special annotated fields which allows a fine-grained conversion of certain values within a specific context. + * + * @author Christoph Strobl + * @param domain specific type. + * @param store native type. + * @param the store specific {@link ValueConversionContext conversion context}. + * @since 2.7 + */ +public interface PropertyValueConverter>> { + + /** + * Convert the given store specific value into it's domain value representation. Typically, a {@literal read} + * operation. + * + * @param value can be {@literal null}. + * @param context never {@literal null}. + * @return the converted value. Can be {@literal null}. + */ + @Nullable + A read(@Nullable B value, C context); + + /** + * Convert the given domain specific value into it's native store representation. Typically, a {@literal write} + * operation. + * + * @param value can be {@literal null}. + * @param context never {@literal null}. + * @return the converted value. Can be {@literal null}. + */ + @Nullable + B write(@Nullable A value, C context); + + /** + * NoOp {@link PropertyValueConverter} implementation. + * + * @author Christoph Strobl + */ + @SuppressWarnings({ "rawtypes", "null" }) + enum ObjectToObjectPropertyValueConverter implements PropertyValueConverter { + + INSTANCE; + + @Nullable + @Override + public Object read(@Nullable Object value, ValueConversionContext context) { + return value; + } + + @Nullable + @Override + public Object write(@Nullable Object value, ValueConversionContext context) { + return value; + } + } + + /** + * A {@link PropertyValueConverter} that delegates conversion to the given {@link BiFunction}s. + * + * @author Oliver Drotbohm + */ + class FunctionPropertyValueConverter> + 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; + } + + @Nullable + @Override + public B write(@Nullable A value, ValueConversionContext