From a391c891b3a032ffcfc7c3eb0ba7e6eb97d37539 Mon Sep 17 00:00:00 2001 From: jaguililla Date: Sat, 21 Oct 2023 16:52:50 +0200 Subject: [PATCH 1/2] Add jte template adapter :experimental --- .gitignore | 3 + build.gradle.kts | 1 + gradle.properties | 3 + site/pages/index.md | 3 +- templates/templates/README.md | 2 - templates/templates_freemarker/README.md | 2 - templates/templates_jte/README.md | 63 +++++++++++++++++ templates/templates_jte/build.gradle.kts | 45 ++++++++++++ .../com/hexagonkt/templates/jte/JteAdapter.kt | 70 +++++++++++++++++++ .../src/main/kotlin/module-info.java | 11 +++ .../hexagonkt/templates/jte/JteAdapterTest.kt | 61 ++++++++++++++++ .../JteTemplateAdapterPrecompiledBaseTest.kt | 11 +++ .../jte/JteTemplateAdapterPrecompiledTest.kt | 8 +++ .../templates/jte/JteTemplateAdapterTest.kt | 10 +++ .../templates_jte/native-image.properties | 3 + .../src/test/resources/templates/test.jte | 26 +++++++ templates/templates_pebble/README.md | 2 - templates/templates_rocker/README.md | 1 - 18 files changed, 317 insertions(+), 8 deletions(-) create mode 100644 templates/templates_jte/README.md create mode 100644 templates/templates_jte/build.gradle.kts create mode 100644 templates/templates_jte/src/main/kotlin/com/hexagonkt/templates/jte/JteAdapter.kt create mode 100644 templates/templates_jte/src/main/kotlin/module-info.java create mode 100644 templates/templates_jte/src/test/kotlin/com/hexagonkt/templates/jte/JteAdapterTest.kt create mode 100644 templates/templates_jte/src/test/kotlin/com/hexagonkt/templates/jte/JteTemplateAdapterPrecompiledBaseTest.kt create mode 100644 templates/templates_jte/src/test/kotlin/com/hexagonkt/templates/jte/JteTemplateAdapterPrecompiledTest.kt create mode 100644 templates/templates_jte/src/test/kotlin/com/hexagonkt/templates/jte/JteTemplateAdapterTest.kt create mode 100644 templates/templates_jte/src/test/resources/META-INF/native-image/com.hexagonkt/templates_jte/native-image.properties create mode 100644 templates/templates_jte/src/test/resources/templates/test.jte diff --git a/.gitignore b/.gitignore index 358bb619df..5bfae36ff8 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,6 @@ package-lock.json # System Files .DS_Store Thumbs.db + +# TODO Delete this when the folder is no longer generated +jte-classes/ diff --git a/build.gradle.kts b/build.gradle.kts index 2346156949..6bc9c9c99e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -160,6 +160,7 @@ apiValidation { "rest", "rest_tools", "web", + "templates_jte", ) ) } diff --git a/gradle.properties b/gradle.properties index fa884fec9a..c43ac2bab6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -66,6 +66,9 @@ dslJsonVersion=2.0.2 # templates_freemarker freemarkerVersion=2.3.32 +# templates_jte +jteVersion=3.1.3 + # templates_pebble pebbleVersion=3.2.1 diff --git a/site/pages/index.md b/site/pages/index.md index c53659f2f0..f237ef48a1 100644 --- a/site/pages/index.md +++ b/site/pages/index.md @@ -144,7 +144,7 @@ Ports with their provided implementations (Adapters). |-------------------------|-------------------------------------------------------| | [HTTP Server] | [Netty], [Netty Epoll], [Jetty], [Servlet], [Helidon] | | [HTTP Client] | [Jetty][Jetty Client] | -| [Templates] | [Pebble], [FreeMarker], [Rocker] | +| [Templates] | [Pebble], [FreeMarker], [Rocker], [jte] | | [Serialization Formats] | [JSON], [YAML], [CSV], [XML], [TOML] | [HTTP Server]: /http_server @@ -159,6 +159,7 @@ Ports with their provided implementations (Adapters). [Pebble]: /templates_pebble [FreeMarker]: /templates_freemarker [Rocker]: /templates_rocker +[jte]: /templates_jte [Serialization Formats]: /core/#serialization [JSON]: /api/serialization_jackson_json/com.hexagonkt.serialization.jackson.json/-json [YAML]: /api/serialization_jackson_yaml/com.hexagonkt.serialization.jackson.yaml/-yaml diff --git a/templates/templates/README.md b/templates/templates/README.md index 67529e099e..98c203d7cc 100644 --- a/templates/templates/README.md +++ b/templates/templates/README.md @@ -1,6 +1,5 @@ # Module templates - This port provides a common interface for rendering templates with multiple different template engines. @@ -35,5 +34,4 @@ and `_now_` variables) are added to the context automatically. Check the code be [TemplateManager]: /api/templates/com.hexagonkt.templates/-template-manager/index.html # Package com.hexagonkt.templates - Feature implementation code. diff --git a/templates/templates_freemarker/README.md b/templates/templates_freemarker/README.md index 13ccf55252..92e7108e06 100644 --- a/templates/templates_freemarker/README.md +++ b/templates/templates_freemarker/README.md @@ -1,6 +1,5 @@ # Module templates_freemarker - This module provides an adapter for the templates Port supporting the Apache [FreeMarker] template engine. @@ -31,5 +30,4 @@ For usage instructions, refer to the [Templates Port documentation](/templates/) ``` # Package com.hexagonkt.templates.freemarker - Classes that implement the Templates Port interface with the [FreeMarker] engine. diff --git a/templates/templates_jte/README.md b/templates/templates_jte/README.md new file mode 100644 index 0000000000..778a17e29b --- /dev/null +++ b/templates/templates_jte/README.md @@ -0,0 +1,63 @@ + +# Module templates_jte +[jte] template engine adapter for Hexagon. + +For usage instructions, refer to the [Templates Port documentation](/templates/). + +[jte]: https://jte.gg + +### Install the Dependency + +=== "build.gradle" + + ```groovy + repositories { + mavenCentral() + } + + implementation("com.hexagonkt:templates_jte:$hexagonVersion") + ``` + +=== "pom.xml" + + ```xml + + com.hexagonkt + templates_jte + $hexagonVersion + + ``` + +## Use the Adapter +In order to use this adapter you need to set up a build plugin to compile the templates. To do so in +Gradle, add the following lines to `build.gradle.kts`: + +```kotlin +plugins { + id("gg.jte.gradle") version("3.1.3") +} + +dependencies { + "jteGenerate"("gg.jte:jte-native-resources:$jteVersion") +} + +tasks.named("compileKotlin") { dependsOn("generateJte") } + +jte { + sourceDirectory.set(projectDir.resolve("src/main/resources/templates").toPath()) + contentType.set(gg.jte.ContentType.Html) + + jteExtension("gg.jte.nativeimage.NativeResourcesExtension") + + generate() +} +``` + +# TODO +* Don't create `jte-classes` directory +* Generate template classes only for tests +* Test file loaded templates +* Test plain test templates + +# Package com.hexagonkt.templates.jte +Classes that implement the Templates Port interface with the [jte] engine. diff --git a/templates/templates_jte/build.gradle.kts b/templates/templates_jte/build.gradle.kts new file mode 100644 index 0000000000..c34395c195 --- /dev/null +++ b/templates/templates_jte/build.gradle.kts @@ -0,0 +1,45 @@ +import gg.jte.ContentType.Html + +plugins { + id("java-library") + id("gg.jte.gradle") version("3.1.3") +} + +apply(from = "$rootDir/gradle/kotlin.gradle") +apply(from = "$rootDir/gradle/publish.gradle") +apply(from = "$rootDir/gradle/dokka.gradle") +apply(from = "$rootDir/gradle/native.gradle") +apply(from = "$rootDir/gradle/detekt.gradle") + +description = "Template processor adapter for 'jte'." + +dependencies { + val jteVersion = properties["jteVersion"] + + "api"(project(":templates:templates")) + "api"("gg.jte:jte:$jteVersion") + + "testImplementation"(project(":templates:templates_test")) + "testImplementation"(project(":serialization:serialization_jackson_json")) + + "jteGenerate"("gg.jte:jte-native-resources:$jteVersion") +} + +tasks.named("compileKotlin") { dependsOn("generateJte") } +tasks.named("processResources") { dependsOn("processTestResources") } +tasks.named("detektMain") { dependsOn("compileTestKotlin") } + +// TODO Remove when settings prevent this directory from being created (check .gitignore also) +tasks.named("clean") { + delete("jte-classes") +} + +jte { + sourceDirectory.set(projectDir.resolve("src/test/resources/templates").toPath()) + targetDirectory.set(projectDir.resolve("build/classes/kotlin/test").toPath()) + contentType.set(Html) + + jteExtension("gg.jte.nativeimage.NativeResourcesExtension") + + generate() +} diff --git a/templates/templates_jte/src/main/kotlin/com/hexagonkt/templates/jte/JteAdapter.kt b/templates/templates_jte/src/main/kotlin/com/hexagonkt/templates/jte/JteAdapter.kt new file mode 100644 index 0000000000..09d9c6eea0 --- /dev/null +++ b/templates/templates_jte/src/main/kotlin/com/hexagonkt/templates/jte/JteAdapter.kt @@ -0,0 +1,70 @@ +package com.hexagonkt.templates.jte + +import com.hexagonkt.core.media.MediaType +import com.hexagonkt.core.media.TEXT_HTML +import com.hexagonkt.core.media.TEXT_PLAIN +import com.hexagonkt.templates.TemplatePort +import gg.jte.CodeResolver +import gg.jte.ContentType +import gg.jte.TemplateEngine +import gg.jte.TemplateOutput +import gg.jte.output.StringOutput +import gg.jte.resolve.DirectoryCodeResolver +import gg.jte.resolve.ResourceCodeResolver +import java.net.URL +import java.nio.file.Path +import java.util.* + +class JteAdapter( + mediaType: MediaType, + resolverBase: URL? = null, + precompiled: Boolean = false +) : TemplatePort { + + private companion object { + val allowedTypes: String = setOf(TEXT_HTML, TEXT_PLAIN).joinToString(", ") { it.fullType } + } + + private val contentType = when (mediaType) { + TEXT_HTML -> ContentType.Html + TEXT_PLAIN -> ContentType.Plain + else -> + error("Unsupported media type not in: $allowedTypes (${mediaType.fullType})") + } + + private val resolver: CodeResolver = + when (resolverBase?.protocol) { + "classpath" -> ResourceCodeResolver(resolverBase.path) + "file" -> DirectoryCodeResolver(Path.of(resolverBase.path)) + null -> ResourceCodeResolver("") + else -> error("Invalid base schema not in: classpath, file (${resolverBase.protocol})") + } + + private val templateEngine: TemplateEngine = + if (precompiled) { + if (resolverBase === null) { + TemplateEngine.createPrecompiled(contentType) + } + else { + val protocol = resolverBase.protocol + check(protocol == "classpath") { + "Precompiled base must be classpath URLs ($protocol)" + } + TemplateEngine.createPrecompiled(Path.of(resolverBase.path), contentType) + } + } + else { + TemplateEngine.create(resolver, contentType) + } + + override fun render(url: URL, context: Map, locale: Locale): String { + val output: TemplateOutput = StringOutput() + templateEngine.render(url.path, context, output) + return output.toString() + } + + override fun render( + name: String, templates: Map, context: Map, locale: Locale + ): String = + throw UnsupportedOperationException("jte does not support memory templates") +} diff --git a/templates/templates_jte/src/main/kotlin/module-info.java b/templates/templates_jte/src/main/kotlin/module-info.java new file mode 100644 index 0000000000..b8e8c82dc3 --- /dev/null +++ b/templates/templates_jte/src/main/kotlin/module-info.java @@ -0,0 +1,11 @@ + +module com.hexagonkt.templates_jte { + + requires transitive kotlin.stdlib; + requires transitive com.hexagonkt.templates; + + requires gg.jte; + requires gg.jte.runtime; + + exports com.hexagonkt.templates.jte; +} diff --git a/templates/templates_jte/src/test/kotlin/com/hexagonkt/templates/jte/JteAdapterTest.kt b/templates/templates_jte/src/test/kotlin/com/hexagonkt/templates/jte/JteAdapterTest.kt new file mode 100644 index 0000000000..30eae2d626 --- /dev/null +++ b/templates/templates_jte/src/test/kotlin/com/hexagonkt/templates/jte/JteAdapterTest.kt @@ -0,0 +1,61 @@ +package com.hexagonkt.templates.jte + +import com.hexagonkt.core.media.TEXT_CSS +import com.hexagonkt.core.media.TEXT_HTML +import com.hexagonkt.core.urlOf +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.condition.DisabledInNativeImage +import java.time.LocalDateTime +import java.util.Locale +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +internal class JteAdapterTest { + + private val locale = Locale.getDefault() + + @Test + @DisabledInNativeImage + fun `Dates are converted properly`() { + val context = mapOf("localDate" to LocalDateTime.of(2000, 12, 31, 23, 45)) + val resource = "classpath:templates/test.jte" + val html = JteAdapter(TEXT_HTML).render(urlOf(resource), context, locale) + assert(html.contains("23:45")) + assert(html.contains("2000")) + assert(html.contains("31")) + } + + @Test fun `Dates are converted properly with precompiled templates`() { + val context = mapOf("localDate" to LocalDateTime.of(2000, 12, 31, 23, 45)) + val resource = "classpath:test.jte" + val adapter = JteAdapter(TEXT_HTML, precompiled = true) + val html = adapter.render(urlOf(resource), context, locale) + assert(html.contains("23:45")) + assert(html.contains("2000")) + assert(html.contains("31")) + } + + @Test fun `Literal templates are not supported`() { + val context = mapOf("localDate" to LocalDateTime.of(2000, 12, 31, 23, 45)) + val e = assertFailsWith { + JteAdapter(TEXT_HTML).render("template code", context, locale) + } + assertEquals("jte does not support memory templates", e.message) + } + + @Test fun `Invalid jte adapters throw exceptions on creation`() { + assertIllegalState("Unsupported media type not in: text/html, text/plain (text/css)") { + JteAdapter(TEXT_CSS) + } + assertIllegalState("Invalid base schema not in: classpath, file (http)") { + JteAdapter(TEXT_HTML, urlOf("http://example.com")) + } + assertIllegalState("Precompiled base must be classpath URLs (file)") { + JteAdapter(TEXT_HTML, urlOf("file://example.com"), true) + } + } + + private inline fun assertIllegalState(message: String, block: () -> Unit) { + assertEquals(message, assertFailsWith(IllegalStateException::class, block).message) + } +} diff --git a/templates/templates_jte/src/test/kotlin/com/hexagonkt/templates/jte/JteTemplateAdapterPrecompiledBaseTest.kt b/templates/templates_jte/src/test/kotlin/com/hexagonkt/templates/jte/JteTemplateAdapterPrecompiledBaseTest.kt new file mode 100644 index 0000000000..31ed82e3ee --- /dev/null +++ b/templates/templates_jte/src/test/kotlin/com/hexagonkt/templates/jte/JteTemplateAdapterPrecompiledBaseTest.kt @@ -0,0 +1,11 @@ +package com.hexagonkt.templates.jte + +import com.hexagonkt.core.media.TEXT_HTML +import com.hexagonkt.core.urlOf +import com.hexagonkt.templates.test.TemplateAdapterTest + +internal class JteTemplateAdapterPrecompiledBaseTest : + TemplateAdapterTest( + urlOf("classpath:test.jte"), + JteAdapter(TEXT_HTML, urlOf("classpath:/"), precompiled = true) + ) diff --git a/templates/templates_jte/src/test/kotlin/com/hexagonkt/templates/jte/JteTemplateAdapterPrecompiledTest.kt b/templates/templates_jte/src/test/kotlin/com/hexagonkt/templates/jte/JteTemplateAdapterPrecompiledTest.kt new file mode 100644 index 0000000000..8ef5698806 --- /dev/null +++ b/templates/templates_jte/src/test/kotlin/com/hexagonkt/templates/jte/JteTemplateAdapterPrecompiledTest.kt @@ -0,0 +1,8 @@ +package com.hexagonkt.templates.jte + +import com.hexagonkt.core.media.TEXT_HTML +import com.hexagonkt.core.urlOf +import com.hexagonkt.templates.test.TemplateAdapterTest + +internal class JteTemplateAdapterPrecompiledTest : + TemplateAdapterTest(urlOf("classpath:test.jte"), JteAdapter(TEXT_HTML, precompiled = true)) diff --git a/templates/templates_jte/src/test/kotlin/com/hexagonkt/templates/jte/JteTemplateAdapterTest.kt b/templates/templates_jte/src/test/kotlin/com/hexagonkt/templates/jte/JteTemplateAdapterTest.kt new file mode 100644 index 0000000000..383f90be34 --- /dev/null +++ b/templates/templates_jte/src/test/kotlin/com/hexagonkt/templates/jte/JteTemplateAdapterTest.kt @@ -0,0 +1,10 @@ +package com.hexagonkt.templates.jte + +import com.hexagonkt.core.media.TEXT_HTML +import com.hexagonkt.core.urlOf +import com.hexagonkt.templates.test.TemplateAdapterTest +import org.junit.jupiter.api.condition.DisabledInNativeImage + +@DisabledInNativeImage +internal class JteTemplateAdapterTest : + TemplateAdapterTest(urlOf("classpath:templates/test.jte"), JteAdapter(TEXT_HTML)) diff --git a/templates/templates_jte/src/test/resources/META-INF/native-image/com.hexagonkt/templates_jte/native-image.properties b/templates/templates_jte/src/test/resources/META-INF/native-image/com.hexagonkt/templates_jte/native-image.properties new file mode 100644 index 0000000000..f68b9ce179 --- /dev/null +++ b/templates/templates_jte/src/test/resources/META-INF/native-image/com.hexagonkt/templates_jte/native-image.properties @@ -0,0 +1,3 @@ +Args= \ + --initialize-at-build-time=kotlin.annotation.AnnotationRetention \ + --initialize-at-build-time=kotlin.annotation.AnnotationTarget diff --git a/templates/templates_jte/src/test/resources/templates/test.jte b/templates/templates_jte/src/test/resources/templates/test.jte new file mode 100644 index 0000000000..e9bc6e3536 --- /dev/null +++ b/templates/templates_jte/src/test/resources/templates/test.jte @@ -0,0 +1,26 @@ +@import java.net.URL +@import java.time.LocalDateTime + +@param String key1 +@param String key2 +@param String a +@param LocalDateTime localDate +@param URL _template_ +@param LocalDateTime _now_ + + + + + + Fortunes + + +

This is a test template

+

${key1 == null? "key1" : key1}

+

${key2 == null? "key2" : key2}

+

${a == null? "a" : a}

+

${localDate == null? "localDate" : localDate.toString()}

+

${_template_ == null? "_template_" : _template_.toString()}

+

${_now_ == null? "_now_" : _now_.toString()}

+ + diff --git a/templates/templates_pebble/README.md b/templates/templates_pebble/README.md index 1a59d5dd15..3772c65038 100644 --- a/templates/templates_pebble/README.md +++ b/templates/templates_pebble/README.md @@ -1,6 +1,5 @@ # Module templates_pebble - [Pebble] template engine adapter for Hexagon. For usage instructions, refer to the [Templates Port documentation](/templates/). @@ -30,5 +29,4 @@ For usage instructions, refer to the [Templates Port documentation](/templates/) ``` # Package com.hexagonkt.templates.pebble - Classes that implement the Templates Port interface with the [Pebble] engine. diff --git a/templates/templates_rocker/README.md b/templates/templates_rocker/README.md index f187f32fc1..de13fe1161 100644 --- a/templates/templates_rocker/README.md +++ b/templates/templates_rocker/README.md @@ -51,5 +51,4 @@ On top of that, you must also declare the template parameters this way: `@args(java.util.Map context)` and use the data from the map. # Package com.hexagonkt.templates.rocker - Classes that implement the Templates Port interface with the [Rocker] engine. From ce5b6b0b6b9e1cd68314c17b9fc7a607f88d0774 Mon Sep 17 00:00:00 2001 From: jaguililla Date: Sat, 21 Oct 2023 17:11:24 +0200 Subject: [PATCH 2/2] Fix CI --- templates/templates_jte/build.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/templates/templates_jte/build.gradle.kts b/templates/templates_jte/build.gradle.kts index c34395c195..2403cf00da 100644 --- a/templates/templates_jte/build.gradle.kts +++ b/templates/templates_jte/build.gradle.kts @@ -28,6 +28,7 @@ dependencies { tasks.named("compileKotlin") { dependsOn("generateJte") } tasks.named("processResources") { dependsOn("processTestResources") } tasks.named("detektMain") { dependsOn("compileTestKotlin") } +tasks.named("sourcesJar") { dependsOn("compileTestKotlin") } // TODO Remove when settings prevent this directory from being created (check .gitignore also) tasks.named("clean") {