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 715ba82d5..91cfc6892 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 @@ -922,6 +922,7 @@ public class QueryMapper { */ protected static class MetadataBackedField extends Field { + private static final Pattern POSITIONAL_PARAMETER_PATTERN = 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; @@ -963,11 +964,15 @@ public class QueryMapper { this.entity = entity; this.mappingContext = context; - this.path = getPath(name); + this.path = getPath(removePositionalPlaceholders(name)); this.property = path == null ? property : path.getLeafProperty(); this.association = findAssociation(); } + private static String removePositionalPlaceholders(String raw) { + return POSITIONAL_PARAMETER_PATTERN.matcher(raw).replaceAll(""); + } + /* * (non-Javadoc) * @see org.springframework.data.mongodb.core.convert.QueryMapper.Field#with(java.lang.String) @@ -1168,7 +1173,7 @@ public class QueryMapper { * @since 1.7 */ protected Converter getAssociationConverter() { - return new AssociationConverter(getAssociation()); + return new AssociationConverter(name, getAssociation()); } protected MappingContext, MongoPersistentProperty> getMappingContext() { @@ -1247,21 +1252,10 @@ public class QueryMapper { StringBuilder mappedName = new StringBuilder(PropertyToFieldNameConverter.INSTANCE.convert(property)); boolean inspect = iterator.hasNext(); - int depth = 0; while (inspect) { String partial = iterator.next(); - if (depth > 0 && property.isCollectionLike() && property.isEntity() && property.getComponentType() != null) { - - MongoPersistentEntity persistentEntity = mappingContext - .getRequiredPersistentEntity(property.getComponentType()); - MongoPersistentProperty persistentProperty = persistentEntity.getPersistentProperty(partial); - if (persistentProperty != null) { - partial = mapPropertyName(persistentProperty); - } - } - boolean isPositional = (isPositionalParameter(partial) && (property.isMap() || property.isCollectionLike())); if (isPositional) { @@ -1269,13 +1263,12 @@ public class QueryMapper { } inspect = isPositional && iterator.hasNext(); - depth++; } return mappedName.toString(); } - private static boolean isPositionalParameter(String partial) { + static boolean isPositionalParameter(String partial) { if ("$".equals(partial)) { return true; @@ -1303,6 +1296,7 @@ public class QueryMapper { */ protected static class AssociationConverter implements Converter { + private final String name; private final MongoPersistentProperty property; private boolean associationFound; @@ -1311,10 +1305,11 @@ public class QueryMapper { * * @param association must not be {@literal null}. */ - public AssociationConverter(Association association) { + public AssociationConverter(String name, Association association) { Assert.notNull(association, "Association must not be null!"); this.property = association.getInverse(); + this.name = name; } /* @@ -1332,6 +1327,12 @@ public class QueryMapper { associationFound = true; } + if (associationFound) { + if (name.endsWith("$") && property.isCollectionLike()) { + return source.getFieldName() + ".$"; + } + } + return source.getFieldName(); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/UpdateMapper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/UpdateMapper.java index 463c15d4e..afe13f472 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/UpdateMapper.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/UpdateMapper.java @@ -272,6 +272,7 @@ public class UpdateMapper extends QueryMapper { * * @author Thomas Darimont * @author Oliver Gierke + * @author Christoph Strobl */ private static class MetadataBackedUpdateField extends MetadataBackedField { @@ -289,7 +290,7 @@ public class UpdateMapper extends QueryMapper { public MetadataBackedUpdateField(MongoPersistentEntity entity, String key, MappingContext, MongoPersistentProperty> mappingContext) { - super(key.replaceAll("\\.\\$(\\[.*\\])?", ""), entity, mappingContext); + super(key, entity, mappingContext); this.key = key; } @@ -338,7 +339,7 @@ public class UpdateMapper extends QueryMapper { MappingContext, MongoPersistentProperty> mappingContext, Association association, String key) { - super(association); + super(key, association); this.mapper = new KeyMapper(key, mappingContext); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultBulkOperationsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultBulkOperationsUnitTests.java index 4c9933995..8868f608a 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultBulkOperationsUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultBulkOperationsUnitTests.java @@ -333,6 +333,51 @@ public class DefaultBulkOperationsUnitTests { .isEqualTo(new org.bson.Document("element", new Document("$gte", 100))); } + @Test // DATAMONGO-2502 + void shouldRetainNestedArrayPathWithPlaceholdersForNoMatchingPaths() { + + ops.updateOne(new BasicQuery("{}"), new Update().set("items.$.documents.0.fileId", "new-id")).execute(); + + verify(collection).bulkWrite(captor.capture(), any()); + + UpdateOneModel updateModel = (UpdateOneModel) captor.getValue().get(0); + assertThat(updateModel.getUpdate()) + .isEqualTo(new Document("$set", new Document("items.$.documents.0.fileId", "new-id"))); + } + + @Test // DATAMONGO-2502 + void shouldRetainNestedArrayPathWithPlaceholdersForMappedEntity() { + + DefaultBulkOperations ops = new DefaultBulkOperations(template, "collection-1", + new BulkOperationContext(BulkMode.ORDERED, Optional.of(mappingContext.getPersistentEntity(OrderTest.class)), + new QueryMapper(converter), new UpdateMapper(converter), null, null)); + + ops.updateOne(new BasicQuery("{}"), Update.update("items.$.documents.0.fileId", "file-id")).execute(); + + verify(collection).bulkWrite(captor.capture(), any()); + + UpdateOneModel updateModel = (UpdateOneModel) captor.getValue().get(0); + assertThat(updateModel.getUpdate()) + .isEqualTo(new Document("$set", new Document("items.$.documents.0.fileId", "file-id"))); + } + + static class OrderTest { + + String id; + List items; + } + + static class OrderTestItem { + + private String cartId; + private List documents; + } + + static class OrderTestDocument { + + private String fileId; + } + class SomeDomainType { @Id String id; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java index 92c9fb43d..4b6a00e26 100755 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java @@ -967,6 +967,32 @@ public class QueryMapperUnitTests { assertThat(target).isEqualTo(new org.bson.Document("arrayCustomName.$[some_item].nes-ted.$[other_item]", "value")); } + @Test // DATAMONGO-2502 + void shouldAllowDeeplyNestedPlaceholders() { + + org.bson.Document target = mapper.getMappedObject( + query(where("level0.$[some_item].arrayObj.$[other_item].nested").is("value")).getQueryObject(), + context.getPersistentEntity(WithDeepArrayNesting.class)); + + assertThat(target).isEqualTo(new org.bson.Document("level0.$[some_item].arrayObj.$[other_item].nested", "value")); + } + + @Test // DATAMONGO-2502 + void shouldAllowDeeplyNestedPlaceholdersWithCustomName() { + + org.bson.Document target = mapper.getMappedObject( + query(where("level0.$[some_item].arrayCustomName.$[other_item].nested").is("value")).getQueryObject(), + context.getPersistentEntity(WithDeepArrayNesting.class)); + + assertThat(target) + .isEqualTo(new org.bson.Document("level0.$[some_item].arrayCustomName.$[other_item].nes-ted", "value")); + } + + class WithDeepArrayNesting { + + List level0; + } + class WithNestedArray { List arrayObj;