├── .gitignore
├── .idea
├── compiler.xml
├── gradle.xml
├── misc.xml
├── modules.xml
└── vcs.xml
├── README.md
├── build.gradle
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── src
├── main
├── java
│ └── com
│ │ └── coxautodev
│ │ └── graphql
│ │ └── tools
│ │ ├── GraphQLMutationResolver.java
│ │ ├── GraphQLQueryResolver.java
│ │ ├── GraphQLResolver.java
│ │ ├── GraphQLSubscriptionResolver.java
│ │ ├── GuiceAopProxyHandler.java
│ │ ├── ObjectMapperConfigurer.java
│ │ └── ProxyHandler.java
├── kotlin
│ └── com
│ │ ├── coxautodev
│ │ └── graphql
│ │ │ └── tools
│ │ │ ├── DictionaryTypeResolver.kt
│ │ │ ├── FieldResolver.kt
│ │ │ ├── FieldResolverScanner.kt
│ │ │ ├── GenericType.kt
│ │ │ ├── MethodFieldResolver.kt
│ │ │ ├── ObjectMapperConfigurerContext.kt
│ │ │ ├── PropertyFieldResolver.kt
│ │ │ ├── ResolverInfo.kt
│ │ │ ├── RootTypeInfo.kt
│ │ │ ├── ScannedSchemaObjects.kt
│ │ │ ├── SchemaClassScanner.kt
│ │ │ ├── SchemaObjects.kt
│ │ │ ├── SchemaParser.kt
│ │ │ ├── SchemaParserBuilder.kt
│ │ │ ├── Spring4AopProxyHandler.kt
│ │ │ ├── TypeClassMatcher.kt
│ │ │ └── Utils.kt
│ │ ├── framework
│ │ ├── Application.kt
│ │ ├── datamodel
│ │ │ ├── edges
│ │ │ │ ├── Linked.kt
│ │ │ │ ├── Relationship.kt
│ │ │ │ └── Traversal.kt
│ │ │ └── node
│ │ │ │ ├── Node.kt
│ │ │ │ ├── NodeQueryResolver.kt
│ │ │ │ └── NodeTypeResolver.kt
│ │ ├── graphql
│ │ │ ├── Mutation.kt
│ │ │ └── TraversalDataLoader.kt
│ │ └── gremlin
│ │ │ └── Extensions.kt
│ │ └── starwars
│ │ ├── StarwarsGraph.kt
│ │ ├── character
│ │ ├── Character.kt
│ │ ├── CharacterQueryResolver.kt
│ │ └── CharacterTypeResolver.kt
│ │ ├── droid
│ │ ├── Droid.kt
│ │ ├── DroidMutationResolver.kt
│ │ ├── DroidQueryResolver.kt
│ │ └── DroidTypeResolver.kt
│ │ ├── episode
│ │ ├── Episode.kt
│ │ └── EpisodeTypeResolver.kt
│ │ └── human
│ │ ├── Human.kt
│ │ ├── HumanQueryResolver.kt
│ │ └── HumanTypeResolver.kt
└── resources
│ ├── application.properties
│ ├── graphqls
│ └── starwars.graphqls
│ ├── janusgraph-configuration.properties
│ └── static
│ └── graphiql.html
└── test
└── kotlin
└── com
└── starwars
└── GraphTests.kt
/.gitignore:
--------------------------------------------------------------------------------
1 | .gradle
2 | /build/
3 | /out/
4 | !gradle/wrapper/gradle-wrapper.jar
5 |
6 | ### IntelliJ IDEA ###
7 | .idea/**/libraries
8 | .idea/**/workspace.xml
9 | *.iws
10 | *.iml
11 | *.ipr
12 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
18 |
19 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # An Example GraphQL + JanusGraph Web App
2 |
3 | This repository demonstrates how to implement a graph-based web application using JanusGraph. It is written in Kotlin and exposes a GraphQL interface.
4 |
5 | The application was written using:
6 |
7 | - [Kotlin](https://kotlinlang.org/)
8 | - [Janus Graph](http://janusgraph.org/)
9 | - [Ferma OGM](http://syncleus.com/Ferma/)
10 | - [Spring Boot](https://projects.spring.io/spring-boot/)
11 | - [GraphQL](http://graphql.org/)
12 | - [GraphQL Tools](https://github.com/graphql-java/graphql-java-tools)
13 |
14 | The project is split into 2 packages, `framework` and `starwars`. `framework` adds some features to the Ferma OGM, such as providing type-safe traversals. The `starwars` package shows how any data model could plug into the `framework` package and Ferma OGM with minimal effort. The `framework` package is not currently offered as a library due to its lack of documentation and testing.
15 |
16 |
17 | To build and run the application locally, from the repository root:
18 | ```
19 | gradlew run
20 | ```
21 |
22 | Then load `http://localhost:5000/graphiql.html` and start playing around in GraphiQL
23 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | ext {
3 | kotlinVersion = '1.2.21'
4 | springBootVersion = '1.5.10.BUILD-SNAPSHOT'
5 | }
6 | repositories {
7 | mavenCentral()
8 | maven { url "https://repo.spring.io/snapshot" }
9 | maven { url "https://repo.spring.io/milestone" }
10 | }
11 | dependencies {
12 | classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
13 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}")
14 | classpath("org.jetbrains.kotlin:kotlin-allopen:${kotlinVersion}")
15 | }
16 | }
17 |
18 | apply plugin: 'kotlin'
19 | apply plugin: 'kotlin-spring'
20 | apply plugin: 'org.springframework.boot'
21 | apply plugin: 'io.spring.dependency-management'
22 | apply plugin: 'application'
23 | apply plugin: 'idea'
24 |
25 | repositories {
26 | mavenCentral()
27 | maven { url "https://repo.spring.io/snapshot" }
28 | maven { url "https://repo.spring.io/milestone" }
29 | }
30 |
31 | mainClassName = 'com.framework.ApplicationKt'
32 |
33 | kotlin { experimental { coroutines 'enable' } }
34 |
35 | dependencies {
36 |
37 | compile("org.springframework.boot:spring-boot-starter-actuator")
38 | compile("org.springframework.boot:spring-boot-starter-web")
39 | compile("org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlinVersion}")
40 | compile("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}")
41 | compile("com.fasterxml.jackson.module:jackson-module-kotlin:2.9.3")
42 | compile("com.graphql-java:graphql-spring-boot-starter:3.10.0")
43 | compile("org.janusgraph:janusgraph-core:0.2.0")
44 | compile("com.syncleus.ferma:ferma:3.2.1")
45 | compile("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:0.21.2")
46 |
47 | testCompile('org.springframework.boot:spring-boot-starter-test')
48 |
49 |
50 | // Graphql tools dependency
51 | compile("com.esotericsoftware:reflectasm:1.11.3")
52 | }
53 |
54 | tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
55 | sourceCompatibility = JavaVersion.VERSION_1_8
56 | targetCompatibility = JavaVersion.VERSION_1_8
57 | kotlinOptions {
58 | jvmTarget = "1.8"
59 | apiVersion = "1.1"
60 | languageVersion = "1.2"
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pm-dev/janusgraph-exploration/638b34af9ac67ed28c1acdff468a7b978f678079/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Wed Jan 17 20:58:48 PST 2018
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.2-all.zip
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/src/main/java/com/coxautodev/graphql/tools/GraphQLMutationResolver.java:
--------------------------------------------------------------------------------
1 | package com.coxautodev.graphql.tools;
2 |
3 | /**
4 | * @author Andrew Potter
5 | */
6 | public interface GraphQLMutationResolver extends GraphQLResolver {
7 | }
8 |
--------------------------------------------------------------------------------
/src/main/java/com/coxautodev/graphql/tools/GraphQLQueryResolver.java:
--------------------------------------------------------------------------------
1 | package com.coxautodev.graphql.tools;
2 |
3 | /**
4 | * @author Andrew Potter
5 | */
6 | public interface GraphQLQueryResolver extends GraphQLResolver {
7 | }
8 |
--------------------------------------------------------------------------------
/src/main/java/com/coxautodev/graphql/tools/GraphQLResolver.java:
--------------------------------------------------------------------------------
1 | package com.coxautodev.graphql.tools;
2 |
3 | /**
4 | * @author Andrew Potter
5 | */
6 | public interface GraphQLResolver {
7 | }
8 |
9 |
--------------------------------------------------------------------------------
/src/main/java/com/coxautodev/graphql/tools/GraphQLSubscriptionResolver.java:
--------------------------------------------------------------------------------
1 | package com.coxautodev.graphql.tools;
2 |
3 | public interface GraphQLSubscriptionResolver extends GraphQLResolver {
4 | }
5 |
--------------------------------------------------------------------------------
/src/main/java/com/coxautodev/graphql/tools/GuiceAopProxyHandler.java:
--------------------------------------------------------------------------------
1 | package com.coxautodev.graphql.tools;
2 |
3 | public class GuiceAopProxyHandler implements ProxyHandler {
4 | @Override
5 | public boolean canHandle(GraphQLResolver> resolver) {
6 | return isGuiceProxy(resolver);
7 | }
8 |
9 | @Override
10 | public Class> getTargetClass(GraphQLResolver> resolver) {
11 | return resolver.getClass().getSuperclass();
12 | }
13 |
14 | private boolean isGuiceProxy(GraphQLResolver> resolver) {
15 | return resolver.getClass().getName().contains("$$EnhancerByGuice$$");
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/main/java/com/coxautodev/graphql/tools/ObjectMapperConfigurer.java:
--------------------------------------------------------------------------------
1 | package com.coxautodev.graphql.tools;
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper;
4 |
5 | /**
6 | * @author Andrew Potter
7 | */
8 | public interface ObjectMapperConfigurer {
9 | void configure(ObjectMapper mapper, ObjectMapperConfigurerContext context);
10 | }
11 |
--------------------------------------------------------------------------------
/src/main/java/com/coxautodev/graphql/tools/ProxyHandler.java:
--------------------------------------------------------------------------------
1 | package com.coxautodev.graphql.tools;
2 |
3 | /**
4 | * @author Andrew Potter
5 | */
6 | public interface ProxyHandler {
7 | boolean canHandle(GraphQLResolver> resolver);
8 | Class> getTargetClass(GraphQLResolver> resolver);
9 | }
10 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/coxautodev/graphql/tools/DictionaryTypeResolver.kt:
--------------------------------------------------------------------------------
1 | package com.coxautodev.graphql.tools
2 |
3 | import com.google.common.collect.BiMap
4 | import graphql.TypeResolutionEnvironment
5 | import graphql.language.TypeDefinition
6 | import graphql.schema.GraphQLInterfaceType
7 | import graphql.schema.GraphQLObjectType
8 | import graphql.schema.GraphQLUnionType
9 | import graphql.schema.TypeResolver
10 |
11 | /**
12 | * @author Andrew Potter
13 | */
14 | abstract class DictionaryTypeResolver(private val dictionary: BiMap, TypeDefinition>, private val types: Map) : TypeResolver {
15 |
16 | private val namesToClass = dictionary.inverse().mapKeys { it.key.name }
17 |
18 | override fun getType(env: TypeResolutionEnvironment): GraphQLObjectType? {
19 | val obj = env.getObject()
20 | val clazz = obj.javaClass
21 | val name = dictionary[clazz]?.name ?: clazz.simpleName
22 | val type = types[name]
23 | if (type != null) {
24 | return type
25 | }
26 | for (validType in types) {
27 | if (namesToClass[validType.key]?.isInstance(obj) == true) {
28 | return validType.value
29 | }
30 | }
31 | throw TypeResolverError(getError(name))
32 | }
33 |
34 | abstract fun getError(name: String): String
35 | }
36 |
37 | class InterfaceTypeResolver(dictionary: BiMap, TypeDefinition>, private val thisInterface: GraphQLInterfaceType, types: List) : DictionaryTypeResolver(dictionary, types.filter { it.interfaces.any { it.name == thisInterface.name } }.associateBy { it.name }) {
38 | override fun getError(name: String) = "Expected object type with name '$name' to implement interface '${thisInterface.name}', but it doesn't!"
39 | }
40 |
41 | class UnionTypeResolver(dictionary: BiMap, TypeDefinition>, private val thisUnion: GraphQLUnionType, types: List) : DictionaryTypeResolver(dictionary, types.filter { type -> thisUnion.types.any { it.name == type.name } }.associateBy { it.name }) {
42 | override fun getError(name: String) = "Expected object type with name '$name' to exist for union '${thisUnion.name}', but it doesn't!"
43 | }
44 |
45 | class TypeResolverError(message: String, cause: Throwable? = null) : RuntimeException(message, cause)
46 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/coxautodev/graphql/tools/FieldResolver.kt:
--------------------------------------------------------------------------------
1 | package com.coxautodev.graphql.tools
2 |
3 | import graphql.language.FieldDefinition
4 | import graphql.schema.DataFetcher
5 | import graphql.schema.DataFetchingEnvironment
6 |
7 | /**
8 | * @author Andrew Potter
9 | */
10 | internal abstract class FieldResolver(val field: FieldDefinition, val search: FieldResolverScanner.Search, val options: SchemaParserOptions, relativeTo: JavaType) {
11 | val resolverInfo: ResolverInfo = search.resolverInfo
12 | val genericType = GenericType(search.type, options).relativeToPotentialParent(relativeTo)
13 |
14 | abstract fun scanForMatches(): List
15 | abstract fun createDataFetcher(): DataFetcher<*>
16 |
17 | /**
18 | * Add source resolver depending on whether or not this is a resolver method
19 | */
20 | protected fun getSourceResolver(): SourceResolver = if(this.search.source != null) {
21 | ({ this.search.source })
22 | } else {
23 | ({ environment ->
24 | val source = environment.getSource()
25 |
26 | if(!this.genericType.isAssignableFrom(source.javaClass)) {
27 | throw ResolverError("Expected source object to be an instance of '${this.genericType.getRawClass().name}' but instead got '${source.javaClass.name}'")
28 | }
29 |
30 | source
31 | })
32 | }
33 | }
34 |
35 | internal class MissingFieldResolver(field: FieldDefinition, options: SchemaParserOptions): FieldResolver(field, FieldResolverScanner.Search(Any::class.java, MissingResolverInfo(), null), options, Any::class.java) {
36 | override fun scanForMatches(): List = listOf()
37 | override fun createDataFetcher(): DataFetcher<*> = DataFetcher { TODO("Schema resolver not implemented") }
38 | }
39 |
40 | internal typealias SourceResolver = (DataFetchingEnvironment) -> Any
41 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/coxautodev/graphql/tools/FieldResolverScanner.kt:
--------------------------------------------------------------------------------
1 | package com.coxautodev.graphql.tools
2 |
3 | import graphql.Scalars
4 | import graphql.language.FieldDefinition
5 | import graphql.language.TypeName
6 | import graphql.schema.DataFetchingEnvironment
7 | import org.apache.commons.lang3.ClassUtils
8 | import org.apache.commons.lang3.reflect.FieldUtils
9 | import org.slf4j.LoggerFactory
10 | import java.lang.reflect.Modifier
11 | import java.lang.reflect.ParameterizedType
12 |
13 | /**
14 | * @author Andrew Potter
15 | */
16 | internal class FieldResolverScanner(val options: SchemaParserOptions) {
17 |
18 | companion object {
19 | private val log = LoggerFactory.getLogger(FieldResolverScanner::class.java)
20 |
21 | fun getAllMethods(type: Class<*>) =
22 | type.declaredMethods.toList() + ClassUtils.getAllSuperclasses(type).flatMap { it.declaredMethods.toList() }.filter { !Modifier.isPrivate(it.modifiers) }
23 | }
24 |
25 | fun findFieldResolver(field: FieldDefinition, resolverInfo: ResolverInfo): FieldResolver {
26 | val searches = resolverInfo.getFieldSearches()
27 |
28 | val scanProperties = field.inputValueDefinitions.isEmpty()
29 | val found = searches.mapNotNull { search -> findFieldResolver(field, search, scanProperties) }
30 |
31 | if (resolverInfo is RootResolverInfo && found.size > 1) {
32 | throw FieldResolverError("Found more than one matching resolver for field '$field': $found")
33 | }
34 |
35 | return found.firstOrNull() ?: missingFieldResolver(field, searches, scanProperties)
36 | }
37 |
38 | private fun missingFieldResolver(field: FieldDefinition, searches: List, scanProperties: Boolean): FieldResolver {
39 | return if (options.allowUnimplementedResolvers) {
40 | log.warn("Missing resolver for field: $field")
41 |
42 | MissingFieldResolver(field, options)
43 | } else {
44 | throw FieldResolverError(getMissingFieldMessage(field, searches, scanProperties))
45 | }
46 | }
47 |
48 | private fun findFieldResolver(field: FieldDefinition, search: Search, scanProperties: Boolean): FieldResolver? {
49 | val method = findResolverMethod(field, search)
50 | if (method != null) {
51 | return MethodFieldResolver(field, search, options, method.apply { isAccessible = true })
52 | }
53 |
54 | if (scanProperties) {
55 | val property = findResolverProperty(field, search)
56 | if (property != null) {
57 | return PropertyFieldResolver(field, search, options, property.apply { isAccessible = true })
58 | }
59 | }
60 |
61 | return null
62 | }
63 |
64 | private fun isBoolean(type: GraphQLLangType) = type.unwrap().let { it is TypeName && it.name == Scalars.GraphQLBoolean.name }
65 |
66 | private fun findResolverMethod(field: FieldDefinition, search: Search): java.lang.reflect.Method? {
67 |
68 | val methods = getAllMethods(search.type)
69 | val argumentCount = field.inputValueDefinitions.size + if (search.requiredFirstParameterType != null) 1 else 0
70 | val name = field.name
71 |
72 | val isBoolean = isBoolean(field.type)
73 |
74 | // Check for the following one by one:
75 | // 1. Method with exact field name
76 | // 2. Method that returns a boolean with "is" style getter
77 | // 3. Method with "get" style getter
78 | return methods.find {
79 | it.name == name && verifyMethodArguments(it, argumentCount, search)
80 | } ?: methods.find {
81 | (isBoolean && it.name == "is${name.capitalize()}") && verifyMethodArguments(it, argumentCount, search)
82 | } ?: methods.find {
83 | it.name == "get${name.capitalize()}" && verifyMethodArguments(it, argumentCount, search)
84 | }
85 | }
86 |
87 | private fun verifyMethodArguments(method: java.lang.reflect.Method, requiredCount: Int, search: Search): Boolean {
88 | val appropriateFirstParameter = if (search.requiredFirstParameterType != null) {
89 | if(MethodFieldResolver.isBatched(method, search)) {
90 | verifyBatchedMethodFirstArgument(method.genericParameterTypes.firstOrNull(), search.requiredFirstParameterType)
91 | } else {
92 | method.parameterTypes.firstOrNull() == search.requiredFirstParameterType
93 | }
94 | } else {
95 | true
96 | }
97 |
98 | val correctParameterCount = method.parameterCount == requiredCount || (method.parameterCount == (requiredCount + 1) && method.parameterTypes.last() == DataFetchingEnvironment::class.java)
99 | return correctParameterCount && appropriateFirstParameter
100 | }
101 |
102 | private fun verifyBatchedMethodFirstArgument(firstType: JavaType?, requiredFirstParameterType: Class<*>?): Boolean {
103 | if(firstType == null) {
104 | return false
105 | }
106 |
107 | if(firstType !is ParameterizedType) {
108 | return false
109 | }
110 |
111 | if(!TypeClassMatcher.isListType(firstType, GenericType(firstType, options))) {
112 | return false
113 | }
114 |
115 | val typeArgument = firstType.actualTypeArguments.first() as? Class<*> ?: return false
116 |
117 | return typeArgument == requiredFirstParameterType
118 | }
119 |
120 | private fun findResolverProperty(field: FieldDefinition, search: Search) =
121 | FieldUtils.getAllFields(search.type).find { it.name == field.name }
122 |
123 | private fun getMissingFieldMessage(field: FieldDefinition, searches: List, scannedProperties: Boolean): String {
124 | val signatures = mutableListOf("")
125 | val isBoolean = isBoolean(field.type)
126 |
127 | searches.forEach { search ->
128 | signatures.addAll(getMissingMethodSignatures(field, search, isBoolean, scannedProperties))
129 | }
130 |
131 | return "No method${if (scannedProperties) " or field" else ""} found with any of the following signatures (with or without ${DataFetchingEnvironment::class.java.name} as the last argument), in priority order:\n${signatures.joinToString("\n ")}"
132 | }
133 |
134 | private fun getMissingMethodSignatures(field: FieldDefinition, search: Search, isBoolean: Boolean, scannedProperties: Boolean): List {
135 | val baseType = search.type
136 | val signatures = mutableListOf()
137 | val args = mutableListOf()
138 | val sep = ", "
139 |
140 | if (search.requiredFirstParameterType != null) {
141 | args.add(search.requiredFirstParameterType.name)
142 | }
143 |
144 | args.addAll(field.inputValueDefinitions.map { "~${it.name}" })
145 |
146 | val argString = args.joinToString(sep)
147 |
148 | signatures.add("${baseType.name}.${field.name}($argString)")
149 | if (isBoolean) {
150 | signatures.add("${baseType.name}.is${field.name.capitalize()}($argString)")
151 | }
152 | signatures.add("${baseType.name}.get${field.name.capitalize()}($argString)")
153 | if (scannedProperties) {
154 | signatures.add("${baseType.name}.${field.name}")
155 | }
156 |
157 | return signatures
158 | }
159 |
160 | data class Search(val type: Class<*>, val resolverInfo: ResolverInfo, val source: Any?, val requiredFirstParameterType: Class<*>? = null, val allowBatched: Boolean = false)
161 | }
162 |
163 | class FieldResolverError(msg: String): RuntimeException(msg)
164 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/coxautodev/graphql/tools/GenericType.kt:
--------------------------------------------------------------------------------
1 | package com.coxautodev.graphql.tools
2 |
3 | import com.google.common.primitives.Primitives
4 | import org.apache.commons.lang3.reflect.TypeUtils
5 | import sun.reflect.generics.reflectiveObjects.WildcardTypeImpl
6 | import java.lang.reflect.ParameterizedType
7 | import java.lang.reflect.TypeVariable
8 |
9 | /**
10 | * @author Andrew Potter
11 | */
12 | open internal class GenericType(protected val mostSpecificType: JavaType, protected val options: SchemaParserOptions) {
13 |
14 | fun isTypeAssignableFromRawClass(type: ParameterizedType, clazz: Class<*>) =
15 | clazz.isAssignableFrom(getRawClass(type.rawType))
16 |
17 | fun getRawClass() = getRawClass(mostSpecificType)
18 |
19 | fun getRawClass(type: JavaType): Class<*> = TypeUtils.getRawType(type, mostSpecificType)
20 |
21 | fun isAssignableFrom(type: JavaType) = TypeUtils.isAssignable(type, mostSpecificType)
22 |
23 | fun relativeToPotentialParent(declaringType: JavaType): RelativeTo {
24 | if(declaringType !is Class<*>) {
25 | return relativeToType(declaringType)
26 | }
27 |
28 | val type = getGenericSuperType(mostSpecificType, declaringType)
29 | if(type == null) {
30 | error("Unable to find generic type of class ${TypeUtils.toString(declaringType)} relative to ${TypeUtils.toString(mostSpecificType)}")
31 | } else {
32 | return relativeToType(type)
33 | }
34 | }
35 | fun relativeToType(declaringType: JavaType) = RelativeTo(declaringType, mostSpecificType, options)
36 |
37 | fun getGenericInterface(targetInterface: Class<*>) = getGenericInterface(mostSpecificType, targetInterface)
38 |
39 | private fun getGenericInterface(type: JavaType?, targetInterface: Class<*>): JavaType? {
40 | if(type == null) {
41 | return null
42 | }
43 |
44 | val raw = type as? Class<*> ?: getRawClass(type)
45 |
46 | if(raw == targetInterface) {
47 | return type
48 | }
49 |
50 | val possibleSubInterface = raw.genericInterfaces.find { genericInterface ->
51 | TypeUtils.isAssignable(genericInterface, targetInterface)
52 | } ?: raw.interfaces.find { iface ->
53 | TypeUtils.isAssignable(iface, targetInterface)
54 | } ?: getGenericInterface(raw.genericSuperclass, targetInterface) ?: return null
55 |
56 | return getGenericInterface(possibleSubInterface, targetInterface)
57 | }
58 |
59 | fun getGenericSuperType(targetSuperClass: Class<*>) = getGenericSuperType(mostSpecificType, targetSuperClass)
60 |
61 | private fun getGenericSuperType(type: JavaType?, targetSuperClass: Class<*>): JavaType? {
62 | if(type == null) {
63 | return null
64 | }
65 |
66 | val raw = type as? Class<*> ?: TypeUtils.getRawType(type, type)
67 |
68 | if(raw == targetSuperClass) {
69 | return type
70 | }
71 |
72 | return getGenericSuperType(raw.genericSuperclass, targetSuperClass)
73 | }
74 |
75 | class RelativeTo(private val declaringType: JavaType, mostSpecificType: JavaType, options: SchemaParserOptions): GenericType(mostSpecificType, options) {
76 |
77 | /**
78 | * Unwrap certain Java types to find the "real" class.
79 | */
80 | fun unwrapGenericType(type: JavaType): JavaType {
81 | return when(type) {
82 | is ParameterizedType -> {
83 | val rawType = type.rawType
84 | val genericType = options.genericWrappers.find { it.type == rawType } ?: return type
85 |
86 | val typeArguments = type.actualTypeArguments
87 | if(typeArguments.size <= genericType.index) {
88 | throw IndexOutOfBoundsException("Generic type '${TypeUtils.toString(type)}' does not have a type argument at index ${genericType.index}!")
89 | }
90 |
91 | return unwrapGenericType(typeArguments[genericType.index])
92 | }
93 | is Class<*> -> if(type.isPrimitive) Primitives.wrap(type) else type
94 | is TypeVariable<*> -> {
95 | if(declaringType !is ParameterizedType) {
96 | error("Could not resolve type variable '${TypeUtils.toLongString(type)}' because declaring type is not parameterized: ${TypeUtils.toString(declaringType)}")
97 | }
98 |
99 | unwrapGenericType(TypeUtils.determineTypeArguments(getRawClass(mostSpecificType), declaringType)[type] ?: error("No type variable found for: ${TypeUtils.toLongString(type)}"))
100 | }
101 | is WildcardTypeImpl -> type.upperBounds.firstOrNull() ?: throw error("Unable to unwrap type, wildcard has no upper bound: $type")
102 | else -> error("Unable to unwrap type: $type")
103 | }
104 | }
105 | }
106 | }
--------------------------------------------------------------------------------
/src/main/kotlin/com/coxautodev/graphql/tools/MethodFieldResolver.kt:
--------------------------------------------------------------------------------
1 | package com.coxautodev.graphql.tools
2 |
3 | import com.esotericsoftware.reflectasm.MethodAccess
4 | import com.fasterxml.jackson.core.type.TypeReference
5 | import com.fasterxml.jackson.databind.ObjectMapper
6 | import com.fasterxml.jackson.datatype.jdk8.Jdk8Module
7 | import com.fasterxml.jackson.module.kotlin.registerKotlinModule
8 | import graphql.execution.batched.Batched
9 | import graphql.language.FieldDefinition
10 | import graphql.language.NonNullType
11 | import graphql.schema.DataFetcher
12 | import graphql.schema.DataFetchingEnvironment
13 | import java.lang.reflect.Method
14 | import java.util.Optional
15 |
16 | /**
17 | * @author Andrew Potter
18 | */
19 | internal class MethodFieldResolver(field: FieldDefinition, search: FieldResolverScanner.Search, options: SchemaParserOptions, val method: Method): FieldResolver(field, search, options, method.declaringClass) {
20 |
21 | companion object {
22 | fun isBatched(method: Method, search: FieldResolverScanner.Search): Boolean {
23 | if(method.getAnnotation(Batched::class.java) != null) {
24 | if(!search.allowBatched) {
25 | throw ResolverError("The @Batched annotation is only allowed on non-root resolver methods, but it was found on ${search.type.name}#${method.name}!")
26 | }
27 |
28 | return true
29 | }
30 |
31 | return false
32 | }
33 | }
34 |
35 | private val dataFetchingEnvironment = method.parameterCount == (field.inputValueDefinitions.size + getIndexOffset() + 1)
36 |
37 | override fun createDataFetcher(): DataFetcher<*> {
38 | val batched = isBatched(method, search)
39 | val args = mutableListOf()
40 | val mapper = ObjectMapper().apply {
41 | options.objectMapperConfigurer.configure(this, ObjectMapperConfigurerContext(field))
42 | }.registerModule(Jdk8Module()).registerKotlinModule()
43 |
44 | // Add source argument if this is a resolver (but not a root resolver)
45 | if(this.search.requiredFirstParameterType != null) {
46 | val expectedType = if(batched) Iterable::class.java else this.search.requiredFirstParameterType
47 |
48 | args.add({ environment ->
49 | val source = environment.getSource()
50 | if (!(expectedType.isAssignableFrom(source.javaClass))) {
51 | throw ResolverError("Source type (${source.javaClass.name}) is not expected type (${expectedType.name})!")
52 | }
53 |
54 | source
55 | })
56 | }
57 |
58 | // Add an argument for each argument defined in the GraphQL schema
59 | this.field.inputValueDefinitions.forEachIndexed { index, definition ->
60 |
61 | val genericParameterType = this.getJavaMethodParameterType(index) ?: throw ResolverError("Missing method type at position ${this.getJavaMethodParameterIndex(index)}, this is most likely a bug with graphql-java-tools")
62 |
63 | val isNonNull = definition.type is NonNullType
64 | val isOptional = this.genericType.getRawClass(genericParameterType) == Optional::class.java
65 |
66 | val typeReference = object: TypeReference() {
67 | override fun getType() = genericParameterType
68 | }
69 |
70 | args.add({ environment ->
71 | val value = environment.arguments[definition.name] ?: if(isNonNull) {
72 | throw ResolverError("Missing required argument with name '${definition.name}', this is most likely a bug with graphql-java-tools")
73 | } else {
74 | null
75 | }
76 |
77 | if(value == null && isOptional) {
78 | return@add Optional.empty()
79 | }
80 |
81 | return@add mapper.convertValue(value, typeReference)
82 | })
83 | }
84 |
85 | // Add DataFetchingEnvironment argument
86 | if(this.dataFetchingEnvironment) {
87 | args.add({ environment -> environment })
88 | }
89 |
90 | return if(batched) {
91 | BatchedMethodFieldResolverDataFetcher(getSourceResolver(), this.method, args)
92 | } else {
93 | MethodFieldResolverDataFetcher(getSourceResolver(), this.method, args)
94 | }
95 | }
96 |
97 | override fun scanForMatches(): List {
98 | val batched = isBatched(method, search)
99 | val returnValueMatch = TypeClassMatcher.PotentialMatch.returnValue(field.type, method.genericReturnType, genericType, SchemaClassScanner.ReturnValueReference(method), batched)
100 |
101 | return field.inputValueDefinitions.mapIndexed { i, inputDefinition ->
102 | TypeClassMatcher.PotentialMatch.parameterType(inputDefinition.type, getJavaMethodParameterType(i)!!, genericType, SchemaClassScanner.MethodParameterReference(method, i), batched)
103 | } + listOf(returnValueMatch)
104 | }
105 |
106 | private fun getIndexOffset() = if(resolverInfo is NormalResolverInfo) 1 else 0
107 | private fun getJavaMethodParameterIndex(index: Int) = index + getIndexOffset()
108 |
109 | private fun getJavaMethodParameterType(index: Int): JavaType? {
110 | val methodIndex = getJavaMethodParameterIndex(index)
111 | val parameters = method.parameterTypes
112 |
113 | return if(parameters.size > methodIndex) {
114 | method.genericParameterTypes[getJavaMethodParameterIndex(index)]
115 | } else {
116 | null
117 | }
118 | }
119 |
120 | override fun toString() = "MethodFieldResolver{method=$method}"
121 | }
122 |
123 | open class MethodFieldResolverDataFetcher(private val sourceResolver: SourceResolver, method: Method, private val args: List): DataFetcher {
124 |
125 | // Convert to reflactasm reflection
126 | private val methodAccess = MethodAccess.get(method.declaringClass)!!
127 | private val methodIndex = methodAccess.getIndex(method.name, *method.parameterTypes)
128 |
129 | override fun get(environment: DataFetchingEnvironment): Any? {
130 | val source = sourceResolver(environment)
131 | val args = this.args.map { it(environment) }.toTypedArray()
132 | val result = methodAccess.invoke(source, methodIndex, *args)
133 | return if(result != null && result is Optional<*>) result.orElse(null) else result
134 | }
135 | }
136 |
137 | class BatchedMethodFieldResolverDataFetcher(sourceResolver: SourceResolver, method: Method, args: List): MethodFieldResolverDataFetcher(sourceResolver, method, args) {
138 | @Batched override fun get(environment: DataFetchingEnvironment) = super.get(environment)
139 | }
140 |
141 | internal typealias ArgumentPlaceholder = (DataFetchingEnvironment) -> Any?
142 |
143 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/coxautodev/graphql/tools/ObjectMapperConfigurerContext.kt:
--------------------------------------------------------------------------------
1 | package com.coxautodev.graphql.tools
2 |
3 | import graphql.language.FieldDefinition
4 |
5 | /**
6 | * @author Andrew Potter
7 | */
8 | data class ObjectMapperConfigurerContext(val field: FieldDefinition)
--------------------------------------------------------------------------------
/src/main/kotlin/com/coxautodev/graphql/tools/PropertyFieldResolver.kt:
--------------------------------------------------------------------------------
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.Field
7 |
8 | /**
9 | * @author Andrew Potter
10 | */
11 | internal class PropertyFieldResolver(field: FieldDefinition, search: FieldResolverScanner.Search, options: SchemaParserOptions, val property: Field): FieldResolver(field, search, options, property.declaringClass) {
12 | override fun createDataFetcher(): DataFetcher<*> {
13 | return PropertyFieldResolverDataFetcher(getSourceResolver(), property)
14 | }
15 |
16 | override fun scanForMatches(): List {
17 | return listOf(TypeClassMatcher.PotentialMatch.returnValue(field.type, property.genericType, genericType, SchemaClassScanner.FieldTypeReference(property), false))
18 | }
19 |
20 | override fun toString() = "PropertyFieldResolver{property=$property}"
21 | }
22 |
23 | class PropertyFieldResolverDataFetcher(private val sourceResolver: SourceResolver, private val field: Field): DataFetcher {
24 | override fun get(environment: DataFetchingEnvironment): Any? {
25 | return field.get(sourceResolver(environment))
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/coxautodev/graphql/tools/ResolverInfo.kt:
--------------------------------------------------------------------------------
1 | package com.coxautodev.graphql.tools
2 |
3 | import org.apache.commons.lang3.reflect.TypeUtils
4 | import java.lang.reflect.ParameterizedType
5 |
6 | internal abstract class ResolverInfo {
7 | abstract fun getFieldSearches(): List
8 |
9 | fun getRealResolverClass(resolver: GraphQLResolver<*>, options: SchemaParserOptions) =
10 | options.proxyHandlers.find { it.canHandle(resolver) }?.getTargetClass(resolver) ?: resolver.javaClass
11 | }
12 |
13 | internal class NormalResolverInfo(val resolver: GraphQLResolver<*>, private val options: SchemaParserOptions): ResolverInfo() {
14 | private val resolverType = getRealResolverClass(resolver, options)
15 | val dataClassType = findDataClass()
16 |
17 | private fun findDataClass(): Class<*> {
18 | // Grab the parent interface with type GraphQLResolver from our resolver and get its first type argument.
19 | val interfaceType = GenericType(resolverType, options).getGenericInterface(GraphQLResolver::class.java)
20 | if(interfaceType == null || interfaceType !is ParameterizedType) {
21 | error("${GraphQLResolver::class.java.simpleName} interface was not parameterized for: ${resolverType.name}")
22 | }
23 |
24 | val type = TypeUtils.determineTypeArguments(resolverType, interfaceType)[GraphQLResolver::class.java.typeParameters[0]]
25 |
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.")
28 | }
29 |
30 | if(type == Void::class.java) {
31 | throw ResolverError("Resolvers may not have ${Void::class.java.name} as their type, use a real type or use a root resolver interface.")
32 | }
33 |
34 | return type
35 | }
36 |
37 | override fun getFieldSearches(): List {
38 | return listOf(
39 | FieldResolverScanner.Search(resolverType, this, resolver, dataClassType, true),
40 | FieldResolverScanner.Search(dataClassType, this, null)
41 | )
42 | }
43 | }
44 |
45 | internal class RootResolverInfo(val resolvers: List, private val options: SchemaParserOptions): ResolverInfo() {
46 | override fun getFieldSearches() =
47 | resolvers.map { FieldResolverScanner.Search(getRealResolverClass(it, options), this, it) }
48 | }
49 |
50 | internal class DataClassResolverInfo(private val dataClass: Class<*>): ResolverInfo() {
51 | override fun getFieldSearches() =
52 | listOf(FieldResolverScanner.Search(dataClass, this, null))
53 | }
54 |
55 | internal class MissingResolverInfo: ResolverInfo() {
56 | override fun getFieldSearches(): List = listOf()
57 | }
58 |
59 | class ResolverError(message: String, cause: Throwable? = null) : RuntimeException(message, cause)
60 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/coxautodev/graphql/tools/RootTypeInfo.kt:
--------------------------------------------------------------------------------
1 | package com.coxautodev.graphql.tools
2 |
3 | import graphql.language.SchemaDefinition
4 | import graphql.language.TypeName
5 |
6 | /**
7 | * @author Andrew Potter
8 | */
9 | internal class RootTypeInfo private constructor(val queryType: TypeName?, val mutationType: TypeName?, val subscriptionType: TypeName?) {
10 | companion object {
11 | const val DEFAULT_QUERY_NAME = "Query"
12 | const val DEFAULT_MUTATION_NAME = "Mutation"
13 | const val DEFAULT_SUBSCRIPTION_NAME = "Subscription"
14 |
15 | fun fromSchemaDefinitions(definitions: List): RootTypeInfo {
16 | val queryType = definitions.lastOrNull()?.operationTypeDefinitions?.find { it.name == "query" }?.type as TypeName?
17 | val mutationType = definitions.lastOrNull()?.operationTypeDefinitions?.find { it.name == "mutation" }?.type as TypeName?
18 | val subscriptionType = definitions.lastOrNull()?.operationTypeDefinitions?.find { it.name == "subscription" }?.type as TypeName?
19 |
20 | return RootTypeInfo(queryType, mutationType, subscriptionType)
21 | }
22 | }
23 |
24 | fun getQueryName() = queryType?.name ?: DEFAULT_QUERY_NAME
25 | fun getMutationName() = mutationType?.name ?: DEFAULT_MUTATION_NAME
26 | fun getSubscriptionName() = subscriptionType?.name ?: DEFAULT_SUBSCRIPTION_NAME
27 |
28 | fun isMutationRequired() = mutationType != null
29 | fun isSubscriptionRequired() = subscriptionType != null
30 | }
--------------------------------------------------------------------------------
/src/main/kotlin/com/coxautodev/graphql/tools/ScannedSchemaObjects.kt:
--------------------------------------------------------------------------------
1 | package com.coxautodev.graphql.tools
2 |
3 | import com.google.common.collect.BiMap
4 | import graphql.language.FieldDefinition
5 | import graphql.language.ObjectTypeDefinition
6 | import graphql.language.TypeDefinition
7 | import graphql.schema.GraphQLScalarType
8 |
9 | /**
10 | * @author Andrew Potter
11 | */
12 | internal data class ScannedSchemaObjects(
13 | val dictionary: TypeClassDictionary,
14 | val definitions: Set,
15 | val customScalars: CustomScalarMap,
16 | val rootInfo: RootTypeInfo,
17 | val fieldResolversByType: Map>
18 | )
19 |
20 | internal typealias TypeClassDictionary = BiMap>
21 | internal typealias CustomScalarMap = Map
22 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/coxautodev/graphql/tools/SchemaClassScanner.kt:
--------------------------------------------------------------------------------
1 | package com.coxautodev.graphql.tools
2 |
3 | import com.google.common.collect.BiMap
4 | import com.google.common.collect.HashBiMap
5 | import com.google.common.collect.Maps
6 | import graphql.language.Definition
7 | import graphql.language.FieldDefinition import graphql.language.InputObjectTypeDefinition
8 | import graphql.language.InputValueDefinition
9 | import graphql.language.InterfaceTypeDefinition
10 | import graphql.language.ObjectTypeDefinition
11 | import graphql.language.ScalarTypeDefinition
12 | import graphql.language.SchemaDefinition
13 | import graphql.language.TypeDefinition
14 | import graphql.language.TypeExtensionDefinition
15 | import graphql.language.TypeName
16 | import graphql.language.UnionTypeDefinition
17 | import graphql.schema.GraphQLScalarType
18 | import graphql.schema.idl.ScalarInfo
19 | import org.slf4j.LoggerFactory
20 | import java.lang.reflect.Field
21 | import java.lang.reflect.Method
22 |
23 | /**
24 | * @author Andrew Potter
25 | */
26 | internal class SchemaClassScanner(initialDictionary: BiMap>, allDefinitions: List, resolvers: List>, private val scalars: CustomScalarMap, private val options: SchemaParserOptions) {
27 |
28 | companion object {
29 | val log = LoggerFactory.getLogger(SchemaClassScanner::class.java)!!
30 | }
31 |
32 | private val rootInfo = RootTypeInfo.fromSchemaDefinitions(allDefinitions.filterIsInstance())
33 |
34 | private val queryResolvers = resolvers.filterIsInstance()
35 | private val mutationResolvers = resolvers.filterIsInstance()
36 | private val subscriptionResolvers = resolvers.filterIsInstance()
37 |
38 | private val resolverInfos = resolvers.minus(queryResolvers).minus(mutationResolvers).minus(subscriptionResolvers).map { NormalResolverInfo(it, options) }
39 | private val resolverInfosByDataClass = this.resolverInfos.associateBy { it.dataClassType }
40 |
41 | private val initialDictionary = initialDictionary.mapValues { InitialDictionaryEntry(it.value) }
42 | private val extensionDefinitions = allDefinitions.filterIsInstance()
43 |
44 | private val definitionsByName = (allDefinitions.filterIsInstance() - extensionDefinitions).associateBy { it.name }
45 | private val objectDefinitions = (allDefinitions.filterIsInstance() - extensionDefinitions)
46 | private val objectDefinitionsByName = objectDefinitions.associateBy { it.name }
47 | private val interfaceDefinitionsByName = allDefinitions.filterIsInstance().associateBy { it.name }
48 |
49 | private val fieldResolverScanner = FieldResolverScanner(options)
50 | private val typeClassMatcher = TypeClassMatcher(definitionsByName)
51 | private val dictionary = mutableMapOf()
52 | private val unvalidatedTypes = mutableSetOf()
53 | private val queue = linkedSetOf()
54 |
55 | private val fieldResolversByType = mutableMapOf>()
56 |
57 | init {
58 | initialDictionary.forEach { (name, clazz) ->
59 | if(!definitionsByName.containsKey(name)) {
60 | throw SchemaClassScannerError("Class in supplied dictionary '${clazz.name}' specified type name '$name', but a type definition with that name was not found!")
61 | }
62 | }
63 |
64 | if(options.allowUnimplementedResolvers) {
65 | log.warn("Option 'allowUnimplementedResolvers' should only be set to true during development, as it can cause schema errors to be moved to query time instead of schema creation time. Make sure this is turned off in production.")
66 | }
67 | }
68 |
69 | /**
70 | * Attempts to discover GraphQL Type -> Java Class relationships by matching return types/argument types on known fields
71 | */
72 | fun scanForClasses(): ScannedSchemaObjects {
73 |
74 | // Figure out what query, mutation and subscription types are called
75 | val rootTypeHolder = RootTypesHolder(options, rootInfo, definitionsByName, queryResolvers, mutationResolvers, subscriptionResolvers)
76 |
77 | handleRootType(rootTypeHolder.query)
78 | handleRootType(rootTypeHolder.mutation)
79 | handleRootType(rootTypeHolder.subscription)
80 |
81 | scanQueue()
82 |
83 | // Loop over all objects scanning each one only once for more objects to discover.
84 | do {
85 | do {
86 | // Require all implementors of discovered interfaces to be discovered or provided.
87 | handleInterfaceOrUnionSubTypes(getAllObjectTypesImplementingDiscoveredInterfaces(), { "Object type '${it.name}' implements a known interface, but no class could be found for that type name. Please pass a class for type '${it.name}' in the parser's dictionary." })
88 | } while (scanQueue())
89 |
90 | // Require all members of discovered unions to be discovered.
91 | handleInterfaceOrUnionSubTypes(getAllObjectTypeMembersOfDiscoveredUnions(), { "Object type '${it.name}' is a member of a known union, but no class could be found for that type name. Please pass a class for type '${it.name}' in the parser's dictionary." })
92 | } while (scanQueue())
93 |
94 | return validateAndCreateResult(rootTypeHolder)
95 | }
96 |
97 | private fun scanQueue(): Boolean {
98 | if(queue.isEmpty()) {
99 | return false
100 | }
101 |
102 | while (queue.isNotEmpty()) {
103 | scanQueueItemForPotentialMatches(queue.iterator().run { val item = next(); remove(); item })
104 | }
105 |
106 | return true
107 | }
108 |
109 | /**
110 | * Adds all root resolvers for a type to the list of classes to scan
111 | */
112 | private fun handleRootType(rootType: RootType?) {
113 | if(rootType == null) {
114 | return
115 | }
116 |
117 | unvalidatedTypes.add(rootType.type)
118 | scanInterfacesOfType(rootType.type)
119 | scanResolverInfoForPotentialMatches(rootType.type, rootType.resolverInfo)
120 | }
121 |
122 | private fun validateAndCreateResult(rootTypeHolder: RootTypesHolder): ScannedSchemaObjects {
123 | initialDictionary.filter { !it.value.accessed }.forEach {
124 | log.warn("Dictionary mapping was provided but never used, and can be safely deleted: \"${it.key}\" -> ${it.value.get().name}")
125 | }
126 |
127 | val observedDefinitions = dictionary.keys.toSet() + unvalidatedTypes
128 |
129 | // The dictionary doesn't need to know what classes are used with scalars.
130 | // In addition, scalars can have duplicate classes so that breaks the bi-map.
131 | // Input types can also be excluded from the dictionary, since it's only used for interfaces, unions, and enums.
132 | // Union types can also be excluded, as their possible types are resolved recursively later
133 | val dictionary = try {
134 | Maps.unmodifiableBiMap(HashBiMap.create>().also {
135 | dictionary.filter { it.value.typeClass != null && it.key !is InputObjectTypeDefinition && it.key !is UnionTypeDefinition}.mapValuesTo(it) { it.value.typeClass }
136 | })
137 | } catch (t: Throwable) {
138 | throw SchemaClassScannerError("Error creating bimap of type => class", t)
139 | }
140 | val scalarDefinitions = observedDefinitions.filterIsInstance()
141 |
142 | // Ensure all scalar definitions have implementations and add the definition to those.
143 | val scalars = scalarDefinitions.filter { !ScalarInfo.STANDARD_SCALAR_DEFINITIONS.containsKey(it.name) }.map { definition ->
144 | val provided = scalars[definition.name] ?: throw SchemaClassScannerError("Expected a user-defined GraphQL scalar type with name '${definition.name}' but found none!")
145 | GraphQLScalarType(provided.name, SchemaParser.getDocumentation(definition) ?: provided.description, provided.coercing, definition)
146 | }.associateBy { it.name!! }
147 |
148 | (definitionsByName.values - observedDefinitions).forEach { definition ->
149 | log.warn("Schema type was defined but can never be accessed, and can be safely deleted: ${definition.name}")
150 | }
151 |
152 | val fieldResolvers = fieldResolversByType.flatMap { it.value.map { it.value } }
153 | val observedNormalResolverInfos = fieldResolvers.map { it.resolverInfo }.distinct().filterIsInstance()
154 |
155 | (resolverInfos - observedNormalResolverInfos).forEach { resolverInfo ->
156 | log.warn("Resolver was provided but no methods on it were used in data fetchers, and can be safely deleted: ${resolverInfo.resolver}")
157 | }
158 |
159 | validateRootResolversWereUsed(rootTypeHolder.query, fieldResolvers)
160 | validateRootResolversWereUsed(rootTypeHolder.mutation, fieldResolvers)
161 | validateRootResolversWereUsed(rootTypeHolder.subscription, fieldResolvers)
162 |
163 | return ScannedSchemaObjects(dictionary, observedDefinitions + extensionDefinitions, scalars, rootInfo, fieldResolversByType.toMap())
164 | }
165 |
166 | private fun validateRootResolversWereUsed(rootType: RootType?, fieldResolvers: List) {
167 | if(rootType == null) {
168 | return
169 | }
170 |
171 | val observedRootTypes = fieldResolvers.filter { it.resolverInfo is RootResolverInfo && it.resolverInfo == rootType.resolverInfo }.map { it.search.type }.toSet()
172 | rootType.resolvers.forEach { resolver ->
173 | if(rootType.resolverInfo.getRealResolverClass(resolver, options) !in observedRootTypes) {
174 | log.warn("Root ${rootType.name} resolver was provided but no methods on it were used in data fetchers for GraphQL type '${rootType.type.name}'! Either remove the ${rootType.resolverInterface.name} interface from the resolver or remove the resolver entirely: $resolver")
175 | }
176 | }
177 | }
178 |
179 | private fun getAllObjectTypesImplementingDiscoveredInterfaces(): List {
180 | return dictionary.keys.filterIsInstance().map { iface ->
181 | objectDefinitions.filter { obj -> obj.implements.filterIsInstance().any { it.name == iface.name } }
182 | }.flatten().distinctBy{ it.name }
183 | }
184 |
185 | private fun getAllObjectTypeMembersOfDiscoveredUnions(): List {
186 | val unionTypeNames = dictionary.keys.filterIsInstance().map { union -> union.name }.toSet()
187 | return dictionary.keys.filterIsInstance().map { union ->
188 | union.memberTypes.filterIsInstance().filter { !unionTypeNames.contains(it.name) }.map { objectDefinitionsByName[it.name] ?: throw SchemaClassScannerError("No object type found with name '${it.name}' for union: $union") }
189 | }.flatten().distinct()
190 | }
191 |
192 | private fun handleInterfaceOrUnionSubTypes(types: List, failureMessage: (ObjectTypeDefinition) -> String) {
193 | types.forEach { type ->
194 | val dictionaryContainsType = dictionary.filter{ it.key.name == type.name }.isNotEmpty()
195 | if(!unvalidatedTypes.contains(type) && !dictionaryContainsType) {
196 | val initialEntry = initialDictionary[type.name] ?: throw SchemaClassScannerError(failureMessage(type))
197 | handleFoundType(type, initialEntry.get(), DictionaryReference())
198 | }
199 | }
200 | }
201 |
202 | /**
203 | * Scan a new object for types that haven't been mapped yet.
204 | */
205 | private fun scanQueueItemForPotentialMatches(item: QueueItem) {
206 | scanResolverInfoForPotentialMatches(item.type, resolverInfosByDataClass[item.clazz] ?: DataClassResolverInfo(item.clazz))
207 | }
208 |
209 | private fun scanResolverInfoForPotentialMatches(type: ObjectTypeDefinition, resolverInfo: ResolverInfo) {
210 | type.getExtendedFieldDefinitions(extensionDefinitions).forEach { field ->
211 | val fieldResolver = fieldResolverScanner.findFieldResolver(field, resolverInfo)
212 |
213 | fieldResolversByType.getOrPut(type, { mutableMapOf() })[fieldResolver.field] = fieldResolver
214 | fieldResolver.scanForMatches().forEach { potentialMatch ->
215 | handleFoundType(typeClassMatcher.match(potentialMatch))
216 | }
217 | }
218 | }
219 |
220 | private fun handleFoundType(match: TypeClassMatcher.Match) {
221 | when(match) {
222 | is TypeClassMatcher.ScalarMatch -> {
223 | handleFoundScalarType(match.type)
224 | }
225 |
226 | is TypeClassMatcher.ValidMatch -> {
227 | handleFoundType(match.type, match.clazz, match.reference)
228 | }
229 | }
230 | }
231 |
232 | private fun handleFoundScalarType(type: ScalarTypeDefinition) {
233 | unvalidatedTypes.add(type)
234 | }
235 |
236 | /**
237 | * Enter a found type into the dictionary if it doesn't exist yet, add a reference pointing back to where it was discovered.
238 | */
239 | private fun handleFoundType(type: TypeDefinition, clazz: Class<*>?, reference: Reference) {
240 | val realEntry = dictionary.getOrPut(type, { DictionaryEntry() })
241 | var typeWasSet = false
242 |
243 | if (clazz != null) {
244 | typeWasSet = realEntry.setTypeIfMissing(clazz)
245 |
246 | if (realEntry.typeClass != clazz) {
247 | throw SchemaClassScannerError("Two different classes used for type ${type.name}:\n${realEntry.joinReferences()}\n\n- $clazz:\n| ${reference.getDescription()}")
248 | }
249 | }
250 |
251 | realEntry.addReference(reference)
252 |
253 | // Check if we just added the entry... a little odd, but it works (and thread-safe, FWIW)
254 | if (typeWasSet && clazz != null) {
255 | handleNewType(type, clazz)
256 | }
257 | }
258 |
259 | /**
260 | * 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.
261 | */
262 | private fun handleNewType(graphQLType: TypeDefinition, javaType: Class<*>) {
263 | when(graphQLType) {
264 | is ObjectTypeDefinition -> {
265 | enqueue(graphQLType, javaType)
266 | scanInterfacesOfType(graphQLType)
267 | }
268 |
269 | is InputObjectTypeDefinition -> {
270 | graphQLType.inputValueDefinitions.forEach { inputValueDefinition ->
271 | findInputValueType(inputValueDefinition.name, javaType)?.let { inputValueJavaType ->
272 | val inputGraphQLType = inputValueDefinition.type.unwrap()
273 | if(inputGraphQLType is TypeName && !ScalarInfo.STANDARD_SCALAR_DEFINITIONS.containsKey(inputGraphQLType.name)) {
274 | handleFoundType(typeClassMatcher.match(TypeClassMatcher.PotentialMatch.parameterType(
275 | inputValueDefinition.type,
276 | inputValueJavaType,
277 | GenericType(javaType, options).relativeToType(inputValueJavaType),
278 | InputObjectReference(inputValueDefinition),
279 | false
280 | )))
281 | }
282 | }
283 | }
284 | }
285 | }
286 | }
287 |
288 | private fun scanInterfacesOfType(graphQLType: ObjectTypeDefinition) {
289 | graphQLType.implements.forEach {
290 | if(it is TypeName) {
291 | handleFoundType(interfaceDefinitionsByName[it.name] ?: throw SchemaClassScannerError("Object type ${graphQLType.name} declared interface ${it.name}, but no interface with that name was found in the schema!"), null, InterfaceReference(graphQLType))
292 | }
293 | }
294 | }
295 |
296 | private fun enqueue(graphQLType: ObjectTypeDefinition, javaType: Class<*>) {
297 | queue.add(QueueItem(graphQLType, javaType))
298 | }
299 |
300 | private fun findInputValueType(name: String, clazz: Class<*>): JavaType? {
301 | val methods = clazz.methods
302 |
303 | return (methods.find {
304 | it.name == name
305 | } ?: methods.find {
306 | it.name == "get${name.capitalize()}"
307 | })?.genericReturnType ?: clazz.fields.find {
308 | it.name == name
309 | }?.genericType
310 | }
311 |
312 | private data class QueueItem(val type: ObjectTypeDefinition, val clazz: Class<*>)
313 |
314 | private class DictionaryEntry {
315 | private val references = mutableListOf()
316 | var typeClass: Class<*>? = null
317 | private set
318 |
319 | fun setTypeIfMissing(typeClass: Class<*>): Boolean {
320 | if(this.typeClass == null) {
321 | this.typeClass = typeClass
322 | return true
323 | }
324 |
325 | return false
326 | }
327 |
328 | fun addReference(reference: Reference) {
329 | references.add(reference)
330 | }
331 |
332 | fun joinReferences() = "- $typeClass:\n| " + references.joinToString("\n| ") { it.getDescription() }
333 | }
334 |
335 | abstract class Reference {
336 | abstract fun getDescription(): String
337 | override fun toString() = getDescription()
338 | }
339 |
340 | private class DictionaryReference: Reference() {
341 | override fun getDescription() = "provided dictionary"
342 | }
343 |
344 | private class InterfaceReference(private val type: ObjectTypeDefinition): Reference() {
345 | override fun getDescription() = "interface declarations of ${type.name}"
346 | }
347 |
348 | private class InputObjectReference(private val type: InputValueDefinition): Reference() {
349 | override fun getDescription() = "input object $type"
350 | }
351 |
352 | private class InitialDictionaryEntry(private val clazz: Class<*>) {
353 | var accessed = false
354 | private set
355 |
356 | fun get(): Class<*> {
357 | accessed = true
358 | return clazz
359 | }
360 | }
361 |
362 | class ReturnValueReference(private val method: Method): Reference() {
363 | override fun getDescription() = "return type of method $method"
364 | }
365 |
366 | class MethodParameterReference(private val method: Method, private val index: Int): Reference() {
367 | override fun getDescription() = "parameter $index of method $method"
368 | }
369 |
370 | class FieldTypeReference(private val field: Field): Reference() {
371 | override fun getDescription() = "type of field $field"
372 | }
373 |
374 | class RootTypesHolder(options: SchemaParserOptions, rootInfo: RootTypeInfo, definitionsByName: Map, queryResolvers: List, mutationResolvers: List, subscriptionResolvers: List) {
375 | private val queryName = rootInfo.getQueryName()
376 | private val mutationName = rootInfo.getMutationName()
377 | private val subscriptionName = rootInfo.getSubscriptionName()
378 |
379 | private val queryDefinition = definitionsByName[queryName]
380 | private val mutationDefinition = definitionsByName[mutationName]
381 | private val subscriptionDefinition = definitionsByName[subscriptionName]
382 |
383 | private val queryResolverInfo = RootResolverInfo(queryResolvers, options)
384 | private val mutationResolverInfo = RootResolverInfo(mutationResolvers, options)
385 | private val subscriptionResolverInfo = RootResolverInfo(subscriptionResolvers, options)
386 |
387 | val query = createRootType("query", queryDefinition, queryName, true, queryResolvers, GraphQLQueryResolver::class.java, queryResolverInfo)
388 | val mutation = createRootType("mutation", mutationDefinition, mutationName, rootInfo.isMutationRequired(), mutationResolvers, GraphQLMutationResolver::class.java, mutationResolverInfo)
389 | val subscription = createRootType("subscription", subscriptionDefinition, subscriptionName, rootInfo.isSubscriptionRequired(), subscriptionResolvers, GraphQLSubscriptionResolver::class.java, subscriptionResolverInfo)
390 |
391 | private fun createRootType(name: String, type: TypeDefinition?, typeName: String, required: Boolean, resolvers: List, resolverInterface: Class<*>, resolverInfo: RootResolverInfo): RootType? {
392 | if(type == null) {
393 | if(required) {
394 | throw SchemaClassScannerError("Type definition for root $name type '$typeName' not found!")
395 | }
396 |
397 | return null
398 | }
399 |
400 | if(type !is ObjectTypeDefinition) {
401 | throw SchemaClassScannerError("Expected root query type's type to be ${ObjectTypeDefinition::class.java.simpleName}, but it was ${type.javaClass.simpleName}")
402 | }
403 |
404 | // Find query resolver class
405 | if(resolvers.isEmpty()) {
406 | throw SchemaClassScannerError("No Root resolvers for $name type '$typeName' found! Provide one or more ${resolverInterface.name} to the builder.")
407 | }
408 |
409 | return RootType(name, type, resolvers, resolverInterface, resolverInfo)
410 | }
411 | }
412 |
413 | class RootType(val name: String, val type: ObjectTypeDefinition, val resolvers: List, val resolverInterface: Class<*>, val resolverInfo: RootResolverInfo)
414 | }
415 |
416 | class SchemaClassScannerError(message: String, throwable: Throwable? = null) : RuntimeException(message, throwable)
417 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/coxautodev/graphql/tools/SchemaObjects.kt:
--------------------------------------------------------------------------------
1 | package com.coxautodev.graphql.tools
2 |
3 | import graphql.schema.GraphQLObjectType
4 | import graphql.schema.GraphQLSchema
5 | import graphql.schema.GraphQLType
6 |
7 | /**
8 | * @author Andrew Potter
9 | */
10 | data class SchemaObjects(val query: GraphQLObjectType, val mutation: GraphQLObjectType?, val subscription: GraphQLObjectType?, val dictionary: Set) {
11 |
12 | /**
13 | * Makes a GraphQLSchema with query, mutation and subscription.
14 | */
15 | fun toSchema(): GraphQLSchema = GraphQLSchema.newSchema()
16 | .query(query)
17 | .mutation(mutation)
18 | .subscription(subscription)
19 | .build(dictionary)
20 |
21 | /**
22 | * Makes a GraphQLSchema with query but without mutation and subscription.
23 | */
24 | fun toReadOnlySchema(): GraphQLSchema = GraphQLSchema.newSchema()
25 | .query(query)
26 | .build(dictionary)
27 | }
--------------------------------------------------------------------------------
/src/main/kotlin/com/coxautodev/graphql/tools/SchemaParser.kt:
--------------------------------------------------------------------------------
1 | package com.coxautodev.graphql.tools
2 |
3 | import graphql.language.AbstractNode
4 | import graphql.language.ArrayValue
5 | import graphql.language.BooleanValue
6 | import graphql.language.Directive
7 | import graphql.language.EnumTypeDefinition
8 | import graphql.language.EnumValue
9 | import graphql.language.FieldDefinition
10 | import graphql.language.FloatValue
11 | import graphql.language.InputObjectTypeDefinition
12 | import graphql.language.IntValue
13 | import graphql.language.InterfaceTypeDefinition
14 | import graphql.language.ListType
15 | import graphql.language.NonNullType
16 | import graphql.language.ObjectTypeDefinition
17 | import graphql.language.ObjectValue
18 | import graphql.language.StringValue
19 | import graphql.language.Type
20 | import graphql.language.TypeExtensionDefinition
21 | import graphql.language.TypeName
22 | import graphql.language.UnionTypeDefinition
23 | import graphql.language.Value
24 | import graphql.schema.GraphQLEnumType
25 | import graphql.schema.GraphQLFieldDefinition
26 | import graphql.schema.GraphQLInputObjectType
27 | import graphql.schema.GraphQLInputType
28 | import graphql.schema.GraphQLInterfaceType
29 | import graphql.schema.GraphQLList
30 | import graphql.schema.GraphQLNonNull
31 | import graphql.schema.GraphQLObjectType
32 | import graphql.schema.GraphQLOutputType
33 | import graphql.schema.GraphQLSchema
34 | import graphql.schema.GraphQLType
35 | import graphql.schema.GraphQLTypeReference
36 | import graphql.schema.GraphQLUnionType
37 | import graphql.schema.TypeResolverProxy
38 | import graphql.schema.idl.ScalarInfo
39 | import kotlin.reflect.KClass
40 |
41 | /**
42 | * Parses a GraphQL Schema and maps object fields to provided class methods.
43 | *
44 | * @author Andrew Potter
45 | */
46 | class SchemaParser internal constructor(scanResult: ScannedSchemaObjects) {
47 |
48 | companion object {
49 | val DEFAULT_DEPRECATION_MESSAGE = "No longer supported"
50 |
51 | @JvmStatic fun newParser() = SchemaParserBuilder()
52 | internal fun getDocumentation(node: AbstractNode): String? = node.comments?.map { it.content.trim() }?.joinToString("\n")
53 | }
54 |
55 | private val dictionary = scanResult.dictionary
56 | private val definitions = scanResult.definitions
57 | private val customScalars = scanResult.customScalars
58 | private val rootInfo = scanResult.rootInfo
59 | private val fieldResolversByType = scanResult.fieldResolversByType
60 |
61 | private val extensionDefinitions = definitions.filterIsInstance()
62 | private val objectDefinitions = (definitions.filterIsInstance() - extensionDefinitions)
63 |
64 | private val inputObjectDefinitions = definitions.filterIsInstance()
65 | private val enumDefinitions = definitions.filterIsInstance()
66 | private val interfaceDefinitions = definitions.filterIsInstance()
67 | private val unionDefinitions = definitions.filterIsInstance()
68 |
69 | private val permittedTypesForObject: Set = (objectDefinitions.map { it.name } +
70 | enumDefinitions.map { it.name } +
71 | interfaceDefinitions.map { it.name } +
72 | unionDefinitions.map { it.name }).toSet()
73 | private val permittedTypesForInputObject: Set =
74 | (inputObjectDefinitions.map { it.name } + enumDefinitions.map { it.name }).toSet()
75 |
76 | /**
77 | * Parses the given schema with respect to the given dictionary and returns GraphQL objects.
78 | */
79 | fun parseSchemaObjects(): SchemaObjects {
80 |
81 | // Create GraphQL objects
82 | val interfaces = interfaceDefinitions.map { createInterfaceObject(it) }
83 | val objects = objectDefinitions.map { createObject(it, interfaces) }
84 | val unions = unionDefinitions.map { createUnionObject(it, objects) }
85 | val inputObjects = inputObjectDefinitions.map { createInputObject(it) }
86 | val enums = enumDefinitions.map { createEnumObject(it) }
87 |
88 | // Assign type resolver to interfaces now that we know all of the object types
89 | interfaces.forEach { (it.typeResolver as TypeResolverProxy).typeResolver = InterfaceTypeResolver(dictionary.inverse(), it, objects) }
90 | unions.forEach { (it.typeResolver as TypeResolverProxy).typeResolver = UnionTypeResolver(dictionary.inverse(), it, objects) }
91 |
92 | // Find query type and mutation/subscription type (if mutation/subscription type exists)
93 | val queryName = rootInfo.getQueryName()
94 | val mutationName = rootInfo.getMutationName()
95 | val subscriptionName = rootInfo.getSubscriptionName()
96 |
97 | val query = objects.find { it.name == queryName } ?: throw SchemaError("Expected a Query object with name '$queryName' but found none!")
98 | val mutation = objects.find { it.name == mutationName } ?: if(rootInfo.isMutationRequired()) throw SchemaError("Expected a Mutation object with name '$mutationName' but found none!") else null
99 | val subscription = objects.find { it.name == subscriptionName } ?: if (rootInfo.isSubscriptionRequired()) throw SchemaError("Expected a Subscription object with name '$subscriptionName' but found none!") else null
100 |
101 | return SchemaObjects(query, mutation, subscription, (objects + inputObjects + enums + interfaces + unions).toSet())
102 | }
103 |
104 | /**
105 | * Parses the given schema with respect to the given dictionary and returns a GraphQLSchema
106 | */
107 | fun makeExecutableSchema(): GraphQLSchema = parseSchemaObjects().toSchema()
108 |
109 | private fun createObject(definition: ObjectTypeDefinition, interfaces: List): GraphQLObjectType {
110 | val name = definition.name
111 | val builder = GraphQLObjectType.newObject()
112 | .name(name)
113 | .definition(definition)
114 | .description(getDocumentation(definition))
115 |
116 | definition.implements.forEach { implementsDefinition ->
117 | val interfaceName = (implementsDefinition as TypeName).name
118 | builder.withInterface(interfaces.find { it.name == interfaceName } ?: throw SchemaError("Expected interface type with name '$interfaceName' but found none!"))
119 | }
120 |
121 | definition.getExtendedFieldDefinitions(extensionDefinitions).forEach { fieldDefinition ->
122 | builder.field { field ->
123 | createField(field, fieldDefinition)
124 | field.dataFetcher(fieldResolversByType[definition]?.get(fieldDefinition)?.createDataFetcher() ?: throw SchemaError("No resolver method found for object type '${definition.name}' and field '${fieldDefinition.name}', this is most likely a bug with graphql-java-tools"))
125 | }
126 | }
127 |
128 | return builder.build()
129 | }
130 |
131 | private fun createInputObject(definition: InputObjectTypeDefinition): GraphQLInputObjectType {
132 | val builder = GraphQLInputObjectType.newInputObject()
133 | .name(definition.name)
134 | .definition(definition)
135 | .description(getDocumentation(definition))
136 |
137 | definition.inputValueDefinitions.forEach { inputDefinition ->
138 | builder.field { field ->
139 | field.name(inputDefinition.name)
140 | field.definition(inputDefinition)
141 | field.description(getDocumentation(inputDefinition))
142 | field.defaultValue(inputDefinition.defaultValue)
143 | field.type(determineInputType(inputDefinition.type))
144 | }
145 | }
146 |
147 | return builder.build()
148 | }
149 |
150 | private fun createEnumObject(definition: EnumTypeDefinition): GraphQLEnumType {
151 | val name = definition.name
152 | val type = dictionary[definition] ?: throw SchemaError("Expected enum with name '$name' but found none!")
153 | if (!type.isEnum) throw SchemaError("Type '$name' is declared as an enum in the GraphQL schema but is not a Java enum!")
154 |
155 | val builder = GraphQLEnumType.newEnum()
156 | .name(name)
157 | .definition(definition)
158 | .description(getDocumentation(definition))
159 |
160 | definition.enumValueDefinitions.forEach { enumDefinition ->
161 | val enumName = enumDefinition.name
162 | val enumValue = type.enumConstants.find { (it as Enum<*>).name == enumName } ?: throw SchemaError("Expected value for name '$enumName' in enum '${type.simpleName}' but found none!")
163 | getDeprecated(enumDefinition.directives).let {
164 | when (it) {
165 | is String -> builder.value(enumName, enumValue, getDocumentation(enumDefinition), it)
166 | else -> builder.value(enumName, enumValue, getDocumentation(enumDefinition))
167 | }
168 | }
169 | }
170 |
171 | return builder.build()
172 | }
173 |
174 | private fun createInterfaceObject(definition: InterfaceTypeDefinition): GraphQLInterfaceType {
175 | val name = definition.name
176 | val builder = GraphQLInterfaceType.newInterface()
177 | .name(name)
178 | .definition(definition)
179 | .description(getDocumentation(definition))
180 | .typeResolver(TypeResolverProxy())
181 |
182 | definition.fieldDefinitions.forEach { fieldDefinition ->
183 | builder.field { field -> createField(field, fieldDefinition) }
184 | }
185 |
186 | return builder.build()
187 | }
188 |
189 | private fun createUnionObject(definition: UnionTypeDefinition, types: List): GraphQLUnionType {
190 | val name = definition.name
191 | val builder = GraphQLUnionType.newUnionType()
192 | .name(name)
193 | .definition(definition)
194 | .description(getDocumentation(definition))
195 | .typeResolver(TypeResolverProxy())
196 |
197 | getLeafUnionObjects(definition, types).forEach { builder.possibleType(it) }
198 | return builder.build()
199 | }
200 |
201 | private fun getLeafUnionObjects(definition: UnionTypeDefinition, types: List): List {
202 | val name = definition.name
203 | val leafObjects = mutableListOf()
204 |
205 | definition.memberTypes.forEach {
206 | val typeName = (it as TypeName).name
207 |
208 | // Is this a nested union? If so, expand
209 | val nestedUnion : UnionTypeDefinition? = unionDefinitions.find { otherDefinition -> typeName == otherDefinition.name }
210 |
211 | if (nestedUnion != null) {
212 | leafObjects.addAll(getLeafUnionObjects(nestedUnion, types))
213 | } else {
214 | leafObjects.add(types.find { it.name == typeName } ?: throw SchemaError("Expected object type '$typeName' for union type '$name', but found none!"))
215 | }
216 | }
217 | return leafObjects
218 | }
219 |
220 | private fun createField(field: GraphQLFieldDefinition.Builder, fieldDefinition : FieldDefinition): GraphQLFieldDefinition.Builder {
221 | field.name(fieldDefinition.name)
222 | field.description(getDocumentation(fieldDefinition))
223 | field.definition(fieldDefinition)
224 | getDeprecated(fieldDefinition.directives)?.let { field.deprecate(it) }
225 | field.type(determineOutputType(fieldDefinition.type))
226 | fieldDefinition.inputValueDefinitions.forEach { argumentDefinition ->
227 | field.argument { argument ->
228 | argument.name(argumentDefinition.name)
229 | argument.definition(argumentDefinition)
230 | argument.description(getDocumentation(argumentDefinition))
231 | argument.defaultValue(buildDefaultValue(argumentDefinition.defaultValue))
232 | argument.type(determineInputType(argumentDefinition.type))
233 | }
234 | }
235 | return field
236 | }
237 |
238 | private fun buildDefaultValue(value: Value?): Any? {
239 | return when(value) {
240 | null -> null
241 | is IntValue -> value.value
242 | is FloatValue -> value.value
243 | is StringValue -> value.value
244 | is EnumValue -> value.name
245 | is BooleanValue -> value.isValue
246 | is ArrayValue -> value.values.map { buildDefaultValue(it) }.toTypedArray()
247 | is ObjectValue -> value.objectFields.associate { it.name to buildDefaultValue(it.value) }
248 | else -> throw SchemaError("Unrecognized default value: $value")
249 | }
250 | }
251 |
252 | private fun determineOutputType(typeDefinition: Type) =
253 | determineType(GraphQLOutputType::class, typeDefinition, permittedTypesForObject) as GraphQLOutputType
254 | private fun determineInputType(typeDefinition: Type) =
255 | determineType(GraphQLInputType::class, typeDefinition, permittedTypesForInputObject) as GraphQLInputType
256 |
257 | private fun determineType(expectedType: KClass, typeDefinition: Type, allowedTypeReferences: Set): GraphQLType =
258 | when (typeDefinition) {
259 | is ListType -> GraphQLList(determineType(expectedType, typeDefinition.type, allowedTypeReferences))
260 | is NonNullType -> GraphQLNonNull(determineType(expectedType, typeDefinition.type, allowedTypeReferences))
261 | is TypeName -> {
262 | val scalarType = graphQLScalars[typeDefinition.name] ?: customScalars[typeDefinition.name]
263 | if (scalarType != null) {
264 | scalarType
265 | } else {
266 | if (!allowedTypeReferences.contains(typeDefinition.name)) {
267 | throw SchemaError("Expected type '${typeDefinition.name}' to be a ${expectedType.simpleName}, but it wasn't! " +
268 | "Was a type only permitted for object types incorrectly used as an input type, or vice-versa?")
269 | }
270 | GraphQLTypeReference(typeDefinition.name)
271 | }
272 | }
273 | else -> throw SchemaError("Unknown type: $typeDefinition")
274 | }
275 |
276 | /**
277 | * Returns an optional [String] describing a deprecated field/enum.
278 | * If a deprecation directive was defined using the @deprecated directive,
279 | * then a String containing either the contents of the 'reason' argument, if present, or a default
280 | * message defined in [DEFAULT_DEPRECATION_MESSAGE] will be returned. Otherwise, [null] will be returned
281 | * indicating no deprecation directive was found within the directives list.
282 | */
283 | private fun getDeprecated(directives: List): String? =
284 | getDirective(directives, "deprecated")?.let { directive ->
285 | (directive.arguments.find { it.name == "reason" }?.value as? StringValue)?.value ?:
286 | DEFAULT_DEPRECATION_MESSAGE
287 | }
288 |
289 | private fun getDirective(directives: List, name: String): Directive? = directives.find {
290 | it.name == name
291 | }
292 | }
293 |
294 | class SchemaError(message: String, cause: Throwable? = null) : RuntimeException(message, cause)
295 |
296 | val graphQLScalars = ScalarInfo.STANDARD_SCALARS.associateBy { it.name }
297 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/coxautodev/graphql/tools/SchemaParserBuilder.kt:
--------------------------------------------------------------------------------
1 | package com.coxautodev.graphql.tools
2 |
3 | import com.fasterxml.jackson.databind.ObjectMapper
4 | import com.google.common.collect.BiMap
5 | import com.google.common.collect.HashBiMap
6 | import com.google.common.collect.Maps
7 | import graphql.parser.Parser
8 | import graphql.schema.GraphQLScalarType
9 | import org.antlr.v4.runtime.RecognitionException
10 | import org.antlr.v4.runtime.misc.ParseCancellationException
11 | import org.reactivestreams.Publisher
12 | import java.util.concurrent.CompletableFuture
13 | import java.util.concurrent.CompletionStage
14 | import java.util.concurrent.Future
15 | import kotlin.reflect.KClass
16 |
17 | /**
18 | * @author Andrew Potter
19 | */
20 | class SchemaParserBuilder constructor(private val dictionary: SchemaParserDictionary = SchemaParserDictionary()) {
21 |
22 | private val schemaString = StringBuilder()
23 | private val resolvers = mutableListOf>()
24 | private val scalars = mutableListOf()
25 | private var options = SchemaParserOptions.defaultOptions()
26 |
27 | /**
28 | * Add GraphQL schema files from the classpath.
29 | */
30 | fun files(vararg files: String) = this.apply {
31 | files.forEach { this.file(it) }
32 | }
33 |
34 | /**
35 | * Add a GraphQL Schema file from the classpath.
36 | */
37 | fun file(filename: String) = this.apply {
38 | this.schemaString(java.io.BufferedReader(java.io.InputStreamReader(
39 | object : Any() {}.javaClass.classLoader.getResourceAsStream(filename) ?: throw java.io.FileNotFoundException("classpath:$filename")
40 | )).readText())
41 | }
42 |
43 | /**
44 | * Add a GraphQL schema string directly.
45 | */
46 | fun schemaString(string: String) = this.apply {
47 | schemaString.append("\n").append(string)
48 | }
49 |
50 | /**
51 | * Add GraphQLResolvers to the parser's dictionary.
52 | */
53 | fun resolvers(vararg resolvers: GraphQLResolver<*>) = this.apply {
54 | this.resolvers.addAll(resolvers)
55 | }
56 |
57 | /**
58 | * Add GraphQLResolvers to the parser's dictionary.
59 | */
60 | fun resolvers(resolvers: List>) = this.apply {
61 | this.resolvers.addAll(resolvers)
62 | }
63 |
64 | /**
65 | * Add scalars to the parser's dictionary.
66 | */
67 | fun scalars(vararg scalars: GraphQLScalarType) = this.apply {
68 | this.scalars.addAll(scalars)
69 | }
70 |
71 | /**
72 | * Add arbitrary classes to the parser's dictionary, overriding the generated type name.
73 | */
74 | fun dictionary(name: String, clazz: Class<*>) = this.apply {
75 | this.dictionary.add(name, clazz)
76 | }
77 |
78 | /**
79 | * Add arbitrary classes to the parser's dictionary, overriding the generated type name.
80 | */
81 | fun dictionary(name: String, clazz: KClass<*>) = this.apply {
82 | this.dictionary.add(name, clazz)
83 | }
84 |
85 | /**
86 | * Add arbitrary classes to the parser's dictionary, overriding the generated type name.
87 | */
88 | fun dictionary(dictionary: Map>) = this.apply {
89 | this.dictionary.add(dictionary)
90 | }
91 |
92 | /**
93 | * Add arbitrary classes to the parser's dictionary.
94 | */
95 | fun dictionary(clazz: Class<*>) = this.apply {
96 | this.dictionary.add(clazz)
97 | }
98 |
99 | /**
100 | * Add arbitrary classes to the parser's dictionary.
101 | */
102 | fun dictionary(clazz: KClass<*>) = this.apply {
103 | this.dictionary.add(clazz)
104 | }
105 |
106 | /**
107 | * Add arbitrary classes to the parser's dictionary.
108 | */
109 | fun dictionary(vararg dictionary: Class<*>) = this.apply {
110 | this.dictionary.add(*dictionary)
111 | }
112 |
113 | /**
114 | * Add arbitrary classes to the parser's dictionary.
115 | */
116 | fun dictionary(vararg dictionary: KClass<*>) = this.apply {
117 | this.dictionary.add(*dictionary)
118 | }
119 |
120 | /**
121 | * Add arbitrary classes to the parser's dictionary.
122 | */
123 | fun dictionary(dictionary: Collection>) = this.apply {
124 | this.dictionary.add(dictionary)
125 | }
126 |
127 | fun options(options: SchemaParserOptions) = this.apply {
128 | this.options = options
129 | }
130 |
131 | /**
132 | * Scan for classes with the supplied schema and dictionary. Used for testing.
133 | */
134 | private fun scan(): ScannedSchemaObjects {
135 | val document = try {
136 | Parser().parseDocument(this.schemaString.toString())
137 | } catch (pce: ParseCancellationException) {
138 | val cause = pce.cause
139 | if(cause != null && cause is RecognitionException) {
140 | throw InvalidSchemaError(pce, cause)
141 | } else {
142 | throw pce
143 | }
144 | }
145 |
146 | val definitions = document.definitions
147 | val customScalars = scalars.associateBy { it.name }
148 |
149 | return SchemaClassScanner(dictionary.getDictionary(), definitions, resolvers, customScalars, options).scanForClasses()
150 | }
151 |
152 | /**
153 | * Build the parser with the supplied schema and dictionary.
154 | */
155 | fun build() = SchemaParser(scan())
156 | }
157 |
158 | class InvalidSchemaError(pce: ParseCancellationException, private val recognitionException: RecognitionException): RuntimeException(pce) {
159 | override val message: String?
160 | get() = "Invalid schema provided (${recognitionException.javaClass.name}) at: ${recognitionException.offendingToken}"
161 | }
162 |
163 | class SchemaParserDictionary {
164 |
165 | private val dictionary: BiMap> = HashBiMap.create()
166 |
167 | fun getDictionary(): BiMap> = Maps.unmodifiableBiMap(dictionary)
168 |
169 | /**
170 | * Add arbitrary classes to the parser's dictionary, overriding the generated type name.
171 | */
172 | fun add(name: String, clazz: Class<*>) = this.apply {
173 | this.dictionary.put(name, clazz)
174 | }
175 |
176 | /**
177 | * Add arbitrary classes to the parser's dictionary, overriding the generated type name.
178 | */
179 | fun add(name: String, clazz: KClass<*>) = this.apply {
180 | this.dictionary.put(name, clazz.java)
181 | }
182 |
183 | /**
184 | * Add arbitrary classes to the parser's dictionary, overriding the generated type name.
185 | */
186 | fun add(dictionary: Map>) = this.apply {
187 | this.dictionary.putAll(dictionary)
188 | }
189 |
190 | /**
191 | * Add arbitrary classes to the parser's dictionary.
192 | */
193 | fun add(clazz: Class<*>) = this.apply {
194 | this.add(clazz.simpleName, clazz)
195 | }
196 |
197 | /**
198 | * Add arbitrary classes to the parser's dictionary.
199 | */
200 | fun add(clazz: KClass<*>) = this.apply {
201 | this.add(clazz.java.simpleName, clazz)
202 | }
203 |
204 | /**
205 | * Add arbitrary classes to the parser's dictionary.
206 | */
207 | fun add(vararg dictionary: Class<*>) = this.apply {
208 | dictionary.forEach { this.add(it) }
209 | }
210 |
211 | /**
212 | * Add arbitrary classes to the parser's dictionary.
213 | */
214 | fun add(vararg dictionary: KClass<*>) = this.apply {
215 | dictionary.forEach { this.add(it) }
216 | }
217 |
218 | /**
219 | * Add arbitrary classes to the parser's dictionary.
220 | */
221 | fun add(dictionary: Collection>) = this.apply {
222 | dictionary.forEach { this.add(it) }
223 | }
224 | }
225 |
226 | data class SchemaParserOptions internal constructor(val genericWrappers: List, val allowUnimplementedResolvers: Boolean, val objectMapperConfigurer: ObjectMapperConfigurer, val proxyHandlers: List) {
227 | companion object {
228 | @JvmStatic fun newOptions() = Builder()
229 | @JvmStatic fun defaultOptions() = Builder().build()
230 | }
231 |
232 | class Builder {
233 | private val genericWrappers: MutableList = mutableListOf()
234 | private var useDefaultGenericWrappers = true
235 | private var allowUnimplementedResolvers = false
236 | private var objectMapperConfigurer: ObjectMapperConfigurer = ObjectMapperConfigurer { _, _ -> }
237 | private val proxyHandlers: MutableList = mutableListOf(Spring4AopProxyHandler(), GuiceAopProxyHandler())
238 |
239 | fun genericWrappers(genericWrappers: List) = this.apply {
240 | this.genericWrappers.addAll(genericWrappers)
241 | }
242 |
243 | fun genericWrappers(vararg genericWrappers: GenericWrapper) = this.apply {
244 | this.genericWrappers.addAll(genericWrappers)
245 | }
246 |
247 | fun useDefaultGenericWrappers(useDefaultGenericWrappers: Boolean) = this.apply {
248 | this.useDefaultGenericWrappers = useDefaultGenericWrappers
249 | }
250 |
251 | fun allowUnimplementedResolvers(allowUnimplementedResolvers: Boolean) = this.apply {
252 | this.allowUnimplementedResolvers = allowUnimplementedResolvers
253 | }
254 |
255 | fun objectMapperConfigurer(objectMapperConfigurer: ObjectMapperConfigurer) = this.apply {
256 | this.objectMapperConfigurer = objectMapperConfigurer
257 | }
258 |
259 | fun objectMapperConfigurer(objectMapperConfigurer: (ObjectMapper, ObjectMapperConfigurerContext) -> Unit) = this.apply {
260 | this.objectMapperConfigurer(ObjectMapperConfigurer(objectMapperConfigurer))
261 | }
262 |
263 | fun addProxyHandler(proxyHandler: ProxyHandler) = this.apply {
264 | this.proxyHandlers.add(proxyHandler)
265 | }
266 |
267 | fun build(): SchemaParserOptions {
268 | val wrappers = if(useDefaultGenericWrappers) {
269 | genericWrappers + listOf(
270 | GenericWrapper(Future::class, 0),
271 | GenericWrapper(CompletableFuture::class, 0),
272 | GenericWrapper(CompletionStage::class, 0),
273 | GenericWrapper(Publisher::class, 0)
274 | )
275 | } else {
276 | genericWrappers
277 | }
278 |
279 | return SchemaParserOptions(wrappers, allowUnimplementedResolvers, objectMapperConfigurer, proxyHandlers)
280 | }
281 | }
282 |
283 | data class GenericWrapper(val type: Class<*>, val index: Int) {
284 | constructor(type: KClass<*>, index: Int): this(type.java, index)
285 | }
286 | }
287 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/coxautodev/graphql/tools/Spring4AopProxyHandler.kt:
--------------------------------------------------------------------------------
1 | package com.coxautodev.graphql.tools
2 |
3 | import org.springframework.aop.support.AopUtils
4 |
5 | /**
6 | * @author Andrew Potter
7 | */
8 |
9 | class Spring4AopProxyHandler: ProxyHandler {
10 |
11 | val isEnabled: Boolean =
12 | try {
13 | Class.forName("org.springframework.aop.support.AopUtils")
14 | true
15 | } catch (_: ClassNotFoundException) {
16 | false
17 | }
18 |
19 | override fun canHandle(resolver: GraphQLResolver<*>?): Boolean {
20 | return isEnabled && AopUtils.isAopProxy(resolver)
21 | }
22 |
23 | override fun getTargetClass(resolver: GraphQLResolver<*>?): Class<*> = AopUtils.getTargetClass(resolver)
24 | }
--------------------------------------------------------------------------------
/src/main/kotlin/com/coxautodev/graphql/tools/TypeClassMatcher.kt:
--------------------------------------------------------------------------------
1 | package com.coxautodev.graphql.tools
2 |
3 | import graphql.language.ListType
4 | import graphql.language.NonNullType
5 | import graphql.language.ScalarTypeDefinition
6 | import graphql.language.TypeDefinition
7 | import graphql.language.TypeName
8 | import graphql.schema.idl.ScalarInfo
9 | import org.apache.commons.lang3.reflect.TypeUtils
10 | import java.lang.reflect.ParameterizedType
11 | import java.util.Optional
12 |
13 | /**
14 | * @author Andrew Potter
15 | */
16 | internal class TypeClassMatcher(private val definitionsByName: Map) {
17 |
18 | companion object {
19 | fun isListType(realType: ParameterizedType, generic: GenericType) = generic.isTypeAssignableFromRawClass(realType, Iterable::class.java)
20 | }
21 |
22 | private fun error(potentialMatch: PotentialMatch, msg: String) = SchemaClassScannerError("Unable to match type definition (${potentialMatch.graphQLType}) with java type (${potentialMatch.javaType}): $msg")
23 |
24 | fun match(potentialMatch: PotentialMatch): Match {
25 | return if(potentialMatch.batched) {
26 | match(stripBatchedType(potentialMatch)) // stripBatchedType sets 'batched' to false
27 | } else {
28 | match(potentialMatch, potentialMatch.graphQLType, potentialMatch.javaType, true)
29 | }
30 | }
31 |
32 | private fun match(potentialMatch: PotentialMatch, graphQLType: GraphQLLangType, javaType: JavaType, root: Boolean = false): Match {
33 |
34 | var realType = potentialMatch.generic.unwrapGenericType(javaType)
35 | var optional = false
36 |
37 | // Handle jdk8 Optionals
38 | if(realType is ParameterizedType && potentialMatch.generic.isTypeAssignableFromRawClass(realType, Optional::class.java)) {
39 | optional = true
40 |
41 | if(potentialMatch.location == Location.RETURN_TYPE && !root) {
42 | throw error(potentialMatch, "${Optional::class.java.name} can only be used at the top level of a return type")
43 | }
44 |
45 | realType = potentialMatch.generic.unwrapGenericType(realType.actualTypeArguments.first())
46 |
47 | if(realType is ParameterizedType && potentialMatch.generic.isTypeAssignableFromRawClass(realType, Optional::class.java)) {
48 | throw error(potentialMatch, "${Optional::class.java.name} cannot be nested within itself")
49 | }
50 | }
51 |
52 | // Match graphql type to java type.
53 | return when(graphQLType) {
54 | is NonNullType -> {
55 | if(optional) {
56 | throw error(potentialMatch, "graphql type is marked as nonnull but ${Optional::class.java.name} was used")
57 | }
58 | match(potentialMatch, graphQLType.type, realType)
59 | }
60 |
61 | is ListType -> {
62 | if(realType is ParameterizedType && isListType(realType, potentialMatch)) {
63 | match(potentialMatch, graphQLType.type, realType.actualTypeArguments.first())
64 | } else {
65 | throw error(potentialMatch, "Java class is not a List or generic type information was lost: $realType")
66 | }
67 | }
68 |
69 | is TypeName -> {
70 | val typeDefinition = ScalarInfo.STANDARD_SCALAR_DEFINITIONS[graphQLType.name] ?: definitionsByName[graphQLType.name] ?: throw error(potentialMatch, "No ${TypeDefinition::class.java.simpleName} for type name ${graphQLType.name}")
71 | if(typeDefinition is ScalarTypeDefinition) {
72 | ScalarMatch(typeDefinition)
73 | } else {
74 | ValidMatch(typeDefinition, requireRawClass(realType), potentialMatch.reference)
75 | }
76 | }
77 |
78 | is TypeDefinition -> ValidMatch(graphQLType, requireRawClass(realType), potentialMatch.reference)
79 | else -> throw error(potentialMatch, "Unknown type: ${realType.javaClass.name}")
80 | }
81 | }
82 |
83 | private fun isListType(realType: ParameterizedType, potentialMatch: PotentialMatch) = isListType(realType, potentialMatch.generic)
84 |
85 | private fun requireRawClass(type: JavaType): Class<*> {
86 | if(type !is Class<*>) {
87 | throw RawClassRequiredForGraphQLMappingException("Type ${TypeUtils.toString(type)} cannot be mapped to a GraphQL type! Since GraphQL-Java deals with erased types at runtime, only non-parameterized classes can represent a GraphQL type. This allows for reverse-lookup by java class in interfaces and union types.")
88 | }
89 |
90 | return type
91 | }
92 |
93 | private fun stripBatchedType(potentialMatch: PotentialMatch): PotentialMatch {
94 | val realType = potentialMatch.generic.unwrapGenericType(potentialMatch.javaType)
95 |
96 | if(realType is ParameterizedType && isListType(realType, potentialMatch)) {
97 | return potentialMatch.copy(javaType = realType.actualTypeArguments.first(), batched = false)
98 | } else {
99 | throw error(potentialMatch, "Method was marked as @Batched but ${potentialMatch.location.prettyName} was not a list!")
100 | }
101 | }
102 |
103 | internal interface Match
104 | internal data class ScalarMatch(val type: ScalarTypeDefinition): Match
105 | internal data class ValidMatch(val type: TypeDefinition, val clazz: Class<*>, val reference: SchemaClassScanner.Reference): Match
106 | internal enum class Location(val prettyName: String) {
107 | RETURN_TYPE("return type"),
108 | PARAMETER_TYPE("parameter"),
109 | }
110 |
111 | internal data class PotentialMatch(val graphQLType: GraphQLLangType, val javaType: JavaType, val generic: GenericType.RelativeTo, val reference: SchemaClassScanner.Reference, val location: Location, val batched: Boolean) {
112 | companion object {
113 | fun returnValue(graphQLType: GraphQLLangType, javaType: JavaType, generic: GenericType.RelativeTo, reference: SchemaClassScanner.Reference, batched: Boolean) =
114 | PotentialMatch(graphQLType, javaType, generic, reference, Location.RETURN_TYPE, batched)
115 |
116 | fun parameterType(graphQLType: GraphQLLangType, javaType: JavaType, generic: GenericType.RelativeTo, reference: SchemaClassScanner.Reference, batched: Boolean) =
117 | PotentialMatch(graphQLType, javaType, generic, reference, Location.PARAMETER_TYPE, batched)
118 | }
119 | }
120 | class RawClassRequiredForGraphQLMappingException(msg: String): RuntimeException(msg)
121 | }
122 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/coxautodev/graphql/tools/Utils.kt:
--------------------------------------------------------------------------------
1 | package com.coxautodev.graphql.tools
2 |
3 | import graphql.language.FieldDefinition
4 | import graphql.language.ListType
5 | import graphql.language.NonNullType
6 | import graphql.language.ObjectTypeDefinition
7 | import graphql.language.Type
8 | import graphql.language.TypeExtensionDefinition
9 |
10 | /**
11 | * @author Andrew Potter
12 | */
13 |
14 | internal typealias GraphQLRootResolver = GraphQLResolver
15 | internal typealias JavaType = java.lang.reflect.Type
16 | internal typealias GraphQLLangType = graphql.language.Type
17 |
18 | internal fun Type.unwrap(): Type = when(this) {
19 | is NonNullType -> this.type.unwrap()
20 | is ListType -> this.type.unwrap()
21 | else -> this
22 | }
23 |
24 | internal fun ObjectTypeDefinition.getExtendedFieldDefinitions(extensions: List): List {
25 | return this.fieldDefinitions + extensions.filter { it.name == this.name }.flatMap { it.fieldDefinitions }
26 | }
27 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/framework/Application.kt:
--------------------------------------------------------------------------------
1 | package com.framework
2 |
3 | import org.janusgraph.core.JanusGraph
4 | import org.janusgraph.core.JanusGraphFactory
5 | import org.springframework.boot.SpringApplication
6 | import org.springframework.boot.autoconfigure.SpringBootApplication
7 | import org.springframework.context.annotation.Bean
8 | import org.springframework.context.annotation.ComponentScan
9 | import org.springframework.core.io.ClassPathResource
10 |
11 |
12 | @SpringBootApplication
13 | @ComponentScan(basePackages = ["com"])
14 | class Application {
15 |
16 | @Bean
17 | fun graph(): JanusGraph =
18 | JanusGraphFactory.open(ClassPathResource("janusgraph-configuration.properties").file.absolutePath)
19 | }
20 |
21 | fun main(args: Array) {
22 | SpringApplication.run(Application::class.java, *args)
23 | }
24 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/framework/datamodel/edges/Linked.kt:
--------------------------------------------------------------------------------
1 | package com.framework.datamodel.edges
2 |
3 |
4 | internal interface Linked: Relationship {
5 |
6 | override val inverse: Linked
7 |
8 | override val hops: List> get() = first.hops + last.hops
9 |
10 | val first: Relationship
11 | val last: Relationship<*, TO>
12 |
13 | data class OptionalToOptional(
14 | override val first: Relationship.OneToOne,
15 | override val last: Relationship.OneToOne<*, TO>
16 | ): Linked, Relationship.OptionalToOptional {
17 |
18 | override val inverse: OptionalToOptional get() =
19 | OptionalToOptional(first = last.inverse, last = first.inverse)
20 | }
21 |
22 | data class OptionalToSingle(
23 | override val first: Relationship.OneToSingle,
24 | override val last: Relationship.OneToSingle<*, TO>
25 | ): Linked, Relationship.OptionalToSingle {
26 |
27 | override val inverse: SingleToOptional get() =
28 | SingleToOptional(first = last.inverse, last = first.inverse)
29 | }
30 |
31 | data class SingleToOptional(
32 | override val first: Relationship.SingleToOne,
33 | override val last: Relationship.SingleToOne<*, TO>
34 | ): Linked, Relationship.SingleToOptional {
35 |
36 | override val inverse: OptionalToSingle get() =
37 | OptionalToSingle(first = last.inverse, last = first.inverse)
38 | }
39 |
40 | data class SingleToSingle(
41 | override val first: Relationship.SingleToSingle,
42 | override val last: Relationship.SingleToSingle<*, TO>
43 | ): Linked, Relationship.SingleToSingle {
44 |
45 | override val inverse: SingleToSingle get() =
46 | SingleToSingle(first = last.inverse, last = first.inverse)
47 | }
48 |
49 | data class OptionalToMany(
50 | override val first: Relationship.FromOne,
51 | override val last: Relationship.FromOne<*, TO>
52 | ): Linked, Relationship.OptionalToMany {
53 |
54 | override val inverse: ManyToOptional get() =
55 | ManyToOptional(first = last.inverse, last = first.inverse)
56 | }
57 |
58 | data class SingleToMany(
59 | override val first: Relationship.FromSingle,
60 | override val last: Relationship.FromSingle<*, TO>
61 | ): Linked, Relationship.SingleToMany {
62 |
63 | override val inverse: ManyToSingle get() =
64 | ManyToSingle(first = last.inverse, last = first.inverse)
65 | }
66 |
67 | data class ManyToOptional(
68 | override val first: Relationship.ToOne,
69 | override val last: Relationship.ToOne<*, TO>
70 | ): Linked, Relationship.ManyToOptional {
71 |
72 | override val inverse: OptionalToMany get() =
73 | OptionalToMany(first = last.inverse, last = first.inverse)
74 | }
75 |
76 | data class ManyToSingle(
77 | override val first: Relationship.ToSingle,
78 | override val last: Relationship.ToSingle<*, TO>
79 | ): Linked, Relationship.ManyToSingle {
80 |
81 | override val inverse: SingleToMany get() =
82 | SingleToMany(first = last.inverse, last = first.inverse)
83 | }
84 |
85 | data class ManyToMany(
86 | override val first: Relationship,
87 | override val last: Relationship<*, TO>
88 | ): Linked, Relationship.ManyToMany {
89 |
90 | override val inverse: ManyToMany get() =
91 | ManyToMany(first = last.inverse, last = first.inverse)
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/framework/datamodel/edges/Relationship.kt:
--------------------------------------------------------------------------------
1 | package com.framework.datamodel.edges
2 |
3 | interface Relationship {
4 |
5 | val inverse: Relationship
6 |
7 | val hops: List>
8 |
9 | fun to(
10 | next: ManyToMany
11 | ): ManyToMany =
12 | Linked.ManyToMany(first = this, last = next)
13 |
14 | interface FromOne: Relationship {
15 | override val inverse: ToOne
16 | }
17 |
18 | interface FromOptional: FromOne {
19 |
20 | override val inverse: ToOptional
21 |
22 | fun to(next: OptionalToMany): OptionalToMany =
23 | Linked.OptionalToMany(first = this, last = next)
24 |
25 | fun to(next: SingleToMany): OptionalToMany =
26 | Linked.OptionalToMany(first = this, last = next)
27 | }
28 |
29 | interface FromSingle: FromOne {
30 |
31 | override val inverse: ToSingle
32 |
33 | fun to(next: OptionalToMany): OptionalToMany =
34 | Linked.OptionalToMany(first = this, last = next)
35 |
36 | fun to(next: SingleToMany): SingleToMany =
37 | Linked.SingleToMany(first = this, last = next)
38 | }
39 |
40 |
41 | interface FromMany: Relationship {
42 |
43 | override val inverse: ToMany
44 |
45 | fun to(next: OptionalToMany): ManyToMany =
46 | Linked.ManyToMany(first = this, last = next)
47 |
48 | fun to(next: SingleToMany): ManyToMany =
49 | Linked.ManyToMany(first = this, last = next)
50 | }
51 |
52 | interface ToOne: Relationship {
53 | override val inverse: FromOne
54 | }
55 |
56 | interface ToOptional: ToOne {
57 |
58 | override val inverse: FromOptional
59 |
60 | fun to(next: ManyToOptional): ManyToOptional =
61 | Linked.ManyToOptional(first = this, last = next)
62 |
63 | fun to(next: ManyToSingle): ManyToOptional =
64 | Linked.ManyToOptional(first = this, last = next)
65 | }
66 |
67 | interface ToSingle: ToOne {
68 |
69 | override val inverse: FromSingle
70 |
71 | fun to(next: ManyToOptional): ManyToOptional =
72 | Linked.ManyToOptional(first = this, last = next)
73 |
74 | fun to(next: ManyToSingle): ManyToSingle =
75 | Linked.ManyToSingle(first = this, last = next)
76 | }
77 |
78 | interface ToMany: Relationship {
79 |
80 | override val inverse: FromMany
81 |
82 | fun to(next: ManyToOptional): ManyToMany =
83 | Linked.ManyToMany(first = this, last = next)
84 |
85 | fun to(next: ManyToSingle): ManyToMany =
86 | Linked.ManyToMany(first = this, last = next)
87 | }
88 |
89 | interface OneToOne: FromOne, ToOne {
90 | override val inverse: OneToOne
91 | }
92 |
93 | interface OneToOptional: OneToOne, ToOptional {
94 | override val inverse: OptionalToOne
95 | }
96 |
97 | interface OneToSingle: OneToOne, ToSingle {
98 | override val inverse: SingleToOne
99 | }
100 |
101 | interface OptionalToOne: FromOptional, OneToOne {
102 | override val inverse: OneToOptional
103 | }
104 |
105 | interface SingleToOne: FromSingle, OneToOne {
106 | override val inverse: OneToSingle
107 | }
108 |
109 | interface OneToMany: FromOne, ToMany {
110 | override val inverse: ManyToOne
111 | }
112 |
113 | interface ManyToOne: FromMany, ToOne {
114 | override val inverse: OneToMany