Merge branch '5.3.x'

This commit is contained in:
Sam Brannen 2022-06-13 17:23:14 +02:00
commit c3b29960ed
3 changed files with 147 additions and 4 deletions

View File

@ -41,15 +41,21 @@ import org.springframework.util.ReflectionUtils;
* <h3>Conversion Algorithm</h3>
* <ol>
* <li>Invoke a non-static {@code to[targetType.simpleName]()} method on the
* source object that has a return type equal to {@code targetType}, if such
* source object that has a return type assignable to {@code targetType}, if such
* a method exists. For example, {@code org.example.Bar Foo#toBar()} is a
* method that follows this convention.
* <li>Otherwise invoke a <em>static</em> {@code valueOf(sourceType)} or Java
* 8 style <em>static</em> {@code of(sourceType)} or {@code from(sourceType)}
* method on the {@code targetType}, if such a method exists.
* method on the {@code targetType} that has a return type <em>related</em> to
* {@code targetType}, if such a method exists. For example, a static
* {@code Foo.of(sourceType)} method that returns a {@code Foo},
* {@code SuperFooType}, or {@code SubFooType} is a method that follows this
* convention. {@link java.time.ZoneId#of(String)} is a concrete example of
* such a static factory method which returns a subtype of {@code ZoneId}.
* <li>Otherwise invoke a constructor on the {@code targetType} that accepts
* a single {@code sourceType} argument, if such a constructor exists.
* <li>Otherwise throw a {@link ConversionFailedException}.
* <li>Otherwise throw a {@link ConversionFailedException} or
* {@link IllegalStateException}.
* </ol>
*
* <p><strong>Warning</strong>: this converter does <em>not</em> support the
@ -189,7 +195,18 @@ final class ObjectToObjectConverter implements ConditionalGenericConverter {
method = ClassUtils.getStaticMethod(targetClass, "from", sourceClass);
}
}
return method;
return (method != null && areRelatedTypes(targetClass, method.getReturnType()) ? method : null);
}
/**
* Determine if the two types reside in the same type hierarchy (i.e., type 1
* is assignable to type 2 or vice versa).
* @since 5.3.21
* @see ClassUtils#isAssignable(Class, Class)
*/
private static boolean areRelatedTypes(Class<?> type1, Class<?> type2) {
return (ClassUtils.isAssignable(type1, type2) || ClassUtils.isAssignable(type2, type1));
}
@Nullable

View File

@ -824,6 +824,9 @@ class DefaultConversionServiceTests {
assertThat(ISBN.toStringCount).as("toString() invocations").isEqualTo(1);
}
/**
* @see org.springframework.core.convert.support.ObjectToObjectConverterTests
*/
@Test
void convertObjectToObjectUsingValueOfMethod() {
ISBN.reset();

View File

@ -0,0 +1,123 @@
/*
* Copyright 2002-2022 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.core.convert.support;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.springframework.core.convert.ConverterNotFoundException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* Unit tests for {@link ObjectToObjectConverter}.
*
* @author Sam Brannen
* @author Phil Webb
* @since 5.3.21
* @see org.springframework.core.convert.converter.DefaultConversionServiceTests#convertObjectToObjectUsingValueOfMethod()
*/
class ObjectToObjectConverterTests {
private final GenericConversionService conversionService = new GenericConversionService() {{
addConverter(new ObjectToObjectConverter());
}};
/**
* This test effectively verifies that the {@link ObjectToObjectConverter}
* was properly registered with the {@link GenericConversionService}.
*/
@Test
void nonStaticToTargetTypeSimpleNameMethodWithMatchingReturnType() {
assertThat(conversionService.canConvert(Source.class, Data.class))
.as("can convert Source to Data").isTrue();
Data data = conversionService.convert(new Source("test"), Data.class);
assertThat(data).asString().isEqualTo("test");
}
@Test
void nonStaticToTargetTypeSimpleNameMethodWithDifferentReturnType() {
assertThat(conversionService.canConvert(Text.class, Data.class))
.as("can convert Text to Data").isFalse();
assertThat(conversionService.canConvert(Text.class, Optional.class))
.as("can convert Text to Optional").isFalse();
assertThatExceptionOfType(ConverterNotFoundException.class)
.as("convert Text to Data")
.isThrownBy(() -> conversionService.convert(new Text("test"), Data.class));
}
@Test
void staticValueOfFactoryMethodWithDifferentReturnType() {
assertThat(conversionService.canConvert(String.class, Data.class))
.as("can convert String to Data").isFalse();
assertThatExceptionOfType(ConverterNotFoundException.class)
.as("convert String to Data")
.isThrownBy(() -> conversionService.convert("test", Data.class));
}
static class Source {
private final String value;
private Source(String value) {
this.value = value;
}
public Data toData() {
return new Data(this.value);
}
}
static class Text {
private final String value;
private Text(String value) {
this.value = value;
}
public Optional<Data> toData() {
return Optional.of(new Data(this.value));
}
}
static class Data {
private final String value;
private Data(String value) {
this.value = value;
}
@Override
public String toString() {
return this.value;
}
public static Optional<Data> valueOf(String string) {
return (string != null) ? Optional.of(new Data(string)) : Optional.empty();
}
}
}