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 {
@@ -120,51 +120,47 @@ final class PlaceholderParser {
120120 * @return the supplied value with placeholders replaced inline
121121 */
122122 public String replacePlaceholders (String value , PlaceholderResolver placeholderResolver ) {
123- Assert .notNull (value , "'value' must not be null" );
124- ParsedValue parsedValue = parse (value );
123+ List <Part > parts = parse (value , false );
124+ if (parts == null ) {
125+ return value ;
126+ }
127+ ParsedValue parsedValue = new ParsedValue (value , parts );
125128 PartResolutionContext resolutionContext = new PartResolutionContext (placeholderResolver ,
126129 this .prefix , this .suffix , this .ignoreUnresolvablePlaceholders ,
127130 candidate -> parse (candidate , false ));
128131 return parsedValue .resolve (resolutionContext );
129132 }
130133
131- /**
132- * Parse the specified value.
133- * @param value the value containing the placeholders to be replaced
134- * @return the different parts that have been identified
135- */
136- ParsedValue parse (String value ) {
137- List <Part > parts = parse (value , false );
138- return new ParsedValue (value , parts );
139- }
140-
141- private List <Part > parse (String value , boolean inPlaceholder ) {
142- LinkedList <Part > parts = new LinkedList <>();
134+ private @ Nullable List <Part > parse (String value , boolean inPlaceholder ) {
143135 int startIndex = nextStartPrefix (value , 0 );
144136 if (startIndex == -1 ) {
145- Part part = (inPlaceholder ? createSimplePlaceholderPart (value ) : new TextPart (value ));
146- parts .add (part );
147- return parts ;
137+ return null ;
148138 }
139+ List <Part > parts = new ArrayList <>(4 );
149140 int position = 0 ;
150141 while (startIndex != -1 ) {
151142 int endIndex = nextValidEndPrefix (value , startIndex );
152- if (endIndex == -1 ) { // Not a valid placeholder, consume the prefix and continue
143+ if (endIndex == -1 ) { // Not a valid placeholder, consume the prefix and continue
153144 addText (value , position , startIndex + this .prefix .length (), parts );
154145 position = startIndex + this .prefix .length ();
155146 startIndex = nextStartPrefix (value , position );
156147 }
157- else if (isEscaped (value , startIndex )) { // Not a valid index, accumulate and skip the escape character
148+ else if (isEscaped (value , startIndex )) { // Not a valid index, accumulate and skip the escape character
158149 addText (value , position , startIndex - 1 , parts );
159150 addText (value , startIndex , startIndex + this .prefix .length (), parts );
160151 position = startIndex + this .prefix .length ();
161152 startIndex = nextStartPrefix (value , position );
162153 }
163- else { // Found valid placeholder, recursive parsing
154+ else { // Found valid placeholder, recursive parsing
164155 addText (value , position , startIndex , parts );
165156 String placeholder = value .substring (startIndex + this .prefix .length (), endIndex );
166157 List <Part > placeholderParts = parse (placeholder , true );
167- parts .addAll (placeholderParts );
158+ if (placeholderParts == null ) {
159+ parts .add (createSimplePlaceholderPart (placeholder ));
160+ }
161+ else {
162+ parts .addAll (placeholderParts );
163+ }
168164 startIndex = nextStartPrefix (value , endIndex + this .suffix .length ());
169165 position = endIndex + this .suffix .length ();
170166 }
@@ -241,29 +237,6 @@ private ParsedSection parseSection(String value) {
241237 return new ParsedSection (buffer .toString (), null );
242238 }
243239
244- private static void addText (String value , int start , int end , LinkedList <Part > parts ) {
245- if (start > end ) {
246- return ;
247- }
248- String text = value .substring (start , end );
249- if (!text .isEmpty ()) {
250- if (!parts .isEmpty ()) {
251- Part current = parts .removeLast ();
252- if (current instanceof TextPart textPart ) {
253- parts .add (new TextPart (textPart .text () + text ));
254- }
255- else {
256- parts .add (current );
257- parts .add (new TextPart (text ));
258- }
259- }
260- else {
261- parts .add (new TextPart (text ));
262- }
263- }
264- }
265-
266-
267240 private int nextStartPrefix (String value , int index ) {
268241 return value .indexOf (this .prefix , index );
269242 }
@@ -296,15 +269,46 @@ private boolean isEscaped(String value, int index) {
296269 return (this .escape != null && index > 0 && value .charAt (index - 1 ) == this .escape );
297270 }
298271
299- record ParsedSection (String key , @ Nullable String fallback ) {
272+ private static void addText (String value , int start , int end , List <Part > parts ) {
273+ if (start >= end ) {
274+ return ;
275+ }
276+ String text = value .substring (start , end );
277+ if (!parts .isEmpty () && parts .get (parts .size () - 1 ) instanceof TextPart textPart ) {
278+ parts .set (parts .size () - 1 , new TextPart (textPart .text () + text ));
279+ }
280+ else {
281+ parts .add (new TextPart (text ));
282+ }
283+ }
284+
285+
286+ /**
287+ * A representation of the parsing of an input string.
288+ * @param text the raw input string
289+ * @param parts the parts that appear in the string, in order
290+ */
291+ private record ParsedValue (String text , List <Part > parts ) {
300292
293+ public String resolve (PartResolutionContext resolutionContext ) {
294+ try {
295+ return Part .resolveAll (this .parts , resolutionContext );
296+ }
297+ catch (PlaceholderResolutionException ex ) {
298+ throw ex .withValue (this .text );
299+ }
300+ }
301+ }
302+
303+
304+ private record ParsedSection (String key , @ Nullable String fallback ) {
301305 }
302306
303307
304308 /**
305309 * Provide the necessary context to handle and resolve underlying placeholders.
306310 */
307- static class PartResolutionContext implements PlaceholderResolver {
311+ private static class PartResolutionContext implements PlaceholderResolver {
308312
309313 private final String prefix ;
310314
@@ -319,7 +323,6 @@ static class PartResolutionContext implements PlaceholderResolver {
319323 @ Nullable
320324 private Set <String > visitedPlaceholders ;
321325
322-
323326 PartResolutionContext (PlaceholderResolver resolver , String prefix , String suffix ,
324327 boolean ignoreUnresolvablePlaceholders , Function <String , List <Part >> parser ) {
325328 this .prefix = prefix ;
@@ -352,7 +355,7 @@ private String toPlaceholderText(String text) {
352355 return this .prefix + text + this .suffix ;
353356 }
354357
355- public List <Part > parse (String text ) {
358+ public @ Nullable List <Part > parse (String text ) {
356359 return this .parser .apply (text );
357360 }
358361
@@ -367,17 +370,17 @@ public void flagPlaceholderAsVisited(String placeholder) {
367370 }
368371
369372 public void removePlaceholder (String placeholder ) {
370- Assert .state (this .visitedPlaceholders != null , "Visited placeholders must not be null" );
371- this .visitedPlaceholders .remove (placeholder );
373+ if (this .visitedPlaceholders != null ) {
374+ this .visitedPlaceholders .remove (placeholder );
375+ }
372376 }
373-
374377 }
375378
376379
377380 /**
378381 * A part is a section of a String containing placeholders to replace.
379382 */
380- interface Part {
383+ private interface Part {
381384
382385 /**
383386 * Resolve this part using the specified {@link PartResolutionContext}.
@@ -408,30 +411,12 @@ static String resolveAll(Iterable<Part> parts, PartResolutionContext resolutionC
408411 }
409412
410413
411- /**
412- * A representation of the parsing of an input string.
413- * @param text the raw input string
414- * @param parts the parts that appear in the string, in order
415- */
416- record ParsedValue (String text , List <Part > parts ) {
417-
418- public String resolve (PartResolutionContext resolutionContext ) {
419- try {
420- return Part .resolveAll (this .parts , resolutionContext );
421- }
422- catch (PlaceholderResolutionException ex ) {
423- throw ex .withValue (this .text );
424- }
425- }
426- }
427-
428-
429414 /**
430415 * A base {@link Part} implementation.
431416 */
432- abstract static class AbstractPart implements Part {
417+ private abstract static class AbstractPart implements Part {
433418
434- private final String text ;
419+ final String text ;
435420
436421 protected AbstractPart (String text ) {
437422 this .text = text ;
@@ -454,37 +439,27 @@ public String text() {
454439 @ Nullable
455440 protected String resolveRecursively (PartResolutionContext resolutionContext , String key ) {
456441 String resolvedValue = resolutionContext .resolvePlaceholder (key );
457- if (resolvedValue != null ) {
458- resolutionContext .flagPlaceholderAsVisited (key );
459- // Let's check if we need to recursively resolve that value
460- List <Part > nestedParts = resolutionContext .parse (resolvedValue );
461- String value = toText (nestedParts );
462- if (!isTextOnly (nestedParts )) {
463- value = new ParsedValue (resolvedValue , nestedParts ).resolve (resolutionContext );
464- }
465- resolutionContext .removePlaceholder (key );
466- return value ;
442+ if (resolvedValue == null ) {
443+ // Not found
444+ return null ;
467445 }
468- // Not found
469- return null ;
470- }
471-
472- private boolean isTextOnly (List <Part > parts ) {
473- return parts .stream ().allMatch (TextPart .class ::isInstance );
474- }
475-
476- private String toText (List <Part > parts ) {
477- StringBuilder sb = new StringBuilder ();
478- parts .forEach (part -> sb .append (part .text ()));
479- return sb .toString ();
446+ // Let's check if we need to recursively resolve that value
447+ List <Part > nestedParts = resolutionContext .parse (resolvedValue );
448+ if (nestedParts == null ) {
449+ return resolvedValue ;
450+ }
451+ resolutionContext .flagPlaceholderAsVisited (key );
452+ String value = new ParsedValue (resolvedValue , nestedParts ).resolve (resolutionContext );
453+ resolutionContext .removePlaceholder (key );
454+ return value ;
480455 }
481456 }
482457
483458
484459 /**
485460 * A {@link Part} implementation that does not contain a valid placeholder.
486461 */
487- static class TextPart extends AbstractPart {
462+ private static class TextPart extends AbstractPart {
488463
489464 /**
490465 * Create a new instance.
@@ -496,7 +471,7 @@ public TextPart(String text) {
496471
497472 @ Override
498473 public String resolve (PartResolutionContext resolutionContext ) {
499- return text () ;
474+ return this . text ;
500475 }
501476 }
502477
@@ -505,7 +480,7 @@ public String resolve(PartResolutionContext resolutionContext) {
505480 * A {@link Part} implementation that represents a single placeholder with
506481 * a hard-coded fallback.
507482 */
508- static class SimplePlaceholderPart extends AbstractPart {
483+ private static class SimplePlaceholderPart extends AbstractPart {
509484
510485 private final String key ;
511486
@@ -533,13 +508,13 @@ public String resolve(PartResolutionContext resolutionContext) {
533508 else if (this .fallback != null ) {
534509 return this .fallback ;
535510 }
536- return resolutionContext .handleUnresolvablePlaceholder (this .key , text () );
511+ return resolutionContext .handleUnresolvablePlaceholder (this .key , this . text );
537512 }
538513
539514 @ Nullable
540515 private String resolveRecursively (PartResolutionContext resolutionContext ) {
541- if (!this .text () .equals (this .key )) {
542- String value = resolveRecursively (resolutionContext , this .text () );
516+ if (!this .text .equals (this .key )) {
517+ String value = resolveRecursively (resolutionContext , this .text );
543518 if (value != null ) {
544519 return value ;
545520 }
@@ -553,7 +528,7 @@ private String resolveRecursively(PartResolutionContext resolutionContext) {
553528 * A {@link Part} implementation that represents a single placeholder
554529 * containing nested placeholders.
555530 */
556- static class NestedPlaceholderPart extends AbstractPart {
531+ private static class NestedPlaceholderPart extends AbstractPart {
557532
558533 private final List <Part > keyParts ;
559534
@@ -582,7 +557,7 @@ public String resolve(PartResolutionContext resolutionContext) {
582557 else if (this .defaultParts != null ) {
583558 return Part .resolveAll (this .defaultParts , resolutionContext );
584559 }
585- return resolutionContext .handleUnresolvablePlaceholder (resolvedKey , text () );
560+ return resolutionContext .handleUnresolvablePlaceholder (resolvedKey , this . text );
586561 }
587562 }
588563
0 commit comments