Refactor package structure and add web components.

Reorganized Kotlin package structure under 'nl.astraeus' and removed 'gradle.properties' file. Updated '.gitignore' to exclude 'gradle.properties'. Added new web functionalities including ID generation, websocket and HTTP request handling to support dynamic web content delivery. Adjusted server configuration and modified build version.
This commit is contained in:
2024-12-01 11:59:26 +01:00
parent bf9d72a20c
commit 88d09ecdbf
13 changed files with 245 additions and 42 deletions

View File

@@ -0,0 +1,69 @@
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()
val REPO_NAME = "dummy so the gitea template compiles, please remove"
val SERVER_PORT = 7001
val JDBC_PORT = 8001
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/${REPO_NAME}.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()
}

View 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()
}
}

View 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()
}
}
}
}

View 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()
}

View File

@@ -0,0 +1,41 @@
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
fun generateIndex(patch: String?): String {
val result = StringBuilder();
if (patch == null) {
result.appendHTML(true).html {
head {
title("${REPO_NAME}")
//link("/css/all.min.css", "stylesheet")
}
body {
script(src = "/${REPO_NAME}.js") {}
}
}
} else {
result.appendHTML(true).html {
head {
title("${REPO_NAME}")
meta {
httpEquiv = "refresh"
content = "0; url=/$itemUrl/$patch"
}
}
body {
+"Redirecting to $itemUrl $patch..."
}
}
}
return result.toString()
}

View File

@@ -0,0 +1,39 @@
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 java.nio.file.Paths
import kotlin.text.startsWith
val itemUrl = "blaat"
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("/song", IndexHandler)
pathHandler.addExactPath("/ws", WebsocketConnectHandler)
}
override fun handleRequest(exchange: HttpServerExchange) {
pathHandler.handleRequest(exchange)
}
}

View 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)
}
}