1818
1919import java .util .ArrayList ;
2020import java .util .HashSet ;
21- import java .util .LinkedList ;
2221import java .util .List ;
2322import java .util .Map ;
2423import java .util .Set ;
3635 * that can be resolved using a {@link PlaceholderResolver PlaceholderResolver},
3736 * <code>${</code> the prefix, and <code>}</code> the suffix.
3837 *
39- * <p>A placeholder can also have a default value if its key does not represent a
40- * known property. The default value is separated from the key using a
41- * {@code separator}. For instance {@code ${name:John}} resolves to {@code John} if
42- * the placeholder resolver does not provide a value for the {@code name}
38+ * <p>A placeholder can also have a default value if its key does not represent
39+ * a known property. The default value is separated from the key using a
40+ * {@code separator}. For instance {@code ${name:John}} resolves to {@code John}
41+ * if the placeholder resolver does not provide a value for the {@code name}
4342 * property.
4443 *
4544 * <p>Placeholders can also have a more complex structure, and the resolution of
5049 * must be rendered as is, the placeholder can be escaped using an {@code escape}
5150 * character. For instance {@code \${name}} resolves as {@code ${name}}.
5251 *
53- * <p>The prefix, suffix, separator, and escape characters are configurable. Only
54- * the prefix and suffix are mandatory, and the support for default values or
55- * escaping is conditional on providing non-null values for them.
52+ * <p>The prefix, suffix, separator, and escape characters are configurable.
53+ * Only the prefix and suffix are mandatory, and the support for default values
54+ * or escaping is conditional on providing non-null values for them.
5655 *
5756 * <p>This parser makes sure to resolves placeholders as lazily as possible.
5857 *
5958 * @author Stephane Nicoll
59+ * @author Juergen Hoeller
6060 * @since 6.2
6161 */
6262final class PlaceholderParser {
@@ -118,51 +118,47 @@ final class PlaceholderParser {
118118 * @return the supplied value with placeholders replaced inline
119119 */
120120 public String replacePlaceholders (String value , PlaceholderResolver placeholderResolver ) {
121- Assert .notNull (value , "'value' must not be null" );
122- ParsedValue parsedValue = parse (value );
121+ List <Part > parts = parse (value , false );
122+ if (parts == null ) {
123+ return value ;
124+ }
125+ ParsedValue parsedValue = new ParsedValue (value , parts );
123126 PartResolutionContext resolutionContext = new PartResolutionContext (placeholderResolver ,
124127 this .prefix , this .suffix , this .ignoreUnresolvablePlaceholders ,
125128 candidate -> parse (candidate , false ));
126129 return parsedValue .resolve (resolutionContext );
127130 }
128131
129- /**
130- * Parse the specified value.
131- * @param value the value containing the placeholders to be replaced
132- * @return the different parts that have been identified
133- */
134- ParsedValue parse (String value ) {
135- List <Part > parts = parse (value , false );
136- return new ParsedValue (value , parts );
137- }
138-
139- private List <Part > parse (String value , boolean inPlaceholder ) {
140- LinkedList <Part > parts = new LinkedList <>();
132+ private @ Nullable List <Part > parse (String value , boolean inPlaceholder ) {
141133 int startIndex = nextStartPrefix (value , 0 );
142134 if (startIndex == -1 ) {
143- Part part = (inPlaceholder ? createSimplePlaceholderPart (value ) : new TextPart (value ));
144- parts .add (part );
145- return parts ;
135+ return null ;
146136 }
137+ List <Part > parts = new ArrayList <>(4 );
147138 int position = 0 ;
148139 while (startIndex != -1 ) {
149140 int endIndex = nextValidEndPrefix (value , startIndex );
150- if (endIndex == -1 ) { // Not a valid placeholder, consume the prefix and continue
141+ if (endIndex == -1 ) { // Not a valid placeholder, consume the prefix and continue
151142 addText (value , position , startIndex + this .prefix .length (), parts );
152143 position = startIndex + this .prefix .length ();
153144 startIndex = nextStartPrefix (value , position );
154145 }
155- else if (isEscaped (value , startIndex )) { // Not a valid index, accumulate and skip the escape character
146+ else if (isEscaped (value , startIndex )) { // Not a valid index, accumulate and skip the escape character
156147 addText (value , position , startIndex - 1 , parts );
157148 addText (value , startIndex , startIndex + this .prefix .length (), parts );
158149 position = startIndex + this .prefix .length ();
159150 startIndex = nextStartPrefix (value , position );
160151 }
161- else { // Found valid placeholder, recursive parsing
152+ else { // Found valid placeholder, recursive parsing
162153 addText (value , position , startIndex , parts );
163154 String placeholder = value .substring (startIndex + this .prefix .length (), endIndex );
164155 List <Part > placeholderParts = parse (placeholder , true );
165- parts .addAll (placeholderParts );
156+ if (placeholderParts == null ) {
157+ parts .add (createSimplePlaceholderPart (placeholder ));
158+ }
159+ else {
160+ parts .addAll (placeholderParts );
161+ }
166162 startIndex = nextStartPrefix (value , endIndex + this .suffix .length ());
167163 position = endIndex + this .suffix .length ();
168164 }
@@ -239,29 +235,6 @@ private ParsedSection parseSection(String value) {
239235 return new ParsedSection (buffer .toString (), null );
240236 }
241237
242- private static void addText (String value , int start , int end , LinkedList <Part > parts ) {
243- if (start > end ) {
244- return ;
245- }
246- String text = value .substring (start , end );
247- if (!text .isEmpty ()) {
248- if (!parts .isEmpty ()) {
249- Part current = parts .removeLast ();
250- if (current instanceof TextPart textPart ) {
251- parts .add (new TextPart (textPart .text () + text ));
252- }
253- else {
254- parts .add (current );
255- parts .add (new TextPart (text ));
256- }
257- }
258- else {
259- parts .add (new TextPart (text ));
260- }
261- }
262- }
263-
264-
265238 private int nextStartPrefix (String value , int index ) {
266239 return value .indexOf (this .prefix , index );
267240 }
@@ -294,15 +267,46 @@ private boolean isEscaped(String value, int index) {
294267 return (this .escape != null && index > 0 && value .charAt (index - 1 ) == this .escape );
295268 }
296269
297- record ParsedSection (String key , @ Nullable String fallback ) {
270+ private static void addText (String value , int start , int end , List <Part > parts ) {
271+ if (start >= end ) {
272+ return ;
273+ }
274+ String text = value .substring (start , end );
275+ if (!parts .isEmpty () && parts .get (parts .size () - 1 ) instanceof TextPart textPart ) {
276+ parts .set (parts .size () - 1 , new TextPart (textPart .text () + text ));
277+ }
278+ else {
279+ parts .add (new TextPart (text ));
280+ }
281+ }
282+
283+
284+ /**
285+ * A representation of the parsing of an input string.
286+ * @param text the raw input string
287+ * @param parts the parts that appear in the string, in order
288+ */
289+ private record ParsedValue (String text , List <Part > parts ) {
298290
291+ public String resolve (PartResolutionContext resolutionContext ) {
292+ try {
293+ return Part .resolveAll (this .parts , resolutionContext );
294+ }
295+ catch (PlaceholderResolutionException ex ) {
296+ throw ex .withValue (this .text );
297+ }
298+ }
299+ }
300+
301+
302+ private record ParsedSection (String key , @ Nullable String fallback ) {
299303 }
300304
301305
302306 /**
303307 * Provide the necessary context to handle and resolve underlying placeholders.
304308 */
305- static class PartResolutionContext implements PlaceholderResolver {
309+ private static class PartResolutionContext implements PlaceholderResolver {
306310
307311 private final String prefix ;
308312
@@ -316,7 +320,6 @@ static class PartResolutionContext implements PlaceholderResolver {
316320
317321 private @ Nullable Set <String > visitedPlaceholders ;
318322
319-
320323 PartResolutionContext (PlaceholderResolver resolver , String prefix , String suffix ,
321324 boolean ignoreUnresolvablePlaceholders , Function <String , List <Part >> parser ) {
322325 this .prefix = prefix ;
@@ -348,7 +351,7 @@ private String toPlaceholderText(String text) {
348351 return this .prefix + text + this .suffix ;
349352 }
350353
351- public List <Part > parse (String text ) {
354+ public @ Nullable List <Part > parse (String text ) {
352355 return this .parser .apply (text );
353356 }
354357
@@ -363,17 +366,17 @@ public void flagPlaceholderAsVisited(String placeholder) {
363366 }
364367
365368 public void removePlaceholder (String placeholder ) {
366- Assert .state (this .visitedPlaceholders != null , "Visited placeholders must not be null" );
367- this .visitedPlaceholders .remove (placeholder );
369+ if (this .visitedPlaceholders != null ) {
370+ this .visitedPlaceholders .remove (placeholder );
371+ }
368372 }
369-
370373 }
371374
372375
373376 /**
374377 * A part is a section of a String containing placeholders to replace.
375378 */
376- interface Part {
379+ private interface Part {
377380
378381 /**
379382 * Resolve this part using the specified {@link PartResolutionContext}.
@@ -404,30 +407,12 @@ static String resolveAll(Iterable<Part> parts, PartResolutionContext resolutionC
404407 }
405408
406409
407- /**
408- * A representation of the parsing of an input string.
409- * @param text the raw input string
410- * @param parts the parts that appear in the string, in order
411- */
412- record ParsedValue (String text , List <Part > parts ) {
413-
414- public String resolve (PartResolutionContext resolutionContext ) {
415- try {
416- return Part .resolveAll (this .parts , resolutionContext );
417- }
418- catch (PlaceholderResolutionException ex ) {
419- throw ex .withValue (this .text );
420- }
421- }
422- }
423-
424-
425410 /**
426411 * A base {@link Part} implementation.
427412 */
428- abstract static class AbstractPart implements Part {
413+ private abstract static class AbstractPart implements Part {
429414
430- private final String text ;
415+ final String text ;
431416
432417 protected AbstractPart (String text ) {
433418 this .text = text ;
@@ -449,37 +434,27 @@ public String text() {
449434 */
450435 protected @ Nullable String resolveRecursively (PartResolutionContext resolutionContext , String key ) {
451436 String resolvedValue = resolutionContext .resolvePlaceholder (key );
452- if (resolvedValue != null ) {
453- resolutionContext .flagPlaceholderAsVisited (key );
454- // Let's check if we need to recursively resolve that value
455- List <Part > nestedParts = resolutionContext .parse (resolvedValue );
456- String value = toText (nestedParts );
457- if (!isTextOnly (nestedParts )) {
458- value = new ParsedValue (resolvedValue , nestedParts ).resolve (resolutionContext );
459- }
460- resolutionContext .removePlaceholder (key );
461- return value ;
437+ if (resolvedValue == null ) {
438+ // Not found
439+ return null ;
462440 }
463- // Not found
464- return null ;
465- }
466-
467- private boolean isTextOnly (List <Part > parts ) {
468- return parts .stream ().allMatch (TextPart .class ::isInstance );
469- }
470-
471- private String toText (List <Part > parts ) {
472- StringBuilder sb = new StringBuilder ();
473- parts .forEach (part -> sb .append (part .text ()));
474- return sb .toString ();
441+ // Let's check if we need to recursively resolve that value
442+ List <Part > nestedParts = resolutionContext .parse (resolvedValue );
443+ if (nestedParts == null ) {
444+ return resolvedValue ;
445+ }
446+ resolutionContext .flagPlaceholderAsVisited (key );
447+ String value = new ParsedValue (resolvedValue , nestedParts ).resolve (resolutionContext );
448+ resolutionContext .removePlaceholder (key );
449+ return value ;
475450 }
476451 }
477452
478453
479454 /**
480455 * A {@link Part} implementation that does not contain a valid placeholder.
481456 */
482- static class TextPart extends AbstractPart {
457+ private static class TextPart extends AbstractPart {
483458
484459 /**
485460 * Create a new instance.
@@ -491,7 +466,7 @@ public TextPart(String text) {
491466
492467 @ Override
493468 public String resolve (PartResolutionContext resolutionContext ) {
494- return text () ;
469+ return this . text ;
495470 }
496471 }
497472
@@ -500,7 +475,7 @@ public String resolve(PartResolutionContext resolutionContext) {
500475 * A {@link Part} implementation that represents a single placeholder with
501476 * a hard-coded fallback.
502477 */
503- static class SimplePlaceholderPart extends AbstractPart {
478+ private static class SimplePlaceholderPart extends AbstractPart {
504479
505480 private final String key ;
506481
@@ -527,13 +502,13 @@ public String resolve(PartResolutionContext resolutionContext) {
527502 else if (this .fallback != null ) {
528503 return this .fallback ;
529504 }
530- return resolutionContext .handleUnresolvablePlaceholder (this .key , text () );
505+ return resolutionContext .handleUnresolvablePlaceholder (this .key , this . text );
531506 }
532507
533508 @ Nullable
534509 private String resolveRecursively (PartResolutionContext resolutionContext ) {
535- if (!this .text () .equals (this .key )) {
536- String value = resolveRecursively (resolutionContext , this .text () );
510+ if (!this .text .equals (this .key )) {
511+ String value = resolveRecursively (resolutionContext , this .text );
537512 if (value != null ) {
538513 return value ;
539514 }
@@ -547,7 +522,7 @@ private String resolveRecursively(PartResolutionContext resolutionContext) {
547522 * A {@link Part} implementation that represents a single placeholder
548523 * containing nested placeholders.
549524 */
550- static class NestedPlaceholderPart extends AbstractPart {
525+ private static class NestedPlaceholderPart extends AbstractPart {
551526
552527 private final List <Part > keyParts ;
553528
@@ -575,7 +550,7 @@ public String resolve(PartResolutionContext resolutionContext) {
575550 else if (this .defaultParts != null ) {
576551 return Part .resolveAll (this .defaultParts , resolutionContext );
577552 }
578- return resolutionContext .handleUnresolvablePlaceholder (resolvedKey , text () );
553+ return resolutionContext .handleUnresolvablePlaceholder (resolvedKey , this . text );
579554 }
580555 }
581556
0 commit comments