Repository: tipsy/javalin
Pull request #1234 (2021-05-01 10:46:11)

[static-files] Rework everything!


Patch:
diff --git a/javalin/src/main/java/io/javalin/core/JavalinConfig.java b/javalin/src/main/java/io/javalin/core/JavalinConfig.java
index b12d9afe..7b90c20c 100644
--- a/javalin/src/main/java/io/javalin/core/JavalinConfig.java
+++ b/javalin/src/main/java/io/javalin/core/JavalinConfig.java
@@ -18,6 +18,7 @@ import io.javalin.core.plugin.PluginNotFoundException;
 import io.javalin.core.security.AccessManager;
 import io.javalin.core.security.SecurityUtil;
 import io.javalin.core.util.CorsPlugin;
+import io.javalin.core.util.Header;
 import io.javalin.core.util.LogUtil;
 import io.javalin.http.Handler;
 import io.javalin.http.RequestLogger;
@@ -46,8 +47,6 @@ public class JavalinConfig {
     public boolean autogenerateEtags = false;
     public boolean prefer405over404 = false;
     public boolean enforceSsl = false;
-    public boolean precompressStaticFiles = false;
-    public ContextHandler.AliasCheck aliasCheckForStaticFiles = null;
     public boolean showJavalinBanner = true;
     public boolean logIfServerNotStarted = true;
     public boolean ignoreTrailingSlashes = true;
@@ -104,37 +103,42 @@ public class JavalinConfig {
     }
 
     public JavalinConfig enableWebjars() {
-        return addStaticFiles("/webjars", Location.CLASSPATH);
-    }
-
-    public JavalinConfig addStaticFiles(@NotNull String classpathPath) {
-        return addStaticFiles(classpathPath, Location.CLASSPATH);
+        return addStaticFiles(staticFiles -> {
+            staticFiles.directory = "META-INF/resources/webjars";
+            staticFiles.headers.put(Header.CACHE_CONTROL, "max-age=31622400");
+        });
     }
 
-    public JavalinConfig addStaticFiles(@NotNull String path, @NotNull Location location) {
-        return addStaticFiles("/", path, location);
+    public JavalinConfig addStaticFiles(@NotNull String directory, @NotNull Location location) {
+        return addStaticFiles(staticFiles -> {
+            staticFiles.directory = directory;
+            staticFiles.location = location;
+        });
     }
 
-    public JavalinConfig addStaticFiles(@NotNull String urlPathPrefix, @NotNull String path, @NotNull Location location) {
+    public JavalinConfig addStaticFiles(@NotNull Consumer<StaticFileConfig> userConfig) {
         JettyUtil.disableJettyLogger();
-        if (inner.resourceHandler == null)
-            inner.resourceHandler = new JettyResourceHandler(precompressStaticFiles, aliasCheckForStaticFiles);
-        inner.resourceHandler.addStaticFileConfig(new StaticFileConfig(urlPathPrefix, path, location));
+        if (inner.resourceHandler == null) {
+            inner.resourceHandler = new JettyResourceHandler();
+        }
+        StaticFileConfig finalConfig = new StaticFileConfig();
+        userConfig.accept(finalConfig);
+        inner.resourceHandler.addStaticFileConfig(finalConfig);
         return this;
     }
 
-    public JavalinConfig addSinglePageRoot(@NotNull String path, @NotNull String filePath) {
-        addSinglePageRoot(path, filePath, Location.CLASSPATH);
+    public JavalinConfig addSinglePageRoot(@NotNull String hostedPath, @NotNull String filePath) {
+        addSinglePageRoot(hostedPath, filePath, Location.CLASSPATH);
         return this;
     }
 
-    public JavalinConfig addSinglePageRoot(@NotNull String path, @NotNull String filePath, @NotNull Location location) {
-        inner.singlePageHandler.add(path, filePath, location);
+    public JavalinConfig addSinglePageRoot(@NotNull String hostedPath, @NotNull String filePath, @NotNull Location location) {
+        inner.singlePageHandler.add(hostedPath, filePath, location);
         return this;
     }
 
-    public JavalinConfig addSinglePageHandler(@NotNull String path, @NotNull Handler customHandler) {
-        inner.singlePageHandler.add(path, customHandler);
+    public JavalinConfig addSinglePageHandler(@NotNull String hostedPath, @NotNull Handler customHandler) {
+        inner.singlePageHandler.add(hostedPath, customHandler);
         return this;
     }
 
diff --git a/javalin/src/main/java/io/javalin/http/SinglePageHandler.kt b/javalin/src/main/java/io/javalin/http/SinglePageHandler.kt
index 47c475f0..c8341c40 100644
--- a/javalin/src/main/java/io/javalin/http/SinglePageHandler.kt
+++ b/javalin/src/main/java/io/javalin/http/SinglePageHandler.kt
@@ -27,16 +27,16 @@ class SinglePageHandler {
     private val pathPageMap = mutableMapOf<String, String>()
     private val pathCustomHandlerMap = mutableMapOf<String, Handler>()
 
-    fun add(path: String, filePath: String, location: Location) {
-        pathUrlMap[path] = when (location) {
+    fun add(hostedPath: String, filePath: String, location: Location) {
+        pathUrlMap[hostedPath] = when (location) {
             Location.CLASSPATH -> Util.getResourceUrl(filePath.removePrefix("/")) ?: throw IllegalArgumentException("File at '$filePath' not found. Path should be relative to resource folder.")
             Location.EXTERNAL -> Util.getFileUrl(filePath) ?: throw IllegalArgumentException("External file at '$filePath' not found.")
         }
-        pathPageMap[path] = pathUrlMap[path]!!.readText()
+        pathPageMap[hostedPath] = pathUrlMap[hostedPath]!!.readText()
     }
 
-    fun add(path: String, customHandler: Handler) {
-        pathCustomHandlerMap[path] = customHandler
+    fun add(hostedPath: String, customHandler: Handler) {
+        pathCustomHandlerMap[hostedPath] = customHandler
     }
 
     fun handle(ctx: Context): Boolean {
@@ -51,8 +51,8 @@ class SinglePageHandler {
                     return true
                 }
             }
-            for ((path, customHandler) in pathCustomHandlerMap) {
-                if (ctx.path().startsWith(path)) {
+            for ((hostedPath, customHandler) in pathCustomHandlerMap) {
+                if (ctx.path().startsWith(hostedPath)) {
                     customHandler.handle(ctx)
                     return true
                 }
diff --git a/javalin/src/main/java/io/javalin/http/staticfiles/JettyResourceHandler.kt b/javalin/src/main/java/io/javalin/http/staticfiles/JettyResourceHandler.kt
index 73eeb17a..a017c2bc 100644
--- a/javalin/src/main/java/io/javalin/http/staticfiles/JettyResourceHandler.kt
+++ b/javalin/src/main/java/io/javalin/http/staticfiles/JettyResourceHandler.kt
@@ -7,11 +7,9 @@
 package io.javalin.http.staticfiles
 
 import io.javalin.Javalin
-import io.javalin.core.util.Header
 import io.javalin.core.util.Util
 import io.javalin.http.JavalinResponseWrapper
 import org.eclipse.jetty.server.Request
-import org.eclipse.jetty.server.handler.ContextHandler.AliasCheck
 import org.eclipse.jetty.server.handler.ResourceHandler
 import org.eclipse.jetty.util.URIUtil
 import org.eclipse.jetty.util.resource.EmptyResource
@@ -22,16 +20,12 @@ import javax.servlet.http.HttpServletRequest
 import javax.servlet.http.HttpServletResponse
 import io.javalin.http.staticfiles.ResourceHandler as JavalinResourceHandler
 
-class JettyResourceHandler(val precompressStaticFiles: Boolean = false, private val aliasCheck: AliasCheck? = null) : JavalinResourceHandler {
+class JettyResourceHandler : JavalinResourceHandler {
 
-    val handlers = mutableListOf<ResourceHandler>()
+    val handlers = mutableListOf<ConfigurableHandler>()
 
     override fun addStaticFileConfig(config: StaticFileConfig) {
-        handlers.add(when {
-            config.path == "/webjars" -> WebjarHandler()
-            aliasCheck != null -> AliasHandler(config, aliasCheck)
-            else -> PrefixableHandler(config)
-        }.apply { start() })
+        handlers.add(ConfigurableHandler(config).apply { start() })
     }
 
     override fun handle(httpRequest: HttpServletRequest, httpResponse: HttpServletResponse): Boolean {
@@ -41,14 +35,11 @@ class JettyResourceHandler(val precompressStaticFiles: Boolean = false, private
             try {
                 val resource = handler.getResource(target)
                 if (resource.isFile() || resource.isDirectoryWithWelcomeFile(handler, target)) {
-                    val maxAge = if (target.startsWith("/immutable/") || handler is WebjarHandler) 31622400 else 0
-                    httpResponse.setHeader(Header.CACHE_CONTROL, "max-age=$maxAge")
-                    // Remove the default content type because Jetty will not set the correct one
-                    // if the HTTP response already has a content type set
-                    if (precompressStaticFiles && PrecompressingResourceHandler.handle(resource, httpRequest, httpResponse)) {
+                    handler.config.headers.forEach { httpResponse.setHeader(it.key, it.value) }
+                    if (handler.config.precompress && PrecompressingResourceHandler.handle(resource, httpRequest, httpResponse)) {
                         return true
                     }
-                    httpResponse.contentType = null
+                    httpResponse.contentType = null // Jetty will only set the content-type if it's null
                     handler.handle(target, baseRequest, httpRequest, httpResponse)
                     httpRequest.setAttribute("handled-as-static-file", true)
                     (httpResponse as JavalinResponseWrapper).outputStream.finalize()
@@ -69,52 +60,41 @@ class JettyResourceHandler(val precompressStaticFiles: Boolean = false, private
             this != null && this.isDirectory && handler.getResource("${target.removeSuffix("/")}/index.html")?.exists() == true
 }
 
-private class WebjarHandler : ResourceHandler() {
-    override fun getResource(path: String) = Resource.newClassPathResource("META-INF/resources$path") ?: super.getResource(path)
-}
-
-private open class PrefixableHandler(private val config: StaticFileConfig) : ResourceHandler() {
+open class ConfigurableHandler(val config: StaticFileConfig) : ResourceHandler() {
 
     init {
         resourceBase = getResourceBase(config)
         isDirAllowed = false
         isEtags = true
         Javalin.log?.info("""Static file handler added:
-        |    {urlPathPrefix: "${config.urlPathPrefix}", path: "${config.path}", location: Location.${config.location}}
+        |    {hostedPath: "${config.hostedPath}", directory: "${config.directory}", location: Location.${config.location}}
         |    Resolved path: '${getResourceBase(config)}'
         """.trimMargin())
     }
 
+    override fun getResource(path: String): Resource {
+        val aliasResource by lazy { baseResource!!.addPath(URIUtil.canonicalPath(path)) }
+        return when {
+            config.directory == "META-INF/resources/webjars" ->
+                Resource.newClassPathResource("META-INF/resources$path")
+            config.aliasCheck != null && aliasResource.isAlias ->
+                if (config.aliasCheck.check(path, aliasResource)) aliasResource else throw AccessDeniedException("Failed alias check")
+            config.hostedPath == "/" -> super.getResource(path) // same as regular ResourceHandler
+            path.startsWith(config.hostedPath) -> super.getResource(path.removePrefix(config.hostedPath))
+            else -> EmptyResource.INSTANCE // files that don't start with hostedPath should not be accessible
+        }
+    }
+
     private fun getResourceBase(config: StaticFileConfig): String {
-        val noSuchDirMessage = "Static resource directory with path: '${config.path}' does not exist."
+        val noSuchDirMessage = "Static resource directory with path: '${config.directory}' does not exist."
         val classpathHint = "Depending on your setup, empty folders might not get copied to classpath."
         if (config.location == Location.CLASSPATH) {
-            return Resource.newClassPathResource(config.path)?.toString() ?: throw RuntimeException("$noSuchDirMessage $classpathHint")
+            return Resource.newClassPathResource(config.directory)?.toString() ?: throw RuntimeException("$noSuchDirMessage $classpathHint")
         }
-        if (!File(config.path).exists()) {
+        if (!File(config.directory).exists()) {
             throw RuntimeException(noSuchDirMessage)
         }
-        return config.path
+        return config.directory
     }
 
-    override fun getResource(path: String): Resource {
-        val targetResource by lazy { path.removePrefix(config.urlPathPrefix) }
-        return when {
-            config.urlPathPrefix == "/" -> super.getResource(path)!! // same as regular ResourceHandler
-            targetResource == "" -> super.getResource("/")!! // directory without trailing '/'
-            !path.startsWith(config.urlPathPrefix) -> EmptyResource.INSTANCE
-            !targetResource.startsWith("/") -> EmptyResource.INSTANCE
-            else -> super.getResource(targetResource)!!
-        }
-    }
-
-}
-
-private class AliasHandler(config: StaticFileConfig, private val aliasCheck: AliasCheck) : PrefixableHandler(config) {
-    override fun getResource(path: String): Resource {  // if this method throws, we get a 404
-        val resource = baseResource?.addPath(URIUtil.canonicalPath(path))!!
-        if (!resource.isAlias) return super.getResource(path) // treat as prefixablehandler
-        if (!aliasCheck.check(path, resource)) throw AccessDeniedException("Failed alias check")
-        return resource // passed check, return the resource
-    }
 }
diff --git a/javalin/src/main/java/io/javalin/http/staticfiles/ResourceHandler.kt b/javalin/src/main/java/io/javalin/http/staticfiles/ResourceHandler.kt
index 7ee29acf..cf7c1ee6 100644
--- a/javalin/src/main/java/io/javalin/http/staticfiles/ResourceHandler.kt
+++ b/javalin/src/main/java/io/javalin/http/staticfiles/ResourceHandler.kt
@@ -1,10 +1,10 @@
 package io.javalin.http.staticfiles
 
+import org.eclipse.jetty.server.handler.ContextHandler
 import javax.servlet.http.HttpServletRequest
 import javax.servlet.http.HttpServletResponse
 
 enum class Location { CLASSPATH, EXTERNAL; }
-data class StaticFileConfig(val urlPathPrefix: String, val path: String, val location: Location)
 
 interface ResourceHandler {
     fun handle(httpRequest: HttpServletRequest, httpResponse: HttpServletResponse): Boolean
diff --git a/javalin/src/main/java/io/javalin/http/staticfiles/StaticFileConfig.java b/javalin/src/main/java/io/javalin/http/staticfiles/StaticFileConfig.java
new file mode 100644
index 00000000..472ec51f
--- /dev/null
+++ b/javalin/src/main/java/io/javalin/http/staticfiles/StaticFileConfig.java
@@ -0,0 +1,19 @@
+package io.javalin.http.staticfiles;
+
+import io.javalin.core.util.Header;
+import java.util.HashMap;
+import java.util.Map;
+import org.eclipse.jetty.server.handler.ContextHandler;
+import org.jetbrains.annotations.NotNull;
+import static java.util.Collections.singletonMap;
+
+public class StaticFileConfig {
+    // @formatter:off
+    public @NotNull String hostedPath = "/";
+    public @NotNull String directory = "/public";
+    public @NotNull Location location = Location.CLASSPATH;
+    public boolean precompress = false;
+    public ContextHandler.AliasCheck aliasCheck = null;
+    public Map<String, String> headers = new HashMap<>(singletonMap(Header.CACHE_CONTROL, "max-age=0"));
+    // @formatter:on
+}


Test patch:
diff --git a/javalin/src/test/java/io/javalin/TestCompression.kt b/javalin/src/test/java/io/javalin/TestCompression.kt
index 4f447b47..2079085e 100644
--- a/javalin/src/test/java/io/javalin/TestCompression.kt
+++ b/javalin/src/test/java/io/javalin/TestCompression.kt
@@ -15,6 +15,7 @@ import io.javalin.core.util.FileUtil
 import io.javalin.core.util.Header
 import io.javalin.core.util.OptionalDependency
 import io.javalin.http.OutputStreamWrapper
+import io.javalin.http.staticfiles.Location
 import io.javalin.testing.TestUtil
 import okhttp3.Headers
 import okhttp3.OkHttpClient
@@ -42,20 +43,20 @@ class TestCompression {
     val fullCompressionApp by lazy {
         Javalin.create {
             it.compressionStrategy(Brotli(), Gzip())
-            it.addStaticFiles("/public")
+            it.addStaticFiles("/public", Location.CLASSPATH)
         }.addTestEndpoints()
     }
 
     val brotliDisabledApp by lazy {
         Javalin.create {
             it.compressionStrategy(null, Gzip())
-            it.addStaticFiles("/public")
+            it.addStaticFiles("/public", Location.CLASSPATH)
         }.addTestEndpoints()
     }
 
     val etagApp by lazy {
         Javalin.create {
-            it.addStaticFiles("/public")
+            it.addStaticFiles("/public", Location.CLASSPATH)
             it.autogenerateEtags = true
         }.addTestEndpoints()
     }
@@ -113,7 +114,7 @@ class TestCompression {
     fun `doesn't gzip when gzip is disabled`() {
         val gzipDisabledApp = Javalin.create {
             it.compressionStrategy(Brotli(), null)
-            it.addStaticFiles("/public")
+            it.addStaticFiles("/public", Location.CLASSPATH)
         }.addTestEndpoints()
         TestUtil.test(gzipDisabledApp) { _, http ->
             assertThat(getResponse(http.origin, "/huge", "gzip").headers().get(Header.CONTENT_ENCODING)).isNull()
@@ -152,7 +153,7 @@ class TestCompression {
     @Test
     fun `does gzip when CompressionStrategy not set`() {
         val defaultApp = Javalin.create {
-            it.addStaticFiles("/public")
+            it.addStaticFiles("/public", Location.CLASSPATH)
         }.addTestEndpoints()
         TestUtil.test(defaultApp) { _, http ->
             assertThat(getResponse(http.origin, "/huge", "br, gzip").headers().get(Header.CONTENT_ENCODING)).isEqualTo("gzip")
@@ -258,7 +259,7 @@ class TestCompression {
     fun `doesn't compress media files`() {
         val mediaTestApp = Javalin.create {
             it.compressionStrategy(null, Gzip())
-            it.addStaticFiles("/upload-test")
+            it.addStaticFiles("/upload-test", Location.CLASSPATH)
         }
         TestUtil.test(mediaTestApp) { _, http ->
             assertUncompressedResponse(http.origin, "/image.png")
@@ -270,7 +271,7 @@ class TestCompression {
     fun `doesn't compress pre-compressed files`() {
         val preCompressedTestApp = Javalin.create {
             it.compressionStrategy(null, Gzip())
-            it.addStaticFiles("/public")
+            it.addStaticFiles("/public", Location.CLASSPATH)
             it.enableWebjars()
         }
         TestUtil.test(preCompressedTestApp) { _, http ->
diff --git a/javalin/src/test/java/io/javalin/TestConfiguration.kt b/javalin/src/test/java/io/javalin/TestConfiguration.kt
index 90b63fb0..164bc302 100644
--- a/javalin/src/test/java/io/javalin/TestConfiguration.kt
+++ b/javalin/src/test/java/io/javalin/TestConfiguration.kt
@@ -27,7 +27,7 @@ class TestConfiguration {
             // JavalinServlet
             it.addSinglePageRoot("/", "/public/html.html")
             it.addSinglePageRoot("/", "src/test/resources/public/html.html", Location.EXTERNAL)
-            it.addStaticFiles("/public")
+            it.addStaticFiles("/public", Location.CLASSPATH)
             it.addStaticFiles("src/test/resources/public", Location.EXTERNAL)
             it.asyncRequestTimeout = 10_000L
             it.autogenerateEtags = true
diff --git a/javalin/src/test/java/io/javalin/TestContextPath.kt b/javalin/src/test/java/io/javalin/TestContextPath.kt
index 76d8e6e2..1a1c2121 100644
--- a/javalin/src/test/java/io/javalin/TestContextPath.kt
+++ b/javalin/src/test/java/io/javalin/TestContextPath.kt
@@ -7,6 +7,7 @@
 package io.javalin
 
 import io.javalin.core.util.Util
+import io.javalin.http.staticfiles.Location
 import io.javalin.testing.TestUtil
 import org.assertj.core.api.Assertions.assertThat
 import org.junit.Test
@@ -61,7 +62,7 @@ class TestContextPath {
     @Test
     fun `static-files work with context-path`() {
         val javalin = Javalin.create { servlet ->
-            servlet.addStaticFiles("/public")
+            servlet.addStaticFiles("/public", Location.CLASSPATH)
             servlet.contextPath = "/context-path"
         }
         TestUtil.test(javalin) { _, http ->
@@ -73,7 +74,7 @@ class TestContextPath {
     @Test
     fun `welcome-files work with context-path`() {
         val javalin = Javalin.create { servlet ->
-            servlet.addStaticFiles("/public")
+            servlet.addStaticFiles("/public", Location.CLASSPATH)
             servlet.contextPath = "/context-path"
         }
         TestUtil.test(javalin) { _, http ->
diff --git a/javalin/src/test/java/io/javalin/TestFuture.kt b/javalin/src/test/java/io/javalin/TestFuture.kt
index 3405d3ca..a6a93aa2 100644
--- a/javalin/src/test/java/io/javalin/TestFuture.kt
+++ b/javalin/src/test/java/io/javalin/TestFuture.kt
@@ -7,7 +7,6 @@ import org.assertj.core.api.Assertions.fail
 import org.junit.Test
 import java.io.IOException
 import java.io.InputStream
-import java.net.SocketTimeoutException
 import java.util.concurrent.CancellationException
 import java.util.concurrent.CompletableFuture
 import java.util.concurrent.Executors
diff --git a/javalin/src/test/java/io/javalin/TestJavalinVue.kt b/javalin/src/test/java/io/javalin/TestJavalinVue.kt
index 4c122fbb..9170dba0 100644
--- a/javalin/src/test/java/io/javalin/TestJavalinVue.kt
+++ b/javalin/src/test/java/io/javalin/TestJavalinVue.kt
@@ -9,8 +9,8 @@ package io.javalin
 import io.javalin.http.Context
 import io.javalin.http.staticfiles.Location
 import io.javalin.plugin.rendering.vue.JavalinVue
-import io.javalin.plugin.rendering.vue.VueVersion
 import io.javalin.plugin.rendering.vue.VueComponent
+import io.javalin.plugin.rendering.vue.VueVersion
 import io.javalin.testing.TestUtil
 import io.mockk.every
 import io.mockk.mockk
@@ -47,7 +47,7 @@ class TestJavalinVue {
     private val state = State(User("tipsy", "tipsy@tipsy.tipsy"), Role("Maintainer"))
 
     private fun String.uriEncodeForJavascript() =
-        URLEncoder.encode(this, Charsets.UTF_8.name()).replace("+","%20")
+            URLEncoder.encode(this, Charsets.UTF_8.name()).replace("+", "%20")
 
     @Test
     fun `vue component with state`() = TestUtil.test { app, http ->
@@ -83,7 +83,7 @@ class TestJavalinVue {
         assertThat(res).contains(encodedEmptyState)
         assertThat(res).contains("<body><test-component-3></test-component-3></body>")
         assertThat(res).contains("app.config.globalProperties.\$javalin")
-}
+    }
 
 
     @Test
diff --git a/javalin/src/test/java/io/javalin/TestJavalinVueResolution.java b/javalin/src/test/java/io/javalin/TestJavalinVueResolution.java
index b6bbb248..6d934af1 100644
--- a/javalin/src/test/java/io/javalin/TestJavalinVueResolution.java
+++ b/javalin/src/test/java/io/javalin/TestJavalinVueResolution.java
@@ -15,7 +15,6 @@
  */
 package io.javalin;
 
-import io.javalin.http.staticfiles.Location;
 import io.javalin.plugin.rendering.vue.JavalinVue;
 import io.javalin.plugin.rendering.vue.VueComponent;
 import io.javalin.plugin.rendering.vue.VueVersion;
diff --git a/javalin/src/test/java/io/javalin/TestRequest.kt b/javalin/src/test/java/io/javalin/TestRequest.kt
index d9d311a1..00f2cd5d 100644
--- a/javalin/src/test/java/io/javalin/TestRequest.kt
+++ b/javalin/src/test/java/io/javalin/TestRequest.kt
@@ -10,6 +10,7 @@ import com.mashape.unirest.http.Unirest
 import io.javalin.core.security.BasicAuthFilter
 import io.javalin.core.util.Header
 import io.javalin.http.context.header
+import io.javalin.http.staticfiles.Location
 import io.javalin.testing.TestUtil
 import org.assertj.core.api.Assertions.assertThat
 import org.junit.Test
@@ -249,7 +250,7 @@ class TestRequest {
     fun `basic auth filter plugin works`() {
         val basicauthApp = Javalin.create {
             it.registerPlugin(BasicAuthFilter("u", "p"))
-            it.addStaticFiles("/public")
+            it.addStaticFiles("/public", Location.CLASSPATH)
         }.get("/hellopath") { it.result("Hello") }
         TestUtil.test(basicauthApp) { app, http ->
             assertThat(http.getBody("/hellopath")).isEqualTo("Unauthorized")
diff --git a/javalin/src/test/java/io/javalin/examples/HelloWorldStaticFiles.java b/javalin/src/test/java/io/javalin/examples/HelloWorldStaticFiles.java
index 060b2b3e..5ff2c701 100644
--- a/javalin/src/test/java/io/javalin/examples/HelloWorldStaticFiles.java
+++ b/javalin/src/test/java/io/javalin/examples/HelloWorldStaticFiles.java
@@ -8,12 +8,13 @@
 package io.javalin.examples;
 
 import io.javalin.Javalin;
+import io.javalin.http.staticfiles.Location;
 
 public class HelloWorldStaticFiles {
 
     public static void main(String[] args) {
         Javalin.create(config -> {
-            config.addStaticFiles("/public");
+            config.addStaticFiles("/public", Location.CLASSPATH);
         }).start(7070);
     }
 
diff --git a/javalin/src/test/java/io/javalin/examples/HelloWorldStaticFiles_linked.java b/javalin/src/test/java/io/javalin/examples/HelloWorldStaticFiles_linked.java
index 8f56dc8f..59638681 100644
--- a/javalin/src/test/java/io/javalin/examples/HelloWorldStaticFiles_linked.java
+++ b/javalin/src/test/java/io/javalin/examples/HelloWorldStaticFiles_linked.java
@@ -19,9 +19,12 @@ public class HelloWorldStaticFiles_linked {
     public static void main(String[] args) {
         createSymLink("src/test/external/html.html", "src/test/external/linked_html.html");
 
-        Javalin.create(config -> {
-            config.aliasCheckForStaticFiles = new ContextHandler.ApproveAliases();
-            config.addStaticFiles("src/test/external/", Location.EXTERNAL);
+        Javalin.create(javalin -> {
+            javalin.addStaticFiles(staticFiles -> {
+                staticFiles.directory = "src/test/external/";
+                staticFiles.location = Location.EXTERNAL;
+                staticFiles.aliasCheck = new ContextHandler.ApproveAliases();
+            });
         }).start(7070);
     }
 
diff --git a/javalin/src/test/java/io/javalin/TestSinglePageMode.kt b/javalin/src/test/java/io/javalin/staticfiles/TestSinglePageMode.kt
similarity index 97%
rename from javalin/src/test/java/io/javalin/TestSinglePageMode.kt
rename to javalin/src/test/java/io/javalin/staticfiles/TestSinglePageMode.kt
index 700eb569..d377f330 100644
--- a/javalin/src/test/java/io/javalin/TestSinglePageMode.kt
+++ b/javalin/src/test/java/io/javalin/staticfiles/TestSinglePageMode.kt
@@ -4,9 +4,10 @@
  * Licensed under Apache 2.0: https://github.com/tipsy/javalin/blob/master/LICENSE
  */
 
-package io.javalin
+package io.javalin.staticfiles
 
 import com.mashape.unirest.http.Unirest
+import io.javalin.Javalin
 import io.javalin.core.util.Header
 import io.javalin.core.util.OptionalDependency
 import io.javalin.http.Context
@@ -21,7 +22,7 @@ class TestSinglePageMode {
 
     private val rootSinglePageApp_classPath: Javalin by lazy {
         Javalin.create {
-            it.addStaticFiles("/public")
+            it.addStaticFiles("/public", Location.CLASSPATH)
             it.addSinglePageRoot("/", "/public/html.html")
             it.enableWebjars()
         }
@@ -29,7 +30,7 @@ class TestSinglePageMode {
 
     private val dualSinglePageApp_classPath: Javalin by lazy {
         Javalin.create {
-            it.addStaticFiles("/public")
+            it.addStaticFiles("/public", Location.CLASSPATH)
             it.addSinglePageRoot("/admin", "/public/protected/secret.html")
             it.addSinglePageRoot("/public", "/public/html.html")
         }
@@ -52,7 +53,7 @@ class TestSinglePageMode {
 
     private val mixedSinglePageHandlerApp: Javalin by lazy {
         Javalin.create {
-            it.addStaticFiles("/public")
+            it.addStaticFiles("/public", Location.CLASSPATH)
             it.addSinglePageRoot("/public", "/public/html.html")
             it.addSinglePageHandler("/public") { ctx: Context -> ctx.result("Will never be seen") }
             it.addSinglePageHandler("/special") { ctx: Context ->
diff --git a/javalin/src/test/java/io/javalin/TestStaticDirectorySlash.kt b/javalin/src/test/java/io/javalin/staticfiles/TestStaticDirectorySlash.kt
similarity index 86%
rename from javalin/src/test/java/io/javalin/TestStaticDirectorySlash.kt
rename to javalin/src/test/java/io/javalin/staticfiles/TestStaticDirectorySlash.kt
index cded09c0..211210d5 100644
--- a/javalin/src/test/java/io/javalin/TestStaticDirectorySlash.kt
+++ b/javalin/src/test/java/io/javalin/staticfiles/TestStaticDirectorySlash.kt
@@ -6,15 +6,17 @@
  * @author: Plasmoxy
  */
 
-package io.javalin
+package io.javalin.staticfiles
 
+import io.javalin.Javalin
+import io.javalin.http.staticfiles.Location
 import io.javalin.testing.TestUtil
 import org.assertj.core.api.Assertions.assertThat
 import org.junit.Test
 
 class TestStaticDirectorySlash {
 
-    private val normalJavalin: Javalin by lazy { Javalin.create { it.addStaticFiles("public") } }
+    private val normalJavalin: Javalin by lazy { Javalin.create { it.addStaticFiles("public", Location.CLASSPATH) } }
 
     @Test
     fun `normal javalin ignores static directory slashes`() = TestUtil.test(normalJavalin) { _, http ->
diff --git a/javalin/src/test/java/io/javalin/TestStaticFiles.kt b/javalin/src/test/java/io/javalin/staticfiles/TestStaticFiles.kt
similarity index 86%
rename from javalin/src/test/java/io/javalin/TestStaticFiles.kt
rename to javalin/src/test/java/io/javalin/staticfiles/TestStaticFiles.kt
index 986c6971..ce1f3b0b 100644
--- a/javalin/src/test/java/io/javalin/TestStaticFiles.kt
+++ b/javalin/src/test/java/io/javalin/staticfiles/TestStaticFiles.kt
@@ -5,8 +5,9 @@
  *
  */
 
-package io.javalin
+package io.javalin.staticfiles
 
+import io.javalin.Javalin
 import io.javalin.core.util.Header
 import io.javalin.core.util.OptionalDependency
 import io.javalin.http.UnauthorizedResponse
@@ -32,20 +33,21 @@ import javax.servlet.ServletResponse
 
 class TestStaticFiles {
 
-    private val defaultStaticResourceApp: Javalin by lazy { Javalin.create { it.addStaticFiles("/public") } } // classpath
+    private val defaultStaticResourceApp: Javalin by lazy { Javalin.create { it.addStaticFiles("/public", Location.CLASSPATH) } }
     private val externalStaticResourceApp: Javalin by lazy { Javalin.create { it.addStaticFiles("src/test/external/", Location.EXTERNAL) } }
     private val multiLocationStaticResourceApp: Javalin by lazy {
         Javalin.create {
             it.addStaticFiles("src/test/external/", Location.EXTERNAL)
-            it.addStaticFiles("/public/immutable")
-            it.addStaticFiles("/public/protected")
-            it.addStaticFiles("/public/subdir")
+            it.addStaticFiles("/public/immutable", Location.CLASSPATH)
+            it.addStaticFiles("/public/protected", Location.CLASSPATH)
+            it.addStaticFiles("/public/subdir", Location.CLASSPATH)
         }
     }
+    private val customHeaderApp: Javalin by lazy { Javalin.create { it.addStaticFiles { it.headers = mapOf(Header.CACHE_CONTROL to "max-age=31622400") } } }
     private val devLoggingApp: Javalin by lazy {
         Javalin.create {
-            it.addStaticFiles("/public")
             it.enableDevLogging()
+            it.addStaticFiles("/public", Location.CLASSPATH)
         }
     }
 
@@ -62,7 +64,7 @@ class TestStaticFiles {
                 override fun destroy() {
                 }
             }
-            it.addStaticFiles("/public")
+            it.addStaticFiles("/public", Location.CLASSPATH)
             it.configureServletContextHandler { handler ->
                 handler.addFilter(FilterHolder(filter), "/*", EnumSet.allOf(DispatcherType::class.java))
             }
@@ -87,9 +89,16 @@ class TestStaticFiles {
     fun `alias checks for static files should work`() {
         val staticWithAliasResourceApp = Javalin.create {
             // block aliases for txt files
-            it.aliasCheckForStaticFiles = ContextHandler.AliasCheck { path, resource -> !path.endsWith(".txt") }
-            it.addStaticFiles("src/test/external/", Location.EXTERNAL)
-            it.addStaticFiles("/url-prefix", "/public", Location.CLASSPATH)
+            val aliasCheck = ContextHandler.AliasCheck { path, resource -> !path.endsWith(".txt") }
+            it.addStaticFiles {
+                it.aliasCheck = aliasCheck
+                it.directory = "src/test/external/"
+                it.location = Location.EXTERNAL
+            }
+            it.addStaticFiles {
+                it.hostedPath = "/url-prefix"
+                it.aliasCheck = aliasCheck
+            }
         }
 
         val createdHtml = createSymLink("src/test/external/html.html", "src/test/external/linked_html.html")
@@ -169,7 +178,7 @@ class TestStaticFiles {
     }
 
     @Test
-    fun `expires is set to 1 year for files in immutable directory`() = TestUtil.test(defaultStaticResourceApp) { _, http ->
+    fun `can set custom headers`() = TestUtil.test(customHeaderApp) { _, http ->
         assertThat(http.get("/immutable/library-1.0.0.min.js").headers.getFirst(Header.CACHE_CONTROL)).isEqualTo("max-age=31622400")
     }
 
@@ -219,9 +228,13 @@ class TestStaticFiles {
 
     @Test
     fun `serving from custom url path works`() {
-        TestUtil.test(Javalin.create { servlet ->
-            servlet.addStaticFiles("/public")
-            servlet.addStaticFiles("/url-prefix", "/public", Location.CLASSPATH)
+        TestUtil.test(Javalin.create { javalin ->
+            javalin.addStaticFiles("/public", Location.CLASSPATH)
+            javalin.addStaticFiles { staticFiles ->
+                staticFiles.hostedPath = "/url-prefix"
+                staticFiles.directory = "/public"
+                staticFiles.location = Location.CLASSPATH
+            }
         }) { _, http ->
             assertThat(http.get("/styles.css").status).isEqualTo(200)
             assertThat(http.get("/url-prefix/styles.css").status).isEqualTo(200)
@@ -233,7 +246,11 @@ class TestStaticFiles {
         TestUtil.test(Javalin.create { servlet ->
             // effectively equivalent to servlet.addStaticFiles("/public", Location.CLASSPATH)
             // but with benefit of additional "filtering": only requests matching /assets/* will be matched against static resources handler
-            servlet.addStaticFiles("/assets", "/public/assets", Location.CLASSPATH)
+            servlet.addStaticFiles {
+                it.hostedPath = "/assets"
+                it.directory = "/public/assets"
+                it.location = Location.CLASSPATH
+            }
         }) { _, http ->
             assertThat(http.get("/assets/filtered-styles.css").status).isEqualTo(200) // access to urls matching /assets/* is allowed
             assertThat(http.get("/filtered-styles.css").status).isEqualTo(404) // direct access to a file in the subfolder is not allowed
diff --git a/javalin/src/test/java/io/javalin/TestStaticFilesEdgeCases.kt b/javalin/src/test/java/io/javalin/staticfiles/TestStaticFilesEdgeCases.kt
similarity index 95%
rename from javalin/src/test/java/io/javalin/TestStaticFilesEdgeCases.kt
rename to javalin/src/test/java/io/javalin/staticfiles/TestStaticFilesEdgeCases.kt
index c23a99fc..150f3532 100644
--- a/javalin/src/test/java/io/javalin/TestStaticFilesEdgeCases.kt
+++ b/javalin/src/test/java/io/javalin/staticfiles/TestStaticFilesEdgeCases.kt
@@ -4,8 +4,9 @@
  * Licensed under Apache 2.0: https://github.com/tipsy/javalin/blob/master/LICENSE
  */
 
-package io.javalin
+package io.javalin.staticfiles
 
+import io.javalin.Javalin
 import io.javalin.core.util.FileUtil
 import io.javalin.http.staticfiles.Location
 import org.assertj.core.api.Assertions.assertThat
@@ -18,7 +19,7 @@ class TestStaticFilesEdgeCases {
     @Test
     fun `server doesn't start for non-existent classpath folder`() {
         assertThatExceptionOfType(RuntimeException::class.java)
-                .isThrownBy { Javalin.create { it.addStaticFiles("classpath-fake-folder") }.start() }
+                .isThrownBy { Javalin.create { it.addStaticFiles("classpath-fake-folder", Location.CLASSPATH) }.start() }
                 .withMessageStartingWith("Static resource directory with path: 'classpath-fake-folder' does not exist.")
     }
 
diff --git a/javalin/src/test/java/io/javalin/TestStaticFilesPrecompressor.kt b/javalin/src/test/java/io/javalin/staticfiles/TestStaticFilesPrecompressor.kt
similarity index 82%
rename from javalin/src/test/java/io/javalin/TestStaticFilesPrecompressor.kt
rename to javalin/src/test/java/io/javalin/staticfiles/TestStaticFilesPrecompressor.kt
index 2068a986..8f046b16 100644
--- a/javalin/src/test/java/io/javalin/TestStaticFilesPrecompressor.kt
+++ b/javalin/src/test/java/io/javalin/staticfiles/TestStaticFilesPrecompressor.kt
@@ -4,8 +4,9 @@
  * Licensed under Apache 2.0: https://github.com/tipsy/javalin/blob/master/LICENSE
  *
  */
-package io.javalin
+package io.javalin.staticfiles
 
+import io.javalin.Javalin
 import io.javalin.core.compression.Brotli
 import io.javalin.core.compression.Gzip
 import io.javalin.core.util.Header
@@ -24,12 +25,20 @@ class TestStaticFilesPrecompressor {
     private val swaggerBasePath = "/webjars/swagger-ui/${OptionalDependency.SWAGGERUI.version}"
 
     private val configPrecompressionStaticResourceApp: Javalin by lazy {
-        Javalin.create { config ->
-            config.precompressStaticFiles = true
-            config.compressionStrategy(Brotli(), Gzip())
-            config.enableWebjars()
-            config.addStaticFiles("/public/immutable")
-            config.addStaticFiles("/public/protected")
+        Javalin.create { javalin ->
+            javalin.compressionStrategy(Brotli(), Gzip())
+            javalin.addStaticFiles { staticFiles ->
+                staticFiles.directory = "META-INF/resources/webjars"
+                staticFiles.precompress = true
+            }
+            javalin.addStaticFiles { staticFiles ->
+                staticFiles.directory = "/public/immutable"
+                staticFiles.precompress = true
+            }
+            javalin.addStaticFiles { staticFiles ->
+                staticFiles.directory = "/public/protected"
+                staticFiles.precompress = true
+            }
         }
     }
 
diff --git a/javalin/src/test/kotlin/io/javalin/examples/AdvancedServerSentEvent.kt b/javalin/src/test/kotlin/io/javalin/examples/AdvancedServerSentEvent.kt
index 66606701..b95d412d 100644
--- a/javalin/src/test/kotlin/io/javalin/examples/AdvancedServerSentEvent.kt
+++ b/javalin/src/test/kotlin/io/javalin/examples/AdvancedServerSentEvent.kt
@@ -8,6 +8,7 @@ package io.javalin.examples
 
 import io.javalin.Javalin
 import io.javalin.http.sse.SseClient
+import io.javalin.http.staticfiles.Location
 import org.eclipse.jetty.server.Server
 import org.eclipse.jetty.util.thread.QueuedThreadPool
 import java.util.concurrent.ConcurrentLinkedQueue
@@ -20,7 +21,7 @@ fun main() {
     val statsClients = ConcurrentLinkedQueue<SseClient>()
 
     Javalin.create {
-        it.addStaticFiles("/public")
+        it.addStaticFiles("/public", Location.CLASSPATH)
         it.server { Server(tp) }
     }.apply {
         get("/") { it.redirect("/sse/sse-example.html") }
diff --git a/javalin/src/test/kotlin/io/javalin/examples/HelloWorldStaticFiles.kt b/javalin/src/test/kotlin/io/javalin/examples/HelloWorldStaticFiles.kt
index 313b4d2a..2579cee7 100644
--- a/javalin/src/test/kotlin/io/javalin/examples/HelloWorldStaticFiles.kt
+++ b/javalin/src/test/kotlin/io/javalin/examples/HelloWorldStaticFiles.kt
@@ -7,8 +7,9 @@
 package io.javalin.examples
 
 import io.javalin.Javalin
+import io.javalin.http.staticfiles.Location
 
 fun main() {
-    Javalin.create { it.addStaticFiles("/public") }.start(7070)
+    Javalin.create { it.addStaticFiles("/public", Location.CLASSPATH) }.start(7070)
 }