From ca3fb399da94f9c8cb09e856a2fb11d8a5abd5e2 Mon Sep 17 00:00:00 2001 From: Vignesh Nayak Manel Date: Tue, 17 Feb 2026 01:23:09 +0530 Subject: [PATCH 1/3] Added camel-jbang diagram command --- .../modules/ROOT/pages/camel-jbang.adoc | 18 +++ .../jbang/core/commands/CamelJBangMain.java | 1 + .../dsl/jbang/core/commands/Diagram.java | 147 ++++++++++++++++++ .../dsl/jbang/core/commands/DiagramTest.java | 50 ++++++ 4 files changed, 216 insertions(+) create mode 100644 dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Diagram.java create mode 100644 dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/DiagramTest.java diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang.adoc b/docs/user-manual/modules/ROOT/pages/camel-jbang.adoc index cb01716706155..0e6bfcbb1ace0 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-jbang.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-jbang.adoc @@ -2429,6 +2429,24 @@ then Camel will attempt to uninstall the Jolokia JVM Agent. However, this may no able to do this always, because the JVM is being terminated which can prevent camel-jbang from doing JVM process communication to the running Camel integration. +=== Visualizing routes with diagram + +The `camel diagram` command uses Hawtio to visualize Camel routes. You can run route files in the background and open Hawtio in a browser: + +[source,bash] +---- +camel diagram routes.yaml +---- + +To attach to an existing integration: + +[source,bash] +---- +camel diagram --name my-integration +---- + +By default Camel JBang stops the background integration when you exit Hawtio; use `--keep-running` to leave it running. Image export is currently manual via the Hawtio UI. + === Scripting from terminal using pipes You can also execute a Camel JBang file as a script that can be used for terminal scripting with pipes and filters. diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java index 8a55de137ba18..6c75c79909672 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java @@ -161,6 +161,7 @@ public static void run(CamelJBangMain main, String... args) { .addSubcommand("transformer", new CommandLine(new ListTransformer(main))) .addSubcommand("variable", new CommandLine(new ListVariable(main))) .addSubcommand("vault", new CommandLine(new ListVault(main)))) + .addSubcommand("diagram", new CommandLine(new Diagram(main))) .addSubcommand("hawtio", new CommandLine(new Hawtio(main))) .addSubcommand("infra", new CommandLine(new InfraCommand(main)) .addSubcommand("get", new CommandLine(new InfraGet(main))) diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Diagram.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Diagram.java new file mode 100644 index 0000000000000..4fc5f5434eb7e --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Diagram.java @@ -0,0 +1,147 @@ +/* + * 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.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Stack; + +import org.apache.camel.dsl.jbang.core.commands.process.Hawtio; +import org.apache.camel.dsl.jbang.core.commands.process.StopProcess; +import org.apache.camel.util.FileUtil; +import picocli.CommandLine; + +@CommandLine.Command(name = "diagram", description = "Visualize Camel routes using Hawtio", sortOptions = false, + showDefaultValues = true) +public class Diagram extends CamelCommand { + + @CommandLine.Parameters(description = "The Camel file(s) to run. If no files specified then use --name to attach to a running integration.", + arity = "0..9", paramLabel = "", parameterConsumer = FilesConsumer.class) + Path[] filePaths; // Defined only for file path completion; the field never used + List files = new ArrayList<>(); + + @CommandLine.Option(names = { "--name" }, + description = "Name or pid of running Camel integration") + String name; + + @CommandLine.Option(names = { "--renderer" }, + description = "Renderer to use (hawtio)", + defaultValue = "hawtio") + String renderer = "hawtio"; + + @CommandLine.Option(names = { "--port" }, + description = "Port number to use for Hawtio web console (port 8888 by default)", defaultValue = "8888") + int port = 8888; + + @CommandLine.Option(names = { "--openUrl" }, + description = "To automatic open Hawtio web console in the web browser", defaultValue = "true") + boolean openUrl = true; + + @CommandLine.Option(names = { "--background-wait" }, defaultValue = "true", + description = "To wait for run in background to startup successfully, before returning") + boolean backgroundWait = true; + + @CommandLine.Option(names = { "--keep-running" }, defaultValue = "false", + description = "Keep the background Camel integration running after exiting Hawtio") + boolean keepRunning; + + public Diagram(CamelJBangMain main) { + super(main); + } + + @Override + public Integer doCall() throws Exception { + String selectedRenderer = renderer == null ? "hawtio" : renderer.toLowerCase(Locale.ROOT); + if (!"hawtio".equals(selectedRenderer)) { + printer().printErr("Unsupported renderer: " + renderer); + return 1; + } + + boolean hasFiles = files != null && !files.isEmpty(); + String runName = name; + if (hasFiles && (runName == null || runName.isBlank())) { + runName = FileUtil.onlyName(FileUtil.stripPath(files.get(0))); + } + String target = runName; + if (!hasFiles && (target == null || target.isBlank())) { + new CommandLine(this).execute("--help"); + return 0; + } + + long pid = 0; + boolean started = false; + int exit = 0; + try { + if (hasFiles) { + Run run = new Run(getMain()); + run.backgroundWait = backgroundWait; + if (runName != null && !runName.isBlank()) { + run.name = runName; + } + List args = new ArrayList<>(); + args.add("run"); + if (runName != null && !runName.isBlank()) { + args.add("--name=" + runName); + } + args.addAll(files); + RunHelper.addCamelJBangCommand(args); + ProcessBuilder pb = new ProcessBuilder(); + pb.command(args); + int rc = run.runBackgroundProcess(pb, "Camel Main"); + if (rc != 0) { + return rc; + } + pid = run.spawnPid; + if (pid <= 0) { + printer().printErr("Unable to determine the running Camel PID"); + return 1; + } + target = Long.toString(pid); + started = true; + } + + Hawtio hawtio = new Hawtio(getMain()); + List hawtioArgs = new ArrayList<>(); + if (target != null && !target.isBlank()) { + hawtioArgs.add(target); + } + hawtioArgs.add("--port=" + port); + hawtioArgs.add("--openUrl=" + openUrl); + CommandLine.populateCommand(hawtio, hawtioArgs.toArray(new String[0])); + exit = hawtio.doCall(); + return exit; + } finally { + if (started && !keepRunning) { + StopProcess stop = new StopProcess(getMain()); + if (target != null && !target.isBlank()) { + CommandLine.populateCommand(stop, target); + } + stop.doCall(); + } + } + } + + static class FilesConsumer extends ParameterConsumer { + @Override + protected void doConsumeParameters(Stack args, Diagram cmd) { + String arg = args.pop(); + cmd.files.add(arg); + } + } +} diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/DiagramTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/DiagramTest.java new file mode 100644 index 0000000000000..e3d4c52936c43 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/DiagramTest.java @@ -0,0 +1,50 @@ +/* + * 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.List; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import picocli.CommandLine; + +class DiagramTest extends CamelCommandBaseTestSupport { + + @Test + void shouldRejectUnknownRenderer() throws Exception { + Diagram command = new Diagram(new CamelJBangMain().withPrinter(printer)); + CommandLine.populateCommand(command, "--renderer=unknown"); + int exit = command.doCall(); + Assertions.assertEquals(1, exit); + Assertions.assertTrue(printer.getOutput().contains("Unsupported renderer")); + } + + @Test + void shouldCollectFilesFromParameters() { + Diagram command = new Diagram(new CamelJBangMain().withPrinter(printer)); + CommandLine.populateCommand(command, "a.yaml", "b.yaml"); + Assertions.assertEquals(List.of("a.yaml", "b.yaml"), command.files); + } + + @Test + void shouldShowHelpWhenNoArgs() throws Exception { + Diagram command = new Diagram(new CamelJBangMain().withPrinter(printer)); + int exit = command.doCall(); + Assertions.assertEquals(0, exit); + Assertions.assertTrue(printer.getOutput().isEmpty()); + } +} From 671deab12ff6330e53fe95d62278697ebf416d6f Mon Sep 17 00:00:00 2001 From: Vignesh Nayak Manel Date: Wed, 18 Feb 2026 23:19:29 +0530 Subject: [PATCH 2/3] Use playwright to capture png image of camel routes --- .../modules/ROOT/pages/camel-jbang.adoc | 12 +- dsl/camel-jbang/camel-jbang-core/pom.xml | 5 + .../dsl/jbang/core/commands/Diagram.java | 816 +++++++++++++++++- .../dsl/jbang/core/commands/DiagramTest.java | 19 + parent/pom.xml | 1 + 5 files changed, 843 insertions(+), 10 deletions(-) diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang.adoc b/docs/user-manual/modules/ROOT/pages/camel-jbang.adoc index 0e6bfcbb1ace0..a1ef2a8088367 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-jbang.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-jbang.adoc @@ -2445,7 +2445,17 @@ To attach to an existing integration: camel diagram --name my-integration ---- -By default Camel JBang stops the background integration when you exit Hawtio; use `--keep-running` to leave it running. Image export is currently manual via the Hawtio UI. +By default Camel JBang stops the background integration when you exit Hawtio; use `--keep-running` to leave it running. + +You can export a PNG snapshot using Playwright (requires a Chromium browser binary and executable path configured): + +[source,bash] +---- +camel diagram routes.yaml --output=routes.png --playwright-browser-path=/path/to/chrome +---- + +When using `--output`, Hawtio runs headless and the PNG is captured automatically. +NOTE: PNG export currently supports Chromium only. Use `PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH` or `--playwright-browser-path`. === Scripting from terminal using pipes diff --git a/dsl/camel-jbang/camel-jbang-core/pom.xml b/dsl/camel-jbang/camel-jbang-core/pom.xml index 8ef78c3154885..7333cc8b632f3 100644 --- a/dsl/camel-jbang/camel-jbang-core/pom.xml +++ b/dsl/camel-jbang/camel-jbang-core/pom.xml @@ -103,6 +103,11 @@ ascii-table ${ascii-table-version} + + com.microsoft.playwright + playwright + ${playwright-version} + diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Diagram.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Diagram.java index 4fc5f5434eb7e..7cbc8515fb993 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Diagram.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Diagram.java @@ -16,15 +16,40 @@ */ package org.apache.camel.dsl.jbang.core.commands; +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Stack; +import java.util.concurrent.ThreadLocalRandom; +import com.microsoft.playwright.Browser; +import com.microsoft.playwright.BrowserType; +import com.microsoft.playwright.Locator; +import com.microsoft.playwright.Page; +import com.microsoft.playwright.Page.NavigateOptions; +import com.microsoft.playwright.Page.ScreenshotOptions; +import com.microsoft.playwright.Page.WaitForFunctionOptions; +import com.microsoft.playwright.Page.WaitForSelectorOptions; +import com.microsoft.playwright.Playwright; +import com.microsoft.playwright.options.ViewportSize; +import com.microsoft.playwright.options.WaitUntilState; import org.apache.camel.dsl.jbang.core.commands.process.Hawtio; +import org.apache.camel.dsl.jbang.core.commands.process.Jolokia; import org.apache.camel.dsl.jbang.core.commands.process.StopProcess; import org.apache.camel.util.FileUtil; +import org.apache.camel.util.StopWatch; import picocli.CommandLine; @CommandLine.Command(name = "diagram", description = "Visualize Camel routes using Hawtio", sortOptions = false, @@ -61,6 +86,33 @@ public class Diagram extends CamelCommand { description = "Keep the background Camel integration running after exiting Hawtio") boolean keepRunning; + @CommandLine.Option(names = { "--output" }, + description = "Write a PNG snapshot of the route diagram to the given file") + Path output; + + @CommandLine.Option(names = { "--browser" }, + description = "Playwright browser to use (chromium only)", + defaultValue = "chromium") + String browser = "chromium"; + + @CommandLine.Option(names = { "--playwright-browser-path" }, + description = "Path to the Playwright browser executable") + String playwrightBrowserPath; + + @CommandLine.Option(names = { "--playwright-timeout" }, defaultValue = "120000", + description = "Timeout in millis for Playwright navigation and rendering") + long playwrightTimeout = 120000; + + @CommandLine.Option(names = { "--route-id" }, + description = "Route id to render (defaults to the first route)") + String routeId; + + @CommandLine.Option(names = { "--jolokia-port" }, defaultValue = "8778", + description = "Jolokia port to attach when exporting PNG") + int jolokiaPort = 8778; + + private boolean jolokiaAttached; + public Diagram(CamelJBangMain main) { super(main); } @@ -74,6 +126,11 @@ public Integer doCall() throws Exception { } boolean hasFiles = files != null && !files.isEmpty(); + boolean exportPng = output != null; + if (exportPng && openUrl) { + openUrl = false; + } + String runName = name; if (hasFiles && (runName == null || runName.isBlank())) { runName = FileUtil.onlyName(FileUtil.stripPath(files.get(0))); @@ -116,16 +173,36 @@ public Integer doCall() throws Exception { started = true; } - Hawtio hawtio = new Hawtio(getMain()); - List hawtioArgs = new ArrayList<>(); - if (target != null && !target.isBlank()) { - hawtioArgs.add(target); + if (exportPng) { + String hawtioUrl = "http://localhost:" + port + "/hawtio"; + String jolokiaUrl = "http://127.0.0.1:" + jolokiaPort + "/jolokia"; + int attachCode = attachJolokia(target, jolokiaUrl); + if (attachCode != 0) { + return attachCode; + } + HawtioProcess hawtioProcess = startHawtioProcess(port); + try { + waitForHawtio(hawtioUrl, hawtioProcess); + exit = exportDiagramPng(hawtioUrl, jolokiaUrl); + } finally { + stopProcess(hawtioProcess.process); + if (keepRunning && jolokiaAttached) { + detachJolokia(target); + } + } + return exit; + } else { + Hawtio hawtio = new Hawtio(getMain()); + List hawtioArgs = new ArrayList<>(); + if (target != null && !target.isBlank()) { + hawtioArgs.add(target); + } + hawtioArgs.add("--port=" + port); + hawtioArgs.add("--openUrl=" + openUrl); + CommandLine.populateCommand(hawtio, hawtioArgs.toArray(new String[0])); + exit = hawtio.doCall(); + return exit; } - hawtioArgs.add("--port=" + port); - hawtioArgs.add("--openUrl=" + openUrl); - CommandLine.populateCommand(hawtio, hawtioArgs.toArray(new String[0])); - exit = hawtio.doCall(); - return exit; } finally { if (started && !keepRunning) { StopProcess stop = new StopProcess(getMain()); @@ -137,6 +214,727 @@ public Integer doCall() throws Exception { } } + private int attachJolokia(String target, String jolokiaUrl) throws Exception { + if (target == null || target.isBlank()) { + printer().printErr("Name or PID required to attach Jolokia for PNG export"); + return 1; + } + jolokiaAttached = false; + if (isJolokiaAvailable(jolokiaUrl)) { + return 0; + } + Jolokia jolokia = new Jolokia(getMain()); + List args = new ArrayList<>(); + args.add(target); + args.add("--port=" + jolokiaPort); + CommandLine.populateCommand(jolokia, args.toArray(new String[0])); + int code = jolokia.doCall(); + if (code != 0) { + return waitForJolokia(jolokiaUrl, 5000) ? 0 : code; + } + if (waitForJolokia(jolokiaUrl, 5000)) { + jolokiaAttached = true; + return 0; + } + printer().printErr("Unable to attach Jolokia at " + jolokiaUrl); + return 1; + } + + private void detachJolokia(String target) { + try { + if (target == null || target.isBlank()) { + return; + } + Jolokia jolokia = new Jolokia(getMain()); + CommandLine.populateCommand(jolokia, "--stop", target); + jolokia.doCall(); + } catch (Exception e) { + printer().printErr("Failed to stop Jolokia: " + e.getMessage()); + } + } + + private HawtioProcess startHawtioProcess(int port) throws Exception { + List args = new ArrayList<>(); + args.add("hawtio"); + args.add("--port=" + port); + args.add("--openUrl=false"); + RunHelper.addCamelJBangCommand(args); + ProcessBuilder pb = new ProcessBuilder(); + pb.command(args); + pb.redirectErrorStream(true); + Process process = pb.start(); + List output = Collections.synchronizedList(new ArrayList<>()); + startOutputPump(process.getInputStream(), output); + return new HawtioProcess(process, output); + } + + private void stopProcess(Process process) { + if (process == null) { + return; + } + try { + process.destroy(); + process.waitFor(); + } catch (Exception e) { + // ignore + } + } + + private void waitForHawtio(String hawtioUrl, HawtioProcess hawtioProcess) throws Exception { + StopWatch watch = new StopWatch(); + while (watch.taken() < playwrightTimeout) { + try { + URL url = URI.create(hawtioUrl).toURL(); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setConnectTimeout(1000); + conn.setReadTimeout(1000); + conn.setRequestMethod("GET"); + int code = conn.getResponseCode(); + if (code >= 200 && code < 500) { + return; + } + } catch (Exception e) { + // ignore until timeout + } + if (hawtioProcess != null && !hawtioProcess.process.isAlive()) { + throw new IllegalStateException("Hawtio terminated before startup." + formatHawtioOutput(hawtioProcess.output)); + } + Thread.sleep(500); + } + throw new IllegalStateException( + "Hawtio did not start within " + playwrightTimeout + " ms." + formatHawtioOutput(hawtioProcess.output)); + } + + private boolean isJolokiaAvailable(String jolokiaUrl) { + try { + String probeUrl = jolokiaUrl.endsWith("/") ? jolokiaUrl + "version" : jolokiaUrl + "/version"; + URL url = URI.create(probeUrl).toURL(); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setConnectTimeout(1000); + conn.setReadTimeout(1000); + conn.setRequestMethod("GET"); + int code = conn.getResponseCode(); + return code >= 200 && code < 500 && code != 404; + } catch (Exception e) { + return false; + } + } + + private boolean waitForJolokia(String jolokiaUrl, long timeoutMs) { + StopWatch watch = new StopWatch(); + while (watch.taken() < timeoutMs) { + if (isJolokiaAvailable(jolokiaUrl)) { + return true; + } + try { + Thread.sleep(200); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return false; + } + } + return false; + } + + private int exportDiagramPng(String hawtioUrl, String jolokiaUrl) throws Exception { + Path outputPath = output; + if (outputPath == null) { + printer().printErr("Output file is required for PNG export"); + return 1; + } + if (outputPath.getParent() != null) { + Files.createDirectories(outputPath.getParent()); + } + if (browser != null && !"chromium".equalsIgnoreCase(browser)) { + printer().printErr("Only chromium is supported for PNG export at the moment."); + return 1; + } + try (Playwright playwright = Playwright.create()) { + BrowserType browserType = selectBrowser(playwright); + String execPath = resolveBrowserPath(); + if (execPath == null) { + printer().printErr("Playwright browser executable path not configured. " + + "Set --playwright-browser-path or PLAYWRIGHT_*_EXECUTABLE_PATH."); + return 1; + } + BrowserType.LaunchOptions launchOptions = new BrowserType.LaunchOptions() + .setHeadless(true) + .setExecutablePath(Paths.get(execPath)); + try (Browser browserInstance = browserType.launch(launchOptions)) { + Page page = browserInstance.newPage(); + page.setDefaultTimeout(playwrightTimeout); + page.setDefaultNavigationTimeout(playwrightTimeout); + connectToJolokia(page, hawtioUrl, jolokiaUrl); + openRouteDiagram(page); + captureDiagramScreenshot(page, outputPath); + } + } + return 0; + } + + private BrowserType selectBrowser(Playwright playwright) { + return playwright.chromium(); + } + + private String resolveBrowserPath() { + if (playwrightBrowserPath != null && !playwrightBrowserPath.isBlank()) { + return playwrightBrowserPath; + } + Map env = System.getenv(); + return env.get("PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH"); + } + + private void connectToJolokia(Page page, String hawtioUrl, String jolokiaUrl) { + String connectionId = generateConnectionId(); + String connectionsJson = buildConnectionJson(connectionId, jolokiaUrl); + String escapedConnections = escapeJavaScript(connectionsJson); + page.addInitScript( + "(() => { const connections = JSON.parse('" + escapedConnections + "'); " + + "localStorage.setItem('connect.connections', JSON.stringify(connections)); " + + "const id = Object.keys(connections)[0]; " + + "sessionStorage.setItem('connect.currentConnection', JSON.stringify(id)); })();"); + page.navigate(hawtioUrl + "?con=" + connectionId + "#/camel/routes", + new NavigateOptions().setWaitUntil(WaitUntilState.DOMCONTENTLOADED)); + waitForFunction(page, "sessionStorage.getItem('connect.currentConnection') !== null", + playwrightTimeout / 3); + } + + private void openRouteDiagram(Page page) { + navigateToHash(page, "#/camel"); + if (routeId != null && !routeId.isBlank()) { + if (!selectRouteInTree(page, routeId)) { + throw new IllegalStateException("Route id not found in Hawtio tree: " + routeId); + } + } else { + selectRoutesFolder(page); + } + navigateToHash(page, "#/camel/routeDiagram"); + if (!waitForSelector(page, ".react-flow__node, .react-flow__nodes, svg", playwrightTimeout / 2)) { + throw new IllegalStateException("Route diagram not available in Hawtio. Ensure Jolokia connection succeeded."); + } + waitForDiagramAssets(page); + waitForDiagramStable(page); + prepareDiagramForScreenshot(page); + } + + private boolean selectRouteInTree(Page page, String routeId) { + try { + waitForSelector(page, "#camel-tree-view .pf-v5-c-tree-view__node", playwrightTimeout / 6); + Locator expandAll = page.locator("#camel-tree-view button:has-text(\"Expand all\")"); + if (expandAll.count() > 0) { + expandAll.first().click(); + page.waitForTimeout(300); + } + Locator search = page.locator("#input-search"); + if (search.count() > 0) { + search.fill(routeId); + page.waitForTimeout(300); + } + Locator routeNode = null; + Locator routeNodes = page.locator("#camel-tree-view button.pf-v5-c-tree-view__node-text"); + List labels = routeNodes.allTextContents(); + for (int i = 0; i < labels.size(); i++) { + if (routeId.equals(labels.get(i).trim())) { + routeNode = routeNodes.nth(i); + break; + } + } + if (routeNode == null) { + routeNode = page.locator("#camel-tree-view button.pf-v5-c-tree-view__node-text:has-text(\"" + routeId + "\")"); + } + if (routeNode.count() == 0) { + return false; + } + routeNode.first().scrollIntoViewIfNeeded(); + routeNode.first().click(); + waitForFunction( + page, + "routeId => Array.from(document.querySelectorAll('#camel-tree-view [aria-selected=\"true\"]'" + + ")).some(el => Array.from(el.querySelectorAll('button.pf-v5-c-tree-view__node-text'))" + + ".some(btn => btn.textContent && btn.textContent.trim() === routeId))", + routeId, + playwrightTimeout / 6); + return true; + } catch (Exception e) { + return false; + } + } + + private void selectRoutesFolder(Page page) { + try { + waitForSelector(page, "#camel-tree-view .pf-v5-c-tree-view__node", playwrightTimeout / 6); + Locator expandAll = page.locator("#camel-tree-view button:has-text(\"Expand all\")"); + if (expandAll.count() > 0) { + expandAll.first().click(); + page.waitForTimeout(300); + } + Locator routeNodes = page.locator("#camel-tree-view button.pf-v5-c-tree-view__node-text"); + List labels = routeNodes.allTextContents(); + for (int i = 0; i < labels.size(); i++) { + if ("routes".equalsIgnoreCase(labels.get(i).trim())) { + Locator routesNode = routeNodes.nth(i); + routesNode.scrollIntoViewIfNeeded(); + routesNode.click(); + waitForFunction( + page, + "(() => Array.from(document.querySelectorAll('#camel-tree-view [aria-selected=\"true\"]'))" + + ".some(el => Array.from(el.querySelectorAll('button.pf-v5-c-tree-view__node-text'))" + + ".some(btn => btn.textContent && btn.textContent.trim().toLowerCase() === 'routes'))" + + ")()", + playwrightTimeout / 6); + break; + } + } + } catch (Exception e) { + printer().printErr("Failed to select routes folder: " + e.getMessage()); + } + } + + private void captureDiagramScreenshot(Page page, Path outputPath) { + prepareDiagramForScreenshot(page); + waitForDiagramStable(page); + if (routeId == null || routeId.isBlank()) { + normalizeDiagramLayout(page); + waitForDiagramStable(page); + } + if (captureDiagramClip(page, outputPath)) { + return; + } + Locator container = page.locator("#camel-route-diagram-outer-div"); + if (container.count() == 0) { + container = page.locator(".react-flow"); + } + if (container.count() > 0) { + try { + container.first().screenshot(new Locator.ScreenshotOptions().setPath(outputPath)); + return; + } catch (com.microsoft.playwright.PlaywrightException e) { + printer().printErr("Diagram container changed while rendering, capturing full page instead: " + e.getMessage()); + } + } + Locator diagram = page.locator("svg").first(); + if (diagram.count() > 0) { + try { + diagram.screenshot(new Locator.ScreenshotOptions().setPath(outputPath)); + return; + } catch (com.microsoft.playwright.PlaywrightException e) { + printer().printErr("Diagram element changed while rendering, capturing full page instead: " + e.getMessage()); + } + } + page.screenshot(new ScreenshotOptions().setPath(outputPath).setFullPage(true)); + } + + private void normalizeDiagramLayout(Page page) { + try { + page.evaluate("(() => {" + + "const outer = document.querySelector('#camel-route-diagram-outer-div');" + + "const viewport = outer ? outer.querySelector('.react-flow__viewport') : null;" + + "const nodes = outer ? Array.from(outer.querySelectorAll('.react-flow__node')) : [];" + + "const edges = outer ? Array.from(outer.querySelectorAll('.react-flow__edge-path')) : [];" + + "const labels = outer ? Array.from(outer.querySelectorAll('.react-flow__node *, .react-flow__edge-text, .react-flow__edge-textwrapper')) : [];" + + "const renderer = outer ? outer.querySelector('.react-flow__renderer') : null;" + + "if (!outer || !viewport || (nodes.length === 0 && edges.length === 0)) { return; }" + + "const transform = viewport.style.transform || '';" + + "let scale = 1;" + + "let tx = 0;" + + "let ty = 0;" + + "let match = transform.match(/translate\\(([-0-9.]+)px,\\s*([-0-9.]+)px\\)/);" + + "if (!match) { match = transform.match(/translate3d\\(([-0-9.]+)px,\\s*([-0-9.]+)px/); }" + + "if (match) { tx = parseFloat(match[1]); ty = parseFloat(match[2]); }" + + "const scaleMatch = transform.match(/scale\\(([-0-9.]+)\\)/);" + + "if (scaleMatch) { scale = parseFloat(scaleMatch[1]); }" + + "if (!isFinite(scale) || scale <= 0) { scale = 1; }" + + "if (!isFinite(tx)) { tx = 0; }" + + "if (!isFinite(ty)) { ty = 0; }" + + "const base = renderer || outer;" + + "const baseRect = base.getBoundingClientRect();" + + "let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;" + + "const updateBounds = (rect) => {" + + " const x1 = (rect.left - baseRect.left - tx) / scale;" + + " const y1 = (rect.top - baseRect.top - ty) / scale;" + + " const x2 = (rect.right - baseRect.left - tx) / scale;" + + " const y2 = (rect.bottom - baseRect.top - ty) / scale;" + + " minX = Math.min(minX, x1);" + + " minY = Math.min(minY, y1);" + + " maxX = Math.max(maxX, x2);" + + " maxY = Math.max(maxY, y2);" + + "};" + + "nodes.forEach(node => updateBounds(node.getBoundingClientRect()));" + + "edges.forEach(edge => updateBounds(edge.getBoundingClientRect()));" + + "labels.forEach(label => updateBounds(label.getBoundingClientRect()));" + + "if (!isFinite(minX) || !isFinite(minY) || !isFinite(maxX) || !isFinite(maxY)) { return; }" + + "const padding = 24;" + + "const extraLeft = 48;" + + "const extraRight = 96;" + + "const extraTop = 0;" + + "const extraBottom = 24;" + + "const leftPadding = padding + extraLeft;" + + "const rightPadding = padding + extraRight;" + + "const topPadding = padding + extraTop;" + + "const bottomPadding = padding + extraBottom;" + + "const width = Math.ceil((maxX - minX) * scale + leftPadding + rightPadding);" + + "const height = Math.ceil((maxY - minY) * scale + topPadding + bottomPadding);" + + "outer.style.width = `${width}px`;" + + "outer.style.height = `${height}px`;" + + "outer.style.minWidth = outer.style.width;" + + "outer.style.minHeight = outer.style.height;" + + "const container = outer.querySelector('.react-flow');" + + "if (container) {" + + " container.style.width = outer.style.width;" + + " container.style.height = outer.style.height;" + + "}" + + "viewport.style.transformOrigin = '0 0';" + + "viewport.style.transform = `translate(${leftPadding - minX * scale}px, ${topPadding - minY * scale}px)`" + + " + ` scale(${scale})`;" + + "})();"); + } catch (Exception e) { + // ignore layout failures + } + } + + private void waitForDiagramStable(Page page) { + waitForFunction( + page, + "(() => {" + + "const nodes = Array.from(document.querySelectorAll('.react-flow__node'));" + + "if (!nodes.length) { return false; }" + + "let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;" + + "for (const node of nodes) {" + + " const rect = node.getBoundingClientRect();" + + " minX = Math.min(minX, rect.left);" + + " minY = Math.min(minY, rect.top);" + + " maxX = Math.max(maxX, rect.right);" + + " maxY = Math.max(maxY, rect.bottom);" + + "}" + + "const bounds = {" + + "minX: Math.round(minX), minY: Math.round(minY)," + + "maxX: Math.round(maxX), maxY: Math.round(maxY) };" + + "const viewport = document.querySelector('.react-flow__viewport');" + + "const transform = viewport ? viewport.style.transform : '';" + + "const now = Date.now();" + + "const state = window.__camelDiagramState || (window.__camelDiagramState = {" + + "count: nodes.length, transform, bounds, at: now });" + + "const changed = state.count !== nodes.length || state.transform !== transform" + + "|| !state.bounds || state.bounds.minX !== bounds.minX || state.bounds.minY !== bounds.minY" + + "|| state.bounds.maxX !== bounds.maxX || state.bounds.maxY !== bounds.maxY;" + + "if (changed) {" + + "state.count = nodes.length; state.transform = transform; state.bounds = bounds; state.at = now;" + + "return false; }" + + "return (now - state.at) > 500;" + + "})()", + playwrightTimeout / 2); + } + + private void prepareDiagramForScreenshot(Page page) { + try { + // Hide Hawtio chrome and allow overflow so the diagram renders unclipped. + page.evaluate("(() => {" + + "const sidebar = document.querySelector('.pf-v5-c-page__sidebar');" + + "if (sidebar) { sidebar.style.display = 'none'; }" + + "const header = document.querySelector('.pf-v5-c-page__header');" + + "if (header) { header.style.display = 'none'; }" + + "const split = document.querySelector('.camel-split');" + + "if (split && split.children && split.children.length > 1) {" + + " split.children[0].style.display = 'none';" + + " split.children[1].style.width = '100%';" + + "}" + + "const outer = document.querySelector('#camel-route-diagram-outer-div');" + + "const container = outer ? outer.querySelector('.react-flow') : null;" + + "const layers = [" + + " '#camel-route-diagram-outer-div .react-flow__renderer'," + + " '#camel-route-diagram-outer-div .react-flow__pane'," + + " '#camel-route-diagram-outer-div .react-flow__viewport'," + + " '#camel-route-diagram-outer-div .react-flow__container'," + + " '#camel-route-diagram-outer-div .camel-route-diagram'" + + "];" + + "if (outer) { outer.style.overflow = 'visible'; }" + + "if (container) { container.style.overflow = 'visible'; }" + + "layers.forEach(sel => document.querySelectorAll(sel)" + + " .forEach(el => { el.style.overflow = 'visible'; }));" + + "document.querySelectorAll('.pf-v5-c-scroll-outer-wrapper, .pf-v5-c-scroll-inner-wrapper')" + + " .forEach(el => {" + + " el.style.overflow = 'visible';" + + " el.style.maxWidth = 'none';" + + " el.style.maxHeight = 'none';" + + " });" + + "const unclip = (el) => {" + + " let node = el;" + + " while (node && node !== document.body) {" + + " node.style.overflow = 'visible';" + + " node.style.maxWidth = 'none';" + + " node.style.maxHeight = 'none';" + + " node = node.parentElement;" + + " }" + + "};" + + "if (outer) { unclip(outer); }" + + "const main = document.querySelector('#camel-content-main');" + + "if (main) { main.style.overflow = 'visible'; }" + + "document.documentElement.style.overflow = 'visible';" + + "document.body.style.overflow = 'visible';" + + "})();"); + } catch (Exception e) { + // ignore prepare failures + } + } + + private boolean captureDiagramClip(Page page, Path outputPath) { + try { + // Compute a tight clip around nodes/edges so we avoid full-page screenshots. + Object clip = page.evaluate("(() => {" + + "const outer = document.querySelector('#camel-route-diagram-outer-div');" + + "const viewport = document.querySelector('#camel-route-diagram-outer-div .react-flow__viewport');" + + "const nodes = Array.from(document.querySelectorAll('#camel-route-diagram-outer-div .react-flow__node'));" + + "const edges = Array.from(document.querySelectorAll('#camel-route-diagram-outer-div .react-flow__edge-path'));" + + "const labels = Array.from(document.querySelectorAll('#camel-route-diagram-outer-div .react-flow__node *,'" + + " + ' #camel-route-diagram-outer-div .react-flow__edge-text,'" + + " + ' #camel-route-diagram-outer-div .react-flow__edge-textwrapper'));" + + "if (nodes.length === 0 && edges.length === 0) { return null; }" + + "const parseTransform = () => {" + + " const transform = viewport ? viewport.style.transform || '' : '';" + + " let tx = 0, ty = 0, scale = 1;" + + " let match = transform.match(/translate\\(([-0-9.]+)px,\\s*([-0-9.]+)px\\)/);" + + " if (!match) { match = transform.match(/translate3d\\(([-0-9.]+)px,\\s*([-0-9.]+)px/); }" + + " if (match) { tx = parseFloat(match[1]); ty = parseFloat(match[2]); }" + + " const scaleMatch = transform.match(/scale\\(([-0-9.]+)\\)/);" + + " if (scaleMatch) { scale = parseFloat(scaleMatch[1]); }" + + " if (!isFinite(scale) || scale <= 0) { scale = 1; }" + + " return { tx, ty, scale };" + + "};" + + "const computeBounds = () => {" + + " let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;" + + " const updateBounds = (rect) => {" + + " minX = Math.min(minX, rect.left);" + + " minY = Math.min(minY, rect.top);" + + " maxX = Math.max(maxX, rect.right);" + + " maxY = Math.max(maxY, rect.bottom);" + + " };" + + " const applyBounds = (list) => {" + + " for (const el of list) { updateBounds(el.getBoundingClientRect()); }" + + " };" + + " if (nodes.length > 0) { applyBounds(nodes); }" + + " if (labels.length > 0) { applyBounds(labels); }" + + " if (edges.length > 0) {" + + " let eMinX = Infinity, eMinY = Infinity, eMaxX = -Infinity, eMaxY = -Infinity;" + + " for (const el of edges) {" + + " const rect = el.getBoundingClientRect();" + + " eMinX = Math.min(eMinX, rect.left);" + + " eMinY = Math.min(eMinY, rect.top);" + + " eMaxX = Math.max(eMaxX, rect.right);" + + " eMaxY = Math.max(eMaxY, rect.bottom);" + + " }" + + " if (nodes.length == 0) {" + + " minX = eMinX; minY = eMinY; maxX = eMaxX; maxY = eMaxY;" + + " } else {" + + " const edgeSlack = 24;" + + " if (eMinX < minX - edgeSlack) { minX = eMinX; }" + + " if (eMinY < minY - edgeSlack) { minY = eMinY; }" + + " if (eMaxX > maxX + edgeSlack) { maxX = eMaxX; }" + + " if (eMaxY > maxY + edgeSlack) { maxY = eMaxY; }" + + " }" + + " }" + + " if (!isFinite(minX) || !isFinite(minY) || !isFinite(maxX) || !isFinite(maxY)) { return null; }" + + " return { minX, minY, maxX, maxY };" + + "};" + + "const padding = 24;" + + "const extraLeft = 48;" + + "const extraRight = 96;" + + "const extraTop = 0;" + + "const extraBottom = 24;" + + "const leftPadding = padding + extraLeft;" + + "const rightPadding = padding + extraRight;" + + "const topPadding = padding + extraTop;" + + "const bottomPadding = padding + extraBottom;" + + "let bounds = computeBounds();" + + "if (!bounds) { return null; }" + + "if (viewport) {" + + " const rect = outer ? outer.getBoundingClientRect() : { left: 0, top: 0 };" + + " const desiredLeft = rect.left + leftPadding;" + + " const desiredTop = rect.top + topPadding;" + + " const deltaX = bounds.minX < desiredLeft ? (desiredLeft - bounds.minX) : 0;" + + " const deltaY = bounds.minY < desiredTop ? (desiredTop - bounds.minY) : 0;" + + " if (Math.abs(deltaX) > 1 || Math.abs(deltaY) > 1) {" + + " const transform = parseTransform();" + + " const tx = transform.tx + deltaX;" + + " const ty = transform.ty + deltaY;" + + " viewport.style.transformOrigin = '0 0';" + + " viewport.style.transform = `translate(${tx}px, ${ty}px) scale(${transform.scale})`;" + + " bounds = computeBounds();" + + " if (!bounds) { return null; }" + + " }" + + "}" + + "const scrollX = window.scrollX || window.pageXOffset || 0;" + + "const scrollY = window.scrollY || window.pageYOffset || 0;" + + "const x1 = bounds.minX + scrollX - leftPadding;" + + "const y1 = bounds.minY + scrollY - topPadding;" + + "const x2 = bounds.maxX + scrollX + rightPadding;" + + "const y2 = bounds.maxY + scrollY + bottomPadding;" + + "const x = Math.max(0, x1);" + + "const y = Math.max(0, y1);" + + "const width = Math.max(1, x2 - x1);" + + "const height = Math.max(1, y2 - y1);" + + "return {x, y, width, height};" + + "})()"); + if (!(clip instanceof Map clipMap)) { + return false; + } + Double x = toDouble(clipMap.get("x")); + Double y = toDouble(clipMap.get("y")); + Double width = toDouble(clipMap.get("width")); + Double height = toDouble(clipMap.get("height")); + if (x == null || y == null || width == null || height == null || width <= 0 || height <= 0) { + return false; + } + ensureViewportForClip(page, x + width, y + height); + page.screenshot(new ScreenshotOptions().setPath(outputPath).setClip(x, y, width, height)); + return true; + } catch (Exception e) { + return false; + } + } + + private void ensureViewportForClip(Page page, double requiredWidth, double requiredHeight) { + if (requiredWidth <= 0 || requiredHeight <= 0) { + return; + } + ViewportSize current = page.viewportSize(); + int currentWidth = current != null ? current.width : 0; + int currentHeight = current != null ? current.height : 0; + int targetWidth = (int) Math.ceil(requiredWidth); + int targetHeight = (int) Math.ceil(requiredHeight); + if (targetWidth > currentWidth || targetHeight > currentHeight) { + page.setViewportSize(Math.max(targetWidth, currentWidth), Math.max(targetHeight, currentHeight)); + } + } + + private void waitForDiagramAssets(Page page) { + try { + page.evaluate("async () => {" + + "if (document.fonts && document.fonts.ready) { await document.fonts.ready; }" + + "const imgs = Array.from(document.querySelectorAll('#camel-route-diagram-outer-div img'));" + + "await Promise.all(imgs.map(img => img.decode().catch(() => {})));" + + "}"); + } catch (Exception e) { + // ignore asset wait failures + } + } + + private Double toDouble(Object value) { + if (value instanceof Number number) { + return number.doubleValue(); + } + return null; + } + + private String generateConnectionId() { + int random = ThreadLocalRandom.current().nextInt(1_000_000); + return "c" + String.format("%06d", random) + "-" + System.currentTimeMillis(); + } + + private String buildConnectionJson(String connectionId, String jolokiaUrl) { + URI uri = URI.create(jolokiaUrl); + String scheme = uri.getScheme() != null ? uri.getScheme() : "http"; + String host = uri.getHost() != null ? uri.getHost() : "127.0.0.1"; + int port = uri.getPort(); + if (port <= 0) { + port = "https".equalsIgnoreCase(scheme) ? 443 : 80; + } + String path = uri.getPath(); + if (path == null || path.isBlank()) { + path = "/jolokia"; + } + return "{\"" + escapeJson(connectionId) + "\":{\"id\":\"" + escapeJson(connectionId) + "\"," + + "\"name\":\"local\"," + "\"scheme\":\"" + escapeJson(scheme) + "\"," + + "\"host\":\"" + escapeJson(host) + "\"," + "\"port\":" + port + "," + + "\"path\":\"" + escapeJson(path) + "\"}}"; + } + + private String escapeJson(String value) { + return value.replace("\\", "\\\\").replace("\"", "\\\""); + } + + private String escapeJavaScript(String value) { + return value.replace("\\", "\\\\").replace("'", "\\'").replace("\n", "\\n").replace("\r", "\\r"); + } + + private void navigateToHash(Page page, String hash) { + try { + page.evaluate("hash => { window.location.hash = hash; }", hash); + page.waitForTimeout(500); + } catch (Exception e) { + // ignore navigation issues + } + } + + private boolean waitForSelector(Page page, String selector, long timeout) { + try { + page.waitForSelector(selector, new WaitForSelectorOptions().setTimeout(timeout)); + return true; + } catch (Exception e) { + return false; + } + } + + private boolean waitForFunction(Page page, String expression, long timeout) { + try { + page.waitForFunction(expression, new WaitForFunctionOptions().setTimeout(timeout)); + return true; + } catch (Exception e) { + return false; + } + } + + private boolean waitForFunction(Page page, String expression, Object arg, long timeout) { + try { + page.waitForFunction(expression, arg, new WaitForFunctionOptions().setTimeout(timeout)); + return true; + } catch (Exception e) { + return false; + } + } + + private void startOutputPump(InputStream inputStream, List output) { + Thread thread = new Thread(() -> { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + synchronized (output) { + if (output.size() >= 200) { + output.remove(0); + } + output.add(line); + } + } + } catch (Exception e) { + // ignore + } + }, "camel-diagram-hawtio-output"); + thread.setDaemon(true); + thread.start(); + } + + private String formatHawtioOutput(List output) { + if (output == null || output.isEmpty()) { + return ""; + } + synchronized (output) { + if (output.isEmpty()) { + return ""; + } + return System.lineSeparator() + "Hawtio output:" + System.lineSeparator() + + String.join(System.lineSeparator(), output); + } + } + + private static final class HawtioProcess { + private final Process process; + private final List output; + + private HawtioProcess(Process process, List output) { + this.process = process; + this.output = output; + } + } + static class FilesConsumer extends ParameterConsumer { @Override protected void doConsumeParameters(Stack args, Diagram cmd) { diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/DiagramTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/DiagramTest.java index e3d4c52936c43..3f328bb42ba74 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/DiagramTest.java +++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/DiagramTest.java @@ -16,6 +16,7 @@ */ package org.apache.camel.dsl.jbang.core.commands; +import java.nio.file.Paths; import java.util.List; import org.junit.jupiter.api.Assertions; @@ -47,4 +48,22 @@ void shouldShowHelpWhenNoArgs() throws Exception { Assertions.assertEquals(0, exit); Assertions.assertTrue(printer.getOutput().isEmpty()); } + + @Test + void shouldPopulateExportOptions() { + Diagram command = new Diagram(new CamelJBangMain().withPrinter(printer)); + CommandLine.populateCommand(command, + "--output=routes.png", + "--browser=chromium", + "--playwright-browser-path=/bin/chromium", + "--playwright-timeout=5000", + "--route-id=route1", + "--jolokia-port=8889"); + Assertions.assertEquals(Paths.get("routes.png"), command.output); + Assertions.assertEquals("chromium", command.browser); + Assertions.assertEquals("/bin/chromium", command.playwrightBrowserPath); + Assertions.assertEquals(5000, command.playwrightTimeout); + Assertions.assertEquals("route1", command.routeId); + Assertions.assertEquals(8889, command.jolokiaPort); + } } diff --git a/parent/pom.xml b/parent/pom.xml index 2a950bd0d84b8..f858c387b5e63 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -450,6 +450,7 @@ 0.8.9 4.7.7 3.1.0 + 1.43.0 0.13.1 4.11.0 1.2.0 From 9e895ac2e25e7edabaae96cc14470387cc263288 Mon Sep 17 00:00:00 2001 From: Vignesh Nayak Manel Date: Thu, 19 Feb 2026 00:02:57 +0530 Subject: [PATCH 3/3] Update as per latest code to resolve compile error --- .../org/apache/camel/dsl/jbang/core/commands/Diagram.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Diagram.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Diagram.java index 7cbc8515fb993..b193436cde17c 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Diagram.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Diagram.java @@ -157,7 +157,7 @@ public Integer doCall() throws Exception { args.add("--name=" + runName); } args.addAll(files); - RunHelper.addCamelJBangCommand(args); + RunHelper.addCamelCLICommand(args); ProcessBuilder pb = new ProcessBuilder(); pb.command(args); int rc = run.runBackgroundProcess(pb, "Camel Main"); @@ -258,7 +258,7 @@ private HawtioProcess startHawtioProcess(int port) throws Exception { args.add("hawtio"); args.add("--port=" + port); args.add("--openUrl=false"); - RunHelper.addCamelJBangCommand(args); + RunHelper.addCamelCLICommand(args); ProcessBuilder pb = new ProcessBuilder(); pb.command(args); pb.redirectErrorStream(true);