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

Issue:
Camel Connector: Allow properties to require RAW handling
When providing RAW values, it is sometime necessary to wrap them in RAW indicators -- this tells the Camel mechanism to alter any URL encoding going on.  For some cases, we'd like to have the connector mechanism handle this, so add handling for a new config property which is a set of lists of property names that require RAW handling.  Upon receipt of the configuration, these values will be wrapped in RAW indicators before being given to Camel's Property component.


Discussion / hints:

Fix patch:
diff --git a/build.gradle b/build.gradle
index 9acd36dd..ea0f4584 100644
--- a/build.gradle
+++ b/build.gradle
@@ -40,7 +40,7 @@ ext {
     vantiqSDKVersion = '1.1.1'
     log4jVersion = '2.17.0'
     lombokVersion = '1.18.26'
-    camelVersion = '3.18.3'
+    camelVersion = '3.21.0'
 }
 
 
@@ -83,7 +83,7 @@ subprojects {
     apply plugin: 'groovy'
     apply plugin: 'idea'
     apply plugin: 'eclipse'
-    if (!['extpdk', 'pythonExecSource'].contains(it.name)) {
+    if (!['extpdk', 'pythonExecSource', 'kamelet2assembly'].contains(it.name)) {
         apply plugin: 'java'
     }
     apply plugin: 'com.chrisgahlert.gradle-dcompose-plugin'
diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle
new file mode 100644
index 00000000..e065617a
--- /dev/null
+++ b/buildSrc/build.gradle
@@ -0,0 +1,34 @@
+
+plugins {
+    id 'java'
+    id 'io.franzbecker.gradle-lombok' version '5.0.0'
+}
+
+repositories {
+    mavenCentral()
+}
+
+ext {
+    apacheCommonsLangVersion = '3.12.0'
+    jacksonVersion = '2.14.2'
+    log4JVersion='2.17.2'
+    lombokVersion = '1.18.24'
+    slf4jApiVersion = '1.7.25'
+    vantiqSDKVersion = '1.1.1'
+}
+
+dependencies {
+    implementation "org.projectlombok:lombok:${lombokVersion}"
+    implementation "org.snakeyaml:snakeyaml-engine:2.6"
+    implementation "org.apache.logging.log4j:log4j-slf4j-impl:${log4JVersion}"
+    implementation "org.apache.commons:commons-lang3:${apacheCommonsLangVersion}"
+    implementation "com.fasterxml.jackson.core:jackson-databind:${jacksonVersion}"
+}
+
+tasks.withType(JavaCompile) {
+    options.encoding = 'UTF-8'
+}
+
+test {
+    useJUnitPlatform()
+}
diff --git a/buildSrc/src/main/groovy/io/vantiq/extsrc/assy/tasks/AssemblyResourceGeneration.groovy b/buildSrc/src/main/groovy/io/vantiq/extsrc/assy/tasks/AssemblyResourceGeneration.groovy
new file mode 100644
index 00000000..c3536376
--- /dev/null
+++ b/buildSrc/src/main/groovy/io/vantiq/extsrc/assy/tasks/AssemblyResourceGeneration.groovy
@@ -0,0 +1,1008 @@
+/*
+ * Copyright (c) 2023 Vantiq, Inc.
+ *
+ * All rights reserved.
+ *
+ * SPDX: MIT
+ */
+
+package io.vantiq.extsrc.assy.tasks
+
+import static org.snakeyaml.engine.v2.common.FlowStyle.BLOCK
+
+import groovy.json.JsonOutput
+import groovy.text.SimpleTemplateEngine
+import groovy.util.logging.Slf4j
+import org.apache.commons.lang3.ArrayUtils
+import org.apache.commons.lang3.StringUtils
+import org.gradle.api.DefaultTask
+import org.gradle.api.GradleException
+import org.gradle.api.Project
+import org.gradle.api.artifacts.Configuration
+import org.gradle.api.tasks.OutputDirectory
+import org.gradle.api.tasks.TaskAction
+import org.snakeyaml.engine.v2.api.DumpSettings
+import org.snakeyaml.engine.v2.api.Load
+import org.snakeyaml.engine.v2.api.LoadSettings
+import org.snakeyaml.engine.v2.api.Dump
+
+import javax.inject.Inject
+import java.nio.file.Files
+import java.nio.file.Path
+import java.nio.file.Paths
+import java.util.jar.JarFile
+import java.util.regex.Pattern
+
+/**
+ * Gradle task to build assembly specifications from Camel Kamelet definitions.
+ */
+
+@Slf4j
+class AssemblyResourceGeneration extends DefaultTask {
+    public static final String VANTIQ_SERVER_CONFIG = 'vantiq://server.config'
+    public static final String VANTIQ_SERVER_CONFIG_JSON = VANTIQ_SERVER_CONFIG + '?consumerOutputJsonStream=true'
+    public static final String VANTIQ_SERVER_CONFIG_STRUCTURED = VANTIQ_SERVER_CONFIG + '?structuredMessageHeader=true'
+    public static final String VANTIQ_SERVER_CONFIG_JSON_STRUCTURED = VANTIQ_SERVER_CONFIG_JSON +
+        '&structuredMessageHeader=true'
+
+    public static final String KAMELETS_RESOURCE_FOLDER = 'kamelets'
+    public static final String KAMELETS_RESOURCE_PATH = KAMELETS_RESOURCE_FOLDER + '/'
+    public static final String YAML_FILE_TYPE = '.yaml'
+    public static final String YML_FILE_TYPE = '.yml'
+    public static final String SOURCE_KAMELET_PREFIX = '-source.kamelet'
+    public static final String SINK_KAMELET_PREFIX = '-sink.kamelet'
+    public static final String SOURCE_KAMELET_YAML_SUFFIX = SOURCE_KAMELET_PREFIX + YAML_FILE_TYPE
+    public static final String SOURCE_KAMELET_YML_SUFFIX = SOURCE_KAMELET_PREFIX + YML_FILE_TYPE
+    public static final String SINK_KAMELET_YAML_SUFFIX = SINK_KAMELET_PREFIX + YAML_FILE_TYPE
+    public static final String SINK_KAMELET_YML_SUFFIX = SINK_KAMELET_PREFIX + YML_FILE_TYPE
+    public static final String PROPERTY_X_DESCRIPTORS = 'x-descriptors'
+    public static final String DISPLAY_HIDE_VALUES = [
+        'urn:alm:descriptor:com.tectonic.ui:password',
+        'urn:camel:group:credentials'
+        ]
+    // The following is used in an all-lower-case comparison, so it should be entirely lower case words
+    public static final List<String> PASSWORD_LIKE = ['password']
+
+    public static final String KAMELET_SOURCE = 'kamelet:source'
+    public static final String KAMELET_SINK = 'kamelet:sink'
+    
+    public static final String ASSEMBLY_PACKAGE_BASE = 'com.vantiq.extsrc.camel.kamelets'
+    public static final String PACKAGE_SEPARATOR = '.'
+
+    public static final String FROM_ROUTE_TAG = 'from'
+    public static final String ID_ROUTE_TAG = 'id'
+    public static final String ROUTE_TEMPLATE_TAG = 'route-template'
+    public static final String TO_ROUTE_TAG = 'to'
+    public static final String URI_ROUTE_TAG = 'uri'
+
+    // Vantiq specific names for various things
+    public static final String PROPERTY_BAG_PROPERTY = 'ars_properties'
+    public static final String ADDITIONAL_CONFIG_PROPERTIES = 'additionalConfigProps'
+    public final static String VANTIQ_SYSTEM_PREFIX = 'system.'
+    public static final String VANTIQ_DOCUMENTS = 'documents'
+    public static final String VANTIQ_PROJECTS = 'projects'
+    public static final String VANTIQ_RULES = 'rules'
+    public static final String VANTIQ_SERVICES = 'services'
+    public static final String VANTIQ_SOURCES = 'sources'
+    public static final String VANTIQ_TYPES = 'types'
+    public static final String RULE_SINK_NAME_SUFFIX = '_svcToSrc'
+    public static final String RULE_SOURCE_NAME_SUFFIX = '_srcToSvc'
+    public static final String SERVICE_NAME_SUFFIX = '_service'
+    public static final String SOURCE_NAME_SUFFIX = '_source'
+
+    public static final String CAMEL_SOURCE_TYPE = 'CAMEL'
+    public static final String CONFIG_CAMEL_RUNTIME = 'camelRuntime'
+    public static final String CONFIG_CAMEL_GENERAL = 'general'
+    public static final String CAMEL_RUNTIME_APPNAME = 'appName'
+    public static final String CAMEL_RUNTIME_PROPERTY_VALUES = 'propertyValues'
+    public static final String GENERAL_ADDITIONAL_LIBRARIES = 'additionalLibraries'
+    public static final String CAMEL_RAW_HANDLING = 'rawValuesRequired'
+    // TODO: Add ability to add some overrides here where we want Camel RAW handling to be performed
+    public static final String CAMEL_DISCOVERED_RAW = 'discovered'
+    public static final String CAMEL_RUNTIME_ROUTE_DOCUMENT = 'routesDocument'
+    public static final String YAML_ROUTE_SUFFIX = '.routes.yaml'
+    public static final String JSON_SUFFIX = '.json'
+    public static final String VAIL_SUFFIX = '.vail'
+    public static final String CAMEL_MESSAGE_SCHEMA = 'com.vantiq.extsrc.camelcomp.message'
+
+    public static final String OVERVIEW_SUFFIX = '.overview.md'
+    public static final String OVERVIEW_SOURCE_DISCLAIMER =
+        '\n\n> _This description has been provided by the Kamelet from which this assembly was generated. ' +
+        'Consider that context when using the information contained herein._\n\n'
+    public static final String VANTIQ_NOTES_SUFFIX = '.vantiqNotes.md'
+
+    public final Project project
+    public static String packageSafeCamelVersion
+    public static String rawCamelVersion
+    public static final String RESOURCE_BASE_DIRECTORY = 'src/main/resources'
+    public static final String VANTIQ_NOTES_DIRECTORY = 'vantiqNotes'
+
+    public static final String ASSEMBLY_RESOURCE_BASE_PROPERTY = 'generatedResourceBase'
+
+    // Note that the following annotation 1) allows gradle to figure out what's up, and 2) will have gradle
+    // create the directory (path)
+    @OutputDirectory
+    final File assemblyResourceBase = new File(project.buildDir,
+        project.getExtensions()?.getExtraProperties()?.getAt(ASSEMBLY_RESOURCE_BASE_PROPERTY) as String)
+    
+    @Inject
+    AssemblyResourceGeneration(Project project) {
+        this.project = project
+        if (project == null) {
+            throw new GradleException('AssemblyGen requires a non-null project.')
+        }
+        // Since we depend only on dependencies (& this code), we'll just always re-run
+        this.outputs.upToDateWhen { false }
+
+        if (this.assemblyResourceBase == null) {
+            throw new GradleException('AssemblyGen requires a non-null project extension property.')
+        }
+        log.info('Creating assembly resources under {}', this.assemblyResourceBase)
+    }
+    
+    /**
+     * Overall processing for the generation of assemblies from kamelet declarations.
+     *
+     * @throws Exception
+     */
+    @TaskAction
+    void generateAssemblies() throws Exception {
+        Path buildDir = project.getBuildDir()?.toPath()
+        assert buildDir != null
+        if (!(Files.exists(buildDir) && Files.isDirectory(buildDir))) {
+            throw new GradleException('Internal Error: buildDir is not present or is not a directory: ' + buildDir)
+        }
+        Path generateBase = assemblyResourceBase.toPath()
+        LoadSettings settings = LoadSettings.builder().build()
+        Load load = new Load(settings)
+    
+        // From the gradle data, construct the list of resolved dependencies & build a class loader to use
+        Configuration conf = project.getConfigurations().getByName('runtimeClasspath')
+        Set<File> files = conf.resolve()
+        log.info('File count from runtimeClasspath: {}', files.size())
+        File[] jars = files.toArray(new File[0])
+        ArrayList<URL> urls = new ArrayList<>(jars.length)
+        for (File f: jars) {
+            log.debug('Adding {} to list of URLs', f.toURI().toURL())
+            urls.add(f.toURI().toURL())
+        }
+        URLClassLoader classloader =
+                new URLClassLoader(urls.toArray(new URL[0]) as URL[])
+        // Find the URL that contains the 'kamelets/' folder (which contains the kamelets to process)
+        URL klUrl = classloader.getResource(KAMELETS_RESOURCE_FOLDER)
+        log.debug('URL for kamelets: {}', klUrl)
+        if (klUrl == null) {
+            throw new GradleException("kamelets' resource is null")
+        }
+        
+        // Find path for jar file containing 'kamelets' folder
+        String kameletJarFilename = klUrl.toString()
+        kameletJarFilename = kameletJarFilename.substring(0, kameletJarFilename.indexOf('!/'))
+        kameletJarFilename = kameletJarFilename.substring(KAMELETS_RESOURCE_PATH.length())
+
+        def kjVersion = kameletJarFilename.substring(kameletJarFilename.lastIndexOf('/') + 1) - '.jar'
+        rawCamelVersion = kjVersion - 'camel-kamelets-'
+        packageSafeCamelVersion = 'v' + rawCamelVersion.replaceAll('\\.', '_')
+        log.lifecycle('Using Kamelets from Camel version {}.', rawCamelVersion)
+
+        processKameletsFromJar(kameletJarFilename, load, classloader, generateBase)
+    }
+
+    /**
+     * Given a jar file, process Kamelets defined therein
+     *
+     * @param kameletJarFile String Name for the jar file containing the kamelets
+     * @param yamlLoader Load YAML file loader
+     * @param classLoader URLClassLoader classloader for files within the named jar file
+     * @param outputRoot Path Root of location into which results will be placed
+     */
+    void processKameletsFromJar(String kameletJarFile, Load yamlLoader,
+                                URLClassLoader classLoader, Path outputRoot) {
+        def jff = new JarFile(kameletJarFile)
+        jff.withCloseable { jf ->
+            try {
+                String[] discards = new String[0]
+                int kameletCount = 0
+                // For each entry in this JAR file, determine if it's one we care about
+                jf.entries().each { jarEntry ->
+                    if (jarEntry.getName().startsWith(KAMELETS_RESOURCE_PATH)) {
+                        String kamelet = jarEntry.getName().substring(KAMELETS_RESOURCE_PATH.length())
+                        boolean isSink = kamelet.endsWith(SINK_KAMELET_YAML_SUFFIX) || kamelet.endsWith(SINK_KAMELET_YML_SUFFIX)
+                        boolean isSource =
+                            kamelet.endsWith(SOURCE_KAMELET_YAML_SUFFIX) || kamelet.endsWith(SOURCE_KAMELET_YML_SUFFIX)
+                        // ignore things neither sink nor source
+                        if (isSink || isSource) {
+                            kameletCount += 1
+                            createAssemblyFromKamelet(kamelet, isSink, yamlLoader, classLoader, outputRoot)
+                        }
+                    } else {
+                        discards = ArrayUtils.add(discards, jarEntry.getName())
+                    }
+                }
+
+                if (log.isDebugEnabled() && discards.length > 0) {
+                    log.debug('Discards')
+                    for (String dumped : discards) {
+                        log.debug('\tignored: {}', dumped)
+                    }
+                }
+                project.getLogger().lifecycle('{} kamelets found & parsed.',
+                    kameletCount)
+                project.getLogger().lifecycle('    {} kamelets ignored -- (neither source nor sink as ' +
+                    'per naming conventions).', discards.length)
+            } catch (Exception e) {
+                throw new GradleException('Error opening jar file: ' + kameletJarFile + ' :: ' +
+                    e.getClass().getName() + ' -- ' + e.getMessage())
+            }
+        }
+    }
+
+    /**
+     * Given a single, named kamelet, construct a Vantiq assembly to perform our processing
+     *
+     * @param kameletDefinition String name of kamelet to be processed
+     * @param isSink Boolean whether this is a SINK (vs. SOURCE -- others are ignored by the caller)
+     * @param yamlLoader Load loader for YAML files in the kamelet
+     * @param classLoader URLClassLoader loader to use to fetch the Kamelet definition
+     * @param outputRoot Path location into which to save the Assembly definition
+     */
+    void createAssemblyFromKamelet(String kameletDefinition, Boolean isSink, Load yamlLoader, URLClassLoader classLoader,
+                                   Path outputRoot) {
+        // If it's a source or sink kamelet, we'll turn it into an assembly containing a source to
+        // process messages to or from that connection.
+        log.info('    Processing kamelet name: {}', kameletDefinition)
+        // First, read the YAML file that defines the kamelet.  From this, we'll extract the
+        // appropriate information to construct our assembly.
+        Map<String, Object> yamlMap = getYamlEnt(yamlLoader, classLoader, kameletDefinition)
+        log.debug('    \n<<<Result: parsed.size(): {}, parsed: {}>>>\n', yamlMap.size(),
+            yamlMap.get('spec'))
+        String typeLabel = (String) ((Map<String, String>) ((Map<String, Object>) yamlMap
+            .get('metadata')).get('labels')) .get('camel.apache.org/kamelet.type')
+        if (typeLabel != 'sink' && typeLabel != 'source')  {
+            throw new GradleException('Unexpected camel.apache.org/kamelet.type label: ' + typeLabel)
+        } else if (isSink != (typeLabel == 'sink')) {
+            throw new GradleException('Name vs. camel.apache.org/kamelet.type label mismatch: ' + typeLabel)
+        }
+
+        //noinspection unchecked
+        String title =
+            (String) ((Map<String, Object>) ((Map<String, Object>) yamlMap.get('spec'))
+                .get('definition')).get('title')
+        log.info('Found spec for {}', title)
+        String overview = (String) ((Map<String, Object>) ((Map<String, Object>) yamlMap.get('spec'))
+            .get('definition')).get('description')
+        List<String> requiredProps = (List<String>) ((Map<String, Object>)((Map<String, Object>) yamlMap.get('spec'))
+            .get('definition')).get('required')
+        log.debug('Required Props from kamelet definition: {}', requiredProps ?: '<<empty>>')
+        List<String> dependencies = (List<String>) ((Map<String, Object>) yamlMap.get('spec')).get('dependencies')
+        // Dependencies can also be declared at the type level, so grab those as well.
+        Map<String, Object> types = (Map<String, Object>) ((Map<String, Object>) yamlMap.get('spec'))
+            .get('types')
+        if (types != null) {
+            types.forEach (( k, v ) -> {
+                if (v.containsKey('dependencies')) {
+                    List<String> tDeps = (List<String>) v.get('dependencies')
+                    dependencies.putAll(tDeps)
+                    log.debug('For type {}, found dependencies: {}', k, tDeps)
+                }
+            })
+        }
+        log.debug('Kamelet-defined dependencies: {}', dependencies)
+
+        // Read any defined properties.  These will become properties for our assembly.
+        //noinspection unchecked
+        Map<String, Object> props =
+            (Map<String, Object>) ((Map<String, Object>) ((Map<String, Object>) yamlMap.get('spec'))
+                .get('definition')).get('properties')
+        if (log.isDebugEnabled()) {
+            log.debug('    containing {} properties:', props.size())
+            props.forEach((k, v) -> {
+                //noinspection unchecked
+                log.debug('        name: {}, [type: {}, desc: {}]', k,
+                    ((Map<String, String>) v).get('type'),
+                    ((Map<String, String>) v).get('description'))
+            })
+        }
+        String kamName = kameletDefinition.substring(0, kameletDefinition.indexOf('.kamelet.yaml'))
+        kamName = kamName.replace('-', '_')
+        String packageName = String.join(PACKAGE_SEPARATOR, ASSEMBLY_PACKAGE_BASE, packageSafeCamelVersion, kamName)
+
+        DumpSettings yamlDumpSettings = DumpSettings.builder()
+            .setIndent(4)
+                // FLOW (default)serializes any embedded maps.
+                // This will recurse.
+            .setDefaultFlowStyle(BLOCK)
+            .build()
+        Dump yamlDumper = new Dump(yamlDumpSettings)
+
+        // Now, extract the route template.  This will become our route document after we change
+        // kamelet:source or kamelet:sink to vantiq server URLs
+
+        //noinspection unchecked
+        Map<String, Object> template = (Map<String, Object>) ((Map<String, Object>) yamlMap
+            .get('spec')).get('template')
+        String originalTemplateString = yamlDumper.dumpToString(template)
+        log.debug('\n>>>   Original Template as Yaml:\n{}', originalTemplateString)
+
+        List<Map<String, Object>> routeSpec = constructRouteText(kamName, template)
+        log.info('YAML document Map: {}', routeSpec)
+
+        // Now, generate & populate results
+        Path assemblyRoot = Paths.get(outputRoot.toAbsolutePath().toString(), kamName + "_${packageSafeCamelVersion}")
+        if (!Files.exists(assemblyRoot)) {
+            log.info('Creating kamelet-specific project directory: {}', assemblyRoot)
+            assemblyRoot = Files.createDirectories(assemblyRoot)
+        }
+        String routesDocumentString = yamlDumper.dumpToString(routeSpec)
+
+        if (log.isDebugEnabled()) {
+            log.debug('\nResults for Kamelet: {}', kamName)
+            log.debug('\n>>>   Properties:')
+            props.forEach((name, val) -> {
+                //noinspection unchecked
+                log.debug('    Name: {} :: [title: {}, type: {}, desc: {}]', name,
+                    ((Map<String, String>) val).get('title'),
+                    ((Map<String, String>) val).get('type'),
+                    ((Map<String, String>) val).get('description'))
+            })
+            log.debug('\n>>>   Converted Template as Yaml:\n{}', routesDocumentString)
+        }
+        log.info('\nWriting project to: {}', assemblyRoot)
+
+        Map routeDoc = writeRoutesDocument(assemblyRoot, packageName, kamName, routesDocumentString)
+        Map overviewDoc = (overview ? writeOverviewDocument(assemblyRoot, packageName, kamName, overview) : null)
+        Map notesDoc = writeVantiqNotesDocument(assemblyRoot, packageName, kamName)
+        Map sourceDef = addSourceDefinition(assemblyRoot, packageName, kamName,
+            routeDoc.path.fileName.toString(), props, dependencies)
+        log.debug('Created sourceDef: {}', sourceDef)
+        Map serviceDef = addServiceDefinition(assemblyRoot, packageName, kamName, isSink)
+        log.debug('Created serviceDef: {}', serviceDef)
+
+        Map ruleDef = addRoutingRule(assemblyRoot, packageName, kamName,
+            sourceDef.vailName as String, (serviceDef.vailName as String) - (packageName + PACKAGE_SEPARATOR), isSink)
+        log.debug('Created rule def: {}', ruleDef)
+        List<String> componentList = [
+            routeDoc.reference as String,
+            sourceDef.reference as String,
+            serviceDef.reference as String,
+        ]
+        if (overview != null) {
+            componentList << (overviewDoc.reference as String)
+        }
+        if (notesDoc != null) {
+            componentList << (notesDoc.reference as String)
+        }
+        if (ruleDef) {
+            componentList << (ruleDef.reference as String)
+        }
+        log.debug("Adding project def: addProjDef({}, {}, {}, {}, {}, {}",
+            assemblyRoot, packageName, kamName, title, props, componentList)
+        //noinspection GroovyUnusedAssignment       // Useful for debugging...
+        Map projectDef = addProjectDefinition(assemblyRoot, packageName, kamName,
+            title, props, requiredProps, componentList, serviceDef.service as Map)
+        log.debug('Created projectDef: {}', projectDef)
+    }
+
+    /**
+     * Add service definition
+     *
+     * @param kameletAssemblyDir Path location of the base location for the project artifacts
+     * @param packageName String name of the package to which this service will belong
+     * @param kamName String name of the kamelet which will be used to construct the service name
+     * @param isSink Boolean representing whether this service is for a sink or source kamelet/assembly
+     * @returns Map<String, Object> containing Path written & resourceReference
+     */
+    static Map<String, Object> addServiceDefinition(Path kameletAssemblyDir, String packageName, String kamName,
+                                                    Boolean isSink) {
+        def plainName = kamName + SERVICE_NAME_SUFFIX
+        def name = packageName + PACKAGE_SEPARATOR + plainName
+        def service = [
+            active: true,
+            ars_relationships: [],
+            description: 'Service connecting source ' + packageName + PACKAGE_SEPARATOR + kamName +
+                ' to Vantiq service events',
+            globalType: null,
+            interface: [],
+            internalEventHandlers: [],
+            name: name,
+            partitionType: null,
+            replicationFactor: 1,
+            scheduledProcedures: [:]
+        ]
+        def eventName = plainName + 'Event'
+        def eventType = [
+            (eventName): [
+                direction: (isSink ? 'INBOUND' : 'OUTBOUND'),
+                eventSchema: CAMEL_MESSAGE_SCHEMA,
+                isReliable: false
+            ]
+        ]
+
+        def associatedRule = "/rules/${name + (isSink ? RULE_SINK_NAME_SUFFIX : RULE_SOURCE_NAME_SUFFIX)}"
+        if (isSink) {
+            eventType[eventName].implementingResource = associatedRule
+        } else {
+            service.internalEventHandlers += associatedRule
+        }
+        service.eventTypes = eventType
+
+        String svcDefJson = JsonOutput.prettyPrint(JsonOutput.toJson(service))
+        log.info('Creating service {}:\n{}', service.name, svcDefJson)
+        Map<String, Object> retVal = [:]
+        retVal.path = writeVantiqEntity(VANTIQ_SERVICES, kameletAssemblyDir, packageName,
+            plainName + JSON_SUFFIX, svcDefJson, true)
+        retVal.reference = buildResourceRef(VANTIQ_SERVICES, service.name as String)
+        retVal.vailName = service.name
+        retVal.service = service
+        return retVal
+    }
+
+    /**
+     * Add source definition for Vantiq source for this kamelet.
+     *
+     * Will generate a camelConnector source defined for this kamelet using the properties and routes
+     * defined.
+     *
+     * @param kameletAssemblyDir Path location of the base location for the project artifacts
+     * @param packageName String name of the package to which this source will belong
+     * @param kamName String name of the kamelet which will be used to construct the source name
+     * @param routeDoc String name of the document containing the route
+     * @param props Map<String, Object> The Camel properties associated with this kamelet.
+     * @param declaredDependencies List<String> dependencies found in the kamelet file
+     * @returns Map<String, Object> containing Path written & resourceReference
+     */
+    static Map<String, Object> addSourceDefinition(Path kameletAssemblyDir, String packageName, String kamName,
+                                                   String routeDoc, Map<String, Object> props,
+                                                   List<String> declaredDependencies) {
+        log.info('Creating source for kamelet: {} in package {} using route: {}, ')
+        def sourceDef = [:]
+        def plainName = kamName + SOURCE_NAME_SUFFIX
+        sourceDef.active = true
+        sourceDef.name = packageName + PACKAGE_SEPARATOR + plainName
+        sourceDef.messageType = CAMEL_MESSAGE_SCHEMA
+        sourceDef.activationConstraint = ''
+        sourceDef.type = CAMEL_SOURCE_TYPE
+        Map<String, Object> camelAppConfig = [(CAMEL_RUNTIME_APPNAME): kamName,
+                              (CAMEL_RUNTIME_ROUTE_DOCUMENT): routeDoc]
+        List<String> propNameList = []
+        List<String> rawHandlingRequired = []
+
+        // Note: Here, we could have the CAMEL source type define camelRuntime,propertyValues as configurable.
+        // However, if we did that, on installation, the user would have to provide an object with the various lower
+        // level items & their values.  And any descriptive information provided by the kamelet definition would be
+        // lost.  Consequently, it seems preferable for each source to define the set it needs and let the assembly
+        // installation and configuration mechanism take its course.
+        props.each { aProp ->
+            // Placeholder values for each property here are not required. But we need to generate the list of
+            // configurable properties to keep the assembly substitution mechanism happy.
+            //
+            // Note that adding all the placeholders was problematic when not all the properties are provided.  When
+            // an assembly installer configured their assembly & did not provide a value for some (optional)
+            // property, the placeholder value is replaced, but replaced with null.  So, we got properties sent down
+            // with the value null.  We work around that (indeed, the CamelConnector config handler just ignores
+            // these as good practice), but avoidance at the higher level seems preferable.
+            propNameList << new String().join('.', ['config',
+                                                    CONFIG_CAMEL_RUNTIME,
+                                                    CAMEL_RUNTIME_PROPERTY_VALUES,
+                                                    aProp.key])
+            if (aProp.value instanceof Map) {
+                Map pDesc = aProp.value as Map
+                // FIXME: With more full dependency handling, this no longer seems necessary, at least for aws things.  Not
+                //  sure why that would make a difference, but the templates we get have the RAW encoding of the properties
+                //  when defining the endpoint
+                // TODO: Remove or otherwise make this section conditional...
+
+//                if (PASSWORD_LIKE.contains(pDesc.format?.toLowerCase()) ||
+//                    (pDesc[PROPERTY_X_DESCRIPTORS] != null &&
+//                        DISPLAY_HIDE_VALUES.contains(((String) pDesc[PROPERTY_X_DESCRIPTORS])))) {
+//                    // for things that are passwords or are marked to be masked in display, tell connector to instruct
+//                    // Camel to use RAW handling
+//                    rawHandlingRequired << aProp.key
+//                }
+            } else {
+                throw new GradleException('Unexpected type for property description: ' + aProp.value?.class?.name)
+            }
+        }
+        log.debug('Configurable properties: {}', propNameList)
+        log.debug('Configuration properties that required raw handling: {}', rawHandlingRequired)
+        if (propNameList) {
+            sourceDef[PROPERTY_BAG_PROPERTY] = [:]
+            sourceDef[PROPERTY_BAG_PROPERTY][ADDITIONAL_CONFIG_PROPERTIES] = propNameList
+            // As noted above, we don't need all the placeholders inserted. But we do need their container...
+            camelAppConfig[CAMEL_RUNTIME_PROPERTY_VALUES] = [:]
+            if (rawHandlingRequired) {
+                camelAppConfig[CAMEL_RAW_HANDLING] = [:]
+                camelAppConfig[CAMEL_RAW_HANDLING][CAMEL_DISCOVERED_RAW] = rawHandlingRequired
+            }
+        }
+        def generalConfig = [:]
+        if (declaredDependencies && declaredDependencies.size() > 0) {
+            List<String> additionalLibraries = new ArrayList<>(declaredDependencies.size())
+            declaredDependencies.forEach( ( dep ) -> {
+                if (dep.startsWith('camel:')) {
+                    additionalLibraries.add('org.apache.camel:camel-' + dep.substring('camel:'.length()))
+                } else if (dep.startsWith('mvn:')) {
+                    additionalLibraries.add(dep.substring('mvn:'.length()))
+                } // Ignore github things...
+            })
+            generalConfig[GENERAL_ADDITIONAL_LIBRARIES] = additionalLibraries
+        }
+        sourceDef.config = [ (CONFIG_CAMEL_RUNTIME): camelAppConfig, (CONFIG_CAMEL_GENERAL): generalConfig]
+        String srcDefJson = JsonOutput.prettyPrint(JsonOutput.toJson(sourceDef))
+        log.debug('Creating source {}:\n{}', sourceDef.name, srcDefJson)
+        Map<String, Object> retVal = [:]
+        retVal.path = writeVantiqEntity(VANTIQ_SOURCES, kameletAssemblyDir, packageName,
+            plainName + JSON_SUFFIX, srcDefJson, true)
+        retVal.reference = buildResourceRef(VANTIQ_SOURCES, sourceDef.name as String)
+        retVal.vailName = sourceDef.name
+        return retVal
+    }
+
+    /**
+     * Construct basic project definition for kamelet.
+     *
+     * Builds project definition corresponding to the kamelet.  Kamelet defined properties become configuration
+     * properties & mappings, and the components used are added as resources.
+     *
+     * @param kameletAssemblyDir Path the directory used for this kamelet-based assembly
+     * @param packageName String package name we'll use for this assembly
+     * @param projectName String name of the kamelet on which we're working
+     * @param description String Description of the project
+     * @param props Map<String, Object> Kamelet properties extracted from the definition
+     * @param reqProps List<String> Kamelet properties listed as required
+     * @param components List<String> List of resources references that comprise this assembly project.
+     * @param serviceDef Map<String, Object> Definition of the service used here.
+     * @returns Map<String, Object> containing Path written & resourceReference
+     */
+    static Map<String, Object> addProjectDefinition(Path kameletAssemblyDir, String packageName, String projectName,
+                                                    String description, Map<String, Object> props,
+                                                    List<String> reqProps, List<String> components,
+                                                    Map<String, Object> serviceDef) {
+        def project = [:]
+        project.name = packageName
+        project.type = 'dev'
+        project.ars_relationships = []
+        project.links = []
+        project.tools = []
+        project.views = []
+        project.partitions = []
+        project.isAssembly = true
+
+        project.options = [
+            description: description + " from Camel version ${rawCamelVersion}",
+            filterBitArray: 'ffffffffffffffffffffffffffffffff',
+            type: 'dev',
+            v: 5,
+            isModeloProject: true,
+            exclusionList: [ "type/${CAMEL_MESSAGE_SCHEMA}",
+                             "sourceimpl/${CAMEL_SOURCE_TYPE}",
+            ],
+        ]
+
+        // Insert the config properties & mappings first so they'll be known if/when encountered
+        Map<String, Map<String, Object>> cfgProps = [:]
+        Map<String, List<Map<String, Object>>> cfgMappings = [:]
+        def sourceName
+        props.each { pName, o ->
+            log.debug("\t processing property {}: {}", pName, o)
+            Map pDesc
+            if (o instanceof Map) {
+                pDesc = o as Map
+            } else {
+                throw new GradleException("prop desc not map: " + o.class.name)
+            }
+            log.debug("\t processing property {}: {}", pName, pDesc)
+
+            // Properties are required if there is no default...
+            Map<String, Object> propDesc = [:]
+            if (pDesc.required == null && reqProps && reqProps.contains(pName)) {
+                // If property itself has no "required" entry but the outer level does, set it on the property for
+                // our processing.  This allows us to streamline this handling.
+                pDesc.required = true
+            }
+            log.trace('Pdesc.required for {}: {}', pName, pDesc.required)
+            if (pDesc.required instanceof Boolean) {
+                propDesc.required = pDesc.required
+            } else if (pDesc.required instanceof String) {
+                propDesc.required = ((String) pDesc.required).toBoolean()
+            } else if (pDesc.required != null) {
+                throw new GradleException('Unexpected value/type for property ' + propDesc.pName + ': type: ' +
+                        propDesc.required?.class?.name + ', value: ' + propDesc.required)
+            } // Else, it's null so ignore it.
+
+            // Here, we need to be wary of Groovy Truth. Check for non-null since default values of '', false, etc.
+            // are "false" in Groovy Truth land.
+            if (pDesc.default != null) {
+                propDesc.default = pDesc.default
+                // Kamelets allow a required parameter to have a default value, Vantiq assemblies do not.
+                // If we have a default value, don't set the assembly config prop "required" flag.
+                if (propDesc.required != null && (pDesc.required as Boolean == true)) {
+                    propDesc.remove('required')
+                    log.debug('Project {} :: propDesc.required for {}: removed due to default value of "{}"',
+                        projectName, pName, propDesc.default)
+                }
+            }
+            if (pDesc.description != null) {
+                propDesc.description = pDesc.description
+            } else if (pDesc.title != null) {
+                propDesc.description = pDesc.title
+            } else {
+                propDesc.description = 'Value for ' + pDesc.pName + '.'
+            }
+            if (pDesc.type != null) {
+                propDesc.type = StringUtils.capitalize(pDesc.type as String)
+            } else {
+                propDesc.type = 'String'
+            }
+            // TODO: Secret type parameters re delivered to the source as resource refs to the secret.  The secret
+            //  expansion doesn't handle this.  Make server-side changes to improve this situation, then re-enable
+            //  this enforcement of secret values.
+//            if (PASSWORD_LIKE.contains(pDesc.format?.toLowerCase()) ||
+//                (pDesc[PROPERTY_X_DESCRIPTORS] != null &&
+//                    DISPLAY_HIDE_VALUES.contains(((String) pDesc[PROPERTY_X_DESCRIPTORS])))) {
+//                // for things that are passwords or are marked to be masked in display, we will require a secret.
+//                propDesc.type = 'Secret'
+//                propDesc.description += ' (Please provide the name of the Vantiq Secret containing this value.)'
+//            }
+            if (PASSWORD_LIKE.contains(pDesc.format?.toLowerCase()) ||
+                (pDesc[PROPERTY_X_DESCRIPTORS] != null &&
+                    DISPLAY_HIDE_VALUES.contains(((String) pDesc[PROPERTY_X_DESCRIPTORS])))) {
+                // for things that are passwords or are marked to be masked in display, tell camel to use RAW handling
+                propDesc.description += ' (We suggest that you store this value in a Vantiq Secret and use ' +
+                    '"@secrets(<<secret name>>)" for the value of this property.)'
+            }
+            if (pDesc.enum != null) {
+                // Present the enumeration as a drop down list. Adapt structure to that used by the assembly mechanism.
+                propDesc.enumValues = pDesc.enum
+                propDesc.type = 'Enum'  // UI seems to treat this as an alias for String, which is fine for us.
+            }
+            log.debug('PropDesc for {} : {}', pName, propDesc)
+            cfgProps[pName] = propDesc
+            log.debug('finding source...')
+            sourceName = components.find {
+                it.startsWith('/' + VANTIQ_SYSTEM_PREFIX + VANTIQ_SOURCES)
+            }
+            log.debug('Found projects source reference: {}', sourceName)
+            sourceName = sourceName.substring(sourceName.lastIndexOf('/') + 1)
+            log.debug('Found projects source name {}', sourceName)
+
+            Map<String, Object> newMapping = [resource: VANTIQ_SOURCES, resourceId: sourceName,
+                              property:
+                                  "config.${CONFIG_CAMEL_RUNTIME}." +
+                                      "${CAMEL_RUNTIME_PROPERTY_VALUES}.${pName}"]
+            if (cfgMappings[pName]) {
+                cfgMappings[pName] << newMapping
+            } else {
+                cfgMappings[pName] = [newMapping]
+            }
+        }
+        project.configurationProperties = cfgProps
+        project.configurationMappings = cfgMappings
+
+        List<Map<String, Object>> resources = []
+        components.each {comp ->
+            resources << ([resourceReference: comp] as Map<String, Object>)
+        }
+        project.resources = resources
+
+        // Things seem to work more consistently when visibleResources are after resources.
+        log.info('Creating visible resources')
+        log.debug('ServiceDef for {}: {}', project.name, serviceDef)
+        project.visibleResources = components.findAll {
+            it.startsWith('/' + VANTIQ_SYSTEM_PREFIX + VANTIQ_SERVICES)
+        }.collect { comp ->
+            [ resourceReference: comp,
+              description: "Assembly ${project.name}'s service bridging source ${sourceName} " +
+                  "and event ${ -> serviceDef.eventTypes ? ((Map) serviceDef.eventTypes).keySet()[0] : ''}."
+            ]
+        }
+
+        def projectJson =  JsonOutput.prettyPrint(JsonOutput.toJson(project))
+        log.debug('Built project {}:\n {}', project.name, projectJson)
+        Map<String, Object> retVal = [:]
+
+        retVal.path = writeVantiqEntity(VANTIQ_PROJECTS, kameletAssemblyDir, packageName, projectName + JSON_SUFFIX,
+            projectJson, false)
+        retVal.reference = buildResourceRef(VANTIQ_PROJECTS, packageName, projectName)
+        retVal.vailName = packageName
+        return retVal
+    }
+
+    /**
+     * For the routesDocumentString provided, write it out as a Vantiq document as part of the kamelets project.
+     *
+     * @param kameletAssemblyDir Path the directory used for this kamelet assembly
+     * @param packageName String package name we'll use for this assembly
+     * @param kamName String name of the kamelet on which we're working
+     * @param routesDocumentString String the actual route document as extracted from the Kamelet
+     * @return Map<String, Object> containing Path of document written & resourceReference to it when imported
+     */
+    static Map<String, Object> writeRoutesDocument(Path kameletAssemblyDir,
+                                                   String packageName, String kamName, String routesDocumentString) {
+        String docName = kamName + YAML_ROUTE_SUFFIX
+        Map<String, Object> retVal = [:]
+        retVal.path = writeVantiqEntity(VANTIQ_DOCUMENTS, kameletAssemblyDir, packageName,
+            docName, routesDocumentString, false)
+        retVal.reference = buildResourceRef(VANTIQ_DOCUMENTS, packageName + PACKAGE_SEPARATOR + docName)
+        return retVal
+    }
+
+    /**
+     * For the Overview provided, write it out as a Vantiq document as part of the kamelets project.
+     *
+     * @param kameletAssemblyDir Path the directory used for this kamelet assembly
+     * @param packageName String package name we'll use for this assembly
+     * @param kamName String name of the kamelet on which we're working
+     * @param overview String the (usually) longer descriptive text as extracted from the Kamelet
+     * @return Map<String, Object> containing Path of document written & resourceReference to it when imported
+     */
+    static Map<String, Object> writeOverviewDocument(Path kameletAssemblyDir,
+                                                   String packageName, String kamName, String overview) {
+        String docName = kamName + OVERVIEW_SUFFIX
+        Map<String, Object> retVal = [:]
+        String titledOverview =
+            "# Description of ${kamName} from Camel ${rawCamelVersion}" + OVERVIEW_SOURCE_DISCLAIMER + overview
+        retVal.path = writeVantiqEntity(VANTIQ_DOCUMENTS, kameletAssemblyDir, packageName,
+            docName, titledOverview, false)
+        retVal.reference = buildResourceRef(VANTIQ_DOCUMENTS, packageName + PACKAGE_SEPARATOR + docName)
+        return retVal
+    }
+
+    /**
+     * If a VantiqNotes file is present in the resource directory, place it in the assembly to be included as a
+     * document provided.
+     *
+     * @param kameletAssemblyDir Path the directory used for this kamelet assembly
+     * @param packageName String package name we'll use for this assembly
+     * @param kamName String name of the kamelet on which we're working
+     * @return Map<String, Object> containing Path of document written & resourceReference to it when imported
+     */
+    Map<String, Object> writeVantiqNotesDocument(Path kameletAssemblyDir,
+                                                     String packageName, String kamName) {
+        File notesFile = project.file(RESOURCE_BASE_DIRECTORY + File.separator + VANTIQ_NOTES_DIRECTORY
+            + File.separator + kamName + '.md')
+        if (!notesFile || !notesFile.exists()) {
+            log.trace('No Vantiq Notes document found for {}', notesFile.absolutePath)
+            return null
+        }
+        log.debug('Found VantiqNotes doc for assembly {}: {}', kamName, notesFile.getAbsolutePath())
+
+        String docName = kamName + VANTIQ_NOTES_SUFFIX
+        Map<String, Object> retVal = [:]
+        // Escape _'s in kamName to avoid strange italics in .md files
+        def kamNameForTitle = kamName.replace('_', '\\_')
+        String notesTitle =
+            "# Using the ${kamNameForTitle} Assembly (Version ${rawCamelVersion})"
+        String notesDoc = notesTitle + '\n\n'
+        notesFile.readLines().each { aLine ->
+            notesDoc += aLine + '\n'
+        }
+        retVal.path = writeVantiqEntity(VANTIQ_DOCUMENTS, kameletAssemblyDir, packageName,
+            docName, notesDoc, false)
+        retVal.reference = buildResourceRef(VANTIQ_DOCUMENTS, packageName + PACKAGE_SEPARATOR + docName)
+        return retVal
+    }
+
+    /**
+     * Create simple rules for routing message from source to service event or vice versa
+     *
+     * @param kameletAssemblyDir Path Directory into which to place results
+     * @param packageName String name of package to which the rule belongs
+     * @param kamName String Name of the kamelet/assembly for which the rule is being constructed
+     * @param sourceName String name of the source about which the rule is being constructed
+     * @param serviceName String name of th service involved
+     * @param isSink Boolean is the kamelet/assembly a SINK (sends messages to the source)
+     * @return Map<String, Object> containing the Path of the rule written and a resource reference to it.
+     */
+    static Map<String, Object> addRoutingRule(Path kameletAssemblyDir, String packageName, String kamName,
+                                              String sourceName, String serviceName, Boolean isSink) {
+        Map<String, Object> retVal = [:]
+        String ruleName = serviceName + (isSink ? RULE_SINK_NAME_SUFFIX : RULE_SOURCE_NAME_SUFFIX)\
+
+        String ruleText
+        def engine = new SimpleTemplateEngine()
+        if (isSink) {
+            ruleText = engine.createTemplate(SINK_RULE_TEMPLATE).make([packageName: packageName,
+                                                                       ruleName: ruleName,
+                                                                       serviceName: serviceName,
+                                                                       sourceName: sourceName]).toString()
+        } else {
+            ruleText = engine.createTemplate(SOURCE_RULE_TEMPLATE).make([packageName: packageName,
+                                                                         ruleName: ruleName,
+                                                                         serviceName: serviceName,
+                                                                         sourceName: sourceName])
+        }
+
+        if (ruleText) {
+            retVal.path = writeVantiqEntity(VANTIQ_RULES, kameletAssemblyDir, packageName,
+                ruleName + VAIL_SUFFIX, ruleText, true)
+            retVal.reference = buildResourceRef(VANTIQ_RULES, packageName, ruleName)
+            return retVal
+        } else {
+            return null
+        }
+    }
+
+    static Path writeVantiqEntity(String entType, Path kameletAssemblyDir, String packageName,
+                                  String fileName, String content, Boolean packageIsDirectory) {
+
+        List<String> entDirectoryList = [kameletAssemblyDir.toAbsolutePath().toString(), entType]
+        if (packageIsDirectory) {
+             entDirectoryList += packageName.split(Pattern.quote(PACKAGE_SEPARATOR)).toList()
+        }
+        log.debug('Creating def for packageIsDirectory: {}, package: {}, fileName: {}',
+            packageIsDirectory, packageName, fileName)
+
+        //noinspection GroovyAssignabilityCheck
+        Path entDir = Paths.get(*entDirectoryList)
+        if (!Files.exists(entDir)) {
+            Files.createDirectories(entDir)
+        }
+        def completeFileName = fileName
+        if (!packageIsDirectory) {
+            completeFileName = packageName + PACKAGE_SEPARATOR + fileName
+        }
+        Path entFilePath = Paths.get(entDir.toAbsolutePath().toString(), completeFileName)
+        log.info('Creating {} definition at path: {}', entType, entFilePath.toString())
+        return Files.writeString(entFilePath, content)
+    }
+
+    /**
+     * Convert the route template into a reified route using the vantiq://server.config style endpoints
+     *
+     * The route is returned as a list of routes as that's the CAMEL DSL requirement.
+     *
+     * @param kamName String name of kamelet from which this/these routes originate
+     * @param template Map<String, Object> route template from Kamelet YAML file
+     * @returns List<Map<String, Object>> Rewritten route template with kamelet sources & sinks replaced with Vantiq
+     *                          server references
+     */
+    List<Map<String, Object>> constructRouteText(String kamName, Map<String, Object> template) {
+        processStep(template)
+        Map<String, Object> retVal = [:]
+        // Must GString --> String lest the yaml dumper get confused.  Easier to just convert to string as opposed to
+        // writing a representer
+        // Kamelets come generated as templates, so let's preserve that encoding
+        retVal[ROUTE_TEMPLATE_TAG] =
+            [(ID_ROUTE_TAG): "Route templates from ${kamName}:${packageSafeCamelVersion}".toString()]
+        retVal[ROUTE_TEMPLATE_TAG] << template
+        // Camel wants a list (even though with kamelets, there's really only one route. The Camel Yaml route parser
+        // actually has special handling to recognized that it's reading from a kamelet. But to get along, we'll
+        // return a list.
+        return [retVal]
+    }
+
+    /**
+     * For a particular step, convert it & its children
+     *
+     * Look through the step, searching for kamelet source or sink references, replacing them with the appropriate
+     * vantiq component reference.  If any step is more complex (a sub route, as it were), recurse or handle lists
+     * as appropriate.
+     *
+     * @param step Map<String, Object> The step to process
+     * @return void
+     */
+    void processStep(Map<String, Object> step) {
+        log.debug('processStep() processing {}', step)
+        for (Map.Entry<String, Object> ent: step.entrySet()) {
+            String k = ent.getKey()
+            Object v = ent.getValue()
+            Map<String, Object> stepDef = null
+            if (v instanceof Map) {
+                //noinspection unchecked
+                stepDef = (Map<String, Object>) v
+            }
+            log.debug('Found template key: {}, value: {}', k, v)
+            if (k == FROM_ROUTE_TAG && stepDef != null) {
+                if (stepDef.containsKey(URI_ROUTE_TAG) && stepDef.get(URI_ROUTE_TAG) instanceof String &&
+                        ((String) stepDef.get(URI_ROUTE_TAG)) == KAMELET_SOURCE) {
+                    stepDef.put(URI_ROUTE_TAG, VANTIQ_SERVER_CONFIG_JSON_STRUCTURED)
+                    log.debug('Replaced {} with{}', stepDef.get('uri'), VANTIQ_SERVER_CONFIG_JSON_STRUCTURED)
+                }
+                
+                for (Map.Entry<String, Object> oneEntry: stepDef.entrySet()) {
+                    String stepKey = oneEntry.getKey()
+                    Object stepVal = oneEntry.getValue()
+                
+                    if (stepKey.equalsIgnoreCase('steps')
+                            || stepKey.equalsIgnoreCase('choice')
+                            || stepKey.equalsIgnoreCase('when')) {
+                        if (stepVal instanceof Map) {
+                            //noinspection unchecked
+                            processStep((Map<String, Object>) stepVal)
+                        } else if (stepVal instanceof List) {
+                            //noinspection unchecked
+                            processSteps((List<Map<String, Object>>) stepVal)
+                        }
+                    }
+                }
+            } else if (k == TO_ROUTE_TAG && KAMELET_SINK == v) {
+                log.debug('Replacing value of key {}:{} with \'vantiq://server.config\'', k, v)
+                ent.setValue(VANTIQ_SERVER_CONFIG_STRUCTURED)
+            } else if (k == TO_ROUTE_TAG && stepDef != null && stepDef.containsKey(URI_ROUTE_TAG)
+                    && KAMELET_SINK == stepDef.get(URI_ROUTE_TAG)) {
+                stepDef.put(URI_ROUTE_TAG, VANTIQ_SERVER_CONFIG_STRUCTURED)
+            }
+        }
+    }
+
+    /**
+     * Given a list of steps, process each one
+     * @param steps List<Map<String, Object>> The steps to process
+     * @return void
+     */
+    void processSteps(List<Map<String, Object>> steps) {
+        for (Map<String, Object> step: steps) {
+            processStep(step)
+        }
+    }
+    
+    /**
+     * Using the YAML loader, read the named resource from the project classloader (remember, we are in a Gradle task).
+     *
+     * Return as a Map.
+     *
+     * @param loader Load Yaml loader
+     * @param projectClassLoader ClassLoader class loader from which to get the named entity
+     * @param name String name of resource from which to extract the YAML file information
+     * @return Map<String, Object> representing the route
+     */
+    static Map<String, Object> getYamlEnt(Load loader, ClassLoader projectClassLoader, String name) {
+        String kameletToFetch = KAMELETS_RESOURCE_PATH + name
+        InputStream kameletStream = projectClassLoader.getResourceAsStream(kameletToFetch)
+    
+        if (kameletStream == null) {
+            throw new GradleException('Cannot read kamelet Stream: ' + kameletToFetch)
+        }
+        Object raw = loader.loadFromInputStream(kameletStream)
+        if (!(raw instanceof Map)) {
+            throw new GradleException('Internal Error:  Expected Map from kamelStream, got ' +
+                                              kameletStream.getClass().getName())
+        }
+        //noinspection unchecked
+        Map<String, Object> parsed = (Map<String, Object>) raw
+        if (parsed.size() == 0) {
+            throw new GradleException('Setup Conditions: No values found in kameletStream.')
+        }
+        return parsed
+    }
+
+    // Resource references in projects require the leading /
+    static String buildResourceRef(String resourceType, String packageName, String resourceId) {
+        if (packageName) {
+            return '/' + VANTIQ_SYSTEM_PREFIX + "${resourceType}/${packageName}" + PACKAGE_SEPARATOR + "${resourceId}"
+        } else {
+            return buildResourceRef(resourceType, resourceId)
+        }
+    }
+
+    static String buildResourceRef(String resourceType, String resourceId) {
+        return '/' + VANTIQ_SYSTEM_PREFIX + "${resourceType}/${resourceId}"
+    }
+
+    // Basic rule to route message sent to a service to the source implementing the kamelet route
+    public static final String SINK_RULE_TEMPLATE = '''
+package ${packageName}
+RULE ${ruleName}
+WHEN EVENT OCCURS ON "/services/${packageName + '.' + serviceName}/${serviceName + 'Event'}" AS svcMsg
+
+var srcName = "${sourceName}"
+
+log.debug("Service rule got message {} to forward to source {}", [ svcMsg, srcName ] )
+
+PUBLISH { headers: svcMsg.value.headers, message: svcMsg.value.message } TO SOURCE @srcName
+'''
+
+    // Basic rule to route message sent from a source to the service representing the kamelet route
+    public static final String SOURCE_RULE_TEMPLATE = '''
+package ${packageName}
+RULE ${ruleName}
+WHEN EVENT OCCURS ON "/sources/${sourceName}" as svcMsg
+
+log.debug("Source ${sourceName} rule got message {} to forward to service  {}", [ svcMsg, 
+    "${packageName + '.' + serviceName}/${serviceName + 'Event'}" ])
+
+PUBLISH { headers: svcMsg.value.headers, message: svcMsg.value.message } 
+    TO SERVICE EVENT "${packageName + '.' + serviceName}/${serviceName + 'Event'}"
+'''
+}
diff --git a/camelAssemblies/README.md b/camelAssemblies/README.md
new file mode 100644
index 00000000..bd8a7765
--- /dev/null
+++ b/camelAssemblies/README.md
@@ -0,0 +1,160 @@
+# Overview
+
+This document outlines the contents of the `CamelAssemblies` subproject within the extension sources repo.
+
+The `CamelAssemblies` subproject creates Vantiq projects from
+[Camel Kamelet Definitions](https://camel.apache.org/camel-kamelets/3.21.x/). These projects will
+be made available in a Vantiq Public Catalog as _Vantiq assemblies_. These assemblies, when imported,
+create Vantiq services and sources for connections to or from external systems supported by Apache Camel.
+
+# Prerequisites <a name="pre" id="pre"></a>
+
+**IMPORTANT:** A basic understanding of the [Vantiq Camel Connector](../camelConnector/README.md) is assumed.
+
+All assemblies contained herein (Vantiq Camel assemblies) are built atop the base Camel Connector assembly, 
+`com.vantiq.extsrc.camelconn.camelConnector`.  Specifically, the Camel Connector 
+assembly defines the Camel Connector source implementation type and the schema type
+`com.vantiq.extsrc.camelcomp.message` that defines the message format for messages used between the Camel connector
+and the Vantiq system.
+
+# Repository Contents
+
+Apache Camel, briefly, is organized into a set of _components_ that are used to integrate with other software 
+systems. The Vantiq Camel Connector provides the ability to load the components necessary to operate _routes_, 
+Camel's means for describing the interaction between information systems.
+
+Apache Kamelets are packages containing the routes and associated settings needed to set up connections using Apache
+Camel-K. 
+
+The assemblies generated by this subproject take the information contained in the Kamelet
+definitions, constructing
+Vantiq Camel assemblies that contain services and Vantiq sources ready to use in a Vantiq system.
+This subproject will generate Vantiq assemblies from the Camel definitions downloaded 
+as the `camel-kamelet` library from the dependencies (see the `build.gradle` file in this subproject).
+To generate the assembles, just run `../gradlew assemble`. After that, the Vantiq assemblies will be
+located in zip files under the build/distributions directory.
+For example,`build/distributions/ftp_sink_v3_21_0.zip`.
+
+## Terminology and Naming
+
+Generally, there are two Vantiq Camel assemblies provided for each system for which assemblies are created. Assemblies 
+for 
+sending data _to_ another system are thought of as _sinks_, and have the word `sink` in the assembly name and the
+names of the included Vantiq components. Those that get data _from_ another system have the word `source` in the names.
+
+Each assembly and its included components are named including the name of the system with which they
+interact and the Apache Camel version in use (kamelets are Camel version specific).  For example, an assembly using 
+Camel version 3.21.x for consuming information from Amazon's AWS S3 will be in the Vantiq Camel assembly
+
+    com.vantiq.extsrc.camel.kamelets.v3_21_0.aws_s3_source
+
+while an assembly for sending data to Amazon's AWS SNS will be found in
+
+    com.vantiq.extsrc.camel.kamelets.v3_21_0.aws_sns_sink
+
+For each of these cases, the Vantiq components contained in the assembly will have that assembly name as their 
+package name. The _base name_ for Vantiq components will be the kamelet name, with suffixes specific to the Vantiq 
+component type.
+
+Each Vantiq Camel assembly is realized via a [Vantiq Camel Connector](../camelConnector/README.md). The connector is 
+configured to make the connection to the associated information system via the Camel route defined in the assembly.
+
+## Constructed Vantiq Camel Assemblies
+
+The Vantiq Camel assemblies contained herein are composed of the following.
+
+* Vantiq Source -- this is the Vantiq component that represents the external system to the Vantiq environment.
+  * The Vantiq source will be of type `CAMEL`. This is defined in the Camel Connector assembly `com.vantiq.extsrc.camelconn.camelConnector`.
+  * The source's configuration will include the Camel route to/from (source/sink) the external source via Camel 
+    Component(s).
+  * The configuration may include configuration information (such as credentials, type of interaction) generated 
+    from Vantiq assembly configuration parameters.
+  * The Vantiq source is in the package matching the assembly name, with the name consisting
+      of the base name with the suffix `_source`.
+* Vantiq Service -- this is the Vantiq item with which Vail code interacts.
+  * The service interacts with the Vantiq source to move information to/from the Vantiq system.
+  * The service is in the package matching the assembly name, and named with the base name with the suffix 
+    `_service`.  For example, `com.vantiq.extsrc.camel.kamelets.v3_21_0.aws_sns_sink.aws_sns_sink_service`.
+  * Each service is defined with an _inbound_ or _outbound_ event named with the base name with the suffix 
+    `_serviceEvent`. Continuing the example above, `aws_sns_sink_serviceEvent` (so the full service event name is 
+    `com.vantiq.extsrc.camel.kamelets.v3_21_0.aws_sns_sink.aws_sns_sink_service/aws_sns_sink_serviceEvent`).
+* Vantiq Rule -- this is the Vantiq component which marshals events between the Vantiq source and Vantiq service.
+  * The rules are in the package matching the assembly name, and named using the associated 
+       service name with the suffix `_srcToSvc` (for a _sink_ assembly)
+       or `_svcToSrc` (for a _source_ assembly).
+* Vantiq Documents -- there are a few documents that may be present in the Vantiq Camel assembly.
+  * Overview Document -- `<assembly name>_overview.md`.  This contains the provided high-level description of the 
+    assembly. It will often provide other information concerning use of the assembly.
+  * Route Template Document -- `<assembly name>_routes.yaml`.  This is the routing information used by the Camel 
+    Connector. It is referenced by the Vantiq Source configuration.
+    * Notes Document (optional) -- `<assembly name>_notes.md`.  When present, this contains information that Vail 
+      developers need to know about use of the assembly. Typically, this is information regarding non-standard message 
+      structure or functionality. The assemblies are interacting with external systems whose methods of operation are 
+      different from that of Vantiq. Amongst other things, the _notes_ document(s) are used to communicate this 
+      information when required.
+
+# Vantiq Camel Assemblies in a Vantiq Namespace
+
+## Vantiq Camel Assembly Installation
+
+To make use of any Vantiq Camel assembly defined herein, the user should perform the following tasks.
+
+* Add the Vantiq Catalog containing the definitions. In Vantiq public systems, this is, at the time of this writing, 
+  named `CamelCatalog`. This must be done once per namespace into which any of these assemblies will be installed.
+* From that catalog, import the `com.vantiq.extsrc.camelconn.camelConnector` assembly. This is a prerequisite for 
+  the Vantiq Camel assemblies.
+  * As with the addition of the Vantiq Catalog, this needs to be done once per Vantiq namespace in which these 
+    assemblies are being used.
+  * This assembly defines the following items:
+    * The `CAMEL` source implementation type.
+    * The Vantiq _schema_ used for the _structured messages_ used to interact with the source.
+* From that catalog, import the Vantiq Camel assembly or assemblies desired, providing the configuration properties as 
+  requested. 
+  * These configuration properties
+  will often include addressing information and credentials for the remote systems. It is strongly suggested that 
+    credential information (or other confidential information) be stored in Vantiq 
+    Secrets, and referenced using the `@secrets()` notation.
+
+## Using the Vantiq Camel Assemblies
+
+Once installed, each Vantiq Camel assembly creates a source & service for interacting with that source.
+
+The source is defined, but it is implemented via a [Camel Connector](../camelConnector/README.md). To run the Camel 
+Connector, please see the [Camel Connector overview](../camelConnector/README.md).
+
+As noted above, the service will have an inbound or outbound event defined, and interactions with the service are
+generally performed by publishing to that event or handling it (for _inbound_ or _outbound_ events).
+All are constructed to use _structured messages_ as defined by the 
+[Camel Component](../camelComponent/README.md#structured-headers-and-messages). Consequently, messages sent to/from 
+the service should be in this format. Specifically, such messages contain two (2) properties: `header` and `message`,
+where the `header` property contains the Camel headers, and the `message` property contains Camel message.
+The associated Vantiq _schema_ is defined via the `com.vantiq.extsrc.camelconn.camelConnector` assembly imported as 
+described above.  This imported assembly is a prerequisite for the other Vantiq Camel assemblies.
+
+**Note**: Some of these Vantiq Camel assemblies (specifically those that work with systems
+with limited messaging capabilities),
+the Vantiq Component will construct output messages (the `message` property) with a single 
+property (`stringVal` or `byteVal`) containing the 
+message content. This allows Vantiq applications to interact in a Vantiq application native manner.
+Users of these systems will need to be aware of this _impendance mismatch_ and respond accordingly.
+
+## Logging & Debugging
+
+These Vantiq Camel assemblies themselves provide no direct support for logging or debugging -- they are simply 
+specifications for 
+Apache Camel applications. To debug what is happening, you will need to look at the component definitions used in 
+the assembly.  Generally, these will include the component for the target system and the [Vantiq component](../camelComponent/README.md).
+Information about other components is (usually) found in the 
+[Apache Component catalog](https://camel.apache.org/components/3.21.x/index.html).
+
+# Licensing
+
+The source code uses the [MIT License](https://opensource.org/licenses/MIT).  
+
+okhttp3, log4j, gson, and jackson-databind are licensed under
+[Apache Version 2.0 License](http://www.apache.org/licenses/LICENSE-2.0).  
+
+slf4j and lombok are licensed under the [MIT License](https://opensource.org/licenses/MIT).  
+
+Apache, Camel, and Apache Camel are trademarks of the Apache Software Foundation and are licensed under
+[Apache Version 2.0 License](http://www.apache.org/licenses/LICENSE-2.0). 
diff --git a/camelAssemblies/build.gradle b/camelAssemblies/build.gradle
new file mode 100644
index 00000000..769e37c2
--- /dev/null
+++ b/camelAssemblies/build.gradle
@@ -0,0 +1,49 @@
+import io.vantiq.extsrc.assy.tasks.AssemblyResourceGeneration
+
+repositories {
+    mavenCentral()
+}
+
+dependencies {
+    implementation "org.apache.camel.kamelets:camel-kamelets:${camelVersion}"
+}
+
+ext {
+    generatedResourceBase = 'vantiqGeneratedResources'
+    distributionsDir = 'distributions'
+}
+
+tasks.withType(JavaCompile) {
+    options.encoding = 'UTF-8'
+}
+
+test {
+    useJUnitPlatform()
+}
+
+tasks.register('zipAssemblies') {
+    outputs.upToDateWhen { false }
+    def assyCount = 0
+    doLast {
+        File vtqResources = new File(project.buildDir, "${generatedResourceBase}")
+        def distributions = new File(project.buildDir, "${distributionsDir}")
+        mkdir(distributions)
+        vtqResources.eachDir { resourceDir ->
+            def projName = resourceDir.name
+            logger.info('Processing {}', projName)
+            project.ant.zip(destfile: new File(distributions, projName + '.zip')) {
+                // Here, we use the project name (projName) as the base directory within the zip file
+                // This is the required format for projects
+                zipfileset(dir: resourceDir, prefix: projName)
+            }
+            assyCount += 1
+        }
+        logger.lifecycle('Created {} assemblies.', assyCount)
+    }
+}
+
+tasks.register('generateAssemblyResources', AssemblyResourceGeneration)
+
+assemble.finalizedBy('generateAssemblyResources')
+generateAssemblyResources.finalizedBy('zipAssemblies')
+zipAssemblies.mustRunAfter( 'generateAssemblyResources' )
diff --git a/camelAssemblies/src/main/resources/vantiqNotes/azure_servicebus_sink.md b/camelAssemblies/src/main/resources/vantiqNotes/azure_servicebus_sink.md
new file mode 100644
index 00000000..c8b48295
--- /dev/null
+++ b/camelAssemblies/src/main/resources/vantiqNotes/azure_servicebus_sink.md
@@ -0,0 +1,65 @@
+## Message Paradigms
+
+The Azure Service Bus operates using messages consisting of headers (standard Apache Camel operations) and String
+content. This String content can take the form of a single String or a list of Strings.  In the latter case,
+each String in the list is sent as a separate Service Bus message.
+
+Moreover, if the Service Bus sees a message that is not a String, it will try and interpret it as a list of Strings,
+each of which is sent as a separate Service Bus message.
+
+The Vantiq system, and specifically these assemblies, exchanges messages with the underlying source using Vail Objects.
+
+## Sending Messages
+
+The combination of these two operational paradigms results in the following interpretation.
+
+The Vantiq system will send a Vail Object (specifically, an object with`headers` and `message` property). The 
+`headers` property is placed into the Camel Exchange headers, and the `message` property is placed into the Camel 
+Exchange body.
+
+Once the Azure Service Bus receives the message, it will look at the message body.  Generally, it will see a Vail 
+Object (which appears as a JSON Object), and, since that's not a String, it will interpret it as a list of Strings.
+
+The result of this is that each property in the `message` is sent as a separate String, and the property name is
+ignored.  For example, if you send a message such as the following:
+
+```js
+{
+    headers: {
+        myHeader: "my header value"
+    },
+    message: {
+        someProp: "I am a property value"
+    }
+}
+```
+
+the message sent across the Azure Service Bus will contain the `headers` as specified, but the message body will be 
+the string _I am a property value_. The property name `someProp` will not be present.
+
+If you were to send the same message, but it had two properties:
+
+```js
+{
+    headers: {
+        myHeader: "my header value"
+    },
+    message: {
+        someProp: "I am a property value",
+        someOtherProp: "A different property value"
+    }
+}
+```
+
+two (2) messages would be sent across the Azure Service Bus:  both would have the `headers` as specified, one 
+with the message value _I am a property value_, and the second with the message value _A different property value_.
+
+# Legal
+
+Apache Camel, Camel, and Apache are trademarks of The Apache Software Foundation.
+
+Azure Service Bus is a trademark of Microsoft Corporation.
+
+Vantiq is a trademark of Vantiq, Inc.
+
+All other trademarks mentioned are trademarks or registered trademarks of their respective owners.
\ No newline at end of file
diff --git a/camelAssemblies/src/main/resources/vantiqNotes/azure_servicebus_source.md b/camelAssemblies/src/main/resources/vantiqNotes/azure_servicebus_source.md
new file mode 100644
index 00000000..956952b7
--- /dev/null
+++ b/camelAssemblies/src/main/resources/vantiqNotes/azure_servicebus_source.md
@@ -0,0 +1,49 @@
+## Message Paradigms
+
+The Azure Service Bus operates using messages consisting of headers (standard Apache Camel operations) and String
+content. This String content can take the form of a single String or a list of Strings.  In the latter case,
+each String in the list is sent as a separate service bus message.
+
+Moreover, if the service bus sees a message that is not a String, it will try and interpret it as a list of Strings,
+each of which is sent as a separate service bus message.
+
+The Vantiq system, and specifically these assemblies, exchanges messages with the underlying source using Vail Objects.
+
+## Receiving Messages
+
+The combination of these two operational paradigms results in the following interpretation.
+
+When a message is delivered to the Vantiq Camel Component, that message, in the form of a Camel Exchange, contains 
+headers and the message body. The message body will contain only a String.
+
+The Vantiq system will send a Vail Object (specifically, an object with`headers` and `message` property. The 
+`headers` property will contain the headers specified by the Camel Exchange headers, and the `message` property will 
+contain the message body (a String).  Since the Vail Object is a set of properties and their values, the property 
+name used here will be `stringVal` (no property name is otherwise delivered), and that property value will be the 
+value delivered in the Camel Exchange message body.
+
+
+The result of this is that each property in the `message` is sent as a separate String, and the property name is
+ignored.  For example, if a Camel Exchange containing the header `myHeader: "my header value"` and the body `I am a 
+property value` is received, the resulting Vantiq message will be the following:
+
+```js
+{
+    headers: {
+        myHeader: "my header value"
+    },
+    message: {
+        stringVal: "I am a property value"
+    }
+}
+```
+
+# Legal
+
+Apache Camel, Camel, and Apache are trademarks of The Apache Software Foundation.
+
+Azure Service Bus is a trademark of Microsoft Corporation.
+
+Vantiq is a trademark of Vantiq, Inc.
+
+All other trademarks mentioned are trademarks or registered trademarks of their respective owners.
\ No newline at end of file
diff --git a/camelComponent/README.md b/camelComponent/README.md
index 2e5d3613..3a20af33 100644
--- a/camelComponent/README.md
+++ b/camelComponent/README.md
@@ -1,7 +1,8 @@
 # Overview
 
-This document outlines how to incorporate a Vantiq Camel Component Source into your project. The Camel Component source allows a user to construct Apache Camel applications that interoperate with the Vantiq system. The Camel Component uses the Apache Camel
-mechanism to specify the Vantiq connection details.
+This document outlines how to incorporate a Vantiq Camel Component Source into your project. The Camel Component 
+source allows a user to construct Apache Camel applications that interoperate with the Vantiq system. The Camel
+Component uses the Apache Camel mechanism to specify the Vantiq connection details.
 
 The documentation has been split into two parts, [Setting Up Your Machine](#machine)
 and [Setting Up Your Vantiq](#vantiq).
@@ -45,13 +46,16 @@ The Vantiq Camel Connector simplifies the configuration and construction of an A
 See the [Vantiq Camel Connector documentation](../camelConnector/README.md) for information.
 
 ## Logging
-To change the logging settings, edit the logging config file `<install location>/camelComponent/src/main/resources/log4j2.xml`,
-which is an [Apache Log4j configuration file.](https://logging.apache.org/log4j/2.x/manual/configuration.html). The logger 
-name for each class is the class's fully qualified class name, *e.g.* "io.vantiq.extjsdk.ExtensionWebSocketClient".  
+To change the logging settings, edit the logging config file 
+`<install location>/camelComponent/src/main/resources/log4j2.xml`,
+which is an [Apache Log4j configuration file.](https://logging.apache.org/log4j/2.x/manual/configuration.html). The 
+logger name for each class is the class's fully qualified class name, *e.g.* "io.vantiq.extjsdk.
+ExtensionWebSocketClient".  
 
 # Setting Up Vantiq <a name="vantiq" id="vantiq"></a>
 
-An understanding of the Vantiq Extension Source SDK is assumed. Please read the [Extension Source README.md](../README.md) for more information.
+An understanding of the Vantiq Extension Source SDK is assumed. Please read the 
+[Extension Source README.md](../README.md) for more information.
 
 In order to incorporate this Extension Source, you will need to create the Source Implementation in the Vantiq system.
 
@@ -82,7 +86,7 @@ your Source Implementation) as the Source Type. No configuration information is
 
 Messages that are sent to the component will arrive in a Camel Exchange as a Java Map, where the keys in the map
 correspond to the property names in the object sent from Vantiq. If desired, you change this behavior
-by adding the `consumerOutputJson` component endpoint option (see below) with a value of `true`.
+by adding the `consumerOutputJsonStream` component endpoint option (see below) with a value of `true`.
 When so specified, messages sent to a Vantiq consumer will arrive in a Camel Exchange as a JSON string.
 
 Messages sent to Vantiq from the component will
@@ -94,9 +98,63 @@ The underlying communication is JSON, so binary data must be Base64 encoded.
 
 ### Structured Headers and Messages
 
-By adding the `structuredMessageHeader` component endpoint option with a value of `true` (see below), you can set or access the Camel message headers.  When this option is set to `true`, messages sent to or received from the Camel Component will have two properties: `headers` and `message`, both containing Vail objects. The `message` property will contain a Vail object where each property corresponds to same-named property in the underlying message.  The `headers` property will contain a Vail object where each property contains the value of the named message header.
+By adding the `structuredMessageHeader` component endpoint option with a value of `true` (see below), you can set or 
+access the Camel message headers.  When this option is set to `true`, messages sent to or received from the
+Camel Component will have two properties: `headers` and `message`, both containing Vail objects. The `message`
+property will contain a Vail object where each property corresponds to same-named property in the underlying message.
+The `headers` property will contain a Vail object where each property contains the value of the named message header.
+If no headers are present in the underlying Camel message, no `headers` property will be present.
+
+A schema type definition for this message structure is available in
+`src/main/resources/types/com/vantiq/extsrc/camelcomp/message.json`.
+
+#### Header Duplication
+
+There are numerous components in the Apache Camel universe, and many include Camel Headers that convey the same 
+information. For example, a number of components include headers whose name is of the form `Camel<componentn 
+name>ContentType` (_e.g._, `CamelAzureServiceBusContentType` to name but one). 
+
+If the Vantiq application wants to deal with these, it does not want to know all possible variations here -- that is,
+after all, the purpose of an integration system: to manage these discrepancies.  To ease the handling of these 
+situations, the Vantiq Camel Component can duplicate (NOT replace) these header values into alternative names. When 
+this is requested, the original set of headers will be sent or received, but the duplicates with values will be sent 
+or received as well.
+
+To do this, the Camel system must be configured with a Bean of type `io.vantiq.extsrc.camel.HeaderDuplicationBean`, 
+containing a Map<String, String> where the keys of the map represent the header names to be duplicated, and the 
+values are the new header names into which the header value will be duplicated. (Note: we do this via a configured 
+bean since the number of headers could be large, and having URI of great length is sometimes problematic.)
+
+To use this, add the `headerDuplicationBeanName` component/endpoint option with the value being the name of the bean 
+containing the map described above. Note that this header duplication is performed only when the URI parameter 
+`structuredMessageHeader` is present with the value `true`. In other cases, it is silently ignored.
+
+For example, using a Java example, if you want to duplicate the `CamelComponentContentType` header into the 
+header `VantiqContentType`, we would configure things as follows:
+
+```java
+    ...
+    HeaderDuplicationBean hdBean = new HeaderDuplicationBean();
+    Map<String, String> hdMap = Map.of("CamelComponentContentType", "VantiqContentType");
+    hdBean.setHeaderDuplicationMap(hdMap);
+    context.getRegistry().bind("someBeanName", hdBean);
+    
+    // Now, a route endpoint could be defined such as
+    String vantiqEndpoint = "vantiq://<host>:<port>/&sourceName=xxx?accessToken=YYY" +
+        "?structuredMessageHeader=true" +
+        "?headerDuplicationBeanName=someBeanName"
+```
+
+When this definition is in place, messages arriving (via the Azure ServiceBus) to that endpoint will contain the 
+headers `CamelComponentContentType` & `VantiqContentType` with the same values. If the `CamelComponentContentType` 
+header is not present, no duplication will happen.
 
-A schema type definition for this message structure is available in `src/main/resources/types/com/vantiq/extsrc/camelcomp/message.json`.
+Note that if multiple headers are specified to duplicate to a single duplicate (e.g., both header foo & bar
+are both supposed to be duplicated to the header baz), the results are unpredictable.  Something will happen, 
+but the results depend upon the order on which the headers are processed, and may vary from instance to
+instance.  Callers should take care to avoid these situations.  There may be cases where components have 
+alternatives that are used, both of which are desired to duplicate to the same header, so we allow such a 
+definition. But the caller should take care with such definitions.
 
 ## Exchanges to Vantiq
 
@@ -110,7 +168,8 @@ in Json format will be accepted as well.
 The URI for the Vantiq connection takes the following form:
 
 ```
-    vantiq://host[:port]?sourceName=<source name>&accessToken=<access token>[&sendPings=<boolean>][&failedMessageQueueSize=<int>][&consumerOutputJson=<boolean>][&structuredMessageHeader=<boolean>]
+    vantiq://host[:port]?sourceName=<source name>&accessToken=<access token>[&sendPings=<boolean>] \
+    [&failedMessageQueueSize=<int>][&consumerOutputJsonStream=<boolean>][&structuredMessageHeader=<boolean>]
 ```
 
 ### Component Endpoint Options
@@ -122,9 +181,17 @@ The endpoint options apply to producer and consumer endpoints.  The following ar
 * **sourceName** is the name of the Camel component source to which to connect
 * **accessToken** is the access token used for the connection
 * **sendPings** [optional] a boolean value indicating whether to periodically ping the Vantiq server (default is false)
-* **failedMessageQueueSize** [optional] an integer value indicating how many messages to hold for sending when the connection to the Vantiq server fails.
-* **consumerOutputJson** [optional] a boolean value indicating whether messages sent from Vantiq to the Vantiq component (_i.e._, a Vantiq Consumer) should be put into an Apache Camel Exchange as a JSON message.  If false, messages are put into an exchange as a Java Map.
-* **structuredMessageHeader** [optional] a boolean value indicating whether messages sent to and from the Vantiq component (Vantiq Consumers and Producers) should be structured as
+* **failedMessageQueueSize** [optional] an integer value indicating how many messages to hold for sending when the 
+  connection to the Vantiq server fails.
+* **consumerOutputJsonStream** [optional] a boolean value indicating whether messages sent from Vantiq to the Vantiq 
+  component (_i.e._, a Vantiq Consumer) should be put into an Apache Camel Exchange as a JSON message.  If 
+  false, messages are put into an exchange as a Java Map. Note that these Json messages are wrapped in a Camel 
+  _StreamCache_. This matches what adding a _marshal_ step to the route would do, which tends to improve the 
+  interoperability with other Camel components.
+* **consumerOutputJson** [DEPRECATED, optional] same as **componentOutputJsonStream** but does not wrap the output 
+  in a Camel _StreamCache_. Use is strongly discouraged. Instances where this is necessary should be reported.
+* **structuredMessageHeader** [optional] a boolean value indicating whether messages sent to and from the Vantiq 
+  component (Vantiq Consumers and Producers) should be structured as
 
     ```json
     {
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/camelComponent/src/main/java/io/vantiq/extsrc/camel/HeaderDuplicationBean.java b/camelComponent/src/main/java/io/vantiq/extsrc/camel/HeaderDuplicationBean.java
new file mode 100644
index 00000000..12638e76
--- /dev/null
+++ b/camelComponent/src/main/java/io/vantiq/extsrc/camel/HeaderDuplicationBean.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 2023 Vantiq, Inc.
+ *
+ * All rights reserved.
+ *
+ * SPDX: MIT
+ */
+
+package io.vantiq.extsrc.camel;
+
+import java.util.Map;
+
+public class HeaderDuplicationBean {
+    
+    private Map<String, String> headerDuplicationMap;
+    
+    public Map<String, String> getHeaderDuplicationMap() {
+        return headerDuplicationMap;
+    }
+    
+    public void setHeaderDuplicationMap(Map<String, String> heMap) {
+        this.headerDuplicationMap = heMap;
+    }
+}
diff --git a/camelComponent/src/main/java/io/vantiq/extsrc/camel/VantiqConsumer.java b/camelComponent/src/main/java/io/vantiq/extsrc/camel/VantiqConsumer.java
index 6145dc10..7e7f1b5c 100644
--- a/camelComponent/src/main/java/io/vantiq/extsrc/camel/VantiqConsumer.java
+++ b/camelComponent/src/main/java/io/vantiq/extsrc/camel/VantiqConsumer.java
@@ -19,8 +19,10 @@
 import io.vantiq.extjsdk.ExtensionWebSocketClient;
 import io.vantiq.extjsdk.Handler;
 
-import java.util.HashMap;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
 import java.util.Map;
+import java.util.Objects;
 import java.util.concurrent.ExecutorService;
 import java.util.stream.Collectors;
 import lombok.extern.slf4j.Slf4j;
@@ -28,6 +30,7 @@
 import org.apache.camel.ExchangePattern;
 import org.apache.camel.Processor;
 import org.apache.camel.support.DefaultConsumer;
+import org.apache.camel.support.builder.OutputStreamBuilder;
 
 @Slf4j
 public class VantiqConsumer extends DefaultConsumer {
@@ -93,12 +96,12 @@ private void processMessage(Object msg, ExchangePattern pattern) {
             log.error("Message processor for {} ignoring an unknown message type: {}.",
                     endpoint.getEndpointName(), msg.getClass().getName());
         } else {
+            boolean convertToStream = false;
             ExtensionServiceMessage message = (ExtensionServiceMessage) msg;
             Object msgBody = message.getObject();
             Map<String, Object> camelHdrs = null;
             Object camelBody = null;
             if (endpoint.isStructuredMessageHeader() && msgBody instanceof Map) {
-                camelHdrs = new HashMap<>();
                 Map<?,?> msgAsMap = (Map<?,?>) msgBody;
                 if (msgAsMap.get(STRUCTURED_MESSAGE_HEADERS_PROPERTY) instanceof Map) {
                     Map<?,?> hdrMap = (Map<?,?>) msgAsMap.get(VantiqEndpoint.STRUCTURED_MESSAGE_HEADERS_PROPERTY);
@@ -106,7 +109,10 @@ private void processMessage(Object msg, ExchangePattern pattern) {
                                 .stream()
                                 .filter(e -> e.getKey() instanceof String)
                                 .collect(Collectors.toMap(e -> (String) e.getKey(), Map.Entry::getValue));
-
+                    // If endpoint says to copy header values, do that here now that we have the values.
+                    if (camelHdrs.size() > 0) {
+                        camelHdrs.putAll(endpoint.duplicateHeaders(camelHdrs));
+                    }
                 }
                 if (msgAsMap.get(STRUCTURED_MESSAGE_MESSAGE_PROPERTY) != null) {
                     camelBody = msgAsMap.get(STRUCTURED_MESSAGE_MESSAGE_PROPERTY);
@@ -115,14 +121,18 @@ private void processMessage(Object msg, ExchangePattern pattern) {
                 log.debug("Structured message -- hdrs: {}, message: {}", camelHdrs, camelBody);
             }
             Object output = msgBody;
-            if (endpoint.isConsumerOutputJson()) {
+            if (endpoint.isConsumerOutputJson() || endpoint.isConsumerOutputJsonStream()) {
                 // Convert to JSON output
-                JsonNode jnode =  mapper.convertValue(msgBody, JsonNode.class);
-                output = jnode;
+                // Things coming from Vantiq will be Strings or a Vail objects/Maps.  This should be
+                // sufficient for those conversions.
+                JsonNode jnode  = mapper.valueToTree(msgBody);
+
+                output = Objects.requireNonNullElse(jnode, "");
                 if (log.isDebugEnabled()) {
-                    Object resValueDbg = jnode.isTextual() ? jnode.asText() : jnode;
+                    Object resValueDbg = jnode != null ? (jnode.isTextual() ? jnode.asText() : jnode) : null;
                     log.debug("ConsumerOutputJson: msgBody out: {}", resValueDbg);
                 }
+                convertToStream =  endpoint.isConsumerOutputJsonStream();
             }
     
             // Create an exchange to move our message along.  In the publish case,
@@ -130,7 +140,18 @@ private void processMessage(Object msg, ExchangePattern pattern) {
             // camel operations are complete.  If/when we support queries, we will care about the
             // result, so we'll deal with those separately.
             final Exchange exchange = createExchange(false);
+            log.debug("Process(): using camel context {}", exchange.getContext().getName());
             exchange.setPattern(pattern);
+            if (convertToStream) {
+                OutputStreamBuilder osb = OutputStreamBuilder.withExchange(createExchange(false));
+                try {
+                    osb.write(output.toString().getBytes(StandardCharsets.UTF_8));
+                    output = osb.build();
+                } catch (IOException e) {
+                    log.error("process() unable to write JSON output stream", e);
+                    throw new RuntimeException(e);
+                }
+            }
             exchange.getIn().setBody(output);
             if (camelHdrs != null) {
                 exchange.getIn().setHeaders(camelHdrs);
diff --git a/camelComponent/src/main/java/io/vantiq/extsrc/camel/VantiqEndpoint.java b/camelComponent/src/main/java/io/vantiq/extsrc/camel/VantiqEndpoint.java
index 400904d7..63aa1e50 100644
--- a/camelComponent/src/main/java/io/vantiq/extsrc/camel/VantiqEndpoint.java
+++ b/camelComponent/src/main/java/io/vantiq/extsrc/camel/VantiqEndpoint.java
@@ -34,6 +34,7 @@
 
 import java.net.URI;
 import java.net.URISyntaxException;
+import java.util.HashMap;
 import java.util.Map;
 import java.util.Properties;
 import java.util.concurrent.CompletableFuture;
@@ -80,11 +81,19 @@ public class VantiqEndpoint extends DefaultEndpoint {
     private boolean noSsl;
     
     public static final String CONSUMER_OUTPUT_JSON_PARAM = "consumerOutputJson";
-    @UriParam(defaultValue = "false")
+    @Deprecated
+    @UriParam(defaultValue = "false") @Metadata(deprecationNote = "Use (replaced by) consumerOutputJsonStream which " +
+            "emulates a marshal step more closely.")
     @Getter
     @Setter
     private boolean consumerOutputJson;
     
+    public static final String CONSUMER_OUTPUT_JSON_STREAM_PARAM = "consumerOutputJsonStream";
+    @UriParam(defaultValue = "false")
+    @Getter
+    @Setter
+    private boolean consumerOutputJsonStream;
+    
     public static final String FAILED_MESSAGE_QUEUE_SIZE_PARAM = "failedMessageQueueSize";
     @UriParam(defaultValue = "25")
     @Setter
@@ -96,6 +105,20 @@ public class VantiqEndpoint extends DefaultEndpoint {
     @Setter
     private boolean structuredMessageHeader;
     
+    // Note doing this as a constructed bean rather than a multivalue parameter because the number of headers might
+    // be large, and a large set thereof may violate URI length requirements. Also, it's primarily intended for use
+    // in the Camel Connector, so constructing such a things there based on source parameters is not hard.  Should a
+    // need arise, we can offer another option(s).
+    public static final String HEADER_DUPLICATION_BEAN_NAME = "headerDuplicationBeanName";
+    
+    @UriParam(name = HEADER_DUPLICATION_BEAN_NAME, description = "A bean name where the bean contains a " +
+            "headerDuplicationMap value mapping header names to a duplicate header into which the value should be " +
+            "copied.")
+    @Metadata(required = false)
+    @Getter
+    @Setter
+    private String headerDuplicationBeanName;
+    
     @Setter
     @Getter
     private ExtensionWebSocketClient vantiqClient;
@@ -110,6 +133,9 @@ public class VantiqEndpoint extends DefaultEndpoint {
     @Getter
     private String endpointName;
     
+    @Getter
+    private Map<String, String> headerDuplicationMap;
+    
     InstanceConfigUtils utils;
 
     public VantiqEndpoint() {
@@ -214,6 +240,20 @@ public void startup() throws CamelException {
             }
             CamelException failure = null;
             try {
+                if (headerDuplicationBeanName != null &&
+                        !headerDuplicationBeanName.isEmpty() && !headerDuplicationBeanName.isBlank()) {
+                    // If we have header equivalents specified, fetch them and populate our local store for our consumers &
+                    // producers
+                    HeaderDuplicationBean hdBean = getCamelContext().getRegistry().lookupByNameAndType(
+                            headerDuplicationBeanName,
+                            HeaderDuplicationBean.class);
+                    if (hdBean == null) {
+                        throw new IllegalArgumentException("No HeaderDuplicationBean named " + headerDuplicationBeanName +
+                                                                   " was found in the Camel Registry.");
+                    } else {
+                        headerDuplicationMap = hdBean.getHeaderDuplicationMap();
+                    }
+                }
                 log.debug("Attempting to connect to URL: {} from {}", getEndpointBaseUri(), getEndpointUri());
                 String vtq = getEndpointBaseUri();
                
@@ -367,6 +407,34 @@ public static String adjustVantiqTarget(String baseUrl, boolean noSsl) throws Ca
         return correctedUrl;
     }
     
+    /**
+     * Return map of headers duplicated as specified by the HEADER_EQUIVALENCE_BEAN_NAME parameter.
+     * <p>
+     * If no duplication is specified or nothing found to duplicate, return an empty map.
+     * <p>
+     * Note that if multiple headers are specified to duplicate to a single duplicate (e.g., both header foo & bar
+     * are both supposed to be duplicated to the header baz), the results are unpredictable.  Something will happen,
+     * but the results depend upon the order on which the headers are processed, and may vary from instance to
+     * instance.  Callers should take care to avoid these situations.  There may be cases where components have
+     * alternatives that are used, both of which are desired to duplicate to the same header, so we allow such a
+     * definition.
+     *
+     * @param headers Map<String, Object> specifying the headers supplied by Camel
+     * @return Map<String, Object> specifying the duplicated headers.  Returns an empty map of nothing to duplicated
+     */
+    public Map<String, Object> duplicateHeaders(Map<String, Object> headers) {
+        Map<String, Object> dupedHdrs = new HashMap<>();
+        if (headerDuplicationMap != null && headerDuplicationMap.size() > 0) {
+            headerDuplicationMap.forEach((k, v) -> {
+                if (headers.get(k) != null) {
+                    // If we have a value for a header we're expecting to duplicate, then we duplicate that
+                    // value into the duplicated header's name.
+                    dupedHdrs.put(v, headers.get(k));
+                }
+            });
+        }
+        return dupedHdrs;
+    }
     @Override
     public void doStop() {
         synchronized (startStopLock) {
diff --git a/camelComponent/src/main/java/io/vantiq/extsrc/camel/VantiqProducer.java b/camelComponent/src/main/java/io/vantiq/extsrc/camel/VantiqProducer.java
index 94aceef7..b7f83209 100644
--- a/camelComponent/src/main/java/io/vantiq/extsrc/camel/VantiqProducer.java
+++ b/camelComponent/src/main/java/io/vantiq/extsrc/camel/VantiqProducer.java
@@ -11,27 +11,40 @@
 import static io.vantiq.extsrc.camel.VantiqEndpoint.STRUCTURED_MESSAGE_HEADERS_PROPERTY;
 import static io.vantiq.extsrc.camel.VantiqEndpoint.STRUCTURED_MESSAGE_MESSAGE_PROPERTY;
 
+import com.fasterxml.jackson.core.JsonParseException;
 import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.DatabindException;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.BinaryNode;
+import com.fasterxml.jackson.databind.node.BooleanNode;
+import com.fasterxml.jackson.databind.node.ContainerNode;
+import com.fasterxml.jackson.databind.node.TextNode;
 import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
 import io.vantiq.extjsdk.ExtensionServiceMessage;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.camel.Exchange;
 import org.apache.camel.ExchangePattern;
 import org.apache.camel.StreamCache;
+import org.apache.camel.converter.stream.ByteArrayInputStreamCache;
 import org.apache.camel.converter.stream.InputStreamCache;
 import org.apache.camel.InvalidPayloadException;
 import org.apache.camel.support.DefaultProducer;
 
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
 import java.net.HttpURLConnection;
 import java.nio.ByteBuffer;
 import java.nio.charset.CharacterCodingException;
 import java.nio.charset.CharsetDecoder;
 import java.nio.charset.StandardCharsets;
-
+import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
 import java.util.Map;
 
 @Slf4j
@@ -56,65 +69,114 @@ public void process(Exchange exchange) throws Exception {
             log.trace("Type converter count: {}", exchange.getContext().getTypeConverterRegistry().size());
         }
         
-        Object msg = exchange.getIn().getBody();
-        Map<String, Object> vMsg;
-        if (!(msg instanceof StreamCache)) {
-            vMsg = exchange.getIn().getBody(HashMap.class);
-            // Jackson converts to json nodes but not to map.  Have to hunt for that or convert json node.
+        Map<String, Object> vMsg = exchange.getIn().getBody(HashMap.class);
+    
+        if (vMsg == null) {
+            Object msg = exchange.getIn().getBody();
             if (log.isDebugEnabled()) {
                 log.debug("Camel converted body is {}, msg type: {}, msg: {}", vMsg, msg.getClass().getName(), msg);
             }
-            if (vMsg == null) {
-                // Then Camel couldn't convert on its own.  Let's see if we can help things along...
-                if (msg instanceof String) {
-                    // Then we must be fetching a JSON string.
-                    String strMsg = (String) msg;
+    
+            // If we have a stream, extract the bytes before attempting to convert them
+            if (msg instanceof StreamCache) {
+                // First, let's extract the data from the stream
+                byte[] ba = null;
+                if (msg instanceof InputStreamCache) {
+                    // Here, we have an input stream cache to attempt to process
+                    InputStreamCache isc = (InputStreamCache) msg;
+                    isc.reset();
+                    ba = isc.readAllBytes();
+                } else if (msg instanceof ByteArrayInputStreamCache) {
+                    // Here, we have an input stream cache to attempt to process
+                    ByteArrayInputStreamCache basc = (ByteArrayInputStreamCache) msg;
+                    basc.reset();
+                    ba = basc.readAllBytes();
+                } else {
+                    log.error("Unexpected type: {}.  Unable to convert to Map to send to Vantiq.",
+                              msg.getClass().getName());
+                    throw new InvalidPayloadException(exchange, Map.class);
+                }
+                msg = ba;
+            }
+    
+            // Jackson converts to json nodes but not to map.  Have to hunt for that or convert json node.
+            // Then Camel couldn't convert on its own.  Let's see if we can help things along...
+            if (msg instanceof String) {
+                // Then we must be fetching a JSON string.
+                String strMsg = (String) msg;
+                try {
                     vMsg = mapper.readValue(strMsg, new TypeReference<>() {});
-                } else  if (msg instanceof byte[]) {
-                    byte[] ba = (byte[]) msg;
-                    // Then we must be fetching a JSON string.
+                } catch (Exception e) {
+                    // Not valid JSON here, so we'll assume it's just a plain old string
+                    String strVal = mapper.writeValueAsString(strMsg);
+                    vMsg = Map.of("stringVal", strVal);
+                }
+            } else if (msg instanceof byte[]) {
+                // See if we can get Jackson to do the deserialization for us.
+                byte[] ba = (byte[]) msg;
+    
+                try {
+                    msg = mapper.readTree( ba);
+                    if (msg instanceof TextNode) {
+                        msg = ((TextNode) msg).asText();
+                    } else if (msg instanceof ContainerNode) {
+                        msg = mapper.convertValue(msg, Object.class);
+                        log.debug("Decoded ObjectNode into {}: {}", msg.getClass().getName(), msg);
+                    }
+                } catch (IOException e) {
+                    // Ignore -- we'll sort it out downstream
+                }
+                if (msg instanceof byte[]) {
+                    try {
+                        msg = mapper.readValue(ba, new TypeReference<>() {});
+                        log.trace("Tried readValue() -- got a {}", msg.getClass().getName());
+                    } catch (JsonParseException | DatabindException dbe) {
+                        // This couldn't do the conversion.
+                        log.trace("Exception converting/readValue() input message:", dbe);
+                        // Let's see if this is a serialization of something else. Google pubsub sometimes does this
+                        try {
+                            ByteArrayInputStream baStream = new ByteArrayInputStream(
+                                    (byte[]) exchange.getIn().getBody());
+                            ObjectInputStream objFromByte = new ObjectInputStream(baStream);
+                            msg = objFromByte.readObject();
+                        } catch (Exception e) {
+                            // Guess not...
+                            log.trace("Trapped after Java deserialization", e);
+                        }
+                    }
+                }
+                // If these both fail, msg will still be a byte array. Guess that's what there is...
+                if (msg instanceof byte[]) {
+                    // Then we might be fetching a JSON string.
                     if (checkUTF8(ba)) {
                         String strMsg = new String((byte[]) msg, StandardCharsets.UTF_8);
                         try {
-                            vMsg = mapper.readValue(strMsg, new TypeReference<>() {});
+                            vMsg = mapper.readValue(strMsg, new TypeReference<>() {
+                            });
                         } catch (Exception e) {
                             String strVal = mapper.writeValueAsString(strMsg);
                             vMsg = Map.of("stringVal", strVal);
                         }
                     } else {
+                        // But, maybe it's just a byte array...
                         String strVal = mapper.writeValueAsString(ba);
                         vMsg = Map.of("byteVal", strVal);
                     }
-                } else if (msg instanceof Map) {
+                }
+            }
+            // If we still haven't found anything good, try the already converted
+            if (vMsg == null) {
+                if (msg instanceof Map) {
                     vMsg = (Map<String, Object>) msg;
                 } else if (msg instanceof JsonNode) {
-                    vMsg = mapper.convertValue(msg, new TypeReference<>() {});
+                    // Let's check for more specific value nodes -- possible if someone, say, sent a String
+                    vMsg = fromJsonNode((JsonNode) msg);
                 } else {
                     log.error("Unexpected type: {}.  Unable to convert to Map to send to Vantiq.",
                               msg.getClass().getName());
                     throw new InvalidPayloadException(exchange, Map.class);
                 }
             }
-        } else {
-            if (msg instanceof InputStreamCache) {
-                // Here, we have an input stream cache to attempt to process
-                InputStreamCache isc = (InputStreamCache) msg;
-                isc.reset();
-                String str = new String(isc.readAllBytes());
-                if (str.charAt(0) == '"') {
-                    // Then strip leading & trailing quotes
-                    str = str.substring(1, str.length() - 1);
-                }
-                if (log.isTraceEnabled()) {
-                    log.trace("JSON String as input is: {}", str);
-                }
-        
-                vMsg = mapper.readValue(isc.readAllBytes(), new TypeReference<>() {});
-            } else {
-                log.error("Unexpected type: {}.  Unable to convert to Map to send to Vantiq.",
-                          msg.getClass().getName());
-                throw new InvalidPayloadException(exchange, Map.class);
-            }
         }
         if (log.isTraceEnabled()) {
             log.trace("Sending message: {}", vMsg);
@@ -136,22 +198,33 @@ public void process(Exchange exchange) throws Exception {
             // Note that we can't always use the Jackson objectMapper here because headers sometimes use Java types it
             // doesn't understand (e.g., things from google come with Protobuf timestamps or soemthing like that.
             // Simple .toString() handles it, so we'll copy the map ourselves.
-            Map<String, Object> hdrs;
-            try {
-                hdrs = mapper.convertValue(exchange.getMessage().getHeaders(),
-                                           new TypeReference<>() {});
-            } catch (Exception e) {
-                Map<String, Object> h = new HashMap<>();
-                exchange.getMessage().getHeaders().forEach((k, v) -> {
-                    if (v instanceof Integer || v instanceof Map) {
-                        h.put(k, v);
-                    } else {
-                        h.put(k, v.toString());
-                    }
-                });
-                hdrs = h;
+            Map<String, Object> hdrs = null;
+            if (exchange.getMessage().hasHeaders()) {
+                try {
+                    hdrs = mapper.convertValue(exchange.getMessage().getHeaders(),
+                                               new TypeReference<>() {
+                                               });
+                } catch (Exception e) {
+                    Map<String, Object> h = new HashMap<>();
+                    exchange.getMessage().getHeaders().forEach((k, v) -> {
+                        if (v instanceof Integer || v instanceof Map) {
+                            h.put(k, v);
+                        } else if (v != null) {
+                            h.put(k, v.toString());
+                        } else {
+                            // If they just pass a null value, we'll send it along and let the message consumer deal
+                            // with it.  This seems like an improper thing to do, but it's really up to the message
+                            // consumers here to deal with the vagaries of how various camel connectors operate.
+                            h.put(k, v);
+                        }
+                    });
+                    hdrs = h;
+                }
+            }
+            if (hdrs != null) {
+                hdrs.putAll(endpoint.duplicateHeaders(hdrs));
+                fmtMsg.put(STRUCTURED_MESSAGE_HEADERS_PROPERTY, hdrs);
             }
-            fmtMsg.put(STRUCTURED_MESSAGE_HEADERS_PROPERTY, hdrs);
             Map<String, Object> m = mapper.convertValue(vMsg, new TypeReference<>() {});
             fmtMsg.put(STRUCTURED_MESSAGE_MESSAGE_PROPERTY, m);
             vMsg = fmtMsg;
@@ -171,10 +244,37 @@ public void process(Exchange exchange) throws Exception {
                 endpoint.sendResponse(HttpURLConnection.HTTP_OK, responseAddr, vMsg);
             }
         } else {
+            log.debug("Producing message: {}", vMsg);
             endpoint.sendMessage(vMsg);
         }
     }
     
+    private Map<String, Object> fromJsonNode(JsonNode msg) {
+        Map<String, Object> retVal = null;
+        if (msg instanceof TextNode) {
+            retVal = Map.of("stringVal", msg.asText());
+        } else if (msg instanceof BinaryNode) {
+            retVal = Map.of("byteVal", msg.asText());
+        } else if (msg instanceof BooleanNode) {
+            retVal = Map.of("booleanVal", msg.asText());
+        } else if (msg instanceof ArrayNode) {
+            List<Map<String, Object>> list = new ArrayList<>(msg.size());
+            for (Iterator<JsonNode> it = msg.elements(); it.hasNext(); ) {
+                JsonNode n = it.next();
+                list.add(fromJsonNode(n));
+            }
+            retVal = Map.of("listVal", list);
+        } else {
+            try {
+                retVal = mapper.convertValue(msg, new TypeReference<>() {});
+            } catch (IllegalArgumentException iae) {
+                // If we get this, there's not much we can do except return the text
+                retVal = Map.of("unknownDatatype", msg.asText());
+            }
+        }
+        return retVal;
+    }
+    
     protected boolean checkUTF8(byte[] bytes) {
         
         CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder();
diff --git a/camelConnector/README.md b/camelConnector/README.md
index e9386f97..e3d09c52 100644
--- a/camelConnector/README.md
+++ b/camelConnector/README.md
@@ -98,6 +98,7 @@ To change the logging settings, edit the logging config file
 which is an [Apache Log4j configuration file](https://logging.apache.org/log4j/2.x/manual/configuration.html). The logger 
 name for each class is the class's fully qualified class name, *e.g.* "io.vantiq.extjsdk.ExtensionWebSocketClient".  
 
+<a name="serverConfig" id="serverConfig"></a>
 ## Server Config File
 (Please read the [SDK's server config documentation](../extjsdk/README.md#serverConfig) first.)
 
diff --git a/camelConnector/build.gradle b/camelConnector/build.gradle
index c870d408..79eee857 100644
--- a/camelConnector/build.gradle
+++ b/camelConnector/build.gradle
@@ -54,11 +54,13 @@ dependencies {
     implementation "org.apache.camel:camel-main:${camelVersion}"
     implementation "org.apache.camel:camel-support:${camelVersion}"
     implementation "org.apache.camel:camel-jackson:${camelVersion}"
+
     implementation "org.apache.commons:commons-lang3:${apacheCommonsLangVersion}"
     implementation "org.apache.ivy:ivy:${ivyVersion}"
     implementation project(":extjsdk")
     implementation project(":camelComponent")    // The Vantiq Camel Component is automatically included
     implementation "com.fasterxml.jackson.core:jackson-databind:${jacksonVersion}"
+    implementation "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:{jacksonVersion}"
     implementation "com.google.code.gson:gson:${gsonVersion}"
     // Used for downloading route documents
     implementation "io.vantiq:vantiq-sdk:${vantiqSDKVersion}"
@@ -70,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/CamelDiscovery.java b/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/discover/CamelDiscovery.java
index 17991708..30ff9d7d 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
@@ -68,11 +68,11 @@ Map<String, Set<String>> performComponentDiscovery(RouteBuilder rb, Properties p
             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);
+            // If our current context has property placeholders defined, 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().setInitialProperties(propertyValues);
 
             log.debug("Discovering Camel (version {}) components using context: {}", ectx.getVersion(), ectx.getName());
     
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 62234f06..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
@@ -27,6 +27,8 @@
 import org.apache.ivy.core.settings.IvySettings;
 import org.apache.ivy.plugins.resolver.ChainResolver;
 import org.apache.ivy.plugins.resolver.IBiblioResolver;
+import org.apache.ivy.util.DefaultMessageLogger;
+import org.apache.ivy.util.Message;
 import org.slf4j.helpers.MessageFormatter;
 
 import java.io.File;
@@ -56,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
@@ -81,6 +83,10 @@ public class CamelResolver {
             throw new IllegalArgumentException("The destination parameter cannot be null.");
         }
     
+        if (!log.isTraceEnabled()) {
+            // Disable voluminous Ivy output that's not really helpful.
+            Message.setDefaultLogger(new DefaultMessageLogger(Message.MSG_WARN));
+        }
         //creates clear ivy settings
         ivySettings = new IvySettings();
         if (cache != null) {
@@ -96,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);
                 }
@@ -202,7 +207,7 @@ Collection<File> resolve(String organization, String name, String revision, Stri
         }
         md.addDependency(dd);
         
-        log.info("Resolving required libraries -- {}", purpose);
+        log.trace("Resolving required libraries -- {}", purpose);
         //init resolve report
         ResolveReport report = ivy.resolve(md, resolveOptions);
         
@@ -234,7 +239,7 @@ Collection<File> resolve(String organization, String name, String revision, Stri
         for (File f: necessaryFiles) {
             log.trace("Retrieved or up-to-date file: {} ({})", f.getAbsolutePath(), identity());
         }
-        log.debug("{} -- Making {} artifacts available to {}", identity(), necessaryFiles.size(), purpose);
+        log.trace("{} -- Making {} artifacts available to {}", identity(), necessaryFiles.size(), purpose);
         return necessaryFiles;
     }
 }
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 09fbe96e..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,15 +8,22 @@
 
 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;
+import org.apache.camel.builder.TemplatedRouteBuilder;
 import org.apache.camel.impl.DefaultCamelContext;
 import org.apache.camel.main.MainRegistry;
 import org.apache.camel.main.MainSupport;
 import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.model.ModelCamelContext;
+import org.apache.camel.model.RouteDefinition;
+import org.apache.camel.model.RouteTemplateDefinition;
+import org.apache.camel.model.RoutesDefinition;
 import org.apache.camel.spi.Resource;
 import org.apache.camel.spi.RoutesBuilderLoader;
 import org.apache.camel.spi.RoutesLoader;
@@ -32,6 +39,7 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
@@ -49,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;
     
@@ -57,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;
@@ -81,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;
@@ -98,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);
     }
     
@@ -112,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;
@@ -128,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;
     }
@@ -140,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() {
@@ -161,18 +194,23 @@ public Properties getCamelProperties() {
      * @throws Exception for errors during the process.  IllegalStateException if called before there is a Camel
      *                   context available.  Others may be thrown by called methods.
      */
-    private ClassLoader constructClassLoader() throws Exception {
+    private ClassLoader discoverAndBuildClassloader() throws Exception {
         if (getCamelContext() == null) {
             throw new IllegalStateException("Camel context does not exist.");
         }
         CamelDiscovery discoverer = new CamelDiscovery();
         Map<String, Set<String>> discResults = discoverer.performComponentDiscovery(routeBuilder, camelProperties);
-        
+    
+        // Now, based on our discovery, construct a classLoader
+        return buildClassloader(discoverer, discResults);
+    }
+    
+    private ClassLoader buildClassloader(CamelDiscovery discoverer,
+                                         Map<String, Set<String>> discResults) throws Exception {
         // 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
         // repositories, so we'll use a LinkedHashSet<>.  That acts as a set but retains the order in which elements
         // were inserted.
-        Set<File> jarList = new LinkedHashSet<>();
         List<URI> repsToUse = repositories;
         if (repsToUse == null) {
             repsToUse = Collections.emptyList();
@@ -184,7 +222,7 @@ private ClassLoader constructClassLoader() throws Exception {
                                                 discResults.get(CamelDiscovery.DATAFORMATS_TO_LOAD),
                                                 this.additionalLibraries,
                                                 discoverer);
-       jarList.addAll(aReposWorth);
+        Set<File> jarList = new LinkedHashSet<>(aReposWorth);
     
         // Now, construct a classloader that includes all these loaded files
     
@@ -193,7 +231,10 @@ private ClassLoader constructClassLoader() throws Exception {
             URL url = jar.toURI().toURL();
             urlList.add(url);
         }
-    
+        if (log.isTraceEnabled()) {
+            // 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.
     
         ClassLoader parent = getCamelContext() != null ? getCamelContext().getApplicationContextClassLoader() : null;
@@ -232,40 +273,49 @@ private Set<File> resolveFromRepo(List<URI> repositories, Set<String> components
         Set<File> jarSet = new LinkedHashSet<>();
         CamelResolver cr = new CamelResolver(appName, repositories,
                                              ivyCache, loadedLibraries);
-        for (String comp : componentsToResolve) {
-            String lib = discoverer.findComponentForScheme(comp);
-            if (lib == null) {
-                // If we don't know the name, we have no chance of resolving it.  So complain.
-                throw new ResolutionException("Unable to determine jar file for component: " + comp);
+        if (discoverer != null) {
+            for (String comp : componentsToResolve) {
+                String lib = discoverer.findComponentForScheme(comp);
+                if (lib == null) {
+                    // If we don't know the name, we have no chance of resolving it.  So complain.
+                    throw new ResolutionException("Unable to determine jar file for component: " + comp);
+                }
+                // Our resolver is a chain resolver, including all the IBibloResolvers that are in our list of URLs.
+                // Thus, we will see an error here only if the artifact is resolved by none of the repos in our list.
+                // We could learn to ignore it & let camel throw the error, but our throwing the ResolutionException
+                // seems cleaner.  Notify about the problems closer to the source.
+                Collection<File> resolved = cr.resolve("org.apache.camel", lib, getCamelContext().getVersion(),
+                                                       "Component Resolution");
+                jarSet.addAll(resolved);
             }
-            // Our resolver is a chain resolver, including all the IBibloResolvers that are in our list of URLs.
-            // Thus, we will see an error here only if the artifact is resolved by none of the repos in our list.
-            // We could learn to ignore it & let camel throw the error, but our throwing the ResolutionException
-            // seems cleaner.  Notify about the problems closer to the source.
-            Collection<File> resolved = cr.resolve("org.apache.camel", lib, getCamelContext().getVersion(),
-                                                   "Component Resolution");
-            jarSet.addAll(resolved);
-        }
     
-        for (String df : dataformatsToResolve) {
-            String lib = discoverer.findDataFormatForName(df);
-            if (lib == null) {
-                // If we don't know the name, we have no chance of resolving it.  So complain.
-                throw new ResolutionException("Unable to determine jar file for dataformat: " + df);
+            for (String df : dataformatsToResolve) {
+                String lib = discoverer.findDataFormatForName(df);
+                if (lib == null) {
+                    // If we don't know the name, we have no chance of resolving it.  So complain.
+                    throw new ResolutionException("Unable to determine jar file for dataformat: " + df);
+                }
+                // Our resolver is a chain resolver, including all the IBibloResolvers that are in our list of URLs.
+                // Thus, we will see an error here only if the artifact is resolved by none of the repos in our list.
+                // We could learn to ignore it & let camel throw the error, but our throwing the ResolutionException
+                // seems cleaner.  Notify about the problems closer to the source.
+                Collection<File> resolved = cr.resolve("org.apache.camel", lib, getCamelContext().getVersion(),
+                                                       "Dataformat Resolution");
+                jarSet.addAll(resolved);
             }
-            // Our resolver is a chain resolver, including all the IBibloResolvers that are in our list of URLs.
-            // Thus, we will see an error here only if the artifact is resolved by none of the repos in our list.
-            // We could learn to ignore it & let camel throw the error, but our throwing the ResolutionException
-            // seems cleaner.  Notify about the problems closer to the source.
-            Collection<File> resolved = cr.resolve("org.apache.camel", lib, getCamelContext().getVersion(),
-                                                   "Dataformat Resolution");
-            jarSet.addAll(resolved);
         }
         // If our use has specified additional libraries to include, do those now.
         if (additionalLibraries != null && !additionalLibraries.isEmpty()) {
             for (String comp : additionalLibraries) {
                 String[] compParts = comp.split(":");
-                if (compParts.length != 3) {
+                if (compParts.length == 2 && compParts[0].startsWith("org.apache.camel")) {
+                    // Then, this is a camel component.  Use our current version
+                    String[] newParts = new String[3];
+                    newParts[0] = compParts[0];
+                    newParts[1] = compParts[1];
+                    newParts[2] = getCamelContext().getVersion();
+                    compParts = newParts;
+                } else if (compParts.length != 3) {
                     throw new IllegalArgumentException("The configuration's 'additionalLibraries' property must be a list " +
                                                        "of library specifications of the form " +
                                                        "<organization>:<name>:<revision>.  Found " + comp);
@@ -287,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 {
@@ -374,8 +423,7 @@ protected void beforeStart() throws Exception {
             // 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);
-    
+            camelContext.getPropertiesComponent().setInitialProperties(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 " +
@@ -385,10 +433,10 @@ protected void beforeStart() throws Exception {
                 routeBuilder = loadRouteFromText(this.routeSpec, this.routeSpecType);
             }
             if (routeBasedCL == null) {
-                // FIXME -- need to detect when there are no routes.  Do we need to supply properties here?
+                // FIXME -- need to detect when there are no routes.
                 if (routeBuilder != null) {
                     try {
-                        routeBasedCL = constructClassLoader();
+                        routeBasedCL = discoverAndBuildClassloader();
                     } catch (Exception e) {
                         throw new RuntimeException("Unable to create route-based class loader", e);
                     }
@@ -508,20 +556,70 @@ protected void awaitStartup() throws Exception {
      */
     protected RouteBuilder loadRouteFromText(String specification, String specificationType) throws Exception {
         log.debug("Loading route from {} specificationType", specificationType);
-        if (log.isDebugEnabled()) {
-            // Dump route spec out here only under trace.  Camel may have substituted key values and we don't want
+        if (log.isTraceEnabled()) {
+            // 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.debug("Loading route (specificationType {}):\n{}", specificationType, specification);
+            log.trace("Loading route (specificationType {}):\n{}", specificationType, specification);
         }
-        ExtendedCamelContext extendedCamelContext = camelContext.adapt(ExtendedCamelContext.class);
-        RoutesLoader loader = extendedCamelContext.getRoutesLoader();
-        Resource resource = ResourceHelper.fromString("in-memory." + specificationType, specification);
-        loader.loadRoutes(resource);
-        RoutesBuilderLoader rbl = loader.getRoutesLoader(specificationType);
-        RoutesBuilder rb = rbl.loadRoutesBuilder(resource);
-        log.debug("Route builder: {}", rb.toString());
+        // First, construct a simple classloader.  This is based on the dependencies defined in the Kamelet. Later,
+        // these will be augmented via discovery since these declarations need not be complete. These are used to
+        // satisfy camel's need to instantiate as part of setting up the routes.
+        Map<String, Set<String>> discResults = new HashMap<>();
+        discResults.put(CamelDiscovery.COMPONENTS_TO_LOAD, Set.of());
+        discResults.put(CamelDiscovery.DATAFORMATS_TO_LOAD, Set.of());
+        ClassLoader cl = buildClassloader(null, discResults);
         
-        return (RouteBuilder) rb;
+        ModelCamelContext mcc = (ModelCamelContext) camelContext;
+        ExtendedCamelContext extendedCamelContext = mcc.adapt(ExtendedCamelContext.class);
+        ClassLoader oldAppClassLoader = extendedCamelContext.getApplicationContextClassLoader();
+        try {
+            extendedCamelContext.setApplicationContextClassLoader(cl);
+            RoutesLoader loader = extendedCamelContext.getRoutesLoader();
+            Resource resource = ResourceHelper.fromString("in-memory." + specificationType, specification);
+            loader.loadRoutes(resource);
+            log.debug("loadRoutesFromText(): routes: {}, routeTemplates: {}", mcc.getRoutes(),
+                      mcc.getRouteTemplateDefinitions());
+            // Based on what was loaded, we can now construct our RouteBuilder.
+            // For cases where we have a route, we are done.
+            RoutesBuilderLoader rbl = loader.getRoutesLoader(specificationType);
+            RoutesBuilder rsb = rbl.loadRoutesBuilder(resource);
+            RouteBuilder rb;
+            if (rsb instanceof RouteBuilder) {
+                rb = (RouteBuilder) rsb;
+            } else {
+                String className = rsb == null ? "null" : rsb.getClass().getName();
+                throw new RuntimeException("loadRoutesFromText(): Expected RouteBuilder by got " + className);
+            }
+            if (mcc.getRouteTemplateDefinitions().size() > 0) {
+                // However, if we have a RouteTemplate rather than a route, we need to construct a route from the template.
+                // We don't (currently) support parameters on templates, we'll simply fetch any templates present and
+                // build the routes from them (property placeholder substitution will work as expected).
+                RoutesDefinition rsd = new RoutesDefinition();
+                List<String> beansInvolved = new ArrayList<>();
+                mcc.getRouteTemplateDefinitions().forEach((RouteTemplateDefinition rtd) -> {
+                    log.debug("loadRoutesFromText(): Found route Def id: {} :: {}", rtd.getId(), rtd.getRoute());
+                    String templateId = rtd.getId();
+                    TemplatedRouteBuilder builder = TemplatedRouteBuilder.builder(mcc, templateId).routeId(templateId);
+                    String routeId;
+                    try {
+                        routeId = builder.add();
+                    } catch (Exception e) {
+                        throw new RuntimeException("Error adding route template to context", e);
+                    }
+                    log.debug("loadRoutesFromText(): Added route (id {}) from template (id {}): {}", routeId,
+                              templateId,
+                              rtd.getRoute());
+                    RouteDefinition rd = mcc.getRouteDefinition(routeId);
+                    rsd.route(rd);
+                });
+                rb.setRouteCollection(rsd);
+            } // Else, all the work was done by loading the route.  Only templates have the extra steps above
+            log.debug("loadRoutesFromText(): Returning RouteBuilder: {}", rb);
+            return rb;
+        } finally {
+            // TODO -- tbis may need to go as we may need it for dependency determination.
+            extendedCamelContext.setApplicationContextClassLoader(oldAppClassLoader);
+        }
     }
     
     @Override
diff --git a/camelConnector/src/main/resources/artifactMap.json b/camelConnector/src/main/resources/artifactMap.json
index 476beb53..c006bf82 100644
--- a/camelConnector/src/main/resources/artifactMap.json
+++ b/camelConnector/src/main/resources/artifactMap.json
@@ -1,5 +1,5 @@
 {
-    "camelVersion": "3.18.3",
+    "camelVersion": "3.21.0",
     "components": {
         "dns": "camel-dns",
         "docker": "camel-docker",
@@ -30,6 +30,7 @@
         "spring-redis": "camel-spring-redis",
         "lumberjack": "camel-lumberjack",
         "direct": "camel-direct",
+        "elasticsearch": "camel-elasticsearch",
         "jooq": "camel-jooq",
         "mina": "camel-mina",
         "couchbase": "camel-couchbase",
@@ -37,6 +38,7 @@
         "cxfrs": "camel-cxf-rest",
         "ldif": "camel-ldif",
         "jclouds": "camel-jclouds",
+        "influxdb2": "camel-influxdb2",
         "netty-http": "camel-netty-http",
         "jbpm": "camel-jbpm",
         "mvel": "camel-mvel",
@@ -66,6 +68,8 @@
         "aws2-kinesis": "camel-aws2-kinesis",
         "aws2-translate": "camel-aws2-translate",
         "aws2-kms": "camel-aws2-kms",
+        "aws-kinesis": "camel-aws-cloudtrail",
+        "aws-cloudtrail": "camel-aws-cloudtrail",
         "snmp": "camel-snmp",
         "mongodb-gridfs": "camel-mongodb-gridfs",
         "digitalocean": "camel-digitalocean",
@@ -77,6 +81,7 @@
         "controlbus": "camel-controlbus",
         "paho": "camel-paho",
         "freemarker": "camel-freemarker",
+        "whatsapp": "camel-whatsapp",
         "guava-eventbus": "camel-guava-eventbus",
         "debezium-mongodb": "camel-debezium-mongodb",
         "debezium-sqlserver": "camel-debezium-sqlserver",
@@ -84,18 +89,18 @@
         "debezium-mysql": "camel-debezium-mysql",
         "debezium-oracle": "camel-debezium-oracle",
         "debezium-db2": "camel-debezium-db2",
+        "rocketmq": "camel-rocketmq",
         "scp": "camel-jsch",
         "git": "camel-git",
-        "cmis": "camel-cmis",
         "crypto": "camel-crypto",
         "dynamic-router": "camel-dynamic-router",
+        "zeebe": "camel-zeebe",
         "micrometer": "camel-micrometer",
         "stax": "camel-stax",
         "sjms2": "camel-sjms2",
         "robotframework": "camel-robotframework",
         "zendesk": "camel-zendesk",
         "jetty": "camel-jetty",
-        "dozer": "camel-dozer",
         "hazelcast-ringbuffer": "camel-hazelcast",
         "hazelcast-set": "camel-hazelcast",
         "hazelcast-topic": "camel-hazelcast",
@@ -124,6 +129,7 @@
         "flink": "camel-flink",
         "atmosphere-websocket": "camel-atmosphere-websocket",
         "ssh": "camel-ssh",
+        "etcd3": "camel-etcd3",
         "saga": "camel-saga",
         "websocket-jsr356": "camel-websocket-jsr356",
         "chunk": "camel-chunk",
@@ -154,7 +160,6 @@
         "reactive-streams": "camel-reactive-streams",
         "mock": "camel-mock",
         "vertx-http": "camel-vertx-http",
-        "vertx-kafka": "camel-vertx-kafka",
         "vertx": "camel-vertx",
         "vertx-websocket": "camel-vertx-websocket",
         "pubnub": "camel-pubnub",
@@ -188,6 +193,7 @@
         "bean": "camel-bean",
         "class": "camel-bean",
         "ldap": "camel-ldap",
+        "plc4x": "camel-plc4x",
         "ircs": "camel-irc",
         "irc": "camel-irc",
         "hdfs": "camel-hdfs",
@@ -244,6 +250,7 @@
         "openshift-builds": "camel-kubernetes",
         "kubernetes-secrets": "camel-kubernetes",
         "kubernetes-config-maps": "camel-kubernetes",
+        "kubernetes-events": "camel-kubernetes",
         "kubernetes-replication-controllers": "camel-kubernetes",
         "kubernetes-namespaces": "camel-kubernetes",
         "kubernetes-deployments": "camel-kubernetes",
@@ -254,9 +261,8 @@
         "grape": "camel-grape",
         "servicenow": "camel-servicenow",
         "metrics": "camel-metrics",
-        "ahc-wss": "camel-ahc-ws",
-        "ahc-ws": "camel-ahc-ws",
         "jslt": "camel-jslt",
+        "hyperledger-aries": "camel-hyperledger-aries",
         "mllp": "camel-mllp",
         "infinispan-embedded": "camel-infinispan-embedded",
         "infinispan": "camel-infinispan",
@@ -290,6 +296,7 @@
         "ref": "camel-ref",
         "validator": "camel-validator",
         "json-patch": "camel-json-patch",
+        "mapstruct": "camel-mapstruct",
         "nitrite": "camel-nitrite",
         "flatpack": "camel-flatpack",
         "salesforce": "camel-salesforce",
@@ -310,7 +317,6 @@
         "mongodb": "camel-mongodb",
         "fhir": "camel-fhir",
         "kamelet": "camel-kamelet",
-        "ahc": "camel-ahc",
         "activemq": "camel-activemq",
         "stomp": "camel-stomp",
         "weka": "camel-weka",
@@ -340,6 +346,7 @@
         "pulsar": "camel-pulsar",
         "lpr": "camel-printer",
         "knative": "camel-knative",
+        "dhis2": "camel-dhis2",
         "milo-server": "camel-milo",
         "milo-client": "camel-milo",
         "milo-browse": "camel-milo",
@@ -392,6 +399,8 @@
         "avro": "camel-avro",
         "jaxb": "camel-jaxb",
         "flatpack": "camel-flatpack",
+        "swiftMx": "camel-swift",
+        "swiftMt": "camel-swift",
         "fhirJson": "camel-fhir",
         "fhirXml": "camel-fhir",
         "jsonb": "camel-jsonb",
diff --git a/camelConnector/src/main/resources/log4j2.properties b/camelConnector/src/main/resources/log4j2.properties
index 009d277b..40d35d7d 100644
--- a/camelConnector/src/main/resources/log4j2.properties
+++ b/camelConnector/src/main/resources/log4j2.properties
@@ -8,5 +8,12 @@ rootLogger.appenderRef.out.ref = out
 
 logger.vantiqcamelconn.name = io.vantiq.extsrc.camelconn
 logger.vantiqcamelconn.level = DEBUG
-logger.vantiqcamelconn.appenderRef = Console
+logger.vantiqcamelconn.appenderRef = out
 
+logger.vantiqcamel.name = io.vantiq.extsrc.camel
+logger.vantiqcamel.level = DEBUG
+logger.vantiqcamel.appenderRef = out
+
+logger.cameltrace.name = org.apache.camel.Tracing
+logger.cameltrace.level = DEBUG
+logger.cameltrace.appenderRef = out
diff --git a/settings.gradle b/settings.gradle
index 62d4c857..ba524b12 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -2,6 +2,7 @@ rootProject.name = 'vantiq-extension-source'
 include 'extjsdk'
 include 'camelComponent'
 include 'camelConnector'
+include 'camelAssemblies'
 include 'extpsdk'
 include 'opcuaSource'
 include 'pythonExecSource'


Test patch:
diff --git a/camelComponent/src/test/java/io/vantiq/extsrc/camel/VantiqComponentTest.java b/camelComponent/src/test/java/io/vantiq/extsrc/camel/VantiqComponentTest.java
index 422e139a..b48d1ea5 100644
--- a/camelComponent/src/test/java/io/vantiq/extsrc/camel/VantiqComponentTest.java
+++ b/camelComponent/src/test/java/io/vantiq/extsrc/camel/VantiqComponentTest.java
@@ -10,6 +10,8 @@
 
 import static io.vantiq.extsrc.camel.VantiqEndpoint.ACCESS_TOKEN_PARAM;
 import static io.vantiq.extsrc.camel.VantiqEndpoint.CONSUMER_OUTPUT_JSON_PARAM;
+import static io.vantiq.extsrc.camel.VantiqEndpoint.CONSUMER_OUTPUT_JSON_STREAM_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_HEADER_PARAM;
 import static io.vantiq.extsrc.camel.VantiqEndpoint.STRUCTURED_MESSAGE_HEADERS_PROPERTY;
@@ -17,8 +19,8 @@
 
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.node.ArrayNode;
 import com.fasterxml.jackson.databind.node.BaseJsonNode;
+import com.fasterxml.jackson.databind.node.ContainerNode;
 import com.fasterxml.jackson.databind.node.ObjectNode;
 import com.fasterxml.jackson.databind.node.TextNode;
 import io.vantiq.extjsdk.ExtensionServiceMessage;
@@ -27,6 +29,7 @@
 import io.vantiq.extjsdk.Response;
 import io.vantiq.extjsdk.TestListener;
 
+import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 import java.time.Instant;
 import java.util.ArrayList;
@@ -46,8 +49,11 @@
 import org.apache.camel.ExchangePattern;
 import org.apache.camel.InvalidPayloadException;
 import org.apache.camel.LoggingLevel;
+import org.apache.camel.StreamCache;
 import org.apache.camel.builder.RouteBuilder;
 import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.converter.stream.ByteArrayInputStreamCache;
+import org.apache.camel.converter.stream.InputStreamCache;
 import org.apache.camel.test.junit4.CamelTestSupport;
 import org.junit.Test;
 
@@ -57,10 +63,14 @@ public class VantiqComponentTest extends CamelTestSupport {
     private static final String accessToken = "someAccessToken";
     private static final String TEST_MSG_PREAMBLE = "published message from FauxVantiq #";
     private static final String TEST_MSG_KEY = "someRandomKey";
+    private static final String TEST_HEADER_BEAN_NAME = "TestHEBean";
     
     private final String routeStartUri = "direct:start";
     
     private final String routeStartStructuredUri = "direct:structuredstart";
+    
+    private final String routeStartStructuredHeaderMapUri = "direct:structuredMappedstart";
+    
     private final String routeEndUri = "mock:direct:result";
     
     private final String exceptionEndpoint = "mock:direct:error";
@@ -73,6 +83,12 @@ public class VantiqComponentTest extends CamelTestSupport {
             "&" + ACCESS_TOKEN_PARAM + "=" + accessToken +
             "&" + STRUCTURED_MESSAGE_HEADER_PARAM + "=true";
     
+    private final String vantiqEndpointStructuredHeaderMapUri = "vantiq://doesntmatter/" +
+            "?" + SOURCE_NAME_PARAM + "=" + testSourceName +
+            "&" + ACCESS_TOKEN_PARAM + "=" + accessToken +
+            "&" + STRUCTURED_MESSAGE_HEADER_PARAM + "=true" +
+            "&" + HEADER_DUPLICATION_BEAN_NAME + "=" + TEST_HEADER_BEAN_NAME;
+    
     private final String vantiqSenderUri = "vantiq://senderdoesntmatter/" +
             "?" + SOURCE_NAME_PARAM + "=" + testSourceName +
             "&" + ACCESS_TOKEN_PARAM + "=" + accessToken;
@@ -86,17 +102,47 @@ public class VantiqComponentTest extends CamelTestSupport {
             "&" + ACCESS_TOKEN_PARAM + "=" + accessToken +
             "&" + CONSUMER_OUTPUT_JSON_PARAM + "=true";
     
+    private final String vantiqJsonSenderStreamUri = "vantiq://jsonsenderstreamdoesntmatter/" +
+            "?" + SOURCE_NAME_PARAM + "=" + testSourceName +
+            "&" + ACCESS_TOKEN_PARAM + "=" + accessToken +
+            "&" + CONSUMER_OUTPUT_JSON_STREAM_PARAM + "=true";
+    
     public final String vantiqStructuredJsonSenderUri = "vantiq://structuredsenderdoesntmatter/" +
             "?" + SOURCE_NAME_PARAM + "=" + testSourceName +
             "&" + ACCESS_TOKEN_PARAM + "=" + accessToken +
             "&" + CONSUMER_OUTPUT_JSON_PARAM + "=true" +
             "&" + STRUCTURED_MESSAGE_HEADER_PARAM + "=true";
     
+    public final String vantiqStructuredJsonSenderStreamUri = "vantiq://structuredsenderstreamdoesntmatter/" +
+            "?" + SOURCE_NAME_PARAM + "=" + testSourceName +
+            "&" + ACCESS_TOKEN_PARAM + "=" + accessToken +
+            "&" + CONSUMER_OUTPUT_JSON_STREAM_PARAM + "=true" +
+            "&" + STRUCTURED_MESSAGE_HEADER_PARAM + "=true";
+    
+    public final String vantiqStructuredJsonSenderMappedUri = "vantiq://structuredsenderdoesntmatter/" +
+            "?" + SOURCE_NAME_PARAM + "=" + testSourceName +
+            "&" + ACCESS_TOKEN_PARAM + "=" + accessToken +
+            "&" + CONSUMER_OUTPUT_JSON_PARAM + "=true" +
+            "&" + STRUCTURED_MESSAGE_HEADER_PARAM + "=true" +
+            "&" + HEADER_DUPLICATION_BEAN_NAME + "=" + TEST_HEADER_BEAN_NAME;
+    
+    public final String vantiqStructuredJsonSenderMappedStreamUri = "vantiq://structuredsenderstreamdoesntmatter/" +
+            "?" + SOURCE_NAME_PARAM + "=" + testSourceName +
+            "&" + ACCESS_TOKEN_PARAM + "=" + accessToken +
+            "&" + CONSUMER_OUTPUT_JSON_STREAM_PARAM + "=true" +
+            "&" + STRUCTURED_MESSAGE_HEADER_PARAM + "=true" +
+            "&" + HEADER_DUPLICATION_BEAN_NAME + "=" + TEST_HEADER_BEAN_NAME;
+    
     private final String vantiqJsonQuerierUri = "vantiq://jsonquerierdoesntmatter/" +
             "?" + SOURCE_NAME_PARAM + "=" + testSourceName +
             "&" + ACCESS_TOKEN_PARAM + "=" + accessToken +
             "&" + CONSUMER_OUTPUT_JSON_PARAM + "=true";
     
+    private final String vantiqJsonQuerierStreamUri = "vantiq://jsonquerierstreamdoesntmatter/" +
+            "?" + SOURCE_NAME_PARAM + "=" + testSourceName +
+            "&" + ACCESS_TOKEN_PARAM + "=" + accessToken +
+            "&" + CONSUMER_OUTPUT_JSON_STREAM_PARAM + "=true";
+    
     @Test
     public void testVantiqSetup() {
         // First, grab our test environment.
@@ -186,10 +232,10 @@ public void testVantiqProducerJson() {
         validateExtensionMsg(lastMsg, false, null, new String[] { "stringVal"}, testMsg);
     
         Instant rightNow = Instant.now();
+        //noinspection rawtypes
         Map timeMsg = Map.of("time", rightNow);
         sendBody(routeStartUri, timeMsg);
     
-        //noinspection rawtypes
         lastMsg = fc.getLastMessageAsMap();
         validateExtensionMsg(lastMsg, false, null, new String[] {"time"}, rightNow.toString());
     
@@ -199,6 +245,36 @@ public void testVantiqProducerJson() {
         validateExtensionMsg(lastMsg, false, null, new String[] { "byteVal"}, testBytes);
     }
     
+    @Test
+    public void testVantiqProducerFromString() {
+        
+        // First, grab our test environment.
+        FauxVantiqComponent vc = (FauxVantiqComponent) context.getComponent("vantiq");
+        assert vc != null;
+        // Note that we need to fetch the endpoints by URI since there are more than one of them.
+        FauxVantiqEndpoint ep = (FauxVantiqEndpoint) context.getEndpoint(vantiqEndpointUri);
+        FalseClient fc = ep.myClient;
+        
+        ArrayList<String> msgs =new ArrayList<>();
+        int itemCount = 10;
+        for (int i = 0; i < itemCount; i++) {
+            msgs.add("hi mom " + i);
+        }
+        
+        // Verify that when then producer gets a pure string message, that it turns it a Vail object (aka Map) with
+        // a single key "stringVal" and the test string as the value.
+        for (Object item: msgs) {
+            log.info("Sending msg: " + item);
+            
+            assert item != null;
+            sendBody(routeStartUri, item);
+            //noinspection rawtypes
+            Map lastMsg = fc.getLastMessageAsMap();
+            validateExtensionMsg(lastMsg, false, null, new String[] {"stringVal"}, "hi mom");
+        }
+    }
+    
+    
     @Test
     public void testVantiqProducerStructuredJson() {
         
@@ -239,6 +315,7 @@ public void testVantiqProducerStructuredJson() {
             Map<String, Object> hdrs = new HashMap<>();
             hdrs.put("theMessage", msg);
             hdrs.put("counter", hdrCounter.getAndIncrement());
+            hdrs.put("shouldBeNull", null);
             sendBody(routeStartStructuredUri, msg, hdrs);
     
             //noinspection rawtypes
@@ -273,6 +350,87 @@ public void testVantiqProducerStructuredJson() {
         validateExtensionMsg(lastMsg, true, null, new String[] {"bye"}, "mom");
     }
     
+    @Test
+    public void testVantiqProducerHeaderMapping() {
+        // First, grab our test environment.
+        FauxVantiqComponent vc = (FauxVantiqComponent) context.getComponent("vantiq");
+        assert vc != null;
+        // Note that we need to fetch the endpoints by URI since there are more than one of them.
+        FauxVantiqEndpoint ep = (FauxVantiqEndpoint) context.getEndpoint(vantiqEndpointStructuredHeaderMapUri);
+        FalseClient fc = ep.myClient;
+        
+        ArrayList<String> msgs =new ArrayList<>();
+        int itemCount = 10;
+        for (int i = 0; i < itemCount; i++) {
+            msgs.add("{\"hi\": \"mom " + i + "\"}");
+        }
+        
+        for (Object item: msgs) {
+            log.info("Sending msg: " + item);
+            
+            assert item != null;
+            sendBody(routeStartStructuredHeaderMapUri, item);
+            
+            //noinspection rawtypes
+            Map lastMsg = fc.getLastMessageAsMap();
+            validateExtensionMsg(lastMsg, true, null, new String[] {"hi"}, "mom");
+        }
+        
+        // Now, try a similar test sending maps
+        ArrayList<Map<String, String>> mapList = new ArrayList<>();
+        for (int i = 0; i < itemCount; i++) {
+            Map<String, String> mp = Map.of("hi", "dad " + i);
+            mapList.add(mp);
+        }
+        
+        AtomicInteger hdrCounter = new AtomicInteger(0);
+        
+        mapList.forEach( msg -> {
+            Map<String, Object> hdrs = new HashMap<>();
+            hdrs.put("theMessage", msg);
+            hdrs.put("counter", hdrCounter.getAndIncrement());
+            hdrs.put("shouldBeNull", null);
+            hdrs.put("header1", "value1");
+            hdrs.put("header2", "value2");
+    
+            Map<String, Object> expHdrs = new HashMap<>(hdrs);
+            expHdrs.put("dupHeader1", hdrs.get("header1"));
+            expHdrs.put("dupHeader2", hdrs.get("header2"));
+            sendBody(routeStartStructuredHeaderMapUri, msg, hdrs);
+            
+            //noinspection rawtypes
+            Map lastMsg = fc.getLastMessageAsMap();
+            validateExtensionMsg(lastMsg, true, expHdrs, new String[] {"hi"}, "dad");
+        });
+        
+        ObjectMapper mapper = new ObjectMapper();
+        AtomicInteger counter = new AtomicInteger(0);
+        msgs.forEach( msg -> {
+            try {
+                ObjectNode node = mapper.readValue(msg, ObjectNode.class);
+                node.put("hi", "aki " + counter.getAndIncrement());
+                Map<String, Object> hdrs = new HashMap<>();
+                hdrs.put("theMessage", msg);
+                hdrs.put("counter", hdrCounter.getAndIncrement());
+                
+                sendBody(routeStartStructuredHeaderMapUri, node, hdrs);
+                
+                //noinspection rawtypes
+                Map lastMsg = fc.getLastMessageAsMap();
+                validateExtensionMsg(lastMsg, true, hdrs, new String[] {"hi"}, "aki");
+            } catch (JsonProcessingException e) {
+                throw new RuntimeException(e);
+            }
+        });
+        
+        sendBody(routeStartStructuredHeaderMapUri, "{\"bye\": \"mom\"}");
+        
+        //noinspection rawtypes
+        Map lastMsg = fc.getLastMessageAsMap();
+        validateExtensionMsg(lastMsg, true, null, new String[] {"bye"}, "mom");
+    }
+    
+    
     void validateExtensionMsg(Map<?,?> lastMsg, Boolean isStructured, Map<String, Object> expHdrs,
                               String[] msgKeys, Object msgPreamble) {
         assert lastMsg.containsKey("op");
@@ -285,23 +443,28 @@ void validateExtensionMsg(Map<?,?> lastMsg, Boolean isStructured, Map<String, Ob
         Map<String, Object> msg = (Map<String, Object>) lastMsg.get("object");
         if (isStructured) {
             if (expHdrs != null) {
+                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;
-                assert expHdrs.size() == hdrs.size();
                 expHdrs.forEach( (key, value) -> {
-                    assert hdrs.containsKey(key);
-                    assert hdrs.get(key) != null;
-                    assert expHdrs.get(key) != null;
-                    log.debug("For key {}, comparing value {} ){} with expected {} ({}).",
-                              key, hdrs.get(key), hdrs.get(key).getClass().getName(),
-                              expHdrs.get(key), expHdrs.get(key).getClass().getName());
-                    assert hdrs.get(key).equals(expHdrs.get(key));
+                    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),
+                              expHdrs.get(key),
+                              expHdrs.get(key) != null ? expHdrs.get(key).getClass().getName() : expHdrs.get(key));
+                    if (hdrs.get(key) != null) {
+                        assert hdrs.get(key).equals(expHdrs.get(key));
+                    } else {
+                        assert expHdrs.get(key) == null;
+                        // Also, verify that the header is present -- get(key) will return null if key isn't present.
+                        assert hdrs.containsKey(key);
+                    }
                 });
             }
             assert msg.containsKey(STRUCTURED_MESSAGE_MESSAGE_PROPERTY);
-            assert msg.containsKey(STRUCTURED_MESSAGE_HEADERS_PROPERTY);
             //noinspection unchecked
             msg = (Map<String, Object>) msg.get(STRUCTURED_MESSAGE_MESSAGE_PROPERTY);
         }
@@ -417,12 +580,21 @@ public void testVantiqConsumer() throws Exception {
     
     @Test
     public void testVantiqConsumerOutputJson() throws Exception {
+        performConsumerOutputJsonTest(vantiqJsonSenderUri,false);
+    }
+    
+    @Test
+    public void testVantiqConsumerOutputJsonStreamed() throws Exception {
+        performConsumerOutputJsonTest(vantiqJsonSenderStreamUri,true);
+    }
+    
+    void performConsumerOutputJsonTest(String consumerEndpoint, boolean streamRequired) throws Exception {
         
         // First, grab our test environment.
         FauxVantiqComponent vc = (FauxVantiqComponent) context.getComponent("vantiq");
         assert vc != null;
         // Note that we need to fetch the endpoints by URI since there are more than one of them.
-        FauxVantiqEndpoint endp = (FauxVantiqEndpoint) context.getEndpoint(vantiqJsonSenderUri);
+        FauxVantiqEndpoint endp = (FauxVantiqEndpoint) context.getEndpoint(consumerEndpoint);
         
         assert endp.myClient.getListener() instanceof TestListener;
         TestListener tl = (TestListener) endp.myClient.getListener();
@@ -466,38 +638,19 @@ public void testVantiqConsumerOutputJson() throws Exception {
         AtomicInteger msgsReceived = new AtomicInteger();
         exchanges.forEach(exchange -> {
             Object exchangeBody = exchange.getIn().getBody();
-            assertNotNull("Null exchange body",  exchangeBody);
-            // Verify that we got JSON -- which will manifest here as a String, but we'll verify that we can convert it.
-            assertTrue("Vantiq Consumer Output wrong type: " + exchangeBody.getClass().getName(),
-                exchangeBody instanceof BaseJsonNode);
+            Object msg = deserializeJsonBody(exchangeBody, mapper, streamRequired);
+
             msgsReceived.addAndGet(1);
-            Object msg;
-            try {
-                msg = mapper.convertValue(exchangeBody, Map.class); // readValue((ObjectNode) exchangeBody, Map.class);
-            } catch (Exception trySomethingNew) {
-                try {
-                    msg = mapper.convertValue(exchangeBody, String.class);
-                } catch (Exception tryAgain) {
-                    try {
-                        msg = mapper.convertValue(exchangeBody, List.class);
-                    } catch (Exception e) {
-                        log.error("Trapped exception: ", e);
-                        msg = e;
-                        assertNull("Trapped exception: " + e.getMessage(), e);
-                        // about it
-                    }
-                }
-            }
-            assertNotNull("Deserialized msg is null", msg);
+             assertNotNull("Deserialized msg is null", msg);
             if (msg instanceof Map) {
                 assert ((Map<?,?>) msg).containsKey(TEST_MSG_KEY);
                 assert ((Map<?,?>) msg).get(TEST_MSG_KEY) instanceof String;
                 assert ((String) ((Map<?,?>) msg).get(TEST_MSG_KEY)).startsWith(TEST_MSG_PREAMBLE);
                 uniqueMsgs.add(((String) ((Map<?,?>) msg).get(TEST_MSG_KEY)));
             } else if (msg instanceof String) {
-                assertEquals(msg, extraTestMsgs.get(0));
+                assertEquals(extraTestMsgs.get(0), msg);
             } else {
-                assertEquals(msg, extraTestMsgs.get(1));
+                assertEquals(extraTestMsgs.get(1), msg);
             }
         });
         assert uniqueMsgs.size() == mapMsgCount;
@@ -506,12 +659,21 @@ public void testVantiqConsumerOutputJson() throws Exception {
     
     @Test
     public void testVantiqConsumerOutputJsonStructured() throws Exception {
+        performConsumerOutputJsonStructuredTest(vantiqStructuredJsonSenderUri, false);
+    }
+    
+    @Test
+    public void testVantiqConsumerOutputJsonStructuredStreamed() throws Exception {
+        performConsumerOutputJsonStructuredTest(vantiqStructuredJsonSenderStreamUri, true);
+    }
+    
+    void performConsumerOutputJsonStructuredTest(String consumerEndpoint, boolean streamRequired) throws Exception {
         
         // First, grab our test environment.
         FauxVantiqComponent vc = (FauxVantiqComponent) context.getComponent("vantiq");
         assert vc != null;
         // Note that we need to fetch the endpoints by URI since there are more than one of them.
-        FauxVantiqEndpoint endp = (FauxVantiqEndpoint) context.getEndpoint(vantiqStructuredJsonSenderUri);
+        FauxVantiqEndpoint endp = (FauxVantiqEndpoint) context.getEndpoint(consumerEndpoint);
         
         assert endp.myClient.getListener() instanceof TestListener;
         TestListener tl = (TestListener) endp.myClient.getListener();
@@ -569,27 +731,114 @@ public void testVantiqConsumerOutputJsonStructured() throws Exception {
         exchanges.forEach(exchange -> {
             Object exchangeBody = exchange.getIn().getBody();
             Map<String, Object> exchangeHdrs = exchange.getIn().getHeaders();
-            assertNotNull("Null exchange body",  exchangeBody);
-            // Verify that we got JSON -- which will manifest here as a String, but we'll verify that we can convert it.
-            assertTrue("Vantiq Consumer Output wrong type: " + exchangeBody.getClass().getName(),
-                       exchangeBody instanceof BaseJsonNode);
+            Object msg = deserializeJsonBody(exchangeBody, mapper, streamRequired);
             msgsReceived.addAndGet(1);
-            Object msg;
-            try {
-                msg = mapper.convertValue(exchangeBody, Map.class); //readValue((String) exchangeBody, Map.class);
-            } catch (Exception e) {
-                // Maybe we asked for the wrong type
-                try {
-                    msg = mapper.convertValue(exchangeBody, String.class);
-                } catch (Exception ex) {
-                    try {
-                        msg = mapper.convertValue(exchangeBody, List.class);
-                        // readValue((String) exchangeBody,List.class);
-                    } catch (Exception exc) {
-                        throw new RuntimeException(exc);
-                    }
-                }
+            
+            if (msg instanceof Map) {
+                assert ((Map<?,?>) msg).containsKey(TEST_MSG_KEY);
+                assert ((Map<?,?>) msg).get(TEST_MSG_KEY) instanceof String;
+                assert ((String) ((Map<?,?>) msg).get(TEST_MSG_KEY)).startsWith(TEST_MSG_PREAMBLE);
+                assert exchangeHdrs.get("theMessage") instanceof Map;
+                assert (((Map<?,?>) msg).get(TEST_MSG_KEY)).equals(
+                        ((Map<?,?>) exchangeHdrs.get("theMessage")).get(TEST_MSG_KEY));
+                assert exchangeHdrs.get("counter") instanceof Number;
+                uniqueMsgs.add(((String) ((Map<?,?>) msg).get(TEST_MSG_KEY)));
+            } else if (msg instanceof String) {
+                assertEquals(msg, extraTestMsgs.get(0));
+                assert exchangeHdrs.get("theMessage") instanceof String;
+                assert msg.equals(exchangeHdrs.get("theMessage"));
+                assert exchangeHdrs.get("counter") instanceof Number;
+                assert exchangeHdrs.get("isExtra") instanceof Boolean;
+                assert (Boolean) exchangeHdrs.get("isExtra");
+            } else {
+                assert msg.equals(exchangeHdrs.get("theMessage"));
+                assert exchangeHdrs.get("theMessage").getClass().equals(msg.getClass());
+                assert exchangeHdrs.get("counter") instanceof Number;
+                assert exchangeHdrs.get("isExtra") instanceof Boolean;
+                assert (Boolean) exchangeHdrs.get("isExtra");
+            }
+        });
+        assert uniqueMsgs.size() == mapMsgCount;
+        assert msgsReceived.get() == mapMsgCount + extraTestMsgs.size();
+    }
+    
+    @Test
+    public void testVantiqConsumerHeaderMapping() throws Exception {
+        performConsumerOutputJsonStructuredHeaderTest(vantiqStructuredJsonSenderMappedUri,false);
+    }
+    
+    @Test
+    public void testVantiqConsumerHeaderMappingStreamed() throws Exception {
+        performConsumerOutputJsonStructuredHeaderTest(vantiqStructuredJsonSenderMappedStreamUri,true);
+    }
+    void performConsumerOutputJsonStructuredHeaderTest(String consumerEndpoint, boolean streamRequired) throws Exception {
+        
+        // First, grab our test environment.
+        FauxVantiqComponent vc = (FauxVantiqComponent) context.getComponent("vantiq");
+        assert vc != null;
+        // Note that we need to fetch the endpoints by URI since there are more than one of them.
+        FauxVantiqEndpoint endp = (FauxVantiqEndpoint) context.getEndpoint(consumerEndpoint);
+        
+        assert endp.myClient.getListener() instanceof TestListener;
+        TestListener tl = (TestListener) endp.myClient.getListener();
+        
+        int mapMsgCount = 10;
+        List<Object> extraTestMsgs = List.of("I am a test string", List.of(Map.of("test", "message", "another", "test" +
+                " message")));
+        
+        MockEndpoint mocked = getMockEndpoint(routeEndUri);
+        mocked.expectedMinimumMessageCount(mapMsgCount + extraTestMsgs.size());
+        VantiqEndpoint ve = getMandatoryEndpoint(vantiqEndpointUri, VantiqEndpoint.class);
+        assert ve != null;
+        ObjectMapper mapper = new ObjectMapper();
+        WebSocket ws = new FalseWebSocket();
+        for (int i = 0; i < mapMsgCount + extraTestMsgs.size(); i++) {
+            ExtensionServiceMessage ep = new ExtensionServiceMessage(vantiqEndpointUri);
+            ep.op = ExtensionServiceMessage.OP_PUBLISH;
+            ep.resourceName = "SOURCES";
+            ep.resourceId = testSourceName;
+            if (i < mapMsgCount) {
+                HashMap<String, Object> msg = new HashMap<>();
+                msg.put(TEST_MSG_KEY, TEST_MSG_PREAMBLE + i);
+                HashMap<String, Object> smsg = new HashMap<>();
+                smsg.put(STRUCTURED_MESSAGE_MESSAGE_PROPERTY, msg);
+                HashMap<String, Object> hdrs = new HashMap<>();
+                hdrs.put("theMessage", msg);
+                hdrs.put("counter", i);
+                smsg.put(STRUCTURED_MESSAGE_HEADERS_PROPERTY, hdrs);
+                ep.object = smsg;
+            } else {
+                HashMap<String, Object> smsg = new HashMap<>();
+                HashMap<String, Object> hdrs = new HashMap<>();
+                hdrs.put("theMessage", extraTestMsgs.get(i - mapMsgCount));
+                hdrs.put("counter", i);
+                hdrs.put("isExtra", true);
+                hdrs.put("header1", "header1Value");
+                hdrs.put("header2", "header2Value");
+                smsg.put(STRUCTURED_MESSAGE_HEADERS_PROPERTY, hdrs);
+                smsg.put(STRUCTURED_MESSAGE_MESSAGE_PROPERTY, extraTestMsgs.get(i - mapMsgCount));
+                ep.object = smsg;
             }
+            
+            byte[] msgBytes = mapper.writeValueAsBytes(ep);
+            
+            // Given the message bytes, simulate delivery of our WebSocket message
+            tl.onMessage(ws, new ByteString(msgBytes));
+        }
+        
+        // Consumers run in BG threads, so wait a bit for those to finish.
+        mocked.await(5L, TimeUnit.SECONDS);
+        
+        assertEquals("Mocked service expected vs. actual", mapMsgCount + extraTestMsgs.size(),
+                     mocked.getReceivedCounter());
+        List<Exchange> exchanges = mocked.getReceivedExchanges();
+        Set<String> uniqueMsgs = new HashSet<>();
+        AtomicInteger msgsReceived = new AtomicInteger();
+        exchanges.forEach(exchange -> {
+            Object exchangeBody = exchange.getIn().getBody();
+            Object msg = deserializeJsonBody(exchangeBody, mapper, streamRequired);
+            Map<String, Object> exchangeHdrs = exchange.getIn().getHeaders();
+            msgsReceived.addAndGet(1);
             assertNotNull("Deserialized msg is null", msg);
             if (msg instanceof Map) {
                 assert ((Map<?,?>) msg).containsKey(TEST_MSG_KEY);
@@ -607,12 +856,25 @@ public void testVantiqConsumerOutputJsonStructured() throws Exception {
                 assert exchangeHdrs.get("counter") instanceof Number;
                 assert exchangeHdrs.get("isExtra") instanceof Boolean;
                 assert (Boolean) exchangeHdrs.get("isExtra");
+                assert exchangeHdrs.get("header1") instanceof String;
+                assert exchangeHdrs.get("dupHeader1") instanceof String;
+                assert exchangeHdrs.get("header1").equals(exchangeHdrs.get("dupHeader1"));
+                assert exchangeHdrs.get("header2") instanceof String;
+                assert exchangeHdrs.get("dupHeader2") instanceof String;
+                assert exchangeHdrs.get("header2").equals(exchangeHdrs.get("dupHeader2"));
             } else {
                 assert msg.equals(exchangeHdrs.get("theMessage"));
                 assert exchangeHdrs.get("theMessage").getClass().equals(msg.getClass());
                 assert exchangeHdrs.get("counter") instanceof Number;
                 assert exchangeHdrs.get("isExtra") instanceof Boolean;
                 assert (Boolean) exchangeHdrs.get("isExtra");
+                assert exchangeHdrs.get("header1") instanceof String;
+                assert exchangeHdrs.get("dupHeader1") instanceof String;
+                assert exchangeHdrs.get("header1").equals(exchangeHdrs.get("dupHeader1"));
+                assert exchangeHdrs.get("header2") instanceof String;
+                assert exchangeHdrs.get("dupHeader2") instanceof String;
+                assert exchangeHdrs.get("header2").equals(exchangeHdrs.get("dupHeader2"));
+    
             }
         });
         assert uniqueMsgs.size() == mapMsgCount;
@@ -697,12 +959,20 @@ public void testVantiqConsumerQuery() throws Exception {
     
     @Test
     public void testVantiqConsumerOutputJsonQuery() throws Exception {
+        performConsumerOutputJsonQueryTest(vantiqJsonQuerierUri,false);
+    }
+    
+    @Test
+    public void testVantiqConsumerOutputJsonQueryStreamed() throws Exception {
+        performConsumerOutputJsonQueryTest(vantiqJsonQuerierStreamUri,true);
+    }
+    void performConsumerOutputJsonQueryTest(String consumerEndpoint, boolean streamRequired) throws Exception {
         
         // First, grab our test environment.
         FauxVantiqComponent vc = (FauxVantiqComponent) context.getComponent("vantiq");
         assert vc != null;
         // Note that we need to fetch the endpoints by URI since there are more than one of them.
-        FauxVantiqEndpoint endp = (FauxVantiqEndpoint) context.getEndpoint(vantiqJsonQuerierUri);
+        FauxVantiqEndpoint endp = (FauxVantiqEndpoint) context.getEndpoint(consumerEndpoint);
     
         assertNotNull("Endpoint's client is null", endp.myClient.getListener());
         assertTrue("Endpoint's client is not a TestListener: " +
@@ -746,15 +1016,9 @@ public void testVantiqConsumerOutputJsonQuery() throws Exception {
         Set<String> uniqueMsgs = new HashSet<>();
         exchanges.forEach(exchange -> {
             Object exchBody = exchange.getIn().getBody();
-            assertNotNull("Exchange boddy is null", exchBody);
-            assertTrue("Exchange body wrong type: " + exchBody.getClass().getName(),
-                       exchange.getIn().getBody() instanceof ObjectNode);
-            Map<?,?> msg;
-            try {
-                msg = mapper.convertValue(exchBody, Map.class);
-            } catch (Exception e) {
-                throw new RuntimeException(e);
-            }
+            Object mo = deserializeJsonBody(exchBody, mapper, streamRequired);
+            assert mo instanceof Map;
+            Map<?,?> msg = (Map<?, ?>) mo;
             assertNotNull("Deserialized JSON message is null", msg);
             assert msg.containsKey(TEST_MSG_KEY);
             assert msg.get(TEST_MSG_KEY) instanceof String;
@@ -782,12 +1046,92 @@ public void testVantiqConsumerOutputJsonQuery() throws Exception {
         String ra = rsp.getHeader(ExtensionServiceMessage.RESPONSE_ADDRESS_HEADER);
         assert responseAddresses.contains(ra);
     }
+    
+    protected Object deserializeJsonBody(Object exchangeBody, ObjectMapper mapper, boolean streamExpected) {
+        assertNotNull("Null exchange body",  exchangeBody);
+        // Verify that we got JSON -- which will manifest here as a String, but we'll verify that we can convert it.
+        assertTrue("Vantiq Consumer Output wrong type: " + exchangeBody.getClass().getName(),
+                   exchangeBody instanceof BaseJsonNode || exchangeBody instanceof StreamCache);
+        if (exchangeBody instanceof StreamCache) {
+            assert streamExpected;
+            ((StreamCache) exchangeBody).reset();
+            byte[] ba = null;
+            if (exchangeBody instanceof InputStreamCache) {
+                ba = ((InputStreamCache) exchangeBody).readAllBytes();
+            } else if (exchangeBody instanceof ByteArrayInputStreamCache) {
+                try {
+                    ba = ((ByteArrayInputStreamCache) exchangeBody).readAllBytes();
+                } catch (IOException e) {
+                    fail("Trapped exception reading StreamCache: " + e.getMessage());
+                }
+            } else {
+                fail("Unexpected StreanCache type: " + exchangeBody.getClass().getName());
+            }
+            exchangeBody = ba;
+            try {
+                ba = Base64.getDecoder().decode(ba);
+                exchangeBody = new String(ba, StandardCharsets.UTF_8);
+            } catch (IllegalArgumentException nope) {
+                // Guess it's not encoded
+            }
+        } else {
+            assert !streamExpected;
+        }
+        Object msg = null;
+        if (exchangeBody instanceof byte[]) {
+            try {
+                msg = mapper.readTree((byte[]) exchangeBody);
+                if (msg instanceof TextNode) {
+                    msg = ((TextNode) msg).asText();
+                } else if (msg instanceof ContainerNode) {
+                    msg = mapper.convertValue(msg, Object.class);
+                    log.debug("Decoded ObjectNode into {}: {}", msg.getClass().getName(), msg);
+                }
+            } catch (IOException e) {
+                // Ignore -- we'll sort it out downstream
+            }
+        }
+        if (msg == null) {
+            try {
+                msg = mapper.convertValue(exchangeBody, Map.class);
+            } catch (Exception trySomethingNew) {
+                try {
+                    msg = mapper.convertValue(exchangeBody, String.class);
+                } catch (Exception tryAgain) {
+                    try {
+                        msg = mapper.convertValue(exchangeBody, List.class);
+                    } catch (Exception e) {
+                        log.error("Trapped exception: ", e);
+                        msg = e;
+                        assertNull("Trapped exception: " + e.getMessage(), e);
+                    }
+                }
+            }
+        }
+        return msg;
+    }
 
     @Override
     protected RouteBuilder createRouteBuilder() {
         return new RouteBuilder() {
             public void configure() {
-                
+                // since everything starts here, we need to instantiate this bean before startup so here as things
+                // are defined.
+                HeaderDuplicationBean hdBean = new HeaderDuplicationBean();
+                Map<String, String> heMap = Map.of("header1", "dupHeader1",
+                                                   "header2", "dupHeader2");
+                hdBean.setHeaderDuplicationMap(heMap);
+                context.getRegistry().bind(TEST_HEADER_BEAN_NAME, hdBean);
+                HeaderDuplicationBean testHdBean =
+                        context.getRegistry().lookupByNameAndType(TEST_HEADER_BEAN_NAME, HeaderDuplicationBean.class);
+                assertNotNull(testHdBean);
+                Map<String, String> testHeMap = testHdBean.getHeaderDuplicationMap();
+                assertNotNull(testHeMap);
+                assertEquals(heMap.size(), testHeMap.size());
+    
+                heMap.forEach( (k, v) -> {
+                    assertEquals(heMap.get(k), testHeMap.get(k));
+                });
                 // Override the component type to be used...
                 context.addComponent("vantiq", new FauxVantiqComponent());
                 for (String name: context.getComponentNames()) {
@@ -800,30 +1144,53 @@ public void configure() {
                 
                 from(routeStartUri)
                   .to(vantiqEndpointUri);
-                
+
                 from(routeStartStructuredUri)
                         .to(vantiqEndpointStructuredUri);
-                
+
                 from(vantiqSenderUri)
                         .to(routeEndUri);
-    
+
                 from(vantiqJsonSenderUri)
                         .to(routeEndUri);
     
+                from(vantiqJsonSenderStreamUri)
+                        .to(routeEndUri);
+    
+    
                 from(vantiqStructuredJsonSenderUri)
                         .to(routeEndUri);
     
+                from(vantiqStructuredJsonSenderStreamUri)
+                        .to(routeEndUri);
+    
+                from(vantiqStructuredJsonSenderMappedUri)
+                        .to(routeEndUri);
+    
+                from(vantiqStructuredJsonSenderMappedStreamUri)
+                        .to(routeEndUri);
+    
+    
+                from(routeStartStructuredHeaderMapUri)
+                        .to(vantiqEndpointStructuredHeaderMapUri);
+                
                 from(vantiqQuerySenderUri)
                         .setExchangePattern(ExchangePattern.InOut)
                         .to(routeEndUri)
                             .setBody(constant("{ \"Response\": \"Message\"}"))
                         .to(vantiqQuerySenderUri);
-    
+
                 from(vantiqJsonQuerierUri)
                         .setExchangePattern(ExchangePattern.InOut)
                         .to(routeEndUri)
                         .setBody(constant("{ \"Response\": \"Message\"}"))
                         .to(vantiqJsonQuerierUri);
+    
+                from(vantiqJsonQuerierStreamUri)
+                        .setExchangePattern(ExchangePattern.InOut)
+                        .to(routeEndUri)
+                        .setBody(constant("{ \"Response\": \"Message\"}"))
+                        .to(vantiqJsonQuerierUri);
             }
         };
     }
diff --git a/camelComponent/src/test/resources/log4j2.properties b/camelComponent/src/test/resources/log4j2.properties
index 53aa0207..76041fc8 100644
--- a/camelComponent/src/test/resources/log4j2.properties
+++ b/camelComponent/src/test/resources/log4j2.properties
@@ -8,5 +8,5 @@ rootLogger.appenderRef.out.ref = out
 
 logger.vantiqcamel.name = io.vantiq.extsrc.camel
 logger.vantiqcamel.level = DEBUG
-logger.vantiqcamel.appenderRef = Console
+logger.vantiqcamel.appenderRef = out
 
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 3b0b38ab..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,55 +39,79 @@
 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;
+import java.util.Optional;
 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, Properties props);
+        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) {
-        assumeTrue(!sfLoginUrl.equals(MISSING_VALUE) && !sfClientId.equals(MISSING_VALUE) &&
-                           !sfClientSecret.equals(MISSING_VALUE) && !sfRefreshToken.equals(MISSING_VALUE));
-        
+        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);
@@ -99,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);
@@ -107,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());
@@ -122,7 +172,7 @@ void performConfigTest(String appName, String route, String routeFormat,
         assertTrue("Context Running", runnerContext.isStarted());
 
         try {
-            vfy.doVerify(runnerContext, handler.getCurrentCamelRunner().getCamelProperties());
+            vfy.doVerify(runnerContext);
         } finally {
             handler.getCurrentCamelRunner().close();
         }
@@ -154,14 +204,15 @@ 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, Properties lclProps) -> {
-                                assert lclProps != null;
-                                assert propValues.size() == lclProps.size();
-                                propValues.forEach( (name, val) -> {
-                                    assert lclProps.containsKey(name);
-                                    assert lclProps.getProperty(name).equals(val);
+                          (CamelContext runnerContext) -> {
+                              PropertiesComponent pc = runnerContext.getPropertiesComponent();
+                              propValues.forEach( (name, val) -> {
+                                  Optional<String> os = pc.resolveProperty("{{" + name + "}}");
+                                  assert os.isPresent();
+                                  String s = os.get();
+                                  assert s.equals(val);
                               });
                           });
     }
@@ -180,18 +231,18 @@ 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, Properties lclProps) -> {
-                              assert lclProps != null;
-                              assert propValues.size() == lclProps.size();
+                          (CamelContext runnerContext) -> {
+                              PropertiesComponent pc = runnerContext.getPropertiesComponent();
                               propValues.forEach( (name, val) -> {
-                                  assert lclProps.containsKey(name);
+                                  Optional<String> os = pc.resolveProperty("{{" + name + "}}");
+                                  assert os.isPresent();
+                                  String s = os.get();
                                   if (unchanged.contains(name)) {
-                                      assert lclProps.getProperty(name).equals(val);
+                                      assert s.equals(val);
                                   } else {
-                                      assert lclProps.getProperty(name).equals(CamelHandleConfiguration.RAW_START +
-                                                              val + CamelHandleConfiguration.RAW_END);
+                                      assert s.equals(RAW_START + val + RAW_END);
                                   }
                               });
                           }, raws);
@@ -208,21 +259,22 @@ 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, Properties lclProps) -> {
-                              assert lclProps != null;
-                              assert propValues.size() == lclProps.size();
+                          (CamelContext runnerContext) -> {
+                              PropertiesComponent pc = runnerContext.getPropertiesComponent();
                               propValues.forEach( (name, val) -> {
-                                  assert lclProps.containsKey(name);
+                                  Optional<String> os = pc.resolveProperty("{{" + name + "}}");
+                                  assert os.isPresent();
+                                  String s = os.get();
                                   if (raws.contains(name)) {
-                                      assert lclProps.getProperty(name).equals(CamelHandleConfiguration.RAW_START +
-                                              val + CamelHandleConfiguration.RAW_END);
+                                      assert s.equals(
+                                          CamelHandleConfiguration.RAW_START + val + CamelHandleConfiguration.RAW_END);
                                   } else {
-                                      assert lclProps.getProperty(name).equals(val);
+                                      assert s.equals(val);
                                   }
                               });
-                          }, raws);
+                         }, raws);
     }
     
     @Test
@@ -236,21 +288,20 @@ 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, Properties lclProps) -> {
-                              assert lclProps != null;
-                              assert propValues.size() == lclProps.size();
+                          (CamelContext runnerContext) -> {
+                              PropertiesComponent pc = runnerContext.getPropertiesComponent();
                               propValues.forEach( (name, val) -> {
-                                  assert lclProps.containsKey(name);
+                                  Optional<String> os = pc.resolveProperty("{{" + name + "}}");
+                                  assert os.isPresent();
+                                  String s = os.get();
                                   if (name.equals("prop1")) {
-                                      assert lclProps.getProperty(name).equals(CamelHandleConfiguration.RAW_START_ALT +
-                                                                                       val + CamelHandleConfiguration.RAW_END_ALT);
+                                      assert s.equals(RAW_START_ALT + val + RAW_END_ALT);
                                   } else if (name.equals("prop3")) {
-                                      assert lclProps.getProperty(name).equals(CamelHandleConfiguration.RAW_START +
-                                                                                       val + CamelHandleConfiguration.RAW_END);
+                                      assert s.equals(RAW_START + val + RAW_END);
                                   } else {
-                                      assert lclProps.getProperty(name).equals(val);
+                                      assert s.equals(val);
                                   }
                               });
                           }, raws);
@@ -266,17 +317,20 @@ 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, Properties lclProps) -> {
-                              assert lclProps != null;
-                              assert propValues.size() == lclProps.size();
+                          (CamelContext runnerContext) -> {
+                              PropertiesComponent pc = runnerContext.getPropertiesComponent();
                               propValues.forEach( (name, val) -> {
-                                  assert lclProps.containsKey(name);
+                                  Optional<String> os = pc.resolveProperty("{{" + name + "}}");
+                                  assert os.isPresent();
+                                  String s = os.get();
                                   if (raws.contains(name)) {
                                       String newVal = ((String) propValues.get(name))
                                               .substring(NO_RAW_REQUEST.length());
-                                      assert lclProps.getProperty(name).equals(newVal);
+                                      assert newVal.equals(s);
+                                  } else {
+                                      assert s.equals(val);
                                   }
                               });
                           }, raws);
@@ -293,17 +347,18 @@ 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, Properties lclProps) -> {
-                              assert lclProps != null;
-                              assert propValues.size() == lclProps.size();
+                          (CamelContext runnerContext) -> {
+                              PropertiesComponent pc = runnerContext.getPropertiesComponent();
                               propValues.forEach( (name, val) -> {
-                                  assert lclProps.containsKey(name);
+                                  Optional<String> os = pc.resolveProperty("{{" + name + "}}");
+                                  assert os.isPresent();
+                                  String s = os.get();
                                   if (name.equals("prop1")) {
-                                      assert lclProps.getProperty(name).equals(RAW_START + val + RAW_END);
+                                      assert s.equals(RAW_START + val + RAW_END);
                                   } else {
-                                      assert lclProps.getProperty(name).equals(val);
+                                      assert s.equals(val);
                                   }
                               });
                           }, raws);
@@ -311,9 +366,9 @@ public void testCannotWrap() {
     }
     @Test
     public void testSimpleConfiguration() {
-        performConfigTest(name.getMethodName(), XML_ROUTE, "xml",
+        performConfigTest(testName, XML_ROUTE, "xml",
                           null, null,
-                          (CamelContext runnerContext, Properties props) -> {
+                          (CamelContext runnerContext) -> {
                               TriFunction<CamelContext, String, Object, Boolean> verifyOperation =
                                       defineVerifyOperation();
                               assert verifyOperation.apply(runnerContext, QUERY_MONKEY, RESPONSE_MONKEY);
@@ -323,9 +378,11 @@ public void testSimpleConfiguration() {
     
     @Test
     public void testComponentInitConfiguration() {
-        performConfigTest(name.getMethodName(), SALESFORCETASKS_YAML, "yaml",
+        assumeTrue(!sfLoginUrl.equals(MISSING_VALUE) && !sfClientId.equals(MISSING_VALUE) &&
+                           !sfClientSecret.equals(MISSING_VALUE) && !sfRefreshToken.equals(MISSING_VALUE));
+        performConfigTest(testName, SALESFORCETASKS_YAML, "yaml",
                           getComponentsToInit(), null,
-                          (CamelContext runnerContext, Properties props) -> {
+                          (CamelContext runnerContext) -> {
                               TriFunction<CamelContext, String, Object, Boolean> verifyOperation =
                                       defineVerifyOperation();
                             assert verifyOperation.apply(runnerContext, "not used", Map.of("done", true));
@@ -335,13 +392,16 @@ public void testComponentInitConfiguration() {
     
     @Test
     public void testComponentInitConfigurationWithPropertyValues() {
+        assumeTrue(!sfLoginUrl.equals(MISSING_VALUE) && !sfClientId.equals(MISSING_VALUE) &&
+                           !sfClientSecret.equals(MISSING_VALUE) && !sfRefreshToken.equals(MISSING_VALUE));
+    
         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",
+        performConfigTest(testName, PARAMETERIZED_SALESFORCE_ROUTE, "yaml",
                           getComponentsToInit(), props,
-                          (CamelContext runnerContext, Properties pros) -> {
+                          (CamelContext runnerContext) -> {
                               TriFunction<CamelContext, String, Object, Boolean> verifyOperation =
                                       defineVerifyOperation();
                               assert verifyOperation.apply(runnerContext, "not used", Map.of("done", true));
@@ -349,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);
@@ -400,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/VantiqComponentDiscoveryTest.java b/camelConnector/src/test/java/io/vantiq/extsrc/camelconn/discover/VantiqComponentDiscoveryTest.java
index b02ca54a..03cffd57 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
@@ -316,8 +316,8 @@ void performDiscoveryTest(RouteBuilder rb, Properties propertyValues) throws Exc
      */
     interface TestExpectations {
         List<String> getExpectedComponentsToLoad();
-        List<String> getExpectedSystemComponents();
         
+        default List<String> getExpectedSystemComponents() { return List.of(); }
         default List<String> getExpectedDataFormatsToLoad() {
             return List.of();
         }
@@ -556,9 +556,11 @@ public Properties getRequiredProperties() {
     private static class BeanIncludingRoutes extends RouteBuilder implements TestExpectations {
     
         CamelContext ctx;
+    
         BeanIncludingRoutes(CamelContext ctx) {
             this.ctx = ctx;
         }
+    
         @Override
         public List<String> getExpectedComponentsToLoad() {
             return List.of("azure-eventhubs");
@@ -578,33 +580,33 @@ public List<String> getExpectedDataFormatsToLoad() {
         @Override
         public void configure() throws Exception {
             String content = ""
-            + "- route: \n"
-            + "    id: \"EventHub Sink\" \n"
-            + "    from: \n"
-            + "        uri: \"vantiq://server.config\" \n"
-            + "        steps: \n"
-            + "        - choice: \n"
-            + "            when: \n"
-            + "            - simple: \"${header[partition-id]}\" \n"
-            + "              steps: \n"
-            + "              - set-header: \n"
-            + "                  name: CamelAzureEventHubsPartitionId \n"
-            + "                  simple: \"${header[partition-id]}\" \n"
-            + "            - simple: \"${header[ce-partition-id]}\" \n"
-            + "              steps: \n"
-            + "              - set-header: \n"
-            + "                  name: CamelAzureEventHubsPartitionId \n"
-            + "                  simple: \"${header[ce-partition-id]}\" \n"
-            + "        - setBody: \n"
-            + "            simple: \"${body.message}\" \n"
-            + "        - marshal: \n"
-            + "            json: \n"
-            + "              library: jackson \n"
-            + "        - to: \n"
-            + "             uri: \"azure-eventhubs://vantiq-test/vantiqNotReallyThere\" \n"
-            + "             parameters: \n"
-            + "                 sharedAccessName: \"someRandomKey\" \n"
-            + "                 sharedAccessKey: \"RAW(MY TOKEN)\" \n";
+                    + "- route: \n"
+                    + "    id: \"EventHub Sink\" \n"
+                    + "    from: \n"
+                    + "        uri: \"vantiq://server.config\" \n"
+                    + "        steps: \n"
+                    + "        - choice: \n"
+                    + "            when: \n"
+                    + "            - simple: \"${header[partition-id]}\" \n"
+                    + "              steps: \n"
+                    + "              - set-header: \n"
+                    + "                  name: CamelAzureEventHubsPartitionId \n"
+                    + "                  simple: \"${header[partition-id]}\" \n"
+                    + "            - simple: \"${header[ce-partition-id]}\" \n"
+                    + "              steps: \n"
+                    + "              - set-header: \n"
+                    + "                  name: CamelAzureEventHubsPartitionId \n"
+                    + "                  simple: \"${header[ce-partition-id]}\" \n"
+                    + "        - setBody: \n"
+                    + "            simple: \"${body.message}\" \n"
+                    + "        - marshal: \n"
+                    + "            json: \n"
+                    + "              library: jackson \n"
+                    + "        - to: \n"
+                    + "             uri: \"azure-eventhubs://vantiq-test/vantiqNotReallyThere\" \n"
+                    + "             parameters: \n"
+                    + "                 sharedAccessName: \"someRandomKey\" \n"
+                    + "                 sharedAccessKey: \"RAW(MY TOKEN)\" \n";
     
             // YAML support needs a camel context.  So provide one during setup...
             RouteBuilder rb = new YamlRouteBuilder(ctx, content).getRouteBuilder();
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 00c4875e..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,21 +8,21 @@
 
 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;
 import org.apache.camel.model.dataformat.AvroLibrary;
 import org.apache.camel.model.dataformat.JsonLibrary;
 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;
@@ -43,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
@@ -54,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();
     
@@ -139,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());
@@ -156,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;");
@@ -175,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");
@@ -193,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");
@@ -227,39 +231,38 @@ 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);
         }
     }
     
-    
     @Test
     public void testStartRouteLoadedComponentsMultiRepo() throws Exception {
         FileUtil.forceDelete(cache);    // Clear the cache
@@ -270,11 +273,102 @@ 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() {
+        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.
+        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"
+        );
+        try (CamelRunner runner = new CamelRunner(this.getTestMethodName(),rb, List.of(),
+                                                  IVY_CACHE_PATH, DEST_PATH, rb.getComponentsToInit(),
+                                                   null, null, null)) {
+            runner.setAdditionalLibraries(discoveredDependencies);
+            runner.createCamelContext();
+            runner.loadRouteFromText(rb.content, "yaml");
+            // We only need to test loading the route -- we needn't run this route (no context, etc.)
+        } catch (Exception e) {
+            fail("Trapped exception during test: " + e.getMessage() +
+                         (e.getCause() != null ? e.getCause().getMessage() : ""));
+        }
+    }
+    
+    @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 "
@@ -320,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
@@ -336,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);
@@ -478,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>
      */
@@ -517,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<>();
@@ -576,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();
@@ -613,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();
@@ -813,4 +906,98 @@ public void configure() {
                     .to("mock:result");
         }
     }
+    
+    private static class BeanIncludingRouteTemplate extends RouteBuilderWithProps {
+        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://server.config?structuredMessageHeader=true";
+
+        @Override
+        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() {
+        
+        }
+    }
 }
diff --git a/camelConnector/src/test/resources/log4j2.properties b/camelConnector/src/test/resources/log4j2.properties
index 009d277b..c685380f 100644
--- a/camelConnector/src/test/resources/log4j2.properties
+++ b/camelConnector/src/test/resources/log4j2.properties
@@ -8,5 +8,12 @@ rootLogger.appenderRef.out.ref = out
 
 logger.vantiqcamelconn.name = io.vantiq.extsrc.camelconn
 logger.vantiqcamelconn.level = DEBUG
-logger.vantiqcamelconn.appenderRef = Console
+logger.vantiqcamelconn.appenderRef = out
 
+logger.vantiqcamel.name = io.vantiq.extsrc.camel
+logger.vantiqcamel.level = DEBUG
+logger.vantiqcamel.appenderRef = out
+
+logger.cameltrace.name = org.apache.camel.Tracing
+logger.cameltrace.level = DEBUG
+logger.cameltrace.appenderRef = out
\ No newline at end of file