summaryrefslogtreecommitdiff
path: root/support/arara/source/src/main/kotlin/org/islandoftex/arara/utils/CommonUtils.kt
diff options
context:
space:
mode:
Diffstat (limited to 'support/arara/source/src/main/kotlin/org/islandoftex/arara/utils/CommonUtils.kt')
-rw-r--r--support/arara/source/src/main/kotlin/org/islandoftex/arara/utils/CommonUtils.kt422
1 files changed, 422 insertions, 0 deletions
diff --git a/support/arara/source/src/main/kotlin/org/islandoftex/arara/utils/CommonUtils.kt b/support/arara/source/src/main/kotlin/org/islandoftex/arara/utils/CommonUtils.kt
new file mode 100644
index 0000000000..6887cd4e84
--- /dev/null
+++ b/support/arara/source/src/main/kotlin/org/islandoftex/arara/utils/CommonUtils.kt
@@ -0,0 +1,422 @@
+// SPDX-License-Identifier: BSD-3-Clause
+package org.islandoftex.arara.utils
+
+import java.io.File
+import java.io.IOException
+import java.util.MissingFormatArgumentException
+import java.util.regex.Pattern
+import kotlin.math.ln
+import kotlin.math.pow
+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.ruleset.Argument
+
+/**
+ * Implements common utilitary methods.
+ *
+ * @author Island of TeX
+ * @version 5.0
+ * @since 4.0
+ */
+object CommonUtils {
+ // the application messages obtained from the
+ // language controller
+ private val messages = LanguageController
+
+ /**
+ * Gets the list of file types as string, in order.
+ *
+ * @return A string representation of the list of file types, in order.
+ */
+ val fileTypesList: String
+ get() = "[ " + Arara.config[AraraSpec.Execution.fileTypes]
+ .joinToString(" | ") + " ]"
+
+ /**
+ * Gets the rule error header, containing the identifier and the path, if
+ * any.
+ *
+ * @return A string representation of the rule error header, containing the
+ * identifier and the path, if any.
+ */
+ val ruleErrorHeader: String
+ get() {
+ return if (Arara.config[AraraSpec.Execution.InfoSpec.ruleId] != null &&
+ Arara.config[AraraSpec.Execution.InfoSpec.rulePath] != null) {
+ val id = Arara.config[AraraSpec.Execution.InfoSpec.ruleId]!!
+ val path = Arara.config[AraraSpec.Execution.InfoSpec.rulePath]!!
+ messages.getMessage(
+ Messages.ERROR_RULE_IDENTIFIER_AND_PATH,
+ id,
+ path
+ ) + " "
+ } else {
+ ""
+ }
+ }
+
+ /**
+ * Gets a list of all rule paths.
+ *
+ * @return A list of all rule paths.
+ * @throws AraraException Something wrong happened, to be caught in the
+ * higher levels.
+ */
+ val allRulePaths: List<String>
+ @Throws(AraraException::class)
+ get() = Arara.config[AraraSpec.Execution.rulePaths].map {
+ val location = File(InterpreterUtils.construct(it, "quack"))
+ FileHandlingUtils.getParentCanonicalPath(location)
+ }
+
+ /**
+ * Returns the exit status of the application.
+ *
+ * @return An integer representing the exit status of the application.
+ */
+ val exitStatus: Int
+ get() = Arara.config[AraraSpec.Execution.status]
+
+ /**
+ * Gets the preamble content, converting a single string into a list of
+ * strings, based on new lines.
+ *
+ * @return A list of strings representing the preamble content.
+ */
+ val preambleContent: List<String>
+ get() = if (Arara.config[AraraSpec.Execution.preamblesActive]) {
+ Arara.config[AraraSpec.Execution.preamblesContent]
+ .split("\n")
+ .dropLastWhile { it.isEmpty() }
+ .toList()
+ } else {
+ listOf()
+ }
+
+ /**
+ * Checks if the input string is equal to a valid boolean value.
+ *
+ * @param value The input string.
+ * @return A boolean value represented by the provided string.
+ * @throws AraraException Something wrong happened, to be caught in the
+ * higher levels.
+ */
+ @Throws(AraraException::class)
+ fun checkBoolean(value: String): Boolean {
+ val yes = listOf("yes", "true", "1", "on")
+ val no = listOf("no", "false", "0", "off")
+ return if (!yes.union(no).contains(value.toLowerCase())) {
+ throw AraraException(
+ messages.getMessage(
+ Messages.ERROR_CHECKBOOLEAN_NOT_VALID_BOOLEAN,
+ value
+ )
+ )
+ } else {
+ yes.contains(value.toLowerCase())
+ }
+ }
+
+ /**
+ * Removes the keyword from the beginning of the provided string.
+ *
+ * @param line A string to be analyzed.
+ * @return The provided string without the keyword.
+ */
+ fun removeKeyword(line: String?): String? {
+ return if (line == null) null
+ else removeKeywordNotNull(line)
+ }
+
+ /**
+ * Removes the keyword from the beginning of the provided string.
+ *
+ * @param line A string to be analyzed.
+ * @return The provided string without the keyword.
+ */
+ fun removeKeywordNotNull(line: String): String {
+ var tempLine = line
+ val pattern = "^(\\s)*<arara>\\s".toPattern()
+ val matcher = pattern.matcher(tempLine)
+ if (matcher.find()) {
+ tempLine = tempLine.substring(matcher.end())
+ }
+ return tempLine.trim()
+ }
+
+ /**
+ * Flattens a potential list of lists into a list of objects.
+ *
+ * @param list The list to be flattened.
+ * @return The flattened list.
+ */
+ // TODO: check nullity
+ fun flatten(list: List<*>): List<Any> {
+ val result = mutableListOf<Any>()
+ list.forEach { item ->
+ if (item is List<*>)
+ result.addAll(flatten(item))
+ else
+ result.add(item as Any)
+ }
+ return result
+ }
+
+ /**
+ * Gets a set of strings containing unknown keys from a map and a list. It
+ * is a set difference from the keys in the map and the entries in the list.
+ *
+ * @param parameters The map of parameters.
+ * @param arguments The list of arguments.
+ * @return A set of strings representing unknown keys from a map and a list.
+ */
+ fun getUnknownKeys(
+ parameters: Map<String, Any>,
+ arguments: List<Argument>
+ ): Set<String> {
+ val found = parameters.keys
+ val expected = arguments.mapNotNull { it.identifier }
+ return found.subtract(expected)
+ }
+
+ /**
+ * Gets a human readable representation of a size.
+ *
+ * @param size The byte size to be converted.
+ * @return A string representation of the size.
+ */
+ @Suppress("MagicNumber")
+ fun byteSizeToString(size: Long): String {
+ val language = Arara.config[AraraSpec.Execution.language]
+ val conversionFactor = 1000.0
+ return if (size < conversionFactor) "$size B"
+ else
+ (ln(size.toDouble()) / ln(conversionFactor)).toInt().let { exp ->
+ "%.1f %sB".format(language.locale,
+ size / conversionFactor.pow(exp.toDouble()),
+ "kMGTPE"[exp - 1])
+ }
+ }
+
+ /**
+ * Generates a string based on a list of objects, separating each one of
+ * them by one space.
+ *
+ * @param objects A list of objects.
+ * @return A string based on the list of objects, separating each one of
+ * them by one space. Empty values are not considered.
+ */
+ fun generateString(vararg objects: Any): String = objects
+ .map { it.toString() }.filter { it.isNotEmpty() }
+ .joinToString(" ")
+
+ /**
+ * Checks if the file based on the provided extension contains the provided
+ * regex.
+ *
+ * @param extension The file extension.
+ * @param regex The regex.
+ * @return A boolean value indicating if the file contains the provided
+ * regex.
+ * @throws AraraException Something wrong happened, to be caught in the
+ * higher levels.
+ */
+ @Throws(AraraException::class)
+ fun checkRegex(extension: String, regex: String): Boolean {
+ val file = File(FileHandlingUtils.getPath(extension))
+ return checkRegex(file, regex)
+ }
+
+ /**
+ * Checks if the file contains the provided regex.
+ *
+ * As we use [File.readText] this should not be called on files > 2GB.
+ *
+ * @param file The file.
+ * @param regex The regex.
+ * @return A boolean value indicating if the file contains the provided
+ * regex.
+ * @throws AraraException Something wrong happened, to be caught in the
+ * higher levels.
+ */
+ @Throws(AraraException::class)
+ fun checkRegex(file: File, regex: String): Boolean {
+ try {
+ val text = file.readText()
+ val pattern = Pattern.compile(regex)
+ val matcher = pattern.matcher(text)
+ return matcher.find()
+ } catch (exception: IOException) {
+ throw AraraException(
+ messages.getMessage(
+ Messages.ERROR_CHECKREGEX_IO_EXCEPTION,
+ file.name
+ ),
+ exception
+ )
+ }
+ }
+
+ /**
+ * Replicates a string pattern based on a list of objects, generating a list
+ * as result.
+ *
+ * @param pattern The string pattern.
+ * @param values The list of objects to be merged with the pattern.
+ * @return A list containing the string pattern replicated to each object
+ * from the list.
+ * @throws AraraException Something wrong happened, to be caught in the
+ * higher levels.
+ */
+ @Throws(AraraException::class)
+ fun replicateList(
+ pattern: String,
+ values: List<Any>
+ ): List<Any> {
+ return try {
+ values.map { String.format(pattern, it) }
+ } catch (exception: MissingFormatArgumentException) {
+ throw AraraException(
+ messages.getMessage(
+ Messages.ERROR_REPLICATELIST_MISSING_FORMAT_ARGUMENTS_EXCEPTION
+ ),
+ exception
+ )
+ }
+ }
+
+ /**
+ * Checks if the provided operating system string holds according to the
+ * underlying operating system.
+ *
+ * Supported operating systems:
+ *
+ * * Windows
+ * * Linux
+ * * Mac OS X
+ * * Unix (Linux || Mac OS)
+ * * Cygwin
+ *
+ * @param value A string representing an operating system.
+ * @return A boolean value indicating if the provided string refers to the
+ * underlying operating system.
+ * @throws AraraException Something wrong happened, to be caught in the
+ * higher levels.
+ */
+ @Throws(AraraException::class)
+ fun checkOS(value: String): Boolean {
+ fun checkOSProperty(key: String): Boolean =
+ getSystemPropertyOrNull("os.name")
+ ?.toLowerCase()?.startsWith(key.toLowerCase()) ?: false
+
+ val values = mutableMapOf<String, Boolean>()
+ values["windows"] = checkOSProperty("Windows")
+ values["linux"] = checkOSProperty("Linux")
+ values["mac"] = checkOSProperty("Mac OS X")
+ values["unix"] = checkOSProperty("Mac OS X") ||
+ checkOSProperty("Linux")
+ values["cygwin"] = SystemCallUtils["cygwin"] as Boolean
+ if (!values.containsKey(value.toLowerCase())) {
+ throw AraraException(
+ messages.getMessage(
+ Messages.ERROR_CHECKOS_INVALID_OPERATING_SYSTEM,
+ value
+ )
+ )
+ }
+ // will never throw, see check above
+ return values.getValue(value.toLowerCase())
+ }
+
+ /**
+ * Gets the system property according to the provided key, or resort to the
+ * fallback value if an exception is thrown or if the key is invalid.
+ *
+ * @param key The system property key.
+ * @param fallback The fallback value.
+ * @return A string containing the system property value or the fallback.
+ */
+ fun getSystemProperty(key: String, fallback: String): String =
+ System.getProperties().runCatching {
+ getOrDefault(key, fallback).toString().takeIf { it != "" }
+ }.getOrNull() ?: fallback
+
+ /**
+ * Access a system property.
+ *
+ * @param key The key of the property.
+ * @return The value of the system property or null if there is an
+ * exception.
+ */
+ fun getSystemPropertyOrNull(key: String): String? =
+ System.getProperties().runCatching { getValue(key).toString() }
+ .getOrNull()
+
+ /**
+ * Generates a list of filenames from the provided command based on a list
+ * of extensions for each underlying operating system.
+ *
+ * @param command A string representing the command.
+ * @return A list of filenames.
+ */
+ private fun appendExtensions(command: String): List<String> {
+ // list of extensions, specific for
+ // each operating system (in fact, it
+ // is more Windows specific)
+ val extensions = if (checkOS("windows")) {
+ // the application is running on
+ // Windows, so let's look for the
+ // following extensions in order
+
+ // this list is actually a sublist from
+ // the original Windows PATHEXT environment
+ // variable which holds the list of executable
+ // extensions that Windows supports
+ listOf(".com", ".exe", ".bat", ".cmd")
+ } else {
+ // no Windows, so the default
+ // extension will be just an
+ // empty string
+ listOf("")
+ }
+
+ // return the resulting list holding the
+ // filenames generated from the
+ // provided command
+ return extensions.map { "$command$it" }
+ }
+
+ /**
+ * Checks if the provided command name is reachable from the system path.
+ *
+ * @param command A string representing the command.
+ * @return A logic value.
+ */
+ fun isOnPath(command: String): Boolean {
+ // first and foremost, let's build the list
+ // of filenames based on the underlying
+ // operating system
+ val filenames = appendExtensions(command)
+ return kotlin.runCatching {
+ // break the path into several parts
+ // based on the path separator symbol
+ System.getenv("PATH").split(File.pathSeparator)
+ .asSequence()
+ .mapNotNull { File(it).listFiles() }
+ // if the search does not return an empty
+ // list, one of the filenames got a match,
+ // and the command is available somewhere
+ // in the system path
+ .firstOrNull {
+ it.any { file ->
+ filenames.contains(file.name) && !file.isDirectory
+ }
+ }?.let { true }
+ }.getOrNull() ?: false
+ // otherwise (and in case of an exception) it is not in the path
+ }
+}