Repository: tipsy/javalin
Pull request #1325 (2021-07-23 12:07:05)

Add configuration option to filter the URLs served by the static file…
PR for issue #1168 


Patch:
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/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 b64dc27b..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)!! }
 
@@ -233,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
@@ -428,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..8a800de0 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,8 @@ package io.javalin.http.staticfiles
 
 import io.javalin.core.util.Header
 import org.eclipse.jetty.server.handler.ContextHandler.AliasCheck
+import java.util.function.Predicate
+import javax.servlet.http.HttpServletRequest
 
 data class StaticFileConfig(
     @JvmField var hostedPath: String = "/",
@@ -9,5 +11,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 filter: Predicate<HttpServletRequest> = Predicate { true },
 )
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..3addd49e 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) {
+        for (handler in handlers.filter { h -> h.canHandleRequest(httpRequest) }) {
             try {
                 val resource = handler.getResource(target)
                 if (resource.isFile() || resource.isDirectoryWithWelcomeFile(handler, target)) {
@@ -99,4 +99,8 @@ open class ConfigurableHandler(val config: StaticFileConfig) : ResourceHandler()
         return config.directory
     }
 
+    open fun canHandleRequest(httpRequest: HttpServletRequest): Boolean {
+        return config.filter.test(httpRequest)
+    }
+
 }
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) {


Test patch:
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/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..f3c8e2cc 100644
--- a/javalin/src/test/java/io/javalin/staticfiles/TestStaticFiles.kt
+++ b/javalin/src/test/java/io/javalin/staticfiles/TestStaticFiles.kt
@@ -24,6 +24,7 @@ import java.io.IOException
 import java.nio.file.Files
 import java.nio.file.Paths
 import java.util.*
+import java.util.function.Predicate
 import javax.servlet.DispatcherType
 import javax.servlet.Filter
 import javax.servlet.FilterChain
@@ -258,6 +259,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.filter = Predicate { req -> req.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) { _, _ -> } }