Merge branch '2.4.x'

Closes gh-25060
This commit is contained in:
Phillip Webb 2021-01-30 12:35:52 -08:00
commit d8c9b8c329
4 changed files with 109 additions and 6 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2021 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.
@ -21,9 +21,11 @@ import java.util.Set;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterRegistry;
import org.springframework.core.convert.converter.GenericConverter;
import org.springframework.core.convert.converter.GenericConverter.ConvertiblePair;
import org.springframework.core.convert.support.ConfigurableConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.format.Formatter;
@ -61,6 +63,28 @@ public class ApplicationConversionService extends FormattingConversionService {
configure(this);
}
/**
* Return {@code true} if objects of {@code sourceType} can be converted to the
* {@code targetType} and the converter has {@code Object.class} as a supported source
* type.
* @param sourceType the source type to test
* @param targetType the target type to test
* @return is conversion happens via an {@code ObjectTo...} converter
* @since 2.4.3
*/
public boolean isConvertViaObjectSourceType(TypeDescriptor sourceType, TypeDescriptor targetType) {
GenericConverter converter = getConverter(sourceType, targetType);
Set<ConvertiblePair> pairs = (converter != null) ? converter.getConvertibleTypes() : null;
if (pairs != null) {
for (ConvertiblePair pair : pairs) {
if (Object.class.equals(pair.getSourceType())) {
return true;
}
}
}
return false;
}
/**
* Return a shared default application {@code ConversionService} instance, lazily
* building it once needed.

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2021 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.
@ -33,6 +33,8 @@ class CharSequenceToObjectConverter 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<>();
@ -59,14 +61,41 @@ class CharSequenceToObjectConverter implements ConditionalGenericConverter {
}
this.disable.set(Boolean.TRUE);
try {
return !this.conversionService.canConvert(sourceType, targetType)
&& this.conversionService.canConvert(STRING, targetType);
boolean canDirectlyConvertCharSequence = this.conversionService.canConvert(sourceType, targetType);
if (canDirectlyConvertCharSequence && !isStringConversionBetter(sourceType, targetType)) {
return false;
}
return this.conversionService.canConvert(STRING, targetType);
}
finally {
this.disable.set(null);
}
}
/**
* 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 id string conversion is better
*/
private boolean isStringConversionBetter(TypeDescriptor sourceType, TypeDescriptor targetType) {
if (this.conversionService instanceof ApplicationConversionService) {
ApplicationConversionService applicationConversionService = (ApplicationConversionService) this.conversionService;
if (applicationConversionService.isConvertViaObjectSourceType(sourceType, targetType)) {
// If and 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);

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2021 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.
@ -17,6 +17,7 @@
package org.springframework.boot.convert;
import java.text.ParseException;
import java.util.List;
import java.util.Locale;
import java.util.Set;
@ -32,6 +33,7 @@ import org.springframework.format.FormatterRegistry;
import org.springframework.format.Parser;
import org.springframework.format.Printer;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
@ -91,6 +93,26 @@ class ApplicationConversionServiceTests {
}
}
@Test
void isConvertViaObjectSourceTypeWhenObjectSourceReturnsTrue() {
// Uses ObjectToCollectionConverter
ApplicationConversionService conversionService = new ApplicationConversionService();
TypeDescriptor sourceType = TypeDescriptor.valueOf(Long.class);
TypeDescriptor targetType = TypeDescriptor.valueOf(List.class);
assertThat(conversionService.canConvert(sourceType, targetType)).isTrue();
assertThat(conversionService.isConvertViaObjectSourceType(sourceType, targetType)).isTrue();
}
@Test
void isConvertViaObjectSourceTypeWhenNotObjectSourceReturnsFalse() {
// Uses StringToCollectionConverter
ApplicationConversionService conversionService = new ApplicationConversionService();
TypeDescriptor sourceType = TypeDescriptor.valueOf(String.class);
TypeDescriptor targetType = TypeDescriptor.valueOf(List.class);
assertThat(conversionService.canConvert(sourceType, targetType)).isTrue();
assertThat(conversionService.isConvertViaObjectSourceType(sourceType, targetType)).isFalse();
}
static class ExampleGenericConverter implements GenericConverter {
@Override

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2021 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.
@ -16,12 +16,17 @@
package org.springframework.boot.convert;
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.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;
@ -45,6 +50,29 @@ class CharSequenceToObjectConverterTests {
}
}
@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> conveted = (List<String>) conversionService.convert(source, sourceType, targetType);
assertThat(conveted).containsExactly("1", "2", "3");
}
@Test
@SuppressWarnings("unchecked")
void convertWhenTargetIsListAndNotUsingApplicationConversionService() {
FormattingConversionService conversionService = new DefaultFormattingConversionService();
conversionService.addConverter(new CharSequenceToObjectConverter(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> conveted = (List<String>) conversionService.convert(source, sourceType, targetType);
assertThat(conveted).containsExactly("1", "2", "3");
}
static Stream<? extends Arguments> conversionServices() {
return ConversionServiceArguments.with((conversionService) -> {
conversionService.addConverter(new StringToIntegerConverter());