initial commit

This commit is contained in:
Hartmut Bauer committed 2025-03-12 00:30:40 +01:00
commit c81f3ea29e
21 files changed
+903

No files matched your search

@@ -0,0 +1,40 @@
package net.keinekekse.config
import java.io.InputStream
import java.util.*
private const val CONFIG_FILE_NAME = "config.properties"
private const val ERROR_LOADING_CONFIG = "Could not load configuration from $CONFIG_FILE_NAME"
// TODO mit WAR testen wg. Pfad zu config.properties
/**
* Projekt-Properties
*
* Die Datei ```config.properties``` wird aus dem JAR-root geladen.
* (-> in ```main/resources``` anlegen)
*
* ```
* val prop: String = ConfigProperties.get("some.prop")
* ```
*/
object ConfigProperties {
private val properties = Properties()
init {
loadConfigFile()
}
private fun loadConfigFile() {
val configFileStream: InputStream? =
javaClass.classLoader.getResourceAsStream(CONFIG_FILE_NAME)
if (configFileStream == null) {
throw IllegalStateException(ERROR_LOADING_CONFIG)
}
configFileStream.use {
properties.load(it)
}
}
fun get(key: String): String = properties.getProperty(key)
}
@@ -0,0 +1,94 @@
package net.keinekekse.exec
import java.io.File
import java.io.IOException
import java.util.concurrent.TimeUnit
class CommandExecutionException(message: String) : Exception(message)
private const val IO_EXCEPTION_CODE = 0x10000
private const val INTERRUPTED_EXCEPTION_CODE = 0x10001
private const val GENERIC_EXCEPTION_CODE = 0x10002
/**
* Runs program in list [arguments] and saves result to [stdout], [stderr] and [rc].
*
* A return code of >=0x10000 in [rc] signals an exception with the stack trace
* in [stderr].
*
* ```
* val runResult = ExecutionResult(listOf("ebook-meta", "/path/to/epub", "--title", "New Title"))
* println("RC = ${runResult.rc}")
* ```
* @param arguments program and arguments to run
* @param throwAtError throw exception at `rc != 0` (defaults to true)
* @param workingDir working directory for the program (defaults to ".")
* @param timeoutSec timeout in seconds (defaults to 60)
*
* @property stdout program output to STDOUT
* @property stderr program output to STDERR
* @property rc return code
*
* @throws CommandExecutionException if [throwAtError] is true and [rc] is not 0
*/
class ExecutionResult(
val arguments: List<String>,
val throwAtError: Boolean = true,
val workingDir: String = ".",
val timeoutSec: Long = 60,
) {
val stdout: String
val stderr: String
val rc: Int
init {
val executionResult = try {
executeProcess()
} catch (e: Exception) {
handleProcessException(e)
}
stdout = executionResult.first
stderr = executionResult.second
rc = executionResult.third
if (rc != 0) {
logExecutionError()
if (throwAtError) {
throw CommandExecutionException("Error executing ${arguments}\nRC = $rc")
}
}
}
private fun executeProcess(): Triple<String, String, Int> {
return ProcessBuilder(arguments)
.directory(File(workingDir))
.start()
.run {
waitFor(timeoutSec, TimeUnit.SECONDS)
Triple(
inputStream.bufferedReader().use { it.readText() },
errorStream.bufferedReader().use { it.readText() },
exitValue()
)
}
}
private fun handleProcessException(exception: Exception): Triple<String, String, Int> {
val errorCode = when (exception) {
is IOException -> IO_EXCEPTION_CODE
is InterruptedException -> INTERRUPTED_EXCEPTION_CODE
else -> GENERIC_EXCEPTION_CODE
}
val errorMessage =
"${exception::class.simpleName}: ${exception.message}\n${exception.stackTraceToString()}"
return Triple("", errorMessage, errorCode)
}
private fun logExecutionError() {
println("\n*** ERROR executing $arguments")
println("RC = $rc")
println("STDOUT:\n$stdout")
println("STDERR:\n$stderr")
println("***")
}
}
@@ -0,0 +1,218 @@
package net.keinekekse.version
import java.io.File
import java.io.IOException
import java.net.URI
import java.time.Instant
import java.time.format.DateTimeFormatter
import java.time.format.DateTimeParseException
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import java.util.jar.Manifest
import java.util.regex.Pattern
/**
* Version-Info für .jar oder .war
*
*
* Die Versions-Informationen werden aus der Datei META-INF/MANIFEST.MF gelesen.
*
* Fürs Setzen der Werte in der MANIFEST.MF ist das Gradle version-plugin verantwortlich.
*
* Das Auslesen der Datei funktioniert für .jar (liegt in /META-INF) und .war (liegt in
* /WEB-INF/classes/META-INF) die auf einem Wildfly-Server deployed wurden (für alles Andere wäre es
* zu testen)
*
* @param clazz Klasse aus der jeweiligen .jar oder .war
*
* ```
* val version = Version(this::class.java)
* val libraryVersion = Version(Version::class.java)
* ```
*/
class Version(clazz: Class<*>) {
val versionObject: VersionNumber
private val attributes: MutableMap<String, String>
init {
val manifestPath = determineManifestPath(clazz)
attributes = loadManifestAttributes(manifestPath)
versionObject = initializeVersionNumbers()
}
// TODO noch nicht für WAR getestet!!
private fun determineManifestPath(clazz: Class<*>): String {
val classResourcePath = clazz.getResource("${clazz.simpleName}.class")?.toString() ?: ""
val baseFolder = if (classResourcePath.contains(WEB_INF_PATH)) {
classResourcePath.substringBefore(WEB_INF_PATH) + MANIFEST_FILE_PATH
} else {
classResourcePath.substringBefore(
clazz.name.replace(
'.',
File.separatorChar
) + CLASS_EXTENSION
) + MANIFEST_FILE_PATH
}
return baseFolder
}
private fun loadManifestAttributes(manifestPath: String): MutableMap<String, String> {
val attributes = ConcurrentHashMap<String, String>()
try {
URI(manifestPath).toURL().openStream().use {
val manifest = Manifest(it)
manifest.mainAttributes.forEach { key, value ->
attributes[key.toString()] = value.toString()
}
}
} catch (e: IOException) {
throw RuntimeException("Failed to read MANIFEST.MF at $manifestPath", e)
}
return attributes
}
private fun initializeVersionNumbers(): VersionNumber {
return VersionNumber(
attributes[PROP_VERSION] ?: "",
attributes[PROP_BUILD_TIMESTAMP] ?: "",
attributes[PROP_BUILD_NUMBER] ?: ""
)
}
val infoString: String
get() = "$displayname $fullVersion $copyright"
val displayname: String
get() = attributes[PROP_DISPLAYNAME] ?: ""
val copyright: String
get() = attributes[PROP_COPYRIGHT] ?: ""
val name: String
get() = attributes[PROP_NAME] ?: ""
val description: String
get() = attributes[PROP_DESCRIPTION] ?: ""
val version: String
get() = versionObject.version
val fullVersion: String
get() = versionObject.fullVersion
val major: Int
get() = versionObject.major
val minor: Int
get() = versionObject.minor
val patch: Int
get() = versionObject.patch
val identifier: String
get() = versionObject.identifier
val build: String
get() = versionObject.build
val buildTimestamp: String
get() = versionObject.buildTimestamp
companion object {
private const val PROP_NAME = "module-name"
private const val PROP_DISPLAYNAME = "displayname"
private const val PROP_COPYRIGHT = "copyright"
private const val PROP_DESCRIPTION = "description"
private const val PROP_VERSION = "version"
private const val PROP_BUILD_TIMESTAMP = "build-timestamp"
private const val PROP_BUILD_NUMBER = "build-number"
private const val MANIFEST_FILE_PATH = "META-INF/MANIFEST.MF"
private const val CLASS_EXTENSION = ".class"
private const val WEB_INF_PATH = "WEB-INF/classes"
}
}
/**
* Versions-Nr. zerlegen<br></br>
* <br></br>
* Folgendes Format wird verstanden: [v]int[.int[.int]][-|.alpha|rc ..][-|.build-Nr. o.ä.]<br></br>
* Ergebnis:<br></br>
*
* <pre>`String.format("%d.%d.%d-%s-%s", getMajor(), getMinor(), getPatch(), getIdentifier(), getBuild());
`</pre> *
*
* <br></br>
* Außerdem wird Der Maven Build-Timestamp als String des Formats `"yyyy-MM-dd @HH:mm:ss"`
* sowie als `long mavenEpochSeconds` gespeichert. Falls die übergebene Version keinen
* Build-String enthält wird statt dessen `mavenEpochSeconds` verwendet.
*/
class VersionNumber(val version: String, timestamp: String, buildNumber: String = "") {
private companion object {
const val VERSION_REGEX = """v?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:[-.](\w+))?(?:[-.](.*))?"""
val DATE_TIME_FORMATTER: DateTimeFormatter =
DateTimeFormatter.ofPattern("yyyy-MM-dd @HH:mm:ss")
}
var major: Int = 0
private set
var minor: Int = 0
private set
var patch: Int = 0
private set
var identifier: String = "" // e.g., "alpha", etc.
private set
var build: String = "" // Build metadata, e.g., "Timestamp" or "SNAPSHOT"
private set
var buildTimestamp: String = ""
private set
var buildEpochSeconds: Long = 0
private set
init {
parseVersion(version)
initializeBuildTimestamp(timestamp)
if (buildNumber.isNotEmpty()) {
build = buildNumber
}
}
val fullVersion: String
get() = if (identifier.isEmpty()) {
"$major.$minor.$patch (build $build)"
} else {
"$major.$minor.$patch-$identifier-$build"
}
private fun parseVersion(version: String) {
val pattern = Pattern.compile(VERSION_REGEX)
val matcher = pattern.matcher(version)
if (matcher.matches()) {
major = parseVersionComponent(matcher.group(1))
minor = parseVersionComponent(matcher.group(2))
patch = parseVersionComponent(matcher.group(3))
identifier = matcher.group(4) ?: ""
build = resolveBuildMetadata(matcher.group(5) ?: "")
}
}
private fun parseVersionComponent(value: String?) = value?.toIntOrNull() ?: 0
private fun resolveBuildMetadata(extractedBuild: String): String {
return if (extractedBuild.isNotEmpty()) extractedBuild else buildEpochSeconds.toString()
}
private fun initializeBuildTimestamp(timestamp: String) {
try {
val instant = Instant.parse(timestamp)
val date = instant.atZone(TimeZone.getDefault().toZoneId())
buildEpochSeconds = date.toEpochSecond()
buildTimestamp = date.format(DATE_TIME_FORMATTER)
} catch (e: DateTimeParseException) {
buildTimestamp = "Invalid timestamp"
}
}
}