Helpers.kt

  1. package com.hexagonkt.core

  2. import java.io.BufferedReader
  3. import java.io.File
  4. import java.io.InputStreamReader
  5. import java.lang.System.getenv
  6. import java.net.*
  7. import java.util.*
  8. import java.util.concurrent.TimeUnit.SECONDS

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

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

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

  44.     throw MultipleException("Error retrying $times times ($delay ms)", exceptions)
  45. }

  46. /**
  47.  * [TODO](https://github.com/hexagontk/hexagon/issues/271).
  48.  *
  49.  * TODO Assure JVM closes properly after process execution (dispose process resources, etc.)
  50.  */
  51. fun List<String>.exec(
  52.     workingDirectory: File = File(System.getProperty("user.dir")),
  53.     timeout: Long = Long.MAX_VALUE,
  54.     fail: Boolean = true,
  55. ): String {

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

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

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

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

  64.     val exitValue = process.exitValue()
  65.     val output = BufferedReader(InputStreamReader(process.inputStream)).readText()

  66.     if (fail && exitValue != 0)
  67.         throw CodedException(exitValue, output)

  68.     return output
  69. }

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

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