diff --git a/buildSrc/src/main/kotlin/org/kopi/galite/gradle/Utils.kt b/buildSrc/src/main/kotlin/org/kopi/galite/gradle/Utils.kt index 81e7874c4..147975947 100644 --- a/buildSrc/src/main/kotlin/org/kopi/galite/gradle/Utils.kt +++ b/buildSrc/src/main/kotlin/org/kopi/galite/gradle/Utils.kt @@ -14,8 +14,16 @@ * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ + package org.kopi.galite.gradle +import java.io.ByteArrayOutputStream +import java.io.File +import java.time.Instant +import java.time.ZoneOffset +import java.time.format.DateTimeFormatter + +import org.gradle.api.Project import org.gradle.api.artifacts.ExternalModuleDependency import org.gradle.kotlin.dsl.exclude @@ -25,3 +33,53 @@ fun ExternalModuleDependency.excludeWebJars() { "org.webjars.bowergithub.vaadin", "org.webjars.bowergithub.webcomponents") .forEach { group -> exclude(group = group) } } + +/** + * Returns the latest release name : Latest git tag. + */ +fun getLatestReleaseName(project: Project): String { + return try { + listOf("git", "describe", "--tags", "--abbrev=0").runCommand(project.rootDir).trim() + } catch (_: Exception) { + project.version.toString() + } +} + +/** + * Returns the latest release date : Latest git tag commit date. + */ +fun getLatestReleaseDate(project: Project, tag: String): String { + val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss 'UTC'").withZone(ZoneOffset.UTC) + + return try { + // Step 1: get commit date of that tag + val commitDate = listOf("git", "log", "-1", "--format=%aI", tag).runCommand(project.rootDir) + // Step 2: parse and format with timezone as "yyyy-MM-dd HH:mm:ss UTC" + val instant = Instant.parse(commitDate) + val releaseDate = formatter.format(instant) + + releaseDate + } catch (_: Exception) { + formatter.format(Instant.now()) + } +} + +/** + * Runs a command line. + */ +private fun List.runCommand(workingDir: File): String { + val output = ByteArrayOutputStream() + val process = ProcessBuilder(this) + .directory(workingDir) + .redirectErrorStream(true) + .start() + val exitCode = process.waitFor() + + process.inputStream.copyTo(output) + + val result = output.toString().trim() + + if (exitCode != 0) throw RuntimeException("Command failed with exit code $exitCode: $result") + + return result +} diff --git a/galite-data/src/main/kotlin/org/kopi/galite/database/ConnectionOptions.kt b/galite-data/src/main/kotlin/org/kopi/galite/database/ConnectionOptions.kt index 2a382c699..755fe9adf 100644 --- a/galite-data/src/main/kotlin/org/kopi/galite/database/ConnectionOptions.kt +++ b/galite-data/src/main/kotlin/org/kopi/galite/database/ConnectionOptions.kt @@ -2,6 +2,7 @@ package org.kopi.galite.database import org.kopi.galite.util.base.Options +import org.kopi.galite.util.base.Utils import gnu.getopt.Getopt import gnu.getopt.LongOpt @@ -92,20 +93,19 @@ open class ConnectionOptions @JvmOverloads constructor(name: String = "Connectio get() { val parent = super.options val total = arrayOfNulls(parent.size + 11) + System.arraycopy(parent, 0, total, 0, parent.size) - total[parent.size + 0] = " --database, -b: The URL of the database" - total[parent.size + 1] = " --driver, -d: The JDBC driver to use to access the database" - total[parent.size + 2] = " --username, -u: The username for the database" - total[parent.size + 3] = " --password, -p: The password for the database" - total[parent.size + 4] = " --lookupUserId, -U: Lookup user ID in database? [true]" - total[parent.size + 5] = - " --trace, -t: Set the trace level to print database queries before execution (0: none, 1: all but FETCH, 2: all) [0]" - total[parent.size + 6] = - " --properties, -q: These properties override or complete the properties stored in the database." - total[parent.size + 7] = " --schema, -s: The current database schema to be set." - total[parent.size + 8] = " --maxRetries, -r: Set the number of maximum retries if a transaction fails." - total[parent.size + 9] = " --waitMin, -n: The minimum number (inclusive) of milliseconds to wait before retrying a transaction after it has aborted." - total[parent.size + 10] = " --waitMax, -x: The maximum number (inclusive) of milliseconds to wait before retrying a transaction after it has aborted (it has to be greater or equal to waitMin)." + total[parent.size + 0] = " --database, -b: The URL of the database" + total[parent.size + 1] = " --driver, -d: The JDBC driver to use to access the database" + total[parent.size + 2] = " --username, -u: The username for the database" + total[parent.size + 3] = " --password, -p: The password for the database" + total[parent.size + 4] = " --lookupUserId, -U: Lookup user ID in database? [true]" + total[parent.size + 5] = " --trace, -t: Set the trace level to print database queries before execution (0: none, 1: all but FETCH, 2: all) [0]" + total[parent.size + 6] = " --properties, -q: These properties override or complete the properties stored in the database." + total[parent.size + 7] = " --schema, -s: The current database schema to be set." + total[parent.size + 8] = " --maxRetries, -r: Set the number of maximum retries if a transaction fails." + total[parent.size + 9] = " --waitMin, -n: The minimum number (inclusive) of milliseconds to wait before retrying a transaction after it has aborted." + total[parent.size + 10] = " --waitMax, -x: The maximum number (inclusive) of milliseconds to wait before retrying a transaction after it has aborted (it has to be greater or equal to waitMin)." return total } @@ -114,7 +114,9 @@ open class ConnectionOptions @JvmOverloads constructor(name: String = "Connectio get() = "b:d:u:p:Ut::q:s:r:n:x:" + super.shortOptions public override fun version() { - println("Version 2.3B released 17 September 2007") + val releaseInfo = Utils.readReleaseInfo() + + println("Version ${releaseInfo["version"]?.toString().orEmpty()} released at ${releaseInfo["releaseDate"]?.toString().orEmpty()}.") } public override fun usage() { diff --git a/galite-plugins/galite-common-plugin/src/main/kotlin/org/kopi/galite/plugins/common/GradleExtensions.kt b/galite-plugins/galite-common-plugin/src/main/kotlin/org/kopi/galite/plugins/common/GaliteGradleExtensions.kt similarity index 90% rename from galite-plugins/galite-common-plugin/src/main/kotlin/org/kopi/galite/plugins/common/GradleExtensions.kt rename to galite-plugins/galite-common-plugin/src/main/kotlin/org/kopi/galite/plugins/common/GaliteGradleExtensions.kt index c2541d849..5e6058f9a 100644 --- a/galite-plugins/galite-common-plugin/src/main/kotlin/org/kopi/galite/plugins/common/GradleExtensions.kt +++ b/galite-plugins/galite-common-plugin/src/main/kotlin/org/kopi/galite/plugins/common/GaliteGradleExtensions.kt @@ -1,6 +1,6 @@ /* - * Copyright (c) 2013-2025 kopiLeft Services SARL, Tunis TN - * Copyright (c) 1990-2025 kopiRight Managed Solutions GmbH, Wien AT + * Copyright (c) 2013-2026 kopiLeft Services SARL, Tunis TN + * Copyright (c) 1990-2026 kopiRight Managed Solutions GmbH, Wien AT * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -25,7 +25,7 @@ import org.apache.tools.ant.taskdefs.condition.Os import org.gradle.api.Project -open class GradleExtensions(private val project: Project) { +open class GaliteGradleExtensions(private val project: Project) { /** * removes the file with name [fileName] existing in [folder] */ diff --git a/galite-plugins/galite-common-plugin/src/main/kotlin/org/kopi/galite/plugins/common/GradleExtensionsPlugin.kt b/galite-plugins/galite-common-plugin/src/main/kotlin/org/kopi/galite/plugins/common/GradleExtensionsPlugin.kt index 4cfe2acad..7c24723c1 100644 --- a/galite-plugins/galite-common-plugin/src/main/kotlin/org/kopi/galite/plugins/common/GradleExtensionsPlugin.kt +++ b/galite-plugins/galite-common-plugin/src/main/kotlin/org/kopi/galite/plugins/common/GradleExtensionsPlugin.kt @@ -30,7 +30,8 @@ open class GradleExtensionsPlugin: Plugin { * Register the [projectExtensions] extension function for use in the consuming project */ override fun apply(project: Project) { - project.extensions.create("projectExtensions", GradleExtensions::class.java, project) + val projectExtensions = project.extensions.findByType(GaliteGradleExtensions::class.java) + ?: project.extensions.create("projectExtensions", GaliteGradleExtensions::class.java) } /** diff --git a/galite-plugins/galite-dbschema-generator/src/main/kotlin/org/kopi/galite/plugins/DBSchemaGeneratorPlugin.kt b/galite-plugins/galite-dbschema-generator/src/main/kotlin/org/kopi/galite/plugins/DBSchemaGeneratorPlugin.kt index 072695bff..fb856e925 100644 --- a/galite-plugins/galite-dbschema-generator/src/main/kotlin/org/kopi/galite/plugins/DBSchemaGeneratorPlugin.kt +++ b/galite-plugins/galite-dbschema-generator/src/main/kotlin/org/kopi/galite/plugins/DBSchemaGeneratorPlugin.kt @@ -24,7 +24,7 @@ import org.gradle.kotlin.dsl.register import org.jetbrains.kotlin.gradle.tasks.KotlinCompile -import org.kopi.galite.plugins.common.GradleExtensions +import org.kopi.galite.plugins.common.GaliteGradleExtensions import org.kopi.galite.plugins.common.GradleExtensionsPlugin class DBSchemaGeneratorPlugin : GradleExtensionsPlugin() { @@ -41,7 +41,7 @@ class DBSchemaGeneratorPlugin : GradleExtensionsPlugin() { } named("clean") { doLast { - project.extensions.getByType().clean( + project.extensions.getByType().clean( project.layout.projectDirectory.dir(GENERATED_DIRECTORY).asFile.path ) } diff --git a/galite-plugins/galite-dbschema-generator/src/main/kotlin/org/kopi/galite/plugins/DBSchemaGeneratorTask.kt b/galite-plugins/galite-dbschema-generator/src/main/kotlin/org/kopi/galite/plugins/DBSchemaGeneratorTask.kt index 02df4bd35..d690c8b85 100644 --- a/galite-plugins/galite-dbschema-generator/src/main/kotlin/org/kopi/galite/plugins/DBSchemaGeneratorTask.kt +++ b/galite-plugins/galite-dbschema-generator/src/main/kotlin/org/kopi/galite/plugins/DBSchemaGeneratorTask.kt @@ -23,7 +23,6 @@ import org.gradle.kotlin.dsl.get import org.gradle.kotlin.dsl.the import org.kopi.galite.plugins.common.GradleExtensionsPlugin -import org.kopi.galite.plugins.generator.DBSchemaGenerator abstract class DBSchemaGeneratorTask : JavaExec() { init { diff --git a/galite-plugins/galite-factory-generator/src/main/kotlin/org/kopi/galite/plugins/FactoryGeneratorPlugin.kt b/galite-plugins/galite-factory-generator/src/main/kotlin/org/kopi/galite/plugins/FactoryGeneratorPlugin.kt index d58f21455..c6fcd870e 100644 --- a/galite-plugins/galite-factory-generator/src/main/kotlin/org/kopi/galite/plugins/FactoryGeneratorPlugin.kt +++ b/galite-plugins/galite-factory-generator/src/main/kotlin/org/kopi/galite/plugins/FactoryGeneratorPlugin.kt @@ -21,8 +21,8 @@ package org.kopi.galite.plugins import org.gradle.api.Project import org.gradle.kotlin.dsl.getByType import org.gradle.kotlin.dsl.register -import org.kopi.galite.plugins.common.GradleExtensions +import org.kopi.galite.plugins.common.GaliteGradleExtensions import org.kopi.galite.plugins.common.GradleExtensionsPlugin class FactoryGeneratorPlugin : GradleExtensionsPlugin() { @@ -36,7 +36,7 @@ class FactoryGeneratorPlugin : GradleExtensionsPlugin() { register("generateFactory") named("clean") { doLast { - project.extensions.getByType().clean( + project.extensions.getByType().clean( project.layout.projectDirectory.dir(GENERATED_DIRECTORY).asFile.path ) } diff --git a/galite-plugins/galite-optgen/build.gradle.kts b/galite-plugins/galite-optgen/build.gradle.kts new file mode 100644 index 000000000..98c0bd8ad --- /dev/null +++ b/galite-plugins/galite-optgen/build.gradle.kts @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2013-2026 kopiLeft Services SARL, Tunis TN + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +plugins { + `kotlin-dsl` + id("java-gradle-plugin") +} + +gradlePlugin { + plugins { + create("") { + id = "org.kopi.galite-optgen" // Unique plugin ID + implementationClass = "org.kopi.galite.plugins.OptgenPlugin" // The main plugin class + } + } +} diff --git a/galite-plugins/galite-optgen/src/main/kotlin/org/kopi/galite/plugins/OptgenExtention.kt b/galite-plugins/galite-optgen/src/main/kotlin/org/kopi/galite/plugins/OptgenExtention.kt new file mode 100644 index 000000000..c21ec261a --- /dev/null +++ b/galite-plugins/galite-optgen/src/main/kotlin/org/kopi/galite/plugins/OptgenExtention.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2013-2026 kopiLeft Services SARL, Tunis TN + * Copyright (c) 1990-2026 kopiRight Managed Solutions GmbH, Wien AT + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package org.kopi.galite.plugins + +import org.gradle.api.file.FileCollection +import javax.inject.Inject + +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.ListProperty +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFiles +import org.gradle.work.Incremental + +open class OptgenExtention @Inject constructor(objectFactory: ObjectFactory) { + @Input + val parameters: ListProperty = objectFactory.listProperty(OptionParam::class.java) +} + +data class OptionParam(@Input + var release: String, // The release version of the program + @Incremental @InputFiles + var optionFiles: FileCollection) // *Options.xml files to be parsed. diff --git a/galite-plugins/galite-optgen/src/main/kotlin/org/kopi/galite/plugins/OptgenPlugin.kt b/galite-plugins/galite-optgen/src/main/kotlin/org/kopi/galite/plugins/OptgenPlugin.kt new file mode 100644 index 000000000..cd8bc9869 --- /dev/null +++ b/galite-plugins/galite-optgen/src/main/kotlin/org/kopi/galite/plugins/OptgenPlugin.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2013-2026 kopiLeft Services SARL, Tunis TN + * Copyright (c) 1990-2026 kopiRight Managed Solutions GmbH, Wien AT + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package org.kopi.galite.plugins + +import org.gradle.api.Project +import org.gradle.kotlin.dsl.getByType +import org.gradle.kotlin.dsl.register +import org.kopi.galite.plugins.common.GaliteGradleExtensions + +import org.kopi.galite.plugins.common.GradleExtensionsPlugin + +class OptgenPlugin : GradleExtensionsPlugin() { + override fun apply(project: Project) { + super.apply(project) + + createGeneratedSourceSet(project) + + project.extensions.create("optionGen", OptgenExtention::class.java) + project.tasks.apply { + register("generateOptions") + named("clean") { + doLast { + project.extensions.getByType().clean( + project.layout.projectDirectory.dir(GENERATED_DIRECTORY).asFile.path + ) + } + } + } + } +} diff --git a/galite-plugins/galite-optgen/src/main/kotlin/org/kopi/galite/plugins/OptgenTask.kt b/galite-plugins/galite-optgen/src/main/kotlin/org/kopi/galite/plugins/OptgenTask.kt new file mode 100644 index 000000000..69d997310 --- /dev/null +++ b/galite-plugins/galite-optgen/src/main/kotlin/org/kopi/galite/plugins/OptgenTask.kt @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2013-2026 kopiLeft Services SARL, Tunis TN + * Copyright (c) 1990-2026 kopiRight Managed Solutions GmbH, Wien AT + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package org.kopi.galite.plugins + +import org.gradle.api.tasks.* +import org.gradle.kotlin.dsl.get +import org.gradle.kotlin.dsl.the + +import org.kopi.galite.plugins.common.GradleExtensionsPlugin + +abstract class OptgenTask : JavaExec() { + init { + description = "Task to generate java classes to parse *Options.xml files." + mainClass.set("org.kopi.galite.util.optgen.Main") + } + @OutputDirectory + val src = project.layout.projectDirectory.dir(GradleExtensionsPlugin.GENERATED_JAVA_DIRECTORY) + + @TaskAction + override fun exec() { + val extension = project.extensions.getByType(OptgenExtention::class.java) + + if (extension.parameters.getOrElse(emptyList()).isEmpty()) { + project.logger.lifecycle("No Options.xml defined for option parser generation.") + return + } + // Check if generated directory is created + if (!src.asFile.exists()) { src.asFile.mkdirs() } + + // Generate an option parser class per each parameter element + extension.parameters.get().forEach { param -> + if (!param.optionFiles.isEmpty) { + val argsList = buildList { + if (param.release.isNotBlank()) { + add("--release=${param.release}") + } + addAll(param.optionFiles.files.map { it.absolutePath }) + } + + + project.javaexec { + workingDir = project.file(src) + mainClass.set("org.kopi.galite.util.optgen.Main") + classpath = project.the()["main"].runtimeClasspath + args(*argsList.toTypedArray()) + } + } + } + } +} diff --git a/galite-util/build.gradle.kts b/galite-util/build.gradle.kts index e60683831..86fa1984e 100644 --- a/galite-util/build.gradle.kts +++ b/galite-util/build.gradle.kts @@ -16,6 +16,8 @@ */ import org.kopi.galite.gradle.Versions +import org.kopi.galite.gradle.getLatestReleaseDate +import org.kopi.galite.gradle.getLatestReleaseName plugins { kotlin("jvm") apply true @@ -33,3 +35,39 @@ dependencies { //jdom2 implementation("org.jdom","jdom2","2.0.6") } + +sourceSets { + main { + resources { + // Adds the directory generated by the task "generateReleaseInfo" to main source set. + srcDir(layout.buildDirectory.dir("generated-resources/releaseInfo")) + } + } +} + +tasks { + // Generates a properties file containing the latest release version and time. + register("generateReleaseInfo") { + val outputDir = layout.buildDirectory.dir("generated-resources/releaseInfo") + + outputs.dir(outputDir) + doLast { + val version = getLatestReleaseName(project) + val releaseDate = getLatestReleaseDate(project, version) + val file = outputDir.get().file("release.properties").asFile + + file.parentFile.mkdirs() + file.writeText("version=$version\nreleaseDate=$releaseDate") + } + } + + // Adds the dependency of the task "generateReleaseInfo" to the task "processResources" + processResources { + dependsOn("generateReleaseInfo") + } + + sourcesJar { + dependsOn("generateReleaseInfo") + from(sourceSets["main"].allSource) + } +} diff --git a/galite-util/src/main/kotlin/org/kopi/galite/util/base/Options.kt b/galite-util/src/main/kotlin/org/kopi/galite/util/base/Options.kt index 9502851ea..d465a3812 100644 --- a/galite-util/src/main/kotlin/org/kopi/galite/util/base/Options.kt +++ b/galite-util/src/main/kotlin/org/kopi/galite/util/base/Options.kt @@ -1,6 +1,6 @@ /* - * Copyright (c) 2013-2022 kopiLeft Services SARL, Tunis TN - * Copyright (c) 1990-2022 kopiRight Managed Solutions GmbH, Wien AT + * Copyright (c) 2013-2026 kopiLeft Services SARL, Tunis TN + * Copyright (c) 1990-2026 kopiRight Managed Solutions GmbH, Wien AT * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -17,6 +17,8 @@ */ package org.kopi.galite.util.base +import kotlin.system.exitProcess + import gnu.getopt.Getopt import gnu.getopt.LongOpt @@ -43,6 +45,7 @@ abstract class Options(private val name: String?) { */ fun parseCommandLine(argv: Array): Boolean { val parser = Getopt(name, argv, shortOptions, longOptions, true) + while (true) { val code = parser.getopt() @@ -61,52 +64,36 @@ abstract class Options(private val name: String?) { } /** - * @param args the command line arguments + * @param code the argument short name. + * @param g the parser class. */ open fun processOption(code: Int, g: Getopt): Boolean { when (code) { - 'h'.toInt() -> { + 'h'.code -> { help() - System.exit(0) + exitProcess(0) } - 'V'.toInt() -> { + 'V'.code -> { version() - System.exit(0) + exitProcess(0) } else -> return false } - return true } open val options: Array - get() = arrayOf( - " --help, -h: Displays the help information", - " --version, -V: Prints out the version information" - ) + get() = arrayOf(" --help, -h: Displays the help information", + " --version, -V: Prints out the version information") /** * Prints the available options. */ fun printOptions() { - val options = options - - run { - var i = options.size - while (--i >= 0) { - for (j in 0 until i) { - if (options[j]!! > options[j + 1]!!) { - val tmp = options[j] - options[j] = options[j + 1] - options[j + 1] = tmp - } - } - } - } + val sortedOptions = options.filterNotNull().sorted() + println() println("Here are the available options : ") - for (i in options.indices) { - println(options[i]) - } + sortedOptions.forEach { println(it) } } /** @@ -139,11 +126,10 @@ abstract class Options(private val name: String?) { */ protected fun getInt(g: Getopt, defaultValue: Int): Int { return try { - if (g.optarg != null) g.optarg.toInt() else defaultValue - } catch (e: Exception) { + g.optarg?.toInt() ?: defaultValue + } catch (_: Exception) { System.err.println("malformed option: " + g.optarg) - System.exit(0) - 1 + exitProcess(0) } } @@ -151,35 +137,21 @@ abstract class Options(private val name: String?) { * Processes a string argument. */ protected fun getString(g: Getopt, defaultValue: String?): String? { - return if (g.optarg != null) g.optarg else defaultValue + return g.optarg ?: defaultValue } + /** + * Adds a string argument to the pre-existing argument list. + */ protected fun addString(array: Array?, str: String?): Array { - return if (array == null) { - arrayOf(str) - } else { - val size = array.size - val newArray = arrayOfNulls(size + 1) - for (i in 0 until size) { - newArray[i] = array[i] - } - newArray[size] = str - newArray - } + return (array ?: emptyArray()).plus(str) } + /** + * Adds an integer argument to the pre-existing argument list. + */ protected fun addInt(array: IntArray?, value: Int): IntArray { - return if (array == null) { - intArrayOf(value) - } else { - val size = array.size - val newArray = IntArray(size + 1) - for (i in 0 until size) { - newArray[i] = array[i] - } - newArray[size] = value - newArray - } + return (array ?: intArrayOf()).plus(value) } // ---------------------------------------------------------------------- @@ -196,7 +168,7 @@ abstract class Options(private val name: String?) { */ open val longOptions: Array get() = arrayOf( - LongOpt("help", LongOpt.NO_ARGUMENT, null, 'h'.toInt()), - LongOpt("version", LongOpt.NO_ARGUMENT, null, 'V'.toInt()) + LongOpt("help", LongOpt.NO_ARGUMENT, null, 'h'.code), + LongOpt("version", LongOpt.NO_ARGUMENT, null, 'V'.code) ) } diff --git a/galite-util/src/main/kotlin/org/kopi/galite/util/base/Utils.kt b/galite-util/src/main/kotlin/org/kopi/galite/util/base/Utils.kt index 9fcc6df73..4e5d9124a 100644 --- a/galite-util/src/main/kotlin/org/kopi/galite/util/base/Utils.kt +++ b/galite-util/src/main/kotlin/org/kopi/galite/util/base/Utils.kt @@ -18,6 +18,7 @@ package org.kopi.galite.util.base +import java.util.Properties import java.util.Timer import java.util.TimerTask @@ -140,6 +141,18 @@ open class Utils { return if (isKotlinReservedWord(this)) "x$this" else this } + /** + * Read generated release info file. + */ + fun readReleaseInfo(): Properties { + val props = Properties() + val stream = object {}.javaClass.getResourceAsStream("/release.properties") + + props.load(stream) + + return props + } + /** * * Verify if the passed word is a kotlin reserved word. diff --git a/galite-util/src/main/kotlin/org/kopi/galite/util/optgen/DefinitionFile.kt b/galite-util/src/main/kotlin/org/kopi/galite/util/optgen/DefinitionFile.kt new file mode 100644 index 000000000..0216850f6 --- /dev/null +++ b/galite-util/src/main/kotlin/org/kopi/galite/util/optgen/DefinitionFile.kt @@ -0,0 +1,268 @@ +/* + * Copyright (c) 2013-2026 kopiLeft Services SARL, Tunis TN + * Copyright (c) 1990-2026 kopiRight Managed Solutions GmbH, Wien AT + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package org.kopi.galite.util.optgen + +import java.io.File +import java.io.PrintWriter + +import org.jdom2.Document +import org.jdom2.Element +import org.jdom2.input.SAXBuilder + +import org.kopi.galite.util.base.InconsistencyException + +/** + * Constructs a definition file + */ +internal class DefinitionFile(private val sourceFile: String, + private val fileHeader: String?, + private val packageName: String, + private val parent: String?, + private val prefix: String, + private var version: String?, + private val usage: String?, + private val definitions: List) +{ + companion object { + /** + * Reads and parses a token definition file + * + * @param sourceFile the name of the source file + * @return a class info structure holding the information from the source + * + */ + fun read(sourceFile: String): DefinitionFile { + val document: Document + + try { + document = SAXBuilder().build(File(sourceFile)) + } catch (e: Exception) { + throw InconsistencyException("Cannot load file $sourceFile: ${e.message}") + } + val root = document.rootElement + + return DefinitionFile(sourceFile, + root.getAttributeValue("fileHeader"), + root.getAttributeValue("package"), + root.getAttributeValue("parent"), + root.getAttributeValue("prefix"), + root.getAttributeValue("version"), + root.getAttributeValue("usage"), + getOptions(root) + ) + } + + /** + * Reads options from the xml definition file + * + * @param element the xml root element + * @return a class info structure holding the information from the source + */ + private fun getOptions(element: Element): List { + val params = element.getChildren("param") + + return params.map { current -> + val type = current.getAttributeValue("type") + val arg = current.getAttributeValue("optionalDefault") ?: if (type != "boolean") "" else null + + OptionDefinition(current.getAttributeValue("longname"), + current.getAttributeValue("shortname"), + type, + !current.getAttributeValue("multiple").isNullOrBlank(), + current.getAttributeValue("default"), + arg, + current.getAttributeValue("help")) + } + } + } + + // -------------------------------------------------------------------- + // ACCESSORS + // -------------------------------------------------------------------- + + /** + * Sets the version. Overrides the version supplied in the definitions file. + */ + fun setVersion(version: String?) { + this.version = version + } + + /** + * Returns the literal prefix + */ + fun getPrefix(): String { + return prefix + } + + /** + * Returns the literal prefix + */ + fun getPackageName(): String { + return packageName + } + + val className: String + get() = "$packageName.${prefix}Options" + + // -------------------------------------------------------------------- + // CHECK OPERATIONS + // -------------------------------------------------------------------- + + /** + * Checks for duplicate identifiers. + */ + fun checkIdentifiers() { + val identifiers: HashMap = hashMapOf() + + definitions.forEach { it.checkIdentifiers(identifiers, sourceFile) } + } + + /** + * Checks for duplicate shortcuts. + */ + fun checkShortcuts() { + val shortcuts: HashMap = hashMapOf() + + definitions.forEach { it.checkShortcuts(shortcuts, sourceFile) } + } + + // -------------------------------------------------------------------- + // PRINT OPERATIONS + // -------------------------------------------------------------------- + + /** + * Generates the option parser in a java class. + * + * @param out the output stream + */ + fun printJavaFile(out: PrintWriter) { + if (!fileHeader.isNullOrBlank()) { + out.println(fileHeader) + } + out.print("// Generated by optgen from $sourceFile") + out.println() + out.println("package $packageName;") + out.println() + out.println("import gnu.getopt.Getopt;") + out.println("import gnu.getopt.LongOpt;") + out.println() + out.print("public class " + prefix + "Options") + out.print(if (parent == null) "" else " extends $parent") + out.println(" {") + + // CONSTRUCTORS + out.println() + out.println(" public " + prefix + "Options(String name) {") + out.println(" super(name);") + out.println(" }") + out.println() + out.println(" public " + prefix + "Options() {") + out.println(" this(\"$prefix\");") + out.println(" }") + out.println() + + // FIELDS + definitions.forEach { + it.printJavaFields(out) + } + + // PROCESSOPTION + out.println() + out.println(" public boolean processOption(int code, Getopt g) {") + out.println(" switch (code) {") + definitions.forEach { + it.printJavaParseArgument(out) + } + out.println(" default:") + out.println(" return super.processOption(code, g);") + out.println(" }") + out.println(" }") + + + // GETOPTIONS + out.println() + out.println(" public String[] getOptions() {") + out.println(" String[] parent = super.getOptions();") + out.println(" String[] total = new String[parent.length + " + definitions.size + "];") + out.println(" System.arraycopy(parent, 0, total, 0, parent.length);") + + definitions.forEachIndexed { index, definition -> + out.print(" total[parent.length + $index] = ") + definition.printJavaUsage(out) + out.println(";") + } + + out.println(" ") + out.println(" return total;") + out.println(" }") + + // GETSHORTOPTIONS + out.println("\n") + out.println(" public String getShortOptions() {") + out.print(" return \"") + definitions.forEach { + it.printShortOption(out) + + } + out.println("\" + super.getShortOptions();") + out.println(" }") + + // VERSION + out.println("\n") + out.println(" public void version() {") + out.print(" System.out.println(") + out.print(if (version == null) "" else "\"Version $version.\"") + out.println(");") + out.println(" }") + + // USAGE + out.println("\n") + out.println(" public void usage() {") + if (usage != null) { + out.print(" System.err.println(") + out.print("\"" + usage + "\"") + out.println(");") + } + out.println(" }") + + // GETLONGOPTIONS + out.println() + out.println(" public LongOpt[] getLongOptions() {") + out.println(" LongOpt[] parent = super.getLongOptions();") + out.println(" LongOpt[] total = new LongOpt[parent.length + LONGOPTS.length];") + out.println(" ") + out.println(" System.arraycopy(parent, 0, total, 0, parent.length);") + out.println(" System.arraycopy(LONGOPTS, 0, total, parent.length, LONGOPTS.length);") + out.println(" ") + out.println(" return total;") + out.println(" }") + + // LONGOPTS + out.println() + out.println(" private static final LongOpt[] LONGOPTS = {") + definitions.forEachIndexed { index, definition -> + if (index != 0) { out.println(",") } + definition.printJavaLongOpts(out) + } + out.println() + out.println(" };") + + out.println("}") + } +} diff --git a/galite-util/src/main/kotlin/org/kopi/galite/util/optgen/Main.kt b/galite-util/src/main/kotlin/org/kopi/galite/util/optgen/Main.kt new file mode 100644 index 000000000..ae4d7b049 --- /dev/null +++ b/galite-util/src/main/kotlin/org/kopi/galite/util/optgen/Main.kt @@ -0,0 +1,200 @@ +/* + * Copyright (c) 2013-2026 kopiLeft Services SARL, Tunis TN + * Copyright (c) 1990-2026 kopiRight Managed Solutions GmbH, Wien AT + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package org.kopi.galite.util.optgen + +import java.io.BufferedWriter +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.io.PrintWriter +import java.io.OutputStreamWriter + +import kotlin.system.exitProcess + +/** + * This class is the entry point for the Option generator. + */ +class Main { + + // -------------------------------------------------------------------- + // ENTRY POINT + // -------------------------------------------------------------------- + + companion object { + /** + * Program entry point. + */ + @JvmStatic + fun main(args: Array) { + val success = Main().run(args) + + exitProcess(if (success) 0 else 1) + } + } + + /** + * Runs a compilation session. + * + * @param args the command line arguments + */ + fun run(args: Array): Boolean { + if (!parseArguments(args)) { + return false + } + var errorsFound = false + + options.nonOptions.forEach { sourceFile -> + sourceFile?.let { + errorsFound = !processFile(it) + } + } + + return !errorsFound + } + + /* + * Parse command line arguments. + * + * @param args the command line arguments + */ + private fun parseArguments(args: Array): Boolean { + options = OptgenOptions() + + if (!options.parseCommandLine(args)) { + return false + } + if (options.nonOptions.isEmpty()) { + System.err.println("error: No input file given") + options.usage() + return false + } + return true + } + + /** + * Process the source file to check for errors. + * + * @param sourceFile The source file name. + * + * @return a boolean indicating if the method is successfully executed. + */ + private fun processFile(sourceFile: String): Boolean { + if (!parseSource(sourceFile)) { + return false + } + if (options.release != null) { + definition.setVersion(options.release) + } + if (!checkIdentifiers()) { + return false + } + if (!checkShortcuts()) { + return false + } + if (!buildInterfaceFile()) { + return false + } + return true + } + + /** + * Parse the source file and check for errors + * + * @param sourceFile The source file name. + * + * @return a boolean indicating if the method is successfully executed. + */ + private fun parseSource(sourceFile: String): Boolean { + var errorsFound = false + + try { + definition = DefinitionFile.read(sourceFile) + } catch (e: Exception) { + System.err.println("error: ${e.message}") + errorsFound = true + } + + return !errorsFound + } + + /** + * Checks for duplicate identifiers. + */ + private fun checkIdentifiers(): Boolean { + var errorsFound = false + + try { + definition.checkIdentifiers() + } catch (e: Exception) { + System.err.println("error: ${e.message}") + errorsFound = true + } + + return !errorsFound + } + + /** + * Checks for duplicate shortcuts. + */ + private fun checkShortcuts(): Boolean { + var errorsFound = false + + try { + definition.checkShortcuts() + } catch (e: Exception) { + System.err.println("error: ${e.message}") + errorsFound = true + } + + return !errorsFound + } + + /** + * Build the generated class file. + */ + private fun buildInterfaceFile(): Boolean { + val prefix: String = definition.getPrefix() + val destinationDirectory: String = definition.getPackageName().replace(".", File.separator) + val outputFile: File = File(destinationDirectory + File.separator + prefix + "Options.java") + var errorsFound: Boolean = false + + try { + outputFile.parentFile?.mkdirs() + + val out = PrintWriter(BufferedWriter(OutputStreamWriter(FileOutputStream(outputFile), "UTF-8"))) + + definition.printJavaFile(out) + + out.flush() + out.close() + } catch (e: IOException) { + System.err.println("I/O Exception on " + outputFile.path + ": " + e.message) + errorsFound = true + } + + return !errorsFound + } + + // -------------------------------------------------------------------- + // DATA MEMBERS + // -------------------------------------------------------------------- + + private lateinit var options: OptgenOptions + private lateinit var definition: DefinitionFile +} diff --git a/galite-util/src/main/kotlin/org/kopi/galite/util/optgen/OptgenOptions.kt b/galite-util/src/main/kotlin/org/kopi/galite/util/optgen/OptgenOptions.kt new file mode 100644 index 000000000..a87869bd6 --- /dev/null +++ b/galite-util/src/main/kotlin/org/kopi/galite/util/optgen/OptgenOptions.kt @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2013-2026 kopiLeft Services SARL, Tunis TN + * Copyright (c) 1990-2026 kopiRight Managed Solutions GmbH, Wien AT + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package org.kopi.galite.util.optgen + +import gnu.getopt.Getopt +import gnu.getopt.LongOpt + +import org.kopi.galite.util.base.Options +import org.kopi.galite.util.base.Utils + +class OptgenOptions @JvmOverloads constructor(name: String = "Optgen") : Options(name) { + /** + * Process the command line options. + * + * @param code The short option name. + * @param g The parser class. + */ + override fun processOption(code: Int, g: Getopt): Boolean { + return when (code) { + 'r'.code -> { release = getString(g, "") ; true } + else -> super.processOption(code, g) + } + } + + /** + * Print Optgen version and release date. + */ + override fun version() { + val releaseInfo = Utils.readReleaseInfo() + + println("Version ${releaseInfo["version"]?.toString().orEmpty()} released at ${releaseInfo["releaseDate"]?.toString().orEmpty()}.") + } + + /*** + * Print Optgen class usage. + */ + public override fun usage() { + System.err.println("usage: org.kopi.galite.util.optgen.Main [option]* [--help] +") + } + + /** + * Get Optgen class options. + */ + override val options: Array + get() { + val parent: Array = super.options + val total: Array = arrayOfNulls(parent.size + 1) + + System.arraycopy(parent, 0, total, 0, parent.size) + total[parent.size + 0] = " --release, -r: Sets the release version of the program" + + return total + } + + /** + * Get Optgen short options + */ + override val shortOptions: String + get() = "r:" + super.shortOptions + + /** + * Get Optgen long options + */ + override val longOptions: Array + get() { + val parent: Array = super.longOptions + val total = arrayOfNulls(parent.size + LONGOPTS.size) + + System.arraycopy(parent, 0, total, 0, parent.size) + System.arraycopy(LONGOPTS, 0, total, parent.size, LONGOPTS.size) + + return total + } + + // -------------------------------------------------------------------- + // DATA MEMBERS + // -------------------------------------------------------------------- + + var release: String? = null + + companion object { + private val LONGOPTS = arrayOf(LongOpt("release", LongOpt.REQUIRED_ARGUMENT, null, 'r'.code)) + } +} diff --git a/galite-util/src/main/kotlin/org/kopi/galite/util/optgen/OptionDefinition.kt b/galite-util/src/main/kotlin/org/kopi/galite/util/optgen/OptionDefinition.kt new file mode 100644 index 000000000..ec16d96bc --- /dev/null +++ b/galite-util/src/main/kotlin/org/kopi/galite/util/optgen/OptionDefinition.kt @@ -0,0 +1,176 @@ +/* + * Copyright (c) 2013-2026 kopiLeft Services SARL, Tunis TN + * Copyright (c) 1990-2026 kopiRight Managed Solutions GmbH, Wien AT + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License version 2.1 as published by the Free Software Foundation. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package org.kopi.galite.util.optgen + +import java.io.PrintWriter + +import org.kopi.galite.util.base.InconsistencyException + +class OptionDefinition(private val longname: String, + private val shortname: String, + private val type: String, + private val isMultiple: Boolean, + private val defaultValue: String, + private val argument: String?, + private val help: String?) +{ + /** + * Check for duplicate identifiers + * + * @param identifiers a table of all token identifiers + * @param sourceFile the file where the token is defined + */ + fun checkIdentifiers(identifiers: HashMap, sourceFile: String) { + val stored = identifiers[longname] + + if (stored != null) { + throw InconsistencyException("""Option "$longname" redefined in "$sourceFile": previous definition in "$stored"""") + } + identifiers[longname] = sourceFile + } + + /** + * Check for duplicate shortcuts + * + * @param shortcuts a table of all token identifiers + * @param sourceFile the file where the token is defined + */ + fun checkShortcuts(shortcuts: HashMap, sourceFile: String) { + val stored = shortcuts[shortname] + + if (stored != null) { + throw InconsistencyException("""Shortcut "$shortname" redefined in "$sourceFile": previous definition in "$stored"""") + } + shortcuts[shortname] = sourceFile + } + + /** + * Prints the case statement for the parseArgument method + * + * @param out the output stream + */ + fun printJavaParseArgument(out: PrintWriter) { + out.print(" case \'") + out.print(shortname) + out.println("\':") + out.print(" ") + out.print(longname) + out.print(" = ") + if (argument == null) { + if (isMultiple) { + throw InconsistencyException("multiple arguments support for type $type is not yet implemented.") + } + out.print("!$defaultValue") + out.print(";") + } else { + val methodName: String + var arg = argument + if (type == "int") { + methodName = "getInt" + if (arg.isEmpty()) { + arg = "0" + } + } else { + methodName = "getString" + arg = "\"" + arg + "\"" + } + if (isMultiple) { + when (type) { + "int" -> out.print("addInt($longname, $methodName(g, $arg))") + "String" -> out.print("addString($longname, $methodName(g, $arg))") + + else -> throw InconsistencyException("multiple arguments support for type $type is not yet implemented.") + } + } else { + out.print("$methodName(g, $arg)") + } + out.print(";") + } + out.println(" return true;") + } + + /** + * Prints the field declaration + * + * @param out the output stream + */ + fun printJavaFields(out: PrintWriter) { + out.print(" public ") + out.print(if (!isMultiple) type else "$type[]") + out.print(" ") + out.print(longname) + out.print(" = ") + if (type != "String" || defaultValue == "null") { + if (defaultValue == "null") { + out.print(defaultValue) + } else { + out.print(if (!isMultiple) defaultValue else "{ $defaultValue }") + } + } else { + out.print(if (!isMultiple) "\"" + defaultValue + "\"" else "{ \"$defaultValue\" }") + } + out.println(";") + } + + /** + * Prints the usage message + * + * @param out the output stream + */ + fun printJavaUsage(out: PrintWriter) { + val prefix = "\" --$longname, -$shortname${argument?.let { "<$type>" }.orEmpty()}: ".padEnd(33, ' ') + + out.print(prefix + (help?.replace("\"".toRegex(), "\\\\\"") ?: "")) + if (defaultValue != "null") { + out.print(" [") + out.print(defaultValue) + out.print("]") + } + out.print("\"") + } + + /** + * Prints the LongOpt instantiation + * + * @param out the output stream + */ + fun printJavaLongOpts(out: PrintWriter) { + out.print(" new LongOpt(\"") + out.print(longname) + out.print("\", ") + when (argument) { + null -> out.print("LongOpt.NO_ARGUMENT") + "" -> out.print("LongOpt.REQUIRED_ARGUMENT") + else -> out.print("LongOpt.OPTIONAL_ARGUMENT") + } + out.print(", null, \'") + out.print(shortname) + out.print("\')") + } + + /** + * Prints the short option + * + * @param out the output stream + */ + fun printShortOption(out: PrintWriter) { + out.print(shortname) + argument?.let { if (it.isEmpty()) out.print(":") else out.print("::") } + } +} diff --git a/gradle.properties b/gradle.properties index 6f7fb1822..924c59dda 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ org.gradle.jvmargs=-Xmx4g # group=org.kopi -version=1.5.13 +version=1.5.13-02Y5-SNAPSHOT diff --git a/settings.gradle.kts b/settings.gradle.kts index 1e205f6c8..03fa93f02 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -29,3 +29,4 @@ include("galite-plugins") include("galite-plugins:galite-common-plugin") include("galite-plugins:galite-factory-generator") include("galite-plugins:galite-dbschema-generator") +include("galite-plugins:galite-optgen")