From 20109e27be24da74e884c14a03afc8f9561e4196 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Sat, 17 Feb 2018 08:21:49 -0800 Subject: [PATCH] Add ApplicationConversionService and fix binder Create a new `ApplicationConversionService` similar in design to the DefaultFormattingConversionService from Spring Framework. The new conversion service provides a central place for custom conversion logic supported by Spring Boot. Also replace the `BindingConversionService` with an internal `BindConverter` class that now invokes the `SimpleTypeConverter` directly. Binding for `@ConfigurationProperties` has been updated so that any custom property editors registered with the BeanFactory can be used. Fixes gh-12095 --- .../endpoint/web/CorsEndpointProperties.java | 4 +- .../ServiceLevelAgreementBoundary.java | 9 +- ...ConversionServiceParameterValueMapper.java | 13 +- .../autoconfigure/amqp/RabbitProperties.java | 4 +- .../cassandra/CassandraProperties.java | 6 +- .../autoconfigure/jdbc/JdbcProperties.java | 4 +- .../autoconfigure/kafka/KafkaProperties.java | 4 +- .../ldap/embedded/EmbeddedLdapProperties.java | 2 +- ...h2ClientPropertiesRegistrationAdapter.java | 4 +- .../transaction/TransactionProperties.java | 4 +- .../autoconfigure/web/ResourceProperties.java | 12 +- .../autoconfigure/web/ServerProperties.java | 4 +- .../ConfigurationPropertiesBinder.java | 34 ++- .../properties/ConversionServiceDeducer.java | 7 +- .../properties/bind/AggregateBinder.java | 7 +- .../context/properties/bind/ArrayBinder.java | 3 +- .../context/properties/bind/BeanBinder.java | 3 +- .../context/properties/bind/BindContext.java | 23 -- .../properties/bind/BindConverter.java | 106 +++++++ .../boot/context/properties/bind/Binder.java | 132 ++++---- .../properties/bind/CollectionBinder.java | 3 +- .../bind/IndexedElementsBinder.java | 8 +- .../properties/bind/JavaBeanBinder.java | 5 +- .../context/properties/bind/MapBinder.java | 24 +- .../bind/ResolvableTypeDescriptor.java | 87 ------ .../bind/convert/BinderConversionService.java | 128 -------- .../bind/convert/DurationConverter.java | 125 -------- .../bind/convert/PropertyEditorConverter.java | 83 ------ .../properties/bind/convert/package-info.java | 20 -- .../convert/ApplicationConversionService.java | 95 ++++++ .../ArrayToDelimitedStringConverter.java | 61 ++++ .../CharArrayFormatter.java} | 24 +- .../CollectionToDelimitedStringConverter.java | 86 ++++++ .../DelimitedStringToArrayConverter.java | 86 ++++++ .../DelimitedStringToCollectionConverter.java | 14 +- .../bind => }/convert/Delimiter.java | 2 +- .../DurationFormat.java} | 27 +- .../boot/convert/DurationStyle.java | 255 ++++++++++++++++ .../convert/DurationToNumberConverter.java | 65 ++++ .../convert/DurationToStringConverter.java | 59 ++++ .../DurationUnit.java} | 4 +- .../InetAddressFormatter.java} | 25 +- .../boot/convert/IsoOffsetFormatter.java | 46 +++ .../convert/NumberToDurationConverter.java | 51 ++++ .../convert/StringToDurationConverter.java | 61 ++++ ...ngToEnumIgnoringCaseConverterFactory.java} | 5 +- .../boot/jta/narayana/NarayanaProperties.java | 8 +- .../boot/web/servlet/server/Session.java | 6 +- .../ConfigurationPropertiesTests.java | 21 ++ .../context/properties/bind/BinderTests.java | 24 +- .../properties/bind/JavaBeanBinderTests.java | 2 +- .../properties/bind/MapBinderTests.java | 4 +- .../bind/ResolvableTypeDescriptorTests.java | 62 ---- .../convert/AbstractInetAddressTests.java | 40 --- .../convert/BinderConversionServiceTests.java | 207 ------------- .../InetAddressToStringConverterTests.java | 42 --- .../convert/PropertyEditorConverterTests.java | 96 ------ .../StringToCharArrayConverterTests.java | 38 --- .../StringToEnumConverterFactoryTests.java | 91 ------ .../StringToInetAddressConverterTests.java | 53 ---- .../ArrayToDelimitedStringConverterTests.java | 116 ++++++++ .../boot/convert/CharArrayFormatterTests.java | 61 ++++ ...ectionToDelimitedStringConverterTests.java | 119 ++++++++ .../convert/ConversionServiceParameters.java | 76 +++++ .../DelimitedStringToArrayConverterTests.java | 144 +++++++++ ...mitedStringToCollectionConverterTests.java | 99 +++--- .../boot/convert/DurationStyleTests.java | 281 ++++++++++++++++++ .../DurationToNumberConverterTests.java | 81 +++++ .../DurationToStringConverterTests.java | 80 +++++ .../convert/InetAddressFormatterTests.java | 89 ++++++ .../boot/convert/IsoOffsetFormatterTests.java | 66 ++++ .../convert/MockDurationTypeDescriptor.java | 60 ++++ .../NumberToDurationConverterTests.java | 94 ++++++ .../StringToDurationConverterTests.java} | 60 ++-- ...EnumIgnoringCaseConverterFactoryTests.java | 106 +++++++ 75 files changed, 2706 insertions(+), 1354 deletions(-) create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindConverter.java delete mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ResolvableTypeDescriptor.java delete mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/BinderConversionService.java delete mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/DurationConverter.java delete mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/PropertyEditorConverter.java delete mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/package-info.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/ApplicationConversionService.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/ArrayToDelimitedStringConverter.java rename spring-boot-project/spring-boot/src/main/java/org/springframework/boot/{context/properties/bind/convert/InetAddressToStringConverter.java => convert/CharArrayFormatter.java} (55%) create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/CollectionToDelimitedStringConverter.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/DelimitedStringToArrayConverter.java rename spring-boot-project/spring-boot/src/main/java/org/springframework/boot/{context/properties/bind => }/convert/DelimitedStringToCollectionConverter.java (85%) rename spring-boot-project/spring-boot/src/main/java/org/springframework/boot/{context/properties/bind => }/convert/Delimiter.java (95%) rename spring-boot-project/spring-boot/src/main/java/org/springframework/boot/{context/properties/bind/convert/StringToCharArrayConverter.java => convert/DurationFormat.java} (52%) create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/DurationStyle.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/DurationToNumberConverter.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/DurationToStringConverter.java rename spring-boot-project/spring-boot/src/main/java/org/springframework/boot/{context/properties/bind/convert/DefaultDurationUnit.java => convert/DurationUnit.java} (92%) rename spring-boot-project/spring-boot/src/main/java/org/springframework/boot/{context/properties/bind/convert/StringToInetAddressConverter.java => convert/InetAddressFormatter.java} (55%) create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/IsoOffsetFormatter.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/NumberToDurationConverter.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/StringToDurationConverter.java rename spring-boot-project/spring-boot/src/main/java/org/springframework/boot/{context/properties/bind/convert/StringToEnumConverterFactory.java => convert/StringToEnumIgnoringCaseConverterFactory.java} (94%) delete mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ResolvableTypeDescriptorTests.java delete mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/AbstractInetAddressTests.java delete mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/BinderConversionServiceTests.java delete mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/InetAddressToStringConverterTests.java delete mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/PropertyEditorConverterTests.java delete mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/StringToCharArrayConverterTests.java delete mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/StringToEnumConverterFactoryTests.java delete mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/StringToInetAddressConverterTests.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/ArrayToDelimitedStringConverterTests.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/CharArrayFormatterTests.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/CollectionToDelimitedStringConverterTests.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/ConversionServiceParameters.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/DelimitedStringToArrayConverterTests.java rename spring-boot-project/spring-boot/src/test/java/org/springframework/boot/{context/properties/bind => }/convert/DelimitedStringToCollectionConverterTests.java (58%) create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/DurationStyleTests.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/DurationToNumberConverterTests.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/DurationToStringConverterTests.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/InetAddressFormatterTests.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/IsoOffsetFormatterTests.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/MockDurationTypeDescriptor.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/NumberToDurationConverterTests.java rename spring-boot-project/spring-boot/src/test/java/org/springframework/boot/{context/properties/bind/convert/DurationConverterTests.java => convert/StringToDurationConverterTests.java} (72%) create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/StringToEnumIgnoringCaseConverterFactoryTests.java diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/CorsEndpointProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/CorsEndpointProperties.java index 6d3c551d1c..de1a4edad5 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/CorsEndpointProperties.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/CorsEndpointProperties.java @@ -23,7 +23,7 @@ import java.util.List; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.PropertyMapper; -import org.springframework.boot.context.properties.bind.convert.DefaultDurationUnit; +import org.springframework.boot.convert.DurationUnit; import org.springframework.util.CollectionUtils; import org.springframework.web.cors.CorsConfiguration; @@ -67,7 +67,7 @@ public class CorsEndpointProperties { * How long the response from a pre-flight request can be cached by clients. If a * duration suffix is not specified, seconds will be used. */ - @DefaultDurationUnit(ChronoUnit.SECONDS) + @DurationUnit(ChronoUnit.SECONDS) private Duration maxAge = Duration.ofSeconds(1800); public List getAllowedOrigins() { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/ServiceLevelAgreementBoundary.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/ServiceLevelAgreementBoundary.java index fc2a779e69..ee8a647c7b 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/ServiceLevelAgreementBoundary.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/ServiceLevelAgreementBoundary.java @@ -22,7 +22,7 @@ import java.util.concurrent.TimeUnit; import io.micrometer.core.instrument.Meter; import io.micrometer.core.instrument.Meter.Type; -import org.springframework.boot.context.properties.bind.convert.DurationConverter; +import org.springframework.boot.convert.DurationStyle; /** * A service level agreement boundary for use when configuring micrometer. Can be @@ -81,8 +81,7 @@ public final class ServiceLevelAgreementBoundary { if (isNumber(value)) { return new ServiceLevelAgreementBoundary(Long.parseLong(value)); } - return new ServiceLevelAgreementBoundary( - DurationConverter.toDuration(value, null)); + return new ServiceLevelAgreementBoundary(DurationStyle.detectAndParse(value)); } /** @@ -97,8 +96,8 @@ public final class ServiceLevelAgreementBoundary { /** * Return a new {@link ServiceLevelAgreementBoundary} instance for the given String - * value. The value may contain a simple number, or a {@link DurationConverter - * duration formatted value} + * value. The value may contain a simple number, or a {@link DurationStyle duration + * style string}. * @param value the source value * @return a {@link ServiceLevelAgreementBoundary} instance */ diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/convert/ConversionServiceParameterValueMapper.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/convert/ConversionServiceParameterValueMapper.java index f4ba3fc0b1..1034e69cac 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/convert/ConversionServiceParameterValueMapper.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/invoke/convert/ConversionServiceParameterValueMapper.java @@ -19,9 +19,8 @@ package org.springframework.boot.actuate.endpoint.invoke.convert; import org.springframework.boot.actuate.endpoint.invoke.OperationParameter; import org.springframework.boot.actuate.endpoint.invoke.ParameterMappingException; import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper; -import org.springframework.boot.context.properties.bind.convert.BinderConversionService; +import org.springframework.boot.convert.ApplicationConversionService; import org.springframework.core.convert.ConversionService; -import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.util.Assert; /** @@ -39,7 +38,7 @@ public class ConversionServiceParameterValueMapper implements ParameterValueMapp * Create a new {@link ConversionServiceParameterValueMapper} instance. */ public ConversionServiceParameterValueMapper() { - this(createConversionService()); + this(ApplicationConversionService.getSharedInstance()); } /** @@ -49,7 +48,7 @@ public class ConversionServiceParameterValueMapper implements ParameterValueMapp */ public ConversionServiceParameterValueMapper(ConversionService conversionService) { Assert.notNull(conversionService, "ConversionService must not be null"); - this.conversionService = new BinderConversionService(conversionService); + this.conversionService = conversionService; } @Override @@ -63,10 +62,4 @@ public class ConversionServiceParameterValueMapper implements ParameterValueMapp } } - private static ConversionService createConversionService() { - DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService(); - IsoOffsetDateTimeConverter.registerConverter(conversionService); - return conversionService; - } - } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitProperties.java index fad8563d1d..72bde870d5 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitProperties.java @@ -24,7 +24,7 @@ import java.util.List; import org.springframework.amqp.core.AcknowledgeMode; import org.springframework.amqp.rabbit.connection.CachingConnectionFactory.CacheMode; import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.bind.convert.DefaultDurationUnit; +import org.springframework.boot.convert.DurationUnit; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; @@ -80,7 +80,7 @@ public class RabbitProperties { * Requested heartbeat timeout; zero for none. If a duration suffix is not specified, * seconds will be used. */ - @DefaultDurationUnit(ChronoUnit.SECONDS) + @DurationUnit(ChronoUnit.SECONDS) private Duration requestedHeartbeat; /** diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/CassandraProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/CassandraProperties.java index 4ca62d83d1..e07a2a972e 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/CassandraProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/CassandraProperties.java @@ -31,7 +31,7 @@ import com.datastax.driver.core.policies.ReconnectionPolicy; import com.datastax.driver.core.policies.RetryPolicy; import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.bind.convert.DefaultDurationUnit; +import org.springframework.boot.convert.DurationUnit; /** * Configuration properties for Cassandra. @@ -283,7 +283,7 @@ public class CassandraProperties { * Idle timeout before an idle connection is removed. If a duration suffix is not * specified, seconds will be used. */ - @DefaultDurationUnit(ChronoUnit.SECONDS) + @DurationUnit(ChronoUnit.SECONDS) private Duration idleTimeout = Duration.ofSeconds(120); /** @@ -296,7 +296,7 @@ public class CassandraProperties { * sure it's still alive. If a duration suffix is not specified, seconds will be * used. */ - @DefaultDurationUnit(ChronoUnit.SECONDS) + @DurationUnit(ChronoUnit.SECONDS) private Duration heartbeatInterval = Duration.ofSeconds(30); /** diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/JdbcProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/JdbcProperties.java index 391084b306..5e998819f1 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/JdbcProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/JdbcProperties.java @@ -20,7 +20,7 @@ import java.time.Duration; import java.time.temporal.ChronoUnit; import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.bind.convert.DefaultDurationUnit; +import org.springframework.boot.convert.DurationUnit; /** * Configuration properties for JDBC. @@ -58,7 +58,7 @@ public class JdbcProperties { * Query timeout. Default is to use the JDBC driver's default configuration. If a * duration suffix is not specified, seconds will be used. */ - @DefaultDurationUnit(ChronoUnit.SECONDS) + @DurationUnit(ChronoUnit.SECONDS) private Duration queryTimeout; public int getFetchSize() { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaProperties.java index c3a52c2dd9..bd8bf06e20 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/kafka/KafkaProperties.java @@ -33,7 +33,7 @@ import org.apache.kafka.common.serialization.StringDeserializer; import org.apache.kafka.common.serialization.StringSerializer; import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.bind.convert.DefaultDurationUnit; +import org.springframework.boot.convert.DurationUnit; import org.springframework.core.io.Resource; import org.springframework.kafka.listener.AbstractMessageListenerContainer.AckMode; import org.springframework.kafka.security.jaas.KafkaJaasLoginModuleInitializer; @@ -840,7 +840,7 @@ public class KafkaProperties { * Time between checks for non-responsive consumers. If a duration suffix is not * specified, seconds will be used. */ - @DefaultDurationUnit(ChronoUnit.SECONDS) + @DurationUnit(ChronoUnit.SECONDS) private Duration monitorInterval; /** diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ldap/embedded/EmbeddedLdapProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ldap/embedded/EmbeddedLdapProperties.java index 0076d9364b..f99826c079 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ldap/embedded/EmbeddedLdapProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/ldap/embedded/EmbeddedLdapProperties.java @@ -20,7 +20,7 @@ import java.util.ArrayList; import java.util.List; import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.bind.convert.Delimiter; +import org.springframework.boot.convert.Delimiter; import org.springframework.core.io.Resource; /** diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientPropertiesRegistrationAdapter.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientPropertiesRegistrationAdapter.java index e9c655c39e..a3b95ece55 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientPropertiesRegistrationAdapter.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientPropertiesRegistrationAdapter.java @@ -22,7 +22,7 @@ import java.util.Map; import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties.Provider; import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties.Registration; import org.springframework.boot.context.properties.PropertyMapper; -import org.springframework.boot.context.properties.bind.convert.BinderConversionService; +import org.springframework.boot.convert.ApplicationConversionService; import org.springframework.core.convert.ConversionException; import org.springframework.security.config.oauth2.client.CommonOAuth2Provider; import org.springframework.security.oauth2.client.registration.ClientRegistration; @@ -107,7 +107,7 @@ final class OAuth2ClientPropertiesRegistrationAdapter { private static CommonOAuth2Provider getCommonProvider(String providerId) { try { - return new BinderConversionService(null).convert(providerId, + return ApplicationConversionService.getSharedInstance().convert(providerId, CommonOAuth2Provider.class); } catch (ConversionException ex) { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/TransactionProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/TransactionProperties.java index 8b5ea8f69e..8ab158a788 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/TransactionProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/transaction/TransactionProperties.java @@ -20,7 +20,7 @@ import java.time.Duration; import java.time.temporal.ChronoUnit; import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.bind.convert.DefaultDurationUnit; +import org.springframework.boot.convert.DurationUnit; import org.springframework.transaction.support.AbstractPlatformTransactionManager; /** @@ -39,7 +39,7 @@ public class TransactionProperties implements * Default transaction timeout. If a duration suffix is not specified, seconds will be * used. */ - @DefaultDurationUnit(ChronoUnit.SECONDS) + @DurationUnit(ChronoUnit.SECONDS) private Duration defaultTimeout; /** diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ResourceProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ResourceProperties.java index 7bdd699949..30f0aff919 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ResourceProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ResourceProperties.java @@ -22,7 +22,7 @@ import java.util.concurrent.TimeUnit; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.PropertyMapper; -import org.springframework.boot.context.properties.bind.convert.DefaultDurationUnit; +import org.springframework.boot.convert.DurationUnit; import org.springframework.http.CacheControl; /** @@ -277,7 +277,7 @@ public class ResourceProperties { * suffix is not specified, seconds will be used. Can be overridden by the * 'spring.resources.cache.cachecontrol' properties. */ - @DefaultDurationUnit(ChronoUnit.SECONDS) + @DurationUnit(ChronoUnit.SECONDS) private Duration period; /** @@ -307,7 +307,7 @@ public class ResourceProperties { * Maximum time the response should be cached, in seconds if no duration * suffix is not specified. */ - @DefaultDurationUnit(ChronoUnit.SECONDS) + @DurationUnit(ChronoUnit.SECONDS) private Duration maxAge; /** @@ -354,21 +354,21 @@ public class ResourceProperties { * Maximum time the response can be served after it becomes stale, in seconds * if no duration suffix is not specified. */ - @DefaultDurationUnit(ChronoUnit.SECONDS) + @DurationUnit(ChronoUnit.SECONDS) private Duration staleWhileRevalidate; /** * Maximum time the response may be used when errors are encountered, in * seconds if no duration suffix is not specified. */ - @DefaultDurationUnit(ChronoUnit.SECONDS) + @DurationUnit(ChronoUnit.SECONDS) private Duration staleIfError; /** * Maximum time the response should be cached by shared caches, in seconds if * no duration suffix is not specified. */ - @DefaultDurationUnit(ChronoUnit.SECONDS) + @DurationUnit(ChronoUnit.SECONDS) private Duration sMaxAge; public Duration getMaxAge() { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java index 65bd68f914..c409f13ab3 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java @@ -31,7 +31,7 @@ import java.util.TimeZone; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.NestedConfigurationProperty; -import org.springframework.boot.context.properties.bind.convert.DefaultDurationUnit; +import org.springframework.boot.convert.DurationUnit; import org.springframework.boot.web.server.Compression; import org.springframework.boot.web.server.Http2; import org.springframework.boot.web.server.Ssl; @@ -373,7 +373,7 @@ public class ServerProperties { * Delay between the invocation of backgroundProcess methods. If a duration suffix * is not specified, seconds will be used. */ - @DefaultDurationUnit(ChronoUnit.SECONDS) + @DurationUnit(ChronoUnit.SECONDS) private Duration backgroundProcessorDelay = Duration.ofSeconds(30); /** diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinder.java index b1bfd14ccf..0c06260916 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinder.java @@ -18,7 +18,9 @@ package org.springframework.boot.context.properties; import java.util.ArrayList; import java.util.List; +import java.util.function.Consumer; +import org.springframework.beans.PropertyEditorRegistry; import org.springframework.boot.context.properties.bind.BindHandler; import org.springframework.boot.context.properties.bind.Bindable; import org.springframework.boot.context.properties.bind.Binder; @@ -26,9 +28,12 @@ import org.springframework.boot.context.properties.bind.PropertySourcesPlacehold import org.springframework.boot.context.properties.bind.handler.IgnoreErrorsBindHandler; import org.springframework.boot.context.properties.bind.handler.NoUnboundElementsBindHandler; import org.springframework.boot.context.properties.bind.validation.ValidationBindHandler; +import org.springframework.boot.context.properties.source.ConfigurationPropertySource; import org.springframework.boot.context.properties.source.ConfigurationPropertySources; import org.springframework.boot.context.properties.source.UnboundElementsSourceFilter; import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.convert.ConversionService; import org.springframework.core.env.PropertySources; import org.springframework.util.Assert; import org.springframework.validation.Validator; @@ -115,13 +120,32 @@ class ConfigurationPropertiesBinder { private Binder getBinder() { if (this.binder == null) { - this.binder = new Binder( - ConfigurationPropertySources.from(this.propertySources), - new PropertySourcesPlaceholdersResolver(this.propertySources), - new ConversionServiceDeducer(this.applicationContext) - .getConversionService()); + this.binder = new Binder(getConfigurationPropertySources(), + getPropertySourcesPlaceholdersResolver(), getConversionService(), + getPropertyEditorInitializer()); } return this.binder; } + private Iterable getConfigurationPropertySources() { + return ConfigurationPropertySources.from(this.propertySources); + } + + private PropertySourcesPlaceholdersResolver getPropertySourcesPlaceholdersResolver() { + return new PropertySourcesPlaceholdersResolver(this.propertySources); + } + + private ConversionService getConversionService() { + return new ConversionServiceDeducer(this.applicationContext) + .getConversionService(); + } + + private Consumer getPropertyEditorInitializer() { + if (this.applicationContext instanceof ConfigurableApplicationContext) { + return ((ConfigurableApplicationContext) this.applicationContext) + .getBeanFactory()::copyRegisteredEditorsTo; + } + return null; + } + } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConversionServiceDeducer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConversionServiceDeducer.java index 52cce891e7..8af981e802 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConversionServiceDeducer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConversionServiceDeducer.java @@ -21,12 +21,12 @@ import java.util.List; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.convert.ApplicationConversionService; import org.springframework.context.ApplicationContext; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.GenericConverter; -import org.springframework.core.convert.support.DefaultConversionService; /** * Utility to deduce the {@link ConversionService} to use for configuration properties @@ -83,7 +83,10 @@ class ConversionServiceDeducer { } public ConversionService create() { - DefaultConversionService conversionService = new DefaultConversionService(); + if (this.converters.isEmpty() && this.genericConverters.isEmpty()) { + return ApplicationConversionService.getSharedInstance(); + } + ApplicationConversionService conversionService = new ApplicationConversionService(); for (Converter converter : this.converters) { conversionService.addConverter(converter); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/AggregateBinder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/AggregateBinder.java index 4dcd3be831..6e30d659e2 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/AggregateBinder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/AggregateBinder.java @@ -18,6 +18,7 @@ package org.springframework.boot.context.properties.bind; import java.util.function.Supplier; +import org.springframework.boot.context.properties.bind.Binder.Context; import org.springframework.boot.context.properties.source.ConfigurationPropertyName; import org.springframework.boot.context.properties.source.ConfigurationPropertySource; @@ -30,9 +31,9 @@ import org.springframework.boot.context.properties.source.ConfigurationPropertyS */ abstract class AggregateBinder { - private final BindContext context; + private final Context context; - AggregateBinder(BindContext context) { + AggregateBinder(Context context) { this.context = context; } @@ -84,7 +85,7 @@ abstract class AggregateBinder { * Return the context being used by this binder. * @return the context */ - protected final BindContext getContext() { + protected final Context getContext() { return this.context; } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ArrayBinder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ArrayBinder.java index ffbb3a870b..e67800a5cd 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ArrayBinder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ArrayBinder.java @@ -20,6 +20,7 @@ import java.lang.reflect.Array; import java.util.ArrayList; import java.util.List; +import org.springframework.boot.context.properties.bind.Binder.Context; import org.springframework.boot.context.properties.source.ConfigurationPropertyName; import org.springframework.core.ResolvableType; @@ -31,7 +32,7 @@ import org.springframework.core.ResolvableType; */ class ArrayBinder extends IndexedElementsBinder { - ArrayBinder(BindContext context) { + ArrayBinder(Context context) { super(context); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BeanBinder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BeanBinder.java index 5694a564f4..37cda540e7 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BeanBinder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BeanBinder.java @@ -16,6 +16,7 @@ package org.springframework.boot.context.properties.bind; +import org.springframework.boot.context.properties.bind.Binder.Context; import org.springframework.boot.context.properties.source.ConfigurationPropertyName; /** @@ -36,7 +37,7 @@ interface BeanBinder { * @param The source type * @return a bound instance or {@code null} */ - T bind(ConfigurationPropertyName name, Bindable target, BindContext context, + T bind(ConfigurationPropertyName name, Bindable target, Context context, BeanPropertyBinder propertyBinder); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindContext.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindContext.java index 30ecd61c0f..6f69cbb9b8 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindContext.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindContext.java @@ -16,12 +16,8 @@ package org.springframework.boot.context.properties.bind; -import java.util.stream.Stream; - -import org.springframework.boot.context.properties.bind.convert.BinderConversionService; import org.springframework.boot.context.properties.source.ConfigurationProperty; import org.springframework.boot.context.properties.source.ConfigurationPropertySource; -import org.springframework.core.convert.ConversionService; /** * Context information for use by {@link BindHandler BindHandlers}. @@ -46,13 +42,6 @@ public interface BindContext { */ Iterable getSources(); - /** - * Return a {@link Stream} of the {@link ConfigurationPropertySource sources} being - * used by the {@link Binder}. - * @return the sources - */ - Stream streamSources(); - /** * Return the {@link ConfigurationProperty} actually being bound or {@code null} if * the property has not yet been determined. @@ -60,16 +49,4 @@ public interface BindContext { */ ConfigurationProperty getConfigurationProperty(); - /** - * Return the {@link PlaceholdersResolver} being used by the binder. - * @return the {@link PlaceholdersResolver} (never {@code null}) - */ - PlaceholdersResolver getPlaceholdersResolver(); - - /** - * Return the {@link ConversionService} used by the binder. - * @return the conversion service - */ - BinderConversionService getConversionService(); - } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindConverter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindConverter.java new file mode 100644 index 0000000000..c5e5fa9319 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindConverter.java @@ -0,0 +1,106 @@ +/* + * Copyright 2012-2018 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; + +import java.beans.PropertyEditor; +import java.lang.annotation.Annotation; +import java.util.Collection; +import java.util.Map; +import java.util.function.Consumer; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.PropertyEditorRegistry; +import org.springframework.beans.SimpleTypeConverter; +import org.springframework.core.ResolvableType; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.util.Assert; + +/** + * Utility to handle any conversion needed during binding. This class is not thread-safe + * and so a new instance is created for each top-level bind. + * + * @author Phillip Webb + */ +class BindConverter { + + private final ConversionService conversionService; + + private final SimpleTypeConverter simpleTypeConverter; + + BindConverter(ConversionService conversionService, + Consumer propertyEditorInitializer) { + Assert.notNull(conversionService, "ConversionService must not be null"); + this.conversionService = conversionService; + this.simpleTypeConverter = new SimpleTypeConverter(); + if (propertyEditorInitializer != null) { + propertyEditorInitializer.accept(this.simpleTypeConverter); + } + } + + public boolean canConvert(Object value, ResolvableType type, + Annotation... annotations) { + return getPropertyEditor(type.resolve()) != null + || this.conversionService.canConvert(TypeDescriptor.forObject(value), + new ResolvableTypeDescriptor(type, annotations)); + } + + public T convert(Object result, Bindable target) { + return convert(result, target.getType(), target.getAnnotations()); + } + + @SuppressWarnings("unchecked") + public T convert(Object value, ResolvableType type, Annotation... annotations) { + PropertyEditor propertyEditor = getPropertyEditor(type.resolve()); + if (propertyEditor != null) { + if (value == null) { + return null; + } + return (T) this.simpleTypeConverter.convertIfNecessary(value, type.resolve()); + } + return (T) this.conversionService.convert(value, TypeDescriptor.forObject(value), + new ResolvableTypeDescriptor(type, annotations)); + } + + private PropertyEditor getPropertyEditor(Class type) { + if (type == null || type == Object.class + || Collection.class.isAssignableFrom(type) + || Map.class.isAssignableFrom(type)) { + return null; + } + PropertyEditor editor = this.simpleTypeConverter.getDefaultEditor(type); + if (editor == null) { + editor = this.simpleTypeConverter.findCustomEditor(type, null); + } + if (editor == null && String.class != type) { + editor = BeanUtils.findEditorByConvention(type); + } + return editor; + } + + /** + * A {@link TypeDescriptor} backed by a {@link ResolvableType}. + */ + final class ResolvableTypeDescriptor extends TypeDescriptor { + + ResolvableTypeDescriptor(ResolvableType resolvableType, + Annotation[] annotations) { + super(resolvableType, null, annotations); + } + + } +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java index 32f5d04f8a..881645564d 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java @@ -27,16 +27,18 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.function.Consumer; import java.util.function.Supplier; import java.util.stream.Stream; import java.util.stream.StreamSupport; -import org.springframework.boot.context.properties.bind.convert.BinderConversionService; +import org.springframework.beans.PropertyEditorRegistry; import org.springframework.boot.context.properties.source.ConfigurationProperty; import org.springframework.boot.context.properties.source.ConfigurationPropertyName; import org.springframework.boot.context.properties.source.ConfigurationPropertySource; import org.springframework.boot.context.properties.source.ConfigurationPropertySources; import org.springframework.boot.context.properties.source.ConfigurationPropertyState; +import org.springframework.boot.convert.ApplicationConversionService; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.ConverterNotFoundException; import org.springframework.core.env.Environment; @@ -69,7 +71,9 @@ public class Binder { private final PlaceholdersResolver placeholdersResolver; - private final BinderConversionService conversionService; + private final ConversionService conversionService; + + private final Consumer propertyEditorInitializer; /** * Create a new {@link Binder} instance for the specified sources. A @@ -77,7 +81,7 @@ public class Binder { * @param sources the sources used for binding */ public Binder(ConfigurationPropertySource... sources) { - this(Arrays.asList(sources), null, null); + this(Arrays.asList(sources), null, null, null); } /** @@ -86,7 +90,7 @@ public class Binder { * @param sources the sources used for binding */ public Binder(Iterable sources) { - this(sources, null, null); + this(sources, null, null, null); } /** @@ -96,25 +100,29 @@ public class Binder { */ public Binder(Iterable sources, PlaceholdersResolver placeholdersResolver) { - this(sources, placeholdersResolver, null); + this(sources, placeholdersResolver, null, null); } /** * Create a new {@link Binder} instance for the specified sources. * @param sources the sources used for binding * @param placeholdersResolver strategy to resolve any property place-holders - * @param conversionService the conversion service to convert values + * @param conversionService the conversion service to convert values (or {@code null} + * to use {@link ApplicationConversionService}) + * @param propertyEditorInitializer initializer used to configure the property editors + * that can convert values */ public Binder(Iterable sources, PlaceholdersResolver placeholdersResolver, - ConversionService conversionService) { + ConversionService conversionService, + Consumer propertyEditorInitializer) { Assert.notNull(sources, "Sources must not be null"); this.sources = sources; this.placeholdersResolver = (placeholdersResolver != null ? placeholdersResolver : PlaceholdersResolver.NONE); - this.conversionService = (conversionService instanceof BinderConversionService - ? (BinderConversionService) conversionService - : new BinderConversionService(conversionService)); + this.conversionService = (conversionService != null ? conversionService + : ApplicationConversionService.getSharedInstance()); + this.propertyEditorInitializer = propertyEditorInitializer; } /** @@ -208,17 +216,17 @@ public class Binder { BindHandler handler, Context context, Object result) throws Exception { if (result != null) { result = handler.onSuccess(name, target, context, result); - result = convert(result, target); + result = context.getConverter().convert(result, target); } handler.onFinish(name, target, context, result); - return convert(result, target); + return context.getConverter().convert(result, target); } private T handleBindError(ConfigurationPropertyName name, Bindable target, BindHandler handler, Context context, Exception error) { try { Object result = handler.onFailure(name, target, context, error); - return convert(result, target); + return context.getConverter().convert(result, target); } catch (Exception ex) { if (ex instanceof BindException) { @@ -228,11 +236,6 @@ public class Binder { } } - private T convert(Object value, Bindable target) { - return ResolvableTypeDescriptor.forBindable(target) - .convert(this.conversionService, value); - } - private Object bindObject(ConfigurationPropertyName name, Bindable target, BindHandler handler, Context context, boolean allowRecursiveBinding) throws Exception { @@ -300,7 +303,7 @@ public class Binder { context.setConfigurationProperty(property); Object result = property.getValue(); result = this.placeholdersResolver.resolvePlaceholders(result); - result = convert(result, target); + result = context.getConverter().convert(result, target); return result; } @@ -356,35 +359,37 @@ public class Binder { } /** - * {@link BindContext} implementation. + * Context used when binding and the {@link BindContext} implementation. */ final class Context implements BindContext { - private int depth; + private final BindConverter converter; - private int sourcePushCount; + private int depth; private final List source = Arrays .asList((ConfigurationPropertySource) null); + private int sourcePushCount; + private final Deque> beans = new ArrayDeque<>(); private ConfigurationProperty configurationProperty; - void increaseDepth() { + Context() { + this.converter = new BindConverter(Binder.this.conversionService, + Binder.this.propertyEditorInitializer); + } + + private void increaseDepth() { this.depth++; } - void decreaseDepth() { + private void decreaseDepth() { this.depth--; } - @Override - public int getDepth() { - return this.depth; - } - - public T withSource(ConfigurationPropertySource source, + private T withSource(ConfigurationPropertySource source, Supplier supplier) { if (source == null) { return supplier.get(); @@ -399,7 +404,7 @@ public class Binder { } } - public T withBean(Class bean, Supplier supplier) { + private T withBean(Class bean, Supplier supplier) { this.beans.push(bean); try { return withIncreasedDepth(supplier); @@ -409,7 +414,11 @@ public class Binder { } } - public T withIncreasedDepth(Supplier supplier) { + private boolean hasBoundBean(Class bean) { + return this.beans.contains(bean); + } + + private T withIncreasedDepth(Supplier supplier) { increaseDepth(); try { return supplier.get(); @@ -419,6 +428,35 @@ public class Binder { } } + private void setConfigurationProperty( + ConfigurationProperty configurationProperty) { + this.configurationProperty = configurationProperty; + } + + private void clearConfigurationProperty() { + this.configurationProperty = null; + } + + public Stream streamSources() { + if (this.sourcePushCount > 0) { + return this.source.stream(); + } + return StreamSupport.stream(Binder.this.sources.spliterator(), false); + } + + public PlaceholdersResolver getPlaceholdersResolver() { + return Binder.this.placeholdersResolver; + } + + public BindConverter getConverter() { + return this.converter; + } + + @Override + public int getDepth() { + return this.depth; + } + @Override public Iterable getSources() { if (this.sourcePushCount > 0) { @@ -427,41 +465,11 @@ public class Binder { return Binder.this.sources; } - @Override - public Stream streamSources() { - if (this.sourcePushCount > 0) { - return this.source.stream(); - } - return StreamSupport.stream(Binder.this.sources.spliterator(), false); - } - - public boolean hasBoundBean(Class bean) { - return this.beans.contains(bean); - } - @Override public ConfigurationProperty getConfigurationProperty() { return this.configurationProperty; } - void setConfigurationProperty(ConfigurationProperty configurationProperty) { - this.configurationProperty = configurationProperty; - } - - void clearConfigurationProperty() { - this.configurationProperty = null; - } - - @Override - public PlaceholdersResolver getPlaceholdersResolver() { - return Binder.this.placeholdersResolver; - } - - @Override - public BinderConversionService getConversionService() { - return Binder.this.conversionService; - } - } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/CollectionBinder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/CollectionBinder.java index c90080f0b6..102a19d8d4 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/CollectionBinder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/CollectionBinder.java @@ -19,6 +19,7 @@ package org.springframework.boot.context.properties.bind; import java.util.Collection; import java.util.List; +import org.springframework.boot.context.properties.bind.Binder.Context; import org.springframework.boot.context.properties.source.ConfigurationPropertyName; import org.springframework.core.CollectionFactory; import org.springframework.core.ResolvableType; @@ -31,7 +32,7 @@ import org.springframework.core.ResolvableType; */ class CollectionBinder extends IndexedElementsBinder> { - CollectionBinder(BindContext context) { + CollectionBinder(Context context) { super(context); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/IndexedElementsBinder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/IndexedElementsBinder.java index 601a7f4505..d2ef648dd2 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/IndexedElementsBinder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/IndexedElementsBinder.java @@ -23,7 +23,7 @@ import java.util.TreeSet; import java.util.function.Supplier; import java.util.stream.Collectors; -import org.springframework.boot.context.properties.bind.convert.BinderConversionService; +import org.springframework.boot.context.properties.bind.Binder.Context; import org.springframework.boot.context.properties.source.ConfigurationProperty; import org.springframework.boot.context.properties.source.ConfigurationPropertyName; import org.springframework.boot.context.properties.source.ConfigurationPropertyName.Form; @@ -45,7 +45,7 @@ abstract class IndexedElementsBinder extends AggregateBinder { private static final String INDEX_ZERO = "[0]"; - IndexedElementsBinder(BindContext context) { + IndexedElementsBinder(Context context) { super(context); } @@ -140,9 +140,7 @@ abstract class IndexedElementsBinder extends AggregateBinder { private C convert(Object value, ResolvableType type, Annotation... annotations) { value = getContext().getPlaceholdersResolver().resolvePlaceholders(value); - BinderConversionService conversionService = getContext().getConversionService(); - return ResolvableTypeDescriptor.forType(type, annotations) - .convert(conversionService, value); + return getContext().getConverter().convert(value, type, annotations); } /** diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/JavaBeanBinder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/JavaBeanBinder.java index fc6a54eaa1..f8071d0333 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/JavaBeanBinder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/JavaBeanBinder.java @@ -26,6 +26,7 @@ import java.util.Map; import java.util.function.Supplier; import org.springframework.beans.BeanUtils; +import org.springframework.boot.context.properties.bind.Binder.Context; import org.springframework.boot.context.properties.source.ConfigurationPropertyName; import org.springframework.boot.context.properties.source.ConfigurationPropertyState; import org.springframework.core.MethodParameter; @@ -40,8 +41,8 @@ import org.springframework.core.ResolvableType; class JavaBeanBinder implements BeanBinder { @Override - public T bind(ConfigurationPropertyName name, Bindable target, - BindContext context, BeanPropertyBinder propertyBinder) { + public T bind(ConfigurationPropertyName name, Bindable target, Context context, + BeanPropertyBinder propertyBinder) { boolean hasKnownBindableProperties = context.streamSources().anyMatch(( s) -> s.containsDescendantOf(name) == ConfigurationPropertyState.PRESENT); Bean bean = Bean.get(target, hasKnownBindableProperties); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/MapBinder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/MapBinder.java index 7d672a418b..c90a028f01 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/MapBinder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/MapBinder.java @@ -20,6 +20,7 @@ import java.util.Collection; import java.util.Map; import java.util.Properties; +import org.springframework.boot.context.properties.bind.Binder.Context; import org.springframework.boot.context.properties.source.ConfigurationProperty; import org.springframework.boot.context.properties.source.ConfigurationPropertyName; import org.springframework.boot.context.properties.source.ConfigurationPropertyName.Form; @@ -41,7 +42,7 @@ class MapBinder extends AggregateBinder> { private static final Bindable> STRING_STRING_MAP = Bindable .mapOf(String.class, String.class); - MapBinder(BindContext context) { + MapBinder(Context context) { super(context); } @@ -62,10 +63,8 @@ class MapBinder extends AggregateBinder> { if (!ConfigurationPropertyName.EMPTY.equals(name)) { ConfigurationProperty property = source.getConfigurationProperty(name); if (property != null && !hasDescendants) { - Object value = getContext().getPlaceholdersResolver() - .resolvePlaceholders(property.getValue()); - return ResolvableTypeDescriptor.forType(target.getType()) - .convert(getContext().getConversionService(), value); + return getContext().getConverter().convert(property.getValue(), + target); } source = source.filter(name::isAncestorOf); } @@ -116,7 +115,8 @@ class MapBinder extends AggregateBinder> { for (ConfigurationPropertyName name : (IterableConfigurationPropertySource) source) { Bindable valueBindable = getValueBindable(name); ConfigurationPropertyName entryName = getEntryName(source, name); - Object key = convert(getKeyName(entryName), this.keyType); + Object key = getContext().getConverter() + .convert(getKeyName(entryName), this.keyType); map.computeIfAbsent(key, (k) -> this.elementBinder.bind(entryName, valueBindable)); } @@ -172,17 +172,7 @@ class MapBinder extends AggregateBinder> { } Object value = property.getValue(); value = getContext().getPlaceholdersResolver().resolvePlaceholders(value); - return canConvert(value, this.valueType); - } - - private boolean canConvert(Object source, ResolvableType targetType) { - return ResolvableTypeDescriptor.forType(targetType) - .canConvert(getContext().getConversionService(), source); - } - - private Object convert(Object source, ResolvableType targetType) { - return ResolvableTypeDescriptor.forType(targetType) - .convert(getContext().getConversionService(), source); + return getContext().getConverter().canConvert(value, this.valueType); } private String getKeyName(ConfigurationPropertyName name) { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ResolvableTypeDescriptor.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ResolvableTypeDescriptor.java deleted file mode 100644 index cfa4eb6858..0000000000 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ResolvableTypeDescriptor.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * 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; - -import java.lang.annotation.Annotation; - -import org.springframework.core.ResolvableType; -import org.springframework.core.convert.ConversionException; -import org.springframework.core.convert.ConversionService; -import org.springframework.core.convert.TypeDescriptor; - -/** - * A {@link TypeDescriptor} backed by a {@link ResolvableType}. - * - * @author Phillip Webb - */ -@SuppressWarnings("serial") -final class ResolvableTypeDescriptor extends TypeDescriptor { - - private ResolvableTypeDescriptor(ResolvableType resolvableType, - Annotation[] annotations) { - super(resolvableType, null, annotations); - } - - /** - * Determine if the specified source object can be converted to this type. - * @param conversionService the backing conversion service - * @param source the source to check - * @return {@code true} if conversion can be performed - */ - public boolean canConvert(ConversionService conversionService, Object source) { - TypeDescriptor sourceType = TypeDescriptor.forObject(source); - return conversionService.canConvert(sourceType, this); - } - - /** - * Convert the given source object into this type. - * @param conversionService the source conversion service - * @param value the value to convert - * @param the target type - * @return the converted value - * @throws ConversionException if a conversion exception occurred - */ - @SuppressWarnings("unchecked") - public T convert(ConversionService conversionService, Object value) { - if (value == null) { - return null; - } - TypeDescriptor sourceType = TypeDescriptor.forObject(value); - return (T) conversionService.convert(value, sourceType, this); - } - - /** - * Create a {@link TypeDescriptor} for the specified {@link Bindable}. - * @param bindable the bindable - * @return the type descriptor - */ - public static ResolvableTypeDescriptor forBindable(Bindable bindable) { - return forType(bindable.getType(), bindable.getAnnotations()); - } - - /** - * Return a {@link TypeDescriptor} for the specified {@link ResolvableType}. - * @param type the resolvable type - * @param annotations the annotations to include - * @return the type descriptor - */ - public static ResolvableTypeDescriptor forType(ResolvableType type, - Annotation... annotations) { - return new ResolvableTypeDescriptor(type, annotations); - } - -} 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 deleted file mode 100644 index 34e82c0244..0000000000 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/BinderConversionService.java +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright 2012-2018 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.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.function.Function; - -import org.springframework.boot.context.properties.bind.Binder; -import org.springframework.core.convert.ConversionException; -import org.springframework.core.convert.ConversionService; -import org.springframework.core.convert.TypeDescriptor; -import org.springframework.core.convert.support.DefaultConversionService; -import org.springframework.core.convert.support.GenericConversionService; -import org.springframework.format.annotation.DateTimeFormat; -import org.springframework.format.datetime.DateFormatter; -import org.springframework.format.datetime.DateFormatterRegistrar; -import org.springframework.format.support.DefaultFormattingConversionService; - -/** - * {@link ConversionService} used by the {@link Binder}. - * - * @author Phillip Webb - * @author Stephane Nicoll - * @since 2.0.0 - */ -public class BinderConversionService implements ConversionService { - - private static final ConversionService defaultConversionService = new DefaultFormattingConversionService(); - - private final List conversionServices; - - /** - * Create a new {@link BinderConversionService} instance. - * @param conversionService and option root conversion service - */ - public BinderConversionService(ConversionService conversionService) { - List conversionServices = new ArrayList<>(); - conversionServices.add(createOverrideConversionService()); - conversionServices.add( - conversionService != null ? conversionService : defaultConversionService); - conversionServices.add(createAdditionalConversionService()); - this.conversionServices = Collections.unmodifiableList(conversionServices); - } - - private ConversionService createOverrideConversionService() { - GenericConversionService service = new GenericConversionService(); - service.addConverter(new DelimitedStringToCollectionConverter(this)); - return service; - } - - private ConversionService createAdditionalConversionService() { - DefaultFormattingConversionService service = new DefaultFormattingConversionService(); - DefaultConversionService.addCollectionConverters(service); - service.addConverterFactory(new StringToEnumConverterFactory()); - service.addConverter(new StringToCharArrayConverter()); - service.addConverter(new StringToInetAddressConverter()); - service.addConverter(new InetAddressToStringConverter()); - service.addConverter(new PropertyEditorConverter()); - service.addConverter(new DurationConverter()); - DateFormatterRegistrar registrar = new DateFormatterRegistrar(); - DateFormatter formatter = new DateFormatter(); - formatter.setIso(DateTimeFormat.ISO.DATE_TIME); - registrar.setFormatter(formatter); - registrar.registerFormatters(service); - return service; - } - - @Override - public boolean canConvert(Class sourceType, Class targetType) { - for (ConversionService service : this.conversionServices) { - if (service.canConvert(sourceType, targetType)) { - return true; - } - } - return false; - } - - @Override - public boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType) { - for (ConversionService service : this.conversionServices) { - if (service.canConvert(sourceType, targetType)) { - return true; - } - } - return false; - } - - @Override - public T convert(Object source, Class targetType) { - return callConversionServices((c) -> c.convert(source, targetType)); - } - - @Override - public Object convert(Object source, TypeDescriptor sourceType, - TypeDescriptor targetType) { - return callConversionServices((c) -> c.convert(source, sourceType, targetType)); - } - - private T callConversionServices(Function call) { - ConversionException exception = null; - for (ConversionService service : this.conversionServices) { - try { - return call.apply(service); - } - catch (ConversionException ex) { - exception = ex; - } - } - throw exception; - } - -} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/DurationConverter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/DurationConverter.java deleted file mode 100644 index 44de020d96..0000000000 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/DurationConverter.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright 2012-2018 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.LinkedHashSet; -import java.util.Map; -import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.springframework.core.convert.TypeDescriptor; -import org.springframework.core.convert.converter.Converter; -import org.springframework.core.convert.converter.GenericConverter; -import org.springframework.util.Assert; -import org.springframework.util.StringUtils; - -/** - * {@link Converter} for {@link String} to {@link Duration}. Support - * {@link Duration#parse(CharSequence)} as well a more readable {@code 10s} form. - * - * @author Phillip Webb - * @since 2.0.0 - */ -public class DurationConverter implements GenericConverter { - - private static final Set TYPES; - - static { - Set types = new LinkedHashSet<>(); - types.add(new ConvertiblePair(String.class, Duration.class)); - types.add(new ConvertiblePair(Integer.class, Duration.class)); - TYPES = Collections.unmodifiableSet(types); - } - - private static final Pattern ISO8601 = Pattern.compile("^[\\+\\-]?P.*$"); - - private static final Pattern SIMPLE = Pattern - .compile("^([\\+\\-]?\\d+)([a-zA-Z]{0,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 Set getConvertibleTypes() { - return TYPES; - } - - @Override - public Object convert(Object source, TypeDescriptor sourceType, - TypeDescriptor targetType) { - if (source == null) { - return null; - } - DefaultDurationUnit defaultUnit = targetType - .getAnnotation(DefaultDurationUnit.class); - return toDuration(source.toString(), - (defaultUnit == null ? null : defaultUnit.value())); - } - - /** - * Convert the specified source to a {@link Duration}. - * @param source the source to convert - * @param defaultUnit the default unit to use ({@code null} is treated as - * milliseconds) - * @return the duration - */ - public static Duration toDuration(String source, ChronoUnit defaultUnit) { - try { - if (!StringUtils.hasLength(source)) { - return null; - } - 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), defaultUnit); - return Duration.of(amount, unit); - } - catch (Exception ex) { - throw new IllegalStateException("'" + source + "' is not a valid duration", - ex); - } - } - - private static ChronoUnit getUnit(String value, ChronoUnit defaultUnit) { - if (StringUtils.isEmpty(value)) { - return (defaultUnit != null ? defaultUnit : ChronoUnit.MILLIS); - } - ChronoUnit unit = UNITS.get(value.toLowerCase()); - Assert.state(unit != null, () -> "Unknown unit '" + value + "'"); - return unit; - } - -} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/PropertyEditorConverter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/PropertyEditorConverter.java deleted file mode 100644 index 977336eb72..0000000000 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/PropertyEditorConverter.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * 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.beans.PropertyEditor; -import java.util.Collection; -import java.util.Collections; -import java.util.LinkedHashSet; -import java.util.Map; -import java.util.Set; - -import org.springframework.beans.BeanUtils; -import org.springframework.beans.PropertyEditorRegistrySupport; -import org.springframework.beans.SimpleTypeConverter; -import org.springframework.core.convert.TypeDescriptor; -import org.springframework.core.convert.converter.ConditionalConverter; -import org.springframework.core.convert.converter.GenericConverter; - -/** - * {@link GenericConverter} that delegates to Java bean {@link PropertyEditor - * PropertyEditors}. - * - * @author Phillip Webb - */ -class PropertyEditorConverter implements GenericConverter, ConditionalConverter { - - private static final Set> SKIPPED; - - static { - Set> skipped = new LinkedHashSet<>(); - skipped.add(Collection.class); - skipped.add(Map.class); - SKIPPED = Collections.unmodifiableSet(skipped); - } - - /** - * Registry that can be used to check if conversion is supported. Since - * {@link PropertyEditor PropertyEditors} are not thread safe this can't be used for - * actual conversion. - */ - private final PropertyEditorRegistrySupport registry = new SimpleTypeConverter(); - - @Override - public Set getConvertibleTypes() { - return null; - } - - @Override - public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { - Class type = targetType.getType(); - if (isSkipped(type)) { - return false; - } - PropertyEditor editor = this.registry.getDefaultEditor(type); - editor = (editor != null ? editor : BeanUtils.findEditorByConvention(type)); - return editor != null; - } - - private boolean isSkipped(Class type) { - return SKIPPED.stream().anyMatch((c) -> c.isAssignableFrom(type)); - } - - @Override - public Object convert(Object source, TypeDescriptor sourceType, - TypeDescriptor targetType) { - return new SimpleTypeConverter().convertIfNecessary(source, targetType.getType()); - } - -} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/package-info.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/package-info.java deleted file mode 100644 index f2256e0184..0000000000 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/package-info.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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. - */ - -/** - * Conversion support for configuration properties binding. - */ -package org.springframework.boot.context.properties.bind.convert; diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/ApplicationConversionService.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/ApplicationConversionService.java new file mode 100644 index 0000000000..843e30907c --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/ApplicationConversionService.java @@ -0,0 +1,95 @@ +/* + * Copyright 2012-2018 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.convert; + +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.converter.ConverterRegistry; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.format.FormatterRegistry; +import org.springframework.format.support.DefaultFormattingConversionService; +import org.springframework.format.support.FormattingConversionService; +import org.springframework.util.StringValueResolver; + +/** + * A specialization of {@link FormattingConversionService} configured by default with + * converters and formatters appropriate for most Spring Boot applications. + *

+ * Designed for direct instantiation but also exposes the static + * {@link #addApplicationConverters} and + * {@link #addApplicationFormatters(FormatterRegistry)} utility methods for ad-hoc use + * against registry instance. + * + * @author Phillip Webb + * @since 2.0.0 + */ +public class ApplicationConversionService extends FormattingConversionService { + + private static volatile ApplicationConversionService sharedInstance; + + public ApplicationConversionService() { + this(null); + } + + public ApplicationConversionService(StringValueResolver embeddedValueResolver) { + if (embeddedValueResolver != null) { + setEmbeddedValueResolver(embeddedValueResolver); + } + DefaultConversionService.addDefaultConverters(this); + DefaultFormattingConversionService.addDefaultFormatters(this); + addApplicationConverters(this); + addApplicationFormatters(this); + } + + /** + * Return a shared default {@code ApplicationConversionService} instance, lazily + * building it once needed. + * @return the shared {@code ConversionService} instance (never {@code null}) + */ + public static ConversionService getSharedInstance() { + ApplicationConversionService sharedInstance = ApplicationConversionService.sharedInstance; + if (sharedInstance == null) { + synchronized (ApplicationConversionService.class) { + sharedInstance = ApplicationConversionService.sharedInstance; + if (sharedInstance == null) { + sharedInstance = new ApplicationConversionService(); + ApplicationConversionService.sharedInstance = sharedInstance; + } + } + } + return sharedInstance; + } + + public void addApplicationConverters(ConverterRegistry registry) { + ConversionService service = (ConversionService) registry; + registry.addConverter(new ArrayToDelimitedStringConverter(service)); + registry.addConverter(new CollectionToDelimitedStringConverter(service)); + registry.addConverter(new DelimitedStringToArrayConverter(service)); + registry.addConverter(new DelimitedStringToCollectionConverter(service)); + registry.addConverter(new StringToDurationConverter()); + registry.addConverter(new DurationToStringConverter()); + registry.addConverter(new NumberToDurationConverter()); + registry.addConverter(new DurationToNumberConverter()); + registry.addConverterFactory(new StringToEnumIgnoringCaseConverterFactory()); + } + + public void addApplicationFormatters(FormatterRegistry registry) { + registry.addFormatter(new CharArrayFormatter()); + registry.addFormatter(new InetAddressFormatter()); + registry.addFormatter(new IsoOffsetFormatter()); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/ArrayToDelimitedStringConverter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/ArrayToDelimitedStringConverter.java new file mode 100644 index 0000000000..d457d6f345 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/ArrayToDelimitedStringConverter.java @@ -0,0 +1,61 @@ +/* + * Copyright 2012-2018 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.convert; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; +import org.springframework.lang.Nullable; +import org.springframework.util.ObjectUtils; + +/** + * Converts an array to a delimited String. + * + * @author Phillip Webb + */ +final class ArrayToDelimitedStringConverter implements ConditionalGenericConverter { + + private final CollectionToDelimitedStringConverter delegate; + + ArrayToDelimitedStringConverter(ConversionService conversionService) { + this.delegate = new CollectionToDelimitedStringConverter(conversionService); + } + + @Override + public Set getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(Object[].class, String.class)); + } + + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + return this.delegate.matches(sourceType, targetType); + } + + @Override + @Nullable + public Object convert(@Nullable Object source, TypeDescriptor sourceType, + TypeDescriptor targetType) { + List list = Arrays.asList(ObjectUtils.toObjectArray(source)); + return this.delegate.convert(list, sourceType, targetType); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/InetAddressToStringConverter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/CharArrayFormatter.java similarity index 55% rename from spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/InetAddressToStringConverter.java rename to spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/CharArrayFormatter.java index 02a09e37ad..787b343762 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/InetAddressToStringConverter.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/CharArrayFormatter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2017 the original author or authors. + * Copyright 2012-2018 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. @@ -14,24 +14,28 @@ * limitations under the License. */ -package org.springframework.boot.context.properties.bind.convert; +package org.springframework.boot.convert; -import java.beans.PropertyEditor; -import java.net.InetAddress; +import java.text.ParseException; +import java.util.Locale; -import org.springframework.core.convert.converter.Converter; +import org.springframework.format.Formatter; /** - * {@link PropertyEditor} for {@link InetAddress} objects. + * {@link Formatter} for {@code char[]}. * - * @author Dave Syer * @author Phillip Webb */ -class InetAddressToStringConverter implements Converter { +final class CharArrayFormatter implements Formatter { @Override - public String convert(InetAddress source) { - return source.getHostAddress(); + public String print(char[] object, Locale locale) { + return new String(object); + } + + @Override + public char[] parse(String text, Locale locale) throws ParseException { + return text.toCharArray(); } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/CollectionToDelimitedStringConverter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/CollectionToDelimitedStringConverter.java new file mode 100644 index 0000000000..4efc5862a1 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/CollectionToDelimitedStringConverter.java @@ -0,0 +1,86 @@ +/* + * Copyright 2012-2018 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.convert; + +import java.util.Collection; +import java.util.Collections; +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; + +/** + * Converts a Collection to a delimited String. + * + * @author Phillip Webb + */ +final class CollectionToDelimitedStringConverter implements ConditionalGenericConverter { + + private final ConversionService conversionService; + + CollectionToDelimitedStringConverter(ConversionService conversionService) { + this.conversionService = conversionService; + } + + @Override + public Set getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(Collection.class, String.class)); + } + + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + TypeDescriptor sourceElementType = sourceType.getElementTypeDescriptor(); + if (targetType == null || sourceElementType == null) { + return true; + } + if (this.conversionService.canConvert(sourceElementType, targetType) + || sourceElementType.getType().isAssignableFrom(targetType.getType())) { + return true; + } + return false; + } + + @Override + public Object convert(Object source, TypeDescriptor sourceType, + TypeDescriptor targetType) { + if (source == null) { + return null; + } + Collection sourceCollection = (Collection) source; + return convert(sourceCollection, sourceType, targetType); + } + + private Object convert(Collection source, TypeDescriptor sourceType, + TypeDescriptor targetType) { + if (source.isEmpty()) { + return ""; + } + Delimiter delimiter = sourceType.getAnnotation(Delimiter.class); + return source.stream() + .map((element) -> convertElement(element, sourceType, targetType)) + .collect(Collectors.joining(delimiter == null ? "," : delimiter.value())); + } + + private String convertElement(Object element, TypeDescriptor sourceType, + TypeDescriptor targetType) { + return String.valueOf(this.conversionService.convert(element, + sourceType.elementTypeDescriptor(element), targetType)); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/DelimitedStringToArrayConverter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/DelimitedStringToArrayConverter.java new file mode 100644 index 0000000000..cdab8ac834 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/DelimitedStringToArrayConverter.java @@ -0,0 +1,86 @@ +/* + * Copyright 2012-2018 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.convert; + +import java.lang.reflect.Array; +import java.util.Collections; +import java.util.Set; + +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.ConditionalGenericConverter; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Converts a {@link Delimiter delimited} String to an Array. + * + * @author Phillip Webb + */ +final class DelimitedStringToArrayConverter implements ConditionalGenericConverter { + + private final ConversionService conversionService; + + DelimitedStringToArrayConverter(ConversionService conversionService) { + Assert.notNull(conversionService, "ConversionService must not be null"); + this.conversionService = conversionService; + } + + @Override + public Set getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(String.class, Object[].class)); + } + + @Override + public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { + return targetType.getElementTypeDescriptor() == null || this.conversionService + .canConvert(sourceType, targetType.getElementTypeDescriptor()); + } + + @Override + @Nullable + public Object convert(@Nullable Object source, TypeDescriptor sourceType, + TypeDescriptor targetType) { + if (source == null) { + return null; + } + return convert((String) source, sourceType, targetType); + } + + private Object convert(String source, TypeDescriptor sourceType, + TypeDescriptor targetType) { + Delimiter delimiter = targetType.getAnnotation(Delimiter.class); + String[] elements = getElements(source, + (delimiter == null ? "," : delimiter.value())); + TypeDescriptor elementDescriptor = targetType.getElementTypeDescriptor(); + Object target = Array.newInstance(elementDescriptor.getType(), elements.length); + for (int i = 0; i < elements.length; i++) { + String sourceElement = elements[i]; + Object targetElement = this.conversionService.convert(sourceElement.trim(), + sourceType, elementDescriptor); + Array.set(target, i, targetElement); + } + return target; + } + + private String[] getElements(String source, String delimiter) { + return StringUtils.delimitedListToStringArray(source, + Delimiter.NONE.equals(delimiter) ? null : delimiter); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/DelimitedStringToCollectionConverter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/DelimitedStringToCollectionConverter.java similarity index 85% rename from spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/DelimitedStringToCollectionConverter.java rename to spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/DelimitedStringToCollectionConverter.java index a8c92d4531..45589cb613 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/DelimitedStringToCollectionConverter.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/DelimitedStringToCollectionConverter.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.context.properties.bind.convert; +package org.springframework.boot.convert; import java.util.Arrays; import java.util.Collection; @@ -35,7 +35,7 @@ import org.springframework.util.StringUtils; * * @author Phillip Webb */ -class DelimitedStringToCollectionConverter implements ConditionalGenericConverter { +final class DelimitedStringToCollectionConverter implements ConditionalGenericConverter { private final ConversionService conversionService; @@ -51,10 +51,8 @@ class DelimitedStringToCollectionConverter implements ConditionalGenericConverte @Override public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { - return targetType.hasAnnotation(Delimiter.class) - && (targetType.getElementTypeDescriptor() == null - || this.conversionService.canConvert(sourceType, - targetType.getElementTypeDescriptor())); + return targetType.getElementTypeDescriptor() == null || this.conversionService + .canConvert(sourceType, targetType.getElementTypeDescriptor()); } @Override @@ -70,8 +68,8 @@ class DelimitedStringToCollectionConverter implements ConditionalGenericConverte private Object convert(String source, TypeDescriptor sourceType, TypeDescriptor targetType) { Delimiter delimiter = targetType.getAnnotation(Delimiter.class); - Assert.state(delimiter != null, "Missing @DelimitedStringFormat annotation"); - String[] elements = getElements(source, delimiter.value()); + String[] elements = getElements(source, + (delimiter == null ? "," : delimiter.value())); TypeDescriptor elementDescriptor = targetType.getElementTypeDescriptor(); Collection target = createCollection(targetType, elementDescriptor, elements.length); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/Delimiter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/Delimiter.java similarity index 95% rename from spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/Delimiter.java rename to spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/Delimiter.java index be529fad1e..97acccf2c6 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/Delimiter.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/Delimiter.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.context.properties.bind.convert; +package org.springframework.boot.convert; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/StringToCharArrayConverter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/DurationFormat.java similarity index 52% rename from spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/StringToCharArrayConverter.java rename to spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/DurationFormat.java index 67fbd690e9..f12ab642f2 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/StringToCharArrayConverter.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/DurationFormat.java @@ -14,20 +14,31 @@ * limitations under the License. */ -package org.springframework.boot.context.properties.bind.convert; +package org.springframework.boot.convert; -import org.springframework.core.convert.converter.Converter; +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; /** - * Converts a String to a Char Array. + * Annotation that can be used to indivate the format to use when converting a + * {@link Duration}. * * @author Phillip Webb + * @since 2.0.0 */ -class StringToCharArrayConverter implements Converter { +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface DurationFormat { - @Override - public char[] convert(String source) { - return source.toCharArray(); - } + /** + * The duration format style. + * @return the duration format style. + */ + DurationStyle value(); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/DurationStyle.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/DurationStyle.java new file mode 100644 index 0000000000..0e81efbde0 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/DurationStyle.java @@ -0,0 +1,255 @@ +/* + * Copyright 2012-2018 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.convert; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Duration format styles. + * + * @author Phillip Webb + * @since 2.0.0 + */ +public enum DurationStyle { + + /** + * Simple formatting, for example '1s'. + */ + SIMPLE("^([\\+\\-]?\\d+)([a-zA-Z]{0,2})$") { + + @Override + public Duration parse(String value, ChronoUnit unit) { + try { + Matcher matcher = matcher(value); + Assert.state(matcher.matches(), "Does not match simple duration pattern"); + String suffix = matcher.group(2); + return (StringUtils.hasLength(suffix) ? Unit.fromSuffix(suffix) + : Unit.fromChronoUnit(unit)).parse(matcher.group(1)); + } + catch (Exception ex) { + throw new IllegalArgumentException( + "'" + value + "' is not a valid simple duration", ex); + } + } + + @Override + public String print(Duration value, ChronoUnit unit) { + return Unit.fromChronoUnit(unit).print(value); + } + + }, + + /** + * ISO-8601 formatting. + */ + ISO8601("^[\\+\\-]?P.*$") { + + @Override + public Duration parse(String value, ChronoUnit unit) { + try { + return Duration.parse(value); + } + catch (Exception ex) { + throw new IllegalArgumentException( + "'" + value + "' is not a valid ISO-8601 duration", ex); + } + } + + @Override + public String print(Duration value, ChronoUnit unit) { + return value.toString(); + } + + }; + + private final Pattern pattern; + + DurationStyle(String pattern) { + this.pattern = Pattern.compile(pattern); + } + + protected final boolean matches(String value) { + return this.pattern.matcher(value).matches(); + } + + protected final Matcher matcher(String value) { + return this.pattern.matcher(value); + } + + /** + * Parse the given value to a duration. + * @param value the value to parse + * @return a duration + */ + public Duration parse(String value) { + return parse(value, null); + } + + /** + * Parse the given value to a duration. + * @param value the value to parse + * @param unit the duration unit to use if the value doesn't specify one ({@code null} + * will default to ms) + * @return a duration + */ + public abstract Duration parse(String value, ChronoUnit unit); + + /** + * Print the specified duration. + * @param value the value to print + * @return the printed result + */ + public String print(Duration value) { + return print(value, null); + } + + /** + * Print the specified duration using the given unit. + * @param value the value to print + * @param unit the value to use for printing + * @return the printed result + */ + public abstract String print(Duration value, ChronoUnit unit); + + /** + * Detect the style then parse the value to return a duration. + * @param value the value to parse + * @return the parsed duration + * @throws IllegalStateException if the value is not a known style or cannot be parsed + */ + public static Duration detectAndParse(String value) { + return detectAndParse(value, null); + } + + /** + * Detect the style then parse the value to return a duration. + * @param value the value to parse + * @param unit the duration unit to use if the value doesn't specify one ({@code null} + * will default to ms) + * @return the parsed duration + * @throws IllegalStateException if the value is not a known style or cannot be parsed + */ + public static Duration detectAndParse(String value, ChronoUnit unit) { + return detect(value).parse(value, unit); + } + + /** + * Detect the style from the given source value. + * @param value the source value + * @return the duration style + * @throws IllegalStateException if the value is not a known style + */ + public static DurationStyle detect(String value) { + Assert.notNull(value, "Value must not be null"); + for (DurationStyle candidate : values()) { + if (candidate.matches(value)) { + return candidate; + } + } + throw new IllegalArgumentException("'" + value + "' is not a valid duration"); + } + + /** + * Units that we support. + */ + enum Unit { + + /** + * Nanoseconds. + */ + NANOS(ChronoUnit.NANOS, "ns", Duration::toNanos), + + /** + * Milliseconds. + */ + MILLIS(ChronoUnit.MILLIS, "ms", Duration::toMillis), + + /** + * Seconds. + */ + SECONDS(ChronoUnit.SECONDS, "s", Duration::getSeconds), + + /** + * Minutes. + */ + MINUTES(ChronoUnit.MINUTES, "m", Duration::toMinutes), + + /** + * Hours. + */ + HOURS(ChronoUnit.HOURS, "h", Duration::toHours), + + /** + * Days. + */ + DAYS(ChronoUnit.DAYS, "d", Duration::toDays); + + private final ChronoUnit chronoUnit; + + private final String suffix; + + private Function longValue; + + Unit(ChronoUnit chronoUnit, String suffix, Function toUnit) { + this.chronoUnit = chronoUnit; + this.suffix = suffix; + this.longValue = toUnit; + } + + public Duration parse(String value) { + return Duration.of(Long.valueOf(value), this.chronoUnit); + } + + public String print(Duration value) { + return longValue(value) + this.suffix; + } + + public long longValue(Duration value) { + return this.longValue.apply(value); + } + + public static Unit fromChronoUnit(ChronoUnit chronoUnit) { + if (chronoUnit == null) { + return Unit.MILLIS; + } + for (Unit candidate : values()) { + if (candidate.chronoUnit == chronoUnit) { + return candidate; + } + } + throw new IllegalArgumentException("Unknown unit " + chronoUnit); + } + + public static Unit fromSuffix(String suffix) { + for (Unit candidate : values()) { + if (candidate.suffix.equalsIgnoreCase(suffix)) { + return candidate; + } + } + throw new IllegalArgumentException("Unknown unit '" + suffix + "'"); + } + + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/DurationToNumberConverter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/DurationToNumberConverter.java new file mode 100644 index 0000000000..fd587f3f42 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/DurationToNumberConverter.java @@ -0,0 +1,65 @@ +/* + * Copyright 2012-2018 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.convert; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.Collections; +import java.util.Set; + +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.converter.GenericConverter; +import org.springframework.util.ReflectionUtils; + +/** + * {@link Converter} to convert from a {@link Duration} to a {@link Number}. + * + * @author Phillip Webb + * @see DurationFormat + * @see DurationUnit + */ +final class DurationToNumberConverter implements GenericConverter { + + @Override + public Set getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(Duration.class, Number.class)); + } + + @Override + public Object convert(Object source, TypeDescriptor sourceType, + TypeDescriptor targetType) { + if (source == null) { + return null; + } + DurationUnit unit = sourceType.getAnnotation(DurationUnit.class); + return convert((Duration) source, (unit == null ? null : unit.value()), + targetType.getObjectType()); + } + + private Object convert(Duration source, ChronoUnit unit, Class type) { + try { + return type.getConstructor(String.class).newInstance(String + .valueOf(DurationStyle.Unit.fromChronoUnit(unit).longValue(source))); + } + catch (Exception ex) { + ReflectionUtils.rethrowRuntimeException(ex); + throw new IllegalStateException(ex); + } + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/DurationToStringConverter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/DurationToStringConverter.java new file mode 100644 index 0000000000..425b401d9e --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/DurationToStringConverter.java @@ -0,0 +1,59 @@ +/* + * Copyright 2012-2018 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.convert; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.Collections; +import java.util.Set; + +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.converter.GenericConverter; + +/** + * {@link Converter} to convert from a {@link Duration} to a {@link String}. + * + * @author Phillip Webb + * @see DurationFormat + * @see DurationUnit + */ +final class DurationToStringConverter implements GenericConverter { + + @Override + public Set getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(Duration.class, String.class)); + } + + @Override + public Object convert(Object source, TypeDescriptor sourceType, + TypeDescriptor targetType) { + if (source == null) { + return null; + } + DurationFormat format = sourceType.getAnnotation(DurationFormat.class); + DurationUnit unit = sourceType.getAnnotation(DurationUnit.class); + return convert((Duration) source, (format == null ? null : format.value()), + (unit == null ? null : unit.value())); + } + + private String convert(Duration source, DurationStyle style, ChronoUnit unit) { + style = (style != null ? style : DurationStyle.ISO8601); + return style.print(source, unit); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/DefaultDurationUnit.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/DurationUnit.java similarity index 92% rename from spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/DefaultDurationUnit.java rename to spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/DurationUnit.java index 04072f7933..83db732401 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/DefaultDurationUnit.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/DurationUnit.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.context.properties.bind.convert; +package org.springframework.boot.convert; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; @@ -34,7 +34,7 @@ import java.time.temporal.ChronoUnit; @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @Documented -public @interface DefaultDurationUnit { +public @interface DurationUnit { /** * The duration unit to use if one is not specified. diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/StringToInetAddressConverter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/InetAddressFormatter.java similarity index 55% rename from spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/StringToInetAddressConverter.java rename to spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/InetAddressFormatter.java index 9ccb0c58c7..d91022fb5e 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/StringToInetAddressConverter.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/InetAddressFormatter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2017 the original author or authors. + * Copyright 2012-2018 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. @@ -14,29 +14,34 @@ * limitations under the License. */ -package org.springframework.boot.context.properties.bind.convert; +package org.springframework.boot.convert; -import java.beans.PropertyEditor; import java.net.InetAddress; import java.net.UnknownHostException; +import java.text.ParseException; +import java.util.Locale; -import org.springframework.core.convert.converter.Converter; +import org.springframework.format.Formatter; /** - * {@link PropertyEditor} for {@link InetAddress} objects. + * {@link Formatter} for {@link InetAddress}. * - * @author Dave Syer * @author Phillip Webb */ -class StringToInetAddressConverter implements Converter { +final class InetAddressFormatter implements Formatter { @Override - public InetAddress convert(String source) { + public String print(InetAddress object, Locale locale) { + return object.getHostAddress(); + } + + @Override + public InetAddress parse(String text, Locale locale) throws ParseException { try { - return InetAddress.getByName(source); + return InetAddress.getByName(text); } catch (UnknownHostException ex) { - throw new IllegalStateException("Unknown host " + source, ex); + throw new IllegalStateException("Unknown host " + text, ex); } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/IsoOffsetFormatter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/IsoOffsetFormatter.java new file mode 100644 index 0000000000..94ecd17af7 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/IsoOffsetFormatter.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-2018 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.convert; + +import java.text.ParseException; +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Locale; + +import org.springframework.format.Formatter; + +/** + * A {@link Formatter} for {@link OffsetDateTime} that uses + * {@link DateTimeFormatter#ISO_OFFSET_DATE_TIME ISO offset formatting}. + * + * @author Andy Wilkinson + * @author Stephane Nicoll + * @author Phillip Webb + */ +class IsoOffsetFormatter implements Formatter { + + @Override + public String print(OffsetDateTime object, Locale locale) { + return DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(object); + } + + @Override + public OffsetDateTime parse(String text, Locale locale) throws ParseException { + return OffsetDateTime.parse(text, DateTimeFormatter.ISO_OFFSET_DATE_TIME); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/NumberToDurationConverter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/NumberToDurationConverter.java new file mode 100644 index 0000000000..7b1578fa9e --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/NumberToDurationConverter.java @@ -0,0 +1,51 @@ +/* + * Copyright 2012-2018 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.convert; + +import java.time.Duration; +import java.util.Collections; +import java.util.Set; + +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.converter.GenericConverter; + +/** + * {@link Converter} to convert from a {@link Number} to a {@link Duration}. Supports + * {@link Duration#parse(CharSequence)} as well a more readable {@code 10s} form. + * + * @author Phillip Webb + * @see DurationFormat + * @see DurationUnit + */ +final class NumberToDurationConverter implements GenericConverter { + + private final StringToDurationConverter delegate = new StringToDurationConverter(); + + @Override + public Set getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(Number.class, Duration.class)); + } + + @Override + public Object convert(Object source, TypeDescriptor sourceType, + TypeDescriptor targetType) { + return this.delegate.convert(source == null ? null : source.toString(), + TypeDescriptor.valueOf(String.class), targetType); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/StringToDurationConverter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/StringToDurationConverter.java new file mode 100644 index 0000000000..dbf8c827bb --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/StringToDurationConverter.java @@ -0,0 +1,61 @@ +/* + * Copyright 2012-2018 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.convert; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.Collections; +import java.util.Set; + +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.converter.GenericConverter; +import org.springframework.util.ObjectUtils; + +/** + * {@link Converter} to convert from a {@link String} to a {@link Duration}. Supports + * {@link Duration#parse(CharSequence)} as well a more readable {@code 10s} form. + * + * @author Phillip Webb + * @see DurationFormat + * @see DurationUnit + */ +final class StringToDurationConverter implements GenericConverter { + + @Override + public Set getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(String.class, Duration.class)); + } + + @Override + public Object convert(Object source, TypeDescriptor sourceType, + TypeDescriptor targetType) { + if (ObjectUtils.isEmpty(source)) { + return null; + } + DurationFormat format = targetType.getAnnotation(DurationFormat.class); + DurationUnit unit = targetType.getAnnotation(DurationUnit.class); + return convert(source.toString(), (format == null ? null : format.value()), + (unit == null ? null : unit.value())); + } + + private Duration convert(String source, DurationStyle style, ChronoUnit unit) { + style = (style != null ? style : DurationStyle.detect(source)); + return style.parse(source, unit); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/StringToEnumConverterFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/StringToEnumIgnoringCaseConverterFactory.java similarity index 94% rename from spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/StringToEnumConverterFactory.java rename to spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/StringToEnumIgnoringCaseConverterFactory.java index da9b4ab513..b9009e7505 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/convert/StringToEnumConverterFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/StringToEnumIgnoringCaseConverterFactory.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.context.properties.bind.convert; +package org.springframework.boot.convert; import java.util.EnumSet; import java.util.Set; @@ -30,7 +30,8 @@ import org.springframework.util.Assert; * @author Phillip Webb */ @SuppressWarnings({ "unchecked", "rawtypes" }) -class StringToEnumConverterFactory implements ConverterFactory { +final class StringToEnumIgnoringCaseConverterFactory + implements ConverterFactory { @Override public Converter getConverter(Class targetType) { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jta/narayana/NarayanaProperties.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jta/narayana/NarayanaProperties.java index dcb2037422..5c0824dbc4 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jta/narayana/NarayanaProperties.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jta/narayana/NarayanaProperties.java @@ -24,7 +24,7 @@ import java.util.Collections; import java.util.List; import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.bind.convert.DefaultDurationUnit; +import org.springframework.boot.convert.DurationUnit; /** * Subset of Narayana properties which can be configured via Spring configuration. Use @@ -59,21 +59,21 @@ public class NarayanaProperties { /** * Transaction timeout. If a duration suffix is not specified, seconds will be used. */ - @DefaultDurationUnit(ChronoUnit.SECONDS) + @DurationUnit(ChronoUnit.SECONDS) private Duration defaultTimeout = Duration.ofSeconds(60); /** * Interval in which periodic recovery scans are performed. If a duration suffix is * not specified, seconds will be used. */ - @DefaultDurationUnit(ChronoUnit.SECONDS) + @DurationUnit(ChronoUnit.SECONDS) private Duration periodicRecoveryPeriod = Duration.ofSeconds(120); /** * Back off period between first and second phases of the recovery scan. If a duration * suffix is not specified, seconds will be used. */ - @DefaultDurationUnit(ChronoUnit.SECONDS) + @DurationUnit(ChronoUnit.SECONDS) private Duration recoveryBackoffPeriod = Duration.ofSeconds(10); /** diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/server/Session.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/server/Session.java index 3868481395..67ebbad75e 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/server/Session.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/servlet/server/Session.java @@ -21,7 +21,7 @@ import java.time.Duration; import java.time.temporal.ChronoUnit; import java.util.Set; -import org.springframework.boot.context.properties.bind.convert.DefaultDurationUnit; +import org.springframework.boot.convert.DurationUnit; /** * Session properties. @@ -34,7 +34,7 @@ public class Session { /** * Session timeout. If a duration suffix is not specified, seconds will be used. */ - @DefaultDurationUnit(ChronoUnit.SECONDS) + @DurationUnit(ChronoUnit.SECONDS) private Duration timeout = Duration.ofMinutes(30); /** @@ -135,7 +135,7 @@ public class Session { /** * Maximum age of the session cookie. */ - @DefaultDurationUnit(ChronoUnit.SECONDS) + @DurationUnit(ChronoUnit.SECONDS) private Duration maxAge; public String getName() { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java index 7348bfed50..a9376e4e0a 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesTests.java @@ -16,6 +16,7 @@ package org.springframework.boot.context.properties; +import java.beans.PropertyEditorSupport; import java.time.Duration; import java.util.ArrayList; import java.util.Collections; @@ -675,6 +676,16 @@ public class ConfigurationPropertiesTests { load(IgnoreUnknownFieldsFalseConfiguration.class, "name=foo", "bar=baz"); } + @Test + public void loadWhenHasCustomPropertyEditorShouldBind() { + this.context.getBeanFactory().registerCustomEditor(Person.class, + PersonPropertyEditor.class); + load(PersonProperties.class, "test.person=boot,spring"); + PersonProperties bean = this.context.getBean(PersonProperties.class); + assertThat(bean.getPerson().firstName).isEqualTo("spring"); + assertThat(bean.getPerson().lastName).isEqualTo("boot"); + } + private AnnotationConfigApplicationContext load(Class configuration, String... inlinedProperties) { return load(new Class[] { configuration }, inlinedProperties); @@ -1556,6 +1567,16 @@ public class ConfigurationPropertiesTests { } } + static class PersonPropertyEditor extends PropertyEditorSupport { + + @Override + public void setAsText(String text) throws IllegalArgumentException { + String[] split = text.split(","); + setValue(new Person(split[1], split[0])); + } + + } + static class Person { private final String firstName; diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BinderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BinderTests.java index 00f99e2393..8900be87fd 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BinderTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/BinderTests.java @@ -16,6 +16,7 @@ package org.springframework.boot.context.properties.bind; +import java.beans.PropertyEditorSupport; import java.time.LocalDate; import java.util.ArrayList; import java.util.Collections; @@ -160,6 +161,16 @@ public class BinderTests { this.binder.bind("foo", Bindable.of(Integer.class)); } + @Test + public void bindToValueWithCustomPropertyEditorShouldReturnConvertedValue() { + this.binder = new Binder(this.sources, null, null, (registry) -> registry + .registerCustomEditor(JavaBean.class, new JavaBeanPropertyEditor())); + this.sources.add(new MockConfigurationPropertySource("foo", "123")); + BindResult result = this.binder.bind("foo", + Bindable.of(JavaBean.class)); + assertThat(result.get().getValue()).isEqualTo("123"); + } + @Test public void bindToValueShouldTriggerOnSuccess() { this.sources.add(new MockConfigurationPropertySource("foo", "1", "line1")); @@ -280,8 +291,8 @@ public class BinderTests { this.binder.bind("foo", target); } - @SuppressWarnings("rawtypes") @Test + @SuppressWarnings("rawtypes") public void bindToBeanWithUnresolvableGenerics() { MockConfigurationPropertySource source = new MockConfigurationPropertySource(); source.put("foo.bar", "hello"); @@ -373,4 +384,15 @@ public class BinderTests { } + public static class JavaBeanPropertyEditor extends PropertyEditorSupport { + + @Override + public void setAsText(String text) throws IllegalArgumentException { + JavaBean value = new JavaBean(); + value.setValue(text); + setValue(value); + } + + } + } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/JavaBeanBinderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/JavaBeanBinderTests.java index 509e8cd8b6..a4328d9350 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/JavaBeanBinderTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/JavaBeanBinderTests.java @@ -31,11 +31,11 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; -import org.springframework.boot.context.properties.bind.convert.Delimiter; import org.springframework.boot.context.properties.bind.handler.IgnoreErrorsBindHandler; import org.springframework.boot.context.properties.source.ConfigurationPropertyName; import org.springframework.boot.context.properties.source.ConfigurationPropertySource; import org.springframework.boot.context.properties.source.MockConfigurationPropertySource; +import org.springframework.boot.convert.Delimiter; import org.springframework.format.annotation.DateTimeFormat; import static org.assertj.core.api.Assertions.assertThat; diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/MapBinderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/MapBinderTests.java index b583fa1967..aa009fb7d3 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/MapBinderTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/MapBinderTests.java @@ -536,7 +536,7 @@ public class MapBinderTests { public void bindToMapWithCustomConverter() { DefaultConversionService conversionService = new DefaultConversionService(); conversionService.addConverter(new MapConverter()); - Binder binder = new Binder(this.sources, null, conversionService); + Binder binder = new Binder(this.sources, null, conversionService, null); MockConfigurationPropertySource source = new MockConfigurationPropertySource(); source.put("foo", "a,b"); this.sources.add(source); @@ -550,7 +550,7 @@ public class MapBinderTests { // gh-11892 DefaultConversionService conversionService = new DefaultConversionService(); conversionService.addConverter(new MapConverter()); - Binder binder = new Binder(this.sources, null, conversionService); + Binder binder = new Binder(this.sources, null, conversionService, null); MockConfigurationPropertySource source = new MockConfigurationPropertySource(); source.put("foo", "boom"); source.put("foo.a", "a"); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ResolvableTypeDescriptorTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ResolvableTypeDescriptorTests.java deleted file mode 100644 index fa8708bf51..0000000000 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/ResolvableTypeDescriptorTests.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * 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; - -import java.lang.annotation.Annotation; -import java.util.List; - -import org.junit.Test; - -import org.springframework.core.ResolvableType; -import org.springframework.core.annotation.AnnotationUtils; -import org.springframework.core.convert.TypeDescriptor; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link ResolvableTypeDescriptor}. - * - * @author Phillip Webb - */ -public class ResolvableTypeDescriptorTests { - - @Test - public void forBindableShouldIncludeType() { - ResolvableType type = ResolvableType.forClassWithGenerics(List.class, - String.class); - Bindable bindable = Bindable.of(type); - TypeDescriptor descriptor = ResolvableTypeDescriptor.forBindable(bindable); - assertThat(descriptor.getResolvableType()).isEqualTo(type); - } - - @Test - public void forBindableShouldIncludeAnnotations() { - Annotation annotation = AnnotationUtils.synthesizeAnnotation(Test.class); - Bindable bindable = Bindable.of(String.class).withAnnotations(annotation); - TypeDescriptor descriptor = ResolvableTypeDescriptor.forBindable(bindable); - assertThat(descriptor.getAnnotations()).containsExactly(annotation); - } - - @Test - public void forTypeShouldIncludeType() { - ResolvableType type = ResolvableType.forClassWithGenerics(List.class, - String.class); - TypeDescriptor descriptor = ResolvableTypeDescriptor.forType(type); - assertThat(descriptor.getResolvableType()).isEqualTo(type); - } - -} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/AbstractInetAddressTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/AbstractInetAddressTests.java deleted file mode 100644 index 517834f823..0000000000 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/AbstractInetAddressTests.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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.net.InetAddress; -import java.net.UnknownHostException; - -import org.junit.AssumptionViolatedException; - -/** - * Base class for {@link InetAddress} tests. - * - * @author Phillip Webb - */ -public abstract class AbstractInetAddressTests { - - public void assumeResolves(String host) { - try { - InetAddress.getByName(host); - } - catch (UnknownHostException ex) { - throw new AssumptionViolatedException("Host " + host + " not resolvable", ex); - } - } - -} 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 deleted file mode 100644 index fed61b8d67..0000000000 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/BinderConversionServiceTests.java +++ /dev/null @@ -1,207 +0,0 @@ -/* - * Copyright 2012-2018 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.io.InputStream; -import java.net.InetAddress; -import java.time.Duration; -import java.util.List; - -import org.junit.Before; -import org.junit.Test; - -import org.springframework.core.convert.ConversionFailedException; -import org.springframework.core.convert.ConversionService; -import org.springframework.core.convert.TypeDescriptor; -import org.springframework.core.io.Resource; -import org.springframework.util.ReflectionUtils; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - -/** - * Tests for {@link BinderConversionService}. - * - * @author Phillip Webb - */ -public class BinderConversionServiceTests { - - private ConversionService delegate; - - private BinderConversionService service; - - @Before - public void setup() { - this.delegate = mock(ConversionService.class); - this.service = new BinderConversionService(this.delegate); - } - - @Test - public void createConversionServiceShouldAcceptNullConversionService() { - BinderConversionService service = new BinderConversionService(null); - assertThat(service.canConvert(String.class, TestEnum.class)).isTrue(); - assertThat(service.canConvert(TypeDescriptor.valueOf(String.class), - TypeDescriptor.valueOf(TestEnum.class))).isTrue(); - assertThat(service.convert("ONE", TestEnum.class)).isEqualTo(TestEnum.ONE); - assertThat(service.convert("ONE", TypeDescriptor.valueOf(String.class), - TypeDescriptor.valueOf(TestEnum.class))).isEqualTo(TestEnum.ONE); - } - - @Test - public void canConvertShouldDelegateToConversionService() { - Class from = String.class; - Class to = InputStream.class; - given(this.delegate.canConvert(from, to)).willReturn(true); - assertThat(this.service.canConvert(from, to)).isEqualTo(true); - verify(this.delegate).canConvert(from, to); - } - - @Test - public void canConvertTypeDescriptorShouldDelegateToConversionService() { - TypeDescriptor from = TypeDescriptor.valueOf(String.class); - TypeDescriptor to = TypeDescriptor.valueOf(InputStream.class); - given(this.delegate.canConvert(from, to)).willReturn(true); - assertThat(this.service.canConvert(from, to)).isEqualTo(true); - verify(this.delegate).canConvert(from, to); - } - - @Test - public void convertShouldDelegateToConversionService() { - String from = "foo"; - InputStream to = mock(InputStream.class); - given(this.delegate.convert(from, InputStream.class)).willReturn(to); - assertThat(this.service.convert(from, InputStream.class)).isEqualTo(to); - verify(this.delegate).convert(from, InputStream.class); - } - - @Test - public void convertTargetTypeShouldDelegateToConversionService() { - String from = "foo"; - InputStream to = mock(InputStream.class); - TypeDescriptor fromType = TypeDescriptor.valueOf(String.class); - TypeDescriptor toType = TypeDescriptor.valueOf(InputStream.class); - given(this.delegate.convert(from, fromType, toType)).willReturn(to); - assertThat(this.service.convert(from, fromType, toType)).isEqualTo(to); - verify(this.delegate).convert(from, fromType, toType); - } - - @Test - public void convertShouldSwallowDelegateConversionFailedException() { - given(this.delegate.convert("one", TestEnum.class)) - .willThrow(new ConversionFailedException(null, null, null, null)); - assertThat(this.service.convert("one", TestEnum.class)).isEqualTo(TestEnum.ONE); - verify(this.delegate).convert("one", TestEnum.class); - } - - @Test - public void conversionServiceShouldSupportEnums() { - this.service = new BinderConversionService(null); - assertThat(this.service.canConvert(String.class, TestEnum.class)).isTrue(); - assertThat(this.service.convert("one", TestEnum.class)).isEqualTo(TestEnum.ONE); - assertThat(this.service.convert("t-w-o", TestEnum.class)).isEqualTo(TestEnum.TWO); - } - - @Test - public void conversionServiceShouldSupportStringToCharArray() { - this.service = new BinderConversionService(null); - assertThat(this.service.canConvert(String.class, char[].class)).isTrue(); - assertThat(this.service.convert("test", char[].class)).containsExactly('t', 'e', - 's', 't'); - } - - @Test - public void conversionServiceShouldSupportStringToInetAddress() { - this.service = new BinderConversionService(null); - assertThat(this.service.canConvert(String.class, InetAddress.class)).isTrue(); - } - - @Test - public void conversionServiceShouldSupportInetAddressToString() { - this.service = new BinderConversionService(null); - assertThat(this.service.canConvert(InetAddress.class, String.class)).isTrue(); - } - - @Test - public void conversionServiceShouldSupportStringToResource() { - this.service = new BinderConversionService(null); - Resource resource = this.service.convert( - "org/springframework/boot/context/properties/bind/convert/resource.txt", - Resource.class); - assertThat(resource).isNotNull(); - } - - @Test - public void conversionServiceShouldSupportStringToClass() { - this.service = new BinderConversionService(null); - Class converted = this.service.convert(InputStream.class.getName(), - Class.class); - assertThat(converted).isEqualTo(InputStream.class); - } - - @Test - public void conversionServiceShouldSupportStringToDuration() { - this.service = new BinderConversionService(null); - Duration converted = this.service.convert("10s", Duration.class); - assertThat(converted).isEqualTo(Duration.ofSeconds(10)); - } - - @Test - public void conversionServiceShouldSupportIntegerToDuration() { - this.service = new BinderConversionService(null); - Duration converted = this.service.convert(10, Duration.class); - assertThat(converted).isEqualTo(Duration.ofMillis(10)); - } - - @Test - @SuppressWarnings("unchecked") - public void conversionServiceShouldSupportBarDelimitedStrings() { - this.service = new BinderConversionService(null); - List converted = (List) this.service.convert("ONE|ONE|TWO", - TypeDescriptor.valueOf(String.class), TypeDescriptor.nested( - ReflectionUtils.findField(DelimitedValues.class, "bar"), 0)); - assertThat(converted).containsExactly(TestEnum.ONE, TestEnum.ONE, TestEnum.TWO); - } - - @Test - @SuppressWarnings("unchecked") - public void conversionServiceShouldSupportNoneDelimitedStrings() { - this.service = new BinderConversionService(null); - List converted = (List) this.service.convert("a,b,c", - TypeDescriptor.valueOf(String.class), TypeDescriptor.nested( - ReflectionUtils.findField(DelimitedValues.class, "none"), 0)); - assertThat(converted).containsExactly("a,b,c"); - } - - static class DelimitedValues { - - @Delimiter("|") - List bar; - - @Delimiter(Delimiter.NONE) - List none; - - } - - enum TestEnum { - - ONE, TWO - - } - -} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/InetAddressToStringConverterTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/InetAddressToStringConverterTests.java deleted file mode 100644 index 3455ea3a70..0000000000 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/InetAddressToStringConverterTests.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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.net.InetAddress; - -import org.junit.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link InetAddressToStringConverter}. - * - * @author Phillip Webb - */ -public class InetAddressToStringConverterTests extends AbstractInetAddressTests { - - private InetAddressToStringConverter converter = new InetAddressToStringConverter(); - - @Test - public void convertShouldConvertToHostAddress() throws Exception { - assumeResolves("example.com"); - InetAddress address = InetAddress.getByName("example.com"); - String converted = this.converter.convert(address); - assertThat(converted).isEqualTo(address.getHostAddress()); - } - -} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/PropertyEditorConverterTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/PropertyEditorConverterTests.java deleted file mode 100644 index e7c27e3352..0000000000 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/PropertyEditorConverterTests.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * 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.util.Collection; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; -import java.util.SortedMap; - -import org.junit.Test; - -import org.springframework.beans.SimpleTypeConverter; -import org.springframework.core.convert.TypeDescriptor; -import org.springframework.core.io.Resource; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link PropertyEditorConverter}. - * - * @author Phillip Webb - */ -public class PropertyEditorConverterTests { - - private final PropertyEditorConverter converter = new PropertyEditorConverter(); - - @Test - public void matchesShouldLimitToPropertyEditor() { - String converted = new SimpleTypeConverter().convertIfNecessary(123, - String.class); - assertThat(converted).isEqualTo("123"); - // Even though the SimpleTypeConverter can convert, we should limit to just - // PropertyEditors not implicit support - assertThat(this.converter.matches(TypeDescriptor.valueOf(Integer.class), - TypeDescriptor.valueOf(String.class))).isFalse(); - } - - @Test - public void convertShouldSupportConventionBasedEditors() { - String source = "org/springframework/boot/context/properties/bind/convert/resource.txt"; - TypeDescriptor sourceType = TypeDescriptor.forObject(source); - TypeDescriptor targetType = TypeDescriptor.valueOf(Resource.class); - assertThat(this.converter.matches(sourceType, targetType)).isTrue(); - Object converted = this.converter.convert(source, sourceType, targetType); - assertThat(converted).isNotNull().isInstanceOf(Resource.class); - assertThat(converted.toString()).endsWith("resource.txt]"); - } - - @Test - public void convertShouldSupportDefaultEditors() { - String source = "en_UK"; - TypeDescriptor sourceType = TypeDescriptor.forObject(source); - TypeDescriptor targetType = TypeDescriptor.valueOf(Locale.class); - assertThat(this.converter.matches(sourceType, targetType)).isTrue(); - Object converted = this.converter.convert(source, sourceType, targetType); - assertThat(converted).isNotNull().isInstanceOf(Locale.class); - assertThat(converted.toString()).endsWith("en_UK"); - } - - @Test - public void matchShouldNotMatchCollection() { - TypeDescriptor sourceType = TypeDescriptor.valueOf(String.class); - assertThat(this.converter.matches(sourceType, - TypeDescriptor.valueOf(Collection.class))).isFalse(); - assertThat(this.converter.matches(sourceType, TypeDescriptor.valueOf(List.class))) - .isFalse(); - assertThat(this.converter.matches(sourceType, TypeDescriptor.valueOf(Set.class))) - .isFalse(); - } - - @Test - public void matchShouldNotMatchMap() { - TypeDescriptor sourceType = TypeDescriptor.valueOf(String.class); - assertThat(this.converter.matches(sourceType, TypeDescriptor.valueOf(Map.class))) - .isFalse(); - assertThat(this.converter.matches(sourceType, - TypeDescriptor.valueOf(SortedMap.class))).isFalse(); - } - -} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/StringToCharArrayConverterTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/StringToCharArrayConverterTests.java deleted file mode 100644 index cd7b5dbf97..0000000000 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/StringToCharArrayConverterTests.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * 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 org.junit.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link StringToCharArrayConverter}. - * - * @author Phillip Webb - */ -public class StringToCharArrayConverterTests { - - private StringToCharArrayConverter converter = new StringToCharArrayConverter(); - - @Test - public void convertShouldConvertSource() { - char[] converted = this.converter.convert("test"); - assertThat(converted).containsExactly('t', 'e', 's', 't'); - } - -} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/StringToEnumConverterFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/StringToEnumConverterFactoryTests.java deleted file mode 100644 index a3f8ad2d4d..0000000000 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/StringToEnumConverterFactoryTests.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * 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 org.junit.Test; - -import org.springframework.core.convert.converter.Converter; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link StringToEnumConverterFactory}. - * - * @author Phillip Webb - */ -public class StringToEnumConverterFactoryTests { - - private StringToEnumConverterFactory factory = new StringToEnumConverterFactory(); - - @Test - public void getConverterShouldReturnConverter() { - Converter converter = this.factory.getConverter(TestEnum.class); - assertThat(converter).isNotNull(); - } - - @Test - @SuppressWarnings({ "unchecked", "rawtypes" }) - public void getConverterWhenEnumSubclassShouldReturnConverter() { - Converter converter = this.factory - .getConverter((Class) TestSubclassEnum.ONE.getClass()); - assertThat(converter).isNotNull(); - } - - @Test - public void convertWhenExactMatchShouldConvertValue() { - Converter converter = this.factory.getConverter(TestEnum.class); - assertThat(converter.convert("")).isNull(); - assertThat(converter.convert("ONE")).isEqualTo(TestEnum.ONE); - assertThat(converter.convert("TWO")).isEqualTo(TestEnum.TWO); - assertThat(converter.convert("THREE_AND_FOUR")) - .isEqualTo(TestEnum.THREE_AND_FOUR); - } - - @Test - public void convertWhenFuzzyMatchShouldConvertValue() { - Converter converter = this.factory.getConverter(TestEnum.class); - assertThat(converter.convert("")).isNull(); - assertThat(converter.convert("one")).isEqualTo(TestEnum.ONE); - assertThat(converter.convert("tWo")).isEqualTo(TestEnum.TWO); - assertThat(converter.convert("three_and_four")) - .isEqualTo(TestEnum.THREE_AND_FOUR); - assertThat(converter.convert("threeandfour")).isEqualTo(TestEnum.THREE_AND_FOUR); - assertThat(converter.convert("three-and-four")) - .isEqualTo(TestEnum.THREE_AND_FOUR); - assertThat(converter.convert("threeAndFour")).isEqualTo(TestEnum.THREE_AND_FOUR); - } - - enum TestEnum { - - ONE, TWO, THREE_AND_FOUR - - } - - enum TestSubclassEnum { - - ONE { - - @Override - public String toString() { - return "foo"; - } - - } - - } - -} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/StringToInetAddressConverterTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/StringToInetAddressConverterTests.java deleted file mode 100644 index b4c3727b5c..0000000000 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/StringToInetAddressConverterTests.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * 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.net.InetAddress; - -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link StringToInetAddressConverter}. - * - * @author Phillip Webb - */ -public class StringToInetAddressConverterTests extends AbstractInetAddressTests { - - @Rule - public ExpectedException thrown = ExpectedException.none(); - - private StringToInetAddressConverter converter = new StringToInetAddressConverter(); - - @Test - public void convertWhenHostDoesNotExistShouldThrowException() { - this.thrown.expect(IllegalStateException.class); - this.thrown.expectMessage("Unknown host"); - this.converter.convert("ireallydontexist.example.com"); - } - - @Test - public void convertWhenHostExistsShouldConvert() { - assumeResolves("example.com"); - InetAddress converted = this.converter.convert("example.com"); - assertThat(converted.toString()).startsWith("example.com"); - } - -} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/ArrayToDelimitedStringConverterTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/ArrayToDelimitedStringConverterTests.java new file mode 100644 index 0000000000..75387c5f67 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/ArrayToDelimitedStringConverterTests.java @@ -0,0 +1,116 @@ +/* + * Copyright 2012-2018 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.convert; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.format.support.FormattingConversionService; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ArrayToDelimitedStringConverter}. + * + * @author Phillip Webb + */ +@RunWith(Parameterized.class) +public class ArrayToDelimitedStringConverterTests { + + private final ConversionService conversionService; + + public ArrayToDelimitedStringConverterTests(String name, + ConversionService conversionService) { + this.conversionService = conversionService; + } + + @Test + public void convertListToStringShouldConvert() { + String[] list = { "a", "b", "c" }; + String converted = this.conversionService.convert(list, String.class); + assertThat(converted).isEqualTo("a,b,c"); + } + + @Test + public void convertWhenHasDelimiterNoneShouldConvert() { + Data data = new Data(); + data.none = new String[] { "1", "2", "3" }; + String converted = (String) this.conversionService.convert(data.none, + TypeDescriptor.nested(ReflectionUtils.findField(Data.class, "none"), 0), + TypeDescriptor.valueOf(String.class)); + assertThat(converted).isEqualTo("123"); + } + + @Test + public void convertWhenHasDelimiterDashShouldConvert() { + Data data = new Data(); + data.dash = new String[] { "1", "2", "3" }; + String converted = (String) this.conversionService.convert(data.dash, + TypeDescriptor.nested(ReflectionUtils.findField(Data.class, "dash"), 0), + TypeDescriptor.valueOf(String.class)); + assertThat(converted).isEqualTo("1-2-3"); + } + + @Test + public void convertShouldConvertElements() { + if (this.conversionService instanceof ApplicationConversionService) { + Data data = new Data(); + data.type = new int[] { 1, 2, 3 }; + String converted = (String) this.conversionService.convert( + data.type, TypeDescriptor + .nested(ReflectionUtils.findField(Data.class, "type"), 0), + TypeDescriptor.valueOf(String.class)); + assertThat(converted).isEqualTo("1.2.3"); + } + } + + @Test + public void convertShouldConvertNull() { + String[] list = null; + String converted = this.conversionService.convert(list, String.class); + assertThat(converted).isNull(); + } + + @Parameters(name = "{0}") + public static Iterable conversionServices() { + return new ConversionServiceParameters( + ArrayToDelimitedStringConverterTests::addConverter); + } + + private static void addConverter(FormattingConversionService service) { + service.addConverter(new ArrayToDelimitedStringConverter(service)); + } + + static class Data { + + @Delimiter(Delimiter.NONE) + String[] none; + + @Delimiter("-") + String[] dash; + + @Delimiter(".") + int[] type; + + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/CharArrayFormatterTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/CharArrayFormatterTests.java new file mode 100644 index 0000000000..7f5c6880f8 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/CharArrayFormatterTests.java @@ -0,0 +1,61 @@ +/* + * Copyright 2012-2018 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.convert; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +import org.springframework.core.convert.ConversionService; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link CharArrayFormatter}. + * + * @author Phillip Webb + */ +@RunWith(Parameterized.class) +public class CharArrayFormatterTests { + + private final ConversionService conversionService; + + public CharArrayFormatterTests(String name, ConversionService conversionService) { + this.conversionService = conversionService; + } + + @Test + public void convertFromCharArrayToStringShouldConvert() { + char[] source = { 'b', 'o', 'o', 't' }; + String converted = this.conversionService.convert(source, String.class); + assertThat(converted).isEqualTo("boot"); + } + + @Test + public void convertFromStringToCharArrayShouldConvert() { + String source = "boot"; + char[] converted = this.conversionService.convert(source, char[].class); + assertThat(converted).containsExactly('b', 'o', 'o', 't'); + } + + @Parameters(name = "{0}") + public static Iterable conversionServices() { + return new ConversionServiceParameters(new CharArrayFormatter()); + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/CollectionToDelimitedStringConverterTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/CollectionToDelimitedStringConverterTests.java new file mode 100644 index 0000000000..31da094017 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/CollectionToDelimitedStringConverterTests.java @@ -0,0 +1,119 @@ +/* + * Copyright 2012-2018 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.convert; + +import java.util.Arrays; +import java.util.List; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.format.support.FormattingConversionService; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link CollectionToDelimitedStringConverter}. + * + * @author Phillip Webb + */ +@RunWith(Parameterized.class) +public class CollectionToDelimitedStringConverterTests { + + private final ConversionService conversionService; + + public CollectionToDelimitedStringConverterTests(String name, + ConversionService conversionService) { + this.conversionService = conversionService; + } + + @Test + public void convertListToStringShouldConvert() { + List list = Arrays.asList("a", "b", "c"); + String converted = this.conversionService.convert(list, String.class); + assertThat(converted).isEqualTo("a,b,c"); + } + + @Test + public void convertWhenHasDelimiterNoneShouldConvert() { + Data data = new Data(); + data.none = Arrays.asList("1", "2", "3"); + String converted = (String) this.conversionService.convert(data.none, + TypeDescriptor.nested(ReflectionUtils.findField(Data.class, "none"), 0), + TypeDescriptor.valueOf(String.class)); + assertThat(converted).isEqualTo("123"); + } + + @Test + public void convertWhenHasDelimiterDashShouldConvert() { + Data data = new Data(); + data.dash = Arrays.asList("1", "2", "3"); + String converted = (String) this.conversionService.convert(data.dash, + TypeDescriptor.nested(ReflectionUtils.findField(Data.class, "dash"), 0), + TypeDescriptor.valueOf(String.class)); + assertThat(converted).isEqualTo("1-2-3"); + } + + @Test + public void convertShouldConvertElements() { + if (this.conversionService instanceof ApplicationConversionService) { + Data data = new Data(); + data.type = Arrays.asList(1, 2, 3); + String converted = (String) this.conversionService.convert( + data.type, TypeDescriptor + .nested(ReflectionUtils.findField(Data.class, "type"), 0), + TypeDescriptor.valueOf(String.class)); + assertThat(converted).isEqualTo("1.2.3"); + } + } + + @Test + public void convertShouldConvertNull() { + List list = null; + String converted = this.conversionService.convert(list, String.class); + assertThat(converted).isNull(); + } + + @Parameters(name = "{0}") + public static Iterable conversionServices() { + return new ConversionServiceParameters( + CollectionToDelimitedStringConverterTests::addConverter); + } + + private static void addConverter(FormattingConversionService service) { + service.addConverter(new CollectionToDelimitedStringConverter(service)); + } + + static class Data { + + @Delimiter(Delimiter.NONE) + List none; + + @Delimiter("-") + List dash; + + @Delimiter(".") + List type; + + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/ConversionServiceParameters.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/ConversionServiceParameters.java new file mode 100644 index 0000000000..944bffe0de --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/ConversionServiceParameters.java @@ -0,0 +1,76 @@ +/* + * Copyright 2012-2018 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.convert; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.function.Consumer; + +import org.junit.runners.Parameterized.Parameters; + +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.converter.ConverterFactory; +import org.springframework.core.convert.converter.GenericConverter; +import org.springframework.format.Formatter; +import org.springframework.format.support.FormattingConversionService; + +/** + * Factory to create {@link ConversionService ConversionServices} for test + * {@link Parameters}. + * + * @author Phillip Webb + */ +public class ConversionServiceParameters implements Iterable { + + private final List parameters; + + public ConversionServiceParameters(Formatter formatter) { + this((Consumer) ( + conversionService) -> conversionService.addFormatter(formatter)); + } + + public ConversionServiceParameters(ConverterFactory converterFactory) { + this((Consumer) ( + conversionService) -> conversionService + .addConverterFactory(converterFactory)); + } + + public ConversionServiceParameters(GenericConverter converter) { + this((Consumer) ( + conversionService) -> conversionService.addConverter(converter)); + } + + public ConversionServiceParameters( + Consumer initializer) { + FormattingConversionService withoutDefaults = new FormattingConversionService(); + initializer.accept(withoutDefaults); + List parameters = new ArrayList<>(); + parameters.add( + new Object[] { "without defaults conversion service", withoutDefaults }); + parameters.add(new Object[] { "application conversion service", + new ApplicationConversionService() }); + this.parameters = Collections.unmodifiableList(parameters); + } + + @Override + public Iterator iterator() { + return this.parameters.iterator(); + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/DelimitedStringToArrayConverterTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/DelimitedStringToArrayConverterTests.java new file mode 100644 index 0000000000..5617cc10d6 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/DelimitedStringToArrayConverterTests.java @@ -0,0 +1,144 @@ +/* + * Copyright 2012-2018 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.convert; + +import java.util.LinkedList; +import java.util.List; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.format.support.FormattingConversionService; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link DelimitedStringToArrayConverter}. + * + * @author Phillip Webb + */ +@RunWith(Parameterized.class) +public class DelimitedStringToArrayConverterTests { + + private final ConversionService conversionService; + + public DelimitedStringToArrayConverterTests(String name, + ConversionService conversionService) { + this.conversionService = conversionService; + } + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void canConvertFromStringToArrayShouldReturnTrue() { + assertThat(this.conversionService.canConvert(String.class, String[].class)) + .isTrue(); + } + + @Test + public void matchesWhenTargetIsNotAnnotatedShouldReturnTrue() { + TypeDescriptor sourceType = TypeDescriptor.valueOf(String.class); + TypeDescriptor targetType = TypeDescriptor + .nested(ReflectionUtils.findField(Values.class, "noAnnotation"), 0); + assertThat(new DelimitedStringToArrayConverter(this.conversionService) + .matches(sourceType, targetType)).isTrue(); + } + + @Test + public void matchesWhenHasAnnotationAndConvertibleElementTypeShouldReturnTrue() { + if (this.conversionService instanceof ApplicationConversionService) { + TypeDescriptor sourceType = TypeDescriptor.valueOf(String.class); + TypeDescriptor targetType = TypeDescriptor.nested( + ReflectionUtils.findField(Values.class, "convertibleElementType"), 0); + assertThat(new DelimitedStringToArrayConverter(this.conversionService) + .matches(sourceType, targetType)).isTrue(); + } + } + + @Test + public void matchesWhenHasAnnotationAndNonConvertibleElementTypeShouldReturnFalse() { + TypeDescriptor sourceType = TypeDescriptor.valueOf(String.class); + TypeDescriptor targetType = TypeDescriptor.nested( + ReflectionUtils.findField(Values.class, "nonConvertibleElementType"), 0); + assertThat(new DelimitedStringToArrayConverter(this.conversionService) + .matches(sourceType, targetType)).isFalse(); + } + + @Test + public void convertWhenHasConvertibleElementTypeShouldReturnConvertedType() { + if (this.conversionService instanceof ApplicationConversionService) { + TypeDescriptor sourceType = TypeDescriptor.valueOf(String.class); + TypeDescriptor targetType = TypeDescriptor.nested( + ReflectionUtils.findField(Values.class, "convertibleElementType"), 0); + Integer[] converted = (Integer[]) this.conversionService + .convert(" 1 | 2| 3 ", sourceType, targetType); + assertThat(converted).containsExactly(1, 2, 3); + } + } + + @Test + public void convertWhenHasDelimiterOfNoneShouldReturnTrimmedStringElement() { + TypeDescriptor sourceType = TypeDescriptor.valueOf(String.class); + TypeDescriptor targetType = TypeDescriptor + .nested(ReflectionUtils.findField(Values.class, "delimiterNone"), 0); + String[] converted = (String[]) this.conversionService.convert("a,b,c", + sourceType, targetType); + assertThat(converted).containsExactly("a,b,c"); + } + + @Parameters(name = "{0}") + public static Iterable conversionServices() { + return new ConversionServiceParameters( + DelimitedStringToArrayConverterTests::addConverter); + } + + private static void addConverter(FormattingConversionService service) { + service.addConverter(new DelimitedStringToArrayConverter(service)); + } + + static class Values { + + List noAnnotation; + + @Delimiter("|") + Integer[] convertibleElementType; + + @Delimiter("|") + NonConvertible[] nonConvertibleElementType; + + @Delimiter(Delimiter.NONE) + String[] delimiterNone; + + } + + static class NonConvertible { + + } + + static class MyCustomList extends LinkedList { + + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/DelimitedStringToCollectionConverterTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/DelimitedStringToCollectionConverterTests.java similarity index 58% rename from spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/DelimitedStringToCollectionConverterTests.java rename to spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/DelimitedStringToCollectionConverterTests.java index d7f093665c..4ea3bbd168 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/DelimitedStringToCollectionConverterTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/DelimitedStringToCollectionConverterTests.java @@ -14,21 +14,22 @@ * limitations under the License. */ -package org.springframework.boot.context.properties.bind.convert; +package org.springframework.boot.convert; import java.util.Collection; import java.util.LinkedList; import java.util.List; -import java.util.Set; -import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.TypeDescriptor; -import org.springframework.core.convert.converter.GenericConverter.ConvertiblePair; -import org.springframework.format.support.DefaultFormattingConversionService; +import org.springframework.format.support.FormattingConversionService; import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -38,45 +39,32 @@ import static org.assertj.core.api.Assertions.assertThat; * * @author Phillip Webb */ +@RunWith(Parameterized.class) public class DelimitedStringToCollectionConverterTests { - private DefaultFormattingConversionService service; + private final ConversionService conversionService; - private DelimitedStringToCollectionConverter converter; + public DelimitedStringToCollectionConverterTests(String name, + ConversionService conversionService) { + this.conversionService = conversionService; + } @Rule public ExpectedException thrown = ExpectedException.none(); - @Before - public void setup() { - this.service = new DefaultFormattingConversionService(null, false); - this.converter = new DelimitedStringToCollectionConverter(this.service); - this.service.addConverter(this.converter); - DefaultFormattingConversionService.addDefaultFormatters(this.service); + @Test + public void canConvertFromStringToCollectionShouldReturnTrue() { + assertThat(this.conversionService.canConvert(String.class, Collection.class)) + .isTrue(); } @Test - public void createWhenConversionServiceIsNullShouldThrowException() { - this.thrown.expect(IllegalArgumentException.class); - this.thrown.expectMessage("ConversionService must not be null"); - new DelimitedStringToCollectionConverter(null); - } - - @Test - public void getConvertiblePairShouldReturnStringCollectionPair() { - Set types = this.converter.getConvertibleTypes(); - assertThat(types).hasSize(1); - ConvertiblePair pair = types.iterator().next(); - assertThat(pair.getSourceType()).isEqualTo(String.class); - assertThat(pair.getTargetType()).isEqualTo(Collection.class); - } - - @Test - public void matchesWhenTargetIsNotAnnotatedShouldReturnFalse() { + public void matchesWhenTargetIsNotAnnotatedShouldReturnTrue() { TypeDescriptor sourceType = TypeDescriptor.valueOf(String.class); TypeDescriptor targetType = TypeDescriptor .nested(ReflectionUtils.findField(Values.class, "noAnnotation"), 0); - assertThat(this.converter.matches(sourceType, targetType)).isFalse(); + assertThat(new DelimitedStringToCollectionConverter(this.conversionService) + .matches(sourceType, targetType)).isTrue(); } @Test @@ -84,15 +72,19 @@ public class DelimitedStringToCollectionConverterTests { TypeDescriptor sourceType = TypeDescriptor.valueOf(String.class); TypeDescriptor targetType = TypeDescriptor .nested(ReflectionUtils.findField(Values.class, "noElementType"), 0); - assertThat(this.converter.matches(sourceType, targetType)).isTrue(); + assertThat(new DelimitedStringToCollectionConverter(this.conversionService) + .matches(sourceType, targetType)).isTrue(); } @Test public void matchesWhenHasAnnotationAndConvertibleElementTypeShouldReturnTrue() { - TypeDescriptor sourceType = TypeDescriptor.valueOf(String.class); - TypeDescriptor targetType = TypeDescriptor.nested( - ReflectionUtils.findField(Values.class, "convertibleElementType"), 0); - assertThat(this.converter.matches(sourceType, targetType)).isTrue(); + if (this.conversionService instanceof ApplicationConversionService) { + TypeDescriptor sourceType = TypeDescriptor.valueOf(String.class); + TypeDescriptor targetType = TypeDescriptor.nested( + ReflectionUtils.findField(Values.class, "convertibleElementType"), 0); + assertThat(new DelimitedStringToCollectionConverter(this.conversionService) + .matches(sourceType, targetType)).isTrue(); + } } @Test @@ -100,7 +92,8 @@ public class DelimitedStringToCollectionConverterTests { TypeDescriptor sourceType = TypeDescriptor.valueOf(String.class); TypeDescriptor targetType = TypeDescriptor.nested( ReflectionUtils.findField(Values.class, "nonConvertibleElementType"), 0); - assertThat(this.converter.matches(sourceType, targetType)).isFalse(); + assertThat(new DelimitedStringToCollectionConverter(this.conversionService) + .matches(sourceType, targetType)).isFalse(); } @Test @@ -109,20 +102,22 @@ public class DelimitedStringToCollectionConverterTests { TypeDescriptor sourceType = TypeDescriptor.valueOf(String.class); TypeDescriptor targetType = TypeDescriptor .nested(ReflectionUtils.findField(Values.class, "noElementType"), 0); - List converted = (List) this.converter.convert(" a | b| c ", - sourceType, targetType); + Collection converted = (Collection) this.conversionService + .convert(" a | b| c ", sourceType, targetType); assertThat(converted).containsExactly("a", "b", "c"); } @Test @SuppressWarnings("unchecked") public void convertWhenHasConvertibleElementTypeShouldReturnConvertedType() { - TypeDescriptor sourceType = TypeDescriptor.valueOf(String.class); - TypeDescriptor targetType = TypeDescriptor.nested( - ReflectionUtils.findField(Values.class, "convertibleElementType"), 0); - List converted = (List) this.converter.convert(" 1 | 2| 3 ", - sourceType, targetType); - assertThat(converted).containsExactly(1, 2, 3); + if (this.conversionService instanceof ApplicationConversionService) { + TypeDescriptor sourceType = TypeDescriptor.valueOf(String.class); + TypeDescriptor targetType = TypeDescriptor.nested( + ReflectionUtils.findField(Values.class, "convertibleElementType"), 0); + List converted = (List) this.conversionService + .convert(" 1 | 2| 3 ", sourceType, targetType); + assertThat(converted).containsExactly(1, 2, 3); + } } @Test @@ -131,7 +126,7 @@ public class DelimitedStringToCollectionConverterTests { TypeDescriptor sourceType = TypeDescriptor.valueOf(String.class); TypeDescriptor targetType = TypeDescriptor .nested(ReflectionUtils.findField(Values.class, "delimiterNone"), 0); - List converted = (List) this.converter.convert("a,b,c", + List converted = (List) this.conversionService.convert("a,b,c", sourceType, targetType); assertThat(converted).containsExactly("a,b,c"); } @@ -141,10 +136,20 @@ public class DelimitedStringToCollectionConverterTests { TypeDescriptor sourceType = TypeDescriptor.valueOf(String.class); TypeDescriptor targetType = TypeDescriptor .nested(ReflectionUtils.findField(Values.class, "specificType"), 0); - Object converted = this.converter.convert("a*b", sourceType, targetType); + Object converted = this.conversionService.convert("a*b", sourceType, targetType); assertThat(converted).isInstanceOf(MyCustomList.class); } + @Parameters(name = "{0}") + public static Iterable conversionServices() { + return new ConversionServiceParameters( + DelimitedStringToCollectionConverterTests::addConverter); + } + + private static void addConverter(FormattingConversionService service) { + service.addConverter(new DelimitedStringToCollectionConverter(service)); + } + static class Values { List noAnnotation; diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/DurationStyleTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/DurationStyleTests.java new file mode 100644 index 0000000000..3d6aeb1163 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/DurationStyleTests.java @@ -0,0 +1,281 @@ +/* + * Copyright 2012-2018 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.convert; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.fail; + +/** + * Tests for {@link DurationStyle}. + * + * @author Phillip Webb + */ +public class DurationStyleTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void detectAndParseWhenValueIsNullShouldThrowException() { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Value must not be null"); + DurationStyle.detectAndParse(null); + } + + @Test + public void detectAndParseWhenIso8601ShouldReturnDuration() { + assertThat(DurationStyle.detectAndParse("PT20.345S")) + .isEqualTo(Duration.parse("PT20.345S")); + assertThat(DurationStyle.detectAndParse("PT15M")) + .isEqualTo(Duration.parse("PT15M")); + assertThat(DurationStyle.detectAndParse("+PT15M")) + .isEqualTo(Duration.parse("PT15M")); + assertThat(DurationStyle.detectAndParse("PT10H")) + .isEqualTo(Duration.parse("PT10H")); + assertThat(DurationStyle.detectAndParse("P2D")).isEqualTo(Duration.parse("P2D")); + assertThat(DurationStyle.detectAndParse("P2DT3H4M")) + .isEqualTo(Duration.parse("P2DT3H4M")); + assertThat(DurationStyle.detectAndParse("-PT6H3M")) + .isEqualTo(Duration.parse("-PT6H3M")); + assertThat(DurationStyle.detectAndParse("-PT-6H+3M")) + .isEqualTo(Duration.parse("-PT-6H+3M")); + } + + @Test + public void detectAndParseWhenSimpleNanosShouldReturnDuration() { + assertThat(DurationStyle.detectAndParse("10ns")).isEqualTo(Duration.ofNanos(10)); + assertThat(DurationStyle.detectAndParse("10NS")).isEqualTo(Duration.ofNanos(10)); + assertThat(DurationStyle.detectAndParse("+10ns")).isEqualTo(Duration.ofNanos(10)); + assertThat(DurationStyle.detectAndParse("-10ns")) + .isEqualTo(Duration.ofNanos(-10)); + } + + @Test + public void detectAndParseWhenSimpleMillisShouldReturnDuration() { + assertThat(DurationStyle.detectAndParse("10ms")).isEqualTo(Duration.ofMillis(10)); + assertThat(DurationStyle.detectAndParse("10MS")).isEqualTo(Duration.ofMillis(10)); + assertThat(DurationStyle.detectAndParse("+10ms")) + .isEqualTo(Duration.ofMillis(10)); + assertThat(DurationStyle.detectAndParse("-10ms")) + .isEqualTo(Duration.ofMillis(-10)); + } + + @Test + public void detectAndParseWhenSimpleSecondsShouldReturnDuration() { + assertThat(DurationStyle.detectAndParse("10s")).isEqualTo(Duration.ofSeconds(10)); + assertThat(DurationStyle.detectAndParse("10S")).isEqualTo(Duration.ofSeconds(10)); + assertThat(DurationStyle.detectAndParse("+10s")) + .isEqualTo(Duration.ofSeconds(10)); + assertThat(DurationStyle.detectAndParse("-10s")) + .isEqualTo(Duration.ofSeconds(-10)); + } + + @Test + public void detectAndParseWhenSimpleMinutesShouldReturnDuration() { + assertThat(DurationStyle.detectAndParse("10m")).isEqualTo(Duration.ofMinutes(10)); + assertThat(DurationStyle.detectAndParse("10M")).isEqualTo(Duration.ofMinutes(10)); + assertThat(DurationStyle.detectAndParse("+10m")) + .isEqualTo(Duration.ofMinutes(10)); + assertThat(DurationStyle.detectAndParse("-10m")) + .isEqualTo(Duration.ofMinutes(-10)); + } + + @Test + public void detectAndParseWhenSimpleHoursShouldReturnDuration() { + assertThat(DurationStyle.detectAndParse("10h")).isEqualTo(Duration.ofHours(10)); + assertThat(DurationStyle.detectAndParse("10H")).isEqualTo(Duration.ofHours(10)); + assertThat(DurationStyle.detectAndParse("+10h")).isEqualTo(Duration.ofHours(10)); + assertThat(DurationStyle.detectAndParse("-10h")).isEqualTo(Duration.ofHours(-10)); + } + + @Test + public void detectAndParseWhenSimpleDaysShouldReturnDuration() { + assertThat(DurationStyle.detectAndParse("10d")).isEqualTo(Duration.ofDays(10)); + assertThat(DurationStyle.detectAndParse("10D")).isEqualTo(Duration.ofDays(10)); + assertThat(DurationStyle.detectAndParse("+10d")).isEqualTo(Duration.ofDays(10)); + assertThat(DurationStyle.detectAndParse("-10d")).isEqualTo(Duration.ofDays(-10)); + } + + @Test + public void detectAndParseWhenSimpleWithoutSuffixShouldReturnDuration() { + assertThat(DurationStyle.detectAndParse("10")).isEqualTo(Duration.ofMillis(10)); + assertThat(DurationStyle.detectAndParse("+10")).isEqualTo(Duration.ofMillis(10)); + assertThat(DurationStyle.detectAndParse("-10")).isEqualTo(Duration.ofMillis(-10)); + } + + @Test + public void detectAndParseWhenSimpleWithoutSuffixButWithChronoUnitShouldReturnDuration() { + assertThat(DurationStyle.detectAndParse("10", ChronoUnit.SECONDS)) + .isEqualTo(Duration.ofSeconds(10)); + assertThat(DurationStyle.detectAndParse("+10", ChronoUnit.SECONDS)) + .isEqualTo(Duration.ofSeconds(10)); + assertThat(DurationStyle.detectAndParse("-10", ChronoUnit.SECONDS)) + .isEqualTo(Duration.ofSeconds(-10)); + } + + @Test + public void detectAndParseWhenBadFormatShouldThrowException() { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("'10foo' is not a valid duration"); + DurationStyle.detectAndParse("10foo"); + } + + @Test + public void detectWhenSimpleShouldReturnSimple() { + assertThat(DurationStyle.detect("10")).isEqualTo(DurationStyle.SIMPLE); + assertThat(DurationStyle.detect("+10")).isEqualTo(DurationStyle.SIMPLE); + assertThat(DurationStyle.detect("-10")).isEqualTo(DurationStyle.SIMPLE); + assertThat(DurationStyle.detect("10ns")).isEqualTo(DurationStyle.SIMPLE); + assertThat(DurationStyle.detect("10ms")).isEqualTo(DurationStyle.SIMPLE); + assertThat(DurationStyle.detect("10s")).isEqualTo(DurationStyle.SIMPLE); + assertThat(DurationStyle.detect("10m")).isEqualTo(DurationStyle.SIMPLE); + assertThat(DurationStyle.detect("10h")).isEqualTo(DurationStyle.SIMPLE); + assertThat(DurationStyle.detect("10d")).isEqualTo(DurationStyle.SIMPLE); + assertThat(DurationStyle.detect("-10ms")).isEqualTo(DurationStyle.SIMPLE); + assertThat(DurationStyle.detect("-10ms")).isEqualTo(DurationStyle.SIMPLE); + assertThat(DurationStyle.detect("10D")).isEqualTo(DurationStyle.SIMPLE); + } + + @Test + public void detectWhenIso8601ShouldReturnIso8601() { + assertThat(DurationStyle.detect("PT20.345S")).isEqualTo(DurationStyle.ISO8601); + assertThat(DurationStyle.detect("PT15M")).isEqualTo(DurationStyle.ISO8601); + assertThat(DurationStyle.detect("+PT15M")).isEqualTo(DurationStyle.ISO8601); + assertThat(DurationStyle.detect("PT10H")).isEqualTo(DurationStyle.ISO8601); + assertThat(DurationStyle.detect("P2D")).isEqualTo(DurationStyle.ISO8601); + assertThat(DurationStyle.detect("P2DT3H4M")).isEqualTo(DurationStyle.ISO8601); + assertThat(DurationStyle.detect("-PT6H3M")).isEqualTo(DurationStyle.ISO8601); + assertThat(DurationStyle.detect("-PT-6H+3M")).isEqualTo(DurationStyle.ISO8601); + } + + @Test + public void detectWhenUnknownShouldThrowException() { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("'bad' is not a valid duration"); + DurationStyle.detect("bad"); + } + + @Test + public void parseIso8601ShouldParse() { + assertThat(DurationStyle.ISO8601.parse("PT20.345S")) + .isEqualTo(Duration.parse("PT20.345S")); + assertThat(DurationStyle.ISO8601.parse("PT15M")) + .isEqualTo(Duration.parse("PT15M")); + assertThat(DurationStyle.ISO8601.parse("+PT15M")) + .isEqualTo(Duration.parse("PT15M")); + assertThat(DurationStyle.ISO8601.parse("PT10H")) + .isEqualTo(Duration.parse("PT10H")); + assertThat(DurationStyle.ISO8601.parse("P2D")).isEqualTo(Duration.parse("P2D")); + assertThat(DurationStyle.ISO8601.parse("P2DT3H4M")) + .isEqualTo(Duration.parse("P2DT3H4M")); + assertThat(DurationStyle.ISO8601.parse("-PT6H3M")) + .isEqualTo(Duration.parse("-PT6H3M")); + assertThat(DurationStyle.ISO8601.parse("-PT-6H+3M")) + .isEqualTo(Duration.parse("-PT-6H+3M")); + } + + @Test + public void parseIso8601WithUnitShouldIgnoreUnit() { + assertThat(DurationStyle.ISO8601.parse("PT20.345S", ChronoUnit.SECONDS)) + .isEqualTo(Duration.parse("PT20.345S")); + assertThat(DurationStyle.ISO8601.parse("PT15M", ChronoUnit.SECONDS)) + .isEqualTo(Duration.parse("PT15M")); + assertThat(DurationStyle.ISO8601.parse("+PT15M", ChronoUnit.SECONDS)) + .isEqualTo(Duration.parse("PT15M")); + assertThat(DurationStyle.ISO8601.parse("PT10H", ChronoUnit.SECONDS)) + .isEqualTo(Duration.parse("PT10H")); + assertThat(DurationStyle.ISO8601.parse("P2D")).isEqualTo(Duration.parse("P2D")); + assertThat(DurationStyle.ISO8601.parse("P2DT3H4M", ChronoUnit.SECONDS)) + .isEqualTo(Duration.parse("P2DT3H4M")); + assertThat(DurationStyle.ISO8601.parse("-PT6H3M", ChronoUnit.SECONDS)) + .isEqualTo(Duration.parse("-PT6H3M")); + assertThat(DurationStyle.ISO8601.parse("-PT-6H+3M", ChronoUnit.SECONDS)) + .isEqualTo(Duration.parse("-PT-6H+3M")); + } + + @Test + public void parseIso8601WhenSimpleShouldThrowException() { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("'10d' is not a valid ISO-8601 duration"); + DurationStyle.ISO8601.parse("10d"); + } + + @Test + public void parseSimpleShouldParse() { + assertThat(DurationStyle.SIMPLE.parse("10m")).isEqualTo(Duration.ofMinutes(10)); + } + + @Test + public void parseSimpleWithUnitShouldUseUnitAsFallback() { + assertThat(DurationStyle.SIMPLE.parse("10m", ChronoUnit.SECONDS)) + .isEqualTo(Duration.ofMinutes(10)); + assertThat(DurationStyle.SIMPLE.parse("10", ChronoUnit.MINUTES)) + .isEqualTo(Duration.ofMinutes(10)); + } + + @Test + public void parseSimpleWhenUnknownUnitShouldThrowException() { + try { + DurationStyle.SIMPLE.parse("10mb"); + fail("Did not throw"); + } + catch (IllegalArgumentException ex) { + assertThat(ex.getCause().getMessage()).isEqualTo("Unknown unit 'mb'"); + } + } + + @Test + public void parseSimpleWhenIso8601ShouldThrowException() { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("'PT10H' is not a valid simple duration"); + DurationStyle.SIMPLE.parse("PT10H"); + } + + @Test + public void printIso8601ShouldPrint() { + Duration duration = Duration.parse("-PT-6H+3M"); + assertThat(DurationStyle.ISO8601.print(duration)).isEqualTo("PT5H57M"); + } + + @Test + public void printIso8601ShouldIgnoreUnit() { + Duration duration = Duration.parse("-PT-6H+3M"); + assertThat(DurationStyle.ISO8601.print(duration, ChronoUnit.DAYS)) + .isEqualTo("PT5H57M"); + } + + @Test + public void printSimpleWithoutUnitShouldPrintInMs() { + Duration duration = Duration.ofSeconds(1); + assertThat(DurationStyle.SIMPLE.print(duration)).isEqualTo("1000ms"); + } + + @Test + public void printSimpleWithUnitShouldPrintInUnit() { + Duration duration = Duration.ofMillis(1000); + assertThat(DurationStyle.SIMPLE.print(duration, ChronoUnit.SECONDS)) + .isEqualTo("1s"); + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/DurationToNumberConverterTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/DurationToNumberConverterTests.java new file mode 100644 index 0000000000..e3994b7a24 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/DurationToNumberConverterTests.java @@ -0,0 +1,81 @@ +/* + * Copyright 2012-2018 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.convert; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link DurationToNumberConverter}. + * + * @author Phillip Webb + */ +@RunWith(Parameterized.class) +public class DurationToNumberConverterTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private final ConversionService conversionService; + + public DurationToNumberConverterTests(String name, + ConversionService conversionService) { + this.conversionService = conversionService; + } + + @Test + public void convertWithoutStyleShouldReturnMs() { + Long converted = this.conversionService.convert(Duration.ofSeconds(1), + Long.class); + assertThat(converted).isEqualTo(1000); + } + + @Test + public void convertWithFormatShouldUseIgnoreFormat() { + Integer converted = (Integer) this.conversionService.convert( + Duration.ofSeconds(1), + MockDurationTypeDescriptor.get(null, DurationStyle.ISO8601), + TypeDescriptor.valueOf(Integer.class)); + assertThat(converted).isEqualTo(1000); + } + + @Test + public void convertWithFormatAndUnitShouldUseFormatAndUnit() { + Byte converted = (Byte) this.conversionService.convert(Duration.ofSeconds(1), + MockDurationTypeDescriptor.get(ChronoUnit.SECONDS, null), + TypeDescriptor.valueOf(Byte.class)); + assertThat(converted).isEqualTo((byte) 1); + } + + @Parameters(name = "{0}") + public static Iterable conversionServices() { + return new ConversionServiceParameters(new DurationToNumberConverter()); + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/DurationToStringConverterTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/DurationToStringConverterTests.java new file mode 100644 index 0000000000..990e98e569 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/DurationToStringConverterTests.java @@ -0,0 +1,80 @@ +/* + * Copyright 2012-2018 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.convert; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link DurationToStringConverter}. + * + * @author Phillip Webb + */ +@RunWith(Parameterized.class) +public class DurationToStringConverterTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private final ConversionService conversionService; + + public DurationToStringConverterTests(String name, + ConversionService conversionService) { + this.conversionService = conversionService; + } + + @Test + public void convertWithoutStyleShouldReturnIso8601() { + String converted = this.conversionService.convert(Duration.ofSeconds(1), + String.class); + assertThat(converted).isEqualTo("PT1S"); + } + + @Test + public void convertWithFormatShouldUseFormatAndMs() { + String converted = (String) this.conversionService.convert(Duration.ofSeconds(1), + MockDurationTypeDescriptor.get(null, DurationStyle.SIMPLE), + TypeDescriptor.valueOf(String.class)); + assertThat(converted).isEqualTo("1000ms"); + } + + @Test + public void convertWithFormatAndUnitShouldUseFormatAndUnit() { + String converted = (String) this.conversionService.convert(Duration.ofSeconds(1), + MockDurationTypeDescriptor.get(ChronoUnit.SECONDS, DurationStyle.SIMPLE), + TypeDescriptor.valueOf(String.class)); + assertThat(converted).isEqualTo("1s"); + } + + @Parameters(name = "{0}") + public static Iterable conversionServices() { + return new ConversionServiceParameters(new DurationToStringConverter()); + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/InetAddressFormatterTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/InetAddressFormatterTests.java new file mode 100644 index 0000000000..f8d3006d4e --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/InetAddressFormatterTests.java @@ -0,0 +1,89 @@ +/* + * Copyright 2012-2018 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.convert; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +import org.junit.AssumptionViolatedException; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +import org.springframework.core.convert.ConversionFailedException; +import org.springframework.core.convert.ConversionService; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link InetAddressFormatter}. + * + * @author Phillip Webb + */ +@RunWith(Parameterized.class) +public class InetAddressFormatterTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private final ConversionService conversionService; + + public InetAddressFormatterTests(String name, ConversionService conversionService) { + this.conversionService = conversionService; + } + + @Test + public void convertFromInetAddressToStringShouldConvert() + throws UnknownHostException { + assumeResolves("example.com"); + InetAddress address = InetAddress.getByName("example.com"); + String converted = this.conversionService.convert(address, String.class); + assertThat(converted).isEqualTo(address.getHostAddress()); + } + + @Test + public void convertFromStringToInetAddressShouldConvert() { + assumeResolves("example.com"); + InetAddress converted = this.conversionService.convert("example.com", + InetAddress.class); + assertThat(converted.toString()).startsWith("example.com"); + } + + @Test + public void convertFromStringToInetAddressWhenHostDoesNotExistShouldThrowException() { + this.thrown.expect(ConversionFailedException.class); + this.conversionService.convert("ireallydontexist.example.com", InetAddress.class); + } + + private void assumeResolves(String host) { + try { + InetAddress.getByName(host); + } + catch (UnknownHostException ex) { + throw new AssumptionViolatedException("Host " + host + " not resolvable", ex); + } + } + + @Parameters(name = "{0}") + public static Iterable conversionServices() { + return new ConversionServiceParameters(new InetAddressFormatter()); + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/IsoOffsetFormatterTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/IsoOffsetFormatterTests.java new file mode 100644 index 0000000000..fe75283e8b --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/IsoOffsetFormatterTests.java @@ -0,0 +1,66 @@ +/* + * Copyright 2012-2018 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.convert; + +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +import org.springframework.core.convert.ConversionService; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link IsoOffsetFormatter}. + * + * @author Phillip Webb + */ +@RunWith(Parameterized.class) +public class IsoOffsetFormatterTests { + + private final ConversionService conversionService; + + public IsoOffsetFormatterTests(String name, ConversionService conversionService) { + this.conversionService = conversionService; + } + + @Test + public void convertShouldConvertStringToIsoDate() { + OffsetDateTime now = OffsetDateTime.now(); + OffsetDateTime converted = this.conversionService.convert( + DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(now), OffsetDateTime.class); + assertThat(converted).isEqualTo(now); + } + + @Test + public void convertShouldConvertIsoDateToString() { + OffsetDateTime now = OffsetDateTime.now(); + String converted = this.conversionService.convert(now, String.class); + assertThat(converted).isNotNull() + .startsWith(now.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)); + } + + @Parameters(name = "{0}") + public static Iterable conversionServices() { + return new ConversionServiceParameters(new IsoOffsetFormatter()); + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/MockDurationTypeDescriptor.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/MockDurationTypeDescriptor.java new file mode 100644 index 0000000000..065bd2158d --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/MockDurationTypeDescriptor.java @@ -0,0 +1,60 @@ +/* + * Copyright 2012-2018 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.convert; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.Collections; + +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.convert.TypeDescriptor; + +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Create a mock {@link TypeDescriptor} with optional {@link DurationUnit} and + * {@link DurationFormat} annotations. + * + * @author Phillip Webb + */ +public final class MockDurationTypeDescriptor { + + private MockDurationTypeDescriptor() { + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + public static TypeDescriptor get(ChronoUnit unit, DurationStyle style) { + TypeDescriptor descriptor = mock(TypeDescriptor.class); + if (unit != null) { + DurationUnit unitAnnotation = AnnotationUtils.synthesizeAnnotation( + Collections.singletonMap("value", unit), DurationUnit.class, null); + given(descriptor.getAnnotation(DurationUnit.class)) + .willReturn(unitAnnotation); + } + if (style != null) { + DurationFormat formatAnnotation = AnnotationUtils.synthesizeAnnotation( + Collections.singletonMap("value", style), DurationFormat.class, null); + given(descriptor.getAnnotation(DurationFormat.class)) + .willReturn(formatAnnotation); + } + given(descriptor.getType()).willReturn((Class) Duration.class); + given(descriptor.getObjectType()).willReturn((Class) Duration.class); + return descriptor; + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/NumberToDurationConverterTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/NumberToDurationConverterTests.java new file mode 100644 index 0000000000..4159fc56ff --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/NumberToDurationConverterTests.java @@ -0,0 +1,94 @@ +/* + * Copyright 2012-2018 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.convert; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.Collections; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link NumberToDurationConverter}. + * + * @author Phillip Webb + */ +@RunWith(Parameterized.class) +public class NumberToDurationConverterTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private final ConversionService conversionService; + + public NumberToDurationConverterTests(String name, + ConversionService conversionService) { + this.conversionService = conversionService; + } + + @Test + public void convertWhenSimpleWithoutSuffixShouldReturnDuration() { + assertThat(convert(10)).isEqualTo(Duration.ofMillis(10)); + assertThat(convert(+10)).isEqualTo(Duration.ofMillis(10)); + assertThat(convert(-10)).isEqualTo(Duration.ofMillis(-10)); + } + + @Test + public void convertWhenSimpleWithoutSuffixButWithAnnotationShouldReturnDuration() { + assertThat(convert(10, ChronoUnit.SECONDS)).isEqualTo(Duration.ofSeconds(10)); + assertThat(convert(+10, ChronoUnit.SECONDS)).isEqualTo(Duration.ofSeconds(10)); + assertThat(convert(-10, ChronoUnit.SECONDS)).isEqualTo(Duration.ofSeconds(-10)); + } + + private Duration convert(Integer source) { + return this.conversionService.convert(source, Duration.class); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private Duration convert(Integer source, ChronoUnit defaultUnit) { + TypeDescriptor targetType = mock(TypeDescriptor.class); + if (defaultUnit != null) { + DurationUnit unitAnnotation = AnnotationUtils.synthesizeAnnotation( + Collections.singletonMap("value", defaultUnit), DurationUnit.class, + null); + given(targetType.getAnnotation(DurationUnit.class)) + .willReturn(unitAnnotation); + } + given(targetType.getType()).willReturn((Class) Duration.class); + return (Duration) this.conversionService.convert(source, + TypeDescriptor.forObject(source), targetType); + } + + @Parameters(name = "{0}") + public static Iterable conversionServices() { + return new ConversionServiceParameters(new NumberToDurationConverter()); + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/DurationConverterTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/StringToDurationConverterTests.java similarity index 72% rename from spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/DurationConverterTests.java rename to spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/StringToDurationConverterTests.java index ff435e1717..8d31435057 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/convert/DurationConverterTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/StringToDurationConverterTests.java @@ -14,34 +14,41 @@ * limitations under the License. */ -package org.springframework.boot.context.properties.bind.convert; +package org.springframework.boot.convert; import java.time.Duration; import java.time.temporal.ChronoUnit; -import java.util.Collections; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; -import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.convert.ConversionFailedException; +import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.TypeDescriptor; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; /** - * Tests for {@link DurationConverter}. + * Tests for {@link StringToDurationConverter}. * * @author Phillip Webb */ -public class DurationConverterTests { +@RunWith(Parameterized.class) +public class StringToDurationConverterTests { @Rule public ExpectedException thrown = ExpectedException.none(); - private DurationConverter converter = new DurationConverter(); + private final ConversionService conversionService; + + public StringToDurationConverterTests(String name, + ConversionService conversionService) { + this.conversionService = conversionService; + } @Test public void convertWhenIso8601ShouldReturnDuration() { @@ -112,36 +119,45 @@ public class DurationConverterTests { @Test public void convertWhenSimpleWithoutSuffixButWithAnnotationShouldReturnDuration() { - assertThat(convert("10", ChronoUnit.SECONDS)).isEqualTo(Duration.ofSeconds(10)); - assertThat(convert("+10", ChronoUnit.SECONDS)).isEqualTo(Duration.ofSeconds(10)); - assertThat(convert("-10", ChronoUnit.SECONDS)).isEqualTo(Duration.ofSeconds(-10)); + assertThat(convert("10", ChronoUnit.SECONDS, null)) + .isEqualTo(Duration.ofSeconds(10)); + assertThat(convert("+10", ChronoUnit.SECONDS, null)) + .isEqualTo(Duration.ofSeconds(10)); + assertThat(convert("-10", ChronoUnit.SECONDS, null)) + .isEqualTo(Duration.ofSeconds(-10)); } @Test public void convertWhenBadFormatShouldThrowException() { - this.thrown.expect(IllegalStateException.class); + this.thrown.expect(ConversionFailedException.class); this.thrown.expectMessage("'10foo' is not a valid duration"); convert("10foo"); } + @Test + public void convertWhenStyleMismatchShouldThrowException() { + this.thrown.expect(ConversionFailedException.class); + convert("10s", null, DurationStyle.ISO8601); + } + @Test public void convertWhenEmptyShouldReturnNull() { assertThat(convert("")).isNull(); } private Duration convert(String source) { - return (Duration) this.converter.convert(source, TypeDescriptor.forObject(source), - TypeDescriptor.valueOf(Duration.class)); + return this.conversionService.convert(source, Duration.class); } - private Duration convert(String source, ChronoUnit defaultUnit) { - TypeDescriptor targetType = mock(TypeDescriptor.class); - DefaultDurationUnit annotation = AnnotationUtils.synthesizeAnnotation( - Collections.singletonMap("value", defaultUnit), DefaultDurationUnit.class, - null); - given(targetType.getAnnotation(DefaultDurationUnit.class)).willReturn(annotation); - return (Duration) this.converter.convert(source, TypeDescriptor.forObject(source), - targetType); + private Duration convert(String source, ChronoUnit unit, DurationStyle style) { + return (Duration) this.conversionService.convert(source, + TypeDescriptor.forObject(source), + MockDurationTypeDescriptor.get(unit, style)); + } + + @Parameters(name = "{0}") + public static Iterable conversionServices() { + return new ConversionServiceParameters(new StringToDurationConverter()); } } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/StringToEnumIgnoringCaseConverterFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/StringToEnumIgnoringCaseConverterFactoryTests.java new file mode 100644 index 0000000000..e006030a76 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/StringToEnumIgnoringCaseConverterFactoryTests.java @@ -0,0 +1,106 @@ +/* + * Copyright 2012-2018 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.convert; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +import org.springframework.core.convert.ConversionService; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link StringToEnumIgnoringCaseConverterFactory}. + * + * @author Phillip Webb + */ +@RunWith(Parameterized.class) +public class StringToEnumIgnoringCaseConverterFactoryTests { + + private final ConversionService conversionService; + + public StringToEnumIgnoringCaseConverterFactoryTests(String name, + ConversionService conversionService) { + this.conversionService = conversionService; + } + + @Test + public void canConvertFromStringToEnumShouldReturnTrue() { + assertThat(this.conversionService.canConvert(String.class, TestEnum.class)) + .isTrue(); + } + + @Test + public void canConvertFromStringToEnumSubclassShouldReturnTrue() { + assertThat(this.conversionService.canConvert(String.class, + TestSubclassEnum.ONE.getClass())).isTrue(); + } + + @Test + public void convertFromStringToEnumWhenExactMatchShouldConvertValue() { + ConversionService service = this.conversionService; + assertThat(service.convert("", TestEnum.class)).isNull(); + assertThat(service.convert("ONE", TestEnum.class)).isEqualTo(TestEnum.ONE); + assertThat(service.convert("TWO", TestEnum.class)).isEqualTo(TestEnum.TWO); + assertThat(service.convert("THREE_AND_FOUR", TestEnum.class)) + .isEqualTo(TestEnum.THREE_AND_FOUR); + } + + @Test + public void convertFromStringToEnumWhenFuzzyMatchShouldConvertValue() { + ConversionService service = this.conversionService; + assertThat(service.convert("", TestEnum.class)).isNull(); + assertThat(service.convert("one", TestEnum.class)).isEqualTo(TestEnum.ONE); + assertThat(service.convert("tWo", TestEnum.class)).isEqualTo(TestEnum.TWO); + assertThat(service.convert("three_and_four", TestEnum.class)) + .isEqualTo(TestEnum.THREE_AND_FOUR); + assertThat(service.convert("threeandfour", TestEnum.class)) + .isEqualTo(TestEnum.THREE_AND_FOUR); + assertThat(service.convert("three-and-four", TestEnum.class)) + .isEqualTo(TestEnum.THREE_AND_FOUR); + assertThat(service.convert("threeAndFour", TestEnum.class)) + .isEqualTo(TestEnum.THREE_AND_FOUR); + } + + @Parameters(name = "{0}") + public static Iterable conversionServices() { + return new ConversionServiceParameters( + new StringToEnumIgnoringCaseConverterFactory()); + } + + enum TestEnum { + + ONE, TWO, THREE_AND_FOUR + + } + + enum TestSubclassEnum { + + ONE { + + @Override + public String toString() { + return "foo"; + } + + } + + } + +}