diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java index 510e0f3df..03583704a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java @@ -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 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 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 private final EntityProjection returnedTypeDescriptor; - ProjectingConversionContext(CustomConversions customConversions, ObjectPath path, + ProjectingConversionContext(MongoConverter sourceConverter, CustomConversions customConversions, ObjectPath path, ContainerValueConverter> collectionConverter, ContainerValueConverter mapConverter, ContainerValueConverter dbRefConverter, ValueConverter 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 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 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 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 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 */ protected static class ConversionContext { + final MongoConverter sourceConverter; final org.springframework.data.convert.CustomConversions conversions; final ObjectPath path; final ContainerValueConverter documentConverter; @@ -2190,11 +2206,12 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App final ContainerValueConverter dbRefConverter; final ValueConverter elementConverter; - ConversionContext(org.springframework.data.convert.CustomConversions customConversions, ObjectPath path, + ConversionContext(MongoConverter sourceConverter, org.springframework.data.convert.CustomConversions customConversions, ObjectPath path, ContainerValueConverter documentConverter, ContainerValueConverter> collectionConverter, ContainerValueConverter mapConverter, ContainerValueConverter dbRefConverter, ValueConverter elementConverter) { + this.sourceConverter = sourceConverter; this.conversions = customConversions; this.path = path; this.documentConverter = documentConverter; @@ -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); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConversionContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConversionContext.java new file mode 100644 index 000000000..9a83832d4 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConversionContext.java @@ -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 { + + 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 write(@Nullable Object value, TypeInformation target) { + return (T) mongoConverter.convertToMongoType(value, target); + } + + @Override + public T read(@Nullable Object value, TypeInformation target) { + + if (!(value instanceof Bson)) { + return ValueConversionContext.super.read(value, target); + } + + return mongoConverter.read(target.getType(), (Bson) value); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoCustomConversions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoCustomConversions.java index 394d71984..52927d21d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoCustomConversions.java +++ b/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; 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 private boolean useNativeDriverJavaTimeCodecs = false; private final List 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 return this; } + /** + * Gateway to register property specific converters. + * + * @param configurationAdapter must not be {@literal null}. + * @return this. + * @since 3.4 + */ + public MongoConverterConfigurationAdapter configurePropertyConversions( + Consumer> 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 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. + *

+ * 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 } return true; - }); + }, this.propertyValueConversions); } private enum DateToUtcLocalDateTimeConverter implements Converter { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoValueConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoValueConverter.java new file mode 100644 index 000000000..49b9021cc --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoValueConverter.java @@ -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 extends PropertyValueConverter { + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java index ac1794b87..174667435 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java @@ -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)) { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java index fb4d17166..6852cb2cc 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java +++ b/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; 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; 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 { 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() { + + @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 content; } @@ -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 { + + 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 { + + 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 { + + @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}") diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MongoCustomConversionsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MongoCustomConversionsUnitTests.java index 1509a8df7..493eabfdf 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MongoCustomConversionsUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MongoCustomConversionsUnitTests.java @@ -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 { 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 { @Override diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java index b2de94135..d3b73379b 100755 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java @@ -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 { 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 { static class MyAddress { private String street; } + + static class WithPropertyValueConverter { + + @ValueConverter(ReversingValueConverter.class) + String text; + } @WritingConverter public static class MyAddressToDocumentConverter implements Converter { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/ReversingValueConverter.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/ReversingValueConverter.java new file mode 100644 index 000000000..9a90acf63 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/ReversingValueConverter.java @@ -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 { + + @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(); + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/UpdateMapperUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/UpdateMapperUnitTests.java index 3e744f675..5cc3276d8 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/UpdateMapperUnitTests.java +++ b/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; 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 { 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 { private static class TestValue { private int intValue; } + + static class WithPropertyValueConverter { + + @ValueConverter(ReversingValueConverter.class) + String text; + } } diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc index 91b4708b0..e80f2ba29 100644 --- a/src/main/asciidoc/new-features.adoc +++ b/src/main/asciidoc/new-features.adoc @@ -5,6 +5,7 @@ == What's New in Spring Data MongoDB 3.4 * Find and update ``Document``s via <>. +* Property specific <>. [[new-features.3.3]] == What's New in Spring Data MongoDB 3.3 diff --git a/src/main/asciidoc/reference/mapping.adoc b/src/main/asciidoc/reference/mapping.adoc index ab074ba57..c94c6e64d 100644 --- a/src/main/asciidoc/reference/mapping.adoc +++ b/src/main/asciidoc/reference/mapping.adoc @@ -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[] diff --git a/src/main/asciidoc/reference/mongo-property-converters.adoc b/src/main/asciidoc/reference/mongo-property-converters.adoc new file mode 100644 index 000000000..66b0e1374 --- /dev/null +++ b/src/main/asciidoc/reference/mongo-property-converters.adoc @@ -0,0 +1,108 @@ +[[mongo.property-converters]] +== Property Converters - Mapping specific fields + +Although to the <> 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 { + + @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); +}); +---- +====