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/04/14 08:38:45 UTC

[camel] branch main updated (971ebbafb1f -> 646b371c8b8)

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

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


    from 971ebbafb1f Sync deps
     new 3a26c35414c CAMEL-19236: camel-jbang - Command to send a message.
     new 058c5e6c0cb CAMEL-19236: camel-jbang - Command to send a message.
     new 646b371c8b8 CAMEL-19236: camel-jbang - Command to send a message.

The 3 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.


Summary of changes:
 .../org/apache/camel/support/MessageHelper.java    |  79 ++--
 .../modules/ROOT/pages/camel-jbang.adoc            |  84 +++++
 .../camel/cli/connector/LocalCliConnector.java     | 143 ++++++++
 .../dsl/jbang/core/commands/CamelJBangMain.java    |   2 +
 .../core/commands/action/CamelSendAction.java      | 272 ++++++++++++++
 .../core/commands/action/CamelTraceAction.java     | 341 ++---------------
 .../core/commands/action/MessageTableHelper.java   | 408 +++++++++++++++++++++
 7 files changed, 986 insertions(+), 343 deletions(-)
 create mode 100644 dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSendAction.java
 create mode 100644 dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/MessageTableHelper.java


[camel] 01/03: CAMEL-19236: camel-jbang - Command to send a message.

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

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

commit 3a26c35414c3b5536d6efa998128949a32fdaffe
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Fri Apr 14 08:42:58 2023 +0200

    CAMEL-19236: camel-jbang - Command to send a message.
---
 .../org/apache/camel/support/MessageHelper.java    |  79 ++--
 .../camel/cli/connector/LocalCliConnector.java     | 143 ++++++++
 .../dsl/jbang/core/commands/CamelJBangMain.java    |   2 +
 .../core/commands/action/CamelSendAction.java      | 272 ++++++++++++++
 .../core/commands/action/CamelTraceAction.java     | 341 ++---------------
 .../core/commands/action/MessageTableHelper.java   | 408 +++++++++++++++++++++
 6 files changed, 902 insertions(+), 343 deletions(-)

diff --git a/core/camel-support/src/main/java/org/apache/camel/support/MessageHelper.java b/core/camel-support/src/main/java/org/apache/camel/support/MessageHelper.java
index cbc6289d849..e903dbdb25a 100644
--- a/core/camel-support/src/main/java/org/apache/camel/support/MessageHelper.java
+++ b/core/camel-support/src/main/java/org/apache/camel/support/MessageHelper.java
@@ -808,7 +808,7 @@ public final class MessageHelper {
     }
 
     /**
-     * Dumps the message as a generic JSon structure.
+     * Dumps the message as a generic JSon structure as text.
      *
      * @param  message the message
      * @return         the JSon
@@ -818,7 +818,7 @@ public final class MessageHelper {
     }
 
     /**
-     * Dumps the message as a generic JSon structure.
+     * Dumps the message as a generic JSon structure as text.
      *
      * @param  message     the message
      * @param  includeBody whether or not to include the message body
@@ -829,7 +829,7 @@ public final class MessageHelper {
     }
 
     /**
-     * Dumps the message as a generic JSon structure.
+     * Dumps the message as a generic JSon structure as text.
      *
      * @param  message     the message
      * @param  includeBody whether or not to include the message body
@@ -841,7 +841,7 @@ public final class MessageHelper {
     }
 
     /**
-     * Dumps the message as a generic JSon structure.
+     * Dumps the message as a generic JSon structure as text.
      *
      * @param  message      the message
      * @param  includeBody  whether or not to include the message body
@@ -859,7 +859,7 @@ public final class MessageHelper {
     }
 
     /**
-     * Dumps the message as a generic JSon structure.
+     * Dumps the message as a generic JSon structure as text.
      *
      * @param  message                   the message
      * @param  includeExchangeProperties whether or not to include exchange properties
@@ -877,6 +877,35 @@ public final class MessageHelper {
             Message message, boolean includeExchangeProperties, boolean includeBody, int indent,
             boolean allowCachedStreams, boolean allowStreams, boolean allowFiles, int maxChars, boolean pretty) {
 
+        JsonObject jo = dumpAsJSonObject(message, includeExchangeProperties, includeBody, allowCachedStreams, allowStreams, allowFiles, maxChars);
+        String answer = jo.toJson();
+        if (pretty) {
+            if (indent > 0) {
+                answer = Jsoner.prettyPrint(answer, indent);
+            } else {
+                answer = Jsoner.prettyPrint(answer);
+            }
+        }
+        return answer;
+    }
+
+    /**
+     * Dumps the message as a generic JSon Object.
+     *
+     * @param  message                   the message
+     * @param  includeExchangeProperties whether or not to include exchange properties
+     * @param  includeBody               whether or not to include the message body
+     * @param  allowCachedStreams        whether to include message body if they are stream cached based
+     * @param  allowStreams              whether to include message body if they are stream based
+     * @param  allowFiles                whether to include message body if they are file based
+     * @param  maxChars                  clip body after maximum chars (to avoid very big messages). Use 0 or negative
+     *                                   value to not limit at all.
+     * @return                           the JSon Object
+     */
+    public static JsonObject dumpAsJSonObject(
+            Message message, boolean includeExchangeProperties, boolean includeBody,
+            boolean allowCachedStreams, boolean allowStreams, boolean allowFiles, int maxChars) {
+
         JsonObject root = new JsonObject();
         JsonObject jo = new JsonObject();
         root.put("message", jo);
@@ -981,15 +1010,7 @@ public final class MessageHelper {
             }
         }
 
-        String answer = root.toJson();
-        if (pretty) {
-            if (indent > 0) {
-                answer = Jsoner.prettyPrint(answer, indent);
-            } else {
-                answer = Jsoner.prettyPrint(answer);
-            }
-        }
-        return answer;
+        return root;
     }
 
     /**
@@ -1029,13 +1050,31 @@ public final class MessageHelper {
     }
 
     /**
-     * Dumps the exception as a generic JSon structure.
+     * Dumps the exception as a generic JSon structure as text.
      *
      * @param  indent number of spaces to indent
      * @param  pretty whether to pretty print JSon
      * @return        the JSon
      */
     public static String dumpExceptionAsJSon(Throwable exception, int indent, boolean pretty) {
+        JsonObject jo = dumpExceptionAsJSonObject(exception);
+        String answer = jo.toJson();
+        if (pretty) {
+            if (indent > 0) {
+                answer = Jsoner.prettyPrint(answer, indent);
+            } else {
+                answer = Jsoner.prettyPrint(answer);
+            }
+        }
+        return answer;
+    }
+
+    /**
+     * Dumps the exception as a generic JSon object.
+     *
+     * @return        the JSon object
+     */
+    public static JsonObject dumpExceptionAsJSonObject(Throwable exception) {
         JsonObject root = new JsonObject();
         JsonObject jo = new JsonObject();
         root.put("exception", jo);
@@ -1058,15 +1097,7 @@ public final class MessageHelper {
         } catch (Throwable e) {
             // ignore as the body is for logging purpose
         }
-        String answer = root.toJson();
-        if (pretty) {
-            if (indent > 0) {
-                answer = Jsoner.prettyPrint(answer, indent);
-            } else {
-                answer = Jsoner.prettyPrint(answer);
-            }
-        }
-        return answer;
+        return root;
     }
 
 }
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 b63e17482d9..6dd479e5bd0 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
@@ -18,12 +18,15 @@ package org.apache.camel.cli.connector;
 
 import java.io.File;
 import java.io.FileInputStream;
+import java.io.InputStream;
 import java.lang.management.ClassLoadingMXBean;
 import java.lang.management.GarbageCollectorMXBean;
 import java.lang.management.ManagementFactory;
 import java.lang.management.MemoryMXBean;
 import java.lang.management.RuntimeMXBean;
 import java.lang.management.ThreadMXBean;
+import java.util.Collection;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
@@ -37,7 +40,12 @@ import java.util.stream.Collectors;
 
 import org.apache.camel.CamelContext;
 import org.apache.camel.CamelContextAware;
+import org.apache.camel.Endpoint;
 import org.apache.camel.Exchange;
+import org.apache.camel.ExchangePattern;
+import org.apache.camel.NoSuchEndpointException;
+import org.apache.camel.Processor;
+import org.apache.camel.ProducerTemplate;
 import org.apache.camel.Route;
 import org.apache.camel.api.management.ManagedCamelContext;
 import org.apache.camel.console.DevConsole;
@@ -46,12 +54,15 @@ import org.apache.camel.spi.CliConnector;
 import org.apache.camel.spi.CliConnectorFactory;
 import org.apache.camel.spi.ContextReloadStrategy;
 import org.apache.camel.support.DefaultContextReloadStrategy;
+import org.apache.camel.support.EndpointHelper;
+import org.apache.camel.support.MessageHelper;
 import org.apache.camel.support.PatternHelper;
 import org.apache.camel.support.PluginHelper;
 import org.apache.camel.support.service.ServiceHelper;
 import org.apache.camel.support.service.ServiceSupport;
 import org.apache.camel.util.FileUtil;
 import org.apache.camel.util.IOHelper;
+import org.apache.camel.util.StopWatch;
 import org.apache.camel.util.concurrent.ThreadHelper;
 import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
@@ -66,6 +77,8 @@ public class LocalCliConnector extends ServiceSupport implements CliConnector, C
 
     private static final Logger LOG = LoggerFactory.getLogger(LocalCliConnector.class);
 
+    private static final int BODY_MAX_CHARS = 128 * 1024;
+
     private final CliConnectorFactory cliConnectorFactory;
     private CamelContext camelContext;
     private int delay = 2000;
@@ -75,6 +88,7 @@ public class LocalCliConnector extends ServiceSupport implements CliConnector, C
     private final AtomicBoolean terminating = new AtomicBoolean();
     private ScheduledExecutorService executor;
     private volatile ExecutorService terminateExecutor;
+    private ProducerTemplate producer;
     private File lockFile;
     private File statusFile;
     private File actionFile;
@@ -126,6 +140,7 @@ public class LocalCliConnector extends ServiceSupport implements CliConnector, C
             }
         }
         platformVersion = cliConnectorFactory.getRuntimeVersion();
+        producer = camelContext.createProducerTemplate();
 
         // create thread from JDK so it is not managed by Camel because we want the pool to be independent when
         // camel is being stopped which otherwise can lead to stopping the thread pool while the task is running
@@ -294,6 +309,133 @@ public class LocalCliConnector extends ServiceSupport implements CliConnector, C
                     LOG.trace("Updating output file: {}", outputFile);
                     IOHelper.writeText(json.toJson(), outputFile);
                 }
+            } else if ("send".equals(action)) {
+                StopWatch watch = new StopWatch();
+                long timestamp = System.currentTimeMillis();
+                String endpoint = root.getString("endpoint");
+                String body = root.getString("body");
+                String exchangePattern = root.getString("exchangePattern");
+                Collection<JsonObject> headers = root.getCollection("headers");
+                if (body != null) {
+                    InputStream is = null;
+                    Object b = body;
+                    Map<String, Object> map = null;
+                    if (body.startsWith("file:")) {
+                        File file = new File(body.substring(5));
+                        is = new FileInputStream(file);
+                        b = is;
+                    }
+                    if (headers != null) {
+                        map = new HashMap<>();
+                        for (JsonObject jo : headers) {
+                            map.put(jo.getString("key"), jo.getString("value"));
+                        }
+                    }
+                    final Object inputBody = b;
+                    final Map<String, Object> inputHeaders = map;
+                    Exchange out;
+                    Endpoint target = null;
+                    if (endpoint == null) {
+                        List<Route> routes = camelContext.getRoutes();
+                        if (!routes.isEmpty()) {
+                            // grab endpoint from 1st route
+                            target = routes.get(0).getEndpoint();
+                        }
+                    } else {
+                        // is the endpoint a pattern or route id
+                        boolean scheme = endpoint.contains(":");
+                        boolean pattern = endpoint.endsWith("*");
+                        if (!scheme || pattern) {
+                            if (!scheme) {
+                                endpoint = endpoint + "*";
+                            }
+                            for (Route route : camelContext.getRoutes()) {
+                                Endpoint e = route.getEndpoint();
+                                if (EndpointHelper.matchEndpoint(camelContext, e.getEndpointUri(), endpoint)) {
+                                    target = e;
+                                    break;
+                                }
+                            }
+                            if (target == null) {
+                                // okay it may refer to a route id
+                                for (Route route : camelContext.getRoutes()) {
+                                    String id = route.getRouteId();
+                                    Endpoint e = route.getEndpoint();
+                                    if (EndpointHelper.matchEndpoint(camelContext, id, endpoint)) {
+                                        target = e;
+                                        break;
+                                    }
+                                }
+                            }
+                        } else {
+                            target = camelContext.getEndpoint(endpoint);
+                        }
+                    }
+
+                    if (target != null) {
+                        out = producer.send(target, new Processor() {
+                            @Override
+                            public void process(Exchange exchange) throws Exception {
+                                exchange.getMessage().setBody(inputBody);
+                                if (inputHeaders != null) {
+                                    exchange.getMessage().setHeaders(inputHeaders);
+                                }
+                                exchange.setPattern(
+                                        "InOut".equals(exchangePattern) ? ExchangePattern.InOut : ExchangePattern.InOnly);
+                            }
+                        });
+                        IOHelper.close(is);
+                        LOG.trace("Updating output file: {}", outputFile);
+                        if (out.getException() != null) {
+                            JsonObject jo = new JsonObject();
+                            jo.put("endpoint", target.getEndpointUri());
+                            jo.put("exchangeId", out.getExchangeId());
+                            jo.put("exchangePattern", exchangePattern);
+                            jo.put("timestamp", timestamp);
+                            jo.put("elapsed", watch.taken());
+                            jo.put("status", "failed");
+                            // avoid double wrap
+                            jo.put("exception",
+                                    MessageHelper.dumpExceptionAsJSonObject(out.getException()).getMap("exception"));
+                            IOHelper.writeText(jo.toJson(), outputFile);
+                        } else if ("InOut".equals(exchangePattern)) {
+                            JsonObject jo = new JsonObject();
+                            jo.put("endpoint", target.getEndpointUri());
+                            jo.put("exchangeId", out.getExchangeId());
+                            jo.put("exchangePattern", exchangePattern);
+                            jo.put("timestamp", timestamp);
+                            jo.put("elapsed", watch.taken());
+                            jo.put("status", "success");
+                            // avoid double wrap
+                            jo.put("message", MessageHelper.dumpAsJSonObject(out.getMessage(), true, true, true, true, true,
+                                    BODY_MAX_CHARS).getMap("message"));
+                            IOHelper.writeText(jo.toJson(), outputFile);
+                        } else {
+                            JsonObject jo = new JsonObject();
+                            jo.put("endpoint", target.getEndpointUri());
+                            jo.put("exchangeId", out.getExchangeId());
+                            jo.put("exchangePattern", exchangePattern);
+                            jo.put("timestamp", timestamp);
+                            jo.put("elapsed", watch.taken());
+                            jo.put("status", "success");
+                            IOHelper.writeText(jo.toJson(), outputFile);
+                        }
+                    } else {
+                        // there is no valid endpoint
+                        JsonObject jo = new JsonObject();
+                        jo.put("endpoint", root.getString("endpoint"));
+                        jo.put("exchangeId", "");
+                        jo.put("exchangePattern", exchangePattern);
+                        jo.put("timestamp", timestamp);
+                        jo.put("elapsed", watch.taken());
+                        jo.put("status", "failed");
+                        // avoid double wrap
+                        jo.put("exception",
+                                MessageHelper.dumpExceptionAsJSonObject(new NoSuchEndpointException(root.getString("endpoint")))
+                                        .getMap("exception"));
+                        IOHelper.writeText(jo.toJson(), outputFile);
+                    }
+                }
             }
 
             // action done so delete file
@@ -629,6 +771,7 @@ public class LocalCliConnector extends ServiceSupport implements CliConnector, C
             camelContext.getExecutorServiceManager().shutdown(executor);
             executor = null;
         }
+        ServiceHelper.stopService(producer);
     }
 
     private static String getPid() {
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 d0b9bfa7e9f..f536129e90d 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
@@ -27,6 +27,7 @@ import org.apache.camel.dsl.jbang.core.commands.action.CamelReloadAction;
 import org.apache.camel.dsl.jbang.core.commands.action.CamelResetStatsAction;
 import org.apache.camel.dsl.jbang.core.commands.action.CamelRouteStartAction;
 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.CamelThreadDump;
@@ -114,6 +115,7 @@ public class CamelJBangMain implements Callable<Integer> {
                         .addSubcommand("stop-route", new CommandLine(new CamelRouteStopAction(main)))
                         .addSubcommand("reset-stats", new CommandLine(new CamelResetStatsAction(main)))
                         .addSubcommand("reload", new CommandLine(new CamelReloadAction(main)))
+                        .addSubcommand("send", new CommandLine(new CamelSendAction(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/CamelSendAction.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSendAction.java
new file mode 100644
index 00000000000..a8cdf8d650e
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSendAction.java
@@ -0,0 +1,272 @@
+/*
+ * 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.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.List;
+
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.util.FileUtil;
+import org.apache.camel.util.IOHelper;
+import org.apache.camel.util.StopWatch;
+import org.apache.camel.util.StringHelper;
+import org.apache.camel.util.TimeUtils;
+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 org.fusesource.jansi.AnsiConsole;
+import picocli.CommandLine;
+
+@CommandLine.Command(name = "send",
+                     description = "Sends a message to a system via an existing running Camel integration")
+public class CamelSendAction extends ActionBaseCommand {
+
+    @CommandLine.Parameters(description = "Name or pid of running Camel integration", arity = "1")
+    String name;
+
+    @CommandLine.Option(names = { "--endpoint" },
+                        description = "Endpoint where to send the message (can be uri, pattern, or refer to a route id)")
+    String endpoint;
+
+    @CommandLine.Option(names = { "--reply" },
+                        description = "Whether to expect a reply message (InOut vs InOut messaging style)")
+    boolean reply;
+
+    @CommandLine.Option(names = { "--reply-file" },
+                        description = "Saves reply message to the file with the given name (override if exists)")
+    String replyFile;
+
+    @CommandLine.Option(names = { "--body" }, required = true,
+                        description = "Message body to send (prefix with file: to refer to loading message body from file)")
+    String body;
+
+    @CommandLine.Option(names = { "--header" },
+                        description = "Message header (key=value)")
+    List<String> headers;
+
+    @CommandLine.Option(names = { "--timeout" }, defaultValue = "20000",
+                        description = "Timeout in millis waiting for message to be sent (and reply message if InOut messaging)")
+    long timeout = 20000;
+
+    @CommandLine.Option(names = { "--show-exchange-properties" }, defaultValue = "false",
+                        description = "Show exchange properties in traced messages")
+    boolean showExchangeProperties;
+
+    @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 = { "--show-exception" }, defaultValue = "true",
+                        description = "Show exception and stacktrace for failed messages")
+    boolean showException = true;
+
+    @CommandLine.Option(names = { "--logging-color" }, defaultValue = "true", description = "Use colored logging")
+    boolean loggingColor = true;
+
+    @CommandLine.Option(names = { "--pretty" },
+                        description = "Pretty print message body when using JSon or XML format")
+    boolean pretty;
+
+    private volatile long pid;
+
+    private MessageTableHelper tableHelper;
+
+    public CamelSendAction(CamelJBangMain main) {
+        super(main);
+    }
+
+    @Override
+    public Integer doCall() throws Exception {
+        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);
+
+        // ensure output file is deleted before executing action
+        File outputFile = getOutputFile(Long.toString(pid));
+        FileUtil.deleteFile(outputFile);
+
+        JsonObject root = new JsonObject();
+        root.put("action", "send");
+        root.put("endpoint", endpoint);
+        String mep = (reply || replyFile != null) ? "InOut" : "InOnly";
+        root.put("exchangePattern", mep);
+        root.put("body", body);
+        if (headers != null) {
+            JsonArray arr = new JsonArray();
+            for (String h : headers) {
+                JsonObject jo = new JsonObject();
+                if (!h.contains("=")) {
+                    System.out.println("Header must be in key=value format, was: " + h);
+                    return 0;
+                }
+                jo.put("key", StringHelper.before(h, "="));
+                jo.put("value", StringHelper.after(h, "="));
+                arr.add(jo);
+            }
+            root.put("headers", arr);
+        }
+        File f = getActionFile(Long.toString(pid));
+        try {
+            IOHelper.writeText(root.toJson(), f);
+        } catch (Exception e) {
+            // ignore
+        }
+
+        JsonObject jo = waitForOutputFile(outputFile);
+        if (jo != null) {
+            printStatusLine(jo);
+            String exchangeId = jo.getString("exchangeId");
+            JsonObject message = jo.getMap("message");
+            JsonObject cause = jo.getMap("exception");
+            if (message != null || cause != null) {
+                if (replyFile != null) {
+                    File target = new File(replyFile);
+                    String json = jo.toJson();
+                    if (pretty) {
+                        json = Jsoner.prettyPrint(json, 2);
+                    }
+                    IOHelper.writeText(json, target);
+                }
+                if (!showExchangeProperties && message != null) {
+                    message.remove("exchangeProperties");
+                }
+                if (!showHeaders && message != null) {
+                    message.remove("headers");
+                }
+                if (!showBody && message != null) {
+                    message.remove("body");
+                }
+                if (!showException && cause != null) {
+                    cause = null;
+                }
+                if (replyFile == null) {
+                    tableHelper = new MessageTableHelper();
+                    tableHelper.setPretty(pretty);
+                    tableHelper.setLoggingColor(loggingColor);
+                    tableHelper.setShowExchangeProperties(showExchangeProperties);
+                    String table = tableHelper.getDataAsTable(exchangeId, mep, jo, message, cause);
+                    System.out.println(table);
+                }
+            }
+        }
+
+        // delete output file after use
+        FileUtil.deleteFile(outputFile);
+
+        return 0;
+    }
+
+    private void printStatusLine(JsonObject jo) {
+        // timstamp
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
+        String ts = sdf.format(new Date(jo.getLong("timestamp")));
+        if (loggingColor) {
+            AnsiConsole.out().print(Ansi.ansi().fgBrightDefault().a(Ansi.Attribute.INTENSITY_FAINT).a(ts).reset());
+        } else {
+            System.out.print(ts);
+        }
+        // pid
+        System.out.print("  ");
+        String p = String.format("%5.5s", this.pid);
+        if (loggingColor) {
+            AnsiConsole.out().print(Ansi.ansi().fgMagenta().a(p).reset());
+            AnsiConsole.out().print(Ansi.ansi().fgBrightDefault().a(Ansi.Attribute.INTENSITY_FAINT).a(" --- ").reset());
+        } else {
+            System.out.print(p);
+            System.out.print(" --- ");
+        }
+        // endpoint
+        String ids = jo.getString("endpoint");
+        if (ids.length() > 40) {
+            ids = ids.substring(ids.length() - 40);
+        }
+        ids = String.format("%40.40s", ids);
+        if (loggingColor) {
+            AnsiConsole.out().print(Ansi.ansi().fgCyan().a(ids).reset());
+        } else {
+            System.out.print(ids);
+        }
+        System.out.print(" : ");
+        // status
+        System.out.print(getStatus(jo));
+        // elapsed
+        String e = TimeUtils.printDuration(jo.getLong("elapsed"), true);
+        if (loggingColor) {
+            AnsiConsole.out().print(Ansi.ansi().fgBrightDefault().a(" (" + e + ")").reset());
+        } else {
+            System.out.print("(" + e + ")");
+        }
+        System.out.println();
+    }
+
+    private String getStatus(JsonObject r) {
+        boolean failed = "failed".equals(r.getString("status"));
+        boolean reply = r.containsKey("message");
+        String status;
+        if (failed) {
+            status = "Failed (exception)";
+        } else if (replyFile != null) {
+            status = "Reply saved to file (success)";
+        } else if (reply) {
+            status = "Reply received (success)";
+        } else {
+            status = "Sent (success)";
+        }
+        if (loggingColor) {
+            return Ansi.ansi().fg(failed ? Ansi.Color.RED : Ansi.Color.GREEN).a(status).reset().toString();
+        } else {
+            return status;
+        }
+    }
+
+    protected JsonObject waitForOutputFile(File outputFile) {
+        StopWatch watch = new StopWatch();
+        while (watch.taken() < timeout) {
+            try {
+                // give time for response to be ready
+                Thread.sleep(20);
+
+                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;
+    }
+
+}
diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelTraceAction.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelTraceAction.java
index 97de403abc0..6b449764e43 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelTraceAction.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelTraceAction.java
@@ -23,7 +23,6 @@ import java.io.LineNumberReader;
 import java.text.SimpleDateFormat;
 import java.util.ArrayDeque;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -36,15 +35,9 @@ import java.util.Set;
 import java.util.concurrent.ArrayBlockingQueue;
 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.catalog.impl.TimePatternConverter;
 import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
-import org.apache.camel.dsl.jbang.core.common.JSonHelper;
 import org.apache.camel.dsl.jbang.core.common.ProcessHelper;
-import org.apache.camel.dsl.jbang.core.common.XmlHelper;
 import org.apache.camel.util.StopWatch;
 import org.apache.camel.util.StringHelper;
 import org.apache.camel.util.TimeUtils;
@@ -152,6 +145,8 @@ public class CamelTraceAction extends ActionBaseCommand {
     private int nameMaxWidth;
     private boolean prefixShown;
 
+    private MessageTableHelper tableHelper;
+
     private final Map<String, Ansi.Color> nameColors = new HashMap<>();
     private final Map<String, Ansi.Color> exchangeIdColors = new HashMap<>();
     private int exchangeIdColorsIndex = 1;
@@ -162,6 +157,25 @@ public class CamelTraceAction extends ActionBaseCommand {
 
     @Override
     public Integer doCall() throws Exception {
+        // setup table helper
+        tableHelper = new MessageTableHelper();
+        tableHelper.setPretty(pretty);
+        tableHelper.setLoggingColor(loggingColor);
+        tableHelper.setShowExchangeProperties(showExchangeProperties);
+        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;
+        });
+
         Map<Long, Pid> pids = new LinkedHashMap<>();
 
         if (latest) {
@@ -675,130 +689,7 @@ public class CamelTraceAction extends ActionBaseCommand {
     }
 
     private String getDataAsTable(Row r) {
-        List<TableRow> rows = new ArrayList<>();
-
-        TableRow eRow = new TableRow("Exchange", r.message.getString("exchangeType"), r.exchangePattern, r.exchangeId);
-        String tab1 = AsciiTable.getTable(AsciiTable.NO_BORDERS, List.of(eRow), Arrays.asList(
-                new Column().dataAlign(HorizontalAlign.LEFT)
-                        .minWidth(showExchangeProperties ? 12 : 10).with(TableRow::kindAsString),
-                new Column().dataAlign(HorizontalAlign.LEFT).with(TableRow::typeAsString)));
-        String tab1b = AsciiTable.getTable(AsciiTable.NO_BORDERS, List.of(eRow), Arrays.asList(
-                new Column().dataAlign(HorizontalAlign.CENTER)
-                        .minWidth(18).maxWidth(18).with(TableRow::mepAsKey),
-                new Column().dataAlign(HorizontalAlign.RIGHT)
-                        .maxWidth(80).with(TableRow::exchangeIdAsValue)));
-        // exchange properties
-        JsonArray arr = r.message.getCollection("exchangeProperties");
-        if (arr != null) {
-            for (Object o : arr) {
-                JsonObject jo = (JsonObject) o;
-                rows.add(new TableRow("Property", jo.getString("type"), jo.getString("key"), jo.get("value")));
-            }
-        }
-        // internal exchange properties
-        arr = r.message.getCollection("internalExchangeProperties");
-        if (arr != null) {
-            for (Object o : arr) {
-                JsonObject jo = (JsonObject) o;
-                rows.add(new TableRow("Property", jo.getString("type"), jo.getString("key"), jo.get("value")));
-            }
-        }
-        String tab2 = AsciiTable.getTable(AsciiTable.NO_BORDERS, rows, Arrays.asList(
-                new Column().dataAlign(HorizontalAlign.LEFT)
-                        .minWidth(showExchangeProperties ? 12 : 10).with(TableRow::kindAsString),
-                new Column().dataAlign(HorizontalAlign.LEFT)
-                        .minWidth(25).maxWidth(50, OverflowBehaviour.CLIP_LEFT).with(TableRow::typeAsString),
-                new Column().dataAlign(HorizontalAlign.RIGHT)
-                        .minWidth(25).maxWidth(40, OverflowBehaviour.NEWLINE).with(TableRow::keyAsString),
-                new Column().dataAlign(HorizontalAlign.LEFT)
-                        .maxWidth(80, OverflowBehaviour.NEWLINE).with(TableRow::valueAsString)));
-        rows.clear();
-
-        // message type before headers
-        TableRow msgRow = new TableRow("Message", r.message.getString("messageType"), null, null);
-        String tab3 = AsciiTable.getTable(AsciiTable.NO_BORDERS, List.of(msgRow), Arrays.asList(
-                new Column().dataAlign(HorizontalAlign.LEFT)
-                        .minWidth(showExchangeProperties ? 12 : 10).with(TableRow::kindAsString),
-                new Column().dataAlign(HorizontalAlign.LEFT).with(TableRow::typeAsString)));
-        arr = r.message.getCollection("headers");
-        if (arr != null) {
-            for (Object o : arr) {
-                JsonObject jo = (JsonObject) o;
-                rows.add(new TableRow("Header", jo.getString("type"), jo.getString("key"), jo.get("value")));
-            }
-        }
-        // headers
-        String tab4 = AsciiTable.getTable(AsciiTable.NO_BORDERS, rows, Arrays.asList(
-                new Column().dataAlign(HorizontalAlign.LEFT)
-                        .minWidth(showExchangeProperties ? 12 : 10).with(TableRow::kindAsString),
-                new Column().dataAlign(HorizontalAlign.LEFT)
-                        .minWidth(25).maxWidth(50, OverflowBehaviour.CLIP_LEFT).with(TableRow::typeAsString),
-                new Column().dataAlign(HorizontalAlign.RIGHT)
-                        .minWidth(25).maxWidth(40, OverflowBehaviour.NEWLINE).with(TableRow::keyAsString),
-                new Column().dataAlign(HorizontalAlign.LEFT)
-                        .maxWidth(80, OverflowBehaviour.NEWLINE).with(TableRow::valueAsString)));
-
-        // body and type
-        JsonObject jo = r.message.getMap("body");
-        TableRow bodyRow = new TableRow("Body", jo.getString("type"), null, jo.get("value"), jo.getLong("position"));
-        String tab5 = AsciiTable.getTable(AsciiTable.NO_BORDERS, List.of(bodyRow), Arrays.asList(
-                new Column().dataAlign(HorizontalAlign.LEFT)
-                        .minWidth(showExchangeProperties ? 12 : 10).with(TableRow::kindAsString),
-                new Column().dataAlign(HorizontalAlign.LEFT).with(TableRow::typeAndLengthAsString)));
-        // body value only (span)
-        String tab6 = null;
-        if (bodyRow.value != null) {
-            tab6 = AsciiTable.getTable(AsciiTable.NO_BORDERS, List.of(bodyRow), Arrays.asList(
-                    new Column().dataAlign(HorizontalAlign.LEFT).maxWidth(160, OverflowBehaviour.NEWLINE)
-                            .with(b -> pretty ? bodyRow.valueAsStringPretty() : bodyRow.valueAsString())));
-        }
-        String tab7 = null;
-        jo = r.exception;
-        if (jo != null) {
-            eRow = new TableRow("Exception", jo.getString("type"), null, jo.get("message"));
-            tab7 = AsciiTable.getTable(AsciiTable.NO_BORDERS, List.of(eRow), Arrays.asList(
-                    new Column().dataAlign(HorizontalAlign.LEFT)
-                            .minWidth(showExchangeProperties ? 12 : 10)
-                            .with(TableRow::kindAsStringRed),
-                    new Column().dataAlign(HorizontalAlign.LEFT)
-                            .maxWidth(40, OverflowBehaviour.CLIP_LEFT).with(TableRow::typeAsString),
-                    new Column().dataAlign(HorizontalAlign.LEFT)
-                            .maxWidth(80, OverflowBehaviour.NEWLINE).with(TableRow::valueAsStringRed)));
-        }
-        // stacktrace only (span)
-        String tab8 = null;
-        if (jo != null) {
-            eRow = new TableRow("Stacktrace", null, null, jo.get("stackTrace"));
-            tab8 = AsciiTable.getTable(AsciiTable.NO_BORDERS, List.of(eRow), Arrays.asList(
-                    new Column().dataAlign(HorizontalAlign.LEFT).maxWidth(160, OverflowBehaviour.NEWLINE)
-                            .with(TableRow::valueAsStringRed)));
-        }
-        String answer = "";
-        if (tab1 != null && tab1b != null && !tab1.isEmpty()) {
-            answer = answer + tab1 + tab1b + System.lineSeparator();
-        }
-        if (tab2 != null && !tab2.isEmpty()) {
-            answer = answer + tab2 + System.lineSeparator();
-        }
-        if (tab3 != null && !tab3.isEmpty()) {
-            answer = answer + tab3 + System.lineSeparator();
-        }
-        if (tab4 != null && !tab4.isEmpty()) {
-            answer = answer + tab4 + System.lineSeparator();
-        }
-        if (tab5 != null && !tab5.isEmpty()) {
-            answer = answer + tab5 + System.lineSeparator();
-        }
-        if (tab6 != null && !tab6.isEmpty()) {
-            answer = answer + tab6 + System.lineSeparator();
-        }
-        if (tab7 != null && !tab7.isEmpty()) {
-            answer = answer + tab7 + System.lineSeparator();
-        }
-        if (tab8 != null && !tab8.isEmpty()) {
-            answer = answer + tab8 + System.lineSeparator();
-        }
-        return answer;
+        return tableHelper.getDataAsTable(r.exchangeId, r.exchangePattern, null, r.message, r.exception);
     }
 
     private String getElapsed(Row r) {
@@ -891,192 +782,4 @@ public class CamelTraceAction extends ActionBaseCommand {
 
     }
 
-    private class TableRow {
-        String kind;
-        String type;
-        String key;
-        Object value;
-        Long position;
-
-        TableRow(String kind, String type, String key, Object value) {
-            this(kind, type, key, value, null);
-        }
-
-        TableRow(String kind, String type, String key, Object value, Long position) {
-            this.kind = kind;
-            this.type = type;
-            this.key = key;
-            this.value = value;
-            this.position = position;
-        }
-
-        String valueAsString() {
-            return value != null ? value.toString() : "null";
-        }
-
-        String valueAsStringPretty() {
-            if (value == null) {
-                return "null";
-            }
-            boolean json = false;
-            String s = value.toString();
-            if (!s.isEmpty()) {
-                try {
-                    s = Jsoner.unescape(s);
-                    if (loggingColor) {
-                        s = JSonHelper.colorPrint(s, 2, true);
-                    } else {
-                        s = JSonHelper.prettyPrint(s, 2);
-                    }
-                    if (s != null && !s.isEmpty()) {
-                        json = true;
-                    }
-                } catch (Throwable e) {
-                    // ignore as not json
-                }
-                if (s == null || s.isEmpty()) {
-                    s = value.toString();
-                }
-                if (!json) {
-                    // try with xml
-                    try {
-                        s = Jsoner.unescape(s);
-                        if (loggingColor) {
-                            s = XmlHelper.colorPrint(s, 2, true);
-                        } else {
-                            s = XmlHelper.prettyPrint(s, 2);
-                        }
-                    } catch (Throwable e) {
-                        // ignore as not xml
-                    }
-                }
-                if (s == null || s.isEmpty()) {
-                    s = value.toString();
-                }
-            }
-            if (s == null) {
-                return "null";
-            }
-            return s;
-        }
-
-        String valueAsStringRed() {
-            if (value != null) {
-                if (loggingColor) {
-                    return Ansi.ansi().fgRed().a(value).reset().toString();
-                } else {
-                    return value.toString();
-                }
-            }
-            return "";
-        }
-
-        String keyAsString() {
-            if (key == null) {
-                return "";
-            }
-            return key;
-        }
-
-        String kindAsString() {
-            return kind;
-        }
-
-        String kindAsStringRed() {
-            if (loggingColor) {
-                return Ansi.ansi().fgRed().a(kind).reset().toString();
-            } else {
-                return kind;
-            }
-        }
-
-        String typeAsString() {
-            String s;
-            if (type == null) {
-                s = "null";
-            } else if (type.startsWith("java.util.concurrent")) {
-                s = type.substring(21);
-            } else if (type.startsWith("java.lang.") || type.startsWith("java.util.")) {
-                s = type.substring(10);
-            } else if (type.startsWith("org.apache.camel.support.")) {
-                s = type.substring(25);
-            } else if (type.startsWith("org.apache.camel.converter.stream.")) {
-                s = type.substring(34);
-            } else {
-                s = type;
-            }
-            s = "(" + s + ")";
-            if (loggingColor) {
-                s = Ansi.ansi().fgBrightDefault().a(Ansi.Attribute.INTENSITY_FAINT).a(s).reset().toString();
-            }
-            return s;
-        }
-
-        String typeAndLengthAsString() {
-            String s;
-            if (type == null) {
-                s = "null";
-            } else if (type.startsWith("java.util.concurrent")) {
-                s = type.substring(21);
-            } else if (type.startsWith("java.lang.") || type.startsWith("java.util.")) {
-                s = type.substring(10);
-            } else if (type.startsWith("org.apache.camel.support.")) {
-                s = type.substring(25);
-            } else if (type.startsWith("org.apache.camel.converter.stream.")) {
-                s = type.substring(34);
-            } else {
-                s = type;
-            }
-            s = "(" + s + ")";
-            int l = valueLength();
-            long p = position != null ? position : -1;
-            if (l != -1 & p != -1) {
-                s = s + " (pos: " + p + " length: " + l + ")";
-            } else if (l != -1) {
-                s = s + " (length: " + l + ")";
-            } else if (p != -1) {
-                s = s + " (pos: " + p + ")";
-            }
-            if (loggingColor) {
-                s = Ansi.ansi().fgBrightDefault().a(Ansi.Attribute.INTENSITY_FAINT).a(s).reset().toString();
-            }
-            return s;
-        }
-
-        String mepAsKey() {
-            String s = key;
-            if (loggingColor) {
-                s = Ansi.ansi().fgBrightMagenta().a(Ansi.Attribute.INTENSITY_FAINT).a(s).reset().toString();
-            }
-            return s;
-        }
-
-        String exchangeIdAsValue() {
-            String s = value.toString();
-            if (loggingColor) {
-                Ansi.Color color = exchangeIdColors.get(s);
-                if (color == null) {
-                    // grab a new color
-                    exchangeIdColorsIndex++;
-                    if (exchangeIdColorsIndex > 6) {
-                        exchangeIdColorsIndex = 2;
-                    }
-                    color = Ansi.Color.values()[exchangeIdColorsIndex];
-                    exchangeIdColors.put(s, color);
-                }
-                s = Ansi.ansi().fg(color).a(s).reset().toString();
-            }
-            return s;
-        }
-
-        int valueLength() {
-            if (value == null) {
-                return -1;
-            } else {
-                return valueAsString().length();
-            }
-        }
-
-    }
-
 }
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
new file mode 100644
index 00000000000..cf7282e899a
--- /dev/null
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/MessageTableHelper.java
@@ -0,0 +1,408 @@
+/*
+ * 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.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+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.common.JSonHelper;
+import org.apache.camel.dsl.jbang.core.common.XmlHelper;
+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;
+
+/**
+ * Helper to output message details (headers, body) in a table like structure with pretty and color supported.
+ */
+public class MessageTableHelper {
+
+    @FunctionalInterface
+    interface ColorChooser {
+        Ansi.Color color(String value);
+    }
+
+    private boolean loggingColor;
+    private boolean pretty;
+    private boolean showExchangeProperties;
+    private ColorChooser exchangeIdColorChooser;
+
+    public boolean isLoggingColor() {
+        return loggingColor;
+    }
+
+    public void setLoggingColor(boolean loggingColor) {
+        this.loggingColor = loggingColor;
+    }
+
+    public boolean isPretty() {
+        return pretty;
+    }
+
+    public void setPretty(boolean pretty) {
+        this.pretty = pretty;
+    }
+
+    public boolean isShowExchangeProperties() {
+        return showExchangeProperties;
+    }
+
+    public void setShowExchangeProperties(boolean showExchangeProperties) {
+        this.showExchangeProperties = showExchangeProperties;
+    }
+
+    public ColorChooser getExchangeIdColorChooser() {
+        return exchangeIdColorChooser;
+    }
+
+    public void setExchangeIdColorChooser(ColorChooser exchangeIdColorChooser) {
+        this.exchangeIdColorChooser = exchangeIdColorChooser;
+    }
+
+    public String getDataAsTable(
+            String exchangeId, String exchangePattern,
+            JsonObject endpoint, JsonObject root, JsonObject cause) {
+
+        List<TableRow> rows = new ArrayList<>();
+        TableRow eRow;
+        String tab0 = null, tab1 = null, tab1b = null, tab2 = null, tab3 = null, tab4 = null, tab5 = null, tab6 = null;
+
+        if (endpoint != null) {
+            eRow = new TableRow("Endpoint", endpoint.getString("endpoint"), null, null);
+            tab0 = AsciiTable.getTable(AsciiTable.NO_BORDERS, List.of(eRow), Arrays.asList(
+                    new Column().dataAlign(HorizontalAlign.LEFT)
+                            .minWidth(showExchangeProperties ? 12 : 10).with(TableRow::kindAsString),
+                    new Column().dataAlign(HorizontalAlign.LEFT).with(TableRow::typeAsString)));
+        }
+
+        if (root != null) {
+            eRow = new TableRow("Exchange", root.getString("exchangeType"), exchangePattern, exchangeId);
+            tab1 = AsciiTable.getTable(AsciiTable.NO_BORDERS, List.of(eRow), Arrays.asList(
+                    new Column().dataAlign(HorizontalAlign.LEFT)
+                            .minWidth(showExchangeProperties ? 12 : 10).with(TableRow::kindAsString),
+                    new Column().dataAlign(HorizontalAlign.LEFT).with(TableRow::typeAsString)));
+            tab1b = AsciiTable.getTable(AsciiTable.NO_BORDERS, List.of(eRow), Arrays.asList(
+                    new Column().dataAlign(HorizontalAlign.CENTER)
+                            .minWidth(18).maxWidth(18).with(TableRow::mepAsKey),
+                    new Column().dataAlign(HorizontalAlign.RIGHT)
+                            .maxWidth(80).with(TableRow::exchangeIdAsValue)));
+            // exchange properties
+            JsonArray arr = root.getCollection("exchangeProperties");
+            if (arr != null) {
+                for (Object o : arr) {
+                    JsonObject jo = (JsonObject) o;
+                    rows.add(new TableRow("Property", jo.getString("type"), jo.getString("key"), jo.get("value")));
+                }
+            }
+            // internal exchange properties
+            arr = root.getCollection("internalExchangeProperties");
+            if (arr != null) {
+                for (Object o : arr) {
+                    JsonObject jo = (JsonObject) o;
+                    rows.add(new TableRow("Property", jo.getString("type"), jo.getString("key"), jo.get("value")));
+                }
+            }
+            tab2 = AsciiTable.getTable(AsciiTable.NO_BORDERS, rows, Arrays.asList(
+                    new Column().dataAlign(HorizontalAlign.LEFT)
+                            .minWidth(showExchangeProperties ? 12 : 10).with(TableRow::kindAsString),
+                    new Column().dataAlign(HorizontalAlign.LEFT)
+                            .minWidth(25).maxWidth(50, OverflowBehaviour.CLIP_LEFT).with(TableRow::typeAsString),
+                    new Column().dataAlign(HorizontalAlign.RIGHT)
+                            .minWidth(25).maxWidth(40, OverflowBehaviour.NEWLINE).with(TableRow::keyAsString),
+                    new Column().dataAlign(HorizontalAlign.LEFT)
+                            .maxWidth(80, OverflowBehaviour.NEWLINE).with(TableRow::valueAsString)));
+            rows.clear();
+
+            // message type before headers
+            TableRow msgRow = new TableRow("Message", root.getString("messageType"), null, null);
+            tab3 = AsciiTable.getTable(AsciiTable.NO_BORDERS, List.of(msgRow), Arrays.asList(
+                    new Column().dataAlign(HorizontalAlign.LEFT)
+                            .minWidth(showExchangeProperties ? 12 : 10).with(TableRow::kindAsString),
+                    new Column().dataAlign(HorizontalAlign.LEFT).with(TableRow::typeAsString)));
+            arr = root.getCollection("headers");
+            if (arr != null) {
+                for (Object o : arr) {
+                    JsonObject jo = (JsonObject) o;
+                    rows.add(new TableRow("Header", jo.getString("type"), jo.getString("key"), jo.get("value")));
+                }
+            }
+            // headers
+            tab4 = AsciiTable.getTable(AsciiTable.NO_BORDERS, rows, Arrays.asList(
+                    new Column().dataAlign(HorizontalAlign.LEFT)
+                            .minWidth(showExchangeProperties ? 12 : 10).with(TableRow::kindAsString),
+                    new Column().dataAlign(HorizontalAlign.LEFT)
+                            .minWidth(25).maxWidth(50, OverflowBehaviour.CLIP_LEFT).with(TableRow::typeAsString),
+                    new Column().dataAlign(HorizontalAlign.RIGHT)
+                            .minWidth(25).maxWidth(40, OverflowBehaviour.NEWLINE).with(TableRow::keyAsString),
+                    new Column().dataAlign(HorizontalAlign.LEFT)
+                            .maxWidth(80, OverflowBehaviour.NEWLINE).with(TableRow::valueAsString)));
+
+            // body and type
+            JsonObject jo = root.getMap("body");
+            if (jo != null) {
+                TableRow bodyRow = new TableRow("Body", jo.getString("type"), null, jo.get("value"), jo.getLong("position"));
+                tab5 = AsciiTable.getTable(AsciiTable.NO_BORDERS, List.of(bodyRow), Arrays.asList(
+                        new Column().dataAlign(HorizontalAlign.LEFT)
+                                .minWidth(showExchangeProperties ? 12 : 10).with(TableRow::kindAsString),
+                        new Column().dataAlign(HorizontalAlign.LEFT).with(TableRow::typeAndLengthAsString)));
+                // body value only (span)
+                if (bodyRow.value != null) {
+                    tab6 = AsciiTable.getTable(AsciiTable.NO_BORDERS, List.of(bodyRow), Arrays.asList(
+                            new Column().dataAlign(HorizontalAlign.LEFT).maxWidth(160, OverflowBehaviour.NEWLINE)
+                                    .with(b -> pretty ? bodyRow.valueAsStringPretty() : bodyRow.valueAsString())));
+                }
+            }
+        }
+
+        String tab7 = null;
+        if (cause != null) {
+            eRow = new TableRow("Exception", cause.getString("type"), null, cause.get("message"));
+            tab7 = AsciiTable.getTable(AsciiTable.NO_BORDERS, List.of(eRow), Arrays.asList(
+                    new Column().dataAlign(HorizontalAlign.LEFT)
+                            .minWidth(showExchangeProperties ? 12 : 10)
+                            .with(TableRow::kindAsStringRed),
+                    new Column().dataAlign(HorizontalAlign.LEFT)
+                            .maxWidth(40, OverflowBehaviour.CLIP_LEFT).with(TableRow::typeAsString),
+                    new Column().dataAlign(HorizontalAlign.LEFT)
+                            .maxWidth(80, OverflowBehaviour.NEWLINE).with(TableRow::valueAsStringRed)));
+        }
+        // stacktrace only (span)
+        String tab8 = null;
+        if (cause != null) {
+            String value = cause.getString("stackTrace");
+            value = Jsoner.unescape(value);
+            eRow = new TableRow("Stacktrace", null, null, value);
+            tab8 = AsciiTable.getTable(AsciiTable.NO_BORDERS, List.of(eRow), Arrays.asList(
+                    new Column().dataAlign(HorizontalAlign.LEFT).maxWidth(160, OverflowBehaviour.NEWLINE)
+                            .with(TableRow::valueAsStringRed)));
+        }
+        String answer = "";
+        if (tab0 != null && !tab0.isEmpty()) {
+            answer = answer + tab0 + System.lineSeparator();
+        }
+        if (tab1 != null && tab1b != null && !tab1.isEmpty()) {
+            answer = answer + tab1 + tab1b + System.lineSeparator();
+        }
+        if (tab2 != null && !tab2.isEmpty()) {
+            answer = answer + tab2 + System.lineSeparator();
+        }
+        if (tab3 != null && !tab3.isEmpty()) {
+            answer = answer + tab3 + System.lineSeparator();
+        }
+        if (tab4 != null && !tab4.isEmpty()) {
+            answer = answer + tab4 + System.lineSeparator();
+        }
+        if (tab5 != null && !tab5.isEmpty()) {
+            answer = answer + tab5 + System.lineSeparator();
+        }
+        if (tab6 != null && !tab6.isEmpty()) {
+            answer = answer + tab6 + System.lineSeparator();
+        }
+        if (tab7 != null && !tab7.isEmpty()) {
+            answer = answer + tab7 + System.lineSeparator();
+        }
+        if (tab8 != null && !tab8.isEmpty()) {
+            answer = answer + tab8 + System.lineSeparator();
+        }
+        return answer;
+    }
+
+    private class TableRow {
+        String kind;
+        String type;
+        String key;
+        Object value;
+        Long position;
+
+        TableRow(String kind, String type, String key, Object value) {
+            this(kind, type, key, value, null);
+        }
+
+        TableRow(String kind, String type, String key, Object value, Long position) {
+            this.kind = kind;
+            this.type = type;
+            this.key = key;
+            this.value = value;
+            this.position = position;
+        }
+
+        String valueAsString() {
+            return value != null ? value.toString() : "null";
+        }
+
+        String valueAsStringPretty() {
+            if (value == null) {
+                return "null";
+            }
+            boolean json = false;
+            String s = value.toString();
+            if (!s.isEmpty()) {
+                try {
+                    s = Jsoner.unescape(s);
+                    if (loggingColor) {
+                        s = JSonHelper.colorPrint(s, 2, true);
+                    } else {
+                        s = JSonHelper.prettyPrint(s, 2);
+                    }
+                    if (s != null && !s.isEmpty()) {
+                        json = true;
+                    }
+                } catch (Throwable e) {
+                    // ignore as not json
+                }
+                if (s == null || s.isEmpty()) {
+                    s = value.toString();
+                }
+                if (!json) {
+                    // try with xml
+                    try {
+                        s = Jsoner.unescape(s);
+                        if (loggingColor) {
+                            s = XmlHelper.colorPrint(s, 2, true);
+                        } else {
+                            s = XmlHelper.prettyPrint(s, 2);
+                        }
+                    } catch (Throwable e) {
+                        // ignore as not xml
+                    }
+                }
+                if (s == null || s.isEmpty()) {
+                    s = value.toString();
+                }
+            }
+            if (s == null) {
+                return "null";
+            }
+            return s;
+        }
+
+        String valueAsStringRed() {
+            if (value != null) {
+                if (loggingColor) {
+                    return Ansi.ansi().fgRed().a(value).reset().toString();
+                } else {
+                    return value.toString();
+                }
+            }
+            return "";
+        }
+
+        String keyAsString() {
+            if (key == null) {
+                return "";
+            }
+            return key;
+        }
+
+        String kindAsString() {
+            return kind;
+        }
+
+        String kindAsStringRed() {
+            if (loggingColor) {
+                return Ansi.ansi().fgRed().a(kind).reset().toString();
+            } else {
+                return kind;
+            }
+        }
+
+        String typeAsString() {
+            String s;
+            if (type == null) {
+                s = "null";
+            } else if (type.startsWith("java.util.concurrent")) {
+                s = type.substring(21);
+            } else if (type.startsWith("java.lang.") || type.startsWith("java.util.")) {
+                s = type.substring(10);
+            } else if (type.startsWith("org.apache.camel.support.")) {
+                s = type.substring(25);
+            } else if (type.startsWith("org.apache.camel.converter.stream.")) {
+                s = type.substring(34);
+            } else {
+                s = type;
+            }
+            s = "(" + s + ")";
+            if (loggingColor) {
+                s = Ansi.ansi().fgBrightDefault().a(Ansi.Attribute.INTENSITY_FAINT).a(s).reset().toString();
+            }
+            return s;
+        }
+
+        String typeAndLengthAsString() {
+            String s;
+            if (type == null) {
+                s = "null";
+            } else if (type.startsWith("java.util.concurrent")) {
+                s = type.substring(21);
+            } else if (type.startsWith("java.lang.") || type.startsWith("java.util.")) {
+                s = type.substring(10);
+            } else if (type.startsWith("org.apache.camel.support.")) {
+                s = type.substring(25);
+            } else if (type.startsWith("org.apache.camel.converter.stream.")) {
+                s = type.substring(34);
+            } else {
+                s = type;
+            }
+            s = "(" + s + ")";
+            int l = valueLength();
+            long p = position != null ? position : -1;
+            if (l != -1 & p != -1) {
+                s = s + " (pos: " + p + " length: " + l + ")";
+            } else if (l != -1) {
+                s = s + " (length: " + l + ")";
+            } else if (p != -1) {
+                s = s + " (pos: " + p + ")";
+            }
+            if (loggingColor) {
+                s = Ansi.ansi().fgBrightDefault().a(Ansi.Attribute.INTENSITY_FAINT).a(s).reset().toString();
+            }
+            return s;
+        }
+
+        String mepAsKey() {
+            String s = key;
+            if (loggingColor) {
+                s = Ansi.ansi().fgBrightMagenta().a(Ansi.Attribute.INTENSITY_FAINT).a(s).reset().toString();
+            }
+            return s;
+        }
+
+        String exchangeIdAsValue() {
+            String s = value.toString();
+            if (loggingColor) {
+                Ansi.Color color = exchangeIdColorChooser != null ? exchangeIdColorChooser.color(s) : Ansi.Color.DEFAULT;
+                s = Ansi.ansi().fg(color).a(s).reset().toString();
+            }
+            return s;
+        }
+
+        int valueLength() {
+            if (value == null) {
+                return -1;
+            } else {
+                return valueAsString().length();
+            }
+        }
+
+    }
+
+}


[camel] 03/03: CAMEL-19236: camel-jbang - Command to send a message.

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

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

commit 646b371c8b8cf6b5887e4c9d56ecda86905f60ec
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Fri Apr 14 10:38:07 2023 +0200

    CAMEL-19236: camel-jbang - Command to send a message.
---
 .../modules/ROOT/pages/camel-jbang.adoc            | 84 ++++++++++++++++++++++
 1 file changed, 84 insertions(+)

diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang.adoc b/docs/user-manual/modules/ROOT/pages/camel-jbang.adoc
index c5c58b00035..19159d97c7b 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang.adoc
@@ -743,6 +743,90 @@ For example. you can copy this to your clipboard and then run it afterwards:
 camel run clipboard.xml
 ----
 
+=== Sending messages via Camel
+
+*Available since Camel 3.21*
+
+When building integrations with Camel JBang, you may find yourself in need of being able
+to send messages into Camel, to test your Camel routes. This can be challenging when the
+Camel routes are connecting to external systems using different protocols.
+
+The best approach is to send messages into these external systems using standard tools provided,
+by these systems, which often can be done using CLI tools. However, in some situations, where you
+may not be familiar with these tools, you can try to let Camel send the message. Note that this
+can only be possible in some scenarious, and should only be used as _quick way_.
+
+Suppose you have a Camel route that consumes messages from an external MQTT broker:
+
+[source,yaml]
+----
+- route:
+    from:
+      uri: kamelet:mqtt5-source
+      parameters:
+        topic: temperature
+        brokerUrl: tcp://mybroker:1883
+      steps:
+        - transform:
+            expression:
+              jq:
+                expression: .value
+        - log:
+            message: The temperature is ${body}
+----
+
+In the example above the MQTT broker is running on hostname `mybroker` port 1883.
+
+The idea with the `camel cmd send` command is to _tap into_ an existing running Camel integration,
+and reuse an existing endpoint (if possible). In this example we want to use the existing configuration
+to avoid having to configure this again.
+
+By executing the following from a shell
+
+[source,bash]
+----
+$ camel cmd send --body=file:payload.json mqtt
+----
+
+We can send a message, where the payload is loaded from a file (payload.json). You can also specify the payload in the CLI
+argument, but it's cumbersome to specify JSon structure so often its better to refer to a local file.
+
+[source,json]
+----
+{
+  "value": 21
+}
+----
+
+The `mqtt` argument is the name of the existing running Camel integration. You can also specify the PID instead.
+So what happens is that Camel will let the existing integration send the message.
+
+Because the existing integration only have 1 route, then the `send` command will automatic pick
+the _from_ endpoint, i.e. `kamelet:mqtt5-source` with all its configuration. If there are multiple routes,
+then you can filter which route/endpoint by the `--endpoint` option:
+
+For example to pick the first route by _route id_:
+
+[source,bash]
+----
+$ camel cmd send --body=file:payload.json --endpoint=route1 mqtt
+----
+
+Or to pick the first route that uses mqtt component:
+
+[source,bash]
+----
+$ camel cmd send --body=file:payload.json --endpoint=mqtt mqtt
+----
+
+We are fortunate in this situation as the endpoint can be used as both a _consumer_ and _producer_ in Camel,
+and therefore we are able to send the message to the MQTT broker via `tcp://mybroker:1883` on topic _temperate_.
+
+TIP: See more options with `camel cmd send --help`.
+
+The source for this example is provided on GitHub at https://github.com/apache/camel-kamelets-examples/tree/main/jbang/mqtt)[camel-jbang MQTT example].
+
+
 === Controlling local Camel integrations
 
 To list the currently running Camel integrations you use the `ps` command:


[camel] 02/03: CAMEL-19236: camel-jbang - Command to send a message.

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

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

commit 058c5e6c0cb9bf06223c55d38fd10844b16b543f
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Fri Apr 14 09:01:00 2023 +0200

    CAMEL-19236: camel-jbang - Command to send a message.
---
 .../apache/camel/dsl/jbang/core/commands/action/CamelSendAction.java  | 2 +-
 .../camel/dsl/jbang/core/commands/action/MessageTableHelper.java      | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSendAction.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSendAction.java
index a8cdf8d650e..eca9d63b2d0 100644
--- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSendAction.java
+++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSendAction.java
@@ -207,7 +207,7 @@ public class CamelSendAction extends ActionBaseCommand {
         // endpoint
         String ids = jo.getString("endpoint");
         if (ids.length() > 40) {
-            ids = ids.substring(ids.length() - 40);
+            ids = ids.substring(0, 40);
         }
         ids = String.format("%40.40s", ids);
         if (loggingColor) {
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 cf7282e899a..ba0974584d2 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
@@ -87,11 +87,11 @@ public class MessageTableHelper {
         String tab0 = null, tab1 = null, tab1b = null, tab2 = null, tab3 = null, tab4 = null, tab5 = null, tab6 = null;
 
         if (endpoint != null) {
-            eRow = new TableRow("Endpoint", endpoint.getString("endpoint"), null, null);
+            eRow = new TableRow("Endpoint", null, null, endpoint.getString("endpoint"));
             tab0 = AsciiTable.getTable(AsciiTable.NO_BORDERS, List.of(eRow), Arrays.asList(
                     new Column().dataAlign(HorizontalAlign.LEFT)
                             .minWidth(showExchangeProperties ? 12 : 10).with(TableRow::kindAsString),
-                    new Column().dataAlign(HorizontalAlign.LEFT).with(TableRow::typeAsString)));
+                    new Column().dataAlign(HorizontalAlign.LEFT).with(TableRow::valueAsString)));
         }
 
         if (root != null) {