diff options
Diffstat (limited to 'support/arara/source/src/main/kotlin/org/islandoftex/arara/ruleset')
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 + ) + ) + } + } +} |