Skip to content

Commit 452f1b8

Browse files
committed
Consider return type of static methods in ObjectToObjectConverter
Prior to this commit, ObjectToObjectConverter considered the return type of non-static `to[targetType.simpleName]()` methods but did not consider the return type of static `valueOf(sourceType)`, `of(sourceType)`, and `from(sourceType)` methods. This led to scenarios in which `canConvert()` returned `true`, but a subsequent `convert()` invocation resulted in a ConverterNotFoundException, which violates the contract of the converter. This commit addresses this issue by taking into account the return type of a static valueOf/of/from factory method when determining if the ObjectToObjectConverter supports a particular conversion. Whereas the existing check in `determineToMethod()` ensures that the method return type is assignable to the `targetType`, the new check in `determineFactoryMethod()` leniently ensures that the method return type and `targetType` are "related" (i.e., reside in the same type hierarchy). Closes gh-28609
1 parent c278d8c commit 452f1b8

File tree

3 files changed

+148
-5
lines changed

3 files changed

+148
-5
lines changed

spring-core/src/main/java/org/springframework/core/convert/support/ObjectToObjectConverter.java

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2017 the original author or authors.
2+
* Copyright 2002-2022 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.
@@ -41,15 +41,21 @@
4141
* <h3>Conversion Algorithm</h3>
4242
* <ol>
4343
* <li>Invoke a non-static {@code to[targetType.simpleName]()} method on the
44-
* source object that has a return type equal to {@code targetType}, if such
44+
* source object that has a return type assignable to {@code targetType}, if such
4545
* a method exists. For example, {@code org.example.Bar Foo#toBar()} is a
4646
* method that follows this convention.
4747
* <li>Otherwise invoke a <em>static</em> {@code valueOf(sourceType)} or Java
4848
* 8 style <em>static</em> {@code of(sourceType)} or {@code from(sourceType)}
49-
* method on the {@code targetType}, if such a method exists.
49+
* method on the {@code targetType} that has a return type <em>related</em> to
50+
* {@code targetType}, if such a method exists. For example, a static
51+
* {@code Foo.of(sourceType)} method that returns a {@code Foo},
52+
* {@code SuperFooType}, or {@code SubFooType} is a method that follows this
53+
* convention. {@link java.time.ZoneId#of(String)} is a concrete example of
54+
* such a static factory method which returns a subtype of {@code ZoneId}.
5055
* <li>Otherwise invoke a constructor on the {@code targetType} that accepts
5156
* a single {@code sourceType} argument, if such a constructor exists.
52-
* <li>Otherwise throw a {@link ConversionFailedException}.
57+
* <li>Otherwise throw a {@link ConversionFailedException} or
58+
* {@link IllegalStateException}.
5359
* </ol>
5460
*
5561
* <p><strong>Warning</strong>: this converter does <em>not</em> support the
@@ -193,7 +199,18 @@ private static Method determineFactoryMethod(Class<?> targetClass, Class<?> sour
193199
method = ClassUtils.getStaticMethod(targetClass, "from", sourceClass);
194200
}
195201
}
196-
return method;
202+
203+
return (method != null && areRelatedTypes(targetClass, method.getReturnType()) ? method : null);
204+
}
205+
206+
/**
207+
* Determine if the two types reside in the same type hierarchy (i.e., type 1
208+
* is assignable to type 2 or vice versa).
209+
* @since 5.3.21
210+
* @see ClassUtils#isAssignable(Class, Class)
211+
*/
212+
private static boolean areRelatedTypes(Class<?> type1, Class<?> type2) {
213+
return (ClassUtils.isAssignable(type1, type2) || ClassUtils.isAssignable(type2, type1));
197214
}
198215

199216
@Nullable

spring-core/src/test/java/org/springframework/core/convert/converter/DefaultConversionServiceTests.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -824,6 +824,9 @@ void convertObjectToStringWithValueOfMethodPresentUsingToString() {
824824
assertThat(ISBN.toStringCount).as("toString() invocations").isEqualTo(1);
825825
}
826826

827+
/**
828+
* @see org.springframework.core.convert.support.ObjectToObjectConverterTests
829+
*/
827830
@Test
828831
void convertObjectToObjectUsingValueOfMethod() {
829832
ISBN.reset();
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/*
2+
* Copyright 2002-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.core.convert.support;
18+
19+
import java.util.Optional;
20+
21+
import org.junit.jupiter.api.Test;
22+
23+
import org.springframework.core.convert.ConverterNotFoundException;
24+
25+
import static org.assertj.core.api.Assertions.assertThat;
26+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
27+
28+
/**
29+
* Unit tests for {@link ObjectToObjectConverter}.
30+
*
31+
* @author Sam Brannen
32+
* @author Phil Webb
33+
* @since 5.3.21
34+
* @see org.springframework.core.convert.converter.DefaultConversionServiceTests#convertObjectToObjectUsingValueOfMethod()
35+
*/
36+
class ObjectToObjectConverterTests {
37+
38+
private final GenericConversionService conversionService = new GenericConversionService() {{
39+
addConverter(new ObjectToObjectConverter());
40+
}};
41+
42+
43+
/**
44+
* This test effectively verifies that the {@link ObjectToObjectConverter}
45+
* was properly registered with the {@link GenericConversionService}.
46+
*/
47+
@Test
48+
void nonStaticToTargetTypeSimpleNameMethodWithMatchingReturnType() {
49+
assertThat(conversionService.canConvert(Source.class, Data.class))
50+
.as("can convert Source to Data").isTrue();
51+
Data data = conversionService.convert(new Source("test"), Data.class);
52+
assertThat(data).asString().isEqualTo("test");
53+
}
54+
55+
@Test
56+
void nonStaticToTargetTypeSimpleNameMethodWithDifferentReturnType() {
57+
assertThat(conversionService.canConvert(Text.class, Data.class))
58+
.as("can convert Text to Data").isFalse();
59+
assertThat(conversionService.canConvert(Text.class, Optional.class))
60+
.as("can convert Text to Optional").isFalse();
61+
assertThatExceptionOfType(ConverterNotFoundException.class)
62+
.as("convert Text to Data")
63+
.isThrownBy(() -> conversionService.convert(new Text("test"), Data.class));
64+
}
65+
66+
@Test
67+
void staticValueOfFactoryMethodWithDifferentReturnType() {
68+
assertThat(conversionService.canConvert(String.class, Data.class))
69+
.as("can convert String to Data").isFalse();
70+
assertThatExceptionOfType(ConverterNotFoundException.class)
71+
.as("convert String to Data")
72+
.isThrownBy(() -> conversionService.convert("test", Data.class));
73+
}
74+
75+
76+
static class Source {
77+
78+
private final String value;
79+
80+
private Source(String value) {
81+
this.value = value;
82+
}
83+
84+
public Data toData() {
85+
return new Data(this.value);
86+
}
87+
88+
}
89+
90+
static class Text {
91+
92+
private final String value;
93+
94+
private Text(String value) {
95+
this.value = value;
96+
}
97+
98+
public Optional<Data> toData() {
99+
return Optional.of(new Data(this.value));
100+
}
101+
102+
}
103+
104+
static class Data {
105+
106+
private final String value;
107+
108+
private Data(String value) {
109+
this.value = value;
110+
}
111+
112+
@Override
113+
public String toString() {
114+
return this.value;
115+
}
116+
117+
public static Optional<Data> valueOf(String string) {
118+
return (string != null) ? Optional.of(new Data(string)) : Optional.empty();
119+
}
120+
121+
}
122+
123+
}

0 commit comments

Comments
 (0)