Repository: tipsy/javalin
Pull request #1321 (2021-07-22 07:23:37)

Release process failing due to missing javadoc (#1318)

tipsy: Thanks @Gmugra, could you also add a "Release" section in the readme with the updated instructions?

Patch:
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 00000000..522fcfd6
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,1 @@
+github: [tipsy]
diff --git a/README.md b/README.md
index dd90f6dd..0e4e9fab 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
-[![Chat at https://gitter.im/javalin-io/general](https://badges.gitter.im/javalin-io/general.svg)](https://gitter.im/javalin-io/general)
 [![Chat at https://discord.gg/sgak4e5NKv](https://img.shields.io/badge/chat-on%20Discord-%234cb697)](https://discord.gg/sgak4e5NKv)
-[![Travis](https://github.com/tipsy/javalin/workflows/Test%20all%20JDKs%20on%20all%20OSes/badge.svg)](https://github.com/tipsy/javalin/actions)
+[![Chat at https://gitter.im/javalin-io/general](https://badges.gitter.im/javalin-io/general.svg)](https://gitter.im/javalin-io/general)
+[![CI](https://github.com/tipsy/javalin/workflows/Test%20all%20JDKs%20on%20all%20OSes/badge.svg)](https://github.com/tipsy/javalin/actions)
 [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
 [![Maven](https://img.shields.io/maven-central/v/io.javalin/javalin.svg)](https://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22io.javalin%22%20AND%20a%3A%22javalin%22)
 
@@ -15,10 +15,11 @@ Javalin is more of a library than a framework. Some key points:
 * There is no other magic; just code.
 
 General information:
+* [:heart: Sponsor Javalin](https://github.com/sponsors/tipsy)
 * The project webpage is [javalin.io](https://javalin.io) (repo for webpage is at [github.com/javalin/javalin.github.io](https://github.com/javalin/javalin.github.io)).
 * Documentation: [javalin.io/documentation](https://javalin.io/documentation)
-* Chat on Gitter: https://gitter.im/javalin-io/general
 * Chat on Discord: https://discord.gg/sgak4e5NKv
+* Chat on Gitter: https://gitter.im/javalin-io/general
 * Contributions are very welcome: [CONTRIBUTING.md](https://github.com/tipsy/javalin/blob/master/CONTRIBUTING.md)
 * License summary: https://tldrlegal.com/license/apache-license-2.0-(apache-2.0)
 * Interesting issues: [/tipsy/javalin/issues?q=label:INFO](https://github.com/tipsy/javalin/issues?q=is%3Aissue+label%3AINFO)
diff --git a/javalin-bundle/pom.xml b/javalin-bundle/pom.xml
index 3075bc93..9d5ff2fb 100644
--- a/javalin-bundle/pom.xml
+++ b/javalin-bundle/pom.xml
@@ -50,4 +50,30 @@
         </dependency>
     </dependencies>
 
+    <profiles>
+        <profile>
+            <id>sonatype-oss-release</id>
+            <build>
+                <plugins>
+                    <plugin>
+                        <artifactId>maven-jar-plugin</artifactId>
+                        <executions>
+                            <execution>
+                                <id>empty-javadoc-jar</id>
+                                <phase>package</phase>
+                                <goals>
+                                    <goal>jar</goal>
+                                </goals>
+                                <configuration>
+                                    <classifier>javadoc</classifier>
+                                    <classesDirectory>${project.basedir}/javadoc</classesDirectory>
+                                </configuration>
+                            </execution>
+                        </executions>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
+    </profiles>
+
 </project>
diff --git a/javalin-graphql/pom.xml b/javalin-graphql/pom.xml
index ceed0fd4..49a94b1a 100644
--- a/javalin-graphql/pom.xml
+++ b/javalin-graphql/pom.xml
@@ -10,6 +10,7 @@
     <artifactId>javalin-graphql</artifactId>
 
     <properties>
+        <dokka.skip>false</dokka.skip>
         <copy-build-resources.skip>false</copy-build-resources.skip>
     </properties>
 
@@ -26,6 +27,11 @@
             <groupId>com.expediagroup</groupId>
             <artifactId>graphql-kotlin-schema-generator</artifactId>
         </dependency>
+        <dependency>
+            <groupId>com.expediagroup</groupId>
+            <artifactId>graphql-kotlin-server</artifactId>
+        </dependency>
+        <!-- https://mvnrepository.com/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-jdk8 -->
         <dependency>
             <groupId>io.projectreactor</groupId>
             <artifactId>reactor-core</artifactId>
diff --git a/javalin-graphql/src/main/java/io/javalin/plugin/graphql/GraphQLHandler.kt b/javalin-graphql/src/main/java/io/javalin/plugin/graphql/GraphQLHandler.kt
index 44a73208..99a2cec0 100644
--- a/javalin-graphql/src/main/java/io/javalin/plugin/graphql/GraphQLHandler.kt
+++ b/javalin-graphql/src/main/java/io/javalin/plugin/graphql/GraphQLHandler.kt
@@ -1,55 +1,28 @@
 package io.javalin.plugin.graphql
 
-import com.expediagroup.graphql.SchemaGeneratorConfig
-import com.expediagroup.graphql.toSchema
-import graphql.GraphQL
-import io.javalin.http.Context
 import io.javalin.plugin.graphql.graphql.GraphQLRun
 import io.javalin.websocket.WsMessageContext
+import kotlinx.coroutines.runBlocking
 
-class GraphQLHandler(val options: GraphQLOptions) {
-    private var graphQL: GraphQL
+class GraphQLHandler(private val graphQLBuilder: GraphQLPluginBuilder<*>) {
 
-    init {
-        val config = SchemaGeneratorConfig(supportedPackages = options.packages)
-        val schema = toSchema(
-            config = config,
-            queries = options.queries,
-            mutations = options.mutations,
-            subscriptions = options.subscriptions
-        )
-        this.graphQL = GraphQL.newGraphQL(schema).build()!!
-    }
-
-    fun execute(ctx: Context) {
-        val body = ctx.bodyAsClass(Map::class.java)
-        this.options.middleHandler(ctx)
-        ctx.future(this.genericExecute(body).execute()) { result ->
-            if (result != null) {
-                ctx.json(result)
-            }
-        }
-    }
 
     fun execute(ctx: WsMessageContext) {
         val body = ctx.messageAsClass(Map::class.java)
-        this.options.wsMiddleHandler(ctx)
-        this.genericExecute(body)
-            .subscribe(SubscriberGraphQL(ctx))
-    }
-
-
-    private fun genericExecute(body: Map<*, *>): GraphQLRun {
         val query = body.get("query").toString()
-        val variables: Map<String, Any> = if (body["variables"] == null) emptyMap() else body["variables"] as Map<String, Any>
+        val variables: Map<String, Any> = getVariables(body)
         val operationName = body.get("operationName")?.toString()
+        val context = runBlocking { graphQLBuilder.contextWsFactory.generateContext(ctx) }
 
-        return GraphQLRun(graphQL)
+        GraphQLRun(graphQLBuilder.getSchema())
             .withQuery(query)
             .withVariables(variables)
             .withOperationName(operationName)
-            .withContext(options.context);
+            .withContext(context)
+            .subscribe(SubscriberGraphQL(ctx))
     }
 
+    private fun getVariables(body: Map<*, *>) =
+        if (body["variables"] == null) emptyMap() else body["variables"] as Map<String, Any>
 
 }
diff --git a/javalin-graphql/src/main/java/io/javalin/plugin/graphql/GraphQLOptions.kt b/javalin-graphql/src/main/java/io/javalin/plugin/graphql/GraphQLOptions.kt
index 975a4216..f0f72b70 100644
--- a/javalin-graphql/src/main/java/io/javalin/plugin/graphql/GraphQLOptions.kt
+++ b/javalin-graphql/src/main/java/io/javalin/plugin/graphql/GraphQLOptions.kt
@@ -1,6 +1,6 @@
 package io.javalin.plugin.graphql
 
-import com.expediagroup.graphql.TopLevelObject
+import com.expediagroup.graphql.generator.TopLevelObject
 import io.javalin.http.Context
 import io.javalin.plugin.graphql.graphql.MutationGraphql
 import io.javalin.plugin.graphql.graphql.QueryGraphql
diff --git a/javalin-graphql/src/main/java/io/javalin/plugin/graphql/GraphQLPlugin.kt b/javalin-graphql/src/main/java/io/javalin/plugin/graphql/GraphQLPlugin.kt
index 20347b44..58bda750 100644
--- a/javalin-graphql/src/main/java/io/javalin/plugin/graphql/GraphQLPlugin.kt
+++ b/javalin-graphql/src/main/java/io/javalin/plugin/graphql/GraphQLPlugin.kt
@@ -3,27 +3,42 @@ package io.javalin.plugin.graphql
 import io.javalin.Javalin
 import io.javalin.core.plugin.Plugin
 import io.javalin.core.plugin.PluginLifecycleInit
+import io.javalin.plugin.graphql.server.JavalinGraphQLServer
+import kotlinx.coroutines.runBlocking
+import org.eclipse.jetty.http.HttpStatus
 
-class GraphQLPlugin(private val options: GraphQLOptions) : Plugin, PluginLifecycleInit {
+class GraphQLPlugin(private val builder: GraphQLPluginBuilder<*>) : Plugin, PluginLifecycleInit {
 
-    lateinit var graphQLHandler: GraphQLHandler
-
-    override fun init(app: Javalin) {
-        graphQLHandler = GraphQLHandler(options)
+    constructor(options: GraphQLOptions) : this(GraphQLPluginBuilder.create(options)) {
+        GraphQLPluginBuilder
     }
 
+    private val graphQLHandler: GraphQLHandler = GraphQLHandler(builder)
+
+
     override fun apply(app: Javalin) {
-        app.get(options.path) {
+        val server = JavalinGraphQLServer.create(builder)
+        app.get(builder.path) {
             it.contentType("text/html; charset=UTF-8")
-                    .result(
-                            GraphQLPlugin::class.java.getResourceAsStream("graphqli/index.html")
-                    )
+                .result(
+                    GraphQLPlugin::class.java.getResourceAsStream("graphqli/index.html")
+                )
         }
-        app.post(options.path) { ctx ->
-            graphQLHandler.execute(ctx)
+        app.post(builder.path) { ctx ->
+            val response = runBlocking { server.execute(ctx) }
+            if (response != null) {
+                ctx.json(response)
+            } else {
+                ctx.status(HttpStatus.BAD_REQUEST_400).json(mapOf("error" to "Invalid request"))
+            }
         }
-        app.ws(options.path) { ws ->
+        app.ws(builder.path) { ws ->
             ws.onMessage { ctx -> graphQLHandler.execute(ctx) }
         }
     }
+
+    override fun init(app: Javalin) {
+
+    }
+
 }
diff --git a/javalin-graphql/src/main/java/io/javalin/plugin/graphql/GraphQLPluginBuilder.kt b/javalin-graphql/src/main/java/io/javalin/plugin/graphql/GraphQLPluginBuilder.kt
new file mode 100644
index 00000000..3fd6d274
--- /dev/null
+++ b/javalin-graphql/src/main/java/io/javalin/plugin/graphql/GraphQLPluginBuilder.kt
@@ -0,0 +1,78 @@
+package io.javalin.plugin.graphql
+
+import com.expediagroup.graphql.generator.SchemaGeneratorConfig
+import com.expediagroup.graphql.generator.TopLevelObject
+import com.expediagroup.graphql.generator.execution.GraphQLContext
+import com.expediagroup.graphql.generator.toSchema
+import com.expediagroup.graphql.server.execution.GraphQLContextFactory
+import com.expediagroup.graphql.server.execution.KotlinDataLoader
+import graphql.GraphQL
+import io.javalin.http.Context
+import io.javalin.plugin.graphql.context.EmptyGraphQLContext
+import io.javalin.plugin.graphql.context.EmptyGraphQLContextFactory
+import io.javalin.plugin.graphql.context.EmptyWsGraphQLContextFactory
+import io.javalin.plugin.graphql.graphql.MutationGraphql
+import io.javalin.plugin.graphql.graphql.QueryGraphql
+import io.javalin.plugin.graphql.graphql.SubscriptionGraphql
+import io.javalin.plugin.graphql.server.JavalinDataLoaderRegistryFactory
+import io.javalin.websocket.WsMessageContext
+
+class GraphQLPluginBuilder<out T : GraphQLContext>(
+    val path: String,
+    val contextFactory: GraphQLContextFactory<T, Context>,
+    val contextWsFactory: GraphQLContextFactory<T, WsMessageContext>
+) {
+
+    private var graphql: GraphQL? = null
+    private var queries: MutableList<TopLevelObject> = mutableListOf()
+    private var mutations: MutableList<TopLevelObject> = mutableListOf()
+    private var subscriptions: MutableList<TopLevelObject> = mutableListOf()
+    private var packages: MutableList<String> = mutableListOf("kotlin.Unit")
+    private val dataLoaders: MutableList<KotlinDataLoader<*, *>> = mutableListOf()
+
+    companion object {
+        fun create(options: GraphQLOptions): GraphQLPluginBuilder<*> {
+            val graphQLPluginBuilder = GraphQLPluginBuilder(options.path, EmptyGraphQLContextFactory(), EmptyWsGraphQLContextFactory())
+            graphQLPluginBuilder.queries = options.queries
+            graphQLPluginBuilder.mutations = options.mutations
+            graphQLPluginBuilder.subscriptions = options.subscriptions
+            graphQLPluginBuilder.packages = options.packages
+            return graphQLPluginBuilder
+        }
+    }
+
+    fun add(aPackage: String) = apply { packages.add(aPackage) }
+
+    fun register(vararg dataLoaders: KotlinDataLoader<*, *>) = apply { this.dataLoaders.addAll(dataLoaders) }
+
+    fun register(vararg queries: QueryGraphql) = apply {
+        this.queries.addAll(queries.map { TopLevelObject(it) })
+    }
+
+    fun register(vararg mutations: MutationGraphql) = apply {
+        this.mutations.addAll(mutations.map { TopLevelObject(it) })
+    }
+
+    fun register(vararg subscriptions: SubscriptionGraphql) = apply {
+        this.subscriptions.addAll(subscriptions.map { TopLevelObject(it) })
+    }
+
+    fun build() = GraphQLPlugin(this)
+
+    internal fun getSchema(): GraphQL {
+        if (graphql == null) {
+            graphql = GraphQL.newGraphQL(
+                toSchema(
+                    config = SchemaGeneratorConfig(supportedPackages = packages),
+                    queries = queries,
+                    mutations = mutations,
+                    subscriptions = subscriptions
+                )
+            ).build()!!
+        }
+
+        return graphql!!
+    }
+
+    fun toJavalinDataLoaderRegistryFactory() = JavalinDataLoaderRegistryFactory(dataLoaders)
+}
diff --git a/javalin-graphql/src/main/java/io/javalin/plugin/graphql/context/EmptyGraphQLContext.kt b/javalin-graphql/src/main/java/io/javalin/plugin/graphql/context/EmptyGraphQLContext.kt
new file mode 100644
index 00000000..0fc29273
--- /dev/null
+++ b/javalin-graphql/src/main/java/io/javalin/plugin/graphql/context/EmptyGraphQLContext.kt
@@ -0,0 +1,5 @@
+package io.javalin.plugin.graphql.context
+
+import com.expediagroup.graphql.generator.execution.GraphQLContext
+
+class EmptyGraphQLContext : GraphQLContext
diff --git a/javalin-graphql/src/main/java/io/javalin/plugin/graphql/context/EmptyGraphQLContextFactory.kt b/javalin-graphql/src/main/java/io/javalin/plugin/graphql/context/EmptyGraphQLContextFactory.kt
new file mode 100644
index 00000000..a5baf8d7
--- /dev/null
+++ b/javalin-graphql/src/main/java/io/javalin/plugin/graphql/context/EmptyGraphQLContextFactory.kt
@@ -0,0 +1,10 @@
+package io.javalin.plugin.graphql.context
+
+import com.expediagroup.graphql.server.execution.GraphQLContextFactory
+import io.javalin.http.Context
+
+class EmptyGraphQLContextFactory : GraphQLContextFactory<EmptyGraphQLContext, Context> {
+    override suspend fun generateContext(request: Context): EmptyGraphQLContext {
+        return EmptyGraphQLContext()
+    }
+}
diff --git a/javalin-graphql/src/main/java/io/javalin/plugin/graphql/context/EmptyWsGraphQLContextFactory.kt b/javalin-graphql/src/main/java/io/javalin/plugin/graphql/context/EmptyWsGraphQLContextFactory.kt
new file mode 100644
index 00000000..650a1562
--- /dev/null
+++ b/javalin-graphql/src/main/java/io/javalin/plugin/graphql/context/EmptyWsGraphQLContextFactory.kt
@@ -0,0 +1,10 @@
+package io.javalin.plugin.graphql.context
+
+import com.expediagroup.graphql.server.execution.GraphQLContextFactory
+import io.javalin.websocket.WsMessageContext
+
+class EmptyWsGraphQLContextFactory : GraphQLContextFactory<EmptyGraphQLContext, WsMessageContext> {
+    override suspend fun generateContext(request: WsMessageContext): EmptyGraphQLContext {
+        return EmptyGraphQLContext()
+    }
+}
diff --git a/javalin-graphql/src/main/java/io/javalin/plugin/graphql/graphql/GraphQLRun.kt b/javalin-graphql/src/main/java/io/javalin/plugin/graphql/graphql/GraphQLRun.kt
index df708d83..0e987aa0 100644
--- a/javalin-graphql/src/main/java/io/javalin/plugin/graphql/graphql/GraphQLRun.kt
+++ b/javalin-graphql/src/main/java/io/javalin/plugin/graphql/graphql/GraphQLRun.kt
@@ -4,6 +4,7 @@ import graphql.ExceptionWhileDataFetching
 import graphql.ExecutionInput
 import graphql.ExecutionResult
 import graphql.GraphQL
+import org.eclipse.jetty.server.handler.ContextHandler
 import org.reactivestreams.Publisher
 import org.reactivestreams.Subscriber
 import java.util.concurrent.CompletableFuture
diff --git a/javalin-graphql/src/main/java/io/javalin/plugin/graphql/server/JavalinDataLoaderRegistryFactory.kt b/javalin-graphql/src/main/java/io/javalin/plugin/graphql/server/JavalinDataLoaderRegistryFactory.kt
new file mode 100644
index 00000000..a73d3ff0
--- /dev/null
+++ b/javalin-graphql/src/main/java/io/javalin/plugin/graphql/server/JavalinDataLoaderRegistryFactory.kt
@@ -0,0 +1,14 @@
+package io.javalin.plugin.graphql.server
+
+import com.expediagroup.graphql.server.execution.DataLoaderRegistryFactory
+import com.expediagroup.graphql.server.execution.KotlinDataLoader
+import org.dataloader.DataLoaderRegistry
+
+class JavalinDataLoaderRegistryFactory(private val dataLoaders: List<KotlinDataLoader<*, *>>): DataLoaderRegistryFactory {
+    override fun generate(): DataLoaderRegistry {
+        val registry = DataLoaderRegistry()
+        dataLoaders.forEach { registry.register(it.dataLoaderName, it.getDataLoader()) }
+        return registry
+    }
+
+}
diff --git a/javalin-graphql/src/main/java/io/javalin/plugin/graphql/server/JavalinGraphQLRequestParser.kt b/javalin-graphql/src/main/java/io/javalin/plugin/graphql/server/JavalinGraphQLRequestParser.kt
new file mode 100644
index 00000000..e13b86f6
--- /dev/null
+++ b/javalin-graphql/src/main/java/io/javalin/plugin/graphql/server/JavalinGraphQLRequestParser.kt
@@ -0,0 +1,16 @@
+package io.javalin.plugin.graphql.server
+
+import com.expediagroup.graphql.server.execution.GraphQLRequestParser
+import com.expediagroup.graphql.server.types.GraphQLServerRequest
+import io.javalin.http.Context
+import java.io.IOException
+
+class JavalinGraphQLRequestParser : GraphQLRequestParser<Context> {
+
+    @Suppress("BlockingMethodInNonBlockingContext")
+    override suspend fun parseRequest(context: Context): GraphQLServerRequest = try {
+        context.bodyAsClass(GraphQLServerRequest::class.java)
+    } catch (e: IOException) {
+        throw IOException("Unable to parse GraphQL payload.")
+    }
+}
diff --git a/javalin-graphql/src/main/java/io/javalin/plugin/graphql/server/JavalinGraphQLServer.kt b/javalin-graphql/src/main/java/io/javalin/plugin/graphql/server/JavalinGraphQLServer.kt
new file mode 100644
index 00000000..8cd20c71
--- /dev/null
+++ b/javalin-graphql/src/main/java/io/javalin/plugin/graphql/server/JavalinGraphQLServer.kt
@@ -0,0 +1,18 @@
+package io.javalin.plugin.graphql.server
+
+import com.expediagroup.graphql.server.execution.*
+import io.javalin.http.Context
+import io.javalin.plugin.graphql.GraphQLPluginBuilder
+
+class JavalinGraphQLServer(contextFactory: GraphQLContextFactory<*, Context>, requestHandler: GraphQLRequestHandler) : GraphQLServer<Context>(
+    JavalinGraphQLRequestParser(), contextFactory, requestHandler) {
+    companion object {
+        fun create(builder: GraphQLPluginBuilder<*>): JavalinGraphQLServer {
+            val dataLoaderRegistryFactory = builder.toJavalinDataLoaderRegistryFactory()
+            val requestHandler = GraphQLRequestHandler(builder.getSchema(), dataLoaderRegistryFactory)
+
+            return JavalinGraphQLServer(builder.contextFactory, requestHandler)
+        }
+    }
+}
+
diff --git a/javalin-openapi/pom.xml b/javalin-openapi/pom.xml
index c173f127..14fcd805 100644
--- a/javalin-openapi/pom.xml
+++ b/javalin-openapi/pom.xml
@@ -10,6 +10,7 @@
     <artifactId>javalin-openapi</artifactId>
 
     <properties>
+        <dokka.skip>false</dokka.skip>
         <copy-build-resources.skip>false</copy-build-resources.skip>
     </properties>
 
diff --git a/javalin-openapi/src/main/java/io/javalin/plugin/openapi/dsl/openApiBuilderDsl.kt b/javalin-openapi/src/main/java/io/javalin/plugin/openapi/dsl/openApiBuilderDsl.kt
index 625333c7..793b14c9 100644
--- a/javalin-openapi/src/main/java/io/javalin/plugin/openapi/dsl/openApiBuilderDsl.kt
+++ b/javalin-openapi/src/main/java/io/javalin/plugin/openapi/dsl/openApiBuilderDsl.kt
@@ -19,6 +19,7 @@ import io.swagger.v3.oas.models.Operation
 import io.swagger.v3.oas.models.PathItem
 import io.swagger.v3.oas.models.Paths
 import org.slf4j.LoggerFactory
+import java.util.*
 
 private val logger = LoggerFactory.getLogger(JavalinOpenApi::class.java)
 
@@ -187,14 +188,14 @@ fun HandlerType.asPathItemHttpMethod(): PathItem.HttpMethod? = when (this) {
 
 private fun HandlerMetaInfo.createDefaultOperationId(path: PathParser): String {
     val metaInfo = this
-    val lowerCaseMethod = metaInfo.httpMethod.toString().toLowerCase()
+    val lowerCaseMethod = metaInfo.httpMethod.toString().lowercase(Locale.ROOT)
     val capitalizedPath = path.asReadableWords().joinToString("") { it.capitalize() }
     return lowerCaseMethod + capitalizedPath
 }
 
 private fun HandlerMetaInfo.createDefaultSummary(path: PathParser): String {
     val metaInfo = this
-    val capitalizedMethod = metaInfo.httpMethod.toString().toLowerCase().capitalize()
+    val capitalizedMethod = metaInfo.httpMethod.toString().lowercase(Locale.ROOT).capitalize()
     return (listOf(capitalizedMethod) + path.asReadableWords()).joinToString(" ") { it.trim() }
 }
 
@@ -214,6 +215,6 @@ private fun PathParser.asReadableWords(): List<String> {
 }
 
 private fun String.dashCaseToCamelCase() = split("-")
-        .map { it.toLowerCase() }
+        .map { it.lowercase(Locale.ROOT) }
         .mapIndexed { index, s -> if (index > 0) s.capitalize() else s }
         .joinToString("")
diff --git a/javalin-without-jetty/pom.xml b/javalin-without-jetty/pom.xml
index cbe4002e..0b172b2c 100644
--- a/javalin-without-jetty/pom.xml
+++ b/javalin-without-jetty/pom.xml
@@ -18,6 +18,7 @@
     </dependencies>
 
     <properties>
+        <javadoc.skip>false</javadoc.skip>
         <copy-build-resources.skip>false</copy-build-resources.skip>
 
         <original.root>../javalin/src/main/java/io/javalin</original.root>
@@ -151,12 +152,23 @@
                     </execution>
                 </executions>
             </plugin>
-            <plugin>
-                <artifactId>maven-javadoc-plugin</artifactId>
-                <configuration>
-                    <sourcepath>${original.root}</sourcepath>
-                </configuration>
-            </plugin>
         </plugins>
     </build>
+
+    <profiles>
+        <profile>
+            <id>sonatype-oss-release</id>
+            <build>
+                <plugins>
+                    <plugin>
+                        <artifactId>maven-javadoc-plugin</artifactId>
+                        <configuration>
+                            <sourcepath>${original.root}</sourcepath>
+                        </configuration>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
+    </profiles>
+
 </project>
diff --git a/javalin/pom.xml b/javalin/pom.xml
index 828c2ec5..f96346b9 100644
--- a/javalin/pom.xml
+++ b/javalin/pom.xml
@@ -10,6 +10,7 @@
     <artifactId>javalin</artifactId>
 
     <properties>
+        <dokka.skip>false</dokka.skip>
         <copy-build-resources.skip>false</copy-build-resources.skip>
     </properties>
 
diff --git a/javalin/src/main/java/io/javalin/core/util/Headers.kt b/javalin/src/main/java/io/javalin/core/util/Headers.kt
index 143cb1e7..f0146650 100644
--- a/javalin/src/main/java/io/javalin/core/util/Headers.kt
+++ b/javalin/src/main/java/io/javalin/core/util/Headers.kt
@@ -74,7 +74,7 @@ class Headers {
             } else if (this == EXECUTION_CONTEXTS) {
                 return "\"executionContexts\""
             }
-            return "\"" + name.toLowerCase(Locale.ROOT) + "\""
+            return "\"" + name.lowercase(Locale.ROOT) + "\""
         }
     }
 
@@ -110,6 +110,6 @@ class Headers {
     }
 
     private fun String.toHttpHeaderValue() : String {
-        return this.toLowerCase(Locale.ROOT).replace("_", "-")
+        return this.lowercase(Locale.ROOT).replace("_", "-")
     }
 }
diff --git a/javalin/src/main/java/io/javalin/core/util/RouteOverviewUtil.kt b/javalin/src/main/java/io/javalin/core/util/RouteOverviewUtil.kt
index 748de5f7..19e2bdaf 100644
--- a/javalin/src/main/java/io/javalin/core/util/RouteOverviewUtil.kt
+++ b/javalin/src/main/java/io/javalin/core/util/RouteOverviewUtil.kt
@@ -17,6 +17,7 @@ import io.javalin.plugin.openapi.annotations.ContentType
 import io.javalin.plugin.openapi.annotations.OpenApi
 import io.javalin.plugin.openapi.annotations.OpenApiContent
 import io.javalin.plugin.openapi.annotations.OpenApiResponse
+import java.util.*
 
 data class RouteOverviewConfig(val path: String, val roles: Set<RouteRole>)
 
@@ -37,7 +38,7 @@ class RouteOverviewRenderer(val app: Javalin) : Handler {
             ]
     )
     override fun handle(ctx: Context) {
-        if (ctx.header(Header.ACCEPT)?.toLowerCase()?.contains("application/json") == true) {
+        if (ctx.header(Header.ACCEPT)?.lowercase(Locale.ROOT)?.contains("application/json") == true) {
             ctx.header("Content-Type", "application/json")
             ctx.result(RouteOverviewUtil.createJsonOverview(handlerMetaInfoList, wsHandlerMetaInfoList))
         } else {
diff --git a/javalin/src/main/java/io/javalin/http/Context.kt b/javalin/src/main/java/io/javalin/http/Context.kt
index 99e72612..af945290 100644
--- a/javalin/src/main/java/io/javalin/http/Context.kt
+++ b/javalin/src/main/java/io/javalin/http/Context.kt
@@ -109,7 +109,7 @@ open class Context(@JvmField val req: HttpServletRequest, @JvmField val res: Htt
      */
     fun bodyAsBytes(): ByteArray = body
 
-    /** Maps a JSON body to a Java/Kotlin class using the registered [JsonMapper] */
+    /** Maps a JSON body to a Java/Kotlin class using the registered [io.javalin.plugin.json.JsonMapper] */
     fun <T> bodyAsClass(clazz: Class<T>): T =
         jsonMapper().let { if (it.canReadStream()) it.fromJsonStream(req.inputStream, clazz)!! else it.fromJsonString(body(), clazz)!! }
 
@@ -150,10 +150,14 @@ open class Context(@JvmField val req: HttpServletRequest, @JvmField val res: Htt
     /** Gets a list of form params for the specified key, or empty list. */
     fun formParams(key: String): List<String> = formParamMap()[key] ?: emptyList()
 
-    /** Gets a map with all the form param keys and values. */
-    fun formParamMap(): Map<String, List<String>> =
+    /** using an additional map lazily so no new objects are created whenever ctx.formParam*() is called */
+    private val formParams by lazy {
         if (isMultipartFormData()) MultipartUtil.getFieldMap(req)
         else ContextUtil.splitKeyValueStringAndGroupByKey(body(), characterEncoding)
+    }
+
+    /** Gets a map with all the form param keys and values. */
+    fun formParamMap(): Map<String, List<String>> = formParams
 
     /**
      * Gets a path param by name (ex: pathParam("param").
@@ -229,10 +233,10 @@ open class Context(@JvmField val req: HttpServletRequest, @JvmField val res: Htt
     fun ip(): String = req.remoteAddr
 
     /** Returns true if request is multipart. */
-    fun isMultipart(): Boolean = header(Header.CONTENT_TYPE)?.toLowerCase()?.contains("multipart/") == true
+    fun isMultipart(): Boolean = header(Header.CONTENT_TYPE)?.lowercase(Locale.ROOT)?.contains("multipart/") == true
 
     /** Returns true if request is multipart/form-data. */
-    fun isMultipartFormData(): Boolean = header(Header.CONTENT_TYPE)?.toLowerCase()?.contains("multipart/form-data") == true
+    fun isMultipartFormData(): Boolean = header(Header.CONTENT_TYPE)?.lowercase(Locale.ROOT)?.contains("multipart/form-data") == true
 
     /** Gets the request method. */
     fun method(): String = req.method
@@ -258,8 +262,13 @@ open class Context(@JvmField val req: HttpServletRequest, @JvmField val res: Htt
     /** Gets a list of query params for the specified key, or empty list. */
     fun queryParams(key: String): List<String> = queryParamMap()[key] ?: emptyList()
 
+    /** using an additional map lazily so no new objects are created whenever ctx.formParam*() is called */
+    private val queryParams by lazy {
+        ContextUtil.splitKeyValueStringAndGroupByKey(queryString() ?: "", characterEncoding)
+    }
+
     /** Gets a map with all the query param keys and values. */
-    fun queryParamMap(): Map<String, List<String>> = ContextUtil.splitKeyValueStringAndGroupByKey(queryString() ?: "", characterEncoding)
+    fun queryParamMap(): Map<String, List<String>> = queryParams
 
     /** Gets the request query string, or null. */
     fun queryString(): String? = req.queryString
@@ -419,7 +428,7 @@ open class Context(@JvmField val req: HttpServletRequest, @JvmField val res: Htt
     fun html(html: String): Context = contentType("text/html").result(html)
 
     /**
-     * Serializes object to a JSON-string using the registered [JsonMapper] and sets it as the context result.
+     * Serializes object to a JSON-string using the registered [io.javalin.plugin.json.JsonMapper] and sets it as the context result.
      * Also sets content type to application/json.
      */
     @JvmOverloads
diff --git a/javalin/src/main/java/io/javalin/http/HttpResponseExceptionMapper.kt b/javalin/src/main/java/io/javalin/http/HttpResponseExceptionMapper.kt
index 7fc76fe0..fb0de818 100644
--- a/javalin/src/main/java/io/javalin/http/HttpResponseExceptionMapper.kt
+++ b/javalin/src/main/java/io/javalin/http/HttpResponseExceptionMapper.kt
@@ -8,6 +8,7 @@ package io.javalin.http
 
 import io.javalin.core.util.Header
 import io.javalin.http.util.JsonEscapeUtil
+import java.util.*
 import java.util.concurrent.CompletionException
 
 object HttpResponseExceptionMapper {
@@ -20,7 +21,7 @@ object HttpResponseExceptionMapper {
             ctx.status(e.status).result("""{
                 |    "title": "${e.message?.jsonEscape()}",
                 |    "status": ${e.status},
-                |    "type": "${getTypeUrl(e).toLowerCase()}",
+                |    "type": "${getTypeUrl(e).lowercase(Locale.ROOT)}",
                 |    "details": {${e.details.map { """"${it.key}":"${it.value.jsonEscape()}"""" }.joinToString(",")}}
                 |}""".trimMargin()
             ).contentType("application/json")
diff --git a/javalin/src/main/java/io/javalin/http/staticfiles/StaticFileConfig.kt b/javalin/src/main/java/io/javalin/http/staticfiles/StaticFileConfig.kt
index bdcd1165..60679a68 100644
--- a/javalin/src/main/java/io/javalin/http/staticfiles/StaticFileConfig.kt
+++ b/javalin/src/main/java/io/javalin/http/staticfiles/StaticFileConfig.kt
@@ -2,6 +2,7 @@ package io.javalin.http.staticfiles
 
 import io.javalin.core.util.Header
 import org.eclipse.jetty.server.handler.ContextHandler.AliasCheck
+import javax.servlet.http.HttpServletRequest
 
 data class StaticFileConfig(
     @JvmField var hostedPath: String = "/",
@@ -9,5 +10,6 @@ data class StaticFileConfig(
     @JvmField var location: Location = Location.CLASSPATH,
     @JvmField var precompress: Boolean = false,
     @JvmField var aliasCheck: AliasCheck? = null,
-    @JvmField var headers: Map<String, String> = mutableMapOf(Header.CACHE_CONTROL to "max-age=0")
+    @JvmField var headers: Map<String, String> = mutableMapOf(Header.CACHE_CONTROL to "max-age=0"),
+    @JvmField var skipFileFunction: (HttpServletRequest) -> Boolean = { false },
 )
diff --git a/javalin/src/main/java/io/javalin/http/util/RateLimitUtil.kt b/javalin/src/main/java/io/javalin/http/util/RateLimitUtil.kt
index e52fe6ab..5a8226b1 100644
--- a/javalin/src/main/java/io/javalin/http/util/RateLimitUtil.kt
+++ b/javalin/src/main/java/io/javalin/http/util/RateLimitUtil.kt
@@ -8,6 +8,7 @@ package io.javalin.http.util
 
 import io.javalin.http.Context
 import io.javalin.http.HttpResponseException
+import java.util.*
 import java.util.concurrent.ConcurrentHashMap
 import java.util.concurrent.Executors
 import java.util.concurrent.ScheduledExecutorService
@@ -22,7 +23,7 @@ object RateLimitUtil {
 
 class RateLimiter(val timeUnit: TimeUnit) {
 
-    private val timeUnitString = timeUnit.toString().toLowerCase().removeSuffix("s")
+    private val timeUnitString = timeUnit.toString().lowercase(Locale.ROOT).removeSuffix("s")
     private val keyToRequestCount = ConcurrentHashMap<String, Int>().also {
         RateLimitUtil.executor.scheduleAtFixedRate({ it.clear() }, /*delay=*/0,  /*period=*/1, timeUnit)
     }
diff --git a/javalin/src/main/java/io/javalin/http/util/RedirectToLowercasePathPlugin.kt b/javalin/src/main/java/io/javalin/http/util/RedirectToLowercasePathPlugin.kt
index 652db250..4fa77f2d 100644
--- a/javalin/src/main/java/io/javalin/http/util/RedirectToLowercasePathPlugin.kt
+++ b/javalin/src/main/java/io/javalin/http/util/RedirectToLowercasePathPlugin.kt
@@ -12,6 +12,7 @@ import io.javalin.core.routing.PathSegment
 import io.javalin.core.plugin.Plugin
 import io.javalin.core.plugin.PluginLifecycleInit
 import io.javalin.http.HandlerType
+import java.util.*
 
 /**
  * This plugin redirects requests with uppercase/mixcase paths to lowercase paths
@@ -27,7 +28,7 @@ class RedirectToLowercasePathPlugin : Plugin, PluginLifecycleInit {
             e.handlerAdded { h ->
                 val parser = PathParser(h.path, app._conf.ignoreTrailingSlashes)
                 parser.segments.filterIsInstance<PathSegment.Normal>().map { it.asRegexString() }.forEach {
-                    if (it != it.toLowerCase()) throw IllegalArgumentException("Paths must be lowercase when using RedirectToLowercasePathPlugin")
+                    if (it != it.lowercase(Locale.ROOT)) throw IllegalArgumentException("Paths must be lowercase when using RedirectToLowercasePathPlugin")
                 }
                 parser.segments
                     .filterIsInstance<PathSegment.MultipleSegments>()
@@ -35,7 +36,7 @@ class RedirectToLowercasePathPlugin : Plugin, PluginLifecycleInit {
                     .filterIsInstance<PathSegment.Normal>()
                     .map { it.asRegexString() }
                     .forEach {
-                        if (it != it.toLowerCase()) {
+                        if (it != it.lowercase(Locale.ROOT)) {
                             throw IllegalArgumentException("Paths must be lowercase when using RedirectToLowercasePathPlugin")
                         }
                     }
@@ -51,12 +52,12 @@ class RedirectToLowercasePathPlugin : Plugin, PluginLifecycleInit {
             matcher.findEntries(type, requestUri).firstOrNull()?.let {
                 return@before // we found a route for this case, no need to redirect
             }
-            matcher.findEntries(type, requestUri.toLowerCase()).firstOrNull()?.let { entry ->
+            matcher.findEntries(type, requestUri.lowercase(Locale.ROOT)).firstOrNull()?.let { entry ->
                 val clientSegments = requestUri.split("/").filter { it.isNotEmpty() }.toTypedArray()
                 val serverSegments = PathParser(entry.path, app._conf.ignoreTrailingSlashes).segments
                 serverSegments.forEachIndexed { i, serverSegment ->
                     if (serverSegment is PathSegment.Normal) {
-                        clientSegments[i] = clientSegments[i].toLowerCase() // this is also a "Normal" segment
+                        clientSegments[i] = clientSegments[i].lowercase(Locale.ROOT) // this is also a "Normal" segment
                     }
                     if (serverSegment is PathSegment.MultipleSegments) {
                         serverSegments.forEach { innerServerSegment ->
@@ -64,7 +65,7 @@ class RedirectToLowercasePathPlugin : Plugin, PluginLifecycleInit {
                                 // replace the non lowercased part of the segment with the lowercased version
                                 clientSegments[i] = clientSegments[i].replace(
                                     innerServerSegment.content,
-                                    innerServerSegment.content.toLowerCase(),
+                                    innerServerSegment.content.lowercase(Locale.ROOT),
                                     ignoreCase = true
                                 )
                             }
diff --git a/javalin/src/main/java/io/javalin/jetty/JettyResourceHandler.kt b/javalin/src/main/java/io/javalin/jetty/JettyResourceHandler.kt
index 3d14abdf..411d6ef1 100644
--- a/javalin/src/main/java/io/javalin/jetty/JettyResourceHandler.kt
+++ b/javalin/src/main/java/io/javalin/jetty/JettyResourceHandler.kt
@@ -36,7 +36,7 @@ class JettyResourceHandler : JavalinResourceHandler {
     override fun handle(httpRequest: HttpServletRequest, httpResponse: HttpServletResponse): Boolean {
         val target = httpRequest.getAttribute("jetty-target") as String
         val baseRequest = httpRequest.getAttribute("jetty-request") as Request
-        for (handler in handlers) {
+        handlers.filter { !it.config.skipFileFunction(httpRequest) }.forEach { handler ->
             try {
                 val resource = handler.getResource(target)
                 if (resource.isFile() || resource.isDirectoryWithWelcomeFile(handler, target)) {
diff --git a/javalin/src/main/java/io/javalin/websocket/WsContext.kt b/javalin/src/main/java/io/javalin/websocket/WsContext.kt
index a611fe01..61197cb1 100644
--- a/javalin/src/main/java/io/javalin/websocket/WsContext.kt
+++ b/javalin/src/main/java/io/javalin/websocket/WsContext.kt
@@ -17,7 +17,7 @@ import java.nio.ByteBuffer
 
 /**
  * The [WsContext] class holds Jetty's [Session] and provides (convenient) delegate methods.
- * It adds functionality similar to the API found in [io.javalin.Context].
+ * It adds functionality similar to the API found in [io.javalin.http.Context].
  * It also adds a [send] method, which calls [RemoteEndpoint.sendString] on [Session.getRemote]
  */
 abstract class WsContext(val sessionId: String, @JvmField val session: Session) {
diff --git a/pom.xml b/pom.xml
index bee6f143..5cfdeaf1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -52,6 +52,10 @@
 
         <copy-build-resources.skip>true</copy-build-resources.skip>
 
+        <dokka.jdkVersion>8</dokka.jdkVersion>
+        <dokka.skip>true</dokka.skip>
+        <javadoc.skip>true</javadoc.skip>
+
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
         <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
         <kotlin.version>1.5.20</kotlin.version>
@@ -67,12 +71,13 @@
         <surefire.plugin.version>3.0.0-M5</surefire.plugin.version>
         <antrun.plugin.version>3.0.0</antrun.plugin.version>
         <shade.plugin.version>3.2.4</shade.plugin.version>
-        <javadoc.plugin.version>2.7</javadoc.plugin.version>
+        <javadoc.plugin.version>3.3.0</javadoc.plugin.version>
         <build-helper.plugin.version>3.2.0</build-helper.plugin.version>
         <enforcer.plugin.version>3.0.0-M3</enforcer.plugin.version>
         <clean.plugin.version>3.1.0</clean.plugin.version>
         <install.plugin.version>3.0.0-M1</install.plugin.version>
         <resources.plugin.version>3.2.0</resources.plugin.version>
+        <source.plugin.version>3.2.1</source.plugin.version>
 
         <!-- DEPENDENCIES -->
         <classgraph.version>4.8.102</classgraph.version>
@@ -95,7 +100,7 @@
         <thymeleaf.version>3.0.12.RELEASE</thymeleaf.version>
         <velocity.version>2.3</velocity.version>
         <jte.version>1.11.0</jte.version>
-        <graphql-kotlin.version>1.1.0</graphql-kotlin.version>
+        <graphql-kotlin.version>4.1.1</graphql-kotlin.version>
         <reactor-core.version>3.2.9.RELEASE</reactor-core.version>
 
         <!-- TEST DEPENDENCIES -->
@@ -314,6 +319,11 @@
                 <artifactId>graphql-kotlin-schema-generator</artifactId>
                 <version>${graphql-kotlin.version}</version>
             </dependency>
+            <dependency>
+                <groupId>com.expediagroup</groupId>
+                <artifactId>graphql-kotlin-server</artifactId>
+                <version>${graphql-kotlin.version}</version>
+            </dependency>
             <dependency>
                 <groupId>io.projectreactor</groupId>
                 <artifactId>reactor-core</artifactId>
@@ -540,6 +550,7 @@
                             <goal>copy-resources</goal>
                         </goals>
                         <configuration>
+                            <propertiesEncoding>${project.build.sourceEncoding}</propertiesEncoding>
                             <outputDirectory>${project.build.directory}</outputDirectory>
                             <resources>
                                 <resource>
@@ -579,6 +590,13 @@
                     <groupId>org.jetbrains.dokka</groupId>
                     <artifactId>dokka-maven-plugin</artifactId>
                     <version>${dokka.plugin.version}</version>
+                    <configuration>
+                        <skip>${dokka.skip}</skip>
+                        <jdkVersion>${dokka.jdkVersion}</jdkVersion>
+                        <sourceDirectories>
+                            <dir>${project.basedir}/src/main/java</dir>
+                        </sourceDirectories>
+                    </configuration>
                 </plugin>
                 <plugin>
                     <artifactId>maven-surefire-plugin</artifactId>
@@ -599,6 +617,21 @@
                         </archive>
                     </configuration>
                 </plugin>
+                <plugin>
+                    <artifactId>maven-source-plugin</artifactId>
+                    <version>${source.plugin.version}</version>
+                    <configuration>
+                        <archive>
+                            <manifest>
+                                <addDefaultImplementationEntries>true</addDefaultImplementationEntries>
+                            </manifest>
+                            <manifestEntries>
+                                <Build-Time>${maven.build.timestamp}</Build-Time>
+                            </manifestEntries>
+                            <pomPropertiesFile>${project.build.directory}/pom.properties</pomPropertiesFile>
+                        </archive>
+                    </configuration>
+                </plugin>
                 <plugin>
                     <artifactId>maven-shade-plugin</artifactId>
                     <version>${shade.plugin.version}</version>
@@ -610,6 +643,9 @@
                 <plugin>
                     <artifactId>maven-javadoc-plugin</artifactId>
                     <version>${javadoc.plugin.version}</version>
+                    <configuration>
+                        <skip>${javadoc.skip}</skip>
+                    </configuration>
                 </plugin>
                 <plugin>
                     <groupId>org.codehaus.mojo</groupId>
@@ -631,23 +667,43 @@
 
     <profiles>
         <profile>
-            <id>sign-artifacts</id>
-            <activation>
-                <property>
-                    <name>sign</name>
-                    <value>true</value>
-                </property>
-            </activation>
+            <id>sonatype-oss-release</id>
             <build>
                 <plugins>
                     <plugin>
-                        <artifactId>maven-gpg-plugin</artifactId>
+                        <artifactId>maven-source-plugin</artifactId>
+                        <version>${source.plugin.version}</version>
+                        <executions>
+                            <execution>
+                                <id>attach-sources</id>
+                                <goals>
+                                    <goal>jar-no-fork</goal>
+                                </goals>
+                            </execution>
+                        </executions>
+                    </plugin>
+                    <plugin>
+                        <groupId>org.jetbrains.dokka</groupId>
+                        <artifactId>dokka-maven-plugin</artifactId>
+                        <executions>
+                            <execution>
+                                <phase>package</phase>
+                                <id>attach-javadocs-dokka</id>
+                                <goals>
+                                    <goal>javadocJar</goal>
+                                </goals>
+                            </execution>
+                        </executions>
+                    </plugin>
+                    <plugin>
+                        <artifactId>maven-javadoc-plugin</artifactId>
+                        <version>${javadoc.plugin.version}</version>
                         <executions>
                             <execution>
-                                <id>sign-artifacts</id>
-                                <phase>verify</phase>
+                                <phase>package</phase>
+                                <id>attach-javadocs-javadoc</id>
                                 <goals>
-                                    <goal>sign</goal>
+                                    <goal>jar</goal>
                                 </goals>
                             </execution>
                         </executions>


Test patch:
diff --git a/javalin-graphql/src/test/kotlin/io/javalin/examples/HelloWorldGraphQL.kt b/javalin-graphql/src/test/kotlin/io/javalin/examples/HelloWorldGraphQL.kt
index 1b9d39c9..31c8e0ed 100644
--- a/javalin-graphql/src/test/kotlin/io/javalin/examples/HelloWorldGraphQL.kt
+++ b/javalin-graphql/src/test/kotlin/io/javalin/examples/HelloWorldGraphQL.kt
@@ -5,7 +5,7 @@
  */
 package io.javalin.examples
 
-import com.expediagroup.graphql.annotations.GraphQLDescription
+import com.expediagroup.graphql.generator.annotations.GraphQLDescription
 import io.javalin.Javalin
 import io.javalin.plugin.graphql.GraphQLOptions
 import io.javalin.plugin.graphql.GraphQLPlugin
diff --git a/javalin-graphql/src/test/kotlin/io/javalin/plugin/graphql/TestGraphQL.kt b/javalin-graphql/src/test/kotlin/io/javalin/plugin/graphql/TestGraphQL.kt
index 181e723d..a1b06338 100644
--- a/javalin-graphql/src/test/kotlin/io/javalin/plugin/graphql/TestGraphQL.kt
+++ b/javalin-graphql/src/test/kotlin/io/javalin/plugin/graphql/TestGraphQL.kt
@@ -2,14 +2,10 @@ package io.javalin.plugin.graphql
 
 import com.mashape.unirest.http.JsonNode
 import io.javalin.Javalin
-import io.javalin.plugin.graphql.helpers.ContextExample
-import io.javalin.plugin.graphql.helpers.MutationExample
-import io.javalin.plugin.graphql.helpers.QueryExample
-import io.javalin.plugin.graphql.helpers.SubscriptionExample
+import io.javalin.plugin.graphql.helpers.*
 import io.javalin.testing.HttpUtil
 import io.javalin.testing.TestUtil
-import junit.framework.Assert.assertEquals
-import junit.framework.Assert.assertFalse
+import junit.framework.Assert.*
 import org.assertj.core.api.Assertions.assertThat
 import org.java_websocket.client.WebSocketClient
 import org.java_websocket.drafts.Draft_6455
@@ -65,10 +61,15 @@ class TestGraphQL {
     }
 
     @Test
-    fun context() = TestUtil.test(shortTimeoutServer()) { server, httpUtil ->
-        val json = sendPetition(httpUtil, "{\"query\": \"{ context { hello, hi} }\"}")
-        assertEquals(json.getJSONObject("data").getJSONObject("context").getString("hello"), ContextExample().hello)
-        assertEquals(json.getJSONObject("data").getJSONObject("context").getString("hi"), ContextExample().hi)
+    fun contextWithoutAuthorized() = TestUtil.test(shortTimeoutServer()) { server, httpUtil ->
+        val json = sendPetition(httpUtil, "{\"query\": \"{ isAuthorized }\"}")
+        assertFalse(json.getJSONObject("data").getBoolean("isAuthorized"))
+    }
+
+    @Test
+    fun contextWithAuthorized() = TestUtil.test(shortTimeoutServer()) { server, httpUtil ->
+        val json = sendPetition(httpUtil, "Beare token","{\"query\": \"{ isAuthorized }\"}")
+        assertTrue(json.getJSONObject("data").getBoolean("isAuthorized"))
     }
 
     @Test
@@ -80,6 +81,31 @@ class TestGraphQL {
         assertThat(server.logger().log).containsAnyOf("{\"counter\":1}")
     }
 
+    @Test
+    fun subscribeWithoutContext() = TestUtil.test(shortTimeoutServer()) { server, httpUtil ->
+        val testClient1_1 = TestClient(server, graphqlPath)
+        doAndSleepWhile({ testClient1_1.connect() }, { !testClient1_1.isOpen })
+        doAndSleep { testClient1_1.send("{\"query\": \"subscription { counterUser }\"}") }
+        doAndSleepWhile({ testClient1_1.close() }, { testClient1_1.isClosing })
+        assertThat(server.logger().log).containsAnyOf("{\"counterUser\":\"${SubscriptionExample.anonymous_message} ~> 1\"}")
+    }
+
+    @Test
+    fun subscribeWithContext() = TestUtil.test(shortTimeoutServer()) { server, httpUtil ->
+        val testClient1_1 = TestClient(server, graphqlPath)
+        val tokenUser = "token"
+        testClient1_1.addHeader("Authorization", "Beare $tokenUser")
+        doAndSleepWhile({ testClient1_1.connect() }, { !testClient1_1.isOpen })
+        doAndSleep { testClient1_1.send("{\"query\": \"subscription { counterUser }\"}") }
+        doAndSleepWhile({ testClient1_1.close() }, { testClient1_1.isClosing })
+        assertThat(server.logger().log).containsAnyOf("{\"counterUser\":\"$tokenUser ~> 1\"}")
+    }
+
+    private fun sendPetition(httpUtil: HttpUtil, authorization: String, body: String): JSONObject {
+        val response = httpUtil.post(graphqlPath).header("Authorization", authorization).body(JsonNode(body)).asString()
+        return JSONObject(response.body)
+    }
+
     private fun sendPetition(httpUtil: HttpUtil, body: String): JSONObject {
         val response = httpUtil.post(graphqlPath).body(JsonNode(body)).asString()
         val json = JSONObject(response.body)
@@ -126,15 +152,16 @@ class TestGraphQL {
 
     private fun shortTimeoutServer(): Javalin {
         return Javalin.create {
-            val graphQLOption = GraphQLOptions(graphqlPath, ContextExample())
-                    .addPackage("io.javalin.plugin.graphql")
-                    .addPackage("io.javalin.plugin.graphql.helpers")
-                    .register(QueryExample(message))
-                    .register(MutationExample(message))
-                    .register(SubscriptionExample())
-            it.registerPlugin(GraphQLPlugin(graphQLOption))
+            val graphQLPluginBuilder = GraphQLPluginBuilder(graphqlPath, ContextFactoryExample(), ContextWsFactoryExample())
+                .add("io.javalin.plugin.graphql")
+                .add("io.javalin.plugin.graphql.helpers")
+                .register(QueryExample(message))
+                .register(MutationExample(message))
+                .register(SubscriptionExample())
+
+            it.registerPlugin(graphQLPluginBuilder.build())
         }
-                .after { ctx -> ctx.req.asyncContext.timeout = 10 }
+
     }
 
     private fun doAndSleep(func: () -> Unit) = func.invoke().also { Thread.sleep(1000) }
diff --git a/javalin-graphql/src/test/kotlin/io/javalin/plugin/graphql/TestGraphQLAdvanced.kt b/javalin-graphql/src/test/kotlin/io/javalin/plugin/graphql/TestGraphQLAdvanced.kt
deleted file mode 100644
index 0dced74d..00000000
--- a/javalin-graphql/src/test/kotlin/io/javalin/plugin/graphql/TestGraphQLAdvanced.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-package io.javalin.plugin.graphql
-
-import com.mashape.unirest.http.JsonNode
-import io.javalin.Javalin
-import io.javalin.plugin.graphql.helpers.ContextExample
-import io.javalin.plugin.graphql.helpers.MutationExample
-import io.javalin.plugin.graphql.helpers.QueryExample
-import io.javalin.plugin.graphql.helpers.SubscriptionExample
-import io.javalin.testing.TestUtil
-import junit.framework.Assert.assertEquals
-import org.json.JSONObject
-import org.junit.Test
-
-
-class TestGraphQLAdvanced {
-
-    private val graphqlPath = "/graphql"
-    private val message = "Hello World"
-
-
-    @Test
-    fun runMiddleWare() = TestUtil.test(shortTimeoutServer()) { server, httpUtil ->
-        val response = httpUtil.post(graphqlPath)
-                .header("test", message)
-                .body(JsonNode("{\"query\": \"{ context { hi} }\"}")).asString()
-        val json = JSONObject(response.body)
-        assertEquals(json.getJSONObject("data").getJSONObject("context").getString("hi"), message)
-    }
-
-    private fun shortTimeoutServer(): Javalin {
-        val context = ContextExample()
-        return Javalin.create {
-            val graphQLOption = GraphQLOptions(graphqlPath, context)
-                    .addPackage("io.javalin.plugin.graphql")
-                    .addPackage("io.javalin.plugin.graphql.helpers")
-                    .register(QueryExample(message))
-                    .register(MutationExample(message))
-                    .register(SubscriptionExample())
-                    .setMiddleHandler { ctx ->  context.updateHi(ctx.header("test")!!)}
-            it.registerPlugin(GraphQLPlugin(graphQLOption))
-        }
-                .after { ctx -> ctx.req.asyncContext.timeout = 10 }
-    }
-
-    private fun doAndSleep(func: () -> Unit) = func.invoke().also { Thread.sleep(1000) }
-
-}
diff --git a/javalin-graphql/src/test/kotlin/io/javalin/plugin/graphql/helpers/ContextExample.kt b/javalin-graphql/src/test/kotlin/io/javalin/plugin/graphql/helpers/ContextExample.kt
index 628a0bcd..e914b811 100644
--- a/javalin-graphql/src/test/kotlin/io/javalin/plugin/graphql/helpers/ContextExample.kt
+++ b/javalin-graphql/src/test/kotlin/io/javalin/plugin/graphql/helpers/ContextExample.kt
@@ -1,10 +1,7 @@
 package io.javalin.plugin.graphql.helpers
 
-class ContextExample {
-    var hi = "Hi"
-    val hello = "Hello World"
+import com.expediagroup.graphql.generator.execution.GraphQLContext
 
-    fun updateHi(text: String) {
-        this.hi = text
-    }
+data class ContextExample(val authorization: String? = null) : GraphQLContext {
+    val isValid = authorization != null
 }
diff --git a/javalin-graphql/src/test/kotlin/io/javalin/plugin/graphql/helpers/ContextFactoryExample.kt b/javalin-graphql/src/test/kotlin/io/javalin/plugin/graphql/helpers/ContextFactoryExample.kt
new file mode 100644
index 00000000..099b8d5c
--- /dev/null
+++ b/javalin-graphql/src/test/kotlin/io/javalin/plugin/graphql/helpers/ContextFactoryExample.kt
@@ -0,0 +1,10 @@
+package io.javalin.plugin.graphql.helpers
+
+import com.expediagroup.graphql.server.execution.GraphQLContextFactory
+import io.javalin.http.Context
+
+class ContextFactoryExample : GraphQLContextFactory<ContextExample, Context> {
+    override suspend fun generateContext(request: Context): ContextExample {
+        return ContextExample(request.header("Authorization")?.removePrefix("Beare "))
+    }
+}
diff --git a/javalin-graphql/src/test/kotlin/io/javalin/plugin/graphql/helpers/ContextWsFactoryExample.kt b/javalin-graphql/src/test/kotlin/io/javalin/plugin/graphql/helpers/ContextWsFactoryExample.kt
new file mode 100644
index 00000000..10a2f758
--- /dev/null
+++ b/javalin-graphql/src/test/kotlin/io/javalin/plugin/graphql/helpers/ContextWsFactoryExample.kt
@@ -0,0 +1,11 @@
+package io.javalin.plugin.graphql.helpers
+
+import com.expediagroup.graphql.server.execution.GraphQLContextFactory
+import io.javalin.http.Context
+import io.javalin.websocket.WsMessageContext
+
+class ContextWsFactoryExample : GraphQLContextFactory<ContextExample, WsMessageContext> {
+    override suspend fun generateContext(request: WsMessageContext): ContextExample {
+        return ContextExample(request.header("Authorization")?.removePrefix("Beare "))
+    }
+}
diff --git a/javalin-graphql/src/test/kotlin/io/javalin/plugin/graphql/helpers/QueryExample.kt b/javalin-graphql/src/test/kotlin/io/javalin/plugin/graphql/helpers/QueryExample.kt
index 19a9a222..f34ea1f8 100644
--- a/javalin-graphql/src/test/kotlin/io/javalin/plugin/graphql/helpers/QueryExample.kt
+++ b/javalin-graphql/src/test/kotlin/io/javalin/plugin/graphql/helpers/QueryExample.kt
@@ -1,6 +1,5 @@
 package io.javalin.plugin.graphql.helpers
 
-import com.expediagroup.graphql.annotations.GraphQLContext
 import io.javalin.plugin.graphql.graphql.QueryGraphql
 
 class QueryExample(val message: String) : QueryGraphql {
@@ -8,7 +7,7 @@ class QueryExample(val message: String) : QueryGraphql {
 
     fun echo(message: String): String = message
 
-    fun context(@GraphQLContext context: ContextExample): ContextExample {
-        return context
+    fun isAuthorized(context: ContextExample?): Boolean {
+        return context != null && context.isValid
     }
 }
diff --git a/javalin-graphql/src/test/kotlin/io/javalin/plugin/graphql/helpers/SubscriptionExample.kt b/javalin-graphql/src/test/kotlin/io/javalin/plugin/graphql/helpers/SubscriptionExample.kt
index b1af4c22..703792bc 100644
--- a/javalin-graphql/src/test/kotlin/io/javalin/plugin/graphql/helpers/SubscriptionExample.kt
+++ b/javalin-graphql/src/test/kotlin/io/javalin/plugin/graphql/helpers/SubscriptionExample.kt
@@ -5,5 +5,18 @@ import reactor.core.publisher.Flux
 import java.time.Duration
 
 class SubscriptionExample: SubscriptionGraphql {
+
+    companion object {
+        const val anonymous_message = "anonymus"
+    }
+
     fun counter(): Flux<Int> = Flux.interval(Duration.ofMillis(100)).map { 1 }
+
+    fun counterUser(contextExample: ContextExample?): Flux<String> {
+        val interval = Flux.interval(Duration.ofMillis(100))
+        return interval.map {
+            val user = if(contextExample != null && contextExample.isValid) contextExample.authorization!! else anonymous_message
+            "$user ~> 1"
+        }
+    }
 }
diff --git a/javalin-openapi/src/test/java/io/javalin/plugin/openapi/TestOpenApiIncludePath.kt b/javalin-openapi/src/test/java/io/javalin/plugin/openapi/TestOpenApiIncludePath.kt
index 62f5c2ac..3c833cd3 100644
--- a/javalin-openapi/src/test/java/io/javalin/plugin/openapi/TestOpenApiIncludePath.kt
+++ b/javalin-openapi/src/test/java/io/javalin/plugin/openapi/TestOpenApiIncludePath.kt
@@ -7,6 +7,7 @@ import io.swagger.v3.oas.models.OpenAPI
 import io.swagger.v3.oas.models.info.Info
 import org.assertj.core.api.Assertions.assertThat
 import org.junit.Test
+import java.util.*
 
 class TestOpenApiIncludePath {
     @Test
@@ -42,19 +43,19 @@ class TestOpenApiIncludePath {
         includedApp.get("path2", documented(path2Documentation()) { it -> it.result("path2") })
         includedApp.get("path3", documented(path3Documentation()) { it -> it.result("path2") })
 
-        val defaultSchema = JavalinOpenApi.createSchema(defaultApp).toString().toLowerCase()
-        val includedSchema = JavalinOpenApi.createSchema(includedApp).toString().toLowerCase()
+        val defaultSchema = JavalinOpenApi.createSchema(defaultApp).toString().lowercase(Locale.ROOT)
+        val includedSchema = JavalinOpenApi.createSchema(includedApp).toString().lowercase(Locale.ROOT)
 
         assertThat(defaultSchema).isNotEqualTo(includedSchema)
         //the default schema contains no inclusion criteria so should contain paths for path1, 2 and 3
-        assertThat(defaultSchema).contains("/path1=class PathItem".toLowerCase())
-        assertThat(defaultSchema).contains("/path2=class PathItem".toLowerCase())
-        assertThat(defaultSchema).contains("/path3=class PathItem".toLowerCase())
+        assertThat(defaultSchema).contains("/path1=class PathItem".lowercase(Locale.ROOT))
+        assertThat(defaultSchema).contains("/path2=class PathItem".lowercase(Locale.ROOT))
+        assertThat(defaultSchema).contains("/path3=class PathItem".lowercase(Locale.ROOT))
 
         //the included schema has include paths for path1 and path 2 so should only contain them and not path 3
-        assertThat(includedSchema).contains("/path1=class PathItem".toLowerCase())
-        assertThat(includedSchema).contains("/path2=class PathItem".toLowerCase())
-        assertThat(includedSchema).doesNotContain("/path3=class PathItem".toLowerCase())
+        assertThat(includedSchema).contains("/path1=class PathItem".lowercase(Locale.ROOT))
+        assertThat(includedSchema).contains("/path2=class PathItem".lowercase(Locale.ROOT))
+        assertThat(includedSchema).doesNotContain("/path3=class PathItem".lowercase(Locale.ROOT))
 
     }
 
diff --git a/javalin-testtools/pom.xml b/javalin-testtools/pom.xml
index eb7dbad3..3ba57ba7 100644
--- a/javalin-testtools/pom.xml
+++ b/javalin-testtools/pom.xml
@@ -10,6 +10,7 @@
     <artifactId>javalin-testtools</artifactId>
 
     <properties>
+        <dokka.skip>false</dokka.skip>
         <copy-build-resources.skip>false</copy-build-resources.skip>
     </properties>
 
diff --git a/javalin/src/test/java/io/javalin/TestApiBuilder.kt b/javalin/src/test/java/io/javalin/TestApiBuilder.kt
index 974c9503..99198097 100644
--- a/javalin/src/test/java/io/javalin/TestApiBuilder.kt
+++ b/javalin/src/test/java/io/javalin/TestApiBuilder.kt
@@ -194,7 +194,7 @@ class TestApiBuilder {
     }
 
     @Test
-    fun `CrudHandler rejects resource in the middle`() = TestUtil.test { app, http ->
+    fun `CrudHandler rejects resource in the middle`() = TestUtil.test { app, _ ->
         assertThatExceptionOfType(IllegalArgumentException::class.java).isThrownBy {
             app.routes {
                 crud("/foo/bar/{user-id}/users", UserController())
@@ -203,7 +203,7 @@ class TestApiBuilder {
     }
 
     @Test
-    fun `CrudHandler rejects missing resource`() = TestUtil.test { app, http ->
+    fun `CrudHandler rejects missing resource`() = TestUtil.test { app, _ ->
         assertThatExceptionOfType(IllegalArgumentException::class.java).isThrownBy {
             app.routes {
                 crud("/foo/bar/users", UserController())
@@ -212,7 +212,7 @@ class TestApiBuilder {
     }
 
     @Test
-    fun `CrudHandler rejects missing resource base`() = TestUtil.test { app, http ->
+    fun `CrudHandler rejects missing resource base`() = TestUtil.test { app, _ ->
         assertThatExceptionOfType(IllegalArgumentException::class.java).isThrownBy {
             app.routes {
                 crud("/{user-id}", UserController())
diff --git a/javalin/src/test/java/io/javalin/TestConfiguration.kt b/javalin/src/test/java/io/javalin/TestConfiguration.kt
index 926b8723..3f3b1eff 100644
--- a/javalin/src/test/java/io/javalin/TestConfiguration.kt
+++ b/javalin/src/test/java/io/javalin/TestConfiguration.kt
@@ -40,7 +40,7 @@ class TestConfiguration {
             it.enableWebjars()
             it.enforceSsl = true
             it.prefer405over404 = false
-            it.requestLogger { ctx, executionTimeMs -> }
+            it.requestLogger { _, _ -> }
             it.sessionHandler { SessionHandler() }
             // WsServlet
             it.wsFactoryConfig { }
@@ -51,7 +51,7 @@ class TestConfiguration {
             }
             it.registerPlugin(MicrometerPlugin())
             // Misc
-            it.accessManager { handler, ctx, roles -> }
+            it.accessManager { _, _, _ -> }
             it.showJavalinBanner = false
             it.configureServletContextHandler { handler ->
                 handler.addEventListener(object : HttpSessionListener {
@@ -83,7 +83,7 @@ class TestConfiguration {
     }
 
     @Test
-    fun `app throws exception saying port is busy if it is`() = TestUtil.test { app, http ->
+    fun `app throws exception saying port is busy if it is`() = TestUtil.test { app, _ ->
         Assertions.assertThatExceptionOfType(RuntimeException::class.java)
             .isThrownBy { Javalin.create().start(app.port()) }
             .withMessageContaining("Port already in use. Make sure no other process is using port ${app.port()} and try again")
diff --git a/javalin/src/test/java/io/javalin/TestCustomJetty.kt b/javalin/src/test/java/io/javalin/TestCustomJetty.kt
index e90dd900..43ad3641 100644
--- a/javalin/src/test/java/io/javalin/TestCustomJetty.kt
+++ b/javalin/src/test/java/io/javalin/TestCustomJetty.kt
@@ -226,7 +226,7 @@ class TestCustomJetty {
                 }), "/*", EnumSet.allOf(DispatcherType::class.java))
             }
         }
-        TestUtil.test(filterJavalin) { app, http ->
+        TestUtil.test(filterJavalin) { _, http ->
             assertThat(http.get("/test").status).isEqualTo(HttpCode.IM_A_TEAPOT.status)
             assertThat(http.get("/test").body).isNotEqualTo("Test")
         }
diff --git a/javalin/src/test/java/io/javalin/TestFuture.kt b/javalin/src/test/java/io/javalin/TestFuture.kt
index 560f0cff..81112bdb 100644
--- a/javalin/src/test/java/io/javalin/TestFuture.kt
+++ b/javalin/src/test/java/io/javalin/TestFuture.kt
@@ -65,7 +65,7 @@ class TestFuture {
 
     @Test
     fun `calling future in an exception-handler throws`() = TestUtil.test { app, http ->
-        app.get("/test-future") { ctx -> throw Exception() }
+        app.get("/test-future") { throw Exception() }
         app.exception(Exception::class.java) { _, ctx -> ctx.future(getFuture("Not result")) }
         assertThat(http.getBody("/test-future")).isEqualTo("")
         assertThat(http.get("/test-future").status).isEqualTo(500)
diff --git a/javalin/src/test/java/io/javalin/TestHttpResponseExceptions.kt b/javalin/src/test/java/io/javalin/TestHttpResponseExceptions.kt
index d21b77b7..b2cb74b9 100644
--- a/javalin/src/test/java/io/javalin/TestHttpResponseExceptions.kt
+++ b/javalin/src/test/java/io/javalin/TestHttpResponseExceptions.kt
@@ -117,7 +117,7 @@ class TestHttpResponseExceptions {
     @Test
     fun `throwing HttpResponseExceptions in future works`() = TestUtil.test { app, http ->
         fun getThrowingFuture() = CompletableFuture.supplyAsync {
-            if (true) {
+            if (Math.random() < 2) { // it's true!
                 throw UnauthorizedResponse()
             }
             "Result"
@@ -176,7 +176,7 @@ class TestHttpResponseExceptions {
     fun `can override HttpResponseExceptions`() = TestUtil.test { app, http ->
         val randomNumberString = (Math.random() * 10000).toString()
         app.get("/") { throw BadRequestResponse() }
-        app.exception(BadRequestResponse::class.java) { e, ctx -> ctx.result(randomNumberString) }
+        app.exception(BadRequestResponse::class.java) { _, ctx -> ctx.result(randomNumberString) }
         assertThat(http.getBody("/")).isEqualTo(randomNumberString)
     }
 
diff --git a/javalin/src/test/java/io/javalin/TestJavalinVue.kt b/javalin/src/test/java/io/javalin/TestJavalinVue.kt
index 3ecf5173..c3796328 100644
--- a/javalin/src/test/java/io/javalin/TestJavalinVue.kt
+++ b/javalin/src/test/java/io/javalin/TestJavalinVue.kt
@@ -34,7 +34,7 @@ class TestJavalinVue {
             with(JavalinVue) {
                 vueVersion { it.vue2() }
                 isDev = null // reset
-                stateFunction = { ctx -> mapOf<String, String>() } // reset
+                stateFunction = { mapOf<String, String>() } // reset
                 rootDirectory { it.externalPath("src/test/resources/vue") } // src/main ->
                 optimizeDependencies = false
             }
@@ -54,7 +54,7 @@ class TestJavalinVue {
     fun `vue component with state`() = TestUtil.test { app, http ->
         val encodedState =
             """{"pathParams":{"my-param":"test-path-param"},"queryParams":{"qp":["test-query-param"]},"state":{"user":{"name":"tipsy","email":"tipsy@tipsy.tipsy"},"role":{"name":"Maintainer"}}}""".uriEncodeForJavascript()
-        JavalinVue.stateFunction = { ctx -> state }
+        JavalinVue.stateFunction = { state }
         app.get("/vue/{my-param}", VueComponent("<test-component></test-component>"))
         val res = http.getBody("/vue/test-path-param?qp=test-query-param")
         assertThat(res).contains(encodedState)
@@ -92,7 +92,7 @@ class TestJavalinVue {
         JavalinVue.vueVersion { it.vue3("app") }
         val encodedState =
             """{"pathParams":{"my-param":"test-path-param"},"queryParams":{"qp":["test-query-param"]},"state":{"user":{"name":"tipsy","email":"tipsy@tipsy.tipsy"},"role":{"name":"Maintainer"}}}""".uriEncodeForJavascript()
-        JavalinVue.stateFunction = { ctx -> state }
+        JavalinVue.stateFunction = { state }
         app.get("/vue/{my-param}", VueComponent("<test-component-3></test-component-3>"))
         val res = http.getBody("/vue/test-path-param?qp=test-query-param")
         assertThat(res).contains(encodedState)
@@ -189,7 +189,7 @@ class TestJavalinVue {
     }
 
     @Test
-    fun `@cdnWebjar resolves to webjar on localhost`() = TestUtil.test { app, http ->
+    fun `@cdnWebjar resolves to webjar on localhost`() {
         val ctx = mockk<Context>(relaxed = true)
         every { ctx.jsonMapper() } returns JavalinJackson()
         JavalinVue.isDev = true // reset
@@ -200,9 +200,9 @@ class TestJavalinVue {
     }
 
     @Test
-    fun `@cdnWebjar resolves to cdn on non-localhost`() = TestUtil.test { app, http ->
+    fun `@cdnWebjar resolves to cdn on non-localhost`() {
         val ctx = mockk<Context>(relaxed = true)
-       every { ctx.jsonMapper() } returns JavalinJackson()
+        every { ctx.jsonMapper() } returns JavalinJackson()
         every { ctx.url() } returns "https://example.com"
         VueComponent("<test-component></test-component>").handle(ctx)
         val slot = slot<String>().also { verify { ctx.html(html = capture(it)) } }
@@ -210,9 +210,9 @@ class TestJavalinVue {
     }
 
     @Test
-    fun `@cdnWebjar resolves to https even on non https hosts`() = TestUtil.test { app, http ->
+    fun `@cdnWebjar resolves to https even on non https hosts`() {
         val ctx = mockk<Context>(relaxed = true)
-       every { ctx.jsonMapper() } returns JavalinJackson()
+        every { ctx.jsonMapper() } returns JavalinJackson()
         every { ctx.url() } returns "http://123.123.123.123:1234/"
         VueComponent("<test-component></test-component>").handle(ctx)
         val slot = slot<String>().also { verify { ctx.html(html = capture(it)) } }
@@ -220,9 +220,9 @@ class TestJavalinVue {
     }
 
     @Test
-    fun `@inlineFile functionality works as expected if not-dev`() = TestUtil.test { app, http ->
+    fun `@inlineFile functionality works as expected if not-dev`() {
         val ctx = mockk<Context>(relaxed = true)
-       every { ctx.jsonMapper() } returns JavalinJackson()
+        every { ctx.jsonMapper() } returns JavalinJackson()
         every { ctx.url() } returns "http://123.123.123.123:1234/"
         VueComponent("<test-component></test-component>").handle(ctx)
         val slot = slot<String>().also { verify { ctx.html(html = capture(it)) } }
@@ -234,9 +234,9 @@ class TestJavalinVue {
     }
 
     @Test
-    fun `@inlineFile functionality works as expected if dev`() = TestUtil.test { app, http ->
+    fun `@inlineFile functionality works as expected if dev`() {
         val ctx = mockk<Context>(relaxed = true)
-       every { ctx.jsonMapper() } returns JavalinJackson()
+        every { ctx.jsonMapper() } returns JavalinJackson()
         every { ctx.url() } returns "http://localhost:1234/"
         VueComponent("<test-component></test-component>").handle(ctx)
         val slot = slot<String>().also { verify { ctx.html(html = capture(it)) } }
diff --git a/javalin/src/test/java/io/javalin/TestLifecycleEvents.kt b/javalin/src/test/java/io/javalin/TestLifecycleEvents.kt
index d49eeeaa..6dad1b55 100644
--- a/javalin/src/test/java/io/javalin/TestLifecycleEvents.kt
+++ b/javalin/src/test/java/io/javalin/TestLifecycleEvents.kt
@@ -28,7 +28,7 @@ class TestLifecycleEvents {
     }
 
     @Test
-    fun `handlerAdded event works`() = TestUtil.test { app, http ->
+    fun `handlerAdded event works`() = TestUtil.test { app, _ ->
         var log = ""
         app.events { it.handlerAdded { handlerMetaInfo -> log += handlerMetaInfo.path } }
         app.events { it.handlerAdded { handlerMetaInfo -> log += handlerMetaInfo.path } }
@@ -37,7 +37,7 @@ class TestLifecycleEvents {
     }
 
     @Test
-    fun `wsHandlerAdded event works`() = TestUtil.test { app, http ->
+    fun `wsHandlerAdded event works`() = TestUtil.test { app, _ ->
         var log = ""
         app.events { it.wsHandlerAdded { handlerMetaInfo -> log += handlerMetaInfo.path } }
         app.events { it.wsHandlerAdded { handlerMetaInfo -> log += handlerMetaInfo.path } }
diff --git a/javalin/src/test/java/io/javalin/TestLogging.kt b/javalin/src/test/java/io/javalin/TestLogging.kt
index 2d8270b4..03a6aca8 100644
--- a/javalin/src/test/java/io/javalin/TestLogging.kt
+++ b/javalin/src/test/java/io/javalin/TestLogging.kt
@@ -59,7 +59,7 @@ class TestLogging {
     fun `resultString is available in request logger and can be read multiple times`() {
         val loggerLog = mutableListOf<String?>()
         val bodyLoggingJavalin = Javalin.create {
-            it.requestLogger { ctx, ms ->
+            it.requestLogger { ctx, _ ->
                 loggerLog.add(ctx.resultString())
                 loggerLog.add(ctx.resultString())
             }
diff --git a/javalin/src/test/java/io/javalin/TestMicrometerPlugin.kt b/javalin/src/test/java/io/javalin/TestMicrometerPlugin.kt
index 1ae182a8..c1802379 100644
--- a/javalin/src/test/java/io/javalin/TestMicrometerPlugin.kt
+++ b/javalin/src/test/java/io/javalin/TestMicrometerPlugin.kt
@@ -147,7 +147,7 @@ class TestMicrometerPlugin {
     }
 
     @Test
-    fun `not found`() = TestUtil.test(setupApp()) { app, http ->
+    fun `not found`() = TestUtil.test(setupApp()) { _, http ->
         val requestCount = (2..9).random()
         repeat(requestCount) { http.get("/some-unmapped-path") }
         val notFoundCount = meterRegistry.get("jetty.server.requests")
diff --git a/javalin/src/test/java/io/javalin/TestRateLimitUtil.kt b/javalin/src/test/java/io/javalin/TestRateLimitUtil.kt
index e063f80a..7d2c8a1e 100644
--- a/javalin/src/test/java/io/javalin/TestRateLimitUtil.kt
+++ b/javalin/src/test/java/io/javalin/TestRateLimitUtil.kt
@@ -23,14 +23,14 @@ class TestRateLimitUtil {
     }
 
     @Test
-    fun `rate limiting kicks in if number of requests exceeds rate limit`() = TestUtil.test(testApp) { app, http ->
+    fun `rate limiting kicks in if number of requests exceeds rate limit`() = TestUtil.test(testApp) { _, http ->
         repeat(50) { http.get("/") }
         assertThat(http.get("/").status).isEqualTo(429)
         assertThat(http.get("/").body).isEqualTo("Rate limit exceeded - Server allows 5 requests per hour.")
     }
 
     @Test
-    fun `both path and HTTP method must match for rate limiting to kick in`() = TestUtil.test(testApp) { app, http ->
+    fun `both path and HTTP method must match for rate limiting to kick in`() = TestUtil.test(testApp) { _, http ->
         repeat(50) { http.get("/") }
         assertThat(http.get("/").status).isEqualTo(429)
         assertThat(http.get("/test").status).isNotEqualTo(429)
diff --git a/javalin/src/test/java/io/javalin/TestRedirectToLowercasePathPlugin.kt b/javalin/src/test/java/io/javalin/TestRedirectToLowercasePathPlugin.kt
index 35a7453f..c686a003 100644
--- a/javalin/src/test/java/io/javalin/TestRedirectToLowercasePathPlugin.kt
+++ b/javalin/src/test/java/io/javalin/TestRedirectToLowercasePathPlugin.kt
@@ -21,14 +21,14 @@ class TestRedirectToLowercasePathPlugin {
     }
 
     @Test
-    fun `exception is thrown when using non-lowercase paths with TestRedirectToLowercasePathPlugin`() = TestUtil.test(testApp) { app, http ->
+    fun `exception is thrown when using non-lowercase paths with TestRedirectToLowercasePathPlugin`() = TestUtil.test(testApp) { app, _ ->
         assertThatIllegalArgumentException()
                 .isThrownBy { app.get("/TEST") { } }
                 .withMessage("Paths must be lowercase when using RedirectToLowercasePathPlugin")
     }
 
     @Test
-    fun `exception is NOT thrown when using uppercase path params with TestRedirectToLowercasePathPlugin`() = TestUtil.test(testApp) { app, http ->
+    fun `exception is NOT thrown when using uppercase path params with TestRedirectToLowercasePathPlugin`() = TestUtil.test(testApp) { app, _ ->
         app.get("/{TEST}") { }
     }
 
diff --git a/javalin/src/test/java/io/javalin/TestRequest.kt b/javalin/src/test/java/io/javalin/TestRequest.kt
index fc98a158..3844fe3a 100644
--- a/javalin/src/test/java/io/javalin/TestRequest.kt
+++ b/javalin/src/test/java/io/javalin/TestRequest.kt
@@ -254,7 +254,7 @@ class TestRequest {
             it.registerPlugin(BasicAuthFilter("u", "p"))
             it.addStaticFiles("/public", Location.CLASSPATH)
         }.get("/hellopath") { it.result("Hello") }
-        TestUtil.test(basicauthApp) { app, http ->
+        TestUtil.test(basicauthApp) { _, http ->
             assertThat(http.getBody("/hellopath")).isEqualTo("Unauthorized")
             assertThat(http.getBody("/html.html")).contains("Unauthorized")
             Unirest.get("${http.origin}/hellopath").basicAuth("u", "p").asString().let { assertThat(it.body).isEqualTo("Hello") }
diff --git a/javalin/src/test/java/io/javalin/TestValidation.kt b/javalin/src/test/java/io/javalin/TestValidation.kt
index 8338a9bc..a00f39f0 100644
--- a/javalin/src/test/java/io/javalin/TestValidation.kt
+++ b/javalin/src/test/java/io/javalin/TestValidation.kt
@@ -140,11 +140,11 @@ class TestValidation {
 
     @Test
     fun `default converters work`() {
-        assertThat(Validator.create(Boolean::class.java, "true", "?").get() is Boolean).isTrue()
-        assertThat(Validator.create(Double::class.java, "1.2", "?").get() is Double).isTrue()
-        assertThat(Validator.create(Float::class.java, "1.2", "?").get() is Float).isTrue()
-        assertThat(Validator.create(Int::class.java, "123", "?").get() is Int).isTrue()
-        assertThat(Validator.create(Long::class.java, "123", "?").get() is Long).isTrue()
+        assertThat(Validator.create(Boolean::class.java, "true", "?").get()).hasSameClassAs("true".toBoolean())
+        assertThat(Validator.create(Double::class.java, "1.2", "?").get()).hasSameClassAs("1.2".toDouble())
+        assertThat(Validator.create(Float::class.java, "1.2", "?").get()).hasSameClassAs("1.2".toFloat())
+        assertThat(Validator.create(Int::class.java, "123", "?").get()).hasSameClassAs("123".toInt())
+        assertThat(Validator.create(Long::class.java, "123", "?").get()).hasSameClassAs("123".toLong())
     }
 
     @Test
diff --git a/javalin/src/test/java/io/javalin/performance/RouteMatcherPerformanceTest.kt b/javalin/src/test/java/io/javalin/performance/RouteMatcherPerformanceTest.kt
index df454c35..3a46fab2 100644
--- a/javalin/src/test/java/io/javalin/performance/RouteMatcherPerformanceTest.kt
+++ b/javalin/src/test/java/io/javalin/performance/RouteMatcherPerformanceTest.kt
@@ -11,6 +11,8 @@ import io.javalin.http.util.ContextUtil.urlDecode
 import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
+import java.util.*
+import kotlin.collections.HashMap
 
 typealias NewHandlerEntry = HandlerEntry
 
@@ -56,7 +58,7 @@ class RouteMatcherPerformanceTest {
         while (i < requestPaths.size && i < handlerPaths.size) {
             val matchedPart = handlerPaths[i]
             if (matchedPart.startsWith(":")) {
-                params[matchedPart.toLowerCase()] = urlDecode(requestPaths[i])
+                params[matchedPart.lowercase(Locale.ROOT)] = urlDecode(requestPaths[i])
             }
             i++
         }
diff --git a/javalin/src/test/java/io/javalin/staticfiles/TestStaticFiles.kt b/javalin/src/test/java/io/javalin/staticfiles/TestStaticFiles.kt
index ce1f3b0b..938c33c3 100644
--- a/javalin/src/test/java/io/javalin/staticfiles/TestStaticFiles.kt
+++ b/javalin/src/test/java/io/javalin/staticfiles/TestStaticFiles.kt
@@ -258,6 +258,22 @@ class TestStaticFiles {
         }
     }
 
+    @Test
+    fun `only handle resources matching the filter`() {
+        TestUtil.test(Javalin.create { servlet ->
+            // use custom filter - only urls ending with ".css" are handled
+            servlet.addStaticFiles {
+                it.hostedPath = "/"
+                it.directory = "/public"
+                it.location = Location.CLASSPATH
+                it.skipFileFunction = { !it.requestURI.endsWith(".css") }
+            }
+        }) { _, http ->
+            assertThat(http.get("/styles.css").status).isEqualTo(200)
+            assertThat(http.get("/script.js").status).isEqualTo(404)
+        }
+    }
+
     @Test
     fun `logs handlers added on startup`() {
         val logOutput = TestLoggingUtil.captureStdOut { TestUtil.test(multiLocationStaticResourceApp) { _, _ -> } }