Repository: Vantiq/vantiq-extension-sources (language: python)

Issue:
CamelConnector:  Support supplying property values for property placeholders
Apache Camel routes support the notion of property placeholders, indicated by double curly brackets, _e.g._, `{{placeholder}}`. At runtime, values can be supplied for these, and the Camel runtime will fill them in in the route specification.

Making these available allows for better reuse of existing route descriptions.


Discussion / hints:

Fix patch:
diff --git a/camelConnector/README.md b/camelConnector/README.md
index d8f6f540..b68bb0c1 100644
--- a/camelConnector/README.md
+++ b/camelConnector/README.md
@@ -132,11 +132,48 @@ The Configuration document may look similar to the following example:
 
 ### Options Available for camelRuntime
 
-*   **appName**: Optional -- name of the app.  If not provided, default to _camelConnectorApp_.
-*   **routesDocument** or **routesList** and **routesFormat**: Required. This is list of Camel routes to run.
+* **appName**: Optional -- name of the app.  If not provided, default to _camelConnectorApp_.
+* **routesDocument** or **routesList** and **routesFormat**: Required. This is the list of Camel routes to run.
     * **routesDocument** -- the name of a Vantiq Document containing the XML or YAML definition of the routes to run. The format of the document (XML or YAML) will be determined from the file name (_e.g._, `routes.xml`)
     * **routesList** and **routesFormat** -- the XML or YAML specification of the Camel routes to run.  The **routesList** property should contain the appropriate text, and the **routesFormat** should contain either `xml` or `yml` indicating the style used to specify the routes.
-    * *Note*: You must specify either the **routesDocument** or **routesList** _AND_ **routesFormat** properties, but not both.
+      * *Note*: You must specify either the **routesDocument** or **routesList** _AND_ **routesFormat** properties, but not both.
+* **componentProperties** -- a list of JSON objects which contain the component name & associated properties. These are used by some components for their configuration and/or operation. Values here may include security information (clientId or tokens).
+    * The format of this values is a list of JSON objects, where each JSON object contains a **componentName** property containing the name of the component for which these properties are to be applied, and **componentProperties** containing a Json object consisting of property names and values.
+    * For example, if we needed to define the `clientId` and `securityToken` properties for the `example` component, we would add the following to the `camelRuntime` section of the source configuration.
+  
+        ```json
+        {
+            "camelRuntime": {
+            ...
+            "componentProperties": [
+                {
+                    "componentName": "example",
+                    "componentProperties": {
+                        "clientId": "me@example.com",
+                        "securityToken": "someExampleToken..."
+                    }
+                }
+           ],
+           ...
+           }    
+        }
+        ```
+
+* **propertyValues** -- a JSON object where each property specifies the name and value to be provided for the Apache Camel property placeholder. [Property placeholders](https://camel.apache.org/manual/using-propertyplaceholder.html) can be used in Camel routes to supply values at the route configuration time. 
+    * For example, to provide values for the properties `name` and `company`, you would add the following the the `camelRuntime` section.
+  
+    ```json
+    {
+        "camelRuntime": {
+            ...
+            "propertyValues": {
+                "name": "fred",
+                "company": "vantiq"
+            }
+            ...
+        }
+   }
+   ```
 
 ### Options available for general
 
@@ -199,7 +236,7 @@ Messages are sent to the source as _notifications_, and are delivered as _events
 Messages sent to Vantiq from the component/connector will
 arrive as VAIL objects, where the property names correspond to the Map keys.
 The Vantiq Component expects messages in an exchange to arrive in the form of a Java Map.  However, messages arriving
-in Json format will be accepted as well.
+in JSON format will be accepted as well.
 When constructing your Camel Application, you must keep this in mind.
 
 ## Messages from the Source
diff --git a/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/connector/CamelHandleConfiguration.java b/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/connector/CamelHandleConfiguration.java
index 5e13d3e5..fd5557c6 100644
--- a/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/connector/CamelHandleConfiguration.java
+++ b/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/connector/CamelHandleConfiguration.java
@@ -27,6 +27,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Properties;
 
 
 /**
@@ -59,6 +60,7 @@ public class CamelHandleConfiguration extends Handler<ExtensionServiceMessage> {
     public static final String ROUTES_LIST = "routesList";
     public static final String ROUTES_FORMAT = "routesFormat";
     public static final String COMPONENT_PROPERTIES = "componentProperties";
+    public static final String PROPERTY_VALUES = "propertyValues";
     
     public static final String VANTIQ = "vantiq";
     
@@ -130,6 +132,38 @@ public void handleMessage(ExtensionServiceMessage message) {
         if (gotList && !(target instanceof String)) {
             log.error("Camel connector with the {} property requires the {} property as well.",
                       ROUTES_LIST, ROUTES_FORMAT);
+            failConfig();
+            return;
+        }
+        
+        target = camelConfig.get(PROPERTY_VALUES);
+        if (target != null) {
+            if (!(target instanceof Map)) {
+                log.error("Camel connector property {} should be a simple JSON object (found {}).",
+                          PROPERTY_VALUES, target.getClass().getName());
+                failConfig();
+                return;
+            } else {
+                // It's coming from JSON, so the key must be a string...
+                Map<String, Object> input = (Map<String, Object>) target;
+                Properties propVals = new Properties(input.size());
+                input.forEach( (k, v) -> {
+                    if (!(k != null && v instanceof String)) {
+                        String kclass = "null";
+                        String vclass = "null";
+                        if (k != null) {
+                            kclass = k.getClass().getName();
+                        }
+                        if (v != null && v.getClass() != null) {
+                            vclass = v.getClass().getName();
+                        }
+                        log.error("Camel connector {} value must have a String key ({}: {})  and value ({}: {}).",
+                                  PROPERTY_VALUES, k, kclass, v, vclass);
+                        failConfig();
+                    }
+                });
+                
+            }
         }
         
         target = camelConfig.get(COMPONENT_PROPERTIES);
@@ -221,6 +255,16 @@ public boolean runCamelApp(Map<String, Object> camelConfig, Map<String, Object>
             List<Map<String, Object>> componentProperties =
                     (List<Map<String, Object>>) camelConfig.get(COMPONENT_PROPERTIES);
     
+            // This is checked in the caller
+            //noinspection unchecked
+            Map<String, String> input = (Map<String, String>) camelConfig.get(PROPERTY_VALUES);
+            // Camel wants these as a Java Properties object, so perform the conversion as required.
+            Properties propVals = null;
+            if (input != null) {
+                propVals = new Properties(input.size());
+                input.forEach(propVals::setProperty);
+            }
+    
             // If we get this far, then we're ready to run start from the new configuration.  If we have an old
             // configuration running, we'll need to shut it down first.
     
@@ -247,7 +291,7 @@ public boolean runCamelApp(Map<String, Object> camelConfig, Map<String, Object>
             CamelRunner runner =
                          new CamelRunner(appName, Objects.requireNonNull(routeSpec).get(ROUTES_LIST),
                                          routeSpec.get(ROUTES_FORMAT), repoList,
-                                         componentCache, componentLib, componentProperties);
+                                         componentCache, componentLib, componentProperties, propVals);
             if (additionalLibraries != null) {
                 runner.setAdditionalLibraries(additionalLibraries);
             }
diff --git a/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/discover/CamelDiscovery.java b/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/discover/CamelDiscovery.java
index 98f39dad..17991708 100644
--- a/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/discover/CamelDiscovery.java
+++ b/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/discover/CamelDiscovery.java
@@ -19,6 +19,7 @@
 import java.io.InputStream;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Properties;
 import java.util.Set;
 import java.util.StringTokenizer;
 
@@ -34,10 +35,7 @@ public class CamelDiscovery {
     public static final String COMPONENTS_TO_LOAD = "componentsToLoad";
     public static final String SYSTEM_DATAFORMATS = "systemDataFormats";
     public static final String DATAFORMATS_TO_LOAD = "dataFormatsToLoad";
-    
-    
     public static final String VANTIQ_COMPONENT_SCHEME = "vantiq";
-    
     private static final String COMPONENTS_KEY_NAME = "components";
     private static final String DATAFORMATS_KEY_NAME = "dataformats";
     private static final String CAMEL_VERSION_KEY_NAME = "camelVersion";
@@ -49,7 +47,8 @@ public class CamelDiscovery {
     /**
      * Determine the set of components used in the routes in a route builder.
      *
-     ** @param rb RouteBuilder containing the set of routes from which to discover components
+     * @param rb RouteBuilder containing the set of routes from which to discover components
+     * @param propertyValues Properties set of properties, keyed by name, to provide for the route
      *
      * @return Map<String, Set<String>> containing two Set entries:
      *          systemComponents -- (informational) and
@@ -57,7 +56,8 @@ public class CamelDiscovery {
      *
      * @throws DiscoveryException when exceptions encountered during processing.
      */
-    Map<String, Set<String>> performComponentDiscovery(RouteBuilder rb) throws DiscoveryException {
+    Map<String, Set<String>> performComponentDiscovery(RouteBuilder rb, Properties propertyValues)
+            throws DiscoveryException {
         
         try (DefaultCamelContext ctx = new DefaultCamelContext()) {
             ExtendedCamelContext ectx = ctx.adapt(ExtendedCamelContext.class);
@@ -67,9 +67,15 @@ Map<String, Set<String>> performComponentDiscovery(RouteBuilder rb) throws Disco
             ectx.setDataFormatResolver(edfr);
             EnumeratingConfigurerResolver epcr = new EnumeratingConfigurerResolver();
             ectx.setConfigurerResolver(epcr);
+            
+            // If our current context has local properties, we'll need to provide them to the context we create for
+            // discovery, since things like the URLs could be defined in properties, and that's used for discovery.
+            // We define a new context for use here since we're overriding all the resolvers with those specially
+            // designed for discovery.
+            ectx.getPropertiesComponent().setLocalProperties(propertyValues);
+
             log.debug("Discovering Camel (version {}) components using context: {}", ectx.getVersion(), ectx.getName());
     
-            assert ectx.getComponentResolver() instanceof EnumeratingComponentResolver;
             try {
                 ectx.addRoutes(rb);
                 ectx.start();
@@ -97,6 +103,7 @@ Map<String, Set<String>> performComponentDiscovery(RouteBuilder rb) throws Disco
             retVal.put(SYSTEM_DATAFORMATS, edfr.getSystemDataFormatsUsed());
             retVal.put(DATAFORMATS_TO_LOAD, edfr.getDataFormatsToLoad());
             return retVal;
+            // Note that ectx will be closed via the try-with-resources construct
         } catch (Exception e) {
             log.error("Trapped exception preparing or completing component discovery", e);
             throw new DiscoveryException("Exception preparing or completing component discovery processing", e);
diff --git a/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/discover/CamelRunner.java b/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/discover/CamelRunner.java
index b9f2ac60..f62a2b7a 100644
--- a/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/discover/CamelRunner.java
+++ b/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/discover/CamelRunner.java
@@ -35,6 +35,7 @@
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Properties;
 import java.util.Set;
 
 @Slf4j
@@ -47,6 +48,7 @@ public class CamelRunner extends MainSupport implements Closeable {
     
     private final List<Map<String, Object>> initComponents;
     
+    private final Properties camelProperties;
     private final File ivyCache;
     private final File loadedLibraries;
     
@@ -70,16 +72,19 @@ public class CamelRunner extends MainSupport implements Closeable {
      *
      * @param appName String Name for this instance
      * @param routeSpecification String specification for the route(s) to run.  Syntax must match
-     *                                  routeSpecificationType
+     *         routeSpecificationType
      * @param routeSpecificationType String the type of specification provided
      * @param repos List<URI> The list of repos to search for libraries needed by the route(s)
      * @param cacheDirectory String Directory path to use to cache downloaded libraries
      * @param loadedLibDir String Directory path into which to put the libraries needed at run time.
-     * @param initComponents List<Map<String, Object>> List of component names that need
-     *                          initialization using the properties included herein
+     * @param initComponents List<Map<String, Object>> List of component names that need initialization using
+     *         the properties included herein
+     * @param camelProperties Properties set of general property values that Camel can use for property
+     *         resolution
      */
     public CamelRunner(String appName, String routeSpecification, String routeSpecificationType, List<URI> repos,
-                       String cacheDirectory, String loadedLibDir, List<Map<String, Object>> initComponents) {
+                       String cacheDirectory, String loadedLibDir, List<Map<String, Object>> initComponents,
+                       Properties camelProperties) {
         super();
         this.registry = new MainRegistry();
         this.appName = appName;
@@ -92,6 +97,7 @@ public CamelRunner(String appName, String routeSpecification, String routeSpecif
         this.routeSpecType = routeSpecificationType.equals("yml") ? "yaml" : routeSpecificationType;
         this.additionalLibraries = null;
         this.initComponents = initComponents;
+        this.camelProperties = camelProperties;
         mainConfigurationProperties.setRoutesCollectorEnabled(false);
     }
     
@@ -105,9 +111,11 @@ public CamelRunner(String appName, String routeSpecification, String routeSpecif
      * @param loadedLibDir String Directory path into which to put the libraries needed at run time.
      * @param initComponents List<Map<String, Object>> List of component names that need
      *                          initialization using the properties included herein
+     * @param camelProperties Properties set of general property values that Camel can use for property resolution
      */
     CamelRunner(String appName, RouteBuilder routeBuilder, List<URI> repos,
-                String cacheDirectory, String loadedLibDir, List<Map<String, Object>> initComponents) {
+                String cacheDirectory, String loadedLibDir, List<Map<String, Object>> initComponents,
+                Properties camelProperties) {
         super();
         this.registry = new MainRegistry();
         this.appName = appName;
@@ -119,6 +127,7 @@ public CamelRunner(String appName, String routeSpecification, String routeSpecif
         this.routeSpec = null;
         this.additionalLibraries = null;
         this.initComponents = initComponents;
+        this.camelProperties = camelProperties;
     
         mainConfigurationProperties.setRoutesBuilders(List.of(routeBuilder));
         mainConfigurationProperties.setRoutesCollectorEnabled(false);
@@ -153,7 +162,7 @@ private ClassLoader constructClassLoader() throws Exception {
             throw new IllegalStateException("Camel context does not exist.");
         }
         CamelDiscovery discoverer = new CamelDiscovery();
-        Map<String, Set<String>> discResults = discoverer.performComponentDiscovery(routeBuilder);
+        Map<String, Set<String>> discResults = discoverer.performComponentDiscovery(routeBuilder, camelProperties);
         
         // Now, generate the component list to load...
         // We use a set to avoid duplicates, but we want things resolved in the order they are found in the
@@ -355,6 +364,13 @@ protected ProducerTemplate findOrCreateCamelTemplate() {
      */
     @Override
     protected void beforeStart() throws Exception {
+    
+        // Set any property values provided in the configuration.  These will be used in lieu of others in the classpath
+        // This method is tolerant being passed a null value, and will do the right thing. This needs to be done
+        // before the class loader is constructed since some properties may be used as things like the URL which may
+        // be used at class-load time.
+        camelContext.getPropertiesComponent().setLocalProperties(this.camelProperties);
+    
         if (routeBuilder == null) {
             if (StringUtils.isEmpty(routeSpec) || StringUtils.isEmpty(routeSpecType)) {
                 log.error("No routes have been specified to run.  Either {} or {} and {} are " +
@@ -379,6 +395,7 @@ protected void beforeStart() throws Exception {
                 }
             }
         }
+        
         // Some routes may have components (e.g., Salesforce) that need specific configuration.
         // If that's the case, do that now before we start our route(s).
         if (initComponents != null && initComponents.size() > 0) {


Test patch:
diff --git a/camelConnector/src/test/java/io/vantiq/extsrc/camelconn/connector/VantiqConfigurationTest.java b/camelConnector/src/test/java/io/vantiq/extsrc/camelconn/connector/VantiqConfigurationTest.java
index d2a36332..dcfd1af5 100644
--- a/camelConnector/src/test/java/io/vantiq/extsrc/camelconn/connector/VantiqConfigurationTest.java
+++ b/camelConnector/src/test/java/io/vantiq/extsrc/camelconn/connector/VantiqConfigurationTest.java
@@ -8,6 +8,7 @@
 import static io.vantiq.extsrc.camelconn.connector.CamelHandleConfiguration.COMPONENT_LIB;
 import static io.vantiq.extsrc.camelconn.connector.CamelHandleConfiguration.COMPONENT_PROPERTIES;
 import static io.vantiq.extsrc.camelconn.connector.CamelHandleConfiguration.GENERAL;
+import static io.vantiq.extsrc.camelconn.connector.CamelHandleConfiguration.PROPERTY_VALUES;
 import static io.vantiq.extsrc.camelconn.connector.CamelHandleConfiguration.ROUTES_FORMAT;
 import static io.vantiq.extsrc.camelconn.connector.CamelHandleConfiguration.ROUTES_LIST;
 import static io.vantiq.extsrc.camelconn.discover.VantiqComponentResolverTest.MISSING_VALUE;
@@ -26,72 +27,36 @@
 import io.vantiq.extsrc.camelconn.discover.CamelRunner;
 import org.apache.camel.CamelContext;
 import org.apache.commons.lang3.function.TriFunction;
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TestName;
 
 import java.io.File;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Properties;
 import java.util.concurrent.TimeUnit;
 
 public class VantiqConfigurationTest {
     
+    
+    @Rule
+    public TestName name = new TestName();
+    
     private final String BUILD_DIR = System.getProperty("BUILD_DIR", "build");
     private final String CAMEL_CONN_BASE_DIR = "camelConnBase";
     private final String CAMEL_BASE_PATH = BUILD_DIR + File.separator + CAMEL_CONN_BASE_DIR + File.separator;
     private final String CACHE_DIR = CAMEL_BASE_PATH + "cacheDir";
     private final String LOADED_LIBRARIES = CAMEL_BASE_PATH + "loadedLib";
     
-    @Test
-    public void testSimpleConfiguration() {
-        Map<String, Object> simpleConfig = new HashMap<>();
-        Map<String, Object> camelConfig = new HashMap<>();
-        simpleConfig.put(CAMEL_CONFIG, camelConfig);
-        Map<String, String> camelAppConfig = new HashMap<>();
-        camelConfig.put(CAMEL_APP, camelAppConfig);
-        Map<String, String> generalConfig = new HashMap<>();
-        camelConfig.put(GENERAL, generalConfig);
-        
-        generalConfig.put(COMPONENT_CACHE, CACHE_DIR);
-        generalConfig.put(COMPONENT_LIB, LOADED_LIBRARIES);
-        
-        camelAppConfig.put(ROUTES_LIST, XML_ROUTE);
-        camelAppConfig.put(ROUTES_FORMAT, "xml");
-        camelAppConfig.put(APP_NAME, "testSimpleConfiguration");
-    
-        String fauxVantiqUrl = "http://someVantiqServer";
-        ExtensionServiceMessage esm = new ExtensionServiceMessage(fauxVantiqUrl);
-        esm.op = OP_CONFIGURE_EXTENSION;
-        esm.object = simpleConfig;
-        
-        CamelCore core = new CamelCore("testSimpleConfiguration", "someAccessToken", fauxVantiqUrl);
-        CamelHandleConfiguration handler = new CamelHandleConfiguration(core);
-        
-        handler.handleMessage(esm);
-        assertTrue("handler completed", handler.isComplete());
-        assertNotNull("Camel Runner", handler.getCurrentCamelRunner());
-        assertTrue("Runner started", handler.getCurrentCamelRunner().isStarted());
-        assertNotNull("Runner's Camel Context", handler.getCurrentCamelRunner().getCamelContext());
-        assertNotNull("Runner's thread", handler.getCurrentCamelRunner().getCamelThread());
-        
-        // Assuming things worked, now we have a camel app running.  We'll run our test against it & verify
-    
-        TriFunction<CamelContext, String, Object, Boolean> verifyOperation = defineVerifyOperation();
-        CamelContext runnerContext = handler.getCurrentCamelRunner().getCamelContext();
-        assertTrue("Context Running", runnerContext.isStarted());
-        assert verifyOperation.apply(runnerContext, QUERY_MONKEY, RESPONSE_MONKEY);
-        assert verifyOperation.apply(runnerContext, QUERY_AARDVARK, RESPONSE_AARDVARK);
-    
-        handler.getCurrentCamelRunner().close();
-        try {
-            handler.getCurrentCamelRunner().getCamelThread().join(TimeUnit.SECONDS.toMillis(10));
-        } catch (InterruptedException ie) {
-            fail("Trapped Interrupted Exception");
-        }
+    // Interface to use in declaration.  We'll pass lambda's in to do the actual verification work
+    interface Verifier {
+        void doVerify(CamelContext runnerContext);
     }
     
-    @Test
-    public void testComponentInitConfiguration() {
+    void performConfigTest(String appName, String route, String routeFormat,
+                           List<Map<String, Object>> compInitProps, Properties propertyValues, Verifier vfy) {
         assumeTrue(!sfLoginUrl.equals(MISSING_VALUE) && !sfClientId.equals(MISSING_VALUE) &&
                            !sfClientSecret.equals(MISSING_VALUE) && !sfRefreshToken.equals(MISSING_VALUE));
         Map<String, Object> simpleConfig = new HashMap<>();
@@ -105,17 +70,23 @@ public void testComponentInitConfiguration() {
         generalConfig.put(COMPONENT_CACHE, CACHE_DIR);
         generalConfig.put(COMPONENT_LIB, LOADED_LIBRARIES);
         
-        camelAppConfig.put(ROUTES_LIST, SALESFORCETASKS_YAML);
-        camelAppConfig.put(ROUTES_FORMAT, "yaml");
-        camelAppConfig.put(APP_NAME, "testComponentInitConfiguration");
-        camelAppConfig.put(COMPONENT_PROPERTIES, getComponentsToInit());
+        camelAppConfig.put(ROUTES_LIST, route);
+        camelAppConfig.put(ROUTES_FORMAT, routeFormat);
+        camelAppConfig.put(APP_NAME, appName);
+        if (compInitProps != null) {
+            camelAppConfig.put(COMPONENT_PROPERTIES, compInitProps);
+        }
+        if (propertyValues != null) {
+            camelAppConfig.put(PROPERTY_VALUES, propertyValues);
+        }
         
         String fauxVantiqUrl = "http://someVantiqServer";
         ExtensionServiceMessage esm = new ExtensionServiceMessage(fauxVantiqUrl);
         esm.op = OP_CONFIGURE_EXTENSION;
         esm.object = simpleConfig;
         
-        CamelCore core = new CamelCore("testComponentInitConfiguration", "someAccessToken", fauxVantiqUrl);
+        CamelCore core = new CamelCore("testComponentInitConfiguration",
+                                       "someAccessToken", fauxVantiqUrl);
         CamelHandleConfiguration handler = new CamelHandleConfiguration(core);
         
         handler.handleMessage(esm);
@@ -127,12 +98,11 @@ public void testComponentInitConfiguration() {
         
         // Assuming things worked, now we have a camel app running.  We'll run our test against it & verify
         
-        TriFunction<CamelContext, String, Object, Boolean> verifyOperation = defineVerifyOperation();
         CamelContext runnerContext = handler.getCurrentCamelRunner().getCamelContext();
         assertTrue("Context Running", runnerContext.isStarted());
-        assert verifyOperation.apply(runnerContext, "not used", Map.of( "done", true));
-        assert verifyOperation.apply(runnerContext, "not used", Map.of( "done", true));
         
+        vfy.doVerify(runnerContext);
+    
         handler.getCurrentCamelRunner().close();
         try {
             handler.getCurrentCamelRunner().getCamelThread().join(TimeUnit.SECONDS.toMillis(10));
@@ -141,6 +111,45 @@ public void testComponentInitConfiguration() {
         }
     }
     
+    @Test
+    public void testSimpleConfiguration() {
+        performConfigTest(name.getMethodName(), XML_ROUTE, "xml",
+                          null, null,
+                          (CamelContext runnerContext) -> {
+                              TriFunction<CamelContext, String, Object, Boolean> verifyOperation =
+                                      defineVerifyOperation();
+                              assert verifyOperation.apply(runnerContext, QUERY_MONKEY, RESPONSE_MONKEY);
+                              assert verifyOperation.apply(runnerContext, QUERY_AARDVARK, RESPONSE_AARDVARK);
+                          });
+    }
+    
+    @Test
+    public void testComponentInitConfiguration() {
+        performConfigTest(name.getMethodName(), SALESFORCETASKS_YAML, "yaml",
+                          getComponentsToInit(), null,
+                          (CamelContext runnerContext) -> {
+                              TriFunction<CamelContext, String, Object, Boolean> verifyOperation =
+                                      defineVerifyOperation();
+                            assert verifyOperation.apply(runnerContext, "not used", Map.of("done", true));
+                            assert verifyOperation.apply(runnerContext, "not used", Map.of("done", true));
+                        });
+    }
+    
+    @Test
+    public void testComponentInitConfigurationWithPropertyValues() {
+        Properties props = new Properties(pValues.size());
+        // Though officially frowned upon, Properties.putAll here from a Map<String, String> is safe as it cannot put
+        // non-String keys or values into the Properties base map.
+        props.putAll(pValues);
+        performConfigTest(name.getMethodName(), PARAMETERIZED_SALESFORCE_ROUTE, "yaml",
+                          getComponentsToInit(), props,
+                          (CamelContext runnerContext) -> {
+                              TriFunction<CamelContext, String, Object, Boolean> verifyOperation =
+                                      defineVerifyOperation();
+                              assert verifyOperation.apply(runnerContext, "not used", Map.of("done", true));
+                              assert verifyOperation.apply(runnerContext, "not used", Map.of("done", true));
+                          });
+    }
     
     public static final String sfLoginUrl = System.getProperty("camel-salesforce-loginUrl", MISSING_VALUE);
     public static final String sfClientId = System.getProperty("camel-salesforce-clientId", MISSING_VALUE);
@@ -176,4 +185,21 @@ public List<Map<String, Object>> getComponentsToInit() {
         );
     }
     
+    public static final String PARAMETERIZED_SALESFORCE_ROUTE  = "\n"
+            + "- route:\n"
+            + "    id: \"Salesforce from yaml-route\"\n"
+            + "    from:\n"
+            + "      uri: \"{{directStart}}\"\n"
+            + "      steps:\n"
+//        + "        - set-exchange-pattern: \"inOut\"\n" // Leaving as a reminder re: how to do in/out YAML routes
+            + "        - to:\n"
+            + "            uri: \"salesforce:query?rawPayload=true&SObjectQuery={{query}}\"\n"
+            + "        - unmarshal:\n"
+            + "            json: {}\n"
+            + "        - to:\n"
+            + "            uri: \"{{mockResult}}\"\n";
+    
+    public static final Map<String, String> pValues = Map.of("directStart", "direct:start",
+                                                             "query", "SELECT Id, Subject, OwnerId from Task",
+                                                             "mockResult", "mock:result");
 }
diff --git a/camelConnector/src/test/java/io/vantiq/extsrc/camelconn/discover/VantiqComponentDiscoveryTest.java b/camelConnector/src/test/java/io/vantiq/extsrc/camelconn/discover/VantiqComponentDiscoveryTest.java
index 5214a198..b02ca54a 100644
--- a/camelConnector/src/test/java/io/vantiq/extsrc/camelconn/discover/VantiqComponentDiscoveryTest.java
+++ b/camelConnector/src/test/java/io/vantiq/extsrc/camelconn/discover/VantiqComponentDiscoveryTest.java
@@ -26,6 +26,7 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Properties;
 import java.util.Set;
 
 /**
@@ -201,6 +202,12 @@ public void testComplexXmlRouteDiscovery() throws Exception {
         performDiscoveryTest(rb);
     }
     
+    @Test
+    public void testParameterizedXmlRouteDiscovery() throws Exception {
+        ParameterizedXmlRoutes rb = new ParameterizedXmlRoutes();
+        performDiscoveryTest(rb, rb.getRequiredProperties());
+    }
+    
     @Test
     public void testBeansInRouteDiscovery() throws Exception {
         RouteBuilder rb = new BeanIncludingRoutes(context);
@@ -208,11 +215,14 @@ public void testBeansInRouteDiscovery() throws Exception {
     }
     
     void performDiscoveryTest(RouteBuilder rb) throws Exception {
+        performDiscoveryTest(rb, null);
+    }
+    void performDiscoveryTest(RouteBuilder rb, Properties propertyValues) throws Exception {
         assert rb instanceof TestExpectations;
         setUseRouteBuilder(false);
         CamelDiscovery discoverer = new CamelDiscovery();
         
-        Map<String, Set<String>> discResults = discoverer.performComponentDiscovery(rb);
+        Map<String, Set<String>> discResults = discoverer.performComponentDiscovery(rb, propertyValues);
         
         List<String> expectedCTL = ((TestExpectations) rb).getExpectedComponentsToLoad();
         List<String> expectedSysComp = ((TestExpectations) rb).getExpectedSystemComponents();
@@ -505,6 +515,44 @@ public void configure() throws Exception {
         }
     }
     
+    private static class ParameterizedXmlRoutes extends RouteBuilder implements TestExpectations {
+        
+        @Override
+        public List<String> getExpectedComponentsToLoad() {
+            return List.of("salesforce");
+        }
+        
+        @Override
+        public List<String> getExpectedSystemComponents() {
+            return List.of("direct-vm");
+        }
+        
+        @Override
+        public void configure() throws Exception {
+            String content = ""
+                    + "    <routes>"
+                    + "        <route id=\"salesforce1\">"
+                    + "            <from uri=\"salesforce:CamelTestTopic?notifyForFields=ALL"
+                    + "&amp;notifyForOperations=ALL&amp;sObjectName=Merchandise__c&amp;updateTopic=true"
+                    + "&amp;sObjectQuery={{query}}\"/>"
+                    + "            <to uri=\"salesforce:createSObject?sObjectName={{outputObjectName}}\"/>"
+                    + "            <to uri=\"{{directNotifier}}\"/>"
+                    + "        </route>"
+                    + "    </routes>";
+            RouteBuilder rb = new XmlRouteBuilder(content).getRouteBuilder();
+            rb.configure();
+            this.setRouteCollection(rb.getRouteCollection());
+        }
+        
+        public Properties getRequiredProperties() {
+            Properties props = new Properties();
+            props.setProperty("query", "SELECT Id, Name FROM Merchandise__c");
+            props.setProperty("outputObjectName", "TestEvent__e");
+            props.setProperty("directNotifier", "direct-vm:salesforce-notifier");
+            return props;
+        }
+    }
+    
     private static class BeanIncludingRoutes extends RouteBuilder implements TestExpectations {
     
         CamelContext ctx;
@@ -558,7 +606,7 @@ public void configure() throws Exception {
             + "                 sharedAccessName: \"someRandomKey\" \n"
             + "                 sharedAccessKey: \"RAW(MY TOKEN)\" \n";
     
-            // YAML support needs a camel context.  So provide oe during setup...
+            // YAML support needs a camel context.  So provide one during setup...
             RouteBuilder rb = new YamlRouteBuilder(ctx, content).getRouteBuilder();
             rb.configure();
             this.setRouteCollection(rb.getRouteCollection());
diff --git a/camelConnector/src/test/java/io/vantiq/extsrc/camelconn/discover/VantiqComponentResolverTest.java b/camelConnector/src/test/java/io/vantiq/extsrc/camelconn/discover/VantiqComponentResolverTest.java
index 0c721dac..00c4875e 100644
--- a/camelConnector/src/test/java/io/vantiq/extsrc/camelconn/discover/VantiqComponentResolverTest.java
+++ b/camelConnector/src/test/java/io/vantiq/extsrc/camelconn/discover/VantiqComponentResolverTest.java
@@ -43,6 +43,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Properties;
 
 /**
  * Perform unit tests for component resolution
@@ -240,7 +241,7 @@ public void testStartRouteLoadedComponents() throws Exception {
         setUseRouteBuilder(false);
         try (CamelRunner runner = new CamelRunner(this.getTestMethodName(), rb, null,
                                                   IVY_CACHE_PATH, DEST_PATH,
-                                                  rb.getComponentsToInit())) {
+                                                  rb.getComponentsToInit(), null)) {
             runner.runRoutes(false);
         }
     }
@@ -253,7 +254,7 @@ public void testStartRouteLoadedComponentsAndMarshaling() throws Exception {
         setUseRouteBuilder(false);
         try (CamelRunner runner = new CamelRunner(this.getTestMethodName(), rb, null,
                                                   IVY_CACHE_PATH, DEST_PATH,
-                                                  rb.getComponentsToInit())) {
+                                                  rb.getComponentsToInit(), null)) {
             runner.runRoutes(false);
         }
     }
@@ -269,7 +270,7 @@ public void testStartRouteLoadedComponentsMultiRepo() throws Exception {
         repoList.add(new URI("https://vantiqmaven.s3.amazonaws.com/"));
         repoList.add(new URI("https://repo.maven.apache.org/maven2/"));
         try (CamelRunner runner = new CamelRunner(this.getTestMethodName(),rb, repoList,
-                                                  IVY_CACHE_PATH, DEST_PATH, rb.getComponentsToInit())) {
+                                                  IVY_CACHE_PATH, DEST_PATH, rb.getComponentsToInit(), null)) {
             runner.runRoutes(false);
         }
     }
@@ -319,7 +320,7 @@ public void testStartRunLoadedComponentsMarshaledAvroFailure() throws Exception
         RouteBuilderWithProps rb = new MakeDigCallMarshaledAvroFailure();
         assertNotNull("No routebuilder", rb);
         
-        performLoadAndRunTest(rb, false, rb.getComponentsToInit());
+        performLoadAndRunTest(rb, false, rb.getComponentsToInit(), null);
     }
     
     @Test
@@ -396,6 +397,17 @@ public void testStartRunLoadedComponentsUsingPreviouslyRetrievedComponents() thr
             + "        - to:\n"
             + "            uri: \"mock:result\"\n";
     
+    public static final String YAML_ROUTE_PARAMETERIZED =  "\n"
+            + "- route:\n"
+            + "    id: \"Dig from yaml-route\"\n"
+            + "    from:\n"
+            + "      uri: \"{{directStart}}\"\n"
+            + "      steps:\n"
+            + "        - to:\n"
+            + "            uri: \"{{dnsDig}}\"\n"
+            + "        - to:\n"
+            + "            uri: \"{{mockResult}}\"\n";
+    
     public static final String ROUTE_WITH_BEAN = "\n"
             + "- route: \n"
             + "    id: \"EventHub Sink\" \n"
@@ -442,12 +454,26 @@ public void testStartRunLoadedComponentsFromYamlText() throws Exception {
         performLoadAndRunTest(YAML_ROUTE, "yaml", null);
     }
     
+    @Test
+    public void testStartRunLoadedComponentsFromYamlTextParameterized() throws Exception {
+        FileUtil.forceDelete(cache);    // Clear the cache
+        setUseRouteBuilder(false);
+        
+        Properties propertyValues = new Properties(3);
+        propertyValues.setProperty("directStart", "direct:start");
+        propertyValues.setProperty("dnsDig", "dns:dig");
+        propertyValues.setProperty("mockResult", "mock:result");
+        
+        performLoadAndRunTest(YAML_ROUTE_PARAMETERIZED, "yaml", null, false,
+                              propertyValues);
+    }
+    
     @Test
     public void testDiscoverBeanRoute() throws Exception {
         FileUtil.forceDelete(cache);
         setUseRouteBuilder(false);
         
-        performLoadAndRunTest(ROUTE_WITH_BEAN, "yaml", null, true);
+        performLoadAndRunTest(ROUTE_WITH_BEAN, "yaml", null, true, null);
     }
     
     /**
@@ -543,11 +569,12 @@ public boolean matches(Exchange exchange) {
     
     public void performLoadAndRunTest(String content, String contentType,
                                       List<Map<String, Object>> compToInit) throws Exception {
-        performLoadAndRunTest(content, contentType, compToInit, false);
+        performLoadAndRunTest(content, contentType, compToInit, false, null);
     }
     
     public void performLoadAndRunTest(String content, String contentType,
-                                      List<Map<String, Object>> compToInit, boolean defeatVerify) throws Exception {
+                                      List<Map<String, Object>> compToInit, boolean defeatVerify,
+                                      Properties propertyValues) throws Exception {
         // To do this test, we'll create a callable that the test method will call. In this case, the callable "sends"
         // message to the route which, in turn, makes the dig call to lookup a monkey.  We verify that the expected
         // results is presented.
@@ -562,7 +589,7 @@ public void performLoadAndRunTest(String content, String contentType,
         
         try (CamelRunner runner =
                      new CamelRunner(this.getTestMethodName(), content, contentType, null,
-                                     IVY_CACHE_PATH, DEST_PATH, compToInit)) {
+                                     IVY_CACHE_PATH, DEST_PATH, compToInit, propertyValues)) {
             openedRunner = runner;
             runner.runRoutes(false);
             runnerContext = runner.getCamelContext();
@@ -586,10 +613,11 @@ public void performLoadAndRunTest(String content, String contentType,
     }
     
     public void performLoadAndRunTest(RouteBuilder rb, List<Map<String, Object>> compToInit) throws Exception {
-        performLoadAndRunTest(rb, true, compToInit);
+        performLoadAndRunTest(rb, true, compToInit, null);
     }
     public void performLoadAndRunTest(RouteBuilder rb, boolean shouldStart,
-                                      List<Map<String, Object>> componentToInit) throws Exception {
+                                      List<Map<String, Object>> componentToInit,
+                                      Properties propertyValues) throws Exception {
         // To do this test, we'll create a callable that the test method will call. In this case, the callable "sends"
         // message to the route which, in turn, makes the dig call to lookup a monkey & aardvark.  We verify that the
         // expected results are presented.
@@ -603,7 +631,7 @@ public void performLoadAndRunTest(RouteBuilder rb, boolean shouldStart,
         
         try (CamelRunner runner =
                      new CamelRunner(this.getTestMethodName(), rb, null, IVY_CACHE_PATH, DEST_PATH,
-                                     componentToInit)) {
+                                     componentToInit, propertyValues)) {
             openedRunner = runner;
             runner.runRoutes(false);
             runnerContext = runner.getCamelContext();
@@ -642,10 +670,17 @@ public void performLoadAndRunTest(RouteBuilder rb, boolean shouldStart,
         }
     
         assertNotNull("runnerThread is null", runnerThread);
+        assert runnerContext != null && runnerContext.isStopped();
+        int deathWaitCount = 30;
+        // Wait for thread to die off.  Otherwise, spurious errors occasionally.
+        while (runnerThread.isAlive() && deathWaitCount > 0) {
+            Thread.sleep(500);
+            deathWaitCount -= 1;
+        }
+        // Ensure that the running thread has completed.  Issue of test resource starvation, not product
         assertTrue("ShouldStart: " + shouldStart + ", runnerThread: " + runnerThread +
                            ", ...isAlive: " + runnerThread.isAlive(),
-                   !shouldStart || runnerThread != null && !runnerThread.isAlive());
-        assert runnerContext != null && runnerContext.isStopped();
+                   !shouldStart || !runnerThread.isAlive());
     }
     
     private static abstract class RouteBuilderWithProps extends RouteBuilder {