1

I am working on an application that should be highly configurable.

The goal is to have an XML-file that stores its configuration. Inside the configuration "variable" elements can be defined, which can be reused throughout the whole configuration.

Here's an example:

<var name = "QUEUE_PREFIX"      value = "TEST/QUEUE/PREFIX"   />
<var name = "IN_QUEUE-NAME"     value = "${QUEUE_PREFIX}/IN"  />
<var name = "OUT_QUEUE-NAME"    value = "${QUEUE_PREFIX}/OUT" />

<mq-client IN-QUEUE =  "${IN_QUEUE_NAME}" 
           OUT-QUEUE = "${OUT_QUEUE_NAME}"/>

which should result in

<var name = "QUEUE_PREFIX" value = "TEST/QUEUE/PREFIX"     />
<var name = "IN_QUEUE"     value = "TEST/QUEUE/PREFIX/IN"  />
<var name = "OUT_QUEUE"    value = "TEST/QUEUE/PREFIX/OUT" />

<mq-client IN-QUEUE =  "TEST/QUEUE/PREFIX/IN" 
           OUT-QUEUE = "TEST/QUEUE/PREFIX/OUT"/>

This kind of replacement is easy and already works as intended in my prototype. It gets difficult once there is a whole array and multiple "layers" of variables that are referenced. For example a variables references a variables which also already references a variable.

for example:

<var name = "USER_NAME"         value = "TESTUSER"   />
<var name = "USER_HOME"         value = "C:\USERS\${USER_NAME}"   />
<var name = "TEST_DIR"          value = "${USER_HOME}/IN"  />
<var name = "TEST"              value = "${TEST_DIR}/${PID}" />

In this case the application would have to determine / resolve which of these variables it would have to replace first, in order that the others don't get messed up.

And of course there are other issues like, what do we do if two variables reference each other?

My Question

Has anyone done anything similar to this and how did you solve it? Is there a framework, library or something that is capable of resolving configurations with such variables?

Sirop
  • 57
  • 10

3 Answers3

1

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);
    }

}
oliver_t
  • 968
  • 3
  • 10
0

You can try Apache commons-configuration

From their example

application.name = Killer App
application.version = 1.6.2

application.title = ${application.name} ${application.version}

The interpolated string is application.title = Killer App 1.6.2

Mạnh Quyết Nguyễn
  • 17,677
  • 1
  • 23
  • 51
0

Maybe something simple like this:

Map<String,String> vars = new HashMap<>();
vars.put("USER_NAME", "TESTUSER");
vars.put("USER_HOME", "C:/USERS/${USER_NAME}");
vars.put("TEST_DIR", "${USER_HOME}/IN");
vars.put("TEST", "${TEST_DIR}/${PID}");
  1. Then sort the values that reference values and those that don't (lets call them literals).
  2. If there are no new literals found, then there are circular references and you must stop.
  3. If there are no items with references then you're done.

  4. Otherwise, go through all the ones with references and replace the variables with all the literals possible. Replace the values with references with the substitions and update the Map.

  5. Go to step 1.

In a way this is breadth first search that propagates by levels until it gets to the end by doing substitutions.

AminM
  • 822
  • 4
  • 11