Skip to content

Commit

Permalink
Allow more flexible Duration binding
Browse files Browse the repository at this point in the history
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
  • Loading branch information
philwebb committed Nov 19, 2017
1 parent 2f6aca2 commit 99afc4b
Show file tree
Hide file tree
Showing 4 changed files with 199 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ private static ConversionService createAdditionalConversionService() {
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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import java.io.InputStream;
import java.net.InetAddress;
import java.time.Duration;

import org.junit.Before;
import org.junit.Test;
Expand Down Expand Up @@ -153,6 +154,13 @@ public void conversionServiceShouldSupportStringToClass() throws Exception {
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
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}

}

0 comments on commit 99afc4b

Please sign in to comment.