-
Notifications
You must be signed in to change notification settings - Fork 38.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Support multiple style of parsing/printing Durations
This commit introduces a notion of different styles for the formatting of Duration. The `@DurationFormat` annotation is added to ease selection of a style, which are represented as DurationFormat.Style enum, as well as a supported time unit represented as DurationFormat.Unit enum. DurationFormatter has been retroffited to take such a Style, optionally, at construction. The default is still the JDK style a.k.a. ISO-8601. This introduces the new SIMPLE style which uses a single number + a short human-readable suffix. For instance "-3ms" or "2h". This has the same semantics as the DurationStyle in Spring Boot and is intended as a replacement for that feature, providing access to the feature to projects that only depend on Spring Framework. Finally, the `@Scheduled` annotation is improved by adding detection of the style and parsing for the String versions of initial delay, fixed delay and fixed rate. See gh-22013 See gh-22474 Closes gh-30396
- Loading branch information
1 parent
d219362
commit c92e043
Showing
13 changed files
with
875 additions
and
42 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
213 changes: 213 additions & 0 deletions
213
spring-context/src/main/java/org/springframework/format/annotation/DurationFormat.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,213 @@ | ||
/* | ||
* Copyright 2002-2024 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.format.annotation; | ||
|
||
import java.lang.annotation.Documented; | ||
import java.lang.annotation.ElementType; | ||
import java.lang.annotation.Retention; | ||
import java.lang.annotation.RetentionPolicy; | ||
import java.lang.annotation.Target; | ||
import java.time.Duration; | ||
import java.time.temporal.ChronoUnit; | ||
import java.util.function.Function; | ||
|
||
import org.springframework.lang.Nullable; | ||
|
||
/** | ||
* Declares that a field or method parameter should be formatted as a {@link java.time.Duration}, | ||
* according to the specified {@code style}. | ||
* | ||
* @author Simon Baslé | ||
* @since 6.2 | ||
*/ | ||
@Documented | ||
@Retention(RetentionPolicy.RUNTIME) | ||
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE}) | ||
public @interface DurationFormat { | ||
|
||
/** | ||
* Which {@code Style} to use for parsing and printing a {@code Duration}. Defaults to | ||
* the JDK style ({@link Style#ISO8601}). | ||
*/ | ||
Style style() default Style.ISO8601; | ||
|
||
/** | ||
* Define which {@link Unit} to fall back to in case the {@code style()} | ||
* needs a unit for either parsing or printing, and none is explicitly provided in | ||
* the input ({@code Unit.MILLIS} if unspecified). | ||
*/ | ||
Unit defaultUnit() default Unit.MILLIS; | ||
|
||
/** | ||
* Duration format styles. | ||
*/ | ||
enum Style { | ||
|
||
/** | ||
* Simple formatting based on a short suffix, for example '1s'. | ||
* Supported unit suffixes are: {@code ns, us, ms, s, m, h, d}. | ||
* This corresponds to nanoseconds, microseconds, milliseconds, seconds, | ||
* minutes, hours and days respectively. | ||
* <p>Note that when printing a {@code Duration}, this style can be lossy if the | ||
* selected unit is bigger than the resolution of the duration. For example, | ||
* {@code Duration.ofMillis(5).plusNanos(1234)} would get truncated to {@code "5ms"} | ||
* when printing using {@code ChronoUnit.MILLIS}. | ||
*/ | ||
SIMPLE, | ||
|
||
/** | ||
* ISO-8601 formatting. | ||
* <p>This is what the JDK uses in {@link java.time.Duration#parse(CharSequence)} | ||
* and {@link Duration#toString()}. | ||
*/ | ||
ISO8601 | ||
} | ||
|
||
/** | ||
* Duration format unit, which mirrors a subset of {@link ChronoUnit} and allows conversion to and from | ||
* supported {@code ChronoUnit} as well as converting durations to longs. | ||
* The enum includes its corresponding suffix in the {@link Style#SIMPLE simple} Duration format style. | ||
*/ | ||
enum Unit { | ||
/** | ||
* Nanoseconds ({@code "ns"}). | ||
*/ | ||
NANOS(ChronoUnit.NANOS, "ns", Duration::toNanos), | ||
|
||
/** | ||
* Microseconds ({@code "us"}). | ||
*/ | ||
MICROS(ChronoUnit.MICROS, "us", duration -> duration.toNanos() / 1000L), | ||
|
||
/** | ||
* Milliseconds ({@code "ms"}). | ||
*/ | ||
MILLIS(ChronoUnit.MILLIS, "ms", Duration::toMillis), | ||
|
||
/** | ||
* Seconds ({@code "s"}). | ||
*/ | ||
SECONDS(ChronoUnit.SECONDS, "s", Duration::getSeconds), | ||
|
||
/** | ||
* Minutes ({@code "m"}). | ||
*/ | ||
MINUTES(ChronoUnit.MINUTES, "m", Duration::toMinutes), | ||
|
||
/** | ||
* Hours ({@code "h"}). | ||
*/ | ||
HOURS(ChronoUnit.HOURS, "h", Duration::toHours), | ||
|
||
/** | ||
* Days ({@code "d"}). | ||
*/ | ||
DAYS(ChronoUnit.DAYS, "d", Duration::toDays); | ||
|
||
private final ChronoUnit chronoUnit; | ||
|
||
private final String suffix; | ||
|
||
private final Function<Duration, Long> longValue; | ||
|
||
Unit(ChronoUnit chronoUnit, String suffix, Function<Duration, Long> toUnit) { | ||
this.chronoUnit = chronoUnit; | ||
this.suffix = suffix; | ||
this.longValue = toUnit; | ||
} | ||
|
||
/** | ||
* Convert this {@code DurationFormat.Unit} to its {@link ChronoUnit} equivalent. | ||
*/ | ||
public ChronoUnit asChronoUnit() { | ||
return this.chronoUnit; | ||
} | ||
|
||
/** | ||
* Convert this {@code DurationFormat.Unit} to a simple {@code String} suffix, | ||
* suitable for the {@link Style#SIMPLE} style. | ||
*/ | ||
public String asSuffix() { | ||
return this.suffix; | ||
} | ||
|
||
/** | ||
* Parse a {@code long} from a {@code String} and interpret it to be a {@code Duration} | ||
* in the current unit. | ||
* @param value the String representation of the long | ||
* @return the corresponding {@code Duration} | ||
*/ | ||
public Duration parse(String value) { | ||
return Duration.of(Long.parseLong(value), asChronoUnit()); | ||
} | ||
|
||
/** | ||
* Print a {@code Duration} as a {@code String}, converting it to a long value | ||
* using this unit's precision via {@link #longValue(Duration)} and appending | ||
* this unit's simple {@link #asSuffix() suffix}. | ||
* @param value the {@code Duration} to convert to String | ||
* @return the String representation of the {@code Duration} in the {@link Style#SIMPLE SIMPLE style} | ||
*/ | ||
public String print(Duration value) { | ||
return longValue(value) + asSuffix(); | ||
} | ||
|
||
/** | ||
* Convert the given {@code Duration} to a long value in the resolution of this | ||
* unit. Note that this can be lossy if the current unit is bigger than the | ||
* actual resolution of the duration. | ||
* <p>For example, {@code Duration.ofMillis(5).plusNanos(1234)} would get truncated | ||
* to {@code 5} for unit {@code MILLIS}. | ||
* @param value the {@code Duration} to convert to long | ||
* @return the long value for the Duration in this Unit | ||
*/ | ||
public long longValue(Duration value) { | ||
return this.longValue.apply(value); | ||
} | ||
|
||
/** | ||
* Get the {@code Unit} corresponding to the given {@code ChronoUnit}. | ||
* @throws IllegalArgumentException if that particular ChronoUnit isn't supported | ||
*/ | ||
public static Unit fromChronoUnit(@Nullable ChronoUnit chronoUnit) { | ||
if (chronoUnit == null) { | ||
return Unit.MILLIS; | ||
} | ||
for (Unit candidate : values()) { | ||
if (candidate.chronoUnit == chronoUnit) { | ||
return candidate; | ||
} | ||
} | ||
throw new IllegalArgumentException("No matching Unit for ChronoUnit." + chronoUnit.name()); | ||
} | ||
|
||
/** | ||
* Get the {@code Unit} corresponding to the given {@code String} suffix. | ||
* @throws IllegalArgumentException if that particular suffix is unknown | ||
*/ | ||
public static Unit fromSuffix(String suffix) { | ||
for (Unit candidate : values()) { | ||
if (candidate.suffix.equalsIgnoreCase(suffix)) { | ||
return candidate; | ||
} | ||
} | ||
throw new IllegalArgumentException("'" + suffix + "' is not a valid simple duration Unit"); | ||
} | ||
|
||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
57 changes: 57 additions & 0 deletions
57
...rg/springframework/format/datetime/standard/DurationFormatAnnotationFormatterFactory.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
/* | ||
* Copyright 2002-2024 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.format.datetime.standard; | ||
|
||
import java.time.Duration; | ||
import java.util.Set; | ||
|
||
import org.springframework.context.support.EmbeddedValueResolutionSupport; | ||
import org.springframework.format.AnnotationFormatterFactory; | ||
import org.springframework.format.Parser; | ||
import org.springframework.format.Printer; | ||
import org.springframework.format.annotation.DurationFormat; | ||
|
||
/** | ||
* Formats fields annotated with the {@link DurationFormat} annotation using the | ||
* selected style for parsing and printing JSR-310 {@code Duration}. | ||
* | ||
* @author Simon Baslé | ||
* @since 6.2 | ||
* @see DurationFormat | ||
* @see DurationFormatter | ||
*/ | ||
public class DurationFormatAnnotationFormatterFactory extends EmbeddedValueResolutionSupport | ||
implements AnnotationFormatterFactory<DurationFormat> { | ||
|
||
// Create the set of field types that may be annotated with @DurationFormat. | ||
private static final Set<Class<?>> FIELD_TYPES = Set.of(Duration.class); | ||
|
||
@Override | ||
public final Set<Class<?>> getFieldTypes() { | ||
return FIELD_TYPES; | ||
} | ||
|
||
@Override | ||
public Printer<?> getPrinter(DurationFormat annotation, Class<?> fieldType) { | ||
return new DurationFormatter(annotation.style(), annotation.defaultUnit()); | ||
} | ||
|
||
@Override | ||
public Parser<?> getParser(DurationFormat annotation, Class<?> fieldType) { | ||
return new DurationFormatter(annotation.style(), annotation.defaultUnit()); | ||
} | ||
} |
Oops, something went wrong.