generated from rnentjes/kotlin-server-web-undertow
Initial commit
This commit is contained in:
19
src/jsMain/kotlin/nl/astraeus/tmpl/Main.kt
Normal file
19
src/jsMain/kotlin/nl/astraeus/tmpl/Main.kt
Normal file
@@ -0,0 +1,19 @@
|
||||
package nl.astraeus.tmpl
|
||||
|
||||
import kotlinx.browser.document
|
||||
import kotlinx.html.div
|
||||
import nl.astraeus.komp.HtmlBuilder
|
||||
import nl.astraeus.komp.Komponent
|
||||
|
||||
|
||||
class HelloKomponent : Komponent() {
|
||||
override fun HtmlBuilder.render() {
|
||||
div {
|
||||
+ "Hello, world!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun main() {
|
||||
Komponent.create(document.body!!, HelloKomponent())
|
||||
}
|
||||
64
src/jvmMain/kotlin/nl/astraeus/tmpl/Main.kt
Normal file
64
src/jvmMain/kotlin/nl/astraeus/tmpl/Main.kt
Normal file
@@ -0,0 +1,64 @@
|
||||
package nl.astraeus.tmpl
|
||||
|
||||
import com.zaxxer.hikari.HikariConfig
|
||||
import io.undertow.Undertow
|
||||
import io.undertow.UndertowOptions
|
||||
import io.undertow.predicate.Predicates
|
||||
import io.undertow.server.handlers.encoding.ContentEncodingRepository
|
||||
import io.undertow.server.handlers.encoding.EncodingHandler
|
||||
import io.undertow.server.handlers.encoding.GzipEncodingProvider
|
||||
import nl.astraeus.logger.Logger
|
||||
import nl.astraeus.tmpl.db.Database
|
||||
import nl.astraeus.tmpl.web.RequestHandler
|
||||
|
||||
val log = Logger()
|
||||
|
||||
fun main() {
|
||||
Thread.currentThread().uncaughtExceptionHandler = Thread.UncaughtExceptionHandler { t, e ->
|
||||
log.warn(e) {
|
||||
e.message
|
||||
}
|
||||
}
|
||||
|
||||
Runtime.getRuntime().addShutdownHook(
|
||||
object : Thread() {
|
||||
override fun run() {
|
||||
Database.vacuumDatabase()
|
||||
Database.closeDatabase()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
Class.forName("nl.astraeus.jdbc.Driver")
|
||||
Database.initialize(HikariConfig().apply {
|
||||
driverClassName = "nl.astraeus.jdbc.Driver"
|
||||
jdbcUrl = "jdbc:stat:webServerPort=$JDBC_PORT:jdbc:sqlite:data/$repoName.db"
|
||||
username = "sa"
|
||||
password = ""
|
||||
maximumPoolSize = 25
|
||||
isAutoCommit = false
|
||||
|
||||
validate()
|
||||
})
|
||||
|
||||
val compressionHandler =
|
||||
EncodingHandler(
|
||||
ContentEncodingRepository()
|
||||
.addEncodingHandler(
|
||||
"gzip",
|
||||
GzipEncodingProvider(), 50,
|
||||
Predicates.parse("max-content-size(5)")
|
||||
)
|
||||
).setNext(RequestHandler)
|
||||
|
||||
val server = Undertow.builder()
|
||||
.addHttpListener(SERVER_PORT, "localhost")
|
||||
.setIoThreads(4)
|
||||
.setHandler(compressionHandler)
|
||||
.setServerOption(UndertowOptions.SHUTDOWN_TIMEOUT, 1000)
|
||||
.build()
|
||||
|
||||
println("Starting undertow server at port 6007...")
|
||||
server?.start()
|
||||
|
||||
}
|
||||
10
src/jvmMain/kotlin/nl/astraeus/tmpl/Placeholders.kt
Normal file
10
src/jvmMain/kotlin/nl/astraeus/tmpl/Placeholders.kt
Normal file
@@ -0,0 +1,10 @@
|
||||
package nl.astraeus.tmpl
|
||||
|
||||
val REPO_NAME = "dummy so the gitea template compiles, please remove"
|
||||
|
||||
val SERVER_PORT = 7001
|
||||
val JDBC_PORT = 8001
|
||||
|
||||
val pageTitle = "mtmc-web"
|
||||
val itemUrl = "mtmc-web"
|
||||
val repoName = "mtmc-web"
|
||||
73
src/jvmMain/kotlin/nl/astraeus/tmpl/db/Database.kt
Normal file
73
src/jvmMain/kotlin/nl/astraeus/tmpl/db/Database.kt
Normal file
@@ -0,0 +1,73 @@
|
||||
package nl.astraeus.tmpl.db
|
||||
|
||||
import com.zaxxer.hikari.HikariConfig
|
||||
import com.zaxxer.hikari.HikariDataSource
|
||||
import java.sql.Connection
|
||||
import java.util.*
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import kotlin.collections.set
|
||||
import kotlin.use
|
||||
|
||||
private val currentConnection = ThreadLocal<Connection>()
|
||||
|
||||
fun <T> transaction(
|
||||
block: (Connection) -> T
|
||||
): T {
|
||||
val hasConnection = currentConnection.get() != null
|
||||
var oldConnection: Connection? = null
|
||||
|
||||
if (!hasConnection) {
|
||||
currentConnection.set(Database.getConnection())
|
||||
}
|
||||
|
||||
val connection = currentConnection.get()
|
||||
|
||||
try {
|
||||
val result = block(connection)
|
||||
|
||||
connection.commit()
|
||||
|
||||
return result
|
||||
} finally {
|
||||
if (!hasConnection) {
|
||||
currentConnection.set(oldConnection)
|
||||
connection.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object Database {
|
||||
|
||||
var ds: HikariDataSource? = null
|
||||
|
||||
fun initialize(config: HikariConfig) {
|
||||
val properties = Properties()
|
||||
properties["journal_mode"] = "WAL"
|
||||
|
||||
config.dataSourceProperties = properties
|
||||
config.addDataSourceProperty("cachePrepStmts", "true")
|
||||
config.addDataSourceProperty("prepStmtCacheSize", "250")
|
||||
config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048")
|
||||
|
||||
ds = HikariDataSource(config)
|
||||
Migrations.databaseVersionTableCreated = AtomicBoolean(false)
|
||||
Migrations.updateDatabaseIfNeeded()
|
||||
}
|
||||
|
||||
fun getConnection() = ds?.connection ?: error("Database has not been initialized!")
|
||||
|
||||
fun vacuumDatabase() {
|
||||
getConnection().use {
|
||||
it.autoCommit = true
|
||||
|
||||
it.prepareStatement("VACUUM").use { ps ->
|
||||
ps.executeUpdate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun closeDatabase() {
|
||||
ds?.close()
|
||||
}
|
||||
|
||||
}
|
||||
105
src/jvmMain/kotlin/nl/astraeus/tmpl/db/Migrations.kt
Normal file
105
src/jvmMain/kotlin/nl/astraeus/tmpl/db/Migrations.kt
Normal file
@@ -0,0 +1,105 @@
|
||||
package nl.astraeus.tmpl.db
|
||||
|
||||
import nl.astraeus.tmpl.log
|
||||
import java.sql.Connection
|
||||
import java.sql.SQLException
|
||||
import java.sql.Timestamp
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
sealed class Migration {
|
||||
class Query(
|
||||
val query: String
|
||||
) : Migration() {
|
||||
override fun toString(): String {
|
||||
return query
|
||||
}
|
||||
}
|
||||
|
||||
class Code(
|
||||
val code: (Connection) -> Unit
|
||||
) : Migration() {
|
||||
override fun toString(): String {
|
||||
return code.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val DATABASE_MIGRATIONS = arrayOf<Migration>(
|
||||
Migration.Query(
|
||||
"""
|
||||
CREATE TABLE DATABASE_VERSION (
|
||||
ID INTEGER PRIMARY KEY,
|
||||
QUERY TEXT,
|
||||
EXECUTED TIMESTAMP
|
||||
)
|
||||
""".trimIndent()
|
||||
),
|
||||
Migration.Query("SELECT sqlite_version()"),
|
||||
)
|
||||
|
||||
object Migrations {
|
||||
var databaseVersionTableCreated = AtomicBoolean(false)
|
||||
|
||||
fun updateDatabaseIfNeeded() {
|
||||
try {
|
||||
transaction { con ->
|
||||
con.prepareStatement(
|
||||
"""
|
||||
SELECT MAX(ID) FROM DATABASE_VERSION
|
||||
""".trimIndent()
|
||||
).use { ps ->
|
||||
ps.executeQuery().use { rs ->
|
||||
databaseVersionTableCreated.compareAndSet(false, true)
|
||||
|
||||
if(rs.next()) {
|
||||
val maxId = rs.getInt(1)
|
||||
|
||||
for (index in maxId + 1..<DATABASE_MIGRATIONS.size) {
|
||||
executeMigration(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: SQLException) {
|
||||
if (databaseVersionTableCreated.compareAndSet(false, true)) {
|
||||
executeMigration(0)
|
||||
updateDatabaseIfNeeded()
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun executeMigration(index: Int) {
|
||||
transaction { con ->
|
||||
log.debug {
|
||||
"Executing migration index - [DATABASE_MIGRATIONS[index]]"
|
||||
}
|
||||
val description = when(
|
||||
val migration = DATABASE_MIGRATIONS[index]
|
||||
) {
|
||||
is Migration.Query -> {
|
||||
con.prepareStatement(migration.query).use { ps ->
|
||||
ps.execute()
|
||||
}
|
||||
|
||||
migration.query
|
||||
}
|
||||
is Migration.Code -> {
|
||||
migration.code(con)
|
||||
|
||||
migration.code.toString()
|
||||
}
|
||||
}
|
||||
con.prepareStatement("INSERT INTO DATABASE_VERSION VALUES (?, ?, ?)").use { ps ->
|
||||
ps.setInt(1, index)
|
||||
ps.setString(2, description)
|
||||
ps.setTimestamp(3, Timestamp(System.currentTimeMillis()))
|
||||
|
||||
ps.execute()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
16
src/jvmMain/kotlin/nl/astraeus/tmpl/web/GenerateId.kt
Normal file
16
src/jvmMain/kotlin/nl/astraeus/tmpl/web/GenerateId.kt
Normal file
@@ -0,0 +1,16 @@
|
||||
package nl.astraeus.tmpl.web
|
||||
|
||||
import java.security.SecureRandom
|
||||
|
||||
val idChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
val random = SecureRandom()
|
||||
|
||||
fun generateId(length: Int = 8): String {
|
||||
val id = StringBuilder()
|
||||
|
||||
repeat(length) {
|
||||
id.append(idChars[random.nextInt(idChars.length)])
|
||||
}
|
||||
|
||||
return id.toString()
|
||||
}
|
||||
44
src/jvmMain/kotlin/nl/astraeus/tmpl/web/Index.kt
Normal file
44
src/jvmMain/kotlin/nl/astraeus/tmpl/web/Index.kt
Normal file
@@ -0,0 +1,44 @@
|
||||
package nl.astraeus.tmpl.web
|
||||
|
||||
import kotlinx.html.body
|
||||
import kotlinx.html.head
|
||||
import kotlinx.html.html
|
||||
import kotlinx.html.meta
|
||||
import kotlinx.html.script
|
||||
import kotlinx.html.stream.appendHTML
|
||||
import kotlinx.html.title
|
||||
import nl.astraeus.tmpl.REPO_NAME
|
||||
import nl.astraeus.tmpl.itemUrl
|
||||
import nl.astraeus.tmpl.pageTitle
|
||||
import nl.astraeus.tmpl.repoName
|
||||
|
||||
fun generateIndex(patch: String?): String {
|
||||
val result = StringBuilder();
|
||||
|
||||
if (patch == null) {
|
||||
result.appendHTML(true).html {
|
||||
head {
|
||||
title(pageTitle)
|
||||
//link("/css/all.min.css", "stylesheet")
|
||||
}
|
||||
body {
|
||||
script(src = "/$repoName.js") {}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result.appendHTML(true).html {
|
||||
head {
|
||||
title(pageTitle)
|
||||
meta {
|
||||
httpEquiv = "refresh"
|
||||
content = "0; url=/$itemUrl/$patch"
|
||||
}
|
||||
}
|
||||
body {
|
||||
+"Redirecting to $itemUrl $patch..."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result.toString()
|
||||
}
|
||||
38
src/jvmMain/kotlin/nl/astraeus/tmpl/web/RequestHandler.kt
Normal file
38
src/jvmMain/kotlin/nl/astraeus/tmpl/web/RequestHandler.kt
Normal file
@@ -0,0 +1,38 @@
|
||||
package nl.astraeus.tmpl.web
|
||||
|
||||
import io.undertow.server.HttpHandler
|
||||
import io.undertow.server.HttpServerExchange
|
||||
import io.undertow.server.handlers.PathHandler
|
||||
import io.undertow.server.handlers.resource.PathResourceManager
|
||||
import io.undertow.server.handlers.resource.ResourceHandler
|
||||
import nl.astraeus.tmpl.itemUrl
|
||||
import java.nio.file.Paths
|
||||
import kotlin.text.startsWith
|
||||
|
||||
object IndexHandler : HttpHandler {
|
||||
override fun handleRequest(exchange: HttpServerExchange) {
|
||||
if (exchange.requestPath.startsWith("/$itemUrl/")) {
|
||||
exchange.responseSender.send(generateIndex(null))
|
||||
} else {
|
||||
val itemId = generateId()
|
||||
|
||||
exchange.responseSender.send(generateIndex(itemId))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object RequestHandler : HttpHandler {
|
||||
val resourceHandler = ResourceHandler(PathResourceManager(Paths.get("web")))
|
||||
val pathHandler = PathHandler(resourceHandler)
|
||||
|
||||
init {
|
||||
pathHandler.addExactPath("/", IndexHandler)
|
||||
pathHandler.addExactPath("/index.html", IndexHandler)
|
||||
pathHandler.addPrefixPath("/$itemUrl", IndexHandler)
|
||||
pathHandler.addExactPath("/ws", WebsocketConnectHandler)
|
||||
}
|
||||
|
||||
override fun handleRequest(exchange: HttpServerExchange) {
|
||||
pathHandler.handleRequest(exchange)
|
||||
}
|
||||
}
|
||||
55
src/jvmMain/kotlin/nl/astraeus/tmpl/web/WebsockerHandler.kt
Normal file
55
src/jvmMain/kotlin/nl/astraeus/tmpl/web/WebsockerHandler.kt
Normal file
@@ -0,0 +1,55 @@
|
||||
package nl.astraeus.tmpl.web
|
||||
|
||||
import io.undertow.Handlers.websocket
|
||||
import io.undertow.server.HttpHandler
|
||||
import io.undertow.server.HttpServerExchange
|
||||
import io.undertow.websockets.WebSocketConnectionCallback
|
||||
import io.undertow.websockets.core.AbstractReceiveListener
|
||||
import io.undertow.websockets.core.BufferedBinaryMessage
|
||||
import io.undertow.websockets.core.BufferedTextMessage
|
||||
import io.undertow.websockets.core.WebSocketChannel
|
||||
import io.undertow.websockets.extensions.PerMessageDeflateHandshake
|
||||
import io.undertow.websockets.spi.WebSocketHttpExchange
|
||||
import kotlin.also
|
||||
|
||||
class WebsocketHandler : AbstractReceiveListener(), WebSocketConnectionCallback {
|
||||
// var user: String? = null
|
||||
|
||||
override fun onConnect(exchange: WebSocketHttpExchange, channel: WebSocketChannel) {
|
||||
channel.receiveSetter.set(this)
|
||||
channel.resumeReceives()
|
||||
}
|
||||
|
||||
override fun onFullTextMessage(channel: WebSocketChannel, message: BufferedTextMessage) {
|
||||
val data = message.data
|
||||
|
||||
TODO("handle text message")
|
||||
}
|
||||
|
||||
override fun onFullBinaryMessage(channel: WebSocketChannel, message: BufferedBinaryMessage?) {
|
||||
message?.data?.also { data ->
|
||||
var length = 0
|
||||
for (buffer in data.resource) {
|
||||
length += buffer.remaining()
|
||||
}
|
||||
val bytes = ByteArray(length)
|
||||
var offset = 0
|
||||
for (buffer in data.resource) {
|
||||
buffer.get(bytes, offset, buffer.remaining())
|
||||
offset += buffer.remaining()
|
||||
}
|
||||
|
||||
TODO("handle binary message")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object WebsocketConnectHandler : HttpHandler {
|
||||
|
||||
override fun handleRequest(exchange: HttpServerExchange) {
|
||||
val handshakeHandler = websocket(WebsocketHandler())
|
||||
handshakeHandler.addExtension(PerMessageDeflateHandshake())
|
||||
handshakeHandler.handleRequest(exchange)
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user