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/**");
+ }
}