diff --git a/src/main/kotlin/graphql/kickstart/tools/resolver/FieldResolverScanner.kt b/src/main/kotlin/graphql/kickstart/tools/resolver/FieldResolverScanner.kt index 8bace142..8f7a9de9 100644 --- a/src/main/kotlin/graphql/kickstart/tools/resolver/FieldResolverScanner.kt +++ b/src/main/kotlin/graphql/kickstart/tools/resolver/FieldResolverScanner.kt @@ -12,6 +12,7 @@ import graphql.schema.DataFetchingEnvironment import org.apache.commons.lang3.ClassUtils import org.apache.commons.lang3.reflect.FieldUtils import org.slf4j.LoggerFactory +import java.lang.reflect.AccessibleObject import java.lang.reflect.Method import java.lang.reflect.Modifier import java.lang.reflect.Type @@ -44,13 +45,13 @@ internal class FieldResolverScanner(val options: SchemaParserOptions) { private fun findFieldResolver(field: FieldDefinition, search: Search, scanProperties: Boolean): FieldResolver? { val method = findResolverMethod(field, search) if (method != null) { - return MethodFieldResolver(field, search, options, method.apply { isAccessible = true }) + return MethodFieldResolver(field, search, options, method.apply(trySetAccessible(field, search.type))) } if (scanProperties) { val property = findResolverProperty(field, search) if (property != null) { - return PropertyFieldResolver(field, search, options, property.apply { isAccessible = true }) + return PropertyFieldResolver(field, search, options, property.apply(trySetAccessible(field, search.type))) } } @@ -61,6 +62,15 @@ internal class FieldResolverScanner(val options: SchemaParserOptions) { return null } + private fun trySetAccessible(field: FieldDefinition, type: JavaType): AccessibleObject.() -> Unit = { + try { + isAccessible = true + } catch (e: RuntimeException) { + log.warn("Unable to make field ${type.unwrap().name}#${field.name} accessible. " + + "Be sure to provide a resolver or open the enclosing module if possible.") + } + } + private fun missingFieldResolver(field: FieldDefinition, searches: List, scanProperties: Boolean): FieldResolver { return if (options.allowUnimplementedResolvers || options.missingResolverDataFetcher != null) { if (options.allowUnimplementedResolvers) { diff --git a/src/test/kotlin/graphql/kickstart/tools/InaccessibleFieldResolverTest.kt b/src/test/kotlin/graphql/kickstart/tools/InaccessibleFieldResolverTest.kt new file mode 100644 index 00000000..8c78ef04 --- /dev/null +++ b/src/test/kotlin/graphql/kickstart/tools/InaccessibleFieldResolverTest.kt @@ -0,0 +1,98 @@ +package graphql.kickstart.tools + +import graphql.ExceptionWhileDataFetching +import graphql.GraphQL +import graphql.execution.AsyncExecutionStrategy +import graphql.schema.GraphQLSchema +import org.junit.Ignore +import org.junit.Test +import java.util.* + +/** + * Reflective access to private fields in closed modules is not possible since Java 17. + * When using objects from closed modules in the schema the field resolver scanner will try to access their fields but fail. + * If no other resolver is provided that will result in an [IllegalAccessException] + */ +class InaccessibleFieldResolverTest { + + @Test + @Ignore // TODO enable test after upgrading to 17 + fun `private field from closed module is not accessible`() { + val schema: GraphQLSchema = SchemaParser.newParser() + .schemaString( + """ + type Query { + locale: Locale + } + + type Locale { + country: String! + languageTag: String! + } + """) + .resolvers(Query()) + .build() + .makeExecutableSchema() + val gql: GraphQL = GraphQL.newGraphQL(schema) + .queryExecutionStrategy(AsyncExecutionStrategy()) + .build() + + val result = gql.execute( + """ + query { + locale { + country + languageTag + } + } + """ + ) + + assertEquals(result.errors.size, 1) + val exceptionWhileDataFetching = result.errors[0] as ExceptionWhileDataFetching + assert(exceptionWhileDataFetching.exception is IllegalAccessException) + } + + @Test + fun `private field from closed module is accessible through resolver`() { + val schema: GraphQLSchema = SchemaParser.newParser() + .schemaString( + """ + type Query { + locale: Locale + } + + type Locale { + country: String! + languageTag: String! + } + """) + .resolvers(Query(), LocaleResolver()) + .build() + .makeExecutableSchema() + val gql: GraphQL = GraphQL.newGraphQL(schema) + .queryExecutionStrategy(AsyncExecutionStrategy()) + .build() + + val data = assertNoGraphQlErrors(gql) { + """ + query { + locale { + country + languageTag + } + } + """ + } + + assertEquals(data["locale"], mapOf("country" to "US", "languageTag" to "en-US")) + } + + class Query : GraphQLQueryResolver { + fun locale(): Locale = Locale.US + } + + class LocaleResolver : GraphQLResolver { + fun languageTag(locale: Locale): String = locale.toLanguageTag() + } +}