Helpers.kt

  1. package com.hexagontk.helpers

  2. import com.hexagontk.core.CodedException
  3. import com.hexagontk.core.MultipleException
  4. import java.io.BufferedReader
  5. import java.io.File
  6. import java.io.InputStreamReader
  7. import java.lang.System.getenv
  8. import java.net.*
  9. import java.util.*
  10. import java.util.concurrent.TimeUnit.SECONDS

  11. /**
  12.  * Load a '*.properties' file from a URL transforming the content into a plain map. If the resource
  13.  * can not be found, a [com.hexagontk.core.ResourceNotFoundException] is thrown.
  14.  *
  15.  * @param url URL pointing to the file to load.
  16.  * @return Map containing the properties file data.
  17.  */
  18. fun properties(url: URL): Map<String, String> =
  19.     Properties()
  20.         .apply { url.openStream().use { load(it.reader()) } }
  21.         .toMap()
  22.         .mapKeys { it.key as String }
  23.         .mapValues { it.value as String }

  24. /**
  25.  * Execute a lambda until no exception is thrown or a number of times is reached.
  26.  *
  27.  * @param times Number of times to try to execute the callback. Must be greater than 0.
  28.  * @param delay Milliseconds to wait to next execution if there was an error. Must be 0 or greater.
  29.  * @param block Code to be executed.
  30.  * @return Callback's result if succeeded.
  31.  * @throws [MultipleException] if the callback didn't succeed in the given times.
  32.  */
  33. fun <T> retry(times: Int, delay: Long, block: () -> T): T {
  34.     require(times > 0)
  35.     require(delay >= 0)

  36.     val exceptions = mutableListOf<Exception>()
  37.     (1..times).forEach {
  38.         try {
  39.             return block()
  40.         }
  41.         catch (e: Exception) {
  42.             exceptions.add(e)
  43.             Thread.sleep(delay)
  44.         }
  45.     }

  46.     throw MultipleException("Error retrying $times times ($delay ms)", exceptions)
  47. }

  48. /**
  49.  * .
  50.  *
  51.  * TODO Assure JVM closes properly after process execution (dispose process resources, etc.)
  52.  */
  53. fun List<String>.exec(
  54.     workingDirectory: File = File(System.getProperty("user.dir")),
  55.     timeout: Long = Long.MAX_VALUE,
  56.     fail: Boolean = true,
  57. ): String {

  58.     val command = filter { it.isNotBlank() }.toTypedArray()

  59.     require(command.isNotEmpty()) { "Command is empty" }
  60.     require(timeout > 0) { "Process timeout should be greater than zero: $timeout" }

  61.     val process = ProcessBuilder(*command).directory(workingDirectory).start()

  62.     if (!process.waitFor(timeout, SECONDS)) {
  63.         process.destroy()
  64.         error("Command timed out: $this")
  65.     }

  66.     val exitValue = process.exitValue()
  67.     val output = BufferedReader(InputStreamReader(process.inputStream)).readText()

  68.     if (fail && exitValue != 0)
  69.         throw CodedException(exitValue, output)

  70.     return output
  71. }

  72. /**
  73.  * TODO Add use case and example in documentation.
  74.  *
  75.  * Run the receiver's text as a process in the host operating system. The command can have multiple
  76.  * lines and may or may not contain the shell continuation string (` \\n`).
  77.  *
  78.  * Multiple words parameters can not be used, for that requirement you can use the [shell] method,
  79.  * or run exec using a list of items (program and parameters).
  80.  *
  81.  * @receiver String holding the command to be executed.
  82.  * @param workingDirectory Directory on which the process will be executed. Defaults to current
  83.  *  directory.
  84.  * @param timeout Maximum number of seconds allowed for process execution. Defaults to the maximum
  85.  *  long value. It must be greater than zero.
  86.  * @param fail If true Raise an exception if the result code is different from zero. The default
  87.  *  value is `false`.
  88.  * @throws CodedException Thrown if the process return an error code (the actual code is passed
  89.  *  inside [CodedException.code] and the command output is set at [CodedException.message]).
  90.  * @throws IllegalStateException If the command doesn't end within the allowed time or the command
  91.  *  string is blank, an exception will be thrown.
  92.  * @return The output of the command.
  93.  */
  94. fun String.exec(
  95.     workingDirectory: File = File(System.getProperty("user.dir")),
  96.     timeout: Long = Long.MAX_VALUE,
  97.     fail: Boolean = false,
  98. ): String =
  99.     replace("""(\s+\\\s*)?\n""".toRegex(), "")
  100.         .split(" ")
  101.         .map { it.trim() }
  102.         .toList()
  103.         .exec(workingDirectory, timeout, fail)

  104. /**
  105.  * Executes a command in a shell (allowing to use pipes, redirections, etc.).
  106.  *
  107.  * TODO
  108.  *
  109.  * @param workingDirectory
  110.  * @param timeout
  111.  * @param fail
  112.  * @return
  113.  */
  114. fun String.shell(
  115.     workingDirectory: File = File(System.getProperty("user.dir")),
  116.     timeout: Long = Long.MAX_VALUE,
  117.     fail: Boolean = false,
  118. ): String =
  119.     listOf(getenv("SHELL") ?: "bash", "-c", replace("""(\s+\\\s*)?\n""".toRegex(), ""))
  120.         .exec(workingDirectory, timeout, fail)