Skip to content

Commit e4f9d68

Browse files
author
Thomas Darimont
committed
DATAMONGO-1005 - Improve cycle-detection for DbRef's.
Introduced ObjectPath that collects the target objects while converting a DBObject to a domain object. If we detect that a potentially nested DBRef points to an object that is already under construction we simply return a reference to that object in order to avoid StackOverFlowErrors. Original pull request: #209.
1 parent edf0293 commit e4f9d68

File tree

2 files changed

+206
-9
lines changed

2 files changed

+206
-9
lines changed

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

Lines changed: 146 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.util.Collection;
2121
import java.util.Collections;
2222
import java.util.HashSet;
23+
import java.util.Iterator;
2324
import java.util.List;
2425
import java.util.Map;
2526
import java.util.Map.Entry;
@@ -194,6 +195,8 @@ protected <S extends Object> S read(TypeInformation<S> type, DBObject dbo, Objec
194195
return null;
195196
}
196197

198+
Object parentObject = ObjectPath.unwrapCurrentFromPotentialPath(parent);
199+
197200
TypeInformation<? extends S> typeToUse = typeMapper.readType(dbo, type);
198201
Class<? extends S> rawType = typeToUse.getType();
199202

@@ -206,11 +209,11 @@ protected <S extends Object> S read(TypeInformation<S> type, DBObject dbo, Objec
206209
}
207210

208211
if (typeToUse.isCollectionLike() && dbo instanceof BasicDBList) {
209-
return (S) readCollectionOrArray(typeToUse, (BasicDBList) dbo, parent);
212+
return (S) readCollectionOrArray(typeToUse, (BasicDBList) dbo, parentObject);
210213
}
211214

212215
if (typeToUse.isMap()) {
213-
return (S) readMap(typeToUse, dbo, parent);
216+
return (S) readMap(typeToUse, dbo, parentObject);
214217
}
215218

216219
// Retrieve persistent entity info
@@ -238,22 +241,37 @@ private <S extends Object> S read(final MongoPersistentEntity<S> entity, final D
238241

239242
final DefaultSpELExpressionEvaluator evaluator = new DefaultSpELExpressionEvaluator(dbo, spELContext);
240243

241-
ParameterValueProvider<MongoPersistentProperty> provider = getParameterProvider(entity, dbo, evaluator, parent);
244+
final ObjectPath parentPath = ObjectPath.toPath(parent);
245+
246+
ParameterValueProvider<MongoPersistentProperty> provider = getParameterProvider(entity, dbo, evaluator,
247+
parentPath.getCurrentObject());
242248
EntityInstantiator instantiator = instantiators.getInstantiatorFor(entity);
243249
S instance = instantiator.createInstance(entity, provider);
244250

245251
final BeanWrapper<S> wrapper = BeanWrapper.create(instance, conversionService);
246252
final S result = wrapper.getBean();
247253

254+
// make sure id property is set before all other properties
255+
if (entity.getIdProperty() != null) {
256+
wrapper.setProperty(entity.getIdProperty(), getValueInternal(entity.getIdProperty(), dbo, evaluator, result));
257+
}
258+
259+
final ObjectPath currentPath = parentPath.push(result);
260+
248261
// Set properties not already set in the constructor
249262
entity.doWithProperties(new PropertyHandler<MongoPersistentProperty>() {
250263
public void doWithPersistentProperty(MongoPersistentProperty prop) {
251264

265+
// we skip the id property since it was already set
266+
if (entity.getIdProperty() != null && entity.getIdProperty().equals(prop)) {
267+
return;
268+
}
269+
252270
if (!dbo.containsField(prop.getFieldName()) || entity.isConstructorArgument(prop)) {
253271
return;
254272
}
255273

256-
wrapper.setProperty(prop, getValueInternal(prop, dbo, evaluator, result));
274+
wrapper.setProperty(prop, getValueInternal(prop, dbo, evaluator, currentPath.getCurrentObject()));
257275
}
258276
});
259277

@@ -273,7 +291,7 @@ public void doWithAssociation(Association<MongoPersistentProperty> association)
273291

274292
@Override
275293
public Object resolve(MongoPersistentProperty property) {
276-
return getValueInternal(property, dbo, evaluator, parent);
294+
return getValueInternal(property, dbo, evaluator, currentPath);
277295
}
278296
}));
279297
}
@@ -1097,21 +1115,75 @@ protected <T> T potentiallyConvertSpelValue(Object object, Parameter<T, MongoPer
10971115
@SuppressWarnings("unchecked")
10981116
private <T> T readValue(Object value, TypeInformation<?> type, Object parent) {
10991117

1118+
ObjectPath objectStack = ObjectPath.toPath(parent);
1119+
11001120
Class<?> rawType = type.getType();
11011121

11021122
if (conversions.hasCustomReadTarget(value.getClass(), rawType)) {
11031123
return (T) conversionService.convert(value, rawType);
11041124
} else if (value instanceof DBRef) {
1105-
return (T) (rawType.equals(DBRef.class) ? value : read(type, readRef((DBRef) value), parent));
1125+
return potentiallyReadOrResolveDbRef((DBRef) value, type, objectStack, rawType);
11061126
} else if (value instanceof BasicDBList) {
1107-
return (T) readCollectionOrArray(type, (BasicDBList) value, parent);
1127+
return (T) readCollectionOrArray(type, (BasicDBList) value, objectStack.getCurrentObject());
11081128
} else if (value instanceof DBObject) {
1109-
return (T) read(type, (DBObject) value, parent);
1129+
return (T) read(type, (DBObject) value, objectStack.getCurrentObject());
11101130
} else {
11111131
return (T) getPotentiallyConvertedSimpleRead(value, rawType);
11121132
}
11131133
}
11141134

1135+
@SuppressWarnings("unchecked")
1136+
private <T> T potentiallyReadOrResolveDbRef(DBRef dbref, TypeInformation<?> type, ObjectPath path, Class<?> rawType) {
1137+
1138+
if (rawType.equals(DBRef.class)) {
1139+
return (T) dbref;
1140+
}
1141+
1142+
Object object = getObjectFromPathForRefOrNull(path, dbref);
1143+
1144+
if (object != null) {
1145+
return (T) object;
1146+
}
1147+
1148+
return (T) (object != null ? object : read(type, readRef(dbref), path.getCurrentObject()));
1149+
}
1150+
1151+
/**
1152+
* Returns the object from the given {@link ObjectPath} iff the given {@link DBRef} points to it or {@literal null}.
1153+
*
1154+
* @param path
1155+
* @param dbref
1156+
* @return
1157+
*/
1158+
private Object getObjectFromPathForRefOrNull(ObjectPath path, DBRef dbref) {
1159+
1160+
if (path == null || dbref == null) {
1161+
return null;
1162+
}
1163+
1164+
for (Object object : path) {
1165+
1166+
if (object == null) {
1167+
continue;
1168+
}
1169+
1170+
MongoPersistentEntity<?> pe = getMappingContext().getPersistentEntity(object.getClass());
1171+
1172+
if (pe == null || pe.getIdProperty() == null) {
1173+
continue;
1174+
}
1175+
1176+
BeanWrapper<Object> parentBeanWrapper = BeanWrapper.create(object, conversionService);
1177+
Object parentId = parentBeanWrapper.getProperty(pe.getIdProperty());
1178+
1179+
if (dbref.getRef().equals(pe.getCollection()) && dbref.getId().equals(parentId)) {
1180+
return object;
1181+
}
1182+
}
1183+
1184+
return null;
1185+
}
1186+
11151187
/**
11161188
* Performs the fetch operation for the given {@link DBRef}.
11171189
*
@@ -1121,4 +1193,70 @@ private <T> T readValue(Object value, TypeInformation<?> type, Object parent) {
11211193
DBObject readRef(DBRef ref) {
11221194
return ref.fetch();
11231195
}
1196+
1197+
/**
1198+
* An immutable ordered set of target objects for {@link DBObject} to {@link Object} conversions. Object paths can be
1199+
* constructed by the {@link #toPath(Object)} method and extended via {@link #push(Object)}.
1200+
*
1201+
* @author Thomas Darimont
1202+
* @since 1.6
1203+
*/
1204+
static class ObjectPath implements Iterable<Object> {
1205+
1206+
private final List<Object> stack;
1207+
1208+
private ObjectPath(Object current) {
1209+
this(null, current);
1210+
}
1211+
1212+
private ObjectPath(ObjectPath parent, Object current) {
1213+
1214+
if (parent == null) {
1215+
this.stack = Collections.singletonList(current);
1216+
} else {
1217+
List<Object> list = new ArrayList<Object>(parent.stack);
1218+
list.add(current);
1219+
this.stack = list;
1220+
}
1221+
}
1222+
1223+
/**
1224+
* Returns a copy of the {@link ObjectPath} with the given {@link Object} as current object.
1225+
*
1226+
* @param o
1227+
* @return
1228+
*/
1229+
public ObjectPath push(Object o) {
1230+
return new ObjectPath(this, o);
1231+
}
1232+
1233+
public Object getRootObject() {
1234+
return stack.get(0);
1235+
}
1236+
1237+
public Object getCurrentObject() {
1238+
return stack.get(stack.size() - 1);
1239+
}
1240+
1241+
@Override
1242+
public Iterator<Object> iterator() {
1243+
return stack.iterator();
1244+
}
1245+
1246+
/**
1247+
* Returns the {@link ObjectPath} represented by the given {@link Object} or creates a new {@link ObjectPath}
1248+
* wrapping the given {@link Object}.
1249+
*
1250+
* @param object
1251+
* @return
1252+
*/
1253+
public static ObjectPath toPath(Object object) {
1254+
return object instanceof ObjectPath ? ((ObjectPath) object) : new ObjectPath(object);
1255+
}
1256+
1257+
public static Object unwrapCurrentFromPotentialPath(Object potentialObjectPath) {
1258+
return potentialObjectPath instanceof ObjectPath ? ((ObjectPath) potentialObjectPath).getCurrentObject()
1259+
: potentialObjectPath;
1260+
}
1261+
}
11241262
}

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

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*/
1616
package org.springframework.data.mongodb.core.convert;
1717

18-
import static org.hamcrest.Matchers.*;
18+
import static org.hamcrest.CoreMatchers.*;
1919
import static org.junit.Assert.*;
2020
import static org.mockito.Matchers.*;
2121
import static org.mockito.Mockito.*;
@@ -450,6 +450,47 @@ public void shouldNotGenerateLazyLoadingProxyForNullValues() {
450450
assertThat(result.dbRefToConcreteTypeWithPersistenceConstructorWithoutDefaultConstructor, is(nullValue()));
451451
}
452452

453+
/**
454+
* @see DATAMONGO-1005
455+
*/
456+
@Test
457+
public void shouldBeAbleToStoreDirectReferencesToSelf() {
458+
459+
DBObject dbo = new BasicDBObject();
460+
461+
ClassWithDbRefField o = new ClassWithDbRefField();
462+
o.id = "123";
463+
o.reference = o;
464+
converter.write(o, dbo);
465+
466+
ClassWithDbRefField found = converter.read(ClassWithDbRefField.class, dbo);
467+
468+
assertThat(found, is(notNullValue()));
469+
assertThat(found.reference, is(found));
470+
}
471+
472+
/**
473+
* @see DATAMONGO-1005
474+
*/
475+
@Test
476+
public void shouldBeAbleToStoreNestedReferencesToSelf() {
477+
478+
DBObject dbo = new BasicDBObject();
479+
480+
ClassWithNestedDbRefField o = new ClassWithNestedDbRefField();
481+
o.id = "123";
482+
o.nested = new NestedReferenceHolder();
483+
o.nested.reference = o;
484+
485+
converter.write(o, dbo);
486+
487+
ClassWithNestedDbRefField found = converter.read(ClassWithNestedDbRefField.class, dbo);
488+
489+
assertThat(found, is(notNullValue()));
490+
assertThat(found.nested, is(notNullValue()));
491+
assertThat(found.nested.reference, is(found));
492+
}
493+
453494
private Object transport(Object result) {
454495
return SerializationUtils.deserialize(SerializationUtils.serialize(result));
455496
}
@@ -626,4 +667,22 @@ static class WithObjectMethodOverrideLazyDbRefs {
626667
@org.springframework.data.mongodb.core.mapping.DBRef(lazy = true) EqualsAndHashCodeObjectMethodOverrideLazyDbRefTarget dbRefEqualsAndHashcodeObjectMethodOverride2;
627668
@org.springframework.data.mongodb.core.mapping.DBRef(lazy = true) EqualsAndHashCodeObjectMethodOverrideLazyDbRefTarget dbRefEqualsAndHashcodeObjectMethodOverride1;
628669
}
670+
671+
class ClassWithDbRefField {
672+
673+
String id;
674+
@org.springframework.data.mongodb.core.mapping.DBRef ClassWithDbRefField reference;
675+
}
676+
677+
static class NestedReferenceHolder {
678+
679+
String id;
680+
@org.springframework.data.mongodb.core.mapping.DBRef ClassWithNestedDbRefField reference;
681+
}
682+
683+
static class ClassWithNestedDbRefField {
684+
685+
String id;
686+
NestedReferenceHolder nested;
687+
}
629688
}

0 commit comments

Comments
 (0)