diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/CompoundIndex.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/CompoundIndex.java index f58f0d809..0fbd88d72 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/CompoundIndex.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/CompoundIndex.java @@ -36,10 +36,29 @@ import java.lang.annotation.Target; public @interface CompoundIndex { /** - * The actual index definition in JSON format. The keys of the JSON document are the fields to be indexed, the values - * define the index direction (1 for ascending, -1 for descending).
+ * The actual index definition in JSON format or a {@link org.springframework.expression.spel.standard.SpelExpression + * template expression} resolving to either a JSON String or a {@link org.bson.Document}. The keys of the JSON + * document are the fields to be indexed, the values define the index direction (1 for ascending, -1 for descending). + *
* If left empty on nested document, the whole document will be indexed. * + *
+	 * 
+	 *
+	 * @Document
+	 * @CompoundIndex(def = "{'h1': 1, 'h2': 1}")
+	 * class JsonStringIndexDefinition {
+	 *   String h1, h2;
+	 * }
+	 *
+	 * @Document
+	 * @CompoundIndex(def = "#{T(org.bson.Document).parse("{ 'h1': 1, 'h2': 1 }")}")
+	 * class ExpressionIndexDefinition {
+	 *   String h1, h2;
+	 * }
+	 * 
+	 * 
+ * * @return */ String def() default ""; @@ -79,7 +98,8 @@ public @interface CompoundIndex { boolean dropDups() default false; /** - * The name of the index to be created.
+ * Index name of the index to be created either as plain value or as + * {@link org.springframework.expression.spel.standard.SpelExpression template expression}.
*
* The name will only be applied as is when defined on root level. For usage on nested or embedded structures the * provided name will be prefixed with the path leading to the entity.
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeoSpatialIndexed.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeoSpatialIndexed.java index e7e51c905..506e1c742 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeoSpatialIndexed.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeoSpatialIndexed.java @@ -34,8 +34,8 @@ import java.lang.annotation.Target; public @interface GeoSpatialIndexed { /** - * Index name.
- *
+ * Index name either as plain value or as {@link org.springframework.expression.spel.standard.SpelExpression template + * expression}.
* The name will only be applied as is when defined on root level. For usage on nested or embedded structures the * provided name will be prefixed with the path leading to the entity.
*
@@ -52,6 +52,7 @@ public @interface GeoSpatialIndexed { * @Document * class Hybrid { * @GeoSpatialIndexed(name="index") Point h1; + * @GeoSpatialIndexed(name="#{@myBean.indexName}") Point h2; * } * * class Nested { @@ -67,6 +68,7 @@ public @interface GeoSpatialIndexed { * db.root.createIndex( { hybrid.h1: "2d" } , { name: "hybrid.index" } ) * db.root.createIndex( { nested.n1: "2d" } , { name: "nested.index" } ) * db.hybrid.createIndex( { h1: "2d" } , { name: "index" } ) + * db.hybrid.createIndex( { h2: "2d"} , { name: the value myBean.getIndexName() returned } ) * * * diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/Indexed.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/Indexed.java index 196812db0..29b5756c1 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/Indexed.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/Indexed.java @@ -65,7 +65,8 @@ public @interface Indexed { boolean dropDups() default false; /** - * Index name.
+ * Index name either as plain value or as {@link org.springframework.expression.spel.standard.SpelExpression template + * expression}.
*
* The name will only be applied as is when defined on root level. For usage on nested or embedded structures the * provided name will be prefixed with the path leading to the entity.
@@ -83,6 +84,7 @@ public @interface Indexed { * @Document * class Hybrid { * @Indexed(name="index") String h1; + * @Indexed(name="#{@myBean.indexName}") String h2; * } * * class Nested { @@ -98,6 +100,7 @@ public @interface Indexed { * db.root.createIndex( { hybrid.h1: 1 } , { name: "hybrid.index" } ) * db.root.createIndex( { nested.n1: 1 } , { name: "nested.index" } ) * db.hybrid.createIndex( { h1: 1} , { name: "index" } ) + * db.hybrid.createIndex( { h2: 1} , { name: the value myBean.getIndexName() returned } ) * * * @@ -135,7 +138,8 @@ public @interface Indexed { /** * Alternative for {@link #expireAfterSeconds()} to configure the timeout after which the collection should expire. * Defaults to an empty String for no expiry. Accepts numeric values followed by their unit of measure (d(ays), - * h(ours), m(inutes), s(seconds)) or a Spring {@literal template expression}. + * h(ours), m(inutes), s(seconds)) or a Spring {@literal template expression}. The expression can result in a a valid + * expiration {@link String} following the conventions already mentioned or a {@link java.time.Duration}. * *
 	 *     
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 f7481fed4..fdcdd5d2e 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
@@ -40,6 +40,7 @@ import org.springframework.data.domain.Sort;
 import org.springframework.data.mapping.Association;
 import org.springframework.data.mapping.AssociationHandler;
 import org.springframework.data.mapping.MappingException;
+import org.springframework.data.mapping.PersistentEntity;
 import org.springframework.data.mapping.PersistentProperty;
 import org.springframework.data.mapping.PropertyHandler;
 import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexResolver.CycleGuard.Path;
@@ -62,6 +63,7 @@ import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 import org.springframework.util.ClassUtils;
 import org.springframework.util.NumberUtils;
+import org.springframework.util.ObjectUtils;
 import org.springframework.util.StringUtils;
 
 /**
@@ -356,10 +358,10 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
 			MongoPersistentEntity entity) {
 
 		CompoundIndexDefinition indexDefinition = new CompoundIndexDefinition(
-				resolveCompoundIndexKeyFromStringDefinition(dotPath, index.def()));
+				resolveCompoundIndexKeyFromStringDefinition(dotPath, index.def(), entity));
 
 		if (!index.useGeneratedName()) {
-			indexDefinition.named(pathAwareIndexName(index.name(), dotPath, null));
+			indexDefinition.named(pathAwareIndexName(index.name(), dotPath, entity, null));
 		}
 
 		if (index.unique()) {
@@ -377,7 +379,8 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
 		return new IndexDefinitionHolder(dotPath, indexDefinition, collection);
 	}
 
-	private org.bson.Document resolveCompoundIndexKeyFromStringDefinition(String dotPath, String keyDefinitionString) {
+	private org.bson.Document resolveCompoundIndexKeyFromStringDefinition(String dotPath, String keyDefinitionString,
+			PersistentEntity entity) {
 
 		if (!StringUtils.hasText(dotPath) && !StringUtils.hasText(keyDefinitionString)) {
 			throw new InvalidDataAccessApiUsageException("Cannot create index on root level for empty keys.");
@@ -387,7 +390,12 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
 			return new org.bson.Document(dotPath, 1);
 		}
 
-		org.bson.Document dbo = org.bson.Document.parse(keyDefinitionString);
+		Object keyDefToUse = evaluatePotentialTemplateExpression(keyDefinitionString,
+				getEvaluationContextForProperty(entity));
+
+		org.bson.Document dbo = (keyDefToUse instanceof org.bson.Document) ? (org.bson.Document) keyDefToUse
+				: org.bson.Document.parse(ObjectUtils.nullSafeToString(keyDefToUse));
+
 		if (!StringUtils.hasText(dotPath)) {
 			return dbo;
 		}
@@ -423,7 +431,7 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
 				IndexDirection.ASCENDING.equals(index.direction()) ? Sort.Direction.ASC : Sort.Direction.DESC);
 
 		if (!index.useGeneratedName()) {
-			indexDefinition.named(pathAwareIndexName(index.name(), dotPath, persitentProperty));
+			indexDefinition.named(pathAwareIndexName(index.name(), dotPath, persitentProperty.getOwner(), persitentProperty));
 		}
 
 		if (index.unique()) {
@@ -446,21 +454,12 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
 
 			if (index.expireAfterSeconds() >= 0) {
 				throw new IllegalStateException(String.format(
-						"@Indexed already defines an expiration timeout of %s sec. via Indexed#expireAfterSeconds. Please make to use either expireAfterSeconds or expireAfter.", index.expireAfterSeconds()));
+						"@Indexed already defines an expiration timeout of %s sec. via Indexed#expireAfterSeconds. Please make to use either expireAfterSeconds or expireAfter.",
+						index.expireAfterSeconds()));
 			}
 
-			EvaluationContext ctx = getEvaluationContext();
-
-			if (persitentProperty.getOwner() instanceof BasicMongoPersistentEntity) {
-
-				EvaluationContext contextFromEntity = ((BasicMongoPersistentEntity) persitentProperty.getOwner())
-						.getEvaluationContext(null);
-				if (contextFromEntity != null && !EvaluationContextProvider.DEFAULT.equals(contextFromEntity)) {
-					ctx = contextFromEntity;
-				}
-			}
-
-			Duration timeout = computeIndexTimeout(index.expireAfter(), ctx);
+			Duration timeout = computeIndexTimeout(index.expireAfter(),
+					getEvaluationContextForProperty(persitentProperty.getOwner()));
 			if (!timeout.isZero() && !timeout.isNegative()) {
 				indexDefinition.expire(timeout);
 			}
@@ -479,6 +478,27 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
 		return evaluationContextProvider.getEvaluationContext(null);
 	}
 
+	/**
+	 * Get the {@link EvaluationContext} for a given {@link PersistentEntity entity} the default one.
+	 * 
+	 * @param persistentEntity can be {@literal null}
+	 * @return
+	 */
+	private EvaluationContext getEvaluationContextForProperty(@Nullable PersistentEntity persistentEntity) {
+
+		if (persistentEntity == null || !(persistentEntity instanceof BasicMongoPersistentEntity)) {
+			return getEvaluationContext();
+		}
+
+		EvaluationContext contextFromEntity = ((BasicMongoPersistentEntity) persistentEntity).getEvaluationContext(null);
+
+		if (contextFromEntity != null && !EvaluationContextProvider.DEFAULT.equals(contextFromEntity)) {
+			return contextFromEntity;
+		}
+
+		return getEvaluationContext();
+	}
+
 	/**
 	 * Set the {@link EvaluationContextProvider} used for obtaining the {@link EvaluationContext} used to compute
 	 * {@link org.springframework.expression.spel.standard.SpelExpression expressions}.
@@ -514,7 +534,8 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
 		indexDefinition.withMin(index.min()).withMax(index.max());
 
 		if (!index.useGeneratedName()) {
-			indexDefinition.named(pathAwareIndexName(index.name(), dotPath, persistentProperty));
+			indexDefinition
+					.named(pathAwareIndexName(index.name(), dotPath, persistentProperty.getOwner(), persistentProperty));
 		}
 
 		indexDefinition.typed(index.type()).withBucketSize(index.bucketSize()).withAdditionalField(index.additionalField());
@@ -522,9 +543,13 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
 		return new IndexDefinitionHolder(dotPath, indexDefinition, collection);
 	}
 
-	private String pathAwareIndexName(String indexName, String dotPath, @Nullable MongoPersistentProperty property) {
+	private String pathAwareIndexName(String indexName, String dotPath, @Nullable PersistentEntity entity,
+			@Nullable MongoPersistentProperty property) {
 
-		String nameToUse = StringUtils.hasText(indexName) ? indexName : "";
+		String nameToUse = StringUtils.hasText(indexName)
+				? ObjectUtils
+						.nullSafeToString(evaluatePotentialTemplateExpression(indexName, getEvaluationContextForProperty(entity)))
+				: "";
 
 		if (!StringUtils.hasText(dotPath) || (property != null && dotPath.equals(property.getFieldName()))) {
 			return StringUtils.hasText(nameToUse) ? nameToUse : dotPath;
@@ -582,7 +607,17 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
 	 */
 	private static Duration computeIndexTimeout(String timeoutValue, EvaluationContext evaluationContext) {
 
-		String val = evaluatePotentialTemplateExpression(timeoutValue, evaluationContext);
+		Object evaluatedTimeout = evaluatePotentialTemplateExpression(timeoutValue, evaluationContext);
+
+		if (evaluatedTimeout == null) {
+			return Duration.ZERO;
+		}
+
+		if (evaluatedTimeout instanceof Duration) {
+			return (Duration) evaluatedTimeout;
+		}
+
+		String val = evaluatedTimeout.toString();
 
 		if (val == null) {
 			return Duration.ZERO;
@@ -611,15 +646,14 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
 	}
 
 	@Nullable
-	private static String evaluatePotentialTemplateExpression(String value, EvaluationContext evaluationContext) {
+	private static Object evaluatePotentialTemplateExpression(String value, EvaluationContext evaluationContext) {
 
 		Expression expression = PARSER.parseExpression(value, ParserContext.TEMPLATE_EXPRESSION);
 		if (expression instanceof LiteralExpression) {
 			return value;
 		}
 
-		return expression.getValue(evaluationContext, String.class);
-
+		return expression.getValue(evaluationContext, Object.class);
 	}
 
 	/**
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 e9ce7b350..1b35616c0 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
@@ -220,6 +220,15 @@ public class MongoPersistentEntityIndexResolverUnitTests {
 			Assertions.assertThat(indexDefinitions.get(0).getIndexOptions()).containsEntry("expireAfterSeconds", 11L);
 		}
 
+		@Test // DATAMONGO-2112
+		public void shouldResolveTimeoutFromExpressionReturningDuration() {
+
+			List indexDefinitions = prepareMappingContextAndResolveIndexForType(
+					WithExpireAfterAsExpressionResultingInDuration.class);
+
+			Assertions.assertThat(indexDefinitions.get(0).getIndexOptions()).containsEntry("expireAfterSeconds", 100L);
+		}
+
 		@Test // DATAMONGO-2112
 		public void shouldErrorOnInvalidTimeoutExpression() {
 
@@ -241,6 +250,15 @@ public class MongoPersistentEntityIndexResolverUnitTests {
 					.resolveIndexForEntity(mappingContext.getRequiredPersistentEntity(WithDuplicateExpiry.class)));
 		}
 
+		@Test // DATAMONGO-2112
+		public void resolveExpressionIndexName() {
+
+			List indexDefinitions = prepareMappingContextAndResolveIndexForType(
+					WithIndexNameAsExpression.class);
+
+			Assertions.assertThat(indexDefinitions.get(0).getIndexOptions()).containsEntry("name", "my1st");
+		}
+
 		@Document("Zero")
 		static class IndexOnLevelZero {
 			@Indexed String indexedProperty;
@@ -341,6 +359,11 @@ public class MongoPersistentEntityIndexResolverUnitTests {
 			@Indexed(expireAfter = "#{10 + 1 + 's'}") String withTimeout;
 		}
 
+		@Document
+		static class WithExpireAfterAsExpressionResultingInDuration {
+			@Indexed(expireAfter = "#{T(java.time.Duration).ofSeconds(100)}") String withTimeout;
+		}
+
 		@Document
 		class WithInvalidExpireAfter {
 			@Indexed(expireAfter = "123ops") String withTimeout;
@@ -350,6 +373,11 @@ public class MongoPersistentEntityIndexResolverUnitTests {
 		class WithDuplicateExpiry {
 			@Indexed(expireAfter = "1s", expireAfterSeconds = 2) String withTimeout;
 		}
+
+		@Document
+		static class WithIndexNameAsExpression {
+			@Indexed(name = "#{'my' + 1 + 'st'}") String spelIndexName;
+		}
 	}
 
 	@Target({ ElementType.FIELD })
@@ -427,6 +455,15 @@ public class MongoPersistentEntityIndexResolverUnitTests {
 					isBsonObject().containing("name", "my_geo_index_name").containing("bucketSize", 2.0));
 		}
 
+		@Test // DATAMONGO-2112
+		public void resolveExpressionIndexNameForGeoIndex() {
+
+			List indexDefinitions = prepareMappingContextAndResolveIndexForType(
+					GeoIndexWithNameAsExpression.class);
+
+			Assertions.assertThat(indexDefinitions.get(0).getIndexOptions()).containsEntry("name", "my1st");
+		}
+
 		@Document("Zero")
 		static class GeoSpatialIndexOnLevelZero {
 			@GeoSpatialIndexed Point geoIndexedProperty;
@@ -474,6 +511,11 @@ public class MongoPersistentEntityIndexResolverUnitTests {
 			GeoSpatialIndexType indexType() default GeoSpatialIndexType.GEO_HAYSTACK;
 		}
 
+		@Document
+		static class GeoIndexWithNameAsExpression {
+			@GeoSpatialIndexed(name = "#{'my' + 1 + 'st'}") Point spelIndexName;
+		}
+
 	}
 
 	/**
@@ -573,6 +615,26 @@ public class MongoPersistentEntityIndexResolverUnitTests {
 					.containing("unique", true).containing("background", true));
 		}
 
+		@Test // DATAMONGO-2112
+		public void resolveExpressionIndexNameForCompoundIndex() {
+
+			List indexDefinitions = prepareMappingContextAndResolveIndexForType(
+					CompoundIndexWithNameExpression.class);
+
+			Assertions.assertThat(indexDefinitions.get(0).getIndexOptions()).containsEntry("name", "cmp2name");
+		}
+
+		@Test // DATAMONGO-2112
+		public void resolveExpressionDefForCompoundIndex() {
+
+			List indexDefinitions = prepareMappingContextAndResolveIndexForType(
+					CompoundIndexWithDefExpression.class);
+
+			assertThat(indexDefinitions, hasSize(1));
+			assertIndexPathAndCollection(new String[] { "foo", "bar" }, "compoundIndexWithDefExpression",
+					indexDefinitions.get(0));
+		}
+
 		@Document("CompoundIndexOnLevelOne")
 		static class CompoundIndexOnLevelOne {
 
@@ -637,6 +699,14 @@ public class MongoPersistentEntityIndexResolverUnitTests {
 
 		}
 
+		@Document
+		@CompoundIndex(name = "#{'cmp' + 2 + 'name'}", def = "{'foo': 1, 'bar': -1}")
+		static class CompoundIndexWithNameExpression {}
+
+		@Document
+		@CompoundIndex(def = "#{T(org.bson.Document).parse(\"{ 'foo': 1, 'bar': -1 }\")}")
+		static class CompoundIndexWithDefExpression {}
+
 	}
 
 	public static class TextIndexedResolutionTests {