From 332e63ab8c66834b45fe3103ae21ec0c12edd0bf Mon Sep 17 00:00:00 2001 From: Julia <5765049+sxhinzvc@users.noreply.github.com> Date: Wed, 11 Oct 2023 16:04:51 -0400 Subject: [PATCH 1/5] Add support for Compound Wildcard Indexes. See #4471 --- .../CompoundWildcardIndexDefinition.java | 60 +++++++ .../core/index/CompoundWildcardIndexed.java | 131 +++++++++++++++ .../MongoPersistentEntityIndexResolver.java | 61 ++++++- ...ersistentEntityIndexResolverUnitTests.java | 154 +++++++++++++++++- 4 files changed, 400 insertions(+), 6 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/CompoundWildcardIndexDefinition.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/CompoundWildcardIndexed.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/CompoundWildcardIndexDefinition.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/CompoundWildcardIndexDefinition.java new file mode 100644 index 0000000000..8dc99cd84e --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/CompoundWildcardIndexDefinition.java @@ -0,0 +1,60 @@ +/* + * Copyright 2014-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.core.index; + +import org.bson.Document; +import org.springframework.util.Assert; + +/** + * {@link CompoundWildcardIndexDefinition} is a specific {@link Index} that includes one {@link WildcardIndex} and + * one or more non-wildcard fields. + * + * @author Julia Lee + * @since 4.2 + */ +public class CompoundWildcardIndexDefinition extends WildcardIndex { + + private final Document indexKeys; + + /** + * Creates a new {@link CompoundWildcardIndexDefinition} for the given {@literal wildcardPath} and {@literal keys}. + * If {@literal wildcardPath} is empty, the wildcard index will apply to the root entity, using {@code $**}. + *
+ * + * @param wildcardPath can be a {@literal empty} {@link String}. + */ + public CompoundWildcardIndexDefinition(String wildcardPath, Document indexKeys) { + + super(wildcardPath); + this.indexKeys = indexKeys; + } + + @Override + public Document getIndexKeys() { + + Document document = new Document(); + document.putAll(indexKeys); + document.putAll(super.getIndexKeys()); + return document; + } + + @Override + public Document getIndexOptions() { + + Document options = super.getIndexOptions(); + return options; + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/CompoundWildcardIndexed.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/CompoundWildcardIndexed.java new file mode 100644 index 0000000000..78b06f2ac2 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/CompoundWildcardIndexed.java @@ -0,0 +1,131 @@ +/* + * Copyright 2011-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.core.index; + +import org.springframework.core.annotation.AliasFor; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Mark a class to use compound wildcard indexes.
+ * + *
+ * @Document
+ * @CompoundWildcardIndexed(wildcardFieldName = "address", fields = "{'firstname': 1}")
+ * class Person {
+ * 	String firstname;
+ * 	Address address;
+ * }
+ *
+ * db.product.createIndex({"address.$**": 1, "firstname": 1})
+ * 
+ * + * {@literal wildcardProjection} can be used to specify keys to in-/exclude in the index. + * + *
+ *
+ * @Document
+ * @CompoundWildcardIndexed(wildcardProjection = "{'address.zip': 0}", fields = "{'firstname': 1}")
+ * class Person {
+ * 	String firstname;
+ * 	Address address;
+ * }
+ *
+ * db.user.createIndex({"$**": 1, "firstname": 1}, {"wildcardProjection": {"address.zip": 0}})
+ * 
+ * + * @author Julia Lee + */ +@Target({ ElementType.TYPE }) +@Documented +@WildcardIndexed +@CompoundIndex +@Retention(RetentionPolicy.RUNTIME) +public @interface CompoundWildcardIndexed { + + /** + * The name of the sub-field to which a wildcard index is applied. If empty, the wildcard term will resolve to "$**". + * + * @return empty by default. + */ + String wildcardFieldName() default ""; + + /** + * Explicitly specify sub-fields to be in-/excluded as a {@link org.bson.Document#parse(String) parsable} String. + *
+ * NOTE: Can only be applied on when wildcard term is "$**" + * + * @return empty by default. + */ + @AliasFor(annotation = WildcardIndexed.class, attribute = "wildcardProjection") + String wildcardProjection() default ""; + + /** + * Definition of non-wildcard index(es) in JSON format, wherein the keys are the fields to be indexed and the values + * define the index direction (1 for ascending, -1 for descending).
+ * + *
+   * @Document
+   * @CompoundWildcardIndexed(wildcardProjection = "{ 'address.zip' : 0 }", fields = "{'firstname': 1}")
+   * class Person {
+   * 	String firstname;
+   * 	Address address;
+   * }
+	 * 
+ * + * @return empty String by default. + */ + @AliasFor(annotation = CompoundIndex.class, attribute = "def") + String fields(); + + /** + * Index name either as plain value or as {@link org.springframework.expression.spel.standard.SpelExpression template + * expression}.
+ * + * @return empty by default. + */ + @AliasFor(annotation = WildcardIndexed.class, attribute = "name") + String name() default ""; + + /** + * If set to {@literal true} then MongoDB will ignore the given index name and instead generate a new name. Defaults + * to {@literal false}. + * + * @return {@literal false} by default + */ + @AliasFor(annotation = WildcardIndexed.class, attribute = "useGeneratedName") + boolean useGeneratedName() default false; + + /** + * Only index the documents in a collection that meet a specified {@link IndexFilter filter expression}.
+ * + * @return empty by default. + */ + @AliasFor(annotation = WildcardIndexed.class, attribute = "partialFilter") + String partialFilter() default ""; + + /** + * Defines the collation to apply. + * + * @return an empty {@link String} by default. + */ + @AliasFor(annotation = WildcardIndexed.class, attribute = "collation") + String collation() default ""; +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java index 86c896e7ff..9e36d2a601 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java @@ -79,6 +79,7 @@ * @author Mark Paluch * @author Dave Perryman * @author Stefan Tirea + * @author Julia Lee * @since 1.5 */ public class MongoPersistentEntityIndexResolver implements IndexResolver { @@ -129,6 +130,7 @@ public List resolveIndexForEntity(MongoPersistentEntity) property -> this .potentiallyAddIndexForProperty(root, property, indexInformation, new CycleGuard())); @@ -154,6 +156,22 @@ private void verifyWildcardIndexedProjection(MongoPersistentEntity entity) { } } }); + + if (entity.isAnnotationPresent(CompoundWildcardIndexed.class)) { + CompoundWildcardIndexed indexed = entity.getRequiredAnnotation(CompoundWildcardIndexed.class); + + if (!ObjectUtils.isEmpty(indexed.wildcardFieldName()) && !ObjectUtils.isEmpty(indexed.wildcardProjection())) { + + throw new MappingException( + String.format("CompoundWildcardIndex.wildcardProjection is only allowed on \"$**\"; Offending property: %s", + indexed.wildcardFieldName())); + } + + if (ObjectUtils.isEmpty(indexed.wildcardFieldName()) && ObjectUtils.isEmpty(indexed.wildcardProjection())) { + + throw new MappingException(String.format("CompoundWildcardIndex.wildcardProjection is required on \"$**\"")); + } + } } private void potentiallyAddIndexForProperty(MongoPersistentEntity root, MongoPersistentProperty persistentProperty, @@ -280,7 +298,8 @@ private List createIndexDefinitionHolderForProperty(Strin private List potentiallyCreateCompoundIndexDefinitions(String dotPath, String collection, MongoPersistentEntity entity) { - if (entity.findAnnotation(CompoundIndexes.class) == null && entity.findAnnotation(CompoundIndex.class) == null) { + if ((!entity.isAnnotationPresent(CompoundIndexes.class) && !entity.isAnnotationPresent(CompoundIndex.class)) + || entity.isAnnotationPresent(CompoundWildcardIndexed.class)) { return Collections.emptyList(); } @@ -290,7 +309,8 @@ private List potentiallyCreateCompoundIndexDefinitions(St private List potentiallyCreateWildcardIndexDefinitions(String dotPath, String collection, MongoPersistentEntity entity) { - if (!entity.isAnnotationPresent(WildcardIndexed.class)) { + if (!entity.isAnnotationPresent(WildcardIndexed.class) + || entity.isAnnotationPresent(CompoundWildcardIndexed.class)) { return Collections.emptyList(); } @@ -345,6 +365,19 @@ private Collection potentiallyCreateTextIndexDe } + private Collection potentiallyCreateCompoundWildcardDefinition( + MongoPersistentEntity entity, String collection) { + + if (!entity.isAnnotationPresent(CompoundWildcardIndexed.class)) { + return Collections.emptyList(); + } + + CompoundWildcardIndexed compoundWildcardIndex = entity.getRequiredAnnotation(CompoundWildcardIndexed.class); + IndexDefinitionHolder compoundWildcardIndexDefinition = createCompoundWildcardIndexDefinition(collection, + compoundWildcardIndex, entity); + return Collections.singletonList(compoundWildcardIndexDefinition); + } + private void appendTextIndexInformation(DotPath dotPath, Path path, TextIndexDefinitionBuilder indexDefinitionBuilder, MongoPersistentEntity entity, TextIndexIncludeOptions includeOptions, CycleGuard guard) { @@ -483,6 +516,30 @@ protected IndexDefinitionHolder createWildcardIndexDefinition(String dotPath, St return new IndexDefinitionHolder(dotPath, indexDefinition, collection); } + protected IndexDefinitionHolder createCompoundWildcardIndexDefinition(String collection, CompoundWildcardIndexed index, + @Nullable MongoPersistentEntity entity) { + + String wildcardField = index.wildcardFieldName(); + org.bson.Document indexKeys = resolveCompoundIndexKeyFromStringDefinition("", index.fields(), entity); + + CompoundWildcardIndexDefinition indexDefinition = new CompoundWildcardIndexDefinition(wildcardField, indexKeys); + + if (StringUtils.hasText(index.wildcardProjection()) && ObjectUtils.isEmpty(wildcardField)) { + indexDefinition.wildcardProjection(evaluateWildcardProjection(index.wildcardProjection(), entity)); + } + + if (StringUtils.hasText(index.partialFilter())) { + indexDefinition.partial(evaluatePartialFilter(index.partialFilter(), entity)); + } + + if (!index.useGeneratedName()) { + indexDefinition.named(pathAwareIndexName(index.name(), "", entity, null)); + } + + indexDefinition.collation(resolveCollation(index, entity)); + return new IndexDefinitionHolder("", indexDefinition, collection); + } + private org.bson.Document resolveCompoundIndexKeyFromStringDefinition(String dotPath, String keyDefinitionString, PersistentEntity entity) { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java index 0cfb8bd09f..37ed482825 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java @@ -40,6 +40,7 @@ import org.springframework.data.mongodb.core.DocumentTestUtils; import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexResolver.IndexDefinitionHolder; import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexResolverUnitTests.CompoundIndexResolutionTests; +import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexResolverUnitTests.CompoundWildcardIndexResolutionTests; import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexResolverUnitTests.GeoSpatialIndexResolutionTests; import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexResolverUnitTests.IndexResolutionTests; import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexResolverUnitTests.MixedIndexResolutionTests; @@ -62,10 +63,11 @@ * @author Mark Paluch * @author Dave Perryman * @author Stefan Tirea + * @author Julia Lee */ @RunWith(Suite.class) @SuiteClasses({ IndexResolutionTests.class, GeoSpatialIndexResolutionTests.class, CompoundIndexResolutionTests.class, - TextIndexedResolutionTests.class, MixedIndexResolutionTests.class }) + CompoundWildcardIndexResolutionTests.class, TextIndexedResolutionTests.class, MixedIndexResolutionTests.class }) @SuppressWarnings("unused") public class MongoPersistentEntityIndexResolverUnitTests { @@ -601,7 +603,7 @@ public void compoundIndexOnSuperClassResolvedCorrectly() { public void compoundIndexDoesNotSpecifyNameWhenUsingGenerateName() { List indexDefinitions = prepareMappingContextAndResolveIndexForType( - ComountIndexWithAutogeneratedName.class); + CompoundIndexWithAutogeneratedName.class); IndexDefinition indexDefinition = indexDefinitions.get(0).getIndexDefinition(); assertThat(indexDefinition.getIndexOptions()) @@ -766,10 +768,10 @@ class SingleCompoundIndex {} class IndexDefinedOnSuperClass extends CompoundIndexOnLevelZero {} - @Document("ComountIndexWithAutogeneratedName") + @Document("CompoundIndexWithAutogeneratedName") @CompoundIndexes({ @CompoundIndex(useGeneratedName = true, def = "{'foo': 1, 'bar': -1}", background = true, sparse = true, unique = true) }) - class ComountIndexWithAutogeneratedName {} + class CompoundIndexWithAutogeneratedName {} @Document("WithComposedAnnotation") @ComposedCompoundIndex @@ -829,6 +831,150 @@ class WithCompoundCollationFromDocument {} class WithEvaluatedCollationFromCompoundIndex {} } + /** + * Test resolution of {@link CompoundWildcardIndexed}. + * + * @author Julia Lee + */ + public static class CompoundWildcardIndexResolutionTests { + + @Test // GH-4471 + public void compoundWildcardIndexOnSingleField() { + + List indexDefinitions = prepareMappingContextAndResolveIndexForType( + CompoundWildcardIndexOnFields.class); + + assertThat(indexDefinitions).hasSize(1); + assertIndexPathAndCollection(new String[] { "foo.$**", "bar", "baz" }, "CompoundWildcardIndexOnSingleField", + indexDefinitions.get(0)); + } + + @Test // GH-4471 + public void compoundWildcardIndexOnEntityWithProjection() { + + List indexDefinitions = prepareMappingContextAndResolveIndexForType( + CompoundWildcardIndexOnEntity.class); + + assertThat(indexDefinitions).hasSize(1); + assertThat(indexDefinitions.get(0)).satisfies(it -> { + assertThat(it.getIndexKeys()).isEqualTo(new org.bson.Document().append("$**", 1).append("bar", -1)); + assertThat(it.getIndexOptions()).containsEntry("wildcardProjection", + org.bson.Document.parse("{'foo.something' : 0}")); + }); + } + + @Test // GH-4471 + public void compoundWildcardIndexWithOptions() { + + List indexDefinitions = prepareMappingContextAndResolveIndexForType( + CompoundWildcardIndexWithOptions.class); + + assertThat(indexDefinitions).hasSize(1); + assertThat(indexDefinitions.get(0)).satisfies(it -> { + assertThat(it.getIndexKeys()).isEqualTo(new org.bson.Document().append("$**", 1).append("foo", 1)); + + org.bson.Document indexOptions = it.getIndexOptions(); + assertThat(indexOptions).containsEntry("name", "my_index_name"); + assertThat(indexOptions).containsEntry("wildcardProjection", org.bson.Document.parse("{'bar.something' : 1}")); + assertThat(indexOptions).containsEntry("collation", + new org.bson.Document().append("locale", "en_US").append("strength", 2)); + assertThat(indexOptions).containsEntry("partialFilterExpression", + org.bson.Document.parse("{'value': {'$exists': true}}")); + }); + } + + @Test // GH-4471 + public void compoundWildcardIndexWithCollationFromDocumentAnnotation() { + + List indexDefinitions = prepareMappingContextAndResolveIndexForType( + CompoundWildcardIndexWithCollationOnDocument.class); + + assertThat(indexDefinitions.get(0)).satisfies(it -> { + assertThat(it.getIndexKeys()).isEqualTo(new org.bson.Document().append("foo.$**", 1).append("bar", 1)); + assertThat(it.getIndexOptions()).containsEntry("collation", + new org.bson.Document().append("locale", "en_US").append("strength", 2)); + }); + } + + @Test // GH-4471 + public void compoundWildcardIndexWithEvaluatedCollationFromAnnotation() { + + List indexDefinitions = prepareMappingContextAndResolveIndexForType( + CompoundWildcardIndexWithEvaluatedCollation.class); + + assertThat(indexDefinitions.get(0)).satisfies(it -> { + assertThat(it.getIndexKeys()).isEqualTo(new org.bson.Document().append("foo.$**", 1).append("bar", 1)); + assertThat(it.getIndexOptions()).containsEntry("collation", new org.bson.Document().append("locale", "de_AT")); + }); + } + + @Test // GH-4471 + public void rejectsWildcardProjectionOnSingleField() { + + assertThatExceptionOfType(MappingException.class).isThrownBy(() -> + prepareMappingContextAndResolveIndexForType(IncorrectCompoundWildcardIndexOnFieldWithProjection.class)); + } + + @Test // GH-4471 + public void requiresWildcardProjectionOnEntireEntity() { + + assertThatExceptionOfType(MappingException.class).isThrownBy(() -> + prepareMappingContextAndResolveIndexForType(IncorrectCompoundWildcardIndexOnEntityWithoutProjection.class)); + } + + @Test // GH-4471 + public void resolvesMultipleIndexesWithCompoundWildcardIndex() { + + List indexDefinitions = prepareMappingContextAndResolveIndexForType( + MultipleIndexes.class); + + assertThat(indexDefinitions).hasSize(2); + + assertThat(indexDefinitions.get(0).getIndexDefinition()).isInstanceOf(CompoundWildcardIndexDefinition.class); + assertThat(indexDefinitions.get(1).getIndexDefinition()).isInstanceOf(Index.class); + + assertThat(indexDefinitions.get(0).getIndexKeys()).isEqualTo(new org.bson.Document().append("foo.$**", 1) + .append("bar", 1)); + assertThat(indexDefinitions.get(1).getIndexKeys()).isEqualTo(new org.bson.Document().append("one", 1)); + } + + @Document("CompoundWildcardIndexOnSingleField") + @CompoundWildcardIndexed(wildcardFieldName = "foo", fields = "{'bar': 1, 'baz': 1}") + class CompoundWildcardIndexOnFields {} + + @Document + @CompoundWildcardIndexed(wildcardFieldName = "foo", wildcardProjection = "{}", fields = "{'bar': 1}") + class IncorrectCompoundWildcardIndexOnFieldWithProjection {} + + @Document + @CompoundWildcardIndexed(fields = "{ 'bar': 1 }") + class IncorrectCompoundWildcardIndexOnEntityWithoutProjection {} + + @Document + @CompoundWildcardIndexed(wildcardProjection = "{'foo.something' : 0}", fields = "{'bar': -1}") + class CompoundWildcardIndexOnEntity {} + + @Document + @CompoundWildcardIndexed(fields = "{'foo': 1}", wildcardProjection = "{'bar.something': 1}", name = "my_index_name", + collation = "{'locale': 'en_US', 'strength': 2}", partialFilter = "{'value': {'$exists': true}}") + class CompoundWildcardIndexWithOptions {} + + @Document(collation = "{'locale': 'en_US', 'strength': 2}") + @CompoundWildcardIndexed(wildcardFieldName = "foo", fields = "{'bar': 1}") + class CompoundWildcardIndexWithCollationOnDocument {} + + @Document(collation = "{'locale': 'en_US', 'strength': 2}") + @CompoundWildcardIndexed(wildcardFieldName = "foo", fields = "{'bar': 1}", collation = "#{{'locale' : 'de' + '_' + 'AT'}}") + class CompoundWildcardIndexWithEvaluatedCollation {} + + @Document + @CompoundWildcardIndexed(wildcardFieldName = "foo", fields = "{'bar': 1}") + class MultipleIndexes { + @Indexed String one; + } + + } + public static class TextIndexedResolutionTests { @Test // DATAMONGO-937 From c23828829063cfee56abbac414f9bea604f88614 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Mon, 16 Sep 2024 11:30:22 +0200 Subject: [PATCH 2/5] WIP --- pom.xml | 2 +- spring-data-mongodb-benchmarks/pom.xml | 2 +- spring-data-mongodb-distribution/pom.xml | 2 +- spring-data-mongodb/pom.xml | 2 +- .../mongodb/core/index/CompoundWildcardIndexDefinition.java | 4 ++-- .../data/mongodb/core/index/CompoundWildcardIndexed.java | 4 +++- 6 files changed, 9 insertions(+), 7 deletions(-) diff --git a/pom.xml b/pom.xml index 3fa54424af..d45de05d01 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 4.4.0-SNAPSHOT + 4.4.0-4471-SNAPSHOT pom Spring Data MongoDB diff --git a/spring-data-mongodb-benchmarks/pom.xml b/spring-data-mongodb-benchmarks/pom.xml index a3dc49f892..b5a14bf06c 100644 --- a/spring-data-mongodb-benchmarks/pom.xml +++ b/spring-data-mongodb-benchmarks/pom.xml @@ -7,7 +7,7 @@ org.springframework.data spring-data-mongodb-parent - 4.4.0-SNAPSHOT + 4.4.0-4471-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index acdc13437d..8078eb0d42 100644 --- a/spring-data-mongodb-distribution/pom.xml +++ b/spring-data-mongodb-distribution/pom.xml @@ -15,7 +15,7 @@ org.springframework.data spring-data-mongodb-parent - 4.4.0-SNAPSHOT + 4.4.0-4471-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index fafe9c8793..6c33055163 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -13,7 +13,7 @@ org.springframework.data spring-data-mongodb-parent - 4.4.0-SNAPSHOT + 4.4.0-4471-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/CompoundWildcardIndexDefinition.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/CompoundWildcardIndexDefinition.java index 8dc99cd84e..a30323ef17 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/CompoundWildcardIndexDefinition.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/CompoundWildcardIndexDefinition.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2023 the original author or authors. + * Copyright 2014-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,7 +23,7 @@ * one or more non-wildcard fields. * * @author Julia Lee - * @since 4.2 + * @since 4.4 */ public class CompoundWildcardIndexDefinition extends WildcardIndex { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/CompoundWildcardIndexed.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/CompoundWildcardIndexed.java index 78b06f2ac2..0069dc8ac3 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/CompoundWildcardIndexed.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/CompoundWildcardIndexed.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2023 the original author or authors. + * Copyright 2011-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -52,6 +52,8 @@ * * * @author Julia Lee + * @author Marcin Grzejszczak + * @since 4.4.0 */ @Target({ ElementType.TYPE }) @Documented From 4d56a2ded16362073516207565d92b6eae7ea4b6 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Tue, 24 Sep 2024 16:35:43 +0200 Subject: [PATCH 3/5] Changed the default value of wildcardFieldName to * --- .../mongodb/core/index/CompoundWildcardIndexed.java | 13 +++++++++---- .../index/MongoPersistentEntityIndexResolver.java | 12 ++++++++---- .../data/mongodb/core/index/WildcardIndex.java | 2 +- .../data/mongodb/core/index/WildcardIndexed.java | 2 +- 4 files changed, 19 insertions(+), 10 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/CompoundWildcardIndexed.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/CompoundWildcardIndexed.java index 0069dc8ac3..5a008f3c15 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/CompoundWildcardIndexed.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/CompoundWildcardIndexed.java @@ -63,16 +63,21 @@ public @interface CompoundWildcardIndexed { /** - * The name of the sub-field to which a wildcard index is applied. If empty, the wildcard term will resolve to "$**". + * Represents wildcard for all fields starting from the root od the document. + */ + String ALL_FIELDS = "$**"; + + /** + * The name of the sub-field to which a wildcard index is applied. The default value scans all fields. * - * @return empty by default. + * @return {@link #ALL_FIELDS} by default. */ - String wildcardFieldName() default ""; + String wildcardFieldName() default ALL_FIELDS; /** * Explicitly specify sub-fields to be in-/excluded as a {@link org.bson.Document#parse(String) parsable} String. *
- * NOTE: Can only be applied on when wildcard term is "$**" + * NOTE: Can only be applied on when wildcard term is {@link #ALL_FIELDS} * * @return empty by default. */ diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java index 9e36d2a601..05c95e1fee 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java @@ -160,20 +160,24 @@ private void verifyWildcardIndexedProjection(MongoPersistentEntity entity) { if (entity.isAnnotationPresent(CompoundWildcardIndexed.class)) { CompoundWildcardIndexed indexed = entity.getRequiredAnnotation(CompoundWildcardIndexed.class); - if (!ObjectUtils.isEmpty(indexed.wildcardFieldName()) && !ObjectUtils.isEmpty(indexed.wildcardProjection())) { + if (!isWildcardFromRoot(indexed.wildcardFieldName()) && !ObjectUtils.isEmpty(indexed.wildcardProjection())) { throw new MappingException( String.format("CompoundWildcardIndex.wildcardProjection is only allowed on \"$**\"; Offending property: %s", indexed.wildcardFieldName())); } - if (ObjectUtils.isEmpty(indexed.wildcardFieldName()) && ObjectUtils.isEmpty(indexed.wildcardProjection())) { + if (isWildcardFromRoot(indexed.wildcardFieldName()) && ObjectUtils.isEmpty(indexed.wildcardProjection())) { - throw new MappingException(String.format("CompoundWildcardIndex.wildcardProjection is required on \"$**\"")); + throw new MappingException("CompoundWildcardIndex.wildcardProjection is required on \"$**\""); } } } + private static boolean isWildcardFromRoot(String fieldName) { + return CompoundWildcardIndexed.ALL_FIELDS.equals(fieldName); + } + private void potentiallyAddIndexForProperty(MongoPersistentEntity root, MongoPersistentProperty persistentProperty, List indexes, CycleGuard guard) { @@ -524,7 +528,7 @@ protected IndexDefinitionHolder createCompoundWildcardIndexDefinition(String col CompoundWildcardIndexDefinition indexDefinition = new CompoundWildcardIndexDefinition(wildcardField, indexKeys); - if (StringUtils.hasText(index.wildcardProjection()) && ObjectUtils.isEmpty(wildcardField)) { + if (StringUtils.hasText(index.wildcardProjection()) && isWildcardFromRoot(wildcardField)) { indexDefinition.wildcardProjection(evaluateWildcardProjection(index.wildcardProjection(), entity)); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/WildcardIndex.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/WildcardIndex.java index 0668bd5926..34c1a66bdc 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/WildcardIndex.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/WildcardIndex.java @@ -179,7 +179,7 @@ public WildcardIndex wildcardProjection(Map includeExclude) { } private String getTargetFieldName() { - return StringUtils.hasText(fieldName) ? (fieldName + ".$**") : "$**"; + return (StringUtils.hasText(fieldName) && !CompoundWildcardIndexed.ALL_FIELDS.equals(fieldName)) ? (fieldName + ".$**") : "$**"; } @Override diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/WildcardIndexed.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/WildcardIndexed.java index 042f6f4f53..e3d8fb5b46 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/WildcardIndexed.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/WildcardIndexed.java @@ -117,7 +117,7 @@ String partialFilter() default ""; /** - * Explicitly specify sub fields to be in-/excluded as a {@link org.bson.Document#parse(String) prasable} String. + * Explicitly specify sub-fields to be in-/excluded as a {@link org.bson.Document#parse(String) prasable} String. *
* NOTE: Can only be applied on root level documents. * From 5077f9652658f4345827dbf8044676be9335a799 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Wed, 25 Sep 2024 11:39:06 +0200 Subject: [PATCH 4/5] Unified naming for Wildcard indexes --- ...ndexed.java => CompoundWildcardIndex.java} | 24 +++++++++---------- .../MongoPersistentEntityIndexResolver.java | 16 ++++++------- .../mongodb/core/index/WildcardIndex.java | 2 +- ...ersistentEntityIndexResolverUnitTests.java | 18 +++++++------- 4 files changed, 29 insertions(+), 31 deletions(-) rename spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/{CompoundWildcardIndexed.java => CompoundWildcardIndex.java} (85%) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/CompoundWildcardIndexed.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/CompoundWildcardIndex.java similarity index 85% rename from spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/CompoundWildcardIndexed.java rename to spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/CompoundWildcardIndex.java index 5a008f3c15..d22cf77b6a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/CompoundWildcardIndexed.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/CompoundWildcardIndex.java @@ -57,10 +57,9 @@ */ @Target({ ElementType.TYPE }) @Documented -@WildcardIndexed @CompoundIndex @Retention(RetentionPolicy.RUNTIME) -public @interface CompoundWildcardIndexed { +public @interface CompoundWildcardIndex { /** * Represents wildcard for all fields starting from the root od the document. @@ -81,7 +80,6 @@ * * @return empty by default. */ - @AliasFor(annotation = WildcardIndexed.class, attribute = "wildcardProjection") String wildcardProjection() default ""; /** @@ -89,12 +87,12 @@ * define the index direction (1 for ascending, -1 for descending).
* *
-   * @Document
-   * @CompoundWildcardIndexed(wildcardProjection = "{ 'address.zip' : 0 }", fields = "{'firstname': 1}")
-   * class Person {
-   * 	String firstname;
-   * 	Address address;
-   * }
+	 * @Document
+	 * @CompoundWildcardIndexed(wildcardProjection = "{ 'address.zip' : 0 }", fields = "{'firstname': 1}")
+	 * class Person {
+	 * 	String firstname;
+	 * 	Address address;
+	 * }
 	 * 
* * @return empty String by default. @@ -108,7 +106,7 @@ * * @return empty by default. */ - @AliasFor(annotation = WildcardIndexed.class, attribute = "name") + @AliasFor(annotation = CompoundIndex.class, attribute = "name") String name() default ""; /** @@ -117,7 +115,7 @@ * * @return {@literal false} by default */ - @AliasFor(annotation = WildcardIndexed.class, attribute = "useGeneratedName") + @AliasFor(annotation = CompoundIndex.class, attribute = "useGeneratedName") boolean useGeneratedName() default false; /** @@ -125,7 +123,7 @@ * * @return empty by default. */ - @AliasFor(annotation = WildcardIndexed.class, attribute = "partialFilter") + @AliasFor(annotation = CompoundIndex.class, attribute = "partialFilter") String partialFilter() default ""; /** @@ -133,6 +131,6 @@ * * @return an empty {@link String} by default. */ - @AliasFor(annotation = WildcardIndexed.class, attribute = "collation") + @AliasFor(annotation = CompoundIndex.class, attribute = "collation") String collation() default ""; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java index 05c95e1fee..b1a27cab62 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java @@ -157,8 +157,8 @@ private void verifyWildcardIndexedProjection(MongoPersistentEntity entity) { } }); - if (entity.isAnnotationPresent(CompoundWildcardIndexed.class)) { - CompoundWildcardIndexed indexed = entity.getRequiredAnnotation(CompoundWildcardIndexed.class); + if (entity.isAnnotationPresent(CompoundWildcardIndex.class)) { + CompoundWildcardIndex indexed = entity.getRequiredAnnotation(CompoundWildcardIndex.class); if (!isWildcardFromRoot(indexed.wildcardFieldName()) && !ObjectUtils.isEmpty(indexed.wildcardProjection())) { @@ -175,7 +175,7 @@ private void verifyWildcardIndexedProjection(MongoPersistentEntity entity) { } private static boolean isWildcardFromRoot(String fieldName) { - return CompoundWildcardIndexed.ALL_FIELDS.equals(fieldName); + return CompoundWildcardIndex.ALL_FIELDS.equals(fieldName); } private void potentiallyAddIndexForProperty(MongoPersistentEntity root, MongoPersistentProperty persistentProperty, @@ -303,7 +303,7 @@ private List potentiallyCreateCompoundIndexDefinitions(St MongoPersistentEntity entity) { if ((!entity.isAnnotationPresent(CompoundIndexes.class) && !entity.isAnnotationPresent(CompoundIndex.class)) - || entity.isAnnotationPresent(CompoundWildcardIndexed.class)) { + || entity.isAnnotationPresent(CompoundWildcardIndex.class)) { return Collections.emptyList(); } @@ -314,7 +314,7 @@ private List potentiallyCreateWildcardIndexDefinitions(St MongoPersistentEntity entity) { if (!entity.isAnnotationPresent(WildcardIndexed.class) - || entity.isAnnotationPresent(CompoundWildcardIndexed.class)) { + || entity.isAnnotationPresent(CompoundWildcardIndex.class)) { return Collections.emptyList(); } @@ -372,11 +372,11 @@ private Collection potentiallyCreateTextIndexDe private Collection potentiallyCreateCompoundWildcardDefinition( MongoPersistentEntity entity, String collection) { - if (!entity.isAnnotationPresent(CompoundWildcardIndexed.class)) { + if (!entity.isAnnotationPresent(CompoundWildcardIndex.class)) { return Collections.emptyList(); } - CompoundWildcardIndexed compoundWildcardIndex = entity.getRequiredAnnotation(CompoundWildcardIndexed.class); + CompoundWildcardIndex compoundWildcardIndex = entity.getRequiredAnnotation(CompoundWildcardIndex.class); IndexDefinitionHolder compoundWildcardIndexDefinition = createCompoundWildcardIndexDefinition(collection, compoundWildcardIndex, entity); return Collections.singletonList(compoundWildcardIndexDefinition); @@ -520,7 +520,7 @@ protected IndexDefinitionHolder createWildcardIndexDefinition(String dotPath, St return new IndexDefinitionHolder(dotPath, indexDefinition, collection); } - protected IndexDefinitionHolder createCompoundWildcardIndexDefinition(String collection, CompoundWildcardIndexed index, + protected IndexDefinitionHolder createCompoundWildcardIndexDefinition(String collection, CompoundWildcardIndex index, @Nullable MongoPersistentEntity entity) { String wildcardField = index.wildcardFieldName(); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/WildcardIndex.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/WildcardIndex.java index 34c1a66bdc..4a64131820 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/WildcardIndex.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/WildcardIndex.java @@ -179,7 +179,7 @@ public WildcardIndex wildcardProjection(Map includeExclude) { } private String getTargetFieldName() { - return (StringUtils.hasText(fieldName) && !CompoundWildcardIndexed.ALL_FIELDS.equals(fieldName)) ? (fieldName + ".$**") : "$**"; + return (StringUtils.hasText(fieldName) && !CompoundWildcardIndex.ALL_FIELDS.equals(fieldName)) ? (fieldName + ".$**") : "$**"; } @Override diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java index 37ed482825..69524e6cce 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java @@ -832,7 +832,7 @@ class WithEvaluatedCollationFromCompoundIndex {} } /** - * Test resolution of {@link CompoundWildcardIndexed}. + * Test resolution of {@link CompoundWildcardIndex}. * * @author Julia Lee */ @@ -939,36 +939,36 @@ public void resolvesMultipleIndexesWithCompoundWildcardIndex() { } @Document("CompoundWildcardIndexOnSingleField") - @CompoundWildcardIndexed(wildcardFieldName = "foo", fields = "{'bar': 1, 'baz': 1}") + @CompoundWildcardIndex(wildcardFieldName = "foo", fields = "{'bar': 1, 'baz': 1}") class CompoundWildcardIndexOnFields {} @Document - @CompoundWildcardIndexed(wildcardFieldName = "foo", wildcardProjection = "{}", fields = "{'bar': 1}") + @CompoundWildcardIndex(wildcardFieldName = "foo", wildcardProjection = "{}", fields = "{'bar': 1}") class IncorrectCompoundWildcardIndexOnFieldWithProjection {} @Document - @CompoundWildcardIndexed(fields = "{ 'bar': 1 }") + @CompoundWildcardIndex(fields = "{ 'bar': 1 }") class IncorrectCompoundWildcardIndexOnEntityWithoutProjection {} @Document - @CompoundWildcardIndexed(wildcardProjection = "{'foo.something' : 0}", fields = "{'bar': -1}") + @CompoundWildcardIndex(wildcardProjection = "{'foo.something' : 0}", fields = "{'bar': -1}") class CompoundWildcardIndexOnEntity {} @Document - @CompoundWildcardIndexed(fields = "{'foo': 1}", wildcardProjection = "{'bar.something': 1}", name = "my_index_name", + @CompoundWildcardIndex(fields = "{'foo': 1}", wildcardProjection = "{'bar.something': 1}", name = "my_index_name", collation = "{'locale': 'en_US', 'strength': 2}", partialFilter = "{'value': {'$exists': true}}") class CompoundWildcardIndexWithOptions {} @Document(collation = "{'locale': 'en_US', 'strength': 2}") - @CompoundWildcardIndexed(wildcardFieldName = "foo", fields = "{'bar': 1}") + @CompoundWildcardIndex(wildcardFieldName = "foo", fields = "{'bar': 1}") class CompoundWildcardIndexWithCollationOnDocument {} @Document(collation = "{'locale': 'en_US', 'strength': 2}") - @CompoundWildcardIndexed(wildcardFieldName = "foo", fields = "{'bar': 1}", collation = "#{{'locale' : 'de' + '_' + 'AT'}}") + @CompoundWildcardIndex(wildcardFieldName = "foo", fields = "{'bar': 1}", collation = "#{{'locale' : 'de' + '_' + 'AT'}}") class CompoundWildcardIndexWithEvaluatedCollation {} @Document - @CompoundWildcardIndexed(wildcardFieldName = "foo", fields = "{'bar': 1}") + @CompoundWildcardIndex(wildcardFieldName = "foo", fields = "{'bar': 1}") class MultipleIndexes { @Indexed String one; } From ad5cddbbf8bc4529924516488025232c765339bc Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Tue, 8 Oct 2024 12:32:48 +0200 Subject: [PATCH 5/5] Added repeatable options for annotations --- .../core/index/CompoundWildcardIndex.java | 4 +- .../core/index/CompoundWildcardIndexes.java | 41 +++++++++ .../MongoPersistentEntityIndexResolver.java | 90 +++++++++++-------- .../mongodb/core/index/WildcardIndexed.java | 2 + .../mongodb/core/index/WildcardIndexes.java | 41 +++++++++ ...ersistentEntityIndexResolverUnitTests.java | 68 ++++++++++++++ 6 files changed, 207 insertions(+), 39 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/CompoundWildcardIndexes.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/WildcardIndexes.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/CompoundWildcardIndex.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/CompoundWildcardIndex.java index d22cf77b6a..57a42b2108 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/CompoundWildcardIndex.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/CompoundWildcardIndex.java @@ -19,6 +19,7 @@ import java.lang.annotation.Documented; import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @@ -53,12 +54,13 @@ * * @author Julia Lee * @author Marcin Grzejszczak - * @since 4.4.0 + * @since 4.4 */ @Target({ ElementType.TYPE }) @Documented @CompoundIndex @Retention(RetentionPolicy.RUNTIME) +@Repeatable(CompoundWildcardIndexes.class) public @interface CompoundWildcardIndex { /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/CompoundWildcardIndexes.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/CompoundWildcardIndexes.java new file mode 100644 index 0000000000..df3f398212 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/CompoundWildcardIndexes.java @@ -0,0 +1,41 @@ +/* + * Copyright 2011-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.core.index; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Container annotation that allows to collect multiple {@link CompoundWildcardIndex} annotations. + *

+ * Can be used natively, declaring several nested {@link CompoundWildcardIndex} annotations. Can also be used in conjunction + * with Java 8's support for repeatable annotations, where {@link CompoundWildcardIndex} can simply be declared several + * times on the same {@linkplain ElementType#TYPE type}, implicitly generating this container annotation. + * + * @author Marcin Grzejszczak + * @since 4.4 + */ +@Target({ ElementType.TYPE }) +@Documented +@Retention(RetentionPolicy.RUNTIME) +public @interface CompoundWildcardIndexes { + + CompoundWildcardIndex[] value(); + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java index b1a27cab62..25de1d3fc9 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java @@ -157,20 +157,30 @@ private void verifyWildcardIndexedProjection(MongoPersistentEntity entity) { } }); - if (entity.isAnnotationPresent(CompoundWildcardIndex.class)) { - CompoundWildcardIndex indexed = entity.getRequiredAnnotation(CompoundWildcardIndex.class); - - if (!isWildcardFromRoot(indexed.wildcardFieldName()) && !ObjectUtils.isEmpty(indexed.wildcardProjection())) { + if (entity.isAnnotationPresent(CompoundWildcardIndexes.class)) { + CompoundWildcardIndexes indexes = entity.getRequiredAnnotation(CompoundWildcardIndexes.class); + for (CompoundWildcardIndex compoundWildcardIndex : indexes.value()) { + checkSingleIndex(compoundWildcardIndex); + } - throw new MappingException( - String.format("CompoundWildcardIndex.wildcardProjection is only allowed on \"$**\"; Offending property: %s", - indexed.wildcardFieldName())); } + if (entity.isAnnotationPresent(CompoundWildcardIndex.class)) { + checkSingleIndex(entity.getRequiredAnnotation(CompoundWildcardIndex.class)); + } + } - if (isWildcardFromRoot(indexed.wildcardFieldName()) && ObjectUtils.isEmpty(indexed.wildcardProjection())) { + private static void checkSingleIndex(CompoundWildcardIndex indexed) { - throw new MappingException("CompoundWildcardIndex.wildcardProjection is required on \"$**\""); - } + if (!isWildcardFromRoot(indexed.wildcardFieldName()) && !ObjectUtils.isEmpty(indexed.wildcardProjection())) { + + throw new MappingException( + String.format("CompoundWildcardIndex.wildcardProjection is only allowed on \"$**\"; Offending property: %s", + indexed.wildcardFieldName())); + } + + if (isWildcardFromRoot(indexed.wildcardFieldName()) && ObjectUtils.isEmpty(indexed.wildcardProjection())) { + + throw new MappingException("CompoundWildcardIndex.wildcardProjection is required on \"$**\""); } } @@ -204,23 +214,6 @@ private void potentiallyAddIndexForProperty(MongoPersistentEntity root, Mongo } } - /** - * Recursively resolve and inspect properties of given {@literal type} for indexes to be created. - * - * @param type - * @param dotPath The {@literal "dot} path. - * @param path {@link PersistentProperty} path for cycle detection. - * @param collection - * @param guard - * @return List of {@link IndexDefinitionHolder} representing indexes for given type and its referenced property - * types. Will never be {@code null}. - */ - private List resolveIndexForClass(TypeInformation type, String dotPath, Path path, - String collection, CycleGuard guard) { - - return resolveIndexForEntity(mappingContext.getRequiredPersistentEntity(type), dotPath, path, collection, guard); - } - private List resolveIndexForEntity(MongoPersistentEntity entity, String dotPath, Path path, String collection, CycleGuard guard) { @@ -303,7 +296,7 @@ private List potentiallyCreateCompoundIndexDefinitions(St MongoPersistentEntity entity) { if ((!entity.isAnnotationPresent(CompoundIndexes.class) && !entity.isAnnotationPresent(CompoundIndex.class)) - || entity.isAnnotationPresent(CompoundWildcardIndex.class)) { + || entity.isAnnotationPresent(CompoundWildcardIndex.class) || entity.isAnnotationPresent(CompoundWildcardIndexes.class)) { return Collections.emptyList(); } @@ -313,14 +306,23 @@ private List potentiallyCreateCompoundIndexDefinitions(St private List potentiallyCreateWildcardIndexDefinitions(String dotPath, String collection, MongoPersistentEntity entity) { - if (!entity.isAnnotationPresent(WildcardIndexed.class) - || entity.isAnnotationPresent(CompoundWildcardIndex.class)) { + if ((!entity.isAnnotationPresent(WildcardIndexed.class) && !entity.isAnnotationPresent(WildcardIndexes.class)) + || entity.isAnnotationPresent(CompoundWildcardIndex.class) || entity.isAnnotationPresent(CompoundWildcardIndexes.class)) { return Collections.emptyList(); } - return Collections.singletonList(new IndexDefinitionHolder(dotPath, - createWildcardIndexDefinition(dotPath, collection, entity.getRequiredAnnotation(WildcardIndexed.class), entity), - collection)); + WildcardIndexes wildcardIndexes = entity.findAnnotation(WildcardIndexes.class); + if (wildcardIndexes == null) { + return Collections.singletonList(new IndexDefinitionHolder(dotPath, + createWildcardIndexDefinition(dotPath, collection, entity.getRequiredAnnotation(WildcardIndexed.class), entity), + collection)); + } + List holders = new ArrayList<>(); + for (WildcardIndexed indexed : wildcardIndexes.value()) { + holders.add(new IndexDefinitionHolder(dotPath, + createWildcardIndexDefinition(dotPath, collection, indexed, entity), collection)); + } + return holders; } private Collection potentiallyCreateTextIndexDefinition( @@ -372,14 +374,26 @@ private Collection potentiallyCreateTextIndexDe private Collection potentiallyCreateCompoundWildcardDefinition( MongoPersistentEntity entity, String collection) { - if (!entity.isAnnotationPresent(CompoundWildcardIndex.class)) { + boolean singleIndexAnnotationPresent = entity.isAnnotationPresent(CompoundWildcardIndex.class); + boolean indexesAnnotationPresent = entity.isAnnotationPresent(CompoundWildcardIndexes.class); + if (!singleIndexAnnotationPresent && !indexesAnnotationPresent) { return Collections.emptyList(); } - CompoundWildcardIndex compoundWildcardIndex = entity.getRequiredAnnotation(CompoundWildcardIndex.class); - IndexDefinitionHolder compoundWildcardIndexDefinition = createCompoundWildcardIndexDefinition(collection, - compoundWildcardIndex, entity); - return Collections.singletonList(compoundWildcardIndexDefinition); + List definitions = new ArrayList<>(); + if (indexesAnnotationPresent) { + CompoundWildcardIndexes annotation = entity.getRequiredAnnotation(CompoundWildcardIndexes.class); + for (CompoundWildcardIndex index : annotation.value()) { + definitions.add(createCompoundWildcardIndexDefinition(collection, index, entity)); + } + + } + if (singleIndexAnnotationPresent) { + CompoundWildcardIndex compoundWildcardIndex = entity.getRequiredAnnotation(CompoundWildcardIndex.class); + definitions.add(createCompoundWildcardIndexDefinition(collection, + compoundWildcardIndex, entity)); + } + return definitions; } private void appendTextIndexInformation(DotPath dotPath, Path path, TextIndexDefinitionBuilder indexDefinitionBuilder, diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/WildcardIndexed.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/WildcardIndexed.java index e3d8fb5b46..2c095f0752 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/WildcardIndexed.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/WildcardIndexed.java @@ -17,6 +17,7 @@ import java.lang.annotation.Documented; import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @@ -86,6 +87,7 @@ @Documented @Target({ ElementType.TYPE, ElementType.FIELD }) @Retention(RetentionPolicy.RUNTIME) +@Repeatable(WildcardIndexes.class) public @interface WildcardIndexed { /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/WildcardIndexes.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/WildcardIndexes.java new file mode 100644 index 0000000000..fc2d56b1f2 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/WildcardIndexes.java @@ -0,0 +1,41 @@ +/* + * Copyright 2011-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.core.index; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Container annotation that allows to collect multiple {@link WildcardIndexed} annotations. + *

+ * Can be used natively, declaring several nested {@link WildcardIndexed} annotations. Can also be used in conjunction + * with Java 8's support for repeatable annotations, where {@link WildcardIndexed} can simply be declared several + * times on the same {@linkplain ElementType#TYPE type}, implicitly generating this container annotation. + * + * @author Marcin Grzejszczak + * @since 4.4 + */ +@Target({ ElementType.TYPE }) +@Documented +@Retention(RetentionPolicy.RUNTIME) +public @interface WildcardIndexes { + + WildcardIndexed[] value(); + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java index 69524e6cce..4067ef4952 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java @@ -29,6 +29,8 @@ import java.util.Map; import org.junit.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.junit.runner.RunWith; import org.junit.runners.Suite; import org.junit.runners.Suite.SuiteClasses; @@ -55,6 +57,7 @@ import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.core.mapping.Unwrapped; import org.springframework.data.util.ClassTypeInformation; +import org.springframework.util.StringUtils; /** * Tests for {@link MongoPersistentEntityIndexResolver}. @@ -849,6 +852,19 @@ public void compoundWildcardIndexOnSingleField() { indexDefinitions.get(0)); } + @ParameterizedTest // GH-4471 + @ValueSource(classes = {RepeatableCompoundWildcardIndex.class, RepeatableCompoundWildcardIndexThroughIndexes.class}) + public void compoundWildcardIndexOnSingleField(Class clazz) { + + List indexDefinitions = prepareMappingContextAndResolveIndexForType(clazz); + + assertThat(indexDefinitions).hasSize(2); + assertIndexPathAndCollection(new String[] { "foo1.$**", "bar1", "baz1" }, StringUtils.uncapitalize(clazz.getSimpleName()), + indexDefinitions.get(0)); + assertIndexPathAndCollection(new String[] { "foo2.$**", "bar2", "baz2" }, StringUtils.uncapitalize(clazz.getSimpleName()), + indexDefinitions.get(1)); + } + @Test // GH-4471 public void compoundWildcardIndexOnEntityWithProjection() { @@ -942,6 +958,16 @@ public void resolvesMultipleIndexesWithCompoundWildcardIndex() { @CompoundWildcardIndex(wildcardFieldName = "foo", fields = "{'bar': 1, 'baz': 1}") class CompoundWildcardIndexOnFields {} + @Document + @CompoundWildcardIndex(wildcardFieldName = "foo1", fields = "{'bar1': 1, 'baz1': 1}") + @CompoundWildcardIndex(wildcardFieldName = "foo2", fields = "{'bar2': 1, 'baz2': 1}") + class RepeatableCompoundWildcardIndex {} + + @Document + @CompoundWildcardIndexes({@CompoundWildcardIndex(wildcardFieldName = "foo1", fields = "{'bar1': 1, 'baz1': 1}"), + @CompoundWildcardIndex(wildcardFieldName = "foo2", fields = "{'bar2': 1, 'baz2': 1}")}) + class RepeatableCompoundWildcardIndexThroughIndexes {} + @Document @CompoundWildcardIndex(wildcardFieldName = "foo", wildcardProjection = "{}", fields = "{'bar': 1}") class IncorrectCompoundWildcardIndexOnFieldWithProjection {} @@ -1570,6 +1596,26 @@ public void resolvesWildcardOnProperty() { }); } + @ParameterizedTest // GH-4471 + @ValueSource(classes = { WithRepeatableWildcardIndex.class, WithWildcardIndexes.class}) + public void resolvesRepeatableWildcards(Class clazz) { + + List indices = prepareMappingContextAndResolveIndexForType(clazz); + assertThat(indices).hasSize(2); + assertThat(indices.get(0)).satisfies(it -> { + assertThat(it.getIndexKeys()).containsEntry("$**", 1); + assertThat(it.getIndexOptions()).containsEntry("name", "foo") + .containsEntry("collation", new org.bson.Document("locale", "en_US")) + .containsEntry("partialFilterExpression", new org.bson.Document("$eq", 1)); + }); + assertThat(indices.get(1)).satisfies(it -> { + assertThat(it.getIndexKeys()).containsEntry("$**", 1); + assertThat(it.getIndexOptions()).containsEntry("name", "bar") + .containsEntry("collation", new org.bson.Document("locale", "en_UK")) + .containsEntry("partialFilterExpression", new org.bson.Document("$eq", 0)); + }); + } + @Test // GH-3225 public void resolvesWildcardTypeOfNestedProperty() { @@ -1924,6 +1970,28 @@ class WithWildCardIndexOnProperty { } + @WildcardIndexed(name = "foo", partialFilter = "{ '$eq' : 1 }", collation = "en_US") + @WildcardIndexed(name = "bar", partialFilter = "{ '$eq' : 0 }", collation = "en_UK") + @Document + class WithRepeatableWildcardIndex { + + Map value; + + Map value2; + + } + + @WildcardIndexes({ @WildcardIndexed(name = "foo", partialFilter = "{ '$eq' : 1 }", collation = "en_US"), + @WildcardIndexed(name = "bar", partialFilter = "{ '$eq' : 0 }", collation = "en_UK") }) + @Document + class WithWildcardIndexes { + + Map value; + + Map value2; + + } + @Document class WildcardIndexedProjectionOnNestedPath {