The Java String Template Processor

“With the new day comes new strength and new thoughts.”

Eleanor Roosevelt

With Java 22, string interpolation is fighting for its place in the Java ecosystem. What works in other languages and many frameworks should now also simplify the work of Java developers. Some wishes are fulfilled for the developers, others unfortunately not and some would never have been expressed. But more on that later.

Before the String Template Processor existed, variable values had to be incorporated into a String by hand. In the simplest case by String concatenation.

int number = 42;
String answer = "The answer is " + number + "!";

The value of the variable number is implicitly converted into a String and then linked with the two other String to produce the result. This can also be done explicitly with StringBuilder or its now largely unknown sister StringBuffer.

int number 42;
String answer = new StringBuilder("The answer is ").append(number).append("!")
  .toString();

If you use a lot of variables with formatting, you do not want to generate long concatenations but use the formatting options available since Java 5.

int number = 42;
String answer = String.format("The answer is %d!", number);

Since Java 15, however, this can also be written a little more elegantly.

int number = 42;
String answer = "The answer is %d!".formatted(number);

If you have FreshMarker or another template engine at hand, you can also use something like the following.

int number = 42;
Configuration configuration = new Configuration();
Template template = configuration.getTemplate("h2g2", "The answer is ${number}!");
String answer= template.process(Map.of("number", number));

The String Template Processor in Java 22 allows variables to be addressed directly in the String. To do this, you must follow the syntax "\{name}", where name is the name of the desired variable. The standard String Template Processor STR converts all variable values into String and assembles the resulting String. Our example with a String Template Processor looks like this.

int number = 42;
String answer = STR."The answer is \{number}!";

The syntax looks a little strange and is not found anywhere else in the language. If you want to format the variables, you can use the FormatProcessor.FMT instead of STR. Before we explore the possibilities of the String Template Processor with some custom processors, I would like to point out an unexpected feature. The curly brackets can not only contain variables, they can also contain expressions with side effects!

int number = 21;
String answer = STR."The answer is \{number + number}!";
String wrongAnswer= STR."The answer is \{++number + number}!";

In this example, the first text is correct because 21 + 21 gives the value 42. The second text is not correct because the expression first increments number and then 22 + 22 gives the value 44. In addition, the value of the variable number after evaluation by the String Template Processor is no longer 21 but 22! When using String Template Processor, care should be taken to ensure that they do not have any side effects. The maintainers of the software will be grateful to you later.

Now the really exciting part for developers. What can you do yourself? You can create your own String Template Processor class by implementing the StringTemplate.Processor interface.

The following class is a variation of the standard Processor STR

public class StringTemplateProcessor implements StringTemplate.Processor<StringBuilder, RuntimeException> {
    public final static StringTemplateProcessor STP = new StringTemplateProcessor();
    @Override
    public String process(StringTemplate stringTemplate) throws RuntimeException {
        StringBuilder builder = new StringBuilder();
        List<String> fragments = stringTemplate.fragments();
        if (fragments.size() == 1) {
            builder.add(fragments.getFirst());
            return builder;
        }
        List<Object> values = stringTemplate.values();
        StringBuilder builder = new StringBuilder();
        for (int i = 0, n = fragments.size() - 1; i < n; i++) {
            builder.append(fragments.get(i)).append(values.get(i));
        }
        builder.append(fragments.getLast());
        return builder;
    }
}

The StringTemplateProcessor receives the fragments and the values of the variables from the given StringTemplate. The fragments are the String fragments that are interrupted by the variables. The number of fragments is therefore always one more than the number of values. In our first example, the list of fragments contains "The answer is " and "!" and the list of values contains 42. Our processor takes these values and concatenates them via a StringBuilder. The special feature of this implementation is that no String is produced, but a StringBuilder to which further values can be appended.

Unfortunately, there is no way to pass meta information, such as formatting, to the StringTemplate via the curly braces syntax. The FormatProcessor.FMT uses the only existing possibility to inject the formatting information into a fragment before the variable (There is always a fragment before a variable). The processor above could use this option, for example, to display integer values in hex format.

private static final HexFormat HEX_FORMAT = HexFormat.of();
    
@Override
public StringBuilder process(StringTemplate stringTemplate) throws RuntimeException {
    StringBuilder builder = new StringBuilder();
    List<String> fragments = stringTemplate.fragments();
    if (fragments.size() == 1) {
        builder.add(fragments.getFirst());
        return builder;
    }
    List<Object> values = stringTemplate.values();
    StringBuilder builder = new StringBuilder();
    for (int i = 0, n = fragments.size() - 1; i < n; i++) {
        String fragment = fragments.get(i);
        Object value = values.get(i);
        if (fragment.endsWith("@H") && value instanceof Integer number) {
            fragment = fragment.substring(0, fragment.length() - 2);
            value = "0x" + HEX_FORMAT.toHexDigits(number);
        }
        builder.append(fragment).append(value);
    }
    builder.append(fragments.getLast());
    return builder;
}

A slightly modified example then generates a representation of 42 in the notation 0x0000002c.

int number = 42;
String answer = StringTemplateProcessor.STP."The answer is @H\{number}!";

A different example of a custom processor can be derived from the article Sichere Ahnen Prüfung mit Cryptographic Hashes. In this article, a cryptographic hash is generated from a formatted String to obtain a safe and secure comparison value.

private String getHash(Ancestor ancestor) throws NoSuchAlgorithmException {
    String content = "%s:%s:%s".formatted(ancestor.name(), ancestor.dateOfBirth(), ancestor.placeOfBirth());
    MessageDigest digest = MessageDigest.getInstance("SHA3-256");
    return new BigInteger(1, digest.digest(content.getBytes(UTF_8))).toString(16);
}

This functionality can of course also be expressed with a custom Processor.

public class MessageDigestProcessor implements Processor<byte[], NoSuchAlgorithmException > {
    public static final MessageDigestProcessor SHA3_256 = new MessageDigestProcessor("SHA3-256");
    public static final MessageDigestProcessor SHA3_512 = new MessageDigestProcessor("SHA3-512");

    private final String algorithm;

    public MessageDigestProcessor(String algorithm) {
        this.algorithm= algorithm;
    }

    @Override
    public byte[] process(StringTemplate stringTemplate) throws NoSuchAlgorithmException {
        MessageDigest digest = MessageDigest.getInstance(algorithm);
        return digest.digest(STR.process(stringTemplate).getBytes(StandardCharsets.UTF_8));
    }
}

This MessageDigestProcessor can then be used as follows.

byte[] digest = MessageDigestProcessor.SHA3_256."\{ancestor.name()}:\{ancestor.dateOfBirth()}:\{ancestor.placeOfBirth()}";
String digestString = new BigInteger(1, digest.digest(content.getBytes(UTF_8))).toString(16);

These examples show that the String Template Processor is an exciting alternative to the existing options in the Java standard library. However, the String Template Processor cannot offer the same possibilities as real template engines.

Leave a Comment