Skip to content
91 changes: 78 additions & 13 deletions owner/src/main/java/org/aeonbits/owner/StrSubstitutor.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
package org.aeonbits.owner;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
Expand Down Expand Up @@ -48,7 +50,7 @@
class StrSubstitutor implements Serializable {

private final Properties values;
private static final Pattern PATTERN = compile("\\$\\{(.+?)\\}");
private static final Pattern PATTERN = compile("\\$\\{(.+?)}");

/**
* Creates a new instance and initializes it. Uses defaults for variable prefix and suffix and the escaping
Expand All @@ -70,15 +72,16 @@ class StrSubstitutor implements Serializable {
String replace(String source) {
if (source == null)
return null;
Matcher m = PATTERN.matcher(source);
StringBuffer sb = new StringBuffer();
while (m.find()) {
String var = m.group(1);
String value = values.getProperty(var);
String replacement = (value != null) ? replace(value) : "";
m.appendReplacement(sb, Matcher.quoteReplacement(replacement));
StringBuilder sb = new StringBuilder();
List<String> groups = getVariableExpansions(source);
String replacedSource = source;
for (String group : groups) {
String value = values.getProperty(group);
String replacement = isKeyExpansionExpression(group) ? replace(group) : (value != null) ? replace(value) : "";
String replacementValue = calculateReplacementValue(group, replacement);
replacedSource = replacedSource.replaceFirst(Pattern.quote(String.format("${%s}", group)), Matcher.quoteReplacement(replacementValue));
}
m.appendTail(sb);
sb.append(replacedSource);
return sb.toString();
}

Expand All @@ -89,13 +92,75 @@ String replace(String source) {
* Otherwise the return string is formatted by source and arguments as with {@link String#format(String, Object...)}
*
* @param source A source formatting format string. {@code null} returns {@code null}
* @param args Arguments referenced by the format specifiers in the source string.
* @param args Arguments referenced by the format specifiers in the source string.
* @return formatted string
*/
String replace(String source, Object... args) {
if (source == null)
return null;
Matcher m = PATTERN.matcher(source);
return m.find() ? replace(source) : String.format(source, args);
return isKeyExpansionExpression(source) ? replace(source) : String.format(source, args);
}
}

/**
* Finds all top level variable expansion expressions and returns it as a list.
* E.g.: foo.${bar.${baz}}.${biz} -> [bar.${baz}, biz]
*
* @param expression the string for which variable expansion expressions are queried, null returns empty list
* @return list of top level variable expansion expressions
*/
private List<String> getVariableExpansions(String expression) {
final List<String> variables = new ArrayList<String>();
if (expression == null) return variables;

final String variableExpressionBeginning = "${";
int indexOfFirstVariableExpansion = expression.indexOf(variableExpressionBeginning);
if (indexOfFirstVariableExpansion == -1) return variables;

final int expressionLength = expression.length();
indexOfFirstVariableExpansion += variableExpressionBeginning.length();
final int variableStartIndex = indexOfFirstVariableExpansion;
int bracketCounter = 1;

for (int index = indexOfFirstVariableExpansion; index < expressionLength; index++) {
if (expression.charAt(index) == '{') {
bracketCounter += 1;
}
if (expression.charAt(index) == '}') bracketCounter -= 1;
if (bracketCounter == 0) {
variables.add(expression.substring(variableStartIndex, index));
variables.addAll(getVariableExpansions(expression.substring(index + 1, expressionLength)));
break;
}
}
return variables;
}

/**
* Checks if given expression matches PATTERN expression - regex for key expansion expression
*
* @param expression expression to be checked, null returns false
* @return true if expression matches PATTERN, false otherwise
*/
private boolean isKeyExpansionExpression(String expression) {
if (expression == null) return false;
return PATTERN.matcher(expression).find();
}

/**
* calculates value of a replacement
*
* @param group initial possible key expansion expression
* @param replacement evaluation of group.
* @return if replacement represents a property stored in Config, then the property value is returned.
* if group represents a key expansion expression, then if the key expansion represents a property, the property value is returned, otherwise key expansion expression is invalid and thus value should be an empty string.
* If neither replacement nor group represents a property value, then return replacement as a string value
*/
private String calculateReplacementValue(String group, String replacement) {
String groupValue = values.getProperty(group);
String replacementValue = values.getProperty(replacement);
if (replacementValue != null) return replacementValue;
if (isKeyExpansionExpression(group)) return groupValue != null ? groupValue : "";
return replacement;

}
}
22 changes: 22 additions & 0 deletions owner/src/test/java/org/aeonbits/owner/ConfigTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,16 @@ public interface SampleConfig extends Config {
void voidMethodWithValue();

void voidMethodWithoutValue();

@Key("environment")
String env();

@Key("environments.${environment}.browser")
@DefaultValue("chrome")
String usedBrowser();

@Key("environments.${environment}.webdriver.${environments.${environment}.browser}.switches")
String webDriverOptions();
}

@Test
Expand Down Expand Up @@ -141,4 +151,16 @@ public void whenPropertyValueIsNotValidFormatString_thenPropertyValueShouldRemai
assertEquals ("@#$%^&*()", config.password());
}

@Test
public void nestedKeyExpansion() {
Properties values = new Properties() {{
setProperty("environment", "dev");
setProperty("environments.dev.browser", "opera");
setProperty("environments.dev.webdriver.opera.switches", "foo bar");
setProperty("environments.dev.webdriver.chrome.switches", "foo baz");
}};

SampleConfig config = ConfigFactory.create(SampleConfig.class, values);
assertEquals("foo bar", config.webDriverOptions());
}
}
23 changes: 23 additions & 0 deletions owner/src/test/java/org/aeonbits/owner/StrSubstitutorTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,29 @@ public void testRecoursiveResolution() {
assertEquals("The quick brown fox jumped over the lazy dog.", resolvedString);
}

@Test
public void testNestedRecursiveResolution() {
Properties values = new Properties();
values.setProperty("environment", "dev");
values.setProperty("environments.dev.browser", "chrome");
values.setProperty("template", "environments.${environment}.webdriver.${environments.${environment}.browser}.switches.${environment}");
String templateString = "${template}";
StrSubstitutor sub = new StrSubstitutor(values);
String resolvedString = sub.replace(templateString);
assertEquals("environments.dev.webdriver.chrome.switches.dev", resolvedString);
}

@Test
public void testPropertyWithCurlyBracketsInName() {
Properties values = new Properties();
values.setProperty("{foo}", "bar");
values.setProperty("template", "foo.${{foo}}");
String templateString = "${template}";
StrSubstitutor sub = new StrSubstitutor(values);
String resolvedString = sub.replace(templateString);
assertEquals("foo.bar", resolvedString);
}

@Test
public void testMissingPropertyIsReplacedWithEmptyString() {
Properties values = new Properties() {{
Expand Down