-
Notifications
You must be signed in to change notification settings - Fork 0
String Templates
String Templates (Preview) allow for the interpolation of variables and expressions directly within strings. They simplify the process of creating complex strings, making code more concise and easier to understand.
Template processors control how variables are interpolated, ensuring that values are properly escaped or sanitized. This prevents malicious code from being inadvertently executed. Many template processors are context-aware, meaning they understand the context in which a variable is interpolated (e.g., HTML, SQL) and apply appropriate escaping. By clearly delineating code from data, template processors reduce the risk of inadvertently executing untrusted input as code, a common source of injection vulnerabilities.
A common use case for String Templates is incorporating dynamic content into values of type String.
The most basic approach to accomplish this is string concatenation which is hard to read and not the most efficient option.
The StringBuilder and StringBuffer classes are more efficient but also more verbose.
The MessageFormat class and the more recent methods String.format and String.formatted are more concise.
But they separate the format string from the parameters, inviting arity and type mismatches.
Here is an example of using String.formatted in the constructor of the echo server from the previous part about Virtual Threads.
public Server {
System.out.println("listening on port %s".formatted(socket.getLocalPort()));
}By using a String Template, we can simplify this definition as follows.
public Server {
System.out.println(STR."listening on port \{socket.getLocalPort()}");
}Here, STR is a predefined template processor.
The new escape sequence \{ is used to include the values of expression into a string template.
There are other predefined template processors:
-
FMTis similar toSTRbut interprets format specifiers to the left of embedded expressions. -
RAWproduces aStringTemplateinstead of aStringand can be used to preprocess string templates to be processed by another template processor later.
Here are unit tests demonstrating the predefined template processors.
class StringTemplateTests {
@Test
void testDefaultTemplateProcessor() {
String result = STR."\{1} + \{2} = \{1+2}";
assertEquals("1 + 2 = 3", result);
}
@Test
void testFormattingTemplateProcessor() {
String result = FMT."%.2e\{1e-7}";
assertEquals("1.00e-07", result);
}
@Test
void testRawTemplateProcessor() {
int x = 1, y = 2;
StringTemplate template = RAW."\{x} + \{y} = \{x + y}";
assertIterableEquals(List.of("", " + ", " = ", ""), template.fragments());
assertIterableEquals(List.of(1, 2, 3), template.values());
String result = STR.process(template);
assertEquals("1 + 2 = 3", result);
result = template.interpolate();
assertEquals("1 + 2 = 3", result);
}
}The last test uses the methods fragments and values on a StringTemplate to check the components before interpolation.
The process method of template processors can be used to execute a template processor with a given string template.
The STR processor simply calls the method interpolate to interleave the fragments and values of a string template.
The final assertion demonstrates calling interpolate explicitly.
StringTemplate.Processor
is a functional interface parameterized by result and exception types.
We can implement it by providing the process method, say as a lambda expression.
As an example, we define a template processor that parses arithmetic expressions defined in the part about Pattern Matching.
public record Parser(Scanner input) {
public static StringTemplate.Processor<Expr, IllegalArgumentException> EXPR = template -> {
List<Object> invalidValues = template.values()
.stream()
.filter(value -> !(value instanceof Integer))
.toList();
if (!invalidValues.isEmpty()) {
throw new IllegalArgumentException(STR."invalid values: \{invalidValues}");
}
return new Parser(template.interpolate()).parseExpression();
};
// previous definitions omitted
}This template processor returns values of type Expr.
It throws an IllegalArgumentException when trying to interpolate values that are not integers.
We can use this template processor to rewrite a test defined earlier as follows.
@Test
void testParsingFormattedExpr() {
- final Expr expr = new Parser("(1 + 2)").parseExpression();
+ final Expr expr = EXPR."(1 + \{2})";
assertEquals(expr, new Parser(expr.format()).parseExpression());
}The presented examples show that string templates can be used to create values of any data type. The explicit use of template processors for string interpolation allows for domain-specific checks to be performed. As a result String Templates are a safe and extensible mechanism for parsing custom data.