diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntity.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntity.java index 93f29a1a70..9491d78f8c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntity.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntity.java @@ -27,6 +27,7 @@ import org.jspecify.annotations.Nullable; +import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.data.annotation.Id; import org.springframework.data.core.TypeInformation; import org.springframework.data.expression.ValueEvaluationContext; @@ -96,13 +97,24 @@ public BasicMongoPersistentEntity(TypeInformation typeInformation) { this.collection = StringUtils.hasText(document.collection()) ? document.collection() : fallback; this.language = StringUtils.hasText(document.language()) ? document.language() : ""; this.expression = detectExpression(document.collection()); - this.collation = document.collation(); - this.collationExpression = detectExpression(document.collation()); } else { this.collection = fallback; this.language = ""; this.expression = null; + } + + // Pick up the standalone @Collation annotation independently of @Document. @Document.collation + // remains supported because @Document is meta-annotated with @Collation and Document#collation + // is declared as @AliasFor(annotation = Collation.class, attribute = "value"), so the merged + // lookup returns the same value in either case. + org.springframework.data.mongodb.core.annotation.Collation collationAnnotation = AnnotatedElementUtils + .findMergedAnnotation(rawType, org.springframework.data.mongodb.core.annotation.Collation.class); + + if (collationAnnotation != null && StringUtils.hasText(collationAnnotation.value())) { + this.collation = collationAnnotation.value(); + this.collationExpression = detectExpression(collationAnnotation.value()); + } else { this.collation = null; this.collationExpression = null; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepository.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepository.java index 735a4fda53..10b48efe00 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepository.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepository.java @@ -124,6 +124,7 @@ public Optional findById(ID id) { Assert.notNull(id, "The given id must not be null"); Query query = getIdQuery(id); + applyDefaultCollation(query); getReadPreference().ifPresent(query::withReadPreference); return Optional.ofNullable( @@ -136,6 +137,7 @@ public boolean existsById(ID id) { Assert.notNull(id, "The given id must not be null"); Query query = getIdQuery(id); + applyDefaultCollation(query); getReadPreference().ifPresent(query::withReadPreference); return mongoOperations.exists(query, entityInformation.getJavaType(), entityInformation.getCollectionName()); @@ -158,6 +160,7 @@ public List findAllById(Iterable ids) { public long count() { Query query = new Query(); + applyDefaultCollation(query); getReadPreference().ifPresent(query::withReadPreference); return mongoOperations.count(query, entityInformation.getCollectionName()); } @@ -168,6 +171,7 @@ public void deleteById(ID id) { Assert.notNull(id, "The given id must not be null"); Query query = getIdQuery(id); + applyDefaultCollation(query); getReadPreference().ifPresent(query::withReadPreference); mongoOperations.remove(query, entityInformation.getJavaType(), entityInformation.getCollectionName()); } @@ -193,6 +197,7 @@ public void deleteAllById(Iterable ids) { Assert.notNull(ids, "The given Iterable of ids must not be null"); Query query = getIdQuery(ids); + applyDefaultCollation(query); getReadPreference().ifPresent(query::withReadPreference); mongoOperations.remove(query, entityInformation.getJavaType(), entityInformation.getCollectionName()); } @@ -209,6 +214,7 @@ public void deleteAll(Iterable entities) { public void deleteAll() { Query query = new Query(); + applyDefaultCollation(query); getReadPreference().ifPresent(query::withReadPreference); mongoOperations.remove(query, entityInformation.getCollectionName()); @@ -385,6 +391,7 @@ private Criteria getIdCriteria(Object id) { private Query getIdQuery(Iterable ids) { Query query = new Query(new Criteria(entityInformation.getIdAttribute()).in(toCollection(ids))); + applyDefaultCollation(query); getReadPreference().ifPresent(query::withReadPreference); return query; } @@ -400,10 +407,18 @@ private List findAll(@Nullable Query query) { return Collections.emptyList(); } + applyDefaultCollation(query); getReadPreference().ifPresent(query::withReadPreference); return mongoOperations.find(query, entityInformation.getJavaType(), entityInformation.getCollectionName()); } + private void applyDefaultCollation(Query query) { + + if (entityInformation.hasCollation() && !query.getCollation().isPresent()) { + query.collation(entityInformation.getCollation()); + } + } + /** * {@link org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery} using {@link Example}. * diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleReactiveMongoRepository.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleReactiveMongoRepository.java index 5fc0c8c867..92d8213a46 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleReactiveMongoRepository.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleReactiveMongoRepository.java @@ -130,6 +130,7 @@ public Mono findById(ID id) { Assert.notNull(id, "The given id must not be null"); Query query = getIdQuery(id); + applyDefaultCollation(query); getReadPreference().ifPresent(query::withReadPreference); return mongoOperations.findOne(query, entityInformation.getJavaType(), entityInformation.getCollectionName()); } @@ -142,6 +143,7 @@ public Mono findById(Publisher publisher) { return Mono.from(publisher).flatMap(id -> { Query query = getIdQuery(id); + applyDefaultCollation(query); readPreference.ifPresent(query::withReadPreference); return mongoOperations.findOne(query, entityInformation.getJavaType(), entityInformation.getCollectionName()); }); @@ -153,6 +155,7 @@ public Mono existsById(ID id) { Assert.notNull(id, "The given id must not be null"); Query query = getIdQuery(id); + applyDefaultCollation(query); getReadPreference().ifPresent(query::withReadPreference); return mongoOperations.exists(query, entityInformation.getJavaType(), entityInformation.getCollectionName()); } @@ -165,6 +168,7 @@ public Mono existsById(Publisher publisher) { return Mono.from(publisher).flatMap(id -> { Query query = getIdQuery(id); + applyDefaultCollation(query); readPreference.ifPresent(query::withReadPreference); return mongoOperations.exists(query, entityInformation.getJavaType(), entityInformation.getCollectionName()); }); @@ -191,6 +195,7 @@ public Flux findAllById(Publisher ids) { Optional readPreference = getReadPreference(); return Flux.from(ids).buffer().flatMapSequential(listOfIds -> { Query query = getIdQuery(listOfIds); + applyDefaultCollation(query); readPreference.ifPresent(query::withReadPreference); return mongoOperations.find(query, entityInformation.getJavaType(), entityInformation.getCollectionName()); }); @@ -200,6 +205,7 @@ public Flux findAllById(Publisher ids) { public Mono count() { Query query = new Query(); + applyDefaultCollation(query); getReadPreference().ifPresent(query::withReadPreference); return mongoOperations.count(query, entityInformation.getCollectionName()); } @@ -217,6 +223,7 @@ private Mono deleteById(ID id, Optional readPreference) { Assert.notNull(id, "The given id must not be null"); Query query = getIdQuery(id); + applyDefaultCollation(query); readPreference.ifPresent(query::withReadPreference); return mongoOperations.remove(query, entityInformation.getJavaType(), entityInformation.getCollectionName()).then(); } @@ -230,6 +237,7 @@ public Mono deleteById(Publisher publisher) { return Mono.from(publisher).flatMap(id -> { Query query = getIdQuery(id); + applyDefaultCollation(query); readPreference.ifPresent(query::withReadPreference); return mongoOperations.remove(query, entityInformation.getJavaType(), entityInformation.getCollectionName()); }).then(); @@ -272,6 +280,7 @@ public Mono deleteAllById(Iterable ids) { private Mono deleteAllById(Iterable ids, Optional readPreference) { Query query = getIdQuery(ids); + applyDefaultCollation(query); readPreference.ifPresent(query::withReadPreference); return mongoOperations.remove(query, entityInformation.getJavaType(), entityInformation.getCollectionName()).then(); @@ -302,6 +311,7 @@ public Mono deleteAll(Publisher entityStream) { @Override public Mono deleteAll() { Query query = new Query(); + applyDefaultCollation(query); getReadPreference().ifPresent(query::withReadPreference); return mongoOperations.remove(query, entityInformation.getCollectionName()).then(Mono.empty()); } @@ -444,10 +454,18 @@ void setRepositoryMethodMetadata(CrudMethodMetadata crudMethodMetadata) { private Flux findAll(Query query) { + applyDefaultCollation(query); getReadPreference().ifPresent(query::withReadPreference); return mongoOperations.find(query, entityInformation.getJavaType(), entityInformation.getCollectionName()); } + private void applyDefaultCollation(Query query) { + + if (entityInformation.hasCollation() && !query.getCollation().isPresent()) { + query.collation(entityInformation.getCollation()); + } + } + private Optional getReadPreference() { if (crudMethodMetadata == null) { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntityUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntityUnitTests.java index 23231dde29..24c7ef26fa 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntityUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntityUnitTests.java @@ -296,6 +296,24 @@ void usesCorrectExpressionsForCollectionAndCollation() { assertThat(entity.getCollation()).isEqualTo(Collation.of("en_US")); } + @Test // GH-4535 + void readsStandaloneCollationAnnotation() { + + BasicMongoPersistentEntity entity = new BasicMongoPersistentEntity<>( + TypeInformation.of(WithStandaloneCollation.class)); + + assertThat(entity.getCollation()).isEqualTo(Collation.of("en_US")); + } + + @Test // GH-4535 + void readsStandaloneCollationAnnotationWithoutDocumentAnnotation() { + + BasicMongoPersistentEntity entity = new BasicMongoPersistentEntity<>( + TypeInformation.of(WithStandaloneCollationOnly.class)); + + assertThat(entity.getCollation()).isEqualTo(Collation.of("en_US")); + } + @Test // DATAMONGO-2341 void detectsShardedEntityCorrectly() { @@ -398,6 +416,13 @@ class WithSimpleCollation {} @Document(collation = "{ 'locale' : 'en_US' }") class WithDocumentCollation {} + @Document + @org.springframework.data.mongodb.core.annotation.Collation("en_US") + class WithStandaloneCollation {} + + @org.springframework.data.mongodb.core.annotation.Collation("en_US") + class WithStandaloneCollationOnly {} + @Sharded private class WithDefaultShardKey {} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepositoryUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepositoryUnitTests.java index 2437aa7a0a..b46793777e 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepositoryUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepositoryUnitTests.java @@ -145,6 +145,105 @@ public void shouldAddDefaultCollationToFindOneForExampleIfPresent() { assertThat(query.getValue().getCollation()).contains(collation); } + @ParameterizedTest // GH-4535 + @MethodSource("findCallsThatAcceptDefaultCollation") + void shouldAddDefaultCollationToFindMethods( + Consumer> findCall) { + + Collation collation = Collation.of("en_US"); + when(entityInformation.hasCollation()).thenReturn(true); + when(entityInformation.getCollation()).thenReturn(collation); + + findCall.accept(repository); + + ArgumentCaptor query = ArgumentCaptor.forClass(Query.class); + verify(mongoOperations).find(query.capture(), any(), any()); + + assertThat(query.getValue().getCollation()).contains(collation); + } + + @Test // GH-4535 + void shouldAddDefaultCollationToFindById() { + + Collation collation = Collation.of("en_US"); + when(entityInformation.hasCollation()).thenReturn(true); + when(entityInformation.getCollation()).thenReturn(collation); + when(entityInformation.getIdAttribute()).thenReturn("id"); + + repository.findById("42"); + + ArgumentCaptor query = ArgumentCaptor.forClass(Query.class); + verify(mongoOperations).findOne(query.capture(), any(), any()); + + assertThat(query.getValue().getCollation()).contains(collation); + } + + @Test // GH-4535 + void shouldAddDefaultCollationToExistsById() { + + Collation collation = Collation.of("en_US"); + when(entityInformation.hasCollation()).thenReturn(true); + when(entityInformation.getCollation()).thenReturn(collation); + when(entityInformation.getIdAttribute()).thenReturn("id"); + + repository.existsById("42"); + + ArgumentCaptor query = ArgumentCaptor.forClass(Query.class); + verify(mongoOperations).exists(query.capture(), any(), any()); + + assertThat(query.getValue().getCollation()).contains(collation); + } + + @Test // GH-4535 + void shouldAddDefaultCollationToCount() { + + Collation collation = Collation.of("en_US"); + when(entityInformation.hasCollation()).thenReturn(true); + when(entityInformation.getCollation()).thenReturn(collation); + when(entityInformation.getCollectionName()).thenReturn("documents"); + + repository.count(); + + ArgumentCaptor query = ArgumentCaptor.forClass(Query.class); + verify(mongoOperations).count(query.capture(), any(String.class)); + + assertThat(query.getValue().getCollation()).contains(collation); + } + + @Test // GH-4535 + void shouldAddDefaultCollationToDeleteById() { + + Collation collation = Collation.of("en_US"); + when(entityInformation.hasCollation()).thenReturn(true); + when(entityInformation.getCollation()).thenReturn(collation); + when(entityInformation.getIdAttribute()).thenReturn("id"); + when(entityInformation.getJavaType()).thenReturn(Object.class); + when(entityInformation.getCollectionName()).thenReturn("documents"); + + repository.deleteById("42"); + + ArgumentCaptor query = ArgumentCaptor.forClass(Query.class); + verify(mongoOperations).remove(query.capture(), any(Class.class), any(String.class)); + + assertThat(query.getValue().getCollation()).contains(collation); + } + + @Test // GH-4535 + void shouldAddDefaultCollationToDeleteAll() { + + Collation collation = Collation.of("en_US"); + when(entityInformation.hasCollation()).thenReturn(true); + when(entityInformation.getCollation()).thenReturn(collation); + when(entityInformation.getCollectionName()).thenReturn("documents"); + + repository.deleteAll(); + + ArgumentCaptor query = ArgumentCaptor.forClass(Query.class); + verify(mongoOperations).remove(query.capture(), any(String.class)); + + assertThat(query.getValue().getCollation()).contains(collation); + } + @ParameterizedTest // GH-2971 @MethodSource("findAllCalls") void shouldAddReadPreferenceToFindAllMethods(Consumer> findCall) @@ -219,6 +318,16 @@ private static Stream findAllCalls() { Arguments.of(findAllWithExampleAndPage)); } + private static Stream findCallsThatAcceptDefaultCollation() { + + Consumer> findAll = SimpleMongoRepository::findAll; + Consumer> findAllWithSort = repo -> repo.findAll(Sort.by("age")); + Consumer> findAllWithPage = repo -> repo + .findAll(PageRequest.of(1, 20, Sort.by("age"))); + + return Stream.of(Arguments.of(findAll), Arguments.of(findAllWithSort), Arguments.of(findAllWithPage)); + } + static class TestDummy { } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleReactiveMongoRepositoryUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleReactiveMongoRepositoryUnitTests.java index a3c541a176..aaf7621a01 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleReactiveMongoRepositoryUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleReactiveMongoRepositoryUnitTests.java @@ -150,6 +150,61 @@ void shouldAddDefaultCollationToFindOneForExampleIfPresent() { assertThat(query.getValue().getCollation()).contains(collation); } + @Test // GH-4535 + void shouldAddDefaultCollationToFindById() { + + when(mongoOperations.findOne(any(), any(), any())).thenReturn(mono); + + Collation collation = Collation.of("en_US"); + when(entityInformation.hasCollation()).thenReturn(true); + when(entityInformation.getCollation()).thenReturn(collation); + when(entityInformation.getIdAttribute()).thenReturn("id"); + + repository.findById("42").subscribe(); + + ArgumentCaptor query = ArgumentCaptor.forClass(Query.class); + verify(mongoOperations).findOne(query.capture(), any(), any()); + + assertThat(query.getValue().getCollation()).contains(collation); + } + + @Test // GH-4535 + void shouldAddDefaultCollationToCount() { + + when(mongoOperations.count(any(), anyString())).thenReturn(mono); + + Collation collation = Collation.of("en_US"); + when(entityInformation.hasCollation()).thenReturn(true); + when(entityInformation.getCollation()).thenReturn(collation); + when(entityInformation.getCollectionName()).thenReturn("testdummy"); + + repository.count().subscribe(); + + ArgumentCaptor query = ArgumentCaptor.forClass(Query.class); + verify(mongoOperations).count(query.capture(), anyString()); + + assertThat(query.getValue().getCollation()).contains(collation); + } + + @Test // GH-4535 + void shouldAddDefaultCollationToDeleteAll() { + + when(mongoOperations.remove(any(Query.class), anyString())).thenReturn(mono); + when(mono.then(any(Mono.class))).thenReturn(Mono.empty()); + + Collation collation = Collation.of("en_US"); + when(entityInformation.hasCollation()).thenReturn(true); + when(entityInformation.getCollation()).thenReturn(collation); + when(entityInformation.getCollectionName()).thenReturn("testdummy"); + + repository.deleteAll().subscribe(); + + ArgumentCaptor query = ArgumentCaptor.forClass(Query.class); + verify(mongoOperations).remove(query.capture(), anyString()); + + assertThat(query.getValue().getCollation()).contains(collation); + } + @ParameterizedTest // GH-2971 @MethodSource("findAllCalls") void shouldAddReadPreferenceToFindAllMethods(