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 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/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/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/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..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) @@ -65,7 +66,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 +85,7 @@ * @Document * class Hybrid { * @Indexed(name="index") String h1; + * @Indexed(name="#{@myBean.indexName}") String h2; * } * * class Nested { @@ -98,6 +101,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 } ) * * * @@ -131,4 +135,34 @@ * "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 document should expire. + * Defaults to an empty {@link String} for no expiry. Accepts numeric values followed by their unit of measure: + *

+ * Supports ISO-8601 style. + * + *
+	 *
+	 * @Indexed(expireAfter = "10s") String expireAfterTenSeconds;
+	 *
+	 * @Indexed(expireAfter = "1d") String expireAfterOneDay;
+	 *
+	 * @Indexed(expireAfter = "P2D") String expireAfterTwoDays;
+	 *
+	 * @Indexed(expireAfter = "#{@mySpringBean.timeout}") String expireAfterTimeoutObtainedFromSpringBean;
+	 * 
+ * + * @return empty by default. + * @since 2.2 + */ + 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 af56d06b0d..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 @@ -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; @@ -32,25 +33,35 @@ 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; 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; 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.ObjectUtils; import org.springframework.util.StringUtils; /** @@ -68,8 +79,10 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver { private static final Logger LOGGER = LoggerFactory.getLogger(MongoPersistentEntityIndexResolver.class); + private static final SpelExpressionParser PARSER = new SpelExpressionParser(); private final MongoMappingContext mappingContext; + private EvaluationContextProvider evaluationContextProvider = EvaluationContextProvider.DEFAULT; /** * Create new {@link MongoPersistentEntityIndexResolver}. @@ -337,15 +350,14 @@ protected List createCompoundIndexDefinitions(String dotP return indexDefinitions; } - @SuppressWarnings("deprecation") protected IndexDefinitionHolder createCompoundIndexDefinition(String dotPath, String collection, CompoundIndex index, 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()) { @@ -363,7 +375,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."); @@ -373,7 +386,11 @@ 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 = evaluate(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; } @@ -409,7 +426,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()) { @@ -428,9 +445,66 @@ protected IndexDefinitionHolder createIndexDefinition(String dotPath, String col indexDefinition.expire(index.expireAfterSeconds(), TimeUnit.SECONDS); } + if (StringUtils.hasText(index.expireAfter())) { + + if (index.expireAfterSeconds() >= 0) { + throw new IllegalStateException(String.format( + "@Indexed already defines an expiration timeout of %s seconds via Indexed#expireAfterSeconds. Please make to use either expireAfterSeconds or expireAfter.", + index.expireAfterSeconds())); + } + + Duration timeout = computeIndexTimeout(index.expireAfter(), + getEvaluationContextForProperty(persitentProperty.getOwner())); + 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); + } + + /** + * 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}. + * + * @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}. @@ -455,7 +529,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()); @@ -463,9 +538,18 @@ 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 = ""; + if (StringUtils.hasText(indexName)) { - String nameToUse = StringUtils.hasText(indexName) ? 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; @@ -511,6 +595,48 @@ 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(); + }); + + assertThat(indexInfo).isPresent(); + assertThat(indexInfo.get()).containsEntry("expireAfterSeconds", 11L); } @Target({ ElementType.FIELD }) @@ -110,6 +156,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..7bac84fe01 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 @@ -15,10 +15,8 @@ */ package org.springframework.data.mongodb.core.index; -import static org.hamcrest.Matchers.*; -import static org.junit.Assert.*; import static org.mockito.Mockito.*; -import static org.springframework.data.mongodb.test.util.IsBsonObject.*; +import static org.springframework.data.mongodb.test.util.Assertions.*; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -31,6 +29,7 @@ import org.junit.runner.RunWith; import org.junit.runners.Suite; import org.junit.runners.Suite.SuiteClasses; + import org.springframework.core.annotation.AliasFor; import org.springframework.data.annotation.Id; import org.springframework.data.geo.Point; @@ -52,12 +51,15 @@ import org.springframework.data.util.ClassTypeInformation; /** + * Tests for {@link MongoPersistentEntityIndexResolver}. + * * @author Christoph Strobl * @author Mark Paluch */ @RunWith(Suite.class) @SuiteClasses({ IndexResolutionTests.class, GeoSpatialIndexResolutionTests.class, CompoundIndexResolutionTests.class, TextIndexedResolutionTests.class, MixedIndexResolutionTests.class }) +@SuppressWarnings("unused") public class MongoPersistentEntityIndexResolverUnitTests { /** @@ -74,7 +76,7 @@ public void indexPathOnLevelZeroIsResolvedCorrectly() { List indexDefinitions = prepareMappingContextAndResolveIndexForType( IndexOnLevelZero.class); - assertThat(indexDefinitions, hasSize(1)); + assertThat(indexDefinitions).hasSize(1); assertIndexPathAndCollection("indexedProperty", "Zero", indexDefinitions.get(0)); } @@ -83,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)); } @@ -94,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 @@ -102,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)); } @@ -112,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)); } @@ -124,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 @@ -134,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 @@ -143,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 @@ -154,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 @@ -166,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 @@ -177,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 @@ -192,13 +195,80 @@ 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 + public void shouldResolveTimeoutFromString() { + + List indexDefinitions = prepareMappingContextAndResolveIndexForType( + WithExpireAfterAsPlainString.class); + + 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 + public void shouldResolveTimeoutFromExpression() { + + List indexDefinitions = prepareMappingContextAndResolveIndexForType( + WithExpireAfterAsExpression.class); + + assertThat(indexDefinitions.get(0).getIndexOptions()).containsEntry("expireAfterSeconds", 11L); + } + + @Test // DATAMONGO-2112 + public void shouldResolveTimeoutFromExpressionReturningDuration() { + + List indexDefinitions = prepareMappingContextAndResolveIndexForType( + WithExpireAfterAsExpressionResultingInDuration.class); + + assertThat(indexDefinitions.get(0).getIndexOptions()).containsEntry("expireAfterSeconds", 100L); + } + + @Test // DATAMONGO-2112 + public void shouldErrorOnInvalidTimeoutExpression() { + + MongoMappingContext mappingContext = prepareMappingContext(WithInvalidExpireAfter.class); + MongoPersistentEntityIndexResolver indexResolver = new MongoPersistentEntityIndexResolver(mappingContext); + + 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); + + assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> indexResolver + .resolveIndexForEntity(mappingContext.getRequiredPersistentEntity(WithDuplicateExpiry.class))); + } + + @Test // DATAMONGO-2112 + public void resolveExpressionIndexName() { + + List indexDefinitions = prepareMappingContextAndResolveIndexForType( + WithIndexNameAsExpression.class); + + assertThat(indexDefinitions.get(0).getIndexOptions()).containsEntry("name", "my1st"); } @Document("Zero") @@ -264,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; @@ -285,18 +355,52 @@ 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"; } + + @Document + 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; + } + + @Document + static class WithExpireAfterAsExpressionResultingInDuration { + @Indexed(expireAfter = "#{T(java.time.Duration).ofSeconds(100)}") String withTimeout; + } + + @Document + class WithInvalidExpireAfter { + @Indexed(expireAfter = "123ops") String withTimeout; + } + + @Document + class WithDuplicateExpiry { + @Indexed(expireAfter = "1s", expireAfterSeconds = 2) String withTimeout; + } + + @Document + static class WithIndexNameAsExpression { + @Indexed(name = "#{'my' + 1 + 'st'}") String spelIndexName; + } } @Target({ ElementType.FIELD }) @Retention(RetentionPolicy.RUNTIME) @Indexed @interface IndexedFieldAnnotation { - } @Document @@ -317,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)); } @@ -327,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)); } @@ -337,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)); } @@ -349,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 @@ -361,10 +465,19 @@ 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 + public void resolveExpressionIndexNameForGeoIndex() { + + List indexDefinitions = prepareMappingContextAndResolveIndexForType( + GeoIndexWithNameAsExpression.class); + + assertThat(indexDefinitions.get(0).getIndexOptions()).containsEntry("name", "my1st"); } @Document("Zero") @@ -414,6 +527,11 @@ static class GeoSpatialIndexedDocumentWithComposedAnnotation { GeoSpatialIndexType indexType() default GeoSpatialIndexType.GEO_HAYSTACK; } + @Document + static class GeoIndexWithNameAsExpression { + @GeoSpatialIndexed(name = "#{'my' + 1 + 'st'}") Point spelIndexName; + } + } /** @@ -429,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)); } @@ -440,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 @@ -452,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 @@ -464,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 @@ -475,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)); } @@ -486,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)); } @@ -497,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)); } @@ -507,10 +625,30 @@ 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 + public void resolveExpressionIndexNameForCompoundIndex() { + + List indexDefinitions = prepareMappingContextAndResolveIndexForType( + CompoundIndexWithNameExpression.class); + + 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") @@ -538,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 }) @@ -577,6 +709,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 { @@ -586,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)); } @@ -595,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)); } @@ -605,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)); } @@ -614,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 @@ -627,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 @@ -641,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 @@ -649,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 @@ -657,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 @@ -665,7 +807,8 @@ public void shouldNotCreateIndexDefinitionWhenOnlyLanguageButNoTextIndexPresent( List indexDefinitions = prepareMappingContextAndResolveIndexForType( DocumentWithNoTextIndexPropertyButReservedFieldLanguage.class); - assertThat(indexDefinitions, is(empty())); + + assertThat(indexDefinitions).isEmpty(); } @Test // DATAMONGO-1049 @@ -673,7 +816,8 @@ public void shouldNotCreateIndexDefinitionWhenOnlyAnnotatedLanguageButNoTextInde List indexDefinitions = prepareMappingContextAndResolveIndexForType( DocumentWithNoTextIndexPropertyButReservedFieldLanguageAnnotated.class); - assertThat(indexDefinitions, is(empty())); + + assertThat(indexDefinitions).isEmpty(); } @Test // DATAMONGO-1049 @@ -681,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 @@ -691,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 @@ -789,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; @@ -803,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 @@ -829,7 +976,8 @@ public void shouldNotRunIntoStackOverflow() { List indexDefinitions = prepareMappingContextAndResolveIndexForType( CycleStartingInBetween.class); - assertThat(indexDefinitions, hasSize(1)); + + assertThat(indexDefinitions).hasSize(1); } @Test // DATAMONGO-926 @@ -838,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 @@ -846,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 @@ -854,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 @@ -867,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 @@ -875,7 +1024,8 @@ public void shouldDetectSelfCycleViaCollectionTypeCorrectly() { List indexDefinitions = prepareMappingContextAndResolveIndexForType( SelfCyclingViaCollectionType.class); - assertThat(indexDefinitions, empty()); + + assertThat(indexDefinitions).isEmpty(); } @Test // DATAMONGO-962 @@ -883,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); @@ -898,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)) @@ -911,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 @@ -921,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 @@ -930,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 @@ -939,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 @@ -948,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 @@ -957,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 @@ -966,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 @@ -977,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 @@ -988,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 @@ -1001,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 @@ -1235,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