1616
1717package org .springframework .ai .template .st ;
1818
19+ import java .util .Collections ;
1920import java .util .HashSet ;
2021import java .util .Map ;
2122import java .util .Set ;
23+ import java .util .regex .Matcher ;
24+ import java .util .regex .Pattern ;
2225
2326import org .antlr .runtime .Token ;
2427import org .antlr .runtime .TokenStream ;
@@ -57,106 +60,139 @@ public class StTemplateRenderer implements TemplateRenderer {
5760
5861 private static final String VALIDATION_MESSAGE = "Not all variables were replaced in the template. Missing variable names are: %s." ;
5962
60- private static final char DEFAULT_START_DELIMITER_TOKEN = '{' ;
63+ private static final String DEFAULT_START_DELIMITER = "{" ;
6164
62- private static final char DEFAULT_END_DELIMITER_TOKEN = '}' ;
65+ private static final String DEFAULT_END_DELIMITER = "}" ;
6366
67+ private static final char INTERNAL_START_DELIMITER = '{' ;
68+
69+ private static final char INTERNAL_END_DELIMITER = '}' ;
70+
71+ /** Default validation mode: throw an exception if variables are missing */
6472 private static final ValidationMode DEFAULT_VALIDATION_MODE = ValidationMode .THROW ;
6573
74+ /** Default behavior: do not validate ST built-in functions */
6675 private static final boolean DEFAULT_VALIDATE_ST_FUNCTIONS = false ;
6776
68- private final char startDelimiterToken ;
77+ private final String startDelimiterToken ;
6978
70- private final char endDelimiterToken ;
79+ private final String endDelimiterToken ;
7180
7281 private final ValidationMode validationMode ;
7382
7483 private final boolean validateStFunctions ;
7584
7685 /**
77- * Constructs a new {@code StTemplateRenderer} with the specified delimiter tokens,
78- * validation mode, and function validation flag.
79- * @param startDelimiterToken the character used to denote the start of a template
80- * variable (e.g., '{')
81- * @param endDelimiterToken the character used to denote the end of a template
82- * variable (e.g., '}')
83- * @param validationMode the mode to use for template variable validation; must not be
84- * null
85- * @param validateStFunctions whether to validate StringTemplate functions in the
86- * template
86+ * Constructs a StTemplateRenderer with custom delimiters, validation mode, and
87+ * function validation flag.
88+ * @param startDelimiterToken Multi-character start delimiter (non-null/non-empty)
89+ * @param endDelimiterToken Multi-character end delimiter (non-null/non-empty)
90+ * @param validationMode Mode for handling missing variables (non-null)
91+ * @param validateStFunctions Whether to treat ST built-in functions as variables
8792 */
88- public StTemplateRenderer (char startDelimiterToken , char endDelimiterToken , ValidationMode validationMode ,
93+ public StTemplateRenderer (String startDelimiterToken , String endDelimiterToken , ValidationMode validationMode ,
8994 boolean validateStFunctions ) {
90- Assert .notNull (validationMode , "validationMode cannot be null" );
95+ Assert .notNull (validationMode , "validationMode must not be null" );
96+ Assert .hasText (startDelimiterToken , "startDelimiterToken must not be null or empty" );
97+ Assert .hasText (endDelimiterToken , "endDelimiterToken must not be null or empty" );
98+
9199 this .startDelimiterToken = startDelimiterToken ;
92100 this .endDelimiterToken = endDelimiterToken ;
93101 this .validationMode = validationMode ;
94102 this .validateStFunctions = validateStFunctions ;
95103 }
96104
105+ /**
106+ * Renders the template by first converting custom delimiters to ST's native format,
107+ * then replacing variables.
108+ * @param template Template string with variables (non-null/non-empty)
109+ * @param variables Map of variable names to values (non-null, keys must not be null)
110+ * @return Rendered string with variables replaced
111+ */
97112 @ Override
98113 public String apply (String template , Map <String , Object > variables ) {
99- Assert .hasText (template , "template cannot be null or empty" );
100- Assert .notNull (variables , "variables cannot be null" );
101- Assert .noNullElements (variables .keySet (), "variables keys cannot be null" );
114+ Assert .hasText (template , "template must not be null or empty" );
115+ Assert .notNull (variables , "variables must not be null" );
116+ Assert .noNullElements (variables .keySet (), "variables keys must not contain null" );
117+
118+ try {
119+ // Convert custom delimiters (e.g., <name>) to ST's native format ({name})
120+ String processedTemplate = preprocessTemplate (template );
121+ ST st = new ST (processedTemplate , INTERNAL_START_DELIMITER , INTERNAL_END_DELIMITER );
102122
103- ST st = createST (template );
104- for (Map .Entry <String , Object > entry : variables .entrySet ()) {
105- st .add (entry .getKey (), entry .getValue ());
123+ // Add variables to the template
124+ variables .forEach (st ::add );
125+
126+ // Validate variable completeness if enabled
127+ if (validationMode != ValidationMode .NONE ) {
128+ validate (st , variables );
129+ }
130+
131+ // Render directly (no post-processing needed)
132+ return st .render ();
106133 }
107- if (this .validationMode != ValidationMode .NONE ) {
108- validate (st , variables );
134+ catch (Exception e ) {
135+ logger .error ("Template rendering failed for template: {}" , template , e );
136+ throw new RuntimeException ("Failed to render template" , e );
109137 }
110- return st .render ();
111138 }
112139
113- private ST createST (String template ) {
114- try {
115- return new ST (template , this .startDelimiterToken , this .endDelimiterToken );
116- }
117- catch (Exception ex ) {
118- throw new IllegalArgumentException ("The template string is not valid." , ex );
119- }
140+ /**
141+ * Converts custom delimiter-wrapped variables (e.g., <name>) to ST's native format
142+ * ({name}).
143+ */
144+ private String preprocessTemplate (String template ) {
145+ // Escape special regex characters in delimiters
146+ String escapedStart = Pattern .quote (startDelimiterToken );
147+ String escapedEnd = Pattern .quote (endDelimiterToken );
148+
149+ // Regex pattern to match custom variables (e.g., <name> or {{name}})
150+ // Group 1 captures the variable name (letters, numbers, underscores)
151+ String variablePattern = escapedStart + "([a-zA-Z_][a-zA-Z0-9_]*)" + escapedEnd ;
152+
153+ // Replace with ST's native format (e.g., {name})
154+ return template .replaceAll (variablePattern , "{$1}" );
120155 }
121156
122157 /**
123- * Validates that all required template variables are provided in the model. Returns
124- * the set of missing variables for further handling or logging.
125- * @param st the StringTemplate instance
126- * @param templateVariables the provided variables
127- * @return set of missing variable names, or empty set if none are missing
158+ * Validates that all template variables have been provided in the variables map.
128159 */
129- private Set < String > validate (ST st , Map <String , Object > templateVariables ) {
160+ private void validate (ST st , Map <String , Object > templateVariables ) {
130161 Set <String > templateTokens = getInputVariables (st );
131- Set <String > modelKeys = templateVariables != null ? templateVariables .keySet () : new HashSet <> ();
162+ Set <String > modelKeys = templateVariables != null ? templateVariables .keySet () : Collections . emptySet ();
132163 Set <String > missingVariables = new HashSet <>(templateTokens );
133164 missingVariables .removeAll (modelKeys );
134165
135166 if (!missingVariables .isEmpty ()) {
136- if (this .validationMode == ValidationMode .WARN ) {
137- logger .warn (VALIDATION_MESSAGE .formatted (missingVariables ));
167+ String message = VALIDATION_MESSAGE .formatted (missingVariables );
168+ if (validationMode == ValidationMode .WARN ) {
169+ logger .warn (message );
138170 }
139- else if (this . validationMode == ValidationMode .THROW ) {
140- throw new IllegalStateException (VALIDATION_MESSAGE . formatted ( missingVariables ) );
171+ else if (validationMode == ValidationMode .THROW ) {
172+ throw new IllegalStateException (message );
141173 }
142174 }
143- return missingVariables ;
144175 }
145176
177+ /**
178+ * Extracts variable names from the template using ST's token stream and regex
179+ * validation.
180+ */
146181 private Set <String > getInputVariables (ST st ) {
147- TokenStream tokens = st .impl .tokens ;
148182 Set <String > inputVariables = new HashSet <>();
183+ TokenStream tokens = st .impl .tokens ;
149184 boolean isInsideList = false ;
150185
186+ // Primary token-based extraction
151187 for (int i = 0 ; i < tokens .size (); i ++) {
152188 Token token = tokens .get (i );
153189
154- // Handle list variables with option (e.g., {items; separator=", "})
190+ // Handle list variables (e.g., {items; separator=", "})
155191 if (token .getType () == STLexer .LDELIM && i + 1 < tokens .size ()
156192 && tokens .get (i + 1 ).getType () == STLexer .ID ) {
157193 if (i + 2 < tokens .size () && tokens .get (i + 2 ).getType () == STLexer .COLON ) {
158194 String text = tokens .get (i + 1 ).getText ();
159- if (!Compiler .funcs .containsKey (text ) || this . validateStFunctions ) {
195+ if (!Compiler .funcs .containsKey (text ) || validateStFunctions ) {
160196 inputVariables .add (text );
161197 isInsideList = true ;
162198 }
@@ -165,34 +201,43 @@ private Set<String> getInputVariables(ST st) {
165201 else if (token .getType () == STLexer .RDELIM ) {
166202 isInsideList = false ;
167203 }
168- // Only add IDs that are not function calls (i.e., not immediately followed by
204+ // Handle regular variables (exclude functions/properties)
169205 else if (!isInsideList && token .getType () == STLexer .ID ) {
170206 boolean isFunctionCall = (i + 1 < tokens .size () && tokens .get (i + 1 ).getType () == STLexer .LPAREN );
171207 boolean isDotProperty = (i > 0 && tokens .get (i - 1 ).getType () == STLexer .DOT );
172- // Only add as variable if:
173- // - Not a function call
174- // - Not a built-in function used as property (unless validateStFunctions)
175- if (!isFunctionCall && (!Compiler .funcs .containsKey (token .getText ()) || this .validateStFunctions
208+ if (!isFunctionCall && (!Compiler .funcs .containsKey (token .getText ()) || validateStFunctions
176209 || !(isDotProperty && Compiler .funcs .containsKey (token .getText ())))) {
177210 inputVariables .add (token .getText ());
178211 }
179212 }
180213 }
214+
215+ // Secondary regex check to catch edge cases
216+ Pattern varPattern = Pattern .compile (Pattern .quote (String .valueOf (INTERNAL_START_DELIMITER ))
217+ + "([a-zA-Z_][a-zA-Z0-9_]*)" + Pattern .quote (String .valueOf (INTERNAL_END_DELIMITER )));
218+ Matcher matcher = varPattern .matcher (st .impl .template );
219+ while (matcher .find ()) {
220+ inputVariables .add (matcher .group (1 ));
221+ }
222+
181223 return inputVariables ;
182224 }
183225
226+ /**
227+ * Creates a builder for configuring StTemplateRenderer instances.
228+ */
184229 public static Builder builder () {
185230 return new Builder ();
186231 }
187232
188233 /**
189- * Builder for configuring and creating {@link StTemplateRenderer} instances .
234+ * Builder for fluent configuration of StTemplateRenderer.
190235 */
191236 public static final class Builder {
192237
193- private char startDelimiterToken = DEFAULT_START_DELIMITER_TOKEN ;
238+ private String startDelimiterToken = DEFAULT_START_DELIMITER ;
194239
195- private char endDelimiterToken = DEFAULT_END_DELIMITER_TOKEN ;
240+ private String endDelimiterToken = DEFAULT_END_DELIMITER ;
196241
197242 private ValidationMode validationMode = DEFAULT_VALIDATION_MODE ;
198243
@@ -202,65 +247,42 @@ private Builder() {
202247 }
203248
204249 /**
205- * Sets the character used as the start delimiter for template expressions.
206- * Default is '{'.
207- * @param startDelimiterToken The start delimiter character.
208- * @return This builder instance for chaining.
250+ * Sets the multi-character start delimiter (e.g., "{{" or "<").
209251 */
210- public Builder startDelimiterToken (char startDelimiterToken ) {
252+ public Builder startDelimiterToken (String startDelimiterToken ) {
211253 this .startDelimiterToken = startDelimiterToken ;
212254 return this ;
213255 }
214256
215257 /**
216- * Sets the character used as the end delimiter for template expressions. Default
217- * is '}'.
218- * @param endDelimiterToken The end delimiter character.
219- * @return This builder instance for chaining.
258+ * Sets the multi-character end delimiter (e.g., "}}" or ">").
220259 */
221- public Builder endDelimiterToken (char endDelimiterToken ) {
260+ public Builder endDelimiterToken (String endDelimiterToken ) {
222261 this .endDelimiterToken = endDelimiterToken ;
223262 return this ;
224263 }
225264
226265 /**
227- * Sets the validation mode to control behavior when the provided variables do not
228- * match the variables required by the template. Default is
229- * {@link ValidationMode#THROW}.
230- * @param validationMode The desired validation mode.
231- * @return This builder instance for chaining.
266+ * Sets the validation mode for missing variables.
232267 */
233268 public Builder validationMode (ValidationMode validationMode ) {
234269 this .validationMode = validationMode ;
235270 return this ;
236271 }
237272
238273 /**
239- * Configures the renderer to support StringTemplate's built-in functions during
240- * validation.
241- * <p>
242- * When enabled (set to true), identifiers in the template that match known ST
243- * function names (e.g., "first", "rest", "length") will not be treated as
244- * required input variables during validation.
245- * <p>
246- * When disabled (default, false), these identifiers are treated like regular
247- * variables and must be provided in the input map if validation is enabled
248- * ({@link ValidationMode#WARN} or {@link ValidationMode#THROW}).
249- * @return This builder instance for chaining.
274+ * Enables validation of ST built-in functions (treats them as variables).
250275 */
251276 public Builder validateStFunctions () {
252277 this .validateStFunctions = true ;
253278 return this ;
254279 }
255280
256281 /**
257- * Builds and returns a new {@link StTemplateRenderer} instance with the
258- * configured settings.
259- * @return A configured {@link StTemplateRenderer}.
282+ * Builds the configured StTemplateRenderer instance.
260283 */
261284 public StTemplateRenderer build () {
262- return new StTemplateRenderer (this .startDelimiterToken , this .endDelimiterToken , this .validationMode ,
263- this .validateStFunctions );
285+ return new StTemplateRenderer (startDelimiterToken , endDelimiterToken , validationMode , validateStFunctions );
264286 }
265287
266288 }
0 commit comments