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("&", "&") .replace("<", "<") .replace(">", ">") .replace("\"", """) .replace("'", "'"); } /** * 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("<")); assertTrue(result.contains(">")); } @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
- Type Safety: Always specify the correct exception type in processor signature
- Performance: Cache processors when possible, especially for i18n
- Security: Always validate and sanitize inputs in security-sensitive contexts
- Composition: Build complex processors by composing simpler ones
- Testing: Thoroughly test processors with various inputs and edge cases
- Documentation: Clearly document the expected template format for each processor
- 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.