Fix non-association mapping when id value matches already resolved instance of same type.

This commit ensures to fully resolve non association values from the given source document instead of trying attempt a by id lookup in already resolved instances.

Closes: #4098
Original pull request: #4133.
This commit is contained in:
Christoph Strobl
2022-08-03 12:19:17 +02:00
committed by Mark Paluch
parent e1e8684705
commit 48aabfbf56
2 changed files with 132 additions and 32 deletions

View File

@@ -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;
@@ -187,7 +186,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
Assert.notNull(path, "ObjectPath must not be null");
return new ConversionContext(this, conversions, path, this::readDocument, this::readCollectionOrArray,
return new DefaultConversionContext(this, conversions, path, this::readDocument, this::readCollectionOrArray,
this::readMap, this::readDBRef, this::getPotentiallyConvertedSimpleRead);
}
@@ -396,7 +395,46 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
return readDocument(context, source, typeHint);
}
class ProjectingConversionContext extends ConversionContext {
static class AssociationConversionContext implements ConversionContext {
private final ConversionContext delegate;
public AssociationConversionContext(ConversionContext delegate) {
this.delegate = delegate;
}
@Override
public <S> S convert(Object source, TypeInformation<? extends S> typeHint, ConversionContext context) {
return delegate.convert(source, typeHint, context);
}
@Override
public ConversionContext withPath(ObjectPath currentPath) {
return new AssociationConversionContext(delegate.withPath(currentPath));
}
@Override
public ObjectPath getPath() {
return delegate.getPath();
}
@Override
public CustomConversions getCustomConversions() {
return delegate.getCustomConversions();
}
@Override
public MongoConverter getSourceConverter() {
return delegate.getSourceConverter();
}
@Override
public boolean resolveIdsInContext() {
return true;
}
}
class ProjectingConversionContext extends DefaultConversionContext {
private final EntityProjection<?, ?> returnedTypeDescriptor;
@@ -412,12 +450,13 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
}
@Override
public ConversionContext forProperty(String name) {
public DefaultConversionContext forProperty(String name) {
EntityProjection<?, ?> property = returnedTypeDescriptor.findProperty(name);
if (property == null) {
return new ConversionContext(sourceConverter, conversions, path, MappingMongoConverter.this::readDocument,
collectionConverter, mapConverter, dbRefConverter, elementConverter);
return new DefaultConversionContext(sourceConverter, conversions, path,
MappingMongoConverter.this::readDocument, collectionConverter, mapConverter, dbRefConverter,
elementConverter);
}
return new ProjectingConversionContext(sourceConverter, conversions, path, collectionConverter, mapConverter,
@@ -425,7 +464,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
}
@Override
public ConversionContext withPath(ObjectPath currentPath) {
public DefaultConversionContext withPath(ObjectPath currentPath) {
return new ProjectingConversionContext(sourceConverter, conversions, currentPath, collectionConverter,
mapConverter, dbRefConverter, elementConverter, returnedTypeDescriptor);
}
@@ -544,7 +583,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
SpELExpressionEvaluator evaluator = new DefaultSpELExpressionEvaluator(bson, spELContext);
DocumentAccessor documentAccessor = new DocumentAccessor(bson);
if (hasIdentifier(bson)) {
if (context.resolveIdsInContext() && hasIdentifier(bson)) {
S existing = findContextualEntity(context, entity, bson);
if (existing != null) {
return existing;
@@ -647,7 +686,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
continue;
}
ConversionContext propertyContext = context.forProperty(prop.getName());
ConversionContext propertyContext = context.forProperty(prop);
MongoDbPropertyValueProvider valueProviderToUse = valueProvider.withContext(propertyContext);
if (prop.isAssociation() && !entity.isConstructorArgument(prop)) {
@@ -721,7 +760,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
accessor.setProperty(property,
dbRefResolver.resolveReference(property,
new DocumentReferenceSource(documentAccessor.getDocument(), documentAccessor.get(property)),
referenceLookupDelegate, context::convert));
referenceLookupDelegate, context.forProperty(property)::convert));
}
return;
}
@@ -1971,13 +2010,13 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
return null;
}
CustomConversions conversions = context.conversions;
CustomConversions conversions = context.getCustomConversions();
if (conversions.getPropertyValueConversions().hasValueConverter(property)) {
return (T) conversions.getPropertyValueConversions().getValueConverter(property).read(value,
new MongoConversionContext(property, context.sourceConverter));
new MongoConversionContext(property, context.getSourceConverter()));
}
ConversionContext contextToUse = context.forProperty(property.getName());
ConversionContext contextToUse = context.forProperty(property);
return (T) contextToUse.convert(value, property.getTypeInformation());
}
@@ -2201,13 +2240,49 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
}
}
interface ConversionContext {
default <S extends Object> S convert(Object source, TypeInformation<? extends S> typeHint) {
return convert(source, typeHint, this);
}
<S extends Object> S convert(Object source, TypeInformation<? extends S> typeHint, ConversionContext context);
ConversionContext withPath(ObjectPath currentPath);
ObjectPath getPath();
default ConversionContext forProperty(String name) {
return this;
}
default ConversionContext forProperty(@Nullable PersistentProperty property) {
if (property != null) {
if (property.isAssociation()) {
return new AssociationConversionContext(forProperty(property.getName()));
}
return forProperty(property.getName());
}
return this;
}
default boolean resolveIdsInContext() {
return false;
}
CustomConversions getCustomConversions();
MongoConverter getSourceConverter();
}
/**
* Conversion context holding references to simple {@link ValueConverter} and {@link ContainerValueConverter}.
* Entrypoint for recursive conversion of {@link Document} and other types.
*
* @since 3.2
*/
protected static class ConversionContext {
protected static class DefaultConversionContext implements ConversionContext {
final MongoConverter sourceConverter;
final org.springframework.data.convert.CustomConversions conversions;
@@ -2218,7 +2293,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
final ContainerValueConverter<DBRef> dbRefConverter;
final ValueConverter<Object> elementConverter;
ConversionContext(MongoConverter sourceConverter,
DefaultConversionContext(MongoConverter sourceConverter,
org.springframework.data.convert.CustomConversions customConversions, ObjectPath path,
ContainerValueConverter<Bson> documentConverter, ContainerValueConverter<Collection<?>> collectionConverter,
ContainerValueConverter<Bson> mapConverter, ContainerValueConverter<DBRef> dbRefConverter,
@@ -2242,7 +2317,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
* @return the converted object.
*/
@SuppressWarnings("unchecked")
public <S extends Object> S convert(Object source, TypeInformation<? extends S> typeHint) {
public <S extends Object> S convert(Object source, TypeInformation<? extends S> typeHint,
ConversionContext context) {
Assert.notNull(source, "Source must not be null");
Assert.notNull(typeHint, "TypeInformation must not be null");
@@ -2262,18 +2338,18 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
}
if (typeHint.isCollectionLike() || typeHint.getType().isAssignableFrom(Collection.class)) {
return (S) collectionConverter.convert(this, (Collection<?>) source, typeHint);
return (S) collectionConverter.convert(context, (Collection<?>) source, typeHint);
}
}
if (typeHint.isMap()) {
if (ClassUtils.isAssignable(Document.class, typeHint.getType())) {
return (S) documentConverter.convert(this, BsonUtils.asBson(source), typeHint);
return (S) documentConverter.convert(context, BsonUtils.asBson(source), typeHint);
}
if (BsonUtils.supportsBson(source)) {
return (S) mapConverter.convert(this, BsonUtils.asBson(source), typeHint);
return (S) mapConverter.convert(context, BsonUtils.asBson(source), typeHint);
}
throw new IllegalArgumentException(
@@ -2281,7 +2357,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
}
if (source instanceof DBRef) {
return (S) dbRefConverter.convert(this, (DBRef) source, typeHint);
return (S) dbRefConverter.convert(context, (DBRef) source, typeHint);
}
if (source instanceof Collection) {
@@ -2290,31 +2366,41 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
}
if (BsonUtils.supportsBson(source)) {
return (S) documentConverter.convert(this, BsonUtils.asBson(source), typeHint);
return (S) documentConverter.convert(context, BsonUtils.asBson(source), typeHint);
}
return (S) elementConverter.convert(source, typeHint);
}
@Override
public CustomConversions getCustomConversions() {
return conversions;
}
@Override
public MongoConverter getSourceConverter() {
return sourceConverter;
}
/**
* Create a new {@link ConversionContext} with {@link ObjectPath currentPath} applied.
* Create a new {@link DefaultConversionContext} with {@link ObjectPath currentPath} applied.
*
* @param currentPath must not be {@literal null}.
* @return a new {@link ConversionContext} with {@link ObjectPath currentPath} applied.
* @return a new {@link DefaultConversionContext} with {@link ObjectPath currentPath} applied.
*/
public ConversionContext withPath(ObjectPath currentPath) {
public DefaultConversionContext withPath(ObjectPath currentPath) {
Assert.notNull(currentPath, "ObjectPath must not be null");
return new ConversionContext(sourceConverter, conversions, currentPath, documentConverter, collectionConverter,
mapConverter, dbRefConverter, elementConverter);
return new DefaultConversionContext(sourceConverter, conversions, currentPath, documentConverter,
collectionConverter, mapConverter, dbRefConverter, elementConverter);
}
public ObjectPath getPath() {
return path;
}
public ConversionContext forProperty(String name) {
public DefaultConversionContext forProperty(String name) {
return this;
}

View File

@@ -44,7 +44,6 @@ 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;
@@ -2850,6 +2849,15 @@ class MappingMongoConverterUnitTests {
assertThat(read.viaRegisteredConverter).isEqualTo("spring");
}
@Test // GH-4098
void resolvesCyclicNonAssociationValueFromSource/* and does not attempt to be smart and look up id values in context */() {
org.bson.Document source = new org.bson.Document("_id", "id-1").append("value", "v1").append("cycle",
new org.bson.Document("_id", "id-1").append("value", "v2"));
assertThat(converter.read(Cyclic.class, source).cycle.value).isEqualTo("v2");
}
static class GenericType<T> {
T content;
}
@@ -3573,12 +3581,10 @@ class MappingMongoConverterUnitTests {
@org.springframework.data.mongodb.core.mapping.Field(
write = org.springframework.data.mongodb.core.mapping.Field.Write.ALWAYS) Integer writeAlways;
@org.springframework.data.mongodb.core.mapping.DBRef
@org.springframework.data.mongodb.core.mapping.Field(
@org.springframework.data.mongodb.core.mapping.DBRef @org.springframework.data.mongodb.core.mapping.Field(
write = org.springframework.data.mongodb.core.mapping.Field.Write.NON_NULL) Person writeNonNullPerson;
@org.springframework.data.mongodb.core.mapping.DBRef
@org.springframework.data.mongodb.core.mapping.Field(
@org.springframework.data.mongodb.core.mapping.DBRef @org.springframework.data.mongodb.core.mapping.Field(
write = org.springframework.data.mongodb.core.mapping.Field.Write.ALWAYS) Person writeAlwaysPerson;
}
@@ -3692,4 +3698,12 @@ class MappingMongoConverterUnitTests {
}
@Data
static class Cyclic {
@Id String id;
String value;
Cyclic cycle;
}
}