Skip to content

Commit 08b9be7

Browse files
authored
Merge pull request #906 from graphql-java-kickstart/refactor/split-scanner-parser
Split SchemaClassScanner and SchemaParser into smaller classes
2 parents d56121f + 3488a31 commit 08b9be7

12 files changed

Lines changed: 719 additions & 615 deletions

File tree

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
package graphql.kickstart.tools
2+
3+
import com.fasterxml.jackson.databind.JsonNode
4+
import graphql.kickstart.tools.util.JavaType
5+
import graphql.kickstart.tools.util.unwrap
6+
import graphql.kickstart.tools.util.withExtensions
7+
import graphql.language.*
8+
import graphql.schema.idl.ScalarInfo
9+
import org.slf4j.Logger
10+
import java.lang.reflect.Member
11+
import java.lang.reflect.Method
12+
13+
internal class ScanDictionary(
14+
private val options: SchemaParserOptions,
15+
private val log: Logger,
16+
private val typeExtensions: TypeExtensions,
17+
private val definitionsByName: Map<String, TypeDefinition<*>>,
18+
private val typeClassMatcher: TypeClassMatcher,
19+
private val initialDictionary: Map<String, InitialDictionaryEntry>,
20+
private val enqueue: (ObjectTypeDefinition, JavaType) -> Unit,
21+
private val handleFoundScalarType: (ScalarTypeDefinition) -> Unit,
22+
private val dictionary: MutableMap<TypeDefinition<*>, DictionaryEntry> = mutableMapOf(),
23+
) : Map<TypeDefinition<*>, ScanDictionary.EntryView> by dictionary {
24+
private val interfaceDefinitionsByName = definitionsByName.values.filterIsInstance<InterfaceTypeDefinition>().associateBy { it.name }
25+
private val enumsWithoutClass = mutableMapOf<EnumTypeDefinition, String>()
26+
27+
fun handleFoundType(match: TypeClassMatcher.Match) = when (match) {
28+
is TypeClassMatcher.ScalarMatch -> handleFoundScalarType(match.type)
29+
is TypeClassMatcher.ValidMatch -> handleFoundType(match.type, match.javaType, match.reference)
30+
}
31+
32+
/**
33+
* Enter a found type into the dictionary if it doesn't exist yet, add a reference pointing back to where it was discovered.
34+
*/
35+
fun handleFoundType(type: TypeDefinition<*>, javaType: JavaType?, reference: Reference) {
36+
val realEntry = dictionary.getOrPut(type) { DictionaryEntry() }
37+
var typeWasSet = false
38+
39+
if (javaType != null) {
40+
typeWasSet = realEntry.setTypeIfMissing(javaType)
41+
42+
if (realEntry.javaType != javaType) {
43+
if (options.preferGraphQLResolver && realEntry.hasResolverRef()) {
44+
log.warn("The real entry ${realEntry.joinReferences()} is a GraphQLResolver so ignoring this one ${javaType.unwrap()} $reference")
45+
} else {
46+
if (Map::class.java.isAssignableFrom(javaType.unwrap())) {
47+
throw SchemaClassScannerError("Two different property map classes used for type ${type.name}:\n${realEntry.joinReferences()}\n\n- ${javaType}:\n| ${reference.getDescription()}")
48+
}
49+
throw SchemaClassScannerError("Two different classes used for type ${type.name}:\n${realEntry.joinReferences()}\n\n- ${javaType.typeName}:\n| ${reference.getDescription()}")
50+
}
51+
}
52+
}
53+
54+
realEntry.addReference(reference)
55+
56+
// Check if we just added the entry... a little odd, but it works (and thread-safe, FWIW)
57+
if (typeWasSet && javaType != null) {
58+
handleNewType(type, javaType)
59+
}
60+
}
61+
62+
/**
63+
* Handle a newly found type, adding it to the list of actually used types and putting it in the scanning queue if it's an object type.
64+
*/
65+
private fun handleNewType(graphQLType: TypeDefinition<*>, javaType: JavaType) {
66+
when (graphQLType) {
67+
is ObjectTypeDefinition -> {
68+
enqueue(graphQLType, javaType)
69+
scanInterfacesOfType(graphQLType)
70+
}
71+
72+
is InputObjectTypeDefinition -> handleInputObjectFields(graphQLType, javaType)
73+
}
74+
}
75+
76+
/**
77+
* Finds the types of the input object's fields through the properties of its class. A null [javaType] means the input object
78+
* has no known class, e.g. because it's only reachable through a Map.
79+
*/
80+
private fun handleInputObjectFields(graphQLType: InputObjectTypeDefinition, javaType: JavaType?) {
81+
val clazz = javaType?.unwrap()
82+
graphQLType.withExtensions(typeExtensions)
83+
.flatMap { it.inputValueDefinitions }
84+
.forEach { inputValueDefinition ->
85+
val inputGraphQLType = inputValueDefinition.type.unwrap()
86+
if (ScalarInfo.GRAPHQL_SPECIFICATION_SCALARS_DEFINITIONS.containsKey(inputGraphQLType.name)) {
87+
return@forEach
88+
}
89+
90+
val propertyType = clazz?.let { findInputValueTypeInType(inputValueDefinition.name, it) }
91+
val inputValueJavaType = propertyType ?: initialDictionary[inputGraphQLType.name]?.get()
92+
if (inputValueJavaType == null) {
93+
handleInputValueWithoutJavaType(graphQLType, inputValueDefinition, inputGraphQLType, clazz)
94+
return@forEach
95+
}
96+
97+
handleFoundType(typeClassMatcher.match(TypeClassMatcher.PotentialMatch.parameterType(
98+
// a class from the dictionary is for the unwrapped type, not for its list or non-null wrapper
99+
if (propertyType != null) inputValueDefinition.type else inputGraphQLType,
100+
inputValueJavaType,
101+
GenericTypeResolver(javaType ?: inputValueJavaType, options),
102+
InputObjectReference(inputValueDefinition)
103+
)))
104+
}
105+
}
106+
107+
/**
108+
* Without a Java property for an input field its type can still be built, as long as it doesn't need a class of its own.
109+
* At runtime the value is whatever the input object's class (or the Map) deserializes it into.
110+
*/
111+
private fun handleInputValueWithoutJavaType(graphQLType: InputObjectTypeDefinition, inputValueDefinition: InputValueDefinition, inputGraphQLType: TypeName, clazz: Class<*>?) {
112+
when (val definition = definitionsByName[inputGraphQLType.name]) {
113+
is ScalarTypeDefinition -> handleFoundScalarType(definition)
114+
is InputObjectTypeDefinition -> {
115+
val isNewType = !dictionary.containsKey(definition)
116+
// registered without a class, so a class found for it later still gets set and scanned
117+
handleFoundType(definition, null, InputObjectReference(inputValueDefinition))
118+
if (isNewType) {
119+
handleInputObjectFields(definition, null)
120+
}
121+
}
122+
// the enum class may still be found through another field, so this is only reported once scanning is done
123+
is EnumTypeDefinition -> enumsWithoutClass.putIfAbsent(definition, enumWithoutClassMessage(definition, graphQLType, inputValueDefinition, clazz))
124+
// undefined types and output types are reported by the SchemaParser
125+
else -> {}
126+
}
127+
}
128+
129+
// an input bound to Any gets a Map at runtime too
130+
private fun isPropertyMap(clazz: Class<*>) =
131+
Map::class.java.isAssignableFrom(clazz) || JsonNode::class.java.isAssignableFrom(clazz) || clazz == Any::class.java
132+
133+
private fun isLibraryClass(clazz: Class<*>) =
134+
listOf("java.", "kotlin.", "com.fasterxml.jackson.").any { clazz.name.startsWith(it) }
135+
136+
private fun enumWithoutClassMessage(enumType: EnumTypeDefinition, graphQLType: InputObjectTypeDefinition, inputValueDefinition: InputValueDefinition, clazz: Class<*>?): String {
137+
val field = inputValueDefinition.name
138+
val reason = when {
139+
clazz == null -> "no class was found for input type '${graphQLType.name}'"
140+
isPropertyMap(clazz) && isLibraryClass(clazz) -> "'${graphQLType.name}' is bound to ${clazz.name}, which has no type information for its fields"
141+
else -> "${clazz.name} has no public getter or field named '$field'"
142+
}
143+
return "Enum type '${enumType.name}' is used by field '$field' of input type '${graphQLType.name}', but its Java enum can't be determined because $reason. " +
144+
"Pass the enum class for type '${enumType.name}' in the parser's dictionary, or use a class for '${graphQLType.name}' with a public getter or field '$field'."
145+
}
146+
147+
fun throwIfEnumWithoutClass() {
148+
enumsWithoutClass.entries
149+
.find { dictionary[it.key]?.javaType == null }
150+
?.let { throw SchemaClassScannerError(it.value) }
151+
}
152+
153+
fun scanInterfacesOfType(graphQLType: ObjectTypeDefinition) {
154+
graphQLType.withExtensions(typeExtensions).flatMap { it.implements }.forEach {
155+
if (it is TypeName) {
156+
handleFoundType(interfaceDefinitionsByName[it.name]
157+
?: throw SchemaClassScannerError("Object type ${graphQLType.name} declared interface ${it.name}, but no interface with that name was found in the schema!"), null, InterfaceReference(graphQLType))
158+
}
159+
}
160+
}
161+
162+
private fun findInputValueTypeInType(name: String, clazz: Class<*>): JavaType? {
163+
// a Map or JsonNode has no properties for the input's fields, only methods that may share their names, e.g. ObjectNode.fields(),
164+
// but a subclass of one may still declare typed getters or fields of its own
165+
val isProperty = { member: Member -> !isPropertyMap(clazz) || !isLibraryClass(member.declaringClass) }
166+
val methods = clazz.methods.filter(isProperty)
167+
168+
val (getters, methodsWithParameters) = methods.filter {
169+
it.name == name || it.name == "get${name.replaceFirstChar(Char::titlecase)}"
170+
}.sortedBy { it.name.length }.partition { it.parameterCount == 0 }
171+
172+
return getters.find { !it.isSynthetic }?.genericReturnType
173+
?: getters.firstOrNull()?.genericReturnType
174+
?: clazz.fields.find { it.name == name && isProperty(it) }?.genericType
175+
?: methodsWithParameters.find { !it.isSynthetic }?.genericReturnType
176+
?: methodsWithParameters.firstOrNull()?.genericReturnType
177+
}
178+
179+
interface EntryView {
180+
val javaType: JavaType?
181+
fun typeClass(): Class<out Any>?
182+
}
183+
184+
class DictionaryEntry : EntryView {
185+
private val references = mutableListOf<Reference>()
186+
override var javaType: JavaType? = null
187+
private set
188+
189+
fun setTypeIfMissing(javaType: JavaType): Boolean {
190+
if (this.javaType == null) {
191+
this.javaType = javaType
192+
return true
193+
}
194+
195+
return false
196+
}
197+
198+
// fun type(): JavaType? = classEntry?.type
199+
200+
override fun typeClass(): Class<out Any>? = javaType?.unwrap()
201+
202+
fun addReference(reference: Reference) {
203+
references.add(reference)
204+
}
205+
206+
fun joinReferences() = "- ${javaType?.typeName}:\n| " + references.joinToString("\n| ") { it.getDescription() }
207+
208+
fun hasResolverRef(): Boolean {
209+
references.filterIsInstance<ReturnValueReference>().forEach { reference ->
210+
if (GraphQLResolver::class.java.isAssignableFrom(reference.getMethod().declaringClass)) {
211+
return true
212+
}
213+
}
214+
return false
215+
}
216+
}
217+
218+
class InitialDictionaryEntry(private val clazz: Class<*>) {
219+
var accessed = false
220+
private set
221+
222+
fun get(): Class<*> {
223+
accessed = true
224+
return clazz
225+
}
226+
}
227+
228+
abstract class Reference {
229+
abstract fun getDescription(): String
230+
override fun toString() = getDescription()
231+
}
232+
233+
class DictionaryReference : Reference() {
234+
override fun getDescription() = "provided dictionary"
235+
}
236+
237+
class UnusedTypeReference : Reference() {
238+
override fun getDescription() = "unused type"
239+
}
240+
241+
private class InterfaceReference(private val type: ObjectTypeDefinition) : Reference() {
242+
override fun getDescription() = "interface declarations of ${type.name}"
243+
}
244+
245+
class UnionMemberReference(private val union: UnionTypeDefinition) : Reference() {
246+
override fun getDescription() = "member of union ${union.name}"
247+
}
248+
249+
class SealedSubclassReference(private val sealedType: JavaType) : Reference() {
250+
override fun getDescription() = "subclass of sealed type ${sealedType.typeName}"
251+
}
252+
253+
private class InputObjectReference(private val type: InputValueDefinition) : Reference() {
254+
override fun getDescription() = "input object $type"
255+
}
256+
257+
internal class ReturnValueReference(private val method: Method) : Reference() {
258+
fun getMethod() = method
259+
override fun getDescription() = "return type of method $method"
260+
}
261+
262+
internal class MethodParameterReference(private val method: Method, private val index: Int) : Reference() {
263+
override fun getDescription() = "parameter $index of method $method"
264+
}
265+
266+
internal class FieldTypeReference(private val field: String) : Reference() {
267+
override fun getDescription() = "type of field $field"
268+
}
269+
}

0 commit comments

Comments
 (0)