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 b918198190c5 CAMEL-26929: camel-jbang - camel cmd send without a name
sends to the integration that runs (#29914)
b918198190c5 is described below
commit b918198190c5daba4944315ce67953920ce3b1fc
Author: Claus Ibsen <[email protected]>
AuthorDate: Sat Nov 14 19:23:13 2026 +0100
CAMEL-26929: camel-jbang - camel cmd send without a name sends to the
integration that runs (#29914)
Without a name, camel cmd send sent from an empty Camel of the CLI: without
--endpoint nothing was sent and it said "Sent (success)", and an endpoint
that
only an integration has (direct:, seda:, stub:, a route id) waited for the
timeout. camel-jbang-tips recommends camel cmd send --body=... after camel
run
--stub=jms.
Now such a send goes to the integration that runs (with none or several
running
it exits 3 and says what to give); an endpoint of a system outside the
integrations, such as kafka:topic, is still sent from the CLI. The send
console
reports a send without an endpoint to a Camel without routes as an error.
CAMEL-26929: upgrade guide - shorter sentences in the camel cmd send entry
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../apache/camel/impl/console/SendDevConsole.java | 19 ++++--
.../impl/console/SendDevConsoleNoRoutesTest.java | 46 ++++++++++++++
.../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 9 +++
.../modules/ROOT/pages/camel-jbang-devtools.adoc | 8 ++-
.../core/commands/action/CamelSendAction.java | 48 +++++++++++++-
.../core/commands/action/CamelSendActionTest.java | 73 ++++++++++++++++++++++
.../src/test/resources/golden/cli-help.txt | 6 +-
7 files changed, 200 insertions(+), 9 deletions(-)
diff --git
a/core/camel-console/src/main/java/org/apache/camel/impl/console/SendDevConsole.java
b/core/camel-console/src/main/java/org/apache/camel/impl/console/SendDevConsole.java
index cc83ced9e0f1..0ae6c8e82ef5 100644
---
a/core/camel-console/src/main/java/org/apache/camel/impl/console/SendDevConsole.java
+++
b/core/camel-console/src/main/java/org/apache/camel/impl/console/SendDevConsole.java
@@ -154,8 +154,8 @@ public class SendDevConsole extends AbstractDevConsole {
} catch (Exception e) {
cause = e;
}
- if (endpoint != null && target == null) {
- cause = new NoSuchEndpointException(endpoint);
+ if (target == null) {
+ cause = noTarget(endpoint);
}
if (out != null && out.getException() != null) {
cause = out.getException();
@@ -219,8 +219,8 @@ public class SendDevConsole extends AbstractDevConsole {
} catch (Exception e) {
cause = e;
}
- if (cause == null && endpoint != null && target == null) {
- cause = new NoSuchEndpointException(endpoint);
+ if (cause == null && target == null) {
+ cause = noTarget(endpoint);
}
if (out != null && out.getException() != null) {
cause = out.getException();
@@ -284,6 +284,17 @@ public class SendDevConsole extends AbstractDevConsole {
return out;
}
+ /**
+ * Why nothing was sent: no endpoint matches, or, without an endpoint,
there is no route to send to (before, that
+ * was reported as sent).
+ */
+ private static Exception noTarget(String endpoint) {
+ if (endpoint != null) {
+ return new NoSuchEndpointException(endpoint);
+ }
+ return new IllegalArgumentException("No endpoint to send to: give the
endpoint, as there are no routes");
+ }
+
private Endpoint findTarget(String endpoint) {
Endpoint target = null;
if (endpoint == null) {
diff --git
a/core/camel-console/src/test/java/org/apache/camel/impl/console/SendDevConsoleNoRoutesTest.java
b/core/camel-console/src/test/java/org/apache/camel/impl/console/SendDevConsoleNoRoutesTest.java
new file mode 100644
index 000000000000..c280da3e82c9
--- /dev/null
+++
b/core/camel-console/src/test/java/org/apache/camel/impl/console/SendDevConsoleNoRoutesTest.java
@@ -0,0 +1,46 @@
+/*
+ * 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.impl.console;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.camel.console.DevConsole;
+import org.apache.camel.util.json.JsonObject;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * A send without an endpoint to a Camel without routes sends nothing, and
says so (it was reported as sent).
+ */
+public class SendDevConsoleNoRoutesTest extends AbstractDevConsoleTest {
+
+ @Test
+ public void aSendWithoutEndpointAndRoutesIsAnError() {
+ DevConsole console = assertConsoleExists("send");
+ Map<String, Object> options = new HashMap<>();
+ options.put(SendDevConsole.BODY, "Hello");
+
+ JsonObject json = callJson(console, options);
+ assertEquals("error", json.getString("status"));
+ String text = callText(console, options);
+ assertTrue(text.contains("Status: error"), text);
+ assertTrue(text.contains("No endpoint to send to: give the endpoint,
as there are no routes"), text);
+ }
+}
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 3e3659e01173..29d263a043d3 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
@@ -1479,6 +1479,15 @@ A failure of the fallback itself is now also caught by
the `doCatch` (before, it
To have those steps use the error handler of the route, move them out of the
`doTry` block.
+=== camel-jbang - camel cmd send without a name sends to the integration that
runs
+
+`camel cmd send` without an integration name started a Camel of its own,
without routes, and sent from there. That is
+still so for an endpoint of a system outside the integrations (such as
`kafka:topic`). Without `--endpoint`, the
+message went nowhere and was reported as sent. An endpoint that only an
integration has (`direct:`, `seda:`, `stub:`,
+a route id) waited for the timeout. Now both go to the integration that runs.
With none running, or several, the
+command exits with `3` and says what to give. A send without an endpoint to an
integration without routes is now
+an error, rather than reported as sent.
+
=== camel-aws2-kinesis - each endpoint uses its own client
Each `aws2-kinesis` endpoint now uses the Kinesis client of its own
configuration (`region`, credentials,
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-devtools.adoc
b/docs/user-manual/modules/ROOT/pages/camel-jbang-devtools.adoc
index 019ef6c84562..b30d28b846fc 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-devtools.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-devtools.adoc
@@ -97,8 +97,8 @@ rules can be tried on it (see
xref:next@components:others:chaos.adoc[camel-chaos
*Available since Camel 4.12*
-Omit the integration name to start a temporary Camel instance and send
directly.
-All configuration must be provided on the command line:
+To send to a system outside the integrations, such as a broker, omit the
integration name: the CLI starts a
+temporary Camel instance and sends directly. All configuration must be
provided on the command line:
[source,bash]
----
@@ -112,6 +112,10 @@ For complex components (Kafka, AWS, etc.), a Kamelet is
often simpler:
$ camel cmd send --body=file:payload.json
--uri='kamelet:mqtt-sink?brokerUrl=tcp://mybroker:1883&topic=temperature'
----
+Without a name, a message for an integration goes to the integration that
runs: with no `--endpoint` (the first
+route), or an endpoint that only an integration has (`direct:`, `seda:`,
`stub:`, a route id or pattern), such as
+after `camel run --stub=jms`. When several integrations run, give the name or
pid of the one to send to.
+
=== Sending to infrastructure services
*Available since Camel 4.18*
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSendAction.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSendAction.java
index 9dd35d79cc17..87b43b832621 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSendAction.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSendAction.java
@@ -27,7 +27,9 @@ import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
+import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
@@ -51,6 +53,7 @@ import
org.apache.camel.dsl.jbang.core.common.ActionClient.ActionResult;
import org.apache.camel.dsl.jbang.core.common.CliException;
import org.apache.camel.dsl.jbang.core.common.CliExitCode;
import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
+import org.apache.camel.dsl.jbang.core.common.ProcessFinder;
import org.apache.camel.dsl.jbang.core.common.ProcessFinder.CamelProcess;
import org.apache.camel.dsl.jbang.core.common.TimeoutConverter;
import org.apache.camel.dsl.jbang.run.ConfiguredRun;
@@ -78,7 +81,9 @@ import org.jline.jansi.Ansi;
" camel cmd send --endpoint=kafka:refunds
--body=file:test/chaos/refunds-fails/" })
public class CamelSendAction extends ActionBaseCommand {
- @Argument(description = "To use an existing running Camel integration for
sending the message (name or pid)")
+ @Argument(description = "The running Camel integration to send to (name or
pid). Without it, the integration that"
+ + " runs, unless --endpoint is a system outside
the integrations, such as kafka:topic,"
+ + " which the CLI sends to itself")
String name;
@Option(name = "properties",
@@ -210,11 +215,52 @@ public class CamelSendAction extends ActionBaseCommand {
return doCallInfra(infra);
} else if (name != null) {
return doCall(name);
+ } else if (endpoint == null || isInsideAnIntegration(endpoint)) {
+ // the Camel of this command has no routes: camel cmd send
--body=... is for the integration that runs
+ return doCall(String.valueOf(theRunningIntegration().pid()));
} else {
+ // a system outside the integrations, such as kafka:topic, from
the Camel of this command
return doCallLocal();
}
}
+ /**
+ * The schemes of the endpoints that only an integration has: a send from
the Camel of this command reaches nobody.
+ */
+ private static final Set<String> INTERNAL_SCHEMES
+ = Set.of("direct", "seda", "stub", "disruptor", "disruptor-vm",
"mock", "dataset", "vm");
+
+ /** Whether the endpoint is a route id, a pattern or an endpoint in the
memory of an integration. */
+ static boolean isInsideAnIntegration(String endpoint) {
+ int colon = endpoint.indexOf(':');
+ return colon < 0 || endpoint.endsWith("*")
+ || INTERNAL_SCHEMES.contains(endpoint.substring(0,
colon).toLowerCase(Locale.ROOT));
+ }
+
+ /**
+ * The one integration that runs, for a send without a name.
+ *
+ * @throws CliException (exit code 3) when none runs, or several
+ */
+ private CamelProcess theRunningIntegration() {
+ List<CamelProcess> running = ProcessFinder.find(null);
+ if (running.size() == 1) {
+ return running.get(0);
+ }
+ if (running.isEmpty()) {
+ throw CliException.notFound(endpoint == null
+ ? "No running Camel integration to send to: start one with
camel run, or give --endpoint to send"
+ + " to a system such as kafka:topic"
+ : "No running Camel integration to send to: " + endpoint +
" is in an integration, so start it"
+ + " with camel run first");
+ }
+ String names = running.stream()
+ .map(p -> p.name() != null && !p.name().isEmpty() ? p.name() +
" (pid " + p.pid() + ")" : "pid " + p.pid())
+ .collect(Collectors.joining(", "));
+ throw CliException.notFound(running.size() + " Camel integrations are
running: " + names
+ + ". Give the name or pid of the one to
send to.");
+ }
+
/**
* The options of the send action.
*/
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSendActionTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSendActionTest.java
index 670df18a15b6..dffe0c03eb2c 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSendActionTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSendActionTest.java
@@ -25,7 +25,9 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -73,6 +75,77 @@ class CamelSendActionTest extends ActionCommandTestSupport {
assertTrue(printer.getOutput().isEmpty(), "nothing should be printed,
was: " + printer.getOutput());
}
+ @Test
+ void withoutANameTheMessageGoesToTheIntegrationThatRuns() throws Exception
{
+ // camel run myroute.java --stub=jms, then camel cmd send --body=...
(camel-jbang-tips.adoc): before, an empty
+ // Camel of the CLI took the message, which sent nothing and reported
success (CAMEL-26929)
+ writeStatusFile(TEST_PID, "myApp");
+
+ CamelSendAction command = new CamelSendAction(new
CamelJBangMain().withPrinter(printer));
+ command.body = "Something here";
+ command.loggingColor = false;
+
+ assertEquals(0, callWithResponse(command, sentResponse()));
+
+ JsonObject action = readActionFile(TEST_PID);
+ assertNotNull(action, "the running integration should get the send");
+ assertEquals("Something here", action.getString("body"));
+ assertNull(action.get("endpoint"));
+ }
+
+ @Test
+ void withoutANameAnEndpointOfAnIntegrationGoesToTheIntegrationThatRuns()
throws Exception {
+ writeStatusFile(TEST_PID, "myApp");
+
+ CamelSendAction command = new CamelSendAction(new
CamelJBangMain().withPrinter(printer));
+ command.endpoint = "direct:orders";
+ command.loggingColor = false;
+
+ assertEquals(0, callWithResponse(command, sentResponse()));
+
+ assertEquals("direct:orders",
readActionFile(TEST_PID).getString("endpoint"));
+ }
+
+ @Test
+ void withoutANameAndNoIntegrationRunningTheSendIsRefused() {
+ CamelSendAction command = new CamelSendAction(new
CamelJBangMain().withPrinter(printer));
+ command.endpoint = "seda:orders";
+
+ CliException e = assertThrows(CliException.class, () ->
callWithSingleProcess(command));
+
+ assertEquals(CliExitCode.NOT_FOUND, e.getExitCode());
+ assertEquals("No running Camel integration to send to: seda:orders is
in an integration, so start it with"
+ + " camel run first",
+ e.getMessage());
+ }
+
+ @Test
+ void withoutANameAndSeveralIntegrationsRunningTheSendIsRefused() throws
Exception {
+ writeStatusFile(TEST_PID, "myApp");
+ writeStatusFile(TEST_PID + 1, "billing");
+
+ CamelSendAction command = new CamelSendAction(new
CamelJBangMain().withPrinter(printer));
+
+ CliException e = assertThrows(CliException.class,
+ () -> callWithResponse(command, sentResponse(), TEST_PID + 1));
+
+ assertEquals(CliExitCode.NOT_FOUND, e.getExitCode());
+ assertTrue(e.getMessage().startsWith("2 Camel integrations are
running: "), e.getMessage());
+ assertTrue(e.getMessage().endsWith(". Give the name or pid of the one
to send to."), e.getMessage());
+ assertNull(readActionFile(TEST_PID), "nothing is sent");
+ }
+
+ @Test
+ void anEndpointOutsideTheIntegrationsIsSentFromTheCli() {
+ assertTrue(CamelSendAction.isInsideAnIntegration("direct:foo"));
+ assertTrue(CamelSendAction.isInsideAnIntegration("seda://foo"));
+ assertTrue(CamelSendAction.isInsideAnIntegration("STUB:jms:inbox"));
+ assertTrue(CamelSendAction.isInsideAnIntegration("myRoute"));
+ assertTrue(CamelSendAction.isInsideAnIntegration("kafka:*"));
+ assertFalse(CamelSendAction.isInsideAnIntegration("kafka:refunds"));
+ assertFalse(CamelSendAction.isInsideAnIntegration("jms:queue:inbox"));
+ }
+
private static JsonObject sentResponse() {
JsonObject response = new JsonObject();
response.put("timestamp", FIXED_TIMESTAMP);
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/resources/golden/cli-help.txt
b/dsl/camel-jbang/camel-jbang-core/src/test/resources/golden/cli-help.txt
index 72caf84e74e1..9b5fcecb2865 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/test/resources/golden/cli-help.txt
+++ b/dsl/camel-jbang/camel-jbang-core/src/test/resources/golden/cli-help.txt
@@ -1710,8 +1710,10 @@ Options:
(Use commas to separate multiple repositories)
--fresh Make sure we use fresh (i.e. non-cached) resources
Default: false
- <name> To use an existing running Camel integration for
- sending the message (name or pid)
+ <name> The running Camel integration to send to (name or
+ pid). Without it, the integration that runs, unless
+ --endpoint is a system outside the integrations,
+ such as kafka:topic, which the CLI sends to itself
With --repeat the message is sent that many times, with --interval between two
(a range such as 100..500 draws each pause at random), and a summary is printed