initial commit
This commit is contained in:
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user