From 4b70eda5ca62b800bddd654cbf3263f848c21e8a Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 19 Feb 2019 09:39:31 +0100 Subject: [PATCH 1/4] DATAMONGO-2112 - Prepare issue branch. --- pom.xml | 2 +- spring-data-mongodb-benchmarks/pom.xml | 2 +- spring-data-mongodb-distribution/pom.xml | 2 +- spring-data-mongodb/pom.xml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index d0c3937c5b..3ac689c387 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 2.2.0.BUILD-SNAPSHOT + 2.2.0.DATAMONGO-2112-SNAPSHOT pom Spring Data MongoDB diff --git a/spring-data-mongodb-benchmarks/pom.xml b/spring-data-mongodb-benchmarks/pom.xml index bb7d9f03cc..225875429d 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 - 2.2.0.BUILD-SNAPSHOT + 2.2.0.DATAMONGO-2112-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index b32dcba387..46337578f2 100644 --- a/spring-data-mongodb-distribution/pom.xml +++ b/spring-data-mongodb-distribution/pom.xml @@ -14,7 +14,7 @@ org.springframework.data spring-data-mongodb-parent - 2.2.0.BUILD-SNAPSHOT + 2.2.0.DATAMONGO-2112-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index b611cf01a8..bc5377e1c3 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -11,7 +11,7 @@ org.springframework.data spring-data-mongodb-parent - 2.2.0.BUILD-SNAPSHOT + 2.2.0.DATAMONGO-2112-SNAPSHOT ../pom.xml From deeb3526cf2b8b17e71058b3a0317e6cb84d90b1 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 20 Feb 2019 10:23:53 +0100 Subject: [PATCH 2/4] DATAMONGO-2112 - Allow usage of SpEL expression for index timeout. We added expireAfter which accepts numeric values followed by the unit of measure (d(ays), h(ours), m(inutes), s(econds)) or a Spring template expression to the Indexed annotation. @Indexed(expireAfter = "10s") String expireAfterTenSeconds; @Indexed(expireAfter = "1d") String expireAfterOneDay; @Indexed(expireAfter = "#{@mySpringBean.timeout}") String expireAfterTimeoutObtainedFromSpringBean; --- .../data/mongodb/core/index/Index.java | 15 +++ .../data/mongodb/core/index/Indexed.java | 24 ++++ .../MongoPersistentEntityIndexResolver.java | 111 ++++++++++++++++++ .../mapping/BasicMongoPersistentEntity.java | 10 ++ .../core/index/IndexingIntegrationTests.java | 60 +++++++++- ...ersistentEntityIndexResolverUnitTests.java | 60 ++++++++++ 6 files changed, 279 insertions(+), 1 deletion(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/Index.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/Index.java index 747202e80b..3f7b1a4cd0 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/Index.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/Index.java @@ -15,6 +15,7 @@ */ package org.springframework.data.mongodb.core.index; +import java.time.Duration; import java.util.LinkedHashMap; import java.util.Map; import java.util.Map.Entry; @@ -116,6 +117,20 @@ public Index expire(long value) { return expire(value, TimeUnit.SECONDS); } + /** + * Specifies the TTL. + * + * @param timeout must not be {@literal null}. + * @return this. + * @throws IllegalArgumentException if given {@literal timeout} is {@literal null}. + * @since 2.2 + */ + public Index expire(Duration timeout) { + + Assert.notNull(timeout, "Timeout must not be null!"); + return expire(timeout.getSeconds()); + } + /** * Specifies TTL with given {@link TimeUnit}. * 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 1440386618..196812db0f 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 @@ -131,4 +131,28 @@ * "https://docs.mongodb.org/manual/tutorial/expire-data/">https://docs.mongodb.org/manual/tutorial/expire-data/ */ int expireAfterSeconds() default -1; + + /** + * 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}. + * + *
+	 *     
+	 *
+	 * @Indexed(expireAfter = "10s")
+	 * String expireAfterTenSeconds;
+	 *
+	 * @Indexed(expireAfter = "1d")
+	 * String expireAfterOneDay;
+	 *
+	 * @Indexed(expireAfter = "#{@mySpringBean.timeout}")
+	 * String expireAfterTimeoutObtainedFromSpringBean;
+	 *     
+	 * 
+ * + * @return {@literal 0s} by default. + * @since 2.2 + */ + String expireAfter() default "0s"; } 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 af56d06b0d..f7481fed46 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 @@ -19,6 +19,7 @@ import lombok.EqualsAndHashCode; import lombok.RequiredArgsConstructor; +import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -28,6 +29,8 @@ import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.stream.Collectors; import org.slf4j.Logger; @@ -43,14 +46,22 @@ import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexResolver.TextIndexIncludeOptions.IncludeStrategy; import org.springframework.data.mongodb.core.index.TextIndexDefinition.TextIndexDefinitionBuilder; import org.springframework.data.mongodb.core.index.TextIndexDefinition.TextIndexedFieldSpec; +import org.springframework.data.mongodb.core.mapping.BasicMongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; +import org.springframework.data.spel.EvaluationContextProvider; import org.springframework.data.util.TypeInformation; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.expression.ParserContext; +import org.springframework.expression.common.LiteralExpression; +import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.NumberUtils; import org.springframework.util.StringUtils; /** @@ -68,8 +79,11 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver { private static final Logger LOGGER = LoggerFactory.getLogger(MongoPersistentEntityIndexResolver.class); + private static final Pattern TIMEOUT_PATTERN = Pattern.compile("(\\d+)(\\W+)?([dhms])"); + private static final SpelExpressionParser PARSER = new SpelExpressionParser(); private final MongoMappingContext mappingContext; + private EvaluationContextProvider evaluationContextProvider = EvaluationContextProvider.DEFAULT; /** * Create new {@link MongoPersistentEntityIndexResolver}. @@ -428,9 +442,54 @@ protected IndexDefinitionHolder createIndexDefinition(String dotPath, String col indexDefinition.expire(index.expireAfterSeconds(), TimeUnit.SECONDS); } + if (!index.expireAfter().isEmpty() && !index.expireAfter().equals("0s")) { + + 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())); + } + + 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); + if (!timeout.isZero() && !timeout.isNegative()) { + indexDefinition.expire(timeout); + } + } + return new IndexDefinitionHolder(dotPath, indexDefinition, collection); } + /** + * Get the default {@link EvaluationContext}. + * + * @return never {@literal null}. + * @since 2.2 + */ + protected EvaluationContext getEvaluationContext() { + return evaluationContextProvider.getEvaluationContext(null); + } + + /** + * Set the {@link EvaluationContextProvider} used for obtaining the {@link EvaluationContext} used to compute + * {@link org.springframework.expression.spel.standard.SpelExpression expressions}. + * + * @param evaluationContextProvider must not be {@literal null}. + * @since 2.2 + */ + public void setEvaluationContextProvider(EvaluationContextProvider evaluationContextProvider) { + this.evaluationContextProvider = evaluationContextProvider; + } + /** * Creates {@link IndexDefinition} wrapped in {@link IndexDefinitionHolder} out of {@link GeoSpatialIndexed} for * {@link MongoPersistentProperty}. @@ -511,6 +570,58 @@ private void resolveAndAddIndexesForAssociation(Association indexInfo = operations.execute("withSpelIndexTimeout", collection -> { + + return collection.listIndexes(org.bson.Document.class).into(new ArrayList<>()) // + .stream() // + .filter(it -> it.get("name").equals("someString")) // + .findFirst(); + }); + + Assertions.assertThat(indexInfo).isPresent(); + Assertions.assertThat(indexInfo.get()).containsEntry("expireAfterSeconds", 11L); + } + @Target({ ElementType.FIELD }) @Retention(RetentionPolicy.RUNTIME) @Indexed @@ -110,6 +157,17 @@ class IndexedPerson { @Field("_lastname") @IndexedFieldAnnotation String lastname; } + @RequiredArgsConstructor + @Getter + static class TimeoutResolver { + final String timeout; + } + + @Document + class WithSpelIndexTimeout { + @Indexed(expireAfter = "#{@myTimeoutResolver?.timeout}") String someString; + } + /** * Returns whether an index with the given name exists for the given entity type. * 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 e4693df31d..e9ce7b3501 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 @@ -27,6 +27,7 @@ import java.util.Collections; import java.util.List; +import org.assertj.core.api.Assertions; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Suite; @@ -201,6 +202,45 @@ public void resolveIndexDefinitionInCustomComposedAnnotatedFields() { isBsonObject().containing("sparse", true).containing("name", "different_name").notContaining("unique")); } + @Test // DATAMONGO-2112 + public void shouldResolveTimeoutFromString() { + + List indexDefinitions = prepareMappingContextAndResolveIndexForType( + WithExpireAfterAsPlainString.class); + + Assertions.assertThat(indexDefinitions.get(0).getIndexOptions()).containsEntry("expireAfterSeconds", 600L); + } + + @Test // DATAMONGO-2112 + public void shouldResolveTimeoutFromExpression() { + + List indexDefinitions = prepareMappingContextAndResolveIndexForType( + WithExpireAfterAsExpression.class); + + Assertions.assertThat(indexDefinitions.get(0).getIndexOptions()).containsEntry("expireAfterSeconds", 11L); + } + + @Test // DATAMONGO-2112 + public void shouldErrorOnInvalidTimeoutExpression() { + + MongoMappingContext mappingContext = prepareMappingContext(WithInvalidExpireAfter.class); + MongoPersistentEntityIndexResolver indexResolver = new MongoPersistentEntityIndexResolver(mappingContext); + + Assertions.assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> indexResolver + .resolveIndexForEntity(mappingContext.getRequiredPersistentEntity(WithInvalidExpireAfter.class))); + + } + + @Test // DATAMONGO-2112 + public void shouldErrorOnDuplicateTimeoutExpression() { + + MongoMappingContext mappingContext = prepareMappingContext(WithDuplicateExpiry.class); + MongoPersistentEntityIndexResolver indexResolver = new MongoPersistentEntityIndexResolver(mappingContext); + + Assertions.assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> indexResolver + .resolveIndexForEntity(mappingContext.getRequiredPersistentEntity(WithDuplicateExpiry.class))); + } + @Document("Zero") static class IndexOnLevelZero { @Indexed String indexedProperty; @@ -290,6 +330,26 @@ static class IndexedDocumentWithComposedAnnotations { @AliasFor(annotation = org.springframework.data.mongodb.core.mapping.Field.class, attribute = "value") String name() default "_id"; } + + @Document + static class WithExpireAfterAsPlainString { + @Indexed(expireAfter = "10m") String withTimeout; + } + + @Document + static class WithExpireAfterAsExpression { + @Indexed(expireAfter = "#{10 + 1 + 's'}") String withTimeout; + } + + @Document + class WithInvalidExpireAfter { + @Indexed(expireAfter = "123ops") String withTimeout; + } + + @Document + class WithDuplicateExpiry { + @Indexed(expireAfter = "1s", expireAfterSeconds = 2) String withTimeout; + } } @Target({ ElementType.FIELD }) From 73c83b694e60881dee8007fd684e9d59b70c0298 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 20 Feb 2019 12:10:13 +0100 Subject: [PATCH 3/4] DATAMONGO-2112 - Allow SpEL expression to be used for annotated index & geoIndex names as well as compound index definition. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We now also evaluate SpEL expressions for the name of indices as well as the def attribute of the compound index definition. @CompoundIndex(name = "#{'cmp' + 2 + 'name‘}“, def = "#{T(org.bson.Document).parse(\"{ 'foo': 1, 'bar': -1 }\")}") class WithCompoundIndexFromExpression { // … } An expression used for Indexed.expireAfter may now not only return a plain String value with the timeout but also a java.time.Duration. --- .../mongodb/core/index/CompoundIndex.java | 26 +++++- .../mongodb/core/index/GeoSpatialIndexed.java | 6 +- .../data/mongodb/core/index/Indexed.java | 8 +- .../MongoPersistentEntityIndexResolver.java | 84 +++++++++++++------ ...ersistentEntityIndexResolverUnitTests.java | 70 ++++++++++++++++ 5 files changed, 162 insertions(+), 32 deletions(-) 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 f58f0d8095..0fbd88d728 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 @@ 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 @@ 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 e7e51c9059..506e1c7428 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 @@ 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 @@ * @Document * class Hybrid { * @GeoSpatialIndexed(name="index") Point h1; + * @GeoSpatialIndexed(name="#{@myBean.indexName}") Point h2; * } * * class Nested { @@ -67,6 +68,7 @@ * 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 196812db0f..29b5756c13 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 @@ 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 @@ * @Document * class Hybrid { * @Indexed(name="index") String h1; + * @Indexed(name="#{@myBean.indexName}") String h2; * } * * class Nested { @@ -98,6 +100,7 @@ * 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 @@ /** * 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 f7481fed46..fdcdd5d2e1 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.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.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 @@ protected IndexDefinitionHolder createCompoundIndexDefinition(String dotPath, St
 			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 @@ protected IndexDefinitionHolder createCompoundIndexDefinition(String dotPath, St
 		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 @@ private org.bson.Document resolveCompoundIndexKeyFromStringDefinition(String dot
 			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 @@ protected IndexDefinitionHolder createIndexDefinition(String dotPath, String col
 				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 @@ protected IndexDefinitionHolder createIndexDefinition(String dotPath, String col
 
 			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()));
-			}
-
-			EvaluationContext ctx = getEvaluationContext();
-
-			if (persitentProperty.getOwner() instanceof BasicMongoPersistentEntity) {
-
-				EvaluationContext contextFromEntity = ((BasicMongoPersistentEntity) persitentProperty.getOwner())
-						.getEvaluationContext(null);
-				if (contextFromEntity != null && !EvaluationContextProvider.DEFAULT.equals(contextFromEntity)) {
-					ctx = contextFromEntity;
-				}
+						"@Indexed already defines an expiration timeout of %s sec. via Indexed#expireAfterSeconds. Please make to use either expireAfterSeconds or expireAfter.",
+						index.expireAfterSeconds()));
 			}
 
-			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 @@ protected EvaluationContext getEvaluationContext() {
 		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 @@ protected IndexDefinitionHolder createGeoSpatialIndexDefinition(String dotPath,
 		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 @@ protected IndexDefinitionHolder createGeoSpatialIndexDefinition(String dotPath,
 		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 @@ private void resolveAndAddIndexesForAssociation(Association indexDefinitions = prepareMappingContextAndResolveIndexForType(
+					WithExpireAfterAsExpressionResultingInDuration.class);
+
+			Assertions.assertThat(indexDefinitions.get(0).getIndexOptions()).containsEntry("expireAfterSeconds", 100L);
+		}
+
 		@Test // DATAMONGO-2112
 		public void shouldErrorOnInvalidTimeoutExpression() {
 
@@ -241,6 +250,15 @@ public void shouldErrorOnDuplicateTimeoutExpression() {
 					.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 @@ static class WithExpireAfterAsExpression {
 			@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 @@ class WithInvalidExpireAfter {
 		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 void resolvesComposedAnnotationIndexDefinitionOptionsCorrectly() {
 					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 @@ static class GeoSpatialIndexedDocumentWithComposedAnnotation {
 			GeoSpatialIndexType indexType() default GeoSpatialIndexType.GEO_HAYSTACK;
 		}
 
+		@Document
+		static class GeoIndexWithNameAsExpression {
+			@GeoSpatialIndexed(name = "#{'my' + 1 + 'st'}") Point spelIndexName;
+		}
+
 	}
 
 	/**
@@ -573,6 +615,26 @@ public void singleCompoundIndexUsingComposedAnnotationsOnTypeResolvedCorrectly()
 					.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 @@ static class CompoundIndexDocumentWithComposedAnnotation {
 
 		}
 
+		@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 {

From a066ee0754889463e8c99a07d05056fe6124784a Mon Sep 17 00:00:00 2001
From: Mark Paluch 
Date: Tue, 26 Feb 2019 11:22:12 +0100
Subject: [PATCH 4/4] DATAMONGO-2112 - Polishing.

Align Indexed(expireAfter) default to empty string according to the documentation. Slightly reword Javadoc.

Import DurationStyle to reuse duration parsing until Spring Framework provides a similar utility. Document ISO-8601 duration style. Prevent null index name evaluation to render "null" as String.

Convert assertions from Hamcrest to AssertJ.
---
 .../mongodb/core/index/DurationStyle.java     | 216 +++++++++++++
 .../data/mongodb/core/index/Indexed.java      |  36 ++-
 .../MongoPersistentEntityIndexResolver.java   |  53 +---
 .../core/index/IndexingIntegrationTests.java  |  15 +-
 ...ersistentEntityIndexResolverUnitTests.java | 297 ++++++++++--------
 src/main/asciidoc/new-features.adoc           |   1 +
 6 files changed, 422 insertions(+), 196 deletions(-)
 create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/DurationStyle.java

diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/DurationStyle.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/DurationStyle.java
new file mode 100644
index 0000000000..c7f702691d
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/DurationStyle.java
@@ -0,0 +1,216 @@
+/*
+ * Copyright 2012-2019 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.time.Duration;
+import java.time.temporal.ChronoUnit;
+import java.util.function.Function;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+/**
+ * Duration format styles.
+ * 

+ * Fork of {@code org.springframework.boot.convert.DurationStyle}. + * + * @author Phillip Webb + * @since 2.2 + */ +enum DurationStyle { + + /** + * Simple formatting, for example '1s'. + */ + SIMPLE("^([\\+\\-]?\\d+)([a-zA-Z]{0,2})$") { + + @Override + public Duration parse(String value, @Nullable ChronoUnit unit) { + try { + Matcher matcher = matcher(value); + Assert.state(matcher.matches(), "Does not match simple duration pattern"); + String suffix = matcher.group(2); + return (StringUtils.hasLength(suffix) ? Unit.fromSuffix(suffix) : Unit.fromChronoUnit(unit)) + .parse(matcher.group(1)); + } catch (Exception ex) { + throw new IllegalArgumentException("'" + value + "' is not a valid simple duration", ex); + } + } + }, + + /** + * ISO-8601 formatting. + */ + ISO8601("^[\\+\\-]?P.*$") { + + @Override + public Duration parse(String value, @Nullable ChronoUnit unit) { + try { + return Duration.parse(value); + } catch (Exception ex) { + throw new IllegalArgumentException("'" + value + "' is not a valid ISO-8601 duration", ex); + } + } + }; + + private final Pattern pattern; + + DurationStyle(String pattern) { + this.pattern = Pattern.compile(pattern); + } + + protected final boolean matches(String value) { + return this.pattern.matcher(value).matches(); + } + + protected final Matcher matcher(String value) { + return this.pattern.matcher(value); + } + + /** + * Parse the given value to a duration. + * + * @param value the value to parse + * @return a duration + */ + public Duration parse(String value) { + return parse(value, null); + } + + /** + * Parse the given value to a duration. + * + * @param value the value to parse + * @param unit the duration unit to use if the value doesn't specify one ({@code null} will default to ms) + * @return a duration + */ + public abstract Duration parse(String value, @Nullable ChronoUnit unit); + + /** + * Detect the style then parse the value to return a duration. + * + * @param value the value to parse + * @return the parsed duration + * @throws IllegalStateException if the value is not a known style or cannot be parsed + */ + public static Duration detectAndParse(String value) { + return detectAndParse(value, null); + } + + /** + * Detect the style then parse the value to return a duration. + * + * @param value the value to parse + * @param unit the duration unit to use if the value doesn't specify one ({@code null} will default to ms) + * @return the parsed duration + * @throws IllegalStateException if the value is not a known style or cannot be parsed + */ + public static Duration detectAndParse(String value, @Nullable ChronoUnit unit) { + return detect(value).parse(value, unit); + } + + /** + * Detect the style from the given source value. + * + * @param value the source value + * @return the duration style + * @throws IllegalStateException if the value is not a known style + */ + public static DurationStyle detect(String value) { + Assert.notNull(value, "Value must not be null"); + for (DurationStyle candidate : values()) { + if (candidate.matches(value)) { + return candidate; + } + } + throw new IllegalArgumentException("'" + value + "' is not a valid duration"); + } + + /** + * Units that we support. + */ + enum Unit { + + /** + * Milliseconds. + */ + MILLIS(ChronoUnit.MILLIS, "ms", Duration::toMillis), + + /** + * Seconds. + */ + SECONDS(ChronoUnit.SECONDS, "s", Duration::getSeconds), + + /** + * Minutes. + */ + MINUTES(ChronoUnit.MINUTES, "m", Duration::toMinutes), + + /** + * Hours. + */ + HOURS(ChronoUnit.HOURS, "h", Duration::toHours), + + /** + * Days. + */ + DAYS(ChronoUnit.DAYS, "d", Duration::toDays); + + private final ChronoUnit chronoUnit; + + private final String suffix; + + private Function longValue; + + Unit(ChronoUnit chronoUnit, String suffix, Function toUnit) { + this.chronoUnit = chronoUnit; + this.suffix = suffix; + this.longValue = toUnit; + } + + public Duration parse(String value) { + return Duration.of(Long.valueOf(value), this.chronoUnit); + } + + public long longValue(Duration value) { + return this.longValue.apply(value); + } + + public static Unit fromChronoUnit(ChronoUnit chronoUnit) { + if (chronoUnit == null) { + return Unit.MILLIS; + } + for (Unit candidate : values()) { + if (candidate.chronoUnit == chronoUnit) { + return candidate; + } + } + throw new IllegalArgumentException("Unknown unit " + chronoUnit); + } + + public static Unit fromSuffix(String suffix) { + for (Unit candidate : values()) { + if (candidate.suffix.equalsIgnoreCase(suffix)) { + return candidate; + } + } + throw new IllegalArgumentException("Unknown unit '" + suffix + "'"); + } + } +} 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 29b5756c13..5549260f66 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 @@ -30,6 +30,7 @@ * @author Thomas Darimont * @author Christoph Strobl * @author Jordi Llach + * @author Mark Paluch */ @Target({ ElementType.ANNOTATION_TYPE, ElementType.FIELD }) @Retention(RetentionPolicy.RUNTIME) @@ -136,27 +137,32 @@ int expireAfterSeconds() default -1; /** - * 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}. The expression can result in a a valid - * expiration {@link String} following the conventions already mentioned or a {@link java.time.Duration}. + * Alternative for {@link #expireAfterSeconds()} to configure the timeout after which the document should expire. + * Defaults to an empty {@link String} for no expiry. Accepts numeric values followed by their unit of measure: + *

    + *
  • d: Days
  • + *
  • h: Hours
  • + *
  • m: Minutes
  • + *
  • s: Seconds
  • + *
  • Alternatively: A Spring {@literal template expression}. The expression can result in a + * {@link java.time.Duration} or a valid expiration {@link String} according to the already mentioned + * conventions.
  • + *
+ * Supports ISO-8601 style. * - *
-	 *     
+	 * 
+	 *
+	 * @Indexed(expireAfter = "10s") String expireAfterTenSeconds;
 	 *
-	 * @Indexed(expireAfter = "10s")
-	 * String expireAfterTenSeconds;
+	 * @Indexed(expireAfter = "1d") String expireAfterOneDay;
 	 *
-	 * @Indexed(expireAfter = "1d")
-	 * String expireAfterOneDay;
+	 * @Indexed(expireAfter = "P2D") String expireAfterTwoDays;
 	 *
-	 * @Indexed(expireAfter = "#{@mySpringBean.timeout}")
-	 * String expireAfterTimeoutObtainedFromSpringBean;
-	 *     
+	 * @Indexed(expireAfter = "#{@mySpringBean.timeout}") String expireAfterTimeoutObtainedFromSpringBean;
 	 * 
* - * @return {@literal 0s} by default. + * @return empty by default. * @since 2.2 */ - String expireAfter() default "0s"; + String expireAfter() 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 fdcdd5d2e1..041241c0fc 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 @@ -29,12 +29,11 @@ import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; + import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.domain.Sort; import org.springframework.data.mapping.Association; @@ -62,7 +61,6 @@ 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; @@ -81,7 +79,6 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver { private static final Logger LOGGER = LoggerFactory.getLogger(MongoPersistentEntityIndexResolver.class); - private static final Pattern TIMEOUT_PATTERN = Pattern.compile("(\\d+)(\\W+)?([dhms])"); private static final SpelExpressionParser PARSER = new SpelExpressionParser(); private final MongoMappingContext mappingContext; @@ -353,7 +350,6 @@ protected List createCompoundIndexDefinitions(String dotP return indexDefinitions; } - @SuppressWarnings("deprecation") protected IndexDefinitionHolder createCompoundIndexDefinition(String dotPath, String collection, CompoundIndex index, MongoPersistentEntity entity) { @@ -390,8 +386,7 @@ private org.bson.Document resolveCompoundIndexKeyFromStringDefinition(String dot return new org.bson.Document(dotPath, 1); } - Object keyDefToUse = evaluatePotentialTemplateExpression(keyDefinitionString, - getEvaluationContextForProperty(entity)); + Object keyDefToUse = evaluate(keyDefinitionString, getEvaluationContextForProperty(entity)); org.bson.Document dbo = (keyDefToUse instanceof org.bson.Document) ? (org.bson.Document) keyDefToUse : org.bson.Document.parse(ObjectUtils.nullSafeToString(keyDefToUse)); @@ -450,11 +445,11 @@ protected IndexDefinitionHolder createIndexDefinition(String dotPath, String col indexDefinition.expire(index.expireAfterSeconds(), TimeUnit.SECONDS); } - if (!index.expireAfter().isEmpty() && !index.expireAfter().equals("0s")) { + if (StringUtils.hasText(index.expireAfter())) { 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.", + "@Indexed already defines an expiration timeout of %s seconds via Indexed#expireAfterSeconds. Please make to use either expireAfterSeconds or expireAfter.", index.expireAfterSeconds())); } @@ -480,7 +475,7 @@ protected EvaluationContext getEvaluationContext() { /** * Get the {@link EvaluationContext} for a given {@link PersistentEntity entity} the default one. - * + * * @param persistentEntity can be {@literal null} * @return */ @@ -546,10 +541,15 @@ protected IndexDefinitionHolder createGeoSpatialIndexDefinition(String dotPath, private String pathAwareIndexName(String indexName, String dotPath, @Nullable PersistentEntity entity, @Nullable MongoPersistentProperty property) { - String nameToUse = StringUtils.hasText(indexName) - ? ObjectUtils - .nullSafeToString(evaluatePotentialTemplateExpression(indexName, getEvaluationContextForProperty(entity))) - : ""; + String nameToUse = ""; + if (StringUtils.hasText(indexName)) { + + Object result = evaluate(indexName, getEvaluationContextForProperty(entity)); + + if (result != null) { + nameToUse = ObjectUtils.nullSafeToString(result); + } + } if (!StringUtils.hasText(dotPath) || (property != null && dotPath.equals(property.getFieldName()))) { return StringUtils.hasText(nameToUse) ? nameToUse : dotPath; @@ -607,7 +607,7 @@ private void resolveAndAddIndexesForAssociation(Association indexDefinitions = prepareMappingContextAndResolveIndexForType( IndexOnLevelZero.class); - assertThat(indexDefinitions, hasSize(1)); + assertThat(indexDefinitions).hasSize(1); assertIndexPathAndCollection("indexedProperty", "Zero", indexDefinitions.get(0)); } @@ -84,7 +85,7 @@ public void indexPathOnLevelOneIsResolvedCorrectly() { List indexDefinitions = prepareMappingContextAndResolveIndexForType(IndexOnLevelOne.class); - assertThat(indexDefinitions, hasSize(1)); + assertThat(indexDefinitions).hasSize(1); assertIndexPathAndCollection("zero.indexedProperty", "One", indexDefinitions.get(0)); } @@ -95,7 +96,7 @@ public void shouldResolveIndexViaClass() { IndexResolver indexResolver = IndexResolver.create(mappingContext); Iterable definitions = indexResolver.resolveIndexFor(IndexOnLevelOne.class); - assertThat(definitions.iterator().hasNext(), is(true)); + assertThat(definitions).isNotEmpty(); } @Test // DATAMONGO-899 @@ -103,7 +104,7 @@ public void deeplyNestedIndexPathIsResolvedCorrectly() { List indexDefinitions = prepareMappingContextAndResolveIndexForType(IndexOnLevelTwo.class); - assertThat(indexDefinitions, hasSize(1)); + assertThat(indexDefinitions).hasSize(1); assertIndexPathAndCollection("one.zero.indexedProperty", "Two", indexDefinitions.get(0)); } @@ -113,7 +114,7 @@ public void resolvesIndexPathNameForNamedPropertiesCorrectly() { List indexDefinitions = prepareMappingContextAndResolveIndexForType( IndexOnLevelOneWithExplicitlyNamedField.class); - assertThat(indexDefinitions, hasSize(1)); + assertThat(indexDefinitions).hasSize(1); assertIndexPathAndCollection("customZero.customFieldName", "indexOnLevelOneWithExplicitlyNamedField", indexDefinitions.get(0)); } @@ -125,7 +126,7 @@ public void resolvesIndexDefinitionCorrectly() { IndexOnLevelZero.class); IndexDefinition indexDefinition = indexDefinitions.get(0).getIndexDefinition(); - assertThat(indexDefinition.getIndexOptions(), equalTo(new org.bson.Document().append("name", "indexedProperty"))); + assertThat(indexDefinition.getIndexOptions()).isEqualTo(new org.bson.Document("name", "indexedProperty")); } @Test // DATAMONGO-899 @@ -135,8 +136,8 @@ public void resolvesIndexDefinitionOptionsCorrectly() { WithOptionsOnIndexedProperty.class); IndexDefinition indexDefinition = indexDefinitions.get(0).getIndexDefinition(); - assertThat(indexDefinition.getIndexOptions(), equalTo(new org.bson.Document().append("name", "indexedProperty") - .append("unique", true).append("sparse", true).append("background", true).append("expireAfterSeconds", 10L))); + assertThat(indexDefinition.getIndexOptions()).isEqualTo(new org.bson.Document().append("name", "indexedProperty") + .append("unique", true).append("sparse", true).append("background", true).append("expireAfterSeconds", 10L)); } @Test // DATAMONGO-1297 @@ -144,9 +145,9 @@ public void resolvesIndexOnDbrefWhenDefined() { List indexDefinitions = prepareMappingContextAndResolveIndexForType(WithDbRef.class); - assertThat(indexDefinitions, hasSize(1)); - assertThat(indexDefinitions.get(0).getCollection(), equalTo("withDbRef")); - assertThat(indexDefinitions.get(0).getIndexKeys(), equalTo(new org.bson.Document().append("indexedDbRef", 1))); + assertThat(indexDefinitions).hasSize(1); + assertThat(indexDefinitions.get(0).getCollection()).isEqualTo("withDbRef"); + assertThat(indexDefinitions.get(0).getIndexKeys()).isEqualTo(new org.bson.Document("indexedDbRef", 1)); } @Test // DATAMONGO-1297 @@ -155,10 +156,9 @@ public void resolvesIndexOnDbrefWhenDefinedOnNestedElement() { List indexDefinitions = prepareMappingContextAndResolveIndexForType( WrapperOfWithDbRef.class); - assertThat(indexDefinitions, hasSize(1)); - assertThat(indexDefinitions.get(0).getCollection(), equalTo("wrapperOfWithDbRef")); - assertThat(indexDefinitions.get(0).getIndexKeys(), - equalTo(new org.bson.Document().append("nested.indexedDbRef", 1))); + assertThat(indexDefinitions).hasSize(1); + assertThat(indexDefinitions.get(0).getCollection()).isEqualTo("wrapperOfWithDbRef"); + assertThat(indexDefinitions.get(0).getIndexKeys()).isEqualTo(new org.bson.Document("nested.indexedDbRef", 1)); } @Test // DATAMONGO-1163 @@ -167,9 +167,9 @@ public void resolveIndexDefinitionInMetaAnnotatedFields() { List indexDefinitions = prepareMappingContextAndResolveIndexForType( IndexOnMetaAnnotatedField.class); - assertThat(indexDefinitions, hasSize(1)); - assertThat(indexDefinitions.get(0).getCollection(), equalTo("indexOnMetaAnnotatedField")); - assertThat(indexDefinitions.get(0).getIndexOptions(), equalTo(new org.bson.Document().append("name", "_name"))); + assertThat(indexDefinitions).hasSize(1); + assertThat(indexDefinitions.get(0).getCollection()).isEqualTo("indexOnMetaAnnotatedField"); + assertThat(indexDefinitions.get(0).getIndexOptions()).isEqualTo(new org.bson.Document("name", "_name")); } @Test // DATAMONGO-1373 @@ -178,13 +178,15 @@ public void resolveIndexDefinitionInComposedAnnotatedFields() { List indexDefinitions = prepareMappingContextAndResolveIndexForType( IndexedDocumentWithComposedAnnotations.class); - assertThat(indexDefinitions, hasSize(2)); + assertThat(indexDefinitions).hasSize(2); IndexDefinitionHolder indexDefinitionHolder = indexDefinitions.get(1); - assertThat(indexDefinitionHolder.getIndexKeys(), isBsonObject().containing("fieldWithMyIndexName", 1)); - assertThat(indexDefinitionHolder.getIndexOptions(), - isBsonObject().containing("sparse", true).containing("unique", true).containing("name", "my_index_name")); + assertThat(indexDefinitionHolder.getIndexKeys()).containsEntry("fieldWithMyIndexName", 1); + assertThat(indexDefinitionHolder.getIndexOptions()) // + .containsEntry("sparse", true) // + .containsEntry("unique", true) // + .containsEntry("name", "my_index_name"); } @Test // DATAMONGO-1373 @@ -193,13 +195,15 @@ public void resolveIndexDefinitionInCustomComposedAnnotatedFields() { List indexDefinitions = prepareMappingContextAndResolveIndexForType( IndexedDocumentWithComposedAnnotations.class); - assertThat(indexDefinitions, hasSize(2)); + assertThat(indexDefinitions).hasSize(2); IndexDefinitionHolder indexDefinitionHolder = indexDefinitions.get(0); - assertThat(indexDefinitionHolder.getIndexKeys(), isBsonObject().containing("fieldWithDifferentIndexName", 1)); - assertThat(indexDefinitionHolder.getIndexOptions(), - isBsonObject().containing("sparse", true).containing("name", "different_name").notContaining("unique")); + assertThat(indexDefinitionHolder.getIndexKeys()).containsEntry("fieldWithDifferentIndexName", 1); + assertThat(indexDefinitionHolder.getIndexOptions()) // + .containsEntry("sparse", true) // + .containsEntry("name", "different_name") // + .doesNotContainKey("unique"); } @Test // DATAMONGO-2112 @@ -208,7 +212,16 @@ public void shouldResolveTimeoutFromString() { List indexDefinitions = prepareMappingContextAndResolveIndexForType( WithExpireAfterAsPlainString.class); - Assertions.assertThat(indexDefinitions.get(0).getIndexOptions()).containsEntry("expireAfterSeconds", 600L); + assertThat(indexDefinitions.get(0).getIndexOptions()).containsEntry("expireAfterSeconds", 600L); + } + + @Test // DATAMONGO-2112 + public void shouldResolveTimeoutFromIso8601String() { + + List indexDefinitions = prepareMappingContextAndResolveIndexForType( + WithIso8601Style.class); + + assertThat(indexDefinitions.get(0).getIndexOptions()).containsEntry("expireAfterSeconds", 86400L); } @Test // DATAMONGO-2112 @@ -217,7 +230,7 @@ public void shouldResolveTimeoutFromExpression() { List indexDefinitions = prepareMappingContextAndResolveIndexForType( WithExpireAfterAsExpression.class); - Assertions.assertThat(indexDefinitions.get(0).getIndexOptions()).containsEntry("expireAfterSeconds", 11L); + assertThat(indexDefinitions.get(0).getIndexOptions()).containsEntry("expireAfterSeconds", 11L); } @Test // DATAMONGO-2112 @@ -226,7 +239,7 @@ public void shouldResolveTimeoutFromExpressionReturningDuration() { List indexDefinitions = prepareMappingContextAndResolveIndexForType( WithExpireAfterAsExpressionResultingInDuration.class); - Assertions.assertThat(indexDefinitions.get(0).getIndexOptions()).containsEntry("expireAfterSeconds", 100L); + assertThat(indexDefinitions.get(0).getIndexOptions()).containsEntry("expireAfterSeconds", 100L); } @Test // DATAMONGO-2112 @@ -235,9 +248,8 @@ public void shouldErrorOnInvalidTimeoutExpression() { MongoMappingContext mappingContext = prepareMappingContext(WithInvalidExpireAfter.class); MongoPersistentEntityIndexResolver indexResolver = new MongoPersistentEntityIndexResolver(mappingContext); - Assertions.assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> indexResolver + assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> indexResolver .resolveIndexForEntity(mappingContext.getRequiredPersistentEntity(WithInvalidExpireAfter.class))); - } @Test // DATAMONGO-2112 @@ -246,7 +258,7 @@ public void shouldErrorOnDuplicateTimeoutExpression() { MongoMappingContext mappingContext = prepareMappingContext(WithDuplicateExpiry.class); MongoPersistentEntityIndexResolver indexResolver = new MongoPersistentEntityIndexResolver(mappingContext); - Assertions.assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> indexResolver + assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> indexResolver .resolveIndexForEntity(mappingContext.getRequiredPersistentEntity(WithDuplicateExpiry.class))); } @@ -256,7 +268,7 @@ public void resolveExpressionIndexName() { List indexDefinitions = prepareMappingContextAndResolveIndexForType( WithIndexNameAsExpression.class); - Assertions.assertThat(indexDefinitions.get(0).getIndexOptions()).containsEntry("name", "my1st"); + assertThat(indexDefinitions.get(0).getIndexOptions()).containsEntry("name", "my1st"); } @Document("Zero") @@ -322,13 +334,13 @@ static class IndexedDocumentWithComposedAnnotations { @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.FIELD }) @ComposedIndexedAnnotation(indexName = "different_name", beUnique = false) - static @interface CustomIndexedAnnotation { + @interface CustomIndexedAnnotation { } @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.FIELD, ElementType.ANNOTATION_TYPE }) @Indexed - static @interface ComposedIndexedAnnotation { + @interface ComposedIndexedAnnotation { @AliasFor(annotation = Indexed.class, attribute = "unique") boolean beUnique() default true; @@ -343,7 +355,7 @@ static class IndexedDocumentWithComposedAnnotations { @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) @org.springframework.data.mongodb.core.mapping.Field - static @interface ComposedFieldAnnotation { + @interface ComposedFieldAnnotation { @AliasFor(annotation = org.springframework.data.mongodb.core.mapping.Field.class, attribute = "value") String name() default "_id"; @@ -354,6 +366,11 @@ static class WithExpireAfterAsPlainString { @Indexed(expireAfter = "10m") String withTimeout; } + @Document + class WithIso8601Style { + @Indexed(expireAfter = "P1D") String withTimeout; + } + @Document static class WithExpireAfterAsExpression { @Indexed(expireAfter = "#{10 + 1 + 's'}") String withTimeout; @@ -384,7 +401,6 @@ static class WithIndexNameAsExpression { @Retention(RetentionPolicy.RUNTIME) @Indexed @interface IndexedFieldAnnotation { - } @Document @@ -405,7 +421,7 @@ public void geoSpatialIndexPathOnLevelZeroIsResolvedCorrectly() { List indexDefinitions = prepareMappingContextAndResolveIndexForType( GeoSpatialIndexOnLevelZero.class); - assertThat(indexDefinitions, hasSize(1)); + assertThat(indexDefinitions).hasSize(1); assertIndexPathAndCollection("geoIndexedProperty", "Zero", indexDefinitions.get(0)); } @@ -415,7 +431,7 @@ public void geoSpatialIndexPathOnLevelOneIsResolvedCorrectly() { List indexDefinitions = prepareMappingContextAndResolveIndexForType( GeoSpatialIndexOnLevelOne.class); - assertThat(indexDefinitions, hasSize(1)); + assertThat(indexDefinitions).hasSize(1); assertIndexPathAndCollection("zero.geoIndexedProperty", "One", indexDefinitions.get(0)); } @@ -425,7 +441,7 @@ public void depplyNestedGeoSpatialIndexPathIsResolvedCorrectly() { List indexDefinitions = prepareMappingContextAndResolveIndexForType( GeoSpatialIndexOnLevelTwo.class); - assertThat(indexDefinitions, hasSize(1)); + assertThat(indexDefinitions).hasSize(1); assertIndexPathAndCollection("one.zero.geoIndexedProperty", "Two", indexDefinitions.get(0)); } @@ -437,8 +453,8 @@ public void resolvesIndexDefinitionOptionsCorrectly() { IndexDefinition indexDefinition = indexDefinitions.get(0).getIndexDefinition(); - assertThat(indexDefinition.getIndexOptions(), equalTo( - new org.bson.Document().append("name", "location").append("min", 1).append("max", 100).append("bits", 2))); + assertThat(indexDefinition.getIndexOptions()).isEqualTo( + new org.bson.Document().append("name", "location").append("min", 1).append("max", 100).append("bits", 2)); } @Test // DATAMONGO-1373 @@ -449,10 +465,10 @@ public void resolvesComposedAnnotationIndexDefinitionOptionsCorrectly() { IndexDefinition indexDefinition = indexDefinitions.get(0).getIndexDefinition(); - assertThat(indexDefinition.getIndexKeys(), - isBsonObject().containing("location", "geoHaystack").containing("What light?", 1)); - assertThat(indexDefinition.getIndexOptions(), - isBsonObject().containing("name", "my_geo_index_name").containing("bucketSize", 2.0)); + assertThat(indexDefinition.getIndexKeys()).containsEntry("location", "geoHaystack").containsEntry("What light?", + 1); + assertThat(indexDefinition.getIndexOptions()).containsEntry("name", "my_geo_index_name") + .containsEntry("bucketSize", 2.0); } @Test // DATAMONGO-2112 @@ -461,7 +477,7 @@ public void resolveExpressionIndexNameForGeoIndex() { List indexDefinitions = prepareMappingContextAndResolveIndexForType( GeoIndexWithNameAsExpression.class); - Assertions.assertThat(indexDefinitions.get(0).getIndexOptions()).containsEntry("name", "my1st"); + assertThat(indexDefinitions.get(0).getIndexOptions()).containsEntry("name", "my1st"); } @Document("Zero") @@ -531,7 +547,7 @@ public void compoundIndexPathOnLevelZeroIsResolvedCorrectly() { List indexDefinitions = prepareMappingContextAndResolveIndexForType( CompoundIndexOnLevelZero.class); - assertThat(indexDefinitions, hasSize(1)); + assertThat(indexDefinitions).hasSize(1); assertIndexPathAndCollection(new String[] { "foo", "bar" }, "CompoundIndexOnLevelZero", indexDefinitions.get(0)); } @@ -542,9 +558,9 @@ public void compoundIndexOptionsResolvedCorrectly() { CompoundIndexOnLevelZero.class); IndexDefinition indexDefinition = indexDefinitions.get(0).getIndexDefinition(); - assertThat(indexDefinition.getIndexOptions(), equalTo(new org.bson.Document().append("name", "compound_index") - .append("unique", true).append("sparse", true).append("background", true))); - assertThat(indexDefinition.getIndexKeys(), equalTo(new org.bson.Document().append("foo", 1).append("bar", -1))); + assertThat(indexDefinition.getIndexOptions()).isEqualTo(new org.bson.Document("name", "compound_index") + .append("unique", true).append("sparse", true).append("background", true)); + assertThat(indexDefinition.getIndexKeys()).isEqualTo(new org.bson.Document().append("foo", 1).append("bar", -1)); } @Test // DATAMONGO-909 @@ -554,9 +570,9 @@ public void compoundIndexOnSuperClassResolvedCorrectly() { IndexDefinedOnSuperClass.class); IndexDefinition indexDefinition = indexDefinitions.get(0).getIndexDefinition(); - assertThat(indexDefinition.getIndexOptions(), equalTo(new org.bson.Document().append("name", "compound_index") - .append("unique", true).append("sparse", true).append("background", true))); - assertThat(indexDefinition.getIndexKeys(), equalTo(new org.bson.Document().append("foo", 1).append("bar", -1))); + assertThat(indexDefinition.getIndexOptions()).isEqualTo(new org.bson.Document().append("name", "compound_index") + .append("unique", true).append("sparse", true).append("background", true)); + assertThat(indexDefinition.getIndexKeys()).isEqualTo(new org.bson.Document().append("foo", 1).append("bar", -1)); } @Test // DATAMONGO-827 @@ -566,9 +582,9 @@ public void compoundIndexDoesNotSpecifyNameWhenUsingGenerateName() { ComountIndexWithAutogeneratedName.class); IndexDefinition indexDefinition = indexDefinitions.get(0).getIndexDefinition(); - assertThat(indexDefinition.getIndexOptions(), - equalTo(new org.bson.Document().append("unique", true).append("sparse", true).append("background", true))); - assertThat(indexDefinition.getIndexKeys(), equalTo(new org.bson.Document().append("foo", 1).append("bar", -1))); + assertThat(indexDefinition.getIndexOptions()) + .isEqualTo(new org.bson.Document().append("unique", true).append("sparse", true).append("background", true)); + assertThat(indexDefinition.getIndexKeys()).isEqualTo(new org.bson.Document().append("foo", 1).append("bar", -1)); } @Test // DATAMONGO-929 @@ -577,7 +593,7 @@ public void compoundIndexPathOnLevelOneIsResolvedCorrectly() { List indexDefinitions = prepareMappingContextAndResolveIndexForType( CompoundIndexOnLevelOne.class); - assertThat(indexDefinitions, hasSize(1)); + assertThat(indexDefinitions).hasSize(1); assertIndexPathAndCollection(new String[] { "zero.foo", "zero.bar" }, "CompoundIndexOnLevelOne", indexDefinitions.get(0)); } @@ -588,7 +604,7 @@ public void emptyCompoundIndexPathOnLevelOneIsResolvedCorrectly() { List indexDefinitions = prepareMappingContextAndResolveIndexForType( CompoundIndexOnLevelOneWithEmptyIndexDefinition.class); - assertThat(indexDefinitions, hasSize(1)); + assertThat(indexDefinitions).hasSize(1); assertIndexPathAndCollection(new String[] { "zero" }, "CompoundIndexOnLevelZeroWithEmptyIndexDef", indexDefinitions.get(0)); } @@ -599,7 +615,7 @@ public void singleCompoundIndexPathOnLevelZeroIsResolvedCorrectly() { List indexDefinitions = prepareMappingContextAndResolveIndexForType( SingleCompoundIndex.class); - assertThat(indexDefinitions, hasSize(1)); + assertThat(indexDefinitions).hasSize(1); assertIndexPathAndCollection(new String[] { "foo", "bar" }, "CompoundIndexOnLevelZero", indexDefinitions.get(0)); } @@ -609,10 +625,10 @@ public void singleCompoundIndexUsingComposedAnnotationsOnTypeResolvedCorrectly() List indexDefinitions = prepareMappingContextAndResolveIndexForType( CompoundIndexDocumentWithComposedAnnotation.class); - assertThat(indexDefinitions, hasSize(1)); - assertThat(indexDefinitions.get(0).getIndexKeys(), isBsonObject().containing("foo", 1).containing("bar", -1)); - assertThat(indexDefinitions.get(0).getIndexOptions(), isBsonObject().containing("name", "my_compound_index_name") - .containing("unique", true).containing("background", true)); + assertThat(indexDefinitions).hasSize(1); + assertThat(indexDefinitions.get(0).getIndexKeys()).containsEntry("foo", 1).containsEntry("bar", -1); + assertThat(indexDefinitions.get(0).getIndexOptions()).containsEntry("name", "my_compound_index_name") + .containsEntry("unique", true).containsEntry("background", true); } @Test // DATAMONGO-2112 @@ -621,7 +637,7 @@ public void resolveExpressionIndexNameForCompoundIndex() { List indexDefinitions = prepareMappingContextAndResolveIndexForType( CompoundIndexWithNameExpression.class); - Assertions.assertThat(indexDefinitions.get(0).getIndexOptions()).containsEntry("name", "cmp2name"); + assertThat(indexDefinitions.get(0).getIndexOptions()).containsEntry("name", "cmp2name"); } @Test // DATAMONGO-2112 @@ -630,7 +646,7 @@ public void resolveExpressionDefForCompoundIndex() { List indexDefinitions = prepareMappingContextAndResolveIndexForType( CompoundIndexWithDefExpression.class); - assertThat(indexDefinitions, hasSize(1)); + assertThat(indexDefinitions).hasSize(1); assertIndexPathAndCollection(new String[] { "foo", "bar" }, "compoundIndexWithDefExpression", indexDefinitions.get(0)); } @@ -660,22 +676,16 @@ static class CompoundIndexOnLevelZeroWithEmptyIndexDef {} unique = true) static class SingleCompoundIndex {} - static class IndexDefinedOnSuperClass extends CompoundIndexOnLevelZero { - - } + static class IndexDefinedOnSuperClass extends CompoundIndexOnLevelZero {} @Document("ComountIndexWithAutogeneratedName") @CompoundIndexes({ @CompoundIndex(useGeneratedName = true, def = "{'foo': 1, 'bar': -1}", background = true, sparse = true, unique = true) }) - static class ComountIndexWithAutogeneratedName { - - } + static class ComountIndexWithAutogeneratedName {} @Document("WithComposedAnnotation") @ComposedCompoundIndex - static class CompoundIndexDocumentWithComposedAnnotation { - - } + static class CompoundIndexDocumentWithComposedAnnotation {} @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.TYPE }) @@ -716,7 +726,8 @@ public void shouldResolveSingleFieldTextIndexCorrectly() { List indexDefinitions = prepareMappingContextAndResolveIndexForType( TextIndexOnSinglePropertyInRoot.class); - assertThat(indexDefinitions.size(), equalTo(1)); + + assertThat(indexDefinitions).hasSize(1); assertIndexPathAndCollection("bar", "textIndexOnSinglePropertyInRoot", indexDefinitions.get(0)); } @@ -725,7 +736,8 @@ public void shouldResolveMultiFieldTextIndexCorrectly() { List indexDefinitions = prepareMappingContextAndResolveIndexForType( TextIndexOnMutiplePropertiesInRoot.class); - assertThat(indexDefinitions.size(), equalTo(1)); + + assertThat(indexDefinitions).hasSize(1); assertIndexPathAndCollection(new String[] { "foo", "bar" }, "textIndexOnMutiplePropertiesInRoot", indexDefinitions.get(0)); } @@ -735,7 +747,7 @@ public void shouldResolveTextIndexOnElementCorrectly() { List indexDefinitions = prepareMappingContextAndResolveIndexForType( TextIndexOnNestedRoot.class); - assertThat(indexDefinitions.size(), equalTo(1)); + assertThat(indexDefinitions).hasSize(1); assertIndexPathAndCollection(new String[] { "nested.foo" }, "textIndexOnNestedRoot", indexDefinitions.get(0)); } @@ -744,12 +756,12 @@ public void shouldResolveTextIndexOnElementWithWeightCorrectly() { List indexDefinitions = prepareMappingContextAndResolveIndexForType( TextIndexOnNestedWithWeightRoot.class); - assertThat(indexDefinitions.size(), equalTo(1)); + assertThat(indexDefinitions).hasSize(1); assertIndexPathAndCollection(new String[] { "nested.foo" }, "textIndexOnNestedWithWeightRoot", indexDefinitions.get(0)); org.bson.Document weights = DocumentTestUtils.getAsDocument(indexDefinitions.get(0).getIndexOptions(), "weights"); - assertThat(weights.get("nested.foo"), is((Object) 5F)); + assertThat(weights.get("nested.foo")).isEqualTo(5F); } @Test // DATAMONGO-937 @@ -757,13 +769,13 @@ public void shouldResolveTextIndexOnElementWithMostSpecificWeightCorrectly() { List indexDefinitions = prepareMappingContextAndResolveIndexForType( TextIndexOnNestedWithMostSpecificValueRoot.class); - assertThat(indexDefinitions.size(), equalTo(1)); + assertThat(indexDefinitions).hasSize(1); assertIndexPathAndCollection(new String[] { "nested.foo", "nested.bar" }, "textIndexOnNestedWithMostSpecificValueRoot", indexDefinitions.get(0)); org.bson.Document weights = DocumentTestUtils.getAsDocument(indexDefinitions.get(0).getIndexOptions(), "weights"); - assertThat(weights.get("nested.foo"), is((Object) 5F)); - assertThat(weights.get("nested.bar"), is((Object) 10F)); + assertThat(weights.get("nested.foo")).isEqualTo(5F); + assertThat(weights.get("nested.bar")).isEqualTo(10F); } @Test // DATAMONGO-937 @@ -771,7 +783,7 @@ public void shouldSetDefaultLanguageCorrectly() { List indexDefinitions = prepareMappingContextAndResolveIndexForType( DocumentWithDefaultLanguage.class); - assertThat(indexDefinitions.get(0).getIndexOptions().get("default_language"), is((Object) "spanish")); + assertThat(indexDefinitions.get(0).getIndexOptions()).containsEntry("default_language", "spanish"); } @Test // DATAMONGO-937, DATAMONGO-1049 @@ -779,7 +791,7 @@ public void shouldResolveTextIndexLanguageOverrideCorrectly() { List indexDefinitions = prepareMappingContextAndResolveIndexForType( DocumentWithLanguageOverride.class); - assertThat(indexDefinitions.get(0).getIndexOptions().get("language_override"), is((Object) "lang")); + assertThat(indexDefinitions.get(0).getIndexOptions()).containsEntry("language_override", "lang"); } @Test // DATAMONGO-1049 @@ -787,7 +799,7 @@ public void shouldIgnoreTextIndexLanguageOverrideOnNestedElements() { List indexDefinitions = prepareMappingContextAndResolveIndexForType( DocumentWithLanguageOverrideOnNestedElement.class); - assertThat(indexDefinitions.get(0).getIndexOptions().get("language_override"), is(nullValue())); + assertThat(indexDefinitions.get(0).getIndexOptions().get("language_override")).isNull(); } @Test // DATAMONGO-1049 @@ -795,7 +807,8 @@ public void shouldNotCreateIndexDefinitionWhenOnlyLanguageButNoTextIndexPresent( List indexDefinitions = prepareMappingContextAndResolveIndexForType( DocumentWithNoTextIndexPropertyButReservedFieldLanguage.class); - assertThat(indexDefinitions, is(empty())); + + assertThat(indexDefinitions).isEmpty(); } @Test // DATAMONGO-1049 @@ -803,7 +816,8 @@ public void shouldNotCreateIndexDefinitionWhenOnlyAnnotatedLanguageButNoTextInde List indexDefinitions = prepareMappingContextAndResolveIndexForType( DocumentWithNoTextIndexPropertyButReservedFieldLanguageAnnotated.class); - assertThat(indexDefinitions, is(empty())); + + assertThat(indexDefinitions).isEmpty(); } @Test // DATAMONGO-1049 @@ -811,7 +825,8 @@ public void shouldPreferExplicitlyAnnotatedLanguageProperty() { List indexDefinitions = prepareMappingContextAndResolveIndexForType( DocumentWithOverlappingLanguageProps.class); - assertThat(indexDefinitions.get(0).getIndexOptions().get("language_override"), is((Object) "lang")); + + assertThat(indexDefinitions.get(0).getIndexOptions()).containsEntry("language_override", "lang"); } @Test // DATAMONGO-1373 @@ -821,7 +836,7 @@ public void shouldResolveComposedAnnotationCorrectly() { TextIndexedDocumentWithComposedAnnotation.class); org.bson.Document weights = DocumentTestUtils.getAsDocument(indexDefinitions.get(0).getIndexOptions(), "weights"); - assertThat(weights, isBsonObject().containing("foo", 99f)); + assertThat(weights).containsEntry("foo", 99f); } @Document @@ -919,7 +934,7 @@ static class TextIndexedDocumentWithComposedAnnotation { @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.FIELD, ElementType.ANNOTATION_TYPE }) @TextIndexed - static @interface ComposedTextIndexedAnnotation { + @interface ComposedTextIndexedAnnotation { @AliasFor(annotation = TextIndexed.class, attribute = "weight") float heavyweight() default 99f; @@ -933,25 +948,27 @@ public void multipleIndexesResolvedCorrectly() { List indexDefinitions = prepareMappingContextAndResolveIndexForType(MixedIndexRoot.class); - assertThat(indexDefinitions, hasSize(2)); - assertThat(indexDefinitions.get(0).getIndexDefinition(), instanceOf(Index.class)); - assertThat(indexDefinitions.get(1).getIndexDefinition(), instanceOf(GeospatialIndex.class)); + assertThat(indexDefinitions).hasSize(2); + assertThat(indexDefinitions.get(0).getIndexDefinition()).isInstanceOf(Index.class); + assertThat(indexDefinitions.get(1).getIndexDefinition()).isInstanceOf(GeospatialIndex.class); } @Test // DATAMONGO-899 public void cyclicPropertyReferenceOverDBRefShouldNotBeTraversed() { List indexDefinitions = prepareMappingContextAndResolveIndexForType(Inner.class); - assertThat(indexDefinitions, hasSize(1)); - assertThat(indexDefinitions.get(0).getIndexDefinition().getIndexKeys(), - equalTo(new org.bson.Document().append("outer", 1))); + + assertThat(indexDefinitions).hasSize(1); + assertThat(indexDefinitions.get(0).getIndexDefinition().getIndexKeys()) + .isEqualTo(new org.bson.Document().append("outer", 1)); } @Test // DATAMONGO-899 public void associationsShouldNotBeTraversed() { List indexDefinitions = prepareMappingContextAndResolveIndexForType(Outer.class); - assertThat(indexDefinitions, empty()); + + assertThat(indexDefinitions).isEmpty(); } @Test // DATAMONGO-926 @@ -959,7 +976,8 @@ public void shouldNotRunIntoStackOverflow() { List indexDefinitions = prepareMappingContextAndResolveIndexForType( CycleStartingInBetween.class); - assertThat(indexDefinitions, hasSize(1)); + + assertThat(indexDefinitions).hasSize(1); } @Test // DATAMONGO-926 @@ -968,7 +986,7 @@ public void indexShouldBeFoundEvenForCyclePropertyReferenceOnLevelZero() { List indexDefinitions = prepareMappingContextAndResolveIndexForType(CycleLevelZero.class); assertIndexPathAndCollection("indexedProperty", "cycleLevelZero", indexDefinitions.get(0)); assertIndexPathAndCollection("cyclicReference.indexedProperty", "cycleLevelZero", indexDefinitions.get(1)); - assertThat(indexDefinitions, hasSize(2)); + assertThat(indexDefinitions).hasSize(2); } @Test // DATAMONGO-926 @@ -976,7 +994,7 @@ public void indexShouldBeFoundEvenForCyclePropertyReferenceOnLevelOne() { List indexDefinitions = prepareMappingContextAndResolveIndexForType(CycleOnLevelOne.class); assertIndexPathAndCollection("reference.indexedProperty", "cycleOnLevelOne", indexDefinitions.get(0)); - assertThat(indexDefinitions, hasSize(1)); + assertThat(indexDefinitions).hasSize(1); } @Test // DATAMONGO-926 @@ -984,11 +1002,12 @@ public void indexBeResolvedCorrectlyWhenPropertiesOfDifferentTypesAreNamedEquall List indexDefinitions = prepareMappingContextAndResolveIndexForType( NoCycleButIdenticallyNamedProperties.class); + + assertThat(indexDefinitions).hasSize(3); assertIndexPathAndCollection("foo", "noCycleButIdenticallyNamedProperties", indexDefinitions.get(0)); assertIndexPathAndCollection("reference.foo", "noCycleButIdenticallyNamedProperties", indexDefinitions.get(1)); assertIndexPathAndCollection("reference.deep.foo", "noCycleButIdenticallyNamedProperties", indexDefinitions.get(2)); - assertThat(indexDefinitions, hasSize(3)); } @Test // DATAMONGO-949 @@ -997,7 +1016,7 @@ public void shouldNotDetectCycleInSimilarlyNamedProperties() { List indexDefinitions = prepareMappingContextAndResolveIndexForType( SimilarityHolingBean.class); assertIndexPathAndCollection("norm", "similarityHolingBean", indexDefinitions.get(0)); - assertThat(indexDefinitions, hasSize(1)); + assertThat(indexDefinitions).hasSize(1); } @Test // DATAMONGO-962 @@ -1005,7 +1024,8 @@ public void shouldDetectSelfCycleViaCollectionTypeCorrectly() { List indexDefinitions = prepareMappingContextAndResolveIndexForType( SelfCyclingViaCollectionType.class); - assertThat(indexDefinitions, empty()); + + assertThat(indexDefinitions).isEmpty(); } @Test // DATAMONGO-962 @@ -1013,14 +1033,15 @@ public void shouldNotDetectCycleWhenTypeIsUsedMoreThanOnce() { List indexDefinitions = prepareMappingContextAndResolveIndexForType( MultipleObjectsOfSameType.class); - assertThat(indexDefinitions, empty()); + + assertThat(indexDefinitions).isEmpty(); } @Test // DATAMONGO-962 @SuppressWarnings({ "rawtypes", "unchecked" }) public void shouldCatchCyclicReferenceExceptionOnRoot() { - MongoPersistentEntity entity = new BasicMongoPersistentEntity(ClassTypeInformation.from(Object.class)); + MongoPersistentEntity entity = new BasicMongoPersistentEntity<>(ClassTypeInformation.from(Object.class)); MongoPersistentProperty propertyMock = mock(MongoPersistentProperty.class); when(propertyMock.isEntity()).thenReturn(true); @@ -1028,7 +1049,7 @@ public void shouldCatchCyclicReferenceExceptionOnRoot() { when(propertyMock.getActualType()).thenThrow( new MongoPersistentEntityIndexResolver.CyclicPropertyReferenceException("foo", Object.class, "bar")); - MongoPersistentEntity selfCyclingEntity = new BasicMongoPersistentEntity( + MongoPersistentEntity selfCyclingEntity = new BasicMongoPersistentEntity<>( ClassTypeInformation.from(SelfCyclingViaCollectionType.class)); new MongoPersistentEntityIndexResolver(prepareMappingContext(SelfCyclingViaCollectionType.class)) @@ -1041,9 +1062,9 @@ public void shouldAllowMultiplePathsToDeeplyType() { List indexDefinitions = prepareMappingContextAndResolveIndexForType( NoCycleManyPathsToDeepValueObject.class); + assertThat(indexDefinitions).hasSize(2); assertIndexPathAndCollection("l3.valueObject.value", "rules", indexDefinitions.get(0)); assertIndexPathAndCollection("l2.l3.valueObject.value", "rules", indexDefinitions.get(1)); - assertThat(indexDefinitions, hasSize(2)); } @Test // DATAMONGO-1025 @@ -1051,8 +1072,9 @@ public void shouldUsePathIndexAsIndexNameForDocumentsHavingNamedNestedCompoundIn List indexDefinitions = prepareMappingContextAndResolveIndexForType( DocumentWithNestedDocumentHavingNamedCompoundIndex.class); - assertThat((String) indexDefinitions.get(0).getIndexOptions().get("name"), - equalTo("propertyOfTypeHavingNamedCompoundIndex.c_index")); + + assertThat(indexDefinitions.get(0).getIndexOptions()).containsEntry("name", + "propertyOfTypeHavingNamedCompoundIndex.c_index"); } @Test // DATAMONGO-1025 @@ -1060,8 +1082,8 @@ public void shouldUseIndexNameForNestedTypesWithNamedCompoundIndexDefinition() { List indexDefinitions = prepareMappingContextAndResolveIndexForType( DocumentWithNestedTypeHavingNamedCompoundIndex.class); - assertThat((String) indexDefinitions.get(0).getIndexOptions().get("name"), - equalTo("propertyOfTypeHavingNamedCompoundIndex.c_index")); + assertThat(indexDefinitions.get(0).getIndexOptions()).containsEntry("name", + "propertyOfTypeHavingNamedCompoundIndex.c_index"); } @Test // DATAMONGO-1025 @@ -1069,8 +1091,9 @@ public void shouldUsePathIndexAsIndexNameForDocumentsHavingNamedNestedIndexFixed List indexDefinitions = prepareMappingContextAndResolveIndexForType( DocumentWithNestedDocumentHavingNamedIndex.class); - assertThat((String) indexDefinitions.get(0).getIndexOptions().get("name"), - equalTo("propertyOfTypeHavingNamedIndex.property_index")); + + assertThat(indexDefinitions.get(0).getIndexOptions()).containsEntry("name", + "propertyOfTypeHavingNamedIndex.property_index"); } @Test // DATAMONGO-1025 @@ -1078,8 +1101,9 @@ public void shouldUseIndexNameForNestedTypesWithNamedIndexDefinition() { List indexDefinitions = prepareMappingContextAndResolveIndexForType( DocumentWithNestedTypeHavingNamedIndex.class); - assertThat((String) indexDefinitions.get(0).getIndexOptions().get("name"), - equalTo("propertyOfTypeHavingNamedIndex.property_index")); + + assertThat(indexDefinitions.get(0).getIndexOptions()).containsEntry("name", + "propertyOfTypeHavingNamedIndex.property_index"); } @Test // DATAMONGO-1025 @@ -1087,7 +1111,7 @@ public void shouldUseIndexNameOnRootLevel() { List indexDefinitions = prepareMappingContextAndResolveIndexForType( DocumentWithNamedIndex.class); - assertThat((String) indexDefinitions.get(0).getIndexOptions().get("name"), equalTo("property_index")); + assertThat(indexDefinitions.get(0).getIndexOptions()).containsEntry("name", "property_index"); } @Test // DATAMONGO-1087 @@ -1096,9 +1120,9 @@ public void shouldAllowMultiplePropertiesOfSameTypeWithMatchingStartLettersOnRoo List indexDefinitions = prepareMappingContextAndResolveIndexForType( MultiplePropertiesOfSameTypeWithMatchingStartLetters.class); - assertThat(indexDefinitions, hasSize(2)); - assertThat((String) indexDefinitions.get(0).getIndexOptions().get("name"), equalTo("name.component")); - assertThat((String) indexDefinitions.get(1).getIndexOptions().get("name"), equalTo("nameLast.component")); + assertThat(indexDefinitions).hasSize(2); + assertThat(indexDefinitions.get(0).getIndexOptions()).containsEntry("name", "name.component"); + assertThat(indexDefinitions.get(1).getIndexOptions()).containsEntry("name", "nameLast.component"); } @Test // DATAMONGO-1087 @@ -1107,9 +1131,9 @@ public void shouldAllowMultiplePropertiesOfSameTypeWithMatchingStartLettersOnNes List indexDefinitions = prepareMappingContextAndResolveIndexForType( MultiplePropertiesOfSameTypeWithMatchingStartLettersOnNestedProperty.class); - assertThat(indexDefinitions, hasSize(2)); - assertThat((String) indexDefinitions.get(0).getIndexOptions().get("name"), equalTo("component.nameLast")); - assertThat((String) indexDefinitions.get(1).getIndexOptions().get("name"), equalTo("component.name")); + assertThat(indexDefinitions).hasSize(2); + assertThat(indexDefinitions.get(0).getIndexOptions()).containsEntry("name", "component.nameLast"); + assertThat(indexDefinitions.get(1).getIndexOptions()).containsEntry("name", "component.name"); } @Test // DATAMONGO-1121 @@ -1118,11 +1142,10 @@ public void shouldOnlyConsiderEntitiesAsPotentialCycleCandidates() { List indexDefinitions = prepareMappingContextAndResolveIndexForType( OuterDocumentReferingToIndexedPropertyViaDifferentNonCyclingPaths.class); - assertThat(indexDefinitions, hasSize(2)); - assertThat((String) indexDefinitions.get(0).getIndexOptions().get("name"), equalTo("path1.foo")); - assertThat((String) indexDefinitions.get(1).getIndexOptions().get("name"), - equalTo("path2.propertyWithIndexedStructure.foo")); - + assertThat(indexDefinitions).hasSize(2); + assertThat(indexDefinitions.get(0).getIndexOptions()).containsEntry("name", "path1.foo"); + assertThat(indexDefinitions.get(1).getIndexOptions()).containsEntry("name", + "path2.propertyWithIndexedStructure.foo"); } @Test // DATAMONGO-1263 @@ -1131,9 +1154,9 @@ public void shouldConsiderGenericTypeArgumentsOfCollectionElements() { List indexDefinitions = prepareMappingContextAndResolveIndexForType( EntityWithGenericTypeWrapperAsElement.class); - assertThat(indexDefinitions, hasSize(1)); - assertThat((String) indexDefinitions.get(0).getIndexOptions().get("name"), - equalTo("listWithGeneircTypeElement.entity.property_index")); + assertThat(indexDefinitions).hasSize(1); + assertThat(indexDefinitions.get(0).getIndexOptions()).containsEntry("name", + "listWithGeneircTypeElement.entity.property_index"); } @Document @@ -1365,9 +1388,9 @@ private static void assertIndexPathAndCollection(String[] expectedPaths, String IndexDefinitionHolder holder) { for (String expectedPath : expectedPaths) { - assertThat(holder.getIndexDefinition().getIndexKeys().containsKey(expectedPath), equalTo(true)); + assertThat(holder.getIndexDefinition().getIndexKeys()).containsKey(expectedPath); } - assertThat(holder.getCollection(), equalTo(expectedCollection)); + assertThat(holder.getCollection()).isEqualTo(expectedCollection); } } diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc index 56d2962065..a7098caa87 100644 --- a/src/main/asciidoc/new-features.adoc +++ b/src/main/asciidoc/new-features.adoc @@ -14,6 +14,7 @@ * Kotlin extension methods accepting `KClass` are deprecated now in favor of `reified` methods. * Support of array filters in `Update` operations. * <> from domain types. +* SpEL support in for expressions in `@Indexed`. [[new-features.2-1-0]] == What's New in Spring Data MongoDB 2.1