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 d22cf77b6..57a42b210 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 org.springframework.core.annotation.AliasFor;
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 @@ import java.lang.annotation.Target;
*
* @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 000000000..df3f39821
--- /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 b1a27cab6..25de1d3fc 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 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
}
});
- 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 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
}
}
- /**
- * 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 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
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 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
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 extends IndexDefinitionHolder> potentiallyCreateTextIndexDefinition(
@@ -372,14 +374,26 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
private Collection extends IndexDefinitionHolder> 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 e3d8fb5b4..2c095f075 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 @@ package org.springframework.data.mongodb.core.index;
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 @@ import org.springframework.data.mongodb.core.annotation.Collation;
@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 000000000..fc2d56b1f
--- /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 69524e6cc..4067ef495 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.List;
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.MongoPersistentEntity;
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 class MongoPersistentEntityIndexResolverUnitTests {
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 class MongoPersistentEntityIndexResolverUnitTests {
@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 class MongoPersistentEntityIndexResolverUnitTests {
});
}
+ @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 @@ public class MongoPersistentEntityIndexResolverUnitTests {
}
+ @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 {