This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch jp in repository https://gitbox.apache.org/repos/asf/camel.git
commit 36f2b334e7583cd6293c9cf877455e869440163e Author: Claus Ibsen <claus.ib...@gmail.com> AuthorDate: Sat Nov 25 14:24:41 2023 +0100 camel-jbang - Prototype for transform message command. --- .../camel/cli/connector/LocalCliConnector.java | 71 ++++++ .../dsl/jbang/core/commands/CamelJBangMain.java | 5 +- .../dsl/jbang/core/commands/TransformCommand.java | 35 +++ .../{Transform.java => TransformRoute.java} | 10 +- .../commands/action/TransformMessageAction.java | 270 +++++++++++++++++++++ 5 files changed, 385 insertions(+), 6 deletions(-) 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 e85ccfd8380..110adf2b29c 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 @@ -27,6 +27,7 @@ import java.lang.management.RuntimeMXBean; import java.lang.management.ThreadMXBean; import java.util.Collection; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; @@ -42,6 +43,7 @@ import org.apache.camel.CamelContextAware; import org.apache.camel.Endpoint; import org.apache.camel.Exchange; import org.apache.camel.ExchangePattern; +import org.apache.camel.Expression; import org.apache.camel.NoSuchEndpointException; import org.apache.camel.Processor; import org.apache.camel.ProducerTemplate; @@ -52,6 +54,7 @@ import org.apache.camel.console.DevConsoleRegistry; import org.apache.camel.spi.CliConnector; import org.apache.camel.spi.CliConnectorFactory; import org.apache.camel.spi.ContextReloadStrategy; +import org.apache.camel.spi.Language; import org.apache.camel.spi.ResourceReloadStrategy; import org.apache.camel.support.EndpointHelper; import org.apache.camel.support.MessageHelper; @@ -486,6 +489,74 @@ public class LocalCliConnector extends ServiceSupport implements CliConnector, C IOHelper.writeText(jo.toJson(), outputFile); } } + } else if ("transform".equals(action)) { + StopWatch watch = new StopWatch(); + long timestamp = System.currentTimeMillis(); + String language = root.getString("language"); + String expression = Jsoner.unescape(root.getStringOrDefault("expression", "")); + String body = root.getString("body"); + 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 LinkedHashMap<>(); + for (JsonObject jo : headers) { + map.put(jo.getString("key"), jo.getString("value")); + } + } + final Object inputBody = b; + final Map<String, Object> inputHeaders = map; + + Exchange out = camelContext.getCamelContextExtension().getExchangeFactory().create(false); + try { + Language lan = camelContext.resolveLanguage(language); + Expression exp = lan.createExpression(expression); + exp.init(camelContext); + // create dummy exchange with + out.setPattern(ExchangePattern.InOut); + out.getMessage().setBody(inputBody); + if (inputHeaders != null) { + out.getMessage().setHeaders(inputHeaders); + } + String result = exp.evaluate(out, String.class); + out.getMessage().setBody(result); + IOHelper.close(is); + } catch (Exception e) { + out.setException(e); + } + LOG.trace("Updating output file: {}", outputFile); + if (out.getException() != null) { + JsonObject jo = new JsonObject(); + jo.put("language", language); + jo.put("exchangeId", out.getExchangeId()); + 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 { + JsonObject jo = new JsonObject(); + jo.put("language", language); + jo.put("exchangeId", out.getExchangeId()); + 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); + } + camelContext.getCamelContextExtension().getExchangeFactory().release(out); + } } // action done so delete file 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 dd9993ad45e..836773601c4 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 @@ -37,6 +37,7 @@ 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; import org.apache.camel.dsl.jbang.core.commands.action.RouteControllerAction; +import org.apache.camel.dsl.jbang.core.commands.action.TransformMessageAction; import org.apache.camel.dsl.jbang.core.commands.catalog.CatalogCommand; import org.apache.camel.dsl.jbang.core.commands.catalog.CatalogComponent; import org.apache.camel.dsl.jbang.core.commands.catalog.CatalogDataFormat; @@ -93,7 +94,9 @@ public class CamelJBangMain implements Callable<Integer> { .addSubcommand("ps", new CommandLine(new ListProcess(main))) .addSubcommand("stop", new CommandLine(new StopProcess(main))) .addSubcommand("trace", new CommandLine(new CamelTraceAction(main))) - .addSubcommand("transform", new CommandLine(new Transform(main))) + .addSubcommand("transform", new CommandLine(new TransformCommand(main)) + .addSubcommand("route", new CommandLine(new TransformRoute(main))) + .addSubcommand("message", new CommandLine(new TransformMessageAction(main)))) .addSubcommand("get", new CommandLine(new CamelStatus(main)) .addSubcommand("context", new CommandLine(new CamelContextStatus(main))) .addSubcommand("route", new CommandLine(new CamelRouteStatus(main))) diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/TransformCommand.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/TransformCommand.java new file mode 100644 index 00000000000..fb4f910af19 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/TransformCommand.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.dsl.jbang.core.commands; + +import picocli.CommandLine; + +@CommandLine.Command(name = "transform", + description = "Transform message or Camel routes (use transform --help to see sub commands)") +public class TransformCommand extends CamelCommand { + + public TransformCommand(CamelJBangMain main) { + super(main); + } + + @Override + public Integer doCall() throws Exception { + new CommandLine(this).execute("--help"); + return 0; + } + +} diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Transform.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/TransformRoute.java similarity index 95% rename from dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Transform.java rename to dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/TransformRoute.java index 6c5e47acab9..1b648000fd6 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Transform.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/TransformRoute.java @@ -29,8 +29,8 @@ import org.apache.camel.util.StopWatch; import picocli.CommandLine; import picocli.CommandLine.Command; -@Command(name = "transform", description = "Transform Camel routes to XML or YAML format", sortOptions = false) -public class Transform extends CamelCommand { +@Command(name = "route", description = "Transform Camel routes to XML or YAML format", sortOptions = false) +public class TransformRoute extends CamelCommand { @CommandLine.Parameters(description = "The Camel file(s) to run. If no files specified then application.properties is used as source for which files to run.", arity = "0..9", paramLabel = "<files>", parameterConsumer = FilesConsumer.class) @@ -60,7 +60,7 @@ public class Transform extends CamelCommand { description = "Whether to ignore route loading and compilation errors (use this with care!)") boolean ignoreLoadingError; - public Transform(CamelJBangMain main) { + public TransformRoute(CamelJBangMain main) { super(main); } @@ -130,9 +130,9 @@ public class Transform extends CamelCommand { return null; } - static class FilesConsumer extends ParameterConsumer<Transform> { + static class FilesConsumer extends ParameterConsumer<TransformRoute> { @Override - protected void doConsumeParameters(Stack<String> args, Transform cmd) { + protected void doConsumeParameters(Stack<String> args, TransformRoute cmd) { String arg = args.pop(); cmd.files.add(arg); } diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/TransformMessageAction.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/TransformMessageAction.java new file mode 100644 index 00000000000..aa8d2e34637 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/TransformMessageAction.java @@ -0,0 +1,270 @@ +/* + * 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 = "message", + description = "Transform message from one format to another via an existing running Camel integration", + sortOptions = false) +public class TransformMessageAction extends ActionBaseCommand { + + @CommandLine.Parameters(description = "Name or pid of running Camel integration", arity = "0..1") + String name = "*"; + + @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 = { + "--language" }, + required = true, + description = "The language to use for message transformation") + private String language; + + @CommandLine.Option(names = { + "--expression" }, + required = true, + description = "The expression (template) to use for message transformation") + private String expression; + + @CommandLine.Option(names = { + "--output" }, + description = "File to store output. If none provide then output is printed to console.") + private String output; + + @CommandLine.Option(names = { "--show-exchange-properties" }, defaultValue = "false", + description = "Show exchange properties from the output message") + boolean showExchangeProperties; + + @CommandLine.Option(names = { "--show-headers" }, defaultValue = "true", + description = "Show message headers from the output message") + boolean showHeaders = true; + + @CommandLine.Option(names = { "--show-body" }, defaultValue = "true", + description = "Show message body from the output message") + boolean showBody = true; + + @CommandLine.Option(names = { "--show-exception" }, defaultValue = "true", + description = "Show exception and stacktrace for failed transformation") + boolean showException = true; + + @CommandLine.Option(names = { "--timeout" }, defaultValue = "20000", + description = "Timeout in millis waiting for message to be transformed") + long timeout = 20000; + + @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 TransformMessageAction(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", "transform"); + root.put("language", language); + String exp = expression; + if (expression.startsWith("file:")) { + File f = new File(expression.substring(5)); + if (!f.isFile() && !f.exists()) { + System.err.println("File " + f.getName() + " does not exists"); + return -1; + } + exp = IOHelper.loadText(new FileInputStream(f)); + } + root.put("expression", Jsoner.escape(exp)); + 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 (output != null) { + File target = new File(output); + 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 (output == null) { + tableHelper = new MessageTableHelper(); + tableHelper.setPretty(pretty); + tableHelper.setLoggingColor(loggingColor); + tableHelper.setShowExchangeProperties(showExchangeProperties); + String table = tableHelper.getDataAsTable(exchangeId, "InOut", null, 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(" --- "); + } + // 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")); + String status; + if (failed) { + status = "Failed (exception)"; + } else if (output != null) { + status = "Output saved to file (success)"; + } else { + status = "Message transformed (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; + } +}