Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@

// AUTO-GENERATED by camel-package-maven-plugin - DO NOT EDIT THIS FILE
= camel cmd route-diagram

Display Camel route diagram in the terminal


== Usage

[source,bash]
----
camel cmd route-diagram [options]
----



== Options

[cols="2,5,1,2",options="header"]
|===
| Option | Description | Default | Type
| `--filter` | Filter route by filename or route id | | String
| `--output` | Save diagram to a PNG file instead of displaying in terminal | | String
| `--theme,--colors` | Color theme preset (dark, light, transparent) or custom colors (e.g. bg=#1e1e1e:from=#2e7d32:to=#1565c0). Values can be #hex or ANSI color names (e.g. from=seagreen:to=steelblue). Use bg= for transparent. Can also be set via DIAGRAM_COLORS env var. | dark | String
| `--width` | Image width in pixels (0 = auto) | 0 | int
| `-h,--help` | Display the help and sub-commands | | boolean
|===


Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ camel cmd [options]
| xref:jbang-commands/camel-jbang-cmd-reload.adoc[reload] | Trigger reloading Camel
| xref:jbang-commands/camel-jbang-cmd-reset-stats.adoc[reset-stats] | Reset performance statistics
| xref:jbang-commands/camel-jbang-cmd-resume-route.adoc[resume-route] | Resume Camel routes
| xref:jbang-commands/camel-jbang-cmd-route-diagram.adoc[route-diagram] | Display Camel route diagram in the terminal
| xref:jbang-commands/camel-jbang-cmd-route-structure.adoc[route-structure] | Dump Camel route structure
| xref:jbang-commands/camel-jbang-cmd-send.adoc[send] | Send messages to endpoints
| xref:jbang-commands/camel-jbang-cmd-start-group.adoc[start-group] | Start Camel route groups
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ public void execute(String... args) {
.addSubcommand("reload", new CommandLine(new CamelReloadAction(this)))
.addSubcommand("reset-stats", new CommandLine(new CamelResetStatsAction(this)))
.addSubcommand("resume-route", new CommandLine(new CamelRouteResumeAction(this)))
.addSubcommand("route-diagram", new CommandLine(new CamelRouteDiagramAction(this)))
.addSubcommand("route-structure", new CommandLine(new CamelRouteStructureAction(this)))
.addSubcommand("send", new CommandLine(new CamelSendAction(this)))
.addSubcommand("start-group", new CommandLine(new CamelRouteGroupStartAction(this)))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
/*
* 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.action;

import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

import javax.imageio.ImageIO;

import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
import org.apache.camel.dsl.jbang.core.commands.action.RouteDiagramLayoutEngine.LayoutRoute;
import org.apache.camel.dsl.jbang.core.commands.action.RouteDiagramLayoutEngine.NodeInfo;
import org.apache.camel.dsl.jbang.core.commands.action.RouteDiagramLayoutEngine.RouteInfo;
import org.apache.camel.dsl.jbang.core.commands.action.RouteDiagramRenderer.DiagramColors;
import org.apache.camel.dsl.jbang.core.common.PathUtils;
import org.apache.camel.support.PatternHelper;
import org.apache.camel.util.json.JsonArray;
import org.apache.camel.util.json.JsonObject;
import org.apache.camel.util.json.Jsoner;
import org.jline.terminal.Terminal;
import org.jline.terminal.TerminalBuilder;
import org.jline.terminal.impl.TerminalGraphics;
import org.jline.terminal.impl.TerminalGraphicsManager;
import picocli.CommandLine;
import picocli.CommandLine.Command;

@Command(name = "route-diagram", description = "Display Camel route diagram in the terminal", sortOptions = false,
showDefaultValues = true)
public class CamelRouteDiagramAction extends ActionBaseCommand {

@CommandLine.Parameters(description = "Name or pid of running Camel integration", arity = "0..1")
String name = "*";

@CommandLine.Option(names = { "--filter" },
description = "Filter route by filename or route id")
String filter;

@CommandLine.Option(names = { "--width" },
description = "Image width in pixels (0 = auto)", defaultValue = "0")
int width;

@CommandLine.Option(names = { "--output" },
description = "Save diagram to a PNG file instead of displaying in terminal")
String output;

@CommandLine.Option(names = { "--theme", "--colors" },
description = "Color theme preset (dark, light, transparent) or custom colors "
+ "(e.g. bg=#1e1e1e:from=#2e7d32:to=#1565c0). Values can be #hex or "
+ "ANSI color names (e.g. from=seagreen:to=steelblue). "
+ "Use bg= for transparent. Can also be set via DIAGRAM_COLORS env var.",
defaultValue = "dark")
String theme;

public CamelRouteDiagramAction(CamelJBangMain main) {
super(main);
}

@Override
public Integer doCall() throws Exception {
System.setProperty("java.awt.headless", "true");

String colorSpec = System.getenv("DIAGRAM_COLORS");
DiagramColors colors = DiagramColors.parse(colorSpec != null ? colorSpec : theme);

List<Long> pids = findPids(name);
if (pids.isEmpty()) {
return 1;
} else if (pids.size() > 1) {
printer().println("Name or pid " + name + " matches " + pids.size()
+ " running Camel integrations. Specify a name or PID that matches exactly one.");
return 1;
}

long pid = pids.get(0);

Path outputFile = prepareAction(Long.toString(pid), "route-structure", root -> {
root.put("filter", "*");
root.put("brief", false);
});

try {
JsonObject jo = getJsonObject(outputFile);
if (jo == null) {
printer().println("Response from running Camel with PID " + pid + " not received within 5 seconds");
return 1;
}

List<RouteInfo> routes = parseRoutes(jo);
if (routes.isEmpty()) {
printer().println("No routes found");
return 0;
}

if (filter != null) {
routes = routes.stream()
.filter(r -> (r.routeId != null && PatternHelper.matchPattern(r.routeId, filter))
|| (r.source != null && PatternHelper.matchPattern(r.source, filter)))
.toList();
}

if (routes.isEmpty()) {
printer().println("No routes match filter: " + filter);
return 0;
}

RouteDiagramLayoutEngine engine = new RouteDiagramLayoutEngine();
RouteDiagramRenderer renderer = new RouteDiagramRenderer();

List<LayoutRoute> layoutRoutes = new ArrayList<>();
int currentY = RouteDiagramLayoutEngine.PADDING;
for (RouteInfo route : routes) {
LayoutRoute lr = engine.layoutRoute(route, currentY);
layoutRoutes.add(lr);
currentY = lr.maxY + RouteDiagramLayoutEngine.V_GAP;
}

java.awt.image.BufferedImage image;
try {
image = renderer.renderDiagram(layoutRoutes, currentY, colors);
} catch (IllegalStateException e) {
printer().println(e.getMessage());
return 1;
}

if (output != null) {
File file = new File(output);
File parentDir = file.getParentFile();
if (parentDir != null) {
parentDir.mkdirs();
}
ImageIO.write(image, "PNG", file);
printer().println("Diagram saved to: " + file.getAbsolutePath());
} else {
try (Terminal terminal = TerminalBuilder.builder().system(true).build()) {
try {
Optional<TerminalGraphics> protocol = TerminalGraphicsManager.getBestProtocol(terminal);
if (protocol.isPresent()) {
TerminalGraphics.ImageOptions opts = new TerminalGraphics.ImageOptions()
.preserveAspectRatio(true);
if (width > 0) {
opts.width(width);
}
protocol.get().displayImage(terminal, image, opts);
terminal.writer().println();
terminal.flush();
} else {
printer().println(
"Terminal does not support graphics protocols (Kitty, iTerm2, or Sixel).");
printer().println(
"Try running in a supported terminal: Kitty, iTerm2, WezTerm, Ghostty, or VS Code.");
renderer.printTextDiagram(routes, printer());
}
} catch (IOException | UnsupportedOperationException e) {
printer().println("Failed to display diagram in terminal: " + e.getMessage());
printer().println("Falling back to text diagram.");
renderer.printTextDiagram(routes, printer());
}
}
}

return 0;
} finally {
PathUtils.deleteFile(outputFile);
}
}

List<RouteInfo> parseRoutes(JsonObject jo) {
List<RouteInfo> routes = new ArrayList<>();
JsonArray arr = (JsonArray) jo.get("routes");
if (arr == null) {
return routes;
}

for (int i = 0; i < arr.size(); i++) {
JsonObject o = (JsonObject) arr.get(i);
RouteInfo route = new RouteInfo();
route.routeId = o.getString("routeId");
route.source = CamelRouteStructureAction.extractSourceName(o.getString("source"));

List<JsonObject> lines = o.getCollection("code");
if (lines != null) {
for (JsonObject line : lines) {
NodeInfo node = new NodeInfo();
node.type = line.getString("type");
node.code = Jsoner.unescape(line.getString("code"));
Integer level = line.getInteger("level");
node.level = level != null ? level : 0;
route.nodes.add(node);
}
}
routes.add(route);
}
return routes;
}
}
Loading
Loading