You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@qpid.apache.org by ro...@apache.org on 2019/04/01 17:29:10 UTC

[qpid-jms] branch master updated: QPIDJMS-448: support simple variable expansion for JNDI config

This is an automated email from the ASF dual-hosted git repository.

robbie pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/qpid-jms.git


The following commit(s) were added to refs/heads/master by this push:
     new 74e0427  QPIDJMS-448: support simple variable expansion for JNDI config
74e0427 is described below

commit 74e0427f43049b357342d1a9e69b32027c59cf2a
Author: Robbie Gemmell <ro...@apache.org>
AuthorDate: Mon Apr 1 18:23:20 2019 +0100

    QPIDJMS-448: support simple variable expansion for JNDI config
---
 qpid-jms-client/pom.xml                            |  10 +
 .../qpid/jms/jndi/JmsInitialContextFactory.java    |  32 ++-
 .../apache/qpid/jms/util/VariableExpansion.java    | 137 ++++++++++
 .../jms/jndi/JmsInitialContextFactoryTest.java     | 133 +++++++++
 .../qpid/jms/util/VariableExpansionTest.java       | 302 +++++++++++++++++++++
 qpid-jms-docs/Configuration.md                     |   8 +-
 6 files changed, 613 insertions(+), 9 deletions(-)

diff --git a/qpid-jms-client/pom.xml b/qpid-jms-client/pom.xml
index a952fc4..6875d71 100644
--- a/qpid-jms-client/pom.xml
+++ b/qpid-jms-client/pom.xml
@@ -128,6 +128,16 @@
     </resources>
     <plugins>
       <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-surefire-plugin</artifactId>
+        <configuration>
+          <environmentVariables combine.children="append">
+            <!-- Env variable for use with VariableExpansionTest and JmsInitialContextFactoryTest -->
+            <VAR_EXPANSION_TEST_ENV_VAR>TestEnvVariableValue123</VAR_EXPANSION_TEST_ENV_VAR>
+          </environmentVariables>
+        </configuration>
+      </plugin>
+      <plugin>
         <groupId>org.apache.felix</groupId>
         <artifactId>maven-bundle-plugin</artifactId>
         <configuration>
diff --git a/qpid-jms-client/src/main/java/org/apache/qpid/jms/jndi/JmsInitialContextFactory.java b/qpid-jms-client/src/main/java/org/apache/qpid/jms/jndi/JmsInitialContextFactory.java
index 5588095..e55970c 100644
--- a/qpid-jms-client/src/main/java/org/apache/qpid/jms/jndi/JmsInitialContextFactory.java
+++ b/qpid-jms-client/src/main/java/org/apache/qpid/jms/jndi/JmsInitialContextFactory.java
@@ -40,6 +40,7 @@ import javax.naming.spi.InitialContextFactory;
 import org.apache.qpid.jms.JmsConnectionFactory;
 import org.apache.qpid.jms.JmsQueue;
 import org.apache.qpid.jms.JmsTopic;
+import org.apache.qpid.jms.util.VariableExpansion;
 
 public class JmsInitialContextFactory implements InitialContextFactory {
 
@@ -184,7 +185,7 @@ public class JmsInitialContextFactory implements InitialContextFactory {
                     value = String.valueOf(entry.getValue());
                 }
 
-                factories.put(factoryName, value);
+                factories.put(factoryName, expand(value, environment));
             }
         }
 
@@ -206,7 +207,8 @@ public class JmsInitialContextFactory implements InitialContextFactory {
             String key = String.valueOf(entry.getKey());
             if (key.toLowerCase().startsWith(CONNECTION_FACTORY_DEFAULT_KEY_PREFIX)) {
                 String jndiName = key.substring(CONNECTION_FACTORY_DEFAULT_KEY_PREFIX.length());
-                map.put(jndiName, String.valueOf(entry.getValue()));
+                String value = String.valueOf(entry.getValue());
+                map.put(jndiName, expand(value, environment));
             }
         }
 
@@ -224,7 +226,8 @@ public class JmsInitialContextFactory implements InitialContextFactory {
             if (key.toLowerCase().startsWith(CONNECTION_FACTORY_PROPERTY_KEY_PREFIX)) {
                 if(key.substring(CONNECTION_FACTORY_PROPERTY_KEY_PREFIX.length()).startsWith(factoryNameSuffix)) {
                     String propertyName = key.substring(CONNECTION_FACTORY_PROPERTY_KEY_PREFIX.length() + factoryNameSuffix.length());
-                    map.put(propertyName, String.valueOf(entry.getValue()));
+                    String value = String.valueOf(entry.getValue());
+                    map.put(propertyName, expand(value, environment));
                 }
             }
         }
@@ -238,7 +241,8 @@ public class JmsInitialContextFactory implements InitialContextFactory {
             String key = entry.getKey().toString();
             if (key.startsWith(QUEUE_KEY_PREFIX)) {
                 String jndiName = key.substring(QUEUE_KEY_PREFIX.length());
-                bindings.put(jndiName, createQueue(entry.getValue().toString()));
+                String value = expand(entry.getValue().toString(), environment);
+                bindings.put(jndiName, createQueue(value));
             }
         }
     }
@@ -249,7 +253,8 @@ public class JmsInitialContextFactory implements InitialContextFactory {
             String key = entry.getKey().toString();
             if (key.startsWith(TOPIC_KEY_PREFIX)) {
                 String jndiName = key.substring(TOPIC_KEY_PREFIX.length());
-                bindings.put(jndiName, createTopic(entry.getValue().toString()));
+                String value = expand(entry.getValue().toString(), environment);
+                bindings.put(jndiName, createTopic(value));
             }
         }
     }
@@ -284,4 +289,21 @@ public class JmsInitialContextFactory implements InitialContextFactory {
 
         return factory;
     }
+
+    protected static String expand(String input, Map<Object, Object> environment) {
+        return VariableExpansion.expand(input, variable -> {
+            String resolve = VariableExpansion.SYS_PROP_RESOLVER.resolve(variable);
+            if (resolve == null) {
+                resolve = VariableExpansion.ENV_VAR_RESOLVER.resolve(variable);
+                if (resolve == null) {
+                    Object o = environment.get(variable);
+                    if (o != null) {
+                        resolve = String.valueOf(o);
+                    }
+                }
+            }
+
+            return resolve;
+        });
+    }
 }
diff --git a/qpid-jms-client/src/main/java/org/apache/qpid/jms/util/VariableExpansion.java b/qpid-jms-client/src/main/java/org/apache/qpid/jms/util/VariableExpansion.java
new file mode 100644
index 0000000..f5093c5
--- /dev/null
+++ b/qpid-jms-client/src/main/java/org/apache/qpid/jms/util/VariableExpansion.java
@@ -0,0 +1,137 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+package org.apache.qpid.jms.util;
+
+import java.util.Map;
+import java.util.Stack;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public final class VariableExpansion {
+
+    @FunctionalInterface
+    public interface Resolver {
+        String resolve(String name);
+    }
+
+    private static final Pattern VARIABLE_OR_ESCAPE_PATTERN = Pattern.compile("(?:\\$\\{([^\\}]*)\\})|(?:\\$(\\$))");
+    private static final String ESCAPE = "$";
+
+    private VariableExpansion() {
+        // No instances
+    }
+
+    //================== Helper Resolvers ==================
+
+    public static final Resolver ENV_VAR_RESOLVER = prop -> System.getenv(prop);
+
+    public static final Resolver SYS_PROP_RESOLVER = prop -> System.getProperty(prop);
+
+    public static final class MapResolver implements Resolver {
+        private final Map<String, String> map;
+
+        public MapResolver(Map<String, String> map) {
+            this.map = map;
+        }
+
+        public String resolve(String name) {
+            return map.get(name);
+        }
+    }
+
+    //======================= Methods ======================
+
+    /**
+     * Expands any variables found in the given input string.
+     *
+     * @param input
+     *            the string to expand any variables in
+     * @param resolver
+     *            the resolver to use
+     * @return the expanded output
+     *
+     * @throws IllegalArgumentException
+     *             if an argument can't be expanded, e.g because a variable is not resolvable.
+     * @throws NullPointerException
+     *             if a resolver is not supplied
+     */
+    public static final String expand(String input, Resolver resolver) throws IllegalArgumentException, NullPointerException {
+        if(resolver == null) {
+            throw new NullPointerException("Resolver must be supplied");
+        }
+
+        if (input == null) {
+            return null;
+        }
+
+        return expand(input, resolver, new Stack<String>());
+    }
+
+    private static final String expand(String input, Resolver resolver, Stack<String> stack) {
+        Matcher matcher = VARIABLE_OR_ESCAPE_PATTERN.matcher(input);
+
+        StringBuffer result = null;
+        while (matcher.find()) {
+            if(result == null) {
+                result = new StringBuffer();
+            }
+
+            String var = matcher.group(1); // Variable match
+            if (var != null) {
+                matcher.appendReplacement(result, Matcher.quoteReplacement(resolve(var, resolver, stack)));
+            } else {
+                String esc = matcher.group(2); // Escape matcher
+                if (ESCAPE.equals(esc)) {
+                    matcher.appendReplacement(result, Matcher.quoteReplacement(ESCAPE));
+                } else {
+                    throw new IllegalArgumentException(esc);
+                }
+            }
+        }
+
+        if(result == null) {
+            // No match found, return the original input
+            return input;
+        }
+
+        matcher.appendTail(result);
+
+        return result.toString();
+    }
+
+    private static final String resolve(String var, Resolver resolver, Stack<String> stack) {
+        if (stack.contains(var)) {
+            throw new IllegalArgumentException(String.format("Recursively defined variable '%s', stack=%s", var, stack));
+        }
+
+        String result = resolver.resolve(var);
+        if (result == null) {
+            throw new IllegalArgumentException("Unable to resolve variable: " + var);
+        }
+
+        stack.push(var);
+        try {
+            return expand(result, resolver, stack);
+        } finally {
+            stack.pop();
+        }
+    }
+}
\ No newline at end of file
diff --git a/qpid-jms-client/src/test/java/org/apache/qpid/jms/jndi/JmsInitialContextFactoryTest.java b/qpid-jms-client/src/test/java/org/apache/qpid/jms/jndi/JmsInitialContextFactoryTest.java
index 9400ad5..7d4a7ad 100644
--- a/qpid-jms-client/src/test/java/org/apache/qpid/jms/jndi/JmsInitialContextFactoryTest.java
+++ b/qpid-jms-client/src/test/java/org/apache/qpid/jms/jndi/JmsInitialContextFactoryTest.java
@@ -17,9 +17,11 @@
 package org.apache.qpid.jms.jndi;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
 
 import java.io.File;
 import java.io.FileNotFoundException;
@@ -43,6 +45,10 @@ import org.junit.Test;
 
 public class JmsInitialContextFactoryTest extends QpidJmsTestCase {
 
+    // Environment variable name+value for test, configured in Surefire config
+    private static final String TEST_ENV_VARIABLE_NAME = "VAR_EXPANSION_TEST_ENV_VAR";
+    private static final String TEST_ENV_VARIABLE_VALUE = "TestEnvVariableValue123";
+
     private JmsInitialContextFactory factory;
     private Context context;
 
@@ -450,4 +456,131 @@ public class JmsInitialContextFactoryTest extends QpidJmsTestCase {
             f.delete();
         }
     }
+
+    @Test
+    public void testVariableExpansionUnresolvableVariable() throws Exception {
+        //Check exception is thrown for variable that doesn't resolve
+        String factoryName = "myFactory";
+        String unknownVariable = "unknownVariable";
+        String uri = "amqp://${"+ unknownVariable +"}:1234";
+
+        Hashtable<Object, Object> env = new Hashtable<Object, Object>();
+        env.put("connectionfactory." + factoryName, uri);
+
+        try {
+            createInitialContext(env);
+            fail("Expected to fail due to unresolved variable");
+        } catch (IllegalArgumentException iae) {
+            // Expected
+        }
+
+        String nowKnownHostValue = "nowKnownValue";
+
+        //Now make the variable resolve, check the exact same env+URI now works
+        setTestSystemProperty(unknownVariable, nowKnownHostValue);
+
+        Context ctx = createInitialContext(env);
+
+        Object o = ctx.lookup("myFactory");
+
+        assertNotNull("No object returned", o);
+        assertEquals("Unexpected class type for returned object", JmsConnectionFactory.class, o.getClass());
+
+        assertEquals("Unexpected URI for returned factory", "amqp://" + nowKnownHostValue + ":1234", ((JmsConnectionFactory) o).getRemoteURI());
+    }
+
+    @Test
+    public void testVariableExpansionConnectionFactory() throws Exception {
+        doVariableExpansionConnectionFactoryTestImpl(false);
+    }
+
+    @Test
+    public void testVariableExpansionConnectionFactoryWithEnvVar() throws Exception {
+        doVariableExpansionConnectionFactoryTestImpl(true);
+    }
+
+    private void doVariableExpansionConnectionFactoryTestImpl(boolean useEnvVarForHost) throws NamingException {
+        String factoryName = "myFactory";
+
+        String hostVariableName = useEnvVarForHost ? TEST_ENV_VARIABLE_NAME : "myHostVar";
+        String portVariableName = "myPortVar";
+        String clientIdVariableName = "myClientIDVar";
+        String hostVariableValue = useEnvVarForHost ? TEST_ENV_VARIABLE_VALUE : "myHostValue";
+        String portVariableValue= "1234";
+        String clientIdVariableValue= "myClientIDValue" + getTestName();
+        Object environmentProperty = "connectionfactory." + factoryName;
+
+        if(useEnvVarForHost) {
+            // Verify variable is set (by Surefire config),
+            // prevents spurious failure if not manually configured when run in IDE.
+            assertEquals("Expected to use env variable name", TEST_ENV_VARIABLE_NAME, hostVariableName);
+            assumeTrue("Environment variable not set as required", System.getenv().containsKey(TEST_ENV_VARIABLE_NAME));
+            assertEquals("Environment variable value not as expected", TEST_ENV_VARIABLE_VALUE, System.getenv(TEST_ENV_VARIABLE_NAME));
+        } else {
+            assertNotEquals("Expected to use a different name", TEST_ENV_VARIABLE_NAME, hostVariableName);
+
+            setTestSystemProperty(hostVariableName, hostVariableValue);
+        }
+        setTestSystemProperty(portVariableName, portVariableValue);
+
+        String uri = "amqp://${" + hostVariableName + "}:${" + portVariableName + "}?jms.clientID=${" + clientIdVariableName + "}";
+
+        Hashtable<Object, Object> env = new Hashtable<Object, Object>();
+        env.put(environmentProperty, uri);
+        env.put(clientIdVariableName, clientIdVariableValue);
+
+        Context ctx = createInitialContext(env);
+
+        Object o = ctx.lookup(factoryName);
+
+        assertNotNull("No object returned", o);
+        assertEquals("Unexpected class type for returned object", JmsConnectionFactory.class, o.getClass());
+
+        assertEquals("Unexpected ClientID for returned factory", clientIdVariableValue, ((JmsConnectionFactory) o).getClientID());
+
+        String expectedURI = "amqp://" + hostVariableValue + ":" + portVariableValue;
+        assertEquals("Unexpected URI for returned factory", expectedURI, ((JmsConnectionFactory) o).getRemoteURI());
+    }
+
+    @Test
+    public void testVariableExpansionQueue() throws Exception {
+        String lookupName = "myQueueLookup";
+        String variableName = "myQueueVariable";
+        String variableValue = "myQueueName";
+
+        Hashtable<Object, Object> env = new Hashtable<Object, Object>();
+        env.put("queue." + lookupName, "${" + variableName +"}");
+
+        setTestSystemProperty(variableName, variableValue);
+
+        Context ctx = createInitialContext(env);
+
+        Object o = ctx.lookup(lookupName);
+
+        assertNotNull("No object returned", o);
+        assertEquals("Unexpected class type for returned object", JmsQueue.class, o.getClass());
+
+        assertEquals("Unexpected name for returned queue", variableValue, ((JmsQueue) o).getQueueName());
+    }
+
+    @Test
+    public void testVariableExpansionTopic() throws Exception {
+        String lookupName = "myTopicLookup";
+        String variableName = "myTopicVariable";
+        String variableValue = "myTopicName";
+
+        Hashtable<Object, Object> env = new Hashtable<Object, Object>();
+        env.put("topic." + lookupName, "${" + variableName +"}");
+
+        setTestSystemProperty(variableName, variableValue);
+
+        Context ctx = createInitialContext(env);
+
+        Object o = ctx.lookup(lookupName);
+
+        assertNotNull("No object returned", o);
+        assertEquals("Unexpected class type for returned object", JmsTopic.class, o.getClass());
+
+        assertEquals("Unexpected name for returned queue", variableValue, ((JmsTopic) o).getTopicName());
+    }
 }
diff --git a/qpid-jms-client/src/test/java/org/apache/qpid/jms/util/VariableExpansionTest.java b/qpid-jms-client/src/test/java/org/apache/qpid/jms/util/VariableExpansionTest.java
new file mode 100644
index 0000000..d13aa89
--- /dev/null
+++ b/qpid-jms-client/src/test/java/org/apache/qpid/jms/util/VariableExpansionTest.java
@@ -0,0 +1,302 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+package org.apache.qpid.jms.util;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeTrue;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.qpid.jms.test.QpidJmsTestCase;
+import org.apache.qpid.jms.util.VariableExpansion.Resolver;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+public class VariableExpansionTest extends QpidJmsTestCase {
+
+    // Environment variable name+value for test, configured in Surefire config
+    private static final String TEST_ENV_VARIABLE_NAME = "VAR_EXPANSION_TEST_ENV_VAR";
+    private static final String TEST_ENV_VARIABLE_VALUE = "TestEnvVariableValue123";
+
+    private static final String TEST_ENV_VARIABLE_NAME_NOT_SET = "VAR_EXPANSION_TEST_ENV_VAR_NOT_SET";
+    private static final String ESCAPE = "$";
+
+    private String testNamePrefix;
+    private String testPropName;
+    private String testPropValue;
+    private String testVariableForExpansion;
+
+    @Before
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+
+        testNamePrefix = getTestName() + ".";
+
+        testPropName = testNamePrefix + "myPropKey";
+        testPropValue = testNamePrefix + "myPropValue";
+        testVariableForExpansion = "${" + testPropName + "}";
+    }
+
+    // ===== Resolver tests =====
+
+    @Test
+    public void testResolveWithSysPropResolver() {
+        Resolver sysPropResolver = VariableExpansion.SYS_PROP_RESOLVER;
+
+        assertNull("System property value unexpectedly set already", System.getProperty(testPropName));
+        assertNull("Expected resolve to return null as property not set", sysPropResolver.resolve(testPropName));
+
+        setTestSystemProperty(testPropName, testPropValue);
+
+        assertEquals("System property value not as expected", testPropValue, System.getProperty(testPropName));
+        assertEquals("Resolved variable not as expected", testPropValue, sysPropResolver.resolve(testPropName));
+    }
+
+    @Test
+    public void testResolveWithEnvVarResolver() {
+        // Verify variable is set (by Surefire config),
+        // prevents spurious failure if not manually configured when run in IDE.
+        assumeTrue("Environment variable not set as required", System.getenv().containsKey(TEST_ENV_VARIABLE_NAME));
+        assumeFalse("Environment variable unexpectedly set", System.getenv().containsKey(TEST_ENV_VARIABLE_NAME_NOT_SET));
+
+        assertEquals("Environment variable value not as expected", TEST_ENV_VARIABLE_VALUE, System.getenv(TEST_ENV_VARIABLE_NAME));
+
+        final Resolver envVarResolver = VariableExpansion.ENV_VAR_RESOLVER;
+
+        assertNull("Expected resolve to return null as property not set", envVarResolver.resolve(TEST_ENV_VARIABLE_NAME_NOT_SET));
+
+        assertEquals("Resolved variable not as expected", TEST_ENV_VARIABLE_VALUE, envVarResolver.resolve(TEST_ENV_VARIABLE_NAME));
+    }
+
+    // ===== Expansion tests =====
+
+    @Test
+    public void testExpandWithResolverNotProvided() {
+        try {
+            VariableExpansion.expand("no-expansion-needed", null);
+            fail("Should have failed to expand,resolver not given");
+        } catch (NullPointerException npe) {
+            // Expected
+        }
+    }
+
+    @Test
+    public void testExpandNull() {
+        assertNull("Expected null", VariableExpansion.expand(null, variable -> "foo"));
+    }
+
+    @Test
+    public void testExpandWithSysPropResolver() {
+        final Resolver resolver = VariableExpansion.SYS_PROP_RESOLVER;
+
+        try {
+            VariableExpansion.expand(testVariableForExpansion, resolver);
+            fail("Should have failed to expand, property not set");
+        } catch (IllegalArgumentException iae) {
+            // Expected
+        }
+
+        setTestSystemProperty(testPropName, testPropValue);
+
+        assertEquals("System property value not as expected", testPropValue, System.getProperty(testPropName));
+
+        String expanded = VariableExpansion.expand(testVariableForExpansion, resolver);
+        assertEquals("Expanded variable not as expected", testPropValue, expanded);
+    }
+
+    @Test
+    public void testExpandWithEnvVarResolver() {
+        // Verify variable is set (by Surefire config),
+        // prevents spurious failure if not manually configured when run in IDE.
+        assumeTrue("Environment variable not set as required", System.getenv().containsKey(TEST_ENV_VARIABLE_NAME));
+
+        assertEquals("Environment variable value not as expected", TEST_ENV_VARIABLE_VALUE, System.getenv(TEST_ENV_VARIABLE_NAME));
+
+        final Resolver resolver = VariableExpansion.ENV_VAR_RESOLVER;
+
+        try {
+            VariableExpansion.expand("${" + TEST_ENV_VARIABLE_NAME + "_NOT_SET" + "}", resolver);
+            fail("Should have failed to expand unset env variable");
+        } catch (IllegalArgumentException iae) {
+            // Expected
+        }
+
+        String expanded = VariableExpansion.expand("${" + TEST_ENV_VARIABLE_NAME + "}", resolver);
+
+        assertEquals("Expanded variable not as expected", TEST_ENV_VARIABLE_VALUE, expanded);
+    }
+
+    @Test
+    public void testExpandBasicWithMapResolver() {
+        Map<String,String> propsMap = new HashMap<>();
+        propsMap.put(testPropName, testPropValue);
+        Resolver resolver = new VariableExpansion.MapResolver(propsMap);
+
+        try {
+            VariableExpansion.expand("${" + testNamePrefix + "-not-set" + "}", resolver);
+            fail("Should have failed to expand, property not set");
+        } catch (IllegalArgumentException iae) {
+            // Expected
+        }
+
+        String expanded = VariableExpansion.expand(testVariableForExpansion, resolver);
+
+        assertEquals("Expanded variable not as expected", testPropValue, expanded);
+    }
+
+    @Test
+    public void testExpandBasic() {
+        // Variable is the full input
+        doBasicExpansionTestImpl(testVariableForExpansion, testPropValue);
+
+        // Variable trails a prefix
+        String prefix = "prefix";
+        doBasicExpansionTestImpl(prefix + testVariableForExpansion, prefix + testPropValue);
+
+        // Variable precedes a suffix
+        String suffix = "suffix";
+        doBasicExpansionTestImpl(testVariableForExpansion + suffix, testPropValue + suffix);
+
+        // Variable is between prefix and suffix
+        doBasicExpansionTestImpl(prefix + testVariableForExpansion + suffix, prefix + testPropValue + suffix);
+    }
+
+    @Test
+    public void testExpandMultipleVariables() {
+        String propName2 = "propName2";
+        String propValue2 = "propValue2";
+        String propName3 = "propName3";
+        String propValue3 = "propValue3";
+
+        Map<String,String> propsMap = new HashMap<>();
+        propsMap.put(testPropName, testPropValue);
+        propsMap.put(propName2, propValue2);
+        propsMap.put(propName3, propValue3);
+
+        Resolver resolver = new VariableExpansion.MapResolver(propsMap);
+
+        // Variables are the full input
+        String toExpand = testVariableForExpansion + "${" + propName2 +"}${" + propName3 + "}";
+        String expected = testPropValue + propValue2 + propValue3;
+
+        doBasicExpansionTestImpl(toExpand, expected, resolver);
+
+        // Variable internal to overall input
+        toExpand = "prefix" + testVariableForExpansion + "-foo-${" + propName2 +"}-bar-${" + propName3 + "}" + "suffix";
+        expected = "prefix" + testPropValue + "-foo-" + propValue2 +"-bar-" + propValue3 + "suffix";
+
+        doBasicExpansionTestImpl(toExpand, expected, resolver);
+    }
+
+    @Test
+    public void testExpandMultipleInstancesOfVariable() {
+        Map<String,String> propsMap = new HashMap<>();
+        propsMap.put(testPropName, testPropValue);
+
+        Resolver resolver = new VariableExpansion.MapResolver(propsMap);
+
+        String toExpand = "1-" + testVariableForExpansion + "2-" + testVariableForExpansion + "3-" + testVariableForExpansion;
+        String expected = "1-" + testPropValue + "2-" + testPropValue + "3-" + testPropValue;
+
+        doBasicExpansionTestImpl(toExpand, expected, resolver);
+    }
+
+    @Test
+    public void testExpandRecursiveThrows() {
+        String propName1 = "propName1";
+        String propName2 = "propName2";
+        String propValue1 = "propValue1-${" + propName2 + "}";
+        String propValue2 = "recursive-${" + propName1 + "}";
+
+        Map<String,String> propsMap = new HashMap<>();
+        propsMap.put(propName1, propValue1);
+        propsMap.put(propName2, propValue2);
+        try {
+            VariableExpansion.expand("${" + propName1 + "}", new VariableExpansion.MapResolver(propsMap));
+            fail("Expected exception to be thrown");
+        } catch (IllegalArgumentException iae) {
+            // Expected
+        }
+    }
+
+    @Test
+    public void testExpandWithoutVariable() {
+        doBasicExpansionTestImpl("no-expansion-needed", "no-expansion-needed");
+        doBasicExpansionTestImpl(ESCAPE + "no-expansion-needed", ESCAPE + "no-expansion-needed");
+        doBasicExpansionTestImpl("no-expansion-needed" + ESCAPE, "no-expansion-needed" + ESCAPE);
+        doBasicExpansionTestImpl(ESCAPE + "no-expansion-needed" + ESCAPE, ESCAPE + "no-expansion-needed" + ESCAPE);
+        doBasicExpansionTestImpl("no" + ESCAPE + "-expansion-needed", "no" + ESCAPE + "-expansion-needed");
+    }
+
+    @Test
+    public void testExpandSkipsEscapedVariables() {
+        doBasicExpansionTestImpl(ESCAPE + testVariableForExpansion, testVariableForExpansion);
+
+        String prefix = "prefix";
+        doBasicExpansionTestImpl(prefix + ESCAPE + testVariableForExpansion, prefix + testVariableForExpansion);
+
+        String suffix = "suffix";
+        doBasicExpansionTestImpl(ESCAPE + testVariableForExpansion + suffix, testVariableForExpansion + suffix);
+ 
+        doBasicExpansionTestImpl(prefix + ESCAPE + testVariableForExpansion + suffix, prefix + testVariableForExpansion + suffix);
+    }
+
+
+    private void doBasicExpansionTestImpl(String toExpand, String expectedExpansion) {
+        final Resolver mockResolver = Mockito.mock(Resolver.class);
+
+        Mockito.when(mockResolver.resolve(testPropName)).thenReturn(testPropValue);
+
+        doBasicExpansionTestImpl(toExpand, expectedExpansion, mockResolver);
+    }
+
+    private void doBasicExpansionTestImpl(String toExpand, String expectedExpansion, Resolver resolver) {
+        String expanded = VariableExpansion.expand(toExpand, resolver);
+        assertEquals("Expanded variable not as expected", expectedExpansion, expanded);
+    }
+
+    @Test
+    public void testExpandFailsToResolve() {
+        doBasicExpansionTestImpl(testVariableForExpansion);
+    }
+
+    private void doBasicExpansionTestImpl(String toExpand) {
+        final Resolver mockResolver = Mockito.mock(Resolver.class);
+
+        // Check when resolution fails
+        Mockito.when(mockResolver.resolve(testPropName)).thenReturn(null);
+        try {
+            VariableExpansion.expand(toExpand, mockResolver);
+            fail("Should have failed to expand, property not resolve");
+        } catch (IllegalArgumentException iae) {
+            // Expected
+        }
+
+        Mockito.verify(mockResolver).resolve(testPropName);
+        Mockito.verifyNoMoreInteractions(mockResolver);
+    }
+}
diff --git a/qpid-jms-docs/Configuration.md b/qpid-jms-docs/Configuration.md
index ee51c44..7d9b520 100644
--- a/qpid-jms-docs/Configuration.md
+++ b/qpid-jms-docs/Configuration.md
@@ -43,11 +43,11 @@ Applications use a JNDI InitialContext, itself obtained from an InitialContextFa
 
 The property syntax used in the properties file or environment Hashtable is as follows:
 
-+   To define a ConnectionFactory, use format: *connectionfactory.lookupName = URI*
-+   To define a Queue, use format: *queue.lookupName = queueName*
-+   To define a Topic use format: *topic.lookupName = topicName*
++   To define a ConnectionFactory, use format: *connectionfactory.&lt;lookup-name&gt; = &lt;connection-uri&gt;*
++   To define a Queue, use format: *queue.&lt;lookup-name&gt; = &lt;queue-name&gt;*
++   To define a Topic use format: *topic.&lt;lookup-name&gt; = &lt;topic-name&gt;*
 
-For more details of the Connection URI, see the next section.
+The property values which which constitute the connection URI, queue name, or topic name can also utilise simple *${variable}* expansion resolved in order from system properties, environment variables, or the properties file / environment Hashtable. For more details of the Connection URI, see the next section.
 
 As an example, consider the following properties used to define a ConnectionFactory, Queue, and Topic:
 


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@qpid.apache.org
For additional commands, e-mail: commits-help@qpid.apache.org