Allow more flexible Duration binding

Extend `BinderConversionService` to support `Duration` parsing of
the more readable `10s` form (equivalent to 10 seconds). Standard
ISO-8601 parsing also remains as an option.

Fixes gh-11078
pull/11066/merge
Phillip Webb 7 years ago
parent 2f6aca222e
commit 99afc4bc1f

@ -107,6 +107,7 @@ public class BinderConversionService implements ConversionService {
service.addConverter(new StringToInetAddressConverter());
service.addConverter(new InetAddressToStringConverter());
service.addConverter(new PropertyEditorConverter());
service.addConverter(new StringToDurationConverter());
DateFormatterRegistrar registrar = new DateFormatterRegistrar();
DateFormatter formatter = new DateFormatter();
formatter.setIso(DateTimeFormat.ISO.DATE_TIME);

@ -0,0 +1,79 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.context.properties.bind.convert;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.core.convert.converter.Converter;
import org.springframework.util.Assert;
/**
* {@link Converter} for {@link String} to {@link Duration}. Support
* {@link Duration#parse(CharSequence)} as well a more readable {@code 10s} form.
*
* @author Phillip Webb
*/
class StringToDurationConverter implements Converter<String, Duration> {
private static Pattern ISO8601 = Pattern.compile("^[\\+\\-]?P.*$");
private static Pattern SIMPLE = Pattern.compile("^([\\+\\-]?\\d+)([a-zA-Z]{1,2})$");
private static final Map<String, ChronoUnit> UNITS;
static {
Map<String, ChronoUnit> units = new LinkedHashMap<>();
units.put("ns", ChronoUnit.NANOS);
units.put("ms", ChronoUnit.MILLIS);
units.put("s", ChronoUnit.SECONDS);
units.put("m", ChronoUnit.MINUTES);
units.put("h", ChronoUnit.HOURS);
units.put("d", ChronoUnit.DAYS);
UNITS = Collections.unmodifiableMap(units);
}
@Override
public Duration convert(String source) {
try {
if (ISO8601.matcher(source).matches()) {
return Duration.parse(source);
}
Matcher matcher = SIMPLE.matcher(source);
Assert.state(matcher.matches(), "'" + source + "' is not a valid duration");
long amount = Long.parseLong(matcher.group(1));
ChronoUnit unit = getUnit(matcher.group(2));
return Duration.of(amount, unit);
}
catch (Exception ex) {
throw new IllegalStateException("'" + source + "' is not a valid duration",
ex);
}
}
private ChronoUnit getUnit(String value) {
ChronoUnit unit = UNITS.get(value.toLowerCase());
Assert.state(unit != null, "Unknown unit '" + value + "'");
return unit;
}
}

@ -18,6 +18,7 @@ package org.springframework.boot.context.properties.bind.convert;
import java.io.InputStream;
import java.net.InetAddress;
import java.time.Duration;
import org.junit.Before;
import org.junit.Test;
@ -153,6 +154,13 @@ public class BinderConversionServiceTests {
assertThat(converted).isEqualTo(InputStream.class);
}
@Test
public void conversionServiceShouldSupportStringToDuration() throws Exception {
this.service = new BinderConversionService(null);
Duration converted = this.service.convert("10s", Duration.class);
assertThat(converted).isEqualTo(Duration.ofSeconds(10));
}
enum TestEnum {
ONE, TWO

@ -0,0 +1,111 @@
/*
* Copyright 2012-2017 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
*
* http://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.boot.context.properties.bind.convert;
import java.time.Duration;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link StringToDurationConverter}.
*
* @author Phillip Webb
*/
public class StringToDurationConverterTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
private StringToDurationConverter converter = new StringToDurationConverter();
@Test
public void convertWhenIso8601ShouldReturnDuration() throws Exception {
assertThat(convert("PT20.345S")).isEqualTo(Duration.parse("PT20.345S"));
assertThat(convert("PT15M")).isEqualTo(Duration.parse("PT15M"));
assertThat(convert("+PT15M")).isEqualTo(Duration.parse("PT15M"));
assertThat(convert("PT10H")).isEqualTo(Duration.parse("PT10H"));
assertThat(convert("P2D")).isEqualTo(Duration.parse("P2D"));
assertThat(convert("P2DT3H4M")).isEqualTo(Duration.parse("P2DT3H4M"));
assertThat(convert("P2DT3H4M")).isEqualTo(Duration.parse("P2DT3H4M"));
assertThat(convert("-PT6H3M")).isEqualTo(Duration.parse("-PT6H3M"));
assertThat(convert("-PT-6H+3M")).isEqualTo(Duration.parse("-PT-6H+3M"));
}
@Test
public void convertWhenSimpleNanosShouldReturnDuration() {
assertThat(convert("10ns")).isEqualTo(Duration.ofNanos(10));
assertThat(convert("10NS")).isEqualTo(Duration.ofNanos(10));
assertThat(convert("+10ns")).isEqualTo(Duration.ofNanos(10));
assertThat(convert("-10ns")).isEqualTo(Duration.ofNanos(-10));
}
@Test
public void convertWhenSimpleMillisShouldReturnDuration() {
assertThat(convert("10ms")).isEqualTo(Duration.ofMillis(10));
assertThat(convert("10MS")).isEqualTo(Duration.ofMillis(10));
assertThat(convert("+10ms")).isEqualTo(Duration.ofMillis(10));
assertThat(convert("-10ms")).isEqualTo(Duration.ofMillis(-10));
}
@Test
public void convertWhenSimpleSecondsShouldReturnDuration() {
assertThat(convert("10s")).isEqualTo(Duration.ofSeconds(10));
assertThat(convert("10S")).isEqualTo(Duration.ofSeconds(10));
assertThat(convert("+10s")).isEqualTo(Duration.ofSeconds(10));
assertThat(convert("-10s")).isEqualTo(Duration.ofSeconds(-10));
}
@Test
public void convertWhenSimpleMinutesShouldReturnDuration() {
assertThat(convert("10m")).isEqualTo(Duration.ofMinutes(10));
assertThat(convert("10M")).isEqualTo(Duration.ofMinutes(10));
assertThat(convert("+10m")).isEqualTo(Duration.ofMinutes(10));
assertThat(convert("-10m")).isEqualTo(Duration.ofMinutes(-10));
}
@Test
public void convertWhenSimpleHoursShouldReturnDuration() {
assertThat(convert("10h")).isEqualTo(Duration.ofHours(10));
assertThat(convert("10H")).isEqualTo(Duration.ofHours(10));
assertThat(convert("+10h")).isEqualTo(Duration.ofHours(10));
assertThat(convert("-10h")).isEqualTo(Duration.ofHours(-10));
}
@Test
public void convertWhenSimpleDaysShouldReturnDuration() {
assertThat(convert("10d")).isEqualTo(Duration.ofDays(10));
assertThat(convert("10D")).isEqualTo(Duration.ofDays(10));
assertThat(convert("+10d")).isEqualTo(Duration.ofDays(10));
assertThat(convert("-10d")).isEqualTo(Duration.ofDays(-10));
}
@Test
public void convertWhenBadFormatShouldThrowException() throws Exception {
this.thrown.expect(IllegalStateException.class);
this.thrown.expectMessage("'10foo' is not a valid duration");
convert("10foo");
}
private Duration convert(String source) {
return this.converter.convert(source);
}
}
Loading…
Cancel
Save