Skip to content

Commit 960b24d

Browse files
committed
DATAMONGO-2112 - Polishing.
Import DurationStyle to reuse duration parsing until Spring Framework provides a similar utility. Document ISO-8601 duration style. Prevent null index name evaluation to render "null" as String. Convert assertions from Hamcrest to AssertJ.
1 parent 1a571a1 commit 960b24d

File tree

6 files changed

+397
-167
lines changed

6 files changed

+397
-167
lines changed
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
/*
2+
* Copyright 2012-2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.mongodb.core.index;
17+
18+
import java.time.Duration;
19+
import java.time.temporal.ChronoUnit;
20+
import java.util.function.Function;
21+
import java.util.regex.Matcher;
22+
import java.util.regex.Pattern;
23+
24+
import org.springframework.lang.Nullable;
25+
import org.springframework.util.Assert;
26+
import org.springframework.util.StringUtils;
27+
28+
/**
29+
* Duration format styles.
30+
* <p/>
31+
* Fork of {@code org.springframework.boot.convert.DurationStyle}.
32+
*
33+
* @author Phillip Webb
34+
* @since 2.2
35+
*/
36+
enum DurationStyle {
37+
38+
/**
39+
* Simple formatting, for example '1s'.
40+
*/
41+
SIMPLE("^([\\+\\-]?\\d+)([a-zA-Z]{0,2})$") {
42+
43+
@Override
44+
public Duration parse(String value, @Nullable ChronoUnit unit) {
45+
try {
46+
Matcher matcher = matcher(value);
47+
Assert.state(matcher.matches(), "Does not match simple duration pattern");
48+
String suffix = matcher.group(2);
49+
return (StringUtils.hasLength(suffix) ? Unit.fromSuffix(suffix) : Unit.fromChronoUnit(unit))
50+
.parse(matcher.group(1));
51+
} catch (Exception ex) {
52+
throw new IllegalArgumentException("'" + value + "' is not a valid simple duration", ex);
53+
}
54+
}
55+
},
56+
57+
/**
58+
* ISO-8601 formatting.
59+
*/
60+
ISO8601("^[\\+\\-]?P.*$") {
61+
62+
@Override
63+
public Duration parse(String value, @Nullable ChronoUnit unit) {
64+
try {
65+
return Duration.parse(value);
66+
} catch (Exception ex) {
67+
throw new IllegalArgumentException("'" + value + "' is not a valid ISO-8601 duration", ex);
68+
}
69+
}
70+
};
71+
72+
private final Pattern pattern;
73+
74+
DurationStyle(String pattern) {
75+
this.pattern = Pattern.compile(pattern);
76+
}
77+
78+
protected final boolean matches(String value) {
79+
return this.pattern.matcher(value).matches();
80+
}
81+
82+
protected final Matcher matcher(String value) {
83+
return this.pattern.matcher(value);
84+
}
85+
86+
/**
87+
* Parse the given value to a duration.
88+
*
89+
* @param value the value to parse
90+
* @return a duration
91+
*/
92+
public Duration parse(String value) {
93+
return parse(value, null);
94+
}
95+
96+
/**
97+
* Parse the given value to a duration.
98+
*
99+
* @param value the value to parse
100+
* @param unit the duration unit to use if the value doesn't specify one ({@code null} will default to ms)
101+
* @return a duration
102+
*/
103+
public abstract Duration parse(String value, @Nullable ChronoUnit unit);
104+
105+
/**
106+
* Detect the style then parse the value to return a duration.
107+
*
108+
* @param value the value to parse
109+
* @return the parsed duration
110+
* @throws IllegalStateException if the value is not a known style or cannot be parsed
111+
*/
112+
public static Duration detectAndParse(String value) {
113+
return detectAndParse(value, null);
114+
}
115+
116+
/**
117+
* Detect the style then parse the value to return a duration.
118+
*
119+
* @param value the value to parse
120+
* @param unit the duration unit to use if the value doesn't specify one ({@code null} will default to ms)
121+
* @return the parsed duration
122+
* @throws IllegalStateException if the value is not a known style or cannot be parsed
123+
*/
124+
public static Duration detectAndParse(String value, @Nullable ChronoUnit unit) {
125+
return detect(value).parse(value, unit);
126+
}
127+
128+
/**
129+
* Detect the style from the given source value.
130+
*
131+
* @param value the source value
132+
* @return the duration style
133+
* @throws IllegalStateException if the value is not a known style
134+
*/
135+
public static DurationStyle detect(String value) {
136+
Assert.notNull(value, "Value must not be null");
137+
for (DurationStyle candidate : values()) {
138+
if (candidate.matches(value)) {
139+
return candidate;
140+
}
141+
}
142+
throw new IllegalArgumentException("'" + value + "' is not a valid duration");
143+
}
144+
145+
/**
146+
* Units that we support.
147+
*/
148+
enum Unit {
149+
150+
/**
151+
* Milliseconds.
152+
*/
153+
MILLIS(ChronoUnit.MILLIS, "ms", Duration::toMillis),
154+
155+
/**
156+
* Seconds.
157+
*/
158+
SECONDS(ChronoUnit.SECONDS, "s", Duration::getSeconds),
159+
160+
/**
161+
* Minutes.
162+
*/
163+
MINUTES(ChronoUnit.MINUTES, "m", Duration::toMinutes),
164+
165+
/**
166+
* Hours.
167+
*/
168+
HOURS(ChronoUnit.HOURS, "h", Duration::toHours),
169+
170+
/**
171+
* Days.
172+
*/
173+
DAYS(ChronoUnit.DAYS, "d", Duration::toDays);
174+
175+
private final ChronoUnit chronoUnit;
176+
177+
private final String suffix;
178+
179+
private Function<Duration, Long> longValue;
180+
181+
Unit(ChronoUnit chronoUnit, String suffix, Function<Duration, Long> toUnit) {
182+
this.chronoUnit = chronoUnit;
183+
this.suffix = suffix;
184+
this.longValue = toUnit;
185+
}
186+
187+
public Duration parse(String value) {
188+
return Duration.of(Long.valueOf(value), this.chronoUnit);
189+
}
190+
191+
public long longValue(Duration value) {
192+
return this.longValue.apply(value);
193+
}
194+
195+
public static Unit fromChronoUnit(ChronoUnit chronoUnit) {
196+
if (chronoUnit == null) {
197+
return Unit.MILLIS;
198+
}
199+
for (Unit candidate : values()) {
200+
if (candidate.chronoUnit == chronoUnit) {
201+
return candidate;
202+
}
203+
}
204+
throw new IllegalArgumentException("Unknown unit " + chronoUnit);
205+
}
206+
207+
public static Unit fromSuffix(String suffix) {
208+
for (Unit candidate : values()) {
209+
if (candidate.suffix.equalsIgnoreCase(suffix)) {
210+
return candidate;
211+
}
212+
}
213+
throw new IllegalArgumentException("Unknown unit '" + suffix + "'");
214+
}
215+
}
216+
}

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/Indexed.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
* @author Thomas Darimont
3131
* @author Christoph Strobl
3232
* @author Jordi Llach
33+
* @author Mark Paluch
3334
*/
3435
@Target({ ElementType.ANNOTATION_TYPE, ElementType.FIELD })
3536
@Retention(RetentionPolicy.RUNTIME)
@@ -147,13 +148,16 @@
147148
* {@link java.time.Duration} or a valid expiration {@link String} according to the already mentioned
148149
* conventions.</li>
149150
* </ul>
151+
* Supports ISO-8601 style.
150152
*
151153
* <pre class="code">
152154
*
153155
* &#0064;Indexed(expireAfter = "10s") String expireAfterTenSeconds;
154156
*
155157
* &#0064;Indexed(expireAfter = "1d") String expireAfterOneDay;
156158
*
159+
* &#0064;Indexed(expireAfter = "P2D") String expireAfterTwoDays;
160+
*
157161
* &#0064;Indexed(expireAfter = "#{&#0064;mySpringBean.timeout}") String expireAfterTimeoutObtainedFromSpringBean;
158162
* </pre>
159163
*

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java

Lines changed: 13 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,6 @@
2929
import java.util.List;
3030
import java.util.Set;
3131
import java.util.concurrent.TimeUnit;
32-
import java.util.regex.Matcher;
33-
import java.util.regex.Pattern;
3432
import java.util.stream.Collectors;
3533

3634
import org.slf4j.Logger;
@@ -63,7 +61,6 @@
6361
import org.springframework.lang.Nullable;
6462
import org.springframework.util.Assert;
6563
import org.springframework.util.ClassUtils;
66-
import org.springframework.util.NumberUtils;
6764
import org.springframework.util.ObjectUtils;
6865
import org.springframework.util.StringUtils;
6966

@@ -82,7 +79,6 @@
8279
public class MongoPersistentEntityIndexResolver implements IndexResolver {
8380

8481
private static final Logger LOGGER = LoggerFactory.getLogger(MongoPersistentEntityIndexResolver.class);
85-
private static final Pattern TIMEOUT_PATTERN = Pattern.compile("(\\d+)(\\W+)?([dhms])");
8682
private static final SpelExpressionParser PARSER = new SpelExpressionParser();
8783

8884
private final MongoMappingContext mappingContext;
@@ -354,7 +350,6 @@ protected List<IndexDefinitionHolder> createCompoundIndexDefinitions(String dotP
354350
return indexDefinitions;
355351
}
356352

357-
@SuppressWarnings("deprecation")
358353
protected IndexDefinitionHolder createCompoundIndexDefinition(String dotPath, String collection, CompoundIndex index,
359354
MongoPersistentEntity<?> entity) {
360355

@@ -391,8 +386,7 @@ private org.bson.Document resolveCompoundIndexKeyFromStringDefinition(String dot
391386
return new org.bson.Document(dotPath, 1);
392387
}
393388

394-
Object keyDefToUse = evaluatePotentialTemplateExpression(keyDefinitionString,
395-
getEvaluationContextForProperty(entity));
389+
Object keyDefToUse = evaluate(keyDefinitionString, getEvaluationContextForProperty(entity));
396390

397391
org.bson.Document dbo = (keyDefToUse instanceof org.bson.Document) ? (org.bson.Document) keyDefToUse
398392
: org.bson.Document.parse(ObjectUtils.nullSafeToString(keyDefToUse));
@@ -547,10 +541,15 @@ protected IndexDefinitionHolder createGeoSpatialIndexDefinition(String dotPath,
547541
private String pathAwareIndexName(String indexName, String dotPath, @Nullable PersistentEntity<?, ?> entity,
548542
@Nullable MongoPersistentProperty property) {
549543

550-
String nameToUse = StringUtils.hasText(indexName)
551-
? ObjectUtils
552-
.nullSafeToString(evaluatePotentialTemplateExpression(indexName, getEvaluationContextForProperty(entity)))
553-
: "";
544+
String nameToUse = "";
545+
if (StringUtils.hasText(indexName)) {
546+
547+
Object result = evaluate(indexName, getEvaluationContextForProperty(entity));
548+
549+
if (result != null) {
550+
nameToUse = ObjectUtils.nullSafeToString(result);
551+
}
552+
}
554553

555554
if (!StringUtils.hasText(dotPath) || (property != null && dotPath.equals(property.getFieldName()))) {
556555
return StringUtils.hasText(nameToUse) ? nameToUse : dotPath;
@@ -608,7 +607,7 @@ private void resolveAndAddIndexesForAssociation(Association<MongoPersistentPrope
608607
*/
609608
private static Duration computeIndexTimeout(String timeoutValue, EvaluationContext evaluationContext) {
610609

611-
Object evaluatedTimeout = evaluatePotentialTemplateExpression(timeoutValue, evaluationContext);
610+
Object evaluatedTimeout = evaluate(timeoutValue, evaluationContext);
612611

613612
if (evaluatedTimeout == null) {
614613
return Duration.ZERO;
@@ -624,30 +623,11 @@ private static Duration computeIndexTimeout(String timeoutValue, EvaluationConte
624623
return Duration.ZERO;
625624
}
626625

627-
Matcher matcher = TIMEOUT_PATTERN.matcher(val);
628-
if (matcher.find()) {
629-
630-
long timeout = NumberUtils.parseNumber(matcher.group(1), Long.class);
631-
String unit = matcher.group(3);
632-
633-
switch (unit) {
634-
case "d":
635-
return Duration.ofDays(timeout);
636-
case "h":
637-
return Duration.ofHours(timeout);
638-
case "m":
639-
return Duration.ofMinutes(timeout);
640-
case "s":
641-
return Duration.ofSeconds(timeout);
642-
}
643-
}
644-
645-
throw new IllegalArgumentException(
646-
String.format("Index timeout %s cannot be parsed. Please use the pattern '\\d+\\W?[dhms]'.", val));
626+
return DurationStyle.detectAndParse(val);
647627
}
648628

649629
@Nullable
650-
private static Object evaluatePotentialTemplateExpression(String value, EvaluationContext evaluationContext) {
630+
private static Object evaluate(String value, EvaluationContext evaluationContext) {
651631

652632
Expression expression = PARSER.parseExpression(value, ParserContext.TEMPLATE_EXPRESSION);
653633
if (expression instanceof LiteralExpression) {

0 commit comments

Comments
 (0)