summaryrefslogtreecommitdiff
path: root/support/arara/source/src/main/kotlin/org/islandoftex/arara/model
diff options
context:
space:
mode:
Diffstat (limited to 'support/arara/source/src/main/kotlin/org/islandoftex/arara/model')
-rw-r--r--support/arara/source/src/main/kotlin/org/islandoftex/arara/model/AraraException.kt50
-rw-r--r--support/arara/source/src/main/kotlin/org/islandoftex/arara/model/Evaluator.kt122
-rw-r--r--support/arara/source/src/main/kotlin/org/islandoftex/arara/model/Extractor.kt50
-rw-r--r--support/arara/source/src/main/kotlin/org/islandoftex/arara/model/FileType.kt110
-rw-r--r--support/arara/source/src/main/kotlin/org/islandoftex/arara/model/Interpreter.kt375
-rw-r--r--support/arara/source/src/main/kotlin/org/islandoftex/arara/model/Session.kt119
6 files changed, 826 insertions, 0 deletions
diff --git a/support/arara/source/src/main/kotlin/org/islandoftex/arara/model/AraraException.kt b/support/arara/source/src/main/kotlin/org/islandoftex/arara/model/AraraException.kt
new file mode 100644
index 0000000000..6bf2d1e617
--- /dev/null
+++ b/support/arara/source/src/main/kotlin/org/islandoftex/arara/model/AraraException.kt
@@ -0,0 +1,50 @@
+// SPDX-License-Identifier: BSD-3-Clause
+package org.islandoftex.arara.model
+
+/**
+ * Implements the specific exception model for arara.
+ *
+ * @author Island of TeX
+ * @version 5.0
+ * @since 4.0
+ */
+class AraraException : Exception {
+ /**
+ * The underlying exception, used to hold more details
+ * on what really happened
+ */
+ val exception: Exception?
+
+ /**
+ * Constructor. Takes the exception message.
+ * @param message The exception message.
+ */
+ constructor(message: String) : super(message) {
+ this.exception = null
+ }
+
+ /**
+ * Constructor. Takes the exception message and the underlying exception.
+ * @param message The exception message.
+ * @param exception The underlying exception object.
+ */
+ constructor(message: String, exception: Exception) : super(message) {
+ this.exception = exception
+ }
+
+ /**
+ * Constructor. Takes the exception message and the underlying exception.
+ * @param message The exception message.
+ * @param throwable The underlying exception as generic throwable.
+ */
+ constructor(message: String, throwable: Throwable) : super(message) {
+ this.exception = RuntimeException(throwable)
+ }
+
+ /**
+ * Checks if there is an underlying exception defined in the current object.
+ * @return A boolean value indicating if the current object has an
+ * underlying exception.
+ */
+ fun hasException(): Boolean = exception?.message != null
+}
diff --git a/support/arara/source/src/main/kotlin/org/islandoftex/arara/model/Evaluator.kt b/support/arara/source/src/main/kotlin/org/islandoftex/arara/model/Evaluator.kt
new file mode 100644
index 0000000000..78b98e709a
--- /dev/null
+++ b/support/arara/source/src/main/kotlin/org/islandoftex/arara/model/Evaluator.kt
@@ -0,0 +1,122 @@
+// SPDX-License-Identifier: BSD-3-Clause
+package org.islandoftex.arara.model
+
+import org.islandoftex.arara.Arara
+import org.islandoftex.arara.configuration.AraraSpec
+import org.islandoftex.arara.localization.LanguageController
+import org.islandoftex.arara.localization.Messages
+import org.islandoftex.arara.ruleset.Conditional
+import org.islandoftex.arara.utils.Methods
+import org.mvel2.templates.TemplateRuntime
+
+/**
+ * Implements the evaluator model, on which a conditional can be analyzed and
+ * processed.
+ *
+ * @author Island of TeX
+ * @version 5.0
+ * @since 4.0
+ */
+class Evaluator {
+ // this attribute holds the maximum number of
+ // loops arara will accept; it's like
+ // reaching infinity
+ private val loops: Int = Arara.config[AraraSpec.Execution.maxLoops]
+
+ // the counter for the current execution, it
+ // helps us keep track of the number of times
+ // this evaluation has happened, and also to
+ // prevent potential infinite loops
+ private var counter: Int = 0
+
+ // a flag that indicates the
+ // evaluation to halt regardless
+ // of the the result
+ private var halt: Boolean = false
+
+ /**
+ * Check if a condition is of type if or unless and whether halt
+ * is set.
+ * @param type The type to check.
+ * @param haltCheck The value [halt] should have.
+ * @return `(type == if || type == unless) && haltCheck`
+ */
+ private fun isIfUnlessAndHalt(
+ type: Conditional.ConditionalType,
+ haltCheck: Boolean = true
+ ): Boolean =
+ (type == Conditional.ConditionalType.IF ||
+ type == Conditional.ConditionalType.UNLESS) &&
+ halt == haltCheck
+
+ /**
+ * Only run the evaluation of the conditional including a check whether
+ * the result needs to be inverted.
+ * @param conditional The conditional.
+ * @return The result of the evaluation.
+ */
+ @Throws(AraraException::class, RuntimeException::class)
+ private fun evaluateCondition(conditional: Conditional): Boolean {
+ val result = TemplateRuntime.eval("@{ " + conditional.condition + " }",
+ Methods.getConditionalMethods())
+ return if (result is Boolean) {
+ if (conditional.type == Conditional.ConditionalType.UNLESS ||
+ conditional.type == Conditional.ConditionalType.UNTIL)
+ !result
+ else
+ result
+ } else {
+ throw AraraException(messages.getMessage(
+ Messages.ERROR_EVALUATE_NOT_BOOLEAN_VALUE))
+ }
+ }
+
+ /**
+ * Evaluate the provided conditional.
+ *
+ * @param conditional The conditional object.
+ * @return A boolean value indicating if the conditional holds.
+ * @throws AraraException Something wrong happened, to be caught in the
+ * higher levels.
+ */
+ @Throws(AraraException::class)
+ @Suppress("TooGenericExceptionCaught")
+ fun evaluate(conditional: Conditional): Boolean {
+ // when in dry-run mode or not evaluating a
+ // conditional, arara always ignores conditional
+ // evaluations
+ if (conditional.type == Conditional.ConditionalType.NONE ||
+ Arara.config[AraraSpec.Execution.dryrun] ||
+ isIfUnlessAndHalt(conditional.type, true))
+ return false
+ else if (isIfUnlessAndHalt(conditional.type, false)) {
+ halt = true
+ }
+
+ // check counters and see if the execution
+ // has reached our concept of infinity,
+ // thus breaking the cycles
+ counter++
+ return when {
+ conditional.type === Conditional.ConditionalType.WHILE
+ && counter > loops -> false
+ conditional.type === Conditional.ConditionalType.UNTIL
+ && counter >= loops -> false
+ else -> {
+ try {
+ evaluateCondition(conditional)
+ } catch (exception: RuntimeException) {
+ throw AraraException(messages.getMessage(Messages
+ .ERROR_EVALUATE_COMPILATION_FAILED),
+ exception)
+ }
+ }
+ }
+ }
+
+ companion object {
+ // the application messages obtained from the
+ // language controller
+ private val messages = LanguageController
+ }
+}
diff --git a/support/arara/source/src/main/kotlin/org/islandoftex/arara/model/Extractor.kt b/support/arara/source/src/main/kotlin/org/islandoftex/arara/model/Extractor.kt
new file mode 100644
index 0000000000..ed4d811d22
--- /dev/null
+++ b/support/arara/source/src/main/kotlin/org/islandoftex/arara/model/Extractor.kt
@@ -0,0 +1,50 @@
+// SPDX-License-Identifier: BSD-3-Clause
+package org.islandoftex.arara.model
+
+import java.io.File
+import java.io.IOException
+import java.nio.charset.Charset
+import org.islandoftex.arara.localization.LanguageController
+import org.islandoftex.arara.localization.Messages
+import org.islandoftex.arara.ruleset.Directive
+import org.islandoftex.arara.ruleset.DirectiveUtils
+import org.islandoftex.arara.utils.CommonUtils
+
+/**
+ * Extractor for directives from the provided main file.
+ *
+ * @author Island of TeX
+ * @version 5.0
+ * @since 4.0
+ */
+object Extractor {
+ // the application messages obtained from the
+ // language controller
+ private val messages = LanguageController
+
+ /**
+ * Extracts a list of directives from the provided main file, obtained from
+ * the configuration controller.
+ * @param file The file to extract the directives from.
+ * @param charset The charset of the file.
+ * @return A list of directives.
+ * @throws AraraException Something wrong happened, to be caught in the
+ * higher levels.
+ */
+ @Throws(AraraException::class)
+ fun extract(file: File, charset: Charset = Charsets.UTF_8):
+ List<Directive> {
+ try {
+ val content = CommonUtils.preambleContent.toMutableList()
+ content.addAll(file.readLines(charset))
+ return DirectiveUtils.extractDirectives(content)
+ } catch (ioexception: IOException) {
+ throw AraraException(
+ messages.getMessage(
+ Messages.ERROR_EXTRACTOR_IO_ERROR
+ ),
+ ioexception
+ )
+ }
+ }
+}
diff --git a/support/arara/source/src/main/kotlin/org/islandoftex/arara/model/FileType.kt b/support/arara/source/src/main/kotlin/org/islandoftex/arara/model/FileType.kt
new file mode 100644
index 0000000000..e6ed27d51a
--- /dev/null
+++ b/support/arara/source/src/main/kotlin/org/islandoftex/arara/model/FileType.kt
@@ -0,0 +1,110 @@
+// SPDX-License-Identifier: BSD-3-Clause
+package org.islandoftex.arara.model
+
+import java.util.regex.PatternSyntaxException
+import kotlinx.serialization.Serializable
+import org.islandoftex.arara.configuration.ConfigurationUtils
+import org.islandoftex.arara.localization.LanguageController
+import org.islandoftex.arara.localization.Messages
+import org.islandoftex.arara.utils.CommonUtils
+
+/**
+ * Implements the file type model.
+ *
+ * @author Island of TeX
+ * @version 5.0
+ * @since 4.0
+ */
+@Serializable
+class FileType {
+ // string representing the
+ // file extension
+ var extension: String = INVALID_EXTENSION
+ get() = CommonUtils.removeKeywordNotNull(field)
+ private set
+ // string representing the
+ // file pattern to be used
+ // as directive lookup
+ var pattern: String = INVALID_PATTERN
+ @Throws(AraraException::class)
+ get() {
+ CommonUtils.removeKeywordNotNull(field)
+ if (field == INVALID_PATTERN) {
+ field = ConfigurationUtils.defaultFileTypePatterns[extension]
+ ?: throw AraraException(
+ LanguageController.getMessage(
+ Messages.ERROR_FILETYPE_UNKNOWN_EXTENSION,
+ extension,
+ CommonUtils.fileTypesList
+ )
+ )
+ }
+ return field
+ }
+ private set
+
+ constructor(extension: String, pattern: String) {
+ this.extension = extension
+ this.pattern = pattern
+
+ try {
+ pattern.toPattern()
+ } catch (e: PatternSyntaxException) {
+ if (!ConfigurationUtils.defaultFileTypePatterns.containsKey(extension))
+ throw AraraException(
+ LanguageController.getMessage(
+ Messages.ERROR_FILETYPE_UNKNOWN_EXTENSION,
+ extension,
+ CommonUtils.fileTypesList
+ )
+ )
+ }
+ }
+
+ companion object {
+ /**
+ * This constant identifies an invalid extension. As unices do not
+ * allow a forward and Windows does not allow a backward slash, this
+ * should suffice.
+ */
+ const val INVALID_EXTENSION = "/\\"
+ /**
+ * This constant identifies an invalid pattern. This is a opening
+ * character class which is invalid.
+ */
+ const val INVALID_PATTERN = "["
+ }
+
+ /**
+ * Provides a textual representation of the current file type object.
+ * @return A string containing a textual representation of the current file
+ * type object.
+ */
+ override fun toString(): String {
+ return ".$extension"
+ }
+
+ /**
+ * Implements the file type equals method, checking if one file type is
+ * equal to another. Note that only the file extension is considered.
+ * @param other The object to be analyzed.
+ * @return A boolean value indicating if those two objects are equal.
+ */
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as FileType
+ if (extension != other.extension) return false
+ return true
+ }
+
+ /**
+ * Implements the file type hash code. Note that only the file extension is
+ * considered.
+ * @return An integer representing the file type hash code.
+ */
+ override fun hashCode(): Int {
+ return extension.hashCode()
+ }
+}
diff --git a/support/arara/source/src/main/kotlin/org/islandoftex/arara/model/Interpreter.kt b/support/arara/source/src/main/kotlin/org/islandoftex/arara/model/Interpreter.kt
new file mode 100644
index 0000000000..49cf40f1a2
--- /dev/null
+++ b/support/arara/source/src/main/kotlin/org/islandoftex/arara/model/Interpreter.kt
@@ -0,0 +1,375 @@
+// SPDX-License-Identifier: BSD-3-Clause
+package org.islandoftex.arara.model
+
+import java.io.File
+import kotlin.time.ExperimentalTime
+import org.islandoftex.arara.Arara
+import org.islandoftex.arara.configuration.AraraSpec
+import org.islandoftex.arara.localization.LanguageController
+import org.islandoftex.arara.localization.Messages
+import org.islandoftex.arara.ruleset.Argument
+import org.islandoftex.arara.ruleset.Command
+import org.islandoftex.arara.ruleset.Conditional
+import org.islandoftex.arara.ruleset.Directive
+import org.islandoftex.arara.ruleset.Rule
+import org.islandoftex.arara.ruleset.RuleCommand
+import org.islandoftex.arara.ruleset.RuleUtils
+import org.islandoftex.arara.utils.CommonUtils
+import org.islandoftex.arara.utils.DisplayUtils
+import org.islandoftex.arara.utils.InterpreterUtils
+import org.islandoftex.arara.utils.Methods
+import org.mvel2.templates.TemplateRuntime
+import org.slf4j.LoggerFactory
+
+/**
+ * Interprets the list of directives.
+ *
+ * @author Island of TeX
+ * @version 5.0
+ * @since 4.0
+ */
+class Interpreter(
+ /**
+ * The list of directives to be interpreted and evaluated.
+ */
+ val directives: List<Directive>
+) {
+ /**
+ * Exception class to represent that the interpreter should stop for some
+ * reason
+ */
+ private class HaltExpectedException(msg: String) : Exception(msg)
+
+ /**
+ * Gets the rule according to the provided directive.
+ *
+ * @param directive The provided directive.
+ * @return The absolute canonical path of the rule, given the provided
+ * directive.
+ * @throws AraraException Something wrong happened, to be caught in the
+ * higher levels.
+ */
+ @Throws(AraraException::class)
+ private fun getRule(directive: Directive): File {
+ return InterpreterUtils.buildRulePath(directive.identifier)
+ ?: throw AraraException(
+ messages.getMessage(
+ Messages.ERROR_INTERPRETER_RULE_NOT_FOUND,
+ directive.identifier,
+ "(" + CommonUtils.allRulePaths
+ .joinToString("; ") + ")"
+ )
+ )
+ }
+
+ // TODO: in the following, extract the printing into the higher level
+ // function
+ /**
+ * "Run" a boolean return value
+ * @param value The boolean.
+ * @param conditional The conditional to print in dry-run mode.
+ * @param authors The authors of the rule.
+ * @return Returns [value]
+ */
+ private fun runBoolean(
+ value: Boolean,
+ conditional: Conditional,
+ authors: List<String>
+ ): Boolean {
+ logger.info(messages.getMessage(Messages.LOG_INFO_BOOLEAN_MODE,
+ value.toString()))
+
+ if (Arara.config[AraraSpec.Execution.dryrun]) {
+ DisplayUtils.printAuthors(authors)
+ DisplayUtils.wrapText(messages.getMessage(Messages
+ .INFO_INTERPRETER_DRYRUN_MODE_BOOLEAN_MODE,
+ value))
+ DisplayUtils.printConditional(conditional)
+ }
+
+ return value
+ }
+
+ /**
+ * Run a command
+ * @param command The command to run.
+ * @param conditional The conditional applied to the run (only for printing).
+ * @param authors The rule authors (only for printing).
+ * @param ruleCommandExitValue The exit value of the rule command.
+ * @return Success of the execution.
+ * @throws AraraException Execution failed.
+ */
+ @ExperimentalTime
+ @Throws(AraraException::class)
+ @Suppress("TooGenericExceptionCaught")
+ private fun runCommand(
+ command: Command,
+ conditional: Conditional,
+ authors: List<String>,
+ ruleCommandExitValue: String?
+ ): Boolean {
+ logger.info(messages.getMessage(Messages.LOG_INFO_SYSTEM_COMMAND,
+ command))
+ var success = true
+
+ if (!Arara.config[AraraSpec.Execution.dryrun]) {
+ val code = InterpreterUtils.run(command)
+ val check: Any = try {
+ val context = mapOf<String, Any>("value" to code)
+ TemplateRuntime.eval(
+ "@{ " + (ruleCommandExitValue ?: "value == 0") + " }",
+ context)
+ } catch (exception: RuntimeException) {
+ throw AraraException(CommonUtils.ruleErrorHeader +
+ messages.getMessage(Messages
+ .ERROR_INTERPRETER_EXIT_RUNTIME_ERROR),
+ exception)
+ }
+
+ success = if (check is Boolean) {
+ check
+ } else {
+ throw AraraException(
+ CommonUtils.ruleErrorHeader + messages.getMessage(
+ Messages.ERROR_INTERPRETER_WRONG_EXIT_CLOSURE_RETURN
+ )
+ )
+ }
+ } else {
+ DisplayUtils.printAuthors(authors)
+ DisplayUtils.wrapText(messages.getMessage(
+ Messages.INFO_INTERPRETER_DRYRUN_MODE_SYSTEM_COMMAND,
+ command))
+ DisplayUtils.printConditional(conditional)
+ }
+
+ return success
+ }
+
+ /**
+ * Converts the command evaluation result to a flat list.
+ * @param result The result
+ * @return A flat list.
+ */
+ private fun resultToList(result: Any) = if (result is List<*>) {
+ CommonUtils.flatten(result)
+ } else {
+ listOf(result)
+ }
+
+ /**
+ * Execute a command.
+ * @param command The command to evaluate.
+ * @param conditional Under which condition to execute.
+ * @param rule The rule (only passed for output purposes).
+ * @param parameters The parameters for evaluation
+ * @throws AraraException Running the command failed.
+ */
+ @ExperimentalTime
+ @Throws(AraraException::class)
+ @Suppress("TooGenericExceptionCaught", "ThrowsCount")
+ private fun executeCommand(
+ command: RuleCommand,
+ conditional: Conditional,
+ rule: Rule,
+ parameters: Map<String, Any>
+ ) {
+ val result: Any = try {
+ TemplateRuntime.eval(command.command!!, parameters)
+ } catch (exception: RuntimeException) {
+ throw AraraException(CommonUtils.ruleErrorHeader +
+ messages.getMessage(Messages
+ .ERROR_INTERPRETER_COMMAND_RUNTIME_ERROR),
+ exception)
+ }
+
+ // TODO: check nullability
+ resultToList(result).filter { it.toString().isNotBlank() }
+ .forEach { current ->
+ DisplayUtils.printEntry(rule.name, command.name
+ ?: messages.getMessage(Messages
+ .INFO_LABEL_UNNAMED_TASK))
+
+ val success = when (current) {
+ is Boolean -> runBoolean(current, conditional,
+ rule.authors)
+ is Command -> runCommand(current, conditional,
+ rule.authors, command.exit)
+ else -> TODO("error: this should not happen" +
+ "we are only supporting boolean + command")
+ }
+
+ DisplayUtils.printEntryResult(success)
+
+ if (Arara.config[AraraSpec.Execution.haltOnErrors] && !success)
+ // TODO: localize
+ throw HaltExpectedException("Command failed")
+
+ // TODO: document this key
+ val haltKey = "arara:${Arara.config[AraraSpec
+ .Execution.reference].name}:halt"
+ if (Session.contains(haltKey)) {
+ Arara.config[AraraSpec.Execution.status] =
+ Session[haltKey].toString().toInt()
+ // TODO: localize
+ throw HaltExpectedException("User requested halt")
+ }
+ }
+ }
+
+ /**
+ * Executes each directive, throwing an exception if something bad has
+ * happened.
+ *
+ * @throws AraraException Something wrong happened, to be caught in the
+ * higher levels.
+ */
+ @ExperimentalTime
+ @Throws(AraraException::class)
+ @Suppress("NestedBlockDepth")
+ fun execute() {
+ for (directive in directives) {
+ logger.info(messages.getMessage(Messages.LOG_INFO_INTERPRET_RULE,
+ directive.identifier))
+
+ Arara.config[AraraSpec.Execution.file] =
+ directive.parameters.getValue("reference") as File
+ val file = getRule(directive)
+
+ logger.info(messages.getMessage(Messages.LOG_INFO_RULE_LOCATION,
+ file.parent))
+
+ Arara.config[AraraSpec.Execution.InfoSpec.ruleId] =
+ directive.identifier
+ Arara.config[AraraSpec.Execution.InfoSpec.rulePath] =
+ file.parent
+ Arara.config[AraraSpec.Execution.DirectiveSpec.lines] =
+ directive.lineNumbers
+
+ // parse the rule identified by the directive
+ // (may throw an exception)
+ val rule = RuleUtils.parseRule(file, directive.identifier)
+ val parameters = parseArguments(rule, directive)
+ .plus(Methods.getRuleMethods())
+
+ val evaluator = Evaluator()
+
+ var available = true
+ if (InterpreterUtils.runPriorEvaluation(directive.conditional)) {
+ available = evaluator.evaluate(directive.conditional)
+ }
+
+ // if this directive is conditionally disabled, skip
+ if (!available) continue
+ // if not execute the commands associated with the directive
+ do {
+ rule.commands.forEach { command ->
+ try {
+ executeCommand(command, directive.conditional, rule, parameters)
+ } catch (_: HaltExpectedException) {
+ // if the user uses the halt rule to trigger
+ // a halt, this will be raised
+ return
+ }
+ }
+ } while (evaluator.evaluate(directive.conditional))
+ }
+ }
+
+ /**
+ * Parses the rule arguments against the provided directive.
+ *
+ * @param rule The rule object.
+ * @param directive The directive.
+ * @return A map containing all arguments resolved according to the
+ * directive parameters.
+ * @throws AraraException Something wrong happened, to be caught in the
+ * higher levels.
+ */
+ @Throws(AraraException::class)
+ private fun parseArguments(rule: Rule, directive: Directive):
+ Map<String, Any> {
+ val arguments = rule.arguments
+ val unknown = CommonUtils.getUnknownKeys(directive.parameters,
+ arguments).minus("reference")
+ if (unknown.isNotEmpty())
+ throw AraraException(CommonUtils.ruleErrorHeader +
+ messages.getMessage(
+ Messages.ERROR_INTERPRETER_UNKNOWN_KEYS,
+ "(" + unknown.joinToString(", ") + ")"))
+
+ val resolvedArguments = mutableMapOf<String, Any>()
+ resolvedArguments["reference"] = directive.parameters
+ .getValue("reference")
+
+ val context = mapOf(
+ "parameters" to directive.parameters,
+ "reference" to directive.parameters.getValue("reference")
+ ).plus(Methods.getRuleMethods())
+
+ arguments.forEach { argument ->
+ resolvedArguments[argument.identifier!!] = processArgument(argument,
+ directive.parameters.containsKey(argument.identifier!!),
+ context)
+ }
+
+ return resolvedArguments
+ }
+
+ /**
+ * Process a single argument and return the evaluated result.
+ * @param argument The argument to process.
+ * @param idInDirectiveParams Whether the argument's identifier is
+ * contained in the directive's parameters field.
+ * @param context The context for the evaluation.
+ * @return The result of the evaluation.
+ * @throws AraraException The argument could not be processed.
+ */
+ @Throws(AraraException::class)
+ @Suppress("TooGenericExceptionCaught", "ThrowsCount")
+ private fun processArgument(
+ argument: Argument,
+ idInDirectiveParams: Boolean,
+ context: Map<String, Any>
+ ): Any {
+ if (argument.isRequired && !idInDirectiveParams)
+ throw AraraException(CommonUtils.ruleErrorHeader +
+ messages.getMessage(
+ Messages.ERROR_INTERPRETER_ARGUMENT_IS_REQUIRED,
+ argument.identifier!!))
+
+ var ret = argument.default?.let {
+ try {
+ TemplateRuntime.eval(it, context)
+ } catch (exception: RuntimeException) {
+ throw AraraException(CommonUtils.ruleErrorHeader +
+ messages.getMessage(Messages
+ .ERROR_INTERPRETER_DEFAULT_VALUE_RUNTIME_ERROR),
+ exception)
+ }
+ } ?: ""
+
+ if (argument.flag != null && idInDirectiveParams) {
+ ret = try {
+ TemplateRuntime.eval(argument.flag!!, context)
+ } catch (exception: RuntimeException) {
+ throw AraraException(CommonUtils.ruleErrorHeader + messages
+ .getMessage(Messages
+ .ERROR_INTERPRETER_FLAG_RUNTIME_EXCEPTION),
+ exception)
+ }
+ }
+
+ return ret
+ }
+
+ companion object {
+ // the application messages obtained from the
+ // language controller
+ private val messages = LanguageController
+
+ // the class logger obtained from
+ // the logger factory
+ private val logger = LoggerFactory.getLogger(Interpreter::class.java)
+ }
+}
diff --git a/support/arara/source/src/main/kotlin/org/islandoftex/arara/model/Session.kt b/support/arara/source/src/main/kotlin/org/islandoftex/arara/model/Session.kt
new file mode 100644
index 0000000000..c2093429e1
--- /dev/null
+++ b/support/arara/source/src/main/kotlin/org/islandoftex/arara/model/Session.kt
@@ -0,0 +1,119 @@
+// SPDX-License-Identifier: BSD-3-Clause
+package org.islandoftex.arara.model
+
+import org.islandoftex.arara.localization.LanguageController
+import org.islandoftex.arara.localization.Messages
+
+/**
+ * Implements the session.
+ *
+ * This class wraps a map that holds the execution session, that is, a dirty
+ * maneuver to exchange pretty much any data between commands and even rules.
+ *
+ * @author Island of TeX
+ * @version 5.0
+ * @since 4.0
+ */
+object Session {
+ // the application messages obtained from the
+ // language controller
+ private val messages = LanguageController
+
+ // the session map which holds the execution session;
+ // the idea here is to provide wrappers to the map
+ // methods, so it could be easily manipulated
+ private val map = mutableMapOf<String, Any>()
+
+ /**
+ * Gets the object indexed by the provided key from the session. This method
+ * holds the map method of the very same name.
+ *
+ * @param key The provided key.
+ * @return The object indexed by the provided key.
+ * @throws AraraException Something wrong happened, to be caught in the
+ * higher levels.
+ */
+ @Throws(AraraException::class)
+ operator fun get(key: String): Any {
+ return if (contains(key)) {
+ map.getValue(key)
+ } else {
+ throw AraraException(
+ messages.getMessage(
+ Messages.ERROR_SESSION_OBTAIN_UNKNOWN_KEY,
+ key
+ )
+ )
+ }
+ }
+
+ /**
+ * Inserts (or overwrites) the object indexed by the provided key into the
+ * session. This method holds the map method of the very same name.
+ *
+ * @param key The provided key.
+ * @param value The value to be inserted.
+ */
+ fun put(key: String, value: Any) {
+ map[key] = value
+ }
+
+ /**
+ * Removes the entry indexed by the provided key from the session. This method
+ * holds the map method of the same name.
+ *
+ * @param key The provided key.
+ * @throws AraraException Something wrong happened, to be caught in the
+ * higher levels.
+ */
+ @Throws(AraraException::class)
+ fun remove(key: String) {
+ if (contains(key)) {
+ map.remove(key)
+ } else {
+ throw AraraException(
+ messages.getMessage(
+ Messages.ERROR_SESSION_REMOVE_UNKNOWN_KEY,
+ key
+ )
+ )
+ }
+ }
+
+ /**
+ * Checks if the provided key exists in the session.
+ *
+ * @param key The provided key.
+ * @return A boolean value indicating if the provided key exists in the
+ * session.
+ */
+ operator fun contains(key: String): Boolean = map.containsKey(key)
+
+ /**
+ * Clears the session (map). This method, as usual, holds the map method of
+ * the same name.
+ */
+ fun clear() = map.clear()
+
+ /**
+ * Update the environment variables stored in the session.
+ *
+ * @param additionFilter Which environment variables to include. You can
+ * filter their names (the string parameter) but not their values. By
+ * default all values will be added.
+ * @param removalFilter Which environment variables to remove beforehand.
+ * By default all values will be removed.
+ */
+ fun updateEnvironmentVariables(
+ additionFilter: (String) -> Boolean = { true },
+ removalFilter: (String) -> Boolean = { true }
+ ) {
+ // remove all current environment variables to clean up the session
+ map.filterKeys { it.startsWith("environment:") }
+ .filterKeys(removalFilter)
+ .forEach { remove(it.key) }
+ // add all relevant new environment variables
+ System.getenv().filterKeys(additionFilter)
+ .forEach { map["environment:${it.key}"] = it.value }
+ }
+}