You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by da...@apache.org on 2021/11/18 08:43:00 UTC

[camel] branch main updated (945f035 -> 67d2afd)

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

davsclaus pushed a change to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git.


    from 945f035  Use Maven Enforcer 3.0.0 to make Camel build on Java 17
     new 9dd31ad  CAMEL-16656: camel-core - ResourceReloader SPI - Allow to reload on changes
     new 67d2afd  CAMEL-16656: camel-core - ResourceReloader SPI - Allow to reload on changes

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../main/camel-main-configuration-metadata.json    |   3 +
 .../main/java/org/apache/camel/RoutesBuilder.java  |   8 +
 .../ResourceReload.java}                           |  17 +-
 .../ResourceReloadStrategy.java}                   |  40 ++-
 .../java/org/apache/camel/spi/RoutesLoader.java    |  17 ++
 .../org/apache/camel/builder/RouteBuilder.java     |  55 ++++
 .../MainConfigurationPropertiesConfigurer.java     |  18 ++
 .../camel-main-configuration-metadata.json         |   3 +
 core/camel-main/src/main/docs/main.adoc            |   5 +-
 .../camel/main/DefaultConfigurationConfigurer.java |   6 +
 .../camel/main/DefaultConfigurationProperties.java |  60 ++++
 .../support/FileWatcherResourceReloadStrategy.java | 301 +++++++++++++++++++++
 .../support/ResourceReloadStrategySupport.java     |  90 ++++++
 .../camel/support/RouteWatcherReloadStrategy.java  |  99 +++++++
 dsl/camel-xml-io-dsl/pom.xml                       |   5 +
 .../org/apache/camel/dsl/xml/io/XmlMainTest.java   |   2 +-
 .../io/reload/RouteWatcherReloadStrategyTest.java  | 205 ++++++++++++++
 .../resources/org/apache/camel/reload/barRoute.xml |  29 ++
 .../org/apache/camel/reload}/barUpdatedRoute.xml   |   0
 19 files changed, 942 insertions(+), 21 deletions(-)
 copy core/camel-api/src/main/java/org/apache/camel/{RoutesBuilder.java => spi/ResourceReload.java} (62%)
 copy core/camel-api/src/main/java/org/apache/camel/{RoutesBuilder.java => spi/ResourceReloadStrategy.java} (50%)
 create mode 100644 core/camel-support/src/main/java/org/apache/camel/support/FileWatcherResourceReloadStrategy.java
 create mode 100644 core/camel-support/src/main/java/org/apache/camel/support/ResourceReloadStrategySupport.java
 create mode 100644 core/camel-support/src/main/java/org/apache/camel/support/RouteWatcherReloadStrategy.java
 create mode 100644 dsl/camel-xml-io-dsl/src/test/java/org/apache/camel/dsl/xml/io/reload/RouteWatcherReloadStrategyTest.java
 create mode 100644 dsl/camel-xml-io-dsl/src/test/resources/org/apache/camel/reload/barRoute.xml
 copy {core/camel-xml-io/src/test/resources => dsl/camel-xml-io-dsl/src/test/resources/org/apache/camel/reload}/barUpdatedRoute.xml (100%)

[camel] 01/02: CAMEL-16656: camel-core - ResourceReloader SPI - Allow to reload on changes

Posted by da...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 9dd31ad6b7e961799f0681c752b942fdb5a98257
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Thu Nov 18 08:57:48 2021 +0100

    CAMEL-16656: camel-core - ResourceReloader SPI - Allow to reload on changes
---
 .../main/camel-main-configuration-metadata.json    |   3 +
 .../main/java/org/apache/camel/RoutesBuilder.java  |   8 +
 .../ResourceReload.java}                           |  17 +-
 .../ResourceReloadStrategy.java}                   |  40 ++-
 .../java/org/apache/camel/spi/RoutesLoader.java    |  17 ++
 .../org/apache/camel/builder/RouteBuilder.java     |  55 ++++
 .../MainConfigurationPropertiesConfigurer.java     |  18 ++
 .../camel-main-configuration-metadata.json         |   3 +
 core/camel-main/src/main/docs/main.adoc            |   5 +-
 .../camel/main/DefaultConfigurationConfigurer.java |   6 +
 .../camel/main/DefaultConfigurationProperties.java |  60 ++++
 .../support/FileWatcherResourceReloadStrategy.java | 301 +++++++++++++++++++++
 .../support/ResourceReloadStrategySupport.java     |  90 ++++++
 .../camel/support/RouteWatcherReloadStrategy.java  |  96 +++++++
 14 files changed, 699 insertions(+), 20 deletions(-)

diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/main/camel-main-configuration-metadata.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/main/camel-main-configuration-metadata.json
index 58c12b9..fc2c494 100644
--- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/main/camel-main-configuration-metadata.json
+++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/main/camel-main-configuration-metadata.json
@@ -75,6 +75,9 @@
     { "name": "camel.main.routesCollectorEnabled", "description": "Whether the routes collector is enabled or not. When enabled Camel will auto-discover routes (RouteBuilder instances from the registry and also load additional routes from the file system). The routes collector is default enabled.", "sourceType": "org.apache.camel.main.DefaultConfigurationProperties", "type": "boolean", "javaType": "boolean", "defaultValue": true },
     { "name": "camel.main.routesExcludePattern", "description": "Used for exclusive filtering of routes from directories. The exclusive filtering takes precedence over inclusive filtering. The pattern is using Ant-path style pattern. Multiple patterns can be specified separated by comma, as example, to exclude all the routes from a directory whose name contains foo use: &#42;&#42;\/foo.", "sourceType": "org.apache.camel.main.DefaultConfigurationProperties", "type": "string", "javaType":  [...]
     { "name": "camel.main.routesIncludePattern", "description": "Used for inclusive filtering of routes from directories. The exclusive filtering takes precedence over inclusive filtering. The pattern is using Ant-path style pattern. Multiple patterns can be specified separated by comma, as example, to include all the routes from a directory whose name contains foo use: &#42;&#42;\/foo.", "sourceType": "org.apache.camel.main.DefaultConfigurationProperties", "type": "string", "javaType":  [...]
+    { "name": "camel.main.routesReloadDirectory", "description": "Directory to scan (incl subdirectories) for route changes. Camel cannot scan the classpath, so this must be configured to a file directory. Development with Maven as build tool, you can configure the directory to be src\/main\/resources to scan for Camel routes in XML or YAML files.", "sourceType": "org.apache.camel.main.DefaultConfigurationProperties", "type": "string", "javaType": "java.lang.String", "defaultValue": "src [...]
+    { "name": "camel.main.routesReloadEnabled", "description": "Used for enabling automatic routes reloading. If enabled then Camel will watch for file changes in the given reload directory, and trigger reloading routes if files are changed.", "sourceType": "org.apache.camel.main.DefaultConfigurationProperties", "type": "boolean", "javaType": "boolean", "defaultValue": "false" },
+    { "name": "camel.main.routesReloadPattern", "description": "Used for inclusive filtering of routes from directories. Typical used for specifying to accept routes in XML or YAML files. The default pattern is .yaml,.xml Multiple patterns can be specified separated by comma.", "sourceType": "org.apache.camel.main.DefaultConfigurationProperties", "type": "string", "javaType": "java.lang.String", "defaultValue": "*.xml,*.yaml" },
     { "name": "camel.main.shutdownLogInflightExchangesOnTimeout", "description": "Sets whether to log information about the inflight Exchanges which are still running during a shutdown which didn't complete without the given timeout. This requires to enable the option inflightRepositoryBrowseEnabled.", "sourceType": "org.apache.camel.main.DefaultConfigurationProperties", "type": "boolean", "javaType": "boolean", "defaultValue": true },
     { "name": "camel.main.shutdownNowOnTimeout", "description": "Sets whether to force shutdown of all consumers when a timeout occurred and thus not all consumers was shutdown within that period. You should have good reasons to set this option to false as it means that the routes keep running and is halted abruptly when CamelContext has been shutdown.", "sourceType": "org.apache.camel.main.DefaultConfigurationProperties", "type": "boolean", "javaType": "boolean", "defaultValue": true },
     { "name": "camel.main.shutdownRoutesInReverseOrder", "description": "Sets whether routes should be shutdown in reverse or the same order as they were started.", "sourceType": "org.apache.camel.main.DefaultConfigurationProperties", "type": "boolean", "javaType": "boolean", "defaultValue": true },
diff --git a/core/camel-api/src/main/java/org/apache/camel/RoutesBuilder.java b/core/camel-api/src/main/java/org/apache/camel/RoutesBuilder.java
index 1775a19..9855b47 100644
--- a/core/camel-api/src/main/java/org/apache/camel/RoutesBuilder.java
+++ b/core/camel-api/src/main/java/org/apache/camel/RoutesBuilder.java
@@ -31,4 +31,12 @@ public interface RoutesBuilder {
      */
     void addRoutesToCamelContext(CamelContext context) throws Exception;
 
+    /**
+     * Adds or updates the routes from this Route Builder to the CamelContext.
+     *
+     * @param  context   the Camel context
+     * @throws Exception is thrown if initialization of routes failed
+     */
+    void updateRoutesToCamelContext(CamelContext context) throws Exception;
+
 }
diff --git a/core/camel-api/src/main/java/org/apache/camel/RoutesBuilder.java b/core/camel-api/src/main/java/org/apache/camel/spi/ResourceReload.java
similarity index 62%
copy from core/camel-api/src/main/java/org/apache/camel/RoutesBuilder.java
copy to core/camel-api/src/main/java/org/apache/camel/spi/ResourceReload.java
index 1775a19..cdbb632 100644
--- a/core/camel-api/src/main/java/org/apache/camel/RoutesBuilder.java
+++ b/core/camel-api/src/main/java/org/apache/camel/spi/ResourceReload.java
@@ -14,21 +14,20 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.camel;
+package org.apache.camel.spi;
 
 /**
- * A routes builder is capable of building routes using the builder and model classes.
- * <p/>
- * Eventually the routes are added to a {@link org.apache.camel.CamelContext} where they run inside.
+ * Listener for re-loading a {@link Resource} such as a Camel route.
  */
-public interface RoutesBuilder {
+@FunctionalInterface
+public interface ResourceReload {
 
     /**
-     * Adds the routes from this Route Builder to the CamelContext.
+     * Callback when the resource is re-loaded.
      *
-     * @param  context   the Camel context
-     * @throws Exception is thrown if initialization of routes failed
+     * @param name     name of the resource such as the file name (absolute)
+     * @param resource the resource
      */
-    void addRoutesToCamelContext(CamelContext context) throws Exception;
+    void onReload(String name, Resource resource);
 
 }
diff --git a/core/camel-api/src/main/java/org/apache/camel/RoutesBuilder.java b/core/camel-api/src/main/java/org/apache/camel/spi/ResourceReloadStrategy.java
similarity index 50%
copy from core/camel-api/src/main/java/org/apache/camel/RoutesBuilder.java
copy to core/camel-api/src/main/java/org/apache/camel/spi/ResourceReloadStrategy.java
index 1775a19..1fd5bb4 100644
--- a/core/camel-api/src/main/java/org/apache/camel/RoutesBuilder.java
+++ b/core/camel-api/src/main/java/org/apache/camel/spi/ResourceReloadStrategy.java
@@ -14,21 +14,41 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.camel;
+package org.apache.camel.spi;
+
+import org.apache.camel.CamelContextAware;
+import org.apache.camel.StaticService;
 
 /**
- * A routes builder is capable of building routes using the builder and model classes.
- * <p/>
- * Eventually the routes are added to a {@link org.apache.camel.CamelContext} where they run inside.
+ * SPI strategy for reloading {@link Resource} in an existing running {@link org.apache.camel.CamelContext}
+ *
+ * @see ResourceReload
+ * @see Resource
  */
-public interface RoutesBuilder {
+public interface ResourceReloadStrategy extends StaticService, CamelContextAware {
 
     /**
-     * Adds the routes from this Route Builder to the CamelContext.
-     *
-     * @param  context   the Camel context
-     * @throws Exception is thrown if initialization of routes failed
+     * Gets the resource listener that is triggered on reload.
      */
-    void addRoutesToCamelContext(CamelContext context) throws Exception;
+    ResourceReload getResourceReload();
 
+    /**
+     * Sets the resource listener to trigger on reload.
+     */
+    void setResourceReload(ResourceReload listener);
+
+    /**
+     * Number of reloads succeeded.
+     */
+    int getReloadCounter();
+
+    /**
+     * Number of reloads failed.
+     */
+    int getFailedCounter();
+
+    /**
+     * Reset the counters.
+     */
+    void resetCounters();
 }
diff --git a/core/camel-api/src/main/java/org/apache/camel/spi/RoutesLoader.java b/core/camel-api/src/main/java/org/apache/camel/spi/RoutesLoader.java
index b75a01e..dd357c1 100644
--- a/core/camel-api/src/main/java/org/apache/camel/spi/RoutesLoader.java
+++ b/core/camel-api/src/main/java/org/apache/camel/spi/RoutesLoader.java
@@ -72,6 +72,23 @@ public interface RoutesLoader extends CamelContextAware {
     }
 
     /**
+     * Loads or updates existing {@link RoutesBuilder} from the give list of {@link Resource} into the current
+     * {@link org.apache.camel.CamelContext}.
+     *
+     * If a route is loaded with a route id for an existing route, then the existing route is stopped and remove, so it
+     * can be updated.
+     *
+     * @param resources the resources to be loaded or updated.
+     */
+    default void updateRoutes(Resource... resources) throws Exception {
+        Collection<RoutesBuilder> builders = findRoutesBuilders(resources);
+        for (RoutesBuilder builder : builders) {
+            // update any existing routes
+            builder.updateRoutesToCamelContext(getCamelContext());
+        }
+    }
+
+    /**
      * Find {@link RoutesBuilder} from the give list of {@link Resource}.
      *
      * @param  resources the resource to be loaded.
diff --git a/core/camel-core-model/src/main/java/org/apache/camel/builder/RouteBuilder.java b/core/camel-core-model/src/main/java/org/apache/camel/builder/RouteBuilder.java
index d5185c8d..5df4719 100644
--- a/core/camel-core-model/src/main/java/org/apache/camel/builder/RouteBuilder.java
+++ b/core/camel-core-model/src/main/java/org/apache/camel/builder/RouteBuilder.java
@@ -20,6 +20,7 @@ import java.io.Reader;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
+import java.util.StringJoiner;
 import java.util.concurrent.atomic.AtomicBoolean;
 
 import org.apache.camel.CamelContext;
@@ -490,6 +491,46 @@ public abstract class RouteBuilder extends BuilderSupport implements RoutesBuild
         }
     }
 
+    @Override
+    public void updateRoutesToCamelContext(CamelContext context) throws Exception {
+        // must configure routes before rests
+        configureRoutes(context);
+        configureRests(context);
+
+        // but populate rests before routes, as we want to turn rests into routes
+        populateRests();
+        populateTransformers();
+        populateValidators();
+        populateRouteTemplates();
+
+        // ensure routes are prepared before being populated
+        for (RouteDefinition route : routeCollection.getRoutes()) {
+            routeCollection.prepareRoute(route);
+        }
+
+        if (!routeCollection.getRoutes().isEmpty()) {
+            StringJoiner csb = new StringJoiner("\n    ");
+            // collect route ids and force assign ids if not in use
+            for (RouteDefinition route : routeCollection.getRoutes()) {
+                if (!route.hasCustomIdAssigned()) {
+                    csb.add(route.getInput().getEndpointUri());
+                }
+            }
+            if (csb.length() > 0) {
+                log.warn(
+                        "Routes with no id's detected. Its recommended to assign route id's to your routes so Camel can reload the routes correctly.\n    Unassigned routes:\n    {}",
+                        csb);
+            }
+        }
+
+        // trigger update of the routes
+        populateOrUpdateRoutes();
+
+        if (this instanceof OnCamelContextEvent) {
+            context.addLifecycleStrategy(LifecycleStrategySupport.adapt((OnCamelContextEvent) this));
+        }
+    }
+
     /**
      * Configures the routes
      *
@@ -584,6 +625,20 @@ public abstract class RouteBuilder extends BuilderSupport implements RoutesBuild
         camelContext.getExtension(Model.class).addRouteDefinitions(getRouteCollection().getRoutes());
     }
 
+    protected void populateOrUpdateRoutes() throws Exception {
+        CamelContext camelContext = getContext();
+        if (camelContext == null) {
+            throw new IllegalArgumentException("CamelContext has not been injected!");
+        }
+        getRouteCollection().setCamelContext(camelContext);
+        // must stop and remove existing running routes
+        for (RouteDefinition route : getRouteCollection().getRoutes()) {
+            camelContext.getRouteController().stopRoute(route.getRouteId());
+            camelContext.removeRoute(route.getRouteId());
+        }
+        camelContext.getExtension(Model.class).addRouteDefinitions(getRouteCollection().getRoutes());
+    }
+
     protected void populateRests() throws Exception {
         CamelContext camelContext = getContext();
         if (camelContext == null) {
diff --git a/core/camel-main/src/generated/java/org/apache/camel/main/MainConfigurationPropertiesConfigurer.java b/core/camel-main/src/generated/java/org/apache/camel/main/MainConfigurationPropertiesConfigurer.java
index efc8393..6da68ed 100644
--- a/core/camel-main/src/generated/java/org/apache/camel/main/MainConfigurationPropertiesConfigurer.java
+++ b/core/camel-main/src/generated/java/org/apache/camel/main/MainConfigurationPropertiesConfigurer.java
@@ -151,6 +151,12 @@ public class MainConfigurationPropertiesConfigurer extends org.apache.camel.supp
         case "RoutesExcludePattern": target.setRoutesExcludePattern(property(camelContext, java.lang.String.class, value)); return true;
         case "routesincludepattern":
         case "RoutesIncludePattern": target.setRoutesIncludePattern(property(camelContext, java.lang.String.class, value)); return true;
+        case "routesreloaddirectory":
+        case "RoutesReloadDirectory": target.setRoutesReloadDirectory(property(camelContext, java.lang.String.class, value)); return true;
+        case "routesreloadenabled":
+        case "RoutesReloadEnabled": target.setRoutesReloadEnabled(property(camelContext, boolean.class, value)); return true;
+        case "routesreloadpattern":
+        case "RoutesReloadPattern": target.setRoutesReloadPattern(property(camelContext, java.lang.String.class, value)); return true;
         case "shutdownloginflightexchangesontimeout":
         case "ShutdownLogInflightExchangesOnTimeout": target.setShutdownLogInflightExchangesOnTimeout(property(camelContext, boolean.class, value)); return true;
         case "shutdownnowontimeout":
@@ -348,6 +354,12 @@ public class MainConfigurationPropertiesConfigurer extends org.apache.camel.supp
         case "RoutesExcludePattern": return java.lang.String.class;
         case "routesincludepattern":
         case "RoutesIncludePattern": return java.lang.String.class;
+        case "routesreloaddirectory":
+        case "RoutesReloadDirectory": return java.lang.String.class;
+        case "routesreloadenabled":
+        case "RoutesReloadEnabled": return boolean.class;
+        case "routesreloadpattern":
+        case "RoutesReloadPattern": return java.lang.String.class;
         case "shutdownloginflightexchangesontimeout":
         case "ShutdownLogInflightExchangesOnTimeout": return boolean.class;
         case "shutdownnowontimeout":
@@ -546,6 +558,12 @@ public class MainConfigurationPropertiesConfigurer extends org.apache.camel.supp
         case "RoutesExcludePattern": return target.getRoutesExcludePattern();
         case "routesincludepattern":
         case "RoutesIncludePattern": return target.getRoutesIncludePattern();
+        case "routesreloaddirectory":
+        case "RoutesReloadDirectory": return target.getRoutesReloadDirectory();
+        case "routesreloadenabled":
+        case "RoutesReloadEnabled": return target.isRoutesReloadEnabled();
+        case "routesreloadpattern":
+        case "RoutesReloadPattern": return target.getRoutesReloadPattern();
         case "shutdownloginflightexchangesontimeout":
         case "ShutdownLogInflightExchangesOnTimeout": return target.isShutdownLogInflightExchangesOnTimeout();
         case "shutdownnowontimeout":
diff --git a/core/camel-main/src/generated/resources/META-INF/camel-main-configuration-metadata.json b/core/camel-main/src/generated/resources/META-INF/camel-main-configuration-metadata.json
index 58c12b9..fc2c494 100644
--- a/core/camel-main/src/generated/resources/META-INF/camel-main-configuration-metadata.json
+++ b/core/camel-main/src/generated/resources/META-INF/camel-main-configuration-metadata.json
@@ -75,6 +75,9 @@
     { "name": "camel.main.routesCollectorEnabled", "description": "Whether the routes collector is enabled or not. When enabled Camel will auto-discover routes (RouteBuilder instances from the registry and also load additional routes from the file system). The routes collector is default enabled.", "sourceType": "org.apache.camel.main.DefaultConfigurationProperties", "type": "boolean", "javaType": "boolean", "defaultValue": true },
     { "name": "camel.main.routesExcludePattern", "description": "Used for exclusive filtering of routes from directories. The exclusive filtering takes precedence over inclusive filtering. The pattern is using Ant-path style pattern. Multiple patterns can be specified separated by comma, as example, to exclude all the routes from a directory whose name contains foo use: &#42;&#42;\/foo.", "sourceType": "org.apache.camel.main.DefaultConfigurationProperties", "type": "string", "javaType":  [...]
     { "name": "camel.main.routesIncludePattern", "description": "Used for inclusive filtering of routes from directories. The exclusive filtering takes precedence over inclusive filtering. The pattern is using Ant-path style pattern. Multiple patterns can be specified separated by comma, as example, to include all the routes from a directory whose name contains foo use: &#42;&#42;\/foo.", "sourceType": "org.apache.camel.main.DefaultConfigurationProperties", "type": "string", "javaType":  [...]
+    { "name": "camel.main.routesReloadDirectory", "description": "Directory to scan (incl subdirectories) for route changes. Camel cannot scan the classpath, so this must be configured to a file directory. Development with Maven as build tool, you can configure the directory to be src\/main\/resources to scan for Camel routes in XML or YAML files.", "sourceType": "org.apache.camel.main.DefaultConfigurationProperties", "type": "string", "javaType": "java.lang.String", "defaultValue": "src [...]
+    { "name": "camel.main.routesReloadEnabled", "description": "Used for enabling automatic routes reloading. If enabled then Camel will watch for file changes in the given reload directory, and trigger reloading routes if files are changed.", "sourceType": "org.apache.camel.main.DefaultConfigurationProperties", "type": "boolean", "javaType": "boolean", "defaultValue": "false" },
+    { "name": "camel.main.routesReloadPattern", "description": "Used for inclusive filtering of routes from directories. Typical used for specifying to accept routes in XML or YAML files. The default pattern is .yaml,.xml Multiple patterns can be specified separated by comma.", "sourceType": "org.apache.camel.main.DefaultConfigurationProperties", "type": "string", "javaType": "java.lang.String", "defaultValue": "*.xml,*.yaml" },
     { "name": "camel.main.shutdownLogInflightExchangesOnTimeout", "description": "Sets whether to log information about the inflight Exchanges which are still running during a shutdown which didn't complete without the given timeout. This requires to enable the option inflightRepositoryBrowseEnabled.", "sourceType": "org.apache.camel.main.DefaultConfigurationProperties", "type": "boolean", "javaType": "boolean", "defaultValue": true },
     { "name": "camel.main.shutdownNowOnTimeout", "description": "Sets whether to force shutdown of all consumers when a timeout occurred and thus not all consumers was shutdown within that period. You should have good reasons to set this option to false as it means that the routes keep running and is halted abruptly when CamelContext has been shutdown.", "sourceType": "org.apache.camel.main.DefaultConfigurationProperties", "type": "boolean", "javaType": "boolean", "defaultValue": true },
     { "name": "camel.main.shutdownRoutesInReverseOrder", "description": "Sets whether routes should be shutdown in reverse or the same order as they were started.", "sourceType": "org.apache.camel.main.DefaultConfigurationProperties", "type": "boolean", "javaType": "boolean", "defaultValue": true },
diff --git a/core/camel-main/src/main/docs/main.adoc b/core/camel-main/src/main/docs/main.adoc
index 32e17a9..47a0f8b 100644
--- a/core/camel-main/src/main/docs/main.adoc
+++ b/core/camel-main/src/main/docs/main.adoc
@@ -21,7 +21,7 @@ The following tables lists all the options:
 
 // main options: START
 === Camel Main configurations
-The camel.main supports 95 options, which are listed below.
+The camel.main supports 98 options, which are listed below.
 
 [width="100%",cols="2,5,^1,2",options="header"]
 |===
@@ -91,6 +91,9 @@ The camel.main supports 95 options, which are listed below.
 | *camel.main.routesCollector{zwsp}Enabled* | Whether the routes collector is enabled or not. When enabled Camel will auto-discover routes (RouteBuilder instances from the registry and also load additional routes from the file system). The routes collector is default enabled. | true | boolean
 | *camel.main.routesExclude{zwsp}Pattern* | Used for exclusive filtering of routes from directories. The exclusive filtering takes precedence over inclusive filtering. The pattern is using Ant-path style pattern. Multiple patterns can be specified separated by comma, as example, to exclude all the routes from a directory whose name contains foo use: &#42;&#42;/foo. |  | String
 | *camel.main.routesInclude{zwsp}Pattern* | Used for inclusive filtering of routes from directories. The exclusive filtering takes precedence over inclusive filtering. The pattern is using Ant-path style pattern. Multiple patterns can be specified separated by comma, as example, to include all the routes from a directory whose name contains foo use: &#42;&#42;/foo. | classpath:camel/*,classpath:camel-template/*,classpath:camel-rest/* | String
+| *camel.main.routesReload{zwsp}Directory* | Directory to scan (incl subdirectories) for route changes. Camel cannot scan the classpath, so this must be configured to a file directory. Development with Maven as build tool, you can configure the directory to be src/main/resources to scan for Camel routes in XML or YAML files. | src/main/resources | String
+| *camel.main.routesReloadEnabled* | Used for enabling automatic routes reloading. If enabled then Camel will watch for file changes in the given reload directory, and trigger reloading routes if files are changed. | false | boolean
+| *camel.main.routesReloadPattern* | Used for inclusive filtering of routes from directories. Typical used for specifying to accept routes in XML or YAML files. The default pattern is .yaml,.xml Multiple patterns can be specified separated by comma. | *.xml,*.yaml | String
 | *camel.main.shutdownLogInflight{zwsp}ExchangesOnTimeout* | Sets whether to log information about the inflight Exchanges which are still running during a shutdown which didn't complete without the given timeout. This requires to enable the option inflightRepositoryBrowseEnabled. | true | boolean
 | *camel.main.shutdownNowOn{zwsp}Timeout* | Sets whether to force shutdown of all consumers when a timeout occurred and thus not all consumers was shutdown within that period. You should have good reasons to set this option to false as it means that the routes keep running and is halted abruptly when CamelContext has been shutdown. | true | boolean
 | *camel.main.shutdownRoutesIn{zwsp}ReverseOrder* | Sets whether routes should be shutdown in reverse or the same order as they were started. | true | boolean
diff --git a/core/camel-main/src/main/java/org/apache/camel/main/DefaultConfigurationConfigurer.java b/core/camel-main/src/main/java/org/apache/camel/main/DefaultConfigurationConfigurer.java
index 23a366f..87482b9 100644
--- a/core/camel-main/src/main/java/org/apache/camel/main/DefaultConfigurationConfigurer.java
+++ b/core/camel-main/src/main/java/org/apache/camel/main/DefaultConfigurationConfigurer.java
@@ -75,6 +75,7 @@ import org.apache.camel.spi.UuidGenerator;
 import org.apache.camel.support.ClassicUuidGenerator;
 import org.apache.camel.support.DefaultUuidGenerator;
 import org.apache.camel.support.OffUuidGenerator;
+import org.apache.camel.support.RouteWatcherReloadStrategy;
 import org.apache.camel.support.ShortUuidGenerator;
 import org.apache.camel.support.SimpleUuidGenerator;
 import org.apache.camel.support.jsse.GlobalSSLContextParametersSupplier;
@@ -225,6 +226,11 @@ public final class DefaultConfigurationConfigurer {
         camelContext.setUseMDCLogging(config.isUseMdcLogging());
         camelContext.setMDCLoggingKeysPattern(config.getMdcLoggingKeysPattern());
         camelContext.setLoadTypeConverters(config.isLoadTypeConverters());
+        if (config.isRoutesReloadEnabled()) {
+            RouteWatcherReloadStrategy reloader = new RouteWatcherReloadStrategy(config.getRoutesReloadDirectory());
+            reloader.setPattern(config.getRoutesReloadPattern());
+            camelContext.addService(reloader);
+        }
 
         if (camelContext.getManagementStrategy().getManagementAgent() != null) {
             camelContext.getManagementStrategy().getManagementAgent()
diff --git a/core/camel-main/src/main/java/org/apache/camel/main/DefaultConfigurationProperties.java b/core/camel-main/src/main/java/org/apache/camel/main/DefaultConfigurationProperties.java
index 2caa4ad..6fba9de 100644
--- a/core/camel-main/src/main/java/org/apache/camel/main/DefaultConfigurationProperties.java
+++ b/core/camel-main/src/main/java/org/apache/camel/main/DefaultConfigurationProperties.java
@@ -93,6 +93,11 @@ public abstract class DefaultConfigurationProperties<T> {
     private String javaRoutesExcludePattern;
     private String routesIncludePattern = "classpath:camel/*,classpath:camel-template/*,classpath:camel-rest/*";
     private String routesExcludePattern;
+    private boolean routesReloadEnabled;
+    @Metadata(defaultValue = "*.xml,*.yaml")
+    private String routesReloadPattern = "*.xml,*.yaml";
+    @Metadata(defaultValue = "src/main/resources")
+    private String routesReloadDirectory = "src/main/resources";
     private boolean lightweight;
     private boolean eagerClassloading;
     @Metadata(defaultValue = "default", enums = "default,prototype,pooled")
@@ -953,6 +958,46 @@ public abstract class DefaultConfigurationProperties<T> {
         this.routesExcludePattern = routesExcludePattern;
     }
 
+    public boolean isRoutesReloadEnabled() {
+        return routesReloadEnabled;
+    }
+
+    /**
+     * Used for enabling automatic routes reloading. If enabled then Camel will watch for file changes in the given
+     * reload directory, and trigger reloading routes if files are changed.
+     */
+    public void setRoutesReloadEnabled(boolean routesReloadEnabled) {
+        this.routesReloadEnabled = routesReloadEnabled;
+    }
+
+    public String getRoutesReloadDirectory() {
+        return routesReloadDirectory;
+    }
+
+    /**
+     * Directory to scan (incl subdirectories) for route changes. Camel cannot scan the classpath, so this must be
+     * configured to a file directory. Development with Maven as build tool, you can configure the directory to be
+     * src/main/resources to scan for Camel routes in XML or YAML files.
+     */
+    public void setRoutesReloadDirectory(String routesReloadDirectory) {
+        this.routesReloadDirectory = routesReloadDirectory;
+    }
+
+    public String getRoutesReloadPattern() {
+        return routesReloadPattern;
+    }
+
+    /**
+     * Used for inclusive filtering of routes from directories.
+     *
+     * Typical used for specifying to accept routes in XML or YAML files. The default pattern is <tt>*.yaml,*.xml</tt>
+     *
+     * Multiple patterns can be specified separated by comma.
+     */
+    public void setRoutesReloadPattern(String routesReloadPattern) {
+        this.routesReloadPattern = routesReloadPattern;
+    }
+
     public boolean isLightweight() {
         return lightweight;
     }
@@ -1899,6 +1944,21 @@ public abstract class DefaultConfigurationProperties<T> {
         return (T) this;
     }
 
+    public T withRoutesReloadEnabled(boolean routesReloadEnabled) {
+        this.routesReloadEnabled = routesReloadEnabled;
+        return (T) this;
+    }
+
+    public T withRoutesReloadDirectory(String routesReloadDirectory) {
+        this.routesReloadDirectory = routesReloadDirectory;
+        return (T) this;
+    }
+
+    public T withRoutesReloadPattern(String routesReloadPattern) {
+        this.routesReloadPattern = routesReloadPattern;
+        return (T) this;
+    }
+
     /**
      * Configure the context to be lightweight. This will trigger some optimizations and memory reduction options.
      * <p/>
diff --git a/core/camel-support/src/main/java/org/apache/camel/support/FileWatcherResourceReloadStrategy.java b/core/camel-support/src/main/java/org/apache/camel/support/FileWatcherResourceReloadStrategy.java
new file mode 100644
index 0000000..26f092d
--- /dev/null
+++ b/core/camel-support/src/main/java/org/apache/camel/support/FileWatcherResourceReloadStrategy.java
@@ -0,0 +1,301 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.support;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.io.IOException;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.WatchEvent;
+import java.nio.file.WatchKey;
+import java.nio.file.WatchService;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.camel.ExtendedCamelContext;
+import org.apache.camel.api.management.ManagedAttribute;
+import org.apache.camel.api.management.ManagedResource;
+import org.apache.camel.spi.Resource;
+import org.apache.camel.util.IOHelper;
+import org.apache.camel.util.ObjectHelper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
+import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
+
+/**
+ * A file based {@link org.apache.camel.spi.ResourceReloadStrategy} which watches a file folder for modified files and
+ * reload on file changes.
+ * <p/>
+ * This implementation uses the JDK {@link WatchService} to watch for when files are created or modified. Mac OS X users
+ * should be noted the osx JDK does not support native file system changes and therefore the watch service is much
+ * slower than on Linux or Windows systems.
+ */
+@ManagedResource(description = "Managed FileWatcherResourceReloadStrategy")
+public class FileWatcherResourceReloadStrategy extends ResourceReloadStrategySupport {
+
+    private static final Logger LOG = LoggerFactory.getLogger(FileWatcherResourceReloadStrategy.class);
+
+    private String folder;
+    private boolean isRecursive;
+    private WatchService watcher;
+    private ExecutorService executorService;
+    private WatchFileChangesTask task;
+    private Map<WatchKey, Path> folderKeys;
+    private long pollTimeout = 2000;
+    private FileFilter fileFilter;
+
+    public FileWatcherResourceReloadStrategy() {
+        setRecursive(false);
+    }
+
+    public FileWatcherResourceReloadStrategy(String directory) {
+        setFolder(directory);
+        setRecursive(false);
+    }
+
+    public FileWatcherResourceReloadStrategy(String directory, boolean isRecursive) {
+        setFolder(directory);
+        setRecursive(isRecursive);
+    }
+
+    public void setFolder(String folder) {
+        this.folder = folder;
+    }
+
+    public void setRecursive(boolean isRecursive) {
+        this.isRecursive = isRecursive;
+    }
+
+    /**
+     * Sets the poll timeout in millis. The default value is 2000.
+     */
+    public void setPollTimeout(long pollTimeout) {
+        this.pollTimeout = pollTimeout;
+    }
+
+    @ManagedAttribute(description = "Folder being watched")
+    public String getFolder() {
+        return folder;
+    }
+
+    @ManagedAttribute(description = "Whether the reload strategy watches directory recursively")
+    public boolean isRecursive() {
+        return isRecursive;
+    }
+
+    @ManagedAttribute(description = "Whether the watcher is running")
+    public boolean isRunning() {
+        return task != null && task.isRunning();
+    }
+
+    public FileFilter getFileFilter() {
+        return fileFilter;
+    }
+
+    /**
+     * To use a custom filter for accepting files.
+     */
+    public void setFileFilter(FileFilter fileFilter) {
+        this.fileFilter = fileFilter;
+    }
+
+    @Override
+    protected void doStart() throws Exception {
+        super.doStart();
+
+        if (folder == null) {
+            // no folder configured
+            return;
+        }
+
+        File dir = new File(folder);
+        if (dir.exists() && dir.isDirectory()) {
+            LOG.info("Starting ReloadStrategy to watch directory: {}", dir);
+
+            WatchEvent.Modifier modifier = null;
+
+            // if its mac OSX then attempt to apply workaround or warn its slower
+            String os = ObjectHelper.getSystemProperty("os.name", "");
+            if (os.toLowerCase(Locale.US).startsWith("mac")) {
+                // this modifier can speedup the scanner on mac osx (as java on mac has no native file notification integration)
+                Class<WatchEvent.Modifier> clazz = getCamelContext().getClassResolver()
+                        .resolveClass("com.sun.nio.file.SensitivityWatchEventModifier", WatchEvent.Modifier.class);
+                if (clazz != null) {
+                    WatchEvent.Modifier[] modifiers = clazz.getEnumConstants();
+                    for (WatchEvent.Modifier mod : modifiers) {
+                        if ("HIGH".equals(mod.name())) {
+                            modifier = mod;
+                            break;
+                        }
+                    }
+                }
+                if (modifier != null) {
+                    LOG.info(
+                            "On Mac OS X the JDK WatchService is slow by default so enabling SensitivityWatchEventModifier.HIGH as workaround");
+                } else {
+                    LOG.warn(
+                            "On Mac OS X the JDK WatchService is slow and it may take up till 10 seconds to notice file changes");
+                }
+            }
+
+            try {
+                Path path = dir.toPath();
+                watcher = path.getFileSystem().newWatchService();
+                // we cannot support deleting files as we don't know which routes that would be
+                if (isRecursive) {
+                    this.folderKeys = new HashMap<>();
+                    registerRecursive(watcher, path, modifier);
+                } else {
+                    registerPathToWatcher(modifier, path, watcher);
+                }
+
+                task = new WatchFileChangesTask(watcher, path);
+
+                executorService = getCamelContext().getExecutorServiceManager().newSingleThreadExecutor(this,
+                        "FileWatcherReloadStrategy");
+                executorService.submit(task);
+            } catch (IOException e) {
+                throw ObjectHelper.wrapRuntimeCamelException(e);
+            }
+        }
+    }
+
+    private WatchKey registerPathToWatcher(WatchEvent.Modifier modifier, Path path, WatchService watcher) throws IOException {
+        WatchKey key;
+        if (modifier != null) {
+            key = path.register(watcher, new WatchEvent.Kind<?>[] { ENTRY_CREATE, ENTRY_MODIFY }, modifier);
+        } else {
+            key = path.register(watcher, ENTRY_CREATE, ENTRY_MODIFY);
+        }
+        return key;
+    }
+
+    private void registerRecursive(final WatchService watcher, final Path root, final WatchEvent.Modifier modifier)
+            throws IOException {
+        Files.walkFileTree(root, new SimpleFileVisitor<Path>() {
+            @Override
+            public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
+                WatchKey key = registerPathToWatcher(modifier, dir, watcher);
+                folderKeys.put(key, dir);
+                return FileVisitResult.CONTINUE;
+            }
+        });
+    }
+
+    @Override
+    protected void doStop() throws Exception {
+        super.doStop();
+
+        if (executorService != null) {
+            getCamelContext().getExecutorServiceManager().shutdownGraceful(executorService);
+            executorService = null;
+        }
+
+        if (watcher != null) {
+            IOHelper.close(watcher);
+        }
+    }
+
+    /**
+     * Background task which watches for file changes
+     */
+    protected class WatchFileChangesTask implements Runnable {
+
+        private final WatchService watcher;
+        private final Path folder;
+        private volatile boolean running;
+
+        public WatchFileChangesTask(WatchService watcher, Path folder) {
+            this.watcher = watcher;
+            this.folder = folder;
+        }
+
+        public boolean isRunning() {
+            return running;
+        }
+
+        public void run() {
+            LOG.debug("FileReloadStrategy is starting watching folder: {}", folder);
+
+            // allow running while starting Camel
+            while (isStarting() || isRunAllowed()) {
+                running = true;
+
+                WatchKey key;
+                try {
+                    LOG.trace("FileReloadStrategy is polling for file changes in directory: {}", folder);
+                    // wait for a key to be available
+                    key = watcher.poll(pollTimeout, TimeUnit.MILLISECONDS);
+                } catch (InterruptedException ex) {
+                    break;
+                }
+
+                if (key != null) {
+                    Path pathToReload;
+                    if (isRecursive) {
+                        pathToReload = folderKeys.get(key);
+                    } else {
+                        pathToReload = folder;
+                    }
+
+                    for (WatchEvent<?> event : key.pollEvents()) {
+                        WatchEvent<Path> we = (WatchEvent<Path>) event;
+                        Path path = we.context();
+                        File file = pathToReload.resolve(path).toAbsolutePath().toFile();
+                        boolean accept = fileFilter == null || fileFilter.accept(file);
+                        if (accept) {
+                            String name = file.getAbsolutePath();
+                            LOG.trace("Modified/Created file: {}", name);
+                            try {
+                                ExtendedCamelContext ecc = getCamelContext().adapt(ExtendedCamelContext.class);
+                                // must use file resource loader as we cannot load from classpath
+                                Resource resource = ecc.getResourceLoader().resolveResource("file:" + name);
+                                getResourceReload().onReload(name, resource);
+                                succeeded++;
+                            } catch (Exception e) {
+                                failed++;
+                                LOG.warn("Error reloading routes from file: " + name + " due " + e.getMessage()
+                                         + ". This exception is ignored.",
+                                        e);
+                            }
+                        }
+                    }
+
+                    // the key must be reset after processed
+                    boolean valid = key.reset();
+                    if (!valid) {
+                        break;
+                    }
+                }
+            }
+
+            running = false;
+
+            LOG.info("FileReloadStrategy is stopping watching folder: {}", folder);
+        }
+    }
+
+}
diff --git a/core/camel-support/src/main/java/org/apache/camel/support/ResourceReloadStrategySupport.java b/core/camel-support/src/main/java/org/apache/camel/support/ResourceReloadStrategySupport.java
new file mode 100644
index 0000000..7cf27b7
--- /dev/null
+++ b/core/camel-support/src/main/java/org/apache/camel/support/ResourceReloadStrategySupport.java
@@ -0,0 +1,90 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.support;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.api.management.ManagedAttribute;
+import org.apache.camel.api.management.ManagedOperation;
+import org.apache.camel.spi.ResourceReload;
+import org.apache.camel.spi.ResourceReloadStrategy;
+import org.apache.camel.support.service.ServiceSupport;
+
+/**
+ * Base class for implementing custom {@link ResourceReloadStrategy} SPI plugins.
+ */
+public abstract class ResourceReloadStrategySupport extends ServiceSupport implements ResourceReloadStrategy {
+
+    private ResourceReload resourceReload;
+    private CamelContext camelContext;
+    protected int succeeded;
+    protected int failed;
+
+    @Override
+    public CamelContext getCamelContext() {
+        return camelContext;
+    }
+
+    @Override
+    public void setCamelContext(CamelContext camelContext) {
+        this.camelContext = camelContext;
+    }
+
+    @Override
+    public ResourceReload getResourceReload() {
+        return resourceReload;
+    }
+
+    @Override
+    public void setResourceReload(ResourceReload resourceReload) {
+        this.resourceReload = resourceReload;
+    }
+
+    @ManagedAttribute(description = "Number of reloads succeeded")
+    public int getReloadCounter() {
+        return succeeded;
+    }
+
+    @ManagedAttribute(description = "Number of reloads failed")
+    public int getFailedCounter() {
+        return failed;
+    }
+
+    public void setSucceeded(int succeeded) {
+        this.succeeded = succeeded;
+    }
+
+    public void setFailed(int failed) {
+        this.failed = failed;
+    }
+
+    @ManagedOperation(description = "Reset counters")
+    public void resetCounters() {
+        succeeded = 0;
+        failed = 0;
+    }
+
+    @Override
+    protected void doStart() throws Exception {
+        // noop
+    }
+
+    @Override
+    protected void doStop() throws Exception {
+        // noop
+    }
+
+}
diff --git a/core/camel-support/src/main/java/org/apache/camel/support/RouteWatcherReloadStrategy.java b/core/camel-support/src/main/java/org/apache/camel/support/RouteWatcherReloadStrategy.java
new file mode 100644
index 0000000..15cb62e
--- /dev/null
+++ b/core/camel-support/src/main/java/org/apache/camel/support/RouteWatcherReloadStrategy.java
@@ -0,0 +1,96 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.support;
+
+import java.io.File;
+
+import org.apache.camel.ExtendedCamelContext;
+import org.apache.camel.RuntimeCamelException;
+import org.apache.camel.util.AntPathMatcher;
+import org.apache.camel.util.FileUtil;
+import org.apache.camel.util.ObjectHelper;
+
+/**
+ * Watcher strategy for triggering reloading of Camel routes in a running Camel application. The strategy watches a
+ * directory (and subdirectories) for file changes. By default, the strategy is matching Camel routes in XML or YAML
+ * files.
+ */
+public class RouteWatcherReloadStrategy extends FileWatcherResourceReloadStrategy {
+
+    private String pattern = "*.yaml,*.xml";
+
+    public RouteWatcherReloadStrategy(String directory) {
+        super(directory, true);
+    }
+
+    public String getPattern() {
+        return pattern;
+    }
+
+    /**
+     * Used for inclusive filtering of routes from directories.
+     *
+     * Typical used for specifying to accept routes in XML or YAML files. The default pattern is <tt>*.yaml,*.xml</tt>
+     *
+     * Multiple patterns can be specified separated by comma.
+     */
+    public void setPattern(String pattern) {
+        this.pattern = pattern;
+    }
+
+    @Override
+    protected void doStart() throws Exception {
+        ObjectHelper.notNull(getFolder(), "folder", this);
+
+        final String base = new File(getFolder()).getAbsolutePath();
+        final AntPathMatcher matcher = new AntPathMatcher();
+
+        if (getFileFilter() == null) {
+            // file matcher that matches via the ant path matcher
+            final String[] parts = pattern.split(",");
+            setFileFilter(f -> {
+                for (String part : parts) {
+                    // strip starting directory so we have a relative name to the starting folder
+                    String path = f.getAbsolutePath();
+                    if (path.startsWith(base)) {
+                        path = path.substring(base.length());
+                    }
+                    path = FileUtil.stripLeadingSeparator(path);
+
+                    if (matcher.match(part, path, false)) {
+                        return true;
+                    }
+                }
+                return false;
+            });
+        }
+
+        if (getResourceReload() == null) {
+            // attach listener that triggers the route update
+            setResourceReload((name, resource) -> {
+                try {
+                    getCamelContext().adapt(ExtendedCamelContext.class).getRoutesLoader().updateRoutes(resource);
+                } catch (Exception e) {
+                    throw RuntimeCamelException.wrapRuntimeException(e);
+                }
+            });
+        }
+
+        super.doStart();
+    }
+
+}

[camel] 02/02: CAMEL-16656: camel-core - ResourceReloader SPI - Allow to reload on changes

Posted by da...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 67d2afd0c6485d8e0bc1a25fe21b64f7343f9111
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Thu Nov 18 09:20:18 2021 +0100

    CAMEL-16656: camel-core - ResourceReloader SPI - Allow to reload on changes
---
 .../camel/support/RouteWatcherReloadStrategy.java  |   3 +
 dsl/camel-xml-io-dsl/pom.xml                       |   5 +
 .../org/apache/camel/dsl/xml/io/XmlMainTest.java   |   2 +-
 .../io/reload/RouteWatcherReloadStrategyTest.java  | 205 +++++++++++++++++++++
 .../resources/org/apache/camel/reload/barRoute.xml |  29 +++
 .../org/apache/camel/reload/barUpdatedRoute.xml    |  34 ++++
 6 files changed, 277 insertions(+), 1 deletion(-)

diff --git a/core/camel-support/src/main/java/org/apache/camel/support/RouteWatcherReloadStrategy.java b/core/camel-support/src/main/java/org/apache/camel/support/RouteWatcherReloadStrategy.java
index 15cb62e..f8a0748 100644
--- a/core/camel-support/src/main/java/org/apache/camel/support/RouteWatcherReloadStrategy.java
+++ b/core/camel-support/src/main/java/org/apache/camel/support/RouteWatcherReloadStrategy.java
@@ -33,6 +33,9 @@ public class RouteWatcherReloadStrategy extends FileWatcherResourceReloadStrateg
 
     private String pattern = "*.yaml,*.xml";
 
+    public RouteWatcherReloadStrategy() {
+    }
+
     public RouteWatcherReloadStrategy(String directory) {
         super(directory, true);
     }
diff --git a/dsl/camel-xml-io-dsl/pom.xml b/dsl/camel-xml-io-dsl/pom.xml
index 187f5ec..896f4c0 100644
--- a/dsl/camel-xml-io-dsl/pom.xml
+++ b/dsl/camel-xml-io-dsl/pom.xml
@@ -123,6 +123,11 @@
             <artifactId>camel-test-junit5</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.awaitility</groupId>
+            <artifactId>awaitility</artifactId>
+            <scope>test</scope>
+        </dependency>
 
         <dependency>
             <groupId>org.apache.logging.log4j</groupId>
diff --git a/dsl/camel-xml-io-dsl/src/test/java/org/apache/camel/dsl/xml/io/XmlMainTest.java b/dsl/camel-xml-io-dsl/src/test/java/org/apache/camel/dsl/xml/io/XmlMainTest.java
index 60ebf97..67715ed 100644
--- a/dsl/camel-xml-io-dsl/src/test/java/org/apache/camel/dsl/xml/io/XmlMainTest.java
+++ b/dsl/camel-xml-io-dsl/src/test/java/org/apache/camel/dsl/xml/io/XmlMainTest.java
@@ -78,7 +78,7 @@ public class XmlMainTest {
     public void testMainRoutesCollectorScanWildcardDirFilePath() throws Exception {
         doTestMain(
                 "file:src/test/resources/**/*.xml",
-                "**/org/apache/camel/dsl/**,**/camel-rests.xml,**/camel-template.xml");
+                "**/org/apache/camel/dsl/**,**/camel-rests.xml,**/camel-template.xml,**/reload/*");
     }
 
     @Test
diff --git a/dsl/camel-xml-io-dsl/src/test/java/org/apache/camel/dsl/xml/io/reload/RouteWatcherReloadStrategyTest.java b/dsl/camel-xml-io-dsl/src/test/java/org/apache/camel/dsl/xml/io/reload/RouteWatcherReloadStrategyTest.java
new file mode 100644
index 0000000..58b4f5f
--- /dev/null
+++ b/dsl/camel-xml-io-dsl/src/test/java/org/apache/camel/dsl/xml/io/reload/RouteWatcherReloadStrategyTest.java
@@ -0,0 +1,205 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.xml.io.reload;
+
+import java.io.File;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.spi.CamelEvent;
+import org.apache.camel.support.EventNotifierSupport;
+import org.apache.camel.support.RouteWatcherReloadStrategy;
+import org.apache.camel.test.junit5.CamelTestSupport;
+import org.apache.camel.util.FileUtil;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static org.apache.camel.test.junit5.TestSupport.createDirectory;
+import static org.apache.camel.test.junit5.TestSupport.deleteDirectory;
+import static org.awaitility.Awaitility.await;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class RouteWatcherReloadStrategyTest extends CamelTestSupport {
+
+    private static final Logger LOG = LoggerFactory.getLogger(RouteWatcherReloadStrategyTest.class);
+
+    private RouteWatcherReloadStrategy reloadStrategy;
+
+    @Override
+    public boolean isUseRouteBuilder() {
+        return false;
+    }
+
+    @Override
+    protected CamelContext createCamelContext() throws Exception {
+        CamelContext context = super.createCamelContext();
+        reloadStrategy = new RouteWatcherReloadStrategy();
+        reloadStrategy.setFolder("target/dummy");
+        reloadStrategy.setPattern("*.xml");
+        // to make unit test faster
+        reloadStrategy.setPollTimeout(100);
+        context.addService(reloadStrategy);
+        return context;
+    }
+
+    @Test
+    public void testAddNewRoute() throws Exception {
+        deleteDirectory("target/dummy");
+        createDirectory("target/dummy");
+
+        context.start();
+
+        // there are 0 routes to begin with
+        assertEquals(0, context.getRoutes().size());
+
+        LOG.info("Copying file to target/dummy");
+
+        // create an xml file with some routes
+        FileUtil.copyFile(new File("src/test/resources/org/apache/camel/reload/barRoute.xml"),
+                new File("target/dummy/barRoute.xml"));
+
+        // wait for that file to be processed
+        // (is slow on osx, so wait up till 20 seconds)
+        await().atMost(20, TimeUnit.SECONDS).untilAsserted(() -> assertEquals(1, context.getRoutes().size()));
+
+        // and the route should work
+        getMockEndpoint("mock:bar").expectedMessageCount(1);
+        template.sendBody("direct:bar", "Hello World");
+        assertMockEndpointsSatisfied();
+    }
+
+    @Test
+    public void testUpdateExistingRoute() throws Exception {
+        deleteDirectory("target/dummy");
+        createDirectory("target/dummy");
+
+        // the bar route is added two times, at first, and then when updated
+        final CountDownLatch latch = new CountDownLatch(2);
+        context.getManagementStrategy().addEventNotifier(new EventNotifierSupport() {
+            @Override
+            public void notify(CamelEvent event) throws Exception {
+                latch.countDown();
+            }
+
+            @Override
+            public boolean isEnabled(CamelEvent event) {
+                return event instanceof CamelEvent.RouteAddedEvent;
+            }
+        });
+
+        context.addRoutes(new RouteBuilder() {
+            @Override
+            public void configure() throws Exception {
+                from("direct:bar").routeId("bar").to("mock:foo");
+            }
+        });
+
+        context.start();
+
+        assertEquals(1, context.getRoutes().size());
+
+        // and the route should work sending to mock:foo
+        getMockEndpoint("mock:bar").expectedMessageCount(0);
+        getMockEndpoint("mock:foo").expectedMessageCount(1);
+        template.sendBody("direct:bar", "Hello World");
+        assertMockEndpointsSatisfied();
+
+        resetMocks();
+
+        LOG.info("Copying file to target/dummy");
+
+        // create an xml file with some routes
+        FileUtil.copyFile(new File("src/test/resources/org/apache/camel/reload/barRoute.xml"),
+                new File("target/dummy/barRoute.xml"));
+
+        // wait for that file to be processed and remove/add the route
+        // (is slow on osx, so wait up till 20 seconds)
+        boolean done = latch.await(20, TimeUnit.SECONDS);
+        assertTrue(done, "Should reload file within 20 seconds");
+
+        // and the route should be changed to route to mock:bar instead of mock:foo
+        Thread.sleep(500);
+        getMockEndpoint("mock:bar").expectedMessageCount(1);
+        getMockEndpoint("mock:foo").expectedMessageCount(0);
+        template.sendBody("direct:bar", "Bye World");
+        assertMockEndpointsSatisfied();
+    }
+
+    @Test
+    public void testUpdateXmlRoute() throws Exception {
+        deleteDirectory("target/dummy");
+        createDirectory("target/dummy");
+
+        // the bar route is added two times, at first, and then when updated
+        final CountDownLatch latch = new CountDownLatch(2);
+        context.getManagementStrategy().addEventNotifier(new EventNotifierSupport() {
+            @Override
+            public void notify(CamelEvent event) throws Exception {
+                latch.countDown();
+            }
+
+            @Override
+            public boolean isEnabled(CamelEvent event) {
+                return event instanceof CamelEvent.RouteAddedEvent;
+            }
+        });
+
+        context.start();
+
+        // there are 0 routes to begin with
+        assertEquals(0, context.getRoutes().size());
+
+        LOG.info("Copying file to target/dummy");
+
+        // create an xml file with some routes
+        FileUtil.copyFile(new File("src/test/resources/org/apache/camel/reload/barRoute.xml"),
+                new File("target/dummy/barRoute.xml"));
+
+        // wait for that file to be processed
+        // (is slow on osx, so wait up till 20 seconds)
+        await().atMost(20, TimeUnit.SECONDS).untilAsserted(() -> assertEquals(1, context.getRoutes().size()));
+
+        // and the route should work
+        getMockEndpoint("mock:bar").expectedMessageCount(1);
+        template.sendBody("direct:bar", "Hello World");
+        assertMockEndpointsSatisfied();
+
+        resetMocks();
+
+        // now update the file
+        LOG.info("Updating file in target/dummy");
+
+        // create an xml file with some routes
+        FileUtil.copyFile(new File("src/test/resources/org/apache/camel/reload/barUpdatedRoute.xml"),
+                new File("target/dummy/barRoute.xml"));
+
+        // wait for that file to be processed and remove/add the route
+        // (is slow on osx, so wait up till 20 seconds)
+        boolean done = latch.await(20, TimeUnit.SECONDS);
+        assertTrue(done, "Should reload file within 20 seconds");
+
+        // and the route should work with the update
+        Thread.sleep(500);
+        getMockEndpoint("mock:bar").expectedBodiesReceived("Bye Camel");
+        template.sendBody("direct:bar", "Camel");
+        assertMockEndpointsSatisfied();
+    }
+}
diff --git a/dsl/camel-xml-io-dsl/src/test/resources/org/apache/camel/reload/barRoute.xml b/dsl/camel-xml-io-dsl/src/test/resources/org/apache/camel/reload/barRoute.xml
new file mode 100644
index 0000000..e3b5136
--- /dev/null
+++ b/dsl/camel-xml-io-dsl/src/test/resources/org/apache/camel/reload/barRoute.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<routes xmlns="http://camel.apache.org/schema/spring">
+    <!-- here we define the bar route -->
+    <route id="bar">
+        <from uri="direct:bar"/>
+        <to uri="mock:bar"/>
+    </route>
+
+    <!-- we could add more routes if we like,
+         but in this example we stick to one route only -->
+</routes>
diff --git a/dsl/camel-xml-io-dsl/src/test/resources/org/apache/camel/reload/barUpdatedRoute.xml b/dsl/camel-xml-io-dsl/src/test/resources/org/apache/camel/reload/barUpdatedRoute.xml
new file mode 100644
index 0000000..e7ce805
--- /dev/null
+++ b/dsl/camel-xml-io-dsl/src/test/resources/org/apache/camel/reload/barUpdatedRoute.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<!-- START SNIPPET: e1 -->
+<routes xmlns="http://camel.apache.org/schema/spring">
+    <!-- here we define the bar route -->
+    <route id="bar">
+        <from uri="direct:bar"/>
+        <transform>
+          <simple>Bye ${body}</simple>
+        </transform>
+        <to uri="mock:bar"/>
+    </route>
+
+    <!-- we could add more routes if we like,
+         but in this example we stick to one route only -->
+</routes>
+<!-- END SNIPPET: e1 -->
\ No newline at end of file