You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by da...@apache.org on 2023/08/16 17:14:42 UTC

[camel] branch jbang-stub created (now 0c675fe8cb2)

This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a change to branch jbang-stub
in repository https://gitbox.apache.org/repos/asf/camel.git


      at 0c675fe8cb2 CAMEL-19747: camel-jbang - Add command to browse stub queues

This branch includes the following new commits:

     new 0c675fe8cb2 CAMEL-19747: camel-jbang - Add command to browse stub queues

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



[camel] 01/01: CAMEL-19747: camel-jbang - Add command to browse stub queues

Posted by da...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch jbang-stub
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 0c675fe8cb2c8db49111c9dd79764f258ad73145
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Wed Aug 16 19:14:22 2023 +0200

    CAMEL-19747: camel-jbang - Add command to browse stub queues
---
 .../apache/camel/component/stub/StubConsole.java   | 119 +++++--
 .../camel/impl/console/EndpointDevConsole.java     |  12 +-
 .../camel/impl/console/SourceDevConsole.java       |   4 +-
 .../camel/cli/connector/LocalCliConnector.java     |  16 +-
 .../dsl/jbang/core/commands/CamelJBangMain.java    |   2 +
 .../jbang/core/commands/action/CamelSourceTop.java |   4 +-
 .../core/commands/action/CamelStubAction.java      | 378 +++++++++++++++++++++
 .../core/commands/action/CamelThreadDump.java      |   4 +-
 .../core/commands/action/MessageTableHelper.java   |   3 +
 .../commands/action/RouteControllerAction.java     |   4 +-
 .../jbang/core/commands/process/ListEndpoint.java  |   3 +
 11 files changed, 509 insertions(+), 40 deletions(-)

diff --git a/components/camel-stub/src/main/java/org/apache/camel/component/stub/StubConsole.java b/components/camel-stub/src/main/java/org/apache/camel/component/stub/StubConsole.java
index 0e5430d7b76..4974e752ebe 100644
--- a/components/camel-stub/src/main/java/org/apache/camel/component/stub/StubConsole.java
+++ b/components/camel-stub/src/main/java/org/apache/camel/component/stub/StubConsole.java
@@ -26,6 +26,7 @@ import java.util.Set;
 import org.apache.camel.Exchange;
 import org.apache.camel.spi.annotations.DevConsole;
 import org.apache.camel.support.MessageHelper;
+import org.apache.camel.support.PatternHelper;
 import org.apache.camel.support.console.AbstractDevConsole;
 import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
@@ -33,24 +34,45 @@ import org.apache.camel.util.json.JsonObject;
 @DevConsole("stub")
 public class StubConsole extends AbstractDevConsole {
 
+    /**
+     * Filters the routes matching by queue name
+     */
+    public static final String FILTER = "filter";
+
+    /**
+     * Limits the number of messages dumped
+     */
+    public static final String LIMIT = "limit";
+
     /**
      * To use either xml or json output format
      */
     public static final String FORMAT = "format";
 
+    /**
+     * Whether to browse messages
+     */
+    public static final String BROWSE = "browse";
+
     public StubConsole() {
         super("camel", "stub", "Stub", "Browse messages on stub");
     }
 
     @Override
     protected String doCallText(Map<String, Object> options) {
-        StringBuilder sb = new StringBuilder();
+        String filter = (String) options.get(FILTER);
+        String limit = (String) options.get(LIMIT);
+        String browse = (String) options.get(BROWSE);
+        final int max = limit == null ? Integer.MAX_VALUE : Integer.parseInt(limit);
+        final boolean dump = browse == null ? Boolean.FALSE : Boolean.parseBoolean(browse);
 
-        StubComponent sc = getCamelContext().getComponent("stub", StubComponent.class);
+        StringBuilder sb = new StringBuilder();
 
         List<StubEndpoint> list = getCamelContext().getEndpoints()
-                .stream().filter(e -> e instanceof StubEndpoint)
+                .stream()
+                .filter(e -> e instanceof StubEndpoint)
                 .map(StubEndpoint.class::cast)
+                .filter(e -> accept(e.getName(), filter))
                 .toList();
 
         Set<String> names = new HashSet<>();
@@ -65,23 +87,30 @@ public class StubConsole extends AbstractDevConsole {
 
             sb.append(String.format("Queue: %s (max: %d, size: %d)%n", name, se.getSize(), se.getCurrentQueueSize()));
 
-            // browse messages
-            Queue<Exchange> q = se.getQueue();
-            for (Exchange exchange : q) {
-                // dump to xml or json
-                try {
-                    String format = (String) options.get(FORMAT);
-                    String dump = null;
-                    if (format == null || "xml".equals(format)) {
-                        dump = MessageHelper.dumpAsXml(exchange.getMessage(), true, 4);
-                    } else if ("json".equals(format)) {
-                        dump = MessageHelper.dumpAsJSon(exchange.getMessage(), true, 4);
-                    }
-                    if (dump != null) {
-                        sb.append("\n").append(dump).append("\n");
+            if (dump) {
+                Queue<Exchange> q = se.getQueue();
+                List<Exchange> copy = new ArrayList<>(q);
+                if (max > 0 && q.size() > max) {
+                    int pos = q.size() - 1 - max;
+                    int end = q.size() - 1;
+                    copy = copy.subList(pos, end);
+                }
+                for (Exchange exchange : copy) {
+                    // dump to xml or json
+                    try {
+                        String format = (String) options.get(FORMAT);
+                        String msg = null;
+                        if (format == null || "xml".equals(format)) {
+                            msg = MessageHelper.dumpAsXml(exchange.getMessage(), true, 4);
+                        } else if ("json".equals(format)) {
+                            msg = MessageHelper.dumpAsJSon(exchange.getMessage(), true, 4);
+                        }
+                        if (msg != null) {
+                            sb.append("\n").append(msg).append("\n");
+                        }
+                    } catch (Exception e) {
+                        // ignore
                     }
-                } catch (Exception e) {
-                    // ignore
                 }
             }
         }
@@ -91,12 +120,19 @@ public class StubConsole extends AbstractDevConsole {
 
     @Override
     protected JsonObject doCallJson(Map<String, Object> options) {
+        String filter = (String) options.get(FILTER);
+        String limit = (String) options.get(LIMIT);
+        String browse = (String) options.get(BROWSE);
+        final int max = limit == null ? Integer.MAX_VALUE : Integer.parseInt(limit);
+        final boolean dump = browse == null ? Boolean.FALSE : Boolean.parseBoolean(browse);
+
         JsonObject root = new JsonObject();
         JsonArray queues = new JsonArray();
 
         List<StubEndpoint> list = getCamelContext().getEndpoints()
                 .stream().filter(e -> e instanceof StubEndpoint)
                 .map(StubEndpoint.class::cast)
+                .filter(e -> accept(e.getName(), filter))
                 .toList();
 
         Set<String> names = new HashSet<>();
@@ -111,21 +147,34 @@ public class StubConsole extends AbstractDevConsole {
 
             JsonObject jo = new JsonObject();
             jo.put("name", name);
+            jo.put("endpointUri", se.getEndpointUri());
             jo.put("max", se.getSize());
             jo.put("size", se.getCurrentQueueSize());
-            List<JsonObject> arr = new ArrayList<>();
-            Queue<Exchange> q = se.getQueue();
-            for (Exchange exchange : q) {
-                try {
-                    JsonObject dump
-                            = MessageHelper.dumpAsJSonObject(exchange.getMessage(), false, true, true, false, true, 128 * 1024);
-                    arr.add(dump);
-                } catch (Exception e) {
-                    // ignore
+
+            // browse messages
+            if (dump) {
+                List<JsonObject> arr = new ArrayList<>();
+
+                Queue<Exchange> q = se.getQueue();
+                List<Exchange> copy = new ArrayList<>(q);
+                if (max > 0 && q.size() > max) {
+                    int pos = q.size() - 1 - max;
+                    int end = q.size() - 1;
+                    copy = copy.subList(pos, end);
+                }
+                for (Exchange exchange : copy) {
+                    try {
+                        JsonObject msg
+                                = MessageHelper.dumpAsJSonObject(exchange.getMessage(), false, true, true, false, true,
+                                        128 * 1024);
+                        arr.add(msg);
+                    } catch (Exception e) {
+                        // ignore
+                    }
+                }
+                if (!arr.isEmpty()) {
+                    jo.put("messages", arr);
                 }
-            }
-            if (!arr.isEmpty()) {
-                jo.put("messages", arr);
             }
             queues.add(jo);
         }
@@ -134,4 +183,12 @@ public class StubConsole extends AbstractDevConsole {
         return root;
     }
 
+    private static boolean accept(String name, String filter) {
+        if (filter == null || filter.isBlank()) {
+            return true;
+        }
+
+        return PatternHelper.matchPattern(name, filter);
+    }
+
 }
diff --git a/core/camel-console/src/main/java/org/apache/camel/impl/console/EndpointDevConsole.java b/core/camel-console/src/main/java/org/apache/camel/impl/console/EndpointDevConsole.java
index 13caa235924..d5180306d91 100644
--- a/core/camel-console/src/main/java/org/apache/camel/impl/console/EndpointDevConsole.java
+++ b/core/camel-console/src/main/java/org/apache/camel/impl/console/EndpointDevConsole.java
@@ -53,12 +53,18 @@ public class EndpointDevConsole extends AbstractDevConsole {
         Collection<Endpoint> col = reg.getReadOnlyValues();
         if (!col.isEmpty()) {
             for (Endpoint e : col) {
+                boolean stub = e.getComponent().getClass().getSimpleName().equals("StubComponent");
+                String uri = e.toString();
+                if (!uri.startsWith("stub:") && stub) {
+                    // shadow-stub
+                    uri = uri + " (stub)";
+                }
                 var stat = findStats(stats, e.getEndpointUri());
                 if (stat.isPresent()) {
                     var st = stat.get();
-                    sb.append(String.format("\n    %s (direction: %s, usage: %s)", e, st.getDirection(), st.getHits()));
+                    sb.append(String.format("\n    %s (direction: %s, usage: %s)", uri, st.getDirection(), st.getHits()));
                 } else {
-                    sb.append(String.format("\n    %s", e));
+                    sb.append(String.format("\n    %s", uri));
                 }
             }
         }
@@ -88,7 +94,9 @@ public class EndpointDevConsole extends AbstractDevConsole {
         Collection<Endpoint> col = reg.getReadOnlyValues();
         for (Endpoint e : col) {
             JsonObject jo = new JsonObject();
+            boolean stub = e.getComponent().getClass().getSimpleName().equals("StubComponent");
             jo.put("uri", e.getEndpointUri());
+            jo.put("stub", stub);
             var stat = findStats(stats, e.getEndpointUri());
             if (stat.isPresent()) {
                 var st = stat.get();
diff --git a/core/camel-console/src/main/java/org/apache/camel/impl/console/SourceDevConsole.java b/core/camel-console/src/main/java/org/apache/camel/impl/console/SourceDevConsole.java
index 079a6ccf4a6..ea96a307af6 100644
--- a/core/camel-console/src/main/java/org/apache/camel/impl/console/SourceDevConsole.java
+++ b/core/camel-console/src/main/java/org/apache/camel/impl/console/SourceDevConsole.java
@@ -64,7 +64,7 @@ public class SourceDevConsole extends AbstractDevConsole {
                 try {
                     Resource resource = PluginHelper.getResourceLoader(getCamelContext()).resolveResource(loc);
                     if (resource != null) {
-                        if (sb.length() > 0) {
+                        if (!sb.isEmpty()) {
                             sb.append("\n");
                         }
 
@@ -87,7 +87,7 @@ public class SourceDevConsole extends AbstractDevConsole {
                 if (mrb.getSourceLocation() != null) {
                     sb.append(String.format("\n    Source: %s", mrb.getSourceLocation()));
                 }
-                if (code.length() > 0) {
+                if (!code.isEmpty()) {
                     sb.append(code);
                 }
             }
diff --git a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
index c748fd242dd..347dbd9256d 100644
--- a/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
+++ b/dsl/camel-cli-connector/src/main/java/org/apache/camel/cli/connector/LocalCliConnector.java
@@ -36,7 +36,6 @@ import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.stream.Collectors;
 
 import org.apache.camel.CamelContext;
 import org.apache.camel.CamelContextAware;
@@ -225,7 +224,7 @@ public class LocalCliConnector extends ServiceSupport implements CliConnector, C
                             }
                             return false;
                         })
-                        .collect(Collectors.toList());
+                        .toList();
                 for (String id : ids) {
                     try {
                         String command = root.getString("command");
@@ -325,6 +324,19 @@ public class LocalCliConnector extends ServiceSupport implements CliConnector, C
                     LOG.trace("Updating output file: {}", outputFile);
                     IOHelper.writeText(json.toJson(), outputFile);
                 }
+            } else if ("stub".equals(action)) {
+                String filter = root.getString("filter");
+                String limit = root.getString("limit");
+                String browse = root.getString("browse");
+
+                DevConsole dc = camelContext.getCamelContextExtension().getContextPlugin(DevConsoleRegistry.class)
+                        .resolveById("stub");
+                if (dc != null) {
+                    JsonObject json = (JsonObject) dc.call(DevConsole.MediaType.JSON,
+                            Map.of("filter", filter, "limit", limit, "browse", browse));
+                    LOG.trace("Updating output file: {}", outputFile);
+                    IOHelper.writeText(json.toJson(), outputFile);
+                }
             } else if ("send".equals(action)) {
                 StopWatch watch = new StopWatch();
                 long timestamp = System.currentTimeMillis();
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 220f3907b9d..b602c8a7426 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
@@ -31,6 +31,7 @@ import org.apache.camel.dsl.jbang.core.commands.action.CamelRouteStopAction;
 import org.apache.camel.dsl.jbang.core.commands.action.CamelSendAction;
 import org.apache.camel.dsl.jbang.core.commands.action.CamelSourceAction;
 import org.apache.camel.dsl.jbang.core.commands.action.CamelSourceTop;
+import org.apache.camel.dsl.jbang.core.commands.action.CamelStubAction;
 import org.apache.camel.dsl.jbang.core.commands.action.CamelThreadDump;
 import org.apache.camel.dsl.jbang.core.commands.action.CamelTraceAction;
 import org.apache.camel.dsl.jbang.core.commands.action.LoggerAction;
@@ -118,6 +119,7 @@ public class CamelJBangMain implements Callable<Integer> {
                         .addSubcommand("reset-stats", new CommandLine(new CamelResetStatsAction(main)))
                         .addSubcommand("reload", new CommandLine(new CamelReloadAction(main)))
                         .addSubcommand("send", new CommandLine(new CamelSendAction(main)))
+                        .addSubcommand("stub", new CommandLine(new CamelStubAction(main)))
                         .addSubcommand("thread-dump", new CommandLine(new CamelThreadDump(main)))
                         .addSubcommand("logger", new CommandLine(new LoggerAction(main)))
                         .addSubcommand("gc", new CommandLine(new CamelGCAction(main))))
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSourceTop.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSourceTop.java
index b378979df26..816fe6f8484 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSourceTop.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSourceTop.java
@@ -140,7 +140,9 @@ public class CamelSourceTop extends ActionWatchCommand {
         // sort rows
         rows.sort(this::sortRow);
 
-        clearScreen();
+        if (watch) {
+            clearScreen();
+        }
         if (!rows.isEmpty()) {
             printSource(rows);
         }
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelStubAction.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelStubAction.java
new file mode 100644
index 00000000000..ba9241d1e36
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelStubAction.java
@@ -0,0 +1,378 @@
+/*
+ * 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.FileInputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+import com.github.freva.asciitable.AsciiTable;
+import com.github.freva.asciitable.Column;
+import com.github.freva.asciitable.HorizontalAlign;
+import com.github.freva.asciitable.OverflowBehaviour;
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.support.PatternHelper;
+import org.apache.camel.util.FileUtil;
+import org.apache.camel.util.IOHelper;
+import org.apache.camel.util.StopWatch;
+import org.apache.camel.util.URISupport;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+import org.fusesource.jansi.Ansi;
+import picocli.CommandLine;
+import picocli.CommandLine.Command;
+
+@Command(name = "stub", description = "Browse stub endpoints", sortOptions = false)
+public class CamelStubAction extends ActionWatchCommand {
+
+    @CommandLine.Parameters(description = "Name or pid of running Camel integration", arity = "0..1")
+    String name = "*";
+
+    @CommandLine.Option(names = { "--sort" },
+                        description = "Sort by name, or total", defaultValue = "name")
+    String sort;
+
+    @CommandLine.Option(names = { "--filter" },
+                        description = "Filter endpoints by queue name")
+    String filter;
+
+    @CommandLine.Option(names = { "--browse" },
+                        description = "Whether to browse messages queued in the stub endpoints")
+    boolean browse;
+
+    @CommandLine.Option(names = { "--top" }, defaultValue = "true",
+                        description = "Whether to browse top (latest) messages queued in the stub endpoints")
+    boolean top = true;
+
+    @CommandLine.Option(names = { "--limit" }, defaultValue = "10",
+                        description = "Filter browsing queues by limiting to the given latest number of messages")
+    int limit = 10;
+
+    @CommandLine.Option(names = { "--find" },
+                        description = "Find and highlight matching text (ignore case).", arity = "0..*")
+    String[] find;
+
+    @CommandLine.Option(names = { "--grep" },
+                        description = "Filter browsing messages to only output trace matching text (ignore case).",
+                        arity = "0..*")
+    String[] grep;
+
+    @CommandLine.Option(names = { "--show-headers" }, defaultValue = "true",
+                        description = "Show message headers in traced messages")
+    boolean showHeaders = true;
+
+    @CommandLine.Option(names = { "--show-body" }, defaultValue = "true",
+                        description = "Show message body in traced messages")
+    boolean showBody = true;
+
+    @CommandLine.Option(names = { "--compact" }, defaultValue = "true",
+                        description = "Compact output (no empty line separating browsed messages)")
+    boolean compact = true;
+
+    @CommandLine.Option(names = { "--mask" },
+                        description = "Whether to mask endpoint URIs to avoid printing sensitive information such as password or access keys")
+    boolean mask;
+
+    @CommandLine.Option(names = { "--pretty" },
+                        description = "Pretty print message body when using JSon or XML format")
+    boolean pretty;
+
+    @CommandLine.Option(names = { "--logging-color" }, defaultValue = "true", description = "Use colored logging")
+    boolean loggingColor = true;
+
+    private volatile long pid;
+    String findAnsi;
+    private MessageTableHelper tableHelper;
+    private final Map<String, Ansi.Color> exchangeIdColors = new HashMap<>();
+    private int exchangeIdColorsIndex = 1;
+
+    public CamelStubAction(CamelJBangMain main) {
+        super(main);
+    }
+
+    @Override
+    protected Integer doWatchCall() throws Exception {
+        // setup table helper
+        tableHelper = new MessageTableHelper();
+        tableHelper.setPretty(pretty);
+        tableHelper.setLoggingColor(loggingColor);
+        tableHelper.setExchangeIdColorChooser(value -> {
+            Ansi.Color color = exchangeIdColors.get(value);
+            if (color == null) {
+                // grab a new color
+                exchangeIdColorsIndex++;
+                if (exchangeIdColorsIndex > 6) {
+                    exchangeIdColorsIndex = 2;
+                }
+                color = Ansi.Color.values()[exchangeIdColorsIndex];
+                exchangeIdColors.put(value, color);
+            }
+            return color;
+        });
+        if (find != null || grep != null) {
+            findAnsi = Ansi.ansi().fg(Ansi.Color.BLACK).bg(Ansi.Color.YELLOW).a("$0").reset().toString();
+        }
+
+        List<Row> rows = new ArrayList<>();
+
+        List<Long> pids = findPids(name);
+        if (pids.isEmpty()) {
+            return 0;
+        } else if (pids.size() > 1) {
+            System.out.println("Name or pid " + name + " matches " + pids.size()
+                               + " running Camel integrations. Specify a name or PID that matches exactly one.");
+            return 0;
+        }
+
+        this.pid = pids.get(0);
+
+        if (filter == null) {
+            filter = "*";
+        }
+
+        // ensure output file is deleted before executing action
+        File outputFile = getOutputFile(Long.toString(pid));
+        FileUtil.deleteFile(outputFile);
+
+        JsonObject root = new JsonObject();
+        root.put("action", "stub");
+        root.put("format", "json");
+        root.put("browse", browse);
+        root.put("filter", "*");
+        root.put("limit", limit);
+        File file = getActionFile(Long.toString(pid));
+        try {
+            IOHelper.writeText(root.toJson(), file);
+        } catch (Exception e) {
+            // ignore
+        }
+
+        JsonObject jo = waitForOutputFile(outputFile);
+        if (jo != null) {
+            JsonObject me = loadStatus(pid);
+            if (me != null) {
+                me = (JsonObject) me.get("context");
+            }
+
+            JsonArray arr = (JsonArray) jo.get("queues");
+            if (arr != null) {
+                for (int i = 0; i < arr.size(); i++) {
+                    JsonObject o = (JsonObject) arr.get(i);
+                    Row row = new Row();
+                    row.pid = pid;
+                    row.name = me != null ? me.getString("name") : null;
+                    row.queue = o.getString("name");
+                    row.max = o.getInteger("max");
+                    row.size = o.getInteger("size");
+                    String uri = o.getString("endpointUri");
+                    if (uri != null) {
+                        row.endpoint = new JsonObject();
+                        if (mask) {
+                            uri = URISupport.sanitizeUri(uri);
+                        }
+                        row.endpoint.put("endpoint", uri);
+                    }
+                    row.messages = o.getCollection("messages");
+                    boolean add = true;
+                    if (filter != null) {
+                        String f = filter;
+                        boolean negate = filter.startsWith("-");
+                        if (negate) {
+                            f = f.substring(1);
+                        }
+                        // make filtering easier
+                        if (!f.endsWith("*")) {
+                            f += "*";
+                        }
+                        boolean match = PatternHelper.matchPattern(row.queue, f);
+                        if (negate) {
+                            match = !match;
+                        }
+                        if (!match) {
+                            add = false;
+                        }
+                    }
+                    if (add) {
+                        rows.add(row);
+                    }
+                }
+            }
+        } else {
+            System.out.println("Response from running Camel with PID " + pid + " not received within 5 seconds");
+            return 1;
+        }
+
+        // sort rows
+        rows.sort(this::sortRow);
+
+        if (watch) {
+            clearScreen();
+        }
+        if (!rows.isEmpty()) {
+            printStub(rows);
+        }
+
+        // delete output file after use
+        FileUtil.deleteFile(outputFile);
+
+        return 0;
+    }
+
+    protected int sortRow(Row o1, Row o2) {
+        String s = sort;
+        int negate = 1;
+        if (s.startsWith("-")) {
+            s = s.substring(1);
+            negate = -1;
+        }
+        switch (s) {
+            case "name":
+                return o1.name.compareToIgnoreCase(o2.name) * negate;
+            case "total":
+                return Integer.compare(o1.size, o2.size) * negate;
+            default:
+                return 0;
+        }
+    }
+
+    private boolean isValidGrep(String line) {
+        if (grep == null) {
+            return true;
+        }
+        for (String g : grep) {
+            boolean m = Pattern.compile("(?i)" + g).matcher(line).find();
+            if (m) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    protected void printStub(List<Row> rows) {
+        if (browse) {
+            for (Row row : rows) {
+                System.out.println(AsciiTable.getTable(AsciiTable.NO_BORDERS, List.of(row), Arrays.asList(
+                        new Column().header("PID").headerAlign(HorizontalAlign.CENTER).with(r -> "" + r.pid),
+                        new Column().header("NAME").dataAlign(HorizontalAlign.LEFT)
+                                .maxWidth(30, OverflowBehaviour.ELLIPSIS_RIGHT)
+                                .with(r -> r.name),
+                        new Column().header("QUEUE").dataAlign(HorizontalAlign.LEFT).with(r -> r.queue),
+                        new Column().header("MAX").dataAlign(HorizontalAlign.RIGHT).with(r -> "" + r.max),
+                        new Column().header("TOTAL").dataAlign(HorizontalAlign.RIGHT).with(r -> "" + r.size))));
+
+                if (row.messages != null) {
+                    List<JsonObject> list = row.messages;
+                    if (top) {
+                        Collections.reverse(list);
+                    }
+                    boolean first = true;
+                    for (JsonObject jo : list) {
+                        JsonObject root = (JsonObject) jo.get("message");
+                        if (!showHeaders) {
+                            root.remove("headers");
+                        }
+                        if (!showBody) {
+                            root.remove("body");
+                        }
+                        String data
+                                = tableHelper.getDataAsTable(root.getString("exchangeId"), root.getString("exchangePattern"),
+                                        row.endpoint, root, null);
+                        if (data != null) {
+                            String[] lines = data.split(System.lineSeparator());
+                            if (lines.length > 0) {
+
+                                boolean valid = isValidGrep(data);
+                                if (!valid) {
+                                    continue;
+                                }
+
+                                if (!compact && first) {
+                                    System.out.println();
+                                }
+                                for (String line : lines) {
+                                    if (find != null) {
+                                        for (String f : find) {
+                                            line = line.replaceAll("(?i)" + f, findAnsi);
+                                        }
+                                    }
+                                    if (grep != null) {
+                                        for (String g : grep) {
+                                            line = line.replaceAll("(?i)" + g, findAnsi);
+                                        }
+                                    }
+                                    System.out.print(" ");
+                                    System.out.println(line);
+                                }
+                                if (!compact) {
+                                    System.out.println();
+                                }
+                                first = false;
+                            }
+                        }
+                    }
+                }
+            }
+        } else {
+            System.out.println(AsciiTable.getTable(AsciiTable.NO_BORDERS, rows, Arrays.asList(
+                    new Column().header("PID").headerAlign(HorizontalAlign.CENTER).with(r -> "" + r.pid),
+                    new Column().header("NAME").dataAlign(HorizontalAlign.LEFT).maxWidth(30, OverflowBehaviour.ELLIPSIS_RIGHT)
+                            .with(r -> r.name),
+                    new Column().header("QUEUE").dataAlign(HorizontalAlign.LEFT).with(r -> r.queue),
+                    new Column().header("MAX").dataAlign(HorizontalAlign.RIGHT).with(r -> "" + r.max),
+                    new Column().header("TOTAL").dataAlign(HorizontalAlign.RIGHT).with(r -> "" + r.size))));
+        }
+    }
+
+    protected JsonObject waitForOutputFile(File outputFile) {
+        StopWatch watch = new StopWatch();
+        while (watch.taken() < 5000) {
+            try {
+                // give time for response to be ready
+                Thread.sleep(100);
+
+                if (outputFile.exists()) {
+                    FileInputStream fis = new FileInputStream(outputFile);
+                    String text = IOHelper.loadText(fis);
+                    IOHelper.close(fis);
+                    return (JsonObject) Jsoner.deserialize(text);
+                }
+
+            } catch (Exception e) {
+                // ignore
+            }
+        }
+        return null;
+    }
+
+    private static class Row {
+        long pid;
+        String name;
+        String queue;
+        int max;
+        int size;
+        JsonObject endpoint;
+        List<JsonObject> messages;
+    }
+
+}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelThreadDump.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelThreadDump.java
index 257c424c074..991c8cf1bcd 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelThreadDump.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelThreadDump.java
@@ -145,7 +145,9 @@ public class CamelThreadDump extends ActionWatchCommand {
         // sort rows
         rows.sort(this::sortRow);
 
-        clearScreen();
+        if (watch) {
+            clearScreen();
+        }
         if (!rows.isEmpty()) {
             int total = jo.getInteger("threadCount");
             int peak = jo.getInteger("peakThreadCount");
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/MessageTableHelper.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/MessageTableHelper.java
index f8080cf796f..8e35ad46696 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/MessageTableHelper.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/MessageTableHelper.java
@@ -401,6 +401,9 @@ public class MessageTableHelper {
         }
 
         String exchangeIdAsValue() {
+            if (value == null) {
+                return "";
+            }
             String s = value.toString();
             if (loggingColor) {
                 Ansi.Color color = exchangeIdColorChooser != null ? exchangeIdColorChooser.color(s) : Ansi.Color.DEFAULT;
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/RouteControllerAction.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/RouteControllerAction.java
index a6280cb63e0..a1bbb302ef5 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/RouteControllerAction.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/RouteControllerAction.java
@@ -148,7 +148,9 @@ public class RouteControllerAction extends ActionWatchCommand {
         // sort rows
         rows.sort(this::sortRow);
 
-        clearScreen();
+        if (watch) {
+            clearScreen();
+        }
         if (!rows.isEmpty()) {
             if (supervising) {
                 if (header) {
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListEndpoint.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListEndpoint.java
index eb174fb731f..fc29a58f9b8 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListEndpoint.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/process/ListEndpoint.java
@@ -112,6 +112,7 @@ public class ListEndpoint extends ProcessWatchCommand {
                                 }
                                 row.pid = Long.toString(ph.pid());
                                 row.endpoint = o.getString("uri");
+                                row.stub = o.getBooleanOrDefault("stub", false);
                                 row.direction = o.getString("direction");
                                 row.total = o.getString("hits");
                                 row.uptime = extractSince(ph);
@@ -166,6 +167,7 @@ public class ListEndpoint extends ProcessWatchCommand {
                 new Column().header("AGE").headerAlign(HorizontalAlign.CENTER).with(r -> r.age),
                 new Column().header("DIR").with(r -> r.direction),
                 new Column().header("TOTAL").with(r -> r.total),
+                new Column().header("STUB").dataAlign(HorizontalAlign.CENTER).with(r -> r.stub ? "x" : ""),
                 new Column().header("URI").visible(!wideUri).dataAlign(HorizontalAlign.LEFT)
                         .maxWidth(90, OverflowBehaviour.ELLIPSIS_RIGHT)
                         .with(this::getUri),
@@ -214,6 +216,7 @@ public class ListEndpoint extends ProcessWatchCommand {
         String endpoint;
         String direction;
         String total;
+        boolean stub;
     }
 
 }