3 String Templates

String templates complement Java's existing string literals and text blocks by coupling literal text with embedded expressions and template processors to produce specialized results. An embedded expression is a Java expression except it has additional syntax to differentiate it from the literal text in the string template. A template processor combines the literal text in the template with the values of the embedded expressions to produce a result.

See the StringTemplate interface in the Java SE API specification for more information.

Note:

This is a preview feature. A preview feature is a feature whose design, specification, and implementation are complete, but is not permanent. A preview feature may exist in a different form or not at all in future Java SE releases. To compile and run code that contains preview features, you must specify additional command-line options. See Preview Language and VM Features.

For background information about string templates, see JEP 430.

Basic Usage of String Templates

The following example declares a template expression that uses the template processor STR and contains one embedded expression, name:

String name = "Duke";
String info = STR."My name is \{name}";
System.out.println(info);

It prints the following:

My name is Duke

The template processor STR is one of the template processors that's included in the JDK. It automatically performs string interpolation by replacing each embedded expression in the template with its value, converted to a string. The JDK includes two other template processors:

  • The FMT Template Processor: It's like the STR template processor except that it accepts format specifiers as defined in java.util.Formatter and locale information in a similar way as in printf method invocations.
  • The RAW Template Processor: It doesn't automatically process the string template like the STR template processors. You can use it to help you create your own template processors. Note that you can also implement the StringTemplate.Processor interface to create a template processor. See Creating a Template Processor.

A dot (.) follows the template processor, which makes template expressions look similar to accessing an object's field or calling an object's method.

A string template follows the dot character, which is a string that contains one or more embedded expressions. An embedded expression is a Java expression that's surrounded by a backslash and opening brace (\{) and a closing brace }. See Embedded Expressions in String Templates for additional examples.

Embedded Expressions in String Templates

You can use any Java expression as an embedded expression in a string template.

You can use strings and characters as embedded expressions.

String user = "Duke";   
char option = 'b';
String choice  = STR."\{user} has chosen option \{option}";
System.out.println(choice);

This example prints the following:

Duke has chosen option b

Embedded expressions can perform arithmetic operations.

double x = 10.5, y = 20.6;
String p = STR."\{x} * \{y} = \{x * y}";
System.out.println(p);

This example prints the following:

10.5 * 20.6 = 216.3

As with any Java expression, embedded expressions are evaluated from left to right. They can also contain prefix and postfix operators.

int index = 0;
String data = STR."\{index++}, \{index++}, \{++index}, \{index++}, \{index}";
System.out.println(data);

This example prints the following:

0, 1, 3, 3, 4

Embedded expressions can invoke methods and access fields.

String time = STR."Today is \{java.time.LocalDate.now()}";
System.out.println(time);
String canLang = STR."The language code of \{
    Locale.CANADA_FRENCH} is \{
    Locale.CANADA_FRENCH.getLanguage()}";
System.out.println(canLang);

This example prints output similar to the following:

Today is 2023-07-11
The language code of fr_CA is fr

Note that you can insert line breaks in an embedded expression and not introduce newlines in the result like in the previous example. In addition, you don't have to escape quotation marks in an embedded expression.

Path filePath = Paths.get("Stemp.java");
String msg = STR."The file \{filePath} \{
    // The Files class is in the package java.nio.file
    Files.exists(filePath) ? "does" : "does not"} exist";
System.out.println(msg);

String currentTime = STR."The time is \{
    DateTimeFormatter
        .ofPattern("HH:mm:ss")
        .format(LocalTime.now())
} right now";
System.out.println(currentTime);

This example prints output similar to the following:

The file Stemp.java does exist
The time is 11:32:38 right now

You can embed a template expression in a string template.

String[] a = { "X", "Y", "Z" };
String letters = STR."\{a[0]}, \{STR."\{a[1]}, \{a[2]}"}";
System.out.println(letters);

This example prints the following:

X, Y, Z

To make this example clearer, you can substitute the embedded template expression with a variable:

String temp     = STR."\{a[1]}, \{a[2]}";
String letters2 = STR."\{a[0]}, \{temp}";
System.out.println(letters2);

Multiline String Templates

When you specify the template for a string template, you can use a text block instead of a regular string. See Text Blocks for more information. This enables you to use HTML documents and JSON data more easily with string templates as demonstrated in the following examples.

String title = "My Web Page";
String text = "Hello, world";
String webpage = STR."""
    <html>
      <head>
        <title>\{title}</title>
      </head>
      <body>
        <p>\{text}</p>
      </body>
    </html>
    """;
System.out.println(webpage);

This example prints the following:

<html>
  <head>
    <title>My Web Page</title>
  </head>
  <body>
    <p>Hello, world</p>
  </body>
</html>

The following example creates JSON data.

String customerName    = "Java Duke";
String phone           = "555-123-4567";
String address         = "1 Maple Drive, Anytown";
String json = STR."""
{
    "name":    "\{customerName}",
    "phone":   "\{phone}",
    "address": "\{address}"
}
""";

It prints the following:

{
    "name":    "Java Duke",
    "phone":   "555-123-4567",
    "address": "1 Maple Drive, Anytown"
}

You can use a text block to work with tabular data more clearly.

record Rectangle(String name, double width, double height) {
    double area() {
        return width * height;
    }
}

Rectangle[] zone = new Rectangle[] {
    new Rectangle("First",  17.8, 31.4),
    new Rectangle("Second",  9.6, 12.2),
};
    
String table = STR."""
    Description\tWidth\tHeight\tArea
    \{zone[0].name}\t\t\{zone[0].width}\t\{zone[0].height}\t\{zone[0].area()}
    \{zone[1].name}\t\t\{zone[1].width}\t\{zone[1].height}\t\{zone[1].area()}
    Total \{zone[0].area() + zone[1].area()}
    """;
    
System.out.println(table);

This example prints the following:

Description     Width   Height  Area
First           17.8    31.4    558.92
Second          9.6     12.2    117.11999999999999
Total 676.04

The FMT Template Processor

The FMT template processor is like the STR template processor except that it can use format specifiers that appear to the left of an embedded expression. These format specifiers are the same as those defined in the class java.util.Formatter.

The following example is just like the tabular data example in Multiline String Templates but better formatted.

String formattedTable = FormatProcessor.FMT."""
    Description     Width    Height     Area
    %-12s\{zone[0].name}  %7.2f\{zone[0].width}  %7.2f\{zone[0].height}     %7.2f\{zone[0].area()}
    %-12s\{zone[1].name}  %7.2f\{zone[1].width}  %7.2f\{zone[1].height}     %7.2f\{zone[1].area()}
    \{" ".repeat(28)} Total %7.2f\{zone[0].area() + zone[1].area()}
    """;    
    
System.out.println(formattedTable);

It prints the following:

Description     Width    Height     Area
First           17.80    31.40      558.92
Second           9.60    12.20      117.12
                             Total  676.04

The RAW Template Processor

The RAW template processor defers the processing of the template to a later time. Consequently, you can retrieve the template's string literals and embedded expression results before processing it.

To retrieve a template's string literals and embedded expression results, call StringTemplate::fragments and StringTemplate::values, respectively. To process a string template, call StringTemplate.process(StringTemplate.Processor) or StringTemplate.Processor.process(StringTemplate). You can also call the method StringTemplate::interpolate, which returns the same result as the STR template processor.

The following example demonstrates these methods.

int v = 10, w = 20;
StringTemplate rawST = StringTemplate.RAW."\{v} plus \{w} equals \{v + w}";
java.util.List<String> fragments = rawST.fragments();
java.util.List<Object> values = rawST.values();
    
System.out.println(rawST.toString());
    
fragments.stream().forEach(f -> System.out.print("[" + f + "]"));
System.out.println();
    
values.stream().forEach(val -> System.out.print("[" + val + "]"));
System.out.println();
    
System.out.println(rawST.process(STR));
System.out.println(STR.process(rawST));
System.out.println(rawST.interpolate());

It prints the following:

StringTemplate{ fragments = [ "", " plus ", " equals ", "" ], values = [10, 20, 30] }
[][ plus ][ equals ][]
[10][20][30]
10 plus 20 equals 30
10 plus 20 equals 30
10 plus 20 equals 30

The method StringTemplate::fragments returns a list of fragment literals, which are the character sequences preceding each of the embedded expressions, plus the character sequence following the last embedded expression. In this example, the first and last fragment literals are zero-length strings because an embedded expression appears at the beginning and end of the template.

The method StringTemplate::values returns a list of embedded expression results. These values are computed every time a string template is evaluated. However, fragment literals are constant across all evaluations of a template expression. The following example demonstrates this.

int t = 20;
for (int u = 0; u < 3; u++) {
    StringTemplate stLoop = StringTemplate.RAW."\{t} plus \{u} equals \{t + u}";
    System.out.println("Fragments: " + stLoop.fragments());
    System.out.println("Values:    " + stLoop.values());
}

It prints the following:

Fragments: [,  plus ,  equals , ]
Values:    [20, 0, 20]
Fragments: [,  plus ,  equals , ]
Values:    [20, 1, 21]
Fragments: [,  plus ,  equals , ]
Values:    [20, 2, 22]

Creating a Template Processor

By implementing the StringTemplate.Processor interface, you can create your own template processor, which can return objects of any type, not just String, and throw check exceptions if processing fails.

Creating a Template Processor that Returns JSON Objects

The following is an example of a template processor that returns JSON objects.

Note:

These examples use the classes Json, JsonException, JsonObject, and JsonReader from the jakarta.json package, which contains the Jakarta JSON Processing API. It also indirectly uses the org.eclipse.parsson.JsonProviderImpl class from Eclipse Parsson, which is an implementation of Jakarta JSON Processing Specification. Obtain libraries for these APIs from Eclipse GlassFish.
var JSON = StringTemplate.Processor.of(
    (StringTemplate stJSON) -> {
        try (JsonReader jsonReader = Json.createReader(new StringReader(
            stJSON.interpolate()))) {
            return jsonReader.readObject();
        }    
    }
);

String accountType = "user";
String userName = "Duke";
String pw = "my_password";

JsonObject newAccount = JSON."""
{
    "account":    "\{accountType}",
    "user":       "\{userName}",
    "password":   "\{pw}"
}
""";

System.out.println(newAccount);

userName = "Duke\", \"account\": \"administrator";

newAccount = JSON."""
{
    "account":    "\{accountType}",
    "user":       "\{userName}",
    "password":   "\{pw}"
}
""";

System.out.println(newAccount);

It prints the following:

{"account":"user","user":"Duke","password":"my_password"}
{"account":"administrator","user":"Duke","password":"my_password"}

There's a problem with this example: It is susceptible to one type of JSON injection attack. This type of attack involves inserting malicious data containing quotation marks in a JSON string, changing the JSON string itself. In this example, it changes the user name to "Duke\", \"account\": \"administrator". The resulting JSON string becomes the following:

{
    "account":    "user",
    "user":       "Duke",
    "account":    "administrator",
    "password":   "my_password"
}

If the JSON parser encounters entries with the same name, it takes the last one. Consequently, the user Duke now has administrator privileges.

The following example addresses this kind of JSON injection attack. It throws an exception if the template contains any strings with a quotation mark. It also throws an exception if any of its values aren't numbers, or Boolean values.

StringTemplate.Processor<JsonObject, JsonException> JSON_VALIDATE =
    (StringTemplate stJVAL) -> {
    String[] invalidStrings = new String[] { "\"", "'" };
    List<Object> filtered = new ArrayList<>();
    for (Object value : stJVAL.values()) {
        if (value instanceof String str) {
            if (Arrays.stream(invalidStrings).anyMatch(str::contains)) {
                throw new JsonException("Injection vulnerability");
            }
            filtered.add(str);
        } else if (value instanceof Number ||
                   value instanceof Boolean) {
            filtered.add(value);
        } else {
            throw new JsonException("Invalid value type");
        }
    }
    
    String jsonSource =
        StringTemplate.interpolate(stJVAL.fragments(), filtered);

    try (JsonReader jsonReader = Json.createReader(new StringReader(
        jsonSource))) {
        return jsonReader.readObject();
    }             
};        
    
String accountType = "user";
String userName = "Duke";
String pw = "my_password";

try {
    JsonObject newAccount = JSON_VALIDATE."""
    {
        "account":    "\{accountType}",
        "user":       "\{userName}",
        "password":   "\{pw}"
    }
    """;

    System.out.println(newAccount);

    userName = "Duke\", \"account\": \"administrator";

    newAccount = JSON_VALIDATE."""
    {
        "account":    "\{accountType}",
        "user":       "\{userName}",
        "password":   "\{pw}"
    }
    """;

    System.out.println(newAccount);
    
} catch (JsonException ex) {
    System.out.println(ex.getMessage());
}

It prints the following:

{"account":"user","user":"Duke","password":"my_password"}
Injection vulnerability

Creating a Template Processor that Safely Composes and Runs Database Queries

You can create a template processor that returns a prepared statement.

Consider the following example that retrieves information about a specific coffee supplier (identified by SUP_NAME) from the table SUPPLIERS. See the subsection SUPPLIERS Table from the JDBC tutorial in The Java Tutorials (Java SE 8 and earlier) for more information about this database table, including how to create and populate it.

  public static void getSupplierInfo(Connection con, String supName) throws SQLException {
    String query = "SELECT * from SUPPLIERS s WHERE s.SUP_NAME = '" + supName + "'";
      try (Statement stmt = con.createStatement()) {
        ResultSet rs = stmt.executeQuery(query);
        System.out.println("ID   " +
                           "Name                       " +
                           "Street               " +
                           "City          State  Zip");
        while (rs.next()) {
          int supplierID = rs.getInt("SUP_ID");
          String supplierName = rs.getString("SUP_NAME");
          String street = rs.getString("STREET");
          String city = rs.getString("CITY");
          String state = rs.getString("STATE");
          String zip = rs.getString("ZIP");
          String supRow = FormatProcessor.FMT."%-4s\{supplierID} %-26s\{
            supplierName} %-20s\{street} %-13s\{city} %-6s\{state} \{zip}";
          System.out.println(supRow);
        }
      } catch (SQLException e) {
        JDBCTutorialUtilities.printSQLException(e);
    }
  }

Note:

You can add this method to SuppliersTable.java.

Suppose you call this method as follows:

SuppliersTable.getSupplierInfoST(myConnection, "Superior Coffee");

The method prints the following:

ID   Name                       Street               City          State  Zip
49   Superior Coffee            1 Party Place        Mendocino     CA     95460

There's a problem with this example: It is susceptible to SQL injection attacks. Suppose you call the method as follows:

SuppliersTable.getSupplierInfo(
  myConnection, "Superior Coffee' OR s.SUP_NAME <> 'Superior Coffee");

The SQL query becomes the following:

SELECT * from SUPPLIERS s WHERE
  s.SUP_NAME = 'Superior Coffee' OR
  s.SUP_NAME <> 'Superior Coffee'

The Statement object treats the "invalid" supplier name as part of an SQL statement. As a result, the method prints all entries in the SUPPLIERS table, potentially exposing confidential information.

Prepared statements help prevent SQL injection attacks. They always treat client-supplied data as content of a parameter and never as a part of an SQL statement. The following example creates an SQL query string from a string template, creates a JDBC PreparedStatement from the query string, and then sets its parameters to the embedded expressions' values.

  record QueryBuilder(Connection conn)
    implements StringTemplate.Processor<PreparedStatement, SQLException> {

      public PreparedStatement process(StringTemplate st) throws SQLException {
        // 1. Replace StringTemplate placeholders with PreparedStatement placeholders
        String query = String.join("?", st.fragments());

        // 2. Create the PreparedStatement on the connection
        PreparedStatement ps = conn.prepareStatement(query);

        // 3. Set parameters of the PreparedStatement
        int index = 1;
        for (Object value : st.values()) {
            switch (value) {
                case Integer i -> ps.setInt(index++, i);
                case Float f   -> ps.setFloat(index++, f);
                case Double d  -> ps.setDouble(index++, d);
                case Boolean b -> ps.setBoolean(index++, b);
                default        -> ps.setString(index++, String.valueOf(value));
            }
        }
        return ps;
    }
  }     

The following example is like the getSupplierInfo example except that it uses the QueryBuilder template processor:

  public static void getSupplierInfoST(Connection con, String supName) throws SQLException {
      var DB = new QueryBuilder(con);
      try (PreparedStatement ps = DB."SELECT * from SUPPLIERS s WHERE s.SUP_NAME = \{supName}") {
        ResultSet rs = ps.executeQuery();
        System.out.println("ID   " +
                           "Name                       " +
                           "Street               " +
                           "City          State  Zip");
        while (rs.next()) {
          int supplierID = rs.getInt("SUP_ID");
          String supplierName = rs.getString("SUP_NAME");
          String street = rs.getString("STREET");
          String city = rs.getString("CITY");
          String state = rs.getString("STATE");
          String zip = rs.getString("ZIP");
          String supRow = FormatProcessor.FMT."%-4s\{supplierID} %-26s\{
            supplierName} %-20s\{street} %-13s\{city} %-6s\{state} \{zip}";
          System.out.println(supRow);
        }
      } catch (SQLException e) {
        JDBCTutorialUtilities.printSQLException(e);
    }
  } 

Creating a Template Processor that Simplifies the Use of Resource Bundles

The following template processor, LocalizationProcessor, maps a string to a corresponding property in a resource bundle. When using this template processor, in your resource bundles, the property names are the string templates in your applications, where embedded expressions are substituted with underscores (_) and spaces with periods (.).

record LocalizationProcessor(Locale locale)
    implements StringTemplate.Processor<String, RuntimeException> {
    
    public String process(StringTemplate st) {
        ResourceBundle resource = ResourceBundle.getBundle("resources", locale);
        String stencil = String.join("_", st.fragments());
        String msgFormat = resource.getString(stencil.replace(' ', '.'));
        return MessageFormat.format(msgFormat, st.values().toArray());
    }
}

Suppose the following are your resource bundles:

Figure 3-1 resources_en_US.properties

# resources_en_US.properties file
_.chose.option._=\
    {0} chose option {1}

Figure 3-2 resources_fr_CA.properties

# resources_fr_CA.properties file
_.chose.option._=\
    {0} a choisi l''option {1}

The following example uses LocalizationProcessor and these two resource bundles:

var userLocale = new Locale("en", "US");
var LOCALIZE = new LocalizationProcessor(userLocale);

String user = "Duke", option = "b";
System.out.println(LOCALIZE."\{user} chose option \{option}");  
    
userLocale = new Locale("fr", "CA");
LOCALIZE = new LocalizationProcessor(userLocale);
System.out.println(LOCALIZE."\{user} chose option \{option}"); 

It prints the following:

Duke chose option b
Duke a choisi l'option b