Skip to content

Commit 8c49c17

Browse files
committed
Additional test coverage
1 parent 545c832 commit 8c49c17

File tree

2 files changed

+187
-13
lines changed

2 files changed

+187
-13
lines changed

src/main/java/com/fasterxml/jackson/databind/introspect/MethodGenericTypeResolver.java

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -35,25 +35,39 @@ static TypeResolutionContext narrowMethodTypeParameters(
3535
JavaType requestedType,
3636
TypeFactory typeFactory,
3737
TypeResolutionContext emptyTypeResCtxt) {
38+
TypeBindings newTypeBindings = bindMethodTypeParameters(candidate, requestedType, emptyTypeResCtxt);
39+
return newTypeBindings == null
40+
? emptyTypeResCtxt
41+
: new TypeResolutionContext.Basic(typeFactory, newTypeBindings);
42+
}
43+
44+
/**
45+
* Returns {@link TypeBindings} with additional type information
46+
* based on {@code requestedType} if possible, otherwise {@code null}.
47+
*/
48+
static TypeBindings bindMethodTypeParameters(
49+
Method candidate,
50+
JavaType requestedType,
51+
TypeResolutionContext emptyTypeResCtxt) {
3852
TypeVariable<Method>[] methodTypeParameters = candidate.getTypeParameters();
3953
if (methodTypeParameters.length == 0
4054
// If the primary type has no type parameters, there's nothing to do
4155
|| requestedType.getBindings().isEmpty()) {
4256
// Method has no type parameters: no need to modify the resolution context.
43-
return emptyTypeResCtxt;
57+
return null;
4458
}
4559
Type genericReturnType = candidate.getGenericReturnType();
4660
if (!(genericReturnType instanceof ParameterizedType)) {
4761
// Return value is not parameterized, it cannot be used to associate the requestedType expectations
4862
// onto parameters.
49-
return emptyTypeResCtxt;
63+
return null;
5064
}
5165

5266
ParameterizedType parameterizedGenericReturnType = (ParameterizedType) genericReturnType;
5367
// Primary type and result type must be the same class, otherwise we would need to
5468
// trace generic parameters to a common superclass or interface.
5569
if (!Objects.equals(requestedType.getRawClass(), parameterizedGenericReturnType.getRawType())) {
56-
return emptyTypeResCtxt;
70+
return null;
5771
}
5872

5973
// Construct TypeBindings based on the requested type, and type variables that occur in the generic return type.
@@ -63,30 +77,31 @@ static TypeResolutionContext narrowMethodTypeParameters(
6377
Type[] methodReturnTypeArguments = parameterizedGenericReturnType.getActualTypeArguments();
6478
ArrayList<String> names = new ArrayList<>(methodTypeParameters.length);
6579
ArrayList<JavaType> types = new ArrayList<>(methodTypeParameters.length);
66-
for (int j = 0; j < methodReturnTypeArguments.length; j++) {
67-
Type methodReturnTypeArgument = methodReturnTypeArguments[j];
68-
// Note: This strictly supports only TypeVariables, not wildcards with nested type variables
80+
for (int i = 0; i < methodReturnTypeArguments.length; i++) {
81+
Type methodReturnTypeArgument = methodReturnTypeArguments[i];
82+
// Note: This strictly supports only TypeVariables of the forms "T" and "? extends T",
83+
// not complex wildcards with nested type variables
6984
TypeVariable<?> typeVar = maybeGetTypeVariable(methodReturnTypeArgument);
7085
if (typeVar != null) {
7186
String typeParameterName = typeVar.getName();
7287
if (typeParameterName == null) {
73-
return emptyTypeResCtxt;
88+
return null;
7489
}
7590

7691
// Avoid duplicates
7792
if (names.contains(typeParameterName)) {
7893
continue;
7994
}
8095

81-
JavaType bindTarget = requestedType.getBindings().getBoundType(j);
96+
JavaType bindTarget = requestedType.getBindings().getBoundType(i);
8297
if (bindTarget == null) {
83-
return emptyTypeResCtxt;
98+
return null;
8499
}
85100
// If the type parameter name is not present in the method type parameters we
86101
// fall back to default type handling.
87102
TypeVariable<?> methodTypeVariable = findByName(methodTypeParameters, typeParameterName);
88103
if (methodTypeVariable == null) {
89-
return emptyTypeResCtxt;
104+
return null;
90105
}
91106
if (pessimisticallyValidateBounds(emptyTypeResCtxt, bindTarget, methodTypeVariable.getBounds())) {
92107
names.add(methodTypeVariable.getName());
@@ -96,10 +111,9 @@ static TypeResolutionContext narrowMethodTypeParameters(
96111
}
97112
// Fall back to default handling if no specific types from the requestedType are used
98113
if (names.isEmpty()) {
99-
return emptyTypeResCtxt;
114+
return null;
100115
}
101-
TypeBindings newTypeBindings = TypeBindings.create(names, types);
102-
return new TypeResolutionContext.Basic(typeFactory, newTypeBindings);
116+
return TypeBindings.create(names, types);
103117
}
104118

105119
/* Returns the TypeVariable if it can be extracted, otherwise null. */
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
package com.fasterxml.jackson.databind.introspect;
2+
3+
import com.fasterxml.jackson.core.type.TypeReference;
4+
import com.fasterxml.jackson.databind.JavaType;
5+
import com.fasterxml.jackson.databind.type.TypeBindings;
6+
import com.fasterxml.jackson.databind.type.TypeFactory;
7+
import org.junit.Test;
8+
9+
import java.lang.reflect.Method;
10+
import java.lang.reflect.Modifier;
11+
import java.lang.reflect.Type;
12+
import java.util.Collections;
13+
import java.util.HashMap;
14+
import java.util.List;
15+
import java.util.Map;
16+
import java.util.Optional;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
20+
public class MethodGenericTypeResolverTest {
21+
22+
private static final TypeResolutionContext EMPTY_CONTEXT =
23+
new TypeResolutionContext.Empty(TypeFactory.defaultInstance());
24+
25+
public static <T> Optional<T> simple(T input) {
26+
throw new UnsupportedOperationException();
27+
}
28+
29+
public static Optional<?> noGenerics(String input) {
30+
throw new UnsupportedOperationException();
31+
}
32+
33+
public static <T> Map<T, T> mapWithSameKeysAndValues(List<T> input) {
34+
throw new UnsupportedOperationException();
35+
}
36+
37+
public static <T> Map<?, ?> disconnected(List<T> input) {
38+
throw new UnsupportedOperationException();
39+
}
40+
41+
public static <A, B> Map<A, B> multipleTypeVariables(Map<A, B> input) {
42+
throw new UnsupportedOperationException();
43+
}
44+
45+
public static <A, B> Map<? extends A, ? extends B> multipleTypeVariablesWithUpperBound(Map<A, B> input) {
46+
throw new UnsupportedOperationException();
47+
}
48+
49+
@Test
50+
public void testWithoutGenerics() {
51+
TypeBindings bindings = MethodGenericTypeResolver.bindMethodTypeParameters(
52+
method("noGenerics"), type(String.class), EMPTY_CONTEXT);
53+
assertThat(bindings).isNull();
54+
}
55+
56+
@Test
57+
public void testWithoutGenericsInResult() {
58+
TypeBindings bindings = MethodGenericTypeResolver.bindMethodTypeParameters(
59+
method("simple"), type(Optional.class), EMPTY_CONTEXT);
60+
assertThat(bindings).isNull();
61+
}
62+
63+
@Test
64+
public void testResultDoesNotUseTypeVariables() {
65+
TypeBindings bindings = MethodGenericTypeResolver.bindMethodTypeParameters(
66+
method("disconnected"), type(new TypeReference<Map<String, String>>() {
67+
}), EMPTY_CONTEXT);
68+
assertThat(bindings).isNull();
69+
}
70+
71+
@Test
72+
public void testWithoutGenericsInMethod() {
73+
TypeBindings bindings = MethodGenericTypeResolver.bindMethodTypeParameters(
74+
method("noGenerics"), type(new TypeReference<Map<String, String>>() {
75+
}), EMPTY_CONTEXT);
76+
assertThat(bindings).isNull();
77+
}
78+
79+
@Test
80+
public void testWithRepeatedGenericInReturn() {
81+
TypeBindings bindings = MethodGenericTypeResolver.bindMethodTypeParameters(
82+
method("mapWithSameKeysAndValues"), type(new TypeReference<Map<String, String>>() {
83+
}), EMPTY_CONTEXT);
84+
assertThat(asMap(bindings)).isEqualTo(asMap("T", type(String.class)));
85+
}
86+
87+
@Test
88+
public void testMultipleTypeVariables() {
89+
TypeBindings bindings = MethodGenericTypeResolver.bindMethodTypeParameters(
90+
method("multipleTypeVariables"), type(new TypeReference<Map<Integer, Long>>() {
91+
}), EMPTY_CONTEXT);
92+
assertThat(asMap(bindings)).isEqualTo(asMap(
93+
"A", type(Integer.class),
94+
"B", type(Long.class)));
95+
}
96+
97+
@Test
98+
public void testMultipleTypeVariablesWithUpperBounds() {
99+
TypeBindings bindings = MethodGenericTypeResolver.bindMethodTypeParameters(
100+
method("multipleTypeVariablesWithUpperBound"), type(new TypeReference<Map<Integer, Long>>() {
101+
}), EMPTY_CONTEXT);
102+
assertThat(asMap(bindings)).isEqualTo(asMap(
103+
"A", type(Integer.class),
104+
"B", type(Long.class)));
105+
}
106+
107+
@Test
108+
public void testResultTypeDoesNotExactlyMatch() {
109+
TypeBindings bindings = MethodGenericTypeResolver.bindMethodTypeParameters(
110+
method("multipleTypeVariables"), type(new TypeReference<HashMap<Integer, Long>>() {
111+
}), EMPTY_CONTEXT);
112+
// Mapping the result to a common supertype is not supported.
113+
assertThat(bindings).isNull();
114+
}
115+
116+
private static Method method(String name) {
117+
Method result = null;
118+
for (Method method : MethodGenericTypeResolverTest.class.getMethods()) {
119+
if (Modifier.isStatic(method.getModifiers()) && name.equals(method.getName())) {
120+
if (result != null) {
121+
throw new AssertionError("Multiple methods discovered with name "
122+
+ name + ": " + result + " and " + method);
123+
}
124+
result = method;
125+
}
126+
}
127+
assertThat(result).as("Failed to find method named '%s'", name).isNotNull();
128+
return result;
129+
}
130+
131+
private static JavaType type(TypeReference<?> reference) {
132+
return type(reference.getType());
133+
}
134+
135+
private static JavaType type(Type type) {
136+
return EMPTY_CONTEXT.resolveType(type);
137+
}
138+
139+
private static Map<String, JavaType> asMap(TypeBindings bindings) {
140+
assertThat(bindings).isNotNull();
141+
Map<String, JavaType> result = new HashMap<>(bindings.size());
142+
for (int i = 0; i < bindings.size(); i++) {
143+
result.put(bindings.getBoundName(i), bindings.getBoundType(i));
144+
}
145+
assertThat(result).hasSize(bindings.size());
146+
return result;
147+
}
148+
149+
private static Map<String, JavaType> asMap(String name, JavaType javaType) {
150+
return Collections.singletonMap(name, javaType);
151+
}
152+
153+
private static Map<String, JavaType> asMap(
154+
String name0, JavaType javaType0, String name1, JavaType javaType1) {
155+
Map<String, JavaType> result = new HashMap<>(2);
156+
result.put(name0, javaType0);
157+
result.put(name1, javaType1);
158+
return result;
159+
}
160+
}

0 commit comments

Comments
 (0)