Skip to content

Commit 77478d9

Browse files
committed
Refine CharSequenceToObjectConverter logic
Update `CharSequenceToObjectConverter` so that conversion that would apply using an `ObjectTo...` converter now favors `toString()` based conversion. Prior to this commit, when converting a `CharSequence` to a `Collection` the `ObjectToCollectionConveter` would be picked instead of the `StringToCollectionConverter`. This resulted in a `Collection` containing a single `String` value, rather than the expected list of values split around ",". Fixes gh-25057
1 parent dd997cd commit 77478d9

File tree

4 files changed

+109
-6
lines changed

4 files changed

+109
-6
lines changed

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/ApplicationConversionService.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2020 the original author or authors.
2+
* Copyright 2012-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -21,9 +21,11 @@
2121

2222
import org.springframework.beans.factory.ListableBeanFactory;
2323
import org.springframework.core.convert.ConversionService;
24+
import org.springframework.core.convert.TypeDescriptor;
2425
import org.springframework.core.convert.converter.Converter;
2526
import org.springframework.core.convert.converter.ConverterRegistry;
2627
import org.springframework.core.convert.converter.GenericConverter;
28+
import org.springframework.core.convert.converter.GenericConverter.ConvertiblePair;
2729
import org.springframework.core.convert.support.ConfigurableConversionService;
2830
import org.springframework.core.convert.support.DefaultConversionService;
2931
import org.springframework.format.Formatter;
@@ -61,6 +63,28 @@ public ApplicationConversionService(StringValueResolver embeddedValueResolver) {
6163
configure(this);
6264
}
6365

66+
/**
67+
* Return {@code true} if objects of {@code sourceType} can be converted to the
68+
* {@code targetType} and the converter has {@code Object.class} as a supported source
69+
* type.
70+
* @param sourceType the source type to test
71+
* @param targetType the target type to test
72+
* @return is conversion happens via an {@code ObjectTo...} converter
73+
* @since 2.4.3
74+
*/
75+
public boolean isConvertViaObjectSourceType(TypeDescriptor sourceType, TypeDescriptor targetType) {
76+
GenericConverter converter = getConverter(sourceType, targetType);
77+
Set<ConvertiblePair> pairs = (converter != null) ? converter.getConvertibleTypes() : null;
78+
if (pairs != null) {
79+
for (ConvertiblePair pair : pairs) {
80+
if (Object.class.equals(pair.getSourceType())) {
81+
return true;
82+
}
83+
}
84+
}
85+
return false;
86+
}
87+
6488
/**
6589
* Return a shared default application {@code ConversionService} instance, lazily
6690
* building it once needed.

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/convert/CharSequenceToObjectConverter.java

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2020 the original author or authors.
2+
* Copyright 2012-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -33,6 +33,8 @@ class CharSequenceToObjectConverter implements ConditionalGenericConverter {
3333

3434
private static final TypeDescriptor STRING = TypeDescriptor.valueOf(String.class);
3535

36+
private static final TypeDescriptor BYTE_ARRAY = TypeDescriptor.valueOf(byte[].class);
37+
3638
private static final Set<ConvertiblePair> TYPES;
3739

3840
private final ThreadLocal<Boolean> disable = new ThreadLocal<>();
@@ -59,14 +61,41 @@ public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
5961
}
6062
this.disable.set(Boolean.TRUE);
6163
try {
62-
return !this.conversionService.canConvert(sourceType, targetType)
63-
&& this.conversionService.canConvert(STRING, targetType);
64+
boolean canDirectlyConvertCharSequence = this.conversionService.canConvert(sourceType, targetType);
65+
if (canDirectlyConvertCharSequence && !isStringConversionBetter(sourceType, targetType)) {
66+
return false;
67+
}
68+
return this.conversionService.canConvert(STRING, targetType);
6469
}
6570
finally {
6671
this.disable.set(null);
6772
}
6873
}
6974

75+
/**
76+
* Return if String based conversion is better based on the target type. This is
77+
* required when ObjectTo... conversion produces incorrect results.
78+
* @param sourceType the source type to test
79+
* @param targetType the target type to test
80+
* @return id string conversion is better
81+
*/
82+
private boolean isStringConversionBetter(TypeDescriptor sourceType, TypeDescriptor targetType) {
83+
if (this.conversionService instanceof ApplicationConversionService) {
84+
ApplicationConversionService applicationConversionService = (ApplicationConversionService) this.conversionService;
85+
if (applicationConversionService.isConvertViaObjectSourceType(sourceType, targetType)) {
86+
// If and ObjectTo... converter is being used then there might be a better
87+
// StringTo... version
88+
return true;
89+
}
90+
}
91+
if ((targetType.isArray() || targetType.isCollection()) && !targetType.equals(BYTE_ARRAY)) {
92+
// StringToArrayConverter / StringToCollectionConverter are better than
93+
// ObjectToArrayConverter / ObjectToCollectionConverter
94+
return true;
95+
}
96+
return false;
97+
}
98+
7099
@Override
71100
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
72101
return this.conversionService.convert(source.toString(), STRING, targetType);

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/ApplicationConversionServiceTests.java

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2019 the original author or authors.
2+
* Copyright 2012-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,6 +17,7 @@
1717
package org.springframework.boot.convert;
1818

1919
import java.text.ParseException;
20+
import java.util.List;
2021
import java.util.Locale;
2122
import java.util.Set;
2223

@@ -32,6 +33,7 @@
3233
import org.springframework.format.Parser;
3334
import org.springframework.format.Printer;
3435

36+
import static org.assertj.core.api.Assertions.assertThat;
3537
import static org.mockito.Mockito.mock;
3638
import static org.mockito.Mockito.verify;
3739
import static org.mockito.Mockito.verifyNoMoreInteractions;
@@ -91,6 +93,26 @@ void addBeansWhenHasParserBeanAddParser() {
9193
}
9294
}
9395

96+
@Test
97+
void isConvertViaObjectSourceTypeWhenObjectSourceReturnsTrue() {
98+
// Uses ObjectToCollectionConverter
99+
ApplicationConversionService conversionService = new ApplicationConversionService();
100+
TypeDescriptor sourceType = TypeDescriptor.valueOf(Long.class);
101+
TypeDescriptor targetType = TypeDescriptor.valueOf(List.class);
102+
assertThat(conversionService.canConvert(sourceType, targetType)).isTrue();
103+
assertThat(conversionService.isConvertViaObjectSourceType(sourceType, targetType)).isTrue();
104+
}
105+
106+
@Test
107+
void isConvertViaObjectSourceTypeWhenNotObjectSourceReturnsFalse() {
108+
// Uses StringToCollectionConverter
109+
ApplicationConversionService conversionService = new ApplicationConversionService();
110+
TypeDescriptor sourceType = TypeDescriptor.valueOf(String.class);
111+
TypeDescriptor targetType = TypeDescriptor.valueOf(List.class);
112+
assertThat(conversionService.canConvert(sourceType, targetType)).isTrue();
113+
assertThat(conversionService.isConvertViaObjectSourceType(sourceType, targetType)).isFalse();
114+
}
115+
94116
static class ExampleGenericConverter implements GenericConverter {
95117

96118
@Override

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/convert/CharSequenceToObjectConverterTests.java

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2020 the original author or authors.
2+
* Copyright 2012-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,12 +16,17 @@
1616

1717
package org.springframework.boot.convert;
1818

19+
import java.util.List;
1920
import java.util.stream.Stream;
2021

22+
import org.junit.jupiter.api.Test;
2123
import org.junit.jupiter.params.provider.Arguments;
2224

2325
import org.springframework.core.convert.ConversionService;
26+
import org.springframework.core.convert.TypeDescriptor;
2427
import org.springframework.core.convert.converter.Converter;
28+
import org.springframework.format.support.DefaultFormattingConversionService;
29+
import org.springframework.format.support.FormattingConversionService;
2530

2631
import static org.assertj.core.api.Assertions.assertThat;
2732

@@ -45,6 +50,29 @@ void convertWhenCanConvertDirectlySkipsStringConversion(ConversionService conver
4550
}
4651
}
4752

53+
@Test
54+
@SuppressWarnings("unchecked")
55+
void convertWhenTargetIsList() {
56+
ConversionService conversionService = new ApplicationConversionService();
57+
StringBuilder source = new StringBuilder("1,2,3");
58+
TypeDescriptor sourceType = TypeDescriptor.valueOf(StringBuilder.class);
59+
TypeDescriptor targetType = TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(String.class));
60+
List<String> conveted = (List<String>) conversionService.convert(source, sourceType, targetType);
61+
assertThat(conveted).containsExactly("1", "2", "3");
62+
}
63+
64+
@Test
65+
@SuppressWarnings("unchecked")
66+
void convertWhenTargetIsListAndNotUsingApplicationConversionService() {
67+
FormattingConversionService conversionService = new DefaultFormattingConversionService();
68+
conversionService.addConverter(new CharSequenceToObjectConverter(conversionService));
69+
StringBuilder source = new StringBuilder("1,2,3");
70+
TypeDescriptor sourceType = TypeDescriptor.valueOf(StringBuilder.class);
71+
TypeDescriptor targetType = TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(String.class));
72+
List<String> conveted = (List<String>) conversionService.convert(source, sourceType, targetType);
73+
assertThat(conveted).containsExactly("1", "2", "3");
74+
}
75+
4876
static Stream<? extends Arguments> conversionServices() {
4977
return ConversionServiceArguments.with((conversionService) -> {
5078
conversionService.addConverter(new StringToIntegerConverter());

0 commit comments

Comments
 (0)