@@ -67,6 +67,8 @@ public class StTemplateRenderer implements TemplateRenderer {
6767
6868 private static final boolean DEFAULT_VALIDATE_ST_FUNCTIONS = false ;
6969
70+ private static final boolean DEFAULT_KEEP_MISSING_VARIABLES = false ;
71+
7072 private final char startDelimiterToken ;
7173
7274 private final char endDelimiterToken ;
@@ -75,6 +77,8 @@ public class StTemplateRenderer implements TemplateRenderer {
7577
7678 private final boolean validateStFunctions ;
7779
80+ private final boolean keepMissingVariables ;
81+
7882 /**
7983 * Constructs a new {@code StTemplateRenderer} with the specified delimiter tokens,
8084 * validation mode, and function validation flag.
@@ -88,12 +92,13 @@ public class StTemplateRenderer implements TemplateRenderer {
8892 * template
8993 */
9094 public StTemplateRenderer (char startDelimiterToken , char endDelimiterToken , ValidationMode validationMode ,
91- boolean validateStFunctions ) {
95+ boolean validateStFunctions , boolean keepMissingVariables ) {
9296 Assert .notNull (validationMode , "validationMode cannot be null" );
9397 this .startDelimiterToken = startDelimiterToken ;
9498 this .endDelimiterToken = endDelimiterToken ;
9599 this .validationMode = validationMode ;
96100 this .validateStFunctions = validateStFunctions ;
101+ this .keepMissingVariables = keepMissingVariables ;
97102 }
98103
99104 @ Override
@@ -103,6 +108,17 @@ public String apply(String template, Map<String, Object> variables) {
103108 Assert .noNullElements (variables .keySet (), "variables keys cannot be null" );
104109
105110 ST st = createST (template );
111+ // If keepMissingVariables is enabled, first fill missing variables with placeholders.
112+ if (this .keepMissingVariables ) {
113+ Set <String > allVars = getInputVariables (st );
114+ Set <String > missingVars = new HashSet <>(allVars );
115+ missingVars .removeAll (variables .keySet ());
116+
117+ for (String missingVar : missingVars ) {
118+ st .add (missingVar , String .format ("%c%s%c" ,
119+ this .startDelimiterToken , missingVar , this .endDelimiterToken ));
120+ }
121+ }
106122 for (Map .Entry <String , Object > entry : variables .entrySet ()) {
107123 st .add (entry .getKey (), entry .getValue ());
108124 }
@@ -211,6 +227,8 @@ public static final class Builder {
211227
212228 private boolean validateStFunctions = DEFAULT_VALIDATE_ST_FUNCTIONS ;
213229
230+ private boolean keepMissingVariables = DEFAULT_KEEP_MISSING_VARIABLES ;
231+
214232 private Builder () {
215233 }
216234
@@ -266,14 +284,53 @@ public Builder validateStFunctions() {
266284 return this ;
267285 }
268286
287+ /**
288+ * Configures the renderer to keep missing variables in their original placeholder
289+ * form instead of letting StringTemplate render them as empty strings.
290+ *
291+ * <p>When enabled, variables that are referenced in the template but not provided in
292+ * the input map will be rendered as their placeholder text (for example:
293+ * {@code "{username}"} remains {@code "{username}"} in the final output).</p>
294+ *
295+ * <p><b>Important:</b> {@code keepMissingVariables(true)} can only be used when
296+ * {@code validationMode} is set to {@link ValidationMode#NONE}. If validation is
297+ * enabled ({@link ValidationMode#WARN} or {@link ValidationMode#THROW}), enabling
298+ * this flag will result in an {@link IllegalArgumentException} when building the
299+ * renderer.</p>
300+ *
301+ * <p>Example usage:</p>
302+ * <pre>{@code
303+ * TemplateRenderer renderer = MyStTemplateRenderer.builder()
304+ * .validationMode(ValidationMode.NONE)
305+ * .keepMissingVariables(true)
306+ * .build();
307+ *
308+ * String result = renderer.apply("Hello, {name}, today is {date}",
309+ * Map.of("name", "Alice"));
310+ * // result: "Hello, Alice, today is {date}"
311+ * }</pre>
312+ *
313+ * @param keepMissingVariables whether to keep missing variables as placeholders
314+ * (e.g. "{var}") in the rendered output
315+ * @return this builder instance for chaining
316+ */
317+ public Builder keepMissingVariables (boolean keepMissingVariables ) {
318+ this .keepMissingVariables = keepMissingVariables ;
319+ return this ;
320+ }
321+
269322 /**
270323 * Builds and returns a new {@link StTemplateRenderer} instance with the
271324 * configured settings.
272325 * @return A configured {@link StTemplateRenderer}.
273326 */
274327 public StTemplateRenderer build () {
328+ if (this .keepMissingVariables && this .validationMode != ValidationMode .NONE ) {
329+ throw new IllegalArgumentException (
330+ "keepMissingVariables can only be enabled when validationMode is NONE." );
331+ }
275332 return new StTemplateRenderer (this .startDelimiterToken , this .endDelimiterToken , this .validationMode ,
276- this .validateStFunctions );
333+ this .validateStFunctions , this . keepMissingVariables );
277334 }
278335
279336 }
0 commit comments