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. + *
+ * @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}.
- * @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
+ * 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
+ * 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