Summary
Enhance the Java programming language with string templates. String templates complement Java’s existing string literals and text blocks by coupling literal text with embedded expressions and processors to produce specialized results. This is a preview language feature and API.
Goals
-
Simplify the writing of Java programs by making it easy to express strings that include values computed at run time.
-
Enhance the readability of expressions that mix text and expressions, whether the text fits on a single source line (like string literals) or span several source lines (like text blocks).
-
Improve the security of Java programs that compose strings from user-provided values and pass them to other systems (e.g., building queries for databases) by supporting validation and transformation of both the template and the values of its embedded expressions.
-
Retain flexibility by allowing Java libraries to define the formatting syntax used in string templates.
-
Simplify the use of APIs that accept strings written in non-Java languages (e.g., SQL, XML, and JSON).
-
Enable the creation of non-String expressions derived from combining literal text and embedded expressions, without having to transit through a temporary string representation.
Non-Goals
-
It is not a goal to introduce syntactic sugar for Java’s string concatenation operator
(+
), since that would circumvent the goal of validation. -
It is not a goal to deprecate or remove the
StringBuilder
andStringBuffer
classes, which have traditionally been used for complex or programmatic string composition.
Motivation
Developers routinely compose strings from a combination of literal text and expressions. Java provides several mechanisms for string composition, though unfortunately all have drawbacks.
-
String concatenation with the
+
operator produces hard-to-read code:String s = x + " plus " + y + " equals " + (x + y);
-
StringBuilder
is verbose:String s = new StringBuilder() .append(x) .append(" plus ") .append(y) .append(" equals ") .append(x + y) .toString();
-
String::format
andString::formatted
separate the format string from the parameters, inviting arity and type mismatches:String s = String.format("%2$d plus %1$d equals %3$d", x, y, x + y); String t = "%2$d plus %1$d equals %3$d".formatted(x, y, x + y);
-
java.text.MessageFormat
requires too much ceremony and uses an unfamiliar syntax in the
format string:MessageFormat mf = new MessageFormat("{0} plus {1} equals {2}"); String s = mf.format(x, y, x + y);
String interpolation
Many programming languages offer string interpolation as an alternative to string concatenation. Typically, this takes the form of a string literal that contains embedded expressions as well as literal text. Embedding expressions in situ means that readers can easily discern the intended result. At run time, the embedded expressions are replaced with their (stringified) values — the values are said to be interpolated into the string. Here are some examples of interpolation in other languages:
JavaScript `${x} plus ${y} equals ${x + y}`
C# $"{x} plus {y} equals {x + y}"
Visual Basic $"{x} plus {y} equals {x + y}"
Scala f"$x%d plus $y%d equals ${x + y}%d"
Python f"{x} plus {y} equals {x + y}"
Ruby "#{x} plus #{y} equals #{x + y}"
Groovy "$x plus $y equals ${x + y}"
Kotlin "$x plus $y equals ${x + y}"
Swift "(x) plus (y) equals (x + y)"
Some of these languages enable interpolation for all string literals while others require interpolation to be enabled when desired, for example by prefixing the literal’s opening delimiter with $
or f
. The syntax of embedded expressions also varies but often involves characters such as $
or { }
, which means that those characters cannot appear literally unless they are escaped.
Not only is interpolation more convenient than concatenation when writing code, it also offers greater clarity when reading code. The clarity is especially striking with larger strings. For example, in JavaScript:
const title = "My Web Page";
const text = "Hello, world";
var html = `
${title}
${text}
`;
String interpolation is dangerous
Unfortunately, the convenience of interpolation has a downside: It is easy to construct strings that will be interpreted by other systems but which are dangerously incorrect in those systems.
Strings that hold SQL statements, HTML/XML documents, JSON snippets, shell scripts, and natural-language text all need to be validated and sanitized according to domain-specific rules. Since the Java programming language cannot possibly enforce all such rules, it is up to developers using interpolation to validate and sanitize. Typically, this means remembering to wrap embedded expressions in calls to escape
or validate
methods, and relying on IDEs or static analysis tools to help to validate the literal text.
Interpolation is especially dangerous for SQL statements because it can lead to injection attacks. For example, consider this hypothetical Java code with the embedded expression ${name}
:
String query = "SELECT * FROM Person p WHERE p.last_name = '${name}'";
ResultSet rs = connection.createStatement().executeQuery(query);
If name
had the troublesome value
Smith' OR p.last_name <> 'Smith
then the query string would be
SELECT * FROM Person p WHERE p.last_name = 'Smith' OR p.last_name <> 'Smith'
and the code would select all rows, potentially exposing confidential information. Composing a query string with simple-minded interpolation is just as unsafe as composing it with traditional concatenation:
String query = "SELECT * FROM Person p WHERE p.last_name = '" + name + "'";
Can we do better?
For Java, we would like to have a string composition feature that achieves the clarity of interpolation but achieves a safer result out-of-the-box, perhaps trading off a small amount of convenience to gain a large amount of safety.
For example, when composing SQL statements any quotes in the values of embedded expressions must be escaped, and the string overall must have balanced quotes. Given the troublesome value of name
shown above, the query that should be composed is a safe one:
SELECT * FROM Person p WHERE p.last_name = ''Smith' OR p.last_name <> 'Smith''
Almost every use of string interpolation involves structuring the string to fit some kind of template: A SQL statement usually follows the template SELECT ... FROM ... WHERE ...
, an HTML document follows ...
, and even a message in a natural language follows a template that intersperses dynamic values (e.g., a username) amongst literal text. Each kind of template has rules for validation and transformation, such as “escape all quotes” for SQL statements, “allow only legal character entities” for HTML documents, and “localize to the language configured in the OS” for natural-language messages.
Ideally a string’s template could be expressed directly in the code, as if annotating the string, and the Java runtime would apply template-specific rules to the string automatically. The result would be SQL statements with escaped quotes, HTML documents with no illegal entities, and boilerplate-free message localization. Composing a string from a template would relieve developers of having to laboriously escape each embedded expression, call validate()
on the whole string, or use java.util.ResourceBundle
to look up a localized string.
For another example, a developer might construct a string denoting a JSON document and then feed it to a JSON parser in order to obtain a strongly-typed JSONObject
:
String name = "Joan Smith";
String phone = "555-123-4567";
String address = "1 Maple Drive, Anytown";
String json = """
{
"name": "%s",
"phone": "%s",
"address": "%s"
}
""".formatted(name, phone, address);
JSONObject doc = JSON.parse(json);
... doc.entrySet().stream().map(...) ...
Ideally the JSON structure of the string could be expressed directly in the code, and the Java runtime would transform the string into a JSONObject
automatically. The manual detour through the parser would not be necessary.
In summary, we could improve the readability and reliability of almost every Java program if we had a first-class, template-based mechanism for composing strings. Such a feature would offer the benefits of interpolation, as seen in other programming languages, but would be less prone to introducing security vulnerabilities. It would also reduce the ceremony of working with libraries that take complex input as strings.
Description
A template expression is a new kind of expression in the Java programming language. A template expression can perform string interpolation, but is also programmable in a way that helps developers compose strings safely and efficiently. In addition, a template expression is not limited to composing strings — it can turn structured text into any kind of object, according to domain-specific rules.
Syntactically, a template expression resembles a string literal with a prefix. There is a template expression on the second line of this code:
String name = "Joan";
String info = STR."My name is {name}";
assert info.equals("My name is Joan"); // true
The template expression STR."My name is {name}"
consists of:
- A template processor (
STR
); - A dot character (U+002E), as seen in other kinds of expressions; and
- A template (
"My name is {name}"
) which contains an embedded expression ({name}
).
When a template expression is evaluated at run time, its template processor combines the literal text in the template with the values of the embedded expressions in order to produce a result. The result of the template processor, and thus the result of evaluating the template expression, is often a String
— though not always.
The STR
template processor
STR
is a template processor defined in the Java library. It performs string interpolation by replacing each embedded expression in the template with the (stringified) value of that expression. The result of evaluating a template expression which uses STR
is a String
; e.g., "My name is Joan"
.
In everyday conversation, developers are likely to use the term “template” when referring to either the whole or part of a template expression. This informal usage is reasonable as long as the template processor of a particular template expression is not mixed up with the processor argument of the same template expression.
STR
is a public
static
final
field that is automatically imported in every Java source file.
Here are more examples of template expressions that use the STR
template processor. The symbol |
in the left margin means that the line shows the value of the previous statement, similar to jshell.
// Embedded expressions can be strings
String firstName = "Bill";
String lastName = "Duck";
String fullName = STR."{firstName} {lastName}";
| "Bill Duck"
String sortName = STR."{lastName}, {firstName}";
| "Duck, Bill"
// Embedded expressions can perform arithmetic
int x = 10, y = 20;
String s = STR."{x} + {y} = {x + y}";
| "10 + 20 = 30"
// Embedded expressions can invoke methods and access fields
String s = STR."You have a {getOfferType()} waiting for you!";
| "You have a gift waiting for you!"
String t = STR."Access at {req.date} {req.time} from {req.ipAddress}";
| "Access at 2022-03-25 15:34 from 8.8.8.8"
To aid refactoring, double-quote characters can be used inside embedded expressions without escaping them as "
. This means that an embedded expression can appear in a template expression exactly as it would appear outside the template expression, easing the switch from +
to template expressions. For example:
String filePath = "tmp.dat";
File file = new File(filePath);
String old = "The file " + filePath + " " + file.exists() ? "does" : "does not" + " exist";
String msg = STR."The file {filePath} {file.exists() ? "does" : "does not"} exist";
| "The file tmp.dat does exist" or "The file tmp.dat does not exist"
To aid readability, an embedded expression can be spread over multiple lines in the source file. (This does not introduce newlines into the result.) The value of the embedded expression is interpolated into the result at the position of the of the embedded expression; the template is then considered to continue on the same line as the
. For example:
String time = STR."The time is {
// The java.time.format package is very useful
DateTimeFormatter
.ofPattern("HH:mm:ss")
.format(LocalTime.now())
} right now";
| "The time is 12:34:56 right now"
A template expression may have one or more embedded expressions and there is no limit to how many. The embedded expressions are evaluated from left to right, just like the arguments in a method invocation expression. For example:
// Embedded expressions can be postfix increment expressions
int index = 0;
String data = STR."{index++}, {index++}, {index++}, {index++}";
| "0, 1, 2, 3"
Any Java expression can be used as an embedded expression — even a template expression. For example:
// Embedded expression is a (nested) template expression
String[] fruit = { "apples", "oranges", "peaches" };
String s = STR."{fruit[0]}, {STR."{fruit[1]}, {fruit[2]}"}";
| "apples, oranges, peaches"
Here, the template expression STR."{fruit[1]}, {fruit[2]}"
is embedded in the template of another template expression. This code is difficult to read due to the abundance of "
, , and
{ }
characters, so it would be better to format it for easier readability:
String s = STR."{fruit[0]}, {
STR."{fruit[1]}, {fruit[2]}"
}";
Alternatively, since the embedded expression has no side effects, it could be refactored into a separate template expression:
String tmp = STR."{fruit[1]}, {fruit[2]}";
String s = STR."{fruit[0]}, {tmp}";
Multi-line template expressions
The template of a template expression can span multiple lines of source code, using a syntax similar to that of text blocks. (We saw an embedded expression spanning multiple lines above, but the template which contained the embedded expression was logically one line.)
Here are examples of template expressions denoting HTML text, JSON text, and a zone form, all spread over multiple lines:
String title = "My Web Page";
String text = "Hello, world";
String html = STR."""
{title}
{text}
""";
| """
|
|
| My Web Page
|
|
| Hello, world
|
|
| """
String name = "Joan Smith";
String phone = "555-123-4567";
String address = "1 Maple Drive, Anytown";
String json = STR."""
{
"name": "{name}",
"phone": "{phone}",
"address": "{address}"
}
""";
| """
| {
| "name": "Joan Smith",
| "phone": "555-123-4567",
| "address": "1 Maple Drive, Anytown"
| }
| """
record Rectangle(String name, double width, double height) {
double area() {
return width * height;
}
}
Rectangle[] zone = new Rectangle[] {
new Rectangle("Alfa", 17.8, 31.4),
new Rectangle("Bravo", 9.6, 12.4),
new Rectangle("Charlie", 7.1, 11.23),
};
String form = STR."""
Description Width Height Area
{zone[0].name} {zone[0].width} {zone[0].height} {zone[0].area()}
{zone[1].name} {zone[1].width} {zone[1].height} {zone[1].area()}
{zone[2].name} {zone[2].width} {zone[2].height} {zone[2].area()}
Total {zone[0].area() + zone[1].area() + zone[2].area()}
""";
| """
| Description Width Height Area
| Alfa 17.8 31.4 558.92
| Bravo 9.6 12.4 119.03999999999999
| Charlie 7.1 11.23 79.733
| Total 757.693
| """
| """
The FMT
template processor
FMT
is another template processor defined in the Java library. FMT
is like STR
in that it performs interpolation, but it also interprets format specifiers which appear to the left of embedded expressions. The format specifiers are the same as those defined in java.util.Formatter
. Here is the zone form example, tidied up by format specifiers in the template:
record Rectangle(String name, double width, double height) {
double area() {
return width * height;
}
}
Rectangle[] zone = new Rectangle[] {
new Rectangle("Alfa", 17.8, 31.4),
new Rectangle("Bravo", 9.6, 12.4),
new Rectangle("Charlie", 7.1, 11.23),
};
String form = 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()}
%-12s{zone[2].name} %7.2f{zone[2].width} %7.2f{zone[2].height} %7.2f{zone[2].area()}
{" ".repeat(28)} Total %7.2f{zone[0].area() + zone[1].area() + zone[2].area()}
""";
| """
| Description Width Height Area
| Alfa 17.80 31.40 558.92
| Bravo 9.60 12.40 119.04
| Charlie 7.10 11.23 79.73
| Total 757.69
| """
Template expressions
The template expression STR."..."
is a shortcut for invoking the process
method of the STR
template processor. That is, the now-familiar example:
String name = "Joan";
String info = STR."My name is {name}";
is a shortcut for:
String name = "Joan";
StringTemplate st = RAW."My name is {name}";
String info = STR.process(st);
The design of template expressions deliberately makes it impossible to go directly from a string literal or text block with embedded expressions to a String
with the expressions’ values interpolated. This prevents dangerously incorrect strings from spreading through the program. The string literal is processed by a template processor, which has explicit responsibility for safely interpolating and validating a result, String
or otherwise. Thus if a developer forgets to use a template processor such as STR
, RAW
or FMT
then a compile-time error occurs:
String name = "Joan";
String info = "My name is {name}";
| error: processor missing from template expression
When the unprocessed template is required, the standard RAW
template processor can be use to produce a StringTemplate
object.
String name = "Joan";
StringTemplate st = RAW."My name is {name}";
Syntax and semantics
The four kinds of template in a template expression are shown by its grammar, which starts at TemplateExpression
:
TemplateExpression:
TemplateProcessor . TemplateArgument
TemplateProcessor:
Expression
TemplateArgument:
Template
StringLiteral
TextBlock
Template:
StringTemplate
TextBlockTemplate
StringTemplate:
Resembles a StringLiteral but has one or more embedded expressions,
and can be spread over multiple lines of source code
TextBlockTemplate:
Resembles a TextBlock but has one or more embedded expressions
The Java compiler scans the term "..."
and determines whether to parse it as a StringLiteral
or a StringTemplate
based on the presence of embedded expressions. The compiler similarly scans the term """..."""
and determines whether to parse it as a TextBlock
or a TextBlockTemplate
. We refer uniformly to the ...
portion of these terms as the content of a string literal, string template, text block, or text block template.
We strongly encourage IDEs to visually distinguish a string template from a string literal, and a text block template from a text block. Within the content of a string template or text block template, IDEs should visually distinguish an embedded expression from literal text.
The Java programming language distinguishes string literals from string templates, and text blocks from text block templates, primarily because the type of a string template or text block template is not the familiar String
. The type of a string template or text block template is StringTemplate
, which is an interface, and String
does not implement StringTemplate
. When the template of a template expression is a string literal or a text block, therefore, the Java compiler automatically transforms the String
denoted by the template into a StringTemplate
with no embedded expressions.
At run time, a template expression is evaluated as follows:
- The expression to the left of the dot is evaluated to obtain an instance of
ValidatingProcessor
, - The expression to the right of the dot is evaluated to obtain an instance of
StringTemplate
(which, if the expression was aString
will have been
automatically transformed to an instance ofStringTemplate
) - The
StringTemplate
instance is passed to theprocess
method of theValidatingProcessor
instance, which composes a result.
The type of a template expression is, thus, the return type of the process
method of the ValidatingProcessor
instance.
The StringTemplate
and ValidatingProcessor
APIs, along with related APIs, are declared in the new package java.lang.template
.
String literals inside template expressions
The ability to use a string literal or a text block as a template improves the flexibility of template expressions. Developers can write template expressions that initially have placeholder text in a string literal, such as
String s = STR."Welcome to your account";
| "Welcome to your account"
and gradually embed expressions into the text to create a string template without changing any delimiters or inserting any special prefixes:
String s = STR."Welcome, {user.firstName()}, to your account {user.accountNumber()}";
| "Welcome, Lisa, to your account 12345"
User-defined template processors
Earlier we saw the template processors STR
and FMT
, which make it look as if a template processor is an object accessed via a field. That is useful shorthand, but it is more accurate to say that a template processor is a object which provides the functional interface ValidatingProcessor
. In particular, the object’s class implements the single abstract method of ValidatingProcessor
, process
, which takes a StringTemplate
and returns an object. A field such as STR
merely stores an instance of such a class. (The actual class whose instance is stored in STR
has an process
method that performs a stateless interpolation for which a singleton instance is suitable, hence the upper-case field name.)
Developers can easily create template processors for use in template expressions. However, before discussing how to create a template processor, it is necessary to discuss the class StringTemplate
.
An instance of StringTemplate
represents the string template or text block template that appears as the template in a template expression. Consider this code:
int x = 10, y = 20;
StringTemplate st = RAW."{x} plus {y} equals {x + y}";
String s = st.toString();
| StringTemplate{ fragments = [ "", " plus ", " equals ", "" ], values = [10, 20, 30] }
The result is, perhaps, a surprise. Where is the interpolation of 10
, 20
, and 30
into the text " plus "
and " equals "
? Recall that one of the goals of template expressions is to provide secure string composition. Having StringTemplate::toString
simply concatenate "10"
, " plus "
, "20"
, " equals "
, and "30"
into a String
would circumvent that goal. Instead, the toString
method renders the two useful parts of a StringTemplate
:
- The text fragments,
"", " plus ", " equals ", ""
, and - The values,
10
,20
,30
.
The StringTemplate
class exposes these parts directly:
-
StringTemplate::fragments
returns a list of the text fragments coming before and after the embedded expressions in the string template or text block template:int x = 10, y = 20; StringTemplate st = RAW."{x} plus {y} equals {x + y}"; List
fragments = st.fragments(); String result = String.join("\{}", fragments); | "{} plus {} equals {}" -
StringTemplate::values
returns a list of the values produced by evaluating the embedded expressions in the order they appear in the source code. In the current example, this is equivalent toList.of(x, y, x + y)
.int x = 10, y = 20; StringTemplate st = RAW."{x} plus {y} equals {x + y}"; List
The fragments()
of a StringTemplate
are constant across all evaluations of a template expression, while values()
is computed fresh for each evaluation. For example:
int y = 20;
for (int x = 0; x < 3; x++) {
StringTemplate st = RAW."{x} plus {y} equals {x + y}";
System.out.println(st);
}
| ["Adding ", " and ", " yields ", ""](0, 20, 20)
| ["Adding ", " and ", " yields ", ""](1, 20, 21)
| ["Adding ", " and ", " yields ", ""](2, 20, 22)
Using fragments()
and values()
, it is straightforward to write an interpolating template processor. For brevity, the following example does not show code that implements TemplateProcessor
directly; rather, it implements a useful subinterface of TemplateProcessor
, namely StringProcessor
, that returns a String
.
StringProcessor INTER = (StringTemplate st) -> {
String placeHolder = "•";
String stencil = String.join(placeHolder, fragments);
for (Object value : st.values()) {
String v = String.valueOf(value);
stencil = stencil.replaceFirst(placeHolder, v);
}
return stencil;
};
int x = 10, y = 20;
String s = INTER."{x} plus {y} equals {x + y}";
| 10 plus 20 equals 30
A template processor always executes at run time, never at compile time. It is not possible for a template processor to perform compile-time processing on the template. Moreover, it is not possible for a template processor to obtain, from a StringTemplate
,the exact characters which appear in a template in source code; only the values of the embedded expressions are available, not the embedded expressions themselves.
Efficient template processors
We can make the interpolating template processor shown earlier more efficient by building up a result from fragments and values, taking advantage of the fact that every template represents an alternating sequence of fragments and values:
StringProcessor INTER = (StringTemplate st) -> {
StringBuilder sb = new StringBuilder();
Iterator fragIter = st.fragments().iterator();
for (Object value : st.values()) {
sb.append(fragIter.next());
sb.append(value);
}
sb.append(fragIter.next());
return sb.toString();
};
int x = 10, y = 20;
String s = INTER."{x} plus {y} equals {x + y}";
| 10 and 20 equals 30
The auxiliary method StringTemplate::interpolate
does the same thing as the code above, successively concatenating fragments and values, thus:
StringProcessor INTER = StringTemplate::interpolate;
Given that the values of embedded expressions are usually unpredictable, it is generally not worthwhile for a template processor to intern the String
that it produces. For example, STR
does not intern its result. However, it is straightforward to create an interning, interpolating template processor if needed:
StringProcessor INTERN = st -> st.interpolate().intern();
Run-time validation
All the examples so far have created template processors that implement the StringProcessor
interface. Such template processors always return a String
and perform no validation at run time, so template expressions which use them will always evaluate successfully.
In contrast, a template processor that implements the ValidatingProcessor
interface is fully general: It may return objects of any type, not just String
, and it may validate the string template and the values of embedded expressions, throwing a checked or unchecked exception if validation fails. By throwing a checked exception, the template processor forces developers who use it in template expressions to handle invalid string composition with a try-catch
statement.
The relationship between StringProcessor
and ValidatingProcessor
is as follows:
public interface ValidatingProcessor {
R process(StringTemplate stringTemplate) throws E;
}
public interface TemplateProcessor extends ValidatingProcessor {}
public interface StringProcessor extends TemplateProcessor {}
Note that the second type parameter of ValidatingProcessor
, E
, is the type of the exception thrown by the process
method. A non-validating template processor will usually be declared to supply RuntimeException
as the type argument for E
. This allows developers to use the processor in template expressions without try-catch
statements. For convenience, ValidatingProcessor
has a subinterface, TemplateProcessor
, which already supplies RuntimeException
as the type argument.
Here is an example of a template processor, JSON
. It implements
TemplateProcessor
and therefore returns instances of JSONObject
. It uses
the auxiliary method StringTemplate::interpolate
, shown earlier.
TemplateProcessor JSON = (StringTemplate st) -> new JSONObject(st.interpolate());
String name = "Joan Smith";
String phone = "555-123-4567";
String address = "1 Maple Drive, Anytown";
JSONObject doc = JSON."""
{
"name": "{name}",
"phone": "{phone}",
"address": "{address}"
};
""";
A developer who uses the JSON
template processor never sees the String
produced by st.interpolate()
. Moreover, since the text block template is constant, a more advanced template processor could compile the template into a JSONObject
with placeholder values, cache that result, and then at each evaluation inject the field values into a fresh deep copy of that cached JSONObject
. There would be no intermediate String
anywhere.
A template processor that throws checked exceptions must implement ValidatingProcessor
directly. For example, here is a template processor that expects a JSON document to be surrounded by { }
, throwing a checked exception at run time otherwise:
class JSONException extends Exception {}
ValidatingProcessor JSON_VALIDATE = (StringTemplate st) -> {
String stripped = st.interpolate().strip();
if (!stripped.startsWith("{") || !stripped.endsWith("}")) {
throws new JSONException("Missing brace");
}
return new JSONObject(stripped);
};
String name = "Joan Smith";
String phone = "555-123-4567";
String address = "1 Maple Drive, Anytown";
try {
JSONObject doc = JSON_VALIDATE."""
{
"name": "{name}",
"phone": "{phone}",
"address": "{address}"
};
""";
} catch (JSONException ex) {
...
}
Safely composing and executing database queries
The template processor below, QueryProcessor
, first creates a query string from a string template. It then creates a PreparedStatement
from that query string and sets its parameters to the values of the embedded expressions. Finally, the processor executes the PreparedStatement
to obtain a ResultSet
.
record QueryProcessor(Connection conn)
implements ValidatingProcessor {
public ResultSet 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));
}
}
// 4. Execute the PreparedStatement, returning a ResultSet
return ps.executeQuery();
}
}
If we instantiate the QueryProcessor
for a specific Connection
:
ValidatingProcessor DB = new QueryProcessor(...);
then instead of the unsafe, injection-attack-prone code
String query = "SELECT * FROM Person p WHERE p.last_name = '" + name + "'";
ResultSet rs = conn.createStatement().executeQuery(query);
we can write the more secure and more readable code
ResultSet rs = DB."SELECT * FROM Person p WHERE p.last_name = {name}";
Localized formatter processors
FMT
, shown earlier, is an instance of the format-aware template processor class java.util.FormatProcessor
. That processor uses the default locale, but it is straightforward to create a variant for a different locale. For example, this code creates a format-aware template processor for the Thai locale, and stores it in the THAI
variable:
Locale thaiLocale = Locale.forLanguageTag("th-TH-u-nu-thai");
FormatProcessor THAI = new FormatProcessor(thaiLocale);
for (int i = 1; i <= 10000; i *= 10) {
String s = THAI."This answer is %5d{i}";
System.out.println(s);
}
| This answer is ๑
| This answer is ๑๐
| This answer is ๑๐๐
| This answer is ๑๐๐๐
| This answer is ๑๐๐๐๐
Simplifying message localization
Here is a template processor that simplifies working with resource bundles. For a given locale, it maps a string to a corresponding property in a resource bundle:
record LocalizationProcessor(Locale locale) implements StringProcessor {
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());
}
}
Assuming there is a property-file resource bundle for each locale:
# resources_en_CA.properties file
no.suitable._.found.for._(_)=
no suitable {0} found for {1}({2})
# resources_zh_CN.properties file
no.suitable._.found.for._(_)=
u5BF9u4E8E{1}({2}), u627Eu4E0Du5230u5408u9002u7684{0}
# resources_jp.properties file
no.suitable._.found.for._(_)=
{1}u306Bu9069u5207u306A{0}u304Cu898Bu3064u304Bu308Au307Eu305Bu3093({2})
then a program can compose a localized string based upon the property:
var userLocale = new Locale("en", "CA");
var LOCALIZE = new LocalizationProcessor(userLocale);
...
var symbolKind = "field", name = "tax", type = "double";
System.out.println(LOCALIZE."no suitable {symbolKind} found for {name}({type})");
and the template processor will map the string to the corresponding property in the locale-appropriate resource bundle:
no suitable field found for tax(double)
If the program instead performed
var userLocale = new Locale("zh", "CN");
then the output would be:
对于tax(double), 找不到合适的field
Finally, if the program instead performed
var userLocale = new Locale("jp");
then the output would be:
taxに適切なfieldが見つかりません(double)
Alternatives
-
When a string template appears without a template processor then we could simply perform basic interpolation. However, this choice would violate the safety goal. It would be too tempting to construct SQL queries using interpolation, for example, and this would in the aggregate reduce the safety of Java programs. Always requiring a template processor ensures that the developer at least recognizes the possibility of domain-specific rules in a string template.
-
The syntax of a template expression — with the template processor appearing first — is not strictly necessary. It would be possible to denote the template processor as an argument to
StringTemplate::process
. For example:String s = "The answer is %5d{i}".process(FMT);
Having the template processor appear first is preferable because the result of evaluating the template expression is entirely dependent on the operation of the template processor.
-
For the syntax of embedded expressions we considered using
${...}
, but that would require a tag on string templates (either a prefix or a delimiter other than"
) to avoid conflicts with legacy code. We also considered[...]
and(...)
, but[ ]
and( )
are likely to appear in embedded expressions;{ }
is less likely to appear, so visually determining the start and end of embedded expressions will be easier. -
It would be possible to bake format specifiers into string templates, as done in C#:
var date = DateTime.Now; Console.WriteLine($"The time is {date:HH:mm}");
but this would require changes to the Java Language Specification any time a
new format specifier is introduced.
Risks and Assumptions
The implementation of java.util.FormatProcessor
depends strongly upon java.util.Formatter
, which may require a significant rewrite.
Leave A Comment