Add support for PropertyValueConverters.
Closes: #3596 Original pull request: #3982.
This commit is contained in:
committed by
Mark Paluch
parent
15cac49f9c
commit
29fb085d8b
@@ -176,7 +176,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);
|
||||
}
|
||||
|
||||
@@ -294,7 +294,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);
|
||||
|
||||
@@ -377,11 +377,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);
|
||||
@@ -397,13 +397,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);
|
||||
}
|
||||
}
|
||||
@@ -935,6 +935,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();
|
||||
@@ -1264,6 +1269,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));
|
||||
}
|
||||
@@ -1905,6 +1916,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());
|
||||
}
|
||||
|
||||
@@ -2123,6 +2139,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;
|
||||
@@ -2131,11 +2148,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;
|
||||
@@ -2217,7 +2235,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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -36,8 +36,14 @@ import org.springframework.core.convert.TypeDescriptor;
|
||||
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.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;
|
||||
@@ -149,6 +155,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.
|
||||
@@ -220,6 +228,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.
|
||||
*
|
||||
@@ -248,10 +277,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);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -276,7 +349,7 @@ public class MongoCustomConversions extends org.springframework.data.convert.Cus
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}, this.propertyValueConversions);
|
||||
}
|
||||
|
||||
private enum DateToUtcLocalDateTimeConverter implements Converter<Date, LocalDateTime> {
|
||||
|
||||
@@ -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> {
|
||||
|
||||
}
|
||||
@@ -432,6 +432,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)) {
|
||||
|
||||
@@ -45,10 +45,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;
|
||||
@@ -58,7 +60,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;
|
||||
@@ -2702,6 +2707,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;
|
||||
}
|
||||
@@ -3435,6 +3538,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}")
|
||||
|
||||
@@ -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<Date, ZonedDateTime> {
|
||||
|
||||
@Override
|
||||
|
||||
@@ -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;
|
||||
@@ -1429,6 +1429,13 @@ public class QueryMapperUnitTests {
|
||||
.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 {
|
||||
|
||||
List<WithNestedArray> level0;
|
||||
@@ -1708,6 +1715,12 @@ public class QueryMapperUnitTests {
|
||||
private String street;
|
||||
}
|
||||
|
||||
static class WithPropertyValueConverter {
|
||||
|
||||
@ValueConverter(ReversingValueConverter.class)
|
||||
String text;
|
||||
}
|
||||
|
||||
@WritingConverter
|
||||
public static class MyAddressToDocumentConverter implements Converter<MyAddress, org.bson.Document> {
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,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;
|
||||
@@ -1342,6 +1343,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;
|
||||
}
|
||||
@@ -1753,4 +1763,10 @@ class UpdateMapperUnitTests {
|
||||
private static class TestValue {
|
||||
private int intValue;
|
||||
}
|
||||
|
||||
static class WithPropertyValueConverter {
|
||||
|
||||
@ValueConverter(ReversingValueConverter.class)
|
||||
String text;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
Normal file
108
src/main/asciidoc/reference/mongo-property-converters.adoc
Normal file
@@ -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);
|
||||
});
|
||||
----
|
||||
====
|
||||
Reference in New Issue
Block a user