diff --git a/src/main/java/org/codehaus/plexus/interpolation/StringSearchInterpolator.java b/src/main/java/org/codehaus/plexus/interpolation/StringSearchInterpolator.java index 28e2189..9f244e4 100644 --- a/src/main/java/org/codehaus/plexus/interpolation/StringSearchInterpolator.java +++ b/src/main/java/org/codehaus/plexus/interpolation/StringSearchInterpolator.java @@ -124,7 +124,7 @@ private String interpolate(String input, RecursionInterceptor recursionIntercept do { result.append(input, endIdx + 1, startIdx); - endIdx = input.indexOf(endExpr, startIdx + 1); + endIdx = findMatchingEndExpr(input, startIdx, startExpr, endExpr); if (endIdx < 0) { break; } @@ -178,6 +178,33 @@ private String interpolate(String input, RecursionInterceptor recursionIntercept throw new InterpolationCycleException(recursionInterceptor, realExpr, wholeExpr); } + // If value is null, try to extract a default value (format: key:default) + if (value == null) { + int colonIndex = realExpr.indexOf(':'); + if (colonIndex > 0) { + String key = realExpr.substring(0, colonIndex); + String defaultValue = realExpr.substring(colonIndex + 1); + + // Try to resolve the key part only + for (ValueSource valueSource : valueSources) { + if (value != null) { + break; + } + value = valueSource.getValue(key, startExpr, endExpr); + + if (value != null && value.toString().contains(wholeExpr)) { + bestAnswer = value; + value = null; + } + } + + // If still null, use the default value + if (value == null) { + value = defaultValue; + } + } + } + if (value != null) { value = interpolate(String.valueOf(value), recursionInterceptor, unresolvable); @@ -268,6 +295,44 @@ public void setCacheAnswers(boolean cacheAnswers) { this.cacheAnswers = cacheAnswers; } + /** + * Find the matching end expression, accounting for nested expressions. + * @param input The input string + * @param startIdx The index of the start expression + * @param startExpr The start expression delimiter + * @param endExpr The end expression delimiter + * @return The index of the matching end expression, or -1 if not found + */ + private int findMatchingEndExpr(String input, int startIdx, String startExpr, String endExpr) { + int depth = 1; + int searchFrom = startIdx + startExpr.length(); + + while (depth > 0 && searchFrom < input.length()) { + int nextStart = input.indexOf(startExpr, searchFrom); + int nextEnd = input.indexOf(endExpr, searchFrom); + + if (nextEnd < 0) { + // No more end delimiters found + return -1; + } + + if (nextStart >= 0 && nextStart < nextEnd) { + // Found a nested start expression + depth++; + searchFrom = nextStart + startExpr.length(); + } else { + // Found an end expression + depth--; + if (depth == 0) { + return nextEnd; + } + searchFrom = nextEnd + endExpr.length(); + } + } + + return -1; + } + public void clearAnswers() { existingAnswers.clear(); } diff --git a/src/main/java/org/codehaus/plexus/interpolation/fixed/FixedStringSearchInterpolator.java b/src/main/java/org/codehaus/plexus/interpolation/fixed/FixedStringSearchInterpolator.java index cbffe0c..bb61532 100644 --- a/src/main/java/org/codehaus/plexus/interpolation/fixed/FixedStringSearchInterpolator.java +++ b/src/main/java/org/codehaus/plexus/interpolation/fixed/FixedStringSearchInterpolator.java @@ -181,7 +181,7 @@ public String interpolate(String input, InterpolationState interpolationState) t while ((startIdx = input.indexOf(startExpr, endIdx + 1)) > -1) { result.append(input, endIdx + 1, startIdx); - endIdx = input.indexOf(endExpr, startIdx + 1); + endIdx = findMatchingEndExpr(input, startIdx, startExpr, endExpr); if (endIdx < 0) { break; } @@ -212,6 +212,24 @@ public String interpolate(String input, InterpolationState interpolationState) t } Object value = getValue(realExpr, interpolationState); + + // If value is null, try to extract a default value (format: key:default) + if (value == null) { + int colonIndex = realExpr.indexOf(':'); + if (colonIndex > 0) { + String key = realExpr.substring(0, colonIndex); + String defaultValue = realExpr.substring(colonIndex + 1); + + // Try to resolve the key part only + value = getValue(key, interpolationState); + + // If still null, use the default value + if (value == null) { + value = defaultValue; + } + } + } + if (value != null) { value = interpolate(String.valueOf(value), interpolationState); @@ -246,4 +264,42 @@ public String interpolate(String input, InterpolationState interpolationState) t return result.toString(); } + + /** + * Find the matching end expression, accounting for nested expressions. + * @param input The input string + * @param startIdx The index of the start expression + * @param startExpr The start expression delimiter + * @param endExpr The end expression delimiter + * @return The index of the matching end expression, or -1 if not found + */ + private int findMatchingEndExpr(String input, int startIdx, String startExpr, String endExpr) { + int depth = 1; + int searchFrom = startIdx + startExpr.length(); + + while (depth > 0 && searchFrom < input.length()) { + int nextStart = input.indexOf(startExpr, searchFrom); + int nextEnd = input.indexOf(endExpr, searchFrom); + + if (nextEnd < 0) { + // No more end delimiters found + return -1; + } + + if (nextStart >= 0 && nextStart < nextEnd) { + // Found a nested start expression + depth++; + searchFrom = nextStart + startExpr.length(); + } else { + // Found an end expression + depth--; + if (depth == 0) { + return nextEnd; + } + searchFrom = nextEnd + endExpr.length(); + } + } + + return -1; + } } diff --git a/src/main/java/org/codehaus/plexus/interpolation/multi/MultiDelimiterStringSearchInterpolator.java b/src/main/java/org/codehaus/plexus/interpolation/multi/MultiDelimiterStringSearchInterpolator.java index c1a8541..1323504 100644 --- a/src/main/java/org/codehaus/plexus/interpolation/multi/MultiDelimiterStringSearchInterpolator.java +++ b/src/main/java/org/codehaus/plexus/interpolation/multi/MultiDelimiterStringSearchInterpolator.java @@ -161,7 +161,7 @@ private String interpolate(String input, RecursionInterceptor recursionIntercept startIdx = selectedSpec.getNextStartIndex(); result.append(input, endIdx + 1, startIdx); - endIdx = input.indexOf(endExpr, startIdx + 1); + endIdx = findMatchingEndExpr(input, startIdx, startExpr, endExpr); if (endIdx < 0) { break; } @@ -216,6 +216,32 @@ private String interpolate(String input, RecursionInterceptor recursionIntercept throw new InterpolationCycleException(recursionInterceptor, realExpr, wholeExpr); } + // If value is null, try to extract a default value (format: key:default) + if (value == null) { + int colonIndex = realExpr.indexOf(':'); + if (colonIndex > 0) { + String key = realExpr.substring(0, colonIndex); + String defaultValue = realExpr.substring(colonIndex + 1); + + // Try to resolve the key part only + for (ValueSource vs : valueSources) { + if (value != null) break; + + value = vs.getValue(key, startExpr, endExpr); + + if (value != null && value.toString().contains(wholeExpr)) { + bestAnswer = value; + value = null; + } + } + + // If still null, use the default value + if (value == null) { + value = defaultValue; + } + } + } + if (value != null) { value = interpolate(String.valueOf(value), recursionInterceptor, unresolvable); @@ -303,6 +329,44 @@ public List getFeedback() { return messages; } + /** + * Find the matching end expression, accounting for nested expressions. + * @param input The input string + * @param startIdx The index of the start expression + * @param startExpr The start expression delimiter + * @param endExpr The end expression delimiter + * @return The index of the matching end expression, or -1 if not found + */ + private int findMatchingEndExpr(String input, int startIdx, String startExpr, String endExpr) { + int depth = 1; + int searchFrom = startIdx + startExpr.length(); + + while (depth > 0 && searchFrom < input.length()) { + int nextStart = input.indexOf(startExpr, searchFrom); + int nextEnd = input.indexOf(endExpr, searchFrom); + + if (nextEnd < 0) { + // No more end delimiters found + return -1; + } + + if (nextStart >= 0 && nextStart < nextEnd) { + // Found a nested start expression + depth++; + searchFrom = nextStart + startExpr.length(); + } else { + // Found an end expression + depth--; + if (depth == 0) { + return nextEnd; + } + searchFrom = nextEnd + endExpr.length(); + } + } + + return -1; + } + /** * Clear the feedback messages from previous interpolate(..) calls. */ diff --git a/src/test/java/org/codehaus/plexus/interpolation/StringSearchInterpolatorTest.java b/src/test/java/org/codehaus/plexus/interpolation/StringSearchInterpolatorTest.java index e1fa00a..a9c0109 100644 --- a/src/test/java/org/codehaus/plexus/interpolation/StringSearchInterpolatorTest.java +++ b/src/test/java/org/codehaus/plexus/interpolation/StringSearchInterpolatorTest.java @@ -538,4 +538,72 @@ public String getName() { return name; } } + + @Test + public void testDefaultValueWithExistingKey() throws InterpolationException { + Properties p = new Properties(); + p.setProperty("key", "value"); + + StringSearchInterpolator interpolator = new StringSearchInterpolator(); + interpolator.addValueSource(new PropertiesBasedValueSource(p)); + + assertEquals("This is a test value.", interpolator.interpolate("This is a test ${key:default}.")); + } + + @Test + public void testDefaultValueWithMissingKey() throws InterpolationException { + Properties p = new Properties(); + + StringSearchInterpolator interpolator = new StringSearchInterpolator(); + interpolator.addValueSource(new PropertiesBasedValueSource(p)); + + assertEquals("This is a test default.", interpolator.interpolate("This is a test ${missingkey:default}.")); + } + + @Test + public void testDefaultValueWithEmptyDefault() throws InterpolationException { + Properties p = new Properties(); + + StringSearchInterpolator interpolator = new StringSearchInterpolator(); + interpolator.addValueSource(new PropertiesBasedValueSource(p)); + + assertEquals("This is a test .", interpolator.interpolate("This is a test ${missingkey:}.")); + } + + @Test + public void testDefaultValueWithColonInDefault() throws InterpolationException { + Properties p = new Properties(); + + StringSearchInterpolator interpolator = new StringSearchInterpolator(); + interpolator.addValueSource(new PropertiesBasedValueSource(p)); + + assertEquals( + "This is a test http://example.com.", + interpolator.interpolate("This is a test ${missingkey:http://example.com}.")); + } + + @Test + public void testDefaultValueWithNestedExpression() throws InterpolationException { + Properties p = new Properties(); + p.setProperty("fallback.key", "fallbackValue"); + + StringSearchInterpolator interpolator = new StringSearchInterpolator(); + interpolator.addValueSource(new PropertiesBasedValueSource(p)); + + assertEquals( + "This is a test fallbackValue.", + interpolator.interpolate("This is a test ${missingkey:${fallback.key}}.")); + } + + @Test + public void testNoDefaultValueSyntax() throws InterpolationException { + Properties p = new Properties(); + p.setProperty("key:with:colons", "colonValue"); + + StringSearchInterpolator interpolator = new StringSearchInterpolator(); + interpolator.addValueSource(new PropertiesBasedValueSource(p)); + + // When a key actually contains colons, it should still work if the key exists + assertEquals("This is a test colonValue.", interpolator.interpolate("This is a test ${key:with:colons}.")); + } } diff --git a/src/test/java/org/codehaus/plexus/interpolation/fixed/FixedStringSearchInterpolatorTest.java b/src/test/java/org/codehaus/plexus/interpolation/fixed/FixedStringSearchInterpolatorTest.java index afef83e..e946418 100644 --- a/src/test/java/org/codehaus/plexus/interpolation/fixed/FixedStringSearchInterpolatorTest.java +++ b/src/test/java/org/codehaus/plexus/interpolation/fixed/FixedStringSearchInterpolatorTest.java @@ -493,6 +493,74 @@ void fixedInjectedIntoRegular() throws InterpolationException { assertEquals("v1X", interpolator.interpolate("${key1}${key}")); } + @Test + void testDefaultValueWithExistingKey() { + Properties p = new Properties(); + p.setProperty("key", "value"); + + FixedStringSearchInterpolator interpolator = + FixedStringSearchInterpolator.create(new PropertiesBasedValueSource(p)); + + assertEquals("This is a test value.", interpolator.interpolate("This is a test ${key:default}.")); + } + + @Test + void testDefaultValueWithMissingKey() { + Properties p = new Properties(); + + FixedStringSearchInterpolator interpolator = + FixedStringSearchInterpolator.create(new PropertiesBasedValueSource(p)); + + assertEquals("This is a test default.", interpolator.interpolate("This is a test ${missingkey:default}.")); + } + + @Test + void testDefaultValueWithEmptyDefault() { + Properties p = new Properties(); + + FixedStringSearchInterpolator interpolator = + FixedStringSearchInterpolator.create(new PropertiesBasedValueSource(p)); + + assertEquals("This is a test .", interpolator.interpolate("This is a test ${missingkey:}.")); + } + + @Test + void testDefaultValueWithColonInDefault() { + Properties p = new Properties(); + + FixedStringSearchInterpolator interpolator = + FixedStringSearchInterpolator.create(new PropertiesBasedValueSource(p)); + + assertEquals( + "This is a test http://example.com.", + interpolator.interpolate("This is a test ${missingkey:http://example.com}.")); + } + + @Test + void testDefaultValueWithNestedExpression() { + Properties p = new Properties(); + p.setProperty("fallback.key", "fallbackValue"); + + FixedStringSearchInterpolator interpolator = + FixedStringSearchInterpolator.create(new PropertiesBasedValueSource(p)); + + assertEquals( + "This is a test fallbackValue.", + interpolator.interpolate("This is a test ${missingkey:${fallback.key}}.")); + } + + @Test + void testNoDefaultValueSyntax() { + Properties p = new Properties(); + p.setProperty("key:with:colons", "colonValue"); + + FixedStringSearchInterpolator interpolator = + FixedStringSearchInterpolator.create(new PropertiesBasedValueSource(p)); + + // When a key actually contains colons, it should still work if the key exists + assertEquals("This is a test colonValue.", interpolator.interpolate("This is a test ${key:with:colons}.")); + } + private PropertiesBasedValueSource properttyBasedValueSource(String... values) { Properties p = new Properties(); for (int i = 0; i < values.length; i += 2) { diff --git a/src/test/java/org/codehaus/plexus/interpolation/multi/MultiDelimiterStringSearchInterpolatorTest.java b/src/test/java/org/codehaus/plexus/interpolation/multi/MultiDelimiterStringSearchInterpolatorTest.java index 7b38a16..2f7196f 100644 --- a/src/test/java/org/codehaus/plexus/interpolation/multi/MultiDelimiterStringSearchInterpolatorTest.java +++ b/src/test/java/org/codehaus/plexus/interpolation/multi/MultiDelimiterStringSearchInterpolatorTest.java @@ -18,10 +18,12 @@ import java.util.HashMap; import java.util.Map; +import java.util.Properties; import org.codehaus.plexus.interpolation.AbstractValueSource; import org.codehaus.plexus.interpolation.InterpolationException; import org.codehaus.plexus.interpolation.MapBasedValueSource; +import org.codehaus.plexus.interpolation.PropertiesBasedValueSource; import org.codehaus.plexus.interpolation.ValueSource; import org.junit.jupiter.api.Test; @@ -204,4 +206,72 @@ public Object getValue(String expression) { // In this case: 4 expressions evaluated in 2 passes = 8 calls assertEquals(8, valueSourceCallCount[0]); } + + @Test + void testDefaultValueWithExistingKey() throws Exception { + Properties p = new Properties(); + p.setProperty("key", "value"); + + MultiDelimiterStringSearchInterpolator interpolator = new MultiDelimiterStringSearchInterpolator(); + interpolator.addValueSource(new PropertiesBasedValueSource(p)); + + assertEquals("This is a test value.", interpolator.interpolate("This is a test ${key:default}.")); + } + + @Test + void testDefaultValueWithMissingKey() throws Exception { + Properties p = new Properties(); + + MultiDelimiterStringSearchInterpolator interpolator = new MultiDelimiterStringSearchInterpolator(); + interpolator.addValueSource(new PropertiesBasedValueSource(p)); + + assertEquals("This is a test default.", interpolator.interpolate("This is a test ${missingkey:default}.")); + } + + @Test + void testDefaultValueWithEmptyDefault() throws Exception { + Properties p = new Properties(); + + MultiDelimiterStringSearchInterpolator interpolator = new MultiDelimiterStringSearchInterpolator(); + interpolator.addValueSource(new PropertiesBasedValueSource(p)); + + assertEquals("This is a test .", interpolator.interpolate("This is a test ${missingkey:}.")); + } + + @Test + void testDefaultValueWithColonInDefault() throws Exception { + Properties p = new Properties(); + + MultiDelimiterStringSearchInterpolator interpolator = new MultiDelimiterStringSearchInterpolator(); + interpolator.addValueSource(new PropertiesBasedValueSource(p)); + + assertEquals( + "This is a test http://example.com.", + interpolator.interpolate("This is a test ${missingkey:http://example.com}.")); + } + + @Test + void testDefaultValueWithNestedExpression() throws Exception { + Properties p = new Properties(); + p.setProperty("fallback.key", "fallbackValue"); + + MultiDelimiterStringSearchInterpolator interpolator = new MultiDelimiterStringSearchInterpolator(); + interpolator.addValueSource(new PropertiesBasedValueSource(p)); + + assertEquals( + "This is a test fallbackValue.", + interpolator.interpolate("This is a test ${missingkey:${fallback.key}}.")); + } + + @Test + void testNoDefaultValueSyntax() throws Exception { + Properties p = new Properties(); + p.setProperty("key:with:colons", "colonValue"); + + MultiDelimiterStringSearchInterpolator interpolator = new MultiDelimiterStringSearchInterpolator(); + interpolator.addValueSource(new PropertiesBasedValueSource(p)); + + // When a key actually contains colons, it should still work if the key exists + assertEquals("This is a test colonValue.", interpolator.interpolate("This is a test ${key:with:colons}.")); + } }