diff --git a/src/main/java/org/springframework/data/convert/PropertyValueConversionService.java b/src/main/java/org/springframework/data/convert/PropertyValueConversionService.java new file mode 100644 index 000000000..4df6bdeda --- /dev/null +++ b/src/main/java/org/springframework/data/convert/PropertyValueConversionService.java @@ -0,0 +1,111 @@ +/* + * 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; +import org.springframework.util.Assert; + +/** + * Conversion service based on {@link CustomConversions} to convert domain and store values using + * {@link PropertyValueConverter property-specific converters}. + * + * @author Mark Paluch + * @since 2.7 + */ +public class PropertyValueConversionService { + + private final CustomConversions conversions; + + public PropertyValueConversionService(CustomConversions conversions) { + + Assert.notNull(conversions, "CustomConversions must not be null"); + + this.conversions = conversions; + } + + /** + * Return {@literal true} there is a converter registered for {@link PersistentProperty}. + *

+ * If this method returns {@literal true}, it means {@link #read(Object, PersistentProperty, ValueConversionContext)} + * and {@link #write(Object, PersistentProperty, ValueConversionContext)} are capable to invoke conversion. + * + * @param property the underlying property. + * @return {@literal true} there is a converter registered for {@link PersistentProperty}. + */ + public boolean hasConverter(PersistentProperty property) { + return conversions.hasPropertyValueConverter(property); + } + + /** + * Convert a value from its store-native representation into its domain-specific type. + * + * @param value the value to convert. Can be {@code null}. + * @param property the underlying property. + * @param context the context object. + * @param

property type. + * @param value conversion context type. + * @return the value to be used in the domain model. Can be {@code null}. + */ + @Nullable + public

, VCC extends ValueConversionContext

> Object read(@Nullable Object value, + P property, VCC context) { + + PropertyValueConverter> converter = getRequiredConverter(property); + + if (value == null) { + return converter.readNull(context); + } + + return converter.read(value, context); + } + + /** + * Convert a value from its domain-specific value into its store-native representation. + * + * @param value the value to convert. Can be {@code null}. + * @param property the underlying property. + * @param context the context object. + * @param

property type. + * @param value conversion context type. + * @return the value to be written to the data store. Can be {@code null}. + */ + @Nullable + public

, VCC extends ValueConversionContext

> Object write(@Nullable Object value, + P property, VCC context) { + + PropertyValueConverter> converter = getRequiredConverter(property); + + if (value == null) { + return converter.writeNull(context); + } + + return converter.write(value, context); + } + + private

> PropertyValueConverter> getRequiredConverter( + P property) { + + PropertyValueConverter> converter = conversions + .getPropertyValueConverter(property); + + if (converter == null) { + throw new IllegalArgumentException(String.format("No converter registered for property %s", property)); + } + + return converter; + } +} diff --git a/src/main/java/org/springframework/data/convert/PropertyValueConverter.java b/src/main/java/org/springframework/data/convert/PropertyValueConverter.java index f89aba108..b58532d7b 100644 --- a/src/main/java/org/springframework/data/convert/PropertyValueConverter.java +++ b/src/main/java/org/springframework/data/convert/PropertyValueConverter.java @@ -26,8 +26,13 @@ import org.springframework.lang.Nullable; *

* 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. + *

+ * Converter methods are called with non-null values only and provide specific hooks for {@code null} value handling. + * {@link #readNull(ValueConversionContext)} and {@link #writeNull(ValueConversionContext)} methods are specifically + * designated to either retain {@code null} values or return a different value to indicate {@code null} values. * * @author Christoph Strobl + * @author Mark Paluch * @param domain-specific type. * @param store-native type. * @param the store specific {@link ValueConversionContext conversion context}. @@ -39,23 +44,47 @@ public interface PropertyValueConverter context) { + public SV write(DV value, ValueConversionContext

context) { return writer.apply(value, context); } + @Override + public SV writeNull(ValueConversionContext

context) { + return writer.apply(null, context); + } + @Nullable @Override public DV read(@Nullable SV value, ValueConversionContext

context) { return reader.apply(value, context); } + + @Override + public DV readNull(ValueConversionContext

context) { + return reader.apply(null, context); + } } } diff --git a/src/test/benchmark/org/springframework/data/convert/PropertyValueConversionServiceUnitTests.java b/src/test/benchmark/org/springframework/data/convert/PropertyValueConversionServiceUnitTests.java new file mode 100644 index 000000000..cab3f8f20 --- /dev/null +++ b/src/test/benchmark/org/springframework/data/convert/PropertyValueConversionServiceUnitTests.java @@ -0,0 +1,138 @@ +/* + * 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 java.util.Collections; + +import org.junit.jupiter.api.Test; + +import org.springframework.data.mapping.Person; +import org.springframework.data.mapping.context.SampleMappingContext; +import org.springframework.data.mapping.context.SamplePersistentProperty; +import org.springframework.data.mapping.model.BasicPersistentEntity; +import org.springframework.data.util.Predicates; + +/** + * Unit tests for {@link PropertyValueConversionService}. + * + * @author Mark Paluch + */ +class PropertyValueConversionServiceUnitTests { + + SampleMappingContext mappingContext = new SampleMappingContext(); + + PropertyValueConversions conversions = PropertyValueConversions.simple(it -> { + it.registerConverter(Person.class, "firstName", String.class).writing(w -> "Writing " + w) + .reading(r -> "Reading " + r); + }); + PropertyValueConversionService service = createConversionService(conversions); + + @Test // GH-2557 + void shouldReportConverter() { + + BasicPersistentEntity entity = mappingContext + .getRequiredPersistentEntity(Person.class); + + assertThat(service.hasConverter(entity.getRequiredPersistentProperty("firstName"))).isTrue(); + assertThat(service.hasConverter(entity.getRequiredPersistentProperty("lastName"))).isFalse(); + } + + @Test // GH-2557 + void conversionWithoutConverterShouldFail() { + + BasicPersistentEntity entity = mappingContext + .getRequiredPersistentEntity(Person.class); + + SamplePersistentProperty property = entity.getRequiredPersistentProperty("lastName"); + assertThatIllegalArgumentException().isThrownBy(() -> service.read("foo", property, () -> property)); + assertThatIllegalArgumentException().isThrownBy(() -> service.write("foo", property, () -> property)); + } + + @Test // GH-2557 + void readShouldUseReadConverter() { + + BasicPersistentEntity entity = mappingContext + .getRequiredPersistentEntity(Person.class); + + SamplePersistentProperty property = entity.getRequiredPersistentProperty("firstName"); + assertThat(service.read("Walter", property, () -> property)).isEqualTo("Reading Walter"); + assertThat(service.read(null, property, () -> property)).isEqualTo("Reading null"); + } + + @Test // GH-2557 + void readShouldUseWriteConverter() { + + BasicPersistentEntity entity = mappingContext + .getRequiredPersistentEntity(Person.class); + + SamplePersistentProperty property = entity.getRequiredPersistentProperty("firstName"); + assertThat(service.write("Walter", property, () -> property)).isEqualTo("Writing Walter"); + assertThat(service.write(null, property, () -> property)).isEqualTo("Writing null"); + } + + @Test // GH-2557 + void readShouldUseNullConvertersConverter() { + + PropertyValueConversions conversions = PropertyValueConversions.simple(it -> { + it.registerConverter(Person.class, "firstName", WithNullConverters.INSTANCE); + }); + + PropertyValueConversionService service = createConversionService(conversions); + + BasicPersistentEntity entity = mappingContext + .getRequiredPersistentEntity(Person.class); + + SamplePersistentProperty property = entity.getRequiredPersistentProperty("firstName"); + + assertThat(service.read(null, property, () -> property)).isEqualTo("readNull"); + assertThat(service.write(null, property, () -> property)).isEqualTo("writeNull"); + } + + private static PropertyValueConversionService createConversionService(PropertyValueConversions conversions) { + + CustomConversions.ConverterConfiguration configuration = new CustomConversions.ConverterConfiguration( + CustomConversions.StoreConversions.NONE, Collections.emptyList(), Predicates.isTrue(), conversions); + + return new PropertyValueConversionService(new CustomConversions(configuration)); + } + + enum WithNullConverters implements PropertyValueConverter> { + INSTANCE; + + @Override + public String read(String value, ValueConversionContext context) { + return value; + } + + @Override + public String readNull(ValueConversionContext context) { + return "readNull"; + } + + @Override + public String write(String value, ValueConversionContext context) { + return value; + } + + @Override + public String writeNull(ValueConversionContext context) { + return "writeNull"; + } + } + +} diff --git a/src/test/java/org/springframework/data/convert/PropertyValueConverterFactoryUnitTests.java b/src/test/java/org/springframework/data/convert/PropertyValueConverterFactoryUnitTests.java index 89961a326..c8883157e 100644 --- a/src/test/java/org/springframework/data/convert/PropertyValueConverterFactoryUnitTests.java +++ b/src/test/java/org/springframework/data/convert/PropertyValueConverterFactoryUnitTests.java @@ -226,13 +226,13 @@ public class PropertyValueConverterFactoryUnitTests { @Nullable @Override - public String read(@Nullable UUID value, ValueConversionContext context) { + public String read(UUID value, ValueConversionContext context) { return value.toString(); } @Nullable @Override - public UUID write(@Nullable String value, ValueConversionContext context) { + public UUID write(String value, ValueConversionContext context) { return UUID.fromString(value); } } @@ -248,7 +248,7 @@ public class PropertyValueConverterFactoryUnitTests { @Nullable @Override - public String read(@Nullable UUID value, ValueConversionContext context) { + public String read(UUID value, ValueConversionContext context) { assertThat(someDependency).isNotNull(); return value.toString(); @@ -256,7 +256,7 @@ public class PropertyValueConverterFactoryUnitTests { @Nullable @Override - public UUID write(@Nullable String value, ValueConversionContext context) { + public UUID write(String value, ValueConversionContext context) { assertThat(someDependency).isNotNull(); return UUID.fromString(value);