Fluent registration API for PropertyValueConverters.

Introduce a builder API to register PropertyValueConverters using simple (Bi)Functions. Additional methods on ValueConversionContext to enable advanced use cases in converter implementation. Tweak generics of VCC to be able to expose store-specific PersistentProperty implementations via the context.

See #1484
Original pull request: #2566.
This commit is contained in:
Oliver Drotbohm
2022-02-16 15:40:12 +01:00
committed by Mark Paluch
parent caf49ad739
commit e7292279bf
12 changed files with 345 additions and 59 deletions

View File

@@ -26,6 +26,7 @@ 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.data.mapping.context.SamplePersistentProperty;
import org.springframework.lang.Nullable;
/**
@@ -114,14 +115,14 @@ public class PropertyValueConverterFactoryUnitTests {
PropertyValueConverterFactory factory = PropertyValueConverterFactory.chained(new PropertyValueConverterFactory() {
@Nullable
@Override
public <S, T, C extends ValueConversionContext> PropertyValueConverter<S, T, C> getConverter(
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(
public <S, T, C extends ValueConversionContext<?>> PropertyValueConverter<S, T, C> getConverter(
Class<? extends PropertyValueConverter<S, T, C>> converterType) {
return expected;
}
@@ -136,14 +137,14 @@ public class PropertyValueConverterFactoryUnitTests {
PropertyValueConverterFactory factory = PropertyValueConverterFactory.chained(new PropertyValueConverterFactory() {
@Nullable
@Override
public <S, T, C extends ValueConversionContext> PropertyValueConverter<S, T, C> getConverter(
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(
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!");
}
@@ -176,22 +177,23 @@ public class PropertyValueConverterFactoryUnitTests {
.isSameAs(factory.getConverter(ConverterWithDefaultCtor.class)); // TODO: is this a valid assumption?
}
static class ConverterWithDefaultCtor implements PropertyValueConverter<String, UUID, ValueConversionContext> {
static class ConverterWithDefaultCtor
implements PropertyValueConverter<String, UUID, ValueConversionContext<SamplePersistentProperty>> {
@Nullable
@Override
public String nativeToDomain(@Nullable UUID nativeValue, ValueConversionContext context) {
public String nativeToDomain(@Nullable UUID nativeValue, ValueConversionContext<SamplePersistentProperty> context) {
return nativeValue.toString();
}
@Nullable
@Override
public UUID domainToNative(@Nullable String domainValue, ValueConversionContext context) {
public UUID domainToNative(@Nullable String domainValue, ValueConversionContext<SamplePersistentProperty> context) {
return UUID.fromString(domainValue);
}
}
enum ConverterEnum implements PropertyValueConverter<String, UUID, ValueConversionContext> {
enum ConverterEnum implements PropertyValueConverter<String, UUID, ValueConversionContext<SamplePersistentProperty>> {
INSTANCE;
@@ -208,7 +210,8 @@ public class PropertyValueConverterFactoryUnitTests {
}
}
static class ConverterWithDependency implements PropertyValueConverter<String, UUID, ValueConversionContext> {
static class ConverterWithDependency
implements PropertyValueConverter<String, UUID, ValueConversionContext<SamplePersistentProperty>> {
private final SomeDependency someDependency;
@@ -218,7 +221,7 @@ public class PropertyValueConverterFactoryUnitTests {
@Nullable
@Override
public String nativeToDomain(@Nullable UUID nativeValue, ValueConversionContext context) {
public String nativeToDomain(@Nullable UUID nativeValue, ValueConversionContext<SamplePersistentProperty> context) {
assertThat(someDependency).isNotNull();
return nativeValue.toString();
@@ -226,7 +229,7 @@ public class PropertyValueConverterFactoryUnitTests {
@Nullable
@Override
public UUID domainToNative(@Nullable String domainValue, ValueConversionContext context) {
public UUID domainToNative(@Nullable String domainValue, ValueConversionContext<SamplePersistentProperty> context) {
assertThat(someDependency).isNotNull();
return UUID.fromString(domainValue);

View File

@@ -31,11 +31,19 @@
*/
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.convert.PropertyValueConverter.ValueConversionContext;
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;
/**
@@ -47,7 +55,33 @@ public class WhatWeWant {
@Test
void converterConfig() {
ConverterConfig converterConfig = null;
ConverterConfig<SamplePersistentProperty> 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<Object, Object> 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() {
@@ -63,28 +97,185 @@ public class WhatWeWant {
return null;
}
});
}
static class ConverterConfig {
static String reverse(String source) {
return new StringBuilder(source).reverse().toString();
}
ConverterConfig registerConverter(Predicate<PersistentProperty<?>> filter, PropertyValueConverter<?,?, ? extends PropertyValueConverter.ValueConversionContext> converter) {
static class ConverterConfig<P extends PersistentProperty<P>> {
ConverterConfig registerConverter(Predicate<P> filter,
PropertyValueConverter<?, ?, ? extends ValueConversionContext<P>> converter) {
return this;
}
ConverterConfig registerConverter(Class type, String property, PropertyValueConverter<?,?, ? extends PropertyValueConverter.ValueConversionContext> converter) {
ConverterConfig registerConverter(Class type, String property,
PropertyValueConverter<?, ?, ? extends ValueConversionContext<P>> converter) {
PropertyPath.from(property, type);
return this;
}
/**
* Starts a converter registration by pointing to a property of a domain type.
*
* @param <T> the domain type
* @param <S> 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}.
*/
<T, S> WritingConverterRegistrationBuilder<T, S, P> registerConverter(
Class<T> type, Function<T, S> property) {
String propertyName = MethodInvocationRecorder.forProxyOf(type)
.record(property)
.getPropertyPath()
.orElseThrow(() -> new IllegalArgumentException("Cannot obtain property name!"));
return new WritingConverterRegistrationBuilder<T, S, P>(type, propertyName, this);
}
<T, S> WritingConverterRegistrationBuilder<T, S, P> registerConverter(Predicate<P> predicate) {
return new WritingConverterRegistrationBuilder<T, S, P>(predicate, this);
}
/**
* Helper to build up a fluent registration API starting on
* {@link ConverterConfig#registerConverter(Class, Function)}.
*
* @author Oliver Drotbohm
*/
static class WritingConverterRegistrationBuilder<T, S, P extends PersistentProperty<P>> {
private final Consumer<PropertyValueConverter> registration;
private final ConverterConfig config;
public WritingConverterRegistrationBuilder(Class<T> type, String property, ConverterConfig config) {
this.config = config;
this.registration = (converter) -> config.registerConverter(type, property, converter);
}
public WritingConverterRegistrationBuilder(Predicate<P> predicate, ConverterConfig config) {
this.config = config;
this.registration = (converter) -> config.registerConverter(predicate, converter);
}
<R> ReadingConverterRegistrationBuilder<T, S, S, P> writingAsIs() {
return writing((source, context) -> source);
}
<R> ReadingConverterRegistrationBuilder<T, S, R, P> writing(Function<S, R> writer) {
return writing((source, context) -> writer.apply(source));
}
/**
* Describes how to convert the domain property value into the database native property.
*
* @param <R> 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}.
*/
<R> ReadingConverterRegistrationBuilder<T, S, R, P> writing(BiFunction<S, ValueConversionContext<P>, 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<T, S, R, P extends PersistentProperty<P>> {
private WritingConverterRegistrationBuilder<T, S, P> origin;
private BiFunction<S, ValueConversionContext<P>, R> writer;
public ReadingConverterRegistrationBuilder(WritingConverterRegistrationBuilder<T, S, P> origin,
BiFunction<S, ValueConversionContext<P>, R> writer) {
this.origin = origin;
this.writer = writer;
}
ConverterConfig<P> readingAsIs() {
return reading((source, context) -> (S) source);
}
ConverterConfig<P> reading(Function<R, S> 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<P> reading(BiFunction<R, ValueConversionContext<P>, 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<A, B, P extends PersistentProperty<P>>
implements PropertyValueConverter<A, B, ValueConversionContext<P>> {
private final BiFunction<A, ValueConversionContext<P>, B> writer;
private final BiFunction<B, ValueConversionContext<P>, A> reader;
public FunctionPropertyValueConverter(BiFunction<A, ValueConversionContext<P>, B> writer,
BiFunction<B, ValueConversionContext<P>, A> reader) {
this.writer = writer;
this.reader = reader;
}
@Override
public B domainToNative(A domainValue, ValueConversionContext<P> context) {
return writer.apply(domainValue, context);
}
@Override
public A nativeToDomain(B nativeValue, ValueConversionContext<P> context) {
return reader.apply(nativeValue, context);
}
}
}
}
static class Foo {
String value;
Address address;
public String getValue() {
return value;
}
public Address getAddress() {
return address;
}
}
interface SpecificValueConversionContext extends PropertyValueConverter.ValueConversionContext {
static class Address {
String street;
ZipCode zipCode;
}
static class ZipCode {
}
interface SpecificPropertyValueConverter<S,T> extends PropertyValueConverter<S,T,SpecificValueConversionContext> {}
interface SpecificValueConversionContext<P extends PersistentProperty<P>> extends ValueConversionContext<P> {
}
interface SpecificPropertyValueConverter<S, T>
extends PropertyValueConverter<S, T, SpecificValueConversionContext<SamplePersistentProperty>> {}
}

View File

@@ -543,7 +543,8 @@ public class AnnotationBasedPersistentPropertyUnitTests<P extends AnnotationBase
String value2;
}
static class MyPropertyConverter implements PropertyValueConverter<Object,Object, ValueConversionContext> {
static class MyPropertyConverter
implements PropertyValueConverter<Object, Object, ValueConversionContext<SamplePersistentProperty>> {
@Override
public Object nativeToDomain(Object value, ValueConversionContext context) {
@@ -556,7 +557,8 @@ public class AnnotationBasedPersistentPropertyUnitTests<P extends AnnotationBase
}
}
static class MyPropertyConverterThatRequiresComponents implements PropertyValueConverter<Object,Object, ValueConversionContext> {
static class MyPropertyConverterThatRequiresComponents
implements PropertyValueConverter<Object, Object, ValueConversionContext<SamplePersistentProperty>> {
private final SomeDependency someDependency;