Skip to content

Commit 2c09039

Browse files
committed
fix broken generic cases; add root type generics resolution from bounds
1 parent 20bb08b commit 2c09039

File tree

14 files changed

+152
-14
lines changed

14 files changed

+152
-14
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
* Root class generics now resolved (from generic bounds)
2+
* Support broken hierarchies parsing (when root class generic passed or when target class did not set generics (as with root generics resolved from signature))
3+
14
### 1.1.0 (2014-12-15)
25

36
* Add wildcards support

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@ Class hierarchy needs to be parsed to properly resolve all generics:
6767
GenericsContext context = GenericsResolver.resolve(Root.class)
6868
```
6969

70-
If root class also contains generics, they will not be resolved (it's impossible).
70+
If root class also contains generics, they are resolved by generic bound (e.g. `<T extends Model>` will be resolved as T=Model.class, and
71+
resolved as Object.class when no bounds set)
7172

7273
Resolved class hierarchy is cached internally, so it's cheap to resolve single class few times.
7374

@@ -104,7 +105,7 @@ All classes in root class hierarchy may be obtained like this:
104105
context.getGenericsInfo().getComposingTypes()
105106
```
106107

107-
This will be all classes (and interfaces) in hierarchy, even if they not contain generics.
108+
This will be all classes and interfaces in hierarchy (including root class), even if they not contain generics.
108109

109110
#### Obtaining class generics
110111

src/main/java/ru/vyarus/java/generics/resolver/GenericsResolver.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ private GenericsResolver() {
1515
}
1616

1717
/**
18-
* By default returned context set on root class, which can't be introspected (because only parent class hods
19-
* generics information for current class). To use it switch context to required type from hierarchy:
18+
* By default returned context set on root class (but generic types for root class will be resolved from specified
19+
* generics bounds). To use it switch context to required type from hierarchy:
2020
* {@code returnedContext.type(SomeTypeFromHierarchy.class)}.
2121
* <p>Note: when ignore classes provided, produced {@code GenericsInfo} instance will not be cached
2222
* (and full version from cache will not be used also)</p>

src/main/java/ru/vyarus/java/generics/resolver/context/GenericsContext.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,7 @@ public GenericsContext(final GenericsInfo genericsInfo, final Class<?> type) {
3838
this.genericsInfo = genericsInfo;
3939
this.currentType = type;
4040
// collection resolved for fail fast on wrong type
41-
typeGenerics = type == genericsInfo.getRootClass() ? Collections.<String, Type>emptyMap()
42-
: genericsInfo.getTypeGenerics(type);
41+
typeGenerics = genericsInfo.getTypeGenerics(type);
4342
}
4443

4544
/**

src/main/java/ru/vyarus/java/generics/resolver/util/GenericInfoUtils.java

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@
1414
* @author Vyacheslav Rusakov
1515
* @since 15.12.2014
1616
*/
17+
// LinkedHashMap used instead of usual map to avoid accidental simple map usage (order is important!)
18+
@SuppressWarnings("PMD.LooseCoupling")
1719
public final class GenericInfoUtils {
18-
@SuppressWarnings("PMD.LooseCoupling")
20+
1921
private static final LinkedHashMap<String, Type> EMPTY_MAP = new LinkedHashMap<String, Type>(0);
2022
private static final String GROOVY_OBJECT = "GroovyObject";
2123

@@ -25,6 +27,12 @@ private GenericInfoUtils() {
2527
public static GenericsInfo create(final Class<?> type, final Class<?>... ignoreClasses) {
2628
final Map<Class<?>, LinkedHashMap<String, Type>> generics =
2729
new HashMap<Class<?>, LinkedHashMap<String, Type>>();
30+
if (type.getTypeParameters().length > 0) {
31+
// special case: root class also contains generics
32+
generics.put(type, resolveRawGenerics(type.getTypeParameters()));
33+
} else {
34+
generics.put(type, EMPTY_MAP);
35+
}
2836
analyzeType(generics, type, Arrays.asList(ignoreClasses));
2937
return new GenericsInfo(type, generics);
3038
}
@@ -63,6 +71,9 @@ private static void analyzeInterface(final Map<Class<?>, LinkedHashMap<String, T
6371
+ "can't properly resolve generics.", interfaceType.getName()));
6472
}
6573
types.put(interfaceType, generics);
74+
} else if (interfaceType.getTypeParameters().length > 0) {
75+
// root class didn't declare generics
76+
types.put(interfaceType, resolveRawGenerics(interfaceType.getTypeParameters()));
6677
} else if (!GROOVY_OBJECT.equals(interfaceType.getSimpleName())) {
6778
// avoid groovy specific interface (all groovy objects implements it)
6879
types.put(interfaceType, EMPTY_MAP);
@@ -71,20 +82,20 @@ private static void analyzeInterface(final Map<Class<?>, LinkedHashMap<String, T
7182
}
7283
}
7384

74-
// LinkedHashMap used instead of usual map to avoid accidental simple map usage (order is important!)
75-
@SuppressWarnings("PMD.LooseCoupling")
7685
private static LinkedHashMap<String, Type> analyzeParent(final Class type,
7786
final Map<String, Type> rootGenerics) {
7887
LinkedHashMap<String, Type> generics = null;
7988
final Class parent = type.getSuperclass();
8089
if (!type.isInterface() && parent != null && parent != Object.class
8190
&& type.getGenericSuperclass() instanceof ParameterizedType) {
8291
generics = resolveGenerics((ParameterizedType) type.getGenericSuperclass(), rootGenerics);
92+
} else if (parent != null && parent.getTypeParameters().length > 0) {
93+
// root class didn't declare generics
94+
generics = resolveRawGenerics(parent.getTypeParameters());
8395
}
8496
return generics == null ? EMPTY_MAP : generics;
8597
}
8698

87-
@SuppressWarnings("PMD.LooseCoupling")
8899
private static LinkedHashMap<String, Type> resolveGenerics(final ParameterizedType type,
89100
final Map<String, Type> rootGenerics) {
90101
final LinkedHashMap<String, Type> generics = new LinkedHashMap<String, Type>();
@@ -130,4 +141,13 @@ private static Type[] resolve(final Type[] types, final Map<String, Type> rootGe
130141
}
131142
return resolved;
132143
}
144+
145+
private static LinkedHashMap<String, Type> resolveRawGenerics(
146+
final TypeVariable... declaredGenerics) {
147+
final LinkedHashMap<String, Type> generics = new LinkedHashMap<String, Type>();
148+
for (TypeVariable type : declaredGenerics) {
149+
generics.put(type.getName(), resolveActualType(type.getBounds()[0], Collections.<String, Type>emptyMap()));
150+
}
151+
return generics;
152+
}
133153
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package ru.vyarus.java.generics.resolver
2+
3+
import ru.vyarus.java.generics.resolver.context.GenericsContext
4+
import ru.vyarus.java.generics.resolver.support.brokenhieararchy.BrokenHierarchyBase
5+
import ru.vyarus.java.generics.resolver.support.brokenhieararchy.BrokenHierarchyInterface
6+
import ru.vyarus.java.generics.resolver.support.brokenhieararchy.BrokenHierarchyRoot
7+
import ru.vyarus.java.generics.resolver.support.brokenhieararchy.BypassGenericRoot
8+
import spock.lang.Specification
9+
10+
import java.util.concurrent.Callable
11+
12+
13+
/**
14+
* @author Vyacheslav Rusakov
15+
* @since 11.02.2015
16+
*/
17+
class BrokenHierarchyTest extends Specification {
18+
def "Check broken hierarchy resolution"() {
19+
20+
when: "resolving class with no generics set"
21+
GenericsContext context = GenericsResolver.resolve(BrokenHierarchyRoot).type(BrokenHierarchyBase)
22+
then: "generics resolved just from generic bound"
23+
context.generic("T") == Callable
24+
context.generic("K") == Object
25+
26+
when: "resolving interface with no generics set"
27+
context = context.type(BrokenHierarchyInterface)
28+
then: "generics resolved just from generic bound"
29+
context.generic("T") == Callable
30+
context.generic("K") == Object
31+
}
32+
33+
def "Check bypass generic case"() {
34+
35+
when: "root class bypass it's own generics"
36+
GenericsContext context = GenericsResolver.resolve(BypassGenericRoot).type(BrokenHierarchyBase)
37+
then: "generic resolved from root generic bound, but generic bound become lower"
38+
context.generic("T") == Callable
39+
context.generic("K") == Object
40+
41+
when: "resolving interface with no generics set"
42+
context = context.type(BrokenHierarchyInterface)
43+
then: "generics resolved just from generic bound"
44+
context.generic("T") == Callable
45+
context.generic("K") == Object
46+
}
47+
}

src/test/groovy/ru/vyarus/java/generics/resolver/FailTests.groovy

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ class FailTests extends Specification {
3737
when: "resolving type with duplicate Runnable interface in hierarchy"
3838
GenericsContext context = GenericsResolver.resolve(NoClashRoot)
3939
then: "context is empty, because no generics info available in hierarchy"
40-
context.genericsInfo.composingTypes == [NoClashSub1, NoClashSub2, Runnable, Callable] as Set
40+
context.genericsInfo.composingTypes == [NoClashRoot, NoClashSub1, NoClashSub2, Runnable, Callable] as Set
4141
}
4242

4343
def "Check access with wrong name"() {

src/test/groovy/ru/vyarus/java/generics/resolver/GenericsInfoFactoryTest.groovy

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class GenericsInfoFactoryTest extends Specification {
2222
GenericsInfo info = GenericsInfoFactory.create(Root)
2323
then: "correct generic values resolved"
2424
info.rootClass == Root
25-
info.composingTypes.size() == 7
25+
info.composingTypes.size() == 8
2626
info.getTypeGenerics(Base1) == ['T': Model]
2727
info.getTypeGenerics(Base2) == ['K': Model, 'P': OtherModel]
2828
info.getTypeGenerics(Lvl2Base1) == ['I': Model]
@@ -37,7 +37,7 @@ class GenericsInfoFactoryTest extends Specification {
3737
info = GenericsInfoFactory.create(BeanRoot)
3838
then: "correct generic values resolved"
3939
info.rootClass == BeanRoot
40-
info.composingTypes.size() == 3
40+
info.composingTypes.size() == 4
4141
info.getTypeGenerics(BeanBase) == ['T': Model]
4242
info.getTypeGenerics(Lvl2BeanBase) == ['I': Model]
4343
info.getTypeGenerics(Lvl2Base1) == ['I': Model]

src/test/groovy/ru/vyarus/java/generics/resolver/IgnoreTest.groovy

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,6 @@ class IgnoreTest extends Specification {
2828
when: "using ignore to limit class resolution depth"
2929
context = GenericsResolver.resolve(BeanRoot, Lvl2BeanBase)
3030
then: "only root types resolved"
31-
context.genericsInfo.composingTypes == [BeanBase, Lvl2Base1] as Set
31+
context.genericsInfo.composingTypes == [BeanRoot, BeanBase, Lvl2Base1] as Set
3232
}
3333
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package ru.vyarus.java.generics.resolver
2+
3+
import ru.vyarus.java.generics.resolver.context.GenericsContext
4+
import ru.vyarus.java.generics.resolver.support.brokenhieararchy.BrokenHierarchyInterface
5+
import spock.lang.Specification
6+
7+
import java.util.concurrent.Callable
8+
9+
10+
/**
11+
* @author Vyacheslav Rusakov
12+
* @since 12.02.2015
13+
*/
14+
class SingleClassResolution extends Specification {
15+
16+
def "Check root class generics resolved"() {
17+
18+
when: "resolving single class without hierarchy"
19+
GenericsContext context = GenericsResolver.resolve(BrokenHierarchyInterface)
20+
then: "root generics resolved from bounds"
21+
context.generic("T") == Callable
22+
context.generic("K") == Object
23+
24+
25+
}
26+
}

0 commit comments

Comments
 (0)