Repository: tipsy/javalin
Pull request #1005 (2020-06-13 16:35:02)

add common HTTP/2 dependencies to javalin-bundle
N.B. adding these simplifies coherent version in the future as these need to be synchronized to the underlying Jetty version used by Javalin.

End-users remain free to choose their own custom SSL implementation, for example, those based on the example by @tipsy found at [javalin-http2-example](https://github.com/tipsy/javalin-http2-example).

RalphSteinhagen: As an open-ended question: Should the existing logging dependency 
```xml
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
</dependency>
```
be replaced by:
```xml
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-simple</artifactId>
    <version>${slf4j.version}</version>
    <scope>test</scope>
</dependency>
```
SLF4J works without 'logback-classic'. This may serve two purposes of having a proper logging output for unit-tests as well as leaving the choice of logging back-end with the users.

Patch:
diff --git a/javalin-bundle/pom.xml b/javalin-bundle/pom.xml
index a7d9ab24..1ad9987a 100644
--- a/javalin-bundle/pom.xml
+++ b/javalin-bundle/pom.xml
@@ -30,6 +30,18 @@
             <artifactId>jackson-module-kotlin</artifactId>
         </dependency>
 
+        <!-- necessary for HTTP/2 and ALPN (application layer protocol negotiation) -->
+        <dependency>
+            <groupId>org.eclipse.jetty.http2</groupId>
+            <artifactId>http2-server</artifactId>
+            <version>${jetty.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.jetty</groupId>
+            <artifactId>jetty-alpn-conscrypt-server</artifactId>
+            <version>${jetty.version}</version>
+        </dependency>
+
         <dependency>
             <groupId>ch.qos.logback</groupId>
             <artifactId>logback-classic</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 5a54a222..9a61f9c5 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
@@ -24,6 +24,7 @@ class GraphQLHandler(val options: GraphQLOptions) {
 
     fun execute(ctx: Context) {
         val body = ctx.bodyAsClass(Map::class.java)
+        this.options.middleHandler(ctx)
         val result = this.genericExecute(body)
                 .execute()
                 .thenApplyAsync { JavalinJson.toJson(it) }
@@ -32,6 +33,7 @@ class GraphQLHandler(val options: GraphQLOptions) {
 
     fun execute(ctx: WsMessageContext) {
         val body = ctx.message(Map::class.java)
+        this.options.wsMiddleHandler(ctx)
         this.genericExecute(body)
                 .subscribe(SubscriberGraphQL(ctx))
     }
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 a3248f72..975a4216 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,15 +1,21 @@
 package io.javalin.plugin.graphql
 
 import com.expediagroup.graphql.TopLevelObject
+import io.javalin.http.Context
 import io.javalin.plugin.graphql.graphql.MutationGraphql
 import io.javalin.plugin.graphql.graphql.QueryGraphql
 import io.javalin.plugin.graphql.graphql.SubscriptionGraphql
+import io.javalin.websocket.WsMessageContext
+
 
 class GraphQLOptions(val path: String, val context: Any? = null) {
+    var middleHandler: (Context) -> Unit = {  }
+    var wsMiddleHandler: (WsMessageContext) -> Unit = {  }
+
     var queries: MutableList<TopLevelObject> = mutableListOf()
     var mutations: MutableList<TopLevelObject> = mutableListOf()
     var subscriptions: MutableList<TopLevelObject> = mutableListOf()
-    var packages: MutableList<String> = mutableListOf()
+    var packages: MutableList<String> = mutableListOf("kotlin.Unit")
 
     fun register(vararg queries: QueryGraphql) = apply {
         this.queries.addAll(queries.map { TopLevelObject(it) })
@@ -26,4 +32,14 @@ class GraphQLOptions(val path: String, val context: Any? = null) {
     fun addPackage(`package`: String) = apply {
         this.packages.add(`package`)
     }
+
+    fun setMiddleHandler(middleHandler: (Context) -> Unit) = apply {
+        this.middleHandler = middleHandler
+    }
+
+    fun setWSMiddleHandler(wsMiddleHandler: (WsMessageContext) -> Unit) = apply {
+        this.wsMiddleHandler = wsMiddleHandler
+    }
+
+
 }
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 cf50caf7..163a2754 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-without-jetty/pom.xml b/javalin-without-jetty/pom.xml
index c7005c33..3f5e776c 100644
--- a/javalin-without-jetty/pom.xml
+++ b/javalin-without-jetty/pom.xml
@@ -94,9 +94,10 @@
                                 <replace file="${javalinconfig.hack.path}" token="public JavalinConfig configureServletContextHandler(" value="private JavalinConfig configureServletContextHandler(" />
                                 <replace file="${javalinconfig.hack.path}" token="public boolean logIfServerNotStarted = true;" value="public boolean logIfServerNotStarted = false;" />
                                 <!-- Context.kt -->
-                                <copy file="${context.original.path}" tofile="${context.hack.path}" />
-                                <replace file="${context.hack.path}" token="fun uploadedFile(" value="private fun uploadedFile(" />
-                                <replace file="${context.hack.path}" token="fun uploadedFiles(" value="private fun uploadedFiles(" />
+                                <!-- This stopped working for some reason -->
+<!--                                <copy file="${context.original.path}" tofile="${context.hack.path}" />-->
+<!--                                <replace file="${context.hack.path}" token="fun uploadedFile(" value="private fun uploadedFile(" />-->
+<!--                                <replace file="${context.hack.path}" token="fun uploadedFiles(" value="private fun uploadedFiles(" />-->
                             </target>
                         </configuration><!-- @formatter:on -->
                         <goals>
diff --git a/javalin/src/main/java/io/javalin/http/SinglePageHandler.kt b/javalin/src/main/java/io/javalin/http/SinglePageHandler.kt
index 9c3fc76f..47c475f0 100644
--- a/javalin/src/main/java/io/javalin/http/SinglePageHandler.kt
+++ b/javalin/src/main/java/io/javalin/http/SinglePageHandler.kt
@@ -41,7 +41,7 @@ class SinglePageHandler {
 
     fun handle(ctx: Context): Boolean {
         val accepts = ctx.header(Header.ACCEPT) ?: ""
-        if (accepts.contains("text/html") || accepts == "*/*" || accepts == "") {
+        if (accepts.contains("text/html") || accepts.contains("*/*") || accepts == "") {
             for (path in pathPageMap.keys) {
                 if (ctx.path().startsWith(path)) {
                     ctx.html(when (ctx.isLocalhost()) {
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 c6bc3ca9..722d206a 100644
--- a/javalin/src/main/java/io/javalin/http/staticfiles/JettyResourceHandler.kt
+++ b/javalin/src/main/java/io/javalin/http/staticfiles/JettyResourceHandler.kt
@@ -44,6 +44,7 @@ class JettyResourceHandler(val precompressStaticFiles: Boolean = false) : io.jav
             val targetResource by lazy { path.removePrefix(urlPathPrefix) }
             return when {
                 urlPathPrefix == "/" -> super.getResource(path) // same as regular ResourceHandler
+                targetResource == "" -> super.getResource("/") // directory without trailing '/'
                 !path.startsWith(urlPathPrefix) -> EmptyResource.INSTANCE
                 !targetResource.startsWith("/") -> EmptyResource.INSTANCE
                 else -> super.getResource(targetResource)


Test patch:
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 09275254..daaee9fb 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
@@ -90,7 +90,7 @@ class TestGraphQL {
 
     private fun doAndSleepWhile(slowFunction: () -> Unit, conditionFunction: () -> Boolean, timeout: Duration = Duration.ofSeconds(5)) {
         val startTime = System.currentTimeMillis()
-        val limitTime = startTime + timeout.toMillis();
+        val limitTime = startTime + timeout.toMillis()
 
         slowFunction.invoke()
 
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
new file mode 100644
index 00000000..6afeb137
--- /dev/null
+++ b/javalin-graphql/src/test/kotlin/io/javalin/plugin/graphql/TestGraphQLAdvanced.kt
@@ -0,0 +1,55 @@
+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.HttpUtil
+import io.javalin.testing.TestUtil
+import junit.framework.Assert.assertEquals
+import org.assertj.core.api.Assertions.assertThat
+import org.java_websocket.client.WebSocketClient
+import org.java_websocket.drafts.Draft_6455
+import org.java_websocket.handshake.ServerHandshake
+import org.json.JSONObject
+import org.junit.Test
+import java.net.URI
+import java.time.Duration
+import java.util.concurrent.TimeoutException
+
+
+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 308f9be7..628a0bcd 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,6 +1,10 @@
 package io.javalin.plugin.graphql.helpers
 
 class ContextExample {
-    val hi = "Hi"
+    var hi = "Hi"
     val hello = "Hello World"
+
+    fun updateHi(text: String) {
+        this.hi = text
+    }
 }
diff --git a/javalin/src/test/java/io/javalin/TestSinglePageMode.kt b/javalin/src/test/java/io/javalin/TestSinglePageMode.kt
index fa9c00fb..700eb569 100644
--- a/javalin/src/test/java/io/javalin/TestSinglePageMode.kt
+++ b/javalin/src/test/java/io/javalin/TestSinglePageMode.kt
@@ -91,6 +91,13 @@ class TestSinglePageMode {
         assertThat(starSlashStarAcceptsResponseBody).contains("HTML works")
     }
 
+    @Test
+    fun `SinglePageHandler works when accepts contains star-slash-star`() = TestUtil.test(rootSinglePageApp_classPath) { app, http ->
+        val ieAcceptHeader = "image/jpeg, application/x-ms-application, image/gif, application/xaml+xml, image/pjpeg, application/x-ms-xbap, application/x-shockwave-flash, application/msword, */*"
+        val starSlashStarAcceptsResponseBody = Unirest.get("http://localhost:${app.port()}/not-a-file").header(Header.ACCEPT, ieAcceptHeader).asString().body
+        assertThat(starSlashStarAcceptsResponseBody).contains("HTML works")
+    }
+
     @Test
     fun `SinglePageHandler works for just subpaths - classpath`() = TestUtil.test(dualSinglePageApp_classPath) { _, http ->
         assertThat(http.htmlGet("/").body).contains("Not found")
diff --git a/javalin/src/test/java/io/javalin/TestStaticFiles.kt b/javalin/src/test/java/io/javalin/TestStaticFiles.kt
index 6079bbc6..180e39ae 100644
--- a/javalin/src/test/java/io/javalin/TestStaticFiles.kt
+++ b/javalin/src/test/java/io/javalin/TestStaticFiles.kt
@@ -101,6 +101,8 @@ class TestStaticFiles {
     fun `directory root return welcome file if there is a welcome file`() = TestUtil.test(defaultStaticResourceApp) { _, http ->
         assertThat(http.get("/subdir/").status).isEqualTo(200)
         assertThat(http.getBody("/subdir/")).isEqualTo("<h1>Welcome file</h1>")
+        assertThat(http.get("/subdir").status).isEqualTo(200)
+        assertThat(http.getBody("/subdir")).isEqualTo("<h1>Welcome file</h1>")
     }
 
     @Test