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 6ab3ae520..f07cae764 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 @@ -1077,7 +1077,7 @@ public class QueryMapper { protected static class MetadataBackedField extends Field { private static final Pattern POSITIONAL_PARAMETER_PATTERN = Pattern.compile("\\.\\$(\\[.*?\\])?"); - private static final Pattern DOT_POSITIONAL_PATTERN = Pattern.compile("\\.\\d+(?!$)"); + private static final Pattern NUMERIC_SEGMENT = Pattern.compile("\\d+"); private static final String INVALID_ASSOCIATION_REFERENCE = "Invalid path reference %s! Associations can only be pointed to directly or via their id property!"; private final MongoPersistentEntity entity; @@ -1243,26 +1243,13 @@ public class QueryMapper { private PersistentPropertyPath getPath(String pathExpression, @Nullable MongoPersistentProperty sourceProperty) { - String rawPath = removePlaceholders(POSITIONAL_OPERATOR, - removePlaceholders(DOT_POSITIONAL_PATTERN, pathExpression)); - // fix xx.11.22.33 becomes xx3, it should be xx.33, then path should be null. (test mapNestedLastBigIntegerFieldCorrectly) - if (pathExpression.contains(".")) { - String lastDotString = pathExpression.substring(pathExpression.lastIndexOf(".")); - int lastDotLength = lastDotString.length(); - int newLength = 0; - if (rawPath.contains(".")) { - newLength = rawPath.substring(rawPath.lastIndexOf(".")).length(); - } - if (lastDotLength != newLength) { - rawPath = rawPath.substring(0, rawPath.length() - 1) + lastDotString; - } - } - if (sourceProperty != null && sourceProperty.getOwner().equals(entity)) { return mappingContext.getPersistentPropertyPath( PropertyPath.from(Pattern.quote(sourceProperty.getName()), entity.getTypeInformation())); } + String rawPath = resolvePath(pathExpression); + PropertyPath path = forName(rawPath); if (path == null || isPathToJavaLangClassProperty(path)) { return null; @@ -1357,6 +1344,38 @@ public class QueryMapper { return false; } + private static String resolvePath(String source) { + + String[] segments = source.split("\\."); + if (segments.length == 1) { + return source; + } + + List path = new ArrayList<>(segments.length); + + /* always start from a property, so we can skip the first segment. + from there remove any position placeholder */ + for(int i=1; i < segments.length; i++) { + String segment = segments[i]; + if (segment.startsWith("[") && segment.endsWith("]")) { + continue; + } + if (NUMERIC_SEGMENT.matcher(segment).matches()) { + continue; + } + path.add(segment); + } + + // when property is followed only by placeholders eg. 'values.0.3.90' + // or when there is no difference in the number of segments + if (path.isEmpty() || segments.length == path.size() + 1) { + return source; + } + + path.add(0, segments[0]); + return StringUtils.collectionToDelimitedString(path, "."); + } + /** * Return the {@link Converter} to be used to created the mapped key. Default implementation will use * {@link PropertyToFieldNameConverter}. 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 05adab2b9..bf6604894 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 @@ -35,6 +35,8 @@ import org.bson.types.ObjectId; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; @@ -1212,34 +1214,26 @@ class UpdateMapperUnitTests { assertThat(mappedUpdate).isEqualTo(new org.bson.Document("$set", new org.bson.Document("levelOne.a.b.d", "e"))); } - @Test // GH-3775 - void mapNestedIntegerFieldCorrectly() { + @ParameterizedTest // GH-3775, GH-4426 + @ValueSource(strings = {"levelOne.0.1.3", "levelOne.0.1.32", "levelOne2.0.1.32", "levelOne2.0.1.320"}) + void mapNestedIntegerFieldCorrectly(String path) { - Update update = new Update().set("levelOne.0.1.3", "4"); + Update update = new Update().set(path, "4"); Document mappedUpdate = mapper.getMappedObject(update.getUpdateObject(), context.getPersistentEntity(EntityWithNestedMap.class)); - assertThat(mappedUpdate).isEqualTo(new org.bson.Document("$set", new org.bson.Document("levelOne.0.1.3", "4"))); + assertThat(mappedUpdate).isEqualTo(new org.bson.Document("$set", new org.bson.Document(path, "4"))); } - @Test - void mapNestedLastBigIntegerFieldCorrectly() { - - Update update = new Update().set("levelOne.0.1.32", "4"); - Document mappedUpdate = mapper.getMappedObject(update.getUpdateObject(), - context.getPersistentEntity(EntityWithNestedMap.class)); - - assertThat(mappedUpdate).isEqualTo(new org.bson.Document("$set", new org.bson.Document("levelOne.0.1.32", "4"))); - } - - @Test // GH-3775 - void mapNestedMixedStringIntegerFieldCorrectly() { + @ParameterizedTest // GH-3775, GH-4426 + @ValueSource(strings = {"levelOne.0.1.c", "levelOne.0.1.c.32", "levelOne2.0.1.32.c", "levelOne2.0.1.c.320"}) + void mapNestedMixedStringIntegerFieldCorrectly(String path) { - Update update = new Update().set("levelOne.0.1.c", "4"); + Update update = new Update().set(path, "4"); Document mappedUpdate = mapper.getMappedObject(update.getUpdateObject(), context.getPersistentEntity(EntityWithNestedMap.class)); - assertThat(mappedUpdate).isEqualTo(new org.bson.Document("$set", new org.bson.Document("levelOne.0.1.c", "4"))); + assertThat(mappedUpdate).isEqualTo(new org.bson.Document("$set", new org.bson.Document(path, "4"))); } @Test // GH-3775 @@ -1729,7 +1723,6 @@ class UpdateMapperUnitTests { static class EntityWithNestedMap { Map>> levelOne; - // for test mapNestedLastBigIntegerFieldCorrectly() Map>> levelOne2; }