summaryrefslogtreecommitdiff
path: root/support/arara/source/src/main/kotlin/org/islandoftex/arara/ruleset
diff options
context:
space:
mode:
Diffstat (limited to 'support/arara/source/src/main/kotlin/org/islandoftex/arara/ruleset')
-rw-r--r--support/arara/source/src/main/kotlin/org/islandoftex/arara/ruleset/Argument.kt40
-rw-r--r--support/arara/source/src/main/kotlin/org/islandoftex/arara/ruleset/Command.kt57
-rw-r--r--support/arara/source/src/main/kotlin/org/islandoftex/arara/ruleset/Conditional.kt67
-rw-r--r--support/arara/source/src/main/kotlin/org/islandoftex/arara/ruleset/Directive.kt32
-rw-r--r--support/arara/source/src/main/kotlin/org/islandoftex/arara/ruleset/DirectiveAssembler.kt55
-rw-r--r--support/arara/source/src/main/kotlin/org/islandoftex/arara/ruleset/DirectiveUtils.kt312
-rw-r--r--support/arara/source/src/main/kotlin/org/islandoftex/arara/ruleset/Rule.kt48
-rw-r--r--support/arara/source/src/main/kotlin/org/islandoftex/arara/ruleset/RuleCommand.kt33
-rw-r--r--support/arara/source/src/main/kotlin/org/islandoftex/arara/ruleset/RuleUtils.kt140
9 files changed, 784 insertions, 0 deletions
diff --git a/support/arara/source/src/main/kotlin/org/islandoftex/arara/ruleset/Argument.kt b/support/arara/source/src/main/kotlin/org/islandoftex/arara/ruleset/Argument.kt
new file mode 100644
index 0000000000..dfd2b99403
--- /dev/null
+++ b/support/arara/source/src/main/kotlin/org/islandoftex/arara/ruleset/Argument.kt
@@ -0,0 +1,40 @@
+// SPDX-License-Identifier: BSD-3-Clause
+package org.islandoftex.arara.ruleset
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import org.islandoftex.arara.utils.CommonUtils
+
+/**
+ * The rule argument model.
+ *
+ * @author Island of TeX
+ * @version 5.0
+ * @since 4.0
+ */
+@Serializable
+class Argument {
+ /**
+ * The argument identifier
+ */
+ var identifier: String? = null
+ get() = CommonUtils.removeKeyword(field)
+
+ /**
+ * Boolean indicating if the current argument is required
+ */
+ @SerialName("required")
+ var isRequired: Boolean = false
+
+ /**
+ * Flag to hold the argument value manipulation
+ */
+ var flag: String? = null
+ get() = CommonUtils.removeKeyword(field)
+
+ /**
+ * The argument fallback if it is not defined in the directive
+ */
+ var default: String? = null
+ get() = CommonUtils.removeKeyword(field)
+}
diff --git a/support/arara/source/src/main/kotlin/org/islandoftex/arara/ruleset/Command.kt b/support/arara/source/src/main/kotlin/org/islandoftex/arara/ruleset/Command.kt
new file mode 100644
index 0000000000..df1ce802f5
--- /dev/null
+++ b/support/arara/source/src/main/kotlin/org/islandoftex/arara/ruleset/Command.kt
@@ -0,0 +1,57 @@
+// SPDX-License-Identifier: BSD-3-Clause
+package org.islandoftex.arara.ruleset
+
+import java.io.File
+import org.islandoftex.arara.Arara
+import org.islandoftex.arara.configuration.AraraSpec
+import org.islandoftex.arara.utils.CommonUtils
+
+/**
+ * Implements a command model, containing a list of strings.
+ *
+ * @author Island of TeX
+ * @version 5.0
+ * @since 4.0
+ */
+class Command {
+ /**
+ * A list of elements which are components
+ * of a command and represented as strings
+ */
+ val elements: List<String>
+
+ /**
+ * An optional file acting as a reference for
+ * the default working directory
+ */
+ var workingDirectory: File = Arara.config[AraraSpec.Execution
+ .workingDirectory].toFile()
+
+ /**
+ * Constructor.
+ * @param values An array of objects.
+ */
+ constructor(vararg values: Any) {
+ elements = mutableListOf()
+ val result = CommonUtils.flatten(values.toList())
+ result.map { it.toString() }.filter { it.isNotEmpty() }
+ .forEach { elements.add(it) }
+ }
+
+ /**
+ * Constructor.
+ * @param elements A list of strings.
+ */
+ constructor(elements: List<String>) {
+ this.elements = elements
+ }
+
+ /**
+ * Provides a textual representation of the current command.
+ * @return A string representing the current command.
+ */
+ override fun toString(): String {
+ return "[ " + elements.joinToString(", ") + " ]" +
+ " @ $workingDirectory"
+ }
+}
diff --git a/support/arara/source/src/main/kotlin/org/islandoftex/arara/ruleset/Conditional.kt b/support/arara/source/src/main/kotlin/org/islandoftex/arara/ruleset/Conditional.kt
new file mode 100644
index 0000000000..8a4ec6cb35
--- /dev/null
+++ b/support/arara/source/src/main/kotlin/org/islandoftex/arara/ruleset/Conditional.kt
@@ -0,0 +1,67 @@
+// SPDX-License-Identifier: BSD-3-Clause
+package org.islandoftex.arara.ruleset
+
+/**
+ * The conditional class, it represents the type of conditional available
+ * for a directive and its corresponding expression to be evaluated.
+ *
+ * @author Island of TeX
+ * @version 5.0
+ * @since 4.0
+ */
+data class Conditional(
+ /**
+ * The type of the condition indicates the meaning when evaluated.
+ * Defaults to [ConditionalType.NONE].
+ */
+ val type: ConditionalType = ConditionalType.NONE,
+ /**
+ * The expression to be evaluated according to its type. Defaults
+ * to no evaluation (empty string).
+ */
+ val condition: String = ""
+) {
+ /**
+ * The types of conditionals arara is able to recognize.
+ */
+ enum class ConditionalType {
+ /**
+ * Evaluated beforehand, directive is interpreted if and only if the
+ * result is true.
+ */
+ IF,
+ /**
+ * There is no evaluation, directive is interpreted, no extra effort is
+ * needed.
+ */
+ NONE,
+ /**
+ * Evaluated beforehand, directive is interpreted if and only if the
+ * result is false.
+ */
+ UNLESS,
+ /**
+ * Directive is interpreted the first time, then the evaluation is
+ * done; while the result is false, the directive is interpreted again
+ * and again.
+ */
+ UNTIL,
+ /**
+ * Evaluated beforehand, directive is interpreted if and only if the
+ * result is true, and the process is repeated while the result still
+ * holds true.
+ */
+ WHILE
+ }
+
+ /**
+ * Provides a textual representation of the conditional object.
+ * @return A string representation of this object.
+ */
+ override fun toString(): String {
+ return "{ $type" +
+ if (type != ConditionalType.NONE)
+ ", expression: ${condition.trim()}"
+ else "" + " }"
+ }
+}
diff --git a/support/arara/source/src/main/kotlin/org/islandoftex/arara/ruleset/Directive.kt b/support/arara/source/src/main/kotlin/org/islandoftex/arara/ruleset/Directive.kt
new file mode 100644
index 0000000000..33f8431744
--- /dev/null
+++ b/support/arara/source/src/main/kotlin/org/islandoftex/arara/ruleset/Directive.kt
@@ -0,0 +1,32 @@
+// SPDX-License-Identifier: BSD-3-Clause
+package org.islandoftex.arara.ruleset
+
+/**
+ * Implements the directive model.
+ *
+ * @author Island of TeX
+ * @version 5.0
+ * @since 4.0
+ */
+data class Directive(
+ /**
+ * The directive identifier, it is resolved to the rule identifier
+ * later on.
+ */
+ val identifier: String,
+ /**
+ * A map containing the parameters; they are validated later on in
+ * order to ensure they are valid.
+ */
+ val parameters: Map<String, Any>,
+ /**
+ * A conditional containing the type and the expression to be evaluated
+ * later on.
+ */
+ val conditional: Conditional,
+ /**
+ * A list contained all line numbers from the main file which built the
+ * current directive.
+ */
+ val lineNumbers: List<Int>
+)
diff --git a/support/arara/source/src/main/kotlin/org/islandoftex/arara/ruleset/DirectiveAssembler.kt b/support/arara/source/src/main/kotlin/org/islandoftex/arara/ruleset/DirectiveAssembler.kt
new file mode 100644
index 0000000000..a431df6ae2
--- /dev/null
+++ b/support/arara/source/src/main/kotlin/org/islandoftex/arara/ruleset/DirectiveAssembler.kt
@@ -0,0 +1,55 @@
+// SPDX-License-Identifier: BSD-3-Clause
+package org.islandoftex.arara.ruleset
+
+/**
+ * Implements a directive assembler in order to help build a directive from a
+ * list of strings.
+ *
+ * @author Island of TeX
+ * @version 5.0
+ * @since 4.0
+ */
+class DirectiveAssembler {
+ // this variable holds a list of
+ // line numbers indicating which
+ // lines composed the resulting
+ // potential directive
+ private val lineNumbers = mutableListOf<Int>()
+
+ // this variable holds the textual
+ // representation of the directive
+ private var text: String = ""
+
+ /**
+ * Checks if an append operation is allowed.
+ * @return A boolean value indicating if an append operation is allowed.
+ */
+ val isAppendAllowed: Boolean
+ get() = lineNumbers.isNotEmpty()
+
+ /**
+ * Adds a line number to the assembler.
+ * @param line An integer representing the line number.
+ */
+ fun addLineNumber(line: Int) = lineNumbers.add(line)
+
+ /**
+ * Appends the provided line to the assembler text.
+ * @param line The provided line.
+ */
+ fun appendLine(line: String) {
+ text = text + " " + line.trim()
+ }
+
+ /**
+ * Gets the list of line numbers.
+ * @return The list of line numbers.
+ */
+ fun getLineNumbers(): List<Int> = lineNumbers
+
+ /**
+ * Gets the text.
+ * @return The assembler text, properly trimmed.
+ */
+ fun getText(): String = text.trim()
+}
diff --git a/support/arara/source/src/main/kotlin/org/islandoftex/arara/ruleset/DirectiveUtils.kt b/support/arara/source/src/main/kotlin/org/islandoftex/arara/ruleset/DirectiveUtils.kt
new file mode 100644
index 0000000000..e901e25fcf
--- /dev/null
+++ b/support/arara/source/src/main/kotlin/org/islandoftex/arara/ruleset/DirectiveUtils.kt
@@ -0,0 +1,312 @@
+// SPDX-License-Identifier: BSD-3-Clause
+package org.islandoftex.arara.ruleset
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
+import com.fasterxml.jackson.module.kotlin.readValue
+import com.fasterxml.jackson.module.kotlin.registerKotlinModule
+import java.io.File
+import java.util.regex.Pattern
+import org.islandoftex.arara.Arara
+import org.islandoftex.arara.configuration.AraraSpec
+import org.islandoftex.arara.filehandling.FileHandlingUtils
+import org.islandoftex.arara.localization.LanguageController
+import org.islandoftex.arara.localization.Messages
+import org.islandoftex.arara.model.AraraException
+import org.islandoftex.arara.utils.DisplayUtils
+import org.slf4j.LoggerFactory
+
+/**
+ * Implements directive utilitary methods.
+ *
+ * @author Island of TeX
+ * @version 5.0
+ * @since 4.0
+ */
+@UseExperimental(kotlinx.serialization.ImplicitReflectionSerializer::class)
+object DirectiveUtils {
+ // the application messages obtained from the
+ // language controller
+ private val messages = LanguageController
+
+ // get the logger context from a factory
+ private val logger = LoggerFactory.getLogger(DirectiveUtils::class.java)
+
+ /**
+ * This function filters the lines of a file to identify the potential
+ * directives.
+ *
+ * @param lines The lines of the file.
+ * @return A map containing the line number and the line's content.
+ */
+ private fun getPotentialDirectiveLines(lines: List<String>):
+ Map<Int, String> {
+ val header = Arara.config[AraraSpec.Execution.onlyHeader]
+ val validLineRegex = Arara.config[AraraSpec.Execution.filePattern]
+ val validLinePattern = validLineRegex.toPattern()
+ val validLineStartPattern = (validLineRegex + Arara.config[AraraSpec
+ .Application.namePattern]).toPattern()
+ val map = mutableMapOf<Int, String>()
+ for ((i, text) in lines.withIndex()) {
+ val validLineMatcher = validLineStartPattern.matcher(text)
+ if (validLineMatcher.find()) {
+ val line = text.substring(validLineMatcher.end())
+ map[i + 1] = line
+
+ logger.info(messages.getMessage(
+ Messages.LOG_INFO_POTENTIAL_PATTERN_FOUND,
+ i + 1, line.trim()))
+ } else if (header && !checkLinePattern(validLinePattern, text)) {
+ // if we should only look within the file's header and reached
+ // a point where the line pattern does not match anymore, we
+ // assume we have left the header and break
+ break
+ }
+ }
+ return map
+ }
+
+ /**
+ * Extracts a list of directives from a list of strings.
+ *
+ * @param lines List of strings.
+ * @return A list of directives.
+ * @throws AraraException Something wrong happened, to be caught in the
+ * higher levels.
+ */
+ @Throws(AraraException::class)
+ @Suppress("MagicNumber")
+ fun extractDirectives(lines: List<String>): List<Directive> {
+ val pairs = getPotentialDirectiveLines(lines)
+ .takeIf { it.isNotEmpty() }
+ ?: throw AraraException(messages.getMessage(
+ Messages.ERROR_VALIDATE_NO_DIRECTIVES_FOUND))
+
+ val assemblers = mutableListOf<DirectiveAssembler>()
+ var assembler = DirectiveAssembler()
+ val linebreakPattern = Arara.config[AraraSpec.Directive
+ .linebreakPattern].toPattern()
+ for ((lineno, content) in pairs) {
+ val linebreakMatcher = linebreakPattern.matcher(content)
+ if (linebreakMatcher.find()) {
+ if (!assembler.isAppendAllowed) {
+ throw AraraException(
+ messages.getMessage(
+ Messages.ERROR_VALIDATE_ORPHAN_LINEBREAK,
+ lineno
+ )
+ )
+ } else {
+ assembler.addLineNumber(lineno)
+ assembler.appendLine(linebreakMatcher.group(1))
+ }
+ } else {
+ if (assembler.isAppendAllowed) {
+ assemblers.add(assembler)
+ }
+ assembler = DirectiveAssembler()
+ assembler.addLineNumber(lineno)
+ assembler.appendLine(content)
+ }
+ }
+ if (assembler.isAppendAllowed) {
+ assemblers.add(assembler)
+ }
+
+ return assemblers.map { generateDirective(it) }
+ }
+
+ /**
+ * Generates a directive from a directive assembler.
+ *
+ * @param assembler The directive assembler.
+ * @return The corresponding directive.
+ * @throws AraraException Something wrong happened, to be caught in the
+ * higher levels.
+ */
+ @Throws(AraraException::class)
+ @Suppress("MagicNumber")
+ fun generateDirective(assembler: DirectiveAssembler): Directive {
+ val matcher = Arara.config[AraraSpec.Directive.directivePattern]
+ .toPattern().matcher(assembler.getText())
+ if (matcher.find()) {
+ val directive = Directive(
+ identifier = matcher.group(1)!!,
+ parameters = getParameters(matcher.group(3),
+ assembler.getLineNumbers()),
+ conditional = Conditional(
+ type = getType(matcher.group(5)),
+ condition = matcher.group(6) ?: ""
+ ),
+ lineNumbers = assembler.getLineNumbers()
+ )
+
+ logger.info(messages.getMessage(
+ Messages.LOG_INFO_POTENTIAL_DIRECTIVE_FOUND, directive))
+
+ return directive
+ } else {
+ throw AraraException(
+ messages.getMessage(
+ Messages.ERROR_VALIDATE_INVALID_DIRECTIVE_FORMAT,
+ "(" + assembler.getLineNumbers()
+ .joinToString(", ") + ")"
+ )
+ )
+ }
+ }
+
+ /**
+ * Gets the conditional type based on the input string.
+ *
+ * @param text The input string.
+ * @return The conditional type.
+ */
+ private fun getType(text: String?): Conditional.ConditionalType {
+ return when (text) {
+ null -> Conditional.ConditionalType.NONE
+ "if" -> Conditional.ConditionalType.IF
+ "while" -> Conditional.ConditionalType.WHILE
+ "until" -> Conditional.ConditionalType.UNTIL
+ else -> Conditional.ConditionalType.UNLESS
+ }
+ }
+
+ /**
+ * Gets the parameters from the input string.
+ *
+ * @param text The input string.
+ * @param numbers The list of line numbers.
+ * @return A map containing the directive parameters.
+ * @throws AraraException Something wrong happened, to be caught in the
+ * higher levels.
+ */
+ @Throws(AraraException::class)
+ private fun getParameters(
+ text: String?,
+ numbers: List<Int>
+ ): Map<String, Any> {
+ if (text == null)
+ return mapOf()
+
+ /* Before using kotlinx.serialization, there has been a dedicated
+ * directive resolver which instructed SnakeYAML to do the following:
+ *
+ * addImplicitResolver(Tag.MERGE, MERGE, "<")
+ * addImplicitResolver(Tag.NULL, NULL, "~nN\u0000")
+ * addImplicitResolver(Tag.NULL, EMPTY, null)
+ *
+ * This has been removed.
+ */
+ return ObjectMapper(YAMLFactory()).registerKotlinModule().runCatching {
+ readValue<Map<String, Any>>(text)
+ }.getOrElse {
+ throw AraraException(messages.getMessage(
+ Messages.ERROR_VALIDATE_YAML_EXCEPTION,
+ "(" + numbers.joinToString(", ") + ")"),
+ it)
+ }
+ }
+
+ /**
+ * Replicate a directive for given files.
+ *
+ * @param holder The list of files.
+ * @param parameters The parameters for the directive.
+ * @param directive The directive to clone.
+ * @return List of cloned directives.
+ * @throws AraraException If there is an error validating the [holder]
+ * object.
+ */
+ @Throws(AraraException::class)
+ private fun replicateDirective(
+ holder: Any,
+ parameters: Map<String, Any>,
+ directive: Directive
+ ): List<Directive> {
+ return if (holder is List<*>) {
+ // we received a file list, so we map that list to files
+ holder.filterIsInstance<Any>()
+ .asSequence()
+ .map { File(it.toString()) }
+ .map(FileHandlingUtils::getCanonicalFile)
+ // and because we want directives, we replicate our
+ // directive to be applied to that file
+ .map { reference ->
+ directive.copy(parameters = parameters
+ .plus("reference" to reference))
+ }
+ .toList()
+ // we take the result if and only if we have at least one
+ // file and we did not filter out any invalid argument
+ .takeIf { it.isNotEmpty() && holder.size == it.size }
+ // TODO: check exception according to condition
+ ?: throw AraraException(
+ messages.getMessage(
+ Messages.ERROR_VALIDATE_EMPTY_FILES_LIST,
+ "(" + directive.lineNumbers
+ .joinToString(", ") + ")"
+ )
+ )
+ } else {
+ throw AraraException(
+ messages.getMessage(
+ Messages.ERROR_VALIDATE_FILES_IS_NOT_A_LIST,
+ "(" + directive.lineNumbers.joinToString(", ") + ")"
+ )
+ )
+ }
+ }
+
+ /**
+ * Validates the list of directives, returning a new list.
+ *
+ * @param directives The list of directives.
+ * @return A new list of directives.
+ * @throws AraraException Something wrong happened, to be caught in the
+ * higher levels.
+ */
+ @Throws(AraraException::class)
+ fun process(directives: List<Directive>): List<Directive> {
+ val result = mutableListOf<Directive>()
+ directives.forEach { directive ->
+ val parameters = directive.parameters
+
+ if (parameters.containsKey("reference"))
+ throw AraraException(messages.getMessage(
+ Messages.ERROR_VALIDATE_REFERENCE_IS_RESERVED,
+ "(" + directive.lineNumbers.joinToString(", ") + ")"))
+
+ if (parameters.containsKey("files")) {
+ result.addAll(replicateDirective(parameters.getValue("files"),
+ parameters.minus("files"), directive))
+ } else {
+ result.add(directive.copy(parameters = parameters
+ .plus("reference" to
+ Arara.config[AraraSpec.Execution.reference])))
+ }
+ }
+
+ logger.info(messages.getMessage(
+ Messages.LOG_INFO_VALIDATED_DIRECTIVES))
+ logger.info(DisplayUtils.displayOutputSeparator(
+ messages.getMessage(Messages.LOG_INFO_DIRECTIVES_BLOCK)))
+ result.forEach { logger.info(it.toString()) }
+ logger.info(DisplayUtils.displaySeparator())
+
+ return result
+ }
+
+ /**
+ * Checks if the provided line contains the corresponding pattern, based on
+ * the file type, or an empty line.
+ *
+ * @param pattern Pattern to be matched, based on the file type.
+ * @param line Provided line.
+ * @return Logical value indicating if the provided line contains the
+ * corresponding pattern, based on the file type, or an empty line.
+ */
+ private fun checkLinePattern(pattern: Pattern, line: String): Boolean {
+ return line.isBlank() || pattern.matcher(line).find()
+ }
+}
diff --git a/support/arara/source/src/main/kotlin/org/islandoftex/arara/ruleset/Rule.kt b/support/arara/source/src/main/kotlin/org/islandoftex/arara/ruleset/Rule.kt
new file mode 100644
index 0000000000..53ac2aa8b1
--- /dev/null
+++ b/support/arara/source/src/main/kotlin/org/islandoftex/arara/ruleset/Rule.kt
@@ -0,0 +1,48 @@
+// SPDX-License-Identifier: BSD-3-Clause
+package org.islandoftex.arara.ruleset
+
+import kotlinx.serialization.Serializable
+import org.islandoftex.arara.utils.CommonUtils
+
+/**
+ * Implements the rule model.
+ *
+ * @author Island of TeX
+ * @version 5.0
+ * @since 4.0
+ */
+@Serializable
+class Rule {
+ /**
+ * The rule identifier
+ */
+ var identifier: String = INVALID_RULE_IDENTIFIER
+ get() = CommonUtils.removeKeywordNotNull(field)
+
+ /**
+ * The rule name
+ */
+ var name: String = INVALID_RULE_NAME
+ get() = CommonUtils.removeKeywordNotNull(field)
+
+ /**
+ * The list of authors
+ */
+ var authors: List<String> = listOf()
+ get() = field.mapNotNull { CommonUtils.removeKeyword(it) }
+
+ /**
+ * The list of commands
+ */
+ var commands: List<RuleCommand> = listOf()
+
+ /**
+ * The list of arguments
+ */
+ var arguments: List<Argument> = listOf()
+
+ companion object {
+ const val INVALID_RULE_IDENTIFIER = "INVALID_RULE"
+ const val INVALID_RULE_NAME = "INVALID_RULE"
+ }
+}
diff --git a/support/arara/source/src/main/kotlin/org/islandoftex/arara/ruleset/RuleCommand.kt b/support/arara/source/src/main/kotlin/org/islandoftex/arara/ruleset/RuleCommand.kt
new file mode 100644
index 0000000000..901718d412
--- /dev/null
+++ b/support/arara/source/src/main/kotlin/org/islandoftex/arara/ruleset/RuleCommand.kt
@@ -0,0 +1,33 @@
+// SPDX-License-Identifier: BSD-3-Clause
+package org.islandoftex.arara.ruleset
+
+import kotlinx.serialization.Serializable
+import org.islandoftex.arara.utils.CommonUtils
+
+/**
+ * Implements the rule command model.
+ *
+ * @author Island of TeX
+ * @version 5.0
+ * @since 4.0
+ */
+@Serializable
+class RuleCommand {
+ /**
+ * The command name
+ */
+ var name: String? = null
+ get() = CommonUtils.removeKeyword(field)
+
+ /**
+ * The command instruction
+ */
+ var command: String? = null
+ get() = CommonUtils.removeKeyword(field)
+
+ /**
+ * The exit status expression
+ */
+ var exit: String? = null
+ get() = CommonUtils.removeKeyword(field)
+}
diff --git a/support/arara/source/src/main/kotlin/org/islandoftex/arara/ruleset/RuleUtils.kt b/support/arara/source/src/main/kotlin/org/islandoftex/arara/ruleset/RuleUtils.kt
new file mode 100644
index 0000000000..e823e4e598
--- /dev/null
+++ b/support/arara/source/src/main/kotlin/org/islandoftex/arara/ruleset/RuleUtils.kt
@@ -0,0 +1,140 @@
+// SPDX-License-Identifier: BSD-3-Clause
+package org.islandoftex.arara.ruleset
+
+import com.charleskorn.kaml.Yaml
+import java.io.File
+import org.islandoftex.arara.localization.LanguageController
+import org.islandoftex.arara.localization.Messages
+import org.islandoftex.arara.model.AraraException
+import org.islandoftex.arara.utils.CommonUtils
+
+/**
+ * Implements rule utilitary methods.
+ *
+ * @author Island of TeX
+ * @version 5.0
+ * @since 4.0
+ */
+object RuleUtils {
+ // the application messages obtained from the
+ // language controller
+ private val messages = LanguageController
+
+ /**
+ * Parses the provided file, checks the identifier and returns a rule
+ * representation.
+ *
+ * @param file The rule file.
+ * @param identifier The directive identifier.
+ * @return The rule object.
+ * @throws AraraException Something wrong happened, to be caught in the
+ * higher levels.
+ */
+ @Throws(AraraException::class)
+ fun parseRule(file: File, identifier: String): Rule {
+ val rule = file.runCatching {
+ val text = readText()
+ if (!text.startsWith("!config"))
+ throw Exception("Rule should start with !config")
+ Yaml.default.parse(Rule.serializer(), text)
+ }.getOrElse {
+ throw AraraException(
+ CommonUtils.ruleErrorHeader + messages.getMessage(
+ Messages.ERROR_PARSERULE_GENERIC_ERROR
+ ), it)
+ }
+
+ validateHeader(rule, identifier)
+ validateBody(rule)
+ return rule
+ }
+
+ /**
+ * Validates the rule header according to the directive identifier.
+ *
+ * @param rule The rule object.
+ * @param identifier The directive identifier.
+ * @throws AraraException Something wrong happened, to be caught in the
+ * higher levels.
+ */
+ @Throws(AraraException::class)
+ @Suppress("ThrowsCount")
+ private fun validateHeader(rule: Rule, identifier: String) {
+ if (rule.identifier != Rule.INVALID_RULE_IDENTIFIER) {
+ if (rule.identifier != identifier) {
+ throw AraraException(CommonUtils.ruleErrorHeader +
+ messages.getMessage(
+ Messages.ERROR_VALIDATEHEADER_WRONG_IDENTIFIER,
+ rule.identifier,
+ identifier))
+ }
+ } else {
+ throw AraraException(CommonUtils.ruleErrorHeader +
+ messages.getMessage(Messages.ERROR_VALIDATEHEADER_NULL_ID))
+ }
+ if (rule.name == Rule.INVALID_RULE_NAME) {
+ throw AraraException(
+ CommonUtils.ruleErrorHeader + messages.getMessage(
+ Messages.ERROR_VALIDATEHEADER_NULL_NAME
+ )
+ )
+ }
+ }
+
+ /**
+ * Validates the rule body.
+ *
+ * @param rule The rule object.
+ * @throws AraraException Something wrong happened, to be caught in the
+ * higher levels.
+ */
+ @Throws(AraraException::class)
+ @Suppress("ThrowsCount")
+ private fun validateBody(rule: Rule) {
+ if (rule.commands.any { it.command == null }) {
+ throw AraraException(CommonUtils.ruleErrorHeader +
+ messages.getMessage(
+ Messages.ERROR_VALIDATEBODY_NULL_COMMAND))
+ }
+
+ val arguments = mutableListOf<String>()
+ for (argument in rule.arguments) {
+ if (argument.identifier != null) {
+ if (argument.flag != null || argument.default != null) {
+ arguments.add(argument.identifier!!)
+ } else {
+ throw AraraException(
+ CommonUtils.ruleErrorHeader + messages.getMessage(
+ Messages.ERROR_VALIDATEBODY_MISSING_KEYS
+ )
+ )
+ }
+ } else {
+ throw AraraException(
+ CommonUtils.ruleErrorHeader + messages.getMessage(
+ Messages.ERROR_VALIDATEBODY_NULL_ARGUMENT_ID
+ )
+ )
+ }
+ }
+
+ arguments.intersect(listOf("files", "reference")).forEach {
+ throw AraraException(
+ CommonUtils.ruleErrorHeader + messages.getMessage(
+ Messages.ERROR_VALIDATEBODY_ARGUMENT_ID_IS_RESERVED,
+ it
+ )
+ )
+ }
+
+ val expected = arguments.size
+ val found = arguments.toSet().size
+ if (expected != found) {
+ throw AraraException(
+ CommonUtils.ruleErrorHeader + messages.getMessage(
+ Messages.ERROR_VALIDATEBODY_DUPLICATE_ARGUMENT_IDENTIFIERS
+ )
+ )
+ }
+ }
+}