From 1c1612e481f8a440d4e37fb8229f720e8a48faed Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 19 Feb 2019 09:39:31 +0100 Subject: [PATCH 1/5] DATAMONGO-2112 - Prepare issue branch. --- pom.xml | 2 +- spring-data-mongodb-benchmarks/pom.xml | 2 +- spring-data-mongodb-cross-store/pom.xml | 4 ++-- spring-data-mongodb-distribution/pom.xml | 2 +- spring-data-mongodb/pom.xml | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index a18c83b4d6..6a1c2bf14c 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 c2ff37b35c..9dae183cf6 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-cross-store/pom.xml b/spring-data-mongodb-cross-store/pom.xml index fd36f227c0..2889c27116 100644 --- a/spring-data-mongodb-cross-store/pom.xml +++ b/spring-data-mongodb-cross-store/pom.xml @@ -6,7 +6,7 @@ org.springframework.data spring-data-mongodb-parent - 2.2.0.BUILD-SNAPSHOT + 2.2.0.DATAMONGO-2112-SNAPSHOT ../pom.xml @@ -50,7 +50,7 @@ org.springframework.data spring-data-mongodb - 2.2.0.BUILD-SNAPSHOT + 2.2.0.DATAMONGO-2112-SNAPSHOT diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index fc8d28a2b6..26c88fd8db 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 a30af66d35..22181228ce 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 50b4ea49d96de0f8fa8368c6ff48f7149adff192 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 20 Feb 2019 10:23:53 +0100 Subject: [PATCH 2/5] 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 6626e96041..0cf5d1e07c 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 d0b2535346..bae77209b8 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 4fa61ec207..a12e780661 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 e5be79d43f..e2c29daec9 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 fabb76da3a6de1ba474acee781b7cc798c3d26d3 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 20 Feb 2019 12:10:13 +0100 Subject: [PATCH 3/5] 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 2cac65a39b..9bc0e51aa5 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 4bc4a38fb0..ade52d7dcd 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 bae77209b8..a7db146b04 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 a12e780661..fd55bebb08 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 db07b7156e032d5e33f7a27b80303727a9d395d9 Mon Sep 17 00:00:00 2001
From: Mark Paluch 
Date: Tue, 26 Feb 2019 11:22:12 +0100
Subject: [PATCH 4/5] DATAMONGO-2112 - Polishing.

Align Indexed(expireAfter) default to empty string according to the documentation. Slightly reword Javadoc.
---
 .../data/mongodb/core/index/Indexed.java      | 32 ++++++++++---------
 .../MongoPersistentEntityIndexResolver.java   | 11 ++++---
 ...ersistentEntityIndexResolverUnitTests.java | 15 +++------
 3 files changed, 27 insertions(+), 31 deletions(-)

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 a7db146b04..9e09114c61 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
@@ -136,27 +136,29 @@
 	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.
  • + *
* - *
-	 *     
+	 * 
 	 *
-	 * @Indexed(expireAfter = "10s")
-	 * String expireAfterTenSeconds;
+	 * @Indexed(expireAfter = "10s") String expireAfterTenSeconds;
 	 *
-	 * @Indexed(expireAfter = "1d")
-	 * String expireAfterOneDay;
+	 * @Indexed(expireAfter = "1d") String expireAfterOneDay;
 	 *
-	 * @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 fd55bebb08..00f6c0f969 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 @@ -35,6 +35,7 @@ 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; @@ -450,11 +451,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 +481,7 @@ protected EvaluationContext getEvaluationContext() { /** * Get the {@link EvaluationContext} for a given {@link PersistentEntity entity} the default one. - * + * * @param persistentEntity can be {@literal null} * @return */ @@ -626,7 +627,7 @@ private static Duration computeIndexTimeout(String timeoutValue, EvaluationConte Matcher matcher = TIMEOUT_PATTERN.matcher(val); if (matcher.find()) { - Long timeout = NumberUtils.parseNumber(matcher.group(1), Long.class); + long timeout = NumberUtils.parseNumber(matcher.group(1), Long.class); String unit = matcher.group(3); switch (unit) { @@ -642,7 +643,7 @@ private static Duration computeIndexTimeout(String timeoutValue, EvaluationConte } throw new IllegalArgumentException( - String.format("Index timeout %s cannot be parsed. Please use the following pattern '\\d+\\W?[dhms]'.", val)); + String.format("Index timeout %s cannot be parsed. Please use the pattern '\\d+\\W?[dhms]'.", val)); } @Nullable 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 20f9465736..51909af9f9 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 @@ -32,6 +32,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; @@ -237,7 +238,6 @@ public void shouldErrorOnInvalidTimeoutExpression() { Assertions.assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> indexResolver .resolveIndexForEntity(mappingContext.getRequiredPersistentEntity(WithInvalidExpireAfter.class))); - } @Test // DATAMONGO-2112 @@ -384,7 +384,6 @@ static class WithIndexNameAsExpression { @Retention(RetentionPolicy.RUNTIME) @Indexed @interface IndexedFieldAnnotation { - } @Document @@ -660,22 +659,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 }) From 454c4e0c90af2d3f69ec13de50d767c9f0cb08ac Mon Sep 17 00:00:00 2001 From: Spring Operator Date: Sat, 16 Mar 2019 11:26:59 -0500 Subject: [PATCH 5/5] URL Cleanup This commit updates URLs to prefer the https protocol. Redirects are not followed to avoid accidentally expanding intentionally shortened URLs (i.e. if using a URL shortener). # Fixed URLs ## Fixed Success These URLs were switched to an https URL with a 2xx status. While the status was successful, your review is still recommended. * http://maven.apache.org/xsd/maven-4.0.0.xsd with 3 occurrences migrated to: https://maven.apache.org/xsd/maven-4.0.0.xsd ([https](https://maven.apache.org/xsd/maven-4.0.0.xsd) result 200). * http://www.gopivotal.com (302) with 6 occurrences migrated to: https://pivotal.io ([https](https://www.gopivotal.com) result 200). * http://maven.apache.org/maven-v4_0_0.xsd with 2 occurrences migrated to: https://maven.apache.org/maven-v4_0_0.xsd ([https](https://maven.apache.org/maven-v4_0_0.xsd) result 301). * http://projects.spring.io/spring-data-mongodb with 1 occurrences migrated to: https://projects.spring.io/spring-data-mongodb ([https](https://projects.spring.io/spring-data-mongodb) result 301). * http://www.pivotal.io with 1 occurrences migrated to: https://www.pivotal.io ([https](https://www.pivotal.io) result 301). # Ignored These URLs were intentionally ignored. * http://maven.apache.org/POM/4.0.0 with 10 occurrences * http://www.w3.org/2001/XMLSchema-instance with 5 occurrences --- pom.xml | 18 +++++++++--------- spring-data-mongodb-benchmarks/pom.xml | 2 +- spring-data-mongodb-cross-store/pom.xml | 2 +- spring-data-mongodb-distribution/pom.xml | 2 +- spring-data-mongodb/pom.xml | 2 +- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/pom.xml b/pom.xml index 6a1c2bf14c..252bf71d98 100644 --- a/pom.xml +++ b/pom.xml @@ -1,5 +1,5 @@ - + 4.0.0 @@ -10,7 +10,7 @@ Spring Data MongoDB MongoDB support for Spring Data - http://projects.spring.io/spring-data-mongodb + https://projects.spring.io/spring-data-mongodb org.springframework.data.build @@ -39,7 +39,7 @@ Oliver Gierke ogierke at gopivotal.com Pivotal - http://www.gopivotal.com + https://pivotal.io Project Lead @@ -50,7 +50,7 @@ Thomas Risberg trisberg at vmware.com Pivotal - http://www.gopivotal.com + https://pivotal.io Developer @@ -61,7 +61,7 @@ Mark Pollack mpollack at gopivotal.com Pivotal - http://www.gopivotal.com + https://pivotal.io Developer @@ -72,7 +72,7 @@ Jon Brisbin jbrisbin at gopivotal.com Pivotal - http://www.gopivotal.com + https://pivotal.io Developer @@ -83,7 +83,7 @@ Thomas Darimont tdarimont at gopivotal.com Pivotal - http://www.gopivotal.com + https://pivotal.io Developer @@ -94,7 +94,7 @@ Christoph Strobl cstrobl at gopivotal.com Pivotal - http://www.gopivotal.com + https://pivotal.io Developer @@ -105,7 +105,7 @@ Mark Paluch mpaluch at pivotal.io Pivotal - http://www.pivotal.io + https://www.pivotal.io Developer diff --git a/spring-data-mongodb-benchmarks/pom.xml b/spring-data-mongodb-benchmarks/pom.xml index 9dae183cf6..7d53c3ad8c 100644 --- a/spring-data-mongodb-benchmarks/pom.xml +++ b/spring-data-mongodb-benchmarks/pom.xml @@ -1,6 +1,6 @@ + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/maven-v4_0_0.xsd"> 4.0.0 diff --git a/spring-data-mongodb-cross-store/pom.xml b/spring-data-mongodb-cross-store/pom.xml index 2889c27116..7c0ec25109 100644 --- a/spring-data-mongodb-cross-store/pom.xml +++ b/spring-data-mongodb-cross-store/pom.xml @@ -1,5 +1,5 @@ - + 4.0.0 diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index 26c88fd8db..46337578f2 100644 --- a/spring-data-mongodb-distribution/pom.xml +++ b/spring-data-mongodb-distribution/pom.xml @@ -1,6 +1,6 @@ + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index 22181228ce..5cf83e468e 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -1,5 +1,5 @@ - + 4.0.0