Skip to content

Commit 053875c

Browse files
mp911dejxblum
authored andcommitted
Introduce PropertyValueConversionService and specific null-conversion methods.
PropertyValueConverter read and write methods are never called with null values. Instead, PropertyValueConverter now defines readNull and writeNull to encapsulate null conversion. PropertyValueConversionService is a facade that encapsulates these details to simplify converter usage. Resolves #2577 Closes #2592
1 parent d4cc426 commit 053875c

File tree

4 files changed

+297
-9
lines changed

4 files changed

+297
-9
lines changed
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/*
2+
* Copyright 2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.convert;
17+
18+
import org.springframework.data.mapping.PersistentProperty;
19+
import org.springframework.lang.Nullable;
20+
import org.springframework.util.Assert;
21+
22+
/**
23+
* Conversion service based on {@link CustomConversions} to convert domain and store values using
24+
* {@link PropertyValueConverter property-specific converters}.
25+
*
26+
* @author Mark Paluch
27+
* @since 2.7
28+
*/
29+
public class PropertyValueConversionService {
30+
31+
private final CustomConversions conversions;
32+
33+
public PropertyValueConversionService(CustomConversions conversions) {
34+
35+
Assert.notNull(conversions, "CustomConversions must not be null");
36+
37+
this.conversions = conversions;
38+
}
39+
40+
/**
41+
* Return {@literal true} there is a converter registered for {@link PersistentProperty}.
42+
* <p>
43+
* If this method returns {@literal true}, it means {@link #read(Object, PersistentProperty, ValueConversionContext)}
44+
* and {@link #write(Object, PersistentProperty, ValueConversionContext)} are capable to invoke conversion.
45+
*
46+
* @param property the underlying property.
47+
* @return {@literal true} there is a converter registered for {@link PersistentProperty}.
48+
*/
49+
public boolean hasConverter(PersistentProperty<?> property) {
50+
return conversions.hasPropertyValueConverter(property);
51+
}
52+
53+
/**
54+
* Convert a value from its store-native representation into its domain-specific type.
55+
*
56+
* @param value the value to convert. Can be {@code null}.
57+
* @param property the underlying property.
58+
* @param context the context object.
59+
* @param <P> property type.
60+
* @param <VCC> value conversion context type.
61+
* @return the value to be used in the domain model. Can be {@code null}.
62+
*/
63+
@Nullable
64+
public <P extends PersistentProperty<P>, VCC extends ValueConversionContext<P>> Object read(@Nullable Object value,
65+
P property, VCC context) {
66+
67+
PropertyValueConverter<Object, Object, ValueConversionContext<P>> converter = getRequiredConverter(property);
68+
69+
if (value == null) {
70+
return converter.readNull(context);
71+
}
72+
73+
return converter.read(value, context);
74+
}
75+
76+
/**
77+
* Convert a value from its domain-specific value into its store-native representation.
78+
*
79+
* @param value the value to convert. Can be {@code null}.
80+
* @param property the underlying property.
81+
* @param context the context object.
82+
* @param <P> property type.
83+
* @param <VCC> value conversion context type.
84+
* @return the value to be written to the data store. Can be {@code null}.
85+
*/
86+
@Nullable
87+
public <P extends PersistentProperty<P>, VCC extends ValueConversionContext<P>> Object write(@Nullable Object value,
88+
P property, VCC context) {
89+
90+
PropertyValueConverter<Object, Object, ValueConversionContext<P>> converter = getRequiredConverter(property);
91+
92+
if (value == null) {
93+
return converter.writeNull(context);
94+
}
95+
96+
return converter.write(value, context);
97+
}
98+
99+
private <P extends PersistentProperty<P>> PropertyValueConverter<Object, Object, ValueConversionContext<P>> getRequiredConverter(
100+
P property) {
101+
102+
PropertyValueConverter<Object, Object, ValueConversionContext<P>> converter = conversions
103+
.getPropertyValueConverter(property);
104+
105+
if (converter == null) {
106+
throw new IllegalArgumentException(String.format("No converter registered for property %s", property));
107+
}
108+
109+
return converter;
110+
}
111+
}

src/main/java/org/springframework/data/convert/PropertyValueConverter.java

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,13 @@
2626
* <p>
2727
* A {@link PropertyValueConverter} is, other than a {@link ReadingConverter} or {@link WritingConverter}, only applied
2828
* to special annotated fields which allows a fine-grained conversion of certain values within a specific context.
29+
* <p>
30+
* Converter methods are called with non-null values only and provide specific hooks for {@code null} value handling.
31+
* {@link #readNull(ValueConversionContext)} and {@link #writeNull(ValueConversionContext)} methods are specifically
32+
* designated to either retain {@code null} values or return a different value to indicate {@code null} values.
2933
*
3034
* @author Christoph Strobl
35+
* @author Mark Paluch
3136
* @param <DV> domain-specific type.
3237
* @param <SV> store-native type.
3338
* @param <C> the store specific {@link ValueConversionContext conversion context}.
@@ -39,23 +44,47 @@ public interface PropertyValueConverter<DV, SV, C extends ValueConversionContext
3944
* Convert the given store specific value into it's domain value representation. Typically, a {@literal read}
4045
* operation.
4146
*
42-
* @param value can be {@literal null}.
47+
* @param value the value to read.
48+
* @param context never {@literal null}.
49+
* @return the converted value. Can be {@literal null}.
50+
*/
51+
@Nullable
52+
DV read(SV value, C context);
53+
54+
/**
55+
* Convert the given {@code null} value from the store into it's domain value representation. Typically, a
56+
* {@literal read} operation. Returns {@code null} by default.
57+
*
4358
* @param context never {@literal null}.
4459
* @return the converted value. Can be {@literal null}.
4560
*/
4661
@Nullable
47-
DV read(@Nullable SV value, C context);
62+
default DV readNull(C context) {
63+
return null;
64+
}
4865

4966
/**
5067
* Convert the given domain-specific value into it's native store representation. Typically, a {@literal write}
5168
* operation.
5269
*
53-
* @param value can be {@literal null}.
70+
* @param value the value to write.
5471
* @param context never {@literal null}.
5572
* @return the converted value. Can be {@literal null}.
5673
*/
5774
@Nullable
58-
SV write(@Nullable DV value, C context);
75+
SV write(DV value, C context);
76+
77+
/**
78+
* Convert the given {@code null} value from the domain model into it's native store representation. Typically, a
79+
* {@literal write} operation. Returns {@code null} by default.
80+
*
81+
* @param context never {@literal null}.
82+
* @return the converted value. Can be {@literal null}.
83+
*/
84+
@Nullable
85+
default SV writeNull(C context) {
86+
return null;
87+
}
5988

6089
/**
6190
* No-op {@link PropertyValueConverter} implementation.
@@ -100,14 +129,24 @@ public FunctionPropertyValueConverter(BiFunction<DV, ValueConversionContext<P>,
100129

101130
@Nullable
102131
@Override
103-
public SV write(@Nullable DV value, ValueConversionContext<P> context) {
132+
public SV write(DV value, ValueConversionContext<P> context) {
104133
return writer.apply(value, context);
105134
}
106135

136+
@Override
137+
public SV writeNull(ValueConversionContext<P> context) {
138+
return writer.apply(null, context);
139+
}
140+
107141
@Nullable
108142
@Override
109143
public DV read(@Nullable SV value, ValueConversionContext<P> context) {
110144
return reader.apply(value, context);
111145
}
146+
147+
@Override
148+
public DV readNull(ValueConversionContext<P> context) {
149+
return reader.apply(null, context);
150+
}
112151
}
113152
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
/*
2+
* Copyright 2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.convert;
17+
18+
import static org.assertj.core.api.Assertions.*;
19+
20+
import java.util.Collections;
21+
22+
import org.junit.jupiter.api.Test;
23+
24+
import org.springframework.data.mapping.Person;
25+
import org.springframework.data.mapping.context.SampleMappingContext;
26+
import org.springframework.data.mapping.context.SamplePersistentProperty;
27+
import org.springframework.data.mapping.model.BasicPersistentEntity;
28+
import org.springframework.data.util.Predicates;
29+
30+
/**
31+
* Unit tests for {@link PropertyValueConversionService}.
32+
*
33+
* @author Mark Paluch
34+
*/
35+
class PropertyValueConversionServiceUnitTests {
36+
37+
SampleMappingContext mappingContext = new SampleMappingContext();
38+
39+
PropertyValueConversions conversions = PropertyValueConversions.simple(it -> {
40+
it.registerConverter(Person.class, "firstName", String.class).writing(w -> "Writing " + w)
41+
.reading(r -> "Reading " + r);
42+
});
43+
PropertyValueConversionService service = createConversionService(conversions);
44+
45+
@Test // GH-2557
46+
void shouldReportConverter() {
47+
48+
BasicPersistentEntity<Object, SamplePersistentProperty> entity = mappingContext
49+
.getRequiredPersistentEntity(Person.class);
50+
51+
assertThat(service.hasConverter(entity.getRequiredPersistentProperty("firstName"))).isTrue();
52+
assertThat(service.hasConverter(entity.getRequiredPersistentProperty("lastName"))).isFalse();
53+
}
54+
55+
@Test // GH-2557
56+
void conversionWithoutConverterShouldFail() {
57+
58+
BasicPersistentEntity<Object, SamplePersistentProperty> entity = mappingContext
59+
.getRequiredPersistentEntity(Person.class);
60+
61+
SamplePersistentProperty property = entity.getRequiredPersistentProperty("lastName");
62+
assertThatIllegalArgumentException().isThrownBy(() -> service.read("foo", property, () -> property));
63+
assertThatIllegalArgumentException().isThrownBy(() -> service.write("foo", property, () -> property));
64+
}
65+
66+
@Test // GH-2557
67+
void readShouldUseReadConverter() {
68+
69+
BasicPersistentEntity<Object, SamplePersistentProperty> entity = mappingContext
70+
.getRequiredPersistentEntity(Person.class);
71+
72+
SamplePersistentProperty property = entity.getRequiredPersistentProperty("firstName");
73+
assertThat(service.read("Walter", property, () -> property)).isEqualTo("Reading Walter");
74+
assertThat(service.read(null, property, () -> property)).isEqualTo("Reading null");
75+
}
76+
77+
@Test // GH-2557
78+
void readShouldUseWriteConverter() {
79+
80+
BasicPersistentEntity<Object, SamplePersistentProperty> entity = mappingContext
81+
.getRequiredPersistentEntity(Person.class);
82+
83+
SamplePersistentProperty property = entity.getRequiredPersistentProperty("firstName");
84+
assertThat(service.write("Walter", property, () -> property)).isEqualTo("Writing Walter");
85+
assertThat(service.write(null, property, () -> property)).isEqualTo("Writing null");
86+
}
87+
88+
@Test // GH-2557
89+
void readShouldUseNullConvertersConverter() {
90+
91+
PropertyValueConversions conversions = PropertyValueConversions.simple(it -> {
92+
it.registerConverter(Person.class, "firstName", WithNullConverters.INSTANCE);
93+
});
94+
95+
PropertyValueConversionService service = createConversionService(conversions);
96+
97+
BasicPersistentEntity<Object, SamplePersistentProperty> entity = mappingContext
98+
.getRequiredPersistentEntity(Person.class);
99+
100+
SamplePersistentProperty property = entity.getRequiredPersistentProperty("firstName");
101+
102+
assertThat(service.read(null, property, () -> property)).isEqualTo("readNull");
103+
assertThat(service.write(null, property, () -> property)).isEqualTo("writeNull");
104+
}
105+
106+
private static PropertyValueConversionService createConversionService(PropertyValueConversions conversions) {
107+
108+
CustomConversions.ConverterConfiguration configuration = new CustomConversions.ConverterConfiguration(
109+
CustomConversions.StoreConversions.NONE, Collections.emptyList(), Predicates.isTrue(), conversions);
110+
111+
return new PropertyValueConversionService(new CustomConversions(configuration));
112+
}
113+
114+
enum WithNullConverters implements PropertyValueConverter<String, String, ValueConversionContext<?>> {
115+
INSTANCE;
116+
117+
@Override
118+
public String read(String value, ValueConversionContext<?> context) {
119+
return value;
120+
}
121+
122+
@Override
123+
public String readNull(ValueConversionContext<?> context) {
124+
return "readNull";
125+
}
126+
127+
@Override
128+
public String write(String value, ValueConversionContext<?> context) {
129+
return value;
130+
}
131+
132+
@Override
133+
public String writeNull(ValueConversionContext<?> context) {
134+
return "writeNull";
135+
}
136+
}
137+
138+
}

src/test/java/org/springframework/data/convert/PropertyValueConverterFactoryUnitTests.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -226,13 +226,13 @@ enum ConverterEnum implements PropertyValueConverter<String, UUID, ValueConversi
226226

227227
@Nullable
228228
@Override
229-
public String read(@Nullable UUID value, ValueConversionContext context) {
229+
public String read(UUID value, ValueConversionContext context) {
230230
return value.toString();
231231
}
232232

233233
@Nullable
234234
@Override
235-
public UUID write(@Nullable String value, ValueConversionContext context) {
235+
public UUID write(String value, ValueConversionContext context) {
236236
return UUID.fromString(value);
237237
}
238238
}
@@ -248,15 +248,15 @@ public ConverterWithDependency(@Autowired SomeDependency someDependency) {
248248

249249
@Nullable
250250
@Override
251-
public String read(@Nullable UUID value, ValueConversionContext<SamplePersistentProperty> context) {
251+
public String read(UUID value, ValueConversionContext<SamplePersistentProperty> context) {
252252

253253
assertThat(someDependency).isNotNull();
254254
return value.toString();
255255
}
256256

257257
@Nullable
258258
@Override
259-
public UUID write(@Nullable String value, ValueConversionContext<SamplePersistentProperty> context) {
259+
public UUID write(String value, ValueConversionContext<SamplePersistentProperty> context) {
260260

261261
assertThat(someDependency).isNotNull();
262262
return UUID.fromString(value);

0 commit comments

Comments
 (0)