diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/BinderConversionService.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/BinderConversionService.java index 1ba51bc02f..9bd1583c3c 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/BinderConversionService.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/BinderConversionService.java @@ -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); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/StringToDurationConverter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/StringToDurationConverter.java new file mode 100644 index 0000000000..6af2e969ec --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/StringToDurationConverter.java @@ -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 { + + private static Pattern ISO8601 = Pattern.compile("^[\\+\\-]?P.*$"); + + private static Pattern SIMPLE = Pattern.compile("^([\\+\\-]?\\d+)([a-zA-Z]{1,2})$"); + + private static final Map UNITS; + + static { + Map 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; + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/BinderConversionServiceTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/BinderConversionServiceTests.java index 6674c6879e..d850ade1f8 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/BinderConversionServiceTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/BinderConversionServiceTests.java @@ -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 diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/StringToDurationConverterTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/StringToDurationConverterTests.java new file mode 100644 index 0000000000..0f98d95692 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/StringToDurationConverterTests.java @@ -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); + } + +}