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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -65,6 +65,8 @@ class ConversionServiceDeducer {
|
|||
if (!converterBeans.isEmpty()) {
|
||||
FormattingConversionService beansConverterService = new FormattingConversionService();
|
||||
DefaultConversionService.addCollectionConverters(beansConverterService);
|
||||
beansConverterService
|
||||
.addConverter(new ConfigurationPropertiesCharSequenceToObjectConverter(beansConverterService));
|
||||
converterBeans.addTo(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");
|
||||
* 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.env.MapPropertySource;
|
||||
import org.springframework.core.env.MutablePropertySources;
|
||||
import org.springframework.core.env.PropertySource;
|
||||
import org.springframework.core.env.StandardEnvironment;
|
||||
import org.springframework.core.env.SystemEnvironmentPropertySource;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
|
@ -653,6 +654,41 @@ class ConfigurationPropertiesTests {
|
|||
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
|
||||
void loadWhenBeanFactoryConversionServiceAndConverterBeanCanUseBeanFactoryConverter() {
|
||||
DefaultConversionService conversionService = new DefaultConversionService();
|
||||
|
|
|
@ -78,6 +78,7 @@ class ConversionServiceDeducerTests {
|
|||
assertThat(conversionServices).hasSize(2);
|
||||
assertThat(conversionServices.get(0)).isExactlyInstanceOf(FormattingConversionService.class);
|
||||
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());
|
||||
}
|
||||
|
||||
|
@ -105,6 +106,12 @@ class ConversionServiceDeducerTests {
|
|||
return new TestConverter();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConfigurationPropertiesBinding
|
||||
StringConverter stringConverter() {
|
||||
return new StringConverter();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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");
|
||||
* 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 Andy Wilkinson
|
||||
*/
|
||||
final class ConversionServiceArguments {
|
||||
public final class ConversionServiceArguments {
|
||||
|
||||
private ConversionServiceArguments() {
|
||||
}
|
||||
|
||||
static Stream<? extends Arguments> with(Formatter<?> formatter) {
|
||||
public static Stream<? extends Arguments> with(Formatter<?> 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));
|
||||
}
|
||||
|
||||
static Stream<? extends Arguments> with(Consumer<FormattingConversionService> initializer) {
|
||||
public static Stream<? extends Arguments> with(Consumer<FormattingConversionService> initializer) {
|
||||
FormattingConversionService withoutDefaults = new FormattingConversionService();
|
||||
initializer.accept(withoutDefaults);
|
||||
return Stream.of(
|
||||
|
@ -57,7 +57,7 @@ final class ConversionServiceArguments {
|
|||
"Application conversion service")));
|
||||
}
|
||||
|
||||
static boolean isApplicationConversionService(ConversionService conversionService) {
|
||||
public static boolean isApplicationConversionService(ConversionService conversionService) {
|
||||
if (conversionService instanceof NamedConversionService namedConversionService) {
|
||||
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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -39,6 +39,6 @@ import org.springframework.core.convert.ConversionService;
|
|||
@MethodSource("conversionServices")
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@interface ConversionServiceTest {
|
||||
public @interface ConversionServiceTest {
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue