Skip to content

String Templates

Sebastian Fischer edited this page Dec 19, 2023 · 5 revisions

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.

Using String Templates

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:

  • FMT is similar to STR but interprets format specifiers to the left of embedded expressions.
  • RAW produces a StringTemplate instead of a String and 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.

Defining Template Processors

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 embedded into Java programs.

Clone this wiki locally