diff --git a/spring-context/src/main/java/org/springframework/format/annotation/DurationFormat.java b/spring-context/src/main/java/org/springframework/format/annotation/DurationFormat.java index 64de15c0845..e4541aed245 100644 --- a/spring-context/src/main/java/org/springframework/format/annotation/DurationFormat.java +++ b/spring-context/src/main/java/org/springframework/format/annotation/DurationFormat.java @@ -53,11 +53,19 @@ public @interface DurationFormat { */ Unit defaultUnit() default Unit.MILLIS; + /** * {@link Duration} format styles. */ enum Style { + /** + * ISO-8601 formatting. + *

This is what the JDK uses in {@link Duration#parse(CharSequence)} + * and {@link Duration#toString()}. + */ + ISO8601, + /** * Simple formatting based on a short suffix, for example '1s'. *

Supported unit suffixes include: {@code ns, us, ms, s, m, h, d}. @@ -72,13 +80,6 @@ public @interface DurationFormat { */ SIMPLE, - /** - * ISO-8601 formatting. - *

This is what the JDK uses in {@link Duration#parse(CharSequence)} - * and {@link Duration#toString()}. - */ - ISO8601, - /** * Like {@link #SIMPLE}, but allows multiple segments ordered from * largest-to-smallest units of time, like {@code 1h12m27s}. @@ -90,6 +91,7 @@ public @interface DurationFormat { COMPOSITE } + /** * {@link Duration} format unit, which mirrors a subset of {@link ChronoUnit} and * allows conversion to and from a supported {@code ChronoUnit} as well as @@ -227,7 +229,6 @@ public @interface DurationFormat { } throw new IllegalArgumentException("'" + suffix + "' is not a valid simple duration Unit"); } - } } diff --git a/spring-context/src/main/java/org/springframework/format/datetime/standard/DurationFormatterUtils.java b/spring-context/src/main/java/org/springframework/format/datetime/standard/DurationFormatterUtils.java index 369d7105a25..f63ae687e3a 100644 --- a/spring-context/src/main/java/org/springframework/format/datetime/standard/DurationFormatterUtils.java +++ b/spring-context/src/main/java/org/springframework/format/datetime/standard/DurationFormatterUtils.java @@ -28,6 +28,7 @@ import org.springframework.util.StringUtils; /** * Support {@code Duration} parsing and printing in several styles, as listed in * {@link DurationFormat.Style}. + * *

Some styles may not enforce any unit to be present, defaulting to {@code DurationFormat.Unit#MILLIS} * in that case. Methods in this class offer overloads that take a {@link DurationFormat.Unit} to * be used as a fall-back instead of the ultimate MILLIS default. @@ -39,36 +40,13 @@ import org.springframework.util.StringUtils; */ public abstract class DurationFormatterUtils { - private DurationFormatterUtils() { - // singleton - } + private static final Pattern ISO_8601_PATTERN = Pattern.compile("^[+-]?[pP].*$"); - /** - * Parse the given value to a duration. - * @param value the value to parse - * @param style the style in which to parse - * @return a duration - */ - public static Duration parse(String value, DurationFormat.Style style) { - return parse(value, style, null); - } + private static final Pattern SIMPLE_PATTERN = Pattern.compile("^([+-]?\\d+)([a-zA-Z]{0,2})$"); + + private static final Pattern COMPOSITE_PATTERN = Pattern.compile("^([+-]?)\\(?\\s?(\\d+d)?\\s?(\\d+h)?\\s?(\\d+m)?" + + "\\s?(\\d+s)?\\s?(\\d+ms)?\\s?(\\d+us)?\\s?(\\d+ns)?\\)?$"); - /** - * Parse the given value to a duration. - * @param value the value to parse - * @param style the style in which 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 static Duration parse(String value, DurationFormat.Style style, @Nullable DurationFormat.Unit unit) { - Assert.hasText(value, () -> "Value must not be empty"); - return switch (style) { - case ISO8601 -> parseIso8601(value); - case SIMPLE -> parseSimple(value, unit); - case COMPOSITE -> parseComposite(value); - }; - } /** * Print the specified duration in the specified style. @@ -97,27 +75,30 @@ public abstract class DurationFormatterUtils { } /** - * Detect the style then parse the value to return a duration. + * Parse the given value to a duration. * @param value the value to parse - * @return the parsed duration - * @throws IllegalArgumentException if the value is not a known style or cannot be - * parsed + * @param style the style in which to parse + * @return a duration */ - public static Duration detectAndParse(String value) { - return detectAndParse(value, null); + public static Duration parse(String value, DurationFormat.Style style) { + return parse(value, style, null); } /** - * Detect the style then parse the value to return a duration. + * Parse the given value to a duration. * @param value the value to parse + * @param style the style in which 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 IllegalArgumentException if the value is not a known style or cannot be - * parsed + * @return a duration */ - public static Duration detectAndParse(String value, @Nullable DurationFormat.Unit unit) { - return parse(value, detect(value), unit); + public static Duration parse(String value, DurationFormat.Style style, @Nullable DurationFormat.Unit unit) { + Assert.hasText(value, () -> "Value must not be empty"); + return switch (style) { + case ISO8601 -> parseIso8601(value); + case SIMPLE -> parseSimple(value, unit); + case COMPOSITE -> parseComposite(value); + }; } /** @@ -141,10 +122,30 @@ public abstract class DurationFormatterUtils { throw new IllegalArgumentException("'" + value + "' is not a valid duration, cannot detect any known style"); } - private static final Pattern ISO_8601_PATTERN = Pattern.compile("^[+-]?[pP].*$"); - private static final Pattern SIMPLE_PATTERN = Pattern.compile("^([+-]?\\d+)([a-zA-Z]{0,2})$"); - private static final Pattern COMPOSITE_PATTERN = Pattern.compile("^([+-]?)\\(?\\s?(\\d+d)?\\s?(\\d+h)?\\s?(\\d+m)?" + - "\\s?(\\d+s)?\\s?(\\d+ms)?\\s?(\\d+us)?\\s?(\\d+ns)?\\)?$"); + /** + * Detect the style then parse the value to return a duration. + * @param value the value to parse + * @return the parsed duration + * @throws IllegalArgumentException 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 IllegalArgumentException if the value is not a known style or cannot be + * parsed + */ + public static Duration detectAndParse(String value, @Nullable DurationFormat.Unit unit) { + return parse(value, detect(value), unit); + } + private static Duration parseIso8601(String value) { try { @@ -155,6 +156,11 @@ public abstract class DurationFormatterUtils { } } + private static String printSimple(Duration duration, @Nullable DurationFormat.Unit unit) { + unit = (unit == null ? DurationFormat.Unit.MILLIS : unit); + return unit.print(duration); + } + private static Duration parseSimple(String text, @Nullable DurationFormat.Unit fallbackUnit) { try { Matcher matcher = SIMPLE_PATTERN.matcher(text); @@ -171,34 +177,6 @@ public abstract class DurationFormatterUtils { } } - private static String printSimple(Duration duration, @Nullable DurationFormat.Unit unit) { - unit = (unit == null ? DurationFormat.Unit.MILLIS : unit); - return unit.print(duration); - } - - private static Duration parseComposite(String text) { - try { - Matcher matcher = COMPOSITE_PATTERN.matcher(text); - Assert.state(matcher.matches() && matcher.groupCount() > 1, "Does not match composite duration pattern"); - String sign = matcher.group(1); - boolean negative = sign != null && sign.equals("-"); - - Duration result = Duration.ZERO; - DurationFormat.Unit[] units = DurationFormat.Unit.values(); - for (int i = 2; i < matcher.groupCount() + 1; i++) { - String segment = matcher.group(i); - if (StringUtils.hasText(segment)) { - DurationFormat.Unit unit = units[units.length - i + 1]; - result = result.plus(unit.parse(segment.replace(unit.asSuffix(), ""))); - } - } - return negative ? result.negated() : result; - } - catch (Exception ex) { - throw new IllegalArgumentException("'" + text + "' is not a valid composite duration", ex); - } - } - private static String printComposite(Duration duration) { if (duration.isZero()) { return DurationFormat.Unit.SECONDS.print(duration); @@ -243,4 +221,27 @@ public abstract class DurationFormatterUtils { return result.toString(); } + private static Duration parseComposite(String text) { + try { + Matcher matcher = COMPOSITE_PATTERN.matcher(text); + Assert.state(matcher.matches() && matcher.groupCount() > 1, "Does not match composite duration pattern"); + String sign = matcher.group(1); + boolean negative = sign != null && sign.equals("-"); + + Duration result = Duration.ZERO; + DurationFormat.Unit[] units = DurationFormat.Unit.values(); + for (int i = 2; i < matcher.groupCount() + 1; i++) { + String segment = matcher.group(i); + if (StringUtils.hasText(segment)) { + DurationFormat.Unit unit = units[units.length - i + 1]; + result = result.plus(unit.parse(segment.replace(unit.asSuffix(), ""))); + } + } + return negative ? result.negated() : result; + } + catch (Exception ex) { + throw new IllegalArgumentException("'" + text + "' is not a valid composite duration", ex); + } + } + }