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

Issue:
camelConnector: Add ability to specify header duplication
That is, make use of #422.

This allows configuration of the source so that the associated Vantiq application need not know every possible component-specific header.  Which seems like a good thing:tm: for a integration system.


Discussion / hints:

Fix patch:
diff --git a/camelComponent/build.gradle b/camelComponent/build.gradle
index 0a09133b..5d024cfb 100644
--- a/camelComponent/build.gradle
+++ b/camelComponent/build.gradle
@@ -21,6 +21,10 @@ ext {
     commonsLang3Version = '3.12.0'
 }
 
+configurations {
+    testArtifacts
+}
+
 dependencies {
     annotationProcessor "org.projectlombok:lombok:${lombokVersion}"
 
@@ -62,3 +66,13 @@ tasks.withType(Test) {
     systemProperty "buildDir", "${buildDir}"
 }
 
+
+task testJar (type: Jar) {
+    baseName = "${project.name}-test"
+    from sourceSets.test.output
+}
+
+artifacts {
+    testArtifacts testJar
+}
+
diff --git a/camelConnector/build.gradle b/camelConnector/build.gradle
index 5e679686..79eee857 100644
--- a/camelConnector/build.gradle
+++ b/camelConnector/build.gradle
@@ -72,6 +72,7 @@ dependencies {
     implementation "org.apache.camel:camel-yaml-dsl:${camelVersion}"
     // End of DSL-specific inclusions.
 
+    testImplementation project(path: ":camelComponent", configuration: "testArtifacts")
     testImplementation "org.apache.logging.log4j:log4j-slf4j-impl:${log4JVersion}"
     testImplementation "org.apache.camel:camel-test:${camelVersion}"
     testImplementation "io.vantiq:vantiq-sdk:${vantiqSDKVersion}"
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 6b18b763..43827788 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
@@ -60,6 +60,8 @@ 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 HEADER_DUPLICATION = "headerDuplication";
+    public static final String HEADER_BEAN_NAME = "headerBeanName";
     public static final String PROPERTY_VALUES = "propertyValues";
     public static final String RAW_REQUIRED = "rawValuesRequired";
     public static final String DISCOVERED_RAW = "discovered";
@@ -188,6 +190,32 @@ public void handleMessage(ExtensionServiceMessage message) {
                 }
             }
         }
+        
+        target = camelConfig.get(HEADER_DUPLICATION);
+        if (target != null) {
+            if (!(target instanceof Map)) {
+                log.error("Camel connector property {} should be a Map (found {}).",
+                          HEADER_DUPLICATION, target.getClass().getName());
+                failConfig();
+                return;
+            } else {
+                boolean failed = false;
+                for (Map.Entry<?,?> ent: ((Map<?, ?>) target).entrySet()) {
+                    if (!(ent.getKey() instanceof String && ent.getValue() instanceof String)) {
+                        log.error("Both keys and values of the Camel connector property {} should be strings.  Found " +
+                                          "{} & {}, respectively.", HEADER_DUPLICATION,
+                                  ent.getKey() != null ? ent.getKey().getClass().getName() : null,
+                                  ent.getValue() != null ? ent.getValue().getClass().getName() : null);
+                        failed = true;
+                        break;
+                    }
+                }
+                if (failed) {
+                    failConfig();
+                    return;
+                }
+            }
+        }
         boolean success;
         try {
             success = runCamelApp(camelConfig, general);
@@ -247,7 +275,13 @@ public boolean runCamelApp(Map<String, Object> camelConfig, Map<String, Object>
                     return false;
                 }
             }
-            
+    
+            // This is checked in the caller
+            String headerBeanName = (String) camelConfig.get(HEADER_BEAN_NAME);
+            // This is checked in the caller
+            //noinspection unchecked
+            Map<String, String> headerDuplications = (Map<String, String>) camelConfig.get(HEADER_DUPLICATION);
+    
             // This is checked in the caller
             //noinspection unchecked
             List<Map<String, Object>> componentProperties =
@@ -361,9 +395,10 @@ public boolean runCamelApp(Map<String, Object> camelConfig, Map<String, Object>
             // The connector (or this method, when things are reconfigured -- see a few lines up) will handle closing
             // things as appropriate.
             CamelRunner runner =
-                         new CamelRunner(appName, Objects.requireNonNull(routeSpec).get(ROUTES_LIST),
+                    createCamelRunner(appName, Objects.requireNonNull(routeSpec).get(ROUTES_LIST),
                                          routeSpec.get(ROUTES_FORMAT), repoList,
-                                         componentCache, componentLib, componentProperties, propVals);
+                                         componentCache, componentLib, componentProperties, propVals,
+                                         headerBeanName, headerDuplications);
             if (additionalLibraries != null) {
                 runner.setAdditionalLibraries(additionalLibraries);
             }
@@ -378,6 +413,31 @@ public boolean runCamelApp(Map<String, Object> camelConfig, Map<String, Object>
         return true;
     }
     
+    /**
+     * Create a CamelRunner instance
+     *
+     * @param appName String Name for this instance
+     * @param routeSpecification String specification for the route(s) to run.  Syntax must match
+     *         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 camelProperties Properties set of general property values that Camel can use for property
+     *         resolution
+     * @param headerBeanName String Name of bean to use generate.  Should match what's in route & be unique to this
+     *         camel instance
+     * @param headerDuplications Map<String, String> directed set of header names to duplicate
+     */
+    protected CamelRunner createCamelRunner(String appName, String routeSpecification, String routeSpecificationType,
+                               List<URI> repos,
+                       String cacheDirectory, String loadedLibDir, List<Map<String, Object>> initComponents,
+                       Properties camelProperties, String headerBeanName, Map<String, String> headerDuplications) {
+        return new CamelRunner(appName, routeSpecification, routeSpecificationType, repos, cacheDirectory,
+                                loadedLibDir, initComponents, camelProperties, headerBeanName, headerDuplications);
+    }
     private Map<String, String> fetchDocument(String docName) {
         String token = source.authToken;
         String url = source.targetVantiqServer;
diff --git a/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/discover/CamelResolver.java b/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/discover/CamelResolver.java
index 1890e6fe..260397b8 100644
--- a/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/discover/CamelResolver.java
+++ b/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/discover/CamelResolver.java
@@ -58,7 +58,7 @@ public class CamelResolver {
      * Create resolver for necessary artifacts
      *
      * @param name String name of resolver.  Primarily for logging & debugging
-     * @param repos URI repos from which to fetch.  If null, use maven central
+     * @param repo URI repos from which to fetch.  If null, use maven central
      * @param cache File Specification of directory for ivy's cache.  If null, take Ivy's defaults.
      * @param destination File Specification of directory to which to copy files
      * @throws IllegalArgumentException for invalid parameters
@@ -102,7 +102,6 @@ public class CamelResolver {
                     String repoUrl = repo.toURL().toExternalForm();
                     aResolver.setRoot(repoUrl);
                     aResolver.setName(name + "::" + repoUrl);
-    
                 } catch (MalformedURLException mue) {
                     throw new IllegalArgumentException("Malformed repos URL: " + repo.toString(), mue);
                 }
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 9f2805f8..862c941a 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
@@ -8,8 +8,10 @@
 
 package io.vantiq.extsrc.camelconn.discover;
 
+import io.vantiq.extsrc.camel.HeaderDuplicationBean;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.camel.CamelContext;
+import org.apache.camel.Component;
 import org.apache.camel.ExtendedCamelContext;
 import org.apache.camel.ProducerTemplate;
 import org.apache.camel.RoutesBuilder;
@@ -55,6 +57,8 @@ public class CamelRunner extends MainSupport implements Closeable {
     private final List<Map<String, Object>> initComponents;
     
     private final Properties camelProperties;
+    String headerBeanName;
+    Map<String, String> headerDuplications;
     private final File ivyCache;
     private final File loadedLibraries;
     
@@ -63,6 +67,8 @@ public class CamelRunner extends MainSupport implements Closeable {
     private final String routeSpec;
     private final String routeSpecType;
     private ClassLoader routeBasedCL;
+    
+    private Map<String, Component> componentOverrides = null; // Used in testing only
     private ClassLoader originalClassLoader;
     
     protected final MainRegistry registry;
@@ -87,10 +93,13 @@ public class CamelRunner extends MainSupport implements Closeable {
      *         the properties included herein
      * @param camelProperties Properties set of general property values that Camel can use for property
      *         resolution
+     * @param headerBeanName String Name of bean to use generate.  Should match what's in route & be unique to this
+     *         camel instance
+     * @param headerDuplications Map<String, String> directed set of header names to duplicate
      */
     public CamelRunner(String appName, String routeSpecification, String routeSpecificationType, List<URI> repos,
                        String cacheDirectory, String loadedLibDir, List<Map<String, Object>> initComponents,
-                       Properties camelProperties) {
+                       Properties camelProperties, String headerBeanName, Map<String, String> headerDuplications) {
         super();
         this.registry = new MainRegistry();
         this.appName = appName;
@@ -104,6 +113,8 @@ public CamelRunner(String appName, String routeSpecification, String routeSpecif
         this.additionalLibraries = null;
         this.initComponents = initComponents;
         this.camelProperties = camelProperties;
+        this.headerDuplications = headerDuplications != null ? headerDuplications : new HashMap<>();
+        this.headerBeanName = headerBeanName;
         mainConfigurationProperties.setRoutesCollectorEnabled(false);
     }
     
@@ -118,10 +129,13 @@ public CamelRunner(String appName, String routeSpecification, String routeSpecif
      * @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
+     * @param headerBeanName String Name of bean to use generate.  Should match what's in route & be unique to this
+     *                          camel instance
+     * @param headerDuplications Map<String, String> directed set of header names to duplicate
      */
     CamelRunner(String appName, RouteBuilder routeBuilder, List<URI> repos,
                 String cacheDirectory, String loadedLibDir, List<Map<String, Object>> initComponents,
-                Properties camelProperties) {
+                Properties camelProperties, String headerBeanName, Map<String, String> headerDuplications) {
         super();
         this.registry = new MainRegistry();
         this.appName = appName;
@@ -134,11 +148,16 @@ public CamelRunner(String appName, String routeSpecification, String routeSpecif
         this.additionalLibraries = null;
         this.initComponents = initComponents;
         this.camelProperties = camelProperties;
+        this.headerDuplications = headerDuplications != null ? headerDuplications : new HashMap<>();
+        this.headerBeanName = headerBeanName;
     
         mainConfigurationProperties.setRoutesBuilders(List.of(routeBuilder));
         mainConfigurationProperties.setRoutesCollectorEnabled(false);
     }
     
+    protected void setComponentOverrides(Map<String, Component> compOverrides) {
+        this.componentOverrides = compOverrides;
+    }
     public void setAdditionalLibraries(List<String> libs) {
         this.additionalLibraries = libs;
     }
@@ -146,7 +165,15 @@ public void setAdditionalLibraries(List<String> libs) {
     @Override
     protected void doInit() throws Exception {
         super.doInit();
-        createCamelContext();
+        CamelContext ctx = createCamelContext();
+        if (componentOverrides != null && !componentOverrides.isEmpty()) {
+            componentOverrides.forEach(ctx::addComponent);
+        }
+        if (headerBeanName != null) {
+            HeaderDuplicationBean hdBean = new HeaderDuplicationBean();
+            hdBean.setHeaderDuplicationMap(headerDuplications);
+            ctx.getRegistry().bind(headerBeanName, hdBean);
+        }
     }
     
     public Properties getCamelProperties() {
@@ -205,7 +232,7 @@ private ClassLoader buildClassloader(CamelDiscovery discoverer,
             urlList.add(url);
         }
         if (log.isTraceEnabled()) {
-            // This list is long, so if we're ignore it, no reason to prepare to Stringify it.
+            // This list is long, so if we're ignoring it, no reason to prepare to Stringify it.
             log.trace("URLList: {}", urlList);
         }
         // Now, we'll set up a classLoader based on that list of jar files and use that to run our routes.
@@ -310,7 +337,6 @@ private Set<File> resolveFromRepo(List<URI> repositories, Set<String> components
      * Run the routes specified in this CamelRunner
      *
      * @param waitForThread boolean indicating whether to await the completion of the thread started by this method
-     * @throws Exception if camel thread throws an exception.
      */
     public void runRoutes(boolean waitForThread) {
         try {
@@ -531,7 +557,7 @@ protected void awaitStartup() throws Exception {
     protected RouteBuilder loadRouteFromText(String specification, String specificationType) throws Exception {
         log.debug("Loading route from {} specificationType", specificationType);
         if (log.isTraceEnabled()) {
-            // Dump route spec out here only under trace.  Camel may have substituted key values and we don't want
+            // Dump route spec out here only under trace.  Camel may have substituted key values, and we don't want
             // these in the log files
             log.trace("Loading route (specificationType {}):\n{}", specificationType, specification);
         }


Test patch:
diff --git a/camelConnector/src/test/java/io/vantiq/extsrc/camelconn/connector/HandleConfigurationWithOverrides.java b/camelConnector/src/test/java/io/vantiq/extsrc/camelconn/connector/HandleConfigurationWithOverrides.java
new file mode 100644
index 00000000..f9e302a4
--- /dev/null
+++ b/camelConnector/src/test/java/io/vantiq/extsrc/camelconn/connector/HandleConfigurationWithOverrides.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2023 Vantiq, Inc.
+ *
+ * All rights reserved.
+ *
+ * SPDX: MIT
+ */
+
+package io.vantiq.extsrc.camelconn.connector;
+
+import io.vantiq.extsrc.camelconn.discover.CamelRunner;
+import io.vantiq.extsrc.camelconn.discover.CamelRunnerWithComponentSubs;
+import org.apache.camel.Component;
+
+import java.net.URI;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+/**
+ * Test version of CamelHandleConfiguration.
+ * </p>
+ * This provides a type-safe, test only, mechanism to allow component type overrides for testing.  We do this to
+ * handle, amongst other things, Vantiq-server-less implementation of the Vantiq Camel Component for use in unit tests.
+ */
+public class HandleConfigurationWithOverrides extends CamelHandleConfiguration {
+    
+    Map<String, Component> overrides = null;
+    
+    public HandleConfigurationWithOverrides(CamelCore source) {
+        super(source);
+    }
+    
+    public HandleConfigurationWithOverrides(CamelCore source, Map<String, Component> overrides) {
+        super(source);
+        this.overrides = overrides;
+    }
+    
+    @Override
+    protected CamelRunner createCamelRunner(String appName, String routeSpecification, String routeSpecificationType,
+                                            List<URI> repos, String cacheDirectory, String loadedLibDir,
+                                            List<Map<String, Object>> initComponents, Properties camelProperties,
+                                            String headerBeanName, Map<String, String> headerDuplications) {
+        CamelRunnerWithComponentSubs cr =
+                new CamelRunnerWithComponentSubs(appName, routeSpecification, routeSpecificationType,
+                                                 repos, cacheDirectory, loadedLibDir, initComponents, camelProperties,
+                                                 headerBeanName, headerDuplications, overrides);
+        return cr;
+    }
+}
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 850c0a4f..09e6dce2 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
@@ -1,12 +1,20 @@
-package io.vantiq.extsrc.camelconn.connector;
+/*
+ * Copyright (c) 2022 Vantiq, Inc.
+ *
+ * All rights reserved.
+ *
+ * SPDX: MIT
+ */
 
-import static io.vantiq.extsrc.camelconn.connector.CamelHandleConfiguration.NO_RAW_REQUEST;
-import static io.vantiq.extsrc.camelconn.connector.CamelHandleConfiguration.RAW_END;
-import static io.vantiq.extsrc.camelconn.connector.CamelHandleConfiguration.RAW_END_ALT;
-import static io.vantiq.extsrc.camelconn.connector.CamelHandleConfiguration.RAW_START;
-import static io.vantiq.extsrc.camelconn.connector.CamelHandleConfiguration.RAW_START_ALT;
+package io.vantiq.extsrc.camelconn.connector;
 
 import static io.vantiq.extjsdk.ExtensionServiceMessage.OP_CONFIGURE_EXTENSION;
+import static io.vantiq.extsrc.camel.VantiqEndpoint.ACCESS_TOKEN_PARAM;
+import static io.vantiq.extsrc.camel.VantiqEndpoint.HEADER_DUPLICATION_BEAN_NAME;
+import static io.vantiq.extsrc.camel.VantiqEndpoint.SOURCE_NAME_PARAM;
+import static io.vantiq.extsrc.camel.VantiqEndpoint.STRUCTURED_MESSAGE_HEADERS_PROPERTY;
+import static io.vantiq.extsrc.camel.VantiqEndpoint.STRUCTURED_MESSAGE_HEADER_PARAM;
+import static io.vantiq.extsrc.camel.VantiqEndpoint.STRUCTURED_MESSAGE_MESSAGE_PROPERTY;
 import static io.vantiq.extsrc.camelconn.connector.CamelHandleConfiguration.APP_NAME;
 import static io.vantiq.extsrc.camelconn.connector.CamelHandleConfiguration.CAMEL_APP;
 import static io.vantiq.extsrc.camelconn.connector.CamelHandleConfiguration.CAMEL_CONFIG;
@@ -15,8 +23,15 @@
 import static io.vantiq.extsrc.camelconn.connector.CamelHandleConfiguration.COMPONENT_PROPERTIES;
 import static io.vantiq.extsrc.camelconn.connector.CamelHandleConfiguration.DISCOVERED_RAW;
 import static io.vantiq.extsrc.camelconn.connector.CamelHandleConfiguration.GENERAL;
+import static io.vantiq.extsrc.camelconn.connector.CamelHandleConfiguration.HEADER_BEAN_NAME;
+import static io.vantiq.extsrc.camelconn.connector.CamelHandleConfiguration.HEADER_DUPLICATION;
+import static io.vantiq.extsrc.camelconn.connector.CamelHandleConfiguration.NO_RAW_REQUEST;
 import static io.vantiq.extsrc.camelconn.connector.CamelHandleConfiguration.PROPERTY_VALUES;
+import static io.vantiq.extsrc.camelconn.connector.CamelHandleConfiguration.RAW_END;
+import static io.vantiq.extsrc.camelconn.connector.CamelHandleConfiguration.RAW_END_ALT;
 import static io.vantiq.extsrc.camelconn.connector.CamelHandleConfiguration.RAW_REQUIRED;
+import static io.vantiq.extsrc.camelconn.connector.CamelHandleConfiguration.RAW_START;
+import static io.vantiq.extsrc.camelconn.connector.CamelHandleConfiguration.RAW_START_ALT;
 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;
@@ -24,23 +39,31 @@
 import static io.vantiq.extsrc.camelconn.discover.VantiqComponentResolverTest.QUERY_MONKEY;
 import static io.vantiq.extsrc.camelconn.discover.VantiqComponentResolverTest.RESPONSE_AARDVARK;
 import static io.vantiq.extsrc.camelconn.discover.VantiqComponentResolverTest.RESPONSE_MONKEY;
-import static io.vantiq.extsrc.camelconn.discover.VantiqComponentResolverTest.defineVerifyOperation;
 import static io.vantiq.extsrc.camelconn.discover.VantiqComponentResolverTest.XML_ROUTE;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
+import static io.vantiq.extsrc.camelconn.discover.VantiqComponentResolverTest.defineVerifyOperation;
 import static org.junit.Assume.assumeTrue;
 
 import io.vantiq.extjsdk.ExtensionServiceMessage;
+import io.vantiq.extjsdk.FalseClient;
+import io.vantiq.extsrc.camel.FauxVantiqComponent;
+import io.vantiq.extsrc.camel.FauxVantiqEndpoint;
 import io.vantiq.extsrc.camelconn.discover.CamelRunner;
+import lombok.extern.slf4j.Slf4j;
 import org.apache.camel.CamelContext;
+import org.apache.camel.Component;
+import org.apache.camel.Endpoint;
+import org.apache.camel.ProducerTemplate;
 import org.apache.camel.spi.PropertiesComponent;
+import org.apache.camel.test.junit4.CamelTestSupport;
 import org.apache.commons.lang3.function.TriFunction;
+import org.apache.ivy.util.FileUtil;
+import org.junit.BeforeClass;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.TestName;
 
 import java.io.File;
+import java.io.IOException;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -48,30 +71,47 @@
 import java.util.Properties;
 import java.util.concurrent.TimeUnit;
 
-public class VantiqConfigurationTest {
-    
-    
+@Slf4j
+public class VantiqConfigurationTest extends CamelTestSupport {
     @Rule
     public TestName name = new TestName();
+    public String testName = this.getClass().getSimpleName();
     
-    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";
+    private static final String BUILD_DIR = System.getProperty("BUILD_DIR", "build");
+    private static final String CAMEL_CONN_BASE_DIR = "camelConnBase";
+    private static final String CAMEL_BASE_PATH = BUILD_DIR + File.separator + CAMEL_CONN_BASE_DIR + File.separator;
+    private static final String CACHE_DIR = CAMEL_BASE_PATH + "cacheDir";
+    private static final String LOADED_LIBRARIES = CAMEL_BASE_PATH + "loadedLib";
+    public static final File cache = new File(CACHE_DIR);
     
     // Interface to use in declaration.  We'll pass lambda's in to do the actual verification work
     interface Verifier {
         void doVerify(CamelContext runnerContext);
     }
     
+    @BeforeClass
+    public static void setup() {
+        // Clean out cache to avoid spurious warnings about unknown resolvers.  These come about because our app name
+        // (after which we name our resolvers) vary test by test or test class by test class.
+        FileUtil.forceDelete(cache);    // Clear the cache
+    }
     void performConfigTest(String appName, String route, String routeFormat,
                            List<Map<String, Object>> compInitProps, Properties propertyValues, Verifier vfy) {
-        performConfigTest(appName, route, routeFormat, compInitProps, propertyValues, vfy, null);
+        performConfigTest(appName, route, routeFormat, compInitProps, propertyValues, vfy, null,
+                          null, null, null);
     }
+    
     void performConfigTest(String appName, String route, String routeFormat,
                            List<Map<String, Object>> compInitProps, Properties propertyValues, Verifier vfy,
                            List<String> rawReq) {
+        performConfigTest(appName, route, routeFormat, compInitProps, propertyValues, vfy, rawReq,
+                          null, null, null);
+    }
+    
+    void performConfigTest(String appName, String route, String routeFormat,
+                           List<Map<String, Object>> compInitProps, Properties propertyValues, Verifier vfy,
+                           List<String> rawReq, String headerBeanName, Map<String, String> headerDuplications,
+                           Map<String, Component> overrideComponents) {
         Map<String, Object> simpleConfig = new HashMap<>();
         Map<String, Object> camelConfig = new HashMap<>();
         simpleConfig.put(CAMEL_CONFIG, camelConfig);
@@ -98,6 +138,11 @@ void performConfigTest(String appName, String route, String routeFormat,
             rawReqMap.put(DISCOVERED_RAW, rawReq);
             camelAppConfig.put(RAW_REQUIRED, rawReqMap);
         }
+        
+        if (headerBeanName != null) {
+            camelAppConfig.put(HEADER_BEAN_NAME, headerBeanName);
+            camelAppConfig.put(HEADER_DUPLICATION, headerDuplications);
+        }
 
         String fauxVantiqUrl = "http://someVantiqServer";
         ExtensionServiceMessage esm = new ExtensionServiceMessage(fauxVantiqUrl);
@@ -106,7 +151,13 @@ void performConfigTest(String appName, String route, String routeFormat,
 
         CamelCore core = new CamelCore("testComponentInitConfiguration",
                                        "someAccessToken", fauxVantiqUrl);
-        CamelHandleConfiguration handler = new CamelHandleConfiguration(core);
+        CamelHandleConfiguration handler;
+        
+        if (overrideComponents == null) {
+            handler = new CamelHandleConfiguration(core);
+        } else {
+            handler = new HandleConfigurationWithOverrides(core, overrideComponents);
+        }
 
         handler.handleMessage(esm);
         assertTrue("handler completed", handler.isComplete());
@@ -153,7 +204,7 @@ public void testRawNoRaw() {
         // non-String keys or values into the Properties base map.
         props.putAll(propValues);
     
-        performConfigTest(name.getMethodName(), ROUTE_NOT_USED, "yaml",
+        performConfigTest(testName, ROUTE_NOT_USED, "yaml",
                           null, props,
                           (CamelContext runnerContext) -> {
                               PropertiesComponent pc = runnerContext.getPropertiesComponent();
@@ -180,7 +231,7 @@ public void testRawAlready() {
         List<String> raws = List.of("prop1", "prop2", "prop3", "prop4");
         List<String> unchanged = List.of("prop2", "prop3", "prop5");
     
-        performConfigTest(name.getMethodName(), ROUTE_NOT_USED, "yaml",
+        performConfigTest(testName, ROUTE_NOT_USED, "yaml",
                           null, props,
                           (CamelContext runnerContext) -> {
                               PropertiesComponent pc = runnerContext.getPropertiesComponent();
@@ -208,7 +259,7 @@ public void testRawRequired() {
         props.putAll(propValues);
         List<String> raws = List.of("prop1", "prop3");
     
-        performConfigTest(name.getMethodName(), ROUTE_NOT_USED, "yaml",
+        performConfigTest(testName, ROUTE_NOT_USED, "yaml",
                           null, props,
                           (CamelContext runnerContext) -> {
                               PropertiesComponent pc = runnerContext.getPropertiesComponent();
@@ -237,7 +288,7 @@ public void testRawAltRequired() {
         props.putAll(propValues);
         List<String> raws = List.of("prop1", "prop3");
         
-        performConfigTest(name.getMethodName(), ROUTE_NOT_USED, "yaml",
+        performConfigTest(testName, ROUTE_NOT_USED, "yaml",
                           null, props,
                           (CamelContext runnerContext) -> {
                               PropertiesComponent pc = runnerContext.getPropertiesComponent();
@@ -266,7 +317,7 @@ public void testRawSuppressed() {
         props.putAll(propValues);
         List<String> raws = List.of("prop2");
     
-        performConfigTest(name.getMethodName(), ROUTE_NOT_USED, "yaml",
+        performConfigTest(testName, ROUTE_NOT_USED, "yaml",
                           null, props,
                           (CamelContext runnerContext) -> {
                               PropertiesComponent pc = runnerContext.getPropertiesComponent();
@@ -296,7 +347,7 @@ public void testCannotWrap() {
         props.putAll(propValues);
         List<String> raws = List.of("prop1", "prop3");
     
-        performConfigTest(name.getMethodName(), ROUTE_NOT_USED, "yaml",
+        performConfigTest(testName, ROUTE_NOT_USED, "yaml",
                           null, props,
                           (CamelContext runnerContext) -> {
                               PropertiesComponent pc = runnerContext.getPropertiesComponent();
@@ -315,7 +366,7 @@ public void testCannotWrap() {
     }
     @Test
     public void testSimpleConfiguration() {
-        performConfigTest(name.getMethodName(), XML_ROUTE, "xml",
+        performConfigTest(testName, XML_ROUTE, "xml",
                           null, null,
                           (CamelContext runnerContext) -> {
                               TriFunction<CamelContext, String, Object, Boolean> verifyOperation =
@@ -329,7 +380,7 @@ public void testSimpleConfiguration() {
     public void testComponentInitConfiguration() {
         assumeTrue(!sfLoginUrl.equals(MISSING_VALUE) && !sfClientId.equals(MISSING_VALUE) &&
                            !sfClientSecret.equals(MISSING_VALUE) && !sfRefreshToken.equals(MISSING_VALUE));
-        performConfigTest(name.getMethodName(), SALESFORCETASKS_YAML, "yaml",
+        performConfigTest(testName, SALESFORCETASKS_YAML, "yaml",
                           getComponentsToInit(), null,
                           (CamelContext runnerContext) -> {
                               TriFunction<CamelContext, String, Object, Boolean> verifyOperation =
@@ -348,7 +399,7 @@ public void testComponentInitConfigurationWithPropertyValues() {
         // 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",
+        performConfigTest(testName, PARAMETERIZED_SALESFORCE_ROUTE, "yaml",
                           getComponentsToInit(), props,
                           (CamelContext runnerContext) -> {
                               TriFunction<CamelContext, String, Object, Boolean> verifyOperation =
@@ -358,6 +409,122 @@ public void testComponentInitConfigurationWithPropertyValues() {
                           });
     }
     
+    @Test
+    public void testHdrDupConfiguration() {
+        String dupBeanName = testName + System.currentTimeMillis();
+        Map<String, String> hdrDups = Map.of("hdr1", "otherHdr1",
+                                             "hdr2", "otherHdr2",
+                                             "hdr3", "otherHdr3");
+        // Here, we want these tests to run sans a running Vantiq server, so we'll use the fake Vantiq component of
+        // our component (and, thus, endpoint).  This will allow us to see the results without real network
+        // interactions.
+        Map<String, Component> oRides = Map.of("vantiq", new FauxVantiqComponent());
+        String vantiqEpUri = constructVantiqUri("notReal", dupBeanName);
+        String vantiqRoute = constructVantiqRoute(vantiqEpUri);
+        performConfigTest(testName, vantiqRoute, "yaml",
+                          null, null,
+                          (CamelContext runnerContext) -> {
+                              TriFunction<CamelContext, Map<String, Object>, Map<String, Object>, Boolean> verifyOperation =
+                                      defineVerifyHeaders(1, "direct:vantiqStart",
+                                                          vantiqEpUri);
+                              for (int i = 0; i < 10; i++) {
+                                  String h1Val = "h1Value-" + i;
+                                  String h2Val = "h2Value-" + i;
+                                  String h3Val = "h3Value-" + i;
+                                  assert verifyOperation.apply(runnerContext,
+                                                               Map.of("headers", Map.of("hdr1", h1Val,
+                                                                                        "hdr2", h2Val,
+                                                                                        "hdr3", h3Val),
+                                                                      "message", Map.of("bodyPart", "arm")),
+                                                               Map.of("hdr1", h1Val,
+                                                                      "hdr2", h2Val,
+                                                                      "hdr3", h3Val,
+                                                                      "otherHdr1", h1Val,
+                                                                      "otherHdr2", h2Val,
+                                                                      "otherHdr3", h3Val));
+                              }
+                          }, null, dupBeanName, hdrDups, oRides);
+    }
+    
+    /**
+     * Create a callable that the test method will call.
+     *
+     * In this case, the callable "sends"
+     * message to the route which, in turn, makes a call to return some data.  We verify that the expected
+     * results are presented.
+     *
+     * In this case, our route uses the dynamically loaded component to make a call.
+     * @return TriFunction<CamelContext, Map<String, Object>, Map<String, Object>, Boolean>
+     */
+    public TriFunction<CamelContext, Map<String, Object>, Map<String, Object>, Boolean> defineVerifyHeaders(int msgCount, String startEp,
+                                                                                    String endEp) {
+        TriFunction<CamelContext, Map<String, Object>, Map<String, Object>, Boolean> verifyOperation =
+                (context, query, answerMap) -> {
+            boolean worked = true;
+            String routeId = context.getRoutes().get(0).getId();
+            ProducerTemplate template = null;
+            try {
+                assertFalse("At test start, context " + context.getName() + " is stopped", context.isStopped());
+    
+                template = context.createProducerTemplate();
+    
+                Endpoint res = context.getEndpoint(endEp);
+                assert res instanceof FauxVantiqEndpoint;
+                FauxVantiqEndpoint resultEndpoint = (FauxVantiqEndpoint) res;
+                FalseClient fc = resultEndpoint.myClient;
+    
+                //noinspection unchecked
+                Map<String, Object> hdrsToSend = (Map<String, Object>) query.get(STRUCTURED_MESSAGE_HEADERS_PROPERTY);
+                assert hdrsToSend != null;
+                log.debug("Sending message to {}", startEp);
+                template.sendBodyAndHeaders(startEp, query.get(STRUCTURED_MESSAGE_MESSAGE_PROPERTY), hdrsToSend);
+                Map lastMsg = fc.getLastMessageAsMap();
+                assert lastMsg.containsKey("op");
+                assert "notification".equals(lastMsg.get("op"));
+                assert lastMsg.containsKey("sourceName");
+                assert "someSource".equals(lastMsg.get("sourceName"));
+                assert lastMsg.containsKey("object");
+                assert lastMsg.get("object") instanceof Map;
+                //noinspection unchecked
+                Map<String, Object> msg = (Map<String, Object>) lastMsg.get("object");
+                assert msg.containsKey(STRUCTURED_MESSAGE_HEADERS_PROPERTY);
+                //noinspection unchecked
+                Map<String, Object> hdrs =
+                        (Map<String, Object>) msg.get(STRUCTURED_MESSAGE_HEADERS_PROPERTY);
+                assert hdrs != null;
+                answerMap.forEach( (key, value) -> {
+                    assertTrue("Missing header " + key, hdrs.containsKey(key));
+                    log.debug("For key {}, comparing value {} ({}) with expected {} ({}).",
+                              key, hdrs.get(key),
+                              hdrs.get(key) != null ? hdrs.get(key).getClass().getName() : hdrs.get(key),
+                              value,
+                              value != null ? value.getClass().getName() : value);
+                    if (hdrs.get(key) != null) {
+                        assert hdrs.get(key).equals(value);
+                    } else {
+                        assert value == null;
+                        // Also, verify that the header is present -- get(key) will return null if key isn't present.
+                        assert hdrs.containsKey(key);
+                    }
+                });
+            } catch (Exception e) {
+                log.error("Route " + routeId + ": Trapped exception", e);
+                worked = false;
+            } finally {
+                if (template != null) {
+                    try {
+                        template.stop();
+                        template.close();
+                        template.cleanUp();
+                    } catch (IOException e) {
+                       log.error("Trapped Exception closing template", e);
+                    }
+                }}
+            return worked;
+        };
+        return verifyOperation;
+    }
+
     public static final String sfLoginUrl = System.getProperty("camel-salesforce-loginUrl", MISSING_VALUE);
     public static final String sfClientId = System.getProperty("camel-salesforce-clientId", MISSING_VALUE);
     public static final String sfClientSecret = System.getProperty("camel-salesforce-clientSecret", MISSING_VALUE);
@@ -409,4 +576,26 @@ public List<Map<String, Object>> getComponentsToInit() {
     public static final Map<String, String> pValues = Map.of("directStart", "direct:start",
                                                              "query", "SELECT Id, Subject, OwnerId from Task",
                                                              "mockResult", "mock:result");
+    
+    
+    public String constructVantiqUri(String vantiqSvr, String beanName) {
+        return "vantiq://" + vantiqSvr + "/" +
+                "?" + SOURCE_NAME_PARAM + "=someSource" +
+                "&" + ACCESS_TOKEN_PARAM + "=someAccessToken" +
+                "&" + STRUCTURED_MESSAGE_HEADER_PARAM + "=true" +
+                "&" + HEADER_DUPLICATION_BEAN_NAME + "=" + beanName;
+    }
+    public String constructVantiqRoute(String vantiqEndpointStructuredHeaderMapUri) {
+        return "\n"
+                + "- route:\n"
+                + "    id: \"Vantiq Route with Header Duplication\"\n"
+                + "    from:\n"
+                + "      uri: \"direct:vantiqStart\"\n"
+                + "      steps:\n"
+                + "        - to:\n"
+                + "            uri: log:VantiqConfigurationTest?" +
+                                                "level=info&showHeaders=true&showAllProperties=true\n"
+                + "        - to:\n"
+                + "            uri: " + vantiqEndpointStructuredHeaderMapUri + "\n";
+    }
 }
diff --git a/camelConnector/src/test/java/io/vantiq/extsrc/camelconn/discover/CamelRunnerWithComponentSubs.java b/camelConnector/src/test/java/io/vantiq/extsrc/camelconn/discover/CamelRunnerWithComponentSubs.java
new file mode 100644
index 00000000..5bb76490
--- /dev/null
+++ b/camelConnector/src/test/java/io/vantiq/extsrc/camelconn/discover/CamelRunnerWithComponentSubs.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (c) 2023 Vantiq, Inc.
+ *
+ * All rights reserved.
+ *
+ * SPDX: MIT
+ */
+
+package io.vantiq.extsrc.camelconn.discover;
+
+import org.apache.camel.Component;
+
+import java.net.URI;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+/**
+ * Test version of CamelRunner.
+ * </p>
+ * This provides a type-safe, test only, mechanism to allow component type overrides for testing.  We do this to
+ * handle, amongst other things, Vantiq-server-less implementation of the Vantiq Camel Component for use in unit tests.
+ */
+public class CamelRunnerWithComponentSubs extends CamelRunner {
+    /**
+     * Create a CamelRunner instance
+     *
+     * @param appName String Name for this instance
+     * @param routeSpecification String specification for the route(s) to run.  Syntax must match
+     *         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 camelProperties Properties set of general property values that Camel can use for property resolution
+     * @param headerBeanName String Name of bean to use generate.  Should match what's in route & be unique to this
+     *         camel instance
+     * @param headerDuplications Map<String, String> directed set of header names to duplicate
+     */
+    public CamelRunnerWithComponentSubs(String appName, String routeSpecification, String routeSpecificationType,
+                                        List<URI> repos, String cacheDirectory, String loadedLibDir,
+                                        List<Map<String, Object>> initComponents,
+                                        Properties camelProperties,
+                                        String headerBeanName, Map<String, String> headerDuplications) {
+        
+        super(appName, routeSpecification, routeSpecificationType, repos, cacheDirectory, loadedLibDir, initComponents,
+              camelProperties, headerBeanName, headerDuplications);
+    }
+    
+    public CamelRunnerWithComponentSubs(String appName, String routeSpecification, String routeSpecificationType,
+                                        List<URI> repos, String cacheDirectory, String loadedLibDir,
+                                        List<Map<String, Object>> initComponents,
+                                        Properties camelProperties,
+                                        String headerBeanName, Map<String, String> headerDuplications,
+                                        Map<String, Component> componentOverrides) {
+        super(appName, routeSpecification, routeSpecificationType, repos, cacheDirectory, loadedLibDir, initComponents,
+              camelProperties, headerBeanName, headerDuplications);
+        this.setComponentOverrides(componentOverrides);
+    }
+}
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 b237f203..21c94e1f 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
@@ -8,11 +8,13 @@
 
 package io.vantiq.extsrc.camelconn.discover;
 
+import static org.junit.Assume.assumeTrue;
+
 import com.fasterxml.jackson.databind.JsonNode;
+import io.vantiq.extsrc.camel.HeaderDuplicationBean;
+import io.vantiq.extsrc.camel.VantiqEndpoint;
 import org.apache.camel.CamelContext;
 import org.apache.camel.Endpoint;
-import org.apache.camel.Exchange;
-import org.apache.camel.Predicate;
 import org.apache.camel.ProducerTemplate;
 import org.apache.camel.builder.RouteBuilder;
 import org.apache.camel.component.mock.MockEndpoint;
@@ -21,7 +23,6 @@
 import org.apache.camel.test.junit4.CamelTestSupport;
 import org.apache.commons.lang3.function.TriFunction;
 import org.apache.ivy.util.FileUtil;
-import org.junit.Assume;
 import org.junit.Before;
 import org.junit.FixMethodOrder;
 import org.junit.Rule;
@@ -42,7 +43,9 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Properties;
+import java.util.stream.Collectors;
 
 /**
  * Perform unit tests for component resolution
@@ -53,7 +56,6 @@
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
 public class VantiqComponentResolverTest extends CamelTestSupport {
     public final static String MISSING_VALUE = "<missing>";
-    
     @Rule
     public TestName testName = new TestName();
     
@@ -138,7 +140,12 @@ public void testResolutionSimpleCamel() throws Exception {
     @Test
     public void testResolutionSimpleCamelCached() throws Exception {
         // Here, we leave the cache alone
-        CamelResolver cr = new CamelResolver(this.getTestMethodName(), (URI) null,
+        
+        // Use same app name to avoid spurious Ivy errors about unknown resolvers.  Necessary since we didn't clear
+        // the cache, and Ivy keeps resolver names in the cache records.
+        String nameOfPreviousTest = this.getTestMethodName().substring(0, this.getTestMethodName().lastIndexOf(
+                "Cached"));
+        CamelResolver cr = new CamelResolver(nameOfPreviousTest, (URI) null,
                                              cache, dest);
         Collection<File> resolved = cr.resolve("org.apache.camel", "camel" + "-salesforce",
                                                context.getVersion(), testName.getMethodName());
@@ -155,17 +162,16 @@ public void testResolutionSimpleCamelCached() throws Exception {
     }
     
     @Test
-    public void testResolutionFailure() throws Exception {
+    public void testResolutionFailure() {
+        FileUtil.forceDelete(cache);    // Clear the cache
         CamelResolver cr = new CamelResolver(this.getTestMethodName(), (URI) null, null, dest);
         log.debug(cr.identity());
         assertTrue("Identity check:", cr.identity().contains(this.getTestMethodName()));
         assertTrue("Identity check:", cr.identity().contains(dest.getAbsolutePath()));
     
         try {
-            Collection<File> resolved = cr.resolve("org.apache.camel",
-                                                         "camel" + "-horse-designed-by-committee",
-                                                         context.getVersion(),
-                                                         testName.getMethodName());
+            cr.resolve("org.apache.camel", "camel" + "-horse-designed-by-committee",
+                       context.getVersion(), testName.getMethodName());
         } catch (ResolutionException re) {
             assert re.getMessage().contains("Error(s) encountered during resolution: ");
             assert re.getMessage().contains("org.apache.camel#camel-horse-designed-by-committee;");
@@ -174,7 +180,7 @@ public void testResolutionFailure() throws Exception {
         }
         
         try {
-            CamelResolver nope = new CamelResolver("wontexist",(URI) null, null, null);
+            new CamelResolver("wontexist",(URI) null, null, null);
             fail("Cannot create CamelResolver with a null destination");
         } catch (IllegalArgumentException iae) {
             assert iae.getMessage().contains("The destination parameter cannot be null");
@@ -192,8 +198,7 @@ public void testResolutionFailure() throws Exception {
         }
     
         try {
-            cr.resolve("somegroup", null,
-                             "someVersion", testName.getMethodName());
+            cr.resolve("somegroup", null, "someVersion", testName.getMethodName());
             fail("Null name should not work");
         } catch (IllegalArgumentException iae) {
             assert iae.getMessage().contains("The parameters organization, name, and revision must be non-null");
@@ -226,34 +231,34 @@ public void testResolutionAlternateRepos() throws Exception {
         URI s3Repo = new URI("https://vantiqmaven.s3.amazonaws.com/");
         CamelResolver cr = new CamelResolver(this.getTestMethodName(), s3Repo, cache, dest);
         Collection<File> resolved = cr.resolve("vantiq.models", "coco", "1.1", "meta",
-                                                       testName.getMethodName());
+                                               testName.getMethodName());
         assertEquals("Resolved file count: " + resolved.size(), 1, resolved.size());
         File[] files = resolved.toArray(new File[0]);
         assertEquals("File name match", "coco-1.1.meta", files[0].getName());
     }
     
     @Test
-    public void testStartRouteLoadedComponents() throws Exception {
+    public void testStartRouteLoadedComponents() {
         FileUtil.forceDelete(cache);    // Clear the cache
         RouteBuilderWithProps rb = new SimpleExternalRoute();
         assertNotNull("No routebuilder", rb);
         setUseRouteBuilder(false);
         try (CamelRunner runner = new CamelRunner(this.getTestMethodName(), rb, null,
                                                   IVY_CACHE_PATH, DEST_PATH,
-                                                  rb.getComponentsToInit(), null)) {
+                                                  rb.getComponentsToInit(), null, null, null)) {
             runner.runRoutes(false);
         }
     }
     
     @Test
-    public void testStartRouteLoadedComponentsAndMarshaling() throws Exception {
+    public void testStartRouteLoadedComponentsAndMarshaling() {
         FileUtil.forceDelete(cache);    // Clear the cache
         RouteBuilderWithProps rb = new MarshaledExternalRoute();
         assertNotNull("No routebuilder", rb);
         setUseRouteBuilder(false);
         try (CamelRunner runner = new CamelRunner(this.getTestMethodName(), rb, null,
                                                   IVY_CACHE_PATH, DEST_PATH,
-                                                  rb.getComponentsToInit(), null)) {
+                                                  rb.getComponentsToInit(), null, null, null)) {
             runner.runRoutes(false);
         }
     }
@@ -268,14 +273,16 @@ 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(), null)) {
+                                                  IVY_CACHE_PATH, DEST_PATH, rb.getComponentsToInit(),
+                                                  null, null, null)) {
             runner.runRoutes(false);
         }
     }
     
     @Test
-    public void testRouteTemplate() throws Exception {
-        BeanIncludingRouteTemplate rb = new BeanIncludingRouteTemplate(context);
+    public void testRouteTemplate() {
+        FileUtil.forceDelete(cache);    // Clear the cache
+        BeanIncludingRouteTemplate rb = new BeanIncludingRouteTemplate();
         assertNotNull("No routebuilder", rb);
         setUseRouteBuilder(false);
         // This is the set of discovered dependencies listed in the kamelet from which the route in question is taken.
@@ -287,7 +294,7 @@ public void testRouteTemplate() throws Exception {
         );
         try (CamelRunner runner = new CamelRunner(this.getTestMethodName(),rb, List.of(),
                                                   IVY_CACHE_PATH, DEST_PATH, rb.getComponentsToInit(),
-                                                   null)) {
+                                                   null, null, null)) {
             runner.setAdditionalLibraries(discoveredDependencies);
             runner.createCamelContext();
             runner.loadRouteFromText(rb.content, "yaml");
@@ -298,6 +305,70 @@ public void testRouteTemplate() throws Exception {
         }
     }
     
+    @Test
+    public void testHdrDupSetup() {
+        FileUtil.forceDelete(cache);    // Clear the cache
+        String headerBeanName = "MyHeaderBean" + System.currentTimeMillis();
+        HdrRouteTemplate rb = new HdrRouteTemplate(headerBeanName);
+        assertNotNull("No routebuilder", rb);
+        setUseRouteBuilder(false);
+        // This is the set of discovered dependencies listed in the kamelet from which the route in question is taken.
+        List<String> discoveredDependencies = List.of (
+                "org.apache.camel:camel-core",
+                "org.apache.camel:camel-aws2-s3",
+                "org.apache.camel.kamelets:camel-kamelets-utils:3.21.0",
+                "org.apache.camel:camel-kamelet"
+        );
+        
+        Map<String, String> hdrDupMap = Map.of("testHdr1", "dupOfHdr1",
+                                               "testHdr2", "dupOfHdr2",
+                                               "testHdr3", "dupOfHdr3");
+        try (CamelRunner runner = new CamelRunner(this.getTestMethodName(),rb, List.of(),
+                                                  IVY_CACHE_PATH, DEST_PATH, rb.getComponentsToInit(),
+                                                  null, headerBeanName, hdrDupMap)) {
+            runner.setAdditionalLibraries(discoveredDependencies);
+            CamelContext ctx = runner.createCamelContext();
+            // Override the component type to be used...
+            runner.doInit(); // This call creates & loads the bean.
+            CamelContext newCtx = runner.getCamelContext();
+            assert ctx == newCtx;
+            runner.loadRouteFromText(rb.content, "yaml");
+            // We only need to test loading the route -- we needn't run this route (no context, etc.)
+            // However, to test that we're passing thing beans around as expected, init things & verify that the bean
+            // was correctly created.  The component level tests verify that they operate as expected.
+            HeaderDuplicationBean bean = ctx.getRegistry().lookupByNameAndType(headerBeanName,
+                                                                               HeaderDuplicationBean.class);
+            assert bean != null;
+            Map<String, String> mapCopy = bean.getHeaderDuplicationMap();
+            assert mapCopy.size() == hdrDupMap.size();
+            mapCopy.forEach( (k, v) -> {
+                assert hdrDupMap.containsKey(k);
+                assert hdrDupMap.get(k).equals(v);
+            });
+            String vantiqEpUri = "vantiq://localhost:8080?structuredMessageHeader=true"
+                    + "&" + VantiqEndpoint.HEADER_DUPLICATION_BEAN_NAME + "=" + headerBeanName;
+            // I think that starting here fails due to trying to set up the bean name property.  The URL reported in
+            // the error looks correct (and the bean with that name is present).  But Camel claims it cannot resolve
+            // the endpoint as part of starting it up.  Still don't know why.  That's the $64K question.
+            VantiqEndpoint ep = ctx.getEndpoint(vantiqEpUri, VantiqEndpoint.class);
+            assert ep != null;
+            List<Endpoint> ves = ctx.getEndpointRegistry().values().stream()
+                                    .filter( v -> v instanceof VantiqEndpoint)
+                                    .collect(Collectors.toList());
+            assert ves.size() == 1;
+            assert ves.get(0) instanceof VantiqEndpoint;
+            VantiqEndpoint ve = (VantiqEndpoint) ves.get(0);
+            assert Objects.equals(ve.getHeaderDuplicationBeanName(), headerBeanName);
+            // Component tests verify that the map makes it thru, assuming the bean was created correctly.  Since we
+            // aren't guaranteed to have enough context here to make the real connection, this is as far as we can
+            // effectively test.
+        } catch (Exception e) {
+            fail("Trapped exception during test: " + e.getMessage() +
+                         (e.getCause() != null ? e.getCause().getMessage() : ""));
+        }
+    }
+    
+    
     public static final String QUERY_MONKEY = "monkey.wp.dg.cx";
     public static final String RESPONSE_MONKEY = "\"A Macaque, an old world species of "
             + "monkey native to Southeast Asia|thumb]A monkey is a primate of the "
@@ -343,7 +414,7 @@ public void testStartRunLoadedComponentsMarshaledAvroFailure() throws Exception
         RouteBuilderWithProps rb = new MakeDigCallMarshaledAvroFailure();
         assertNotNull("No routebuilder", rb);
         
-        performLoadAndRunTest(rb, false, rb.getComponentsToInit(), null);
+        performLoadAndRunTest(rb, false, rb.getComponentsToInit(), null, null, null);
     }
     
     @Test
@@ -359,7 +430,7 @@ public void testStartRunLoadedComponentsMarshaledGzip() throws Exception {
     
     @Test
     public void testStartRunLoadedComponentsSalesforceRefreshToken() throws Exception {
-        Assume.assumeTrue(sfLoginUrl != null &&
+        assumeTrue(sfLoginUrl != null &&
                                   sfClientId != null &&
                                   sfClientSecret != null &&
                                   sfRefreshToken != null);
@@ -501,11 +572,11 @@ public void testDiscoverBeanRoute() throws Exception {
     
     /**
      * Create a callable that the test method will call.
-     *
+     * <p>
      * In this case, the callable "sends"
      * message to the route which, in turn, makes a call to return some data.  We verify that the expected
      * results are presented.
-     *
+     * <p>
      * In this case, our route uses the dynamically loaded component to make a call.
      * @return TriFunction<CamelContext, String, Object, Boolean>
      */
@@ -540,31 +611,29 @@ public static TriFunction<CamelContext, String, Object, Boolean> defineVerifyOpe
                 log.debug("Test code using context: {}, route id: {}", context.getName(), routeId);
                 
                 resultEndpoint.expectedMessageCount(2);
-                resultEndpoint.expectedMessagesMatches(new Predicate() {
-                    public boolean matches(Exchange exchange) {
-                        Object msg = exchange.getIn().getBody();
-                        if (msg instanceof Message) {
-                            String str =
-                                    ((Message) exchange.getIn().getBody()).getSection(Section.ANSWER).get(0)
-                                                                          .rdataToString();
-                            log.debug("Matches: Route {} got {}", routeId, str);
-                            assertNotNull(answerStr);
-                            return answerStr.contains(str);
-                        } else if (msg instanceof Map) {
-                            //noinspection unchecked
-                            Map<String, ?> msgMap = (Map<String, ?>) msg;
-                            assertNotNull(answerMap);
-                            boolean result = true;
-                            for (Map.Entry<String, ?> ansEnt: answerMap.entrySet()) {
-                                result &= msgMap.containsKey(ansEnt.getKey());
-                                if (!ansEnt.getValue().equals(MISSING_VALUE)) {
-                                    result &= msgMap.get(ansEnt.getKey()).equals(ansEnt.getValue());
-                                }
+                resultEndpoint.expectedMessagesMatches(exchange -> {
+                    Object msg = exchange.getIn().getBody();
+                    if (msg instanceof Message) {
+                        String str =
+                                ((Message) exchange.getIn().getBody()).getSection(Section.ANSWER).get(0)
+                                                                      .rdataToString();
+                        log.debug("Matches: Route {} got {}", routeId, str);
+                        assertNotNull(answerStr);
+                        return answerStr.contains(str);
+                    } else if (msg instanceof Map) {
+                        //noinspection unchecked
+                        Map<String, ?> msgMap = (Map<String, ?>) msg;
+                        assertNotNull(answerMap);
+                        boolean result = true;
+                        for (Map.Entry<String, ?> ansEnt: answerMap.entrySet()) {
+                            result &= msgMap.containsKey(ansEnt.getKey());
+                            if (!ansEnt.getValue().equals(MISSING_VALUE)) {
+                                result &= msgMap.get(ansEnt.getKey()).equals(ansEnt.getValue());
                             }
-                            return result;
                         }
-                        return false;
+                        return result;
                     }
+                    return false;
                 });
     
                 Map<String, Object> headers = new HashMap<>();
@@ -599,20 +668,20 @@ public void performLoadAndRunTest(String content, String contentType,
                                       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
+        // message to the route which, in turn, makes the dig call to look up a monkey.  We verify that the expected
         // results is presented.
         //
         // In this case, our MakeDigCall route uses the (dynamically loaded) dns component to make a dig call.
     
         TriFunction<CamelContext, String, Object, Boolean> verifyOperation = defineVerifyOperation();
         
-        CamelContext runnerContext = null;
-        Thread runnerThread = null;
-        CamelRunner openedRunner = null;
+        CamelContext runnerContext;
+        Thread runnerThread;
+        CamelRunner openedRunner;
         
         try (CamelRunner runner =
                      new CamelRunner(this.getTestMethodName(), content, contentType, null,
-                                     IVY_CACHE_PATH, DEST_PATH, compToInit, propertyValues)) {
+                                     IVY_CACHE_PATH, DEST_PATH, compToInit, propertyValues, null, null)) {
             openedRunner = runner;
             runner.runRoutes(false);
             runnerContext = runner.getCamelContext();
@@ -636,25 +705,26 @@ public void performLoadAndRunTest(String content, String contentType,
     }
     
     public void performLoadAndRunTest(RouteBuilder rb, List<Map<String, Object>> compToInit) throws Exception {
-        performLoadAndRunTest(rb, true, compToInit, null);
+        performLoadAndRunTest(rb, true, compToInit, null, null, null);
     }
     public void performLoadAndRunTest(RouteBuilder rb, boolean shouldStart,
                                       List<Map<String, Object>> componentToInit,
-                                      Properties propertyValues) throws Exception {
+                                      Properties propertyValues, String headerBeanName,
+                                      Map<String, String> headerDuplications) 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
+        // message to the route which, in turn, makes the dig call to look up a monkey & aardvark.  We verify that the
         // expected results are presented.
         //
         // In this case, our MakeDigCall route uses the (dynamically loaded) dns component to make a dig call.
         TriFunction<CamelContext, String, Object, Boolean> verifyOperation = defineVerifyOperation();
     
-        CamelContext runnerContext = null;
-        Thread runnerThread = null;
+        CamelContext runnerContext;
+        Thread runnerThread;
         CamelRunner openedRunner = null;
         
         try (CamelRunner runner =
                      new CamelRunner(this.getTestMethodName(), rb, null, IVY_CACHE_PATH, DEST_PATH,
-                                     componentToInit, propertyValues)) {
+                                     componentToInit, propertyValues, headerBeanName, headerDuplications)) {
             openedRunner = runner;
             runner.runRoutes(false);
             runnerContext = runner.getCamelContext();
@@ -838,8 +908,6 @@ public void configure() {
     }
     
     private static class BeanIncludingRouteTemplate extends RouteBuilderWithProps {
-        CamelContext ctx;
-    
         public String content = ""
                 + "-   route-template:\n"
                 + "        id: Route templates from aws_s3_source:v3_21_0\n"
@@ -872,14 +940,64 @@ private static class BeanIncludingRouteTemplate extends RouteBuilderWithProps {
                 + "            steps:\n"
                 + "            -   process:\n"
                 + "                    ref: '{{renameHeaders}}'\n"
-                + "            -   to: vantiq://server.config?structuredMessageHeader=true\n";
-    
-        BeanIncludingRouteTemplate(CamelContext ctx) {
-            this.ctx = ctx;
-        }
+                + "            -   to: vantiq://server.config?structuredMessageHeader=true";
+
         @Override
-        public void configure() throws Exception {
+        public void configure() {
  
+        }
+    }
+    private static class HdrRouteTemplate extends RouteBuilderWithProps {
+        String headerDupBeanName;
+        
+        public String content = ""
+                + "-   route-template:\n"
+                + "        id: Route templates from aws_s3_source:v3_21_0\n"
+                + "        beans:\n"
+                + "        -   name: renameHeaders\n"
+                + "            type: '#class:org.apache.camel.kamelets.utils.headers.DuplicateNamingHeaders'\n"
+                + "            property:\n"
+                + "            -   key: prefix\n"
+                + "                value: CamelAwsS3\n"
+                + "            -   key: renamingPrefix\n"
+                + "                value: aws.s3.\n"
+                + "            -   key: mode\n"
+                + "                value: filtering\n"
+                + "            -   key: selectedHeaders\n"
+                + "                value: CamelAwsS3Key,CamelAwsS3BucketName\n"
+                + "        from:\n"
+                + "            uri: aws2-s3:someSillyBucket \n"
+                + "            parameters:\n"
+                + "                autoCreateBucket: 'false'\n"
+                + "                secretKey: 'dont tell'\n"
+                + "                accessKey: 'let me in'\n"
+                + "                region: 'us-west-2'\n"
+                + "                ignoreBody: 'false'\n"
+                + "                deleteAfterRead: 'false'\n"
+                + "                prefix: 'null'\n"
+                + "                useDefaultCredentialsProvider: 'true'\n"
+                + "                uriEndpointOverride: ''\n"
+                + "                overrideEndpoint: 'false'\n"
+                + "                delay: '500'\n"
+                + "            steps:\n"
+                + "            -   process:\n"
+                + "                    ref: '{{renameHeaders}}'\n"
+                + "            -   to: vantiq://localhost:8080?structuredMessageHeader=true";
+        
+        
+        HdrRouteTemplate(String headerDupBeanName) {
+            this.headerDupBeanName = headerDupBeanName;
+            if (headerDupBeanName != null) {
+                content = content.concat(
+                        "&" + VantiqEndpoint.HEADER_DUPLICATION_BEAN_NAME + "=" + headerDupBeanName
+                );
+            }
+            this.content = this.content.concat("\n");
+        }
+        
+        @Override
+        public void configure() {
+        
         }
     }
 }