Skip to content

Commit e618f92

Browse files
committed
Resolve nested placeholders with a fallback having one
This commit fixes a regression in PlaceHolderParser where it would no longer resolve nested placeholders for a case where the fallback has a placeholder itself. This is due to the Part implementations and how they are structure, and this commit makes sure that nested resolution happens consistently. Closes gh-34020
1 parent 81a9f3d commit e618f92

File tree

2 files changed

+101
-37
lines changed

2 files changed

+101
-37
lines changed

spring-core/src/main/java/org/springframework/util/PlaceholderParser.java

+99-37
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ private static void addText(String value, int start, int end, LinkedList<Part> p
247247
if (!parts.isEmpty()) {
248248
Part current = parts.removeLast();
249249
if (current instanceof TextPart textPart) {
250-
parts.add(new TextPart(textPart.text + text));
250+
parts.add(new TextPart(textPart.text() + text));
251251
}
252252
else {
253253
parts.add(current);
@@ -420,51 +420,42 @@ public String resolve(PartResolutionContext resolutionContext) {
420420

421421

422422
/**
423-
* A {@link Part} implementation that does not contain a valid placeholder.
424-
* @param text the raw (and resolved) text
423+
* A base {@link Part} implementation.
425424
*/
426-
record TextPart(String text) implements Part {
427-
428-
@Override
429-
public String resolve(PartResolutionContext resolutionContext) {
430-
return this.text;
431-
}
432-
}
425+
abstract static class AbstractPart implements Part {
433426

427+
private final String text;
434428

435-
/**
436-
* A {@link Part} implementation that represents a single placeholder with
437-
* a hard-coded fallback.
438-
* @param text the raw text
439-
* @param key the key of the placeholder
440-
* @param fallback the fallback to use, if any
441-
*/
442-
record SimplePlaceholderPart(String text, String key, @Nullable String fallback) implements Part {
429+
protected AbstractPart(String text) {
430+
this.text = text;
431+
}
443432

444433
@Override
445-
public String resolve(PartResolutionContext resolutionContext) {
446-
String resolvedValue = resolveToText(resolutionContext, this.key);
447-
if (resolvedValue != null) {
448-
return resolvedValue;
449-
}
450-
else if (this.fallback != null) {
451-
return this.fallback;
452-
}
453-
return resolutionContext.handleUnresolvablePlaceholder(this.key, this.text);
434+
public String text() {
435+
return this.text;
454436
}
455437

438+
/**
439+
* Resolve the placeholder with the given {@code key}. If the result of such
440+
* resolution return other placeholders, those are resolved as well until the
441+
* resolution no longer contains any placeholders.
442+
* @param resolutionContext the resolution context to use
443+
* @param key the initial placeholder
444+
* @return the full resolution of the given {@code key} or {@code null} if
445+
* the placeholder has no value to begin with
446+
*/
456447
@Nullable
457-
private String resolveToText(PartResolutionContext resolutionContext, String text) {
458-
String resolvedValue = resolutionContext.resolvePlaceholder(text);
448+
protected String resolveRecursively(PartResolutionContext resolutionContext, String key) {
449+
String resolvedValue = resolutionContext.resolvePlaceholder(key);
459450
if (resolvedValue != null) {
460-
resolutionContext.flagPlaceholderAsVisited(text);
451+
resolutionContext.flagPlaceholderAsVisited(key);
461452
// Let's check if we need to recursively resolve that value
462453
List<Part> nestedParts = resolutionContext.parse(resolvedValue);
463454
String value = toText(nestedParts);
464455
if (!isTextOnly(nestedParts)) {
465456
value = new ParsedValue(resolvedValue, nestedParts).resolve(resolutionContext);
466457
}
467-
resolutionContext.removePlaceholder(text);
458+
resolutionContext.removePlaceholder(key);
468459
return value;
469460
}
470461
// Not found
@@ -483,26 +474,97 @@ private String toText(List<Part> parts) {
483474
}
484475

485476

477+
/**
478+
* A {@link Part} implementation that does not contain a valid placeholder.
479+
*/
480+
static class TextPart extends AbstractPart {
481+
482+
/**
483+
* Create a new instance.
484+
* @param text the raw (and resolved) text
485+
*/
486+
public TextPart(String text) {
487+
super(text);
488+
}
489+
490+
@Override
491+
public String resolve(PartResolutionContext resolutionContext) {
492+
return text();
493+
}
494+
}
495+
496+
497+
/**
498+
* A {@link Part} implementation that represents a single placeholder with
499+
* a hard-coded fallback.
500+
*/
501+
static class SimplePlaceholderPart extends AbstractPart {
502+
503+
private final String key;
504+
505+
@Nullable
506+
private final String fallback;
507+
508+
/**
509+
* Create a new instance.
510+
* @param text the raw text
511+
* @param key the key of the placeholder
512+
* @param fallback the fallback to use, if any
513+
*/
514+
public SimplePlaceholderPart(String text,String key, @Nullable String fallback) {
515+
super(text);
516+
this.key = key;
517+
this.fallback = fallback;
518+
}
519+
520+
@Override
521+
public String resolve(PartResolutionContext resolutionContext) {
522+
String value = resolveRecursively(resolutionContext, this.key);
523+
if (value != null) {
524+
return value;
525+
}
526+
else if (this.fallback != null) {
527+
return this.fallback;
528+
}
529+
return resolutionContext.handleUnresolvablePlaceholder(this.key, text());
530+
}
531+
}
532+
533+
486534
/**
487535
* A {@link Part} implementation that represents a single placeholder
488536
* containing nested placeholders.
489-
* @param text the raw text of the root placeholder
490-
* @param keyParts the parts of the key
491-
* @param defaultParts the parts of the fallback, if any
492537
*/
493-
record NestedPlaceholderPart(String text, List<Part> keyParts, @Nullable List<Part> defaultParts) implements Part {
538+
static class NestedPlaceholderPart extends AbstractPart {
539+
540+
private final List<Part> keyParts;
541+
542+
@Nullable
543+
private final List<Part> defaultParts;
544+
545+
/**
546+
* Create a new instance.
547+
* @param text the raw text of the root placeholder
548+
* @param keyParts the parts of the key
549+
* @param defaultParts the parts of the fallback, if any
550+
*/
551+
NestedPlaceholderPart(String text, List<Part> keyParts, @Nullable List<Part> defaultParts) {
552+
super(text);
553+
this.keyParts = keyParts;
554+
this.defaultParts = defaultParts;
555+
}
494556

495557
@Override
496558
public String resolve(PartResolutionContext resolutionContext) {
497559
String resolvedKey = Part.resolveAll(this.keyParts, resolutionContext);
498-
String value = resolutionContext.resolvePlaceholder(resolvedKey);
560+
String value = resolveRecursively(resolutionContext, resolvedKey);
499561
if (value != null) {
500562
return value;
501563
}
502564
else if (this.defaultParts != null) {
503565
return Part.resolveAll(this.defaultParts, resolutionContext);
504566
}
505-
return resolutionContext.handleUnresolvablePlaceholder(resolvedKey, this.text);
567+
return resolutionContext.handleUnresolvablePlaceholder(resolvedKey, text());
506568
}
507569
}
508570

spring-core/src/test/java/org/springframework/util/PlaceholderParserTests.java

+2
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,8 @@ void nestedPlaceholdersAreReplaced(String text, String expected) {
210210
static Stream<Arguments> nestedPlaceholders() {
211211
return Stream.of(
212212
Arguments.of("${p6}", "v1:v2:def"),
213+
Arguments.of("${p6:not-used}", "v1:v2:def"),
214+
Arguments.of("${p6:${invalid}}", "v1:v2:def"),
213215
Arguments.of("${invalid:${p1}:${p2}}", "v1:v2"),
214216
Arguments.of("${invalid:${p3}}", "v1:v2"),
215217
Arguments.of("${invalid:${p4}}", "v1:v2"),

0 commit comments

Comments
 (0)