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


The following commit(s) were added to refs/heads/main by this push:
     new cd2ce899764e CAMEL-25670: camel-jbang - camel run --data-dir for data 
folders that dev mode does not watch (#27847)
cd2ce899764e is described below

commit cd2ce899764e3c482bdebfc1695e971afd2a962c
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Oct 15 00:08:03 2026 +0200

    CAMEL-25670: camel-jbang - camel run --data-dir for data folders that dev 
mode does not watch (#27847)
    
    * CAMEL-25670: camel-jbang - camel run --data-dir for data folders that dev 
mode does not watch
    
    With --source-dir, dev mode watches every file under the source directory. 
So routes that write data files into a
    folder under it, such as an inbox or an archive, reloaded themselves on 
every scan, endlessly, and processed the same
    files again on each reload.
    
    - camel-support: a part of the reload pattern that starts with ! excludes 
what it matches (*,!inbox/**), and a folder
      it matches as a whole is not scanned. When five reloads in a row within 
15 s were each for files under the same
      folder, and none of those files is a route source or a properties, Java 
or Groovy file, a WARN names the folder and
      how to leave it out.
    - camel run --data-dir, or camel.jbang.dataDir in the 
application.properties of the source directory, adds those
      excludes.
    - The three built-in examples that write into their own folder declare 
their data folders: file-processing,
      error-handling and csv-to-json.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
    
    * CAMEL-25670: warn when --data-dir is given without a source directory, 
compare route sources by full path, share the path matcher
    
    Review follow-up.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
    
    * CAMEL-25670: clearer warning for --data-dir without --source-dir, and the 
javadoc back on its method
    
    Review follow-up.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
    
    ---------
    
    Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
 .../jbang/camel-jbang-configuration-metadata.json  |   1 +
 .../support/RouteWatcherReloadDataDirTest.java     | 193 ++++++++++++++++++++
 .../support/FileWatcherResourceReloadStrategy.java |  13 ++
 .../camel/support/RouteWatcherReloadStrategy.java  | 197 +++++++++++++++++++--
 .../ROOT/pages/camel-jbang-configuration.adoc      |   3 +-
 .../modules/ROOT/pages/camel-jbang-running.adoc    |  22 +++
 .../pages/jbang-commands/camel-jbang-debug.adoc    |   1 +
 .../ROOT/pages/jbang-commands/camel-jbang-dev.adoc |   1 +
 .../ROOT/pages/jbang-commands/camel-jbang-run.adoc |   1 +
 .../META-INF/camel-jbang-commands-metadata.json    |   6 +-
 .../camel-jbang-configuration-metadata.json        |   1 +
 .../apache/camel/dsl/jbang/core/commands/Run.java  |  54 +++++-
 .../dsl/jbang/core/common/CamelJBangConstants.java |   6 +
 .../connect/file-processing/application.properties |   4 +
 .../error-handling/application.properties          |   4 +
 .../transform/csv-to-json/application.properties   |   4 +
 .../camel/dsl/jbang/core/commands/RunTest.java     |  17 ++
 17 files changed, 503 insertions(+), 25 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/jbang/camel-jbang-configuration-metadata.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/jbang/camel-jbang-configuration-metadata.json
index e61e0029c60a..6ba62ce930ac 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/jbang/camel-jbang-configuration-metadata.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/jbang/camel-jbang-configuration-metadata.json
@@ -9,6 +9,7 @@
     { "name": "camel.jbang.classpathFiles", "required": false, "description": 
"Additional files to add to classpath (Use commas to separate multiple 
files).", "type": "string", "javaType": "String", "secret": false },
     { "name": "camel.jbang.compileWorkDir", "required": false, "description": 
"Work directory for compiler. Can be used to write compiled classes or other 
resources.", "label": "advanced", "type": "string", "javaType": "String", 
"defaultValue": ".camel-jbang\/compile", "secret": false },
     { "name": "camel.jbang.console", "required": false, "description": 
"Developer console at \/q\/dev on local HTTP server (port 8080 by default)", 
"type": "boolean", "javaType": "boolean", "defaultValue": false, "secret": 
false },
+    { "name": "camel.jbang.dataDir", "required": false, "description": 
"Folders under the source directory (comma separated) that hold data the routes 
read or write, not sources: in dev mode a change in them does not reload the 
routes. Use this for an inbox, outbox or archive folder under the source 
directory.", "label": "advanced", "type": "string", "javaType": "String", 
"secret": false },
     { "name": "camel.jbang.dependencies", "required": false, "description": 
"Additional dependencies (Use commas to separate multiple dependencies).", 
"type": "string", "javaType": "String", "secret": false },
     { "name": "camel.jbang.dependencies.main", "required": false, 
"description": "Additional dependencies for Camel Main runtime only", "type": 
"string", "javaType": "String", "secret": false },
     { "name": "camel.jbang.dependencies.quarkus", "required": false, 
"description": "Additional dependencies for Quarkus runtime only", "type": 
"string", "javaType": "String", "secret": false },
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/support/RouteWatcherReloadDataDirTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/support/RouteWatcherReloadDataDirTest.java
new file mode 100644
index 000000000000..16a9e8ad51cd
--- /dev/null
+++ 
b/core/camel-core/src/test/java/org/apache/camel/support/RouteWatcherReloadDataDirTest.java
@@ -0,0 +1,193 @@
+/*
+ * 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.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.attribute.BasicFileAttributes;
+import java.util.List;
+
+import org.apache.camel.ContextTestSupport;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * CAMEL-25670: dev mode with a source directory watches every file under it, 
so routes that write data files into a
+ * folder under it reloaded themselves, again and again. A pattern part that 
starts with ! leaves files out, and a
+ * folder it matches as a whole is not scanned; repeated reloads for files 
under one folder log a warning that names it.
+ */
+public class RouteWatcherReloadDataDirTest extends ContextTestSupport {
+
+    private Path dir;
+    private RouteWatcherReloadStrategy strategy;
+
+    @BeforeEach
+    void createFolder() throws Exception {
+        dir = Files.createTempDirectory("camel-data-dir");
+        Files.writeString(dir.resolve("shop.camel.yaml"), "one");
+        Files.createDirectories(dir.resolve("inbox/done"));
+        Files.writeString(dir.resolve("inbox/order-1.json"), "{}");
+        Files.writeString(dir.resolve("inbox/done/order-0.json"), "{}");
+    }
+
+    @AfterEach
+    void removeFolder() throws Exception {
+        if (strategy != null) {
+            strategy.stop();
+        }
+        deleteRecursively(dir);
+    }
+
+    private void start(String pattern) {
+        strategy = new RouteWatcherReloadStrategy(dir.toString(), true);
+        strategy.setCamelContext(context);
+        strategy.setPattern(pattern);
+        // the test scans itself, at once
+        strategy.setScheduler(false);
+        strategy.setStableTimeout(0);
+        strategy.start();
+    }
+
+    private List<String> scanNames() {
+        return strategy.scan().stream().map(File::getName).sorted().toList();
+    }
+
+    @Test
+    public void testEveryFileIsWatchedWithoutAnExclude() throws Exception {
+        start("*");
+
+        assertThat(scanNames()).containsExactly("order-0.json", 
"order-1.json", "shop.camel.yaml");
+        // the routes write an order into the inbox: a change, which reloads 
the routes
+        Files.writeString(dir.resolve("inbox/order-2.json"), "{}");
+        assertThat(scanNames()).containsExactly("order-2.json");
+    }
+
+    @Test
+    public void testAnExcludedFolderIsNotWatched() throws Exception {
+        start("*,!inbox/**");
+
+        assertThat(scanNames()).containsExactly("shop.camel.yaml");
+        
assertThat(strategy.isExcludedDirectory(dir.resolve("inbox"))).isTrue();
+        
assertThat(strategy.isExcludedDirectory(dir.resolve("inbox/done"))).isTrue();
+
+        // what the routes write and move in the inbox is data: no change
+        Files.writeString(dir.resolve("inbox/order-2.json"), "{}");
+        Files.move(dir.resolve("inbox/order-1.json"), 
dir.resolve("inbox/done/order-1.json"));
+        assertThat(scanNames()).isEmpty();
+
+        // the route is still watched
+        Files.writeString(dir.resolve("shop.camel.yaml"), "one and two");
+        assertThat(scanNames()).containsExactly("shop.camel.yaml");
+    }
+
+    @Test
+    public void testAnExcludeWithoutTheStarsIsTheSameFolder() throws Exception 
{
+        start("*,!inbox");
+
+        
assertThat(strategy.isExcludedDirectory(dir.resolve("inbox"))).isTrue();
+        assertThat(scanNames()).containsExactly("shop.camel.yaml");
+    }
+
+    @Test
+    public void testAFilePatternExcludeLeavesOutFilesNotFolders() throws 
Exception {
+        start("*,!*.json");
+
+        // the folder is still walked, for the files it holds that are not json
+        
assertThat(strategy.isExcludedDirectory(dir.resolve("inbox"))).isFalse();
+        Files.writeString(dir.resolve("inbox/readme.txt"), "a text");
+        assertThat(scanNames()).containsExactly("readme.txt", 
"shop.camel.yaml");
+    }
+
+    @Test
+    public void testRepeatedReloadsForOneFolderWarnOnce() {
+        start("*");
+        List<File> data = List.of(dir.resolve("inbox/order-1.json").toFile(), 
dir.resolve("inbox/done/order-0.json").toFile());
+
+        long now = 1_000_000;
+        for (int i = 0; i < RouteWatcherReloadStrategy.LOOP_RELOADS - 1; i++) {
+            assertThat(strategy.checkReloadLoop(data, now + i * 
2000L)).isFalse();
+        }
+        // the fifth reload within 15 seconds, all for files under inbox
+        assertThat(strategy.checkReloadLoop(data, now + 8000)).isTrue();
+        // once per folder
+        assertThat(strategy.checkReloadLoop(data, now + 10000)).isFalse();
+    }
+
+    @Test
+    public void testNoWarningWhenTheReloadsAreSlowOrForSources() {
+        start("*");
+        List<File> data = List.of(dir.resolve("inbox/order-1.json").toFile());
+        List<File> route = List.of(dir.resolve("shop.camel.yaml").toFile());
+        List<File> properties = 
List.of(dir.resolve("inbox/application.properties").toFile());
+
+        // five reloads, but over more than 15 seconds: a person dropping 
files now and then
+        for (int i = 0; i < RouteWatcherReloadStrategy.LOOP_RELOADS; i++) {
+            assertThat(strategy.checkReloadLoop(data, i * 5000L)).isFalse();
+        }
+        // a change of a file at the top (a route being edited) or of a 
properties file starts the count again
+        long now = 100_000;
+        for (int i = 0; i < RouteWatcherReloadStrategy.LOOP_RELOADS - 1; i++) {
+            assertThat(strategy.checkReloadLoop(data, now + i * 
1000L)).isFalse();
+        }
+        assertThat(strategy.checkReloadLoop(route, now + 4000)).isFalse();
+        for (int i = 0; i < RouteWatcherReloadStrategy.LOOP_RELOADS - 1; i++) {
+            assertThat(strategy.checkReloadLoop(data, now + 5000 + i * 
1000L)).isFalse();
+        }
+        assertThat(strategy.checkReloadLoop(properties, now + 9000)).isFalse();
+        for (int i = 0; i < RouteWatcherReloadStrategy.LOOP_RELOADS - 1; i++) {
+            assertThat(strategy.checkReloadLoop(data, now + 10000 + i * 
1000L)).isFalse();
+        }
+    }
+
+    @Test
+    public void testNoWarningWhenTheFilesAreUnderDifferentFolders() {
+        start("*");
+        List<File> inbox = List.of(dir.resolve("inbox/order-1.json").toFile());
+        List<File> outbox = 
List.of(dir.resolve("outbox/order-1.json").toFile());
+
+        long now = 1_000_000;
+        for (int i = 0; i < RouteWatcherReloadStrategy.LOOP_RELOADS; i++) {
+            assertThat(strategy.checkReloadLoop(i % 2 == 0 ? inbox : outbox, 
now + i * 1000L)).isFalse();
+        }
+    }
+
+    private static void deleteRecursively(Path root) throws IOException {
+        if (!Files.exists(root)) {
+            return;
+        }
+        Files.walkFileTree(root, new SimpleFileVisitor<>() {
+            @Override
+            public FileVisitResult visitFile(Path file, BasicFileAttributes 
attrs) throws IOException {
+                Files.delete(file);
+                return FileVisitResult.CONTINUE;
+            }
+
+            @Override
+            public FileVisitResult postVisitDirectory(Path d, IOException exc) 
throws IOException {
+                Files.delete(d);
+                return FileVisitResult.CONTINUE;
+            }
+        });
+    }
+}
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
index 9e0ed5cd8778..4b3ae2e3f609 100644
--- 
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
@@ -227,6 +227,10 @@ public class FileWatcherResourceReloadStrategy extends 
ResourceReloadStrategySup
                         // .idea are not ours either, and walking them every 
scan costs for nothing (CAMEL-25042)
                         return FileVisitResult.SKIP_SUBTREE;
                     }
+                    if (!dir.equals(root) && isExcludedDirectory(dir)) {
+                        // a folder of data the routes read and write, not 
sources (CAMEL-25670)
+                        return FileVisitResult.SKIP_SUBTREE;
+                    }
                     if (!isRecursive && !dir.equals(root)) {
                         return FileVisitResult.SKIP_SUBTREE;
                     }
@@ -296,6 +300,15 @@ public class FileWatcherResourceReloadStrategy extends 
ResourceReloadStrategySup
         return compileWorkDir != null && 
dir.toAbsolutePath().normalize().startsWith(compileWorkDir);
     }
 
+    /**
+     * Whether the directory is left out of the scan, with all it holds: a 
folder of data the routes read and write, not
+     * sources, whose files would otherwise reload the routes every time the 
routes write there (CAMEL-25670). By
+     * default no directory is left out.
+     */
+    protected boolean isExcludedDirectory(Path dir) {
+        return false;
+    }
+
     private Path resolveCompileWorkDir() {
         CompileStrategy cs = getCamelContext() != null
                 ? 
getCamelContext().getCamelContextExtension().getContextPlugin(CompileStrategy.class)
 : null;
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 22be1ad2f8fc..bbeb6de930f3 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
@@ -20,9 +20,12 @@ import java.io.File;
 import java.io.InputStream;
 import java.net.URI;
 import java.nio.file.Files;
+import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Deque;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
@@ -77,6 +80,20 @@ public class RouteWatcherReloadStrategy extends 
FileWatcherResourceReloadStrateg
     /** The sources of the last failed reload: retried when the properties 
change, as a missing property may be why. */
     private final List<Resource> failedSources = new ArrayList<>();
     private Supplier<Collection<Resource>> beanResources;
+    /** The parts of the pattern that start with ! (without it): what is left 
out of reloading. */
+    private final List<String> excludes = new ArrayList<>();
+
+    /** How many reloads in a row, all for files under the same data folder, 
make the reload loop warning. */
+    static final int LOOP_RELOADS = 5;
+    /** Within how long those reloads must be, in millis. */
+    static final long LOOP_MILLIS = 15000;
+    /** The last reloads that were for files under one data folder: when, and 
which folder. */
+    private final Deque<DataReload> dataReloads = new ArrayDeque<>();
+    /** The folders the reload loop warning has named already. */
+    private final Set<String> loopWarned = new HashSet<>();
+
+    private record DataReload(long time, String folder) {
+    }
 
     public Supplier<Collection<Resource>> getBeanResources() {
         return beanResources;
@@ -110,7 +127,9 @@ public class RouteWatcherReloadStrategy extends 
FileWatcherResourceReloadStrateg
      * Used for inclusive filtering of routes from directories.
      *
      * Typical used for specifying to accept routes in XML or YAML files, such 
as <tt>*.yaml,*.xml</tt>. Multiple
-     * patterns can be specified separated by comma.
+     * patterns can be specified separated by comma. A pattern that starts 
with <tt>!</tt> excludes what it matches,
+     * such as <tt>*,!inbox/**</tt> to watch every file except the files under 
the inbox folder; a folder that an
+     * exclude matches as a whole is not scanned at all.
      */
     public void setPattern(String pattern) {
         this.pattern = pattern;
@@ -159,29 +178,47 @@ public class RouteWatcherReloadStrategy extends 
FileWatcherResourceReloadStrateg
         final String base = new File(getFolder()).getAbsolutePath();
         final AntPathMatcher matcher = new AntPathMatcher();
 
+        // the parts that start with ! exclude what they match, such as the 
data folders the routes read and write
+        List<String> includes = new ArrayList<>();
+        excludes.clear();
+        for (String part : pattern.split(",")) {
+            part = part.trim();
+            if (part.startsWith("!")) {
+                part = part.substring(1).trim();
+                if (!part.isEmpty()) {
+                    excludes.add(part);
+                }
+            } else if (!part.isEmpty()) {
+                includes.add("*".equals(part) ? "**" : part);
+            }
+        }
+        if (includes.isEmpty()) {
+            includes.add("**");
+        }
+
         if (getFileFilter() == null) {
             // file matcher that matches via the ant path matcher
-            final String[] parts = pattern.split(",");
             setFileFilter(f -> {
-                for (String part : parts) {
-                    part = part.trim();
-                    // the file name, and the path relative to the starting 
folder (such as sub/foo.yaml)
-                    String path = f.getAbsolutePath();
-                    String relative = null;
-                    if (path.startsWith(base)) {
-                        relative = path.substring(base.length()).replace('\\', 
'/');
-                        while (relative.startsWith("/")) {
-                            relative = relative.substring(1);
-                        }
-                        path = FileUtil.stripPath(path);
+                // the file name, and the path relative to the starting folder 
(such as sub/foo.yaml)
+                String path = f.getAbsolutePath();
+                String relative = null;
+                if (path.startsWith(base)) {
+                    relative = path.substring(base.length()).replace('\\', 
'/');
+                    while (relative.startsWith("/")) {
+                        relative = relative.substring(1);
                     }
-
-                    String name = FileUtil.compactPath(f.getPath());
-                    boolean exact = name.equals(part);
-                    boolean result = exact || matcher.match(part, path, false)
-                            || relative != null && matcher.match(part, 
relative, false);
+                    path = FileUtil.stripPath(path);
+                }
+                String name = FileUtil.compactPath(f.getPath());
+                for (String part : excludes) {
+                    if (matches(matcher, part, name, path, relative)) {
+                        LOG.trace("Excluding file pattern:!{} path:{}", part, 
path);
+                        return false;
+                    }
+                }
+                for (String part : includes) {
+                    boolean result = matches(matcher, part, name, path, 
relative);
                     LOG.trace("Accepting file pattern:{} path:{} -> {}", part, 
path, result);
-
                     if (result) {
                         return true;
                     }
@@ -193,6 +230,127 @@ public class RouteWatcherReloadStrategy extends 
FileWatcherResourceReloadStrateg
         super.doStart();
     }
 
+    private static boolean matches(AntPathMatcher matcher, String part, String 
name, String path, String relative) {
+        return name.equals(part) || matcher.match(part, path, false)
+                || relative != null && matcher.match(part, relative, false);
+    }
+
+    /**
+     * A folder that an exclude of the pattern matches as a whole, such as 
<tt>inbox</tt> for <tt>!inbox/**</tt>: the
+     * folder is left out of the scan, so the files the routes read and write 
in it are not walked on every scan.
+     */
+    @Override
+    protected boolean isExcludedDirectory(Path dir) {
+        if (excludes.isEmpty() || folder == null) {
+            return false;
+        }
+        String relative = relativePath(dir.toFile());
+        if (relative == null || relative.isEmpty()) {
+            return false;
+        }
+        for (String part : excludes) {
+            // inbox and inbox/** match the folder itself; a file pattern such 
as *.json does not leave a folder out
+            if (part.equals(relative) || AntPathMatcher.INSTANCE.match(part, 
relative + "/_", false)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /** The file a route was loaded from, as an absolute path, or null when it 
is not a file. */
+    private static Path sourcePath(Resource resource) {
+        String location = resource != null ? resource.getLocation() : null;
+        if (location == null || location.contains(":") && 
!location.startsWith("file:")) {
+            return null;
+        }
+        try {
+            return Paths.get(location.startsWith("file:") ? 
location.substring(5) : location).toAbsolutePath().normalize();
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    /** The path of the file relative to the watched folder, with / 
separators, or null when it is not under it. */
+    private String relativePath(File file) {
+        String base = new File(getFolder()).getAbsolutePath();
+        String path = file.getAbsolutePath();
+        if (!path.startsWith(base)) {
+            return null;
+        }
+        String relative = path.substring(base.length()).replace('\\', '/');
+        while (relative.startsWith("/")) {
+            relative = relative.substring(1);
+        }
+        return relative;
+    }
+
+    /**
+     * Routes that write files into a folder under the watched one reload 
themselves: the reload restarts the routes,
+     * they write again, and that is a change again (a file copied into an 
inbox, moved to done or archive). When the
+     * last {@link #LOOP_RELOADS} reloads within {@link #LOOP_MILLIS} were 
each for files under the same folder only,
+     * and none of those files is a source of a route, a properties file or a 
Java or Groovy source, a WARN names the
+     * folder and how to leave it out of reloading, once per folder 
(CAMEL-25670).
+     *
+     * @return whether the warning was logged now
+     */
+    boolean checkReloadLoop(List<File> changed, long now) {
+        String data = dataFolder(changed);
+        if (data == null) {
+            dataReloads.clear();
+            return false;
+        }
+        dataReloads.addLast(new DataReload(now, data));
+        while (dataReloads.size() > LOOP_RELOADS) {
+            dataReloads.removeFirst();
+        }
+        long took = now - dataReloads.getFirst().time();
+        if (dataReloads.size() == LOOP_RELOADS && took <= LOOP_MILLIS
+                && dataReloads.stream().allMatch(r -> r.folder().equals(data)) 
&& loopWarned.add(data)) {
+            LOG.warn("Routes reloaded {} times in {} seconds, each time 
because files under {}/ changed, which are not"
+                     + " sources of the routes. If that folder holds data the 
routes read or write, leave it out of"
+                     + " reloading: camel run --data-dir={}, or !{}/** in the 
reload pattern"
+                     + " (camel.main.routesReloadPattern).",
+                    LOOP_RELOADS, Math.max(1, Math.round(took / 1000.0)), 
data, data, data);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * The folder right under the watched one that holds all the changed 
files, when none of them is a source (a route
+     * was loaded from it, or it is a properties, Java or Groovy file), else 
null.
+     */
+    private String dataFolder(List<File> changed) {
+        // by full path: a data file named as a route source (inbox/route.yaml 
and route.yaml) is still data
+        Set<Path> sources = new HashSet<>();
+        for (Route route : getCamelContext().getRoutes()) {
+            Path source = sourcePath(route.getSourceResource());
+            if (source != null) {
+                sources.add(source);
+            }
+        }
+        String data = null;
+        for (File file : changed) {
+            String name = file.getName();
+            if (name.endsWith(".properties") || name.endsWith(".java") || 
name.endsWith(".groovy")
+                    || 
sources.contains(file.toPath().toAbsolutePath().normalize())) {
+                return null;
+            }
+            String relative = relativePath(file);
+            int slash = relative != null ? relative.indexOf('/') : -1;
+            if (slash < 1) {
+                return null;
+            }
+            String top = relative.substring(0, slash);
+            if (data == null) {
+                data = top;
+            } else if (!data.equals(top)) {
+                return null;
+            }
+        }
+        return data;
+    }
+
     @Override
     protected String startupMessage(File dir) {
         return "Live route reloading enabled (directory: " + dir + ")";
@@ -293,6 +451,7 @@ public class RouteWatcherReloadStrategy extends 
FileWatcherResourceReloadStrateg
      */
     @Override
     protected void onReloadBatch(List<File> changed) {
+        checkReloadLoop(changed, System.currentTimeMillis());
         if (changed.size() < 2) {
             super.onReloadBatch(changed);
             return;
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-configuration.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-configuration.adoc
index 04a469853182..6d67bd97fee8 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-configuration.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-configuration.adoc
@@ -4,7 +4,7 @@ This page covers configuring the Camel CLI — available options, 
configuration
 
 // jbang options: START
 == Camel CLI configurations
-The camel.jbang supports 53 options, which are listed below.
+The camel.jbang supports 54 options, which are listed below.
 
 [width="100%",cols="2,5,^1,2",options="header"]
 |===
@@ -15,6 +15,7 @@ The camel.jbang supports 53 options, which are listed below.
 | `camel.jbang.classpathFiles` | Additional files to add to classpath (Use 
commas to separate multiple files). |  | String
 | `camel.jbang.compileWorkDir` | Work directory for compiler. Can be used to 
write compiled classes or other resources. | .camel-jbang/compile | String
 | `camel.jbang.console` | Developer console at /q/dev on local HTTP server 
(port 8080 by default) | false | boolean
+| `camel.jbang.dataDir` | Folders under the source directory (comma separated) 
that hold data the routes read or write, not sources: in dev mode a change in 
them does not reload the routes. Use this for an inbox, outbox or archive 
folder under the source directory. |  | String
 | `camel.jbang.dependencies` | Additional dependencies (Use commas to separate 
multiple dependencies). |  | String
 | `camel.jbang.dependencies.main` | Additional dependencies for Camel Main 
runtime only |  | String
 | `camel.jbang.dependencies.quarkus` | Additional dependencies for Quarkus 
runtime only |  | String
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-running.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-running.adoc
index 4f50422adef9..9505dc9628c7 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-running.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-running.adoc
@@ -58,6 +58,28 @@ Without `--source-dir`, Camel only watches the specific 
files you listed on the
 
 NOTE: You cannot combine files and source dir: `camel run abc.java 
--source-dir=mycode` is not allowed.
 
+=== Data folders in the source directory
+
+Routes often read and write files in folders of their own, such as an `inbox` 
they consume and an `archive` they
+move the processed files to. When such a folder is in the source directory, 
the files the routes write there are
+changes too, and dev mode reloads the routes for them; the reload restarts the 
routes, which write again, and so on.
+Tell Camel which folders hold data with `--data-dir` (comma separated, 
relative to the source directory):
+
+[source,bash]
+----
+camel run --source-dir=mycode --dev --data-dir=inbox,archive
+----
+
+or in the `application.properties` of the source directory:
+
+[source,properties]
+----
+camel.jbang.dataDir=inbox,archive
+----
+
+Dev mode then does not watch these folders, nor the folders in them. When the 
routes keep reloading for files in one
+folder, Camel logs a warning that names the folder.
+
 === Live reload of resource files
 
 In dev mode, Camel disables `contentCache` on resource-based components (such 
as `xslt`)
diff --git 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-debug.adoc 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-debug.adoc
index 9a34da9b8765..f43025613db9 100644
--- a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-debug.adoc
+++ b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-debug.adoc
@@ -28,6 +28,7 @@ camel debug [options]
 | `--camel-version` | To run using a different Camel version than the default 
version. |  | String
 | `--code` | Run the given text or file as Java DSL routes |  | String
 | `--console` | Developer console on the local HTTP server (port 8080 by 
default): /q/dev with the jbang and Camel Main runtimes, /actuator/camel with 
Spring Boot, and /q/camel/dev-console with Quarkus | false | boolean
+| `--data-dir` | Folders under the source directory (comma separated) that 
hold data the routes read or write, not sources: in dev mode a change in them 
does not reload the routes (such as inbox,archive). |  | String
 | `--dep,--dependency` | Add additional dependencies |  | List
 | `--download` | Whether to allow automatic downloading JAR dependencies (over 
the internet) | true | boolean
 | `--empty` | Run an empty Camel without loading source files | false | boolean
diff --git 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-dev.adoc 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-dev.adoc
index 9e67d3b66591..98eaf15ad93d 100644
--- a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-dev.adoc
+++ b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-dev.adoc
@@ -26,6 +26,7 @@ camel dev [options]
 | `--camel-version` | To run using a different Camel version than the default 
version. |  | String
 | `--code` | Run the given text or file as Java DSL routes |  | String
 | `--console` | Developer console on the local HTTP server (port 8080 by 
default): /q/dev with the jbang and Camel Main runtimes, /actuator/camel with 
Spring Boot, and /q/camel/dev-console with Quarkus | false | boolean
+| `--data-dir` | Folders under the source directory (comma separated) that 
hold data the routes read or write, not sources: in dev mode a change in them 
does not reload the routes (such as inbox,archive). |  | String
 | `--dep,--dependency` | Add additional dependencies |  | List
 | `--download` | Whether to allow automatic downloading JAR dependencies (over 
the internet) | true | boolean
 | `--empty` | Run an empty Camel without loading source files | false | boolean
diff --git 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-run.adoc 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-run.adoc
index d7a6cce8eb27..849cb4e2d082 100644
--- a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-run.adoc
+++ b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-run.adoc
@@ -26,6 +26,7 @@ camel run [options]
 | `--camel-version` | To run using a different Camel version than the default 
version. |  | String
 | `--code` | Run the given text or file as Java DSL routes |  | String
 | `--console` | Developer console on the local HTTP server (port 8080 by 
default): /q/dev with the jbang and Camel Main runtimes, /actuator/camel with 
Spring Boot, and /q/camel/dev-console with Quarkus | false | boolean
+| `--data-dir` | Folders under the source directory (comma separated) that 
hold data the routes read or write, not sources: in dev mode a change in them 
does not reload the routes (such as inbox,archive). |  | String
 | `--dep,--dependency` | Add additional dependencies |  | List
 | `--download` | Whether to allow automatic downloading JAR dependencies (over 
the internet) | true | boolean
 | `--empty` | Run an empty Camel without loading source files | false | boolean
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
 
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
index c674c4e0d388..ee61341736b4 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
@@ -6,9 +6,9 @@
     { "name": "cmd", "fullName": "cmd", "description": "Performs commands in 
the running Camel integrations, such as start\/stop route, or change logging 
levels.", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.action.CamelAction", "options": [ { 
"names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": 
"browse", "fullName": "cmd browse", "description": "Browse pending messages on 
endpoints [...]
     { "name": "completion", "fullName": "completion", "description": "Generate 
completion script for bash\/zsh", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.Complete", "options": [ { "names": 
"-h,--help", "description": "Display the help and sub-commands", "javaType": 
"boolean", "type": "boolean" } ] },
     { "name": "config", "fullName": "config", "description": "Get and set user 
configuration values", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.config.ConfigCommand", "options": [ { 
"names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "get", 
"fullName": "config get", "description": "Display user configuration value", 
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.config. [...]
-    { "name": "debug", "fullName": "debug", "description": "Debug local Camel 
integration", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Debug", 
"options": [ { "names": "--ago", "description": "Use ago instead of yyyy-MM-dd 
HH:mm:ss in timestamp.", "javaType": "boolean", "type": "boolean" }, { "names": 
"--background", "description": "Run in the background", "defaultValue": 
"false", "javaType": "boolean", "type": "boolean" }, { "names": 
"--background-wait", "description": "To  [...]
+    { "name": "debug", "fullName": "debug", "description": "Debug local Camel 
integration", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Debug", 
"options": [ { "names": "--ago", "description": "Use ago instead of yyyy-MM-dd 
HH:mm:ss in timestamp.", "javaType": "boolean", "type": "boolean" }, { "names": 
"--background", "description": "Run in the background", "defaultValue": 
"false", "javaType": "boolean", "type": "boolean" }, { "names": 
"--background-wait", "description": "To  [...]
     { "name": "dependency", "fullName": "dependency", "description": "Displays 
all Camel dependencies required to run", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.DependencyCommand", "options": [ { 
"names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": 
"copy", "fullName": "dependency copy", "description": "Copies all Camel 
dependencies required to run to a specific directory", "sourc [...]
-    { "name": "dev", "fullName": "dev", "description": "Run in dev mode with 
live reload", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Dev", 
"options": [ { "names": "--background", "description": "Run in the background", 
"defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": 
"--background-wait", "description": "To wait for run in background to startup 
successfully, before returning", "defaultValue": "true", "javaType": "boolean", 
"type": "boolean" }, [...]
+    { "name": "dev", "fullName": "dev", "description": "Run in dev mode with 
live reload", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Dev", 
"options": [ { "names": "--background", "description": "Run in the background", 
"defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": 
"--background-wait", "description": "To wait for run in background to startup 
successfully, before returning", "defaultValue": "true", "javaType": "boolean", 
"type": "boolean" }, [...]
     { "name": "dirty", "fullName": "dirty", "description": "Check if there are 
dirty files from previous Camel runs that did not terminate gracefully", 
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.process.Dirty", 
"options": [ { "names": "--clean", "description": "Clean dirty files which are 
no longer in use", "defaultValue": "false", "javaType": "boolean", "type": 
"boolean" }, { "names": "-h,--help", "description": "Display the help and 
sub-commands", "javaType": "boolean", " [...]
     { "name": "doc", "fullName": "doc", "description": "Shows documentation 
for kamelet, component, and other Camel resources", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.catalog.CatalogDoc", "options": [ { 
"names": "--camel-version", "description": "To use a different Camel version 
than the default version", "javaType": "java.lang.String", "type": "string" }, 
{ "names": "--download", "description": "Whether to allow automatic downloading 
JAR dependencies (over the internet [...]
     { "name": "doctor", "fullName": "doctor", "description": "Checks the 
environment and reports potential issues", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.Doctor", "options": [ { "names": 
"-h,--help", "description": "Display the help and sub-commands", "javaType": 
"boolean", "type": "boolean" } ] },
@@ -27,7 +27,7 @@
     { "name": "plugin", "fullName": "plugin", "description": "Manage plugins 
that add sub-commands to this CLI", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.plugin.PluginCommand", "options": [ { 
"names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "add", 
"fullName": "plugin add", "description": "Add new plugin", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.plugin.PluginA [...]
     { "name": "ps", "fullName": "ps", "description": "List running Camel 
integrations", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.process.ListProcess", "options": [ { 
"names": "--json", "description": "Output in JSON Format", "javaType": 
"boolean", "type": "boolean" }, { "names": "--pid", "description": "List only 
pid in the output", "javaType": "boolean", "type": "boolean" }, { "names": 
"--remote", "description": "Break down counters into remote\/total pairs", 
"javaType": [...]
     { "name": "restart", "fullName": "restart", "description": "Restarts 
running Camel integrations (stop + re-launch)", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.process.RestartProcess", "options": [ 
{ "names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ] },
-    { "name": "run", "fullName": "run", "description": "Run as local Camel 
integration", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Run", 
"options": [ { "names": "--background", "description": "Run in the background", 
"defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": 
"--background-wait", "description": "To wait for run in background to startup 
successfully, before returning", "defaultValue": "true", "javaType": "boolean", 
"type": "boolean" }, { [...]
+    { "name": "run", "fullName": "run", "description": "Run as local Camel 
integration", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Run", 
"options": [ { "names": "--background", "description": "Run in the background", 
"defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": 
"--background-wait", "description": "To wait for run in background to startup 
successfully, before returning", "defaultValue": "true", "javaType": "boolean", 
"type": "boolean" }, { [...]
     { "name": "sbom", "fullName": "sbom", "description": "Generate a CycloneDX 
or SPDX SBOM for a specific project", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.SBOMGenerator", "options": [ { 
"names": "--build-property", "description": "Maven build properties, ex. 
--build-property=prop1=foo", "javaType": "java.util.List", "type": "array" }, { 
"names": "--camel-spring-boot-version", "description": "Camel version to use 
with Spring Boot", "javaType": "java.lang.String", "type" [...]
     { "name": "script", "fullName": "script", "description": "Run Camel 
integration as shell script for terminal scripting", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.Script", "options": [ { "names": 
"--logging", "description": "Can be used to turn on logging (logs to file in 
<user home>\/.camel directory)", "defaultValue": "false", "javaType": 
"boolean", "type": "boolean" }, { "names": "--logging-level", "description": 
"Logging level (ERROR, WARN, INFO, DEBUG, TRACE)", "d [...]
     { "name": "semantic", "fullName": "semantic", "description": "List 
semantic definitions and expert contracts", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.action.CamelSemantic", "options": [ { 
"names": "--expert", "description": "Show the operations and parameter contract 
of this expert", "javaType": "java.lang.String", "type": "string" }, { "names": 
"--json", "description": "Output a single JSON document", "javaType": 
"boolean", "type": "boolean" }, { "names": "--timeou [...]
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-configuration-metadata.json
 
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-configuration-metadata.json
index e61e0029c60a..6ba62ce930ac 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-configuration-metadata.json
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-configuration-metadata.json
@@ -9,6 +9,7 @@
     { "name": "camel.jbang.classpathFiles", "required": false, "description": 
"Additional files to add to classpath (Use commas to separate multiple 
files).", "type": "string", "javaType": "String", "secret": false },
     { "name": "camel.jbang.compileWorkDir", "required": false, "description": 
"Work directory for compiler. Can be used to write compiled classes or other 
resources.", "label": "advanced", "type": "string", "javaType": "String", 
"defaultValue": ".camel-jbang\/compile", "secret": false },
     { "name": "camel.jbang.console", "required": false, "description": 
"Developer console at \/q\/dev on local HTTP server (port 8080 by default)", 
"type": "boolean", "javaType": "boolean", "defaultValue": false, "secret": 
false },
+    { "name": "camel.jbang.dataDir", "required": false, "description": 
"Folders under the source directory (comma separated) that hold data the routes 
read or write, not sources: in dev mode a change in them does not reload the 
routes. Use this for an inbox, outbox or archive folder under the source 
directory.", "label": "advanced", "type": "string", "javaType": "String", 
"secret": false },
     { "name": "camel.jbang.dependencies", "required": false, "description": 
"Additional dependencies (Use commas to separate multiple dependencies).", 
"type": "string", "javaType": "String", "secret": false },
     { "name": "camel.jbang.dependencies.main", "required": false, 
"description": "Additional dependencies for Camel Main runtime only", "type": 
"string", "javaType": "String", "secret": false },
     { "name": "camel.jbang.dependencies.quarkus", "required": false, 
"description": "Additional dependencies for Quarkus runtime only", "type": 
"string", "javaType": "String", "secret": false },
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java
index 3093a346b05c..27f6d101c42b 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java
@@ -196,6 +196,10 @@ public class Run extends CamelCommand {
             description = "Source directory for dynamically loading Camel 
file(s) to run. When using this, then files cannot be specified at the same 
time.")
     String sourceDir;
 
+    @Option(names = { "--data-dir" },
+            description = "Folders under the source directory (comma 
separated) that hold data the routes read or write, not sources: in dev mode a 
change in them does not reload the routes (such as inbox,archive).")
+    String dataDir;
+
     @Option(names = { "--resource-dir", "--resource-dirs" },
             description = "Additional resource directories to include on the 
classpath (recursive, preserving structure)",
             split = ",")
@@ -940,6 +944,9 @@ public class Run extends CamelCommand {
         if (sourceDir != null) {
             writeSetting(main, profileProperties, SOURCE_DIR, sourceDir);
         }
+        if (dataDir != null) {
+            writeSetting(main, profileProperties, DATA_DIR, dataDir);
+        }
         if (debugOptions.trace) {
             writeSetting(main, profileProperties, "camel.main.tracing", 
"true");
         }
@@ -2096,6 +2103,46 @@ public class Run extends CamelCommand {
         }
     }
 
+    /**
+     * --data-dir only changes what dev mode watches in a source directory: 
without one only the given files are
+     * watched, and their data folders are not, so the option has nothing to 
leave out. Said when it is given on the
+     * command line; camel.jbang.dataDir in an application.properties is not, 
as an example that declares its data
+     * folders also runs with its files (camel run --example).
+     */
+    private void warnDataDirWithoutSourceDir() {
+        if (dataDir != null && spec != null && spec.commandLine() != null
+                && spec.commandLine().getParseResult() != null
+                && 
spec.commandLine().getParseResult().hasMatchedOption("--data-dir")) {
+            printer().printErr("WARN: --data-dir=" + dataDir + " has no effect 
without --source-dir: dev mode then only"
+                               + " watches the given files");
+        }
+    }
+
+    /**
+     * The reload pattern of dev mode with a source directory: every file 
under it, except the data folders
+     * (--data-dir), whose files the routes read and write: a change there is 
not a change of the sources, and reloading
+     * the routes for it restarts them, so they write again and reload again 
(CAMEL-25670).
+     */
+    static String sourceDirReloadPattern(String dataDir) {
+        StringJoiner sj = new StringJoiner(",");
+        sj.add("*");
+        if (dataDir != null) {
+            for (String dir : dataDir.split(",")) {
+                dir = dir.trim().replace('\\', '/');
+                while (dir.startsWith("./")) {
+                    dir = dir.substring(2);
+                }
+                while (dir.endsWith("/")) {
+                    dir = dir.substring(0, dir.length() - 1);
+                }
+                if (!dir.isEmpty()) {
+                    sj.add("!" + dir + "/**");
+                }
+            }
+        }
+        return sj.toString();
+    }
+
     /**
      * Route reload (dev mode) for Camel Main watches the original source 
files, as the exported project only contains
      * copies of them.
@@ -2106,8 +2153,9 @@ public class Run extends CamelCommand {
         String pattern;
         if (sourceDir != null) {
             reloadDir = sourceDir;
-            pattern = "*";
+            pattern = sourceDirReloadPattern(dataDir);
         } else {
+            warnDataDirWithoutSourceDir();
             // use the directory of the first file that is in another folder 
than the current dir
             reloadDir = ".";
             StringJoiner sj = new StringJoiner(",");
@@ -2673,9 +2721,10 @@ public class Run extends CamelCommand {
             if (sourceDir != null) {
                 main.addInitialProperty(SOURCE_DIR, sourceDir);
                 main.addInitialProperty("camel.main.routesReloadDirectory", 
sourceDir);
-                main.addInitialProperty("camel.main.routesReloadPattern", "*");
+                main.addInitialProperty("camel.main.routesReloadPattern", 
sourceDirReloadPattern(dataDir));
                 
main.addInitialProperty("camel.main.routesReloadDirectoryRecursive", "true");
             } else {
+                warnDataDirWithoutSourceDir();
                 String pattern = sjReload.toString();
                 String reloadDir = ".";
                 // use current dir, however if we run a file that are in 
another folder, then we should track that folder instead
@@ -2773,6 +2822,7 @@ public class Run extends CamelCommand {
             quarkusPlatform = QuarkusPlatformMixin.of(answer, quarkusPlatform);
             gav = answer.getProperty(GAV, gav);
             stub = answer.getProperty(STUB, stub);
+            dataDir = answer.getProperty(DATA_DIR, dataDir);
             excludes = RuntimeUtil.getCommaSeparatedPropertyAsList(answer, 
EXCLUDES, excludes);
         }
 
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/CamelJBangConstants.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/CamelJBangConstants.java
index 26063b36bbb9..0df5e028114d 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/CamelJBangConstants.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/CamelJBangConstants.java
@@ -175,6 +175,12 @@ public final class CamelJBangConstants {
               javaType = "String", label = "advanced")
     public static final String SOURCE_DIR = "camel.jbang.sourceDir";
 
+    @Metadata(description = "Folders under the source directory (comma 
separated) that hold data the routes read or write,"
+                            + " not sources: in dev mode a change in them does 
not reload the routes. Use this for an"
+                            + " inbox, outbox or archive folder under the 
source directory.",
+              javaType = "String", label = "advanced")
+    public static final String DATA_DIR = "camel.jbang.dataDir";
+
     @Metadata(description = "Whether to ignore route loading and compilation 
errors (use this with care!)",
               javaType = "boolean", label = "advanced")
     public static final String IGNORE_LOADING_ERROR = 
"camel.jbang.ignoreLoadingError";
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/connect/file-processing/application.properties
 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/connect/file-processing/application.properties
index 13a83393a912..60bdc8a38599 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/connect/file-processing/application.properties
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/connect/file-processing/application.properties
@@ -14,3 +14,7 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
+
+# the folders the routes read and write hold data, not sources: with camel run 
--source-dir in dev mode,
+# a change in them does not reload the routes
+camel.jbang.dataDir=samples,inbox,archive
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/fail-well/error-handling/application.properties
 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/fail-well/error-handling/application.properties
index 13a83393a912..7121aa72d85d 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/fail-well/error-handling/application.properties
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/fail-well/error-handling/application.properties
@@ -14,3 +14,7 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
+
+# the folders the routes read and write hold data, not sources: with camel run 
--source-dir in dev mode,
+# a change in them does not reload the routes
+camel.jbang.dataDir=orders,parked
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/transform/csv-to-json/application.properties
 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/transform/csv-to-json/application.properties
index 13a83393a912..edab55a1a7a5 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/transform/csv-to-json/application.properties
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/transform/csv-to-json/application.properties
@@ -14,3 +14,7 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
+
+# the folders the routes read and write hold data, not sources: with camel run 
--source-dir in dev mode,
+# a change in them does not reload the routes
+camel.jbang.dataDir=inbox,outbox
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/RunTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/RunTest.java
index bf2f1904e7a1..54621d037ef0 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/RunTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/RunTest.java
@@ -248,4 +248,21 @@ class RunTest extends CamelCommandBaseTestSupport {
         assertThat(Run.mergeJvmArgs("-Xmx256m", 
"-XX:StartFlightRecording=filename=camel.jfr"))
                 .isEqualTo("-Xmx256m 
-XX:StartFlightRecording=filename=camel.jfr");
     }
+
+    @Test
+    public void shouldParseDataDirOption() throws Exception {
+        Run command = new Run(new CamelJBangMain());
+        CommandLine.populateCommand(command, "--source-dir=shop", 
"--data-dir=inbox,archive");
+
+        assertThat(command.dataDir).isEqualTo("inbox,archive");
+    }
+
+    @Test
+    void sourceDirReloadPatternLeavesTheDataDirsOut() {
+        // CAMEL-25670: the routes read and write the files of a data folder, 
which must not reload them
+        assertThat(Run.sourceDirReloadPattern(null)).isEqualTo("*");
+        
assertThat(Run.sourceDirReloadPattern("inbox")).isEqualTo("*,!inbox/**");
+        assertThat(Run.sourceDirReloadPattern(" inbox , ./archive/ 
,data\\in,"))
+                .isEqualTo("*,!inbox/**,!archive/**,!data/in/**");
+    }
 }

Reply via email to