commit
d032b9d234
|
@ -0,0 +1,106 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.context.properties;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.springframework.boot.convert.ApplicationConversionService;
|
||||||
|
import org.springframework.core.convert.ConversionService;
|
||||||
|
import org.springframework.core.convert.TypeDescriptor;
|
||||||
|
import org.springframework.core.convert.converter.ConditionalGenericConverter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy of package-private
|
||||||
|
* {@code org.springframework.boot.convert.CharSequenceToObjectConverter}, renamed for
|
||||||
|
* differentiation.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
*/
|
||||||
|
final class ConfigurationPropertiesCharSequenceToObjectConverter implements ConditionalGenericConverter {
|
||||||
|
|
||||||
|
private static final TypeDescriptor STRING = TypeDescriptor.valueOf(String.class);
|
||||||
|
|
||||||
|
private static final TypeDescriptor BYTE_ARRAY = TypeDescriptor.valueOf(byte[].class);
|
||||||
|
|
||||||
|
private static final Set<ConvertiblePair> TYPES;
|
||||||
|
|
||||||
|
private final ThreadLocal<Boolean> disable = new ThreadLocal<>();
|
||||||
|
|
||||||
|
static {
|
||||||
|
TYPES = Collections.singleton(new ConvertiblePair(CharSequence.class, Object.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
private final ConversionService conversionService;
|
||||||
|
|
||||||
|
ConfigurationPropertiesCharSequenceToObjectConverter(ConversionService conversionService) {
|
||||||
|
this.conversionService = conversionService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<ConvertiblePair> getConvertibleTypes() {
|
||||||
|
return TYPES;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
|
||||||
|
if (sourceType.getType() == String.class || this.disable.get() == Boolean.TRUE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this.disable.set(Boolean.TRUE);
|
||||||
|
try {
|
||||||
|
boolean canDirectlyConvertCharSequence = this.conversionService.canConvert(sourceType, targetType);
|
||||||
|
if (canDirectlyConvertCharSequence && !isStringConversionBetter(sourceType, targetType)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return this.conversionService.canConvert(STRING, targetType);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
this.disable.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return if String based conversion is better based on the target type. This is
|
||||||
|
* required when ObjectTo... conversion produces incorrect results.
|
||||||
|
* @param sourceType the source type to test
|
||||||
|
* @param targetType the target type to test
|
||||||
|
* @return if string conversion is better
|
||||||
|
*/
|
||||||
|
private boolean isStringConversionBetter(TypeDescriptor sourceType, TypeDescriptor targetType) {
|
||||||
|
if (this.conversionService instanceof ApplicationConversionService applicationConversionService) {
|
||||||
|
if (applicationConversionService.isConvertViaObjectSourceType(sourceType, targetType)) {
|
||||||
|
// If an ObjectTo... converter is being used then there might be a
|
||||||
|
// better StringTo... version
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((targetType.isArray() || targetType.isCollection()) && !targetType.equals(BYTE_ARRAY)) {
|
||||||
|
// StringToArrayConverter / StringToCollectionConverter are better than
|
||||||
|
// ObjectToArrayConverter / ObjectToCollectionConverter
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
|
||||||
|
return this.conversionService.convert(source.toString(), STRING, targetType);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2012-2023 the original author or authors.
|
* Copyright 2012-2024 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -65,6 +65,8 @@ class ConversionServiceDeducer {
|
||||||
if (!converterBeans.isEmpty()) {
|
if (!converterBeans.isEmpty()) {
|
||||||
FormattingConversionService beansConverterService = new FormattingConversionService();
|
FormattingConversionService beansConverterService = new FormattingConversionService();
|
||||||
DefaultConversionService.addCollectionConverters(beansConverterService);
|
DefaultConversionService.addCollectionConverters(beansConverterService);
|
||||||
|
beansConverterService
|
||||||
|
.addConverter(new ConfigurationPropertiesCharSequenceToObjectConverter(beansConverterService));
|
||||||
converterBeans.addTo(beansConverterService);
|
converterBeans.addTo(beansConverterService);
|
||||||
conversionServices.add(beansConverterService);
|
conversionServices.add(beansConverterService);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,116 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2024 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.context.properties;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.params.provider.Arguments;
|
||||||
|
|
||||||
|
import org.springframework.boot.convert.ApplicationConversionService;
|
||||||
|
import org.springframework.boot.convert.ConversionServiceArguments;
|
||||||
|
import org.springframework.boot.convert.ConversionServiceTest;
|
||||||
|
import org.springframework.core.convert.ConversionService;
|
||||||
|
import org.springframework.core.convert.TypeDescriptor;
|
||||||
|
import org.springframework.core.convert.converter.Converter;
|
||||||
|
import org.springframework.format.support.DefaultFormattingConversionService;
|
||||||
|
import org.springframework.format.support.FormattingConversionService;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link ConfigurationPropertiesCharSequenceToObjectConverter}
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @author Andy Wilkinson
|
||||||
|
*/
|
||||||
|
class ConfigurationPropertiesCharSequenceToObjectConverterTests {
|
||||||
|
|
||||||
|
@ConversionServiceTest
|
||||||
|
void convertWhenCanConvertViaToString(ConversionService conversionService) {
|
||||||
|
assertThat(conversionService.convert(new StringBuilder("1"), Integer.class)).isOne();
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConversionServiceTest
|
||||||
|
void convertWhenCanConvertDirectlySkipsStringConversion(ConversionService conversionService) {
|
||||||
|
assertThat(conversionService.convert(new String("1"), Long.class)).isOne();
|
||||||
|
if (!ConversionServiceArguments.isApplicationConversionService(conversionService)) {
|
||||||
|
assertThat(conversionService.convert(new StringBuilder("1"), Long.class)).isEqualTo(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
void convertWhenTargetIsList() {
|
||||||
|
ConversionService conversionService = new ApplicationConversionService();
|
||||||
|
StringBuilder source = new StringBuilder("1,2,3");
|
||||||
|
TypeDescriptor sourceType = TypeDescriptor.valueOf(StringBuilder.class);
|
||||||
|
TypeDescriptor targetType = TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(String.class));
|
||||||
|
List<String> converted = (List<String>) conversionService.convert(source, sourceType, targetType);
|
||||||
|
assertThat(converted).containsExactly("1", "2", "3");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
void convertWhenTargetIsListAndNotUsingApplicationConversionService() {
|
||||||
|
FormattingConversionService conversionService = new DefaultFormattingConversionService();
|
||||||
|
conversionService.addConverter(new ConfigurationPropertiesCharSequenceToObjectConverter(conversionService));
|
||||||
|
StringBuilder source = new StringBuilder("1,2,3");
|
||||||
|
TypeDescriptor sourceType = TypeDescriptor.valueOf(StringBuilder.class);
|
||||||
|
TypeDescriptor targetType = TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(String.class));
|
||||||
|
List<String> converted = (List<String>) conversionService.convert(source, sourceType, targetType);
|
||||||
|
assertThat(converted).containsExactly("1", "2", "3");
|
||||||
|
}
|
||||||
|
|
||||||
|
static Stream<? extends Arguments> conversionServices() {
|
||||||
|
return ConversionServiceArguments.with((conversionService) -> {
|
||||||
|
conversionService.addConverter(new StringToIntegerConverter());
|
||||||
|
conversionService.addConverter(new StringToLongConverter());
|
||||||
|
conversionService.addConverter(new CharSequenceToLongConverter());
|
||||||
|
conversionService.addConverter(new ConfigurationPropertiesCharSequenceToObjectConverter(conversionService));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static class StringToIntegerConverter implements Converter<String, Integer> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Integer convert(String source) {
|
||||||
|
return Integer.valueOf(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static class StringToLongConverter implements Converter<String, Long> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Long convert(String source) {
|
||||||
|
return Long.valueOf(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static class CharSequenceToLongConverter implements Converter<CharSequence, Long> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Long convert(CharSequence source) {
|
||||||
|
return Long.valueOf(source.toString()) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2012-2023 the original author or authors.
|
* Copyright 2012-2024 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -83,6 +83,7 @@ import org.springframework.core.convert.converter.GenericConverter;
|
||||||
import org.springframework.core.convert.support.DefaultConversionService;
|
import org.springframework.core.convert.support.DefaultConversionService;
|
||||||
import org.springframework.core.env.MapPropertySource;
|
import org.springframework.core.env.MapPropertySource;
|
||||||
import org.springframework.core.env.MutablePropertySources;
|
import org.springframework.core.env.MutablePropertySources;
|
||||||
|
import org.springframework.core.env.PropertySource;
|
||||||
import org.springframework.core.env.StandardEnvironment;
|
import org.springframework.core.env.StandardEnvironment;
|
||||||
import org.springframework.core.env.SystemEnvironmentPropertySource;
|
import org.springframework.core.env.SystemEnvironmentPropertySource;
|
||||||
import org.springframework.core.io.ClassPathResource;
|
import org.springframework.core.io.ClassPathResource;
|
||||||
|
@ -653,6 +654,41 @@ class ConfigurationPropertiesTests {
|
||||||
assertThat(person.lastName).isEqualTo("Smith");
|
assertThat(person.lastName).isEqualTo("Smith");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void loadShouldUseStringConverterBeanWhenValueIsCharSequence() {
|
||||||
|
this.context.register(PersonConverterConfiguration.class, PersonProperties.class);
|
||||||
|
PropertySource<?> testProperties = new MapPropertySource("test", Map.of("test.person", new CharSequence() {
|
||||||
|
|
||||||
|
private final String value = "John Smith";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int length() {
|
||||||
|
return this.value.length();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public char charAt(int index) {
|
||||||
|
return this.value.charAt(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CharSequence subSequence(int start, int end) {
|
||||||
|
return this.value.subSequence(start, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return this.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
}));
|
||||||
|
this.context.getEnvironment().getPropertySources().addLast(testProperties);
|
||||||
|
this.context.refresh();
|
||||||
|
Person person = this.context.getBean(PersonProperties.class).getPerson();
|
||||||
|
assertThat(person.firstName).isEqualTo("John");
|
||||||
|
assertThat(person.lastName).isEqualTo("Smith");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void loadWhenBeanFactoryConversionServiceAndConverterBeanCanUseBeanFactoryConverter() {
|
void loadWhenBeanFactoryConversionServiceAndConverterBeanCanUseBeanFactoryConverter() {
|
||||||
DefaultConversionService conversionService = new DefaultConversionService();
|
DefaultConversionService conversionService = new DefaultConversionService();
|
||||||
|
|
|
@ -78,6 +78,7 @@ class ConversionServiceDeducerTests {
|
||||||
assertThat(conversionServices).hasSize(2);
|
assertThat(conversionServices).hasSize(2);
|
||||||
assertThat(conversionServices.get(0)).isExactlyInstanceOf(FormattingConversionService.class);
|
assertThat(conversionServices.get(0)).isExactlyInstanceOf(FormattingConversionService.class);
|
||||||
assertThat(conversionServices.get(0).canConvert(InputStream.class, OutputStream.class)).isTrue();
|
assertThat(conversionServices.get(0).canConvert(InputStream.class, OutputStream.class)).isTrue();
|
||||||
|
assertThat(conversionServices.get(0).canConvert(CharSequence.class, InputStream.class)).isTrue();
|
||||||
assertThat(conversionServices.get(1)).isSameAs(ApplicationConversionService.getSharedInstance());
|
assertThat(conversionServices.get(1)).isSameAs(ApplicationConversionService.getSharedInstance());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,6 +106,12 @@ class ConversionServiceDeducerTests {
|
||||||
return new TestConverter();
|
return new TestConverter();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConfigurationPropertiesBinding
|
||||||
|
StringConverter stringConverter() {
|
||||||
|
return new StringConverter();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class TestApplicationConversionService extends ApplicationConversionService {
|
private static final class TestApplicationConversionService extends ApplicationConversionService {
|
||||||
|
@ -120,4 +127,13 @@ class ConversionServiceDeducerTests {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final class StringConverter implements Converter<String, InputStream> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InputStream convert(String source) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2012-2022 the original author or authors.
|
* Copyright 2012-2024 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -35,20 +35,20 @@ import org.springframework.format.support.FormattingConversionService;
|
||||||
* @author Phillip Webb
|
* @author Phillip Webb
|
||||||
* @author Andy Wilkinson
|
* @author Andy Wilkinson
|
||||||
*/
|
*/
|
||||||
final class ConversionServiceArguments {
|
public final class ConversionServiceArguments {
|
||||||
|
|
||||||
private ConversionServiceArguments() {
|
private ConversionServiceArguments() {
|
||||||
}
|
}
|
||||||
|
|
||||||
static Stream<? extends Arguments> with(Formatter<?> formatter) {
|
public static Stream<? extends Arguments> with(Formatter<?> formatter) {
|
||||||
return with((conversionService) -> conversionService.addFormatter(formatter));
|
return with((conversionService) -> conversionService.addFormatter(formatter));
|
||||||
}
|
}
|
||||||
|
|
||||||
static Stream<? extends Arguments> with(GenericConverter converter) {
|
public static Stream<? extends Arguments> with(GenericConverter converter) {
|
||||||
return with((conversionService) -> conversionService.addConverter(converter));
|
return with((conversionService) -> conversionService.addConverter(converter));
|
||||||
}
|
}
|
||||||
|
|
||||||
static Stream<? extends Arguments> with(Consumer<FormattingConversionService> initializer) {
|
public static Stream<? extends Arguments> with(Consumer<FormattingConversionService> initializer) {
|
||||||
FormattingConversionService withoutDefaults = new FormattingConversionService();
|
FormattingConversionService withoutDefaults = new FormattingConversionService();
|
||||||
initializer.accept(withoutDefaults);
|
initializer.accept(withoutDefaults);
|
||||||
return Stream.of(
|
return Stream.of(
|
||||||
|
@ -57,7 +57,7 @@ final class ConversionServiceArguments {
|
||||||
"Application conversion service")));
|
"Application conversion service")));
|
||||||
}
|
}
|
||||||
|
|
||||||
static boolean isApplicationConversionService(ConversionService conversionService) {
|
public static boolean isApplicationConversionService(ConversionService conversionService) {
|
||||||
if (conversionService instanceof NamedConversionService namedConversionService) {
|
if (conversionService instanceof NamedConversionService namedConversionService) {
|
||||||
return isApplicationConversionService(namedConversionService.delegate);
|
return isApplicationConversionService(namedConversionService.delegate);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2012-2019 the original author or authors.
|
* Copyright 2012-2024 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -39,6 +39,6 @@ import org.springframework.core.convert.ConversionService;
|
||||||
@MethodSource("conversionServices")
|
@MethodSource("conversionServices")
|
||||||
@Target(ElementType.METHOD)
|
@Target(ElementType.METHOD)
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
@interface ConversionServiceTest {
|
public @interface ConversionServiceTest {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue