Browse Source
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 #2592pull/2627/head
4 changed files with 297 additions and 9 deletions
@ -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}. |
||||||
|
* <p> |
||||||
|
* 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 <P> property type. |
||||||
|
* @param <VCC> value conversion context type. |
||||||
|
* @return the value to be used in the domain model. Can be {@code null}. |
||||||
|
*/ |
||||||
|
@Nullable |
||||||
|
public <P extends PersistentProperty<P>, VCC extends ValueConversionContext<P>> Object read(@Nullable Object value, |
||||||
|
P property, VCC context) { |
||||||
|
|
||||||
|
PropertyValueConverter<Object, Object, ValueConversionContext<P>> 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 <P> property type. |
||||||
|
* @param <VCC> value conversion context type. |
||||||
|
* @return the value to be written to the data store. Can be {@code null}. |
||||||
|
*/ |
||||||
|
@Nullable |
||||||
|
public <P extends PersistentProperty<P>, VCC extends ValueConversionContext<P>> Object write(@Nullable Object value, |
||||||
|
P property, VCC context) { |
||||||
|
|
||||||
|
PropertyValueConverter<Object, Object, ValueConversionContext<P>> converter = getRequiredConverter(property); |
||||||
|
|
||||||
|
if (value == null) { |
||||||
|
return converter.writeNull(context); |
||||||
|
} |
||||||
|
|
||||||
|
return converter.write(value, context); |
||||||
|
} |
||||||
|
|
||||||
|
private <P extends PersistentProperty<P>> PropertyValueConverter<Object, Object, ValueConversionContext<P>> getRequiredConverter( |
||||||
|
P property) { |
||||||
|
|
||||||
|
PropertyValueConverter<Object, Object, ValueConversionContext<P>> converter = conversions |
||||||
|
.getPropertyValueConverter(property); |
||||||
|
|
||||||
|
if (converter == null) { |
||||||
|
throw new IllegalArgumentException(String.format("No converter registered for property %s", property)); |
||||||
|
} |
||||||
|
|
||||||
|
return converter; |
||||||
|
} |
||||||
|
} |
||||||
@ -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<Object, SamplePersistentProperty> 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<Object, SamplePersistentProperty> 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<Object, SamplePersistentProperty> 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<Object, SamplePersistentProperty> 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<Object, SamplePersistentProperty> 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<String, String, ValueConversionContext<?>> { |
||||||
|
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"; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
Loading…
Reference in new issue