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 potentiallyCreateTextIndexDefinition( @@ -372,14 +374,26 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver { 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 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 {