PassbirdLauncher.kt
package de.pflugradts.passbird.application.boot.launcher
import de.pflugradts.passbird.application.RunContext
import de.pflugradts.passbird.application.UserInterfaceAdapterPort
import de.pflugradts.passbird.application.boot.Bootable
import de.pflugradts.passbird.application.boot.main.ApplicationGraph
import de.pflugradts.passbird.application.boot.migration.MigrationGraph
import de.pflugradts.passbird.application.boot.setup.SetupGraph
import de.pflugradts.passbird.application.configuration.ReadableConfiguration
import de.pflugradts.passbird.application.process.migration.MigrationRequest
import de.pflugradts.passbird.application.process.migration.PreLaunchMigrationLocator
import de.pflugradts.passbird.application.toDirectory
import de.pflugradts.passbird.application.toFileName
import de.pflugradts.passbird.application.util.SystemOperation
import de.pflugradts.passbird.domain.model.shell.Shell.Companion.shellOf
import de.pflugradts.passbird.domain.model.transfer.Output
import de.pflugradts.passbird.domain.model.transfer.Output.Companion.emptyOutput
import de.pflugradts.passbird.domain.model.transfer.Output.Companion.outputOf
import de.pflugradts.passbird.domain.model.transfer.OutputFormatting.DEFAULT
import de.pflugradts.passbird.domain.model.transfer.OutputFormatting.ERROR_MESSAGE
import de.pflugradts.passbird.domain.model.transfer.OutputFormatting.NEST
import de.pflugradts.passbird.domain.model.transfer.OutputFormatting.OPERATION_ABORTED
import de.pflugradts.passbird.domain.model.transfer.OutputFormatting.SPECIAL
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import java.time.temporal.ChronoUnit
import java.util.jar.JarInputStream
private const val COPYRIGHT = "\tCopyright 2020 - 2025 Christian Pflugradt"
private const val LICENSE = """${'\t'}This software is licensed under the Apache License, Version 2.0 (APLv2)
${'\t'}You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0
"""
private const val SLOGAN = "\tguarding your digital nest with secure feathers"
class PassbirdLauncher(
private val configuration: ReadableConfiguration,
private val preLaunchMigrationLocator: PreLaunchMigrationLocator,
private val runContext: RunContext,
private val userInterfaceAdapterPort: UserInterfaceAdapterPort,
private val systemOperation: SystemOperation,
private val setupBoot: (RunContext) -> Unit = { SetupGraph(it).bootable.boot() },
private val migrationBoot: (RunContext, MigrationRequest) -> Unit = { context, request ->
MigrationGraph(context, request).bootable.boot()
},
private val applicationBoot: (RunContext) -> Unit = { ApplicationGraph(it).bootable.boot() },
) : Bootable {
private val keyStoreLocation get() = configuration.adapter.keyStore.location
private val ansiEscapeCodesEnabled get() = configuration.adapter.userInterface.ansiEscapeCodes.enabled
override fun boot() {
sendLicenseNotice()
sendBanner()
if (!keystoreExists()) {
setupBoot(runContext)
} else {
preLaunchMigrationLocator.detect().let { migrationRequest ->
if (migrationRequest.required) migrationBoot(runContext, migrationRequest) else applicationBoot(runContext)
}
}
}
private fun keystoreExists() = keyStoreLocation.isNotEmpty() &&
systemOperation.exists(
systemOperation.resolvePath(
keyStoreLocation.toDirectory(),
ReadableConfiguration.KEYSTORE_FILENAME.toFileName(),
),
)
private fun sendBanner() {
userInterfaceAdapterPort.sendLineBreak()
userInterfaceAdapterPort.send(outputOf(shellOf(if (ansiEscapeCodesEnabled) coloredBanner() else plainBanner())))
userInterfaceAdapterPort.send(outputOf(shellOf("\t${javaClass.getPackage().implementationVersion}"), NEST), ageHint())
userInterfaceAdapterPort.sendLineBreak()
userInterfaceAdapterPort.send(outputOf(shellOf(SLOGAN), SPECIAL))
userInterfaceAdapterPort.sendLineBreak()
}
private fun ageHint(): Output {
manifestAttributes()?.getValue("Build-Date")?.also { buildDate ->
ChronoUnit.DAYS.between(
LocalDate.parse(buildDate, DateTimeFormatter.ofPattern("yyyy-MM-dd")),
LocalDate.now(),
).let { age ->
when {
age <= 7 -> "This version is up to date." to DEFAULT
age <= 29 -> "This version is $age days old. Perhaps there's a newer version?" to DEFAULT
age <= 89 -> "This version is $age days old. Perhaps there's a newer version?" to OPERATION_ABORTED
else -> "This version is outdated." to ERROR_MESSAGE
}
}.let { (message, formatting) -> return@ageHint outputOf(shellOf(" | $message"), formatting) }
}
return emptyOutput()
}
private fun sendLicenseNotice() {
userInterfaceAdapterPort.sendLineBreak()
userInterfaceAdapterPort.send(outputOf(shellOf(COPYRIGHT)))
userInterfaceAdapterPort.send(outputOf(shellOf(LICENSE)))
}
private fun plainBanner() = byteArrayOf(
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x2C, 0x5F, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x5F, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5F, 0x20, 0x20, 0x20, 0x20, 0x3E, 0x27, 0x20, 0x29, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x5F, 0x20, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x7C, 0x20, 0x5F, 0x5F, 0x5F, 0x20, 0x5C, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7C, 0x20, 0x7C, 0x20, 0x20, 0x20, 0x28, 0x20, 0x28, 0x20, 0x5C, 0x20, 0x20, 0x20,
0x20, 0x20, 0x7C, 0x20, 0x7C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x7C, 0x20, 0x7C, 0x5F, 0x2F, 0x20, 0x2F, 0x5F, 0x20, 0x5F, 0x20, 0x5F,
0x5F, 0x5F, 0x20, 0x5F, 0x5F, 0x5F, 0x7C, 0x20, 0x7C, 0x5F, 0x5F, 0x20, 0x20, 0x5F, 0x20, 0x5F, 0x20, 0x5F, 0x5F, 0x20, 0x5F, 0x5F,
0x7C, 0x20, 0x7C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x7C, 0x20, 0x20, 0x5F, 0x5F, 0x2F, 0x20, 0x5F, 0x60, 0x20, 0x2F, 0x20, 0x5F, 0x5F,
0x2F, 0x20, 0x5F, 0x5F, 0x7C, 0x20, 0x27, 0x5F, 0x20, 0x5C, 0x7C, 0x20, 0x7C, 0x20, 0x27, 0x5F, 0x5F, 0x2F, 0x20, 0x5F, 0x60, 0x20,
0x7C, 0x0A, 0x20, 0x20, 0x20, 0x20, 0x7C, 0x20, 0x7C, 0x20, 0x7C, 0x20, 0x28, 0x5F, 0x7C, 0x20, 0x5C, 0x5F, 0x5F, 0x20, 0x5C, 0x5F,
0x5F, 0x20, 0x5C, 0x20, 0x7C, 0x5F, 0x29, 0x20, 0x7C, 0x20, 0x7C, 0x20, 0x7C, 0x20, 0x7C, 0x20, 0x28, 0x5F, 0x7C, 0x20, 0x7C, 0x0A,
0x20, 0x20, 0x20, 0x20, 0x5C, 0x5F, 0x7C, 0x20, 0x20, 0x5C, 0x5F, 0x5F, 0x2C, 0x5F, 0x7C, 0x5F, 0x5F, 0x5F, 0x2F, 0x5F, 0x5F, 0x5F,
0x2F, 0x5F, 0x2E, 0x5F, 0x5F, 0x2F, 0x7C, 0x5F, 0x7C, 0x5F, 0x7C, 0x20, 0x20, 0x5C, 0x5F, 0x5F, 0x2C, 0x5F, 0x7C, 0x0A,
)
private fun coloredBanner() = byteArrayOf(
0x0a, 0x1b, 0x5b, 0x33, 0x38, 0x3b, 0x35, 0x3b, 0x33, 0x39, 0x6d, 0x09, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x2c, 0x5f, 0x0a, 0x09, 0x5f, 0x5f, 0x5f,
0x5f, 0x5f, 0x5f, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5f, 0x20, 0x20, 0x20, 0x20, 0x1b,
0x5b, 0x33, 0x38, 0x3b, 0x35, 0x3b, 0x32, 0x32, 0x30, 0x6d, 0x3e, 0x27, 0x20, 0x29, 0x1b, 0x5b, 0x33, 0x38, 0x3b, 0x35, 0x3b, 0x33,
0x39, 0x6d, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x5f, 0x20, 0x0a, 0x09, 0x7c, 0x20, 0x5f, 0x5f, 0x5f, 0x20, 0x5c, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7c, 0x20, 0x7c, 0x20, 0x20, 0x20, 0x1b, 0x5b, 0x33, 0x38, 0x3b, 0x35, 0x3b,
0x31, 0x39, 0x36, 0x6d, 0x28, 0x20, 0x28, 0x20, 0x5c, 0x1b, 0x5b, 0x33, 0x38, 0x3b, 0x35, 0x3b, 0x33, 0x39, 0x6d, 0x20, 0x20, 0x20,
0x20, 0x20, 0x7c, 0x20, 0x7c, 0x0a, 0x09, 0x7c, 0x20, 0x7c, 0x5f, 0x2f, 0x20, 0x2f, 0x5f, 0x20, 0x5f, 0x20, 0x5f, 0x5f, 0x5f, 0x20,
0x5f, 0x5f, 0x5f, 0x7c, 0x20, 0x7c, 0x5f, 0x5f, 0x20, 0x20, 0x5f, 0x20, 0x5f, 0x20, 0x5f, 0x5f, 0x20, 0x5f, 0x5f, 0x7c, 0x20, 0x7c,
0x0a, 0x09, 0x7c, 0x20, 0x20, 0x5f, 0x5f, 0x2f, 0x20, 0x5f, 0x60, 0x20, 0x2f, 0x20, 0x5f, 0x5f, 0x2f, 0x20, 0x5f, 0x5f, 0x7c, 0x20,
0x27, 0x5f, 0x20, 0x5c, 0x7c, 0x20, 0x7c, 0x20, 0x27, 0x5f, 0x5f, 0x2f, 0x20, 0x5f, 0x60, 0x20, 0x7c, 0x0a, 0x09, 0x7c, 0x20, 0x7c,
0x20, 0x7c, 0x20, 0x28, 0x5f, 0x7c, 0x20, 0x5c, 0x5f, 0x5f, 0x20, 0x5c, 0x5f, 0x5f, 0x20, 0x5c, 0x20, 0x7c, 0x5f, 0x29, 0x20, 0x7c,
0x20, 0x7c, 0x20, 0x7c, 0x20, 0x7c, 0x20, 0x28, 0x5f, 0x7c, 0x20, 0x7c, 0x0a, 0x09, 0x5c, 0x5f, 0x7c, 0x20, 0x20, 0x5c, 0x5f, 0x5f,
0x2c, 0x5f, 0x7c, 0x5f, 0x5f, 0x5f, 0x2f, 0x5f, 0x5f, 0x5f, 0x2f, 0x5f, 0x2e, 0x5f, 0x5f, 0x2f, 0x7c, 0x5f, 0x7c, 0x5f, 0x7c, 0x20,
0x20, 0x5c, 0x5f, 0x5f, 0x2c, 0x5f, 0x7c, 0x1b, 0x5b, 0x30, 0x6d, 0x0a,
)
}
private fun manifestAttributes() = {}.javaClass.protectionDomain.codeSource.location
?.let { it.openStream().use { stream -> JarInputStream(stream).manifest } }?.mainAttributes