Browse Source

Add support for `PropertyValueConverters`.

Closes: #3596
Original pull request: #3982.
pull/4006/head
Christoph Strobl 4 years ago committed by Mark Paluch
parent
commit
f58e462fc8
No known key found for this signature in database
GPG Key ID: 4406B84C1661DCD1
  1. 35
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java
  2. 60
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConversionContext.java
  3. 77
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoCustomConversions.java
  4. 28
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoValueConverter.java
  5. 4
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java
  6. 171
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java
  7. 24
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MongoCustomConversionsUnitTests.java
  8. 15
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java
  9. 45
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/ReversingValueConverter.java
  10. 16
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/UpdateMapperUnitTests.java
  11. 1
      src/main/asciidoc/new-features.adoc
  12. 1
      src/main/asciidoc/reference/mapping.adoc
  13. 108
      src/main/asciidoc/reference/mongo-property-converters.adoc

35
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java

@ -38,7 +38,6 @@ import org.bson.codecs.DecoderContext; @@ -38,7 +38,6 @@ import org.bson.codecs.DecoderContext;
import org.bson.conversions.Bson;
import org.bson.json.JsonReader;
import org.bson.types.ObjectId;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.context.ApplicationContext;
@ -186,7 +185,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App @@ -186,7 +185,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
Assert.notNull(path, "ObjectPath must not be null");
return new ConversionContext(conversions, path, this::readDocument, this::readCollectionOrArray, this::readMap,
return new ConversionContext(this, conversions, path, this::readDocument, this::readCollectionOrArray, this::readMap,
this::readDBRef, this::getPotentiallyConvertedSimpleRead);
}
@ -316,7 +315,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App @@ -316,7 +315,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
return (R) read(typeToRead, bson);
}
ProjectingConversionContext context = new ProjectingConversionContext(conversions, ObjectPath.ROOT,
ProjectingConversionContext context = new ProjectingConversionContext(this, conversions, ObjectPath.ROOT,
this::readCollectionOrArray, this::readMap, this::readDBRef, this::getPotentiallyConvertedSimpleRead,
projection);
@ -399,11 +398,11 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App @@ -399,11 +398,11 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
private final EntityProjection<?, ?> returnedTypeDescriptor;
ProjectingConversionContext(CustomConversions customConversions, ObjectPath path,
ProjectingConversionContext(MongoConverter sourceConverter, CustomConversions customConversions, ObjectPath path,
ContainerValueConverter<Collection<?>> collectionConverter, ContainerValueConverter<Bson> mapConverter,
ContainerValueConverter<DBRef> dbRefConverter, ValueConverter<Object> elementConverter,
EntityProjection<?, ?> projection) {
super(customConversions, path,
super(sourceConverter, customConversions, path,
(context, source, typeHint) -> doReadOrProject(context, source, typeHint, projection),
collectionConverter, mapConverter, dbRefConverter, elementConverter);
@ -419,13 +418,13 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App @@ -419,13 +418,13 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
mapConverter, dbRefConverter, elementConverter);
}
return new ProjectingConversionContext(conversions, path, collectionConverter, mapConverter, dbRefConverter,
return new ProjectingConversionContext(sourceConverter, conversions, path, collectionConverter, mapConverter, dbRefConverter,
elementConverter, property);
}
@Override
public ConversionContext withPath(ObjectPath currentPath) {
return new ProjectingConversionContext(conversions, currentPath, collectionConverter, mapConverter,
return new ProjectingConversionContext(sourceConverter, conversions, currentPath, collectionConverter, mapConverter,
dbRefConverter, elementConverter, returnedTypeDescriptor);
}
}
@ -965,6 +964,11 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App @@ -965,6 +964,11 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
TypeInformation<?> valueType = ClassTypeInformation.from(obj.getClass());
TypeInformation<?> type = prop.getTypeInformation();
if(conversions.hasPropertyValueConverter(prop)) {
accessor.put(prop, conversions.getPropertyValueConverter(prop).write(obj, new MongoConversionContext(prop, this)));
return;
}
if (prop.isUnwrapped()) {
Document target = new Document();
@ -1294,6 +1298,12 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App @@ -1294,6 +1298,12 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
private void writeSimpleInternal(@Nullable Object value, Bson bson, MongoPersistentProperty property) {
DocumentAccessor accessor = new DocumentAccessor(bson);
if(conversions.hasPropertyValueConverter(property)) {
accessor.put(property, conversions.getPropertyValueConverter(property).write(value, new MongoConversionContext(property, this)));
return;
}
accessor.put(property, getPotentiallyConvertedSimpleWrite(value,
property.hasExplicitWriteTarget() ? property.getFieldType() : Object.class));
}
@ -1957,6 +1967,11 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App @@ -1957,6 +1967,11 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
return null;
}
if(context.conversions.hasPropertyValueConverter(property)) {
return (T) context.conversions.getPropertyValueConverter(property).read(value, new MongoConversionContext(property, context.sourceConverter));
}
return (T) context.convert(value, property.getTypeInformation());
}
@ -2182,6 +2197,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App @@ -2182,6 +2197,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
*/
protected static class ConversionContext {
final MongoConverter sourceConverter;
final org.springframework.data.convert.CustomConversions conversions;
final ObjectPath path;
final ContainerValueConverter<Bson> documentConverter;
@ -2190,11 +2206,12 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App @@ -2190,11 +2206,12 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
final ContainerValueConverter<DBRef> dbRefConverter;
final ValueConverter<Object> elementConverter;
ConversionContext(org.springframework.data.convert.CustomConversions customConversions, ObjectPath path,
ConversionContext(MongoConverter sourceConverter, org.springframework.data.convert.CustomConversions customConversions, ObjectPath path,
ContainerValueConverter<Bson> documentConverter, ContainerValueConverter<Collection<?>> collectionConverter,
ContainerValueConverter<Bson> mapConverter, ContainerValueConverter<DBRef> dbRefConverter,
ValueConverter<Object> elementConverter) {
this.sourceConverter = sourceConverter;
this.conversions = customConversions;
this.path = path;
this.documentConverter = documentConverter;
@ -2276,7 +2293,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App @@ -2276,7 +2293,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
Assert.notNull(currentPath, "ObjectPath must not be null");
return new ConversionContext(conversions, currentPath, documentConverter, collectionConverter, mapConverter,
return new ConversionContext(sourceConverter, conversions, currentPath, documentConverter, collectionConverter, mapConverter,
dbRefConverter, elementConverter);
}

60
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConversionContext.java

@ -0,0 +1,60 @@ @@ -0,0 +1,60 @@
/*
* 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.mongodb.core.convert;
import org.bson.conversions.Bson;
import org.springframework.data.convert.ValueConversionContext;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
/**
* {@link ValueConversionContext} that allows to delegate read/write to an underlying {@link MongoConverter}.
*
* @author Christoph Strobl
* @since 3.4
*/
public class MongoConversionContext implements ValueConversionContext<MongoPersistentProperty> {
private final MongoPersistentProperty persistentProperty;
private final MongoConverter mongoConverter;
public MongoConversionContext(MongoPersistentProperty persistentProperty, MongoConverter mongoConverter) {
this.persistentProperty = persistentProperty;
this.mongoConverter = mongoConverter;
}
@Override
public MongoPersistentProperty getProperty() {
return persistentProperty;
}
@Override
public <T> T write(@Nullable Object value, TypeInformation<T> target) {
return (T) mongoConverter.convertToMongoType(value, target);
}
@Override
public <T> T read(@Nullable Object value, TypeInformation<T> target) {
if (!(value instanceof Bson)) {
return ValueConversionContext.super.read(value, target);
}
return mongoConverter.read(target.getType(), (Bson) value);
}
}

77
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoCustomConversions.java

@ -37,8 +37,14 @@ import org.springframework.core.convert.converter.Converter; @@ -37,8 +37,14 @@ import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterFactory;
import org.springframework.core.convert.converter.GenericConverter;
import org.springframework.data.convert.JodaTimeConverters;
import org.springframework.data.convert.PropertyValueConversions;
import org.springframework.data.convert.PropertyValueConverter;
import org.springframework.data.convert.PropertyValueConverterFactory;
import org.springframework.data.convert.PropertyValueConverterRegistrar;
import org.springframework.data.convert.SimplePropertyValueConversions;
import org.springframework.data.convert.WritingConverter;
import org.springframework.data.mapping.model.SimpleTypeHolder;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.mongodb.core.mapping.MongoSimpleTypes;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@ -159,6 +165,8 @@ public class MongoCustomConversions extends org.springframework.data.convert.Cus @@ -159,6 +165,8 @@ public class MongoCustomConversions extends org.springframework.data.convert.Cus
private boolean useNativeDriverJavaTimeCodecs = false;
private final List<Object> customConverters = new ArrayList<>();
private PropertyValueConversions propertyValueConversions = new SimplePropertyValueConversions();
/**
* Create a {@link MongoConverterConfigurationAdapter} using the provided {@code converters} and our own codecs for
* JSR-310 types.
@ -230,6 +238,27 @@ public class MongoCustomConversions extends org.springframework.data.convert.Cus @@ -230,6 +238,27 @@ public class MongoCustomConversions extends org.springframework.data.convert.Cus
return this;
}
/**
* Gateway to register property specific converters.
*
* @param configurationAdapter must not be {@literal null}.
* @return this.
* @since 3.4
*/
public MongoConverterConfigurationAdapter configurePropertyConversions(
Consumer<PropertyValueConverterRegistrar<MongoPersistentProperty>> configurationAdapter) {
Assert.state(valueConversions() instanceof SimplePropertyValueConversions,
"Configured PropertyValueConversions does not allow setting custom ConverterRegistry.");
PropertyValueConverterRegistrar propertyValueConverterRegistrar = new PropertyValueConverterRegistrar();
configurationAdapter.accept(propertyValueConverterRegistrar);
((SimplePropertyValueConversions) valueConversions())
.setValueConverterRegistry(propertyValueConverterRegistrar.buildRegistry());
return this;
}
/**
* Add a custom {@link ConverterFactory} implementation.
*
@ -258,10 +287,54 @@ public class MongoCustomConversions extends org.springframework.data.convert.Cus @@ -258,10 +287,54 @@ public class MongoCustomConversions extends org.springframework.data.convert.Cus
return this;
}
/**
* Add a custom/default {@link PropertyValueConverterFactory} implementation used to serve
* {@link PropertyValueConverter}.
*
* @param converterFactory must not be {@literal null}.
* @return this.
* @since 3.4
*/
public MongoConverterConfigurationAdapter registerPropertyValueConverterFactory(
PropertyValueConverterFactory converterFactory) {
Assert.state(valueConversions() instanceof SimplePropertyValueConversions,
"Configured PropertyValueConversions does not allow setting custom ConverterRegistry.");
((SimplePropertyValueConversions) valueConversions()).setConverterFactory(converterFactory);
return this;
}
/**
* Optionally set the {@link PropertyValueConversions} to be applied during mapping.
* <p>
* Use this method if {@link #configurePropertyConversions(Consumer)} and
* {@link #registerPropertyValueConverterFactory(PropertyValueConverterFactory)} are not sufficient.
*
* @param valueConversions must not be {@literal null}.
* @return this.
* @since 3.4
*/
public MongoConverterConfigurationAdapter setPropertyValueConversions(PropertyValueConversions valueConversions) {
this.propertyValueConversions = valueConversions;
return this;
}
PropertyValueConversions valueConversions() {
if (this.propertyValueConversions == null) {
this.propertyValueConversions = new SimplePropertyValueConversions();
}
return this.propertyValueConversions;
}
ConverterConfiguration createConverterConfiguration() {
if (!useNativeDriverJavaTimeCodecs) {
return new ConverterConfiguration(STORE_CONVERSIONS, this.customConverters);
return new ConverterConfiguration(STORE_CONVERSIONS, this.customConverters, convertiblePair -> true,
this.propertyValueConversions);
}
/*
@ -286,7 +359,7 @@ public class MongoCustomConversions extends org.springframework.data.convert.Cus @@ -286,7 +359,7 @@ public class MongoCustomConversions extends org.springframework.data.convert.Cus
}
return true;
});
}, this.propertyValueConversions);
}
private enum DateToUtcLocalDateTimeConverter implements Converter<Date, LocalDateTime> {

28
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoValueConverter.java

@ -0,0 +1,28 @@ @@ -0,0 +1,28 @@
/*
* 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.mongodb.core.convert;
import org.springframework.data.convert.PropertyValueConverter;
/**
* Pre typed {@link PropertyValueConverter} specific for the Data MongoDB module.
*
* @author Christoph Strobl
* @since 3.4
*/
public interface MongoValueConverter<S, T> extends PropertyValueConverter<S, T, MongoConversionContext> {
}

4
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java

@ -434,6 +434,10 @@ public class QueryMapper { @@ -434,6 +434,10 @@ public class QueryMapper {
Object value = applyFieldTargetTypeHintToValue(documentField, sourceValue);
if(documentField.getProperty() != null && converter.getCustomConversions().hasPropertyValueConverter(documentField.getProperty())) {
return converter.getCustomConversions().getPropertyValueConverter(documentField.getProperty()).write(value, new MongoConversionContext(documentField.getProperty(), converter));
}
if (documentField.isIdField() && !documentField.isAssociation()) {
if (isDBObject(value)) {

171
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java

@ -44,10 +44,12 @@ import org.junit.jupiter.api.extension.ExtendWith; @@ -44,10 +44,12 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.beans.ConversionNotSupportedException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.StaticApplicationContext;
import org.springframework.core.convert.ConverterNotFoundException;
@ -57,7 +59,10 @@ import org.springframework.data.annotation.PersistenceConstructor; @@ -57,7 +59,10 @@ import org.springframework.data.annotation.PersistenceConstructor;
import org.springframework.data.annotation.Transient;
import org.springframework.data.annotation.TypeAlias;
import org.springframework.data.convert.CustomConversions;
import org.springframework.data.convert.PropertyValueConverter;
import org.springframework.data.convert.PropertyValueConverterFactory;
import org.springframework.data.convert.ReadingConverter;
import org.springframework.data.convert.ValueConverter;
import org.springframework.data.convert.WritingConverter;
import org.springframework.data.geo.Box;
import org.springframework.data.geo.Circle;
@ -2729,6 +2734,104 @@ class MappingMongoConverterUnitTests { @@ -2729,6 +2734,104 @@ class MappingMongoConverterUnitTests {
assertThat(projection.getName()).isEqualTo("my-book by Walter White");
}
@Test // GH-3596
void simpleConverter() {
WithValueConverters wvc = new WithValueConverters();
wvc.converterWithDefaultCtor = "spring";
org.bson.Document target = new org.bson.Document();
converter.write(wvc, target);
assertThat(target).containsEntry("converterWithDefaultCtor", new org.bson.Document("foo", "spring"));
WithValueConverters read = converter.read(WithValueConverters.class, target);
assertThat(read.converterWithDefaultCtor).startsWith("spring");
}
@Test // GH-3596
void enumConverter() {
WithValueConverters wvc = new WithValueConverters();
wvc.converterEnum = "spring";
org.bson.Document target = new org.bson.Document();
converter.write(wvc, target);
assertThat(target).containsEntry("converterEnum", new org.bson.Document("bar", "spring"));
WithValueConverters read = converter.read(WithValueConverters.class, target);
assertThat(read.converterEnum).isEqualTo("spring");
}
@Test // GH-3596
void beanConverter() {
DefaultListableBeanFactory defaultListableBeanFactory = new DefaultListableBeanFactory();
defaultListableBeanFactory.registerBeanDefinition("someDependency",
BeanDefinitionBuilder.rootBeanDefinition(SomeDependency.class).getBeanDefinition());
converter = new MappingMongoConverter(resolver, mappingContext);
converter.setCustomConversions(MongoCustomConversions.create(it -> {
it.registerPropertyValueConverterFactory(
PropertyValueConverterFactory.beanFactoryAware(defaultListableBeanFactory));
}));
converter.afterPropertiesSet();
WithValueConverters wvc = new WithValueConverters();
wvc.converterBean = "spring";
org.bson.Document target = new org.bson.Document();
converter.write(wvc, target);
assertThat(target.get("converterBean", org.bson.Document.class)).satisfies(it -> {
assertThat(it).containsKey("ooo");
assertThat((String) it.get("ooo")).startsWith("spring - ");
});
WithValueConverters read = converter.read(WithValueConverters.class, target);
assertThat(read.converterBean).startsWith("spring -");
}
@Test // GH-3596
void pathConfiguredConverter/*no annotation required*/() {
converter = new MappingMongoConverter(resolver, mappingContext);
converter.setCustomConversions(MongoCustomConversions.create(it -> {
it.configurePropertyConversions(registrar -> {
registrar.registerConverter(WithValueConverters.class, "viaRegisteredConverter",
new PropertyValueConverter<String, org.bson.Document, MongoConversionContext>() {
@Nullable
@Override
public String read(@Nullable org.bson.Document nativeValue, MongoConversionContext context) {
return nativeValue.getString("bar");
}
@Nullable
@Override
public org.bson.Document write(@Nullable String domainValue, MongoConversionContext context) {
return new org.bson.Document("bar", domainValue);
}
});
});
}));
WithValueConverters wvc = new WithValueConverters();
wvc.viaRegisteredConverter = "spring";
org.bson.Document target = new org.bson.Document();
converter.write(wvc, target);
assertThat(target).containsEntry("viaRegisteredConverter", new org.bson.Document("bar", "spring"));
WithValueConverters read = converter.read(WithValueConverters.class, target);
assertThat(read.viaRegisteredConverter).isEqualTo("spring");
}
static class GenericType<T> {
T content;
}
@ -3462,6 +3565,72 @@ class MappingMongoConverterUnitTests { @@ -3462,6 +3565,72 @@ class MappingMongoConverterUnitTests {
}
static class WithValueConverters {
@ValueConverter(Converter1.class) String converterWithDefaultCtor;
@ValueConverter(Converter2.class) String converterEnum;
@ValueConverter(Converter3.class) String converterBean;
String viaRegisteredConverter;
}
static class Converter3 implements MongoValueConverter<Object, org.bson.Document> {
private final SomeDependency someDependency;
public Converter3(@Autowired SomeDependency someDependency) {
this.someDependency = someDependency;
}
@Override
public Object read(org.bson.Document value, MongoConversionContext context) {
return value.get("ooo");
}
@Override
public org.bson.Document write(Object value, MongoConversionContext context) {
return new org.bson.Document("ooo", value + " - " + someDependency.toString());
}
}
static class SomeDependency {
}
enum Converter2 implements MongoValueConverter<String, org.bson.Document> {
INSTANCE;
@Nullable
@Override
public String read(@Nullable org.bson.Document value, MongoConversionContext context) {
return value.getString("bar");
}
@Nullable
@Override
public org.bson.Document write(@Nullable String value, MongoConversionContext context) {
return new org.bson.Document("bar", value);
}
}
static class Converter1 implements MongoValueConverter<String, org.bson.Document> {
@Nullable
@Override
public String read(@Nullable org.bson.Document value, MongoConversionContext context) {
return value.getString("foo");
}
@Nullable
@Override
public org.bson.Document write(@Nullable String value, MongoConversionContext context) {
return new org.bson.Document("foo", value);
}
}
interface BookProjection {
@Value("#{target.name + ' by ' + target.author.firstName + ' ' + target.author.lastName}")

24
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MongoCustomConversionsUnitTests.java

@ -16,14 +16,18 @@ @@ -16,14 +16,18 @@
package org.springframework.data.mongodb.core.convert;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.time.ZonedDateTime;
import java.util.Collections;
import java.util.Date;
import org.junit.jupiter.api.Test;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.convert.PropertyValueConverter;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mongodb.core.convert.QueryMapperUnitTests.Foo;
/**
* Unit tests for {@link MongoCustomConversions}.
@ -42,6 +46,24 @@ class MongoCustomConversionsUnitTests { @@ -42,6 +46,24 @@ class MongoCustomConversionsUnitTests {
assertThat(conversions.hasCustomWriteTarget(Date.class)).isFalse();
}
@Test // GH-3596
void propertyValueConverterRegistrationWorksAsExpected() {
PersistentProperty<?> persistentProperty = mock(PersistentProperty.class);
PersistentEntity owner = mock(PersistentEntity.class);
when(persistentProperty.getName()).thenReturn("name");
when(persistentProperty.getOwner()).thenReturn(owner);
when(owner.getType()).thenReturn(Foo.class);
MongoCustomConversions conversions = MongoCustomConversions.create(config -> {
config.configurePropertyConversions(
registry -> registry.registerConverter(Foo.class, "name", mock(PropertyValueConverter.class)));
});
assertThat(conversions.hasPropertyValueConverter(persistentProperty)).isTrue();
}
static class DateToZonedDateTimeConverter implements Converter<Date, ZonedDateTime> {
@Override

15
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java

@ -33,10 +33,10 @@ import org.bson.types.Code; @@ -33,10 +33,10 @@ import org.bson.types.Code;
import org.bson.types.ObjectId;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Transient;
import org.springframework.data.convert.ValueConverter;
import org.springframework.data.convert.WritingConverter;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
@ -1428,6 +1428,13 @@ public class QueryMapperUnitTests { @@ -1428,6 +1428,13 @@ public class QueryMapperUnitTests {
assertThat(mappedQuery.get("_id"))
.isEqualTo(org.bson.Document.parse("{ $in: [ {$oid: \"5b8bedceb1e0bfc07b008828\" } ]}"));
}
@Test // GH-3596
void considersValueConverterWhenPresent() {
org.bson.Document mappedObject = mapper.getMappedObject(new org.bson.Document("text", "value"), context.getPersistentEntity(WithPropertyValueConverter.class));
assertThat(mappedObject).isEqualTo(new org.bson.Document("text", "eulav"));
}
class WithDeepArrayNesting {
@ -1707,6 +1714,12 @@ public class QueryMapperUnitTests { @@ -1707,6 +1714,12 @@ public class QueryMapperUnitTests {
static class MyAddress {
private String street;
}
static class WithPropertyValueConverter {
@ValueConverter(ReversingValueConverter.class)
String text;
}
@WritingConverter
public static class MyAddressToDocumentConverter implements Converter<MyAddress, org.bson.Document> {

45
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/ReversingValueConverter.java

@ -0,0 +1,45 @@ @@ -0,0 +1,45 @@
/*
* 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.mongodb.core.convert;
import org.springframework.lang.Nullable;
/**
* @author Christoph Strobl
*/
class ReversingValueConverter implements MongoValueConverter<String, String> {
@Nullable
@Override
public String read(@Nullable String value, MongoConversionContext context) {
return reverse(value);
}
@Nullable
@Override
public String write(@Nullable String value, MongoConversionContext context) {
return reverse(value);
}
private String reverse(String source) {
if (source == null) {
return null;
}
return new StringBuilder(source).reverse().toString();
}
}

16
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/UpdateMapperUnitTests.java

@ -42,6 +42,7 @@ import org.springframework.core.convert.converter.Converter; @@ -42,6 +42,7 @@ import org.springframework.core.convert.converter.Converter;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Transient;
import org.springframework.data.convert.CustomConversions;
import org.springframework.data.convert.ValueConverter;
import org.springframework.data.convert.WritingConverter;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
@ -1341,6 +1342,15 @@ class UpdateMapperUnitTests { @@ -1341,6 +1342,15 @@ class UpdateMapperUnitTests {
assertThat(mappedUpdate).isEqualTo("{ $set: { 'testInnerData.testMap.1.nonExistingProperty.2.someValue': '4' }}");
}
@Test // GH-3596
void updateConsidersValueConverterWhenPresent() {
Update update = new Update().set("text", "value");
Document mappedUpdate = mapper.getMappedObject(update.getUpdateObject(), context.getPersistentEntity(WithPropertyValueConverter.class));
assertThat(mappedUpdate).isEqualTo("{ $set : { 'text' : 'eulav' } }");
}
static class DomainTypeWrappingConcreteyTypeHavingListOfInterfaceTypeAttributes {
ListModelWrapper concreteTypeWithListAttributeOfInterfaceType;
}
@ -1752,4 +1762,10 @@ class UpdateMapperUnitTests { @@ -1752,4 +1762,10 @@ class UpdateMapperUnitTests {
private static class TestValue {
private int intValue;
}
static class WithPropertyValueConverter {
@ValueConverter(ReversingValueConverter.class)
String text;
}
}

1
src/main/asciidoc/new-features.adoc

@ -5,6 +5,7 @@ @@ -5,6 +5,7 @@
== What's New in Spring Data MongoDB 3.4
* Find and update ``Document``s via <<mongodb.repositories.queries.update,Repository method>>.
* Property specific <<mongo.property-converters, value converters>>.
[[new-features.3.3]]
== What's New in Spring Data MongoDB 3.3

1
src/main/asciidoc/reference/mapping.adoc

@ -900,3 +900,4 @@ Declaring these beans in your Spring ApplicationContext causes them to be invoke @@ -900,3 +900,4 @@ Declaring these beans in your Spring ApplicationContext causes them to be invoke
include::unwrapping-entities.adoc[]
include::mongo-custom-conversions.adoc[]
include::mongo-property-converters.adoc[]

108
src/main/asciidoc/reference/mongo-property-converters.adoc

@ -0,0 +1,108 @@ @@ -0,0 +1,108 @@
[[mongo.property-converters]]
== Property Converters - Mapping specific fields
Although to the <<mongo.custom-converters, type based conversion>> already offers means to influence the representation of certain types within the target store it has its limitations when not all potential values of that type should be considered as a conversion targets.
Property based converters allow to specify conversion instructions on a per property basis either declarative, via `@ValueConverter`, or programmatic by registering a `PropertyValueConverter` for a specific field.
A `PropertyValueConverter` is responsible of transforming a given value into its store representation (write) and back (read) as shown in the snippet below.
Please mind the presence of the `ValueConversionContext` providing additional information, such as mapping metadata.
.PropertyValueConverter
====
[source,java]
----
class ReversingValueConverter implements PropertyValueConverter<String, String, ValueConversionContext> {
@Override
public String read(String value, ValueConversionContext context) {
return reverse(value);
}
@Override
public String write(String value, ValueConversionContext context) {
return reverse(value);
}
}
----
====
`PropertyValueConverter` instances can be obtained via `CustomConversions#getPropertyValueConverter(...)` delegating to `PropertyValueConversions` typically using a `PropertyValueConverterFactory` to provide the actual converter.
Depending on the applications needs multiple instances of `PropertyValueConverterFactory` can be chained or decorated (eg. for caching).
By default a caching implementation is used that is capable of serving types with a default constructor or enum values.
A set of predefined factories is available via `PropertyValueConverterFactory`.
To obtain a `PropertyValueConverter` from an `ApplicationContext` make sure to use the `PropertyValueConverterFactory.beanFactoryAware(...)` factory.
Changing the default behavior can be done via the `ConverterConfiguration`.
=== Declarative Value Converter
The most straight forward usage of a `PropertyValueConverter` is via the `@ValueConverter` annotation referring to the target converter type.
.Declarative PropertyValueConverter
====
[source,java]
----
public class Person {
// ...
@ValueConverter(ReversingValueConverter.class)
String ssn;
}
----
====
=== Programmatic Value Converter
Following the programmatic approach does not require to put additional annotations on the domain model but registers `PropertyValueConverter` instances for certain paths in a `PropertyValueConverterRegistrar` as shown below.
.Programmatic PropertyValueConverter
====
[source,java]
----
PropertyValueConverterRegistrar registrar = new PropertyValueConverterRegistrar();
registrar.registerConverter(Address.class, "street", new PropertyValueConverter() { ... }); <1>
// type safe registration
registrar.registerConverter(Person.class, Person::getSsn()) <2>
.writing(value -> encrypt(value))
.reading(value -> decrypt(value));
----
<1> Register a converter for the field identified by its name.
<2> Type safe variant that allows to register a converter and its conversion functions.
====
[WARNING]
====
Dot notation (eg. `registerConverter(Person.class, "address.street", ...)`) is *not* supported when registering converters.
====
=== MongoDB property value conversions
The above sections outlined the purpose an overall structure of `PropertyValueConverters`.
This section will focus on MongoDB specific aspects.
==== MongoValueConverter & MongoConversionContext
The `MongoValueConverter` offers a pre typed `PropertyValueConverter` interface leveraging the `MongoConversionContext`.
==== MongoCustomConversions configuration
`MongoCustomConversions` are by default capable of dealing with declarative value converters depending on the configured `PropertyValueConverterFactory`.
The `MongoConverterConfigurationAdapter` is there to help set up programmatic value conversions or define the `PropertyValueConverterFactory` to be used.
.Configuration Sample
====
[source,java]
----
MongoCustomConversions.create(configurationAdapter -> {
SimplePropertyValueConversions valueConversions = new SimplePropertyValueConversions();
valueConversions.setConverterFactory(...);
valueConversions.setValueConverterRegistry(new PropertyValueConverterRegistrar()
.registerConverter(...)
.buildRegistry());
configurationAdapter.setPropertyValueConversions(valueConversions);
});
----
====
Loading…
Cancel
Save