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:43 UTC

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

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;
     }
 
 }