Skip to content

Commit

Permalink
Merge pull request #670 from hexagonkt/feature/#669
Browse files Browse the repository at this point in the history
Add jte template adapter :experimental
  • Loading branch information
jaguililla committed Oct 21, 2023
2 parents 2d12ed8 + ce5b6b0 commit 01e3e7e
Show file tree
Hide file tree
Showing 18 changed files with 318 additions and 8 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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/
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ apiValidation {
"rest",
"rest_tools",
"web",
"templates_jte",
)
)
}
3 changes: 3 additions & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 2 additions & 1 deletion site/pages/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 0 additions & 2 deletions templates/templates/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@

# Module templates

This port provides a common interface for rendering templates with multiple different template
engines.

Expand Down Expand Up @@ -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.
2 changes: 0 additions & 2 deletions templates/templates_freemarker/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@

# Module templates_freemarker

This module provides an adapter for the templates Port supporting the Apache [FreeMarker] template
engine.

Expand Down Expand Up @@ -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.
63 changes: 63 additions & 0 deletions templates/templates_jte/README.md
Original file line number Diff line number Diff line change
@@ -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
<dependency>
<groupId>com.hexagonkt</groupId>
<artifactId>templates_jte</artifactId>
<version>$hexagonVersion</version>
</dependency>
```

## 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.
46 changes: 46 additions & 0 deletions templates/templates_jte/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
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") }
tasks.named("sourcesJar") { dependsOn("compileTestKotlin") }

// TODO Remove when settings prevent this directory from being created (check .gitignore also)
tasks.named<Delete>("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()
}
Original file line number Diff line number Diff line change
@@ -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<String, *>, locale: Locale): String {
val output: TemplateOutput = StringOutput()
templateEngine.render(url.path, context, output)
return output.toString()
}

override fun render(
name: String, templates: Map<String, String>, context: Map<String, *>, locale: Locale
): String =
throw UnsupportedOperationException("jte does not support memory templates")
}
11 changes: 11 additions & 0 deletions templates/templates_jte/src/main/kotlin/module-info.java
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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<UnsupportedOperationException> {
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)
}
}
Original file line number Diff line number Diff line change
@@ -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)
)
Original file line number Diff line number Diff line change
@@ -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))
Original file line number Diff line number Diff line change
@@ -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))
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Args= \
--initialize-at-build-time=kotlin.annotation.AnnotationRetention \
--initialize-at-build-time=kotlin.annotation.AnnotationTarget
26 changes: 26 additions & 0 deletions templates/templates_jte/src/test/resources/templates/test.jte
Original file line number Diff line number Diff line change
@@ -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_

<!DOCTYPE html>

<html>
<head>
<title>Fortunes</title>
</head>
<body>
<p>This is a test template</p>
<p>${key1 == null? "key1" : key1}</p>
<p>${key2 == null? "key2" : key2}</p>
<p>${a == null? "a" : a}</p>
<p>${localDate == null? "localDate" : localDate.toString()}</p>
<p>${_template_ == null? "_template_" : _template_.toString()}</p>
<p>${_now_ == null? "_now_" : _now_.toString()}</p>
</body>
</html>
2 changes: 0 additions & 2 deletions templates/templates_pebble/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@

# Module templates_pebble

[Pebble] template engine adapter for Hexagon.

For usage instructions, refer to the [Templates Port documentation](/templates/).
Expand Down Expand Up @@ -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.
Loading

0 comments on commit 01e3e7e

Please sign in to comment.