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

Issue:
Add Apache Camel Component



Discussion / hints:

Fix patch:
diff --git a/README.md b/README.md
index fb2d91c7..613aed03 100644
--- a/README.md
+++ b/README.md
@@ -373,6 +373,9 @@ The `settings.gradle` file determines the scope of the build.
 Connectors requiring a driver are included in the overall build *only if* the associated driver location environment variable is present.
 Otherwise, the connector is ignored for the build.
 
+Building the connectors require a Java compiler.  To build everything, you'll need at least Java 11.
+Most connectors are at the Java 8 level, but the Camel Component & Connectors require Java 11.
+
 ## Building Docker Images
 
 The connectors in this repository contain `gradle` tasks that can be used to build Docker Images for each connector and 
diff --git a/camelComponent/LICENSE/LICENSE.txt b/camelComponent/LICENSE/LICENSE.txt
new file mode 100644
index 00000000..461db819
--- /dev/null
+++ b/camelComponent/LICENSE/LICENSE.txt
@@ -0,0 +1,14 @@
+Copyright 2022 Vantiq, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
+documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
+rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
+persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
+Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file
diff --git a/camelComponent/LICENSE/apache-2.txt b/camelComponent/LICENSE/apache-2.txt
new file mode 100644
index 00000000..fd400e62
--- /dev/null
+++ b/camelComponent/LICENSE/apache-2.txt
@@ -0,0 +1,203 @@
+
+
+                                     Apache License
+                               Version 2.0, January 2004
+                            http://www.apache.org/licenses/
+     
+       TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+     
+       1. Definitions.
+     
+          "License" shall mean the terms and conditions for use, reproduction,
+          and distribution as defined by Sections 1 through 9 of this document.
+     
+          "Licensor" shall mean the copyright owner or entity authorized by
+          the copyright owner that is granting the License.
+     
+          "Legal Entity" shall mean the union of the acting entity and all
+          other entities that control, are controlled by, or are under common
+          control with that entity. For the purposes of this definition,
+          "control" means (i) the power, direct or indirect, to cause the
+          direction or management of such entity, whether by contract or
+          otherwise, or (ii) ownership of fifty percent (50%) or more of the
+          outstanding shares, or (iii) beneficial ownership of such entity.
+     
+          "You" (or "Your") shall mean an individual or Legal Entity
+          exercising permissions granted by this License.
+     
+          "Source" form shall mean the preferred form for making modifications,
+          including but not limited to software source code, documentation
+          source, and configuration files.
+     
+          "Object" form shall mean any form resulting from mechanical
+          transformation or translation of a Source form, including but
+          not limited to compiled object code, generated documentation,
+          and conversions to other media types.
+     
+          "Work" shall mean the work of authorship, whether in Source or
+          Object form, made available under the License, as indicated by a
+          copyright notice that is included in or attached to the work
+          (an example is provided in the Appendix below).
+     
+          "Derivative Works" shall mean any work, whether in Source or Object
+          form, that is based on (or derived from) the Work and for which the
+          editorial revisions, annotations, elaborations, or other modifications
+          represent, as a whole, an original work of authorship. For the purposes
+          of this License, Derivative Works shall not include works that remain
+          separable from, or merely link (or bind by name) to the interfaces of,
+          the Work and Derivative Works thereof.
+     
+          "Contribution" shall mean any work of authorship, including
+          the original version of the Work and any modifications or additions
+          to that Work or Derivative Works thereof, that is intentionally
+          submitted to Licensor for inclusion in the Work by the copyright owner
+          or by an individual or Legal Entity authorized to submit on behalf of
+          the copyright owner. For the purposes of this definition, "submitted"
+          means any form of electronic, verbal, or written communication sent
+          to the Licensor or its representatives, including but not limited to
+          communication on electronic mailing lists, source code control systems,
+          and issue tracking systems that are managed by, or on behalf of, the
+          Licensor for the purpose of discussing and improving the Work, but
+          excluding communication that is conspicuously marked or otherwise
+          designated in writing by the copyright owner as "Not a Contribution."
+     
+          "Contributor" shall mean Licensor and any individual or Legal Entity
+          on behalf of whom a Contribution has been received by Licensor and
+          subsequently incorporated within the Work.
+     
+       2. Grant of Copyright License. Subject to the terms and conditions of
+          this License, each Contributor hereby grants to You a perpetual,
+          worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+          copyright license to reproduce, prepare Derivative Works of,
+          publicly display, publicly perform, sublicense, and distribute the
+          Work and such Derivative Works in Source or Object form.
+     
+       3. Grant of Patent License. Subject to the terms and conditions of
+          this License, each Contributor hereby grants to You a perpetual,
+          worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+          (except as stated in this section) patent license to make, have made,
+          use, offer to sell, sell, import, and otherwise transfer the Work,
+          where such license applies only to those patent claims licensable
+          by such Contributor that are necessarily infringed by their
+          Contribution(s) alone or by combination of their Contribution(s)
+          with the Work to which such Contribution(s) was submitted. If You
+          institute patent litigation against any entity (including a
+          cross-claim or counterclaim in a lawsuit) alleging that the Work
+          or a Contribution incorporated within the Work constitutes direct
+          or contributory patent infringement, then any patent licenses
+          granted to You under this License for that Work shall terminate
+          as of the date such litigation is filed.
+     
+       4. Redistribution. You may reproduce and distribute copies of the
+          Work or Derivative Works thereof in any medium, with or without
+          modifications, and in Source or Object form, provided that You
+          meet the following conditions:
+     
+          (a) You must give any other recipients of the Work or
+              Derivative Works a copy of this License; and
+     
+          (b) You must cause any modified files to carry prominent notices
+              stating that You changed the files; and
+     
+          (c) You must retain, in the Source form of any Derivative Works
+              that You distribute, all copyright, patent, trademark, and
+              attribution notices from the Source form of the Work,
+              excluding those notices that do not pertain to any part of
+              the Derivative Works; and
+     
+          (d) If the Work includes a "NOTICE" text file as part of its
+              distribution, then any Derivative Works that You distribute must
+              include a readable copy of the attribution notices contained
+              within such NOTICE file, excluding those notices that do not
+              pertain to any part of the Derivative Works, in at least one
+              of the following places: within a NOTICE text file distributed
+              as part of the Derivative Works; within the Source form or
+              documentation, if provided along with the Derivative Works; or,
+              within a display generated by the Derivative Works, if and
+              wherever such third-party notices normally appear. The contents
+              of the NOTICE file are for informational purposes only and
+              do not modify the License. You may add Your own attribution
+              notices within Derivative Works that You distribute, alongside
+              or as an addendum to the NOTICE text from the Work, provided
+              that such additional attribution notices cannot be construed
+              as modifying the License.
+     
+          You may add Your own copyright statement to Your modifications and
+          may provide additional or different license terms and conditions
+          for use, reproduction, or distribution of Your modifications, or
+          for any such Derivative Works as a whole, provided Your use,
+          reproduction, and distribution of the Work otherwise complies with
+          the conditions stated in this License.
+     
+       5. Submission of Contributions. Unless You explicitly state otherwise,
+          any Contribution intentionally submitted for inclusion in the Work
+          by You to the Licensor shall be under the terms and conditions of
+          this License, without any additional terms or conditions.
+          Notwithstanding the above, nothing herein shall supersede or modify
+          the terms of any separate license agreement you may have executed
+          with Licensor regarding such Contributions.
+     
+       6. Trademarks. This License does not grant permission to use the trade
+          names, trademarks, service marks, or product names of the Licensor,
+          except as required for reasonable and customary use in describing the
+          origin of the Work and reproducing the content of the NOTICE file.
+     
+       7. Disclaimer of Warranty. Unless required by applicable law or
+          agreed to in writing, Licensor provides the Work (and each
+          Contributor provides its Contributions) on an "AS IS" BASIS,
+          WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+          implied, including, without limitation, any warranties or conditions
+          of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+          PARTICULAR PURPOSE. You are solely responsible for determining the
+          appropriateness of using or redistributing the Work and assume any
+          risks associated with Your exercise of permissions under this License.
+     
+       8. Limitation of Liability. In no event and under no legal theory,
+          whether in tort (including negligence), contract, or otherwise,
+          unless required by applicable law (such as deliberate and grossly
+          negligent acts) or agreed to in writing, shall any Contributor be
+          liable to You for damages, including any direct, indirect, special,
+          incidental, or consequential damages of any character arising as a
+          result of this License or out of the use or inability to use the
+          Work (including but not limited to damages for loss of goodwill,
+          work stoppage, computer failure or malfunction, or any and all
+          other commercial damages or losses), even if such Contributor
+          has been advised of the possibility of such damages.
+     
+       9. Accepting Warranty or Additional Liability. While redistributing
+          the Work or Derivative Works thereof, You may choose to offer,
+          and charge a fee for, acceptance of support, warranty, indemnity,
+          or other liability obligations and/or rights consistent with this
+          License. However, in accepting such obligations, You may act only
+          on Your own behalf and on Your sole responsibility, not on behalf
+          of any other Contributor, and only if You agree to indemnify,
+          defend, and hold each Contributor harmless for any liability
+          incurred by, or claims asserted against, such Contributor by reason
+          of your accepting any such warranty or additional liability.
+     
+       END OF TERMS AND CONDITIONS
+     
+       APPENDIX: How to apply the Apache License to your work.
+     
+          To apply the Apache License to your work, attach the following
+          boilerplate notice, with the fields enclosed by brackets "[]"
+          replaced with your own identifying information. (Don't include
+          the brackets!)  The text should be enclosed in the appropriate
+          comment syntax for the file format. We also recommend that a
+          file or class name and description of purpose be included on the
+          same "printed page" as the copyright notice for easier
+          identification within third-party archives.
+     
+       Copyright [yyyy] [name of copyright owner]
+     
+       Licensed under the Apache License, Version 2.0 (the "License");
+       you may not use this file except in compliance with the License.
+       You may obtain a copy of the License at
+     
+           http://www.apache.org/licenses/LICENSE-2.0
+     
+       Unless required by applicable law or agreed to in writing, software
+       distributed under the License is distributed on an "AS IS" BASIS,
+       WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+       See the License for the specific language governing permissions and
+       limitations under the License.
diff --git a/camelComponent/LICENSE/log4j-license.txt b/camelComponent/LICENSE/log4j-license.txt
new file mode 100644
index 00000000..cd89e7d7
--- /dev/null
+++ b/camelComponent/LICENSE/log4j-license.txt
@@ -0,0 +1,203 @@
+
+
+                                     Apache License
+                               Version 2.0, January 2004
+                            http://www.apache.org/licenses/
+     
+       TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+     
+       1. Definitions.
+     
+          "License" shall mean the terms and conditions for use, reproduction,
+          and distribution as defined by Sections 1 through 9 of this document.
+     
+          "Licensor" shall mean the copyright owner or entity authorized by
+          the copyright owner that is granting the License.
+     
+          "Legal Entity" shall mean the union of the acting entity and all
+          other entities that control, are controlled by, or are under common
+          control with that entity. For the purposes of this definition,
+          "control" means (i) the power, direct or indirect, to cause the
+          direction or management of such entity, whether by contract or
+          otherwise, or (ii) ownership of fifty percent (50%) or more of the
+          outstanding shares, or (iii) beneficial ownership of such entity.
+     
+          "You" (or "Your") shall mean an individual or Legal Entity
+          exercising permissions granted by this License.
+     
+          "Source" form shall mean the preferred form for making modifications,
+          including but not limited to software source code, documentation
+          source, and configuration files.
+     
+          "Object" form shall mean any form resulting from mechanical
+          transformation or translation of a Source form, including but
+          not limited to compiled object code, generated documentation,
+          and conversions to other media types.
+     
+          "Work" shall mean the work of authorship, whether in Source or
+          Object form, made available under the License, as indicated by a
+          copyright notice that is included in or attached to the work
+          (an example is provided in the Appendix below).
+     
+          "Derivative Works" shall mean any work, whether in Source or Object
+          form, that is based on (or derived from) the Work and for which the
+          editorial revisions, annotations, elaborations, or other modifications
+          represent, as a whole, an original work of authorship. For the purposes
+          of this License, Derivative Works shall not include works that remain
+          separable from, or merely link (or bind by name) to the interfaces of,
+          the Work and Derivative Works thereof.
+     
+          "Contribution" shall mean any work of authorship, including
+          the original version of the Work and any modifications or additions
+          to that Work or Derivative Works thereof, that is intentionally
+          submitted to Licensor for inclusion in the Work by the copyright owner
+          or by an individual or Legal Entity authorized to submit on behalf of
+          the copyright owner. For the purposes of this definition, "submitted"
+          means any form of electronic, verbal, or written communication sent
+          to the Licensor or its representatives, including but not limited to
+          communication on electronic mailing lists, source code control systems,
+          and issue tracking systems that are managed by, or on behalf of, the
+          Licensor for the purpose of discussing and improving the Work, but
+          excluding communication that is conspicuously marked or otherwise
+          designated in writing by the copyright owner as "Not a Contribution."
+     
+          "Contributor" shall mean Licensor and any individual or Legal Entity
+          on behalf of whom a Contribution has been received by Licensor and
+          subsequently incorporated within the Work.
+     
+       2. Grant of Copyright License. Subject to the terms and conditions of
+          this License, each Contributor hereby grants to You a perpetual,
+          worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+          copyright license to reproduce, prepare Derivative Works of,
+          publicly display, publicly perform, sublicense, and distribute the
+          Work and such Derivative Works in Source or Object form.
+     
+       3. Grant of Patent License. Subject to the terms and conditions of
+          this License, each Contributor hereby grants to You a perpetual,
+          worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+          (except as stated in this section) patent license to make, have made,
+          use, offer to sell, sell, import, and otherwise transfer the Work,
+          where such license applies only to those patent claims licensable
+          by such Contributor that are necessarily infringed by their
+          Contribution(s) alone or by combination of their Contribution(s)
+          with the Work to which such Contribution(s) was submitted. If You
+          institute patent litigation against any entity (including a
+          cross-claim or counterclaim in a lawsuit) alleging that the Work
+          or a Contribution incorporated within the Work constitutes direct
+          or contributory patent infringement, then any patent licenses
+          granted to You under this License for that Work shall terminate
+          as of the date such litigation is filed.
+     
+       4. Redistribution. You may reproduce and distribute copies of the
+          Work or Derivative Works thereof in any medium, with or without
+          modifications, and in Source or Object form, provided that You
+          meet the following conditions:
+     
+          (a) You must give any other recipients of the Work or
+              Derivative Works a copy of this License; and
+     
+          (b) You must cause any modified files to carry prominent notices
+              stating that You changed the files; and
+     
+          (c) You must retain, in the Source form of any Derivative Works
+              that You distribute, all copyright, patent, trademark, and
+              attribution notices from the Source form of the Work,
+              excluding those notices that do not pertain to any part of
+              the Derivative Works; and
+     
+          (d) If the Work includes a "NOTICE" text file as part of its
+              distribution, then any Derivative Works that You distribute must
+              include a readable copy of the attribution notices contained
+              within such NOTICE file, excluding those notices that do not
+              pertain to any part of the Derivative Works, in at least one
+              of the following places: within a NOTICE text file distributed
+              as part of the Derivative Works; within the Source form or
+              documentation, if provided along with the Derivative Works; or,
+              within a display generated by the Derivative Works, if and
+              wherever such third-party notices normally appear. The contents
+              of the NOTICE file are for informational purposes only and
+              do not modify the License. You may add Your own attribution
+              notices within Derivative Works that You distribute, alongside
+              or as an addendum to the NOTICE text from the Work, provided
+              that such additional attribution notices cannot be construed
+              as modifying the License.
+     
+          You may add Your own copyright statement to Your modifications and
+          may provide additional or different license terms and conditions
+          for use, reproduction, or distribution of Your modifications, or
+          for any such Derivative Works as a whole, provided Your use,
+          reproduction, and distribution of the Work otherwise complies with
+          the conditions stated in this License.
+     
+       5. Submission of Contributions. Unless You explicitly state otherwise,
+          any Contribution intentionally submitted for inclusion in the Work
+          by You to the Licensor shall be under the terms and conditions of
+          this License, without any additional terms or conditions.
+          Notwithstanding the above, nothing herein shall supersede or modify
+          the terms of any separate license agreement you may have executed
+          with Licensor regarding such Contributions.
+     
+       6. Trademarks. This License does not grant permission to use the trade
+          names, trademarks, service marks, or product names of the Licensor,
+          except as required for reasonable and customary use in describing the
+          origin of the Work and reproducing the content of the NOTICE file.
+     
+       7. Disclaimer of Warranty. Unless required by applicable law or
+          agreed to in writing, Licensor provides the Work (and each
+          Contributor provides its Contributions) on an "AS IS" BASIS,
+          WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+          implied, including, without limitation, any warranties or conditions
+          of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+          PARTICULAR PURPOSE. You are solely responsible for determining the
+          appropriateness of using or redistributing the Work and assume any
+          risks associated with Your exercise of permissions under this License.
+     
+       8. Limitation of Liability. In no event and under no legal theory,
+          whether in tort (including negligence), contract, or otherwise,
+          unless required by applicable law (such as deliberate and grossly
+          negligent acts) or agreed to in writing, shall any Contributor be
+          liable to You for damages, including any direct, indirect, special,
+          incidental, or consequential damages of any character arising as a
+          result of this License or out of the use or inability to use the
+          Work (including but not limited to damages for loss of goodwill,
+          work stoppage, computer failure or malfunction, or any and all
+          other commercial damages or losses), even if such Contributor
+          has been advised of the possibility of such damages.
+     
+       9. Accepting Warranty or Additional Liability. While redistributing
+          the Work or Derivative Works thereof, You may choose to offer,
+          and charge a fee for, acceptance of support, warranty, indemnity,
+          or other liability obligations and/or rights consistent with this
+          License. However, in accepting such obligations, You may act only
+          on Your own behalf and on Your sole responsibility, not on behalf
+          of any other Contributor, and only if You agree to indemnify,
+          defend, and hold each Contributor harmless for any liability
+          incurred by, or claims asserted against, such Contributor by reason
+          of your accepting any such warranty or additional liability.
+     
+       END OF TERMS AND CONDITIONS
+     
+       APPENDIX: How to apply the Apache License to your work.
+     
+          To apply the Apache License to your work, attach the following
+          boilerplate notice, with the fields enclosed by brackets "[]"
+          replaced with your own identifying information. (Don't include
+          the brackets!)  The text should be enclosed in the appropriate
+          comment syntax for the file format. We also recommend that a
+          file or class name and description of purpose be included on the
+          same "printed page" as the copyright notice for easier
+          identification within third-party archives.
+     
+       Copyright [yyyy] [name of copyright owner]
+     
+       Licensed under the Apache License, Version 2.0 (the "License");
+       you may not use this file except in compliance with the License.
+       You may obtain a copy of the License at
+     
+           http://www.apache.org/licenses/LICENSE-2.0
+     
+       Unless required by applicable law or agreed to in writing, software
+       distributed under the License is distributed on an "AS IS" BASIS,
+       WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+       See the License for the specific language governing permissions and
+       limitations under the License.
\ No newline at end of file
diff --git a/camelComponent/LICENSE/slf4j-license.txt b/camelComponent/LICENSE/slf4j-license.txt
new file mode 100644
index 00000000..2aa85a10
--- /dev/null
+++ b/camelComponent/LICENSE/slf4j-license.txt
@@ -0,0 +1,15 @@
+Copyright (c) 2004-2017 QOS.ch
+All rights reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
+documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
+rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
+persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
+Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file
diff --git a/camelComponent/README.md b/camelComponent/README.md
new file mode 100644
index 00000000..0265603e
--- /dev/null
+++ b/camelComponent/README.md
@@ -0,0 +1,147 @@
+# 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.
+
+The documentation has been split into two parts, [Setting Up Your Machine](#machine)
+and [Setting Up Your Vantiq](#vantiq).
+
+# Prerequisites <a name="pre" id="pre"></a>
+
+**IMPORTANT:** Read the [Testing](#testing) section before building this project.
+
+# Setting Up Your Machine <a name="machine" id="machine"></a>
+
+## Repository Contents
+
+Apache Camel, briefly, is organized into a set of _components_ that are used to produce _endpoints_. 
+In turn, the _endpoints_ can be used in the context of a _consumer_ or _producer_.
+A Camel consumer sits at the start of a Camel route and converts a message from Vantiq
+(_e.g._, received from a publish connector operation/publish to a Vantiq source)
+into a Camel Exchange that can travel down the route. 
+A Camel producer converts the Camel Exchange into a specific message 
+that can be sent to Vantiq (_e.g._,
+through a notification connector operation or query response).
+These are from the Apache Camel point of view, so a _producer_ produces messages 
+from the Camel application, and the _consumer_ takes them in _for_ the Camel application.
+
+*   **VantiqComponent** -- The base definition of the Vantiq Apache Camel Component.
+*   **VantiqConsumer** -- The part of the component that receives messages from Vantiq.
+*   **VantiqEndpoint** -- The base connecting the Apache Component infrastrucure to Vantiq.
+*   **VantiqProducer** -- The part of the component that can send messages to Vantiq.
+
+## How to Run the Program
+
+### Method 1 -- Directly from/as an Apache Camel application
+
+1.  Clone this repository (vantiq-extension-sources) and navigate into `<repo location>/vantiq-extension-sources`.
+2.  Run `./gradlew camelComponent:assemble`.
+4.  Navigate to `<repo location>/vantiq-extension-sources/camelComponent/build/libs`.
+5.  Use the camelComponent jar file to build your Apache Camel Application.  See the [Apache Camel documentation](https://camel.apache.org) for details.
+
+### Method 2 -- Use the Vantiq Apache Camel Connector.
+
+The Vantiq Camel Connector simplifies the configuration and construction of an Apache Camel application.
+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".  
+
+# 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.
+
+In order to incorporate this Extension Source, you will need to create the Source Implementation in the Vantiq system.
+
+## Source Implementation
+
+When creating a Camel Component Extension source,
+you must first create the source implementation.
+This is done by using the `camelComponentImpl.json` file found in `src/test/resources/camelComponentImpl.json`.
+To make the source type known to Vantiq, use the `vantiq` cli command
+
+```
+vantiq -s <profileName> load sourceimpls <fileName>
+```
+
+where `<profileName>` is replaced by the Vantiq profile name, and `<fileName>` is the file to be loaded.
+
+(You can, of course, change the source implementation name from that provided in this definition file.)
+
+That completed, you will need to create the Source in the Vantiq system. 
+
+## Source Configuration
+
+To set up the Source in the Vantiq system, you will need to add a Source to your project. Make sure you have properly
+added a Source Implementation to Vantiq. Once this is complete, you can select CAMEL_COMPONENT (or whatever you named
+your Source Implementation) as the Source Type. No configuration information is required.
+
+## Messages from the Source
+
+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.  Messages sent to Vantiq from the component will
+arrive as Vail objects, where the property names correspond to the Map keys.
+
+## Exchanges to Vantiq
+
+The Vantiq Component expects messages in an exchange to arrive in the form of a Java Map.  However, messages arriving
+in Json format will be accepted as well.
+
+## Adding a Vantiq Component to an Apache Camel Applications
+
+### URI Format
+
+The URI for the Vantiq connection takes the following form:
+
+```
+    vantiq://host[:port]?sourceName=<source name>&accessToken=<access token>[&sendPings=<boolean>][&failedMessageQueueSize=<int>]
+```
+
+### Component Endpoint Options
+
+The endpoint options apply to producer and consumer endpoints.  The following are supported.
+
+* **host** is the host on which the Vantiq server is running
+* **port** [optional] is the port on which the server is running
+* **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.
+
+For example, to create an Apache Camel application that receives messages from a Vantiq system at `vantiq.example.com`
+from source `exampleSource` and access token `FD10s0x...`, we would use something like the following:
+
+```js
+    from("vantiq://vantiq.example.com?accessToken=FD10s0x...&sourceName=exampleSource")
+        .to("log:INFO")
+```
+
+(expressed here using the Apache Camel Java DSL).
+
+## Testing <a name="testing" id="testing"></a>
+
+In order to properly run the tests, you must add properties to your _gradle.properties_ file in the _~/.gradle_
+directory (or to the gradle command line).
+These properties include the Vantiq server against which the tests will be run.
+
+One can control the configuration parameter for the testing by customizing gradle.properties file.
+The following shows what the gradle.properties file should look like:
+
+```
+    TestVantiqServer=<yourVantiqServer>
+    TestAuthToken=<yourAuthToken>
+```
+
+## 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/camelComponent/build.gradle b/camelComponent/build.gradle
new file mode 100644
index 00000000..e07dbf29
--- /dev/null
+++ b/camelComponent/build.gradle
@@ -0,0 +1,64 @@
+/*
+ * This file was generated by the Gradle 'init' task.
+ */
+
+plugins {
+    id 'java'
+}
+
+repositories {
+    mavenLocal()
+    maven {
+        url = uri('https://repo.maven.apache.org/maven2/')
+    }
+}
+
+ext {
+    camelVersion='3.18.1'
+    log4JVersion='2.17.2'
+    extjsdkVersion='1.0'
+    jacksonVersion = '2.14.2'
+    gsonVersion = '2.9.1'
+    commonsLang3Version = '3.12.0'
+}
+
+dependencies {
+    annotationProcessor "org.projectlombok:lombok:${lombokVersion}"
+
+    implementation "org.apache.camel:camel-core:${camelVersion}"
+    implementation "org.apache.camel:camel-support:${camelVersion}"
+    implementation "org.apache.camel:camel-jackson:${camelVersion}"
+    implementation "org.apache.commons:commons-lang3:${commonsLang3Version}"
+
+    implementation project(":extjsdk")
+    implementation "org.projectlombok:lombok:${lombokVersion}"
+    implementation "com.fasterxml.jackson.core:jackson-databind:${jacksonVersion}"
+    implementation "com.google.code.gson:gson:${gsonVersion}"
+
+    testImplementation "org.apache.logging.log4j:log4j-slf4j-impl:${log4JVersion}"
+    testImplementation "org.apache.camel:camel-test:${camelVersion}"
+    testImplementation "io.vantiq:vantiq-sdk:${vantiqSDKVersion}"
+    testImplementation project(path: ":extjsdk", configuration: 'testArtifacts')
+}
+
+group = 'io.vantiq.extsrc.camel'
+version = '1.0-SNAPSHOT'
+description = 'Camel Vantiq Component'
+java.sourceCompatibility = 1.11
+java.targetCompatibility = JavaVersion.VERSION_11
+
+tasks.withType(JavaCompile) {
+    options.encoding = 'UTF-8'
+}
+
+tasks.withType(Test) {
+    if (rootProject.hasProperty("TestAuthToken")) {
+        systemProperty "TestAuthToken", rootProject.findProperty("TestAuthToken") ?: "empty"
+    }
+    if (rootProject.hasProperty("TestVantiqServer")) {
+        systemProperty "TestVantiqServer", rootProject.findProperty("TestVantiqServer") ?: "empty"
+    }
+    // Use the build dir as a base to get our various test artifacts.
+    systemProperty "buildDir", "${buildDir}"
+}
+
diff --git a/camelComponent/src/main/java/io/vantiq/extsrc/camel/VantiqComponent.java b/camelComponent/src/main/java/io/vantiq/extsrc/camel/VantiqComponent.java
new file mode 100644
index 00000000..ce2b3eca
--- /dev/null
+++ b/camelComponent/src/main/java/io/vantiq/extsrc/camel/VantiqComponent.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2022 Vantiq, Inc.
+ *
+ * All rights reserved.
+ *
+ * SPDX: MIT
+ */
+
+package io.vantiq.extsrc.camel;
+
+import java.util.Map;
+
+import org.apache.camel.Endpoint;
+
+import org.apache.camel.support.DefaultComponent;
+
+@org.apache.camel.spi.annotations.Component("vantiq")
+public class VantiqComponent extends DefaultComponent {
+    
+    protected Endpoint createEndpoint(String uri, String remaining, Map<String, Object> parameters) throws Exception {
+        Endpoint endpoint = new VantiqEndpoint(uri, this);
+        setProperties(endpoint, parameters);
+        return endpoint;
+    }
+}
diff --git a/camelComponent/src/main/java/io/vantiq/extsrc/camel/VantiqConsumer.java b/camelComponent/src/main/java/io/vantiq/extsrc/camel/VantiqConsumer.java
new file mode 100644
index 00000000..1d7519e9
--- /dev/null
+++ b/camelComponent/src/main/java/io/vantiq/extsrc/camel/VantiqConsumer.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (c) 2022 Vantiq, Inc.
+ *
+ * All rights reserved.
+ *
+ * SPDX: MIT
+ */
+
+package io.vantiq.extsrc.camel;
+
+import static org.apache.camel.ExchangePattern.InOnly;
+import static org.apache.camel.ExchangePattern.InOut;
+
+import io.vantiq.extjsdk.ExtensionServiceMessage;
+import io.vantiq.extjsdk.ExtensionWebSocketClient;
+import io.vantiq.extjsdk.Handler;
+
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.camel.Exchange;
+import org.apache.camel.ExchangePattern;
+import org.apache.camel.Processor;
+import org.apache.camel.support.DefaultConsumer;
+
+@Slf4j
+public class VantiqConsumer extends DefaultConsumer {
+    private final VantiqEndpoint endpoint;
+    private ExtensionWebSocketClient vantiqClient;
+
+    private ExecutorService executorService;
+    
+    public VantiqConsumer(VantiqEndpoint endpoint, Processor processor) {
+        super(endpoint, processor);
+        this.endpoint = endpoint;
+    }
+
+    @Override
+    protected void doStart() throws Exception {
+        super.doStart();
+        endpoint.startup();
+        vantiqClient = endpoint.getVantiqClient();
+        vantiqClient.setPublishHandler(publishHandler);
+        vantiqClient.setQueryHandler(queryHandler);
+
+        // start a single threaded pool to process incoming messages
+        executorService = endpoint.createExecutor();
+    }
+
+    @Override
+    protected void doStop() throws Exception {
+        if (null != endpoint) {
+            endpoint.shutdown();
+        }
+        super.doStop();
+
+        // shutdown the thread pool gracefully
+        getEndpoint().getCamelContext().getExecutorServiceManager().shutdownGraceful(executorService);
+    }
+    
+    /**
+     * A handler for dealing with publishes to Camel.
+     */
+    private final Handler<ExtensionServiceMessage> publishHandler = new Handler<ExtensionServiceMessage>() {
+        @Override
+        public void handleMessage(ExtensionServiceMessage message) {
+            // When we get a message, process it in the background...
+            executorService.submit( () -> {
+                processMessage(message, InOnly);
+            });
+        }
+    };
+    
+    /**
+     * A handler for dealing with queries to Camel.
+     */
+    private final Handler<ExtensionServiceMessage> queryHandler = new Handler<ExtensionServiceMessage>() {
+        @Override
+        public void handleMessage(ExtensionServiceMessage message) {
+            // When we get a message, process it in the background...
+            executorService.submit( () -> {
+                processMessage(message, InOut);
+            });
+        }
+    };
+    
+    
+    private void processMessage(Object msg, ExchangePattern pattern) {
+        if (!(msg instanceof ExtensionServiceMessage)) {
+            log.error("Message processor for {} ignoring an unknown message type: {}.",
+                    endpoint.getEndpointName(), msg.getClass().getName());
+        } else {
+            ExtensionServiceMessage message = (ExtensionServiceMessage) msg;
+            Object msgBody = message.getObject();
+    
+            // Create an exchange to move our message along.  In the publish case,
+            // we have no interest in the result, so we'll allow it to be released when
+            // 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);
+            exchange.setPattern(pattern);
+            exchange.getIn().setBody(msgBody);
+            if (pattern == InOut) {
+                // In this case, we need to save the reply address in the exchange so that we can reply appropriately
+                // This is the case when we are processing a Vantiq query.
+                Map<String, Object> hdrs = message.getMessageHeaders();
+                Object ra = hdrs.get(ExtensionServiceMessage.ORIGIN_ADDRESS_HEADER);
+                if (ra instanceof String) {
+                    exchange.setProperty(ExtensionServiceMessage.RESPONSE_ADDRESS_HEADER, ra);
+                } else {
+                    // Internal error -- should have a reply address here if we've been told to be in/out
+                    log.error("Expected reply address header ({}) in {} message",
+                              ExtensionServiceMessage.ORIGIN_ADDRESS_HEADER, message.getOp());
+                }
+            }
+    
+            try {
+                // send message to next processor in the route
+                getProcessor().process(exchange);
+            } catch (Exception e) {
+                exchange.setException(e);
+            } finally {
+                if (exchange.getException() != null) {
+                    getExceptionHandler().handleException("Error processing exchange", exchange, exchange.getException());
+                }
+                releaseExchange(exchange, false);
+            }
+        }
+    }
+}
diff --git a/camelComponent/src/main/java/io/vantiq/extsrc/camel/VantiqEndpoint.java b/camelComponent/src/main/java/io/vantiq/extsrc/camel/VantiqEndpoint.java
new file mode 100644
index 00000000..c2b473c7
--- /dev/null
+++ b/camelComponent/src/main/java/io/vantiq/extsrc/camel/VantiqEndpoint.java
@@ -0,0 +1,338 @@
+/*
+ * Copyright (c) 2022 Vantiq, Inc.
+ *
+ * All rights reserved.
+ *
+ * SPDX: MIT
+ */
+
+package io.vantiq.extsrc.camel;
+
+import static io.vantiq.extjsdk.Utils.AUTH_TOKEN_PROPERTY_NAME;
+import static io.vantiq.extjsdk.Utils.SEND_PING_PROPERTY_NAME;
+import static io.vantiq.extjsdk.Utils.SERVER_CONFIG_FILENAME;
+import static io.vantiq.extjsdk.Utils.SOURCES_PROPERTY_NAME;
+import static io.vantiq.extjsdk.Utils.TARGET_SERVER_PROPERTY_NAME;
+
+import io.vantiq.extjsdk.ExtensionWebSocketClient;
+import io.vantiq.extjsdk.InstanceConfigUtils;
+import io.vantiq.extsrc.camel.utils.ClientRegistry;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.camel.CamelException;
+import org.apache.camel.Category;
+import org.apache.camel.Consumer;
+import org.apache.camel.Processor;
+import org.apache.camel.Producer;
+import org.apache.camel.spi.UriParams;
+import org.apache.camel.support.DefaultEndpoint;
+import org.apache.camel.spi.Metadata;
+import org.apache.camel.spi.UriEndpoint;
+import org.apache.camel.spi.UriParam;
+import org.apache.commons.lang3.StringUtils;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Map;
+import java.util.Properties;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+
+/**
+ * vantiq component to interact with Vantiq systems.
+ *
+ * The vantiq component supports sending and receiving events to/from a Vantiq installation via a Vantiq source. As
+ * messages are sent or received, the producer or consumer, respectively, process these messages.  All such messages
+ * are one-way.
+ *
+ */
+@UriEndpoint(firstVersion = "1.0-SNAPSHOT", scheme = "vantiq", title = "vantiq", syntax="vantiq:name",
+             category = {Category.JAVA})
+@UriParams
+@Slf4j
+public class VantiqEndpoint extends DefaultEndpoint {
+    @UriParam @Metadata(required = true)
+    @Setter
+    private String sourceName;
+    
+    @UriParam(label="security", secret = true) @Metadata(required = true)
+    @Setter
+    private String accessToken;
+    
+    @UriParam(defaultValue = "false")
+    @Getter
+    @Setter
+    private boolean sendPings;
+    
+    @UriParam(defaultValue = "false")
+    @Getter
+    @Setter
+    private boolean noSsl;
+    
+    @UriParam(defaultValue = "25")
+    @Setter
+    private int failedMessageQueueSize;
+    
+    @Setter
+    @Getter
+    private ExtensionWebSocketClient vantiqClient;
+    
+    private String correctedVantiqUrl = null;
+    private boolean started = false;
+    private String startStopLock = "startStopLock";
+    private boolean runningInConnector = false;
+    
+    @Getter
+    private String endpointName;
+    
+    InstanceConfigUtils utils = null;
+
+    public VantiqEndpoint() {
+        utils = new InstanceConfigUtils();
+    }
+    
+    /**
+     * Create the Vantiq endpoint based on the URI provided.
+     *
+     * We have some special handling for the URI.  If it's null (generally harder to do except from bean-style
+     * configurations), we'll let that through.  In such cases, our superclass will call our overridden method
+     * createEndpointUri().  This will construct the URI from the information in the containing connector's
+     * server.config file (if any). As noted, this is sometimes hard to do given programmatic route construction. So,
+     * the alternative is to allow for a URI of "vantiq://server.config".  If we see a "host" name of "server.config",
+     * we will call the superclass constructor with a uri of null, and the processing described above will take over.
+     * This capability is primarily of use in the case of the Camel connector, allowing the user to avoid repeatedly
+     * having to specify the connection information.
+     *
+     * @param uri String the URI to which to connect
+     * @param component VantiqComponent the Camel component from which to get the endpoint.
+     * @throws Exception when the URI is invalid or there are other issues constructing the endpoint
+     */
+    public VantiqEndpoint(String uri, VantiqComponent component) throws Exception {
+        // A bit of a strange construct, but necessary given the required interface and Java's requirement that the
+        // super() call be the first functional line of a constructor. A "factory method" here might be a better
+        // choice, but the constructor interface is fixed by Camel.
+        super( (uri != null && new URI(uri).getHost().equalsIgnoreCase(SERVER_CONFIG_FILENAME)) ?
+                      null : uri, component);
+        log.debug("Creating VantiqEndpoint for uri: {} with sourceName: {}, accessToken: {}",
+                 uri, sourceName, accessTokenForLog());
+        utils = new InstanceConfigUtils();
+    }
+    
+    private void buildEndpointName() {
+        String baseUri = getEndpointBaseUri();
+        String vantiqInstName = baseUri.replace("vantiq://", "");
+        vantiqInstName = vantiqInstName.replace("/", "");
+        endpointName = "vantiq--" + vantiqInstName + "::" + sourceName;
+        log.debug("Endpoint name {} assigned for {}", endpointName, getEndpointBaseUri());
+    }
+
+    public Producer createProducer() throws Exception {
+        log.info("VantiqEndpoint creating producer for uri: {} with sourceName: {}, accessToken: {}",
+                getEndpointBaseUri(), sourceName, accessTokenForLog());
+    
+        return new VantiqProducer(this);
+    }
+
+    public Consumer createConsumer(Processor processor) throws Exception {
+        log.info("VantiqEndpoint creating consumer for uri: {} with sourceName: {}, accessToken: {}",
+                getEndpointBaseUri(), sourceName, accessTokenForLog());
+        Consumer consumer = new VantiqConsumer(this, processor);
+        configureConsumer(consumer);
+        return consumer;
+    }
+    
+    public void sendMessage(Object vMsg) {
+        vantiqClient.sendNotification(vMsg);
+    }
+    
+    public void sendResponse(int httpStatus, String respAddress, Map vMsg) {
+        vantiqClient.sendQueryResponse(httpStatus, respAddress, vMsg);
+    }
+    
+    public void sendQueryError(String respAddress, String msgCode, String msgText, Object[] params) {
+        vantiqClient.sendQueryError(respAddress, msgCode, msgText, params);
+    }
+    
+    /**
+     * Constructs & returns an ExtensionWebSocketClient.
+     *
+     * This is here primarily to override for unit testing. In unit test cases, we override to return a client
+     * that doesn't send to the (nonexistent) Vantiq installation;
+     *
+     * @param source        String name of the source to which to connect
+     * @param msgQueueSize  int number of messages to queue when the connection is broken.
+     * @return ExtensionWebSocketClient
+     */
+    protected ExtensionWebSocketClient buildVantiqClient(String source, int msgQueueSize) {
+        return new ExtensionWebSocketClient(sourceName, failedMessageQueueSize, utils);
+    }
+    
+    protected ExtensionWebSocketClient buildVantiqClientFromTarget(String source, String targetServer) {
+        return new ExtensionWebSocketClient(sourceName, failedMessageQueueSize, utils);
+    }
+    
+    protected void completeFuturesForFauxInstances() {
+        // Does nothing in the real case.  Overridden in FauxVantiqEndpoint.
+    }
+    
+    public void startup() throws CamelException {
+    
+        synchronized (startStopLock) {
+            if (started) {
+                return;
+            }
+            CamelException failure = null;
+            try {
+                log.debug("Attempting to connect to URL: {} from {}", getEndpointBaseUri(), getEndpointUri());
+                String vtq = getEndpointBaseUri();
+                String protocol = "https";
+                if (noSsl) {
+                    // This is primarily for use in development.  But supported...
+                    protocol = "http";
+                }
+                correctedVantiqUrl = vtq.replace("vantiq", protocol);
+                log.trace("Fixed-up Vantiq URL: {}", correctedVantiqUrl);
+                utils.provideServerConfig(correctedVantiqUrl, accessToken, sourceName, sendPings, null);
+                buildEndpointName();
+        
+                // If we are running inside a Vantiq Camel connector, the connector runtime may have already created a
+                // client. If that's the case, use, that client,  Otherwise, we'll create our own, knowing that the
+                // connector runtime is not managing our client.
+        
+                vantiqClient = ClientRegistry.fetchClient(sourceName, correctedVantiqUrl);
+                if (vantiqClient != null) {
+                    runningInConnector = true;
+                    started = true;
+                    return;
+                } else {
+                    // Reconfig's are handled here by auto-reconnect.
+            
+                    vantiqClient = buildVantiqClient(sourceName, failedMessageQueueSize);
+                    CompletableFuture<Boolean> fut =
+                            vantiqClient.initiateFullConnection(correctedVantiqUrl, accessToken, sendPings);
+            
+                    completeFuturesForFauxInstances();
+                    if (fut.get()) {
+                        runningInConnector = false;
+                        started = true; // Mark that we've successfully started up.
+                        vantiqClient.setAutoReconnect(true);
+                        return;
+                    } else {
+                        String errMsg = "Failed to initiate connection to Vantiq server: " + vtq +
+                                " (" + correctedVantiqUrl + "), source: " + sourceName;
+                        // Something didn't work.  Let's try & get a better diagnosis of what went awry.
+                        if (!vantiqClient.isOpen()) {
+                            // Then we failed to connect.
+                            errMsg = "Failed to connect to Vantiq server: " + vtq +
+                                    " (" + correctedVantiqUrl + ").";
+                        } else if (!vantiqClient.isAuthed()) {
+                            errMsg = "Authentication failure connecting to " + vtq +
+                                    " (" + correctedVantiqUrl + ").";
+                        } else if (!vantiqClient.isConnected()) {
+                            errMsg = "Failed to connect to Vantiq source " + sourceName + " at server: " + vtq +
+                                    " (" + correctedVantiqUrl + ").";
+                        }
+                        log.error(errMsg);
+                        failure = new CamelException(errMsg);
+                    }
+                }
+            } catch (InterruptedException | ExecutionException e) {
+                log.error("Failed to initiate connection to Vantiq server: {}, source: {}",
+                          correctedVantiqUrl, sourceName, e);
+                throw new CamelException(e);
+            }
+            throw failure;
+        }
+    }
+    
+    public boolean isConnected() {
+        synchronized (startStopLock) {
+            return started;
+        }
+    }
+    
+    /**
+     * Create and endpoint URI for our VantiqEndpoint.
+     *
+     * This is used by the DefaultEndpoint code when no endpoint URI is specified.  In the Vantiq case, we will
+     * interpret the lack of URI specification to mean that this VantiqEndpoint is to connect to the Vantiq
+     * described in the standard connector server.config file.  We will provide the parameters as is appropriate from
+     * data gleaned from the server.config file.
+     *
+     * @return String representing the URI provided by the source.config file.
+     * @throws IllegalArgumentException when no source.config file is present or the data present is incorrect.
+     */
+    @Override
+    public String createEndpointUri() {
+        
+        Properties scProps = utils.obtainServerConfig();
+        String baseUri = scProps.getProperty(TARGET_SERVER_PROPERTY_NAME);
+        String accessToken = scProps.getProperty(AUTH_TOKEN_PROPERTY_NAME);
+        String sourceName = scProps.getProperty(SOURCES_PROPERTY_NAME);
+        String sendPings = scProps.getProperty(SEND_PING_PROPERTY_NAME);
+        if (StringUtils.isEmpty(baseUri) || StringUtils.isEmpty(accessToken)) {
+            throw new IllegalArgumentException("source.config file is missing or does not contain sufficient " +
+                                                       "information from which to construct an endpoint URI.");
+        }
+        if (StringUtils.isEmpty(sourceName) || sourceName.contains(",")) {
+            throw new IllegalArgumentException("Default vantiq: endpoints require a source.config file with a single" +
+                                                       " source name. Found: '" + sourceName + "'.");
+        }
+        
+        try {
+            URI vantiqURI = new URI(baseUri);
+            this.setEndpointUri(baseUri);
+            String origScheme = vantiqURI.getScheme();
+            
+            StringBuffer epString = new StringBuffer(vantiqURI.toString());
+            epString.append("?sourceName=").append(sourceName);
+            this.sourceName = sourceName;
+            epString.append("&accessToken=").append(accessToken);
+            this.accessToken = accessToken;
+            if (sendPings != null) {
+                epString.append("&sendPings=").append(sendPings);
+                this.sendPings = Boolean.parseBoolean(sendPings);
+            }
+            if (origScheme.equals("http") || origScheme.equals("ws")) {
+                epString.append("&noSsl=").append("true");
+                noSsl = true;
+            }
+            epString.replace(0, origScheme.length(), "vantiq");
+            URI endpointUri = URI.create(String.valueOf(epString));
+            return endpointUri.toString();
+        } catch (URISyntaxException  mue) {
+            throw new IllegalArgumentException(TARGET_SERVER_PROPERTY_NAME + " from server config file is invalid",
+                                               mue);
+        }
+    }
+    
+    @Override
+    public void doStop() {
+        synchronized (startStopLock) {
+            if (vantiqClient != null) {
+                if (!runningInConnector) {
+                    // Only close if we are the one that opened things
+                    vantiqClient.close();
+                }
+                vantiqClient = null;
+            }
+            started = false;
+        }
+    }
+
+    public ExecutorService createExecutor() {
+        // TODO: Pool min/max sizes should be configurable in URI or properties
+        return getCamelContext().getExecutorServiceManager().newThreadPool(this, endpointName,
+                1, 5);
+    }
+    
+    /**
+     * Sanitize access token for logging
+     * @return String access token representation suitable for logging.
+     */
+    private String accessTokenForLog() {
+        return StringUtils.isEmpty(accessToken) ? "null/empty" : "present";
+    }
+}
diff --git a/camelComponent/src/main/java/io/vantiq/extsrc/camel/VantiqProducer.java b/camelComponent/src/main/java/io/vantiq/extsrc/camel/VantiqProducer.java
new file mode 100644
index 00000000..e21a285c
--- /dev/null
+++ b/camelComponent/src/main/java/io/vantiq/extsrc/camel/VantiqProducer.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (c) 2022 Vantiq, Inc.
+ *
+ * All rights reserved.
+ *
+ * SPDX: MIT
+ */
+
+package io.vantiq.extsrc.camel;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+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.InputStreamCache;
+import org.apache.camel.InvalidPayloadException;
+import org.apache.camel.support.DefaultProducer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.net.HttpURLConnection;
+import java.util.HashMap;
+import java.util.Map;
+
+@Slf4j
+public class VantiqProducer extends DefaultProducer {
+    private VantiqEndpoint endpoint;
+    ObjectMapper mapper = new ObjectMapper();
+    
+    public VantiqProducer(VantiqEndpoint endpoint) {
+        super(endpoint);
+        this.endpoint = endpoint;
+    }
+    
+    @SuppressWarnings("unchecked")
+    public void process(Exchange exchange) throws Exception {
+        if (log.isTraceEnabled()) {
+            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.
+            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 it's 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;
+                    vMsg = mapper.readValue(strMsg, new TypeReference<>() {});
+                } else if (msg instanceof Map) {
+                    vMsg = (Map<String, Object>) msg;
+                } else if (msg instanceof JsonNode) {
+                    vMsg = mapper.convertValue(msg, 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);
+                }
+            }
+        } 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);
+        }
+        
+        // The RESPONSE_ADDRESS_HEADER is sent by Vantiq when the message represents a Vantiq query.  It is used on
+        // the Vantiq side to link up the query & response, so it needs to be preserved and returned to Vantiq.
+        Object ra = exchange.getProperty(ExtensionServiceMessage.RESPONSE_ADDRESS_HEADER);
+        String responseAddr = null;
+        if (ra instanceof String) {
+            responseAddr = (String) ra;
+        }
+        if (exchange.getPattern() == ExchangePattern.InOut) {
+            if (exchange.getException() != null) {
+                Exception e = exchange.getException();
+                String emsg = "Exception while processing message: {0}::{1}";
+                endpoint.sendQueryError(responseAddr, "io.vantiq.extsrc.camel.appexception",
+                                        emsg, new Object[] {e.getClass().getName(), e.getMessage()});
+            } else {
+                log.debug("Sending response message: code; {}, addr: {}, msg: {}",
+                          HttpURLConnection.HTTP_OK, responseAddr, vMsg);
+                endpoint.sendResponse(HttpURLConnection.HTTP_OK, responseAddr, vMsg);
+            }
+        } else {
+            endpoint.sendMessage(vMsg);
+        }
+    }
+    
+    @Override
+    protected void doStart() throws Exception {
+        super.doStart();
+        endpoint.startup();
+    }
+    
+    @Override
+    protected void doStop() throws Exception {
+        if (null != endpoint) {
+            endpoint.shutdown();
+        }
+        super.doStop();
+    }
+}
diff --git a/camelComponent/src/main/java/io/vantiq/extsrc/camel/utils/ClientRegistry.java b/camelComponent/src/main/java/io/vantiq/extsrc/camel/utils/ClientRegistry.java
new file mode 100644
index 00000000..53ff0fa2
--- /dev/null
+++ b/camelComponent/src/main/java/io/vantiq/extsrc/camel/utils/ClientRegistry.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (c) 2022 Vantiq, Inc.
+ *
+ * All rights reserved.
+ *
+ * SPDX: MIT
+ */
+
+package io.vantiq.extsrc.camel.utils;
+
+import io.vantiq.extjsdk.ExtensionWebSocketClient;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.function.BiFunction;
+
+/**
+ * A registry of known clients.
+ *
+ * This is used by camel connector to avoid having multiple clients for the same source/server combination.  Since
+ * the camel components can construct their own clients (when used as a pure Camel component rather than in our
+ * connector), we need to avoid this situation.
+ *
+ * Although this is (at least, originally) used by Camel connector, it is co-located with the Camel components so
+ * that they can call it.  This allows Apache Camel applications that make use of the Vantiq component to be
+ * constructed without the inclusion of the Camel connector.
+ */
+public class ClientRegistry {
+    
+    private static final String SOURCE_NAME_PARAM = "?sourceName=";
+    public static final String SOURCE_NAME_KEY = "sourceName";
+    public static final String SERVER_URL_KEY = "serverUrl";
+    private static final ConcurrentMap<String, ExtensionWebSocketClient> knownClients = new ConcurrentHashMap<>();
+    
+    /**
+     * Get the known client for the source name and server specified.
+     *
+     * @param sourceName String name of the Vantiq source this client represents
+     * @param serverUrl String URL of the Vantiq installation within which the source resides
+     * @return ExtensionWebSocketClient corresponding to the sourceName & serverUrl, or null if none is present
+     */
+    public static ExtensionWebSocketClient fetchClient(String sourceName, String serverUrl) {
+        String key = constructClientKey(sourceName, serverUrl);
+        return knownClients.get(key);
+    }
+    
+    /**
+     * Register a client in the registry
+     * @param sourceName String name of the Vantiq source this client represents
+     * @param serverUrl String URL of the Vantiq installation within which the source resides
+     * @param clientBuilder BiFunction<String, String, ExtensionWebSocketClient> function to build the client if one
+     *                      does not already exist.
+     * @return ExtensionWebSocketClient if created, null if not created (i.e., already exists).
+     */
+    public static ExtensionWebSocketClient registerClient(String sourceName, String serverUrl,
+                                                          BiFunction<String, String, ExtensionWebSocketClient> clientBuilder) {
+        String key = constructClientKey(sourceName, serverUrl);
+        return knownClients.computeIfAbsent(key, (clientKey) -> {
+            return clientBuilder.apply(sourceName, serverUrl);
+        });
+    }
+    
+    /**
+     * Register a client in the registry
+     * @param sourceName String name of the Vantiq source this client represents
+     * @param serverUrl String URL of the Vantiq installation within which the source resides
+     * @param client ExtensionWebSocketClient client to register
+     * @return ExtensionWebSocketClient associated with the sourceName & serverUrl.  This will be the given client if
+     *                                  did not exist, or the existing client if one as previously registered.
+     */
+    public static ExtensionWebSocketClient registerClient(String sourceName, String serverUrl,
+                                                    ExtensionWebSocketClient client) {
+        String key = constructClientKey(sourceName, serverUrl);
+        return knownClients.putIfAbsent(key, client);
+    }
+    
+    /**
+     * Remove the registered mapping for the given sourceName & serverUrl.
+     *
+     * If the client parameter is non-null, the registered client is removed only if the client is the one
+     * specified.  If the client parameter is null, any registered client matching the sourceName and serverUrl will
+     * be removed from the registry.
+     *
+     * @param sourceName String name of the Vantiq source this client represents
+     * @param serverUrl String URL of the Vantiq installation within which the source resides
+     * @param client ExtensionWebSocketClient client to remove.  Can be null.
+     * @return ExtensionWebSocketClient removed.  Null if no client was removed.
+     */
+    public static ExtensionWebSocketClient removeClient(String sourceName, String serverUrl, ExtensionWebSocketClient client) {
+        String key = constructClientKey(sourceName, serverUrl);
+        ExtensionWebSocketClient removed;
+        if (client != null) {
+            if (knownClients.remove(key, client)) {
+                removed = client;
+            } else {
+                removed = null;
+            }
+        } else {
+            removed = knownClients.remove(key);
+        }
+        return removed;
+    }
+    
+    /**
+     * Return the number of registered clients
+     *
+     * @return int representing the number of currently registered clients.
+     **/
+    public static int getKnownClientCount() {
+        return knownClients.size();
+    }
+    
+    /**
+     * Construct the key used in our registry.
+     *
+     * @param sourceName String name of the Vantiq source this client represents
+     * @param serverUrl String URL of the Vantiq installation within which the source resides
+     * @return String representing the key to use in our registry.
+     */
+    public static String constructClientKey(String sourceName, String serverUrl) {
+        // Normalize URL just in case...
+        serverUrl = serverUrl.trim();
+        if (!serverUrl.endsWith("/")) {
+            serverUrl = serverUrl + "/";
+        }
+        return serverUrl + SOURCE_NAME_PARAM + sourceName;
+    }
+}
diff --git a/camelComponent/src/main/resources/META-INF/services/org/apache/camel/component/vantiq b/camelComponent/src/main/resources/META-INF/services/org/apache/camel/component/vantiq
new file mode 100644
index 00000000..fb19ec11
--- /dev/null
+++ b/camelComponent/src/main/resources/META-INF/services/org/apache/camel/component/vantiq
@@ -0,0 +1,1 @@
+class=io.vantiq.extsrc.camel.VantiqComponent
diff --git a/camelConnector/LICENSE/LICENSE.txt b/camelConnector/LICENSE/LICENSE.txt
new file mode 100644
index 00000000..461db819
--- /dev/null
+++ b/camelConnector/LICENSE/LICENSE.txt
@@ -0,0 +1,14 @@
+Copyright 2022 Vantiq, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
+documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
+rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
+persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
+Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file
diff --git a/camelConnector/LICENSE/apache-2.txt b/camelConnector/LICENSE/apache-2.txt
new file mode 100644
index 00000000..fd400e62
--- /dev/null
+++ b/camelConnector/LICENSE/apache-2.txt
@@ -0,0 +1,203 @@
+
+
+                                     Apache License
+                               Version 2.0, January 2004
+                            http://www.apache.org/licenses/
+     
+       TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+     
+       1. Definitions.
+     
+          "License" shall mean the terms and conditions for use, reproduction,
+          and distribution as defined by Sections 1 through 9 of this document.
+     
+          "Licensor" shall mean the copyright owner or entity authorized by
+          the copyright owner that is granting the License.
+     
+          "Legal Entity" shall mean the union of the acting entity and all
+          other entities that control, are controlled by, or are under common
+          control with that entity. For the purposes of this definition,
+          "control" means (i) the power, direct or indirect, to cause the
+          direction or management of such entity, whether by contract or
+          otherwise, or (ii) ownership of fifty percent (50%) or more of the
+          outstanding shares, or (iii) beneficial ownership of such entity.
+     
+          "You" (or "Your") shall mean an individual or Legal Entity
+          exercising permissions granted by this License.
+     
+          "Source" form shall mean the preferred form for making modifications,
+          including but not limited to software source code, documentation
+          source, and configuration files.
+     
+          "Object" form shall mean any form resulting from mechanical
+          transformation or translation of a Source form, including but
+          not limited to compiled object code, generated documentation,
+          and conversions to other media types.
+     
+          "Work" shall mean the work of authorship, whether in Source or
+          Object form, made available under the License, as indicated by a
+          copyright notice that is included in or attached to the work
+          (an example is provided in the Appendix below).
+     
+          "Derivative Works" shall mean any work, whether in Source or Object
+          form, that is based on (or derived from) the Work and for which the
+          editorial revisions, annotations, elaborations, or other modifications
+          represent, as a whole, an original work of authorship. For the purposes
+          of this License, Derivative Works shall not include works that remain
+          separable from, or merely link (or bind by name) to the interfaces of,
+          the Work and Derivative Works thereof.
+     
+          "Contribution" shall mean any work of authorship, including
+          the original version of the Work and any modifications or additions
+          to that Work or Derivative Works thereof, that is intentionally
+          submitted to Licensor for inclusion in the Work by the copyright owner
+          or by an individual or Legal Entity authorized to submit on behalf of
+          the copyright owner. For the purposes of this definition, "submitted"
+          means any form of electronic, verbal, or written communication sent
+          to the Licensor or its representatives, including but not limited to
+          communication on electronic mailing lists, source code control systems,
+          and issue tracking systems that are managed by, or on behalf of, the
+          Licensor for the purpose of discussing and improving the Work, but
+          excluding communication that is conspicuously marked or otherwise
+          designated in writing by the copyright owner as "Not a Contribution."
+     
+          "Contributor" shall mean Licensor and any individual or Legal Entity
+          on behalf of whom a Contribution has been received by Licensor and
+          subsequently incorporated within the Work.
+     
+       2. Grant of Copyright License. Subject to the terms and conditions of
+          this License, each Contributor hereby grants to You a perpetual,
+          worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+          copyright license to reproduce, prepare Derivative Works of,
+          publicly display, publicly perform, sublicense, and distribute the
+          Work and such Derivative Works in Source or Object form.
+     
+       3. Grant of Patent License. Subject to the terms and conditions of
+          this License, each Contributor hereby grants to You a perpetual,
+          worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+          (except as stated in this section) patent license to make, have made,
+          use, offer to sell, sell, import, and otherwise transfer the Work,
+          where such license applies only to those patent claims licensable
+          by such Contributor that are necessarily infringed by their
+          Contribution(s) alone or by combination of their Contribution(s)
+          with the Work to which such Contribution(s) was submitted. If You
+          institute patent litigation against any entity (including a
+          cross-claim or counterclaim in a lawsuit) alleging that the Work
+          or a Contribution incorporated within the Work constitutes direct
+          or contributory patent infringement, then any patent licenses
+          granted to You under this License for that Work shall terminate
+          as of the date such litigation is filed.
+     
+       4. Redistribution. You may reproduce and distribute copies of the
+          Work or Derivative Works thereof in any medium, with or without
+          modifications, and in Source or Object form, provided that You
+          meet the following conditions:
+     
+          (a) You must give any other recipients of the Work or
+              Derivative Works a copy of this License; and
+     
+          (b) You must cause any modified files to carry prominent notices
+              stating that You changed the files; and
+     
+          (c) You must retain, in the Source form of any Derivative Works
+              that You distribute, all copyright, patent, trademark, and
+              attribution notices from the Source form of the Work,
+              excluding those notices that do not pertain to any part of
+              the Derivative Works; and
+     
+          (d) If the Work includes a "NOTICE" text file as part of its
+              distribution, then any Derivative Works that You distribute must
+              include a readable copy of the attribution notices contained
+              within such NOTICE file, excluding those notices that do not
+              pertain to any part of the Derivative Works, in at least one
+              of the following places: within a NOTICE text file distributed
+              as part of the Derivative Works; within the Source form or
+              documentation, if provided along with the Derivative Works; or,
+              within a display generated by the Derivative Works, if and
+              wherever such third-party notices normally appear. The contents
+              of the NOTICE file are for informational purposes only and
+              do not modify the License. You may add Your own attribution
+              notices within Derivative Works that You distribute, alongside
+              or as an addendum to the NOTICE text from the Work, provided
+              that such additional attribution notices cannot be construed
+              as modifying the License.
+     
+          You may add Your own copyright statement to Your modifications and
+          may provide additional or different license terms and conditions
+          for use, reproduction, or distribution of Your modifications, or
+          for any such Derivative Works as a whole, provided Your use,
+          reproduction, and distribution of the Work otherwise complies with
+          the conditions stated in this License.
+     
+       5. Submission of Contributions. Unless You explicitly state otherwise,
+          any Contribution intentionally submitted for inclusion in the Work
+          by You to the Licensor shall be under the terms and conditions of
+          this License, without any additional terms or conditions.
+          Notwithstanding the above, nothing herein shall supersede or modify
+          the terms of any separate license agreement you may have executed
+          with Licensor regarding such Contributions.
+     
+       6. Trademarks. This License does not grant permission to use the trade
+          names, trademarks, service marks, or product names of the Licensor,
+          except as required for reasonable and customary use in describing the
+          origin of the Work and reproducing the content of the NOTICE file.
+     
+       7. Disclaimer of Warranty. Unless required by applicable law or
+          agreed to in writing, Licensor provides the Work (and each
+          Contributor provides its Contributions) on an "AS IS" BASIS,
+          WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+          implied, including, without limitation, any warranties or conditions
+          of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+          PARTICULAR PURPOSE. You are solely responsible for determining the
+          appropriateness of using or redistributing the Work and assume any
+          risks associated with Your exercise of permissions under this License.
+     
+       8. Limitation of Liability. In no event and under no legal theory,
+          whether in tort (including negligence), contract, or otherwise,
+          unless required by applicable law (such as deliberate and grossly
+          negligent acts) or agreed to in writing, shall any Contributor be
+          liable to You for damages, including any direct, indirect, special,
+          incidental, or consequential damages of any character arising as a
+          result of this License or out of the use or inability to use the
+          Work (including but not limited to damages for loss of goodwill,
+          work stoppage, computer failure or malfunction, or any and all
+          other commercial damages or losses), even if such Contributor
+          has been advised of the possibility of such damages.
+     
+       9. Accepting Warranty or Additional Liability. While redistributing
+          the Work or Derivative Works thereof, You may choose to offer,
+          and charge a fee for, acceptance of support, warranty, indemnity,
+          or other liability obligations and/or rights consistent with this
+          License. However, in accepting such obligations, You may act only
+          on Your own behalf and on Your sole responsibility, not on behalf
+          of any other Contributor, and only if You agree to indemnify,
+          defend, and hold each Contributor harmless for any liability
+          incurred by, or claims asserted against, such Contributor by reason
+          of your accepting any such warranty or additional liability.
+     
+       END OF TERMS AND CONDITIONS
+     
+       APPENDIX: How to apply the Apache License to your work.
+     
+          To apply the Apache License to your work, attach the following
+          boilerplate notice, with the fields enclosed by brackets "[]"
+          replaced with your own identifying information. (Don't include
+          the brackets!)  The text should be enclosed in the appropriate
+          comment syntax for the file format. We also recommend that a
+          file or class name and description of purpose be included on the
+          same "printed page" as the copyright notice for easier
+          identification within third-party archives.
+     
+       Copyright [yyyy] [name of copyright owner]
+     
+       Licensed under the Apache License, Version 2.0 (the "License");
+       you may not use this file except in compliance with the License.
+       You may obtain a copy of the License at
+     
+           http://www.apache.org/licenses/LICENSE-2.0
+     
+       Unless required by applicable law or agreed to in writing, software
+       distributed under the License is distributed on an "AS IS" BASIS,
+       WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+       See the License for the specific language governing permissions and
+       limitations under the License.
diff --git a/camelConnector/LICENSE/log4j-license.txt b/camelConnector/LICENSE/log4j-license.txt
new file mode 100644
index 00000000..cd89e7d7
--- /dev/null
+++ b/camelConnector/LICENSE/log4j-license.txt
@@ -0,0 +1,203 @@
+
+
+                                     Apache License
+                               Version 2.0, January 2004
+                            http://www.apache.org/licenses/
+     
+       TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+     
+       1. Definitions.
+     
+          "License" shall mean the terms and conditions for use, reproduction,
+          and distribution as defined by Sections 1 through 9 of this document.
+     
+          "Licensor" shall mean the copyright owner or entity authorized by
+          the copyright owner that is granting the License.
+     
+          "Legal Entity" shall mean the union of the acting entity and all
+          other entities that control, are controlled by, or are under common
+          control with that entity. For the purposes of this definition,
+          "control" means (i) the power, direct or indirect, to cause the
+          direction or management of such entity, whether by contract or
+          otherwise, or (ii) ownership of fifty percent (50%) or more of the
+          outstanding shares, or (iii) beneficial ownership of such entity.
+     
+          "You" (or "Your") shall mean an individual or Legal Entity
+          exercising permissions granted by this License.
+     
+          "Source" form shall mean the preferred form for making modifications,
+          including but not limited to software source code, documentation
+          source, and configuration files.
+     
+          "Object" form shall mean any form resulting from mechanical
+          transformation or translation of a Source form, including but
+          not limited to compiled object code, generated documentation,
+          and conversions to other media types.
+     
+          "Work" shall mean the work of authorship, whether in Source or
+          Object form, made available under the License, as indicated by a
+          copyright notice that is included in or attached to the work
+          (an example is provided in the Appendix below).
+     
+          "Derivative Works" shall mean any work, whether in Source or Object
+          form, that is based on (or derived from) the Work and for which the
+          editorial revisions, annotations, elaborations, or other modifications
+          represent, as a whole, an original work of authorship. For the purposes
+          of this License, Derivative Works shall not include works that remain
+          separable from, or merely link (or bind by name) to the interfaces of,
+          the Work and Derivative Works thereof.
+     
+          "Contribution" shall mean any work of authorship, including
+          the original version of the Work and any modifications or additions
+          to that Work or Derivative Works thereof, that is intentionally
+          submitted to Licensor for inclusion in the Work by the copyright owner
+          or by an individual or Legal Entity authorized to submit on behalf of
+          the copyright owner. For the purposes of this definition, "submitted"
+          means any form of electronic, verbal, or written communication sent
+          to the Licensor or its representatives, including but not limited to
+          communication on electronic mailing lists, source code control systems,
+          and issue tracking systems that are managed by, or on behalf of, the
+          Licensor for the purpose of discussing and improving the Work, but
+          excluding communication that is conspicuously marked or otherwise
+          designated in writing by the copyright owner as "Not a Contribution."
+     
+          "Contributor" shall mean Licensor and any individual or Legal Entity
+          on behalf of whom a Contribution has been received by Licensor and
+          subsequently incorporated within the Work.
+     
+       2. Grant of Copyright License. Subject to the terms and conditions of
+          this License, each Contributor hereby grants to You a perpetual,
+          worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+          copyright license to reproduce, prepare Derivative Works of,
+          publicly display, publicly perform, sublicense, and distribute the
+          Work and such Derivative Works in Source or Object form.
+     
+       3. Grant of Patent License. Subject to the terms and conditions of
+          this License, each Contributor hereby grants to You a perpetual,
+          worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+          (except as stated in this section) patent license to make, have made,
+          use, offer to sell, sell, import, and otherwise transfer the Work,
+          where such license applies only to those patent claims licensable
+          by such Contributor that are necessarily infringed by their
+          Contribution(s) alone or by combination of their Contribution(s)
+          with the Work to which such Contribution(s) was submitted. If You
+          institute patent litigation against any entity (including a
+          cross-claim or counterclaim in a lawsuit) alleging that the Work
+          or a Contribution incorporated within the Work constitutes direct
+          or contributory patent infringement, then any patent licenses
+          granted to You under this License for that Work shall terminate
+          as of the date such litigation is filed.
+     
+       4. Redistribution. You may reproduce and distribute copies of the
+          Work or Derivative Works thereof in any medium, with or without
+          modifications, and in Source or Object form, provided that You
+          meet the following conditions:
+     
+          (a) You must give any other recipients of the Work or
+              Derivative Works a copy of this License; and
+     
+          (b) You must cause any modified files to carry prominent notices
+              stating that You changed the files; and
+     
+          (c) You must retain, in the Source form of any Derivative Works
+              that You distribute, all copyright, patent, trademark, and
+              attribution notices from the Source form of the Work,
+              excluding those notices that do not pertain to any part of
+              the Derivative Works; and
+     
+          (d) If the Work includes a "NOTICE" text file as part of its
+              distribution, then any Derivative Works that You distribute must
+              include a readable copy of the attribution notices contained
+              within such NOTICE file, excluding those notices that do not
+              pertain to any part of the Derivative Works, in at least one
+              of the following places: within a NOTICE text file distributed
+              as part of the Derivative Works; within the Source form or
+              documentation, if provided along with the Derivative Works; or,
+              within a display generated by the Derivative Works, if and
+              wherever such third-party notices normally appear. The contents
+              of the NOTICE file are for informational purposes only and
+              do not modify the License. You may add Your own attribution
+              notices within Derivative Works that You distribute, alongside
+              or as an addendum to the NOTICE text from the Work, provided
+              that such additional attribution notices cannot be construed
+              as modifying the License.
+     
+          You may add Your own copyright statement to Your modifications and
+          may provide additional or different license terms and conditions
+          for use, reproduction, or distribution of Your modifications, or
+          for any such Derivative Works as a whole, provided Your use,
+          reproduction, and distribution of the Work otherwise complies with
+          the conditions stated in this License.
+     
+       5. Submission of Contributions. Unless You explicitly state otherwise,
+          any Contribution intentionally submitted for inclusion in the Work
+          by You to the Licensor shall be under the terms and conditions of
+          this License, without any additional terms or conditions.
+          Notwithstanding the above, nothing herein shall supersede or modify
+          the terms of any separate license agreement you may have executed
+          with Licensor regarding such Contributions.
+     
+       6. Trademarks. This License does not grant permission to use the trade
+          names, trademarks, service marks, or product names of the Licensor,
+          except as required for reasonable and customary use in describing the
+          origin of the Work and reproducing the content of the NOTICE file.
+     
+       7. Disclaimer of Warranty. Unless required by applicable law or
+          agreed to in writing, Licensor provides the Work (and each
+          Contributor provides its Contributions) on an "AS IS" BASIS,
+          WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+          implied, including, without limitation, any warranties or conditions
+          of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+          PARTICULAR PURPOSE. You are solely responsible for determining the
+          appropriateness of using or redistributing the Work and assume any
+          risks associated with Your exercise of permissions under this License.
+     
+       8. Limitation of Liability. In no event and under no legal theory,
+          whether in tort (including negligence), contract, or otherwise,
+          unless required by applicable law (such as deliberate and grossly
+          negligent acts) or agreed to in writing, shall any Contributor be
+          liable to You for damages, including any direct, indirect, special,
+          incidental, or consequential damages of any character arising as a
+          result of this License or out of the use or inability to use the
+          Work (including but not limited to damages for loss of goodwill,
+          work stoppage, computer failure or malfunction, or any and all
+          other commercial damages or losses), even if such Contributor
+          has been advised of the possibility of such damages.
+     
+       9. Accepting Warranty or Additional Liability. While redistributing
+          the Work or Derivative Works thereof, You may choose to offer,
+          and charge a fee for, acceptance of support, warranty, indemnity,
+          or other liability obligations and/or rights consistent with this
+          License. However, in accepting such obligations, You may act only
+          on Your own behalf and on Your sole responsibility, not on behalf
+          of any other Contributor, and only if You agree to indemnify,
+          defend, and hold each Contributor harmless for any liability
+          incurred by, or claims asserted against, such Contributor by reason
+          of your accepting any such warranty or additional liability.
+     
+       END OF TERMS AND CONDITIONS
+     
+       APPENDIX: How to apply the Apache License to your work.
+     
+          To apply the Apache License to your work, attach the following
+          boilerplate notice, with the fields enclosed by brackets "[]"
+          replaced with your own identifying information. (Don't include
+          the brackets!)  The text should be enclosed in the appropriate
+          comment syntax for the file format. We also recommend that a
+          file or class name and description of purpose be included on the
+          same "printed page" as the copyright notice for easier
+          identification within third-party archives.
+     
+       Copyright [yyyy] [name of copyright owner]
+     
+       Licensed under the Apache License, Version 2.0 (the "License");
+       you may not use this file except in compliance with the License.
+       You may obtain a copy of the License at
+     
+           http://www.apache.org/licenses/LICENSE-2.0
+     
+       Unless required by applicable law or agreed to in writing, software
+       distributed under the License is distributed on an "AS IS" BASIS,
+       WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+       See the License for the specific language governing permissions and
+       limitations under the License.
\ No newline at end of file
diff --git a/camelConnector/LICENSE/slf4j-license.txt b/camelConnector/LICENSE/slf4j-license.txt
new file mode 100644
index 00000000..2aa85a10
--- /dev/null
+++ b/camelConnector/LICENSE/slf4j-license.txt
@@ -0,0 +1,15 @@
+Copyright (c) 2004-2017 QOS.ch
+All rights reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
+documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
+rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
+persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
+Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file
diff --git a/camelConnector/README.md b/camelConnector/README.md
new file mode 100644
index 00000000..d8f6f540
--- /dev/null
+++ b/camelConnector/README.md
@@ -0,0 +1,276 @@
+# Overview
+
+The following documentation outlines how to incorporate an Apache Camel Connector Source into part of your project.
+This allows a user to 
+construct Apache Camel applications that interact with Vantiq.
+
+The Apache Camel Connector constructs the environment necessary to run an Apache Camel Application.
+It uses the [Vantiq Camel Component](../camelComponent/README.md) (if necessary). The building of an Apache Camel
+application usually involves building the application, including the dependencies, using the Maven system
+(constructing POM.xml files, etc.).
+
+The Vantiq Apache Camel Connector is configured with the Camel application desired, and, using that definition,
+discovers, downloads, provisions the connector, running the application using the appropriate Apache Camel
+Components as specified in the routes included in the configuration.  Within the Vantiq system, the connection point will be a Vantiq Source.
+
+In order to incorporate this Extension Source, you will need to create the
+Source in the VANTIQ Modelo IDE. The documentation 
+has been split into two parts, [Setting Up Your Machine](#machine) and [Setting Up Your VANTIQ Modelo IDE](#vantiq).
+
+## Details
+
+When using the Camel connector, the connector makes a discovery pass to find the components required.  Then, it
+downloads them and constructs a class loader including the components needed. Thus, when using the connector,
+you need only supply the routes (and any specific connection properties you may need),
+and the connector finds & resolves the Apache Components & Dataformats required,
+and then runs your application.  We generally expect this will involve Vantiq, but it need not.
+
+Discovery, at a high level, operates more or less as follows. An Apache Camel system operates 
+with what is called a CamelContext. This provides the directory of directories to what's going on.
+Within the context, one can replace the component resolver and data format resolver -- there are a
+number of special purpose resolvers within the Camel system.
+Generally, the default ones know how to load the components required via the metadata in the jar files.
+
+In the camel connector, discovery operates by providing its own so-called `Enumerating`
+component & data format resolvers. These operate by saving the information about what is required, 
+and providing a `PlaceHolder` parts to allow Apache Camel to think it is operating. Once Camel
+thinks it has successfully started, the connector stops it, using the information from
+the `Enumerating`*`Resolver`s to get the list of components and data formats required.
+
+From that list, we construct the set of libraries required and download those libraries and their 
+dependencies. This is done using Apache Ivy. Once downloaded, we construct a `URLClassLoader`
+that we set up as Camel's application class loader, 
+construct a new CamelContext (with that class loader but no special resolvers), 
+and let Camel operate the Camel application in its normal manner.
+
+# Prerequisites <a name="pre" id="pre"></a>
+
+**IMPORTANT:** Read the [Testing](#testing) section before building this project.
+
+An understanding of the VANTIQ Extension Source SDK is assumed. Please read the [Extension Source README.md](../README.md) for more 
+information.
+
+The user must [define the Apache Camel Connector Source implementation](../README.md#-defining-a-typeimplementation)
+in the Vantiq system. For an example of the definition, 
+please see the [*camelConnectorImpl.json*](src/test/resources/camelConnectorImpl.json) file
+located in the *src/test/resources* directory.
+
+# Setting Up Your Machine <a name="machine" id="machine"></a>
+
+## Repository Contents
+
+The _connector_ directory (`src/main/java/io/vantiq/extsrc/camelconn/connector`) contains the following files:
+
+*   **CamelMain** -- The main function for the program. Connects to sources as specified in a
+    configuration file.
+*   **CamelCore** -- Coordinates the connections to the database, and sends the resulting data back to VANTIQ Modelo if
+    necessary.
+*   **CamelHandleConfiguration** -- Sets up the Apache Camel routes based on the source's configuration document, and
+    initializes the queryHandler and publishHandler.
+
+The _discovery_ directory (`src/main/java/io/vantiq/extsrc/camelconn/discover`) contains a variety of classes
+implementing the discovery, resolution, and provisioning of the components necessary to run the Apache Camel
+application configured as part of the source.
+
+## How to Run the Program <a name="runtheconnector" id="runtheconnector"></a>
+
+1.  Clone this repository (vantiq-extension-sources) and navigate into `<repo location>/vantiq-extension-sources`.
+2.  Run `./gradlew camelConnector:assemble`.
+3.  Navigate to `<repo location>/vantiq-extension-sources/camelConnector/build/distributions`. The zip and tar files both contain 
+    the same files, so choose whichever you prefer.
+4.  Uncompress the file in the location that you would like to install the program.
+5.  Run `<install location>/camelConnector/bin/camelConnector` with a local server.config file or specifying the [server config file](#serverConfig) as the first argument. Note that the `server.config` file can be placed in the `<install location>/camelConnector/serverConfig/server.config` or `<install location>/camelConnector/server.config` locations.
+
+### Changing Camel Versions
+
+The camelConnector has been tested with the version of Apache Camel defined in the `build.gradle` file.
+If you wish to change this, you will need to perform the following actions:
+
+1. Change the `camelVersion` property in the `build.gradle` file.
+2. Update the `artifactMap.json` found in `src/main/resources`. This file is the list of components and data formats provided by the Camel version in question.  To update this map, you will perform the following actions.
+   1. Clone the [Apache Camel source repository](https://github.com/apache/camel)
+   2. Set the `camelRoot` gradle property to the  root directory of the cloned repo above.
+   3. Run the `./gradlew camelConnector:generateComponentList` command.  This will take a little while as it examines the source code in question to determine the set of components and data formats provided. This list is used when determining the set of libraries required to provision the connector for your Apache Camel application.
+
+## Logging
+To change the logging settings, edit the logging config file
+`<install location>/camelConnector/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".  
+
+## Server Config File
+(Please read the [SDK's server config documentation](../extjsdk/README.md#serverConfig) first.)
+
+### Vantiq Options
+*   **authToken**: Required. The authentication token to connect with. These can be obtained from the namespace admin.
+*   **sources**: Required. A comma separated list of the sources to which you wish to connect. Any whitespace will be
+    removed when read.
+*   **targetServer**: Required. The Vantiq server hosting the sources.
+
+# Setting Up Your VANTIQ Modelo IDE <a name="vantiq" id="vantiq"></a>
+
+## Source Configuration
+
+To set up the Source in the VANTIQ Modelo IDE, you will need to add a Source to your project. Please check the [Prerequisites](#pre) 
+to make sure you have properly added a Source Definition to VANTIQ Modelo. Once this is complete,
+you can select CAMEL_CONNECTOR
+(or whatever you named your Source Definition) as the Source Type. You will then need to provide the
+Source Configuration Document.
+
+The Configuration document may look similar to the following example:
+
+```json
+    {
+       "camelRuntime": {
+         "appName": "myCamelAppName",
+         "routesDocument": "someVantiqDocName.xml"
+       },
+      "general": {
+      }
+    }
+```
+
+### Options Available for camelRuntime
+
+*   **appName**: Optional -- name of the app.  If not provided, default to _camelConnectorApp_.
+*   **routesDocument** or **routesList** and **routesFormat**: Required. This is list of Camel routes to run.
+    * **routesDocument** -- the name of a Vantiq Document containing the XML or YAML definition of the routes to run. The format of the document (XML or YAML) will be determined from the file name (_e.g._, `routes.xml`)
+    * **routesList** and **routesFormat** -- the XML or YAML specification of the Camel routes to run.  The **routesList** property should contain the appropriate text, and the **routesFormat** should contain either `xml` or `yml` indicating the style used to specify the routes.
+    * *Note*: You must specify either the **routesDocument** or **routesList** _AND_ **routesFormat** properties, but not both.
+
+### Options available for general
+
+*   **componentCacheDirectory** -- (Optional) the name of a directory into which to place the downloaded components for use later.  If absent, defaults to `componentCache`.
+*   **componentLibraryDirectory** -- (Optional) The name of the directory into which to place component libraries (and associated dependencies) for running.  It is from here that the required component classes will be loaded.  If not present, defaults to `componentLib`.
+*   **repoList** - (Optional) A list of URLs for Maven repositories from which to load components.  If absent, defaults to _maven central_.
+*   **additionalLibraries** -- (Optional) A list of libraries to include _in addition to_ those found via discovery. These should be specified as "<group>:<name>:<revision>" triples.
+
+### Defining Camel Routes that include Vantiq
+
+The [Vantiq Camel Component](../camelComponent/README.md) has details on how to define a Vantiq Camel Endpoint.
+The Vantiq Camel Component, itself, is not a full-connector -- it uses the connector protocol to
+manifest the component.
+
+That said, the Vantiq Camel Connector is a full connector, requiring the `server.config` file 
+as outlined [here](#runtheconnector).
+In most cases, the Camel Application will contain Vantiq endpoints,
+and these endpoints will refer to the Vantiq server specified in the `server.config` file.
+
+Rather than re-specifying this information, the Vantiq Camel Connector provides a mechanism
+whereby routes including Vantiq endpoints can refer to the Vantiq specified in the `server.config` file. 
+This is done by specifying a Vantiq endpoint
+that uses `server.config` as the host name.  That is, specify a route like the following route
+that accepts message from the Vantiq server specified in the connector's `server.config` file and logs the result.
+
+```xml
+    <routes xmlns="http://camel.apache.org/schema/spring" >
+        <route id="xml-route">/
+            <from uri="vantiq://server.config"/>
+            <to uri="log:INFO"/>
+        </route>
+    </routes>
+```
+
+#### Connector Operation
+
+Upon startup (or when the Vantiq Source configuration changes), the Camel Connector will examine the configuration
+to determine the route(s) to run. To configure itself, the connector will perform the following actions:
+
+* Read the configuration and determine the route(s) to be used.
+* Start the Camel routes in an internal _discovery_ mode where the components required to run the routes are
+discovered.
+* Based on the discovered components, download the required libraries and their dependencies.
+    * If the **additionalLibraries** property is provided as part of the configuration, the libraries listed
+therein are downloaded as well.
+    * The downloads happen based on the Maven Central repository, unless the **repoList** property is specified
+in the configuration.  If that is present, that list is used _instead_ of the Maven Central repository. Thus, if
+one wishes to add a repo in addition to Maven Central, one must include the Maven Central repository in the list.
+This allows the configurer to override things for testing, etc.
+* Once the downloads are completed, the runtime is augmented with the required components,
+and the Camel application is started (in _normal_ mode) to run the route(s) requested.
+* This discovery process happens on each restart or reconfiguration.
+However, libraries already downloaded will not be downloaded again (unless a version update is required).
+    * To force a new download, clear the directory specified (or defaulted) by the **componentCacheDirectory**
+configuration property.
+
+## Messages to the Source
+
+Messages are sent to the source as _notifications_, and are delivered as _events_ to the associated source.
+Messages sent to Vantiq from the component/connector will
+arrive as VAIL objects, where the property names correspond to the Map keys.
+The Vantiq Component expects messages in an exchange to arrive in the form of a Java Map.  However, messages arriving
+in Json format will be accepted as well.
+When constructing your Camel Application, you must keep this in mind.
+
+## Messages from the Source
+
+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.  
+
+## Select Statements
+
+In order to interact with the Apache application, one option is to use VAIL to select from the source. To do this, you will need 
+to specify query message using the WITH clause on the VAIL SELECT statement. The data will be returned to 
+VANTIQ as a set of _rows_, where each _row_ contains a set of properties.
+
+The following example uses a VAIL Select Statement to **query** an application:
+```js
+PROCEDURE queryCamel()
+
+try {        
+    // Normal SELECT Statement in VAIL, but using WITH Clause is important.
+    SELECT ONE * from SOURCE camelConnectorSource as result WITH 
+        value1: "some value",
+        value2: 35
+     
+    // We expect a result with a property named "answer"...
+    
+    var ourAnswer = result.answer
+    
+} catch (error) {
+    // Catching any errors and throwing the exception.
+    exception(error.code, error.message)
+}
+```
+
+In this example, we expect only one row to be returned.
+
+## Publish Statements <a name="publish" id="publish"></a>
+
+Another method to interact with the Camel application is to use VAIL to publish to the source. 
+To do this, you will need to send the desired VAIL object to your application using the Publish parameters.
+The following VAIL procedure will send data to the Camel application by sending an event to the associated source.
+
+```js
+PROCEDURE publishToCamel(valueToSend Integer)
+    PUBLISH {value: valueToSend} to SOURCE camelConnectorSource
+```
+
+## Error Messages
+
+Query errors originating from the source will include the code and error message.
+They may also include the cause (the underlying exception).
+
+
+## Testing <a name="testing" id="testing"></a>
+
+In order to properly run the tests, you must add properties to your _gradle.properties_ file in the _~/.gradle_
+directory. These properties include the Target VANTIQ Server URL, as well as an Access Token for that server. 
+The Target VANTIQ Server and Auth Token will be used to create a temporary VANTIQ Source, VANTIQ Type, 
+VANTIQ Topic, VANTIQ Procedure and VANTIQ Rule.
+Some tests may require other gradle properties as well.
+
+* **NOTE:** We strongly encourage users to create a unique VANTIQ Namespace in order to ensure that tests do not
+accidentally override any existing Sources or Types.
+
+## 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, Apache Camel, Ivy, and Apache Ivy 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/camelConnector/build.gradle b/camelConnector/build.gradle
new file mode 100644
index 00000000..0e943e3d
--- /dev/null
+++ b/camelConnector/build.gradle
@@ -0,0 +1,286 @@
+import groovy.json.JsonOutput
+import groovy.json.JsonSlurper
+import groovy.namespace.QName
+import groovy.xml.XmlParser
+
+plugins {
+    id 'java'
+    id 'maven-publish'
+    id 'application'
+}
+
+mainClassName = 'io.vantiq.extsrc.camelconn.connector.CamelMain'
+
+startScripts{
+    doLast{
+        def windowsScriptFile = file getWindowsScript()
+        def unixScriptFile = file getUnixScript()
+        // Add the log Config to the top of the classpath
+        windowsScriptFile.text = windowsScriptFile.text.replace("CLASSPATH=", "CLASSPATH=%APP_HOME%\\logConfig;")
+        unixScriptFile.text = unixScriptFile.text.replace('CLASSPATH=', 'CLASSPATH=$APP_HOME/logConfig:')
+    }
+}
+
+// Copies the logger setup into logConfig in the distribution
+applicationDistribution.from("src/main/resources") {
+    include "log4j2.xml"
+    into "logConfig"
+}
+
+repositories {
+    mavenCentral()
+}
+
+ext {
+    apacheCommonsLangVersion = '3.12.0'
+    camelVersion='3.18.3'
+    dnsJavaVersion = '3.4.2'
+    extjsdkVersion='1.0'
+    gsonVersion = '2.9.1'
+    ivyVersion = '2.5.1'
+    jacksonVersion = '2.14.2'
+    junitVersion = '4.13.2'
+    junitVintageEngineVersion = '5.8.2'
+    junitVersion = '4.13.2'
+    log4JVersion='2.17.2'
+}
+
+dependencies {
+    annotationProcessor "org.projectlombok:lombok:${lombokVersion}"
+    implementation "org.projectlombok:lombok:${lombokVersion}"
+
+    implementation "org.apache.camel:camel-core:${camelVersion}"
+    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.google.code.gson:gson:${gsonVersion}"
+    // Used for downloading route documents
+    implementation "io.vantiq:vantiq-sdk:${vantiqSDKVersion}"
+
+    // The following are the DSL libraries to include for support of the various route builders.
+    // We need to include them in the base product (as oppose to using our constructed classloader)
+    // since we need to parse the routes to determine what to extend via our constructed classloader.
+    implementation "org.apache.camel:camel-xml-io-dsl:${camelVersion}"
+    implementation "org.apache.camel:camel-yaml-dsl:${camelVersion}"
+    // End of DSL-specific inclusions.
+
+    testImplementation "org.apache.logging.log4j:log4j-slf4j-impl:${log4JVersion}"
+    testImplementation "org.apache.camel:camel-test:${camelVersion}"
+    testImplementation "io.vantiq:vantiq-sdk:${vantiqSDKVersion}"
+    testImplementation project(path: ":extjsdk", configuration: 'testArtifacts')
+    testImplementation "dnsjava:dnsjava:${dnsJavaVersion}"
+    testImplementation("junit:junit:${junitVersion}")
+    testRuntimeOnly("org.junit.vintage:junit-vintage-engine:${junitVintageEngineVersion}")
+}
+
+group = 'io.vantiq.extsrc.camelconnector'
+version = 'unspecified'
+description = 'Vantiq Camel Connector'
+java.sourceCompatibility = 1.11
+java.targetCompatibility = JavaVersion.VERSION_11
+
+publishing {
+    publications {
+        maven(MavenPublication) {
+            from(components.java)
+            pom {
+                licenses {
+                    license {
+                        name = 'The MIT License'
+                        url = 'https://opensource.org/licenses/MIT'
+                    }
+                }
+            }
+        }
+    }
+}
+
+tasks.withType(JavaCompile) {
+    options.encoding = 'UTF-8'
+}
+
+/**
+ * This task generates a JSON file containing a map from the component scheme to the required component
+ * This is used when we are discovering components.
+ *
+ * This task requires the gradle property 'camelRoot' is defined, and that it points to the root directory
+ * of a git repo representing the version of camel against which we expect to run.  We use this to scan
+ * the collected pom.xml & META-INF service definitions to build up the map mentioned above.
+ *
+ * We consider this up to date when the version in the existing file matches the version of camel against
+ * which we are building/running.
+ *
+ * The file produced by this task is added to the project's jar file, so there is a dependency from the jar
+ * task to this task.
+ */
+
+final String ARTIFACT_MAP_PATH = project.projectDir.absolutePath + '/src/main/resources'
+final String ARTIFACT_MAP_FILE = ARTIFACT_MAP_PATH + '/artifactMap.json'
+tasks.register('generateComponentList', ) {
+    String[] camVersionParts = camelVersion.tokenize('.')
+    it.outputs.file(ARTIFACT_MAP_FILE)
+    it.outputs.upToDateWhen ({ param ->
+        def f = file(ARTIFACT_MAP_FILE)
+        logger.debug('Exists: {} :: {}', f.exists(), f.getPath())
+        if (f.exists()) {
+            Map artMap = new JsonSlurper().parse(f)
+            def fileVersion = artMap.get('camelVersion')
+            def fileVersionParts = fileVersion.tokenize('.')
+            logger.error('UpToDate: {} vs. build version {}', fileVersion.substring(0,4), camelVersion.substring(0,4))
+            return fileVersionParts[0] == camVersionParts[0] &&
+                fileVersionParts[1] == camVersionParts[1]
+        }
+        logger.debug('Returning false')
+        return false
+    })
+    doFirst {
+        if (!rootProject.hasProperty('camelRoot')) {
+            throw new GradleException('"camelRoot" property is required to generate the component list')
+        }
+    }
+    doLast {
+        def camRootSpec = rootProject.getProperty('camelRoot')
+        logger.info('CamelRootSpec: {}', camRootSpec)
+        def camelRoot = file(camRootSpec)
+        logger.info('Camel root is {}', camelRoot.absolutePath)
+        def components = file("${camelRoot.absolutePath}/components")
+        logger.info('Components is: {}', components.absolutePath)
+        def compDef = fileTree(dir: components,
+            include: '**/src/**/META-INF/services/org/apache/camel/component/*').getFiles()
+        logger.info("Found {} component definition files.", compDef.size())
+        logger.debug("Component definition files: ")
+        Map<String, String> compMap = [:]
+
+        boolean foundErrors = false
+        compDef.each { aComp ->
+            def f = file(aComp)
+            if (!f.path.contains("target/classes/META-INF")) {
+                logger.debug("    --> {} at {}", f.name, f.parent)
+                def pomLoc = file(aComp.parent + "../../../../../../../../../../pom.xml")
+                logger.debug("    --> (pom.xml exists): {} :: {}", pomLoc.exists(), pomLoc.path)
+                def parsed = new XmlParser().parse(pomLoc)
+                def artifactId = parsed.artifactId
+                // Getting a node named 'parent' (the simple way) conflicts with the getting the parent node
+                // Things tend to work, but let's do things properly so that we can avoid complications in the future.
+                def parentNodeName = new QName(parsed.name().namespaceURI, "parent")
+                String componentVersion = parsed.getAt(parentNodeName).version.text()
+                if (artifactId.text().endsWith('-parent')) {
+                    logger.error(">>>> This is a parent <<<<")
+                    foundErrors = true
+                }
+                String[] compVersionParts = componentVersion.tokenize('.')
+                if (compVersionParts[0] != camVersionParts[0] ||
+                    compVersionParts[1] != camVersionParts[1]) {
+                    logger.error('Version mismatch -- component version {} vs our camelVersion: {}',
+                        componentVersion, camelVersion)
+                    foundErrors = true
+                }
+                logger.debug('Artifact {} produced by pom.xml file : {}', artifactId.text(), pomLoc.path)
+                logger.debug('Scheme {} requires Artifact {}', aComp.name, artifactId.text())
+                if (compMap[aComp.name] != null) {
+                    if (compMap[aComp.name] != artifactId.text()) {
+                        foundErrors = true
+                        logger.error("Scheme {} has artifact name confusion/collision: {} vs {}", aComp.name,
+                            compMap[aComp.name], artifactId.text())
+                    }
+                }
+                compMap.put(aComp.name, artifactId.text())
+            } else {
+                logger.debug("            (skipping {} since it's a build artifact duplicate)", aComp.path)
+            }
+        }
+
+        def dfDef = fileTree(dir: components,
+            include: '**/src/**/META-INF/services/org/apache/camel/dataformat/*').getFiles()
+        logger.info("Found {} dataformat definition files.", dfDef.size())
+        logger.debug("DataFormat definition files: ")
+        Map<String, String> dfMap = [:]
+
+        dfDef.each { df ->
+            def f = file(df)
+            if (!f.path.contains("target/classes/META-INF")) {
+                logger.debug("    --> {} at {}", f.name, f.parent)
+                def pomLoc = file(df.parent + "../../../../../../../../../../pom.xml")
+                logger.debug("    --> (pom.xml exists): {} :: {}", pomLoc.exists(), pomLoc.path)
+                def parsed = new XmlParser().parse(pomLoc)
+                def artifactId = parsed.artifactId
+                // Getting a node named 'parent' (the simple way) conflicts with the getting the parent node
+                // Things tend to work, but let's do things properly so that we can avoid complications in the future.
+                def parentNodeName = new QName(parsed.name().namespaceURI, "parent")
+                String componentVersion = parsed.getAt(parentNodeName).version.text()
+                if (artifactId.text().endsWith('-parent')) {
+                    logger.error(">>>> This is a parent <<<<")
+                    foundErrors = true
+                }
+                String[] compVersionParts = componentVersion.tokenize('.')
+                if (compVersionParts[0] != camVersionParts[0] ||
+                    compVersionParts[1] != camVersionParts[1]) {
+                    logger.error('Version mismatch -- component version {} vs our camelVersion: {}',
+                        componentVersion, camelVersion)
+                    foundErrors = true
+                }
+                logger.debug('Artifact {} produced by pom.xml file : {}', artifactId.text(), pomLoc.path)
+                logger.debug('Scheme {} requires Artifact {}', df.name, artifactId.text())
+                if (dfMap[df.name] != null) {
+                    if (dfMap[df.name] != artifactId.text()) {
+                        foundErrors = true
+                        logger.error("Scheme {} has artifact name confusion/collision: {} vs {}", df.name,
+                            dfMap[df.name], artifactId.text())
+                    }
+                }
+                dfMap.put(df.name, artifactId.text())
+            } else {
+                logger.debug("            (skipping {} since it's a build artifact duplicate)", df.path)
+            }
+        }
+
+        if (foundErrors) {
+            throw new GradleException("Found errors gathering scheme to artifact mapping.")
+        }
+        def artifactDef = [ camelVersion: camelVersion,
+                            components: compMap,
+                            dataformats: dfMap]
+        def jsonPretty = JsonOutput.prettyPrint(JsonOutput.toJson(artifactDef))
+        def compFile = file(ARTIFACT_MAP_FILE)
+        if (compFile.exists()) {
+            compFile.delete()
+        }
+        compFile.createNewFile()
+        compFile.write(jsonPretty)
+    }
+}
+
+tasks.withType(Test) {
+    if (rootProject.hasProperty("TestAuthToken")) {
+        systemProperty "TestAuthToken", rootProject.findProperty("TestAuthToken") ?: "empty"
+    }
+    if (rootProject.hasProperty("TestVantiqServer")) {
+        systemProperty "TestVantiqServer", rootProject.findProperty("TestVantiqServer") ?: "empty"
+    }
+    // Use the build dir as a base to get our various test artifacts.
+    systemProperty "buildDir", "${buildDir}"
+
+    if (rootProject.hasProperty("camel-salesforce-loginUrl")) {
+        systemProperty "camel-salesforce-loginUrl", rootProject.findProperty("camel-salesforce-loginUrl") ?: "empty"
+    }
+    if (rootProject.hasProperty("camel-salesforce-clientId")) {
+        systemProperty "camel-salesforce-clientId", rootProject.findProperty("camel-salesforce-clientId") ?: "empty"
+    }
+    if (rootProject.hasProperty("camel-salesforce-clientSecret")) {
+        systemProperty "camel-salesforce-clientSecret",
+            rootProject.findProperty("camel-salesforce-clientSecret") ?: "empty"
+    }
+    if (rootProject.hasProperty("camel-salesforce-refreshToken")) {
+        systemProperty "camel-salesforce-refreshToken",
+            rootProject.findProperty("camel-salesforce-refreshToken") ?: "empty"
+    }
+}
+
+test {
+    useJUnitPlatform()
+}
diff --git a/camelConnector/examples/salesforce/README.md b/camelConnector/examples/salesforce/README.md
new file mode 100644
index 00000000..042d2c05
--- /dev/null
+++ b/camelConnector/examples/salesforce/README.md
@@ -0,0 +1,36 @@
+This directory contains a simple example for connecting a Vantiq system to Salesforce using an Apache Camel application.
+
+# SalesforceExample Project
+
+## Contents
+
+The project file is in `projects/salesforceExample.com`.  That project contains the following, all under the salesforceExample directory:
+
+* **documents/sfGetTasks.yml** -- the YAML definition of the Apache Camel application to fetch 
+the list of Tasks from the Salesforce instance.
+* **sourceimpls/CAMEL_CONNECTOR.json** -- The Camel Connector source implementation definition.
+* **sources/sfSource.json** -- a source definition for a source named `sfSource`.  This source makes use of the Camel Apache application, and can be queried to find the list of Salesforce Tasks.
+* **procedures/getSFTasks.vail** -- a simple procedure that queries the Camel Connector for the list of Salesforce Tasks, then returns the one whose `Subject` is desired.
+
+
+## Requirements/Assumptions
+
+The `sources/sfSource.json` configuration assumes that there are three (3) secrets present:
+
+* **sfClientId** -- the Client Id (_aka_ _Consumer Key_) to gain access to SalesForce
+* **sfClientSecret** -- the Client Secret (_aka_ _Consumer Secret_) to gain access to Salesforce
+* **sfRefreshToken** -- A _refresh token_ used (using the _refresh token flow_) to gain access to Salesforce.
+
+These are used to configure the Salesforce component in the Camel Connector.
+These secrets must be defined before the connector makes a connection to the Vantiq server.
+They are used in the `sfSource` configuration.
+
+## Running the Example Project
+
+1. Import the project. This will create the source and add the configuration.
+1. Add the secrets mentioned in the previous section.
+1. Deploy the CamelConnector, setting the source name to `sfSource` and adding the appropriate connection information.
+1. Start the connector.  It should connect to Vantiq, get the configuration, load the classes required, and start the Camel application.
+1. Run the `getSFTasks` procedure.  This will get the list of tasks in your SalesForce instance.
+
+
diff --git a/camelConnector/examples/salesforce/projects/salesforceExample.zip b/camelConnector/examples/salesforce/projects/salesforceExample.zip
new file mode 100644
index 00000000..cb168a6d
Binary files /dev/null and b/camelConnector/examples/salesforce/projects/salesforceExample.zip differ
diff --git a/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/connector/CamelCore.java b/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/connector/CamelCore.java
new file mode 100644
index 00000000..b6b9c362
--- /dev/null
+++ b/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/connector/CamelCore.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright (c) 2023 Vantiq, Inc.
+ *
+ * All rights reserved.
+ * 
+ * SPDX: MIT
+ */
+
+package io.vantiq.extsrc.camelconn.connector;
+
+import io.vantiq.extjsdk.ExtensionServiceMessage;
+import io.vantiq.extjsdk.ExtensionWebSocketClient;
+import io.vantiq.extjsdk.Handler;
+import io.vantiq.extsrc.camel.utils.ClientRegistry;
+import io.vantiq.extsrc.camelconn.discover.CamelRunner;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Controls the connection and interaction with the Vantiq server. Initialize it and call start() and it will run 
+ * itself. start() will return a boolean describing whether it succeeded, and will wait up to 10 seconds if
+ * necessary.
+ */
+@Slf4j
+public class CamelCore {
+
+    String sourceName;
+    String authToken;
+    String targetVantiqServer;    
+    
+    CamelHandleConfiguration camelConfigHandler;
+    ExtensionWebSocketClient    client  = null;
+    final static int RECONNECT_INTERVAL = 5000;
+    private static final String SYNCH_LOCK = "synchLock";
+
+    /**
+     * Stops sending messages to the source and tries to reconnect, closing on a failure
+     */
+    public final Handler<ExtensionServiceMessage> reconnectHandler = new Handler<ExtensionServiceMessage>() {
+        @Override
+        public void handleMessage(ExtensionServiceMessage message) {
+            log.info("Reconnect message received. Reinitializing configuration");
+            // Do connector-specific stuff here
+            camelConfigHandler.configComplete = false;
+            
+            // Boilerplate reconnect method -- if reconnect fails then we call close(). The code in this reconnect
+            // handler must finish executing before we can process another message from Vantiq, meaning the
+            // reconnectResult will not complete until after we have exited the handler.
+            
+            CompletableFuture<Boolean> reconnectResult = client.doCoreReconnect();
+            reconnectResult.thenAccept(success -> {
+                log.info("Reconnect ran with success value: {}", success);
+    
+                if (!success) {
+                    close();
+                }
+            });
+        }
+    };
+    
+    /**
+     * Stops sending messages to the source and tries to reconnect indefinitely
+     */
+    public final Handler<ExtensionWebSocketClient> closeHandler = new Handler<ExtensionWebSocketClient>() {
+        @Override
+        public void handleMessage(ExtensionWebSocketClient message) {
+            log.trace("WebSocket closed unexpectedly. Attempting to reconnect");
+   
+            camelConfigHandler.configComplete = false;
+            
+            boolean sourcesSucceeded = false;
+            while (!sourcesSucceeded) {
+                client.initiateFullConnection(targetVantiqServer, authToken);
+                sourcesSucceeded = exitIfConnectionFails(client, 10);
+                if (!sourcesSucceeded) {
+                    try {
+                        Thread.sleep(RECONNECT_INTERVAL);
+                    } catch (InterruptedException e) {
+                        log.error("An error occurred when trying to sleep the current thread. Error Message: ", e);
+                    }
+                }
+            }
+        }
+    };    
+    
+    /**
+     * Creates a new CamelCore with the settings given.
+     * @param sourceName            The name of the source to which to connect.
+     * @param authToken             The authentication token to use to connect.
+     * @param targetVantiqServer    The url to connect to.
+     */
+    public CamelCore(String sourceName, String authToken, String targetVantiqServer) {
+        this.sourceName = sourceName;
+        this.authToken = authToken;
+        this.targetVantiqServer = targetVantiqServer;
+    }
+    
+    /**
+     * Returns the name of the source to which this instance is connected.
+     * @return  The name of the source this instance represents.
+     */
+    public String getSourceName() {
+        return sourceName;
+    }
+    
+    /**
+     * Tries to connect to a source and waits up to {@code timeout} seconds before failing and trying again.
+     * @param timeout   The maximum number of seconds to wait before assuming failure and stopping.
+     * @return          true if the source connection succeeds, (will retry indefinitely and never return false).
+     */
+    public boolean start(int timeout) {
+        boolean sourceSucceeded = false;
+        while (!sourceSucceeded) {
+            client = new ExtensionWebSocketClient(sourceName);
+            ClientRegistry.registerClient(sourceName, targetVantiqServer, client);
+            camelConfigHandler = new CamelHandleConfiguration(this);
+            
+            client.setConfigHandler(camelConfigHandler);
+            client.setReconnectHandler(reconnectHandler);
+            client.setCloseHandler(closeHandler);
+            client.initiateFullConnection(targetVantiqServer, authToken);
+            
+            sourceSucceeded = exitIfConnectionFails(client, timeout);
+            if (!sourceSucceeded) {
+                try {
+                    Thread.sleep(RECONNECT_INTERVAL);
+                } catch (InterruptedException e) {
+                    log.error("An error occurred when trying to sleep the current thread. Error Message: ", e);
+                }
+            }
+        }
+        return true;
+    }
+    
+    /**
+     * Closes all resources held by this program except for the {@link ExtensionWebSocketClient}. 
+     */
+    public void close() {
+        
+        if (camelConfigHandler != null) {
+            CamelRunner runner = camelConfigHandler.getCurrentCamelRunner();
+            if (runner != null) {
+                Thread camelThread = runner.getCamelThread();
+    
+                synchronized (SYNCH_LOCK) {
+                    runner.close();
+                    if (camelThread != null) {
+                        try {
+                            camelThread.join(TimeUnit.SECONDS.toMillis(10));
+                        } catch (Exception e) {
+                            // Here, if things go awry, it's not clear what we could do.  Just log it.
+                            log.error("Exception awaiting Camel thread completion", e);
+                        }
+                    }
+                }
+            }
+        }
+    }
+    
+    /**
+     * Closes all resources held by this program and then closes the connection. 
+     */
+    public void stop() {
+        close();
+        ClientRegistry.removeClient(sourceName, targetVantiqServer, client);
+        if (client != null && client.isOpen()) {
+            client.stop();
+            client = null;
+        }
+    }
+
+    /**
+     * Waits for the connection to succeed or fail, logs and exits if the connection does not succeed within
+     * {@code timeout} seconds.
+     *
+     * @param client    The client to watch for success or failure.
+     * @param timeout   The maximum number of seconds to wait before assuming failure and stopping
+     * @return          true if the connection succeeded, false if it failed to connect within {@code timeout} seconds.
+     */
+    public boolean exitIfConnectionFails(ExtensionWebSocketClient client, int timeout) {
+        boolean sourcesSucceeded = false;
+        try {
+            sourcesSucceeded = client.getSourceConnectionFuture().get(timeout, TimeUnit.SECONDS);
+        }
+        catch (TimeoutException e) {
+            log.error("Timeout: full connection did not succeed within {} seconds: {}", timeout, e);
+        }
+        catch (Exception e) {
+            log.error("Exception occurred while waiting for webSocket connection", e);
+        }
+        if (!sourcesSucceeded) {
+            log.error("Failed to connect to all sources.");
+            if (!client.isOpen()) {
+                log.error("Failed to connect to server url '" + targetVantiqServer + "'.");
+            } else if (!client.isAuthed()) {
+                log.error("Failed to authenticate within " + timeout + " seconds using the given authentication data.");
+            } else {
+                log.error("Failed to connect within 10 seconds");
+            }
+            return false;
+        }
+        return sourcesSucceeded;
+    }
+}
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
new file mode 100644
index 00000000..5e13d3e5
--- /dev/null
+++ b/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/connector/CamelHandleConfiguration.java
@@ -0,0 +1,340 @@
+/*
+ * Copyright (c) 2023 Vantiq, Inc.
+ *
+ * All rights reserved.
+ * 
+ * SPDX: MIT
+ */
+
+package io.vantiq.extsrc.camelconn.connector;
+
+import static io.vantiq.client.Vantiq.SystemResources.DOCUMENTS;
+
+import com.google.gson.JsonObject;
+import io.vantiq.client.Vantiq;
+import io.vantiq.client.VantiqResponse;
+import io.vantiq.extjsdk.ExtensionServiceMessage;
+import io.vantiq.extjsdk.Handler;
+import io.vantiq.extsrc.camelconn.discover.CamelRunner;
+import lombok.extern.slf4j.Slf4j;
+import okio.BufferedSource;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+
+/**
+ * Sets up the source using the configuration document, which looks as below.
+ *<pre> {
+ *      camelConfig: {
+ *          camelRuntime: {
+ *               &lt;camel app options&gt;
+ *          }
+ *          general: {
+ *              &lt;general options&gt;
+ *          }
+ *      }
+ * }</pre>
+ *
+ */
+
+@Slf4j
+public class CamelHandleConfiguration extends Handler<ExtensionServiceMessage> {
+    String                  sourceName;
+    CamelCore               source;
+    boolean                 configComplete = false; // Not currently used
+
+    // Constants for getting config options
+    public static final String CAMEL_CONFIG = "config";
+    public static final String CAMEL_APP = "camelRuntime";
+    public static final String APP_NAME = "appName";
+    public static final String APP_NAME_DEFAULT = "camelConnectorApp";
+    public static final String ROUTES_DOCUMENT = "routesDocument";
+    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 VANTIQ = "vantiq";
+    
+    public static final String GENERAL = "general";
+    public static final String COMPONENT_CACHE = "componentCacheDirectory";
+    public static final String COMPONENT_CACHE_DEFAULT = "componentCache";
+    public static final String COMPONENT_LIB = "componentLibraryDirectory";
+    public static final String COMPONENT_LIB_DEFAULT = "componentLib";
+    public static final String REPOSITORY_LIST = "repoList";
+    public static final String ADDITIONAL_LIBRARIES = "additionalLibraries";
+  
+    private static String componentCache;
+    private static String componentLib;
+    
+    private CamelRunner currentCamelRunner = null;
+
+    public CamelHandleConfiguration(CamelCore source) {
+        this.source = source;
+        this.sourceName = source.getSourceName();
+    }
+    
+    /**
+     * Interprets the configuration message sent by the Vantiq server and sets up the Camel Source.
+     */
+    @Override
+    @SuppressWarnings("unchecked")
+    public void handleMessage(ExtensionServiceMessage message) {
+        Map<String, Object> configObject = (Map<String, Object>) message.getObject();
+        Map<String, Object> config;
+        Map<String, Object> vantiq;
+        Map<String, Object> camelConfig;
+        Map<String, Object> general;
+        
+        // Obtain entire config from the message object
+        if ( !(configObject.get(CAMEL_CONFIG) instanceof Map)) {
+            log.error("Configuration failed. No configuration suitable for Camel Source.");
+            failConfig();
+            return;
+        }
+        config = (Map<String, Object>) configObject.get(CAMEL_CONFIG);
+        
+        // Retrieve the Camel application
+        if ( !(config.get(CAMEL_APP) instanceof Map)) {
+            log.error("Configuration failed. Configuration must contain '{}' property.", CAMEL_APP);
+            failConfig();
+            return;
+        }
+        
+        camelConfig = (Map<String, Object>) config.get(CAMEL_APP);
+        // Not currently used. vantiq = (Map<String, Object>) config.get(VANTIQ);
+        general = (Map<String, Object>) config.get(GENERAL);
+        if (general == null) {
+            general = new HashMap<>();
+        }
+        boolean gotList = false;
+        Object target = camelConfig.get(ROUTES_DOCUMENT);
+        if (!(target instanceof String)) {
+            target = camelConfig.get(ROUTES_LIST);
+            gotList = true;
+        }
+        if (!(target instanceof String)) {
+            log.error("Camel connector operation requires that the {} section contain either the {} or {} properties.",
+                      CAMEL_CONFIG, ROUTES_DOCUMENT, ROUTES_LIST);
+            failConfig();
+            return;
+        }
+        
+        target = camelConfig.get(ROUTES_FORMAT);
+        if (gotList && !(target instanceof String)) {
+            log.error("Camel connector with the {} property requires the {} property as well.",
+                      ROUTES_LIST, ROUTES_FORMAT);
+        }
+        
+        target = camelConfig.get(COMPONENT_PROPERTIES);
+        if (target != null) {
+            if (!(target instanceof List)) {
+                log.error("Camel connector property {} should be a list (found {}).",
+                          COMPONENT_PROPERTIES, target.getClass().getName());
+                failConfig();
+                return;
+            } else {
+                boolean failed = false;
+                for (Object obj : (List<?>) target) {
+                    if (!(obj instanceof Map) ||
+                            (((Map<?, ?>) obj).get(CamelRunner.COMPONENT_NAME) == null) ||
+                            (((Map<?, ?>) obj).get(CamelRunner.COMPONENT_PROPERTIES) == null)) {
+                        log.error("Camel connector property {} should be a list of component names and properties.",
+                                  COMPONENT_PROPERTIES);
+                        failed = true;
+                    }
+                }
+                if (failed) {
+                    failConfig();
+                    return;
+                }
+            }
+        }
+        boolean success;
+        try {
+            success = runCamelApp(camelConfig, general);
+        } catch (VantiqCamelException vce) {
+            success = false;
+        }
+        if (!success) {
+            failConfig();
+            return;
+        }
+    
+        log.trace("Setup complete");
+        configComplete = true;
+    }
+    
+    public boolean runCamelApp(Map<String, Object> camelConfig, Map<String, Object> general)
+            throws VantiqCamelException {
+        String appName = null;
+        try {
+            componentCache = (String) general.getOrDefault(COMPONENT_CACHE, COMPONENT_CACHE_DEFAULT);
+            componentLib = (String) general.getOrDefault(COMPONENT_LIB, COMPONENT_LIB_DEFAULT);
+            //noinspection unchecked
+            List<String> repoListRaw = (List<String>) general.get(REPOSITORY_LIST);
+            List<URI> repoList = new ArrayList<>();
+            if (repoListRaw != null) {
+                for (String item : repoListRaw) {
+                    // .forEach here has issues with the URISyntaxException
+                    repoList.add(new URI(item));
+                }
+            }
+            //noinspection unchecked
+            List<String> additionalLibraries = (List<String>) general.get(ADDITIONAL_LIBRARIES);
+            appName = (String) camelConfig.getOrDefault(APP_NAME, APP_NAME_DEFAULT);
+            String routeDocName = (String) camelConfig.get(ROUTES_DOCUMENT);
+            Map<String, String> routeSpec = null;
+            if (routeDocName != null) {
+                routeSpec = fetchDocument(routeDocName);
+                if (routeSpec == null) {
+                    log.error("Camel connector operation requires a valid route specification. " +
+                                      "No document '{}' was found.",
+                              routeDocName);
+                    failConfig();
+                    return false;
+                }
+            } else {
+                String format = (String) camelConfig.get(ROUTES_FORMAT);
+                String routes = null;
+                routes = (String) camelConfig.get(ROUTES_LIST);
+    
+                if (routes != null) {
+                    routeSpec = new HashMap<>();
+                    routeSpec.put(ROUTES_FORMAT, format);
+                    routeSpec.put(ROUTES_LIST, routes);
+                } else {
+                    log.error("Camel connector requires a valid route specification.");
+                    failConfig();
+                    return false;
+                }
+            }
+            
+            // This is checked in the caller
+            //noinspection unchecked
+            List<Map<String, Object>> componentProperties =
+                    (List<Map<String, Object>>) camelConfig.get(COMPONENT_PROPERTIES);
+    
+            // If we get this far, then we're ready to run start from the new configuration.  If we have an old
+            // configuration running, we'll need to shut it down first.
+    
+            if (currentCamelRunner != null) {
+                
+                // TODO: Determine if this config change is really a change.  If not, ignore this change and just
+                //  continue to run.  The config change could have been just due to some change on Vantiq side about
+                //  which a running Camel app doesn't really care. Compute route hash or something to determine if it
+                //  changed.
+    
+                Thread previousThread = currentCamelRunner.getCamelThread();
+                currentCamelRunner.close();
+                if (previousThread != null) {
+                    // Wait for CamelRunner thread to complete
+                    previousThread.join();
+                }
+                currentCamelRunner = null;
+            }
+    
+            // Note:  Cannot use try-with-resource block here.  Since we don't await the run thread, we need to leave
+            // the runner open (no auto-close), so make certain that we don't shut things down before we let things run.
+            // 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),
+                                         routeSpec.get(ROUTES_FORMAT), repoList,
+                                         componentCache, componentLib, componentProperties);
+            if (additionalLibraries != null) {
+                runner.setAdditionalLibraries(additionalLibraries);
+            }
+            runner.runRoutes(false);
+            currentCamelRunner = runner;
+        } catch (URISyntaxException urie) {
+            throw new VantiqCamelException("Invalid repository in " +
+                                                   CAMEL_CONFIG + "." + GENERAL + "." + REPOSITORY_LIST, urie);
+        } catch (Exception e) {
+            throw new VantiqCamelException("Error running Camel App " + appName, e);
+        }
+        return true;
+    }
+    
+    private Map<String, String> fetchDocument(String docName) {
+        String token = source.authToken;
+        String url = source.targetVantiqServer;
+        Vantiq vantiq = new Vantiq(url, 1);
+        vantiq.setAccessToken(token);
+        Map<String, String> routeSpec = null;
+        
+        try {
+            VantiqResponse resp = vantiq.selectOne("system." + DOCUMENTS.value(), docName);
+            if (resp.isSuccess()) {
+                if (resp.getBody() instanceof JsonObject) {
+                    JsonObject doc = (JsonObject) resp.getBody();
+                    String docContents = doc.get("content").getAsString();
+                    log.debug("Fetching contents of document {} from path {}.", docName, docContents);
+                    resp = vantiq.download(docContents);
+                    if (resp.isSuccess()) {
+                        String routeDoc = ((BufferedSource) resp.getBody()).readUtf8();
+                        String routeFormat;
+                        if (docName.endsWith("xml")) {
+                            routeFormat = "xml";
+                        } else if (docName.endsWith("yml") || docName.endsWith("yaml")) {
+                            routeFormat = "yml";
+                        } else {
+                            log.error("Unknown format for document {}.", docName);
+                            return null;
+                        }
+                        routeSpec = new HashMap<>();
+                        routeSpec.put(ROUTES_FORMAT, routeFormat);
+                        routeSpec.put(ROUTES_LIST, routeDoc);
+                        return routeSpec;
+                    } else {
+                        log.error("Could not download contents of document '{}: {}.", docName, resp.getErrors());
+                        return null;
+                    }
+                }
+            } else {
+                log.error("Cannot fetch document: {}: {}", docName, resp.getErrors());
+                return null;
+            }
+        } catch (IOException e) {
+            log.error("Cannot read document '{}' contents.", docName, e);
+        } finally {
+            // code hygiene -- not used below now, but be safe for the future.
+            //noinspection ReassignedVariable
+            vantiq = null;
+        }
+        return null;
+    }
+
+    /**
+     * Closes the source {@link CamelCore} and marks the configuration as completed. The source will
+     * be reactivated when the source reconnects, due either to a Reconnect message (likely created by an update to the
+     * configuration document) or to the WebSocket connection crashing momentarily.
+     */
+    private void failConfig() {
+        source.close();
+        configComplete = true;
+    }
+    
+    /**
+     * Returns whether the configuration handler has completed. Necessary since the sourceConnectionFuture is completed
+     * before the configuration can complete, so a program may need to wait before using configured resources.
+     * @return  true when the configuration has completed (successfully or not), false otherwise
+     */
+    public boolean isComplete() {
+        return configComplete;
+    }
+    
+    /**
+     * Get the current runner.  Useful when poking around in tests...
+     * @return
+     */
+    public CamelRunner getCurrentCamelRunner() {
+        return currentCamelRunner;
+    }
+}
diff --git a/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/connector/CamelMain.java b/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/connector/CamelMain.java
new file mode 100644
index 00000000..4370164a
--- /dev/null
+++ b/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/connector/CamelMain.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (c) 2023 Vantiq, Inc.
+ *
+ * All rights reserved.
+ * 
+ * SPDX: MIT
+ */
+
+package io.vantiq.extsrc.camelconn.connector;
+
+import static io.vantiq.extjsdk.Utils.AUTH_TOKEN_PROPERTY_NAME;
+import static io.vantiq.extjsdk.Utils.SERVER_CONFIG_FILENAME;
+import static io.vantiq.extjsdk.Utils.SOURCES_PROPERTY_NAME;
+import static io.vantiq.extjsdk.Utils.TARGET_SERVER_PROPERTY_NAME;
+
+import io.vantiq.extjsdk.Utils;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Properties;
+
+/**
+ *  The main class of this program. It connects to sources using the specified configuration. All sources must belong to
+ *  the same VANTIQ Server & Namespace (i.e. same authtoken). Options are in a file where each option has its own line,
+ *  and is in the form {@code property=value} with no quotes required for Strings. Options are:
+ *  <ul>
+ *      <li>{@code accessToken}: Required. The authentication token to connect with. These can be obtained from the
+ *                      namespace admin.
+ *      <li>{@code sources}: Required. A comma separated list of the sources to which you wish to connect. Whitespace 
+ *                      around each name will be removed when read.
+ *      <li>{@code targetServer}: Required. The Vantiq server hosting the sources.
+ *  </ul>
+ */
+
+@Slf4j
+public class CamelMain {
+
+    static List<CamelCore> sources;
+        
+    static String accessToken;
+    static String targetVantiqServer;
+    
+    // Exit Error Codes
+    static final int NO_AUTH_EXIT = 1;
+    static final int NO_SOURCE_EXIT = 2;
+    static final int NO_SERVER_EXIT = 3;
+    
+    /**
+     * Connects to the Vantiq source and starts polling for data. Exits when all sources are done running.
+     * @param args  Should be either null or the first argument as a config file
+     */
+    public static void main(String[] args) {
+        Properties config;
+        if (args != null && args.length > 0) {
+            config = Utils.obtainServerConfig(args[0]);
+        } else {
+            config = Utils.obtainServerConfig(SERVER_CONFIG_FILENAME);
+        }
+        
+        sources = createSources(config);
+        
+        startSources(sources);
+        
+        // Can leave now because the threads created by the sources' WebSocket connections will keep the JVM alive
+    }
+    
+    /**
+     * Starts every source, giving each up to 10 seconds to connect. Starts them in new threads so they don't block.
+     * @param sources   The list of sources which should be started.
+     */
+    private static void startSources(List<CamelCore> sources) {
+        for (CamelCore source : sources) {
+            // Starting in threads so they can all connect at once
+            // FIXME -- this should just use camel start up...
+            new Thread( () -> {source.start(10);} ).start();;
+        }
+    }
+    
+    /**
+     * Sets up the defaults for the server based on the configuration file
+     *
+     * @param config    The Properties obtained from the configuration file
+     */
+    static List<CamelCore> createSources(Properties config) {
+        accessToken = config.getProperty(AUTH_TOKEN_PROPERTY_NAME);
+        if (accessToken == null) {
+            log.error("No valid access/authorization token in server settings");
+            log.error("Exiting...");
+            exit(NO_AUTH_EXIT);
+        }
+        
+        String sourceStr = config.getProperty(SOURCES_PROPERTY_NAME);
+        if (StringUtils.isEmpty(sourceStr)) {
+            log.error("No sources in server settings");
+            log.error("Exiting...");
+            exit(NO_SOURCE_EXIT);
+        }
+    
+        // Obtain potentially multiple sources from a comma delimited string of sources
+        String[] sourceNames = sourceStr.split(",");
+        // TODO: Don't know that we really care here -- though I don't know how many Camels fit in a JVM.  So may be
+        //  better to stick to a single one.  We can extend this later if we needed.
+        if (sourceNames.length != 1) {
+            log.error("The Camel connector supports operation as only a single source.");
+            log.error("Exiting...");
+            exit(NO_SOURCE_EXIT);
+        }
+        
+        targetVantiqServer = config.getProperty(TARGET_SERVER_PROPERTY_NAME);
+        if (StringUtils.isEmpty(targetVantiqServer)) {
+            log.error("No server URL specified in server settings");
+            log.error("Exiting...");
+            exit(NO_SERVER_EXIT);
+        }
+        
+        sources = new ArrayList<>();
+        for (String sourceName : sourceNames) {
+            sourceName = sourceName.trim(); // remove any spacing from the name
+            
+            CamelCore source;
+            source = new CamelCore(sourceName, accessToken, targetVantiqServer);
+            sources.add(source);
+        }
+        
+        return sources;
+    }
+    
+    /**
+     * Closes all sources then orders the JVM to exit
+     * @param code  The exit code
+     */
+    public static void exit(int code) {
+        if (sources != null) {
+            for (CamelCore source : sources) {
+                source.stop();
+            }
+        }
+        System.exit(code);
+    }
+}
diff --git a/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/connector/VantiqCamelException.java b/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/connector/VantiqCamelException.java
new file mode 100644
index 00000000..d1fca3e5
--- /dev/null
+++ b/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/connector/VantiqCamelException.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2023 Vantiq, Inc.
+ *
+ * All rights reserved.
+ *
+ * SPDX: MIT
+ */
+
+package io.vantiq.extsrc.camelconn.connector;
+
+/**
+ * A custom exception used to extract the useful information from a SQLException
+ */
+public class VantiqCamelException extends Exception {
+    
+    public VantiqCamelException() {
+        super();
+    }
+    
+    public VantiqCamelException(String message) {
+        super(message);
+    }
+    
+    public VantiqCamelException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
\ No newline at end of file
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
new file mode 100644
index 00000000..98f39dad
--- /dev/null
+++ b/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/discover/CamelDiscovery.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (c) 2022 Vantiq, Inc.
+ *
+ * All rights reserved.
+ *
+ * SPDX: MIT
+ */
+
+package io.vantiq.extsrc.camelconn.discover;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.camel.ExtendedCamelContext;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.impl.DefaultCamelContext;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.StringTokenizer;
+
+/**
+ * Support for discovery of system components from a (set of) Camel routes.  We will use this information to load the
+ * libraries required to run the route(s) provided.
+ */
+
+@Slf4j
+public class CamelDiscovery {
+    
+    public static final String SYSTEM_COMPONENTS = "systemComponents";
+    public static final String COMPONENTS_TO_LOAD = "componentsToLoad";
+    public static final String SYSTEM_DATAFORMATS = "systemDataFormats";
+    public static final String DATAFORMATS_TO_LOAD = "dataFormatsToLoad";
+    
+    
+    public static final String VANTIQ_COMPONENT_SCHEME = "vantiq";
+    
+    private static final String COMPONENTS_KEY_NAME = "components";
+    private static final String DATAFORMATS_KEY_NAME = "dataformats";
+    private static final String CAMEL_VERSION_KEY_NAME = "camelVersion";
+    
+    private static Map<String, Object> artifactMap;
+    private static Map<String, String> components;
+    private static Map<String, String> dataformats;
+    
+    /**
+     * Determine the set of components used in the routes in a route builder.
+     *
+     ** @param rb RouteBuilder containing the set of routes from which to discover components
+     *
+     * @return Map<String, Set<String>> containing two Set entries:
+     *          systemComponents -- (informational) and
+     *          componentsToLoad -- the set of component schemes in use in the collected routes.
+     *
+     * @throws DiscoveryException when exceptions encountered during processing.
+     */
+    Map<String, Set<String>> performComponentDiscovery(RouteBuilder rb) throws DiscoveryException {
+        
+        try (DefaultCamelContext ctx = new DefaultCamelContext()) {
+            ExtendedCamelContext ectx = ctx.adapt(ExtendedCamelContext.class);
+            EnumeratingComponentResolver ecr = new EnumeratingComponentResolver(ectx.getComponentResolver());
+            ectx.setComponentResolver(ecr);
+            EnumeratingDataFormatResolver edfr = new EnumeratingDataFormatResolver(ectx.getDataFormatResolver());
+            ectx.setDataFormatResolver(edfr);
+            EnumeratingConfigurerResolver epcr = new EnumeratingConfigurerResolver();
+            ectx.setConfigurerResolver(epcr);
+            log.debug("Discovering Camel (version {}) components using context: {}", ectx.getVersion(), ectx.getName());
+    
+            assert ectx.getComponentResolver() instanceof EnumeratingComponentResolver;
+            try {
+                ectx.addRoutes(rb);
+                ectx.start();
+            } catch (Exception e) {
+                log.error("Trapped exception during component discovery:  ", e);
+                throw new DiscoveryException("Exception during component discovery", e);
+            }
+    
+            if (log.isDebugEnabled()) {
+                log.debug("Component discovery complete:");
+                ecr.getComponentsToLoad().forEach(comp -> log.debug("    ---> {}", comp));
+    
+                log.debug("System components (ignored): ");
+                ecr.getSystemComponentsUsed().forEach(comp -> log.debug("    ---> {}", comp));
+                
+                log.debug("DataFormat discovery complete:");
+                edfr.getDataFormatsToLoad().forEach(df -> log.debug("    ---> {}", df));
+                log.debug("System dataformats (ignored): ");
+                edfr.getSystemDataFormatsUsed().forEach(df -> log.debug("    ---> {}", df));
+    
+            }
+            Map<String, Set<String>> retVal = new HashMap<>();
+            retVal.put(SYSTEM_COMPONENTS, ecr.getSystemComponentsUsed());
+            retVal.put(COMPONENTS_TO_LOAD, ecr.getComponentsToLoad());
+            retVal.put(SYSTEM_DATAFORMATS, edfr.getSystemDataFormatsUsed());
+            retVal.put(DATAFORMATS_TO_LOAD, edfr.getDataFormatsToLoad());
+            return retVal;
+        } catch (Exception e) {
+            log.error("Trapped exception preparing or completing component discovery", e);
+            throw new DiscoveryException("Exception preparing or completing component discovery processing", e);
+        }
+    }
+    
+    /**
+     * Find the name of the camel component from which it runs these components based on the scheme.
+     *
+     * Using the generated artifactsMap (see {@link build.gradle#generateComponentList}), lookup the schema
+     * name and return the component name found.  It (will be) assumed that these are all in org.apache.camel, and
+     * that they all share the same camel version as that which we are running.  This is the Apache Camel way.
+     *
+     * @param scheme String name of component scheme used to define endpoints, etc.
+     * @return String identifying the name of the loadable artifact that contains the code to run the component.  Can
+     *         be null if no artifact is found.
+     * @throws DiscoveryException If errors occur attempting to find the artifact
+     */
+
+    String findComponentForScheme(String scheme) throws DiscoveryException {
+        if (artifactMap == null || components == null) {
+            loadArtifactMap();
+        }
+        return components.get(scheme);
+    }
+    
+    String findDataFormatForName(String dfName) throws DiscoveryException {
+        if (artifactMap == null || dataformats == null) {
+            loadArtifactMap();
+        }
+        return dataformats.get(dfName);
+    }
+    
+    String getComponentListVersion() throws DiscoveryException {
+        if (artifactMap == null || components == null) {
+            loadArtifactMap();
+        }
+        
+        if (artifactMap.get(CAMEL_VERSION_KEY_NAME) instanceof String) {
+            return (String) artifactMap.get(CAMEL_VERSION_KEY_NAME);
+        } else {
+            throw new DiscoveryException("No " + CAMEL_VERSION_KEY_NAME + " property present");
+        }
+    }
+    
+    boolean isVersionCompatible(String comparisonVersion) throws DiscoveryException {
+        StringTokenizer artifactVersionParts = new StringTokenizer(getComponentListVersion(), ".");
+        StringTokenizer comparisonVersionParts = new StringTokenizer(comparisonVersion, ".");
+        return artifactVersionParts.countTokens() >= 2 && comparisonVersionParts.countTokens() >= 2 &&
+                artifactVersionParts.nextToken().equals(comparisonVersionParts.nextToken()) &&
+                artifactVersionParts.nextToken().equals(comparisonVersionParts.nextToken());
+    }
+    
+    /**
+     * Load the artifact map.
+     *
+     * This is loaded once per class load rather than once/instance.  It's produced by the build and will not change
+     * over the time that the system is running.
+     *
+     * @throws DiscoveryException If there are issues loading the artifact
+     */
+    @SuppressWarnings({"unchecked"})
+    static synchronized void loadArtifactMap() throws DiscoveryException {
+        try (InputStream in =
+                     CamelDiscovery.class.getResourceAsStream("/artifactMap.json")) {
+            if (in == null) {
+                throw new DiscoveryException("Unable to load artifactMap.json file.");
+            }
+            String jsonString = new String(in.readAllBytes());
+            log.trace("Map is {}", jsonString);
+            artifactMap = new ObjectMapper().readValue(jsonString, new TypeReference<>() {});
+            components = (Map<String, String>) artifactMap.get(COMPONENTS_KEY_NAME);
+            dataformats = (Map<String, String>) artifactMap.get(DATAFORMATS_KEY_NAME);
+        } catch (IOException ioe) {
+            throw new DiscoveryException("Error loading artifact map from classpath", ioe);
+        }
+    }
+}
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
new file mode 100644
index 00000000..62234f06
--- /dev/null
+++ b/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/discover/CamelResolver.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright (c) 2022 Vantiq, Inc.
+ *
+ * All rights reserved.
+ *
+ * SPDX: MIT
+ */
+
+package io.vantiq.extsrc.camelconn.discover;
+
+import lombok.NonNull;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.ivy.Ivy;
+import org.apache.ivy.core.LogOptions;
+import org.apache.ivy.core.module.descriptor.Artifact;
+import org.apache.ivy.core.module.descriptor.DefaultArtifact;
+import org.apache.ivy.core.module.descriptor.DefaultDependencyArtifactDescriptor;
+import org.apache.ivy.core.module.descriptor.DefaultDependencyDescriptor;
+import org.apache.ivy.core.module.descriptor.DefaultModuleDescriptor;
+import org.apache.ivy.core.module.descriptor.DependencyArtifactDescriptor;
+import org.apache.ivy.core.module.id.ModuleRevisionId;
+import org.apache.ivy.core.report.ArtifactDownloadReport;
+import org.apache.ivy.core.report.ResolveReport;
+import org.apache.ivy.core.resolve.ResolveOptions;
+import org.apache.ivy.core.retrieve.RetrieveOptions;
+import org.apache.ivy.core.retrieve.RetrieveReport;
+import org.apache.ivy.core.settings.IvySettings;
+import org.apache.ivy.plugins.resolver.ChainResolver;
+import org.apache.ivy.plugins.resolver.IBiblioResolver;
+import org.slf4j.helpers.MessageFormatter;
+
+import java.io.File;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+@Slf4j
+public class CamelResolver {
+    private final String name;
+    private final List<URI> repos;
+    private final File cache;
+    private final File destination;
+    final IvySettings ivySettings;
+    protected final ChainResolver resolver;
+    protected final ResolveOptions resolveOptions;
+    protected final RetrieveOptions retrieveOptions;
+    
+    final Ivy ivy;
+    
+    private static int unnamedResolverCount = 0;
+    private static final String DEFAULT_CONFIGURATION = "default";
+    
+    /**
+     * 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 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
+     */
+    
+    CamelResolver(String name, URI repo, File cache, File destination) {
+        this(name, (repo == null ? Collections.emptyList() : List.of(repo)), cache, destination);
+    }
+    
+    CamelResolver(String name, List<URI> repos, File cache, File destination) {
+        if (name != null) {
+            this.name = name;
+        } else {
+            unnamedResolverCount += 1;
+            this.name = "UnnamedResolver-" + unnamedResolverCount;
+        }
+        this.repos = repos;
+        this.cache = cache;
+        this.destination = destination;
+        
+        if (destination == null) {
+            throw new IllegalArgumentException("The destination parameter cannot be null.");
+        }
+    
+        //creates clear ivy settings
+        ivySettings = new IvySettings();
+        if (cache != null) {
+            ivySettings.setDefaultCache(cache);
+        }
+        // resolver for configuration of maven repos.  Defaults to including maven central
+        resolver = new ChainResolver();
+        resolver.setName(name + "::Chain");
+        if (repos.size() > 0) {
+            for (URI repo : repos) {
+                IBiblioResolver aResolver = new IBiblioResolver();
+                try {
+                    String repoUrl = repo.toURL().toExternalForm();
+                    aResolver.setRoot(repoUrl);
+                    aResolver.setName(name + "::" + repoUrl);
+    
+                } catch (MalformedURLException mue) {
+                    throw new IllegalArgumentException("Malformed repos URL: " + repo.toString(), mue);
+                }
+                
+                aResolver.setM2compatible(true);
+                aResolver.setUsepoms(true);
+                resolver.add(aResolver);
+            }
+        } else {
+            IBiblioResolver aResolver = new IBiblioResolver();
+    
+            aResolver.setM2compatible(true);
+            aResolver.setUsepoms(true);
+            aResolver.setName(name);
+            resolver.add(aResolver);
+        }
+        //adding maven repos resolver
+        ivySettings.addResolver(resolver);
+        //set to the default resolver
+        ivySettings.setDefaultResolver(resolver.getName());
+        
+        String[] confs = new String[1];
+        confs[0] = DEFAULT_CONFIGURATION;
+        resolveOptions = new ResolveOptions();
+        resolveOptions.setConfs(confs);
+        resolveOptions.setTransitive(true);
+        resolveOptions.setDownload(true);
+        if (!log.isTraceEnabled()) {
+            // Reduce the volume of output from Ivy unless we really need/want it.
+            resolveOptions.setLog(LogOptions.LOG_QUIET);
+        }
+    
+        retrieveOptions = new RetrieveOptions();
+        retrieveOptions.setConfs(confs);
+        retrieveOptions.setDestArtifactPattern(destination.getAbsolutePath() +
+                                                       "/[artifact]-[revision](-[classifier])" + ".[ext]");
+        retrieveOptions.setOverwriteMode(RetrieveOptions.OVERWRITEMODE_NEWER);
+        if (!log.isTraceEnabled()) {
+            // Reduce the volume of output from Ivy unless we really need/want it.
+            retrieveOptions.setLog(LogOptions.LOG_QUIET);
+        }
+        
+        //creates an Ivy instance with settings
+        ivy = Ivy.newInstance(ivySettings);
+    }
+    
+    String identity() {
+        String rep = this.repos != null ? this.repos.toString() : "<null>";
+        String ourCache = this.cache != null ? this.cache.getAbsolutePath() : "<null>";
+        String dest = this.destination != null ? this.destination.getAbsolutePath() : "<null>";
+        return MessageFormatter.arrayFormat("CamelResolver {}, repos: {}, cache: {}, destination: {}",
+                                new Object[]{this.name, rep, ourCache, dest}).getMessage();
+    }
+    
+    /**
+     * Resolve module & copy to destination.
+     *
+     * @param organization String organization/group for artifact
+     * @param name String artifact name
+     * @param revision String artifact revision
+     * @param purpose String purpose of this resolution.  Used to clarify things in logging.
+     * @throws Exception When things go awry
+     */
+    Collection<File> resolve(String organization, String name, String revision, @NonNull String purpose) throws Exception {
+        return resolve(organization, name, revision, null, purpose);
+    }
+    
+    /**
+     * Resolve module & copy to destination
+     *
+     * @param name String artifact name
+     * @param revision String artifact revision
+     * @param type String type of artifact.  If null, assumes pom file will specify details
+     * @param purpose String purpose of this resolution.  Used to clarify things in logging.
+     * @throws Exception When things go awry
+     */
+    Collection<File> resolve(String organization, String name, String revision, String type, @NonNull String purpose)
+            throws Exception {
+        if (organization == null || name == null || revision == null) {
+            throw new IllegalArgumentException("The parameters organization, name, and revision must be " +
+                                                       "non-null. (CamelResolver " + identity() + ")");
+        }
+        
+        DefaultModuleDescriptor md =
+                DefaultModuleDescriptor.newDefaultInstance(ModuleRevisionId.newInstance(organization,
+                                                        name + "-caller", "working"));
+        
+        DefaultDependencyDescriptor dd = new DefaultDependencyDescriptor(md,
+                                                                         ModuleRevisionId.newInstance(organization, name, revision),
+                                                                         false,
+                                                                         false,
+                                                                         true);
+        dd.addDependencyConfiguration("default", "default");
+        
+        if (type != null) {
+            // TODO: Figure out why both of these are necessary, but it works when present & not when not.
+            Artifact typedArtifact = new DefaultArtifact(ModuleRevisionId.newInstance(organization, name, revision),
+                                                         null, name, type, "." + type);
+            DependencyArtifactDescriptor dad =
+                    new DefaultDependencyArtifactDescriptor(dd, name, type, type, null, null);
+            dd.addDependencyArtifact(DEFAULT_CONFIGURATION, dad);
+            md.addArtifact(DEFAULT_CONFIGURATION, typedArtifact);
+        }
+        md.addDependency(dd);
+        
+        log.info("Resolving required libraries -- {}", purpose);
+        //init resolve report
+        ResolveReport report = ivy.resolve(md, resolveOptions);
+        
+        if (report.hasError()) {
+            throw new ResolutionException(identity() + ": Error(s) encountered during resolution: " +
+                                                  String.join(", ", report.getAllProblemMessages()));
+        }
+        
+        if (log.isTraceEnabled()) {
+            // do long-winded report only when requested
+            ArtifactDownloadReport[] reps = report.getAllArtifactsReports();
+            for (ArtifactDownloadReport aRep : reps) {
+                Artifact artifact = aRep.getArtifact();
+                File lclFile = aRep.getLocalFile();
+                log.trace("   --> Cached artifact {}:{}:{} is now available at {} ({})",
+                          artifact.getModuleRevisionId().getOrganisation(),
+                          artifact.getName(),
+                          artifact.getModuleRevisionId().getRevision(),
+                          lclFile.getAbsolutePath(),
+                          identity());
+            }
+        }
+        
+        
+        RetrieveReport rr = ivy.retrieve(md.getModuleRevisionId(), retrieveOptions);
+        // This is the union of files retrieved & those found up-to-date.  We need to include both since our
+        // classloader needs to be able to load all the classes, new or otherwise.
+        Collection<File> necessaryFiles = rr.getRetrievedFiles();
+        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);
+        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
new file mode 100644
index 00000000..d0384886
--- /dev/null
+++ b/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/discover/CamelRunner.java
@@ -0,0 +1,519 @@
+/*
+ * Copyright (c) 2022 Vantiq, Inc.
+ *
+ * All rights reserved.
+ *
+ * SPDX: MIT
+ */
+
+package io.vantiq.extsrc.camelconn.discover;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.camel.CamelContext;
+import org.apache.camel.ExtendedCamelContext;
+import org.apache.camel.ProducerTemplate;
+import org.apache.camel.RoutesBuilder;
+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.spi.Resource;
+import org.apache.camel.spi.RoutesBuilderLoader;
+import org.apache.camel.spi.RoutesLoader;
+import org.apache.camel.support.ResourceHelper;
+import org.apache.commons.lang3.StringUtils;
+
+import java.io.Closeable;
+import java.io.File;
+import java.lang.reflect.Method;
+import java.net.URI;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+@Slf4j
+public class CamelRunner extends MainSupport implements Closeable {
+    public static final String COMPONENT_NAME = "componentName";
+    public static final String COMPONENT_PROPERTIES = "componentProperties";
+    private final String appName;
+    private final List<URI> repositories;
+    private List<String> additionalLibraries;
+    
+    private final List<Map<String, Object>> initComponents;
+    
+    private final File ivyCache;
+    private final File loadedLibraries;
+    
+    private RouteBuilder routeBuilder;
+    
+    private final String routeSpec;
+    private final String routeSpecType;
+    private ClassLoader routeBasedCL;
+    private ClassLoader originalClassLoader;
+    
+    protected final MainRegistry registry;
+    
+    private Thread camelThread;
+    
+    private Boolean startupCompleted = false;
+    private Exception startupFailed = null;
+    private final static String startupSync = "startupSync";
+    
+    /**
+     * 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
+     */
+    public CamelRunner(String appName, String routeSpecification, String routeSpecificationType, List<URI> repos,
+                       String cacheDirectory, String loadedLibDir, List<Map<String, Object>> initComponents) {
+        super();
+        this.registry = new MainRegistry();
+        this.appName = appName;
+        this.repositories = repos;
+        this.ivyCache = new File(cacheDirectory);
+        this.loadedLibraries = new File(loadedLibDir);
+        this.routeBuilder = null;
+        this.routeSpec = routeSpecification;
+        // Camel doesn't like simpler yml, so be helpful here.
+        this.routeSpecType = routeSpecificationType.equals("yml") ? "yaml" : routeSpecificationType;
+        this.additionalLibraries = null;
+        this.initComponents = initComponents;
+        mainConfigurationProperties.setRoutesCollectorEnabled(false);
+    }
+    
+    /**
+     * Create a CamelRunner instance
+     *
+     * @param appName String Name for this instance
+     * @param routeBuilder RouteBuilder specification for the route(s) to run
+     * @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
+     */
+    CamelRunner(String appName, RouteBuilder routeBuilder, List<URI> repos,
+                String cacheDirectory, String loadedLibDir, List<Map<String, Object>> initComponents) {
+        super();
+        this.registry = new MainRegistry();
+        this.appName = appName;
+        this.repositories = repos;
+        this.ivyCache = new File(cacheDirectory);
+        this.loadedLibraries = new File(loadedLibDir);
+        this.routeBuilder = routeBuilder;
+        this.routeSpecType = null;
+        this.routeSpec = null;
+        this.additionalLibraries = null;
+        this.initComponents = initComponents;
+    
+        mainConfigurationProperties.setRoutesBuilders(List.of(routeBuilder));
+        mainConfigurationProperties.setRoutesCollectorEnabled(false);
+    }
+    
+    public void setAdditionalLibraries(List<String> libs) {
+        this.additionalLibraries = libs;
+    }
+    
+    @Override
+    protected void doInit() throws Exception {
+        super.doInit();
+        createCamelContext();
+    }
+    
+    /**
+     * Build the application classloader we will use.
+     *
+     * Construct a URLClassLoader that includes the requirements determined during the discovery phase.  We set the
+     * parent classloader to the current application classloader (if any) or that of this class.  This allows this
+     * classloader to find anything built into the caller.
+     *
+     * Constructing this classloader will trigger the requirement resolution, and may involve downloading the
+     * packages required to run the provided routes (via the routeBuilder parameter).
+     *
+     * @return ClassLoader (specifically, a URLClassLoader)
+     * @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 {
+        if (getCamelContext() == null) {
+            throw new IllegalStateException("Camel context does not exist.");
+        }
+        CamelDiscovery discoverer = new CamelDiscovery();
+        Map<String, Set<String>> discResults = discoverer.performComponentDiscovery(routeBuilder);
+        
+        // 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();
+        }
+        // If providing a list of repos, we require the user to include maven-central (if they want to use it).
+        // This allows test cases, etc., not to over-resolve.
+        Set<File> aReposWorth = resolveFromRepo(repsToUse,
+                                                discResults.get(CamelDiscovery.COMPONENTS_TO_LOAD),
+                                                discResults.get(CamelDiscovery.DATAFORMATS_TO_LOAD),
+                                                this.additionalLibraries,
+                                                discoverer);
+       jarList.addAll(aReposWorth);
+    
+        // Now, construct a classloader that includes all these loaded files
+    
+        ArrayList<URL> urlList = new ArrayList<>();
+        for (File jar : jarList) {
+            URL url = jar.toURI().toURL();
+            urlList.add(url);
+        }
+    
+        // 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;
+        if (parent == null) {
+            // Note that this is often the case.  But in our case, we want to include the various camel components
+            // and runtime that we include in our connector class path.  So, we'll have our current classloader be
+            // the parent so those are included.  An alternative would be to include the classpath of the connector
+            // in the generated classloader's list of URIs, but this seems a cleaner solution.
+            log.trace("Camel context {} has no applicationContextClassLoader, using class's loader as parent",
+                      getCamelContext().getName());
+            parent = this.getClass().getClassLoader();
+        }
+        return new URLClassLoader(urlList.toArray(new URL[0]), parent);
+    }
+    
+    /**
+     * Given a repository, resolve (download) the components available.
+     *
+     * @param repositories List<URI> Repository from which to (attempt) to load.  Not all components must be available
+     * here.
+     * @param componentsToResolve Set<String> the components to load
+     * @param dataformatsToResolve Set<String> the data formats to load
+     * @param additionalLibraries List<String> Extra libraries to load (overcome missing from discovery
+     * @param discoverer CamelDiscovery to use.
+     * @return Set<File> The set of (jar) files downloaded.
+     * @throws Exception when things go awry
+     */
+
+    private Set<File> resolveFromRepo(List<URI> repositories, Set<String> componentsToResolve,
+                                      Set<String> dataformatsToResolve,
+                                      List<String> additionalLibraries, CamelDiscovery discoverer)
+            throws Exception {
+        if (getCamelContext() == null) {
+            throw new IllegalStateException("Camel context does not exist.");
+        }
+        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);
+            }
+            // 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);
+            }
+            // 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) {
+                    throw new IllegalArgumentException("The configuration's 'additionalLibraries' property must be a list " +
+                                                       "of library specifications of the form " +
+                                                       "<organization>:<name>:<revision>.  Found " + 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(compParts[0], compParts[1], compParts[2],
+                                                       "Additional Library Resolution");
+                jarSet.addAll(resolved);
+            }
+        }
+        return jarSet;
+    }
+    
+    /**
+     * 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 {
+            log.debug("Calling Run()");
+            camelThread = new Thread(() -> {
+                try {
+                    run();
+                } catch (Exception e) {
+                    throw new RuntimeException("run() in runRoutes failed", e);
+                }
+            });
+            camelThread.start();
+            log.trace("Awaiting camel startup");
+            awaitStartup();
+            
+            if (startupFailed == null) {
+                log.debug("Camel is started");
+                if (waitForThread) {
+                    camelThread.join();
+                }
+            }
+            // else
+            // We have failed to successfully start.  Finally case below will wait for the thread to die
+            // then throw the exception problem
+        } catch (Exception e) {
+            log.error("Exception from runtime", e);
+        } finally {
+            if (waitForThread || startupFailed != null) {
+                try {
+                    close();
+                } catch (Exception e) {
+                    // Log but ignore -- this is going away anyway
+                    log.error("Error closing CamelRunner when the routes complete:", e);
+                }
+            }
+        }
+    }
+    
+    /**
+     * Close this CamelRunner and the context associated with it.
+     *
+     * Once done, this entity cannot be reused.
+     */
+    public void close() {
+        completed();
+        if (!getCamelContext().isStopped()) {
+            getCamelContext().stop();
+            try {
+                getCamelContext().close();
+            } catch (Exception e) {
+                // Ignore -- this is going away anyway
+                log.error("Error closing camel context used in runner:", e);
+            }
+        }
+    }
+    
+    public Thread getCamelThread() {
+        return camelThread;
+    }
+    
+    @Override
+    protected ProducerTemplate findOrCreateCamelTemplate() {
+        if (getCamelContext() != null) {
+            return getCamelContext().createProducerTemplate();
+        } else {
+            return null;
+        }
+    }
+    
+    /**
+     * Set up runtime environment.
+     *
+     * In addition to the super() stuff, we'll (optionally, build and) add our route(s) to the context.  At startup,
+     * these routes/applications will begin to operate. We will also do any specified component initialization.
+     *
+     * @throws IllegalStateException when no routes have been specified.
+     * @throws RuntimeException when routes cannot be added to the Camel environment.
+     */
+    @Override
+    protected void beforeStart() throws Exception {
+        if (routeBuilder == null) {
+            if (StringUtils.isEmpty(routeSpec) || StringUtils.isEmpty(routeSpecType)) {
+                log.error("No routes have been specified to run.  Either {} or {} and {} are " +
+                        "required.", "routeBuilder", "routeSpec", "routeSpecType");
+                throw new IllegalStateException("No routes have been specified to run.");
+            }
+            routeBuilder = loadRouteFromText(this.routeSpec, this.routeSpecType);
+        }
+        if (routeBasedCL == null) {
+            if (routeBuilder != null) {
+                try {
+                    routeBasedCL = constructClassLoader();
+                } catch (Exception e) {
+                    throw new RuntimeException("Unable to create route-based class loader", e);
+                }
+                originalClassLoader = camelContext.getApplicationContextClassLoader();
+                camelContext.setApplicationContextClassLoader(routeBasedCL);
+                try {
+                    camelContext.addRoutes(routeBuilder);
+                } catch (Exception e) {
+                    throw new RuntimeException("Adding routes to camel context failed", e);
+                }
+            }
+        }
+        // Some routes may have components (e.g., Salesforce) that need specific configuration.
+        // If that's the case, do that now before we start our route(s).
+        if (initComponents != null && initComponents.size() > 0) {
+            for (Map<String, Object> compToInit: initComponents) {
+                Object propsObj = compToInit.get(COMPONENT_PROPERTIES);
+                Map<String, ?> props;
+                if (propsObj instanceof Map) {
+                    //noinspection unchecked
+                    props = (Map<String, ?>) propsObj;
+                } else {
+                    throw new IllegalArgumentException("Component initialization type is incorrect.");
+                }
+                if (!props.isEmpty()) {
+                    // Don't bother to go further if there are no properties to set
+    
+                    Object comp = camelContext.getComponent(compToInit.get(COMPONENT_NAME).toString());
+                    if (comp != null) {
+                        Class<?> toBeInit = comp.getClass();
+                        for (Map.Entry<String, ?> compProp : props.entrySet()) {
+                            // For each property to be initialized, we'll look for the setter method
+                            // (i.e., set${propertyName}(value) where the parameter's class matches the class of
+                            // the property value passed in).  If that exists, we'll call it.  Otherwise, log a
+                            // warning and ignore it. Things are likely to fail, but there's not much we can do about
+                            // improper specifications.
+                            String methName = "set" + compProp.getKey().substring(0, 1).toUpperCase() +
+                                            compProp.getKey().substring(1);
+                            Class<?>[] paramSpec = { compProp.getValue().getClass() };
+                            try {
+                                Method meth = toBeInit.getMethod(methName, paramSpec );
+                                log.debug("Initializing component class {}.{}{}) (from property {})",
+                                          comp.getClass().getSimpleName(),
+                                          meth.getName(), compProp.getValue().getClass().getName(), compProp.getKey());
+                                meth.invoke(comp, compProp.getValue());
+                            } catch (NoSuchMethodException nsme) {
+                                log.warn("No setter found for component class: {}.{}({}) :: " +
+                                                 "required by property name: {}",
+                                         toBeInit.getName(), methName,
+                                         compProp.getValue().getClass().getName(),
+                                         compProp.getKey());
+                            } catch (Exception e) {
+                                // This is generally a fatal condition, so we'll toss the error up the chain
+                                log.error("Cannot invoke {}.{}({}) due to Exception.", toBeInit.getName(),
+                                          methName, compProp.getValue().getClass().getName(), e);
+                                throw e;
+                            }
+                        }
+                    } else {
+                        log.warn("No instance of component {} to initialize.",
+                                 compToInit.get(COMPONENT_NAME).toString());
+                    }
+                } else {
+                    log.warn("Component {} was specified for initialization, but no properties were provided. " +
+                                "No initialization was performed.", compToInit.get(COMPONENT_NAME));
+                }
+            }
+        }
+        super.beforeStart();
+    }
+    
+    @Override
+    protected void afterStart() throws Exception {
+        super.afterStart();
+        notifyStarted();
+    }
+    
+    protected void notifyStarted() {
+        log.debug("NotifyStarted()");
+        synchronized (startupSync) {
+            // Avoid busy-wait during startup.
+            startupCompleted = true;
+            startupSync.notify();
+        }
+        log.debug("NotifyStarted() completed");
+    }
+    
+    protected void awaitStartup() throws Exception {
+        log.debug("AwaitStarted()");
+        synchronized (startupSync) {
+            if (!startupCompleted) {
+                // Avoid busy-wait during startup.
+                startupSync.wait();
+            }
+        }
+        log.debug("AwaitStarted() completed");
+    }
+    
+    /**
+     * Load a route from a textual description of the route.
+     *
+     * Here, we'll use the camel runtime to construct a RouteBuilder from the text specification and specification
+     * type we are given.  We let the underlying Camel system find the route loaders so that this code extends to
+     * support new specification types.  Changes to the set of libraries loaded in this image may be required to load
+     * the new DSL interpreters of the new specification types.
+     *
+     * @param specification String specification of the route(s) to load
+     * @param specificationType String syntactic type of the specification (xml, yaml, etc.)
+     * @return RouteBuilder containing the route defined by the specification.
+     * @throws Exception for errors in loading the routes.
+     */
+    protected RouteBuilder loadRouteFromText(String specification, String specificationType) throws Exception {
+        log.debug("Loading route (specificationType {}): {}", 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.trace("Route builder: {}", rb.toString());
+        
+        return (RouteBuilder) rb;
+    }
+    
+    @Override
+    protected void doStart() throws Exception {
+        super.doStart();
+        getCamelContext().start();
+    }
+    
+    @Override
+    protected void doFail(Exception e) {
+        // Save our failure reason
+        startupFailed = e;
+        // In the event of failure, cancel outstanding operations & let the system discover that by marking that
+        // things have started.  Otherwise, we hang awaiting the startup monitor.
+        log.error("Camel startup has failed due to: ", e);
+        notifyStarted();
+        super.doFail(e);
+    }
+    
+    @Override
+    protected CamelContext createCamelContext() {
+        if (camelContext == null) {
+            camelContext = new DefaultCamelContext();
+            log.debug(">>>Using context: {}, stopped: {}", camelContext.getName(), camelContext.isStopped());
+        }
+        return camelContext;
+    }
+}
diff --git a/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/discover/DiscoveryException.java b/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/discover/DiscoveryException.java
new file mode 100644
index 00000000..2bd2c98d
--- /dev/null
+++ b/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/discover/DiscoveryException.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2022 Vantiq, Inc.
+ *
+ * All rights reserved.
+ *
+ * SPDX: MIT
+ */
+
+package io.vantiq.extsrc.camelconn.discover;
+
+/**
+ * Marker for exceptions found during the component discovery process
+ */
+public class DiscoveryException extends Exception {
+    public DiscoveryException(String message, Throwable cause) {
+        super(message, cause);
+    }
+    
+    public DiscoveryException(String message) {
+        super(message);
+    }
+}
diff --git a/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/discover/EnumeratingComponentResolver.java b/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/discover/EnumeratingComponentResolver.java
new file mode 100644
index 00000000..315a32e6
--- /dev/null
+++ b/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/discover/EnumeratingComponentResolver.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (c) 2022 Vantiq, Inc.
+ *
+ * All rights reserved.
+ *
+ * SPDX: MIT
+ */
+
+package io.vantiq.extsrc.camelconn.discover;
+
+import static io.vantiq.extsrc.camelconn.discover.CamelDiscovery.VANTIQ_COMPONENT_SCHEME;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.camel.CamelContext;
+import org.apache.camel.Component;
+import org.apache.camel.spi.ComponentResolver;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * This class is used to determine the set of components used to a collection of routes.  It replaces the Camel
+ * context's component resolver.  The context, when adding routes, will call resolveComponent() with the scheme/name
+ * of the component and the context, with the result expected to be a resolved (class loaded, etc.) component.
+ *
+ * To accomplish this, this resolver notes the component in use.  It then loads a placeholder component which has
+ * enough capability to act as if it has started.  This allows the context to believe that the routes successfully
+ * loaded.  The result of this is that this component resolver has the set of components that need to be made
+ * available (added to the class path) to allow a "real" component resolver to operate.
+ */
+@Slf4j
+class EnumeratingComponentResolver implements ComponentResolver {
+    private final Set<String> componentsToLoad;
+    private final Set<String> systemComponentsUsed;
+    private final ComponentResolver originalResolver;
+    public static final Set<String> CORE_COMPONENTS_SCHEMES = new HashSet<>(
+            Arrays.asList("bean", "binding", "browse", "class", "controlbus", "dataformat", "dataset", "direct",
+                    "direct-vm", "file", "language", "log", "mock", "properties", "ref", "rest", "rest-api",
+                    "saga", "scheduler", "seda", "stub", "test", "timer", "validator", "vm", "xslt"));
+    
+    // These don't load and are handled specially internally
+    public static final Set<String> NONLOADABLE_CORE_COMPONENT_SCHEMES =  new HashSet<>(
+            Arrays.asList("binding", "properties", "test"));
+    
+    public EnumeratingComponentResolver(ComponentResolver origResolver) {
+        componentsToLoad = new HashSet<>();
+        systemComponentsUsed = new HashSet<>();
+        originalResolver = origResolver;
+    }
+    
+    @Override
+    public Component resolveComponent(String name, CamelContext context) throws Exception {
+        log.debug("Resolver asked to resolve component: {}", name);
+        if (CORE_COMPONENTS_SCHEMES.contains(name)) {
+            log.debug("... but this is a system provided entity, so we'll skip our resolution");
+            systemComponentsUsed.add(name);
+            if (originalResolver == null) {
+                // Note for the record, but we'll continue on. Apache Camel may have things here, or the application
+                // will fail on startup (and probably give a better diagnostic).
+                log.error("Original resolver not available to find system component: {}", name);
+            }
+        } else if (!name.equals(VANTIQ_COMPONENT_SCHEME)) {
+            // Our packaging includes the Vantiq component automatically, so we don't want to include it in the list
+            // of things to be resolved.  So we'll just filter it out here.
+            componentsToLoad.add(name);
+        }
+        // In either case, set up the PlaceHolder component.  We return a component here to faithfully satisfy the
+        // interface, but this pass through things is to discover the set of components we need to make available.
+        // Consequently, having the "real" component here is not necessary (or even useful).
+        return new PlaceholderComponent();
+    }
+    
+    public Set<String> getComponentsToLoad() {
+        return componentsToLoad;
+    }
+    
+    public Set<String> getSystemComponentsUsed() {
+        return systemComponentsUsed;
+    }
+}
diff --git a/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/discover/EnumeratingConfigurerResolver.java b/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/discover/EnumeratingConfigurerResolver.java
new file mode 100644
index 00000000..1b398361
--- /dev/null
+++ b/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/discover/EnumeratingConfigurerResolver.java
@@ -0,0 +1,25 @@
+
+/*
+ * Copyright (c) 2023 Vantiq, Inc.
+ *
+ * All rights reserved.
+ *
+ * SPDX: MIT
+ */
+
+package io.vantiq.extsrc.camelconn.discover;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.spi.ConfigurerResolver;
+import org.apache.camel.spi.PropertyConfigurer;
+
+/**
+ * This class is used to as part of our discovery process.  This can load a no-op configurer since we don't
+ * want any real configuration (inside camel) to happen during discovery. During the "real" startup, this is not used.
+ */
+public class EnumeratingConfigurerResolver implements ConfigurerResolver {
+    @Override
+    public PropertyConfigurer resolvePropertyConfigurer(String name, CamelContext context) {
+        return new NoopConfigurer();
+    }
+}
diff --git a/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/discover/EnumeratingDataFormatResolver.java b/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/discover/EnumeratingDataFormatResolver.java
new file mode 100644
index 00000000..8a35f327
--- /dev/null
+++ b/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/discover/EnumeratingDataFormatResolver.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (c) 2023 Vantiq, Inc.
+ *
+ * All rights reserved.
+ *
+ * SPDX: MIT
+ */
+
+package io.vantiq.extsrc.camelconn.discover;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.camel.CamelContext;
+import org.apache.camel.spi.DataFormat;
+import org.apache.camel.spi.DataFormatResolver;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * This class is used to determine the set of dataformats used to a collection of routes.  It replaces the Camel
+ * context's dataformat resolver.  The context, when adding routes, will call createDataFormat() with the scheme/name
+ * of the dataformat and the context, with the result expected to be a resolved (class loaded, etc.) dataformat.
+ *
+ * To accomplish this, this resolver notes the dataformat in use.  It then loads a placeholder dataformat which has
+ * enough capability to act as if it has started.  This allows the context to believe that the routes successfully
+ * loaded.  The result of this is that this dataformat resolver has the set of dataformats that need to be made
+ * available (added to the class path) to allow a "real" dataformat resolver to operate.
+ */
+@Slf4j
+public class EnumeratingDataFormatResolver implements DataFormatResolver {
+    
+    private final Set<String> dataFormatsToLoad;
+    private final Set<String> systemDataFormatsUsed;
+    
+    private final DataFormatResolver originalResolver;
+    
+    public static final Set<String> CORE_DATAFORMAT_NAMES = new HashSet<>(List.of());
+    
+    
+    public EnumeratingDataFormatResolver(DataFormatResolver origResolver) {
+        dataFormatsToLoad = new HashSet<>();
+        systemDataFormatsUsed = new HashSet<>();
+        originalResolver = origResolver;
+    }
+    
+    @Override
+    public DataFormat createDataFormat(String name, CamelContext context) {
+        log.debug("Resolver asked to resolve component: {}", name);
+        if (CORE_DATAFORMAT_NAMES.contains(name)) {
+            log.debug("... but this is a system provided entity, so we'll skip our resolution");
+            systemDataFormatsUsed.add(name);
+            if (originalResolver != null) {
+                return originalResolver.createDataFormat(name, context);
+            } else {
+                log.error("Original resolved not available to find system component: {}", name);
+            }
+        } else {
+            dataFormatsToLoad.add(name);
+        }
+        return new PlaceholderDataFormat();
+    }
+    
+    public Set<String> getDataFormatsToLoad() {
+        return dataFormatsToLoad;
+    }
+    
+    public Set<String> getSystemDataFormatsUsed() {
+        return systemDataFormatsUsed;
+    }
+}
diff --git a/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/discover/NoopConfigurer.java b/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/discover/NoopConfigurer.java
new file mode 100644
index 00000000..e37c2fc3
--- /dev/null
+++ b/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/discover/NoopConfigurer.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 2023 Vantiq, Inc.
+ *
+ * All rights reserved.
+ *
+ * SPDX: MIT
+ */
+
+package io.vantiq.extsrc.camelconn.discover;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.spi.PropertyConfigurer;
+
+/**
+ * Allow any properties to be configured.  This allows discovery to proceed.  Once complete & all classes are found,
+ * the regular Camel property configurer will be used.
+ */
+public class NoopConfigurer implements PropertyConfigurer {
+    @Override
+    public boolean configure(CamelContext camelContext, Object target, String name, Object value, boolean ignoreCase) {
+        // We are not picky -- we accept anything
+        return true;
+    }
+}
diff --git a/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/discover/PlaceholderComponent.java b/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/discover/PlaceholderComponent.java
new file mode 100644
index 00000000..45580acf
--- /dev/null
+++ b/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/discover/PlaceholderComponent.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2022 Vantiq, Inc.
+ *
+ * All rights reserved.
+ *
+ * SPDX: MIT
+ */
+
+package io.vantiq.extsrc.camelconn.discover;
+
+import org.apache.camel.Endpoint;
+import org.apache.camel.support.DefaultComponent;
+
+import java.util.Map;
+
+/**
+ * This component is a placeholder used to allow the connector to gather the set of components it will need.
+ * This component has sufficient content to allow the route building (but not starting) to complete, allowing
+ * the connector to determine the set of components it will need (via the {@link EnumeratingComponentResolver}).
+ **/
+class PlaceholderComponent extends DefaultComponent {
+    
+    @Override
+    protected Endpoint createEndpoint(String uri, String remaining, Map<String, Object> parameters) throws Exception {
+        PlaceholderEndpoint pe = new PlaceholderEndpoint(uri, this);
+        getCamelContext().addEndpoint(uri, pe);
+        return pe;
+    }
+    
+    @Override
+    protected void validateParameters(String uri, Map<String, Object> parameters, String optionPrefix) {
+        return;
+    }
+    
+    /**
+     * This is a placeholder only, used to facilitate our discovery of necessary components.
+     */
+    @Override
+    protected void doStart() {
+        // noop
+    }
+}
diff --git a/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/discover/PlaceholderConsumer.java b/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/discover/PlaceholderConsumer.java
new file mode 100644
index 00000000..ed553f40
--- /dev/null
+++ b/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/discover/PlaceholderConsumer.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2022 Vantiq, Inc.
+ *
+ * All rights reserved.
+ *
+ * SPDX: MIT
+ */
+
+package io.vantiq.extsrc.camelconn.discover;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.camel.Processor;
+import org.apache.camel.support.DefaultConsumer;
+
+import java.util.concurrent.ExecutorService;
+
+/**
+ * This consumer stands in for a "real" consumer, allowing route setup to proceed and, thus, enumerate all the
+ * components necessary.  See {@link EnumeratingComponentResolver} for further explanation.
+ */
+@Slf4j
+public class PlaceholderConsumer extends DefaultConsumer {
+    private final PlaceholderEndpoint endpoint;
+    private ExecutorService executorService;
+    
+    public PlaceholderConsumer(PlaceholderEndpoint endpoint, Processor processor) {
+        super(endpoint, processor);
+        this.endpoint = endpoint;
+    }
+
+    @Override
+    protected void doStart() throws Exception {
+        super.doStart();
+    }
+
+    @Override
+    protected void doStop() throws Exception {
+        if (null != endpoint) {
+            endpoint.shutdown();
+        }
+        super.doStop();
+    }
+    
+    private void processMessage(Object msg) {
+        // noop
+    }
+}
diff --git a/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/discover/PlaceholderDataFormat.java b/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/discover/PlaceholderDataFormat.java
new file mode 100644
index 00000000..fa6ca7e0
--- /dev/null
+++ b/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/discover/PlaceholderDataFormat.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2023 Vantiq, Inc.
+ *
+ * All rights reserved.
+ *
+ * SPDX: MIT
+ */
+
+package io.vantiq.extsrc.camelconn.discover;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.spi.DataFormat;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+
+public class PlaceholderDataFormat implements DataFormat {
+    
+    /**
+     * This is a placeholder only, used to facilitate our discovery of necessary DataFormats.
+     */
+    @Override
+    public void start() {
+        // noop
+    }
+    
+    @Override
+    public void stop() {
+        // noop
+    }
+    
+    @Override
+    public void marshal(Exchange exchange, Object graph, OutputStream stream) throws Exception {
+    
+    }
+    
+    @Override
+    public Object unmarshal(Exchange exchange, InputStream stream) throws Exception {
+        return null;
+    }
+}
diff --git a/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/discover/PlaceholderEndpoint.java b/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/discover/PlaceholderEndpoint.java
new file mode 100644
index 00000000..0f576fbd
--- /dev/null
+++ b/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/discover/PlaceholderEndpoint.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2022 Vantiq, Inc.
+ *
+ * All rights reserved.
+ *
+ * SPDX: MIT
+ */
+
+package io.vantiq.extsrc.camelconn.discover;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.camel.Consumer;
+import org.apache.camel.Processor;
+import org.apache.camel.Producer;
+import org.apache.camel.support.DefaultEndpoint;
+
+import java.util.Map;
+
+/**
+ * This endpoint is a placeholder used to allow the connector to gather the set of components it will need.
+ * This endpoint has sufficient content to allow the route building (but not starting) to complete, allowing
+ * the connector to determine the set of components it will need (via the {@link EnumeratingComponentResolver}).
+ **/
+
+@Slf4j
+public class PlaceholderEndpoint extends DefaultEndpoint {
+    PlaceholderEndpoint(String uri, PlaceholderComponent component) {
+        super(uri, component);
+        setLazyStartProducer(true);
+    }
+    
+    @Override
+    public void configureProperties(Map<String, Object> options) {
+        // This endpoint does nothing & doesn't need any properties.  So just ignore this.
+        log.debug("PlaceholderEndpoint for {} called", getEndpointUri());
+        return;
+    }
+    
+    @Override
+    public Producer createProducer() throws Exception {
+        return new PlaceholderProducer(this);
+    }
+    
+    @Override
+    public Consumer createConsumer(Processor processor) throws Exception {
+        return new PlaceholderConsumer(this, processor);
+    }
+}
\ No newline at end of file
diff --git a/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/discover/PlaceholderProducer.java b/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/discover/PlaceholderProducer.java
new file mode 100644
index 00000000..ed7179a6
--- /dev/null
+++ b/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/discover/PlaceholderProducer.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2022 Vantiq, Inc.
+ *
+ * All rights reserved.
+ *
+ * SPDX: MIT
+ */
+
+package io.vantiq.extsrc.camelconn.discover;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.camel.Exchange;
+import org.apache.camel.support.DefaultProducer;
+
+/**
+ * Placeholder producer to accompany {@link PlaceholderComponent}, {@link PlaceholderConsumer}, and
+ * {@link PlaceholderEndpoint} in support of gathering the names of the required components via
+ * the {@link EnumeratingComponentResolver}.  There is sufficient mechanism here to convince the CamelContext to
+ * continue to add and start routes, allowing the resolution to continue to completion.
+ *
+ */
+@Slf4j
+public class PlaceholderProducer extends DefaultProducer {
+    private final PlaceholderEndpoint endpoint;
+    
+    public PlaceholderProducer(PlaceholderEndpoint endpoint) {
+        super(endpoint);
+        this.endpoint = endpoint;
+    }
+    
+    @Override
+    public void process(Exchange exchange) throws Exception {
+        // noop since we won't get this far.
+    }
+    
+    @Override
+    protected void doStart() throws Exception {
+        super.doStart();
+    }
+    
+    @Override
+    protected void doStop() throws Exception {
+        if (null != endpoint) {
+            endpoint.shutdown();
+        }
+        super.doStop();
+    }
+}
diff --git a/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/discover/ResolutionException.java b/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/discover/ResolutionException.java
new file mode 100644
index 00000000..32b6fd3b
--- /dev/null
+++ b/camelConnector/src/main/java/io/vantiq/extsrc/camelconn/discover/ResolutionException.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2022 Vantiq, Inc.
+ *
+ * All rights reserved.
+ *
+ * SPDX: MIT
+ */
+
+package io.vantiq.extsrc.camelconn.discover;
+
+/**
+ * Marker for exceptions found during the component resolution process.
+ */
+public class ResolutionException extends Exception {
+    public ResolutionException(String message, Throwable cause) {
+        super(message, cause);
+    }
+    
+    public ResolutionException(String message) {
+        super(message);
+    }
+}
diff --git a/camelConnector/src/main/resources/artifactMap.json b/camelConnector/src/main/resources/artifactMap.json
new file mode 100644
index 00000000..476beb53
--- /dev/null
+++ b/camelConnector/src/main/resources/artifactMap.json
@@ -0,0 +1,402 @@
+{
+    "camelVersion": "3.18.3",
+    "components": {
+        "dns": "camel-dns",
+        "docker": "camel-docker",
+        "netty": "camel-netty",
+        "iec60870-client": "camel-iec60870",
+        "iec60870-server": "camel-iec60870",
+        "minio": "camel-minio",
+        "xmlsecurity-sign": "camel-xmlsecurity",
+        "xmlsecurity-verify": "camel-xmlsecurity",
+        "graphql": "camel-graphql",
+        "hwcloud-smn": "camel-huaweicloud-smn",
+        "hwcloud-imagerecognition": "camel-huaweicloud-imagerecognition",
+        "hwcloud-functiongraph": "camel-huaweicloud-functiongraph",
+        "hwcloud-frs": "camel-huaweicloud-frs",
+        "hwcloud-obs": "camel-huaweicloud-obs",
+        "hwcloud-iam": "camel-huaweicloud-iam",
+        "hwcloud-dms": "camel-huaweicloud-dms",
+        "lucene": "camel-lucene",
+        "ipfs": "camel-ipfs",
+        "jms": "camel-jms",
+        "elasticsearch-rest": "camel-elasticsearch-rest",
+        "atmos": "camel-atmos",
+        "atom": "camel-atom",
+        "spring-jdbc": "camel-spring-jdbc",
+        "file": "camel-file",
+        "file-watch": "camel-file-watch",
+        "grpc": "camel-grpc",
+        "spring-redis": "camel-spring-redis",
+        "lumberjack": "camel-lumberjack",
+        "direct": "camel-direct",
+        "jooq": "camel-jooq",
+        "mina": "camel-mina",
+        "couchbase": "camel-couchbase",
+        "cxf": "camel-cxf-soap",
+        "cxfrs": "camel-cxf-rest",
+        "ldif": "camel-ldif",
+        "jclouds": "camel-jclouds",
+        "netty-http": "camel-netty-http",
+        "jbpm": "camel-jbpm",
+        "mvel": "camel-mvel",
+        "xchange": "camel-xchange",
+        "dropbox": "camel-dropbox",
+        "bean-validator": "camel-bean-validator",
+        "aws2-athena": "camel-aws2-athena",
+        "xray-test": "camel-aws-xray",
+        "aws2-mq": "camel-aws2-mq",
+        "aws2-sts": "camel-aws2-sts",
+        "aws2-ec2": "camel-aws2-ec2",
+        "aws2-ses": "camel-aws2-ses",
+        "aws2-eventbridge": "camel-aws2-eventbridge",
+        "aws2-msk": "camel-aws2-msk",
+        "aws2-lambda": "camel-aws2-lambda",
+        "aws2-iam": "camel-aws2-iam",
+        "aws2-cw": "camel-aws2-cw",
+        "aws-secrets-manager": "camel-aws-secrets-manager",
+        "aws2-eks": "camel-aws2-eks",
+        "aws2-ecs": "camel-aws2-ecs",
+        "aws2-sqs": "camel-aws2-sqs",
+        "aws2-ddbstream": "camel-aws2-ddb",
+        "aws2-ddb": "camel-aws2-ddb",
+        "aws2-s3": "camel-aws2-s3",
+        "aws2-sns": "camel-aws2-sns",
+        "aws2-kinesis-firehose": "camel-aws2-kinesis",
+        "aws2-kinesis": "camel-aws2-kinesis",
+        "aws2-translate": "camel-aws2-translate",
+        "aws2-kms": "camel-aws2-kms",
+        "snmp": "camel-snmp",
+        "mongodb-gridfs": "camel-mongodb-gridfs",
+        "digitalocean": "camel-digitalocean",
+        "timer": "camel-timer",
+        "sql-stored": "camel-sql",
+        "sql": "camel-sql",
+        "vm": "camel-vm",
+        "pg-replication-slot": "camel-pg-replication-slot",
+        "controlbus": "camel-controlbus",
+        "paho": "camel-paho",
+        "freemarker": "camel-freemarker",
+        "guava-eventbus": "camel-guava-eventbus",
+        "debezium-mongodb": "camel-debezium-mongodb",
+        "debezium-sqlserver": "camel-debezium-sqlserver",
+        "debezium-postgres": "camel-debezium-postgres",
+        "debezium-mysql": "camel-debezium-mysql",
+        "debezium-oracle": "camel-debezium-oracle",
+        "debezium-db2": "camel-debezium-db2",
+        "scp": "camel-jsch",
+        "git": "camel-git",
+        "cmis": "camel-cmis",
+        "crypto": "camel-crypto",
+        "dynamic-router": "camel-dynamic-router",
+        "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",
+        "hazelcast-map": "camel-hazelcast",
+        "hazelcast-seda": "camel-hazelcast",
+        "hazelcast-atomicvalue": "camel-hazelcast",
+        "hazelcast-queue": "camel-hazelcast",
+        "hazelcast-list": "camel-hazelcast",
+        "hazelcast-multimap": "camel-hazelcast",
+        "hazelcast-instance": "camel-hazelcast",
+        "hazelcast-replicatedmap": "camel-hazelcast",
+        "geocoder": "camel-geocoder",
+        "facebook": "camel-facebook",
+        "ehcache": "camel-ehcache",
+        "oaipmh": "camel-oaipmh",
+        "zookeeper-master": "camel-zookeeper-master",
+        "azure-eventhubs": "camel-azure-eventhubs",
+        "azure-storage-blob": "camel-azure-storage-blob",
+        "azure-cosmosdb": "camel-azure-cosmosdb",
+        "azure-storage-datalake": "camel-azure-storage-datalake",
+        "azure-key-vault": "camel-azure-key-vault",
+        "azure-servicebus": "camel-azure-servicebus",
+        "azure-storage-queue": "camel-azure-storage-queue",
+        "iota": "camel-iota",
+        "seda": "camel-seda",
+        "flink": "camel-flink",
+        "atmosphere-websocket": "camel-atmosphere-websocket",
+        "ssh": "camel-ssh",
+        "saga": "camel-saga",
+        "websocket-jsr356": "camel-websocket-jsr356",
+        "chunk": "camel-chunk",
+        "jgroups-raft": "camel-jgroups-raft",
+        "quickfix": "camel-quickfix",
+        "disruptor-vm": "camel-disruptor",
+        "disruptor": "camel-disruptor",
+        "chatscript": "camel-chatscript",
+        "bonita": "camel-bonita",
+        "resteasy": "camel-resteasy",
+        "paho-mqtt5": "camel-paho-mqtt5",
+        "amqp": "camel-amqp",
+        "spring-integration": "camel-spring-integration",
+        "influxdb": "camel-influxdb",
+        "xj": "camel-xj",
+        "log": "camel-log",
+        "ignite-set": "camel-ignite",
+        "ignite-queue": "camel-ignite",
+        "ignite-messaging": "camel-ignite",
+        "ignite-compute": "camel-ignite",
+        "ignite-idgen": "camel-ignite",
+        "ignite-cache": "camel-ignite",
+        "ignite-events": "camel-ignite",
+        "weather": "camel-weather",
+        "json-validator": "camel-json-validator",
+        "splunk-hec": "camel-splunk-hec",
+        "jcr": "camel-jcr",
+        "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",
+        "google-storage": "camel-google-storage",
+        "google-secret-manager": "camel-google-secret-manager",
+        "google-bigquery-sql": "camel-google-bigquery",
+        "google-bigquery": "camel-google-bigquery",
+        "google-sheets-stream": "camel-google-sheets",
+        "google-sheets": "camel-google-sheets",
+        "google-calendar": "camel-google-calendar",
+        "google-calendar-stream": "camel-google-calendar",
+        "google-mail": "camel-google-mail",
+        "google-mail-stream": "camel-google-mail",
+        "google-drive": "camel-google-drive",
+        "google-pubsub": "camel-google-pubsub",
+        "google-functions": "camel-google-functions",
+        "optaplanner": "camel-optaplanner",
+        "corda": "camel-corda",
+        "pop3s": "camel-mail",
+        "imap": "camel-mail",
+        "imaps": "camel-mail",
+        "smtp": "camel-mail",
+        "pop3": "camel-mail",
+        "smtps": "camel-mail",
+        "telegram": "camel-telegram",
+        "stream": "camel-stream",
+        "xslt-saxon": "camel-xslt-saxon",
+        "service": "camel-service",
+        "schematron": "camel-schematron",
+        "drill": "camel-drill",
+        "bean": "camel-bean",
+        "class": "camel-bean",
+        "ldap": "camel-ldap",
+        "ircs": "camel-irc",
+        "irc": "camel-irc",
+        "hdfs": "camel-hdfs",
+        "exec": "camel-exec",
+        "stitch": "camel-stitch",
+        "gora": "camel-gora",
+        "stub": "camel-stub",
+        "rest-openapi": "camel-rest-openapi",
+        "rss": "camel-rss",
+        "sjms": "camel-sjms",
+        "string-template": "camel-stringtemplate",
+        "pgevent": "camel-pgevent",
+        "scheduler": "camel-scheduler",
+        "couchdb": "camel-couchdb",
+        "spark": "camel-spark",
+        "avro": "camel-avro-rpc",
+        "jcache": "camel-jcache",
+        "cometd": "camel-cometd",
+        "cometds": "camel-cometd",
+        "arangodb": "camel-arangodb",
+        "xmpp": "camel-xmpp",
+        "zookeeper": "camel-zookeeper",
+        "jmx": "camel-jmx",
+        "caffeine-loadcache": "camel-caffeine",
+        "caffeine-cache": "camel-caffeine",
+        "spring-batch": "camel-spring-batch",
+        "cm-sms": "camel-cm-sms",
+        "https": "camel-http",
+        "http": "camel-http",
+        "asterisk": "camel-asterisk",
+        "dataformat": "camel-dataformat",
+        "workday": "camel-workday",
+        "jolt": "camel-jolt",
+        "mybatis": "camel-mybatis",
+        "mybatis-bean": "camel-mybatis",
+        "nats": "camel-nats",
+        "webhook": "camel-webhook",
+        "jira": "camel-jira",
+        "thrift": "camel-thrift",
+        "undertow": "camel-undertow",
+        "direct-vm": "camel-directvm",
+        "web3j": "camel-web3j",
+        "kubernetes-job": "camel-kubernetes",
+        "kubernetes-nodes": "camel-kubernetes",
+        "kubernetes-hpa": "camel-kubernetes",
+        "kubernetes-persistent-volumes-claims": "camel-kubernetes",
+        "kubernetes-custom-resources": "camel-kubernetes",
+        "kubernetes-service-accounts": "camel-kubernetes",
+        "kubernetes-services": "camel-kubernetes",
+        "kubernetes-pods": "camel-kubernetes",
+        "openshift-build-configs": "camel-kubernetes",
+        "kubernetes-resources-quota": "camel-kubernetes",
+        "kubernetes-persistent-volumes": "camel-kubernetes",
+        "openshift-builds": "camel-kubernetes",
+        "kubernetes-secrets": "camel-kubernetes",
+        "kubernetes-config-maps": "camel-kubernetes",
+        "kubernetes-replication-controllers": "camel-kubernetes",
+        "kubernetes-namespaces": "camel-kubernetes",
+        "kubernetes-deployments": "camel-kubernetes",
+        "openshift-deploymentconfigs": "camel-kubernetes",
+        "velocity": "camel-velocity",
+        "mustache": "camel-mustache",
+        "tika": "camel-tika",
+        "grape": "camel-grape",
+        "servicenow": "camel-servicenow",
+        "metrics": "camel-metrics",
+        "ahc-wss": "camel-ahc-ws",
+        "ahc-ws": "camel-ahc-ws",
+        "jslt": "camel-jslt",
+        "mllp": "camel-mllp",
+        "infinispan-embedded": "camel-infinispan-embedded",
+        "infinispan": "camel-infinispan",
+        "platform-http": "camel-platform-http",
+        "solr": "camel-solr",
+        "solrCloud": "camel-solr",
+        "solrs": "camel-solr",
+        "language": "camel-language",
+        "jdbc": "camel-jdbc",
+        "jpa": "camel-jpa",
+        "microprofile-metrics": "camel-microprofile-metrics",
+        "rabbitmq": "camel-rabbitmq",
+        "twitter-search": "camel-twitter",
+        "twitter-directmessage": "camel-twitter",
+        "twitter-timeline": "camel-twitter",
+        "github": "camel-github",
+        "pdf": "camel-pdf",
+        "slack": "camel-slack",
+        "consul": "camel-consul",
+        "servlet": "camel-servlet",
+        "master": "camel-master",
+        "splunk": "camel-splunk",
+        "cron": "camel-cron",
+        "spring-ldap": "camel-spring-ldap",
+        "hbase": "camel-hbase",
+        "olingo2": "camel-olingo2",
+        "rest": "camel-rest",
+        "rest-api": "camel-rest",
+        "ironmq": "camel-ironmq",
+        "rest-swagger": "camel-rest-swagger",
+        "ref": "camel-ref",
+        "validator": "camel-validator",
+        "json-patch": "camel-json-patch",
+        "nitrite": "camel-nitrite",
+        "flatpack": "camel-flatpack",
+        "salesforce": "camel-salesforce",
+        "ftps": "camel-ftp",
+        "ftp": "camel-ftp",
+        "sftp": "camel-ftp",
+        "spring-rabbitmq": "camel-spring-rabbitmq",
+        "cql": "camel-cassandraql",
+        "olingo4": "camel-olingo4",
+        "spring-event": "camel-spring",
+        "wordpress": "camel-wordpress",
+        "coaps": "camel-coap",
+        "coap+tcp": "camel-coap",
+        "coaps+tcp": "camel-coap",
+        "coap": "camel-coap",
+        "kafka": "camel-kafka",
+        "twilio": "camel-twilio",
+        "mongodb": "camel-mongodb",
+        "fhir": "camel-fhir",
+        "kamelet": "camel-kamelet",
+        "ahc": "camel-ahc",
+        "activemq": "camel-activemq",
+        "stomp": "camel-stomp",
+        "weka": "camel-weka",
+        "xquery": "camel-saxon",
+        "jt400": "camel-jt400",
+        "openstack-glance": "camel-openstack",
+        "openstack-keystone": "camel-openstack",
+        "openstack-swift": "camel-openstack",
+        "openstack-neutron": "camel-openstack",
+        "openstack-nova": "camel-openstack",
+        "openstack-cinder": "camel-openstack",
+        "sap-netweaver": "camel-sap-netweaver",
+        "xslt": "camel-xslt",
+        "smpp": "camel-smpp",
+        "smpps": "camel-smpp",
+        "jgroups": "camel-jgroups",
+        "as2": "camel-as2",
+        "browse": "camel-browse",
+        "atlasmap": "camel-atlasmap",
+        "spring-ws": "camel-spring-ws",
+        "hashicorp-vault": "camel-hashicorp-vault",
+        "file_test": "camel-spring-xml",
+        "timer_test": "camel-spring-xml",
+        "dataset": "camel-dataset",
+        "dataset-test": "camel-dataset",
+        "quartz": "camel-quartz",
+        "pulsar": "camel-pulsar",
+        "lpr": "camel-printer",
+        "knative": "camel-knative",
+        "milo-server": "camel-milo",
+        "milo-client": "camel-milo",
+        "milo-browse": "camel-milo",
+        "kudu": "camel-kudu",
+        "fop": "camel-fop",
+        "djl": "camel-djl",
+        "braintree": "camel-braintree",
+        "jsonata": "camel-jsonata",
+        "box": "camel-box",
+        "websocket": "camel-websocket"
+    },
+    "dataformats": {
+        "ical": "camel-ical",
+        "syslog": "camel-syslog",
+        "xmlSecurity": "camel-xmlsecurity",
+        "bindyFixed": "camel-bindy",
+        "bindyKvp": "camel-bindy",
+        "bindyCsv": "camel-bindy",
+        "gson": "camel-gson",
+        "lzf": "camel-lzf",
+        "crypto": "camel-crypto",
+        "pgp": "camel-crypto",
+        "johnzon": "camel-johnzon",
+        "protobuf": "camel-protobuf",
+        "asn1": "camel-asn1",
+        "soap": "camel-soap",
+        "any23": "camel-any23",
+        "gzipDeflater": "camel-zip-deflater",
+        "zipDeflater": "camel-zip-deflater",
+        "grok": "camel-grok",
+        "xstreamJson": "camel-xstream",
+        "xstream": "camel-xstream",
+        "jacksonXml": "camel-jacksonxml",
+        "jackson": "camel-jackson",
+        "univocityTsv": "camel-univocity-parsers",
+        "univocityFixed": "camel-univocity-parsers",
+        "univocityCsv": "camel-univocity-parsers",
+        "hl7": "camel-hl7",
+        "cbor": "camel-cbor",
+        "mimeMultipart": "camel-mail",
+        "csv": "camel-csv",
+        "base64": "camel-base64",
+        "protobufJackson": "camel-jackson-protobuf",
+        "barcode": "camel-barcode",
+        "avroJackson": "camel-jackson-avro",
+        "rss": "camel-rss",
+        "tarFile": "camel-tarfile",
+        "jsonApi": "camel-jsonapi",
+        "thrift": "camel-thrift",
+        "avro": "camel-avro",
+        "jaxb": "camel-jaxb",
+        "flatpack": "camel-flatpack",
+        "fhirJson": "camel-fhir",
+        "fhirXml": "camel-fhir",
+        "jsonb": "camel-jsonb",
+        "fastjson": "camel-fastjson",
+        "zipFile": "camel-zipfile",
+        "snakeYaml": "camel-snakeyaml"
+    }
+}
\ No newline at end of file
diff --git a/camelConnector/src/main/resources/log4j2.properties b/camelConnector/src/main/resources/log4j2.properties
new file mode 100644
index 00000000..009d277b
--- /dev/null
+++ b/camelConnector/src/main/resources/log4j2.properties
@@ -0,0 +1,12 @@
+
+appender.out.type = Console
+appender.out.name = out
+appender.out.layout.type = PatternLayout
+appender.out.layout.pattern = [%30.30t] %-30.30c{1} %-5p %m%n
+rootLogger.level = INFO
+rootLogger.appenderRef.out.ref = out
+
+logger.vantiqcamelconn.name = io.vantiq.extsrc.camelconn
+logger.vantiqcamelconn.level = DEBUG
+logger.vantiqcamelconn.appenderRef = Console
+
diff --git a/extjsdk/build.gradle b/extjsdk/build.gradle
index 583dd631..81bd44e2 100644
--- a/extjsdk/build.gradle
+++ b/extjsdk/build.gradle
@@ -9,8 +9,6 @@ plugins {
 group 'io.vantiq'
 version 'unspecified'
 
-sourceCompatibility = 1.8
-
 ext {
     // Open the Git repository in the current directory and get commit id of HEAD.
 //    git = org.ajoberstar.grgit.Grgit.open(file('.'))
diff --git a/settings.gradle b/settings.gradle
index ce697f30..45e2c85a 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,5 +1,7 @@
 rootProject.name = 'vantiq-extension-source'
 include 'extjsdk'
+include 'camelComponent'
+include 'camelConnector'
 include 'extpsdk'
 include 'opcuaSource'
 include 'pythonExecSource'


Test patch:
diff --git a/camelComponent/src/test/java/io/vantiq/extsrc/camel/ClientRegistryTest.java b/camelComponent/src/test/java/io/vantiq/extsrc/camel/ClientRegistryTest.java
new file mode 100644
index 00000000..b055d1f1
--- /dev/null
+++ b/camelComponent/src/test/java/io/vantiq/extsrc/camel/ClientRegistryTest.java
@@ -0,0 +1,129 @@
+package io.vantiq.extsrc.camel;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import io.vantiq.extjsdk.ExtensionWebSocketClient;
+import io.vantiq.extsrc.camel.utils.ClientRegistry;
+import org.junit.After;
+import org.junit.Test;
+
+import java.util.concurrent.atomic.AtomicReference;
+
+public class ClientRegistryTest {
+    
+    private final static String SOURCE_NAME = "aRandomSourceName";
+    private final static String SOURCE_NAME_2 = "anotherRandomeSourcename";
+    private final static String SERVER_URL = "https://testjunk.vantiq.com";
+    private final static String SERVER_URL_2 = "https://teststuff.vantiq.com";
+    
+    private final static String SERVER_URL_W_SPACES = "   " + SERVER_URL + "/  ";
+    
+    
+    @After
+    public void removeDetritus() {
+        removeClient(SOURCE_NAME, SERVER_URL);
+        removeClient(SOURCE_NAME_2, SERVER_URL);
+        removeClient(SOURCE_NAME, SERVER_URL_2);
+        removeClient(SOURCE_NAME_2, SERVER_URL_2);
+    }
+    
+    @Test
+    public void testNotThere() {
+        ExtensionWebSocketClient client =
+                ClientRegistry.fetchClient(SOURCE_NAME, SERVER_URL);
+        assertNull("Client should be null", client);
+    }
+    
+    @Test
+    public void testSimpleRegister() {
+        try {
+            ExtensionWebSocketClient client =
+                    new ExtensionWebSocketClient(SOURCE_NAME, 15, null);
+            ClientRegistry.registerClient(SOURCE_NAME, SERVER_URL, client);
+            ExtensionWebSocketClient foundClient =
+                    ClientRegistry.fetchClient("bar", SERVER_URL);
+            assertNull("Client should be null", foundClient);
+            foundClient = ClientRegistry.fetchClient(SOURCE_NAME, SERVER_URL);
+            assertNotNull("Client should be found", foundClient);
+            assertEquals(client, foundClient);
+    
+            foundClient = ClientRegistry.fetchClient(SOURCE_NAME, SERVER_URL);
+            assertNotNull("Client should be found", foundClient);
+            assertEquals(client, foundClient);
+    
+            foundClient = ClientRegistry.fetchClient(SOURCE_NAME, SERVER_URL + "/");
+            assertNotNull("Normalized URL-based client should be found", foundClient);
+            assertEquals(client, foundClient);
+        } finally {
+            // Verify remove functionality
+            ExtensionWebSocketClient removedClient = ClientRegistry.removeClient(SOURCE_NAME, SERVER_URL, null);
+            assertNotNull("Client should exist before removal", removedClient);
+            removedClient = ClientRegistry.fetchClient(SOURCE_NAME, SERVER_URL);
+            assertNull("Client should be null after remove", removedClient);
+        }
+    }
+    
+    @Test
+    public void testClientConstruction() {
+        buildAndVerify(SOURCE_NAME, SERVER_URL,true);
+        buildAndVerify(SOURCE_NAME_2, SERVER_URL, true);
+        buildAndVerify(SOURCE_NAME, SERVER_URL_2, true);
+        buildAndVerify(SOURCE_NAME_2, SERVER_URL_2, true);
+        
+        assertEquals("Known client count", 4, ClientRegistry.getKnownClientCount());
+    }
+    
+    @Test
+    public void testClientDuplicates() {
+        buildAndVerify(SOURCE_NAME, SERVER_URL, true);
+        buildAndVerify(SOURCE_NAME_2, SERVER_URL, true);
+        buildAndVerify(SOURCE_NAME, SERVER_URL_2, true);
+        buildAndVerify(SOURCE_NAME_2, SERVER_URL_2,true);
+    
+        assertEquals("Known client count", 4, ClientRegistry.getKnownClientCount());
+    
+        buildAndVerify(SOURCE_NAME_2, SERVER_URL_2, false);
+        buildAndVerify(SOURCE_NAME, SERVER_URL_2, false);
+        buildAndVerify(SOURCE_NAME_2, SERVER_URL, false);
+        buildAndVerify(SOURCE_NAME, SERVER_URL, false);
+        
+        assertEquals("Known client count after duplicate creates", 4, ClientRegistry.getKnownClientCount());
+    }
+    
+    @Test
+    public void testServerUrlWSpaces() {
+        buildAndVerify(SOURCE_NAME, SERVER_URL, true);
+        buildAndVerify(SOURCE_NAME, SERVER_URL_W_SPACES, false);
+        
+        assertEquals("Known client count", 1, ClientRegistry.getKnownClientCount());
+    }
+    
+    public void buildAndVerify(String srcName, String target, boolean shouldBeCreated) {
+        // Lambda's can only reference "effectively final" values, so we'll wrap in an AtomicReference
+        AtomicReference<ExtensionWebSocketClient> client = new AtomicReference<>();
+        ClientRegistry.registerClient(srcName, target, (src, url) -> {
+            client.set(new ExtensionWebSocketClient(srcName, 15, null));
+            return client.get();
+        });
+    
+        if (shouldBeCreated) {
+            assertNotNull("Client should've been created", client.get());
+        } else {
+            assertNull("Should not be created", client.get());
+        }
+        ExtensionWebSocketClient foundClient = ClientRegistry.fetchClient(srcName, target);
+        assertNotNull("Client should be registered", foundClient);
+        if (shouldBeCreated) {
+            assertEquals("Registered client should be what we created", foundClient, client.get());
+        }
+        assertEquals("Source name match", srcName, foundClient.getSourceName());
+    }
+    
+    private void removeClient(String srcName, String target) {
+        ClientRegistry.removeClient(srcName, target, null);
+        ExtensionWebSocketClient removedClient = ClientRegistry.fetchClient(srcName, target);
+        assertNull("Client should be null after remove", removedClient);
+    }
+}
diff --git a/camelComponent/src/test/java/io/vantiq/extsrc/camel/FauxVantiqComponent.java b/camelComponent/src/test/java/io/vantiq/extsrc/camel/FauxVantiqComponent.java
new file mode 100644
index 00000000..95226314
--- /dev/null
+++ b/camelComponent/src/test/java/io/vantiq/extsrc/camel/FauxVantiqComponent.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) 2022 Vantiq, Inc.
+ *
+ * All rights reserved.
+ *
+ * SPDX: MIT
+ */
+
+package io.vantiq.extsrc.camel;
+
+import org.apache.camel.Endpoint;
+
+import java.util.Map;
+
+@org.apache.camel.spi.annotations.Component("vantiq")
+public class FauxVantiqComponent extends VantiqComponent {
+    
+    public FauxVantiqEndpoint myEndpoint = null;
+    
+    protected Endpoint createEndpoint(String uri, String remaining, Map<String, Object> parameters) throws Exception {
+        Endpoint endpoint = new FauxVantiqEndpoint(uri, this);
+        myEndpoint = (FauxVantiqEndpoint) endpoint;
+        setProperties(endpoint, parameters);
+        return endpoint;
+    }
+}
diff --git a/camelComponent/src/test/java/io/vantiq/extsrc/camel/FauxVantiqEndpoint.java b/camelComponent/src/test/java/io/vantiq/extsrc/camel/FauxVantiqEndpoint.java
new file mode 100644
index 00000000..5c9764f9
--- /dev/null
+++ b/camelComponent/src/test/java/io/vantiq/extsrc/camel/FauxVantiqEndpoint.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2022 Vantiq, Inc.
+ *
+ * All rights reserved.
+ *
+ * SPDX: MIT
+ */
+
+package io.vantiq.extsrc.camel;
+
+import io.vantiq.extjsdk.ExtensionWebSocketClient;
+import io.vantiq.extjsdk.FalseClient;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.camel.Category;
+import org.apache.camel.spi.UriEndpoint;
+import org.apache.camel.spi.UriParams;
+
+/**
+ * vantiq endpoint, managing communications with the vantiq server.
+ **/
+@UriEndpoint(firstVersion = "1.0-SNAPSHOT", scheme = "vantiq", title = "vantiq", syntax="vantiq:name",
+             category = {Category.JAVA})
+@UriParams
+@Slf4j
+public class FauxVantiqEndpoint extends VantiqEndpoint {
+    public FalseClient myClient = null;
+    FauxVantiqEndpoint(String uri, VantiqComponent component) throws Exception {
+        super(uri, component);
+    }
+    
+    /**
+     * Constructs & returns an ExtensionWebSocketClient.
+     *
+     * This is here primarily to override for unit testing. In unit test cases, we override to return a client
+     * that doesn't send to the (nonexistent Vantiq installation);
+     *
+     * @param source        String name of the source to which to connect
+     * @param msgQueueSize  int number of messages to queue when the connection is broken.
+     * @return ExtensionWebSocketClient
+     */
+    @Override
+    protected ExtensionWebSocketClient buildVantiqClient(String source, int msgQueueSize) {
+        // Save the client so unit tests can pick it up.
+        myClient = new FalseClient(source);
+        return myClient;
+    }
+    
+    @Override
+    protected void completeFuturesForFauxInstances() {
+        if (myClient != null) {
+            myClient.completeWebSocketConnection(true);
+            myClient.completeAuthentication(true);
+            myClient.completeSourceConnection(true);
+        }
+    }
+ }
diff --git a/camelComponent/src/test/java/io/vantiq/extsrc/camel/VantiqComponentTest.java b/camelComponent/src/test/java/io/vantiq/extsrc/camel/VantiqComponentTest.java
new file mode 100644
index 00000000..60d37996
--- /dev/null
+++ b/camelComponent/src/test/java/io/vantiq/extsrc/camel/VantiqComponentTest.java
@@ -0,0 +1,362 @@
+/*
+ * Copyright (c) 2022 Vantiq, Inc.
+ *
+ * All rights reserved.
+ *
+ * SPDX: MIT
+ */
+
+package io.vantiq.extsrc.camel;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import io.vantiq.extjsdk.ExtensionServiceMessage;
+import io.vantiq.extjsdk.FalseClient;
+import io.vantiq.extjsdk.FalseWebSocket;
+import io.vantiq.extjsdk.Response;
+import io.vantiq.extjsdk.TestListener;
+import okhttp3.WebSocket;
+import okio.ByteString;
+import org.apache.camel.Exchange;
+import org.apache.camel.ExchangePattern;
+import org.apache.camel.InvalidPayloadException;
+import org.apache.camel.LoggingLevel;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.test.junit4.CamelTestSupport;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class VantiqComponentTest extends CamelTestSupport {
+    
+    private static final String testSourceName = "camelSource";
+    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 final String routeStartUri = "direct:start";
+    private final String routeEndUri = "mock:direct:result";
+    
+    private final String exceptionEndpoint = "mock:direct:error";
+    private final String vantiqEndpointUri = "vantiq://doesntmatter/" +
+            "?sourceName=" + testSourceName +
+            "&accessToken=" + accessToken;
+    
+    private final String vantiqSenderUri = "vantiq://senderdoesntmatter/" +
+            "?sourceName=" + testSourceName +
+            "&accessToken=" + accessToken;
+    
+    private final String vantiqQuerySenderUri = "vantiq://querierdoesntmatter/" +
+            "?sourceName=" + testSourceName +
+            "&accessToken=" + accessToken;
+    
+    @Test
+    public void testVantiqSetup() throws Exception {
+        // First, grab our test environment.
+        FauxVantiqComponent vc = (FauxVantiqComponent) context.getComponent("vantiq");
+        assert vc != null;
+        
+        FauxVantiqEndpoint epConsumer = (FauxVantiqEndpoint) context.getEndpoint(vantiqSenderUri);
+        assert epConsumer.isConnected();
+        assert epConsumer.getEndpointUri().contains(accessToken);
+        assert epConsumer.getEndpointUri().contains(testSourceName);
+        assert vantiqSenderUri.startsWith(epConsumer.getEndpointBaseUri());
+        
+        FauxVantiqEndpoint epProducer = (FauxVantiqEndpoint) context.getEndpoint(vantiqEndpointUri);
+        assert epProducer.isConnected();
+        assert epProducer.getEndpointUri().contains(accessToken);
+        assert epProducer.getEndpointUri().contains(testSourceName);
+        assert vantiqEndpointUri.startsWith(epProducer.getEndpointBaseUri());
+    }
+    
+    @Test
+    public void testVantiqProducerJson() 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 ep = (FauxVantiqEndpoint) context.getEndpoint(vantiqEndpointUri);
+        FalseClient fc = ep.myClient;
+        
+        ArrayList<String> msgs =new ArrayList<String>();
+        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(routeStartUri, item);
+            Map lastMsg = fc.getLastMessageAsMap();
+            assert lastMsg != null;
+            assert lastMsg.containsKey("op");
+            assert "notification".equals(lastMsg.get("op"));
+            assert lastMsg.containsKey("sourceName");
+            assert testSourceName.equals(lastMsg.get("sourceName"));
+            assert lastMsg.containsKey("object");
+            assert lastMsg.get("object") instanceof Map;
+            Map<String, Object> msg = (Map<String, Object>) lastMsg.get("object");
+            assert msg.containsKey("hi");
+            assert ((String) msg.get("hi")).contains("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);
+        }
+
+        for (Map item: mapList) {
+            sendBody(routeStartUri, item);
+    
+            Map lastMsg = fc.getLastMessageAsMap();
+            assert lastMsg != null;
+            assert lastMsg.containsKey("op");
+            assert "notification".equals(lastMsg.get("op"));
+            assert lastMsg.containsKey("sourceName");
+            assert testSourceName.equals(lastMsg.get("sourceName"));
+            assert lastMsg.containsKey("object");
+            assert lastMsg.get("object") instanceof Map;
+            Map<String, Object> msg = (Map<String, Object>) lastMsg.get("object");
+            assert msg.containsKey("hi");
+            assert ((String) msg.get("hi")).contains("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());
+                sendBody(routeStartUri, node);
+    
+                Map lastMsg = fc.getLastMessageAsMap();
+                assert lastMsg != null;
+                assert lastMsg.containsKey("op");;
+                assert "notification".equals(lastMsg.get("op"));
+                assert lastMsg.containsKey("sourceName");
+                assert testSourceName.equals(lastMsg.get("sourceName"));
+                assert lastMsg.containsKey("object");
+                assert lastMsg.get("object") instanceof Map;
+                Map<String, Object> testMsg = (Map<String, Object>) lastMsg.get("object");
+                assert testMsg.containsKey("hi");
+                assert ((String) testMsg.get("hi")).contains("aki");
+            } catch (JsonProcessingException e) {
+                throw new RuntimeException(e);
+            }
+        });
+        
+        sendBody(routeStartUri, "{\"bye\": \"mom\"}");
+    
+        Map lastMsg = fc.getLastMessageAsMap();
+        assert lastMsg != null;
+        assert lastMsg.containsKey("op");
+        assert "notification".equals(lastMsg.get("op"));
+        assert lastMsg.containsKey("sourceName");
+        assert testSourceName.equals(lastMsg.get("sourceName"));
+        assert lastMsg.containsKey("object");
+        assert lastMsg.get("object") instanceof Map;
+        Map<String, Object> msg = (Map<String, Object>) lastMsg.get("object");
+        assert msg.containsKey("bye");
+        assert "mom".equals(msg.get("bye"));
+    }
+    
+    @Test
+    public void testVantiqProducerInvalid() throws Exception {
+        
+        // First, grab our test environment.
+        FauxVantiqComponent vc = (FauxVantiqComponent) context.getComponent("vantiq");
+        assert vc != null;
+        
+        ArrayList<Integer> msgs = new ArrayList<>();
+        
+        int itemCount = 10;
+        for (int i = 0; i < itemCount; i++) {
+            msgs.add(i);
+        }
+    
+        MockEndpoint mocked = getMockEndpoint(exceptionEndpoint);
+        mocked.expectedMinimumMessageCount(itemCount);
+        
+        for (Object item: msgs) {
+            log.info("Sending msg: " + item);
+            
+            assert item != null;
+            sendBody(routeStartUri, item);
+        }
+    
+        mocked.await(5L, TimeUnit.SECONDS);
+    
+        assertEquals("Mocked service expected vs. actual", itemCount, mocked.getReceivedCounter());
+    }
+    
+    @Test
+    public void testVantiqConsumer() 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(vantiqSenderUri);
+    
+        assert endp.myClient.getListener() instanceof TestListener;
+        TestListener tl = (TestListener) endp.myClient.getListener();
+    
+        int expectedMsgCount = 10;
+        MockEndpoint mocked = getMockEndpoint(routeEndUri);
+        mocked.expectedMinimumMessageCount(expectedMsgCount);
+        VantiqEndpoint ve = getMandatoryEndpoint(vantiqEndpointUri, VantiqEndpoint.class);
+        assert ve != null;
+        ObjectMapper mapper = new ObjectMapper();
+        WebSocket ws = new FalseWebSocket();
+        for (int i = 0; i < expectedMsgCount; i++) {
+            ExtensionServiceMessage ep = new ExtensionServiceMessage(vantiqEndpointUri);
+            ep.op = ExtensionServiceMessage.OP_PUBLISH;
+            ep.resourceName = "SOURCES";
+            ep.resourceId = testSourceName;
+            HashMap<String, Object> msg = new HashMap<>();
+            msg.put(TEST_MSG_KEY, TEST_MSG_PREAMBLE + i);
+            ep.object = msg;
+            
+            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", expectedMsgCount, mocked.getReceivedCounter());
+        List<Exchange> exchanges = mocked.getReceivedExchanges();
+        Set<String> uniqueMsgs = new HashSet<String>();
+        exchanges.forEach(exchange -> {
+                assert exchange.getIn().getBody() instanceof Map;
+                Map msg = exchange.getIn().getBody(Map.class);
+                assert msg.containsKey(TEST_MSG_KEY);
+                assert msg.get(TEST_MSG_KEY) instanceof String;
+                assert ((String) msg.get(TEST_MSG_KEY)).startsWith(TEST_MSG_PREAMBLE);
+                uniqueMsgs.add(((String) msg.get(TEST_MSG_KEY)));
+        });
+        assert uniqueMsgs.size() == expectedMsgCount;
+    }
+    
+    @Test
+    public void testVantiqConsumerQuery() 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(vantiqQuerySenderUri);
+        
+        assert endp.myClient.getListener() instanceof TestListener;
+        TestListener tl = (TestListener) endp.myClient.getListener();
+        
+        int expectedMsgCount = 10;
+        MockEndpoint mocked = getMockEndpoint(routeEndUri);
+        mocked.expectedMinimumMessageCount(expectedMsgCount);
+        VantiqEndpoint ve = getMandatoryEndpoint(vantiqEndpointUri, VantiqEndpoint.class);
+        assert ve != null;
+        ObjectMapper mapper = new ObjectMapper();
+        WebSocket ws = new FalseWebSocket();
+        Set<String> responseAddresses = new HashSet<>();
+        for (int i = 0; i < expectedMsgCount; i++) {
+            ExtensionServiceMessage ep = new ExtensionServiceMessage(vantiqEndpointUri);
+            ep.op = ExtensionServiceMessage.OP_QUERY;
+            ep.resourceName = "SOURCES";
+            ep.resourceId = testSourceName;
+            Map<String, Object> hdrs = new HashMap<>();
+            String respAddr = UUID.randomUUID().toString();
+            hdrs.put(ExtensionServiceMessage.ORIGIN_ADDRESS_HEADER, respAddr);
+            responseAddresses.add(respAddr);
+            ep.messageHeaders = hdrs;
+            HashMap<String, Object> msg = new HashMap<>();
+            msg.put(TEST_MSG_KEY, TEST_MSG_PREAMBLE + i);
+            ep.object = msg;
+            
+            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", expectedMsgCount, mocked.getReceivedCounter());
+        List<Exchange> exchanges = mocked.getReceivedExchanges();
+        Set<String> uniqueMsgs = new HashSet<String>();
+        exchanges.forEach(exchange -> {
+            assert exchange.getIn().getBody() instanceof Map;
+            Map msg = exchange.getIn().getBody(Map.class);
+            assert msg.containsKey(TEST_MSG_KEY);
+            assert msg.get(TEST_MSG_KEY) instanceof String;
+            assert ((String) msg.get(TEST_MSG_KEY)).startsWith(TEST_MSG_PREAMBLE);
+            uniqueMsgs.add(((String) msg.get(TEST_MSG_KEY)));
+            
+            Map<String, Object> props = exchange.getProperties();
+            assert props.containsKey(ExtensionServiceMessage.RESPONSE_ADDRESS_HEADER);
+        });
+        assert uniqueMsgs.size() == expectedMsgCount;
+      
+        // Note that we need to fetch the endpoints by URI since there are more than one of them.
+        FauxVantiqEndpoint ep = (FauxVantiqEndpoint) context.getEndpoint(vantiqQuerySenderUri);
+        FalseClient fc = ep.myClient;
+        Response rsp = fc.getLastMessageAsResponse();
+        assert rsp != null;
+        assert rsp.getBody() != null;
+        assert rsp.getStatus() == 200;
+        Object o = rsp.getBody();
+        assert o instanceof Map;
+        Map<String, Object> response = (Map) o;
+        log.debug("Found final response message: {}", response);
+        assert response.containsKey("Response");
+        String ra = rsp.getHeader(ExtensionServiceMessage.RESPONSE_ADDRESS_HEADER);
+        assert responseAddresses.contains(ra);
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() throws Exception {
+        return new RouteBuilder() {
+            public void configure() {
+                
+                // Override the component type to be used...
+                context.addComponent("vantiq", new FauxVantiqComponent());
+                for (String name: context.getComponentNames()) {
+                    log.info("Component name: {}", name);
+                }
+    
+                onException(InvalidPayloadException.class)
+                        .log(LoggingLevel.ERROR, "Got InvalidPayloadException")
+                                .to(exceptionEndpoint);
+                
+                from(routeStartUri)
+                  .to(vantiqEndpointUri);
+                
+                from(vantiqSenderUri)
+                        .to(routeEndUri);
+    
+                from(vantiqQuerySenderUri)
+                        .setExchangePattern(ExchangePattern.InOut)
+                        .to(routeEndUri)
+                            .setBody(constant("{ \"Response\": \"Message\"}"))
+                        .to(vantiqQuerySenderUri);
+            }
+        };
+    }
+}
diff --git a/camelComponent/src/test/java/io/vantiq/extsrc/camel/VantiqEmptyURITest.java b/camelComponent/src/test/java/io/vantiq/extsrc/camel/VantiqEmptyURITest.java
new file mode 100644
index 00000000..33149443
--- /dev/null
+++ b/camelComponent/src/test/java/io/vantiq/extsrc/camel/VantiqEmptyURITest.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (c) 2022 Vantiq, Inc.
+ *
+ * All rights reserved.
+ *
+ * SPDX: MIT
+ */
+
+package io.vantiq.extsrc.camel;
+
+import static io.vantiq.extjsdk.Utils.SERVER_CONFIG_FILENAME;
+
+import org.apache.camel.Endpoint;
+import org.apache.camel.InvalidPayloadException;
+import org.apache.camel.LoggingLevel;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.model.ModelCamelContext;
+import org.junit.Test;
+
+import java.util.Collection;
+
+public class VantiqEmptyURITest extends VantiqUriTestBase {
+    private final String routeStartUri = "direct:start";
+    private final String routeEndUri = "mock:direct:result";
+    
+    FauxVantiqEndpoint fve = null;
+    FauxVantiqEndpoint fve2 = null;
+    
+    // We use the structure where we set the host name to "server.config" to mean that we should (re) construct the
+    // endpoint URIs from the information in the server.config file.  These are used to verify that we got what we
+    // expected.
+    public final String vantiqSrcConfigEnd = "vantiq://" + SERVER_CONFIG_FILENAME;
+    public final String vantiqSrcConfigStart = "vantiq://" + SERVER_CONFIG_FILENAME + "/"; // Check both ways
+
+    @Test
+    public void testEndpointsWithNullUri() throws Exception {
+        // First, grab our test environment.
+        
+        context = (ModelCamelContext) createCamelContext();
+        RouteBuilder nullRoutes = nullEndpointRouteBuilder();
+        context.addRoutes(nullRoutes);
+        context.start();
+        try {
+            FauxVantiqComponent vc = (FauxVantiqComponent) context.getComponent("vantiq");
+            assert vc != null;
+    
+            checkEndpoint(fve);
+            checkEndpoint(fve2);
+    
+            Collection<Endpoint> eps = context.getEndpoints();
+            int vantiqEndpoints = 0;
+            for (Endpoint ep : eps) {
+                if (ep.getEndpointBaseUri().startsWith("vantiq")) {
+                    checkEndpoint(ep);
+                    vantiqEndpoints += 1;
+                }
+            }
+    
+            // In the constructed case, these bean-simulation endpoints get registered manually.  Since they have the
+            // same endpointUri, they are, in the registry, the same endpoint (same key)..  So verify that that's
+            // true.
+            assertEquals("Number of Vantiq endpoints", 1, vantiqEndpoints);
+            assertEquals("Number of Endpoints found", 4, eps.size());
+        } finally {
+            context.stop();
+        }
+    }
+    
+    @Test
+    public void testEndpointsFromServerConfig() throws Exception {
+    
+        context = (ModelCamelContext) createCamelContext();
+        RouteBuilder serverConfigRoutes = configEndpointRouteBuilder();
+        context.addRoutes(serverConfigRoutes);
+        context.start();
+        try {
+            Collection<Endpoint> eps = context.getEndpoints();
+            int vantiqEndpoints = 0;
+            for (Endpoint ep: eps) {
+                if (ep.getEndpointBaseUri().startsWith("vantiq")) {
+                    checkEndpoint(ep);
+                    vantiqEndpoints += 1;
+                }
+            }
+            assert vantiqEndpoints == 2;
+            assert eps.size() == 5;
+        } finally {
+            context.stop();
+        }
+    }
+    
+    protected RouteBuilder configEndpointRouteBuilder() throws Exception {
+        return new RouteBuilder() {
+            public void configure() {
+                
+                FauxVantiqComponent us = new FauxVantiqComponent();
+                // Override the component type to be used...
+                context.addComponent("vantiq", us);
+                for (String name: context.getComponentNames()) {
+                    log.info("Component name: {}", name);
+                }
+                onException(InvalidPayloadException.class)
+                        .log(LoggingLevel.ERROR, "Got InvalidPayloadException")
+                        .to(exceptionEndpoint);
+                
+                from(routeStartUri)
+                        .to(vantiqSrcConfigEnd);
+                
+                from(vantiqSrcConfigStart)
+                        .to(routeEndUri);
+            }
+        };
+    }
+    
+    protected RouteBuilder nullEndpointRouteBuilder() throws Exception {
+        return new RouteBuilder() {
+            public void configure() {
+    
+                FauxVantiqComponent us = new FauxVantiqComponent();
+                // Override the component type to be used...
+                context.addComponent("vantiq", us);
+                for (String name: context.getComponentNames()) {
+                    log.info("Component name: {}", name);
+                }
+                onException(InvalidPayloadException.class)
+                        .log(LoggingLevel.ERROR, "Got InvalidPayloadException")
+                                .to(exceptionEndpoint);
+                try {
+                    fve = new FauxVantiqEndpoint(null, us);
+                    context.addEndpoint(fve.getEndpointUri(), fve);
+                    // Note that since the URI's are the same, these are really the same endpoint in the registry
+                    fve2 = new FauxVantiqEndpoint(null, us);
+                    context.addEndpoint(fve2.getEndpointUri(), fve2);
+                } catch (Exception e) {
+                    assertNull("Exception while creating endpoints", e);
+                }
+    
+                from(routeStartUri)
+                        .to(fve);
+
+                from(fve2)
+                        .to(routeEndUri);
+            }
+        };
+    }
+}
diff --git a/camelComponent/src/test/java/io/vantiq/extsrc/camel/VantiqLiveComponentTest.java b/camelComponent/src/test/java/io/vantiq/extsrc/camel/VantiqLiveComponentTest.java
new file mode 100644
index 00000000..8d981c1e
--- /dev/null
+++ b/camelComponent/src/test/java/io/vantiq/extsrc/camel/VantiqLiveComponentTest.java
@@ -0,0 +1,477 @@
+/*
+ * Copyright (c) 2022 Vantiq, Inc.
+ *
+ * All rights reserved.
+ *
+ * SPDX: MIT
+ */
+
+package io.vantiq.extsrc.camel;
+
+import static org.junit.Assume.assumeTrue;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import io.vantiq.client.Vantiq;
+import io.vantiq.client.VantiqError;
+import io.vantiq.client.VantiqResponse;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.camel.Exchange;
+import org.apache.camel.ExchangePattern;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.jackson.converter.JacksonTypeConvertersLoader;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.test.junit4.CamelTestSupport;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.io.InputStream;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+@Slf4j
+public class VantiqLiveComponentTest extends CamelTestSupport {
+    private static final String SRC_IMPL_TYPE = "CAMEL_COMPONENT";
+    private static final String IMPL_DEF = "camelComponentImpl.json";
+    private static final Integer IMPL_MAX_SIZE = 1000;
+    private static final String VANTIQ_SOURCE_IMPL = "system.sourceimpls";
+    
+    private static final String publishStuffName = System.getProperty("TestCamelPublisher", "publishStuff");
+    private static final String queryStuffName = System.getProperty("TestCamelQuerier", "queryStuff");
+    
+    private static final String testSourceName = System.getProperty("TestCamelSourceName", "camelSource");
+    
+    private static final String testQuerySourceName = System.getProperty("TestCamelSourceName",
+                                                                         "camelSource") + "Query";
+    private static final String testRuleName = testSourceName + "Rule";
+    private static final String testTypeName = testSourceName + "Type";
+    private static final String testMsgPreamble = "published message ";
+    
+    private static final String vantiqInstallation = System.getProperty("TestVantiqServer");
+    public static String vantiqAccessToken = System.getProperty("TestAuthToken");
+    
+    private static Vantiq vantiq;
+    
+    private final String routeStartUri = "direct:start";
+    private final String routeEndUri = "mock:direct:result";
+    private static String vantiqEndpointUri;
+    
+    private static String vantiqQueryEndpointUri;
+    
+    @Test
+    public void testVantiqProducerLive() throws Exception {
+        assumeTrue(vantiqInstallation != null && vantiqAccessToken != null);
+        ArrayList<String> msgs =new ArrayList<String>();
+        int itemCount = 10;
+        for (int i = 0; i < itemCount; i++) {
+            msgs.add("{\"hi\": \"mom " + i + "\"}");
+        }
+        JacksonTypeConvertersLoader jtcl = new JacksonTypeConvertersLoader();
+        jtcl.load(context.getTypeConverterRegistry());
+        log.trace("Type converter count: {}", context.getTypeConverterRegistry().size());
+
+        for (Object item: msgs) {
+            log.debug("Sending msg: " + item);
+            sendBody(routeStartUri, item);
+        }
+    
+        // 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);
+        }
+        
+        for (Map item: mapList) {
+            sendBody(routeStartUri, item);
+        }
+    
+        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());
+                sendBody(routeStartUri, node);
+            } catch (JsonProcessingException e) {
+                throw new RuntimeException(e);
+            }
+        });
+        
+        sendBody(routeStartUri, "{\"bye\": \"mom\"}");
+        Thread.sleep(5000);
+        
+        VantiqResponse vr = vantiq.select(testTypeName, null, null, null);
+        Map<String, Set<String>> countBySubject = new HashMap<>();
+        countBySubject.put("mom", new HashSet<>());
+        countBySubject.put("dad", new HashSet<>());
+        countBySubject.put("aki", new HashSet<>());
+        if (!vr.isSuccess()) {
+            for (VantiqError err: vr.getErrors()) {
+               log.error("Error: {}", err);
+            }
+        }
+        assert vr.isSuccess();
+    
+        assert vr.getBody() instanceof List;
+        @SuppressWarnings("unchecked")
+        List<JsonObject> retVal = (List<JsonObject>) vr.getBody();
+        retVal.forEach( row -> {
+            @SuppressWarnings("unchecked")
+            Map<String, Object> ent = (Map<String, Object>) new Gson().fromJson(row, Map.class);
+            log.debug("Got value: {} ({})", ent.get("value"), ent.get("value").getClass().getName());
+            if (((Map) ent.get("value")).containsKey("hi")) {
+                String val = (String) ((Map) ent.get("value")).get("hi");
+                if (val.contains("mom")) {
+                    countBySubject.get("mom").add(val);
+                } else if (val.contains("dad")) {
+                    countBySubject.get("dad").add(val);
+                } else if (val.contains("aki")) {
+                    countBySubject.get("aki").add(val);
+                }
+            }
+        });
+        assertEquals("Mom count", itemCount, countBySubject.get("mom").size());
+        assertEquals("Dad count", itemCount, countBySubject.get("dad").size());
+        assertEquals("Aki count", itemCount, countBySubject.get("aki").size());
+        assert retVal.size() == 3 * itemCount + 1;
+    }
+    
+    @Test
+    public void testVantiqConsumerLive() throws Exception {
+        log.info("tVCR: {}/{}", vantiqInstallation, vantiqAccessToken);
+        assumeTrue(vantiqInstallation != null && vantiqAccessToken != null);
+        for (String name: context.getComponentNames()) {
+            log.info("Component name: {}", name);
+        }
+    
+        int expectedMsgCount = 10;
+        MockEndpoint mocked = getMockEndpoint(routeEndUri);
+        mocked.expectedMinimumMessageCount(expectedMsgCount);
+    
+        Map<String, Object> params = Map.of("messageCount", expectedMsgCount);
+        VantiqResponse vr = vantiq.execute(publishStuffName, params);
+        assert vr.isSuccess();
+        
+        mocked.await(5L, TimeUnit.SECONDS);
+        assert mocked.getReceivedCounter() == expectedMsgCount;
+        List<Exchange> exchanges = mocked.getReceivedExchanges();
+        Set<String> uniqueMsgs = new HashSet<String>();
+        exchanges.forEach(exchange -> {
+                assert exchange.getIn().getBody() instanceof Map;
+                Map msg = exchange.getIn().getBody(Map.class);
+                assert msg.containsKey("test");
+                assert msg.get("test") instanceof String;
+                assert ((String) msg.get("test")).startsWith(testMsgPreamble);
+                uniqueMsgs.add(((String) msg.get("test")));
+        });
+        assert uniqueMsgs.size() == expectedMsgCount;
+    }
+    
+    @Test
+    public void testVantiqQueryLive() throws Exception {
+        log.info("tVCR: {}/{}", vantiqInstallation, vantiqAccessToken);
+        assumeTrue(vantiqInstallation != null && vantiqAccessToken != null);
+        for (String name: context.getComponentNames()) {
+            log.info("Component name: {}", name);
+        }
+        
+        int expectedMsgCount = 10;
+        MockEndpoint mocked = getMockEndpoint(routeEndUri);
+        mocked.expectedMinimumMessageCount(expectedMsgCount);
+        
+        Map<String, Object> params = Map.of("messageCount", expectedMsgCount);
+        VantiqResponse vr = vantiq.execute(queryStuffName, params);
+        assert vr.isSuccess();
+        
+        mocked.await(5L, TimeUnit.SECONDS);
+        assert mocked.getReceivedCounter() == expectedMsgCount;
+        List<Exchange> exchanges = mocked.getReceivedExchanges();
+        Set<String> uniqueMsgs = new HashSet<String>();
+        exchanges.forEach(exchange -> {
+            assert exchange.getIn().getBody() instanceof Map;
+            Map msg = exchange.getIn().getBody(Map.class);
+            assert msg.containsKey("test");
+            assert msg.get("test") instanceof String;
+            assert ((String) msg.get("test")).startsWith(testMsgPreamble);
+            uniqueMsgs.add(((String) msg.get("test")));
+        });
+        assert uniqueMsgs.size() == expectedMsgCount;
+    }
+    
+    @Override
+    protected RouteBuilder createRouteBuilder() throws Exception {
+        return new RouteBuilder() {
+            public void configure() {
+                // Used here as well as in the tests so that we don't try & set up routes
+                // when we're not even going to run the tests.
+                assumeTrue(vantiqInstallation != null && vantiqAccessToken != null);
+
+                from(routeStartUri)
+                  .to(vantiqEndpointUri);
+                
+                from(vantiqEndpointUri)
+                        .to(routeEndUri);
+                
+                from(vantiqQueryEndpointUri)
+                        .to(routeEndUri)
+                        .setExchangePattern(ExchangePattern.InOut)
+                        .setBody(constant("{ \"Response\": \"Message\" }"))
+                        .to(vantiqQueryEndpointUri);
+            }
+        };
+    }
+    
+    @BeforeClass
+    public static void setupVantiqEnvironment() throws Exception {
+        if (vantiqInstallation != null && vantiqAccessToken != null) {
+            URI vuri = new URI(vantiqInstallation);
+            String endpointUri = "vantiq://" + vuri.getHost();
+            if (vuri.getPort() > 0) {
+                endpointUri = endpointUri + ":" + vuri.getPort();
+            }
+            vantiqEndpointUri = endpointUri +
+                    "?sourceName=" + testSourceName +
+                    "&accessToken=" + vantiqAccessToken;
+            if (("http".equals(vuri.getScheme()) || "ws".equals(vuri.getScheme()))) {
+                vantiqEndpointUri = vantiqEndpointUri + "&noSsl=true";
+            }
+            
+            vantiqQueryEndpointUri = endpointUri +
+                    "?sourceName=" + testQuerySourceName +
+                    "&accessToken=" + vantiqAccessToken;
+            if (("http".equals(vuri.getScheme()) || "ws".equals(vuri.getScheme()))) {
+                vantiqQueryEndpointUri = vantiqQueryEndpointUri + "&noSsl=true";
+            }
+            vantiq = new Vantiq(vantiqInstallation);
+            vantiq.setAccessToken(vantiqAccessToken);
+            assertTrue("Vantiq Auth'd", vantiq.isAuthenticated());
+            createSourceImpl();
+            createSource();
+            createQuerySource();
+            createType();
+            setupPublishProcedure();
+            setupQueryProcedure();
+            setupReceiverRule();
+        }
+    }
+    
+    @AfterClass
+    public static void teardownVantiqEnvironment() {
+        if (vantiqInstallation != null && vantiqAccessToken != null) {
+            deletePublishProcedure();
+            deleteQueryProcedure();
+            deleteReceiverRule();
+            deleteType();
+            deleteSource();
+            deleteQuerySource();
+            deleteSourceImpl();
+            vantiq = null;
+        }
+    }
+    
+    public static void createType() {
+        Map<String, Object> typeDef = new LinkedHashMap<>();
+        Map<String, Object> properties = new LinkedHashMap<>();
+        properties.put("value", Map.of("type", "Object", "required", false));
+        typeDef.put("properties", properties);
+        typeDef.put("name", testTypeName);
+        VantiqResponse vr = vantiq.insert("system.types", typeDef);
+        if (!vr.isSuccess()) {
+            for (VantiqError err: vr.getErrors()) {
+                System.out.println("Error: " + err.getMessage());
+            }
+        }
+    }
+    
+    public static void deleteType() {
+        Map<String, Object> where = new LinkedHashMap<>();
+        where.put("name", testTypeName);
+        vantiq.delete("system.types", where);
+    }
+    
+    public static void setupPublishProcedure() {
+        String procedure =
+                "PROCEDURE " + publishStuffName +  "(messageCount Integer)\n"
+                        + "for (i in range(0, messageCount)) {\n"
+                        + "    PUBLISH {test: \"" + testMsgPreamble + "\" + i} to SOURCE " + testSourceName + "\n"
+                        + "    log.info(\"Published message to source: {}\", [\"" + testSourceName + "\"])"
+                        + "}";
+       
+        VantiqResponse vr = vantiq.insert("system.procedures", procedure);
+        assert vr.isSuccess();
+    }
+    
+    public static void setupQueryProcedure() {
+        String procedure =
+                "PROCEDURE " + queryStuffName +  "(messageCount Integer)\n"
+                        + "for (i in range(0, messageCount)) {\n"
+                        + "    var resp = "
+                        +           "SELECT ONE * from SOURCE " + testQuerySourceName + " WITH test: \"" + testMsgPreamble
+                        + "               \" + i, iteration: i\n"
+                        + "    log.info(\"Query message to source: {}\", [\"" + testQuerySourceName + "\"])\n"
+                        + "    if (!resp.containsKey(\"Response\")) {\n"
+                        + "        exception(\"missing.required.key\", \"missing required key\")\n"
+                        + "    }\n"
+                        + "}\n";
+        
+        VantiqResponse vr = vantiq.insert("system.procedures", procedure);
+        assert vr.isSuccess();
+    }
+    
+    public static void deletePublishProcedure() {
+        Map<String, Object> where = new LinkedHashMap<>();
+        where.put("name", publishStuffName);
+        VantiqResponse response = vantiq.delete("system.procedures", where);
+        assert response.isSuccess();
+    }
+    
+    public static void deleteQueryProcedure() {
+        Map<String, Object> where = new LinkedHashMap<>();
+        where.put("name", queryStuffName);
+        VantiqResponse response = vantiq.delete("system.procedures", where);
+        assert response.isSuccess();
+    }
+    public static void setupReceiverRule() {
+        String rule =
+                "RULE " + testRuleName + "\n"
+                        + "when EVENT OCCURS on \"/sources/" + testSourceName + "\" as message\n"
+                        + "\n"
+                        + "insert " + testTypeName + "(value: message.value)";
+        
+        VantiqResponse vr = vantiq.insert("system.rules", rule);
+        assert vr.isSuccess();
+    }
+    
+    public static void deleteReceiverRule() {
+        Map<String, Object> where = new LinkedHashMap<>();
+        where.put("name", testRuleName);
+        VantiqResponse response = vantiq.delete("system.rules", where);
+        if (!response.isSuccess()) {
+            for (VantiqError err: response.getErrors()) {
+                System.out.println("Error: " + err.getMessage());
+            }
+        }
+        assert response.isSuccess();
+    }
+    
+    private static boolean createdImpl = false;
+
+    @SuppressWarnings("unchecked")
+    protected static void createSourceImpl() throws Exception {
+        VantiqResponse resp = vantiq.selectOne(VANTIQ_SOURCE_IMPL, SRC_IMPL_TYPE);
+        if (!resp.isSuccess()) {
+            ClassLoader loader = Thread.currentThread().getContextClassLoader();
+            byte[] implDef = new byte[IMPL_MAX_SIZE];
+            try (InputStream is = loader.getResourceAsStream(IMPL_DEF))
+            {
+                assert is != null;
+                int implSize = is.read(implDef);
+                assert implSize > 0;
+                assert implSize < IMPL_MAX_SIZE;
+            }
+            
+            ObjectMapper mapper = new ObjectMapper();
+            Map<String, Object> implMap = new LinkedHashMap<>();
+            implMap = mapper.readValue(implDef, implMap.getClass());
+            resp = vantiq.insert(VANTIQ_SOURCE_IMPL, implMap);
+            if (!resp.isSuccess()) {
+                System.out.println("Got errors: " +  resp);
+            }
+            assert resp.isSuccess();
+            
+            Map<String, String> where = new LinkedHashMap<>();
+            where.put("name", SRC_IMPL_TYPE);
+            VantiqResponse implResp = vantiq.select(VANTIQ_SOURCE_IMPL, null, where, null);
+            assertFalse("Errors from fetching source impl", implResp.hasErrors());
+            @SuppressWarnings({"rawtypes"})
+            ArrayList responseBody = (ArrayList) implResp.getBody();
+            assertEquals("Missing sourceImpl -- expected a count of 1", 1, responseBody.size());
+            createdImpl = true;
+        }
+    }
+    
+    protected static void deleteSourceImpl() {
+        if (createdImpl) {
+            vantiq.deleteOne(VANTIQ_SOURCE_IMPL, SRC_IMPL_TYPE);
+        }
+        
+        VantiqResponse resp = vantiq.selectOne(VANTIQ_SOURCE_IMPL, SRC_IMPL_TYPE);
+        if (resp.hasErrors()) {
+            List<VantiqError> errors = resp.getErrors();
+            assert errors != null;
+            assert errors.size() > 0;
+            if (errors.size() != 1 || !"io.vantiq.resource.not.found".equals(errors.get(0).getCode())) {
+                fail("Error deleting source impl" + resp.getErrors());
+            }
+        } else if (createdImpl) {
+            fail(SRC_IMPL_TYPE + " source impl found after deletion.");
+        }
+    }
+    
+    public static void createSource() {
+        Map<String, String> where = new LinkedHashMap<>();
+        where.put("name", SRC_IMPL_TYPE);
+        VantiqResponse implResp = vantiq.select(VANTIQ_SOURCE_IMPL, null, where, null);
+        assertFalse("Errors from fetching source impl", implResp.hasErrors());
+        ArrayList responseBody = (ArrayList) implResp.getBody();
+        assertEquals("Missing sourceimpl -- expected a count of 1", 1, responseBody.size());
+    
+        Map<String, Object> sourceDef = new LinkedHashMap<>();
+        // Setting up the source definition
+        sourceDef.put("name", testSourceName);
+        sourceDef.put("type", SRC_IMPL_TYPE);
+        sourceDef.put("active", "true");
+        sourceDef.put("direction", "BOTH");
+        sourceDef.put("config", new LinkedHashMap<String, Object>());
+    
+        VantiqResponse insertResponse = vantiq.insert("system.sources", sourceDef);
+        assert insertResponse.isSuccess();
+    }
+    
+    public static void createQuerySource() {
+        Map<String, String> where = new LinkedHashMap<>();
+        where.put("name", SRC_IMPL_TYPE);
+        VantiqResponse implResp = vantiq.select(VANTIQ_SOURCE_IMPL, null, where, null);
+        assertFalse("Errors from fetching source impl", implResp.hasErrors());
+        ArrayList responseBody = (ArrayList) implResp.getBody();
+        assertEquals("Missing sourceimpl -- expected a count of 1", 1, responseBody.size());
+        
+        Map<String, Object> sourceDef = new LinkedHashMap<>();
+        // Setting up the source definition
+        sourceDef.put("name", testQuerySourceName);
+        sourceDef.put("type", SRC_IMPL_TYPE);
+        sourceDef.put("active", "true");
+        sourceDef.put("direction", "BOTH");
+        sourceDef.put("config", new LinkedHashMap<String, Object>());
+        
+        VantiqResponse insertResponse = vantiq.insert("system.sources", sourceDef);
+        assert insertResponse.isSuccess();
+    }
+    
+    public static void deleteSource() {
+        Map<String, Object> where = new LinkedHashMap<>();
+        where.put("name", testSourceName);
+        VantiqResponse response = vantiq.delete("system.sources", where);
+        assert response.isSuccess();
+    }
+    
+    public static void deleteQuerySource() {
+        Map<String, Object> where = new LinkedHashMap<>();
+        where.put("name", testQuerySourceName);
+        VantiqResponse response = vantiq.delete("system.sources", where);
+        assert response.isSuccess();
+    }
+}
diff --git a/camelComponent/src/test/java/io/vantiq/extsrc/camel/VantiqUriTestBase.java b/camelComponent/src/test/java/io/vantiq/extsrc/camel/VantiqUriTestBase.java
new file mode 100644
index 00000000..29cad226
--- /dev/null
+++ b/camelComponent/src/test/java/io/vantiq/extsrc/camel/VantiqUriTestBase.java
@@ -0,0 +1,51 @@
+package io.vantiq.extsrc.camel;
+
+import static io.vantiq.extjsdk.Utils.AUTH_TOKEN_PROPERTY_NAME;
+import static io.vantiq.extjsdk.Utils.SOURCES_PROPERTY_NAME;
+import static io.vantiq.extjsdk.Utils.TARGET_SERVER_PROPERTY_NAME;
+
+import io.vantiq.extjsdk.Utils;
+import org.apache.camel.Endpoint;
+import org.apache.camel.test.junit4.CamelTestSupport;
+import org.junit.BeforeClass;
+
+import java.io.File;
+import java.net.URI;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+public class VantiqUriTestBase extends CamelTestSupport {
+    protected final String exceptionEndpoint = "mock:direct:error";
+    
+    protected static final String testSourceName = "camelSource";
+    protected static final String accessToken = "someAccessToken";
+    
+    
+    @BeforeClass
+    public static void setupSourceConfig() throws Exception {
+        File sc = new File(Utils.SERVER_CONFIG_FILENAME);
+        sc.deleteOnExit();
+        StringBuffer cf = new StringBuffer();
+        cf.append(TARGET_SERVER_PROPERTY_NAME).append("=http://localhost:8080").append("\n")
+          .append(AUTH_TOKEN_PROPERTY_NAME).append("=").append(accessToken).append("\n")
+          .append(SOURCES_PROPERTY_NAME).append("=").append(testSourceName).append("\n");
+        Files.write(Path.of(sc.getAbsolutePath()), cf.toString().getBytes());
+    }
+    
+    protected void checkEndpoint(Endpoint ep) throws Exception {
+        assert ep instanceof FauxVantiqEndpoint;
+        FauxVantiqEndpoint epConsumer = (FauxVantiqEndpoint) ep;
+        assert epConsumer.getEndpointUri().contains(accessToken);
+        assert epConsumer.getEndpointUri().contains(testSourceName);
+        URI senderUri = new URI(epConsumer.getEndpointUri());
+        assertEquals("vantiq", senderUri.getScheme());
+        assertEquals("localhost", senderUri.getHost());
+        assertEquals(8080, senderUri.getPort());
+        String qry = senderUri.getQuery();
+        assert qry.contains("accessToken");
+        assert qry.contains("sourceName");
+        assert qry.contains(accessToken);
+        assert qry.contains(testSourceName);
+        assert epConsumer.isConnected();
+    }
+}
diff --git a/camelComponent/src/test/resources/camelComponentImpl.json b/camelComponent/src/test/resources/camelComponentImpl.json
new file mode 100644
index 00000000..e886d52e
--- /dev/null
+++ b/camelComponent/src/test/resources/camelComponentImpl.json
@@ -0,0 +1,8 @@
+{
+  "name" : "CAMEL_COMPONENT",
+  "baseType" : "EXTENSION",
+  "verticle" : "service:extensionSource",
+  "config" : {},
+  "baseConfigProperties": [
+  ]
+}
diff --git a/camelComponent/src/test/resources/log4j2.properties b/camelComponent/src/test/resources/log4j2.properties
new file mode 100644
index 00000000..53aa0207
--- /dev/null
+++ b/camelComponent/src/test/resources/log4j2.properties
@@ -0,0 +1,12 @@
+
+appender.out.type = Console
+appender.out.name = out
+appender.out.layout.type = PatternLayout
+appender.out.layout.pattern = [%30.30t] %-30.30c{1} %-5p %m%n
+rootLogger.level = INFO
+rootLogger.appenderRef.out.ref = out
+
+logger.vantiqcamel.name = io.vantiq.extsrc.camel
+logger.vantiqcamel.level = DEBUG
+logger.vantiqcamel.appenderRef = Console
+
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
new file mode 100644
index 00000000..d2a36332
--- /dev/null
+++ b/camelConnector/src/test/java/io/vantiq/extsrc/camelconn/connector/VantiqConfigurationTest.java
@@ -0,0 +1,179 @@
+package io.vantiq.extsrc.camelconn.connector;
+
+import static io.vantiq.extjsdk.ExtensionServiceMessage.OP_CONFIGURE_EXTENSION;
+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;
+import static io.vantiq.extsrc.camelconn.connector.CamelHandleConfiguration.COMPONENT_CACHE;
+import static io.vantiq.extsrc.camelconn.connector.CamelHandleConfiguration.COMPONENT_LIB;
+import static io.vantiq.extsrc.camelconn.connector.CamelHandleConfiguration.COMPONENT_PROPERTIES;
+import static io.vantiq.extsrc.camelconn.connector.CamelHandleConfiguration.GENERAL;
+import static io.vantiq.extsrc.camelconn.connector.CamelHandleConfiguration.ROUTES_FORMAT;
+import static io.vantiq.extsrc.camelconn.connector.CamelHandleConfiguration.ROUTES_LIST;
+import static io.vantiq.extsrc.camelconn.discover.VantiqComponentResolverTest.MISSING_VALUE;
+import static io.vantiq.extsrc.camelconn.discover.VantiqComponentResolverTest.QUERY_AARDVARK;
+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 org.junit.Assume.assumeTrue;
+
+import io.vantiq.extjsdk.ExtensionServiceMessage;
+import io.vantiq.extsrc.camelconn.discover.CamelRunner;
+import org.apache.camel.CamelContext;
+import org.apache.commons.lang3.function.TriFunction;
+import org.junit.Test;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+public class VantiqConfigurationTest {
+    
+    private final String BUILD_DIR = System.getProperty("BUILD_DIR", "build");
+    private final String CAMEL_CONN_BASE_DIR = "camelConnBase";
+    private final String CAMEL_BASE_PATH = BUILD_DIR + File.separator + CAMEL_CONN_BASE_DIR + File.separator;
+    private final String CACHE_DIR = CAMEL_BASE_PATH + "cacheDir";
+    private final String LOADED_LIBRARIES = CAMEL_BASE_PATH + "loadedLib";
+    
+    @Test
+    public void testSimpleConfiguration() {
+        Map<String, Object> simpleConfig = new HashMap<>();
+        Map<String, Object> camelConfig = new HashMap<>();
+        simpleConfig.put(CAMEL_CONFIG, camelConfig);
+        Map<String, String> camelAppConfig = new HashMap<>();
+        camelConfig.put(CAMEL_APP, camelAppConfig);
+        Map<String, String> generalConfig = new HashMap<>();
+        camelConfig.put(GENERAL, generalConfig);
+        
+        generalConfig.put(COMPONENT_CACHE, CACHE_DIR);
+        generalConfig.put(COMPONENT_LIB, LOADED_LIBRARIES);
+        
+        camelAppConfig.put(ROUTES_LIST, XML_ROUTE);
+        camelAppConfig.put(ROUTES_FORMAT, "xml");
+        camelAppConfig.put(APP_NAME, "testSimpleConfiguration");
+    
+        String fauxVantiqUrl = "http://someVantiqServer";
+        ExtensionServiceMessage esm = new ExtensionServiceMessage(fauxVantiqUrl);
+        esm.op = OP_CONFIGURE_EXTENSION;
+        esm.object = simpleConfig;
+        
+        CamelCore core = new CamelCore("testSimpleConfiguration", "someAccessToken", fauxVantiqUrl);
+        CamelHandleConfiguration handler = new CamelHandleConfiguration(core);
+        
+        handler.handleMessage(esm);
+        assertTrue("handler completed", handler.isComplete());
+        assertNotNull("Camel Runner", handler.getCurrentCamelRunner());
+        assertTrue("Runner started", handler.getCurrentCamelRunner().isStarted());
+        assertNotNull("Runner's Camel Context", handler.getCurrentCamelRunner().getCamelContext());
+        assertNotNull("Runner's thread", handler.getCurrentCamelRunner().getCamelThread());
+        
+        // Assuming things worked, now we have a camel app running.  We'll run our test against it & verify
+    
+        TriFunction<CamelContext, String, Object, Boolean> verifyOperation = defineVerifyOperation();
+        CamelContext runnerContext = handler.getCurrentCamelRunner().getCamelContext();
+        assertTrue("Context Running", runnerContext.isStarted());
+        assert verifyOperation.apply(runnerContext, QUERY_MONKEY, RESPONSE_MONKEY);
+        assert verifyOperation.apply(runnerContext, QUERY_AARDVARK, RESPONSE_AARDVARK);
+    
+        handler.getCurrentCamelRunner().close();
+        try {
+            handler.getCurrentCamelRunner().getCamelThread().join(TimeUnit.SECONDS.toMillis(10));
+        } catch (InterruptedException ie) {
+            fail("Trapped Interrupted Exception");
+        }
+    }
+    
+    @Test
+    public void testComponentInitConfiguration() {
+        assumeTrue(!sfLoginUrl.equals(MISSING_VALUE) && !sfClientId.equals(MISSING_VALUE) &&
+                           !sfClientSecret.equals(MISSING_VALUE) && !sfRefreshToken.equals(MISSING_VALUE));
+        Map<String, Object> simpleConfig = new HashMap<>();
+        Map<String, Object> camelConfig = new HashMap<>();
+        simpleConfig.put(CAMEL_CONFIG, camelConfig);
+        Map<String, Object> camelAppConfig = new HashMap<>();
+        camelConfig.put(CAMEL_APP, camelAppConfig);
+        Map<String, String> generalConfig = new HashMap<>();
+        camelConfig.put(GENERAL, generalConfig);
+        
+        generalConfig.put(COMPONENT_CACHE, CACHE_DIR);
+        generalConfig.put(COMPONENT_LIB, LOADED_LIBRARIES);
+        
+        camelAppConfig.put(ROUTES_LIST, SALESFORCETASKS_YAML);
+        camelAppConfig.put(ROUTES_FORMAT, "yaml");
+        camelAppConfig.put(APP_NAME, "testComponentInitConfiguration");
+        camelAppConfig.put(COMPONENT_PROPERTIES, getComponentsToInit());
+        
+        String fauxVantiqUrl = "http://someVantiqServer";
+        ExtensionServiceMessage esm = new ExtensionServiceMessage(fauxVantiqUrl);
+        esm.op = OP_CONFIGURE_EXTENSION;
+        esm.object = simpleConfig;
+        
+        CamelCore core = new CamelCore("testComponentInitConfiguration", "someAccessToken", fauxVantiqUrl);
+        CamelHandleConfiguration handler = new CamelHandleConfiguration(core);
+        
+        handler.handleMessage(esm);
+        assertTrue("handler completed", handler.isComplete());
+        assertNotNull("Camel Runner", handler.getCurrentCamelRunner());
+        assertTrue("Runner started", handler.getCurrentCamelRunner().isStarted());
+        assertNotNull("Runner's Camel Context", handler.getCurrentCamelRunner().getCamelContext());
+        assertNotNull("Runner's thread", handler.getCurrentCamelRunner().getCamelThread());
+        
+        // Assuming things worked, now we have a camel app running.  We'll run our test against it & verify
+        
+        TriFunction<CamelContext, String, Object, Boolean> verifyOperation = defineVerifyOperation();
+        CamelContext runnerContext = handler.getCurrentCamelRunner().getCamelContext();
+        assertTrue("Context Running", runnerContext.isStarted());
+        assert verifyOperation.apply(runnerContext, "not used", Map.of( "done", true));
+        assert verifyOperation.apply(runnerContext, "not used", Map.of( "done", true));
+        
+        handler.getCurrentCamelRunner().close();
+        try {
+            handler.getCurrentCamelRunner().getCamelThread().join(TimeUnit.SECONDS.toMillis(10));
+        } catch (InterruptedException ie) {
+            fail("Trapped Interrupted Exception");
+        }
+    }
+    
+    
+    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);
+    public static final String sfRefreshToken = System.getProperty("camel-salesforce-refreshToken", MISSING_VALUE);
+    
+    private final Map<String, String> propList = Map.of( "loginUrl", sfLoginUrl,
+                                                         "clientId", sfClientId,
+                                                         "clientSecret", sfClientSecret,
+                                                         "refreshToken", sfRefreshToken
+    );
+    
+    public static final String SALESFORCETASKS_YAML =  "\n"
+        + "- route:\n"
+        + "    id: \"Salesforce from yaml-route\"\n"
+        + "    from:\n"
+        + "      uri: \"direct:start\"\n"
+        + "      steps:\n"
+//        + "        - set-exchange-pattern: \"inOut\"\n" // Leaving as a reminder re: how to do in/out YAML routes
+        + "        - to:\n"
+        + "            uri: \"salesforce:query?rawPayload=true&SObjectQuery=SELECT Id, Subject, OwnerId from Task\"\n"
+        + "        - unmarshal:\n"
+        + "            json: {}\n"
+        + "        - to:\n"
+        + "            uri: \"mock:result\"\n";
+    // in/out have to route their answer back to their source. In our
+    // case, we're not really doing a "query", so this is fine.
+    
+    public List<Map<String, Object>> getComponentsToInit() {
+        return List.of(
+                Map.of(CamelRunner.COMPONENT_NAME, "salesforce",
+                       COMPONENT_PROPERTIES, propList)
+        );
+    }
+    
+}
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
new file mode 100644
index 00000000..2cd021c5
--- /dev/null
+++ b/camelConnector/src/test/java/io/vantiq/extsrc/camelconn/discover/VantiqComponentDiscoveryTest.java
@@ -0,0 +1,479 @@
+/*
+ * Copyright (c) 2022 Vantiq, Inc.
+ *
+ * All rights reserved.
+ *
+ * SPDX: MIT
+ */
+
+package io.vantiq.extsrc.camelconn.discover;
+
+import org.apache.camel.ExtendedCamelContext;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.impl.engine.DefaultComponentResolver;
+import org.apache.camel.spi.ComponentResolver;
+import org.apache.camel.spi.Resource;
+import org.apache.camel.support.ResourceHelper;
+import org.apache.camel.test.junit4.CamelTestSupport;
+import org.apache.camel.dsl.xml.io.XmlRoutesBuilderLoader;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Perform unit tests for component discovery
+ */
+public class VantiqComponentDiscoveryTest extends CamelTestSupport {
+    
+    private final String routeStartUri = "direct:start";
+    private final String routeEndUri = "mock:direct:result";
+    private static String vantiqEndpointUri;
+    
+    @BeforeClass
+    public static void setup() throws Exception {
+        URI vuri = new URI("http://localhost:8080");
+        String endpointUri = CamelDiscovery.VANTIQ_COMPONENT_SCHEME + "://" + vuri.getHost();
+        if (vuri.getPort() > 0) {
+            endpointUri = endpointUri + ":" + vuri.getPort();
+        }
+        vantiqEndpointUri = endpointUri +
+                "?sourceName=" + "fauxSource" +
+                "&accessToken=" + "fauxAccessToken";
+        if (("http".equals(vuri.getScheme()) || "ws".equals(vuri.getScheme()))) {
+            vantiqEndpointUri = vantiqEndpointUri + "&noSsl=true";
+        }
+    }
+    
+    @Test
+    public void testVersioning() throws DiscoveryException {
+        CamelDiscovery cd = new CamelDiscovery();
+        String artifactVersion = cd.getComponentListVersion();
+        String camelVersion = context.getVersion();
+        assertTrue("Build version " + artifactVersion + " is incompatible with camel version " + camelVersion,
+                   cd.isVersionCompatible(camelVersion));
+        assertFalse("Bad version -- too short", cd.isVersionCompatible("1"));
+        assertFalse("Bad version -- Major different", cd.isVersionCompatible("6000.3456"));
+        String[] camelVersionParts = camelVersion.split("\\.");
+        assertFalse("Bad version -- Minor different",
+                    cd.isVersionCompatible(camelVersionParts[0] + ".9247877"));
+        assertTrue("Different extraneous parts",
+                   cd.isVersionCompatible(camelVersionParts[0] + "." + camelVersionParts[1] +
+                           "." + "1234.566778"));
+    }
+    
+    @Test
+    public void testComponentLookup() throws DiscoveryException {
+        
+        // Check basic mechanism
+        
+        List<String> shouldExist = List.of("aws2-s3", "jdbc", "activemq", "jms", "box", "salesforce" );
+        List<String> shouldNotExist = List.of("vantiq", "bozoSoftware", "homersHouse");
+        
+        CamelDiscovery cd = new CamelDiscovery();
+        String artifactVersion = cd.getComponentListVersion();
+        String camelVersion = context.getVersion();
+        assertTrue("Build version " + artifactVersion + " is incompatible with camel version " + camelVersion,
+            cd.isVersionCompatible(camelVersion));
+        
+        shouldExist.forEach(comp -> {
+            try {
+                assertNotNull("Missing loadable for " + comp, cd.findComponentForScheme(comp));
+            } catch (DiscoveryException de) {
+                fail("Trapped exception looking up " + comp +"::" + de.getMessage());
+            }
+        });
+    
+        shouldNotExist.forEach(comp -> {
+            try {
+                assertNull("Extraneous loadable for " + comp, cd.findComponentForScheme(comp));
+            } catch (DiscoveryException de) {
+                fail("Trapped exception looking up " + comp +"::" + de.getMessage());
+            }
+        });
+    }
+    
+    @Test
+    public void testDataFormatLookup() throws DiscoveryException {
+        
+        // Check basic mechanism
+        
+        List<String> shouldExist = List.of("csv", "avro");
+        List<String> shouldNotExist = List.of("vantiq", "bozoSoftware", "homersHouse");
+        
+        CamelDiscovery cd = new CamelDiscovery();
+        String artifactVersion = cd.getComponentListVersion();
+        String camelVersion = context.getVersion();
+        assertTrue("Build version " + artifactVersion + " is incompatible with camel version " + camelVersion,
+                   cd.isVersionCompatible(camelVersion));
+        
+        shouldExist.forEach(df -> {
+            try {
+                assertNotNull("Missing loadable for " + df, cd.findDataFormatForName(df));
+            } catch (DiscoveryException de) {
+                fail("Trapped exception looking up " + df +"::" + de.getMessage());
+            }
+        });
+        
+        shouldNotExist.forEach(df -> {
+            try {
+                assertNull("Extraneous loadable for " + df, cd.findDataFormatForName(df));
+            } catch (DiscoveryException de) {
+                fail("Trapped exception looking up " + df +"::" + de.getMessage());
+            }
+        });
+    }
+    
+    /**
+     * Verify that our transcription from code/doc of what's built into Camel core is correct.
+     */
+    @Test
+    public void testTranscription() {
+        assert EnumeratingComponentResolver.CORE_COMPONENTS_SCHEMES.size() == 26;
+        ComponentResolver cr = context.adapt(ExtendedCamelContext.class).getComponentResolver();
+        assertTrue("Non-default component resolver: " + cr.getClass().getName(),
+                cr instanceof DefaultComponentResolver);
+        List<String> failureList = new ArrayList<>();
+        EnumeratingComponentResolver.CORE_COMPONENTS_SCHEMES.forEach( comp -> {
+            if (EnumeratingComponentResolver.NONLOADABLE_CORE_COMPONENT_SCHEMES.contains(comp)) {
+                log.debug("Skipping component scheme '{}' since it is handled internally", comp);
+            } else {
+                log.debug("Resolving component with scheme: {}", comp);
+                Object result;
+                try {
+                    result = cr.resolveComponent(comp, context);
+                    if (result == null) {
+                        failureList.add(comp + " -- no resolution");
+                    }
+                } catch (Exception e) {
+                    failureList.add("comp" + " -- via exception: " + e.getClass().getName() + ":" + e.getMessage());
+                }
+            }
+        });
+        failureList.forEach(failure -> log.error("Failed to load component: {}", failure));
+        assertEquals("Failed to load components", 0, failureList.size());
+    }
+    
+    /**
+     * Verify that we can discover the set of components in a simple set of routes.
+     * @throws Exception as found
+     */
+    @Test
+    public void testBasicDiscovery() throws Exception {
+        RouteBuilder rb = new TwoSimpleRoutes();
+        performDiscoveryTest(rb);
+    }
+    
+    @Test
+    public void testMultiEndpointDiscovery() throws Exception {
+        RouteBuilder rb = new MultiendedRoutes();
+        performDiscoveryTest(rb);
+    }
+    
+    @Test
+    public void testComplexUrlRoutesDiscovery() throws Exception {
+        RouteBuilder rb = new ComplexUrlRoutes();
+        performDiscoveryTest(rb);
+    }
+    
+    @Test
+    public void testMarshalingRoutesDiscovery() throws Exception {
+        RouteBuilder rb = new MarshalingRoutes();
+        performDiscoveryTest(rb);
+    }
+    
+    @Test
+    public void testXmlRouteDiscovery() throws Exception {
+        RouteBuilder rb = new SimpleXmlRoutes();
+        performDiscoveryTest(rb);
+    }
+    
+    @Test
+    public void testComplexXmlRouteDiscovery() throws Exception {
+        RouteBuilder rb = new ComplexXmlRoutes();
+        performDiscoveryTest(rb);
+    }
+    
+    void performDiscoveryTest(RouteBuilder rb) throws Exception {
+        assert rb instanceof TestExpectations;
+        setUseRouteBuilder(false);
+        CamelDiscovery discoverer = new CamelDiscovery();
+        Map<String, Set<String>> discResults = discoverer.performComponentDiscovery(rb);
+        
+        List<String> expectedCTL = ((TestExpectations) rb).getExpectedComponentsToLoad();
+        List<String> expectedSysComp = ((TestExpectations) rb).getExpectedSystemComponents();
+        List<String> expectedDFL = ((TestExpectations) rb).getExpectedDataFormatsToLoad();
+        List<String> expectedSysDF = ((TestExpectations) rb).getExpectedSystemDataFormats();
+    
+        discResults.get(CamelDiscovery.COMPONENTS_TO_LOAD).forEach(comp -> {
+            log.debug("    ---> {}", comp);
+            assertTrue("Unexpected component to load: " + comp, expectedCTL.contains(comp));
+        });
+    
+        discResults.get(CamelDiscovery.SYSTEM_COMPONENTS).forEach(comp -> {
+            log.debug("    ---> {}", comp);
+            assertTrue("Unexpected system component: " + comp, expectedSysComp.contains(comp));
+    
+        });
+        assertEquals("Enumerated Components:",
+                     expectedCTL.size(),  discResults.get(CamelDiscovery.COMPONENTS_TO_LOAD).size());
+        assertEquals("System Components:", expectedSysComp.size(),
+                     discResults.get(CamelDiscovery.SYSTEM_COMPONENTS).size());
+    
+        discResults.get(CamelDiscovery.DATAFORMATS_TO_LOAD).forEach(df -> {
+            log.debug("    ---> {}", df);
+            assertTrue("Unexpected dataformat to load: " + df, expectedDFL.contains(df));
+        });
+    
+        discResults.get(CamelDiscovery.SYSTEM_DATAFORMATS).forEach(df -> {
+            log.debug("    ---> {}", df);
+            assertTrue("Unexpected system dataformat: " + df, expectedSysDF.contains(df));
+        
+        });
+        assertEquals("Enumerated Components:",
+                     expectedCTL.size(),  discResults.get(CamelDiscovery.COMPONENTS_TO_LOAD).size());
+        assertEquals("System Components:", expectedSysComp.size(),
+                     discResults.get(CamelDiscovery.SYSTEM_COMPONENTS).size());
+        assertEquals("Enumerated DataFormats:",
+                     expectedDFL.size(),  discResults.get(CamelDiscovery.DATAFORMATS_TO_LOAD).size());
+        assertEquals("System DataFormats:", expectedSysDF.size(),
+                     discResults.get(CamelDiscovery.SYSTEM_DATAFORMATS).size());
+    
+        // Double check that things are loadable
+    
+        discResults.get(CamelDiscovery.COMPONENTS_TO_LOAD).forEach(compSchema -> {
+            try {
+                if (!compSchema.equals(CamelDiscovery.VANTIQ_COMPONENT_SCHEME)) {
+                    String loadable = discoverer.findComponentForScheme(compSchema);
+                    log.debug("Need to load {} for scheme: {}", loadable, compSchema);
+                    
+                    assertNotNull("Missing loadable artifact: " + compSchema, loadable);
+                }
+            } catch (DiscoveryException de) {
+                fail("Trapped DiscoveryException: " + de.getMessage());
+            }
+        });
+    
+        discResults.get(CamelDiscovery.DATAFORMATS_TO_LOAD).forEach(dfName -> {
+            try {
+                String loadable = discoverer.findDataFormatForName(dfName);
+                log.debug("Need to load {} for name: {}", loadable, dfName);
+                assertNotNull("Missing loadable dataFormat artifact: " + dfName, loadable);
+            } catch (DiscoveryException de) {
+                fail("Trapped DiscoveryException: " + de.getMessage());
+            }
+        });
+        
+        // Same thing should be true of system stuff, though we won't really load them
+        discResults.get(CamelDiscovery.SYSTEM_COMPONENTS).forEach(compSchema -> {
+            try {
+                String loadable = discoverer.findComponentForScheme(compSchema);
+                log.debug("Need to load {} for scheme: {}", loadable, compSchema);
+                assertNotNull("Missing loadable artifact: " + compSchema, loadable);
+            } catch (DiscoveryException de) {
+                fail("Trapped DiscoveryException: " + de.getMessage());
+            }
+        });
+    
+        // Same thing should be true of system stuff, though we won't really load them
+        discResults.get(CamelDiscovery.SYSTEM_DATAFORMATS).forEach(dfName -> {
+            try {
+                String loadable = discoverer.findDataFormatForName(dfName);
+                log.debug("Need to load {} for name: {}", loadable, dfName);
+                assertNotNull("Missing loadable artifact: " + dfName, loadable);
+            } catch (DiscoveryException de) {
+                fail("Trapped DiscoveryException: " + de.getMessage());
+            }
+        });
+    }
+    
+    /**
+     * Used to make our tests more easily managed.
+     */
+    interface TestExpectations {
+        List<String> getExpectedComponentsToLoad();
+        List<String> getExpectedSystemComponents();
+        
+        default List<String> getExpectedDataFormatsToLoad() {
+            return List.of();
+        }
+        default List<String> getExpectedSystemDataFormats() {
+            return List.of();
+        }
+    }
+    
+    private class TwoSimpleRoutes extends RouteBuilder implements TestExpectations {
+
+        @Override
+        public void configure() {
+            from(routeStartUri)
+                    .to(vantiqEndpointUri);
+    
+            from(vantiqEndpointUri)
+                    .to(routeEndUri);
+        }
+        public List<String> getExpectedComponentsToLoad() {
+            return Collections.emptyList();
+        }
+    
+        public List<String> getExpectedSystemComponents() {
+            return List.of("mock", "direct");
+        }
+    }
+    
+    private class MultiendedRoutes extends RouteBuilder implements TestExpectations {
+        
+        @Override
+        public void configure() {
+            from(routeStartUri)
+                    .to("log:debug")
+                    .to("mock:foobar")
+                    .to("direct-vm:something");
+            
+            
+            from(vantiqEndpointUri)
+                    .to(routeEndUri);
+        }
+        public List<String> getExpectedComponentsToLoad() {
+            return Collections.emptyList();
+        }
+        
+        public List<String> getExpectedSystemComponents() {
+            return List.of("direct-vm", "direct", "mock", "log");
+        }
+    }
+    
+    private class MarshalingRoutes extends RouteBuilder implements TestExpectations {
+        
+        @Override
+        public void configure() {
+            from(routeStartUri)
+                    .marshal().csv()
+                    .to("log:debug");
+            
+            
+            from(vantiqEndpointUri)
+                    .to(routeEndUri);
+        }
+        public List<String> getExpectedComponentsToLoad() {
+            return Collections.emptyList();
+        }
+        
+        public List<String> getExpectedSystemComponents() {
+            return List.of("direct", "mock", "log");
+        }
+        
+        public List<String> getExpectedDataFormatsToLoad() {
+            return List.of("csv");
+        }
+    }
+    
+    
+    private static class ComplexUrlRoutes extends RouteBuilder implements TestExpectations {
+        
+        @Override
+        public void configure() {
+            // From some AWS S3 resource, check the message content.  If it's an error, log it & send to jmx queue
+            // Otherwise, send to the Vantiq app
+            from("aws2-s3://helloBucket?accessKey=yourAccessKey&secretKey=yourSecretKey&prefix=hello.txt")
+                .choice()
+                    .when(xpath("/record/error"))
+                        .to("log:error")
+                        .to("jmx:queue:errorNotification?acceptMessagesWhileStopping=false" +
+                            "&acknowledgementModeName=CLIENT_ACKNOWLEDGE&cacheLevelName=NONE" +
+                            "&deliveryMode=PERSISTENT")
+                    .otherwise()
+                        .to(vantiqEndpointUri);
+        }
+        @Override
+        public List<String> getExpectedComponentsToLoad() {
+            return List.of("aws2-s3", "jmx");
+        }
+        
+        @Override
+        public List<String> getExpectedSystemComponents() {
+            return List.of("log");
+        }
+    }
+    
+    private static class XmlRouteBuilder {
+        
+        RouteBuilder builder;
+        XmlRouteBuilder(String content) throws Exception {
+    
+            Resource resource = ResourceHelper.fromString("in-memory.xml", content);
+            try (XmlRoutesBuilderLoader xmlr = new XmlRoutesBuilderLoader()) {
+                builder = (RouteBuilder) xmlr.loadRoutesBuilder(resource);
+            }
+        }
+        
+        public RouteBuilder getRouteBuilder() {
+            return builder;
+        }
+    }
+    private static class SimpleXmlRoutes extends RouteBuilder implements TestExpectations {
+    
+        @Override
+        public List<String> getExpectedComponentsToLoad() {
+            return Collections.emptyList();
+        }
+    
+        @Override
+        public List<String> getExpectedSystemComponents() {
+            return List.of("direct");
+        }
+    
+        @Override
+        public void configure() throws Exception {
+            String content = ""
+                    + "<routes xmlns=\"http://camel.apache.org/schema/spring\" xmlns:foo=\"http://io.vantiq/foo\">"
+                    + "   <route id=\"xpath-route\">"
+                    + "      <from uri=\"direct:test\"/>"
+                    + "      <setBody>"
+                    + "         <xpath resultType=\"java.lang.String\">"
+                    + "            /foo:orders/order[1]/country/text()"
+                    + "         </xpath>"
+                    + "      </setBody>"
+                    + "   </route>"
+                    + "</routes>";
+            RouteBuilder rb = new XmlRouteBuilder(content).getRouteBuilder();
+            rb.configure();
+            this.setRouteCollection(rb.getRouteCollection());
+        }
+    }
+    
+    private static class ComplexXmlRoutes extends RouteBuilder implements TestExpectations {
+    
+        @Override
+        public List<String> getExpectedComponentsToLoad() {
+            return List.of("salesforce");
+        }
+        
+        @Override
+        public List<String> getExpectedSystemComponents() {
+            return List.of("direct-vm");
+        }
+        
+        @Override
+        public void configure() throws Exception {
+            String content = ""
+                    + "    <routes>"
+                    + "        <route id=\"salesforce1\">"
+                    + "            <from uri=\"salesforce:CamelTestTopic?notifyForFields=ALL"
+                    + "&amp;notifyForOperations=ALL&amp;sObjectName=Merchandise__c&amp;updateTopic=true"
+                    + "&amp;sObjectQuery=SELECT Id, Name FROM Merchandise__c\"/>"
+                    + "            <to uri=\"salesforce:createSObject?sObjectName=TestEvent__e\"/>"
+                    + "            <to uri=\"direct-vm:salesforce-notifier\"/>"
+                    + "        </route>"
+                    + "    </routes>";
+            RouteBuilder rb = new XmlRouteBuilder(content).getRouteBuilder();
+            rb.configure();
+            this.setRouteCollection(rb.getRouteCollection());
+        }
+    }
+}
diff --git a/camelConnector/src/test/java/io/vantiq/extsrc/camelconn/discover/VantiqComponentResolverTest.java b/camelConnector/src/test/java/io/vantiq/extsrc/camelconn/discover/VantiqComponentResolverTest.java
new file mode 100644
index 00000000..e709e600
--- /dev/null
+++ b/camelConnector/src/test/java/io/vantiq/extsrc/camelconn/discover/VantiqComponentResolverTest.java
@@ -0,0 +1,737 @@
+/*
+ * Copyright (c) 2022 Vantiq, Inc.
+ *
+ * All rights reserved.
+ *
+ * SPDX: MIT
+ */
+
+package io.vantiq.extsrc.camelconn.discover;
+
+import com.fasterxml.jackson.databind.JsonNode;
+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;
+import org.junit.Test;
+import org.junit.rules.TestName;
+import org.junit.runners.MethodSorters;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.xbill.DNS.Message;
+import org.xbill.DNS.Section;
+
+import java.io.File;
+import java.net.URI;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Perform unit tests for component resolution
+ */
+
+// Method order used to check caching for SimpleCamelResolution
+// Not using Slf4J due to use in static context
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class VantiqComponentResolverTest extends CamelTestSupport {
+    public final static String MISSING_VALUE = "<missing>";
+    
+    @Rule
+    public TestName testName = new TestName();
+    
+    private static final Logger log =  LoggerFactory.getLogger(VantiqComponentResolverTest.class);
+    public static final String DEST_PATH = "build/loadedlib";
+    public static final String IVY_CACHE_PATH = "build/ivyCache";
+    
+    File dest;
+    File cache = new File(IVY_CACHE_PATH);
+    
+    public static final String sfLoginUrl = System.getProperty("camel-salesforce-loginUrl");
+    public static final String sfClientId = System.getProperty("camel-salesforce-clientId");
+    public static final String sfClientSecret = System.getProperty("camel-salesforce-clientSecret");
+    public static final String sfRefreshToken = System.getProperty("camel-salesforce-refreshToken");
+    
+    @Before
+    public void setup() {
+        dest = new File(DEST_PATH);
+        if (dest.exists()) {
+            FileUtil.forceDelete(dest);
+        }
+    }
+    
+    @Test
+    public void testResolutionSimpleCamel() throws Exception {
+        
+        FileUtil.forceDelete(cache);    // Clear the cache
+        CamelResolver cr = new CamelResolver(this.getTestMethodName(), (URI) null,
+                                             cache, dest);
+        Collection<File> resolved = cr.resolve("org.apache.camel", "camel" + "-salesforce",
+                                               context.getVersion(), testName.getMethodName());
+        assert resolved.size() > 0;
+        File[] files = resolved.toArray(new File[0]);
+        boolean foundRequested = false;
+        for (File f: files) {
+            assertEquals("Path in destination", dest.getAbsolutePath(), f.getParent());
+            if (f.getName().equals("camel-salesforce-" + context.getVersion() + ".jar")) {
+                foundRequested = true;
+            }
+        }
+        assertTrue("Found original request", foundRequested);
+        
+        ArrayList<URL> urlList = new ArrayList<>();
+        for (File f: files) {
+            URL url = f.toURI().toURL();
+            urlList.add(url);
+        }
+        for (URL one: urlList) {
+            log.debug("urlList entry: {}", one.toString());
+        }
+    
+        // Now, let's try & load a class from there...
+        
+        URLClassLoader ucl = new URLClassLoader(urlList.toArray(new URL[0]),
+                                                this.getClass().getClassLoader());
+       
+        try {
+            Class<?> sfClass = Class.forName("org.apache.camel.component.salesforce.SalesforceComponent", false, ucl);
+            assertNotNull("SalesforceComponent class null", sfClass);
+            log.debug("Found SalesforceComponent class: {}::{}", sfClass.getPackageName(), sfClass.getName());
+
+            for (java.lang.reflect.Method m: sfClass.getMethods()) {
+                log.debug("SalesforceComponent class has method: {}", m.getName());
+            }
+
+            sfClass = Class.forName("org.apache.camel.component.salesforce.SalesforceEndpoint", false, ucl);
+            assertNotNull("SalesforceEndpoing class null", sfClass);
+            log.debug("Found SalesforceEndpoint class: {}::{}", sfClass.getPackageName(), sfClass.getName());
+
+            for (java.lang.reflect.Method m: sfClass.getMethods()) {
+                log.debug("SalesforceEndpoint class has method: {}", m.getName());
+            }
+            for (Package p: ucl.getDefinedPackages()) {
+                log.debug("Classloader has package: {}", p.getName());
+            }
+        } catch (Exception e) {
+            // Putting assert here so we get information about the unexpected exception
+            assertNull("Unexpected Exception", e);
+        }
+    }
+    
+    @Test
+    public void testResolutionSimpleCamelCached() throws Exception {
+        // Here, we leave the cache alone
+        CamelResolver cr = new CamelResolver(this.getTestMethodName(), (URI) null,
+                                             cache, dest);
+        Collection<File> resolved = cr.resolve("org.apache.camel", "camel" + "-salesforce",
+                                               context.getVersion(), testName.getMethodName());
+        assert resolved.size() > 0;
+        File[] files = resolved.toArray(new File[0]);
+        boolean foundRequested = false;
+        for (File f: files) {
+            assertEquals("Path in destination", dest.getAbsolutePath(), f.getParent());
+            if (f.getName().equals("camel-salesforce-" + context.getVersion() + ".jar")) {
+                foundRequested = true;
+            }
+        }
+        assertTrue("Found original request", foundRequested);
+    }
+    
+    @Test
+    public void testResolutionFailure() throws Exception {
+        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());
+        } catch (ResolutionException re) {
+            assert re.getMessage().contains("Error(s) encountered during resolution: ");
+            assert re.getMessage().contains("org.apache.camel#camel-horse-designed-by-committee;");
+        } catch (Exception e) {
+            fail("Unexpected exception: " + e.getClass().getName() + "::" + e.getMessage());
+        }
+        
+        try {
+            CamelResolver nope = 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");
+        } catch (Exception e) {
+            assertNull("Unexpected exception: " + e.getMessage(), e);
+        }
+    
+        try {
+            cr.resolve(null, "somename", "someVersion", testName.getMethodName());
+            fail("Null organization should not work");
+        } catch (IllegalArgumentException iae) {
+            assert iae.getMessage().contains("The parameters organization, name, and revision must be non-null");
+        } catch (Exception e) {
+            fail("Unexpected exception: " + e.getClass().getName() + "::" + e.getMessage());
+        }
+    
+        try {
+            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");
+        } catch (Exception e) {
+            fail("Unexpected exception: " + e.getClass().getName() + "::" + e.getMessage());
+        }
+        
+        try {
+            cr.resolve("somegroup", "somename", null, testName.getMethodName());
+            fail("Null revision should not work");
+        } catch (IllegalArgumentException iae) {
+            assert iae.getMessage().contains("The parameters organization, name, and revision must be non-null");
+        } catch (Exception e) {
+            fail("Unexpected exception: " + e.getClass().getName() + "::" + e.getMessage());
+        }
+        
+        try {
+            cr.resolve("somegroup", "somename", "someVersion", testName.getMethodName());
+            fail("Non-sensical resolution should fail");
+        } catch (ResolutionException re) {
+            assert re.getMessage().contains("Error(s) encountered during resolution: ");
+            assert re.getMessage().contains("somegroup#somename;someVersion");
+         } catch (Exception e) {
+            fail("Unexpected exception: " + e.getClass().getName() + "::" + e.getMessage());
+        }
+    }
+    @Test
+    public void testResolutionAlternateRepos() throws Exception {
+        FileUtil.forceDelete(cache);    // Clear the cache
+        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());
+        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 {
+        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())) {
+            runner.runRoutes(false);
+        }
+    }
+    
+    @Test
+    public void testStartRouteLoadedComponentsAndMarshaling() throws Exception {
+        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())) {
+            runner.runRoutes(false);
+        }
+    }
+    
+    
+    @Test
+    public void testStartRouteLoadedComponentsMultiRepo() throws Exception {
+        FileUtil.forceDelete(cache);    // Clear the cache
+        RouteBuilderWithProps rb = new SimpleExternalRoute();
+        assertNotNull("No routebuilder", rb);
+        setUseRouteBuilder(false);
+        List<URI> repoList = new ArrayList<>();
+        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())) {
+            runner.runRoutes(false);
+        }
+    }
+    
+    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 "
+            + "Haplorrhini suborder and simian infraorder, either an Old World monkey "
+            + "or a New World monkey, but excluding apes. There are about 260 known "
+            + "living specie\" \"s of monkey. Many are arboreal, although there are "
+            + "species that live primarily on the ground, such as baboons... "
+            + "http://en.wikipedia.org/wiki/Monkey\"";
+    
+    public static final String QUERY_AARDVARK = "aardvark.wp.dg.cx";
+    public static final String RESPONSE_AARDVARK = "\"The aardvark (Orycteropus afer) is a medium-sized, burrowing, " +
+            "nocturnal mammal native to Africa. It is the only living species of the order Tubulidentata, although " +
+            "other prehistoric species and genera of Tubulidentata are known. " +
+            "http://en.wikipedia.org/wi\" \"ki/Aardvark\"";
+    
+    @Test
+    public void testStartRunLoadedComponents() throws Exception {
+        FileUtil.forceDelete(cache);    // Clear the cache
+        
+        setUseRouteBuilder(false);
+        RouteBuilderWithProps rb = new MakeDigCall();
+        assertNotNull("No routebuilder", rb);
+        
+        performLoadAndRunTest(rb, rb.getComponentsToInit());
+    }
+    
+    @Test
+    public void testStartRunLoadedComponentsMarshaled() throws Exception {
+        FileUtil.forceDelete(cache);    // Clear the cache
+        
+        setUseRouteBuilder(false);
+        RouteBuilderWithProps rb = new MakeDigCallMarshaled();
+        assertNotNull("No routebuilder", rb);
+        
+        performLoadAndRunTest(rb, rb.getComponentsToInit());
+    }
+    
+    @Test
+    public void testStartRunLoadedComponentsMarshaledAvroFailure() throws Exception {
+        FileUtil.forceDelete(cache);    // Clear the cache
+        
+        setUseRouteBuilder(false);
+        RouteBuilderWithProps rb = new MakeDigCallMarshaledAvroFailure();
+        assertNotNull("No routebuilder", rb);
+        
+        performLoadAndRunTest(rb, false, rb.getComponentsToInit());
+    }
+    
+    @Test
+    public void testStartRunLoadedComponentsMarshaledGzip() throws Exception {
+        FileUtil.forceDelete(cache);    // Clear the cache
+        
+        setUseRouteBuilder(false);
+        RouteBuilderWithProps rb = new MakeDigCallMarshaledGzip();
+        assertNotNull("No routebuilder", rb);
+        
+        performLoadAndRunTest(rb, rb.getComponentsToInit());
+    }
+    
+    @Test
+    public void testStartRunLoadedComponentsSalesforceRefreshToken() throws Exception {
+        Assume.assumeTrue(sfLoginUrl != null &&
+                                  sfClientId != null &&
+                                  sfClientSecret != null &&
+                                  sfRefreshToken != null);
+        FileUtil.forceDelete(cache);    // Clear the cache
+        
+        setUseRouteBuilder(false);
+        RouteBuilderWithProps rb = new SalesForceTasks();
+        assertNotNull("No routebuilder", rb);
+        
+        performLoadAndRunTest(rb, rb.getComponentsToInit());
+    }
+ 
+    // Test that caching works as expected when thing downloaded, then only cached, and then already in place.
+    // In all cases, our internal classloader should load the things needed by the routes in question.
+    @Test
+    public void testStartRunLoadedComponentsUsingPreviouslyRetrievedComponents() throws Exception {
+        FileUtil.forceDelete(cache);    // Clear the cache
+        
+        setUseRouteBuilder(false);
+        RouteBuilderWithProps rb = new MakeDigCall();
+        assertNotNull("No routebuilder", rb);
+        
+        log.info("Loading cache and library");
+        // First run: cache & lib/dest deleted.  Download all
+        performLoadAndRunTest(rb, rb.getComponentsToInit());
+        
+        // Second run, leave the cache but delete the library.  Should copy from cache
+        if (dest.exists()) {
+            FileUtil.forceDelete(dest);
+        }
+        log.info("Cache loaded, deleted lib/dest, running same route");
+        performLoadAndRunTest(rb, rb.getComponentsToInit());
+        
+        log.info("Leaving cache & lib/dest intact, running again");
+        // Finally, leave everything.  Resolution should do nothing but code will run
+        performLoadAndRunTest(rb, rb.getComponentsToInit());
+    }
+    
+    public static final String XML_ROUTE = ""
+            + "<routes xmlns=\"http://camel.apache.org/schema/spring\" xmlns:foo=\"http://io.vantiq/foo\">"
+            + "   <route id=\"Dig from xml-route\">"
+            + "      <from uri=\"direct:start\"/>"
+            + "      <to uri=\"dns:dig\"/>"
+            + "      <to uri=\"mock:result\"/>"
+            + "   </route>"
+            + "</routes>";
+    
+    // Note: Unlike the example on the site, the following will fail to start (claiming > 1 consumer for
+    // direct:start) if the top-level "- route" line is missing.d
+    public static final String YAML_ROUTE =  "\n"
+            + "- route:\n"
+            + "    id: \"Dig from yaml-route\"\n"
+            + "    from:\n"
+            + "      uri: \"direct:start\"\n"
+            + "      steps:\n"
+            + "        - to:\n"
+            + "            uri: \"dns:dig\"\n"
+            + "        - to:\n"
+            + "            uri: \"mock:result\"\n";
+    
+    @Test
+    public void testStartRunLoadedComponentsFromXmlText() throws Exception {
+        FileUtil.forceDelete(cache);    // Clear the cache
+    
+        setUseRouteBuilder(false);
+    
+        performLoadAndRunTest(XML_ROUTE, "xml", null);
+    }
+    
+    @Test
+    public void testStartRunLoadedComponentsFromYamlText() throws Exception {
+        FileUtil.forceDelete(cache);    // Clear the cache
+        setUseRouteBuilder(false);
+    
+        performLoadAndRunTest(YAML_ROUTE, "yaml", null);
+    }
+    
+    /**
+     * 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, String, Object, Boolean>
+     */
+    public static TriFunction<CamelContext, String, Object, Boolean> defineVerifyOperation() {
+        TriFunction<CamelContext, String, Object, Boolean> verifyOperation = (context, query, answerObj) -> {
+            boolean worked = true;
+            String routeId = context.getRoutes().get(0).getId();
+            final String answerStr;
+            final Map<String, ?> answerMap;
+            
+            if (answerObj instanceof String) {
+                answerStr = (String) answerObj;
+                answerMap = null;
+            } else if (answerObj instanceof Map) {
+                //noinspection unchecked
+                answerMap = (Map<String, ?>) answerObj;
+                answerStr = null;
+            } else {
+                answerMap = null;
+                answerStr = null;
+            }
+    
+            try {
+                if (context.isStopped()) {
+                    log.error("At test start, context {} is stopped", context.getName());
+                }
+                Endpoint res = context.getEndpoint("mock://result");
+                assert res instanceof MockEndpoint;
+                MockEndpoint resultEndpoint = (MockEndpoint) res;
+            
+                ProducerTemplate template = context.createProducerTemplate();
+                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());
+                                }
+                            }
+                            return result;
+                        }
+                        return false;
+                    }
+                });
+    
+                Map<String, Object> headers = new HashMap<>();
+                if (routeId.contains("Dig")) {
+                    headers.put("dns.name", query);
+                    headers.put("dns.type", "TXT");
+                }
+                if (context.isStopped()) {
+                    log.error("Context {} is stopped", context.getName());
+                }
+                template.sendBodyAndHeaders("direct:start", null, headers);
+                
+                assertTrue("Expected some exchanges", resultEndpoint.getReceivedCounter() > 0);
+                if (resultEndpoint.getReceivedCounter() > 1) {
+                    resultEndpoint.assertIsSatisfied();
+                }
+            } catch (Exception e) {
+                log.error("Route " + routeId + ": Trapped exception", e);
+                worked = false;
+            }
+            return worked;
+        };
+        return verifyOperation;
+    }
+    
+    public void performLoadAndRunTest(String content, String contentType,
+                                      List<Map<String, Object>> compToInit) throws Exception {
+        // To do this test, we'll create a callable that the test method will call. In this case, the callable "sends"
+        // message to the route which, in turn, makes the dig call to lookup a monkey.  We verify that the expected
+        // results is presented.
+        //
+        // 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;
+        
+        try (CamelRunner runner =
+                     new CamelRunner(this.getTestMethodName(), content, contentType, null,
+                                     IVY_CACHE_PATH, DEST_PATH, compToInit)) {
+            openedRunner = runner;
+            runner.runRoutes(false);
+            runnerContext = runner.getCamelContext();
+            runnerThread = runner.getCamelThread();
+            assert runner.isStarted();
+    
+            // At this point, the route is running -- let's verify that it works.
+    
+            assert verifyOperation.apply(runnerContext, QUERY_MONKEY, RESPONSE_MONKEY);
+            assert verifyOperation.apply(runnerContext, QUERY_AARDVARK, RESPONSE_AARDVARK);
+        }
+        
+        while (!openedRunner.isStopped()) {
+            Thread.sleep(500);
+        }
+        
+        assert runnerThread != null && !runnerThread.isAlive();
+        assert runnerContext != null && runnerContext.isStopped();
+    }
+    
+    public void performLoadAndRunTest(RouteBuilder rb, List<Map<String, Object>> compToInit) throws Exception {
+        performLoadAndRunTest(rb, true, compToInit);
+    }
+    public void performLoadAndRunTest(RouteBuilder rb, boolean shouldStart,
+                                      List<Map<String, Object>> componentToInit) throws Exception {
+        // To do this test, we'll create a callable that the test method will call. In this case, the callable "sends"
+        // message to the route which, in turn, makes the dig call to lookup a monkey & aardvark.  We verify that the
+        // expected results are presented.
+        //
+        // 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;
+        
+        try (CamelRunner runner =
+                     new CamelRunner(this.getTestMethodName(), rb, null, IVY_CACHE_PATH, DEST_PATH,
+                                     componentToInit)) {
+            openedRunner = runner;
+            runner.runRoutes(false);
+            runnerContext = runner.getCamelContext();
+            runnerThread = runner.getCamelThread();
+            assert runner.isStarted() == shouldStart;
+            if (shouldStart) {
+                // At this point, the route is running -- let's verify that it works.
+                String routeId = runnerContext.getRoutes().get(0).getId();
+                if (routeId.contains("Dig")) {
+                    if (routeId.contains("Marshaled")) {
+                        Map<String, ?> expectedMap =  Map.of("signed", MISSING_VALUE,
+                                                        "rcode", MISSING_VALUE,
+                                                        "verified", MISSING_VALUE,
+                                                        "opt", MISSING_VALUE,
+                                                        "tsig", MISSING_VALUE,
+                                                        "question", MISSING_VALUE,
+                                                        "resolver", MISSING_VALUE,
+                                                        "header", MISSING_VALUE);
+                        assert verifyOperation.apply(runnerContext, QUERY_MONKEY, expectedMap);
+                        assert verifyOperation.apply(runnerContext, QUERY_AARDVARK, expectedMap);
+                    } else {
+                        assert verifyOperation.apply(runnerContext, QUERY_MONKEY, RESPONSE_MONKEY);
+                        assert verifyOperation.apply(runnerContext, QUERY_AARDVARK, RESPONSE_AARDVARK);
+                    }
+                } else if (routeId.contains("Salesforce")) {
+                    assert verifyOperation.apply(runnerContext, "not used",
+                                                 Map.of ("totalSize", MISSING_VALUE, "done", true));
+                    assert verifyOperation.apply(runnerContext, "not used",
+                                                 Map.of ("totalSize", MISSING_VALUE, "done", true));
+                }
+            }
+        } finally {
+            while (openedRunner != null && !openedRunner.isStopped()) {
+                Thread.sleep(500);
+            }
+        }
+    
+        assertNotNull("runnerThread is null", runnerThread);
+        assertTrue("ShouldStart: " + shouldStart + ", runnerThread: " + runnerThread +
+                           ", ...isAlive: " + runnerThread.isAlive(),
+                   !shouldStart || runnerThread != null && !runnerThread.isAlive());
+        assert runnerContext != null && runnerContext.isStopped();
+    }
+    
+    private static abstract class RouteBuilderWithProps extends RouteBuilder {
+        
+        public List<Map<String, Object>> getComponentsToInit() {
+            return null;
+        }
+    }
+    
+    private static class SimpleExternalRoute extends RouteBuilderWithProps  {
+        @Override
+        public void configure() {
+            // From some jetty resource, get the message content & log it.
+            
+            // The following does nothing, but verifies that things all start up.  All we are really verifying here
+            // is that all the classes are loaded so that camel & its associated components are operating.
+            
+            // Since 1) we don't really care, and 2) Jenkins runs may not allow us to pick a "normal" port,
+            // we'll specify port 0 here.  Using port 0 tells Jetty to pick an unused port (well, actually, this tells
+            // the lower-level socket constructor).  In either case, this allows this code to work in
+            // environments more constrained than one's own machine.
+            
+            from("jetty:http://0.0.0.0:0/myapp/myservice/?sessionSupport=true")
+                    .to("log:" + log.getName()  + "level=debug");
+        }
+    }
+    
+    private static class MarshaledExternalRoute extends RouteBuilderWithProps  {
+        
+        @Override
+        public void configure() {
+            // From some jetty resource, get the message content & log it.
+            
+            // The following does nothing, but verifies that things all start up.  All we are really verifying here
+            // is that all the classes are loaded so that camel & its associated components are operating.
+            
+            // Since 1) we don't really care, and 2) Jenkins runs may not allow us to pick a "normal" port,
+            // we'll specify port 0 here.  Using port 0 tells Jetty to pick an unused port (well, actually, this tells
+            // the lower-level socket constructor).  In either case, this allows this code to work in
+            // environments more constrained than one's own machine.
+            
+            from("jetty:http://0.0.0.0:0/myapp/myservice/?sessionSupport=true")
+                    .marshal().csv()
+                    .to("log:" + log.getName()  + "level=debug");
+        }
+    }
+    
+    private static class MakeDigCall extends RouteBuilderWithProps {
+        public void configure() {
+            from("direct:start")
+                    .routeId("Simple Dig Call")
+                    .to("dns:dig")
+                    .to("mock:result");
+        }
+    }
+    
+    // The *Marshaled* route builders here verify that we properly discover & load data formats
+    // Generally, the routes are not expected to be sensible or useful.
+    
+    private static class MakeDigCallMarshaled extends RouteBuilderWithProps {
+        public void configure() {
+            from("direct:start")
+                    .routeId("Dig Call Marshaled")
+                    .to("dns:dig")
+                    .log("INFO")
+                    .marshal().json(JsonLibrary.Jackson)
+                    .log("INFO")
+                    .unmarshal().json(JsonLibrary.Jackson)
+                    .log("INFO")
+                    .to("mock:result");
+        }
+    }
+    
+    private static class MakeDigCallMarshaledGzip extends RouteBuilderWithProps {
+
+        public void configure() {
+            from("direct:start")
+                    .routeId("Dig Call MarshaledGzip")
+                    .to("dns:dig")
+                    .log("INFO")
+                    .marshal().json()
+                    .marshal().gzipDeflater()
+                    .log("INFO")
+                    .unmarshal().gzipDeflater()
+                    .unmarshal().json()
+                    .log("INFO")
+                    .to("mock:result");
+        }
+    }
+    
+    private static class MakeDigCallMarshaledAvroFailure extends RouteBuilderWithProps {
+        
+        public void configure() {
+            from("direct:start")
+                    .routeId("Dig Call Marshaled Avro Failure")
+                    .to("dns:dig")
+                    .log("INFO")
+                    .marshal().json(JsonLibrary.Jackson, JsonNode.class)
+                    .marshal().avro(AvroLibrary.ApacheAvro)
+                    .log("INFO")
+                    .unmarshal().avro(AvroLibrary.ApacheAvro, JsonNode.class)
+                    .unmarshal().json(JsonLibrary.Jackson)
+                    .log("INFO")
+                    .to("mock:result");
+        }
+    }
+    
+    public static class SalesForceTasks extends RouteBuilderWithProps {
+        private final Map<String, String> propList = Map.of( "loginUrl", sfLoginUrl,
+                                                             "clientId", sfClientId,
+                                                             "clientSecret", sfClientSecret,
+                                                             "refreshToken", sfRefreshToken
+        );
+        
+        @Override
+        public List<Map<String, Object>> getComponentsToInit() {
+            return List.of(
+                    Map.of(CamelRunner.COMPONENT_NAME, "salesforce",
+                           CamelRunner.COMPONENT_PROPERTIES, propList));
+        }
+        
+        public void configure() {
+            
+            from("direct:start")
+                    .routeId("Salesforce Query")
+                    .to("salesforce:query?rawPayload=true" +
+                                "&SObjectQuery=SELECT Id, Subject, OwnerId from Task")
+                    .unmarshal().json()
+                    .to("log:info")
+                    .to("mock:result");
+        }
+    }
+}
diff --git a/camelConnector/src/test/resources/camelConnectorImpl.json b/camelConnector/src/test/resources/camelConnectorImpl.json
new file mode 100644
index 00000000..0fc0c22a
--- /dev/null
+++ b/camelConnector/src/test/resources/camelConnectorImpl.json
@@ -0,0 +1,8 @@
+{
+  "name" : "CAMEL_CONNECTOR",
+  "baseType" : "EXTENSION",
+  "verticle" : "service:extensionSource",
+  "config" : {},
+  "baseConfigProperties": [
+  ]
+}
diff --git a/camelConnector/src/test/resources/log4j2.properties b/camelConnector/src/test/resources/log4j2.properties
new file mode 100644
index 00000000..009d277b
--- /dev/null
+++ b/camelConnector/src/test/resources/log4j2.properties
@@ -0,0 +1,12 @@
+
+appender.out.type = Console
+appender.out.name = out
+appender.out.layout.type = PatternLayout
+appender.out.layout.pattern = [%30.30t] %-30.30c{1} %-5p %m%n
+rootLogger.level = INFO
+rootLogger.appenderRef.out.ref = out
+
+logger.vantiqcamelconn.name = io.vantiq.extsrc.camelconn
+logger.vantiqcamelconn.level = DEBUG
+logger.vantiqcamelconn.appenderRef = Console
+
diff --git a/extjsdk/src/test/java/io/vantiq/extjsdk/FalseClient.java b/extjsdk/src/test/java/io/vantiq/extjsdk/FalseClient.java
index c3a9f975..443a8df4 100644
--- a/extjsdk/src/test/java/io/vantiq/extjsdk/FalseClient.java
+++ b/extjsdk/src/test/java/io/vantiq/extjsdk/FalseClient.java
@@ -25,19 +25,26 @@
  *
  */
 public class FalseClient extends ExtensionWebSocketClient {
+    
     public FalseClient(String sourceName) {
         super(sourceName);
+        listener = new TestListener(this);
     }
     
     ObjectMapper mapper = new ObjectMapper();
-
+    
     @Override
-    public CompletableFuture<Boolean> initiateWebsocketConnection(String url) {
+    public CompletableFuture<Boolean> initiateWebsocketConnection(String url, boolean sendPings) {
         webSocketFuture = new CompletableFuture<Boolean>();
         webSocket = new FalseWebSocket();
         return webSocketFuture;
     }
     
+    @Override
+    public CompletableFuture<Boolean> initiateWebsocketConnection(String url) {
+        return initiateWebsocketConnection(url, false);
+    }
+    
     /**
      * Tries to signal that the webSocket connection completed with status {@success}. Returns true if this action
      * could be completed successfully. 
@@ -100,6 +107,7 @@ public boolean completeSourceConnection(boolean success) {
      *          message has been sent.
      */
     public byte[] getLastMessageAsBytes() {
+        acknowledgeNotification();
         return ((FalseWebSocket) webSocket).getMessage();
     }
     
@@ -110,6 +118,7 @@ public byte[] getLastMessageAsBytes() {
      * @return  The last message sent by the client as a Map, or null if no message has been sent yet.
      */
     public Map getLastMessageAsMap() {
+        acknowledgeNotification();
         try {
             return mapper.readValue(((FalseWebSocket) webSocket).getMessage(), Map.class);
         } catch(IOException e) {
@@ -125,7 +134,9 @@ public Map getLastMessageAsMap() {
      * @throws  IOException if the message could not be interpreted as a Response. This is most likely to occur if
      *          the last message was not a Response.
      */
-    public Response getLastMessageAsResponse() throws IOException{
+    public Response getLastMessageAsResponse() throws IOException {
+        acknowledgeNotification();
+    
         byte[] bytes = ((FalseWebSocket) webSocket).getMessage();
         if (bytes == null) {
             return null;
@@ -142,7 +153,8 @@ public Response getLastMessageAsResponse() throws IOException{
      * @throws  IOException if the message could not be interpreted as a ExtensionServiceMessage. This is most
      *          likely to occur if the last message was not a ExtensionServiceMessage.
      */
-    public ExtensionServiceMessage getLastMessageAsExtSvcMsg() throws IOException{
+    public ExtensionServiceMessage getLastMessageAsExtSvcMsg() throws IOException {
+        acknowledgeNotification();
         byte[] bytes = ((FalseWebSocket) webSocket).getMessage();
         if (bytes == null) {
             return null;