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 ab0e692630b5 CAMEL-24727: camel-jbang-export - the export uses the
dependency resolver without picocli, and the old silent run is removed (#28735)
ab0e692630b5 is described below
commit ab0e692630b53433849f421f7752d0129e93b680
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Oct 30 18:26:49 2026 +0100
CAMEL-24727: camel-jbang-export - the export uses the dependency resolver
without picocli, and the old silent run is removed (#28735)
Signed-off-by: Claus Ibsen <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 14 ++++
.../modules/ROOT/pages/camel-jbang-projects.adoc | 8 +--
.../modules/ROOT/pages/camel-jbang-tips.adoc | 2 +
.../dsl/jbang/core/commands/CliExportContext.java | 8 ++-
.../camel/dsl/jbang/core/commands/Debug.java | 4 +-
.../apache/camel/dsl/jbang/core/commands/Run.java | 78 +++++++---------------
.../jbang/core/commands/RunDependencyResolver.java | 73 --------------------
.../dsl/jbang/core/commands/ExportApiTest.java | 48 ++++++++++++-
.../jbang/core/commands/RunExportGoldenTest.java | 48 +++++++++----
.../golden/expected/run-settings.export.txt | 64 ++++++++++++++++++
.../golden/runsettings/application.properties | 19 ++++++
.../resources/golden/runsettings/hello.camel.yaml} | 9 ++-
.../camel/dsl/jbang/export/DependencyResolver.java | 4 +-
.../camel/dsl/jbang/export/ExportContext.java | 47 ++++++++-----
.../export/run/SilentRunDependencyResolver.java | 10 ++-
.../run/SilentRunDependencyResolverTest.java | 42 +++++++++++-
16 files changed, 298 insertions(+), 180 deletions(-)
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index 7301b9e001c9..a468cb884fff 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -4215,3 +4215,17 @@ by `includeId` or `excludeId` is. Before, it failed the
exchange with an `Illega
polled folder failed again on every poll. The exchange now completes, so a
file consumer without `noop` moves such a
file into `.camel` instead of leaving it in place. A hint is logged at INFO. A
document whose type is known but is not a
medium, or is one the embedding model does not support, still fails the
exchange.
+
+=== camel-jbang - camel export boots the integration in the same JVM, no
longer through camel run
+
+To find the dependencies, `camel export` boots the integration silently, as
before, but no longer goes through
+`camel run` for it. Some settings of `camel run` therefore no longer apply to
an export:
+
+* `camel.jbang.background` and `camel.jbang.jvmDebug` in
`application.properties` are ignored by the export. Before,
+ they made the export start a separate `camel run` process (in the
background, or with a debugger), which kept running
+ after the export, or which the export waited for.
+* `--camel-version` is accepted by `camel export` from `camel-launcher` too:
it sets the Camel version of the exported
+ project. Before, it failed with a usage error.
+* `camel export clipboard.yaml` no longer reads the route from the clipboard
when there is no such file: it fails with
+ exit code 3 (file not found), as for any other missing file. Save the
content of the clipboard to a file and export
+ that file.
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-projects.adoc
b/docs/user-manual/modules/ROOT/pages/camel-jbang-projects.adoc
index 0646f15651bd..2d6ed2f648d9 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-projects.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-projects.adoc
@@ -184,7 +184,7 @@ See xref:camel-jbang-configuration.adoc[Configuration] for
all `camel.jbang.*` o
Tools that create Camel projects, such as an IDE or a visual designer, can
export from Java instead of running the `camel` command.
The `camel-jbang-export` module has the same options as `camel export`, as
public fields of an `ExportRequest`.
-It needs `camel-jbang-core` on the classpath as well, to find the dependencies
(see below):
+It does not need `camel-jbang-core` or picocli:
[source,java]
----
@@ -198,7 +198,7 @@ int exit = new ProjectExporter().export(request);
The export reads `application.properties` (and the one of the profile) of the
files, the same as `camel export`, and completes the request with what they
configure.
-An `ExportContext` sets where the export prints, how it asks for confirmation
(for example before it cleans the export directory), and the exporters of
plugins:
+An `ExportContext` sets where the export prints, how it asks for confirmation
(for example before it cleans the export directory), the exporters of plugins,
and what to report when the integration fails to start (`camel export` logs
what `camel validate yaml` says about a YAML route that did not load):
[source,java]
----
@@ -209,8 +209,8 @@ ExportContext context = new ExportContext()
int exit = new ProjectExporter(context).export(request);
----
-To find the dependencies of the integration, the export boots it silently, the
same way as `camel run` does.
-This is done by a `DependencyResolver`, which `camel-jbang-core` provides, so
it must be on the classpath as well (or you set your own resolver on the
`ExportContext`).
+To find the dependencies of the integration, the export boots it silently in
the same JVM, the same way as `camel run` does.
+This is done by the `SilentRunDependencyResolver` of the module, which `camel
export` uses as well (you can set your own `DependencyResolver` on the
`ExportContext`).
The export keeps to the request and leaves the JVM as it is, so an application
can export many times, and several exports can run at the same time:
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tips.adoc
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tips.adoc
index a952e564a16a..4bdaf1643dea 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tips.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tips.adoc
@@ -22,6 +22,8 @@ camel run clipboard.yaml --dev
Edit your code, copy it again, and Camel reloads automatically.
+NOTE: The clipboard is for `camel run` only. To export the route, save it to a
file first, then export that file.
+
== Run with inline code
For quick one-liners, pass the route directly on the command line:
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CliExportContext.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CliExportContext.java
index bc9925cec060..d049f252a5fe 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CliExportContext.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CliExportContext.java
@@ -18,14 +18,16 @@ package org.apache.camel.dsl.jbang.core.commands;
import java.util.Optional;
+import org.apache.camel.dsl.jbang.core.commands.ai.YamlLoadFailureReport;
import org.apache.camel.dsl.jbang.core.common.Plugin;
import org.apache.camel.dsl.jbang.core.common.PluginHelper;
import org.apache.camel.dsl.jbang.export.ExportContext;
import org.apache.camel.dsl.jbang.export.common.Printer;
/**
- * The {@link ExportContext} of the CLI: the dependencies are resolved by a
silent camel run, the exporters and run
- * customizers are those of the active plugins, and a confirmation is asked on
the terminal.
+ * The {@link ExportContext} of the CLI: the exporters and run customizers are
those of the active plugins, a YAML route
+ * file that did not load is reported as camel run does, and a confirmation is
asked on the terminal. The dependencies
+ * are resolved by the default resolver of the export.
*/
public final class CliExportContext {
@@ -40,7 +42,6 @@ public final class CliExportContext {
public static ExportContext create(CamelJBangMain main, Printer printer,
boolean yes) {
return new ExportContext()
.withPrinter(printer)
- .withDependencyResolver(new RunDependencyResolver(main))
.withPluginExporters(repos ->
PluginHelper.getActivePlugins(main, repos).values().stream()
.map(Plugin::getExporter)
.flatMap(Optional::stream)
@@ -49,6 +50,7 @@ public final class CliExportContext {
.map(Plugin::getRunCustomizer)
.flatMap(Optional::stream)
.toList())
+
.withStartFailureListener(YamlLoadFailureReport::logIfYamlLoadFailure)
.withConfirm(message ->
CommandHelper.confirmOperation(message, yes));
}
}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Debug.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Debug.java
index 15b43b6e7f9f..174779da796f 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Debug.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Debug.java
@@ -174,9 +174,7 @@ public class Debug extends Run {
if (!remoteAttach) {
checkFilesExist();
}
- if (!exportRun) {
- printConfigurationValues("Debugging integration with the following
configuration:");
- }
+ printConfigurationValues("Debugging integration with the following
configuration:");
Integer exit;
if (remoteAttach) {
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 4069c47e9b70..f479e65ce187 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
@@ -132,7 +132,6 @@ public class Run extends CamelCommand {
private static final Pattern CLASS_PATTERN = Pattern.compile(
"^\\s*public class\\s+([a-zA-Z0-9]*)[\\s+|;].*$",
Pattern.MULTILINE);
- public boolean exportRun;
protected Path exportBaseDir;
// the working directory, which is deleted and created again when running
Path workDir = CommandLineHelper.getWorkDir();
@@ -350,9 +349,6 @@ public class Run extends CamelCommand {
@Override
public boolean disarrangeLogging() {
- if (exportRun) {
- return false;
- }
// every runtime except the in-process jbang runtime spawns a separate
JVM
return RuntimeType.jbang != runtime;
}
@@ -376,9 +372,7 @@ public class Run extends CamelCommand {
}
checkFilesExist();
- if (!exportRun) {
- printConfigurationValues("Running integration with the following
configuration:");
- }
+ printConfigurationValues("Running integration with the following
configuration:");
// run
return run();
}
@@ -592,7 +586,7 @@ public class Run extends CamelCommand {
// what the example needs that the run does not do (a model to
pull, a key to set): the user does it
printer().println("Needs: " + needs);
}
- if (exportRun || transformRun || spec == null) {
+ if (transformRun || spec == null) {
for (String f : ExampleHelper.getFiles(entry)) {
files.add(dir.resolve(f).toString());
}
@@ -632,17 +626,6 @@ public class Run extends CamelCommand {
// the logback configuration of an existing Spring Boot project run, in a
temp file
private Path springBootLogback;
- public Integer runExport() throws Exception {
- return runExport(false);
- }
-
- protected Integer runExport(boolean ignoreLoadingError) throws Exception {
- // just boot silently and exit
- this.exportRun = true;
- this.ignoreLoadingError = ignoreLoadingError;
- return run();
- }
-
public Integer runTransform(boolean ignoreLoadingError) throws Exception {
// just boot silently and exit
this.transformRun = true;
@@ -715,20 +698,18 @@ public class Run extends CamelCommand {
}
// auto-detect runtime from pom.xml before dispatch (an existing Maven
project cannot run in-process)
- if (!exportRun && RuntimeType.jbang == runtime
+ if (RuntimeType.jbang == runtime
&& files != null && files.size() == 1 &&
files.get(0).endsWith("pom.xml")) {
RuntimeType detected =
RunHelper.detectRuntimeFromPom(Path.of(files.get(0)).toAbsolutePath());
runtime = detected != null ? detected : RuntimeType.main;
}
- if (!exportRun) {
- if (RuntimeType.quarkus == runtime) {
- return runQuarkus();
- } else if (RuntimeType.springBoot == runtime) {
- return runSpringBoot();
- } else if (RuntimeType.main == runtime) {
- return runCamelMain();
- }
+ if (RuntimeType.quarkus == runtime) {
+ return runQuarkus();
+ } else if (RuntimeType.springBoot == runtime) {
+ return runSpringBoot();
+ } else if (RuntimeType.main == runtime) {
+ return runCamelMain();
}
RunPipeline.prepareWorkDir(workDir, printer());
@@ -766,7 +747,7 @@ public class Run extends CamelCommand {
boolean autoDetectFiles = files.isEmpty() ||
RUN_JAVA_SH.equals(files.get(0));
// if no specific file to run then try to auto-detect
- if (!empty && autoDetectFiles && !RunPipeline.detectFiles(files,
sourceDir, profileProperties, exportRun)) {
+ if (!empty && autoDetectFiles && !RunPipeline.detectFiles(files,
sourceDir, profileProperties, false)) {
String run = "run";
if (transformRun) {
run = "transform";
@@ -836,8 +817,7 @@ public class Run extends CamelCommand {
}
// okay we have validated all input and are ready to run
- // (if exporting then we cannot run a different version)
- if (!exportRun && camelVersion != null || isDebugMode()) {
+ if (camelVersion != null || isDebugMode()) {
// TODO: debug camel specific version
boolean custom = false;
if (camelVersion != null) {
@@ -2070,7 +2050,6 @@ public class Run extends CamelCommand {
request.jfrProfile = debugOptions.jfrProfile;
request.openTelemetryAgent = debugOptions.openTelemetryAgent;
request.openTelemetryAgentExport =
debugOptions.openTelemetryAgentExport;
- request.exportRun = exportRun;
request.debugRun = debugRun;
request.transformRun = transformRun;
request.transformMessageRun = transformMessageRun;
@@ -2322,8 +2301,8 @@ public class Run extends CamelCommand {
this.spawnPid = p.pid();
Path logPath = started.log();
- // the commands that run Camel for themselves (export, transform) keep
quiet, and need no log afterwards
- boolean internal = exportRun || transformRun || transformMessageRun;
+ // the commands that run Camel for themselves (transform) keep quiet,
and need no log afterwards
+ boolean internal = transformRun || transformMessageRun;
if (!internal) {
printer().println(
"Running " + kind + ": " + name + " in background with
PID: " + p.pid()
@@ -2507,7 +2486,7 @@ public class Run extends CamelCommand {
* {@code camel cmd send --local} and {@code camel transform}).
*/
boolean keepsPreviousLog() {
- return spec != null && !exportRun && !transformRun &&
!transformMessageRun && !scriptRun;
+ return spec != null && !transformRun && !transformMessageRun &&
!scriptRun;
}
/**
@@ -2522,11 +2501,10 @@ public class Run extends CamelCommand {
/**
* Whether Camel started without routes where routes are expected: not
with --empty, --source-dir or in dev mode
- * (where files can be added or changed later), and not when Camel runs
for another command (export, transform).
+ * (where files can be added or changed later), and not when Camel runs
for another command (such as transform).
*/
boolean hasNoRoutes(KameletMain main) {
- if (empty || sourceDir != null || dev || ignoreLoadingError ||
exportRun || transformRun || transformMessageRun
- || scriptRun) {
+ if (empty || sourceDir != null || dev || ignoreLoadingError ||
transformRun || transformMessageRun || scriptRun) {
return false;
}
CamelContext context = main.getCamelContext();
@@ -2638,8 +2616,6 @@ public class Run extends CamelCommand {
}
private void configureLogging(Path baseDir, Properties profileProperties)
throws Exception {
- // the silent run of an export only writes the logging settings: it
runs inside the export, which logs as its
- // caller has configured (camel export configures logging before it
exports)
if (loggingOptions.logging) {
// allow to configure individual logging levels in
application.properties
Properties prop = loadProfileProperties(baseDir);
@@ -2677,15 +2653,13 @@ public class Run extends CamelCommand {
}
}
}
- if (!exportRun) {
- RuntimeUtil.configureLog(loggingOptions.loggingLevel,
loggingOptions.loggingColor,
- loggingOptions.loggingJson, scriptRun, false,
loggingOptions.loggingConfigPath,
- loggingOptions.loggingCategory, mcpStdioMode);
- }
+ RuntimeUtil.configureLog(loggingOptions.loggingLevel,
loggingOptions.loggingColor,
+ loggingOptions.loggingJson, scriptRun, false,
loggingOptions.loggingConfigPath,
+ loggingOptions.loggingCategory, mcpStdioMode);
writeSettings("loggingLevel", loggingOptions.loggingLevel);
writeSettings("loggingColor", loggingOptions.loggingColor ? "true"
: "false");
writeSettings("loggingJson", loggingOptions.loggingJson ? "true" :
"false");
- if (!scriptRun && !exportRun) {
+ if (!scriptRun) {
// remember log file
String name = RuntimeUtil.getPid() + ".log";
final Path logDir = CommandLineHelper.getCamelDir();
@@ -2700,12 +2674,8 @@ public class Run extends CamelCommand {
}
}
} else {
- if (exportRun) {
- writeSettings("loggingLevel", loggingOptions.loggingLevel);
- } else {
- RuntimeUtil.configureLog("off", false, false, false, false,
null, null);
- writeSettings("loggingLevel", "off");
- }
+ RuntimeUtil.configureLog("off", false, false, false, false, null,
null);
+ writeSettings("loggingLevel", "off");
}
}
@@ -2916,8 +2886,8 @@ public class Run extends CamelCommand {
@Override
protected Printer printer() {
- if (mcpStdioMode || (exportRun && (!loggingOptions.logging &&
!verbose))) {
- // MCP stdio mode and export silent runs must not write
diagnostics to stdout
+ if (mcpStdioMode) {
+ // MCP stdio mode must not write diagnostics to stdout
if (quietPrinter == null) {
quietPrinter = new Printer.QuietPrinter(super.printer());
}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/RunDependencyResolver.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/RunDependencyResolver.java
deleted file mode 100644
index a9756284e456..000000000000
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/RunDependencyResolver.java
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.camel.dsl.jbang.core.commands;
-
-import java.util.Properties;
-
-import org.apache.camel.dsl.jbang.export.DependencyResolver;
-import org.apache.camel.dsl.jbang.export.ExportRequest;
-
-/**
- * Resolves the dependencies of an export by booting the integration silently,
the same way as camel run does, which
- * writes the run settings that the exporter reads.
- */
-public class RunDependencyResolver implements DependencyResolver {
-
- private final CamelJBangMain main;
-
- public RunDependencyResolver() {
- this(new CamelJBangMain());
- }
-
- public RunDependencyResolver(CamelJBangMain main) {
- this.main = main;
- }
-
- @Override
- public int resolve(ExportRequest request) throws Exception {
- Run run = new Run(main);
- // need to declare the profile to use for run
- run.exportBaseDir = request.exportBaseDir;
- run.workDir = request.workDir;
- run.dependencies = request.dependencies;
- run.files = request.files;
- run.name = request.name;
- run.serverOptions.port = request.port;
- run.serverOptions.managementPort = request.managementPort;
- run.excludes = request.excludes;
- run.openapi = request.openapi;
- run.serverOptions.observe = request.observe;
- run.serverOptions.console = request.console;
- run.mavenResolver = MavenResolverMixin.of(new Properties(),
request.mavenResolver);
- run.packageScanJars = request.packageScanJars;
- run.runtime = request.runtime;
- run.camelVersion = request.camelVersion;
- run.camelSpringBootVersion = request.camelSpringBootVersion;
- run.quarkusPlatform = QuarkusPlatformMixin.of(new Properties(),
request.quarkusPlatform);
- run.springBootVersion = request.springBootVersion;
- run.kameletsVersion = request.kameletsVersion;
- run.javaVersion = request.javaVersion;
- run.localKameletDir = request.localKameletDir;
- run.resourceDirs = request.resourceDirs;
- run.ignoreLoadingError = request.ignoreLoadingError;
- run.lazyBean = request.lazyBean;
- run.property = request.applicationProperties;
- run.verbose = request.verbose;
- run.loggingOptions.logging = request.logging;
- return run.runExport(request.ignoreLoadingError);
- }
-}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ExportApiTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ExportApiTest.java
index 130c42e9a7c5..315fc3837c4e 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ExportApiTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ExportApiTest.java
@@ -27,6 +27,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
import org.apache.camel.CamelContext;
+import org.apache.camel.dsl.jbang.core.commands.ai.YamlLoadFailureReport;
import org.apache.camel.dsl.jbang.export.ExportContext;
import org.apache.camel.dsl.jbang.export.ExportRequest;
import org.apache.camel.dsl.jbang.export.ProjectExporter;
@@ -34,19 +35,26 @@ import
org.apache.camel.dsl.jbang.export.common.ExportHelper;
import org.apache.camel.dsl.jbang.export.common.RuntimeType;
import org.apache.camel.dsl.jbang.export.common.StringPrinter;
import org.apache.camel.spi.CamelContextTracker;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.core.Appender;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.Logger;
import org.apache.logging.log4j.core.LoggerContext;
+import org.apache.logging.log4j.core.appender.AbstractAppender;
import org.apache.logging.log4j.core.config.Configuration;
+import org.apache.logging.log4j.core.config.Property;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.awaitility.Awaitility.await;
/**
- * Exports through the Java API with the dependency resolver of
camel-jbang-core (the silent run), as an application
- * that embeds the export does: the export keeps to its work directory, leaves
the logging and the system properties of
- * the JVM as they are, and releases its silent run.
+ * Exports through the Java API with the default dependency resolver (the
silent run), as an application that embeds the
+ * export does: the export keeps to its work directory, leaves the logging and
the system properties of the JVM as they
+ * are, and releases its silent run.
*/
class ExportApiTest {
@@ -124,6 +132,40 @@ class ExportApiTest {
return request;
}
+ @Test
+ void aYamlRouteThatDoesNotLoadIsReportedAsCamelRunDoes() throws Exception {
+ Files.writeString(route, """
+ - route:
+ from:
+ uri: timer:tick
+ steps:
+ - pollEnrich:
+ uri: file:./order.json
+ """);
+ List<String> logged = new CopyOnWriteArrayList<>();
+ Appender appender = new AbstractAppender("report", null, null, true,
Property.EMPTY_ARRAY) {
+ @Override
+ public void append(LogEvent event) {
+ logged.add(event.getMessage().getFormattedMessage());
+ }
+ };
+ appender.start();
+ Logger logger = (Logger)
LogManager.getLogger(YamlLoadFailureReport.class);
+ logger.addAppender(appender);
+ try {
+ ExportContext context = CliExportContext.create(new
CamelJBangMain(), new StringPrinter(), true);
+ assertThatThrownBy(() -> new
ProjectExporter(context).export(request(0))).isNotNull();
+ } finally {
+ logger.removeAppender(appender);
+ appender.stop();
+ }
+
+ // the report of camel validate yaml, as camel run logs it
(CAMEL-24851)
+ assertThat(logged).singleElement().asString()
+ .startsWith("The route file did not load. camel validate yaml
says what to write:")
+ .contains("hello.camel.yaml", "pollEnrich");
+ }
+
private void export(int i) throws Exception {
StringPrinter printer = new StringPrinter();
assertThat(new ProjectExporter(new
ExportContext().withPrinter(printer)).export(request(i)))
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/RunExportGoldenTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/RunExportGoldenTest.java
index fd746e8d40d0..5202864ae17c 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/RunExportGoldenTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/RunExportGoldenTest.java
@@ -36,17 +36,16 @@ import java.util.stream.Stream;
import org.apache.camel.catalog.DefaultCamelCatalog;
import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
-import org.apache.camel.dsl.jbang.export.ExportContext;
import org.apache.camel.dsl.jbang.export.common.ExportHelper;
import org.apache.camel.dsl.jbang.export.common.PathUtils;
import org.apache.camel.dsl.jbang.export.common.RuntimeType;
-import org.apache.camel.dsl.jbang.export.run.SilentRunDependencyResolver;
import org.apache.camel.main.KameletMain;
import org.apache.maven.model.Dependency;
import org.apache.maven.model.Model;
import org.apache.maven.model.io.xpp3.MavenXpp3Reader;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
@@ -122,6 +121,8 @@ class RunExportGoldenTest {
"--management-port=9191", "--observe", "--console",
"--gav=com.acme:golden:2.0"),
// camel run of a pom.xml runs the Maven project in its own JVM
new Case("pom-project", files("pomproject/pom.xml"),
List.of(), true, false),
+ // how camel run starts (in the background, with a debugger)
changes nothing in an export
+ new Case("run-settings", files("runsettings"), List.of(),
true, false),
// the options of camel run only
run("run-options", files("options/hello.camel.yaml"),
"--properties=" +
GOLDEN.resolve("options/extra.properties"), "--prop=foo=bar",
@@ -160,22 +161,41 @@ class RunExportGoldenTest {
export(c, out, new Export(new CamelJBangMain()));
}
+ private void export(Case c, Path out, Export command) throws Exception {
+ verify(c.name() + ".export.txt", exportOutput(c, out, command));
+ }
+
/**
- * The same export, with the dependency resolver of camel-jbang-export
instead of the silent camel run of
- * camel-jbang-core: it must give the same run settings and project.
+ * camel.jbang.background and camel.jbang.jvmDebug in
application.properties only say how camel run starts: the
+ * export boots the integration in its own JVM all the same, and gives the
same project as without them.
*/
- @ParameterizedTest(name = "{0}")
- @MethodSource("exportCases")
- void exportWithSilentRunResolver(Case c, @TempDir Path out) throws
Exception {
- export(c, out, new Export(new CamelJBangMain()) {
- @Override
- protected ExportContext exportContext() {
- return super.exportContext().withDependencyResolver(new
SilentRunDependencyResolver());
+ @Test
+ void exportIgnoresHowCamelRunStarts(@TempDir Path dir) throws Exception {
+ Path with = copyFixture("runsettings", dir.resolve("with"));
+ Path without = copyFixture("runsettings", dir.resolve("without"));
+ Path props = without.resolve("application.properties");
+ Files.writeString(props,
Files.readString(props).replaceAll("(?m)^camel\\.jbang\\.(background|jvmDebug)=.*\\R",
""));
+
+ String withOutput = exportOutput(new Case("run-settings",
files(with.toString()), List.of(), true, false),
+ dir.resolve("with-out"), new Export(new
CamelJBangMain())).replace(with.toString(), "${fixture}");
+ String withoutOutput = exportOutput(new Case("run-settings",
files(without.toString()), List.of(), true, false),
+ dir.resolve("without-out"), new Export(new
CamelJBangMain())).replace(without.toString(), "${fixture}");
+
+
assertThat(Files.readString(props)).doesNotContain("camel.jbang.background",
"camel.jbang.jvmDebug");
+ assertThat(withOutput).isEqualTo(withoutOutput);
+ }
+
+ private static Path copyFixture(String name, Path target) throws Exception
{
+ Files.createDirectories(target);
+ try (Stream<Path> files = Files.list(GOLDEN.resolve(name))) {
+ for (Path f : files.toList()) {
+ Files.copy(f, target.resolve(f.getFileName()));
}
- });
+ }
+ return target.toRealPath();
}
- private void export(Case c, Path out, Export command) throws Exception {
+ private String exportOutput(Case c, Path out, Export command) throws
Exception {
List<String> args = new ArrayList<>(
List.of("--runtime=main", "--quiet", "--dir=" + out,
CamelCommandBaseTestSupport.quarkusExtRegistry()));
if (c.args().stream().noneMatch(a -> a.startsWith("--gav="))) {
@@ -191,7 +211,7 @@ class RunExportGoldenTest {
sb.append("# run settings\n");
sb.append(settings(CommandLineHelper.getWorkDir().resolve(ExportHelper.RUN_SETTINGS_FILE)));
sb.append(project(out));
- verify(c.name() + ".export.txt", normalize(sb.toString(), out));
+ return normalize(sb.toString(), out);
}
/**
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/resources/golden/expected/run-settings.export.txt
b/dsl/camel-jbang/camel-jbang-core/src/test/resources/golden/expected/run-settings.export.txt
new file mode 100644
index 000000000000..391b1c88188a
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/resources/golden/expected/run-settings.export.txt
@@ -0,0 +1,64 @@
+# run settings
+camel.main.modeline=true
+camel.jbang.lazyBean=true
+camel.jbang.compileWorkDir=.camel-jbang/compile
+camel.jbang.download=true
+camel.jbang.fresh=false
+camel.jbang.health=false
+camel.jbang.metrics=false
+camel.jbang.console=false
+camel.jbang.mcp=false
+camel.jbang.openapiUi=false
+camel.jbang.verbose=false
+camel.jbang.camel-version=${camel.version}
+camel.jbang.springBootVersion=${spring-boot.version}
+camel.jbang.quarkusGroupId=io.quarkus.platform
+camel.jbang.quarkusVersion=3.35.3
+camel.jbang.quarkusExtensionRegistryBaseUri=file\://${basedir}/target/test-classes/registry.quarkus.io
+camel.jbang.javaVersion=21
+camel.jbang.kameletsVersion=${kamelets.version}
+camel.main.routesIncludePattern=file\:src/test/resources/golden/runsettings/hello.camel.yaml
+camel.component.properties.location=file\:src/test/resources/golden/runsettings/application.properties
+dependency=mvn\:org.apache.camel\:camel-kamelet\:${camel.version}
+dependency=mvn\:org.apache.camel.kamelets\:camel-kamelets\:${kamelets.version}
+dependency=mvn\:org.apache.camel\:camel-yaml-dsl\:${camel.version}
+dependency=mvn\:org.apache.camel\:camel-timer\:${camel.version}
+dependency=mvn\:org.apache.camel\:camel-core-languages\:${camel.version}
+dependency=mvn\:org.apache.camel\:camel-log\:${camel.version}
+
+# project files
+.mvn/wrapper/maven-wrapper.jar
+.mvn/wrapper/maven-wrapper.properties
+AGENTS.md
+mvnw
+mvnw.cmd
+pom.xml
+readme.md
+src/main/docker/Dockerfile
+src/main/java/org/example/golden/runsettings/CamelApplication.java
+src/main/resources/application.properties
+src/main/resources/camel/hello.camel.yaml
+src/main/resources/log4j2.component.properties
+src/main/resources/log4j2.properties
+
+# pom.xml
+org.example.golden:run-settings:1.0.0
+properties: [java.version, project.build.outputTimestamp]
+dependency org.apache.camel:camel-main
+dependency org.apache.camel:camel-log
+dependency org.apache.camel:camel-timer
+dependency org.apache.camel:camel-yaml-dsl
+dependency org.fusesource.jansi:jansi
+dependency org.apache.logging.log4j:log4j-api
+dependency org.apache.logging.log4j:log4j-slf4j2-impl
+dependency org.apache.logging.log4j:log4j-core
+dependency org.apache.camel:camel-test-junit6 (test)
+repository https://repository.apache.org/content/groups/snapshots/
+plugin org.apache.maven.plugins:maven-compiler-plugin
+plugin org.apache.camel:camel-maven-plugin
+plugin org.apache.maven.plugins:maven-jar-plugin
+plugin org.apache.maven.plugins:maven-dependency-plugin
+plugin org.apache.camel:camel-repackager-maven-plugin
+
+# application.properties
+camel.main.basePackageScan=org.example.golden.runsettings
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/resources/golden/runsettings/application.properties
b/dsl/camel-jbang/camel-jbang-core/src/test/resources/golden/runsettings/application.properties
new file mode 100644
index 000000000000..00afd397f4f8
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/resources/golden/runsettings/application.properties
@@ -0,0 +1,19 @@
+## ---------------------------------------------------------------------------
+## 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.
+## ---------------------------------------------------------------------------
+# how camel run starts: a separate JVM in the background, with a debugger
+camel.jbang.background=true
+camel.jbang.jvmDebug=5005
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/META-INF/services/org.apache.camel.dsl.jbang.export.DependencyResolver
b/dsl/camel-jbang/camel-jbang-core/src/test/resources/golden/runsettings/hello.camel.yaml
similarity index 82%
rename from
dsl/camel-jbang/camel-jbang-core/src/main/resources/META-INF/services/org.apache.camel.dsl.jbang.export.DependencyResolver
rename to
dsl/camel-jbang/camel-jbang-core/src/test/resources/golden/runsettings/hello.camel.yaml
index 992328264d7e..52b940f65f1e 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/resources/META-INF/services/org.apache.camel.dsl.jbang.export.DependencyResolver
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/resources/golden/runsettings/hello.camel.yaml
@@ -6,7 +6,7 @@
# (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
+# 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,
@@ -15,4 +15,9 @@
# limitations under the License.
#
-org.apache.camel.dsl.jbang.core.commands.RunDependencyResolver
+- from:
+ uri: timer:hello
+ steps:
+ - setBody:
+ constant: Hello
+ - to: log:hello
diff --git
a/dsl/camel-jbang/camel-jbang-export/src/main/java/org/apache/camel/dsl/jbang/export/DependencyResolver.java
b/dsl/camel-jbang/camel-jbang-export/src/main/java/org/apache/camel/dsl/jbang/export/DependencyResolver.java
index 61d9255a884a..b8f0b1f68599 100644
---
a/dsl/camel-jbang/camel-jbang-export/src/main/java/org/apache/camel/dsl/jbang/export/DependencyResolver.java
+++
b/dsl/camel-jbang/camel-jbang-export/src/main/java/org/apache/camel/dsl/jbang/export/DependencyResolver.java
@@ -26,8 +26,8 @@ import org.apache.camel.dsl.jbang.export.common.ExportHelper;
* {@link ExportHelper#RUN_SETTINGS_FILE} in the work directory of the request
({@link ExportRequest#workDir}), which
* the exporter then reads.
* <p>
- * camel-jbang-core provides the implementation that boots the integration the
same way as camel run does, and registers
- * it with the {@link java.util.ServiceLoader}.
+ * The default is {@link
org.apache.camel.dsl.jbang.export.run.SilentRunDependencyResolver}, which boots
the integration
+ * the same way as camel run does.
*
* @since 4.24
*/
diff --git
a/dsl/camel-jbang/camel-jbang-export/src/main/java/org/apache/camel/dsl/jbang/export/ExportContext.java
b/dsl/camel-jbang/camel-jbang-export/src/main/java/org/apache/camel/dsl/jbang/export/ExportContext.java
index 65dde0732ad0..39b816d4f003 100644
---
a/dsl/camel-jbang/camel-jbang-export/src/main/java/org/apache/camel/dsl/jbang/export/ExportContext.java
+++
b/dsl/camel-jbang/camel-jbang-export/src/main/java/org/apache/camel/dsl/jbang/export/ExportContext.java
@@ -17,19 +17,20 @@
package org.apache.camel.dsl.jbang.export;
import java.util.Collection;
-import java.util.Iterator;
import java.util.List;
-import java.util.ServiceLoader;
+import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Predicate;
import org.apache.camel.dsl.jbang.export.common.PluginExporter;
import org.apache.camel.dsl.jbang.export.common.PluginRunCustomizer;
import org.apache.camel.dsl.jbang.export.common.Printer;
+import org.apache.camel.dsl.jbang.export.run.SilentRunDependencyResolver;
/**
* What an export uses from its caller: where it prints, how it resolves the
dependencies, the exporters of the active
- * plugins, and how it asks for confirmation (such as before it deletes the
files of the export directory).
+ * plugins, what it reports when the integration fails to start, and how it
asks for confirmation (such as before it
+ * deletes the files of the export directory).
* <p>
* An ExportContext is not thread-safe: configure it, and use it for one
export at a time.
*
@@ -38,9 +39,11 @@ import org.apache.camel.dsl.jbang.export.common.Printer;
public class ExportContext {
private Printer printer = new Printer.SystemOutPrinter();
- private DependencyResolver dependencyResolver;
+ private DependencyResolver dependencyResolver = new
SilentRunDependencyResolver();
private Function<String, Collection<PluginExporter>> pluginExporters =
repos -> List.of();
private Function<String, Collection<PluginRunCustomizer>>
pluginRunCustomizers = repos -> List.of();
+ private BiConsumer<Exception, List<String>> startFailureListener =
(failure, files) -> {
+ };
private Predicate<String> confirm = message -> true;
public Printer getPrinter() {
@@ -55,24 +58,14 @@ public class ExportContext {
return this;
}
- /**
- * The resolver of the dependencies; when none is set, the one registered
with the {@link ServiceLoader} is used.
- */
public DependencyResolver getDependencyResolver() {
- if (dependencyResolver == null) {
- Iterator<DependencyResolver> it
- = ServiceLoader.load(DependencyResolver.class,
ExportContext.class.getClassLoader()).iterator();
- if (!it.hasNext()) {
- throw new IllegalStateException(
- "No " + DependencyResolver.class.getName()
- + " found: add
camel-jbang-core to the classpath, or set a resolver on the "
- +
ExportContext.class.getSimpleName());
- }
- dependencyResolver = it.next();
- }
return dependencyResolver;
}
+ /**
+ * The resolver of the dependencies (default {@link
SilentRunDependencyResolver}, which boots the integration
+ * silently in this JVM).
+ */
public ExportContext withDependencyResolver(DependencyResolver
dependencyResolver) {
this.dependencyResolver = dependencyResolver;
return this;
@@ -109,6 +102,24 @@ public class ExportContext {
return this;
}
+ /**
+ * Called when the silent run of the export fails to start, with the
failure and the files of the run, before the
+ * error is reported.
+ */
+ public void onStartFailure(Exception failure, List<String> files) {
+ startFailureListener.accept(failure, files);
+ }
+
+ /**
+ * What to report when the silent run of the export fails to start, given
the failure and the files of the run
+ * (default nothing): {@code camel export} logs what {@code camel validate
yaml} says about a YAML route file that
+ * did not load.
+ */
+ public ExportContext withStartFailureListener(BiConsumer<Exception,
List<String>> startFailureListener) {
+ this.startFailureListener = startFailureListener;
+ return this;
+ }
+
/**
* Whether the given operation, such as cleaning the export directory, may
proceed.
*/
diff --git
a/dsl/camel-jbang/camel-jbang-export/src/main/java/org/apache/camel/dsl/jbang/export/run/SilentRunDependencyResolver.java
b/dsl/camel-jbang/camel-jbang-export/src/main/java/org/apache/camel/dsl/jbang/export/run/SilentRunDependencyResolver.java
index 5cf813e583f7..ee17ec9e016f 100644
---
a/dsl/camel-jbang/camel-jbang-export/src/main/java/org/apache/camel/dsl/jbang/export/run/SilentRunDependencyResolver.java
+++
b/dsl/camel-jbang/camel-jbang-export/src/main/java/org/apache/camel/dsl/jbang/export/run/SilentRunDependencyResolver.java
@@ -18,7 +18,9 @@ package org.apache.camel.dsl.jbang.export.run;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.util.ArrayList;
import java.util.LinkedHashSet;
+import java.util.List;
import java.util.Properties;
import java.util.stream.Collectors;
@@ -40,8 +42,8 @@ import org.apache.camel.util.StringHelper;
* stubs and without starting the routes, and writes the run settings, which
the export reads. It uses the run settings
* pipeline of this module, so it needs neither picocli nor camel-jbang-core.
* <p>
- * The printer and the plugins (their run customizers and exporters) come from
the {@link ExportContext}. Logging is not
- * configured: the export logs as its caller has configured.
+ * The printer, the plugins (their run customizers and exporters) and what to
report when the integration fails to start
+ * come from the {@link ExportContext}. Logging is not configured: the export
logs as its caller has configured.
*
* @since 4.24
*/
@@ -118,6 +120,10 @@ public class SilentRunDependencyResolver implements
DependencyResolver {
return exit;
}
+ // such as the report of the validator for a YAML route file that did
not load (CAMEL-24851)
+ final List<String> runFiles = new ArrayList<>(request.files);
+ main.setStartFailureListener(ex -> context.onStartFailure(ex,
runFiles));
+
try {
main.start();
main.run();
diff --git
a/dsl/camel-jbang/camel-jbang-export/src/test/java/org/apache/camel/dsl/jbang/export/run/SilentRunDependencyResolverTest.java
b/dsl/camel-jbang/camel-jbang-export/src/test/java/org/apache/camel/dsl/jbang/export/run/SilentRunDependencyResolverTest.java
index b1e44baa830c..290c55a286ca 100644
---
a/dsl/camel-jbang/camel-jbang-export/src/test/java/org/apache/camel/dsl/jbang/export/run/SilentRunDependencyResolverTest.java
+++
b/dsl/camel-jbang/camel-jbang-export/src/test/java/org/apache/camel/dsl/jbang/export/run/SilentRunDependencyResolverTest.java
@@ -70,8 +70,9 @@ class SilentRunDependencyResolverTest {
ExportRequest request = request();
StringPrinter printer = new StringPrinter();
- ExportContext context = new ExportContext().withPrinter(printer)
- .withDependencyResolver(new SilentRunDependencyResolver());
+ // the default resolver of the export
+ ExportContext context = new ExportContext().withPrinter(printer);
+
assertThat(context.getDependencyResolver()).isInstanceOf(SilentRunDependencyResolver.class);
assertThat(new
ProjectExporter(context).export(request)).as(printer.getOutput()).isZero();
@@ -112,6 +113,43 @@ class SilentRunDependencyResolverTest {
.contains("camel.jbang.dependencies=camel\\:jackson");
}
+ @Test
+ void aRouteThatDoesNotLoadIsGivenToTheStartFailureListener() throws
Exception {
+ Files.writeString(route, """
+ - route:
+ from:
+ uri: timer:hello
+ steps:
+ - pollEnrich:
+ uri: file:./order.json
+ """);
+ List<Exception> failures = new CopyOnWriteArrayList<>();
+ List<List<String>> runFiles = new CopyOnWriteArrayList<>();
+ ExportContext context = new ExportContext().withPrinter(new
StringPrinter())
+ .withStartFailureListener((failure, files) -> {
+ failures.add(failure);
+ runFiles.add(files);
+ });
+
+ assertThatThrownBy(() -> new
SilentRunDependencyResolver().resolve(request(), context)).isNotNull();
+
+ assertThat(failures).hasSize(1);
+ assertThat(runFiles).containsExactly(List.of(route.toString()));
+ }
+
+ @Test
+ void aClipboardFileThatDoesNotExistIsNotFound() throws Exception {
+ // camel run reads such a file from the clipboard; the export does not
+ ExportRequest request = request();
+ request.files.clear();
+ request.files.add("clipboard.yaml");
+ StringPrinter printer = new StringPrinter();
+
+ assertThat(new SilentRunDependencyResolver().resolve(request, new
ExportContext().withPrinter(printer)))
+ .isEqualTo(RunPipeline.FILE_NOT_FOUND);
+ assertThat(printer.getStderr()).contains("File does not exist:
file:clipboard.yaml");
+ }
+
private ExportRequest request() {
ExportRequest request = new ExportRequest();
request.files.add(route.toString());