5

I have written a code that reads a text file. The text files contain placeholders which I would like to replace. The substitution does not work this way and the string is printed with the placeholders. Here is the code that I have written for this:

class TestSub(val sub: Sub) {

    fun create() = template()

    fun template() = Files.newBufferedReader(ClassPathResource(templateId.location).file.toPath()).readText()
}

data class Sub(val name: String, val age: Int)

Here is the main function that tries to print the final string:

fun main(args: Array<String>) {
    val sub = Sub("Prashant", 32)

    println(TestSub(sub).create())
}

However, when, instead of reading a file, I use a String, the following code works (Replacing fun template())

fun template() = "<h1>Hello ${sub.name}. Your age is ${sub.age}</h1>"

Is there a way to make string Substitution work when reading the content of a file?

Prashant
  • 4,775
  • 3
  • 28
  • 47

4 Answers4

16

Kotlin does not support String templates from files. I.e. code like "some variable: $variable" gets compiled to "some variable: " + variable. String templates are handled at compile time, which means it does not work with text loaded from files, or if you do something else to get the String escaped into a raw form. Either way, it would, as danielspaniol mentioned, be a security threat.

That leaves three options:

  • String.format(str)
  • MessageFormat.format(str)
  • Creating a custom engine

I don't know what your file contains, but if it's the String you used in the template function, change it to:

<h1>Hello {0}. Your age is {1,integer}</h1>

This is for MessageFormat, which is my personal preference. If you use String.format, use %s instead, and the other appropriate formats.

Now, use that in MessageFormat.format:

val result = MessageFormat.format(theString, name, age);

Note that if you use MessageFormat, you'll need to escape ' as ''. See this.

Zoe
  • 27,060
  • 21
  • 118
  • 148
2

String substitution using ${...} is part of the string literals syntax and works roughly like this

val a = 1
val b = "abc ${a} def"  // gets translated to something like val b = "abc " + a + " def"

So there is no way for this to work when you load from a text file. This would also be a huge security risk as it would allow for arbitrary code execution.

However I assume that Kotlin has something like a sprintf function where you can have placeholders like %s in your string and you can replace them with values


Take a look here. It looks like the easiest way is to use String.format

danielspaniol
  • 2,228
  • 21
  • 38
2

You are looking for something similar to Kotlin String templates for raw Strings, where placeholders like $var or ${var} are substituted by values, but this functionality needs to be available at runtime (for text read from files).

Methods like String.format(str) or MessageFormat.format(str) use other formats than the notation with the dollar prefix of Kotlin String templates. For "Kotlin-like" placeholder substitution you could use the function below (which I developed for similar reasons). It supports placeholders as $var or ${var} as well as dollar escaping by ${'$'}

/**
 * Returns a String in which placeholders (e.g. $var or ${var}) are replaced by the specified values.
 * This function can be used for resolving templates at RUNTIME (e.g. for templates read from files).
 *
 * Example: 
  * "\$var1\${var2}".resolve(mapOf("var1" to "VAL1", "var2" to "VAL2")) 
  * returns VAL1VAL2
 */
fun String.resolve(values: Map<String, String>): String {

    val result = StringBuilder()

    val matcherSimple = "\\$([a-zA-Z_][a-zA-Z_0-9]*)"           // simple placeholder e.g. $var
    val matcherWithBraces = "\\$\\{([a-zA-Z_][a-zA-Z_0-9]*)}"   // placeholder within braces e.g. ${var}

    // match a placeholder (like $var or ${var}) or ${'$'} (escaped dollar)
    val allMatches = Regex("$matcherSimple|$matcherWithBraces|\\\$\\{'(\\\$)'}").findAll(this)

    var position = 0
    allMatches.forEach {
        val range = it.range
        val placeholder = this.substring(range)
        val variableName = it.groups.filterNotNull()[1].value
        val newText =
            if ("\${'\$'}" == placeholder) "$"
            else values[variableName] ?: throw IllegalArgumentException("Could not resolve placeholder $placeholder")
        result.append(this.substring(position, range.start)).append(newText)
        position = range.last + 1
    }
    result.append(this.substring(position))
    return result.toString()
}
Alan Z.
  • 143
  • 6
0

String templates only work for compile-time Sting literals, while what u read from a file is generated at runtime.

What u need is a template engine, which can render templates with variables or models at runtime.

For simple cases, String.format or MessageFormat.format in Java would work.

And for complex cases, check thymeleaf, velocity and so on.

ProtossShuttle
  • 1,623
  • 20
  • 40