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 3ba308884e15 chore: camel-jbang - About names a long route and the
route of the API description in full, and an encoded out is the one the routes
name (#29580)
3ba308884e15 is described below
commit 3ba308884e15a38723095fdd5ecd0c5889d0a39f
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed Nov 11 07:06:46 2026 +0100
chore: camel-jbang - About names a long route and the route of the API
description in full, and an encoded out is the one the routes name (#29580)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
.../dsl/jbang/core/commands/ai/AboutReport.java | 18 ++++++
.../dsl/jbang/core/commands/ai/AboutTest.java | 5 ++
.../dsl/jbang/core/commands/tui/AboutView.java | 26 ++++++++-
.../dsl/jbang/core/commands/tui/AboutViewTest.java | 67 ++++++++++++++++++++++
.../jbang/core/commands/tui/ObservedOutsTest.java | 16 ++++++
5 files changed, 130 insertions(+), 2 deletions(-)
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AboutReport.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AboutReport.java
index a99b40430c9e..45d5396e885f 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AboutReport.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AboutReport.java
@@ -17,6 +17,8 @@
package org.apache.camel.dsl.jbang.core.commands.ai;
import java.io.IOException;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
@@ -347,9 +349,25 @@ public final class AboutReport {
int q = uri.indexOf('?');
String answer = q >= 0 ? uri.substring(0, q) : uri;
answer = answer.replaceFirst("^([a-zA-Z0-9+.-]+):/{0,3}", "$1:");
+ // the runtime has the URI encoded as Camel normalizes it
(rest-openapi://stock-api.json%23reserveStock,
+ // platform-http:///stock/%7Bsku%7D), the sources as written
+ answer = decoded(answer);
return answer.endsWith("/") ? answer.substring(0, answer.length() - 1)
: answer;
}
+ private static String decoded(String uri) {
+ if (uri.indexOf('%') < 0) {
+ return uri;
+ }
+ try {
+ // a + is a plus in a path, not a space
+ return URLDecoder.decode(uri.replace("+", "%2B"),
StandardCharsets.UTF_8);
+ } catch (IllegalArgumentException e) {
+ // a % that is no escape
+ return uri;
+ }
+ }
+
private static String total(JsonObject jo) {
if (jo.get("exchangesTotal") == null) {
return "";
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AboutTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AboutTest.java
index 73b26d7c27c3..ec0fdf4ff262 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AboutTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AboutTest.java
@@ -176,6 +176,11 @@ class AboutTest {
assertThat(AboutReport.baseUri("file:/tmp/in?noop=true")).isEqualTo(AboutReport.baseUri("file:///tmp/in"));
assertThat(AboutReport.baseUri("kafka://orders")).isEqualTo("kafka:orders");
assertThat(AboutReport.baseUri("direct:orders")).isEqualTo("direct:orders");
+ // the runtime has it encoded, the sources as written
(contracts/openapi-client, connect/stock-api)
+
assertThat(AboutReport.baseUri("rest-openapi://stock-api.json%23reserveStock"))
+
.isEqualTo(AboutReport.baseUri("rest-openapi:stock-api.json#reserveStock"));
+
assertThat(AboutReport.baseUri("platform-http:///stock/%7Bsku%7D")).isEqualTo("platform-http:stock/{sku}");
+
assertThat(AboutReport.baseUri("http:shop/a+b/100%")).isEqualTo("http:shop/a+b/100%");
}
@Test
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutView.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutView.java
index 80058d5e2230..fc122d35962e 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutView.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutView.java
@@ -485,6 +485,24 @@ final class AboutView {
* The routes in the order of the Routes panel, where a route comes after
the one that calls it, then those it does
* not have; the integration lists its routes by id, which puts a called
route before its caller.
*/
+ /**
+ * The name of a route on its strip: its id, or for a route the sources do
not have and Camel named route1, its
+ * endpoint (rest-api:openapi, the route that serves the API description
of the REST DSL).
+ */
+ private String stripName(IntegrationInfo info, String routeId) {
+ About.Model m = model;
+ if (m == null || !routeId.matches("route\\d+") ||
m.routes().stream().anyMatch(r -> routeId.equals(r.routeId()))) {
+ return routeId;
+ }
+ String from = info.routes.stream().filter(r ->
routeId.equals(r.routeId) && r.from != null).map(r -> r.from)
+ .findFirst().orElse(null);
+ if (from == null) {
+ return routeId;
+ }
+ int q = from.indexOf('?');
+ return (q >= 0 ? from.substring(0, q) : from).replaceFirst("://", ":");
+ }
+
private List<String> routeOrder(IntegrationInfo info) {
List<String> running = info.routes.stream().map(r ->
r.routeId).filter(id -> id != null).toList();
List<String> order = new ArrayList<>();
@@ -720,7 +738,8 @@ final class AboutView {
int seconds = seconds();
List<String> order = routeOrder(info);
// the names, then one space: every line starts its columns at the
same place
- int label = Math.min(18, Math.max(8,
order.stream().mapToInt(String::length).max().orElse(8))) + 4;
+ int longest = Math.max(8, order.stream().mapToInt(id ->
stripName(info, id).length()).max().orElse(8));
+ int label = Math.min(18, longest) + 4;
List<Timeline.RouteSeries> series = ctx.timeline.routes(info.pid,
order, end, seconds);
// what the line of a route has after its strip (its exchanges, an
open circuit breaker, the worst that
// happened, with the cause of a recovery up to CAUSE_WIDTH) takes the
room it needs, and the strips the rest
@@ -729,6 +748,8 @@ final class AboutView {
notes = Math.max(notes, width(stripNotes(info, r, last, end,
seconds, CAUSE_WIDTH)));
}
int columns = stripColumns(seconds, width - label - notes - 1);
+ // a longer name in full, in what the strips leave
(content-based-router)
+ label += Math.max(0, Math.min(longest + 4 - label, width - label -
notes - 1 - columns));
// the cause gets what the strips leave too
int causeRoom = CAUSE_WIDTH + Math.max(0, width - label - notes - 1 -
columns);
stripColumns = columns;
@@ -741,7 +762,8 @@ final class AboutView {
if (spaced && !lines.isEmpty()) {
lines.add(Line.from(Span.raw("")));
}
- String name = r.route().length() > label - 3 ?
r.route().substring(0, label - 4) + "…" : r.route();
+ String name = stripName(info, r.route());
+ name = name.length() > label - 3 ? name.substring(0, label - 4) +
"…" : name;
long max = 1;
// completed, recovered, handled, failed, down, redelivery
attempts: the height is the exchanges done, the
// attempts only colour the column (an exchange retried many times
is still one exchange)
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutViewTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutViewTest.java
index f748a70566ba..1f8c6b769ee4 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutViewTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutViewTest.java
@@ -444,6 +444,73 @@ class AboutViewTest {
assertThat(about.tableJson(info).getString("keys")).contains("r the
README, s the summary");
}
+ @Test
+ void theRouteCamelAddsForTheApiDescriptionIsNamedByItsEndpoint() throws
Exception {
+ // contracts/rest-code-first: the REST DSL serves /openapi from a
route Camel adds and names route1
+ Files.writeString(dir.resolve("rest-code-first.camel.yaml"), """
+ - route:
+ id: list-orders
+ from:
+ uri: direct:list-orders
+ steps:
+ - log: listed
+ """);
+ IntegrationInfo info = new IntegrationInfo();
+ info.pid = "1234";
+ info.name = "rest-code-first";
+ info.directory = dir.toString();
+ for (String[] r : List.of(new String[] { "list-orders",
"direct://list-orders" },
+ new String[] { "route1", "rest-api://openapi" })) {
+ RouteInfo route = new RouteInfo();
+ route.routeId = r[0];
+ route.from = r[1];
+ route.state = "Started";
+ info.routes.add(route);
+ }
+ TimelineJson t = new TimelineJson(T0 / 1000 + 59, 60);
+ t.route("list-orders", "completed", 55, 1).route("route1",
"completed", 56, 1);
+ MonitorContext ctx = new MonitorContext(new
AtomicReference<>(List.of(info)), new AtomicReference<>(List.of()));
+ ctx.timeline.merge("1234", t.json(), true);
+ AboutView about = new AboutView(ctx);
+
+ await().atMost(20, TimeUnit.SECONDS).untilAsserted(() -> {
+ List<String> strips = render(about, info, 200,
34).lines().dropWhile(l -> !l.contains("What happened"))
+ .filter(l -> l.contains(" list-orders ") ||
l.contains(" rest-api:openapi ")).toList();
+ assertThat(strips).hasSize(2);
+ });
+ assertThat(render(about, info, 200, 34)).doesNotContain(" route1 ");
+ }
+
+ @Test
+ void aLongRouteNameIsShownInFullWhenTheStripsLeaveRoom() throws Exception {
+ // route/content-based-router: its one route has the name of the
example
+ Files.writeString(dir.resolve("content-based-router.camel.yaml"), """
+ - route:
+ id: content-based-router
+ from:
+ uri: file:orders
+ steps:
+ - log: routed
+ """);
+ IntegrationInfo info = new IntegrationInfo();
+ info.pid = "1234";
+ info.name = "content-based-router";
+ info.directory = dir.toString();
+ RouteInfo route = new RouteInfo();
+ route.routeId = "content-based-router";
+ route.state = "Started";
+ info.routes.add(route);
+ TimelineJson t = new TimelineJson(T0 / 1000 + 59, 60);
+ t.route("content-based-router", "completed", 55, 3);
+ MonitorContext ctx = new MonitorContext(new
AtomicReference<>(List.of(info)), new AtomicReference<>(List.of()));
+ ctx.timeline.merge("1234", t.json(), true);
+ AboutView about = new AboutView(ctx);
+
+ await().atMost(20, TimeUnit.SECONDS).untilAsserted(() ->
assertThat(render(about, info, 200, 34).lines()
+ .dropWhile(l -> !l.contains("What happened")).filter(l ->
l.contains("content-based-rout")).toList())
+ .singleElement().satisfies(l ->
assertThat(l).contains("content-based-router ").doesNotContain("…")));
+ }
+
@Test
void lastFiveMinutesCountsAnExchangeOnceNotOncePerRouteItPasses() throws
Exception {
// fail-well/circuit-breaker: stock-check calls supplier with direct,
once a second
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ObservedOutsTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ObservedOutsTest.java
index d7f9c3f9af73..3985234d4034 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ObservedOutsTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ObservedOutsTest.java
@@ -64,6 +64,22 @@ class ObservedOutsTest {
assertThat(ObservedOuts.of(info,
List.of())).extracting(About.Out::uri).containsExactly("kafka:audit");
}
+ @Test
+ void anOutTheRuntimeHasEncodedIsTheOneOfTheRoutes() {
+ // contracts/openapi-client: the route names
rest-openapi:stock-api.json#reserveStock, the runtime counts it
+ // as rest-openapi://stock-api.json%23reserveStock
+ IntegrationInfo info = new IntegrationInfo();
+ info.endpoints.add(endpoint("reserve-order-lines", "out",
"rest-openapi://stock-api.json%23reserveStock",
+ "remote", 6));
+ List<About.Out> known = List.of(new About.Out(
+ "http", "rest-openapi:stock-api.json#reserveStock", "out",
+ "reserve-order-lines"));
+
+ assertThat(ObservedOuts.of(info, known)).isEmpty();
+ assertThat(AboutView.outHits(info,
"rest-openapi:stock-api.json#reserveStock", "reserve-order-lines"))
+ .isEqualTo(6);
+ }
+
@Test
void theServiceOfAUriIsAllButItsLastSegment() {
assertThat(ObservedOuts.group("http://localhost:8080/stock/CAMEL-MUG")).isEqualTo("http://localhost:8080/stock/");