If you are working in a Spring environment, you can delegate your problem to the existing PropertyResolvers:
How to resolve property placeholder in spring
I had the same problem as you described a long time ago and had to solve it without Spring. As you already recognized, the main difficulties are recursive and circular dependencies. So the code is a bit longer, but still works after 15 years. Nevertheless I raised it to Java 8 level.
This is the main class:
package config;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public class ConfigResolver {
private static final String CYCLE_MARKER = "#CYCLE?";
private static final String PARAM_START = "${";
private static final String PARAM_END = "}";
/**
* Creates a now config map with all references resolved.
*
* @throws IllegalStateException
* In case of undefined or circular references.
*/
public Map<String, String> resolve(Map<String, String> config) throws IllegalStateException {
final Map<String, String> result = new LinkedHashMap<>();
config.keySet().stream().forEach(key -> resolve(key, config, result));
return result;
}
/**
* Copies the given key and its value from the source map into the target map.
* If there are references to other keys, those will be resolved recursively and
* copied as well. References to System properties are also valid. If the value
* is already present, nothing will happen.
*
* @throws IllegalStateException
* In case of undefined or circular references.
*/
private String resolve(String key, Map<String, String> source, Map<String, String> target)
throws IllegalStateException {
String value = target.get(key);
if (value == CYCLE_MARKER) {
throw new IllegalStateException("Circular reference for key:" + key);
}
if (value != null) {
return value;
}
value = source.get(key);
if (value == null) {
return System.getProperty(key);
}
target.put(key, CYCLE_MARKER);
final List<Parameter> params = parseParams(value);
if (!params.isEmpty()) {
final StringBuilder resolvedValue = new StringBuilder(value);
int deviation = 0;
for (Parameter p : params) {
final String v = resolve(p.getName(), source, target);
if (v == null) {
throw new IllegalStateException("Undefined parameter: " + p.getName());
}
resolvedValue.replace(p.getStart() + deviation, p.getEnd() + deviation, v);
deviation += v.length() - p.getEnd() + p.getStart();
}
value = resolvedValue.toString();
}
target.put(key, value);
return value;
}
/**
* Extracts all parameters from the given String
*/
private List<Parameter> parseParams(String value) {
final List<Parameter> result = new ArrayList<Parameter>();
int start = 0;
int end = 0;
while (start >= 0) {
start = value.indexOf(PARAM_START, end);
end = value.indexOf(PARAM_END, start) + PARAM_END.length();
if (start >= 0 && end > start + 1) {
final String name = value.substring(start + PARAM_START.length(), end - PARAM_END.length());
result.add(new Parameter(name, start, end));
}
}
return result;
}
/**
* Parameter with position in String
*/
private static class Parameter {
private final String name;
private final int start;
private final int end;
Parameter(String name, int start, int end) {
this.start = start;
this.end = end;
this.name = name;
}
public String getName() {
return name;
}
public int getStart() {
return start;
}
public int getEnd() {
return end;
}
}
}
And here is a JUnit test that covers your example:
package config;
import static org.junit.Assert.assertEquals;
import java.util.LinkedHashMap;
import java.util.Map;
import org.junit.Test;
public class ConfigResolverTest {
private ConfigResolver sut = new ConfigResolver();
@Test
public void testResolve() {
System.setProperty("PID", "1234");
final Map<String, String> raw = new LinkedHashMap<>();
raw.put("USER_NAME", "TESTUSER");
raw.put("USER_HOME", "C:/USERS/${USER_NAME}");
raw.put("TEST_DIR", "${USER_HOME}/IN");
raw.put("TEST", "${TEST_DIR}/${PID}");
final Map<String, String> resolved = sut.resolve(raw);
assertEquals("C:/USERS/TESTUSER/IN/1234", resolved.get("TEST"));
assertEquals(4, resolved.size());
}
@Test(expected = IllegalStateException.class)
public void testResolve_Circular() {
final Map<String, String> raw = new LinkedHashMap<>();
raw.put("first", "${second}");
raw.put("second", "${third}");
raw.put("third", "${first}");
sut.resolve(raw);
}
@Test(expected = IllegalStateException.class)
public void testResolve_Undefined() {
final Map<String, String> raw = new LinkedHashMap<>();
raw.put("first", "${second}");
sut.resolve(raw);
}
}