Compare commits

...

2 Commits

20 changed files with 664 additions and 253 deletions

1
.gitignore vendored
View File

@@ -44,3 +44,4 @@ bin/
### .kotlin ###
.kotlin
kotlin-js-store
gradle.properties

1
.idea/.name generated
View File

@@ -1 +0,0 @@
template

View File

@@ -0,0 +1,8 @@
<component name="ArtifactManager">
<artifact type="jar" name="markdown-parser-js-0.1.0-SNAPSHOT">
<output-path>$PROJECT_DIR$/build/libs</output-path>
<root id="archive" name="markdown-parser-js-0.1.0-SNAPSHOT.jar">
<element id="module-output" name="markdown-parser.jsMain" />
</root>
</artifact>
</component>

View File

@@ -0,0 +1,8 @@
<component name="ArtifactManager">
<artifact type="jar" name="markdown-parser-js-1.0.0">
<output-path>$PROJECT_DIR$/build/libs</output-path>
<root id="archive" name="markdown-parser-js-1.0.0.jar">
<element id="module-output" name="markdown-parser.jsMain" />
</root>
</artifact>
</component>

View File

@@ -0,0 +1,8 @@
<component name="ArtifactManager">
<artifact type="jar" name="markdown-parser-jvm-0.1.0-SNAPSHOT">
<output-path>$PROJECT_DIR$/build/libs</output-path>
<root id="archive" name="markdown-parser-jvm-0.1.0-SNAPSHOT.jar">
<element id="module-output" name="markdown-parser.jvmMain" />
</root>
</artifact>
</component>

View File

@@ -0,0 +1,8 @@
<component name="ArtifactManager">
<artifact type="jar" name="markdown-parser-jvm-1.0.0">
<output-path>$PROJECT_DIR$/build/libs</output-path>
<root id="archive" name="markdown-parser-jvm-1.0.0.jar">
<element id="module-output" name="markdown-parser.jvmMain" />
</root>
</artifact>
</component>

2
.idea/misc.xml generated
View File

@@ -3,7 +3,7 @@
<component name="FrameworkDetectionExcludesConfiguration">
<file type="web" url="file://$PROJECT_DIR$" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" project-jdk-name="17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
<component name="accountSettings">

7
LICENSE.txt Normal file
View File

@@ -0,0 +1,7 @@
Copyright 2025 H.Nentjes
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -1,9 +1,14 @@
import com.vanniktech.maven.publish.SonatypeHost
plugins {
kotlin("multiplatform") version "2.1.10"
kotlin("multiplatform") version "2.2.21"
signing
id("org.jetbrains.dokka") version "2.0.0"
id("com.vanniktech.maven.publish") version "0.31.0"
}
group = "nl.astraeus"
version = "0.1.0-SNAPSHOT"
version = "1.0.1"
repositories {
mavenCentral()
@@ -16,45 +21,80 @@ repositories {
}
kotlin {
jvmToolchain(17)
jvmToolchain(11)
jvm()
js {
binaries.executable()
browser {
distribution {
outputDirectory.set(File("$projectDir/web/"))
}
}
binaries.library()
browser {}
}
sourceSets {
val commonMain by getting {
dependencies {
api("nl.astraeus:kotlin-simple-logging:1.1.1")
api("nl.astraeus:kotlin-css-generator:1.0.10")
}
}
val commonTest by getting
val jvmMain by getting {
val commonTest by getting {
dependencies {
implementation("io.undertow:undertow-core:2.3.14.Final")
implementation("org.jetbrains.kotlinx:kotlinx-html-jvm:0.11.0")
implementation("org.xerial:sqlite-jdbc:3.32.3.2")
implementation("com.zaxxer:HikariCP:4.0.3")
implementation("nl.astraeus:simple-jdbc-stats:1.6.1") {
exclude(group = "org.slf4j", module = "slf4j-api")
}
}
}
val jvmTest by getting {
dependencies {
}
}
val jsMain by getting {
dependencies {
implementation("nl.astraeus:kotlin-komponent:1.2.5")
implementation(kotlin("test"))
}
}
val jvmTest by getting
val jsTest by getting
}
}
publishing {
repositories {
maven {
name = "gitea"
setUrl("https://gitea.astraeus.nl/api/packages/rnentjes/maven")
credentials {
val giteaUsername: String? by project
val giteaPassword: String? by project
username = giteaUsername
password = giteaPassword
}
}
mavenLocal()
}
}
tasks.withType<AbstractPublishToMaven> {
dependsOn(tasks.withType<Sign>())
}
signing {
sign(publishing.publications)
}
mavenPublishing {
publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)
signAllPublications()
coordinates(group.toString(), name, version.toString())
pom {
name = "markdown-parser"
description = "Markdown parser"
inceptionYear = "2025"
url = "https://gitea.astraeus.nl/rnentjes/markdown-parser"
licenses {
license {
name = "MIT"
url = "https://gitea.astraeus.nl/rnentjes/markdown-parser"
}
}
developers {
developer {
id = "rnentjes"
name = "Rien Nentjes"
email = "info@nentjes.com"
}
}
scm {
url = "https://gitea.astraeus.nl/rnentjes/markdown-parser"
}
}
}

View File

@@ -1 +0,0 @@
kotlin.code.style=official

View File

@@ -1,5 +1,21 @@
plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "0.5.0"
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
val REPO_NAME = "dummy so the gitea template compiles, please remove"
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
}
plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0"
}
rootProject.name = "markdown-parser"

View File

@@ -0,0 +1,77 @@
package nl.astraeus.markdown.parser
sealed class MarkdownPart {
data object NewLine : MarkdownPart()
data object PageBreak : MarkdownPart()
sealed class ParagraphPart() {
data class Text(
val text: String
) : ParagraphPart()
data object LineBreak : ParagraphPart()
data class Link(
val url: String,
val label: String? = null,
val title: String? = null,
) : ParagraphPart()
data class Image(
val alt: String,
val src: String,
val url: String? = null,
) : ParagraphPart()
data class Bold(
val text: String
) : ParagraphPart()
data class Italic(
val text: String
) : ParagraphPart()
class BoldItalic(
val text: String
) : ParagraphPart()
class StrikeThrough(
val text: String
) : ParagraphPart()
class InlineCode(
val text: String
) : ParagraphPart()
}
data class Paragraph(
val parts: List<ParagraphPart>
) : MarkdownPart()
data class Header(
val text: String,
val size: Int
) : MarkdownPart()
data class UnorderedList(
val lines: List<String>,
) : MarkdownPart()
data class OrderedList(
val lines: List<String>,
) : MarkdownPart()
data class CodeBlock(
val text: String,
val language: String
) : MarkdownPart()
data class Table(
val headers: List<String>,
val rows: List<List<String>>,
) : MarkdownPart()
class Ruler() : MarkdownPart()
}

View File

@@ -0,0 +1,174 @@
package nl.astraeus.markdown.parser
import nl.astraeus.markdown.parser.MarkdownPart.ParagraphPart.*
private enum class ParType {
TEXT,
LINK_LABEL,
LINK_URL,
LINK_TITLE,
LINK_END,
BOLD,
ITALIC,
BOLD_ITALIC,
STRIKETHROUGH,
INLINE_CODE,
IMAGE_ALT,
IMAGE_SRC,
LINK_IMAGE_ALT,
LINK_IMAGE_SRC,
LINK_IMAGE_LINK,
}
private typealias ParagraphData = MutableMap<ParType, String>
private data class ParState(
val fromType: ParType,
val text: String,
val toType: ParType,
val out: (ParagraphData) -> MarkdownPart.ParagraphPart? = { _ -> null }
)
private val states = listOf(
// Image with link
ParState(ParType.TEXT, "[![", ParType.LINK_IMAGE_ALT) { data ->
Text(data[ParType.TEXT]!!)
},
ParState(ParType.LINK_IMAGE_ALT, "](", ParType.LINK_IMAGE_SRC),
ParState(ParType.LINK_IMAGE_SRC, ")](", ParType.LINK_IMAGE_LINK),
ParState(ParType.LINK_IMAGE_LINK, ")", ParType.TEXT) { data ->
Image(
data[ParType.LINK_IMAGE_ALT]!!,
data[ParType.LINK_IMAGE_SRC]!!,
data[ParType.LINK_IMAGE_LINK],
)
},
// Image without link
ParState(ParType.TEXT, "![", ParType.IMAGE_ALT) { data ->
Text(data[ParType.TEXT]!!)
},
ParState(ParType.IMAGE_ALT, "](", ParType.IMAGE_SRC),
ParState(ParType.IMAGE_SRC, ")", ParType.TEXT) { data ->
Image(
data[ParType.IMAGE_ALT]!!,
data[ParType.IMAGE_SRC]!!,
)
},
// Links
ParState(ParType.TEXT, "[", ParType.LINK_LABEL) { data ->
Text(data[ParType.TEXT]!!)
},
ParState(ParType.LINK_LABEL, "](", ParType.LINK_URL),
ParState(ParType.LINK_LABEL, "]", ParType.LINK_URL) { data ->
Text(data[ParType.LINK_LABEL]!!)
},
ParState(ParType.LINK_URL, ")", ParType.TEXT) { data ->
Link(data[ParType.LINK_URL]!!, data[ParType.LINK_LABEL])
},
ParState(ParType.LINK_URL, "\"", ParType.LINK_TITLE),
ParState(ParType.LINK_TITLE, "\"", ParType.LINK_END),
ParState(ParType.LINK_END, ")", ParType.TEXT) { data ->
Link(
data[ParType.LINK_URL]!!,
data[ParType.LINK_LABEL],
data[ParType.LINK_TITLE],
)
},
ParState(ParType.TEXT, "***", ParType.BOLD_ITALIC) { data ->
Text(data[ParType.TEXT]!!)
},
ParState(ParType.BOLD_ITALIC, "***", ParType.TEXT) { data ->
BoldItalic(data[ParType.BOLD_ITALIC]!!)
},
ParState(ParType.TEXT, "~~", ParType.STRIKETHROUGH) { data ->
Text(data[ParType.TEXT]!!)
},
ParState(ParType.STRIKETHROUGH, "~~", ParType.TEXT) { data ->
StrikeThrough(data[ParType.STRIKETHROUGH]!!)
},
ParState(ParType.TEXT, "**", ParType.BOLD) { data ->
Text(data[ParType.TEXT]!!)
},
ParState(ParType.BOLD, "**", ParType.TEXT) { data ->
Bold(data[ParType.BOLD]!!)
},
ParState(ParType.TEXT, "*", ParType.ITALIC) { data ->
Text(data[ParType.TEXT]!!)
},
ParState(ParType.ITALIC, "*", ParType.TEXT) { data ->
BoldItalic(data[ParType.ITALIC]!!)
},
ParState(ParType.TEXT, "`", ParType.INLINE_CODE) { data ->
Text(data[ParType.TEXT]!!)
},
ParState(ParType.INLINE_CODE, "`", ParType.TEXT) { data ->
InlineCode(data[ParType.INLINE_CODE]!!)
},
)
private fun String.test(index: Int, value: String): Boolean {
return this.length > index + value.length && this.substring(index, index + value.length) == value
}
fun parseParagraph(text: String): MarkdownPart.Paragraph {
val result = mutableListOf<MarkdownPart.ParagraphPart>()
val buffer = StringBuilder()
var type = ParType.TEXT
val data: ParagraphData = mutableMapOf()
var index = 0
var activeStates = states.filter { it.fromType == type }
while (index < text.length) {
var found = false
for (state in activeStates) {
if (state.fromType == type && text.test(index, state.text)) {
data[state.fromType] = buffer.toString()
buffer.clear()
state.out(data)?.let {
if (it !is Text || it.text.isNotBlank()) {
result.add(it)
}
}
type = state.toType
index += state.text.length
found = true
activeStates = states.filter { it.fromType == type }
break
}
}
if (!found) {
val ch = text[index]
if (ch == '\n') {
// Markdown hard line break: two or more spaces at end of line
if (buffer.length >= 2 && buffer.endsWith(" ")) {
val textBefore = buffer.substring(0, buffer.length - 2)
if (textBefore.isNotEmpty()) {
result.add(Text(textBefore))
}
result.add(LineBreak)
buffer.clear()
} else {
// Keep original behavior for soft breaks (collapse later in HTML)
buffer.append(ch)
}
} else {
buffer.append(ch)
}
index++
}
}
if (buffer.isNotEmpty()) {
result.add(Text(buffer.toString()))
}
return MarkdownPart.Paragraph(result)
}

View File

@@ -0,0 +1,166 @@
package nl.astraeus.markdown.parser
enum class MarkdownType {
CODE,
PARAGRAPH,
ORDERED_LIST,
UNORDERED_LIST,
TABLE,
}
fun markdown(text: String): List<MarkdownPart> {
val lines = text.lines()
val parts = mutableListOf<MarkdownPart>()
var language = ""
var type = MarkdownType.PARAGRAPH
var listIndex = 1
var index = 0
val buffer = StringBuilder()
fun parseBuffer() {
if (buffer.isNotBlank()) {
parts.addAll(handleBuffer(type, buffer.toString(), language))
}
buffer.clear()
type = MarkdownType.PARAGRAPH
language = ""
}
while (index < lines.size) {
val rawLine = lines[index]
val line = rawLine.trim()
//println("BUFFER [${buffer.length}] TYPE ${type} \t LINE - ${line}")
when {
type == MarkdownType.ORDERED_LIST -> {
if (!line.startsWith("${listIndex++}.")) {
parseBuffer()
continue
} else {
buffer.append(line.substring(2))
buffer.append("\n")
}
}
type == MarkdownType.UNORDERED_LIST -> {
if (!line.startsWith("- ") &&
!line.startsWith("* ")
) {
parseBuffer()
continue
} else {
buffer.append(line.substring(2))
buffer.append("\n")
}
}
type == MarkdownType.TABLE -> {
if (!line.startsWith("|")) {
parseBuffer()
continue
} else {
buffer.append(line)
buffer.append("\n")
}
}
type == MarkdownType.PARAGRAPH && line.isBlank() -> {
buffer.append("\n")
parseBuffer()
}
line.startsWith("```") -> {
if (type != MarkdownType.CODE) {
parseBuffer()
type = MarkdownType.CODE
language = line.substring(3).trim()
} else {
parseBuffer()
}
}
type == MarkdownType.CODE -> {
buffer.append(rawLine)
buffer.append("\n")
index++
continue
}
line.startsWith("1.") -> {
parseBuffer()
type = MarkdownType.ORDERED_LIST
listIndex = 2
buffer.append(line.substring(2))
buffer.append("\n")
}
line.startsWith("- ") || line.startsWith("* ") -> {
parseBuffer()
type = MarkdownType.UNORDERED_LIST
buffer.append(line.substring(2))
buffer.append("\n")
}
line.startsWith("|") -> {
parseBuffer()
type = MarkdownType.TABLE
buffer.append(line)
buffer.append("\n")
}
line.startsWith("---") -> {
parseBuffer()
parts.add(MarkdownPart.Ruler())
}
line.startsWith("#") -> {
parseBuffer()
val headerLevel = line.takeWhile { it == '#' }.length
val headerText = line.substring(headerLevel).trim()
parts.add(MarkdownPart.Header(headerText, headerLevel))
}
line == "[break]" -> {
parseBuffer()
parts.add(MarkdownPart.PageBreak)
}
else -> {
// Preserve trailing spaces for hard line breaks (two spaces at end of line)
buffer.append(rawLine)
buffer.append("\n")
}
}
index++
}
parseBuffer()
return parts
}
private fun handleBuffer(
type: MarkdownType,
text: String,
language: String = ""
): List<MarkdownPart> = when (type) {
MarkdownType.CODE -> {
listOf(MarkdownPart.CodeBlock(text, language))
}
MarkdownType.PARAGRAPH -> {
listOf(parseParagraph(text))
}
MarkdownType.ORDERED_LIST -> {
listOf(MarkdownPart.OrderedList(text.lines()))
}
MarkdownType.UNORDERED_LIST -> {
listOf(MarkdownPart.UnorderedList(text.lines()))
}
MarkdownType.TABLE -> {
parseTable(text)
}
}

View File

@@ -0,0 +1,48 @@
package nl.astraeus.markdown.parser
fun parseTable(text: String): List<MarkdownPart> {
val lines = text.lines().map { it.trim() }.filter { it.isNotEmpty() }
fun parseCells(line: String): List<String> {
val trimmed = line.trim().trim('|')
return if (trimmed.isEmpty()) emptyList() else trimmed.split("|").map { it.trim() }
}
fun isSeparatorRow(cells: List<String>): Boolean {
if (cells.isEmpty()) return false
return cells.all { cell ->
val dashCount = cell.count { it == '-' }
val cleaned = cell.replace("-", "").replace("|", "").trim()
dashCount >= 3 && cleaned.isEmpty()
}
}
return if (lines.size < 2) {
// Not enough lines to be a table, fallback to code block
listOf(MarkdownPart.CodeBlock(text, "table"))
} else {
val headerCells = parseCells(lines.first())
val sepCells = parseCells(lines[1])
if (headerCells.isEmpty() || !isSeparatorRow(sepCells)) {
// Invalid table format, fallback to code block
listOf(MarkdownPart.CodeBlock(text, "table"))
} else {
val colCount = headerCells.size
val rows = mutableListOf<List<String>>()
for (i in 2 until lines.size) {
val rowCells = parseCells(lines[i]).toMutableList()
// Normalize column count to headers size
if (rowCells.size < colCount) {
while (rowCells.size < colCount) rowCells.add("")
} else if (rowCells.size > colCount) {
// Trim extras
while (rowCells.size > colCount) rowCells.removeAt(rowCells.lastIndex)
}
rows.add(rowCells)
}
listOf(MarkdownPart.Table(headers = headerCells, rows = rows))
}
}
}

View File

@@ -0,0 +1,48 @@
package nl.astraeus.markdown.parser
import kotlin.test.Test
class ParseTest {
@Test
fun testParagraph() {
val input = """
Dit is een **test**, laat ***mij*** maar eens zien!
link: [NOS](www.nos.nl "Nos title") of zo.
- link: [NU](www.nu.nl "Nu site") of zo.
""".trimIndent()
val md = markdown(input)
printMarkdownParts(md)
}
@Test
fun testImage() {
val input = """
[![test2](https://upload.wikimedia.org/wikipedia/commons.png)](https://upload.wikimedia.org/wikipedia/commons.png)
""".trimIndent()
val md = markdown(input)
printMarkdownParts(md)
}
private fun printMarkdownParts(md: List<MarkdownPart>) {
for (part in md) {
if (part is MarkdownPart.Paragraph) {
for (para in part.parts) {
println("PARA: ${para::class.simpleName} - ${para.toString().take(75)}")
}
} else {
println("PART: ${part::class.simpleName} - ${part.toString().take(75)}")
}
}
}
}

View File

@@ -0,0 +1,21 @@
package nl.astraeus.markdown.parser
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class TestParagraph {
@Test
fun testBold() {
val input = "Text **bold** Text"
val result = parseParagraph(input)
assertEquals(3, result.parts.size)
assertTrue(result.parts[0] is MarkdownPart.ParagraphPart.Text)
assertTrue(result.parts[1] is MarkdownPart.ParagraphPart.Bold)
assertTrue(result.parts[2] is MarkdownPart.ParagraphPart.Text)
}
}

View File

@@ -1,39 +0,0 @@
package nl.astraeus.tmpl
import com.zaxxer.hikari.HikariConfig
import nl.astraeus.logger.Logger
import nl.astraeus.tmpl.db.Database
val log = Logger()
val REPO_NAME = "dummy so the gitea template compiles, please remove"
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=6001:jdbc:sqlite:data/markdown-parser.db"
username = "sa"
password = ""
maximumPoolSize = 25
isAutoCommit = false
validate()
})
}

View File

@@ -1,73 +0,0 @@
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

@@ -1,105 +0,0 @@
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()
}
}
}
}