Custom String Template Processors in Java

String Templates (JEP 430) are a new feature in Java that enable string interpolation and custom string processing. This comprehensive guide covers creating custom template processors for various use cases.

Understanding String Template Components

Key Components:

  • String Templates: Embedded expressions in string literals
  • Template Processors: Process templates and embedded expressions
  • STR Processor: Built-in processor for basic interpolation
  • FMT Processor: Built-in processor for formatting
  • RAW Template: Unprocessed template for custom processing

Project Setup (Java 21+)

<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- For JSON processing in examples -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.16.1</version>
</dependency>
<!-- For testing -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<compilerArgs>
<arg>--enable-preview</arg>
</compilerArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.1.2</version>
<configuration>
<argLine>--enable-preview</argLine>
</configuration>
</plugin>
</plugins>
</build>

Basic Template Processor Foundation

package com.example.templates;
import java.lang.StringTemplate;
import java.util.List;
import java.util.function.Function;
/**
* Foundation for custom template processors
*/
public class TemplateProcessorFoundation {
/**
* Basic template processor that simply interpolates values
*/
public static final StringTemplate.Processor<String, RuntimeException> SIMPLE = 
StringTemplate.Processor.of((StringTemplate st) -> {
StringBuilder sb = new StringBuilder();
List<String> fragments = st.fragments();
List<Object> values = st.values();
for (int i = 0; i < fragments.size(); i++) {
sb.append(fragments.get(i));
if (i < values.size()) {
sb.append(values.get(i));
}
}
return sb.toString();
});
/**
* Template processor that applies a transformation to each value
*/
public static StringTemplate.Processor<String, RuntimeException> createTransformingProcessor(
Function<Object, String> transformer) {
return StringTemplate.Processor.of((StringTemplate st) -> {
StringBuilder sb = new StringBuilder();
List<String> fragments = st.fragments();
List<Object> values = st.values();
for (int i = 0; i < fragments.size(); i++) {
sb.append(fragments.get(i));
if (i < values.size()) {
sb.append(transformer.apply(values.get(i)));
}
}
return sb.toString();
});
}
/**
* Processor that validates template structure
*/
public static final StringTemplate.Processor<String, IllegalArgumentException> VALIDATING = 
StringTemplate.Processor.of((StringTemplate st) -> {
List<String> fragments = st.fragments();
List<Object> values = st.values();
// Validate that we have exactly one more fragment than values
if (fragments.size() != values.size() + 1) {
throw new IllegalArgumentException(
"Invalid template structure: expected " + (values.size() + 1) + 
" fragments, got " + fragments.size());
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < fragments.size(); i++) {
sb.append(fragments.get(i));
if (i < values.size()) {
sb.append(values.get(i));
}
}
return sb.toString();
});
}

Security-Focused Template Processors

package com.example.templates;
import java.lang.StringTemplate;
import java.util.List;
import java.util.regex.Pattern;
/**
* Security-focused template processors for safe string interpolation
*/
public class SecurityProcessors {
// Patterns for validation
private static final Pattern SQL_INJECTION_PATTERN = 
Pattern.compile("([';]+|(--)+|(\\/\\*)+|(\\*\\/)+|(xp_)+)", Pattern.CASE_INSENSITIVE);
private static final Pattern HTML_SPECIAL_CHARS = 
Pattern.compile("[<>&\"']");
private static final Pattern PATH_TRAVERSAL_PATTERN = 
Pattern.compile("\\.\\.|~|\\/\\/|\\\\");
/**
* SQL-safe template processor that prevents SQL injection
*/
public static final StringTemplate.Processor<String, SecurityException> SQL_SAFE = 
StringTemplate.Processor.of((StringTemplate st) -> {
StringBuilder sb = new StringBuilder();
List<String> fragments = st.fragments();
List<Object> values = st.values();
for (int i = 0; i < fragments.size(); i++) {
sb.append(fragments.get(i));
if (i < values.size()) {
String value = String.valueOf(values.get(i));
// Validate for SQL injection patterns
if (SQL_INJECTION_PATTERN.matcher(value).find()) {
throw new SecurityException(
"Potential SQL injection detected in value: " + value);
}
// Escape single quotes for SQL safety
String safeValue = value.replace("'", "''");
sb.append(safeValue);
}
}
return sb.toString();
});
/**
* HTML-safe template processor that escapes HTML special characters
*/
public static final StringTemplate.Processor<String, RuntimeException> HTML_SAFE = 
StringTemplate.Processor.of((StringTemplate st) -> {
StringBuilder sb = new StringBuilder();
List<String> fragments = st.fragments();
List<Object> values = st.values();
for (int i = 0; i < fragments.size(); i++) {
sb.append(fragments.get(i));
if (i < values.size()) {
String value = String.valueOf(values.get(i));
String escapedValue = escapeHtml(value);
sb.append(escapedValue);
}
}
return sb.toString();
});
/**
* Path-safe template processor for file system operations
*/
public static final StringTemplate.Processor<String, SecurityException> PATH_SAFE = 
StringTemplate.Processor.of((StringTemplate st) -> {
StringBuilder sb = new StringBuilder();
List<String> fragments = st.fragments();
List<Object> values = st.values();
for (int i = 0; i < fragments.size(); i++) {
sb.append(fragments.get(i));
if (i < values.size()) {
String value = String.valueOf(values.get(i));
// Validate for path traversal attempts
if (PATH_TRAVERSAL_PATTERN.matcher(value).find()) {
throw new SecurityException(
"Potential path traversal detected in value: " + value);
}
sb.append(value);
}
}
return sb.toString();
});
/**
* Creates a processor with custom validation rules
*/
public static StringTemplate.Processor<String, SecurityException> createValidatingProcessor(
Pattern forbiddenPattern, String validationName) {
return StringTemplate.Processor.of((StringTemplate st) -> {
StringBuilder sb = new StringBuilder();
List<String> fragments = st.fragments();
List<Object> values = st.values();
for (int i = 0; i < fragments.size(); i++) {
sb.append(fragments.get(i));
if (i < values.size()) {
String value = String.valueOf(values.get(i));
// Apply custom validation
if (forbiddenPattern.matcher(value).find()) {
throw new SecurityException(
String.format("Validation failed for %s in value: %s", 
validationName, value));
}
sb.append(value);
}
}
return sb.toString();
});
}
private static String escapeHtml(String input) {
return input.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\"", "&quot;")
.replace("'", "&#39;");
}
/**
* Sanitizes input for logging (removes sensitive data)
*/
public static final StringTemplate.Processor<String, RuntimeException> LOG_SAFE = 
StringTemplate.Processor.of((StringTemplate st) -> {
StringBuilder sb = new StringBuilder();
List<String> fragments = st.fragments();
List<Object> values = st.values();
for (int i = 0; i < fragments.size(); i++) {
sb.append(fragments.get(i));
if (i < values.size()) {
Object value = values.get(i);
String safeValue = sanitizeForLogging(value);
sb.append(safeValue);
}
}
return sb.toString();
});
private static String sanitizeForLogging(Object value) {
if (value == null) return "null";
String str = String.valueOf(value);
// Mask potential sensitive data
if (str.matches(".*[pP]assword.*") || str.matches(".*[kK]ey.*")) {
return "***MASKED***";
}
// Mask credit card-like numbers
if (str.matches("\\d{13,19}")) {
return str.substring(0, 4) + "****" + str.substring(str.length() - 4);
}
// Mask email addresses
if (str.matches("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$")) {
String[] parts = str.split("@");
if (parts[0].length() > 2) {
return parts[0].substring(0, 2) + "***@" + parts[1];
}
return "***@" + parts[1];
}
return str;
}
}

JSON Template Processors

package com.example.templates;
import java.lang.StringTemplate;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* Template processors for JSON generation and manipulation
*/
public class JsonProcessors {
private static final ObjectMapper objectMapper = new ObjectMapper();
/**
* Creates a JSON object from template
* Template format: "{"name": \{name}, "age": \{age}}"
*/
public static final StringTemplate.Processor<String, RuntimeException> JSON = 
StringTemplate.Processor.of((StringTemplate st) -> {
// For simple cases, we can process the template as JSON
StringBuilder sb = new StringBuilder();
List<String> fragments = st.fragments();
List<Object> values = st.values();
for (int i = 0; i < fragments.size(); i++) {
String fragment = fragments.get(i);
sb.append(fragment);
if (i < values.size()) {
Object value = values.get(i);
String jsonValue = toJsonValue(value);
sb.append(jsonValue);
}
}
// Validate JSON format
try {
objectMapper.readTree(sb.toString());
return sb.toString();
} catch (Exception e) {
throw new RuntimeException("Invalid JSON generated: " + sb, e);
}
});
/**
* Creates a JSON array from multiple values
*/
public static final StringTemplate.Processor<String, RuntimeException> JSON_ARRAY = 
StringTemplate.Processor.of((StringTemplate st) -> {
StringBuilder sb = new StringBuilder("[");
List<Object> values = st.values();
for (int i = 0; i < values.size(); i++) {
if (i > 0) {
sb.append(", ");
}
String jsonValue = toJsonValue(values.get(i));
sb.append(jsonValue);
}
sb.append("]");
return sb.toString();
});
/**
* Creates a parameterized JSON template processor
*/
public static StringTemplate.Processor<Map<String, Object>, RuntimeException> JSON_OBJECT = 
StringTemplate.Processor.of((StringTemplate st) -> {
Map<String, Object> result = new HashMap<>();
List<String> fragments = st.fragments();
List<Object> values = st.values();
// Extract key-value pairs from template
for (int i = 0; i < fragments.size() - 1; i++) {
String fragment = fragments.get(i);
// Extract key from fragment (simplified approach)
String key = extractJsonKey(fragment);
if (key != null && i < values.size()) {
result.put(key, values.get(i));
}
}
return result;
});
/**
* JSON processor with pretty printing
*/
public static final StringTemplate.Processor<String, RuntimeException> JSON_PRETTY = 
StringTemplate.Processor.of((StringTemplate st) -> {
// First create compact JSON
String compactJson = JSON.process(st);
// Then pretty print it
try {
Object json = objectMapper.readValue(compactJson, Object.class);
return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(json);
} catch (Exception e) {
throw new RuntimeException("Failed to pretty print JSON", e);
}
});
private static String toJsonValue(Object value) {
if (value == null) {
return "null";
} else if (value instanceof String) {
return "\"" + escapeJsonString((String) value) + "\"";
} else if (value instanceof Number) {
return value.toString();
} else if (value instanceof Boolean) {
return value.toString();
} else {
// For complex objects, serialize to JSON
try {
return objectMapper.writeValueAsString(value);
} catch (Exception e) {
throw new RuntimeException("Failed to serialize value to JSON: " + value, e);
}
}
}
private static String escapeJsonString(String input) {
return input.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\b", "\\b")
.replace("\f", "\\f")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\t", "\\t");
}
private static String extractJsonKey(String fragment) {
// Simple extraction - looks for "key": pattern
int quoteIndex = fragment.lastIndexOf('"');
if (quoteIndex > 0) {
int colonIndex = fragment.indexOf(':', quoteIndex);
if (colonIndex > 0) {
int startQuote = fragment.lastIndexOf('"', quoteIndex - 1);
if (startQuote >= 0) {
return fragment.substring(startQuote + 1, quoteIndex);
}
}
}
return null;
}
}

Database and SQL Template Processors

package com.example.templates;
import java.lang.StringTemplate;
import java.util.List;
import java.util.ArrayList;
/**
* Template processors for database and SQL operations
*/
public class DatabaseProcessors {
/**
* SQL query processor with parameter placeholder generation
* Converts template to prepared statement format
*/
public static final StringTemplate.Processor<PreparedStatementTemplate, RuntimeException> SQL = 
StringTemplate.Processor.of((StringTemplate st) -> {
StringBuilder query = new StringBuilder();
List<Object> parameters = new ArrayList<>();
List<String> fragments = st.fragments();
List<Object> values = st.values();
for (int i = 0; i < fragments.size(); i++) {
query.append(fragments.get(i));
if (i < values.size()) {
query.append("?");
parameters.add(values.get(i));
}
}
return new PreparedStatementTemplate(query.toString(), parameters);
});
/**
* SQL IN clause processor for dynamic lists
*/
public static final StringTemplate.Processor<PreparedStatementTemplate, RuntimeException> SQL_IN = 
StringTemplate.Processor.of((StringTemplate st) -> {
if (st.values().size() != 1) {
throw new IllegalArgumentException("SQL_IN processor expects exactly one value (a collection)");
}
Object value = st.values().get(0);
if (!(value instanceof Iterable)) {
throw new IllegalArgumentException("SQL_IN processor expects an Iterable value");
}
StringBuilder query = new StringBuilder();
List<Object> parameters = new ArrayList<>();
List<String> fragments = st.fragments();
// Build IN clause with placeholders
query.append(fragments.get(0));
Iterable<?> iterable = (Iterable<?>) value;
List<String> placeholders = new ArrayList<>();
for (Object item : iterable) {
placeholders.add("?");
parameters.add(item);
}
query.append(String.join(", ", placeholders));
if (fragments.size() > 1) {
query.append(fragments.get(1));
}
return new PreparedStatementTemplate(query.toString(), parameters);
});
/**
* Record representing a prepared statement template
*/
public static record PreparedStatementTemplate(String query, List<Object> parameters) {
public String getQuery() {
return query;
}
public List<Object> getParameters() {
return parameters;
}
@Override
public String toString() {
return "Query: " + query + ", Parameters: " + parameters;
}
}
/**
* SQL UPDATE statement processor
*/
public static final StringTemplate.Processor<PreparedStatementTemplate, RuntimeException> SQL_UPDATE = 
StringTemplate.Processor.of((StringTemplate st) -> {
List<String> fragments = st.fragments();
List<Object> values = st.values();
if (fragments.size() < 2 || values.size() % 2 != 0) {
throw new IllegalArgumentException(
"SQL_UPDATE expects template format: UPDATE table SET \{field1} = \{value1}, \{field2} = \{value2} WHERE \{condition}");
}
StringBuilder query = new StringBuilder();
List<Object> parameters = new ArrayList<>();
query.append(fragments.get(0)); // "UPDATE table SET "
// Process field-value pairs
for (int i = 0; i < values.size() - 1; i += 2) {
if (i > 0) {
query.append(", ");
}
query.append(values.get(i)).append(" = ?");
parameters.add(values.get(i + 1));
}
// Add WHERE clause
query.append(fragments.get(1)); // " WHERE "
query.append("?");
parameters.add(values.get(values.size() - 1));
return new PreparedStatementTemplate(query.toString(), parameters);
});
}

Logging Template Processors

package com.example.templates;
import java.lang.StringTemplate;
import java.util.List;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* Template processors for structured logging
*/
public class LoggingProcessors {
private static final DateTimeFormatter TIMESTAMP_FORMATTER = 
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
/**
* Structured log processor with timestamp and level
*/
public static StringTemplate.Processor<String, RuntimeException> createLogger(String level) {
return StringTemplate.Processor.of((StringTemplate st) -> {
String timestamp = LocalDateTime.now().format(TIMESTAMP_FORMATTER);
String message = StringTemplate.Processor.string().process(st);
return String.format("[%s] [%s] %s", timestamp, level, message);
});
}
// Predefined log level processors
public static final StringTemplate.Processor<String, RuntimeException> LOG_ERROR = 
createLogger("ERROR");
public static final StringTemplate.Processor<String, RuntimeException> LOG_WARN = 
createLogger("WARN");
public static final StringTemplate.Processor<String, RuntimeException> LOG_INFO = 
createLogger("INFO");
public static final StringTemplate.Processor<String, RuntimeException> LOG_DEBUG = 
createLogger("DEBUG");
public static final StringTemplate.Processor<String, RuntimeException> LOG_TRACE = 
createLogger("TRACE");
/**
* JSON log processor for structured logging
*/
public static final StringTemplate.Processor<String, RuntimeException> JSON_LOG = 
StringTemplate.Processor.of((StringTemplate st) -> {
StringBuilder logEntry = new StringBuilder();
List<String> fragments = st.fragments();
List<Object> values = st.values();
// Build a structured log entry
logEntry.append("{");
logEntry.append("\"timestamp\": \"").append(LocalDateTime.now().format(TIMESTAMP_FORMATTER)).append("\"");
logEntry.append(", \"message\": \"");
// Reconstruct the message
for (int i = 0; i < fragments.size(); i++) {
logEntry.append(fragments.get(i));
if (i < values.size()) {
logEntry.append("{}"); // Placeholder for structured logging
}
}
logEntry.append("\"");
// Add values as separate fields
for (int i = 0; i < values.size(); i++) {
logEntry.append(", \"param").append(i).append("\": ");
String jsonValue = toJsonLogValue(values.get(i));
logEntry.append(jsonValue);
}
logEntry.append("}");
return logEntry.toString();
});
/**
* Performance logging processor with timing
*/
public static final StringTemplate.Processor<String, RuntimeException> PERF_LOG = 
StringTemplate.Processor.of((StringTemplate st) -> {
long startTime = System.currentTimeMillis();
String message = StringTemplate.Processor.string().process(st);
long duration = System.currentTimeMillis() - startTime;
return String.format("[PERF] %s (took %d ms)", message, duration);
});
private static String toJsonLogValue(Object value) {
if (value == null) return "null";
if (value instanceof String) return "\"" + escapeJsonString((String) value) + "\"";
if (value instanceof Number) return value.toString();
if (value instanceof Boolean) return value.toString();
return "\"" + String.valueOf(value).replace("\"", "\\\"") + "\"";
}
private static String escapeJsonString(String input) {
return input.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\t", "\\t");
}
}

Internationalization (i18n) Processors

package com.example.templates;
import java.lang.StringTemplate;
import java.util.List;
import java.util.Locale;
import java.util.ResourceBundle;
import java.text.MessageFormat;
/**
* Template processors for internationalization and localization
*/
public class I18nProcessors {
/**
* Creates a localized message processor for a specific locale
*/
public static StringTemplate.Processor<String, RuntimeException> createLocalizedProcessor(
String baseName, Locale locale) {
return StringTemplate.Processor.of((StringTemplate st) -> {
ResourceBundle bundle = ResourceBundle.getBundle(baseName, locale);
List<String> fragments = st.fragments();
// Use the first fragment as the message key
if (fragments.isEmpty()) {
throw new IllegalArgumentException("Template must start with a message key");
}
String messageKey = fragments.get(0).trim();
String pattern = bundle.getString(messageKey);
// Use MessageFormat for parameter substitution
Object[] params = st.values().toArray();
return MessageFormat.format(pattern, params);
});
}
/**
* Currency formatting processor
*/
public static StringTemplate.Processor<String, RuntimeException> createCurrencyProcessor(Locale locale) {
return StringTemplate.Processor.of((StringTemplate st) -> {
java.text.NumberFormat currencyFormat = java.text.NumberFormat.getCurrencyInstance(locale);
StringBuilder result = new StringBuilder();
List<String> fragments = st.fragments();
List<Object> values = st.values();
for (int i = 0; i < fragments.size(); i++) {
result.append(fragments.get(i));
if (i < values.size()) {
Object value = values.get(i);
if (value instanceof Number) {
result.append(currencyFormat.format(value));
} else {
result.append(value);
}
}
}
return result.toString();
});
}
/**
* Date formatting processor
*/
public static StringTemplate.Processor<String, RuntimeException> createDateProcessor(Locale locale) {
return StringTemplate.Processor.of((StringTemplate st) -> {
java.text.DateFormat dateFormat = java.text.DateFormat.getDateInstance(
java.text.DateFormat.MEDIUM, locale);
StringBuilder result = new StringBuilder();
List<String> fragments = st.fragments();
List<Object> values = st.values();
for (int i = 0; i < fragments.size(); i++) {
result.append(fragments.get(i));
if (i < values.size()) {
Object value = values.get(i);
if (value instanceof java.util.Date) {
result.append(dateFormat.format(value));
} else {
result.append(value);
}
}
}
return result.toString();
});
}
}

Advanced Composite Processors

package com.example.templates;
import java.lang.StringTemplate;
import java.util.List;
import java.util.function.Function;
/**
* Advanced template processors with composition and transformation
*/
public class AdvancedProcessors {
/**
* Processor that composes multiple processors
*/
@SafeVarargs
public static StringTemplate.Processor<String, RuntimeException> compose(
Function<String, String>... transformers) {
return StringTemplate.Processor.of((StringTemplate st) -> {
String result = StringTemplate.Processor.string().process(st);
for (Function<String, String> transformer : transformers) {
result = transformer.apply(result);
}
return result;
});
}
/**
* Processor that conditionally processes templates
*/
public static StringTemplate.Processor<String, RuntimeException> conditional(
Function<StringTemplate, Boolean> condition,
StringTemplate.Processor<String, RuntimeException> trueProcessor,
StringTemplate.Processor<String, RuntimeException> falseProcessor) {
return StringTemplate.Processor.of((StringTemplate st) -> {
if (condition.apply(st)) {
return trueProcessor.process(st);
} else {
return falseProcessor.process(st);
}
});
}
/**
* Processor that caches template processing results
*/
public static StringTemplate.Processor<String, RuntimeException> cached() {
return new StringTemplate.Processor<String, RuntimeException>() {
private final java.util.Map<String, String> cache = new java.util.concurrent.ConcurrentHashMap<>();
@Override
public String process(StringTemplate st) {
String templateKey = createTemplateKey(st);
return cache.computeIfAbsent(templateKey, k -> 
StringTemplate.Processor.string().process(st));
}
private String createTemplateKey(StringTemplate st) {
StringBuilder key = new StringBuilder();
List<String> fragments = st.fragments();
List<Object> values = st.values();
for (int i = 0; i < fragments.size(); i++) {
key.append(fragments.get(i));
if (i < values.size()) {
key.append("_").append(values.get(i).getClass().getSimpleName());
}
}
return key.toString();
}
};
}
/**
* Processor that measures processing time
*/
public static StringTemplate.Processor<TimedResult, RuntimeException> timed() {
return StringTemplate.Processor.of((StringTemplate st) -> {
long startTime = System.nanoTime();
String result = StringTemplate.Processor.string().process(st);
long duration = System.nanoTime() - startTime;
return new TimedResult(result, duration);
});
}
/**
* Processor that retries on failure
*/
public static StringTemplate.Processor<String, RuntimeException> withRetry(int maxRetries) {
return StringTemplate.Processor.of((StringTemplate st) -> {
RuntimeException lastException = null;
for (int attempt = 0; attempt < maxRetries; attempt++) {
try {
return StringTemplate.Processor.string().process(st);
} catch (RuntimeException e) {
lastException = e;
if (attempt < maxRetries - 1) {
try {
Thread.sleep(100 * (attempt + 1)); // Exponential backoff
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new RuntimeException("Interrupted during retry", ie);
}
}
}
}
throw new RuntimeException("Failed after " + maxRetries + " attempts", lastException);
});
}
public static record TimedResult(String result, long durationNanos) {
public long getDurationMillis() {
return durationNanos / 1_000_000;
}
@Override
public String toString() {
return String.format("Result: %s (took %d ns)", result, durationNanos);
}
}
}

Practical Usage Examples

package com.example.templates;
import java.lang.StringTemplate;
import java.util.List;
import java.util.Locale;
/**
* Practical examples demonstrating custom template processors
*/
public class TemplateExamples {
public static void main(String[] args) {
demonstrateSecurityProcessors();
demonstrateJsonProcessors();
demonstrateDatabaseProcessors();
demonstrateLoggingProcessors();
demonstrateI18nProcessors();
demonstrateAdvancedProcessors();
}
private static void demonstrateSecurityProcessors() {
System.out.println("=== Security Processors ===");
String userInput = "John'; DROP TABLE users; --";
String name = "John";
try {
// This will throw SecurityException
String sql = SecurityProcessors.SQL_SAFE."SELECT * FROM users WHERE name = '\{userInput}'";
} catch (SecurityException e) {
System.out.println("SQL injection prevented: " + e.getMessage());
}
// Safe usage
String safeSql = SecurityProcessors.SQL_SAFE."SELECT * FROM users WHERE name = '\{name}'";
System.out.println("Safe SQL: " + safeSql);
// HTML escaping
String htmlContent = "<script>alert('XSS')</script>";
String safeHtml = SecurityProcessors.HTML_SAFE."<div>\{htmlContent}</div>";
System.out.println("Safe HTML: " + safeHtml);
// Log sanitization
String password = "mySecretPassword123";
String creditCard = "4111111111111111";
String safeLog = SecurityProcessors.LOG_SAFE."User login: password=\{password}, card=\{creditCard}";
System.out.println("Safe log: " + safeLog);
}
private static void demonstrateJsonProcessors() {
System.out.println("\n=== JSON Processors ===");
String name = "Alice";
int age = 30;
boolean active = true;
// Simple JSON object
String json = JsonProcessors.JSON."""
{"name": "\{name}", "age": \{age}, "active": \{active}}""";
System.out.println("JSON: " + json);
// JSON array
List<String> hobbies = List.of("reading", "gaming", "hiking");
String jsonArray = JsonProcessors.JSON_ARRAY."\{hobbies}";
System.out.println("JSON Array: " + jsonArray);
// Pretty JSON
String prettyJson = JsonProcessors.JSON_PRETTY."""
{"user": {"name": "\{name}", "profile": {"age": \{age}, "active": \{active}}}}""";
System.out.println("Pretty JSON:\n" + prettyJson);
}
private static void demonstrateDatabaseProcessors() {
System.out.println("\n=== Database Processors ===");
String tableName = "users";
String userName = "Alice";
int minAge = 25;
// SQL with parameters
var preparedStmt = DatabaseProcessors.SQL."""
SELECT * FROM \{tableName} 
WHERE name = '\{userName}' AND age > \{minAge}""";
System.out.println("Prepared Statement: " + preparedStmt);
// SQL IN clause
List<Integer> ids = List.of(1, 2, 3, 4, 5);
var inClauseStmt = DatabaseProcessors.SQL_IN."""
SELECT * FROM users WHERE id IN (\{ids})""";
System.out.println("IN Clause Statement: " + inClauseStmt);
}
private static void demonstrateLoggingProcessors() {
System.out.println("\n=== Logging Processors ===");
String userId = "user123";
String action = "login";
int attemptCount = 3;
// Different log levels
String errorLog = LoggingProcessors.LOG_ERROR."User \{userId} failed to \{action} after \{attemptCount} attempts";
String infoLog = LoggingProcessors.LOG_INFO."User \{userId} performed action: \{action}";
System.out.println(errorLog);
System.out.println(infoLog);
// JSON logging
String jsonLog = LoggingProcessors.JSON_LOG."User \{userId} performed \{action}";
System.out.println("JSON Log: " + jsonLog);
// Performance logging
String perfLog = LoggingProcessors.PERF_LOG."Processing user \{userId}";
System.out.println(perfLog);
}
private static void demonstrateI18nProcessors() {
System.out.println("\n=== I18n Processors ===");
// Note: This requires actual resource bundle files
// For demonstration, we'll show the concept
String product = "Java";
double price = 99.99;
int quantity = 5;
// Currency formatting
var usCurrency = I18nProcessors.createCurrencyProcessor(Locale.US);
String priceDisplay = usCurrency."Total: \{price}";
System.out.println("US Price: " + priceDisplay);
var jpCurrency = I18nProcessors.createCurrencyProcessor(Locale.JAPAN);
String jpPriceDisplay = jpCurrency."Total: \{price}";
System.out.println("JP Price: " + jpPriceDisplay);
}
private static void demonstrateAdvancedProcessors() {
System.out.println("\n=== Advanced Processors ===");
String data = "important data";
// Timed processing
var timedResult = AdvancedProcessors.timed()."Processing: \{data}";
System.out.println("Timed result: " + timedResult);
// Cached processing
var cachedProcessor = AdvancedProcessors.cached();
String result1 = cachedProcessor."Hello \{data}";
String result2 = cachedProcessor."Hello \{data}"; // Should be cached
System.out.println("Cached results equal: " + result1.equals(result2));
// Conditional processing
var conditionalProcessor = AdvancedProcessors.conditional(
st -> st.values().size() > 2,
LoggingProcessors.LOG_INFO,
LoggingProcessors.LOG_DEBUG
);
String conditionalResult = conditionalProcessor."Testing: \{data}";
System.out.println("Conditional: " + conditionalResult);
}
}

Testing Custom Template Processors

package com.example.templates;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
/**
* Unit tests for custom template processors
*/
class TemplateProcessorsTest {
@Test
void testSecurityProcessors() {
// Test SQL injection prevention
assertThrows(SecurityException.class, () -> {
SecurityProcessors.SQL_SAFE."SELECT * FROM users WHERE name = '\{"; DROP TABLE users; --}'";
});
// Test HTML escaping
String result = SecurityProcessors.HTML_SAFE."<div>\{"<script>alert('XSS')</script>"}</div>";
assertTrue(result.contains("&lt;"));
assertTrue(result.contains("&gt;"));
}
@Test
void testJsonProcessors() {
String name = "Test";
int value = 42;
String json = JsonProcessors.JSON."{\"name\": \"\{name}\", \"value\": \{value}}";
assertTrue(json.contains("\"name\": \"Test\""));
assertTrue(json.contains("\"value\": 42"));
}
@Test
void testDatabaseProcessors() {
var result = DatabaseProcessors.SQL."SELECT * FROM \{ "users" } WHERE id = \{ 123 }";
assertEquals("SELECT * FROM users WHERE id = ?", result.query());
assertEquals(List.of("users", 123), result.parameters());
}
@Test
void testLoggingProcessors() {
String log = LoggingProcessors.LOG_INFO."User \{ "testuser" } logged in";
assertTrue(log.contains("INFO"));
assertTrue(log.contains("testuser"));
}
}

Best Practices

  1. Type Safety: Always specify the correct exception type in processor signature
  2. Performance: Cache processors when possible, especially for i18n
  3. Security: Always validate and sanitize inputs in security-sensitive contexts
  4. Composition: Build complex processors by composing simpler ones
  5. Testing: Thoroughly test processors with various inputs and edge cases
  6. Documentation: Clearly document the expected template format for each processor
  7. Error Handling: Provide meaningful error messages for template validation failures

Conclusion

Custom String Template Processors in Java provide a powerful mechanism for:

  • Type-Safe String Processing: Compile-time validation of template usage
  • Domain-Specific Languages: Create processors for specific domains (SQL, JSON, etc.)
  • Security: Built-in protection against injection attacks
  • Internationalization: Simplified localization of application strings
  • Performance: Optimized processing for specific use cases
  • Composability: Combine processors for complex transformations

By leveraging custom template processors, you can create more secure, maintainable, and expressive string processing code while maintaining Java's type safety and performance characteristics.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper