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 89929a305815 CAMEL-26823: the Diagram and the source editor of the
Camel CLI show whether a route waits for the answer of the route it sends to
(#29616)
89929a305815 is described below
commit 89929a305815373d42e2f9a513098a4af3d11ea8
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed Nov 11 13:17:33 2026 +0100
CAMEL-26823: the Diagram and the source editor of the Camel CLI show
whether a route waits for the answer of the route it sends to (#29616)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../org/apache/camel/diagram/TopologyHelper.java | 2 +
.../apache/camel/diagram/TopologyLayoutEngine.java | 15 ++-
.../camel/diagram/TopologyHelperErrorPathTest.java | 18 ++++
.../ROOT/pages/camel-jbang-tui-diagram.adoc | 4 +
.../ROOT/pages/camel-jbang-tui-source-editor.adoc | 6 +-
.../jbang/core/commands/tui/DiagramTopology.java | 10 ++
.../dsl/jbang/core/commands/tui/JumpLink.java | 43 ++++++++
.../camel/dsl/jbang/core/commands/tui/SendHow.java | 120 +++++++++++++++++++++
.../dsl/jbang/core/commands/tui/SourceTab.java | 62 +++++++----
.../dsl/jbang/core/commands/tui/SourceViewer.java | 13 +--
.../core/commands/tui/TopologyGroupFolds.java | 10 +-
.../core/commands/tui/diagram/DiagramColors.java | 2 +
.../tui/diagram/TopologyDiagramWidget.java | 6 +-
.../src/main/resources/tui/help/diagram.md | 6 ++
.../src/main/resources/tui/help/source.md | 5 +
.../commands/tui/SourceEditorProjectInfoTest.java | 2 +-
.../jbang/core/commands/tui/SourceSizeTest.java | 2 +-
.../core/commands/tui/SourceTabJumpLinksTest.java | 119 ++++++++++++++++++--
.../core/commands/tui/TopologyGroupFoldsTest.java | 15 +++
.../tui/diagram/TopologyReplyArrowTest.java | 89 +++++++++++++++
20 files changed, 501 insertions(+), 48 deletions(-)
diff --git
a/components/camel-diagram/src/main/java/org/apache/camel/diagram/TopologyHelper.java
b/components/camel-diagram/src/main/java/org/apache/camel/diagram/TopologyHelper.java
index dc5f6ab09319..1b6a790d3b14 100644
---
a/components/camel-diagram/src/main/java/org/apache/camel/diagram/TopologyHelper.java
+++
b/components/camel-diagram/src/main/java/org/apache/camel/diagram/TopologyHelper.java
@@ -88,6 +88,8 @@ public final class TopologyHelper {
edge.kind = eo.getStringOrDefault("kind", "call");
edge.via = eo.getString("via");
edge.handling = eo.getString("handling");
+ // an integration before 4.24 does not say it: its edges are drawn
as before
+ edge.reply = Boolean.TRUE.equals(eo.get("reply"));
if (includeErrorPaths || !edge.isErrorPath()) {
edges.add(edge);
}
diff --git
a/components/camel-diagram/src/main/java/org/apache/camel/diagram/TopologyLayoutEngine.java
b/components/camel-diagram/src/main/java/org/apache/camel/diagram/TopologyLayoutEngine.java
index 1e5d657ead90..40f8bceb3adf 100644
---
a/components/camel-diagram/src/main/java/org/apache/camel/diagram/TopologyLayoutEngine.java
+++
b/components/camel-diagram/src/main/java/org/apache/camel/diagram/TopologyLayoutEngine.java
@@ -155,7 +155,7 @@ public class TopologyLayoutEngine {
boolean backEdge = layers.getOrDefault(e.fromRouteId, 0) >=
layers.getOrDefault(e.toRouteId, 0)
&& !e.fromRouteId.equals(e.toRouteId);
boolean selfLoop = e.fromRouteId.equals(e.toRouteId);
- layoutEdges.add(new TopologyLayoutEdge(from, to, e.endpoint,
e.connectionType, backEdge, selfLoop));
+ layoutEdges.add(new TopologyLayoutEdge(from, to, e.endpoint,
e.connectionType, backEdge, selfLoop, e.reply));
}
}
@@ -338,6 +338,9 @@ public class TopologyLayoutEngine {
public String via;
// for an error path: handled, continued or notHandled (null when a
predicate decides)
public String handling;
+ // whether the source route waits for the answer of the target route
(a direct call, a request-reply over a
+ // broker) rather than handing the message off
+ public boolean reply;
public boolean isErrorPath() {
return "error".equals(kind);
@@ -382,17 +385,27 @@ public class TopologyLayoutEngine {
public final String connectionType;
public final boolean backEdge;
public final boolean selfLoop;
+ // whether the source route waits for the answer of the target route
+ public final boolean reply;
public TopologyLayoutEdge(
TopologyLayoutNode from, TopologyLayoutNode
to,
String endpoint, String connectionType,
boolean backEdge, boolean selfLoop) {
+ this(from, to, endpoint, connectionType, backEdge, selfLoop,
false);
+ }
+
+ public TopologyLayoutEdge(
+ TopologyLayoutNode from, TopologyLayoutNode
to,
+ String endpoint, String connectionType,
+ boolean backEdge, boolean selfLoop, boolean
reply) {
this.from = from;
this.to = to;
this.endpoint = endpoint;
this.connectionType = connectionType;
this.backEdge = backEdge;
this.selfLoop = selfLoop;
+ this.reply = reply;
}
}
diff --git
a/components/camel-diagram/src/test/java/org/apache/camel/diagram/TopologyHelperErrorPathTest.java
b/components/camel-diagram/src/test/java/org/apache/camel/diagram/TopologyHelperErrorPathTest.java
index d3e6f50d7ea2..5a06f235a196 100644
---
a/components/camel-diagram/src/test/java/org/apache/camel/diagram/TopologyHelperErrorPathTest.java
+++
b/components/camel-diagram/src/test/java/org/apache/camel/diagram/TopologyHelperErrorPathTest.java
@@ -52,6 +52,24 @@ class TopologyHelperErrorPathTest {
.containsExactly("errorHandler/handled",
"onException/notHandled");
}
+ @Test
+ void anEdgeSaysWhetherTheRouteWaitsForTheAnswer() {
+ // CAMEL-26823: reply is on every edge of a 4.24 integration; an older
one does not say it
+ JsonObject root = new JsonObject();
+ JsonArray edges = new JsonArray();
+ JsonObject stock = edge("reserve-order", "stock-service", "call",
null, null);
+ stock.put("reply", true);
+ edges.add(stock);
+ JsonObject shipping = edge("reserve-order", "shipping", "call", null,
null);
+ shipping.put("reply", false);
+ edges.add(shipping);
+ edges.add(edge("legacy", "parked", null, null, null));
+ root.put("edges", edges);
+
+ assertThat(TopologyHelper.parseEdges(root)).extracting(e ->
e.toRouteId + (e.reply ? " waits" : ""))
+ .containsExactly("stock-service waits", "shipping", "parked");
+ }
+
private static JsonObject edge(String from, String to, String kind, String
via, String handling) {
JsonObject e = new JsonObject();
e.put("fromRouteId", from);
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-diagram.adoc
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-diagram.adoc
index 0a08066a53bb..493061bef0cd 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-diagram.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-diagram.adoc
@@ -31,6 +31,10 @@ The topology shows all routes as boxes, connected by arrows:
* *Trigger routes* (timer, cron, etc.) appear at the top
* *Downstream routes* appear below, connected by directed arrows
+* The head of an arrow says whether the route waits for the answer of the
route it sends to: `⇅` it does (a call to a
+ `direct` route, or a request-reply over a broker such as
`exchangePattern=InOut` on a JMS queue), `▼` it hands the
+ message off (a queue, a topic, a `wireTap`). An integration before Camel
4.24 does not say it, and all its arrows end
+ in `▼`
* Route boxes show the route ID, the `from` endpoint, and metrics when enabled
Navigate between route boxes with arrow keys. When a route is selected, an
*Info panel* appears
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-source-editor.adoc
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-source-editor.adoc
index 050c0e96b304..13fe2996ee62 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-source-editor.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-source-editor.adoc
@@ -315,7 +315,11 @@ image::jbang/camel-tui-source-xml-quick-doc.png[The
documentation of the logging
* *Jump links* -- a line that sends to another route, such as
`to("seda:shipping")`, shows `↵ shipping`. Press *Enter*
on the line to go to that route, also when it is in another file or another
DSL. A `from` shows which route calls
- it; when more than one does, it shows `↵ 2 callers`, and *Enter* opens a
list of them to choose from.
+ it; when more than one does, it shows `↵ 2 callers`, and *Enter* opens a
list of them to choose from. Before the
+ route, `↳` says that the route waits for the answer (`↵ ↳ audit` for a
`direct` route, a request-reply with
+ `exchangePattern=InOut`, an `enrich`) and `⇢` that it hands the message off
(`↵ ⇢ shipping` for a queue, a topic or a
+ `wireTap`), as in the About view. While the integration runs, and its
Diagram, Routes or About view has been open, the
+ running routes decide: a route that consumes a request, from
`platform-http`, also waits on a send to a queue.
* *Go to route* -- *g* lists all the routes of the project; type to filter,
and *Enter* opens the route.
* *Bean jumps* -- a line that refers to a bean shows `↵ name`, and *Enter*
goes to where the project declares it.
* *Usages* -- *u* on a line with an endpoint lists where it is used: the
routes that consume from it and the steps that
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTopology.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTopology.java
index c97a69d92e81..47981e7978cd 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTopology.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DiagramTopology.java
@@ -60,6 +60,16 @@ final class DiagramTopology {
private volatile Structure structure;
+ /**
+ * The topology asked for last, when it is of this integration, without
asking for it: null until a tab (Diagram,
+ * Routes, About) has asked for it. For what must not wait on the
integration, such as the jump links of the source
+ * editor.
+ */
+ JsonObject cached(String pid) {
+ Structure s = structure;
+ return s != null && pid != null && s.pid().equals(pid) ? s.topology()
: null;
+ }
+
/**
* The step of a route by its id, from the structure asked for last (the
Routes tab shows what each step does); null
* until the structure of the integration has been asked for, or when it
has no such step.
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/JumpLink.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/JumpLink.java
new file mode 100644
index 000000000000..e2e99b5e8b73
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/JumpLink.java
@@ -0,0 +1,43 @@
+/*
+ * 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.tui;
+
+/**
+ * A link on a line to where it leads; callersOf is the endpoint of a route
sent to from more than one place, whose link
+ * opens a popup to choose one (CAMEL-25411), else null. how is how the send
reaches the route: call when the sending
+ * route waits for the answer, async when it hands the message off, null when
not a send between routes.
+ */
+record JumpLink(String routeId, String filePath, int targetLine, String
callersOf, String how) {
+
+ JumpLink(String routeId, String filePath, int targetLine) {
+ this(routeId, filePath, targetLine, null, null);
+ }
+
+ JumpLink(String routeId, String filePath, int targetLine, String
callersOf) {
+ this(routeId, filePath, targetLine, callersOf, null);
+ }
+
+ JumpLink withHow(String how) {
+ return new JumpLink(routeId, filePath, targetLine, callersOf, how);
+ }
+
+ /** The hint after the line: ↳ the route waits for the answer, ⇢ a
hand-off, as the About view marks them. */
+ String hint() {
+ String mark = "call".equals(how) ? "↳ " : "async".equals(how) ? "⇢ " :
"";
+ return " ↵ " + mark + routeId;
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SendHow.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SendHow.java
new file mode 100644
index 000000000000..a970fe7f633d
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SendHow.java
@@ -0,0 +1,120 @@
+/*
+ * 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.tui;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+import org.apache.camel.util.json.JsonObject;
+
+/**
+ * How a send of a route reaches the route that consumes it, for the jump
links of the source editor: call when the
+ * sending route waits for the answer, async when it hands the message off
(CAMEL-26823).
+ */
+final class SendHow {
+
+ // a uri that asks for the answer of what it sends to, and the key of a
step block that does
+ private static final Pattern IN_OUT_PARAMETER =
Pattern.compile("[?&]exchangePattern=InOut(&|$)", Pattern.CASE_INSENSITIVE);
+ private static final Pattern IN_OUT_KEY
+ =
Pattern.compile("(exchangePattern|pattern):\\s*[\"']?InOut[\"']?\\s*(#.*)?",
Pattern.CASE_INSENSITIVE);
+
+ private SendHow() {
+ }
+
+ /**
+ * How a step reaches the route that consumes the endpoint it sends to, as
the sources say it: call when the route
+ * waits for the answer (direct and kamelet run the route before it goes
on, enrich waits for it, and
+ * exchangePattern=InOut asks for it, as a request-reply over a broker
does), async when it hands the message off (a
+ * wire tap always does). null for a dead letter channel or a pollEnrich,
which are no send the route waits on or
+ * hands off. A route that consumes an InOut message (from platform-http)
waits on its sends too; only the running
+ * integration knows that (see {@link #how(String, String, String, Map)}).
+ */
+ static String of(String keyword, String uri, boolean inOut) {
+ if (uri == null || "deadLetterChannel".equals(keyword) ||
"pollEnrich".equals(keyword)) {
+ return null;
+ }
+ if ("wireTap".equals(keyword)) {
+ return "async";
+ }
+ int colon = uri.indexOf(':');
+ String scheme = colon > 0 ? uri.substring(0,
colon).toLowerCase(Locale.ROOT) : "";
+ if ("enrich".equals(keyword) || "direct".equals(scheme) ||
"kamelet".equals(scheme) || inOut
+ || IN_OUT_PARAMETER.matcher(uri).find()) {
+ return "call";
+ }
+ return "async";
+ }
+
+ /** Whether the block of a step (its parameters) asks for the answer:
exchangePattern: InOut, or pattern: InOut. */
+ static boolean blockSaysInOut(List<String> lines, int start, int indent) {
+ if (start < 0) {
+ return false;
+ }
+ for (int j = start + 1; j < lines.size(); j++) {
+ String line = lines.get(j);
+ String trimmed = line.trim();
+ if (trimmed.isEmpty() || trimmed.startsWith("#")) {
+ continue;
+ }
+ if (SourceTab.lineIndent(line) <= indent) {
+ break;
+ }
+ if (IN_OUT_KEY.matcher(trimmed).matches()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * How the route reaches the other: as the running integration has it when
its topology was asked for, else static.
+ */
+ static String how(String fromRouteId, String toRouteId, String staticHow,
Map<String, String> runtime) {
+ String known = fromRouteId != null ? runtime.get(fromRouteId + ">" +
toRouteId) : null;
+ return known != null ? known : staticHow;
+ }
+
+ /**
+ * How the routes of the selected integration reach each other, by
"from>to": call when the route waits for the
+ * answer, else async. Only from the topology a tab has asked for already,
so the jump links never wait on the
+ * integration; empty when there is none.
+ */
+ static Map<String, String> runtime(MonitorContext ctx) {
+ IntegrationInfo info = ctx.findSelectedIntegration();
+ JsonObject topology = info != null ?
ctx.diagramTopology.cached(info.pid) : null;
+ Collection<?> edges = topology != null ?
topology.getCollection("edges") : null;
+ if (edges == null) {
+ return Map.of();
+ }
+ Map<String, String> answer = new HashMap<>();
+ for (Object o : edges) {
+ if (o instanceof JsonObject e &&
!"error".equals(e.getString("kind"))) {
+ String from = e.getString("fromRouteId");
+ String to = e.getString("toRouteId");
+ if (from != null && to != null) {
+ answer.merge(from + ">" + to,
Boolean.TRUE.equals(e.get("reply")) ? "call" : "async",
+ (a, b) -> "call".equals(a) || "call".equals(b) ?
"call" : a);
+ }
+ }
+ }
+ return answer;
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
index a7e361624029..a506de36dc08 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
@@ -143,7 +143,8 @@ class SourceTab extends AbstractTab {
record RouteEntry(String routeId, String fromUri, String filePath, int
fromLine) {
}
- record ToEntry(String routeId, String toUri, String filePath, int toLine) {
+ /** A send of a route; how is how it reaches the route it sends to, see
{@link SendHow#of}. */
+ record ToEntry(String routeId, String toUri, String filePath, int toLine,
String how) {
}
private List<RouteEntry> routeIndex = Collections.emptyList();
@@ -1357,9 +1358,11 @@ class SourceTab extends AbstractTab {
emitRouteEntry(fromEntries, route.id(),
JavaRouteScanner.uri(route.fromUri()), filePath, route.line());
String routeId = fromEntries.size() > before ?
fromEntries.get(before).routeId() : "";
for (ScannedRoute.To to : route.tos()) {
- String toUri =
stripQueryParams(JavaRouteScanner.uri(to.uri()));
+ String uri = JavaRouteScanner.uri(to.uri());
+ String toUri = stripQueryParams(uri);
if (toUri != null && !toUri.isEmpty()) {
- toEntries.add(new ToEntry(routeId, toUri, filePath,
to.line()));
+ // the scanners do not say which step sends (a wire tap):
the uri decides
+ toEntries.add(new ToEntry(routeId, toUri, filePath,
to.line(), SendHow.of(null, uri, false)));
}
}
}
@@ -1420,6 +1423,8 @@ class SourceTab extends AbstractTab {
int pendingFromLine = -1;
boolean inLinkableBlock = false;
int linkableBlockIndent = -1;
+ String blockKeyword = null;
+ int blockStart = -1;
for (int i = 0; i < lines.size(); i++) {
String line = lines.get(i);
@@ -1491,14 +1496,18 @@ class SourceTab extends AbstractTab {
? trimmed.substring(prefix2.length()).trim()
: trimmed.substring(prefix1.length()).trim();
if (!after.isEmpty() && !after.equals("{") &&
!after.startsWith("#")) {
- String toUri = stripQueryParams(unquote(after));
+ String uri = unquote(after);
+ String toUri = stripQueryParams(uri);
if (toUri != null && !toUri.isEmpty()) {
toEntries.add(new ToEntry(
- currentRouteId != null ? currentRouteId :
"", toUri, filePath, i));
+ currentRouteId != null ? currentRouteId :
"", toUri, filePath, i,
+ SendHow.of(kw, uri, false)));
}
} else {
inLinkableBlock = true;
linkableBlockIndent = indent;
+ blockKeyword = kw;
+ blockStart = i;
}
break;
}
@@ -1511,7 +1520,8 @@ class SourceTab extends AbstractTab {
String toUri = stripQueryParams(val);
if (toUri != null && !toUri.isEmpty()) {
toEntries.add(new ToEntry(
- currentRouteId != null ? currentRouteId : "",
toUri, filePath, i));
+ currentRouteId != null ? currentRouteId : "",
toUri, filePath, i,
+ SendHow.of(blockKeyword, val,
SendHow.blockSaysInOut(lines, blockStart, linkableBlockIndent))));
}
}
}
@@ -1561,8 +1571,8 @@ class SourceTab extends AbstractTab {
}
/** The jump links of a file: to the routes its steps send to or are
called from, and to the beans it refers to. */
- Map<Integer, SourceViewer.JumpLink> computeJumpLinks(Path currentFile) {
- Map<Integer, SourceViewer.JumpLink> result = new
LinkedHashMap<>(computeRouteJumpLinks(currentFile));
+ Map<Integer, JumpLink> computeJumpLinks(Path currentFile) {
+ Map<Integer, JumpLink> result = new
LinkedHashMap<>(computeRouteJumpLinks(currentFile));
ProjectBeans beans = projectBeans();
List<String> lines;
try {
@@ -1573,7 +1583,7 @@ class SourceTab extends AbstractTab {
for (int i = 0; i < lines.size(); i++) {
ProjectBeans.Location bean = beans.refOn(lines.get(i));
if (bean != null &&
!(bean.filePath().equals(currentFile.toString()) && bean.line() == i)) {
- result.putIfAbsent(i, new SourceViewer.JumpLink(bean.label(),
bean.filePath(), bean.line()));
+ result.putIfAbsent(i, new JumpLink(bean.label(),
bean.filePath(), bean.line()));
}
}
return result;
@@ -1642,7 +1652,7 @@ class SourceTab extends AbstractTab {
};
}
- private Map<Integer, SourceViewer.JumpLink> computeRouteJumpLinks(Path
currentFile) {
+ private Map<Integer, JumpLink> computeRouteJumpLinks(Path currentFile) {
if (routeIndex.isEmpty()) {
return Collections.emptyMap();
}
@@ -1655,7 +1665,8 @@ class SourceTab extends AbstractTab {
}
String currentFilePath = currentFile.toString();
- Map<Integer, SourceViewer.JumpLink> result = new LinkedHashMap<>();
+ Map<Integer, JumpLink> result = new LinkedHashMap<>();
+ Map<String, String> runtime = SendHow.runtime(ctx);
Map<String, RouteEntry> fromUriToRoute = new HashMap<>();
for (RouteEntry re : routeIndex) {
@@ -1669,17 +1680,20 @@ class SourceTab extends AbstractTab {
RouteEntry target = fromUriToRoute.get(te.toUri());
if (target != null &&
!target.routeId().equals(te.routeId())) {
result.putIfAbsent(te.toLine(),
- new SourceViewer.JumpLink(target.routeId(),
target.filePath(), target.fromLine()));
+ new JumpLink(target.routeId(),
target.filePath(), target.fromLine())
+ .withHow(SendHow.how(te.routeId(),
target.routeId(), te.how(), runtime)));
}
}
}
- addReverseLinks(currentFilePath, result);
+ addReverseLinks(currentFilePath, result, runtime);
return result;
}
// forward links: to/toD/wireTap → target route's from
boolean inLinkableBlock = false;
int linkableBlockIndent = -1;
+ String blockKeyword = null;
+ int blockStart = -1;
String currentRouteId = null;
for (int i = 0; i < lines.size(); i++) {
@@ -1710,6 +1724,7 @@ class SourceTab extends AbstractTab {
}
String uri = null;
+ String how = null;
for (String kw : LINKABLE_KEYWORDS) {
String prefix1 = kw + ":";
String prefix2 = "- " + kw + ":";
@@ -1719,9 +1734,12 @@ class SourceTab extends AbstractTab {
: trimmed.substring(prefix1.length()).trim();
if (!after.isEmpty() && !after.equals("{") &&
!after.startsWith("#")) {
uri = unquote(after);
+ how = SendHow.of(kw, uri, false);
} else {
inLinkableBlock = true;
linkableBlockIndent = indent;
+ blockKeyword = kw;
+ blockStart = i;
}
break;
}
@@ -1731,6 +1749,7 @@ class SourceTab extends AbstractTab {
String val = extractYamlValue(uriLine(trimmed), "uri");
if (val != null && !val.isEmpty()) {
uri = val;
+ how = SendHow.of(blockKeyword, val,
SendHow.blockSaysInOut(lines, blockStart, linkableBlockIndent));
}
}
@@ -1738,12 +1757,13 @@ class SourceTab extends AbstractTab {
String baseUri = stripQueryParams(uri);
RouteEntry target = fromUriToRoute.get(baseUri);
if (target != null &&
!target.routeId().equals(currentRouteId)) {
- result.put(i, new SourceViewer.JumpLink(target.routeId(),
target.filePath(), target.fromLine()));
+ result.put(i, new JumpLink(target.routeId(),
target.filePath(), target.fromLine())
+ .withHow(SendHow.how(currentRouteId,
target.routeId(), how, runtime)));
}
}
}
- addReverseLinks(currentFilePath, result);
+ addReverseLinks(currentFilePath, result, runtime);
return result;
}
@@ -1751,7 +1771,8 @@ class SourceTab extends AbstractTab {
* Reverse links: a from line to the route that sends to it (jumps to the
caller's to line), or when several do, to
* a popup to choose one of them: a Kamelet or a direct: route used by
more than one route.
*/
- private void addReverseLinks(String currentFilePath, Map<Integer,
SourceViewer.JumpLink> result) {
+ private void addReverseLinks(
+ String currentFilePath, Map<Integer, JumpLink> result, Map<String,
String> runtime) {
for (RouteEntry re : routeIndex) {
if (!currentFilePath.equals(re.filePath())) {
continue;
@@ -1760,10 +1781,11 @@ class SourceTab extends AbstractTab {
if (callers.size() == 1) {
ToEntry te = callers.get(0);
String callerRouteId = te.routeId().isEmpty() ? "route" :
te.routeId();
- result.putIfAbsent(re.fromLine(), new
SourceViewer.JumpLink(callerRouteId, te.filePath(), te.toLine()));
+ result.putIfAbsent(re.fromLine(), new JumpLink(callerRouteId,
te.filePath(), te.toLine())
+ .withHow(SendHow.how(te.routeId(), re.routeId(),
te.how(), runtime)));
} else if (callers.size() > 1) {
result.putIfAbsent(re.fromLine(),
- new SourceViewer.JumpLink(callers.size() + " callers",
null, -1, re.fromUri()));
+ new JumpLink(callers.size() + " callers", null, -1,
re.fromUri()));
}
}
}
@@ -1876,7 +1898,7 @@ class SourceTab extends AbstractTab {
}
}
- private void handleJumpLink(SourceViewer.JumpLink link) {
+ private void handleJumpLink(JumpLink link) {
if (link.callersOf() != null) {
// sent to from more than one place: choose which
gotoRoutePopup.openItems(callersOf(link.callersOf()), "Callers of
" + link.callersOf());
@@ -1959,7 +1981,7 @@ class SourceTab extends AbstractTab {
return q >= 0 ? uri.substring(0, q) : uri;
}
- private static int lineIndent(String line) {
+ static int lineIndent(String line) {
int indent = 0;
for (int i = 0; i < line.length(); i++) {
if (line.charAt(i) == ' ') {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
index f696cd38d0de..fcbeea91604c 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
@@ -118,17 +118,6 @@ class SourceViewer {
Set<Integer> scan(List<JsonObject> codeData);
}
- /**
- * A link on a line to where it leads; callersOf is the endpoint of a
route sent to from more than one place, whose
- * link opens a popup to choose one (CAMEL-25411), else null.
- */
- record JumpLink(String routeId, String filePath, int targetLine, String
callersOf) {
-
- JumpLink(String routeId, String filePath, int targetLine) {
- this(routeId, filePath, targetLine, null);
- }
- }
-
private boolean visible;
private List<String> lines = Collections.emptyList();
private List<JsonObject> codeData = Collections.emptyList();
@@ -2845,7 +2834,7 @@ class SourceViewer {
if (isSelected) {
linkStyle = linkStyle.patch(selBg);
}
- spans.add(Span.styled(" ↵ " + jl.routeId(), linkStyle));
+ spans.add(Span.styled(jl.hint(), linkStyle));
}
liveColumn.addBodyTypes(spans, lineIndex, plainMode, isSelected ?
selBg : null, viewportWidth - 1 + hSkip);
Line full = Line.from(spans);
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TopologyGroupFolds.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TopologyGroupFolds.java
index dddbaef0c676..b2b4a69f8fa2 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TopologyGroupFolds.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TopologyGroupFolds.java
@@ -17,7 +17,7 @@
package org.apache.camel.dsl.jbang.core.commands.tui;
import java.util.ArrayList;
-import java.util.HashSet;
+import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -94,7 +94,7 @@ final class TopologyGroupFolds {
nodes.addAll(kept);
List<TopologyEdgeInfo> moved = new ArrayList<>();
- Set<String> seen = new HashSet<>();
+ Map<String, TopologyEdgeInfo> seen = new HashMap<>();
for (TopologyEdgeInfo e : edges) {
String from = boxOf.getOrDefault(e.fromRouteId, e.fromRouteId);
String to = boxOf.getOrDefault(e.toRouteId, e.toRouteId);
@@ -102,10 +102,14 @@ final class TopologyGroupFolds {
// between the routes of the group
continue;
}
- if (seen.add(from + "\u0000" + to + "\u0000" + e.kind)) {
+ TopologyEdgeInfo known = seen.putIfAbsent(from + "\u0000" + to +
"\u0000" + e.kind, e);
+ if (known == null) {
e.fromRouteId = from;
e.toRouteId = to;
moved.add(e);
+ } else if (e.reply) {
+ // one of the routes of the group waits for the answer: the
box does
+ known.reply = true;
}
}
edges.clear();
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/DiagramColors.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/DiagramColors.java
index 86416c408201..ca51b93708f1 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/DiagramColors.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/DiagramColors.java
@@ -33,6 +33,8 @@ public final class DiagramColors {
static final char T_UP = '┴';
static final char CROSS = '┼';
static final char ARROW = '▼';
+ // the head of an edge whose source route waits for the answer: there and
back
+ static final char REPLY_ARROW = '⇅';
static final char DASH_V = '┆';
static final char DASH_H = '┄';
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/TopologyDiagramWidget.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/TopologyDiagramWidget.java
index da63efad5199..cbf10d9559a6 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/TopologyDiagramWidget.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/TopologyDiagramWidget.java
@@ -601,6 +601,8 @@ public class TopologyDiagramWidget implements Widget {
}
boolean dashed = isExternal(edge.from) || isExternal(edge.to) ||
ERROR_HANDLING.equals(edge.to.nodeType);
+ // ⇅ the route waits for the answer (a call, a request-reply over a
broker), ▼ it hands the message off
+ char head = edge.reply ? REPLY_ARROW : ARROW;
char vChar = dashed ? DASH_V : V;
char hChar = dashed ? DASH_H : H;
boolean edgeHighlighted = !dashed
@@ -617,7 +619,7 @@ public class TopologyDiagramWidget implements Widget {
for (int r = fromBottom; r < toTop - 1; r++) {
plotLine(buffer, area, r, fromCx, vChar, edgeStyle);
}
- setChar(buffer, area, toTop - 1, toCx, ARROW, edgeStyle);
+ setChar(buffer, area, toTop - 1, toCx, head, edgeStyle);
} else {
int midRow = fromBottom + (toTop - fromBottom) / 2;
@@ -637,7 +639,7 @@ public class TopologyDiagramWidget implements Widget {
for (int r = midRow + 1; r < toTop - 1; r++) {
plotLine(buffer, area, r, toCx, vChar, edgeStyle);
}
- setChar(buffer, area, toTop - 1, toCx, ARROW, edgeStyle);
+ setChar(buffer, area, toTop - 1, toCx, head, edgeStyle);
}
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/diagram.md
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/diagram.md
index 6e2c4afb5c6d..74e696d1fdb4 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/diagram.md
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/diagram.md
@@ -33,6 +33,12 @@ Each box represents a route. The first line is the route ID,
the second line shows the `from` endpoint, and the bottom line
shows metrics when enabled. Arrows show how routes connect.
+The head of an arrow says whether the route waits for the answer
+of the route it sends to:
+- `⇅` it waits: a call to a `direct` route, or a request-reply over
+ a broker (`exchangePattern=InOut`)
+- `▼` it hands the message off: a queue, a topic, a `wireTap`
+
## Metrics
When metrics are enabled, each route box shows exchange counts:
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/source.md
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/source.md
index 340c3775f65c..04773a4400d0 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/source.md
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/source.md
@@ -188,6 +188,11 @@ Lines with `to:`, `toD:`, `wireTap:`, or similar endpoints
that reference
another route show a **↵ routeId** indicator. Press **Enter** on such a line
to jump to the target route's definition (within the same file or across
files).
Reverse links are shown on `from:` lines, indicating which route calls this
one.
+Before the route, **↳** says the route waits for the answer (a `direct` route,
+`exchangePattern=InOut`, an `enrich`) and **⇢** that it hands the message off
+(a queue, a topic, a `wireTap`), as in the About view. While the integration
+runs, and its Diagram, Routes or About has been open, the running routes decide
+it: a route that consumes a request (from `platform-http`) waits on a queue
too.
This works for YAML, XML and Java DSL routes, across all the folders of the
project and files of different DSLs (build output and `src/test` are left out);
Java routes are read without compiling them. The case and otherwise of a
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceEditorProjectInfoTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceEditorProjectInfoTest.java
index bc9d3224205b..e6c65d9cbeb8 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceEditorProjectInfoTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceEditorProjectInfoTest.java
@@ -157,7 +157,7 @@ class SourceEditorProjectInfoTest {
SourceTab tab = newTab();
assertThat(tab.loadDirectory(dir)).isTrue();
- Map<Integer, SourceViewer.JumpLink> links =
tab.computeJumpLinks(route);
+ Map<Integer, JumpLink> links = tab.computeJumpLinks(route);
assertThat(links.get(5).routeId()).isEqualTo("orders");
assertThat(links.get(5).filePath()).endsWith("OrderService.java");
assertThat(links.get(5).targetLine()).isEqualTo(2);
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceSizeTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceSizeTest.java
index fe7d84bb4d54..1ebcca7a9b16 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceSizeTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceSizeTest.java
@@ -75,7 +75,7 @@ class SourceSizeTest {
SendMessagePopup.handleKeyEvent 220
SourceEditAssist.java 2006
SourceViewer.handleEditKeyEvent 231
- SourceViewer.java 2953
+ SourceViewer.java 2942
SourceViewer.renderContent 207
SourceViewer.renderEditMode 256
TuiToolDefinitions.addInteractionTools 183
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTabJumpLinksTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTabJumpLinksTest.java
index b7da0484a30b..a02cbc49a08f 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTabJumpLinksTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTabJumpLinksTest.java
@@ -81,7 +81,7 @@ class SourceTabJumpLinksTest {
SourceTab tab = newTab();
assertThat(tab.loadDirectory(dir)).isTrue();
- Map<Integer, SourceViewer.JumpLink> links =
tab.computeJumpLinks(tickets);
+ Map<Integer, JumpLink> links = tab.computeJumpLinks(tickets);
assertThat(links.get(11).routeId()).isEqualTo("billing");
assertThat(links.get(12).routeId()).isEqualTo("technical");
// a route without an id is named by its from endpoint
@@ -129,7 +129,7 @@ class SourceTabJumpLinksTest {
uri: kamelet:tag-order-action
""");
assertThat(tab.loadDirectory(dir)).isTrue();
- SourceViewer.JumpLink link = tab.computeJumpLinks(kamelet).get(6);
+ JumpLink link = tab.computeJumpLinks(kamelet).get(6);
assertThat(link.routeId()).isEqualTo("2 callers");
assertThat(link.callersOf()).isEqualTo("kamelet:tag-order-action");
assertThat(tab.callersOf("kamelet:tag-order-action"))
@@ -169,19 +169,124 @@ class SourceTabJumpLinksTest {
SourceTab tab = newTab();
assertThat(tab.loadDirectory(dir)).isTrue();
- Map<Integer, SourceViewer.JumpLink> links =
tab.computeJumpLinks(orders);
+ Map<Integer, JumpLink> links = tab.computeJumpLinks(orders);
assertThat(links.get(5).routeId()).isEqualTo("billing");
assertThat(links.get(5).filePath()).isEqualTo(billing.toString());
assertThat(links.get(9).routeId()).isEqualTo("billing");
// and back: the from: line of the YAML route has two Java callers, to
choose from in a popup (CAMEL-25411)
- SourceViewer.JumpLink back = tab.computeJumpLinks(billing).get(2);
+ JumpLink back = tab.computeJumpLinks(billing).get(2);
assertThat(back.routeId()).isEqualTo("2 callers");
assertThat(back.callersOf()).isEqualTo("direct:billing");
assertThat(tab.callersOf("direct:billing")).extracting(GotoRoutePopup.RouteItem::filePath)
.containsOnly(orders.toString());
}
+ @Test
+ void aHintSaysWhetherTheRouteWaitsForTheAnswer() throws Exception {
+ // CAMEL-26823: ↳ the route waits for the answer, ⇢ it hands the
message off, as the About view marks them
+ Path orders = Files.writeString(dir.resolve("orders.camel.yaml"), """
+ - route:
+ id: reserve-order
+ from:
+ uri: seda:orders
+ steps:
+ - to:
+ uri: seda:stock
+ parameters:
+ exchangePattern: InOut
+ - to: direct:audit
+ - wireTap:
+ uri: seda:metrics
+ - to: seda:reserved
+ - route:
+ id: stock-service
+ from:
+ uri: seda:stock
+ steps:
+ - log: stock
+ - route:
+ id: audit
+ from:
+ uri: direct:audit
+ steps:
+ - log: audit
+ - route:
+ id: metrics
+ from:
+ uri: seda:metrics
+ steps:
+ - log: metrics
+ - route:
+ id: shipping
+ from:
+ uri: seda:reserved
+ steps:
+ - log: ship
+ """);
+ Path quotes = Files.writeString(dir.resolve("Quotes.java"), """
+ import org.apache.camel.builder.RouteBuilder;
+
+ public class Quotes extends RouteBuilder {
+ public void configure() {
+ from("timer:quote").routeId("quotes")
+ .to("seda:stock?exchangePattern=InOut")
+ .to("seda:reserved");
+ }
+ }
+ """);
+ SourceTab tab = newTab();
+ assertThat(tab.loadDirectory(dir)).isTrue();
+
+ List<String> lines = Files.readAllLines(orders);
+ Map<Integer, JumpLink> links = tab.computeJumpLinks(orders);
+ assertThat(links.get(lineOf(lines, "uri:
seda:stock")).hint()).isEqualTo(" ↵ ↳ stock-service");
+ assertThat(links.get(lineOf(lines, "- to:
direct:audit")).hint()).isEqualTo(" ↵ ↳ audit");
+ assertThat(links.get(lineOf(lines, "uri:
seda:metrics")).hint()).isEqualTo(" ↵ ⇢ metrics");
+ assertThat(links.get(lineOf(lines, "- to:
seda:reserved")).hint()).isEqualTo(" ↵ ⇢ shipping");
+ // and back: the from line of a route called from one place says how
its caller reaches it
+ assertThat(links.get(lineOf(lines, "uri: direct:audit") -
1).hint()).isEqualTo(" ↵ ↳ reserve-order");
+
+ // a Java route asks for the answer in the uri
+ Map<Integer, JumpLink> java = tab.computeJumpLinks(quotes);
+ assertThat(java.get(5).hint()).isEqualTo(" ↵ ↳ stock-service");
+ assertThat(java.get(6).hint()).isEqualTo(" ↵ ⇢ shipping");
+ }
+
+ @Test
+ void howAStepReachesTheRouteItSendsTo() {
+ assertThat(SendHow.of("to", "direct:audit", false)).isEqualTo("call");
+ assertThat(SendHow.of("to", "kamelet:my-sink",
false)).isEqualTo("call");
+ assertThat(SendHow.of("to", "jms:queue:stock?exchangePattern=InOut",
false)).isEqualTo("call");
+ assertThat(SendHow.of("to", "jms:queue:stock?exchangePattern=InOnly",
false)).isEqualTo("async");
+ assertThat(SendHow.of("to", "seda:stock", true)).isEqualTo("call");
+ assertThat(SendHow.of("enrich", "seda:prices",
false)).isEqualTo("call");
+ assertThat(SendHow.of("wireTap", "direct:audit",
false)).isEqualTo("async");
+ assertThat(SendHow.of("to", "seda:reserved",
false)).isEqualTo("async");
+ assertThat(SendHow.of("deadLetterChannel", "seda:dead",
false)).isNull();
+ assertThat(SendHow.of("pollEnrich", "seda:prices", false)).isNull();
+
+ List<String> block = List.of(
+ " - to:",
+ " uri: seda:stock",
+ " pattern: \"InOut\"",
+ " - to: seda:next");
+ assertThat(SendHow.blockSaysInOut(block, 0, 8)).isTrue();
+ assertThat(SendHow.blockSaysInOut(block.subList(0, 2), 0,
8)).isFalse();
+ // the next step is not part of the block
+ assertThat(SendHow.blockSaysInOut(List.of(" - to:", "
uri: seda:a", " - to:",
+ " pattern: InOut"), 0, 8)).isFalse();
+ }
+
+ private static int lineOf(List<String> lines, String text) {
+ for (int i = 0; i < lines.size(); i++) {
+ if (lines.get(i).trim().equals(text)) {
+ return i;
+ }
+ }
+ throw new IllegalArgumentException(text);
+ }
+
@Test
void xmlRoutesLinkToAndFromTheOthers() throws Exception {
// CAMEL-25196: XML routes have the jump links of the YAML and Java
ones
@@ -205,10 +310,10 @@ class SourceTabJumpLinksTest {
SourceTab tab = newTab();
assertThat(tab.loadDirectory(dir)).isTrue();
- SourceViewer.JumpLink link = tab.computeJumpLinks(orders).get(3);
+ JumpLink link = tab.computeJumpLinks(orders).get(3);
assertThat(link.routeId()).isEqualTo("billing");
assertThat(link.filePath()).isEqualTo(billing.toString());
- SourceViewer.JumpLink back = tab.computeJumpLinks(billing).get(2);
+ JumpLink back = tab.computeJumpLinks(billing).get(2);
assertThat(back.routeId()).isEqualTo("orders");
assertThat(back.targetLine()).isEqualTo(3);
}
@@ -251,7 +356,7 @@ class SourceTabJumpLinksTest {
tab.showSourceDirectory(dir);
assertThat(tab.routeSources()).containsExactlyInAnyOrder(orders,
billing);
- SourceViewer.JumpLink link = tab.computeJumpLinks(orders).get(5);
+ JumpLink link = tab.computeJumpLinks(orders).get(5);
assertThat(link.routeId()).isEqualTo("billing");
assertThat(link.filePath()).isEqualTo(billing.toString());
assertThat(tab.computeJumpLinks(billing).get(4).filePath()).isEqualTo(orders.toString());
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TopologyGroupFoldsTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TopologyGroupFoldsTest.java
index 748e96de5a4d..9b07d473227b 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TopologyGroupFoldsTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TopologyGroupFoldsTest.java
@@ -79,6 +79,21 @@ class TopologyGroupFoldsTest {
assertThat(TopologyGroupFolds.groupOf("orders")).isNull();
}
+ @Test
+ void aFoldedGroupWaitsWhenOneOfItsRoutesIsWaitedOn() {
+ // CAMEL-26823: orders hands off to audit but waits for archive; the
one link to the box waits
+ List<TopologyNodeInfo> nodes = new ArrayList<>(
+ List.of(route("orders", 10, 0), route("audit", 8, 1),
route("archive", 9, 2)));
+ TopologyEdgeInfo archive = call("orders", "archive");
+ archive.reply = true;
+ List<TopologyEdgeInfo> edges = new ArrayList<>(List.of(call("orders",
"audit"), archive));
+
+ TopologyGroupFolds.fold(nodes, edges, tags, Set.of("utility"));
+
+ assertThat(edges).extracting(e -> e.fromRouteId + ">" + e.toRouteId +
(e.reply ? " waits" : ""))
+ .containsExactly("orders>group:utility waits");
+ }
+
@Test
void nothingFoldedLeavesTheTopologyAsItIs() {
List<TopologyNodeInfo> nodes = new ArrayList<>(List.of(route("orders",
10, 0), route("audit", 8, 1)));
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/TopologyReplyArrowTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/TopologyReplyArrowTest.java
new file mode 100644
index 000000000000..0bec779990e3
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/diagram/TopologyReplyArrowTest.java
@@ -0,0 +1,89 @@
+/*
+ * 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.tui.diagram;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import dev.tamboui.buffer.Buffer;
+import dev.tamboui.layout.Rect;
+import org.apache.camel.diagram.TopologyLayoutEngine;
+import org.apache.camel.diagram.TopologyLayoutEngine.TopologyEdgeInfo;
+import org.apache.camel.diagram.TopologyLayoutEngine.TopologyLayoutResult;
+import org.apache.camel.diagram.TopologyLayoutEngine.TopologyNodeInfo;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * CAMEL-26823: the head of an edge in the topology says whether the route
waits for the answer of the route it sends
+ * to: ⇅ there and back (a direct call, a request-reply over a broker), ▼ a
hand-off.
+ */
+class TopologyReplyArrowTest {
+
+ @Test
+ void anEdgeTheRouteWaitsOnEndsInATwoWayHead() {
+ TopologyEdgeInfo stock = edge("reserve-order", "stock-service",
"seda:stock", true);
+ TopologyEdgeInfo shipping = edge("reserve-order", "shipping",
"seda:reserved", false);
+ TopologyLayoutEngine engine = new TopologyLayoutEngine();
+ TopologyLayoutResult layout = engine.layout(
+ new ArrayList<>(List.of(route("reserve-order"),
route("stock-service"), route("shipping"))),
+ new ArrayList<>(List.of(stock, shipping)));
+ assertThat(layout.edges).extracting(e -> e.to.routeId + (e.reply ? "
waits" : ""))
+ .containsExactlyInAnyOrder("stock-service waits", "shipping");
+
+ TopologyDiagramWidget widget = new TopologyDiagramWidget(layout,
engine.getNodeWidth(), -1, 0, 0, false, false);
+ Rect area = new Rect(0, 0, Math.max(100, widget.getTotalCols()),
Math.max(20, widget.getTotalRows()));
+ Buffer buffer = Buffer.empty(area);
+ widget.render(area, buffer);
+
+ assertThat(headAbove(buffer, widget, "stock-service")).isEqualTo("⇅");
+ assertThat(headAbove(buffer, widget, "shipping")).isEqualTo("▼");
+ }
+
+ /** The arrow head on the row above the box of the route. */
+ private static String headAbove(Buffer buffer, TopologyDiagramWidget
widget, String routeId) {
+ TopologyDiagramWidget.NodeBox box = widget.getNodeBoxes().stream()
+ .filter(b ->
routeId.equals(b.routeId())).findFirst().orElseThrow();
+ StringBuilder heads = new StringBuilder();
+ for (int col = box.startCol(); col <= box.endCol(); col++) {
+ String symbol = buffer.get(col, box.startRow() - 1).symbol();
+ if ("⇅".equals(symbol) || "▼".equals(symbol)) {
+ heads.append(symbol);
+ }
+ }
+ return heads.toString();
+ }
+
+ private static TopologyNodeInfo route(String id) {
+ TopologyNodeInfo n = new TopologyNodeInfo();
+ n.routeId = id;
+ n.from = "seda:" + id;
+ n.nodeType = "route";
+ return n;
+ }
+
+ private static TopologyEdgeInfo edge(String from, String to, String
endpoint, boolean reply) {
+ TopologyEdgeInfo e = new TopologyEdgeInfo();
+ e.fromRouteId = from;
+ e.toRouteId = to;
+ e.endpoint = endpoint;
+ e.connectionType = "internal";
+ e.reply = reply;
+ return e;
+ }
+}