Skip to content

Commit d1dbf2b

Browse files
committed
Implement substitution of generics
1 parent dc5f6d0 commit d1dbf2b

File tree

3 files changed

+224
-0
lines changed

3 files changed

+224
-0
lines changed

src/main/java/net/jodah/typetools/TypeResolver.java

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,14 @@
2828
import java.lang.reflect.ParameterizedType;
2929
import java.lang.reflect.Type;
3030
import java.lang.reflect.TypeVariable;
31+
import java.lang.reflect.WildcardType;
3132
import java.security.AccessController;
3233
import java.security.PrivilegedExceptionAction;
3334
import java.util.Arrays;
3435
import java.util.Collections;
3536
import java.util.HashMap;
3637
import java.util.Map;
38+
import java.util.Objects;
3739
import java.util.WeakHashMap;
3840

3941
import sun.misc.Unsafe;
@@ -56,6 +58,7 @@ public final class TypeResolver {
5658
private static final Map<String, Method> OBJECT_METHODS = new HashMap<String, Method>();
5759
private static final Map<Class<?>, Class<?>> PRIMITIVE_WRAPPERS;
5860
private static final Double JAVA_VERSION;
61+
private static final String NEW_ISSUE_URL = "https://github.com/jhalterman/typetools/issues/new";
5962

6063
static {
6164
JAVA_VERSION = Double.parseDouble(System.getProperty("java.specification.version", "0"));
@@ -115,6 +118,58 @@ private Unknown() {
115118
}
116119
}
117120

121+
private static class ResolvedParameterizedType implements ParameterizedType {
122+
private final ParameterizedType original;
123+
private final Type[] resolvedTypeArguments;
124+
125+
private ResolvedParameterizedType(ParameterizedType original, Type[] resolvedTypeArguments) {
126+
Objects.requireNonNull(original);
127+
Objects.requireNonNull(resolvedTypeArguments);
128+
129+
this.original = original;
130+
this.resolvedTypeArguments = resolvedTypeArguments;
131+
}
132+
133+
public Type[] getGenericActualTypeArguments() {
134+
return original.getActualTypeArguments();
135+
}
136+
137+
@Override
138+
public Type[] getActualTypeArguments() {
139+
return resolvedTypeArguments;
140+
}
141+
142+
@Override
143+
public Type getRawType() {
144+
return original.getRawType();
145+
}
146+
147+
@Override
148+
public Type getOwnerType() {
149+
return original.getOwnerType();
150+
}
151+
152+
@Override
153+
public boolean equals(Object o) {
154+
if (this == o) {
155+
return true;
156+
}
157+
if (o == null || getClass() != o.getClass()) {
158+
return false;
159+
}
160+
ResolvedParameterizedType that = (ResolvedParameterizedType) o;
161+
return original.equals(that.original) &&
162+
Arrays.equals(resolvedTypeArguments, that.resolvedTypeArguments);
163+
}
164+
165+
@Override
166+
public int hashCode() {
167+
int result = Objects.hash(original);
168+
result = 31 * result + Arrays.hashCode(resolvedTypeArguments);
169+
return result;
170+
}
171+
}
172+
118173
private TypeResolver() {
119174
}
120175

@@ -182,6 +237,14 @@ public static <T, S extends T> Class<?>[] resolveRawArguments(Class<T> type, Cla
182237
return resolveRawArguments(resolveGenericType(type, subType), subType);
183238
}
184239

240+
public static <T, S extends T> Type resolveType(Class<T> type, Class<S> subType) {
241+
return substituteGenerics(resolveGenericType(type, subType), getTypeVariableMap(subType, null));
242+
}
243+
244+
public static Type resolveType(Type type, Class<?> context) {
245+
return substituteGenerics(type, getTypeVariableMap(context, null));
246+
}
247+
185248
/**
186249
* Returns an array of raw classes representing arguments for the {@code genericType} using type variable information
187250
* from the {@code subType}. Arguments for {@code genericType} that cannot be resolved are returned as
@@ -334,6 +397,58 @@ private static Map<TypeVariable<?>, Type> getTypeVariableMap(final Class<?> targ
334397
return map;
335398
}
336399

400+
private static Type substituteGenerics(final Type genericType, final Map<TypeVariable<?>, Type> typeVariableMap) {
401+
if (genericType == null) {
402+
return null;
403+
}
404+
if (genericType instanceof Class<?>) {
405+
return genericType;
406+
}
407+
if (genericType instanceof TypeVariable<?>) {
408+
final TypeVariable<?> typeVariable = (TypeVariable<?>) genericType;
409+
final Type mapping = typeVariableMap.get(typeVariable);
410+
if (mapping != null) {
411+
return substituteGenerics(mapping, typeVariableMap);
412+
}
413+
final Type[] upperBounds = typeVariable.getBounds();
414+
// NOTE: According to https://docs.oracle.com/javase/tutorial/java/generics/bounded.html
415+
// if there are multiple upper bounds where one bound is a class, then this must be the
416+
// leftmost/first bound. Therefore we blindly take this one, hoping is the most relevant.
417+
return substituteGenerics(upperBounds[0], typeVariableMap);
418+
}
419+
if (genericType instanceof WildcardType) {
420+
final WildcardType wildcardType = (WildcardType) genericType;
421+
final Type[] upperBounds = wildcardType.getUpperBounds();
422+
final Type[] lowerBounds = wildcardType.getLowerBounds();
423+
if (upperBounds.length == 1 && lowerBounds.length == 0) {
424+
return substituteGenerics(upperBounds[0], typeVariableMap);
425+
}
426+
throw new UnsupportedOperationException(
427+
"Resolution of wildcard types is only supported for the trivial case of exactly one upper bound " +
428+
"and no lower bounds. If you require resolution in a more complex case, please file an issue via " +
429+
NEW_ISSUE_URL
430+
);
431+
}
432+
if (genericType instanceof ParameterizedType) {
433+
final ParameterizedType parameterizedType = (ParameterizedType) genericType;
434+
final Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
435+
final Type[] resolvedTypeArguments = new Type[actualTypeArguments.length];
436+
437+
boolean changed = false;
438+
for (int i = 0; i < actualTypeArguments.length; i++) {
439+
resolvedTypeArguments[i] = substituteGenerics(actualTypeArguments[i], typeVariableMap);
440+
changed = changed || (resolvedTypeArguments[i] != actualTypeArguments[i]);
441+
}
442+
443+
return changed ? new ResolvedParameterizedType(parameterizedType, resolvedTypeArguments) : parameterizedType;
444+
}
445+
throw new UnsupportedOperationException(
446+
"Cannot substitute generics for type with name '" + genericType.getTypeName() + "' and " +
447+
"class name '" + genericType.getClass().getName() + "'. Please file an issue including this message via " +
448+
NEW_ISSUE_URL
449+
);
450+
}
451+
337452
/**
338453
* Populates the {@code map} with with variable/argument pairs for the given {@code types}.
339454
*/

src/test/java/net/jodah/typetools/TypeResolverTest.java

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import java.io.Serializable;
77
import java.lang.reflect.Field;
88
import java.lang.reflect.Method;
9+
import java.lang.reflect.ParameterizedType;
910
import java.lang.reflect.Type;
1011
import java.util.ArrayList;
1112
import java.util.HashMap;
@@ -86,12 +87,53 @@ public void shouldResolveArgumentForList() {
8687
assertEquals(TypeResolver.resolveRawArgument(List.class, SomeList.class), Integer.class);
8788
}
8889

90+
public void shouldResolveTypeForList() {
91+
Type resolvedType = TypeResolver.resolveType(List.class, SomeList.class);
92+
assert resolvedType instanceof ParameterizedType;
93+
assertEquals(((ParameterizedType) resolvedType).getActualTypeArguments()[0], Integer.class);
94+
}
95+
8996
public void shouldResolveArgumentsForBazFromFoo() {
9097
Class<?>[] typeArguments = TypeResolver.resolveRawArguments(Baz.class, Foo.class);
9198
assert typeArguments[0] == HashSet.class;
9299
assert typeArguments[1] == ArrayList.class;
93100
}
94101

102+
public void shouldResolveParameterizedTypeForBazFromFoo() {
103+
Type type = TypeResolver.resolveType(Baz.class, Foo.class);
104+
105+
// Now we walk the type hierarchy:
106+
assert type instanceof ParameterizedType;
107+
Type[] typeArguments = ((ParameterizedType) type).getActualTypeArguments();
108+
109+
assert typeArguments[0] instanceof ParameterizedType;
110+
ParameterizedType firstTypeArgument = (ParameterizedType) typeArguments[0];
111+
assert firstTypeArgument.getRawType() == HashSet.class;
112+
assert firstTypeArgument.getActualTypeArguments()[0] == Object.class;
113+
114+
assert typeArguments[1] instanceof ParameterizedType;
115+
ParameterizedType secondTypeArgument = (ParameterizedType) typeArguments[1];
116+
assert secondTypeArgument.getRawType() == ArrayList.class;
117+
assert secondTypeArgument.getActualTypeArguments()[0] == Object.class;
118+
}
119+
120+
public void shouldResolvePartialParameterizedTypeForBazFromBar() {
121+
Type type = TypeResolver.resolveType(Baz.class, Bar.class);
122+
123+
assert type instanceof ParameterizedType;
124+
Type[] typeArguments = ((ParameterizedType) type).getActualTypeArguments();
125+
126+
assert typeArguments[0] instanceof ParameterizedType;
127+
ParameterizedType firstTypeArgument = (ParameterizedType) typeArguments[0];
128+
assert firstTypeArgument.getRawType() == HashSet.class;
129+
assert firstTypeArgument.getActualTypeArguments()[0] == Object.class;
130+
131+
assert typeArguments[1] instanceof ParameterizedType;
132+
ParameterizedType secondTypeArgument = (ParameterizedType) typeArguments[1];
133+
assert secondTypeArgument.getRawType() == List.class;
134+
assert secondTypeArgument.getActualTypeArguments()[0] == Object.class;
135+
}
136+
95137
public void shouldResolveArgumentsForIRepoFromRepoImplA() {
96138
Class<?>[] types = TypeResolver.resolveRawArguments(IRepo.class, RepoImplA.class);
97139
assertEquals(types[0], Map.class);
@@ -155,13 +197,45 @@ static class TypeArrayFixture<T> {
155197
static class TypeArrayImpl extends TypeArrayFixture<String> {
156198
}
157199

200+
static class TypeListFixture<T> {
201+
List<T> testList;
202+
T testPlain;
203+
}
204+
205+
static class TypeListImpl extends TypeListFixture<String> {
206+
}
207+
158208
public void shouldResolveGenericTypeArray() throws Throwable {
159209
Type arrayField = TypeArrayFixture.class.getDeclaredField("test").getGenericType();
160210

161211
Class<?> arg = TypeResolver.resolveRawClass(arrayField, TypeArrayImpl.class);
162212
assertEquals(arg, String[].class);
163213
}
164214

215+
public void shouldResolveRawTypeList() throws Throwable {
216+
Type listField = TypeListFixture.class.getDeclaredField("testList").getGenericType();
217+
218+
Class<?> arg = TypeResolver.resolveRawClass(listField, TypeListImpl.class);
219+
assertEquals(arg, List.class);
220+
}
221+
222+
public void shouldResolveTypeList() throws Throwable {
223+
Type listField = TypeListFixture.class.getDeclaredField("testList").getGenericType();
224+
225+
Type arg = TypeResolver.resolveType(listField, TypeListImpl.class);
226+
assert arg instanceof ParameterizedType;
227+
ParameterizedType parameterizedType = (ParameterizedType) arg;
228+
assertEquals(parameterizedType.getRawType(), List.class);
229+
assertEquals(parameterizedType.getActualTypeArguments()[0], String.class);
230+
}
231+
232+
public void shouldResolveTypePlain() throws Throwable {
233+
Type plainField = TypeListFixture.class.getDeclaredField("testPlain").getGenericType();
234+
235+
Type arg = TypeResolver.resolveType(plainField, TypeListImpl.class);
236+
assert arg == String.class;
237+
}
238+
165239
public void shouldReturnNullOnResolveArgumentsForNonParameterizedType() {
166240
assertNull(TypeResolver.resolveRawArguments(Object.class, String.class));
167241
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package net.jodah.typetools.issues;
2+
3+
import net.jodah.typetools.TypeResolver;
4+
import org.testng.annotations.Test;
5+
6+
import java.lang.reflect.ParameterizedType;
7+
import java.lang.reflect.Type;
8+
import java.util.List;
9+
10+
import static org.testng.Assert.assertEquals;
11+
import static org.testng.Assert.assertTrue;
12+
13+
/**
14+
* https://github.com/jhalterman/typetools/issues/8
15+
*/
16+
@Test
17+
public class Issue8 {
18+
interface Foo<T1, T2> {
19+
}
20+
21+
class Bar implements Foo<List<Integer>, List<String>> {
22+
}
23+
24+
public void test() {
25+
Type typeArgs = TypeResolver.resolveType(Foo.class, Bar.class);
26+
assertTrue(typeArgs instanceof ParameterizedType);
27+
ParameterizedType par = (ParameterizedType) typeArgs;
28+
assertEquals(par.getRawType(), Foo.class);
29+
assertEquals(par.getActualTypeArguments().length, 2);
30+
assertTrue(par.getActualTypeArguments()[0] instanceof ParameterizedType);
31+
ParameterizedType firstArg = (ParameterizedType) par.getActualTypeArguments()[0];
32+
assertEquals(firstArg.getRawType(), List.class);
33+
assertEquals(firstArg.getActualTypeArguments()[0], Integer.class);
34+
}
35+
}

0 commit comments

Comments
 (0)