generated from rnentjes/kotlin-server-web-empty
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:
69
src/jvmMain/kotlin/nl/astraeus/tmpl/Main.kt
Normal file
69
src/jvmMain/kotlin/nl/astraeus/tmpl/Main.kt
Normal 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()
|
||||
|
||||
}
|
||||
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()
|
||||
}
|
||||
41
src/jvmMain/kotlin/nl/astraeus/tmpl/web/Index.kt
Normal file
41
src/jvmMain/kotlin/nl/astraeus/tmpl/web/Index.kt
Normal 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()
|
||||
}
|
||||
39
src/jvmMain/kotlin/nl/astraeus/tmpl/web/RequestHandler.kt
Normal file
39
src/jvmMain/kotlin/nl/astraeus/tmpl/web/RequestHandler.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
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