This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch feature/CAMEL-25670-data-dir in repository https://gitbox.apache.org/repos/asf/camel.git
commit 91440a5cef732c2e857a9e9420a1ff27b1434c67 Author: Claus Ibsen <[email protected]> AuthorDate: Wed Oct 14 21:06:33 2026 +0200 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 --- .../jbang/camel-jbang-configuration-metadata.json | 1 + .../support/RouteWatcherReloadDataDirTest.java | 193 +++++++++++++++++++++ .../support/FileWatcherResourceReloadStrategy.java | 13 ++ .../camel/support/RouteWatcherReloadStrategy.java | 184 ++++++++++++++++++-- .../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 | 37 +++- .../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, 473 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..fc26ef77c0b1 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,114 @@ 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; + } + AntPathMatcher matcher = new AntPathMatcher(); + 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) || matcher.match(part, relative + "/_", false)) { + return true; + } + } + return false; + } + + /** 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) { + Set<String> sources = new HashSet<>(); + for (Route route : getCamelContext().getRoutes()) { + Resource rs = route.getSourceResource(); + if (rs != null && rs.getLocation() != null) { + sources.add(FileUtil.stripPath(rs.getLocation())); + } + } + String data = null; + for (File file : changed) { + String name = file.getName(); + if (name.endsWith(".properties") || name.endsWith(".java") || name.endsWith(".groovy") + || sources.contains(name)) { + 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 +438,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..b8c3c1be19c8 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,31 @@ public class Run extends CamelCommand { } } + /** + * 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,7 +2138,7 @@ public class Run extends CamelCommand { String pattern; if (sourceDir != null) { reloadDir = sourceDir; - pattern = "*"; + pattern = sourceDirReloadPattern(dataDir); } else { // use the directory of the first file that is in another folder than the current dir reloadDir = "."; @@ -2673,7 +2705,7 @@ 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 { String pattern = sjReload.toString(); @@ -2773,6 +2805,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/**"); + } }
