しおしお

IntelliJ IDEAのことなんかを書いてます

KtorでThymeleafを試す

Ktor標準ではThymeleafに対応していなかったので、Thymeleaf使えるかどうか試してみた感じです。
※Ktor1.2以降からThymeleaf機能が追加されたようです→Ktor 1.2.0(rc)で追加されたThymeleaf Featureを試してみた - しおしお

Thymeleaf用のFeatureを作る

ktor/FreeMarker.kt at master · ktorio/ktor · GitHubを参考に…
やってることは、こんな感じです。

  • ClassLoaderTemplateResolver の生成と初期設定
  • アプリケーションからの戻りが ThymeleafContent の場合に、Thymeleafを使ったレンダリング
import io.ktor.application.ApplicationCallPipeline
import io.ktor.application.ApplicationFeature
import io.ktor.http.content.OutgoingContent
import io.ktor.response.ApplicationSendPipeline
import io.ktor.util.AttributeKey
import io.ktor.util.cio.bufferedWriter
import kotlinx.coroutines.io.ByteWriteChannel
import org.thymeleaf.TemplateEngine
import org.thymeleaf.context.Context
import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver

class ThymeleafContent(
        val template: String,
        val model: Map<String, Any>?
)

class Thymeleaf(private val engine: TemplateEngine) {

    companion object Feature : ApplicationFeature<ApplicationCallPipeline, ClassLoaderTemplateResolver, Thymeleaf> {
        override val key: AttributeKey<Thymeleaf> = AttributeKey("thymeleaf")

        override fun install(pipeline: ApplicationCallPipeline,
                             configure: ClassLoaderTemplateResolver.() -> Unit): Thymeleaf {
            val config = ClassLoaderTemplateResolver(Thread.currentThread().contextClassLoader)
                    .apply {
                        prefix = "templates/"
                        suffix = ".html"
                        characterEncoding = "UTF-8"
                    }
                    .apply(configure)
            val engine = TemplateEngine()
            engine.setTemplateResolver(config)
            val feature = Thymeleaf(engine)
            pipeline.sendPipeline.intercept(ApplicationSendPipeline.Transform) { value ->
                if (value is ThymeleafContent) {
                    val response = feature.process(value)
                    proceedWith(response)
                }
            }
            return feature
        }
    }

    private fun process(content: ThymeleafContent): ThymeleafOutgoingContent {
        return ThymeleafOutgoingContent(
                engine,
                content
        )
    }

    private class ThymeleafOutgoingContent(
            val engine: TemplateEngine,
            val content: ThymeleafContent
    ) : OutgoingContent.WriteChannelContent() {
        override suspend fun writeTo(channel: ByteWriteChannel) {
            channel.bufferedWriter(Charsets.UTF_8).use {
                val context = Context().apply {
                    setVariables(content.model)
                }
                engine.process(content.template, context, it)
            }
        }
    }
}

アプリケーションを作る

サーバサイド

  • 上で作ったThymeleaf Featureを使えるようにするためにインストールする
  • 処理結果として、ThymeleafContentを返す
package siosio

import io.ktor.application.Application
import io.ktor.application.call
import io.ktor.application.install
import io.ktor.response.respond
import io.ktor.routing.get
import io.ktor.routing.routing

fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args)

@Suppress("unused") // Referenced in application.conf
@kotlin.jvm.JvmOverloads
fun Application.module(testing: Boolean = false) {
    install(Thymeleaf)

    routing {
        get("/hello") {
            call.respond(ThymeleafContent("index", mapOf("user" to User(1, "しおしお"))))
        }
    }
}

data class User(val id: Long, val name: String)

Thymeleaf用テンプレート

単純にUserクラスの内容を出力しているだけですね

<!DOCTYPE html >
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org" xml:lang="ja" lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>

<!--/*@thymesVar id="user" type="siosio.User"*/-->
<ul>
  <li th:text="${user.id}"></li>
  <li th:text="${user.name}"></li>
</ul>
</body>
</html>

実行結果

動きましたね(๑•̀ㅂ•́)و✧

f:id:sioiri:20190422063613p:plain