Skip to content

Commit 841d3f2

Browse files
committed
Issue #113: Support for PropertyDataFetcher behavior
* Added an additional field resolver and logic to recognize java.util.Map based objects as property maps. * Fixed issue where presence of a type in the type dictionary did not allow non-scalar map types to be resolved. * Updated documentation with respect to how fields are resolved.
1 parent d53cdf2 commit 841d3f2

File tree

7 files changed

+264
-7
lines changed

7 files changed

+264
-7
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,12 +223,18 @@ Then on the data class:
223223
4. `method getField<Name>(*fieldArgs [, DataFetchingEnvironment])`
224224
5. `field <name>`
225225

226+
Last of all, if the data class implements`java.util.Map` then:
227+
1. `method get(name)`
228+
229+
226230
*Note:* All reflection discovery is done on startup, and runtime reflection method calls use [reflectasm](https://github.com/EsotericSoftware/reflectasm), which increases performance and unifies stacktraces. No more `InvocationTargetException`!
227231

228232
*Note:* `java.util.Optional` can be used for nullable field arguments and nullable return values, and the schema parser will verify that it's not used with non-null field arguments and return values.
229233

230234
*Note:* Methods on `java.lang.Object` are excluded from method matching, for example a field named `class` will require a method named `getFieldClass` defined.
231235

236+
*Note:* If one of the values of a type backed by a `java.util.Map` is non-scalar then this type will need to be added to the `type dictionary` (see below). After adding this type to the dictionary, GraphQL Java Tools will however still be able to find the types used in the fields of this added type.
237+
232238
### Enum Types
233239

234240
Enum values are automatically mapped by `Enum#name()`.

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ internal class FieldResolverScanner(val options: SchemaParserOptions) {
6767
}
6868
}
6969

70+
if(java.util.Map::class.java.isAssignableFrom(search.type.unwrap())) {
71+
return PropertyMapResolver(field, search, options, search.type.unwrap())
72+
}
73+
7074
return null
7175
}
7276

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,13 @@ internal class PropertyFieldResolver(field: FieldDefinition, search: FieldResolv
1414
}
1515

1616
override fun scanForMatches(): List<TypeClassMatcher.PotentialMatch> {
17-
return listOf(TypeClassMatcher.PotentialMatch.returnValue(field.type, property.genericType, genericType, SchemaClassScanner.FieldTypeReference(property), false))
17+
return listOf(
18+
TypeClassMatcher.PotentialMatch.returnValue(
19+
field.type,
20+
property.genericType,
21+
genericType,
22+
SchemaClassScanner.FieldTypeReference(property.toString()),
23+
false))
1824
}
1925

2026
override fun toString() = "PropertyFieldResolver{property=$property}"
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package com.coxautodev.graphql.tools
2+
3+
import graphql.language.FieldDefinition
4+
import graphql.schema.DataFetcher
5+
import graphql.schema.DataFetchingEnvironment
6+
import java.lang.reflect.ParameterizedType
7+
8+
/**
9+
* @author Nick Weedon
10+
*
11+
* The PropertyMapResolver implements the Map (i.e. property map) specific portion of the logic within the GraphQL PropertyDataFetcher class.
12+
*/
13+
internal class PropertyMapResolver(field: FieldDefinition, search: FieldResolverScanner.Search, options: SchemaParserOptions, relativeTo: JavaType): FieldResolver(field, search, options, relativeTo) {
14+
15+
var mapGenericValue : JavaType = getMapGenericType(relativeTo)
16+
17+
fun getMapGenericType(mapClass : JavaType) : JavaType {
18+
if(mapClass is ParameterizedType) {
19+
return mapClass.actualTypeArguments[1]
20+
} else {
21+
return Object::class.java
22+
}
23+
}
24+
25+
override fun createDataFetcher(): DataFetcher<*> {
26+
return PropertyMapResolverDataFetcher(getSourceResolver(), field.name)
27+
}
28+
29+
override fun scanForMatches(): List<TypeClassMatcher.PotentialMatch> {
30+
return listOf(TypeClassMatcher.PotentialMatch.returnValue(field.type, mapGenericValue, genericType, SchemaClassScanner.FieldTypeReference(field.name), false))
31+
}
32+
33+
override fun toString() = "PropertyMapResolverDataFetcher{key=${field.name}}"
34+
}
35+
36+
class PropertyMapResolverDataFetcher(private val sourceResolver: SourceResolver, val key : String): DataFetcher<Any> {
37+
override fun get(environment: DataFetchingEnvironment): Any? {
38+
val resolvedSourceObject = sourceResolver(environment)
39+
if(resolvedSourceObject is Map<*, *>) {
40+
return resolvedSourceObject[key]
41+
} else {
42+
throw RuntimeException("PropertyMapResolverDataFetcher attempt to fetch a field from an object instance that was not a map")
43+
}
44+
}
45+
}

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

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,13 @@ internal class SchemaClassScanner(initialDictionary: BiMap<String, Class<*>>, al
133133
// Union types can also be excluded, as their possible types are resolved recursively later
134134
val dictionary = try {
135135
Maps.unmodifiableBiMap(HashBiMap.create<TypeDefinition<*>, JavaType>().also {
136-
dictionary.filter { it.value.javaType != null && it.key !is InputObjectTypeDefinition && it.key !is UnionTypeDefinition }.mapValuesTo(it) { it.value.javaType }
136+
dictionary.filter {
137+
it.value.javaType != null
138+
&& it.value.typeClass() != java.lang.Object::class.java
139+
&& !java.util.Map::class.java.isAssignableFrom(it.value.typeClass())
140+
&& it.key !is InputObjectTypeDefinition
141+
&& it.key !is UnionTypeDefinition
142+
}.mapValuesTo(it) { it.value.javaType }
137143
})
138144
} catch (t: Throwable) {
139145
throw SchemaClassScannerError("Error creating bimap of type => class", t)
@@ -210,17 +216,31 @@ internal class SchemaClassScanner(initialDictionary: BiMap<String, Class<*>>, al
210216
}
211217
}
212218

219+
private fun getResolverInfoFromTypeDictionary(typeName: String) : ResolverInfo? {
220+
val dictionaryType = initialDictionary[typeName]?.get()
221+
return if(dictionaryType != null) {
222+
resolverInfosByDataClass[dictionaryType] ?: DataClassResolverInfo(dictionaryType);
223+
} else {
224+
null
225+
}
226+
}
227+
213228
/**
214229
* Scan a new object for types that haven't been mapped yet.
215230
*/
216231
private fun scanQueueItemForPotentialMatches(item: QueueItem) {
217232
val resolverInfoList = this.resolverInfos.filter { it.dataClassType == item.clazz }
218-
val resolverInfo: ResolverInfo
219-
220-
resolverInfo = if (resolverInfoList.size > 1) {
233+
val resolverInfo: ResolverInfo? = if (resolverInfoList.size > 1) {
221234
MultiResolverInfo(resolverInfoList)
222235
} else {
223-
resolverInfosByDataClass[item.clazz] ?: DataClassResolverInfo(item.clazz)
236+
if(item.clazz.equals(Object::class.java)) {
237+
getResolverInfoFromTypeDictionary(item.type.name)
238+
} else {
239+
resolverInfosByDataClass[item.clazz] ?: DataClassResolverInfo(item.clazz)
240+
}
241+
}
242+
if(resolverInfo == null) {
243+
throw throw SchemaClassScannerError("The GraphQL schema type '${item.type.name}' maps to a field of type java.lang.Object however there is no matching entry for this type in the type dictionary. You may need to add this type to the dictionary before building the schema.")
224244
}
225245

226246
scanResolverInfoForPotentialMatches(item.type, resolverInfo)
@@ -267,6 +287,9 @@ internal class SchemaClassScanner(initialDictionary: BiMap<String, Class<*>>, al
267287
if (options.preferGraphQLResolver && realEntry.hasResolverRef()) {
268288
log.warn("The real entry ${realEntry.joinReferences()} is a GraphQLResolver so ignoring this one ${javaType.unwrap()} $reference")
269289
} else {
290+
if(java.util.Map::class.java.isAssignableFrom(javaType.unwrap())) {
291+
throw SchemaClassScannerError("Two different property map classes used for type ${type.name}:\n${realEntry.joinReferences()}\n\n- ${javaType}:\n| ${reference.getDescription()}")
292+
}
270293
throw SchemaClassScannerError("Two different classes used for type ${type.name}:\n${realEntry.joinReferences()}\n\n- ${javaType.unwrap()}:\n| ${reference.getDescription()}")
271294
}
272295
}
@@ -424,7 +447,7 @@ internal class SchemaClassScanner(initialDictionary: BiMap<String, Class<*>>, al
424447
override fun getDescription() = "parameter $index of method $method"
425448
}
426449

427-
class FieldTypeReference(private val field: Field) : Reference() {
450+
class FieldTypeReference(private val field: String) : Reference() {
428451
override fun getDescription() = "type of field $field"
429452
}
430453

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

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,108 @@ class EndToEndSpec extends Specification {
196196
data.itemByUUID
197197
}
198198

199+
def "generated schema should handle any java.util.Map (using HashMap) types as property maps"() {
200+
when:
201+
def data = Utils.assertNoGraphQlErrors(gql) {
202+
'''
203+
{
204+
propertyHashMapItems {
205+
name
206+
age
207+
}
208+
}
209+
'''
210+
}
211+
212+
then:
213+
data.propertyHashMapItems == [ [name: "bob", age:55] ]
214+
}
215+
216+
def "generated schema should handle any java.util.Map (using SortedMap) types as property maps"() {
217+
when:
218+
def data = Utils.assertNoGraphQlErrors(gql) {
219+
'''
220+
{
221+
propertySortedMapItems {
222+
name
223+
age
224+
}
225+
}
226+
'''
227+
}
228+
229+
then:
230+
data.propertySortedMapItems == [ [name: "Arthur", age:76], [name: "Jane", age:28] ]
231+
}
232+
233+
// In this test a dictionary entry for the schema type ComplexMapItem is defined
234+
// so that it is possible for a POJO mapping to be known since the ComplexMapItem is contained
235+
// in a property map (i.e. Map<String, Object>) and so the normal resolver and schema traversal code
236+
// will not be able to find the POJO since it does not exist as a strongly typed object in
237+
// resolver/POJO graph.
238+
def "generated schema should handle java.util.Map types as property maps when containing complex data"() {
239+
when:
240+
def data = Utils.assertNoGraphQlErrors(gql) {
241+
'''
242+
{
243+
propertyMapWithComplexItems {
244+
nameId {
245+
id
246+
}
247+
age
248+
}
249+
}
250+
'''
251+
}
252+
253+
then:
254+
data.propertyMapWithComplexItems == [ [nameId:[id:150], age:72] ]
255+
}
256+
257+
// This behavior is consistent with PropertyDataFetcher
258+
def "property map returns null when a property is not defined."() {
259+
when:
260+
def data = Utils.assertNoGraphQlErrors(gql) {
261+
'''
262+
{
263+
propertyMapMissingNamePropItems {
264+
name
265+
age
266+
}
267+
}
268+
'''
269+
}
270+
271+
then:
272+
data.propertyMapMissingNamePropItems == [ [name: null, age:55] ]
273+
}
274+
275+
// In this test a dictonary entry for the schema type NestedComplexMapItem is defined
276+
// however we expect to not be required to define one for the transitive UndiscoveredItem object since
277+
// the schema resolver discovery code should still be able to automatically determine the POJO that
278+
// maps to this schema type.
279+
def "generated schema should continue to associate resolvers for transitive types of a java.util.Map complex data type"() {
280+
when:
281+
def data = Utils.assertNoGraphQlErrors(gql) {
282+
'''
283+
{
284+
propertyMapWithNestedComplexItems {
285+
nested {
286+
item {
287+
id
288+
}
289+
}
290+
age
291+
}
292+
}
293+
'''
294+
}
295+
296+
then:
297+
data.propertyMapWithNestedComplexItems == [ [ nested:[ item: [id:63] ], age:72] ]
298+
}
299+
300+
199301
def "generated schema should handle optional arguments"() {
200302
when:
201303
def data = Utils.assertNoGraphQlErrors(gql) {

src/test/kotlin/com/coxautodev/graphql/tools/EndToEndSpec.kt

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ fun createSchema() = SchemaParser.newParser()
1818
.scalars(customScalarUUID, customScalarMap, customScalarId)
1919
.dictionary("OtherItem", OtherItemWithWrongName::class)
2020
.dictionary("ThirdItem", ThirdItem::class)
21+
.dictionary("ComplexMapItem", ComplexMapItem::class)
22+
.dictionary("NestedComplexMapItem", NestedComplexMapItem::class)
2123
.build()
2224
.makeExecutableSchema()
2325

@@ -63,6 +65,12 @@ type Query {
6365
class: [Item!]
6466
hashCode: [Item!]
6567
68+
propertyHashMapItems: [PropertyHashMapItem!]
69+
propertyMapMissingNamePropItems: [PropertyHashMapItem!]
70+
propertySortedMapItems: [PropertySortedMapItem!]
71+
propertyMapWithComplexItems: [PropertyMapWithComplexItem!]
72+
propertyMapWithNestedComplexItems: [PropertyMapWithNestedComplexItem!]
73+
6674
propertyField: String!
6775
dataFetcherResult: Item!
6876
}
@@ -144,6 +152,38 @@ type ThirdItem {
144152
id: Int!
145153
}
146154
155+
type PropertyHashMapItem {
156+
name: String
157+
age: Int!
158+
}
159+
160+
type PropertySortedMapItem {
161+
name: String!
162+
age: Int!
163+
}
164+
165+
type ComplexMapItem {
166+
id: Int!
167+
}
168+
169+
type UndiscoveredItem {
170+
id: Int!
171+
}
172+
173+
type NestedComplexMapItem {
174+
item: UndiscoveredItem
175+
}
176+
177+
type PropertyMapWithNestedComplexItem {
178+
nested: NestedComplexMapItem!
179+
age: Int!
180+
}
181+
182+
type PropertyMapWithComplexItem {
183+
nameId: ComplexMapItem!
184+
age: Int!
185+
}
186+
147187
union OtherUnion = Item | ThirdItem
148188
149189
union NestedUnion = OtherUnion | OtherItem
@@ -169,6 +209,27 @@ val thirdItems = mutableListOf(
169209
ThirdItem(100)
170210
)
171211

212+
val propetyHashMapItems = mutableListOf(
213+
hashMapOf("name" to "bob", "age" to 55)
214+
)
215+
216+
val propertyMapMissingNamePropItems = mutableListOf(
217+
hashMapOf<String, kotlin.Any>("age" to 55)
218+
)
219+
220+
val propetySortedMapItems = mutableListOf(
221+
sortedMapOf("name" to "Arthur", "age" to 76),
222+
sortedMapOf("name" to "Jane", "age" to 28)
223+
)
224+
225+
val propertyMapWithComplexItems = mutableListOf(
226+
hashMapOf("nameId" to ComplexMapItem(150), "age" to 72)
227+
)
228+
229+
val propertyMapWithNestedComplexItems = mutableListOf(
230+
hashMapOf("nested" to NestedComplexMapItem(UndiscoveredItem(63)), "age" to 72)
231+
)
232+
172233
class Query: GraphQLQueryResolver, ListListResolver<String>() {
173234
fun isEmpty() = items.isEmpty()
174235
fun allBaseItems() = items
@@ -201,6 +262,13 @@ class Query: GraphQLQueryResolver, ListListResolver<String>() {
201262
fun getFieldClass() = items
202263
fun getFieldHashCode() = items
203264

265+
fun propertyHashMapItems() = propetyHashMapItems
266+
fun propertyMapMissingNamePropItems() = propertyMapMissingNamePropItems
267+
fun propertySortedMapItems() = propetySortedMapItems
268+
fun propertyMapWithComplexItems() = propertyMapWithComplexItems
269+
fun propertyMapWithNestedComplexItems() = propertyMapWithNestedComplexItems
270+
271+
204272
private val propertyField = "test"
205273

206274
fun dataFetcherResult(): DataFetcherResult<Item> {
@@ -256,6 +324,9 @@ enum class Type { TYPE_1, TYPE_2 }
256324
data class Item(val id: Int, override val name: String, override val type: Type, override val uuid:UUID, val tags: List<Tag>) : ItemInterface
257325
data class OtherItemWithWrongName(val id: Int, override val name: String, override val type: Type, override val uuid:UUID) : ItemInterface
258326
data class ThirdItem(val id: Int)
327+
data class ComplexMapItem(val id: Int)
328+
data class UndiscoveredItem(val id: Int)
329+
data class NestedComplexMapItem(val item: UndiscoveredItem)
259330
data class Tag(val id: Int, val name: String)
260331
data class ItemSearchInput(val name: String)
261332
data class NewItemInput(val name: String, val type: Type)

0 commit comments

Comments
 (0)