summaryrefslogtreecommitdiff
path: root/support/texplate/source/main/kotlin/org/islandoftex/texplate
diff options
context:
space:
mode:
Diffstat (limited to 'support/texplate/source/main/kotlin/org/islandoftex/texplate')
-rw-r--r--support/texplate/source/main/kotlin/org/islandoftex/texplate/Main.kt23
-rw-r--r--support/texplate/source/main/kotlin/org/islandoftex/texplate/TemplateProcessing.kt228
-rw-r--r--support/texplate/source/main/kotlin/org/islandoftex/texplate/exceptions/InvalidKeySetException.kt30
-rw-r--r--support/texplate/source/main/kotlin/org/islandoftex/texplate/exceptions/InvalidTemplateException.kt27
-rw-r--r--support/texplate/source/main/kotlin/org/islandoftex/texplate/exceptions/TemplateMergingException.kt30
-rw-r--r--support/texplate/source/main/kotlin/org/islandoftex/texplate/model/Configuration.kt67
-rw-r--r--support/texplate/source/main/kotlin/org/islandoftex/texplate/model/Template.kt90
-rw-r--r--support/texplate/source/main/kotlin/org/islandoftex/texplate/model/handlers/BooleanHandler.kt20
-rw-r--r--support/texplate/source/main/kotlin/org/islandoftex/texplate/model/handlers/CSVListHandler.kt22
-rw-r--r--support/texplate/source/main/kotlin/org/islandoftex/texplate/model/handlers/Handler.kt18
-rw-r--r--support/texplate/source/main/kotlin/org/islandoftex/texplate/util/HandlerUtils.kt25
-rw-r--r--support/texplate/source/main/kotlin/org/islandoftex/texplate/util/MergingUtils.kt126
-rw-r--r--support/texplate/source/main/kotlin/org/islandoftex/texplate/util/MessageUtils.kt72
-rw-r--r--support/texplate/source/main/kotlin/org/islandoftex/texplate/util/PathUtils.kt69
-rw-r--r--support/texplate/source/main/kotlin/org/islandoftex/texplate/util/ValidatorUtils.kt54
15 files changed, 901 insertions, 0 deletions
diff --git a/support/texplate/source/main/kotlin/org/islandoftex/texplate/Main.kt b/support/texplate/source/main/kotlin/org/islandoftex/texplate/Main.kt
new file mode 100644
index 0000000000..73de15f8c3
--- /dev/null
+++ b/support/texplate/source/main/kotlin/org/islandoftex/texplate/Main.kt
@@ -0,0 +1,23 @@
+// SPDX-License-Identifier: BSD-3-Clause
+package org.islandoftex.texplate
+
+import kotlin.system.exitProcess
+import org.islandoftex.texplate.util.MessageUtils
+import picocli.CommandLine
+
+/**
+ * Main method. Note that it simply passes the control to the template
+ * processing class.
+ *
+ * @param args The command line arguments.
+ */
+fun main(args: Array<String>) {
+ // draw the application logo in the terminal (please have fixed fonts
+ // in your terminal for a nice display)
+ MessageUtils.drawLogo()
+ // calls the command line processing method and performs the actual
+ // application logic
+ @Suppress("SpreadOperator")
+ val exitCode = CommandLine(TemplateProcessing()).execute(*args)
+ exitProcess(exitCode)
+}
diff --git a/support/texplate/source/main/kotlin/org/islandoftex/texplate/TemplateProcessing.kt b/support/texplate/source/main/kotlin/org/islandoftex/texplate/TemplateProcessing.kt
new file mode 100644
index 0000000000..659a0df631
--- /dev/null
+++ b/support/texplate/source/main/kotlin/org/islandoftex/texplate/TemplateProcessing.kt
@@ -0,0 +1,228 @@
+// SPDX-License-Identifier: BSD-3-Clause
+package org.islandoftex.texplate
+
+import java.nio.file.Path
+import java.util.concurrent.Callable
+import kotlin.math.ln
+import kotlin.math.pow
+import org.islandoftex.texplate.exceptions.InvalidTemplateException
+import org.islandoftex.texplate.model.Configuration
+import org.islandoftex.texplate.model.Template
+import org.islandoftex.texplate.util.MergingUtils.mergeTemplate
+import org.islandoftex.texplate.util.MessageUtils.error
+import org.islandoftex.texplate.util.MessageUtils.line
+import org.islandoftex.texplate.util.MessageUtils.status
+import org.islandoftex.texplate.util.PathUtils.getTemplatePath
+import org.islandoftex.texplate.util.ValidatorUtils.validate
+import picocli.CommandLine
+
+/**
+ * The template processing class.
+ *
+ * @version 1.0
+ * @since 1.0
+ */
+@CommandLine.Command(usageHelpWidth = 70, name = "texplate")
+class TemplateProcessing : Callable<Int> {
+ // the file output, which will hold the result of the merging of both template
+ // and context data map from command line
+ @CommandLine.Option(
+ names = ["-o", "--output"],
+ description = ["The output file in which the chosen " +
+ "template will be effectively written. Make sure " +
+ "the directory has the correct permissions for " +
+ "writing the output file."],
+ required = true,
+ type = [Path::class]
+ )
+ private val output: Path? = null
+
+ // the template name
+ @CommandLine.Option(
+ names = ["-t", "--template"],
+ description = ["The name of the template. The tool will " +
+ "search both user and system locations and set the " +
+ "template model accordingly, based on your specs."]
+ )
+ private var template: String? = null
+
+ // the context data map that holds a set of key/value pairs to be merged
+ // with the template
+ @CommandLine.Option(
+ names = ["-m", "--map"],
+ description = ["The contextual map that provides the " +
+ "data to be merged in the template. This parameter " +
+ "can be used multiple times. You can specify a map " +
+ "entry with the key=value syntax (mind the entry " +
+ "separator)."],
+ arity = "1..*"
+ )
+ private var map: Map<String, String>? = null
+
+ @CommandLine.Option(
+ names = ["-c", "--config"],
+ description = ["The configuration file in which the tool " +
+ "can read template data, for automation purposes. Make " +
+ "sure to follow the correct specification when writing " +
+ "a configuration file."],
+ type = [Path::class]
+ )
+ private val configuration: Path? = null
+
+ /**
+ * The application logic, enclosed as a call method.
+ *
+ * @return An integer value denoting the exit status.
+ * @throws Exception An exception was raised in the application logic.
+ */
+ @Throws(Exception::class)
+ override fun call(): Int {
+ // the exit status, originally set as a valid value
+ var exit = 0
+ // configuration halt flag, indicating whether the tool has to end earlier
+ var halt = false
+ var cmap: Map<String, Any> = mutableMapOf()
+ // ensure the context data map is at least instantiated
+ ensureMap()
+ // there is a configuration file found in the command line
+ if (has(configuration)) {
+ line("Checking configuration")
+ try {
+ val config = Configuration.fromPath(configuration!!)
+ // the configuration file seems to be valid, proceed
+ status(true)
+ // check if the configuration has a proper template
+ if (has(config.template)) {
+ // if so, build the template if, and only if, there's no one already
+ // set through command line
+ template = ensure(template, config.template)
+ }
+ // check if the configuration has a proper string/string map
+ if (has(config.map)) {
+ // set the main configuration map to be dealt later on
+ cmap = config.map
+ }
+ line("Adjusting variables from file")
+ status(true)
+ println()
+ } catch (e: InvalidTemplateException) {
+ // an error occurred, print it, set exit code and halt
+ status(false)
+ error(e)
+ exit = -1
+ halt = true
+ }
+ } else {
+ line("Configuration file mode disabled")
+ status(true)
+ line("Entering full command line mode")
+ // there's no configuration file, so we need to check whether there is
+ // not a pattern set in the command line
+ if (!has(template)) {
+ status(false)
+ error(Exception("The template was not set " +
+ "in the command line through the -t/--template " +
+ "option. If not explicitly specified in a " +
+ "configuration file, this option becomes mandatory, " +
+ "so make sure to define it either in the command " +
+ "line or in a proper configuration file."))
+ exit = -1
+ halt = true
+ } else {
+ status(true)
+ println()
+ }
+ }
+ // check whether we should halt prematurely
+ if (!halt) {
+ println("Please, wait...")
+ println()
+ // now we need to obtain the actual template from a file stored either
+ // in the user home or in the system
+ line("Obtaining reference")
+ try {
+ // let us try to get the corresponding file from the template pattern
+ val file = getTemplatePath(template!!)
+ // the actual template file was found, so we can proceed to
+ // the next phase
+ status(true)
+ line("Composing template")
+ // attempts to retrieve the template attributes from the referenced file
+ // to the actual template object
+ val template = Template.fromPath(file)
+ // the template composition was successful, so we can move on to the
+ // next phase
+ status(true)
+ // once the template object is populated, we need to verify if both
+ // template and data map are not somehow conflicting
+ line("Validating data")
+ val validatedData = validate(template, map!!)
+ // the data validation was consistent, so now the merging can be
+ // applied
+ status(true)
+ line("Merging template and data")
+ val merged = mergeTemplate(template, validatedData, output!!,
+ cmap)
+ status(true)
+ println()
+ println("Done! Enjoy your template!")
+ println("Written: " + getSize(merged))
+ } catch (e: InvalidTemplateException) {
+ status(false)
+ error(e)
+ exit = -1
+ }
+ }
+ // the exit status is returned, denoting whether the application was able
+ // to merge both template and data accordingly
+ return exit
+ }
+
+ /**
+ * Ensures the data map is never pointed to a null reference.
+ */
+ private fun ensureMap() {
+ if (!has(map)) {
+ map = mutableMapOf()
+ }
+ }
+
+ /**
+ * Gets the file size in a human readable format.
+ *
+ * @param bytes The file size, in bytes.
+ * @return The file size in a human readable format.
+ */
+ @Suppress("MagicNumber")
+ private fun getSize(bytes: Long): String {
+ return if (bytes < 1024) {
+ "$bytes B"
+ } else {
+ val exponent = (ln(bytes.toDouble()) / ln(1024.0)).toInt()
+ "%.1f %cB".format(bytes / 1024.0.pow(exponent.toDouble()),
+ "KMGTPE"[exponent - 1])
+ }
+ }
+
+ /**
+ * Ensures the first parameter is not null, or sets it to the second one.
+ *
+ * @param <T> The type.
+ * @param first First parameter.
+ * @param second Second parameter.
+ * @return Either the first or the second one.
+ </T> */
+ private fun <T> ensure(first: T, second: T): T {
+ return if (!has(first)) second else first
+ }
+
+ /**
+ * Checks whether the object exists.
+ *
+ * @param obj The object.
+ * @return Boolean value indicating whether the object exists.
+ */
+ private fun has(obj: Any?): Boolean {
+ return obj != null
+ }
+}
diff --git a/support/texplate/source/main/kotlin/org/islandoftex/texplate/exceptions/InvalidKeySetException.kt b/support/texplate/source/main/kotlin/org/islandoftex/texplate/exceptions/InvalidKeySetException.kt
new file mode 100644
index 0000000000..72ff3bcbd4
--- /dev/null
+++ b/support/texplate/source/main/kotlin/org/islandoftex/texplate/exceptions/InvalidKeySetException.kt
@@ -0,0 +1,30 @@
+// SPDX-License-Identifier: BSD-3-Clause
+package org.islandoftex.texplate.exceptions
+
+/**
+ * Handles exceptions when the context map contains invalid keys.
+ *
+ * @version 1.0
+ * @since 1.0
+ */
+class InvalidKeySetException : Exception {
+ /**
+ * Constructor.
+ */
+ constructor()
+
+ /**
+ * Constructor.
+ *
+ * @param message Message to be attached to the exception.
+ */
+ constructor(message: String?) : super(message)
+
+ /**
+ * Constructor.
+ *
+ * @param message Message to be attached to the exception.
+ * @param cause The throwable cause to be forwarded.
+ */
+ constructor(message: String?, cause: Throwable?) : super(message, cause)
+}
diff --git a/support/texplate/source/main/kotlin/org/islandoftex/texplate/exceptions/InvalidTemplateException.kt b/support/texplate/source/main/kotlin/org/islandoftex/texplate/exceptions/InvalidTemplateException.kt
new file mode 100644
index 0000000000..678c390055
--- /dev/null
+++ b/support/texplate/source/main/kotlin/org/islandoftex/texplate/exceptions/InvalidTemplateException.kt
@@ -0,0 +1,27 @@
+// SPDX-License-Identifier: BSD-3-Clause
+package org.islandoftex.texplate.exceptions
+
+/**
+ * Handles exceptions when the template is somehow invalid.
+ *
+ * @version 1.0
+ * @since 1.0
+ */
+class InvalidTemplateException : Exception {
+ constructor()
+
+ /**
+ * Constructor.
+ *
+ * @param message Message to be attached to the exception.
+ */
+ constructor(message: String?) : super(message)
+
+ /**
+ * Constructor.
+ *
+ * @param message Message to be attached to the exception.
+ * @param cause The throwable cause to be forwarded.
+ */
+ constructor(message: String?, cause: Throwable?) : super(message, cause)
+}
diff --git a/support/texplate/source/main/kotlin/org/islandoftex/texplate/exceptions/TemplateMergingException.kt b/support/texplate/source/main/kotlin/org/islandoftex/texplate/exceptions/TemplateMergingException.kt
new file mode 100644
index 0000000000..e1760f8732
--- /dev/null
+++ b/support/texplate/source/main/kotlin/org/islandoftex/texplate/exceptions/TemplateMergingException.kt
@@ -0,0 +1,30 @@
+// SPDX-License-Identifier: BSD-3-Clause
+package org.islandoftex.texplate.exceptions
+
+/**
+ * Handles exceptions when the template merging failed.
+ *
+ * @version 1.0
+ * @since 1.0
+ */
+class TemplateMergingException : Exception {
+ /**
+ * Constructor.
+ */
+ constructor()
+
+ /**
+ * Constructor.
+ *
+ * @param message Message to be attached to the exception.
+ */
+ constructor(message: String?) : super(message)
+
+ /**
+ * Constructor.
+ *
+ * @param message Message to be attached to the exception.
+ * @param cause The throwable cause to be forwarded.
+ */
+ constructor(message: String?, cause: Throwable?) : super(message, cause)
+}
diff --git a/support/texplate/source/main/kotlin/org/islandoftex/texplate/model/Configuration.kt b/support/texplate/source/main/kotlin/org/islandoftex/texplate/model/Configuration.kt
new file mode 100644
index 0000000000..8d37cbb39a
--- /dev/null
+++ b/support/texplate/source/main/kotlin/org/islandoftex/texplate/model/Configuration.kt
@@ -0,0 +1,67 @@
+// SPDX-License-Identifier: BSD-3-Clause
+package org.islandoftex.texplate.model
+
+import com.moandjiezana.toml.Toml
+import java.nio.file.Path
+import org.islandoftex.texplate.exceptions.InvalidTemplateException
+
+/**
+ * The configuration model.
+ *
+ * @version 1.0
+ * @since 1.0
+ */
+data class Configuration(
+ /**
+ * The template of the template.
+ */
+ val template: String? = null,
+ /**
+ * Map of variables for the configuration.
+ */
+ val map: Map<String, Any> = mapOf()
+) {
+ /**
+ * Whether the configuration is valid.
+ */
+ private val isValid: Boolean
+ get() = template != null
+
+ companion object {
+ /**
+ * Reads the configuration from path.
+ *
+ * @param path The path.
+ * @return Configuration.
+ * @throws InvalidTemplateException The configuration is invalid.
+ */
+ @Throws(InvalidTemplateException::class)
+ fun fromPath(path: Path): Configuration {
+ val configuration: Configuration
+ val message = ("The provided configuration file looks invalid. " +
+ "Please make sure the configuration has a valid syntax and " +
+ "try again. ")
+ configuration = try {
+ // gets the configuration
+ Toml().read(path.toFile()).to(Configuration::class.java)
+ } catch (exception: IllegalStateException) {
+ // the configuration seems invalid
+ throw InvalidTemplateException(message + "In this particular " +
+ "scenario, there is a possibility that the configuration " +
+ "file does not follow the TOML specification. Please " +
+ "refer to the user manual for further details and a " +
+ "possible fix. Also, the raised exception message can " +
+ "give us some hints on what happened.", exception)
+ }
+ return if (configuration.isValid) {
+ configuration
+ } else {
+ throw InvalidTemplateException(message + "Specifically, some " +
+ "mandatory fields are either absent or empty in the " +
+ "configuration file. It is quite important to strictly " +
+ "follow the configuration specification, as detailed in " +
+ "the user manual, or the tool will not work at all.")
+ }
+ }
+ }
+}
diff --git a/support/texplate/source/main/kotlin/org/islandoftex/texplate/model/Template.kt b/support/texplate/source/main/kotlin/org/islandoftex/texplate/model/Template.kt
new file mode 100644
index 0000000000..82616242de
--- /dev/null
+++ b/support/texplate/source/main/kotlin/org/islandoftex/texplate/model/Template.kt
@@ -0,0 +1,90 @@
+// SPDX-License-Identifier: BSD-3-Clause
+package org.islandoftex.texplate.model
+
+import com.moandjiezana.toml.Toml
+import java.nio.file.Path
+import org.islandoftex.texplate.exceptions.InvalidTemplateException
+
+/**
+ * The template model.
+ *
+ * @version 1.0
+ * @since 1.0
+ */
+data class Template(
+ /**
+ * Name of the template
+ */
+ val name: String? = null,
+ /**
+ * Description of the template
+ */
+ val description: String? = null,
+ /**
+ * List of authors who wrote the template
+ */
+ val authors: List<String> = listOf(),
+ /**
+ * List of requirements for the template
+ */
+ val requirements: List<String> = listOf(),
+ /**
+ * The document to be configured
+ */
+ val document: String? = null,
+ /**
+ * The map handlers
+ */
+ val handlers: Map<String, String> = mapOf()
+) {
+ /**
+ * Checks whether the template is valid.
+ *
+ * @return A boolean value indicating whether the template is valid.
+ */
+ private val isValid: Boolean
+ get() = !(name == null || description == null ||
+ document == null || name.isBlank() ||
+ description.isBlank() || authors.isEmpty() ||
+ document.isBlank())
+
+ companion object {
+ /**
+ * Reads the template from the provided path.
+ *
+ * @param path The path to the template file.
+ * @return The template object from the provided path.
+ * @throws InvalidTemplateException The template is invalid.
+ */
+ @JvmStatic
+ @Throws(InvalidTemplateException::class)
+ fun fromPath(path: Path): Template {
+ val template: Template
+ // the exception message, in case the conversion fails or if there are
+ // missing elements from the template
+ val message = ("The provided template file looks invalid. Please " +
+ "make sure the template has a valid syntax and try again. ")
+ template = try {
+ Toml().read(path.toFile()).to(Template::class.java)
+ } catch (exception: IllegalStateException) {
+ throw InvalidTemplateException(message + "In this particular " +
+ "scenario, there is a possibility that the template " +
+ "file does not follow the TOML specification. Please " +
+ "refer to the user manual for further details and a " +
+ "possible fix. Also, the raised exception message can " +
+ "give us some hints on what happened.", exception)
+ }
+ // the conversion hasn't failed, but we need to check whether the
+ // template is valid
+ return if (template.isValid) {
+ template
+ } else {
+ throw InvalidTemplateException(message + "Specifically, some " +
+ "mandatory fields are either absent or empty in the " +
+ "template file. It is quite important to strictly " +
+ "follow the template specification, as detailed in the " +
+ "user manual, or the tool will not work at all.")
+ }
+ }
+ }
+}
diff --git a/support/texplate/source/main/kotlin/org/islandoftex/texplate/model/handlers/BooleanHandler.kt b/support/texplate/source/main/kotlin/org/islandoftex/texplate/model/handlers/BooleanHandler.kt
new file mode 100644
index 0000000000..cbb74a532f
--- /dev/null
+++ b/support/texplate/source/main/kotlin/org/islandoftex/texplate/model/handlers/BooleanHandler.kt
@@ -0,0 +1,20 @@
+// SPDX-License-Identifier: BSD-3-Clause
+package org.islandoftex.texplate.model.handlers
+
+/**
+ * Implements a boolean handler.
+ *
+ * @version 1.0
+ * @since 1.0
+ */
+class BooleanHandler : Handler {
+ /**
+ * Applies the conversion to the string.
+ *
+ * @param string The string.
+ * @return A list.
+ */
+ override fun apply(string: String?): Any? {
+ return listOf("true", "1", "yes").contains(string!!.toLowerCase())
+ }
+}
diff --git a/support/texplate/source/main/kotlin/org/islandoftex/texplate/model/handlers/CSVListHandler.kt b/support/texplate/source/main/kotlin/org/islandoftex/texplate/model/handlers/CSVListHandler.kt
new file mode 100644
index 0000000000..77424ed463
--- /dev/null
+++ b/support/texplate/source/main/kotlin/org/islandoftex/texplate/model/handlers/CSVListHandler.kt
@@ -0,0 +1,22 @@
+// SPDX-License-Identifier: BSD-3-Clause
+package org.islandoftex.texplate.model.handlers
+
+/**
+ * Implements a CSV list handler.
+ *
+ * @version 1.0
+ * @since 1.0
+ */
+class CSVListHandler : Handler {
+ /**
+ * Applies the conversion to the string.
+ *
+ * @param string The string.
+ * @return A list.
+ */
+ override fun apply(string: String?): Any? {
+ return string?.split(",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)".toRegex())
+ ?.map { it.trim() }
+ ?.filter { it.isNotEmpty() }
+ }
+}
diff --git a/support/texplate/source/main/kotlin/org/islandoftex/texplate/model/handlers/Handler.kt b/support/texplate/source/main/kotlin/org/islandoftex/texplate/model/handlers/Handler.kt
new file mode 100644
index 0000000000..47b59a09a5
--- /dev/null
+++ b/support/texplate/source/main/kotlin/org/islandoftex/texplate/model/handlers/Handler.kt
@@ -0,0 +1,18 @@
+// SPDX-License-Identifier: BSD-3-Clause
+package org.islandoftex.texplate.model.handlers
+
+/**
+ * Interface for handlers.
+ *
+ * @version 1.0
+ * @since 1.0
+ */
+interface Handler {
+ /**
+ * Apply the handler in the string.
+ *
+ * @param string The string.
+ * @return The resulting object.
+ */
+ fun apply(string: String?): Any?
+}
diff --git a/support/texplate/source/main/kotlin/org/islandoftex/texplate/util/HandlerUtils.kt b/support/texplate/source/main/kotlin/org/islandoftex/texplate/util/HandlerUtils.kt
new file mode 100644
index 0000000000..44eebb2b69
--- /dev/null
+++ b/support/texplate/source/main/kotlin/org/islandoftex/texplate/util/HandlerUtils.kt
@@ -0,0 +1,25 @@
+// SPDX-License-Identifier: BSD-3-Clause
+package org.islandoftex.texplate.util
+
+import org.islandoftex.texplate.model.handlers.BooleanHandler
+import org.islandoftex.texplate.model.handlers.CSVListHandler
+import org.islandoftex.texplate.model.handlers.Handler
+
+/**
+ * Provides the map of handlers.
+ *
+ * @version 1.0
+ * @since 1.0
+ */
+object HandlerUtils {
+ /**
+ * Gets the map of handlers.
+ *
+ * @return Map of handlers.
+ */
+ @JvmStatic
+ val handlers: Map<String, Handler> = mapOf(
+ "to-csv-list" to CSVListHandler(),
+ "to-boolean" to BooleanHandler()
+ )
+}
diff --git a/support/texplate/source/main/kotlin/org/islandoftex/texplate/util/MergingUtils.kt b/support/texplate/source/main/kotlin/org/islandoftex/texplate/util/MergingUtils.kt
new file mode 100644
index 0000000000..ef02349d5d
--- /dev/null
+++ b/support/texplate/source/main/kotlin/org/islandoftex/texplate/util/MergingUtils.kt
@@ -0,0 +1,126 @@
+// SPDX-License-Identifier: BSD-3-Clause
+package org.islandoftex.texplate.util
+
+import java.io.IOException
+import java.nio.file.Path
+import org.apache.velocity.VelocityContext
+import org.apache.velocity.exception.MethodInvocationException
+import org.apache.velocity.exception.ParseErrorException
+import org.apache.velocity.exception.ResourceNotFoundException
+import org.apache.velocity.exception.TemplateInitException
+import org.apache.velocity.runtime.RuntimeConstants
+import org.apache.velocity.runtime.RuntimeSingleton
+import org.apache.velocity.runtime.parser.ParseException
+import org.islandoftex.texplate.exceptions.TemplateMergingException
+import org.islandoftex.texplate.model.Template
+import org.islandoftex.texplate.util.HandlerUtils.handlers
+import org.slf4j.helpers.NOPLoggerFactory
+
+/**
+ * Merging utilities.
+ *
+ * @version 1.0
+ * @since 1.0
+ */
+object MergingUtils {
+ /**
+ * Merges both template and data.
+ *
+ * @param template The template object.
+ * @param map The data map.
+ * @param output The output path.
+ * @param cmap The configuration map.
+ * @return The length of the generated output.
+ * @throws TemplateMergingException The merging failed.
+ */
+ @JvmStatic
+ @Throws(TemplateMergingException::class)
+ @Suppress("TooGenericExceptionCaught")
+ fun mergeTemplate(
+ template: Template,
+ map: Map<String, String?>,
+ output: Path,
+ cmap: Map<String, Any>
+ ): Long {
+ // create the context map
+ val context = handle(template, map, cmap)
+ // create a file writer for the output reference
+ try {
+ output.toFile().writer().use { writer ->
+ // the document is actually read into a string reader
+ val reader = template.document!!.reader()
+ // load both runtime services and the template model from Velocity
+ val services = RuntimeSingleton.getRuntimeServices()
+ services.addProperty(RuntimeConstants.RUNTIME_LOG_INSTANCE,
+ NOPLoggerFactory().getLogger(""))
+ val reference = org.apache.velocity.Template()
+ // set both runtime services and document data into the template document
+ reference.setRuntimeServices(services)
+ reference.data = services.parse(reader, reference)
+ reference.initDocument()
+ // create the context based on the data map previously set
+ val entries = VelocityContext(context)
+ // merge both template and data into the file writer
+ reference.merge(entries, writer)
+ }
+ } catch (exception: Exception) {
+ // TODO: simplify
+ when (exception) {
+ is IOException, is MethodInvocationException, is ParseErrorException,
+ is ParseException, is ResourceNotFoundException, is TemplateInitException ->
+ throw TemplateMergingException("An error occurred while " +
+ "trying to merge the template reference with the " +
+ "provided data. Make sure the template is correct " +
+ "and try again. The raised exception might give us " +
+ "some hints on what exactly happened. Typically, " +
+ "make sure the template strictly follows the " +
+ "Velocity 2.0 language syntax.", exception)
+ else -> throw TemplateMergingException("Fatal error occured. " +
+ "This error should never happen. Please make a detailed " +
+ "report to the developers.")
+ }
+ }
+ // simply return the length of the generated output file
+ return output.toFile().length()
+ }
+
+ /**
+ * Handles the context map.
+ *
+ * @param template The template model.
+ * @param map The context map.
+ * @param configmap The map from a configuration file.
+ * @return The new context map.
+ */
+ private fun handle(
+ template: Template,
+ map: Map<String, String?>,
+ configmap: Map<String, Any>
+ ): Map<String, Any?> {
+ // no handlers found
+ return if (template.handlers.isEmpty()) {
+ // create a new map from the command line map and put the values from
+ // the configuration file cmap, if absent
+ configmap.mapValues { it.value.toString() }.plus(map)
+ } else {
+ // get default handlers and set the resulting map
+ val result: MutableMap<String, Any?> = mutableMapOf()
+ // check each key from the map
+ map.forEach { (key: String, value: String?) ->
+ // there is a handler for the current key
+ if (template.handlers.containsKey(key) &&
+ handlers.containsKey(template.handlers[key])) {
+ // apply the handler and store the value in the map
+ result[key] = handlers[template.handlers[key]]!!.apply(value)
+ } else {
+ // simply store the value
+ result[key] = value
+ // TODO: should we warn about an invalid handler?
+ }
+ }
+
+ // put remaining values from the configuration file, if absent
+ configmap.plus(result)
+ }
+ }
+}
diff --git a/support/texplate/source/main/kotlin/org/islandoftex/texplate/util/MessageUtils.kt b/support/texplate/source/main/kotlin/org/islandoftex/texplate/util/MessageUtils.kt
new file mode 100644
index 0000000000..fbb68a819e
--- /dev/null
+++ b/support/texplate/source/main/kotlin/org/islandoftex/texplate/util/MessageUtils.kt
@@ -0,0 +1,72 @@
+// SPDX-License-Identifier: BSD-3-Clause
+package org.islandoftex.texplate.util
+
+import java.time.LocalDate
+
+/**
+ * Message helper methods.
+ *
+ * @version 1.0
+ * @since 1.0
+ */
+object MessageUtils {
+ // the message width
+ private const val WIDTH = 60
+ // the application version
+ private val VERSION = MessageUtils::class.java.`package`.implementationVersion
+ ?: "DEVELOPMENT BUILD"
+
+ /**
+ * Prints a line in the terminal, without a line break.
+ *
+ * @param message The message to be printed.
+ */
+ @JvmStatic
+ fun line(message: String) {
+ print("$message ".padEnd(WIDTH - " [FAILED]".length, '.') + " ")
+ }
+
+ /**
+ * Prints the status in the terminal.
+ *
+ * @param result The boolean value.
+ */
+ @JvmStatic
+ fun status(result: Boolean) {
+ println(if (result) "[ DONE ]" else "[FAILED]")
+ }
+
+ /**
+ * Prints the error in the terminal.
+ *
+ * @param throwable The throwable reference.
+ */
+ @JvmStatic
+ fun error(throwable: Throwable) {
+ println("\n" + "HOUSTON, WE'VE GOT A PROBLEM ".padEnd(WIDTH, '-') +
+ "\n" + throwable.message + "\n" +
+ "".padStart(WIDTH, '-') + "\n")
+ }
+
+ /**
+ * Prints the application logo in the terminal.
+ */
+ fun drawLogo() {
+ println(
+ " ______ __ __ ___ __ \n" +
+ "/\\__ _\\ /\\ \\ /\\ \\ /\\_ \\ /\\ \\__ \n" +
+ "\\/_/\\ \\/ __ \\ `\\`\\/'/' _____\\//\\ \\ __ \\ \\ ,_\\ __ \n" +
+ " \\ \\ \\ /'__`\\`\\/ > < /\\ '__`\\\\ \\ \\ /'__`\\ \\ \\ \\/ /'__`\\ \n" +
+ " \\ \\ \\/\\ __/ \\/'/\\`\\\\ \\ \\L\\ \\\\_\\ \\_/\\ \\L\\.\\_\\ \\ \\_/\\ __/ \n" +
+ " \\ \\_\\ \\____\\ /\\_\\\\ \\_\\ \\ ,__//\\____\\ \\__/.\\_\\\\ \\__\\ \\____\\\n" +
+ " \\/_/\\/____/ \\/_/ \\/_/\\ \\ \\/ \\/____/\\/__/\\/_/ \\/__/\\/____/\n" +
+ " \\ \\_\\ \n" +
+ " \\/_/ \n"
+ )
+ println(
+ "TeXplate $VERSION, a document structure creation tool\n" +
+ "Copyright (c) ${LocalDate.now().year}, Island of TeX\n" +
+ "All rights reserved.\n"
+ )
+ }
+}
diff --git a/support/texplate/source/main/kotlin/org/islandoftex/texplate/util/PathUtils.kt b/support/texplate/source/main/kotlin/org/islandoftex/texplate/util/PathUtils.kt
new file mode 100644
index 0000000000..3a74603557
--- /dev/null
+++ b/support/texplate/source/main/kotlin/org/islandoftex/texplate/util/PathUtils.kt
@@ -0,0 +1,69 @@
+// SPDX-License-Identifier: BSD-3-Clause
+package org.islandoftex.texplate.util
+
+import java.io.FileNotFoundException
+import java.nio.file.Files
+import java.nio.file.Path
+import java.nio.file.Paths
+
+/**
+ * Helper methods for path handling.
+ *
+ * @version 1.0
+ * @since 1.0
+ */
+object PathUtils {
+ // the templates folder
+ private const val TEMPLATES_FOLDER = "templates"
+ // the user application folder
+ private const val USER_APPLICATION_FOLDER = ".texplate"
+
+ /**
+ * The user's template path.
+ */
+ private val userTemplatePath: Path
+ get() = try {
+ Paths.get(System.getProperty("user.home"),
+ USER_APPLICATION_FOLDER, TEMPLATES_FOLDER)
+ } catch (e: RuntimeException) {
+ Paths.get(".")
+ }
+
+ /**
+ * Searches all paths looking for the provided template.
+ *
+ * @param name The name to be associated to a template file.
+ * @return The corresponding template file.
+ * @throws FileNotFoundException The template file could not be found.
+ */
+ @JvmStatic
+ @Throws(FileNotFoundException::class)
+ fun getTemplatePath(name: String): Path {
+ // the file has to be a TOML format, so we add the extension
+ val fullName = "$name.toml"
+ // the first reference is based on the user template path resolved with the
+ // file name
+ val reference = userTemplatePath.resolve(fullName)
+ // if the file actually exists, the search is done!
+ return if (Files.exists(reference)) {
+ reference
+ } else {
+ // the reference was not found in the user location, so let us try the
+ // system counterpart
+ try {
+ val tempFile = Files.createTempFile(null, null)
+ tempFile.toFile().writeText(PathUtils::class.java
+ .getResource("/org/islandoftex/texplate/templates/texplate-$fullName")
+ .readText())
+ tempFile
+ } catch (e: RuntimeException) {
+ throw FileNotFoundException("I am sorry, but the template " +
+ "file '" + fullName + "' could not be found in the " +
+ "default template locations (system and user). Make " +
+ "sure the reference is correct and try again. For " +
+ "reference, these are the paths I searched: '" +
+ userTemplatePath + "'.")
+ }
+ }
+ }
+}
diff --git a/support/texplate/source/main/kotlin/org/islandoftex/texplate/util/ValidatorUtils.kt b/support/texplate/source/main/kotlin/org/islandoftex/texplate/util/ValidatorUtils.kt
new file mode 100644
index 0000000000..ef91a012c1
--- /dev/null
+++ b/support/texplate/source/main/kotlin/org/islandoftex/texplate/util/ValidatorUtils.kt
@@ -0,0 +1,54 @@
+// SPDX-License-Identifier: BSD-3-Clause
+package org.islandoftex.texplate.util
+
+import org.islandoftex.texplate.exceptions.InvalidKeySetException
+import org.islandoftex.texplate.model.Template
+
+/**
+ * Helper methods for validation.
+ *
+ * @version 1.0
+ * @since 1.0
+ */
+object ValidatorUtils {
+ /**
+ * Validates the data map based on the template requirements.
+ *
+ * @param template The template.
+ * @param map The data map.
+ * @return A boolean value indicating whether the data map is valid.
+ */
+ private fun validateRequirements(
+ template: Template,
+ map: Map<String, String>
+ ): Boolean {
+ return template.requirements.isNullOrEmpty() ||
+ template.requirements.containsAll(map.keys)
+ }
+
+ /**
+ * Validates the template pattern and the data map and throws an exception
+ * in case of failure.
+ *
+ * @param template The template.
+ * @param map The data map.
+ * @return The data map.
+ * @throws InvalidKeySetException There are invalid keys in the map.
+ */
+ @JvmStatic
+ @Throws(InvalidKeySetException::class)
+ fun validate(
+ template: Template,
+ map: Map<String, String>
+ ): Map<String, String> {
+ // for starters, we try to validate the template requirements
+ return if (validateRequirements(template, map)) {
+ map
+ } else {
+ throw InvalidKeySetException("The provided map does not " +
+ "contain all the keys required by the chosen " +
+ "template. Make sure to define such keys and try " +
+ "again. Check the user manual for further details.")
+ }
+ }
+}