Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support multiple style of parsing/printing Durations #30396

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
6 changes: 4 additions & 2 deletions framework-docs/modules/ROOT/pages/core/validation/format.adoc
Expand Up @@ -74,7 +74,8 @@ The `format` subpackages provide several `Formatter` implementations as a conven
The `number` package provides `NumberStyleFormatter`, `CurrencyStyleFormatter`, and
`PercentStyleFormatter` to format `Number` objects that use a `java.text.NumberFormat`.
The `datetime` package provides a `DateFormatter` to format `java.util.Date` objects with
a `java.text.DateFormat`.
a `java.text.DateFormat`, as well as a `DurationFormatter` to format `Duration` objects
in different styles defined in the `@DurationFormat.Style` enum (see <<format-annotations-api>>).

The following `DateFormatter` is an example `Formatter` implementation:

Expand Down Expand Up @@ -280,7 +281,8 @@ Kotlin::

A portable format annotation API exists in the `org.springframework.format.annotation`
package. You can use `@NumberFormat` to format `Number` fields such as `Double` and
`Long`, and `@DateTimeFormat` to format `java.util.Date`, `java.util.Calendar`, `Long`
`Long`, `@DurationFormat` to format `Duration` fields in ISO8601 and simplified styles,
and `@DateTimeFormat` to format `java.util.Date`, `java.util.Calendar`, `Long`
(for millisecond timestamps) as well as JSR-310 `java.time`.

The following example uses `@DateTimeFormat` to format a `java.util.Date` as an ISO Date
Expand Down
2 changes: 1 addition & 1 deletion framework-docs/modules/ROOT/pages/web/webflux/config.adoc
Expand Up @@ -91,7 +91,7 @@ class WebConfig : WebFluxConfigurer {
[.small]#xref:web/webmvc/mvc-config/conversion.adoc[See equivalent in the Servlet stack]#

By default, formatters for various number and date types are installed, along with support
for customization via `@NumberFormat` and `@DateTimeFormat` on fields.
for customization via `@NumberFormat`, `@DurationFormat` and `@DateTimeFormat` on fields.

To register custom formatters and converters in Java config, use the following:

Expand Down
Expand Up @@ -4,7 +4,7 @@
[.small]#xref:web/webflux/config.adoc#webflux-config-conversion[See equivalent in the Reactive stack]#

By default, formatters for various number and date types are installed, along with support
for customization via `@NumberFormat` and `@DateTimeFormat` on fields.
for customization via `@NumberFormat`, `@DurationFormat` and `@DateTimeFormat` on fields.

To register custom formatters and converters in Java config, use the following:

Expand Down
@@ -0,0 +1,213 @@
/*
* Copyright 2002-2023 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.1
*/
@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");
}

}

}
Expand Up @@ -197,6 +197,7 @@ public void registerFormatters(FormatterRegistry registry) {
registry.addFormatterForFieldType(MonthDay.class, new MonthDayFormatter());

registry.addFormatterForFieldAnnotation(new Jsr310DateTimeFormatAnnotationFormatterFactory());
registry.addFormatterForFieldAnnotation(new DurationFormatAnnotationFormatterFactory());
}

private DateTimeFormatter getFormatter(Type type) {
Expand Down
@@ -0,0 +1,57 @@
/*
* Copyright 2002-2023 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.1
* @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());
}
}