Skip to content

Commit 04ac5d5

Browse files
committed
Resolve generic superclass methods (fix #163)
1 parent 5b3cd85 commit 04ac5d5

File tree

5 files changed

+95
-40
lines changed

5 files changed

+95
-40
lines changed

src/main/kotlin/com/coxautodev/graphql/tools/FieldResolverScanner.kt

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import org.apache.commons.lang3.reflect.FieldUtils
99
import org.slf4j.LoggerFactory
1010
import java.lang.reflect.Modifier
1111
import java.lang.reflect.ParameterizedType
12+
import java.lang.reflect.TypeVariable
1213

1314
/**
1415
* @author Andrew Potter
@@ -22,11 +23,13 @@ internal class FieldResolverScanner(val options: SchemaParserOptions) {
2223

2324
fun getAllMethods(type: Class<*>) =
2425
(type.methods.toList() + ClassUtils.getAllSuperclasses(type).flatMap { it.methods.toList() })
25-
.filter { !it.isSynthetic }
26-
.filter { !Modifier.isPrivate(it.modifiers) }
27-
// discard any methods that are coming off the root of the class hierarchy
28-
// to avoid issues with duplicate method declarations
29-
.filter { it.declaringClass != Object::class.java }
26+
.asSequence()
27+
.filter { !it.isSynthetic }
28+
.filter { !Modifier.isPrivate(it.modifiers) }
29+
// discard any methods that are coming off the root of the class hierarchy
30+
// to avoid issues with duplicate method declarations
31+
.filter { it.declaringClass != Object::class.java }
32+
.toList()
3033
}
3134

3235
fun findFieldResolver(field: FieldDefinition, resolverInfo: ResolverInfo): FieldResolver {
@@ -96,10 +99,12 @@ internal class FieldResolverScanner(val options: SchemaParserOptions) {
9699

97100
private fun verifyMethodArguments(method: java.lang.reflect.Method, requiredCount: Int, search: Search): Boolean {
98101
val appropriateFirstParameter = if (search.requiredFirstParameterType != null) {
99-
if(MethodFieldResolver.isBatched(method, search)) {
102+
if (MethodFieldResolver.isBatched(method, search)) {
100103
verifyBatchedMethodFirstArgument(method.genericParameterTypes.firstOrNull(), search.requiredFirstParameterType)
101104
} else {
102-
method.parameterTypes.firstOrNull() == search.requiredFirstParameterType
105+
method.genericParameterTypes.firstOrNull()?.let {
106+
it == search.requiredFirstParameterType || method.declaringClass.typeParameters.contains(it)
107+
} ?: false
103108
}
104109
} else {
105110
true
@@ -110,15 +115,15 @@ internal class FieldResolverScanner(val options: SchemaParserOptions) {
110115
}
111116

112117
private fun verifyBatchedMethodFirstArgument(firstType: JavaType?, requiredFirstParameterType: Class<*>?): Boolean {
113-
if(firstType == null) {
118+
if (firstType == null) {
114119
return false
115120
}
116121

117-
if(firstType !is ParameterizedType) {
122+
if (firstType !is ParameterizedType) {
118123
return false
119124
}
120125

121-
if(!TypeClassMatcher.isListType(firstType, GenericType(firstType, options))) {
126+
if (!TypeClassMatcher.isListType(firstType, GenericType(firstType, options))) {
122127
return false
123128
}
124129

@@ -128,7 +133,7 @@ internal class FieldResolverScanner(val options: SchemaParserOptions) {
128133
}
129134

130135
private fun findResolverProperty(field: FieldDefinition, search: Search) =
131-
FieldUtils.getAllFields(search.type).find { it.name == field.name }
136+
FieldUtils.getAllFields(search.type).find { it.name == field.name }
132137

133138
private fun getMissingFieldMessage(field: FieldDefinition, searches: List<Search>, scannedProperties: Boolean): String {
134139
val signatures = mutableListOf("")
@@ -138,7 +143,8 @@ internal class FieldResolverScanner(val options: SchemaParserOptions) {
138143
signatures.addAll(getMissingMethodSignatures(field, search, isBoolean, scannedProperties))
139144
}
140145

141-
val sourceLocation = if (field.sourceLocation != null) "${field.sourceLocation.sourceName}:${field.sourceLocation.line}" else "<unknown>"
146+
val sourceName = if (field.sourceLocation != null && field.sourceLocation.sourceName != null) field.sourceLocation.sourceName else "<unknown>"
147+
val sourceLocation = if (field.sourceLocation != null) "$sourceName:${field.sourceLocation.line}" else "<unknown>"
142148
return "No method${if (scannedProperties) " or field" else ""} found as defined in $sourceLocation with any of the following signatures (with or without one of $allowedLastArgumentTypes as the last argument), in priority order:\n${signatures.joinToString("\n ")}"
143149
}
144150

@@ -171,4 +177,4 @@ internal class FieldResolverScanner(val options: SchemaParserOptions) {
171177
data class Search(val type: Class<*>, val resolverInfo: ResolverInfo, val source: Any?, val requiredFirstParameterType: Class<*>? = null, val allowBatched: Boolean = false)
172178
}
173179

174-
class FieldResolverError(msg: String): RuntimeException(msg)
180+
class FieldResolverError(msg: String) : RuntimeException(msg)

src/main/kotlin/com/coxautodev/graphql/tools/ResolverInfo.kt

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,27 +7,27 @@ internal abstract class ResolverInfo {
77
abstract fun getFieldSearches(): List<FieldResolverScanner.Search>
88

99
fun getRealResolverClass(resolver: GraphQLResolver<*>, options: SchemaParserOptions) =
10-
options.proxyHandlers.find { it.canHandle(resolver) }?.getTargetClass(resolver) ?: resolver.javaClass
10+
options.proxyHandlers.find { it.canHandle(resolver) }?.getTargetClass(resolver) ?: resolver.javaClass
1111
}
1212

13-
internal class NormalResolverInfo(val resolver: GraphQLResolver<*>, private val options: SchemaParserOptions): ResolverInfo() {
13+
internal class NormalResolverInfo(val resolver: GraphQLResolver<*>, private val options: SchemaParserOptions) : ResolverInfo() {
1414
val resolverType = getRealResolverClass(resolver, options)
1515
val dataClassType = findDataClass()
1616

1717
private fun findDataClass(): Class<*> {
1818
// Grab the parent interface with type GraphQLResolver from our resolver and get its first type argument.
1919
val interfaceType = GenericType(resolverType, options).getGenericInterface(GraphQLResolver::class.java)
20-
if(interfaceType == null || interfaceType !is ParameterizedType) {
20+
if (interfaceType == null || interfaceType !is ParameterizedType) {
2121
error("${GraphQLResolver::class.java.simpleName} interface was not parameterized for: ${resolverType.name}")
2222
}
2323

2424
val type = TypeUtils.determineTypeArguments(resolverType, interfaceType)[GraphQLResolver::class.java.typeParameters[0]]
2525

26-
if(type == null || type !is Class<*>) {
27-
throw ResolverError("Unable to determine data class for resolver '${resolverType.name}' from generic interface! This is most likely a bug with graphql-java-tools.")
26+
if (type == null || type !is Class<*>) {
27+
throw ResolverError("Unable to determine data class for resolver '${resolverType.name}' from generic interface! This is most likely a bug with graphql-java-tools.")
2828
}
2929

30-
if(type == Void::class.java) {
30+
if (type == Void::class.java) {
3131
throw ResolverError("Resolvers may not have ${Void::class.java.name} as their type, use a real type or use a root resolver interface.")
3232
}
3333

@@ -36,13 +36,13 @@ internal class NormalResolverInfo(val resolver: GraphQLResolver<*>, private val
3636

3737
override fun getFieldSearches(): List<FieldResolverScanner.Search> {
3838
return listOf(
39-
FieldResolverScanner.Search(resolverType, this, resolver, dataClassType, true),
40-
FieldResolverScanner.Search(dataClassType, this, null)
39+
FieldResolverScanner.Search(resolverType, this, resolver, dataClassType, true),
40+
FieldResolverScanner.Search(dataClassType, this, null)
4141
)
4242
}
4343
}
4444

45-
internal class MultiResolverInfo(val resolverInfoList: List<NormalResolverInfo>): ResolverInfo() {
45+
internal class MultiResolverInfo(val resolverInfoList: List<NormalResolverInfo>) : ResolverInfo() {
4646
private val dataClassType = findDataClass()
4747

4848
/**
@@ -51,7 +51,7 @@ internal class MultiResolverInfo(val resolverInfoList: List<NormalResolverInfo>)
5151
private fun findDataClass(): Class<*> {
5252
val dataClass = resolverInfoList.map { it.dataClassType }.distinct().singleOrNull()
5353

54-
if(dataClass == null) {
54+
if (dataClass == null) {
5555
throw ResolverError("Resolvers may not use the same type.")
5656
} else {
5757
return dataClass
@@ -65,17 +65,17 @@ internal class MultiResolverInfo(val resolverInfoList: List<NormalResolverInfo>)
6565
}
6666
}
6767

68-
internal class RootResolverInfo(val resolvers: List<GraphQLRootResolver>, private val options: SchemaParserOptions): ResolverInfo() {
68+
internal class RootResolverInfo(val resolvers: List<GraphQLRootResolver>, private val options: SchemaParserOptions) : ResolverInfo() {
6969
override fun getFieldSearches() =
70-
resolvers.map { FieldResolverScanner.Search(getRealResolverClass(it, options), this, it) }
70+
resolvers.map { FieldResolverScanner.Search(getRealResolverClass(it, options), this, it) }
7171
}
7272

73-
internal class DataClassResolverInfo(private val dataClass: Class<*>): ResolverInfo() {
73+
internal class DataClassResolverInfo(private val dataClass: Class<*>) : ResolverInfo() {
7474
override fun getFieldSearches() =
75-
listOf(FieldResolverScanner.Search(dataClass, this, null))
75+
listOf(FieldResolverScanner.Search(dataClass, this, null))
7676
}
7777

78-
internal class MissingResolverInfo: ResolverInfo() {
78+
internal class MissingResolverInfo : ResolverInfo() {
7979
override fun getFieldSearches(): List<FieldResolverScanner.Search> = listOf()
8080
}
8181

src/main/kotlin/com/coxautodev/graphql/tools/SchemaClassScanner.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -216,10 +216,10 @@ internal class SchemaClassScanner(initialDictionary: BiMap<String, Class<*>>, al
216216
val resolverInfoList = this.resolverInfos.filter { it.dataClassType == item.clazz }
217217
val resolverInfo: ResolverInfo
218218

219-
if (resolverInfoList.size > 1) {
220-
resolverInfo = MultiResolverInfo(resolverInfoList)
219+
resolverInfo = if (resolverInfoList.size > 1) {
220+
MultiResolverInfo(resolverInfoList)
221221
} else {
222-
resolverInfo = resolverInfosByDataClass[item.clazz] ?: DataClassResolverInfo(item.clazz)
222+
resolverInfosByDataClass[item.clazz] ?: DataClassResolverInfo(item.clazz)
223223
}
224224

225225
scanResolverInfoForPotentialMatches(item.type, resolverInfo)
@@ -229,7 +229,7 @@ internal class SchemaClassScanner(initialDictionary: BiMap<String, Class<*>>, al
229229
type.getExtendedFieldDefinitions(extensionDefinitions).forEach { field ->
230230
val fieldResolver = fieldResolverScanner.findFieldResolver(field, resolverInfo)
231231

232-
fieldResolversByType.getOrPut(type, { mutableMapOf() })[fieldResolver.field] = fieldResolver
232+
fieldResolversByType.getOrPut(type) { mutableMapOf() }[fieldResolver.field] = fieldResolver
233233
fieldResolver.scanForMatches().forEach { potentialMatch ->
234234
handleFoundType(typeClassMatcher.match(potentialMatch))
235235
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.coxautodev.graphql.tools
2+
3+
import graphql.schema.GraphQLSchema
4+
import spock.lang.Specification
5+
6+
class GenericResolverSpec extends Specification {
7+
8+
def "methods from generic resolvers are resolved"() {
9+
when:
10+
GraphQLSchema schema = SchemaParser.newParser().schemaString('''\
11+
type Query {
12+
bar: Bar!
13+
}
14+
15+
type Bar {
16+
value: String
17+
}
18+
''')
19+
.resolvers(new QueryResolver(), new BarResolver())
20+
.build()
21+
.makeExecutableSchema()
22+
23+
then:
24+
schema != null
25+
}
26+
27+
}
28+
29+
class QueryResolver implements GraphQLQueryResolver {
30+
Bar getBar() {
31+
return new Bar()
32+
}
33+
}
34+
35+
class Bar {
36+
}
37+
38+
abstract class FooResolver<T> implements GraphQLResolver<T> {
39+
String getValue(T foo) {
40+
return "value"
41+
}
42+
}
43+
44+
class BarResolver extends FooResolver<Bar> implements GraphQLResolver<Bar> {
45+
46+
}

src/test/groovy/com/coxautodev/graphql/tools/SchemaParserSpec.groovy

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ class SchemaParserSpec extends Specification {
3434
def "builder doesn't throw FileNotFound exception when file is present"() {
3535
when:
3636
SchemaParser.newParser().file("test.graphqls")
37-
.resolvers(new GraphQLQueryResolver() { String getId() { "1" }})
37+
.resolvers(new GraphQLQueryResolver() {
38+
String getId() { "1" }
39+
})
3840
.build()
3941

4042
then:
@@ -317,15 +319,15 @@ class SchemaParserSpec extends Specification {
317319
}
318320
'''.stripIndent())
319321
.resolvers(new GraphQLMutationResolver() {
320-
boolean save(SaveInput input) { false }
322+
boolean save(SaveInput input) { false }
321323

322-
class SaveInput {
323-
EnumType type;
324-
}
324+
class SaveInput {
325+
EnumType type;
326+
}
325327

326-
}, new GraphQLQueryResolver() {
327-
boolean test() { false }
328-
})
328+
}, new GraphQLQueryResolver() {
329+
boolean test() { false }
330+
})
329331
.dictionary(EnumType.class)
330332
.build()
331333
.makeExecutableSchema()
@@ -364,6 +366,7 @@ class SchemaParserSpec extends Specification {
364366
then:
365367
noExceptionThrown()
366368
}
369+
367370
}
368371

369372
enum EnumType { TEST }

0 commit comments

Comments
 (0)