Add support for property-specific converters.

Closes #1484
Original pull request: #2566.
This commit is contained in:
Christoph Strobl
2021-12-10 12:18:54 +01:00
committed by Mark Paluch
parent 9a3a38dc22
commit caf49ad739
15 changed files with 1220 additions and 8 deletions

View File

@@ -42,6 +42,7 @@ import org.springframework.data.convert.CustomConversions.ConverterConfiguration
import org.springframework.data.convert.CustomConversions.StoreConversions;
import org.springframework.data.convert.Jsr310Converters.LocalDateTimeToDateConverter;
import org.springframework.data.geo.Point;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.model.SimpleTypeHolder;
/**
@@ -273,6 +274,25 @@ class CustomConversionsUnitTests {
assertThat(conversionService.canConvert(List.class, io.vavr.collection.List.class)).isTrue();
}
@Test // GH-1484
void allowsToRegisterPropertyConversions() {
PropertyValueConversions propertyValueConversions = mock(PropertyValueConversions.class);
when(propertyValueConversions.getValueConverter(any())).thenReturn(mock(PropertyValueConverter.class));
CustomConversions conversions = new CustomConversions(new ConverterConfiguration(StoreConversions.NONE,
Collections.emptyList(), (it) -> true, propertyValueConversions));
assertThat(conversions.getPropertyValueConverter(mock(PersistentProperty.class))).isNotNull();
}
@Test // GH-1484
void doesNotFailIfPropertiesConversionIsNull() {
CustomConversions conversions = new CustomConversions(new ConverterConfiguration(StoreConversions.NONE,
Collections.emptyList(), (it) -> true, null));
assertThat(conversions.getPropertyValueConverter(mock(PersistentProperty.class))).isNull();
}
private static Class<?> createProxyTypeFor(Class<?> type) {
var factory = new ProxyFactory();

View File

@@ -0,0 +1,239 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.convert;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.util.UUID;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.data.convert.PropertyValueConverter.ValueConversionContext;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.lang.Nullable;
/**
* @author Christoph Strobl
*/
public class PropertyValueConverterFactoryUnitTests {
@Test // GH-1484
void simpleConverterFactoryCanInstantiateFactoryWithDefaultCtor() {
assertThat(PropertyValueConverterFactory.simple().getConverter(ConverterWithDefaultCtor.class))
.isInstanceOf(ConverterWithDefaultCtor.class);
}
@Test // GH-1484
void simpleConverterFactoryReadsConverterFromAnnotation() {
PersistentProperty property = mock(PersistentProperty.class);
when(property.hasValueConverter()).thenReturn(true);
when(property.getValueConverterType()).thenReturn(ConverterWithDefaultCtor.class);
assertThat(PropertyValueConverterFactory.simple().getConverter(property))
.isInstanceOf(ConverterWithDefaultCtor.class);
}
@Test // GH-1484
void simpleConverterFactoryErrorsOnNullType() {
assertThatIllegalArgumentException()
.isThrownBy(() -> PropertyValueConverterFactory.simple().getConverter((Class) null));
}
@Test // GH-1484
void simpleConverterFactoryCanExtractFactoryEnumInstance() {
assertThat(PropertyValueConverterFactory.simple().getConverter(ConverterEnum.class))
.isInstanceOf(ConverterEnum.class);
}
@Test // GH-1484
void simpleConverterFactoryCannotInstantiateFactoryWithDependency() {
assertThatExceptionOfType(RuntimeException.class)
.isThrownBy(() -> PropertyValueConverterFactory.simple().getConverter(ConverterWithDependency.class));
}
@Test // GH-1484
void beanFactoryAwareConverterFactoryCanInstantiateFactoryWithDefaultCtor() {
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
assertThat(PropertyValueConverterFactory.beanFactoryAware(beanFactory).getConverter(ConverterWithDefaultCtor.class))
.isInstanceOf(ConverterWithDefaultCtor.class);
}
@Test // GH-1484
void beanFactoryAwareConverterFactoryCanInstantiateFactoryWithBeanReference() {
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
beanFactory.registerBeanDefinition("someDependency",
BeanDefinitionBuilder.rootBeanDefinition(SomeDependency.class).getBeanDefinition());
assertThat(PropertyValueConverterFactory.beanFactoryAware(beanFactory).getConverter(ConverterWithDependency.class))
.isInstanceOf(ConverterWithDependency.class);
}
@Test // GH-1484
void beanFactoryAwareConverterFactoryCanLookupExistingBean() {
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
beanFactory.registerBeanDefinition("someDependency",
BeanDefinitionBuilder.rootBeanDefinition(SomeDependency.class).getBeanDefinition());
beanFactory.registerBeanDefinition("theMightyConverter",
BeanDefinitionBuilder.rootBeanDefinition(ConverterWithDependency.class)
.addConstructorArgReference("someDependency").getBeanDefinition());
assertThat(PropertyValueConverterFactory.beanFactoryAware(beanFactory).getConverter(ConverterWithDependency.class))
.isSameAs(beanFactory.getBean("theMightyConverter"));
}
@Test // GH-1484
void compositeConverterFactoryIteratesFactories() {
PropertyValueConverter expected = mock(PropertyValueConverter.class);
PropertyValueConverterFactory factory = PropertyValueConverterFactory.chained(new PropertyValueConverterFactory() {
@Nullable
@Override
public <S, T, C extends ValueConversionContext> PropertyValueConverter<S, T, C> getConverter(
Class<? extends PropertyValueConverter<S, T, C>> converterType) {
return null;
}
}, new PropertyValueConverterFactory() {
@Nullable
@Override
public <S, T, C extends ValueConversionContext> PropertyValueConverter<S, T, C> getConverter(
Class<? extends PropertyValueConverter<S, T, C>> converterType) {
return expected;
}
});
assertThat(factory.getConverter(ConverterWithDefaultCtor.class)).isSameAs(expected);
}
@Test // GH-1484
void compositeConverterFactoryFailsOnException() {
PropertyValueConverterFactory factory = PropertyValueConverterFactory.chained(new PropertyValueConverterFactory() {
@Nullable
@Override
public <S, T, C extends ValueConversionContext> PropertyValueConverter<S, T, C> getConverter(
Class<? extends PropertyValueConverter<S, T, C>> converterType) {
return null;
}
}, new PropertyValueConverterFactory() {
@Nullable
@Override
public <S, T, C extends ValueConversionContext> PropertyValueConverter<S, T, C> getConverter(
Class<? extends PropertyValueConverter<S, T, C>> converterType) {
throw new RuntimeException("can't touch this!");
}
});
assertThatExceptionOfType(RuntimeException.class)
.isThrownBy(() -> factory.getConverter(ConverterWithDefaultCtor.class));
}
@Test // GH-1484
void cachingConverterFactoryServesCachedInstance() {
PropertyValueConverterFactory factory = PropertyValueConverterFactory
.caching(PropertyValueConverterFactory.simple());
assertThat(factory.getConverter(ConverterWithDefaultCtor.class))
.isSameAs(factory.getConverter(ConverterWithDefaultCtor.class));
}
@Test // GH-1484
void cachingConverterFactoryServesCachedInstanceForProperty() {
PersistentProperty property = mock(PersistentProperty.class);
when(property.hasValueConverter()).thenReturn(true);
when(property.getValueConverterType()).thenReturn(ConverterWithDefaultCtor.class);
PropertyValueConverterFactory factory = PropertyValueConverterFactory
.caching(PropertyValueConverterFactory.simple());
assertThat(factory.getConverter(property)) //
.isSameAs(factory.getConverter(property)) //
.isSameAs(factory.getConverter(ConverterWithDefaultCtor.class)); // TODO: is this a valid assumption?
}
static class ConverterWithDefaultCtor implements PropertyValueConverter<String, UUID, ValueConversionContext> {
@Nullable
@Override
public String nativeToDomain(@Nullable UUID nativeValue, ValueConversionContext context) {
return nativeValue.toString();
}
@Nullable
@Override
public UUID domainToNative(@Nullable String domainValue, ValueConversionContext context) {
return UUID.fromString(domainValue);
}
}
enum ConverterEnum implements PropertyValueConverter<String, UUID, ValueConversionContext> {
INSTANCE;
@Nullable
@Override
public String nativeToDomain(@Nullable UUID nativeValue, ValueConversionContext context) {
return nativeValue.toString();
}
@Nullable
@Override
public UUID domainToNative(@Nullable String domainValue, ValueConversionContext context) {
return UUID.fromString(domainValue);
}
}
static class ConverterWithDependency implements PropertyValueConverter<String, UUID, ValueConversionContext> {
private final SomeDependency someDependency;
public ConverterWithDependency(@Autowired SomeDependency someDependency) {
this.someDependency = someDependency;
}
@Nullable
@Override
public String nativeToDomain(@Nullable UUID nativeValue, ValueConversionContext context) {
assertThat(someDependency).isNotNull();
return nativeValue.toString();
}
@Nullable
@Override
public UUID domainToNative(@Nullable String domainValue, ValueConversionContext context) {
assertThat(someDependency).isNotNull();
return UUID.fromString(domainValue);
}
}
static class SomeDependency {
}
}

View File

@@ -0,0 +1,90 @@
/*
* Copyright 2022. the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* Copyright 2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.convert;
import java.util.function.Predicate;
import org.junit.jupiter.api.Test;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.lang.Nullable;
/**
* @author Christoph Strobl
* @since 2022/01
*/
public class WhatWeWant {
@Test
void converterConfig() {
ConverterConfig converterConfig = null;
converterConfig.registerConverter(Foo.class, "value", new PropertyValueConverter() {
@Nullable
@Override
public Object nativeToDomain(@Nullable Object nativeValue, ValueConversionContext context) {
return null;
}
@Nullable
@Override
public Object domainToNative(@Nullable Object domainValue, ValueConversionContext context) {
return null;
}
});
}
static class ConverterConfig {
ConverterConfig registerConverter(Predicate<PersistentProperty<?>> filter, PropertyValueConverter<?,?, ? extends PropertyValueConverter.ValueConversionContext> converter) {
return this;
}
ConverterConfig registerConverter(Class type, String property, PropertyValueConverter<?,?, ? extends PropertyValueConverter.ValueConversionContext> converter) {
PropertyPath.from(property, type);
return this;
}
}
static class Foo {
String value;
}
interface SpecificValueConversionContext extends PropertyValueConverter.ValueConversionContext {
}
interface SpecificPropertyValueConverter<S,T> extends PropertyValueConverter<S,T,SpecificValueConversionContext> {}
}

View File

@@ -36,6 +36,7 @@ import org.jmolecules.ddd.types.Identifier;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.data.convert.PropertyValueConverter;
import org.springframework.data.mapping.Association;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
@@ -43,6 +44,7 @@ import org.springframework.data.mapping.Person;
import org.springframework.data.util.ClassTypeInformation;
import org.springframework.data.util.Optionals;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
import org.springframework.util.ReflectionUtils;
/**
@@ -233,8 +235,7 @@ public class AbstractPersistentPropertyUnitTests {
assertThat(property.isAssociation()).isTrue();
assertThat(property.getAssociationTargetType()).isEqualTo(JMoleculesAggregate.class);
assertThat(property.getPersistentEntityTypeInformation())
.extracting(it -> it.getType())
assertThat(property.getPersistentEntityTypeInformation()).extracting(it -> it.getType())
.containsExactly((Class) JMoleculesAggregate.class);
}
@@ -385,6 +386,7 @@ public class AbstractPersistentPropertyUnitTests {
public <A extends Annotation> A findPropertyOrOwnerAnnotation(Class<A> annotationType) {
return null;
}
}
static class Sample {

View File

@@ -36,6 +36,7 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.AliasFor;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.annotation.AccessType;
@@ -44,6 +45,9 @@ import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.ReadOnlyProperty;
import org.springframework.data.annotation.Reference;
import org.springframework.data.annotation.Transient;
import org.springframework.data.convert.PropertyConverter;
import org.springframework.data.convert.PropertyValueConverter;
import org.springframework.data.convert.PropertyValueConverter.ValueConversionContext;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.context.SampleMappingContext;
@@ -529,4 +533,49 @@ public class AnnotationBasedPersistentPropertyUnitTests<P extends AnnotationBase
}
interface JMoleculesAggregate extends AggregateRoot<JMoleculesAggregate, Identifier> {}
static class WithPropertyConverter {
@PropertyConverter(MyPropertyConverter.class)
String value;
@PropertyConverter(MyPropertyConverterThatRequiresComponents.class)
String value2;
}
static class MyPropertyConverter implements PropertyValueConverter<Object,Object, ValueConversionContext> {
@Override
public Object nativeToDomain(Object value, ValueConversionContext context) {
return null;
}
@Override
public Object domainToNative(Object value, ValueConversionContext context) {
return null;
}
}
static class MyPropertyConverterThatRequiresComponents implements PropertyValueConverter<Object,Object, ValueConversionContext> {
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 {
}
}