diff options
Diffstat (limited to 'support/arara/source/src/main/kotlin/org/islandoftex/arara/filehandling')
4 files changed, 598 insertions, 0 deletions
diff --git a/support/arara/source/src/main/kotlin/org/islandoftex/arara/filehandling/Database.kt b/support/arara/source/src/main/kotlin/org/islandoftex/arara/filehandling/Database.kt new file mode 100644 index 0000000000..e4b98cd194 --- /dev/null +++ b/support/arara/source/src/main/kotlin/org/islandoftex/arara/filehandling/Database.kt @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: BSD-3-Clause +package org.islandoftex.arara.filehandling + +import kotlinx.serialization.Serializable + +/** + * The database model, which keeps track on file changes. + * + * This database is a map because it maps files to hashes. So the key will + * always be a file representation and the value always a string. + * + * @author Island of TeX + * @version 5.0 + * @since 4.0 + */ +@Serializable +data class Database( + /** + * The whole database is implemented as a map, where + * the key is the absolute canonical file and the value + * is its corresponding CRC32 hash. + */ + val map: MutableMap<String, String> = mutableMapOf() +) : MutableMap<String, String> by map diff --git a/support/arara/source/src/main/kotlin/org/islandoftex/arara/filehandling/DatabaseUtils.kt b/support/arara/source/src/main/kotlin/org/islandoftex/arara/filehandling/DatabaseUtils.kt new file mode 100644 index 0000000000..f4728c9366 --- /dev/null +++ b/support/arara/source/src/main/kotlin/org/islandoftex/arara/filehandling/DatabaseUtils.kt @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: BSD-3-Clause +package org.islandoftex.arara.filehandling + +import com.charleskorn.kaml.Yaml +import java.io.File +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.model.AraraException + +/** + * Implements database utilitary methods. + * + * @author Island of TeX + * @version 5.0 + * @since 4.0 + */ +object DatabaseUtils { + // the application messages obtained from the + // language controller + private val messages = LanguageController + + /** + * Gets the file representing the YAML file (database). + * + * @throws AraraException Something wrong happened, to be caught in the + * higher levels. + */ + private val file: File + @Throws(AraraException::class) + get() { + val reference = Arara.config[AraraSpec.Execution.reference] + val name = "${Arara.config[AraraSpec.Execution.databaseName]}.yaml" + val path = FileHandlingUtils.getParentCanonicalFile(reference) + return path.resolve(name) + } + + /** + * Loads the YAML file representing the database. + * + * @return The database object. + * @throws AraraException Something wrong happened, to be caught in the + * higher levels. + */ + @Throws(AraraException::class) + fun load(): Database { + return if (!exists()) { + Database() + } else { + file.runCatching { + val text = readText() + if (!text.startsWith("!database")) + throw Exception("Database should start with !database") + Yaml.default.parse(Database.serializer(), text) + }.getOrElse { + it.printStackTrace() + throw AraraException(messages.getMessage(Messages + .ERROR_LOAD_COULD_NOT_LOAD_XML, file.name), it) + } + } + } + + /** + * Saves the database on a YAML file. + * + * @param database The database object. + * @throws AraraException Something wrong happened, to be caught in the + * higher levels. + */ + @Throws(AraraException::class) + fun save(database: Database) { + file.runCatching { + val content = "!database\n" + + Yaml.default.stringify(Database.serializer(), database) + writeText(content) + }.getOrElse { + throw AraraException( + messages.getMessage( + Messages.ERROR_SAVE_COULD_NOT_SAVE_XML, + file.name + ), it) + } + } + + /** + * Checks if the YAML file representing the database exists. + * + * @return A boolean value indicating if the YAML file exists. + * @throws AraraException Something wrong happened, to be caught in the + * higher levels. + */ + @Throws(AraraException::class) + private fun exists(): Boolean { + return file.exists() + } +} diff --git a/support/arara/source/src/main/kotlin/org/islandoftex/arara/filehandling/FileHandlingUtils.kt b/support/arara/source/src/main/kotlin/org/islandoftex/arara/filehandling/FileHandlingUtils.kt new file mode 100644 index 0000000000..07f1ecfab9 --- /dev/null +++ b/support/arara/source/src/main/kotlin/org/islandoftex/arara/filehandling/FileHandlingUtils.kt @@ -0,0 +1,319 @@ +// SPDX-License-Identifier: BSD-3-Clause +package org.islandoftex.arara.filehandling + +import java.io.File +import java.io.IOException +import java.text.SimpleDateFormat +import java.util.zip.CRC32 +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.model.AraraException + +/** + * Implements file handling utilitary methods. + * + * @author Island of TeX + * @version 5.0 + * @since 4.0 + */ +object FileHandlingUtils { + // the application messages obtained from the + // language controller + private val messages = LanguageController + + /** + * Gets the reference of the current file in execution. Note that this + * method might return a value different than the main file provided in + * the command line. + * + * @return A reference of the current file in execution. Might be different + * than the main file provided in the command line. + */ + private val currentFile: File + get() = Arara.config[AraraSpec.Execution.file] + + /** + * Writes the string to a file, using UTF-8 as default encoding. + * @param file The file. + * @param text The string to be written. + * @param append A flag whether to append the content. + * @return A logical value indicating whether it was successful. + */ + fun writeToFile(file: File, text: String, append: Boolean): Boolean { + return try { + // try to write the provided + // string to the file, with + // UTF-8 as encoding + if (append) + file.appendText(text, Charsets.UTF_8) + else + file.writeText(text, Charsets.UTF_8) + true + } catch (_: IOException) { + // if something bad happens, + // gracefully fallback to + // reporting the failure + false + } + } + + /** + * Writes the string list to a file, using UTF-8 as default encoding. + * @param file The file. + * @param lines The string list to be written. + * @param append A flag whether to append the content. + * @return A logical value indicating whether it was successful. + */ + fun writeToFile( + file: File, + lines: List<String>, + append: Boolean + ): Boolean = + try { + writeToFile(file, lines.joinToString(System.lineSeparator()), + append) + } catch (_: IOException) { + false + } + + /** + * Reads the provided file (UTF-8) into a list of strings. + * @param file The file. + * @return A list of strings. + */ + fun readFromFile(file: File): List<String> { + return try { + // returns the contents of + // the provided file as + // a list of strings + file.readLines(Charsets.UTF_8) + } catch (_: IOException) { + // if something bad happens, + // gracefully fallback to + // an empty file list + listOf() + } + } + + /** + * Checks if a file exists based on its extension. + * + * @param extension The extension. + * @return A boolean value indicating if the file exists. + * @throws AraraException Something wrong happened, to be caught in the + * higher levels. + */ + @Throws(AraraException::class) + fun exists(extension: String): Boolean { + val file = File(getPath(extension)) + return file.exists() + } + + /** + * Gets the parent canonical path of a file. + * + * @param file The file. + * @return The parent canonical path of a file. + * @throws AraraException Something wrong happened, to be caught in the + * higher levels. + */ + @Throws(AraraException::class) + fun getParentCanonicalPath(file: File): String { + return getParentCanonicalFile(file).toString() + } + + /** + * Gets the parent canonical file of a file. + * + * @param file The file. + * @return The parent canonical file of a file. + * @throws AraraException Something wrong happened, to be caught in the + * higher levels. + */ + @Throws(AraraException::class) + fun getParentCanonicalFile(file: File): File { + return file.runCatching { + this.canonicalFile.parentFile + }.getOrElse { + // it is IOException || is is SecurityException + throw AraraException(messages.getMessage( + Messages.ERROR_GETPARENTCANONICALPATH_IO_EXCEPTION), it) + } + } + + /** + * Gets the full file path based on the provided extension. + * + * @param extension The extension. + * @return A string containing the full file path. + * @throws AraraException Something wrong happened, to be caught in the + * higher levels. + */ + @Throws(AraraException::class) + fun getPath(extension: String): String { + val name = currentFile.nameWithoutExtension + ".$extension" + val path = getParentCanonicalFile(currentFile) + return path.resolve(name).toString() + } + + /** + * Gets the canonical path from the provided file. + * + * @param file The file. + * @return The canonical path from the provided file. + * @throws AraraException Something wrong happened, to be caught in the + * higher levels. + */ + @Throws(AraraException::class) + fun getCanonicalPath(file: File): String { + return getCanonicalFile(file).toString() + } + + /** + * Gets the canonical file from the provided file. + * + * @param file The file. + * @return The canonical file from the provided file. + * @throws AraraException Something wrong happened, to be caught in the + * higher levels. + */ + @Throws(AraraException::class) + fun getCanonicalFile(file: File): File { + try { + return file.canonicalFile + } catch (exception: IOException) { + throw AraraException( + messages.getMessage( + Messages.ERROR_GETCANONICALFILE_IO_EXCEPTION + ), + exception + ) + } + } + + /** + * Gets the date the provided file was last modified. + * + * @param file The file. + * @return A string representation of the date the provided file was last + * modified. + */ + fun getLastModifiedInformation(file: File): String { + return SimpleDateFormat("MM/dd/yyyy HH:mm:ss") + .format(file.lastModified()) + } + + /** + * Calculates the CRC32 checksum of the provided file. + * + * @param file The file. + * @return A string containing the CRC32 checksum of the provided file. + * @throws AraraException Something wrong happened, to be caught in the + * higher levels. + */ + @Throws(AraraException::class) + fun calculateHash(file: File): String { + try { + return String.format("%08x", CRC32().run { + update(file.readBytes()) + value + }) + } catch (exception: IOException) { + throw AraraException(messages.getMessage(Messages + .ERROR_CALCULATEHASH_IO_EXCEPTION), exception) + } + } + + /** + * Gets the extension of a file. + * + * @param file The file. + * @return The corresponding file type. + */ + fun getFileExtension(file: File): String = file.extension + + /** + * Gets the base name of a file. + * + * @param file The file. + * @return The corresponding base name. + */ + fun getBasename(file: File): String = file.nameWithoutExtension + + /** + * Checks if a file has changed since the last verification. + * + * @param file The file. + * @return A boolean value indicating if the file has changed since the last + * verification. + * @throws AraraException Something wrong happened, to be caught in the + * higher levels. + */ + @Throws(AraraException::class) + fun hasChanged(file: File): Boolean { + val database = DatabaseUtils.load() + val path = getCanonicalPath(file) + return if (!file.exists()) { + if (database.containsKey(path)) { + database.remove(path) + DatabaseUtils.save(database) + true + } else { + false + } + } else { + val hash = calculateHash(file) + if (database.containsKey(path)) { + val value = database[path] + if (hash == value) { + false + } else { + database[path] = hash + DatabaseUtils.save(database) + true + } + } else { + database[path] = hash + DatabaseUtils.save(database) + true + } + } + } + + /** + * Checks if the file has changed since the last verification based on the + * provided extension. + * + * @param extension The provided extension. + * @return A boolean value indicating if the file has changed since the last + * verification. + * @throws AraraException Something wrong happened, to be caught in the + * higher levels. + */ + @Throws(AraraException::class) + fun hasChanged(extension: String): Boolean = + hasChanged(File(getPath(extension))) + + /** + * Checks whether a directory is under a root directory. + * + * @param child Directory to be inspected. + * @param parent Root directory. + * @return Logical value indicating whether the directoy is under root. + * @throws AraraException There was a problem with path retrieval. + */ + @Throws(AraraException::class) + fun isSubDirectory(child: File, parent: File): Boolean { + return if (child.isDirectory && parent.isDirectory) { + getCanonicalPath(child).startsWith( + getParentCanonicalPath(parent) + File.separator + ) + } else { + throw AraraException(messages.getMessage( + Messages.ERROR_ISSUBDIRECTORY_NOT_A_DIRECTORY, + child.name)) + } + } +} diff --git a/support/arara/source/src/main/kotlin/org/islandoftex/arara/filehandling/FileSearchingUtils.kt b/support/arara/source/src/main/kotlin/org/islandoftex/arara/filehandling/FileSearchingUtils.kt new file mode 100644 index 0000000000..5c61bdb2db --- /dev/null +++ b/support/arara/source/src/main/kotlin/org/islandoftex/arara/filehandling/FileSearchingUtils.kt @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: BSD-3-Clause +package org.islandoftex.arara.filehandling + +import java.io.File +import java.io.FileFilter +import java.nio.file.FileSystems +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.model.AraraException +import org.islandoftex.arara.utils.CommonUtils + +/** + * Implements file searching utilitary methods. + * + * @author Island of TeX + * @version 5.0 + * @since 4.0 + */ +object FileSearchingUtils { + /** + * List all files from the provided directory according to the list of + * extensions. The leading dot must be omitted, unless it is part of the + * extension. + * @param directory The provided directory. + * @param extensions The list of extensions. + * @param recursive A flag indicating whether the search is recursive. + * @return A list of files. + */ + fun listFilesByExtensions( + directory: File, + extensions: List<String>, + recursive: Boolean + ): + List<File> = try { + // return the result of the + // provided search + if (recursive) + directory.walkTopDown().asSequence() + .filter { !it.isDirectory } + .filter { extensions.contains(it.extension) } + .toList() + else + directory.listFiles( + FileFilter { extensions.contains(it.extension) })!! + .toList() + } catch (_: Exception) { + // if something bad happens, + // gracefully fallback to + // an empty file list + listOf() + } + + /** + * List all files from the provided directory matching the list of file + * name patterns. Such list can contain wildcards. + * @param directory The provided directory. + * @param patterns The list of file name patterns. + * @param recursive A flag indicating whether the search is recursive. + * @return A list of files. + */ + fun listFilesByPatterns( + directory: File, + patterns: List<String>, + recursive: Boolean + ): + List<File> = try { + // return the result of the provided + // search, with the wildcard filter + // and a potential recursive search + val pathMatcher = patterns.map { + FileSystems.getDefault().getPathMatcher("glob:$it") + } + if (recursive) + directory.walkTopDown().asSequence() + .filter { !it.isDirectory } + .filter { file -> + pathMatcher.any { it.matches(file.toPath().fileName) } + }.toList() + else + directory.listFiles { file: File -> + pathMatcher.any { it.matches(file.toPath().fileName) } + }!!.toList() + } catch (_: Exception) { + // if something bad happens, + // gracefully fallback to + // an empty file list + listOf() + } + + /** + * Discovers the file through string reference lookup and sets the + * configuration accordingly. + * + * @param reference The string reference. + * @throws AraraException Something wrong happened, to be caught in the + * higher levels. + */ + @Throws(AraraException::class) + fun discoverFile(reference: String) { + lookupFile(reference) + ?: throw AraraException( + LanguageController.getMessage( + Messages.ERROR_DISCOVERFILE_FILE_NOT_FOUND, + reference, + CommonUtils.fileTypesList + ) + ) + } + + /** + * Performs a file lookup based on a string reference. + * + * @param reference The file reference as a string. + * @return The file as result of the lookup operation. + * @throws AraraException Something wrong happened, to be caught in the + * higher levels. + */ + @Throws(AraraException::class) + private fun lookupFile(reference: String): File? { + val types = Arara.config[AraraSpec.Execution.fileTypes] + val file = Arara.config[AraraSpec.Execution.workingDirectory] + .resolve(reference).toFile() + val name = file.name + val parent = FileHandlingUtils.getParentCanonicalFile(file) + + // direct search, so we are considering + // the reference as a complete name + val testFile = parent.resolve(name) + if (testFile.exists() && testFile.isFile) { + types.firstOrNull { + testFile.toString().endsWith("." + it.extension) + }?.let { + Arara.config[AraraSpec.Execution.filePattern] = + it.pattern + Arara.config[AraraSpec.Execution.reference] = testFile + return testFile + } + } + + // indirect search; in this case, we are considering + // that the file reference has an implicit extension, + // so we need to add it and look again + // TODO: disable this step in safe mode + return types.map { parent.resolve("$name.${it.extension}") to it } + .union(types.map { + parent.resolve("${name.removeSuffix(".").trim()}.${it.extension}") to it + }) + .firstOrNull { it.first.exists() && it.first.isFile } + ?.let { + Arara.config[AraraSpec.Execution.filePattern] = + it.second.pattern + Arara.config[AraraSpec.Execution.reference] = it.first + file + } + } +} |