Skip to content

Commit 164e947

Browse files
christophstroblodrotbohm
authored andcommitted
DATAMONGO-926 - Avoid StackOverflowError while resolving index structures.
We now guard cyclic non transient, non DBRef property references while inspecting domain types for potentially index structures. To do so we check on the properties path and owning type to determine potential cycles. If found we log a warn message pointing to path, entity and property potentially causing problems and skip processing for this path. Original pull request: spring-projects#180.
1 parent 7e65c0c commit 164e947

File tree

2 files changed

+223
-6
lines changed

2 files changed

+223
-6
lines changed

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java

Lines changed: 132 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,15 @@
1717

1818
import java.util.ArrayList;
1919
import java.util.Collections;
20+
import java.util.LinkedHashMap;
2021
import java.util.List;
22+
import java.util.Map;
2123
import java.util.concurrent.TimeUnit;
24+
import java.util.regex.Matcher;
25+
import java.util.regex.Pattern;
2226

27+
import org.slf4j.Logger;
28+
import org.slf4j.LoggerFactory;
2329
import org.springframework.core.annotation.AnnotationUtils;
2430
import org.springframework.dao.InvalidDataAccessApiUsageException;
2531
import org.springframework.data.domain.Sort;
@@ -48,6 +54,8 @@
4854
*/
4955
public class MongoPersistentEntityIndexResolver implements IndexResolver {
5056

57+
private static final Logger LOGGER = LoggerFactory.getLogger(MongoPersistentEntityIndexResolver.class);
58+
5159
private final MongoMappingContext mappingContext;
5260

5361
/**
@@ -88,14 +96,16 @@ public List<IndexDefinitionHolder> resolveIndexForEntity(final MongoPersistentEn
8896
final List<IndexDefinitionHolder> indexInformation = new ArrayList<MongoPersistentEntityIndexResolver.IndexDefinitionHolder>();
8997
indexInformation.addAll(potentiallyCreateCompoundIndexDefinitions("", root.getCollection(), root.getType()));
9098

99+
final CycleGuard guard = new CycleGuard();
100+
91101
root.doWithProperties(new PropertyHandler<MongoPersistentProperty>() {
92102

93103
@Override
94104
public void doWithPersistentProperty(MongoPersistentProperty persistentProperty) {
95105

96106
if (persistentProperty.isEntity()) {
97107
indexInformation.addAll(resolveIndexForClass(persistentProperty.getActualType(),
98-
persistentProperty.getFieldName(), root.getCollection()));
108+
persistentProperty.getFieldName(), root.getCollection(), guard));
99109
}
100110

101111
IndexDefinitionHolder indexDefinitionHolder = createIndexDefinitionHolderForProperty(
@@ -118,7 +128,8 @@ public void doWithPersistentProperty(MongoPersistentProperty persistentProperty)
118128
* @return List of {@link IndexDefinitionHolder} representing indexes for given type and its referenced property
119129
* types. Will never be {@code null}.
120130
*/
121-
private List<IndexDefinitionHolder> resolveIndexForClass(Class<?> type, final String path, final String collection) {
131+
private List<IndexDefinitionHolder> resolveIndexForClass(final Class<?> type, final String path,
132+
final String collection, final CycleGuard guard) {
122133

123134
final List<IndexDefinitionHolder> indexInformation = new ArrayList<MongoPersistentEntityIndexResolver.IndexDefinitionHolder>();
124135
indexInformation.addAll(potentiallyCreateCompoundIndexDefinitions(path, collection, type));
@@ -130,9 +141,15 @@ private List<IndexDefinitionHolder> resolveIndexForClass(Class<?> type, final St
130141
public void doWithPersistentProperty(MongoPersistentProperty persistentProperty) {
131142

132143
String propertyDotPath = (StringUtils.hasText(path) ? path + "." : "") + persistentProperty.getFieldName();
144+
guard.protect(persistentProperty, path);
133145

134146
if (persistentProperty.isEntity()) {
135-
indexInformation.addAll(resolveIndexForClass(persistentProperty.getActualType(), propertyDotPath, collection));
147+
try {
148+
indexInformation.addAll(resolveIndexForClass(persistentProperty.getActualType(), propertyDotPath,
149+
collection, guard));
150+
} catch (CyclicPropertyReferenceException e) {
151+
LOGGER.warn(e.getMessage());
152+
}
136153
}
137154

138155
IndexDefinitionHolder indexDefinitionHolder = createIndexDefinitionHolderForProperty(propertyDotPath,
@@ -320,6 +337,115 @@ protected IndexDefinitionHolder createGeoSpatialIndexDefinition(String dotPath,
320337
return new IndexDefinitionHolder(dotPath, indexDefinition, collection);
321338
}
322339

340+
/**
341+
* {@link CycleGuard} holds information about properties and the paths for accessing those. This information is used
342+
* to detect potential cycles within the references.
343+
*
344+
* @author Christoph Strobl
345+
*/
346+
private static class CycleGuard {
347+
348+
private final Map<String, List<Path>> propertyTypeMap;
349+
350+
CycleGuard() {
351+
this.propertyTypeMap = new LinkedHashMap<String, List<Path>>();
352+
}
353+
354+
/**
355+
* @param property The property to inspect
356+
* @param path The path under which the property can be reached.
357+
* @throws CyclicPropertyReferenceException in case a potential cycle is detected.
358+
*/
359+
void protect(MongoPersistentProperty property, String path) throws CyclicPropertyReferenceException {
360+
361+
String propertyTypeKey = createMapKey(property);
362+
if (propertyTypeMap.containsKey(propertyTypeKey)) {
363+
364+
List<Path> paths = propertyTypeMap.get(propertyTypeKey);
365+
366+
for (Path existingPath : paths) {
367+
368+
if (existingPath.cycles(property)) {
369+
paths.add(new Path(property, path));
370+
throw new CyclicPropertyReferenceException(property.getFieldName(), property.getOwner().getType(),
371+
existingPath.getPath());
372+
}
373+
}
374+
375+
paths.add(new Path(property, path));
376+
377+
} else {
378+
379+
ArrayList<Path> paths = new ArrayList<Path>();
380+
paths.add(new Path(property, path));
381+
propertyTypeMap.put(propertyTypeKey, paths);
382+
}
383+
}
384+
385+
private String createMapKey(MongoPersistentProperty property) {
386+
return property.getOwner().getType().getSimpleName() + ":" + property.getFieldName();
387+
}
388+
389+
private static class Path {
390+
391+
private final MongoPersistentProperty property;
392+
private final String path;
393+
394+
Path(MongoPersistentProperty property, String path) {
395+
396+
this.property = property;
397+
this.path = path;
398+
}
399+
400+
public String getPath() {
401+
return path;
402+
}
403+
404+
boolean cycles(MongoPersistentProperty property) {
405+
406+
Pattern pattern = Pattern.compile("\\p{Punct}?" + Pattern.quote(property.getFieldName()) + "(\\p{Punct}|\\w)?");
407+
Matcher matcher = pattern.matcher(path);
408+
409+
int count = 0;
410+
while (matcher.find()) {
411+
count++;
412+
}
413+
414+
return count >= 1 && property.getOwner().getType().equals(this.property.getOwner().getType());
415+
}
416+
}
417+
}
418+
419+
/**
420+
* @author Christoph Strobl
421+
* @since 1.5
422+
*/
423+
public static class CyclicPropertyReferenceException extends RuntimeException {
424+
425+
private static final long serialVersionUID = -3762979307658772277L;
426+
427+
private final String propertyName;
428+
private final Class<?> type;
429+
private final String dotPath;
430+
431+
public CyclicPropertyReferenceException(String propertyName, Class<?> type, String dotPath) {
432+
433+
this.propertyName = propertyName;
434+
this.type = type;
435+
this.dotPath = dotPath;
436+
}
437+
438+
/*
439+
* (non-Javadoc)
440+
* @see java.lang.Throwable#getMessage()
441+
*/
442+
@Override
443+
public String getMessage() {
444+
return String.format("Found cycle for field '%s' in type '%s' for path '%s'", propertyName, type.getSimpleName(),
445+
dotPath);
446+
}
447+
}
448+
323449
/**
324450
* Implementation of {@link IndexDefinition} holding additional (property)path information used for creating the
325451
* index. The path itself is the properties {@literal "dot"} path representation from its root document.
@@ -329,9 +455,9 @@ protected IndexDefinitionHolder createGeoSpatialIndexDefinition(String dotPath,
329455
*/
330456
public static class IndexDefinitionHolder implements IndexDefinition {
331457

332-
private String path;
333-
private IndexDefinition indexDefinition;
334-
private String collection;
458+
private final String path;
459+
private final IndexDefinition indexDefinition;
460+
private final String collection;
335461

336462
/**
337463
* Create

spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,55 @@ public void associationsShouldNotBeTraversed() {
439439
assertThat(indexDefinitions, empty());
440440
}
441441

442+
/**
443+
* @see DATAMONGO-926
444+
*/
445+
@Test
446+
public void shouldNotRunIntoStackOverflow() {
447+
448+
List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(CycleStartingInBetween.class);
449+
assertThat(indexDefinitions, hasSize(1));
450+
}
451+
452+
/**
453+
* @see DATAMONGO-926
454+
*/
455+
@Test
456+
public void indexShouldBeFoundEvenForCyclePropertyReferenceOnLevelZero() {
457+
458+
List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(CycleLevelZero.class);
459+
assertIndexPathAndCollection("indexedProperty", "cycleLevelZero", indexDefinitions.get(0));
460+
assertIndexPathAndCollection("cyclicReference.indexedProperty", "cycleLevelZero", indexDefinitions.get(1));
461+
assertThat(indexDefinitions, hasSize(2));
462+
}
463+
464+
/**
465+
* @see DATAMONGO-926
466+
*/
467+
@Test
468+
public void indexShouldBeFoundEvenForCyclePropertyReferenceOnLevelOne() {
469+
470+
List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(CycleOnLevelOne.class);
471+
assertIndexPathAndCollection("reference.indexedProperty", "cycleOnLevelOne", indexDefinitions.get(0));
472+
assertIndexPathAndCollection("reference.cyclicReference.reference.indexedProperty", "cycleOnLevelOne",
473+
indexDefinitions.get(1));
474+
assertThat(indexDefinitions, hasSize(2));
475+
}
476+
477+
/**
478+
* @see DATAMONGO-926
479+
*/
480+
@Test
481+
public void indexBeResolvedCorrectlyWhenPropertiesOfDifferentTypesAreNamedEqually() {
482+
483+
List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(NoCycleButIdenticallyNamedProperties.class);
484+
assertIndexPathAndCollection("foo", "noCycleButIdenticallyNamedProperties", indexDefinitions.get(0));
485+
assertIndexPathAndCollection("reference.foo", "noCycleButIdenticallyNamedProperties", indexDefinitions.get(1));
486+
assertIndexPathAndCollection("reference.deep.foo", "noCycleButIdenticallyNamedProperties",
487+
indexDefinitions.get(2));
488+
assertThat(indexDefinitions, hasSize(3));
489+
}
490+
442491
@Document
443492
static class MixedIndexRoot {
444493

@@ -463,6 +512,48 @@ static class Inner {
463512
@Indexed Outer outer;
464513
}
465514

515+
@Document
516+
static class CycleLevelZero {
517+
518+
@Indexed String indexedProperty;
519+
CycleLevelZero cyclicReference;
520+
}
521+
522+
@Document
523+
static class CycleOnLevelOne {
524+
525+
CycleOnLevelOneReferenced reference;
526+
}
527+
528+
static class CycleOnLevelOneReferenced {
529+
530+
@Indexed String indexedProperty;
531+
CycleOnLevelOne cyclicReference;
532+
}
533+
534+
@Document
535+
public static class CycleStartingInBetween {
536+
537+
CycleOnLevelOne referenceToCycleStart;
538+
}
539+
540+
@Document
541+
static class NoCycleButIdenticallyNamedProperties {
542+
543+
@Indexed String foo;
544+
NoCycleButIdenticallyNamedPropertiesNested reference;
545+
}
546+
547+
static class NoCycleButIdenticallyNamedPropertiesNested {
548+
549+
@Indexed String foo;
550+
NoCycleButIndenticallNamedPropertiesDeeplyNested deep;
551+
}
552+
553+
static class NoCycleButIndenticallNamedPropertiesDeeplyNested {
554+
555+
@Indexed String foo;
556+
}
466557
}
467558

468559
private static List<IndexDefinitionHolder> prepareMappingContextAndResolveIndexForType(Class<?> type) {

0 commit comments

Comments
 (0)