Repository: tipsy/javalin
Pull request #1207 (2021-04-05 07:13:55)

Add support for kotlin templates
jte allows template expression in Kotlin instead of Java since [version 1.8.0](https://github.com/casid/jte/releases/tag/1.8.0), which would be a nice fit for Javalin.

See this issue for more details: https://github.com/tipsy/javalin/issues/1206


casid: @tipsy would you like to have a look? I'm unsure, but I think the optional `jte-kotlin` dependency shouldn't do any harm to users that don't have it in their own pom.

Also, currently there is no nice error message if `jte-kotlin` isn't on the classpath and a `.kte` file is rendered. This is because I think most users will strip `jte-kotlin` from their production builds, since it contains a dependency to the kotlin embedded compiler. For precompiled templates, the compiler isn't needed. Is there some sort of devMode boolean in Javalin?

If so, we could add this to `OptionalDependency.kt`:

```kotlin
JTE_KOTLIN("jte-kotlin", "gg.jte.compiler.kotlin.KotlinClassCompiler", "gg.jte", "jte-kotlin", "1.9.0"),
```
And check this in `JavalinJte#render` only when in dev mode:

```kotlin
if (devMode && filePath.endsWith(".kte")) {
    Util.ensureDependencyPresent(OptionalDependency.JTE)
}
```


tipsy: > I think the optional jte-kotlin dependency shouldn't do any harm

Optional dependencies shouldn't do any harm, they're essentially ignored.

> Is there some sort of devMode boolean in Javalin?

No, but there is a `ctx.isLocalhost()` that I use for something similar in `JavalinVue`: https://github.com/tipsy/javalin/blob/master/javalin/src/main/java/io/javalin/plugin/rendering/vue/JavalinVue.kt#L21-L22

casid: Oh nice, thanks! I've pushed the dev mode check as you suggested.
tipsy: Nice, thank you !

Patch:
diff --git a/javalin/pom.xml b/javalin/pom.xml
index 1f6ba6b5..57adf060 100644
--- a/javalin/pom.xml
+++ b/javalin/pom.xml
@@ -86,6 +86,11 @@
             <artifactId>jte</artifactId>
             <optional>true</optional>
         </dependency>
+        <dependency>
+            <groupId>gg.jte</groupId>
+            <artifactId>jte-kotlin</artifactId>
+            <optional>true</optional>
+        </dependency>
         <dependency>
             <groupId>org.commonmark</groupId>
             <artifactId>commonmark</artifactId>
diff --git a/javalin/src/main/java/io/javalin/core/util/OptionalDependency.kt b/javalin/src/main/java/io/javalin/core/util/OptionalDependency.kt
index c9e0d567..1decbfd5 100644
--- a/javalin/src/main/java/io/javalin/core/util/OptionalDependency.kt
+++ b/javalin/src/main/java/io/javalin/core/util/OptionalDependency.kt
@@ -26,5 +26,6 @@ enum class OptionalDependency(val displayName: String, val testClass: String, va
     SWAGGER_CORE("Swagger Core", "io.swagger.v3.oas.models.OpenAPI", "io.swagger.core.v3", "swagger-models", "2.1.6"),
     CLASS_GRAPH("ClassGraph", "io.github.classgraph.ClassGraph", "io.github.classgraph", "classgraph", "4.8.102"),
     JVMBROTLI("Jvm-Brotli", "com.nixxcode.jvmbrotli.common.BrotliLoader", "com.nixxcode.jvmbrotli", "jvmbrotli", "0.2.0"),
-    JTE("jte", "gg.jte.TemplateEngine", "gg.jte", "jte", "1.6.0"),
+    JTE("jte", "gg.jte.TemplateEngine", "gg.jte", "jte", "1.9.0"),
+    JTE_KOTLIN("jte-kotlin", "gg.jte.compiler.kotlin.KotlinClassCompiler", "gg.jte", "jte-kotlin", "1.9.0"),
 }
diff --git a/javalin/src/main/java/io/javalin/plugin/rendering/JavalinRenderer.kt b/javalin/src/main/java/io/javalin/plugin/rendering/JavalinRenderer.kt
index 06a5d819..3c2853da 100644
--- a/javalin/src/main/java/io/javalin/plugin/rendering/JavalinRenderer.kt
+++ b/javalin/src/main/java/io/javalin/plugin/rendering/JavalinRenderer.kt
@@ -29,7 +29,7 @@ object JavalinRenderer {
         register(JavalinPebble, ".peb", ".pebble")
         register(JavalinThymeleaf, ".html", ".tl", ".thyme", ".thymeleaf")
         register(JavalinCommonmark, ".md", ".markdown")
-        register(JavalinJte, ".jte")
+        register(JavalinJte, ".jte", ".kte")
     }
 
     @JvmField
diff --git a/javalin/src/main/java/io/javalin/plugin/rendering/template/JavalinJte.kt b/javalin/src/main/java/io/javalin/plugin/rendering/template/JavalinJte.kt
index 98152ab2..813c4035 100644
--- a/javalin/src/main/java/io/javalin/plugin/rendering/template/JavalinJte.kt
+++ b/javalin/src/main/java/io/javalin/plugin/rendering/template/JavalinJte.kt
@@ -13,11 +13,15 @@ import gg.jte.resolve.DirectoryCodeResolver
 import io.javalin.core.util.OptionalDependency
 import io.javalin.core.util.Util
 import io.javalin.http.Context
+import io.javalin.http.util.ContextUtil.isLocalhost
 import io.javalin.plugin.rendering.FileRenderer
 import java.io.File
 
 object JavalinJte : FileRenderer {
 
+    internal var isDev: Boolean? = null // cached and easily accessible, is set on first request (can't be configured directly by end user)
+    @JvmField var isDevFunction: (Context) -> Boolean = { it.isLocalhost() } // used to set isDev, will be called once
+
     private var templateEngine: TemplateEngine? = null
     private val defaultTemplateEngine: TemplateEngine by lazy { defaultJteEngine() }
 
@@ -29,6 +33,11 @@ object JavalinJte : FileRenderer {
     override fun render(filePath: String, model: Map<String, Any?>, ctx: Context): String {
         Util.ensureDependencyPresent(OptionalDependency.JTE)
 
+        isDev = isDev ?: isDevFunction(ctx)
+        if (isDev == true && filePath.endsWith(".kte")) {
+            Util.ensureDependencyPresent(OptionalDependency.JTE_KOTLIN)
+        }
+
         val stringOutput = StringOutput()
         (templateEngine ?: defaultTemplateEngine).render(filePath, model, stringOutput)
         return stringOutput.toString()
diff --git a/pom.xml b/pom.xml
index 6d396c6d..aa965042 100644
--- a/pom.xml
+++ b/pom.xml
@@ -81,7 +81,7 @@
         <swagger.ui.version>3.43.0</swagger.ui.version>
         <thymeleaf.version>3.0.12.RELEASE</thymeleaf.version>
         <velocity.version>2.2</velocity.version>
-        <jte.version>1.6.0</jte.version>
+        <jte.version>1.9.0</jte.version>
 
         <!-- TEST DEPENDENCIES -->
         <assertj.version>3.15.0</assertj.version>
@@ -206,6 +206,11 @@
                 <artifactId>jte</artifactId>
                 <version>${jte.version}</version>
             </dependency>
+            <dependency>
+                <groupId>gg.jte</groupId>
+                <artifactId>jte-kotlin</artifactId>
+                <version>${jte.version}</version>
+            </dependency>
             <dependency>
                 <groupId>org.commonmark</groupId>
                 <artifactId>commonmark</artifactId>


Test patch:
diff --git a/javalin/src/test/java/io/javalin/TestTemplates.kt b/javalin/src/test/java/io/javalin/TestTemplates.kt
index 9451e1d9..ca4c3c5b 100644
--- a/javalin/src/test/java/io/javalin/TestTemplates.kt
+++ b/javalin/src/test/java/io/javalin/TestTemplates.kt
@@ -12,7 +12,6 @@ import com.mitchellbosecke.pebble.loader.ClasspathLoader
 import gg.jte.ContentType
 import gg.jte.TemplateEngine
 import gg.jte.resolve.ResourceCodeResolver
-import io.javalin.plugin.rendering.FileRenderer
 import io.javalin.plugin.rendering.JavalinRenderer
 import io.javalin.plugin.rendering.template.JavalinJte
 import io.javalin.plugin.rendering.template.JavalinJtwig
@@ -146,24 +145,30 @@ class TestTemplates {
 
     @Test
     fun `jte works`() = TestUtil.test { app, http ->
-        try {
-            JavalinJte.configure(TemplateEngine.create(ResourceCodeResolver("templates/jte"), File("target/jte").toPath(), ContentType.Html))
-            app.get("/hello") { ctx -> ctx.render("test.jte", model("page", JteTestPage("hello", "world"))) }
-            assertThat(http.getBody("/hello")).isEqualToIgnoringNewLines("<h1>hello world!</h1>")
-        } catch (e: UnsupportedClassVersionError) {
-            // jte does require JDK 11+ to work
-        }
+        JavalinJte.configure(TemplateEngine.create(ResourceCodeResolver("templates/jte"), File("target/jte").toPath(), ContentType.Html))
+        app.get("/hello") { ctx -> ctx.render("test.jte", model("page", JteTestPage("hello", "world"))) }
+        assertThat(http.getBody("/hello")).isEqualToIgnoringNewLines("<h1>hello world!</h1>")
     }
 
     @Test
     fun `jte multiple params work`() = TestUtil.test { app, http ->
-        try {
-            JavalinJte.configure(TemplateEngine.create(ResourceCodeResolver("templates/jte"), File("target/jte").toPath(), ContentType.Html))
-            app.get("/hello") { ctx -> ctx.render("multiple-params.jte", model("one", "hello", "two", "world")) }
-            assertThat(http.getBody("/hello")).isEqualToIgnoringNewLines("<h1>hello world!</h1>")
-        } catch (e: UnsupportedClassVersionError) {
-            // jte does require JDK 11+ to work
-        }
+        JavalinJte.configure(TemplateEngine.create(ResourceCodeResolver("templates/jte"), File("target/jte").toPath(), ContentType.Html))
+        app.get("/hello") { ctx -> ctx.render("multiple-params.jte", model("one", "hello", "two", "world")) }
+        assertThat(http.getBody("/hello")).isEqualToIgnoringNewLines("<h1>hello world!</h1>")
+    }
+
+    @Test
+    fun `jte kotlin works`() = TestUtil.test { app, http ->
+        JavalinJte.configure(TemplateEngine.create(ResourceCodeResolver("templates/jte"), File("target/jte").toPath(), ContentType.Html))
+        app.get("/hello") { ctx -> ctx.render("test.kte", model("page", JteTestPage("hello", "world"))) }
+        assertThat(http.getBody("/hello")).isEqualToIgnoringNewLines("<h1>hello world!</h1>")
+    }
+
+    @Test
+    fun `jte kotlin multiple params work`() = TestUtil.test { app, http ->
+        JavalinJte.configure(TemplateEngine.create(ResourceCodeResolver("templates/jte"), File("target/jte").toPath(), ContentType.Html))
+        app.get("/hello") { ctx -> ctx.render("multiple-params.kte", model("one", "hello", "two", "world")) }
+        assertThat(http.getBody("/hello")).isEqualToIgnoringNewLines("<h1>hello world!</h1>")
     }
 
     @Test
@@ -180,7 +185,7 @@ class TestTemplates {
 
     @Test
     fun `registering custom renderer works`() = TestUtil.test { app, http ->
-        JavalinRenderer.register(FileRenderer { _, _, _ -> "Hah." }, ".lol")
+        JavalinRenderer.register({ _, _, _ -> "Hah." }, ".lol")
         app.get("/hello") { ctx -> ctx.render("/markdown/test.lol") }
         assertThat(http.getBody("/hello")).isEqualTo("Hah.")
     }
@@ -199,14 +204,14 @@ class TestTemplates {
 
     @Test
     fun `base Model works`() = TestUtil.test { app, http ->
-        JavalinRenderer.baseModelFunction = { ctx -> defaultBaseModel + mapOf("queryParams" to ctx.queryParamMap(), "pathParams" to ctx.pathParamMap()) };
+        JavalinRenderer.baseModelFunction = { ctx -> defaultBaseModel + mapOf("queryParams" to ctx.queryParamMap(), "pathParams" to ctx.pathParamMap()) }
         app.get("/hello/:pp") { ctx -> ctx.render("/templates/freemarker/test-with-base.ftl", model("message", "Hello Freemarker!")) }
         assertThat(http.getBody("/hello/world?im=good")).isEqualTo("<h1>good</h1><h2>world</h2><h3>bar</h3>")
     }
 
     @Test
     fun `base model overwrite works`() = TestUtil.test { app, http ->
-        JavalinRenderer.baseModelFunction = { ctx -> defaultBaseModel + mapOf("queryParams" to ctx.queryParamMap(), "pathParams" to ctx.pathParamMap()) };
+        JavalinRenderer.baseModelFunction = { ctx -> defaultBaseModel + mapOf("queryParams" to ctx.queryParamMap(), "pathParams" to ctx.pathParamMap()) }
         app.get("/hello/:pp") { ctx -> ctx.render("/templates/freemarker/test-with-base.ftl", model("foo", "baz")) }
         assertThat(http.getBody("/hello/world?im=good")).isEqualTo("<h1>good</h1><h2>world</h2><h3>baz</h3>")
     }
diff --git a/javalin/src/test/resources/templates/jte/multiple-params.kte b/javalin/src/test/resources/templates/jte/multiple-params.kte
new file mode 100644
index 00000000..0c584de0
--- /dev/null
+++ b/javalin/src/test/resources/templates/jte/multiple-params.kte
@@ -0,0 +1,3 @@
+@param one:String
+@param two:String
+<h1>${one} ${two}!</h1>
diff --git a/javalin/src/test/resources/templates/jte/test.kte b/javalin/src/test/resources/templates/jte/test.kte
new file mode 100644
index 00000000..1d7a1392
--- /dev/null
+++ b/javalin/src/test/resources/templates/jte/test.kte
@@ -0,0 +1,3 @@
+@import io.javalin.TestTemplates.JteTestPage
+@param page:JteTestPage
+<h1>${page.hello} ${page.world}!</h1>