This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch tra-msg2 in repository https://gitbox.apache.org/repos/asf/camel.git
commit c257d9cc4308116c50aaee497c69a0afb6062624 Author: Claus Ibsen <[email protected]> AuthorDate: Mon Nov 27 09:59:15 2023 +0100 CAMEL-20153: camel-jbang - Transform message command --- .../ROOT/pages/camel-4x-upgrade-guide-4_3.adoc | 44 ++-- .../modules/ROOT/pages/camel-jbang.adoc | 124 +++++++++ .../camel/cli/connector/LocalCliConnector.java | 71 +++++ .../dsl/jbang/core/commands/CamelJBangMain.java | 5 +- .../apache/camel/dsl/jbang/core/commands/Run.java | 73 +++++- .../dsl/jbang/core/commands/TransformCommand.java | 35 +++ .../{Transform.java => TransformRoute.java} | 10 +- .../commands/action/TransformMessageAction.java | 289 +++++++++++++++++++++ 8 files changed, 615 insertions(+), 36 deletions(-) diff --git a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_3.adoc b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_3.adoc index 1eff390ac1a..29e4cb334bf 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_3.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_3.adoc @@ -6,21 +6,7 @@ from both 4.0 to 4.1 and 4.1 to 4.2. == Upgrading Camel 4.2 to 4.3 -=== camel-kafka - -The behavior for `breakOnFirstError` was altered as numerous issues were fixed. The behavior related to committing -the offset is now determined by the `CommitManager` that is configured. - -When the default `CommitManager` is used (`NoopCommitManager`) then no commit is performed. The route implementation will -be responsible for managing the offset using `KafkaManualCommit` to manage the retrying of the payload. - -When using the `SyncCommitManager` then the offset will be committed so that the payload is continually retried. This was -the behavior described in the documentation. - -When using the `AsyncCommitManager` then the offset will be committed so that the payload is continually retried. This was -the behavior described in the documentation. - -=== throttle +=== Throttle EIP Throttle now uses the number of concurrent requests as the throttling measure instead of the number of requests per period. @@ -30,9 +16,10 @@ and remove any `timePeriodMillis` option. For example, update the following: -[source] +[source,java] ---- long maxRequestsPerPeriod = 100L; + from("seda:a") .throttle(maxRequestsPerPeriod).timePeriodMillis(500) .to("seda:b") @@ -45,9 +32,10 @@ from("seda:c") to use `maxConcurrentRequests`: -[source] +[source,java] ---- long maxConcurrentRequests = 30L; + from("seda:a") .throttle(maxConcurrentRequests) .to("seda:b") @@ -56,3 +44,25 @@ from("seda:c") .throttle(maxConcurrentRequests) .to("seda:d") ---- + +=== camel-jbang + +The `camel transform` command has been renamed to `camel transform route` as this command is used for transforming +routes between DSLs such as XML to YAML. + +There is a new `camel transform message` command to do message transformation. + +=== camel-kafka + +The behavior for `breakOnFirstError` was altered as numerous issues were fixed. The behavior related to committing +the offset is now determined by the `CommitManager` that is configured. + +When the default `CommitManager` is used (`NoopCommitManager`) then no commit is performed. The route implementation will +be responsible for managing the offset using `KafkaManualCommit` to manage the retrying of the payload. + +When using the `SyncCommitManager` then the offset will be committed so that the payload is continually retried. This was +the behavior described in the documentation. + +When using the `AsyncCommitManager` then the offset will be committed so that the payload is continually retried. This was +the behavior described in the documentation. + diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang.adoc b/docs/user-manual/modules/ROOT/pages/camel-jbang.adoc index dc4ac941fd5..a4b7e769ddb 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-jbang.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-jbang.adoc @@ -2121,6 +2121,130 @@ which is due to invalid configuration: `Invalid url in bootstrap.servers: value` TIP: Use `camel get health --help` to see all the various options. +== Transforming message (data mapping) + +When integrating system you often need to transform messages from one system to another. Camel has rich set +of capabilities for this such as various data formats, templating languages, and much more. However, for basic +data mapping such as taking an existing incoming JSon document and transform this to a smaller JSon document, +you may want to do this quickly with Camel. + +The `camel transform message` command can be used for such tasks, where it can take an existing source file as input, +and then a template that defines how to transform the data, and then show the output (in real time). + +For example given this JSon document (in a file named `random.json`), we want to combine the name, and select a few fields: + +[source,json] +---- +{ + "id": 9914, + "uid": "eb5fa603-1db6-45f9-912a-431a6ed59b18", + "password": "ei7gvYKdnN", + "first_name": "Khalilah", + "last_name": "Monahan", + "username": "khalilah.monahan", + "email": "[email protected]", + "avatar": "https://robohash.org/utnumquamexcepturi.png?size=300x300&set=set1", + "gender": "Agender", + "phone_number": "+54 (421) 591-5640 x333", + "social_insurance_number": "268418308", + "date_of_birth": "1975-03-11", + "employment": { + "title": "Product Design Director", + "key_skill": "Work under pressure" + }, + "address": { + "city": "New Fritzchester", + "street_name": "Patrick Common", + "street_address": "4538 Reggie Inlet", + "zip_code": "16282-7045", + "state": "New York", + "country": "United States", + "coordinates": { + "lat": -1.9868753435474673, + "lng": 39.09763956726292 + } + }, + "credit_card": { + "cc_number": "4493983042212" + }, + "subscription": { + "plan": "Student", + "status": "Active", + "payment_method": "Debit card", + "term": "Monthly" + } +} +---- + +Then we can have a `transform.json` file as the beginning of the template, with the structure of the desired output: + +[source,json] +---- +{ + "sid": 123, + "name": "TODO", + "country": "TODO", + "phone": "TODO", + "student": false +} +---- + +We can then run `camel transform message` and have it update (in real time) the output every time we change the template. + +[source,bash] +---- +$ camel transform message --body=file:random.json --language=simple --template=file:transform.json --pretty --watch +---- + +What happens is then Camel will output on the console as you go: + +[source,bash] +---- + Exchange (DefaultExchange) InOut 23F5DD4CE6C260B-0000000000000002 + Message (DefaultMessage) + Body (String) (bytes: 118) + { + "sid": 123, + "name": "TODO", + "country": "TODO", + "phone": "TODO", + "student": false + } +---- + +Then you can update the `transform.json` file and save it and see the generated output: + +[source,json] +---- +{ + "sid": ${jq(.id)}, + "name": "${jq(.first_name)} ${jq(.last_name)}", + "country": "TODO", + "phone": "TODO", + "student": false +} +---- + +And the output: + +[source,bash] +---- + Exchange (DefaultExchange) InOut 23F5DD4CE6C260B-0000000000000018 + Message (DefaultMessage) + Body (String) (bytes: 158) + { + "sid": 9914, + "name": "Khalilah Monahan", + "country": "TODO", + "phone": "TODO", + "student": false + } +---- + +Then you can continue to update the `transform.json` until you have the desired result. And if you make a mistake +then you see an error (in red) with stacktrace that hopefully can help you out how to fix this. + + == Listing what Camel components is available Camel comes with a lot of artifacts out of the box which comes as: 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..641569e55b9 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 template = Jsoner.unescape(root.getString("template")); + if (template.startsWith("file:")) { + template = "resource:" + template; + } + String body = Jsoner.unescape(root.getString("body")); + 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 = IOHelper.loadText(is); + } + Collection<JsonObject> headers = root.getCollection("headers"); + 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(template); + 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/Run.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java index 3df54e87611..e3d76d841f1 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java @@ -110,13 +110,14 @@ public class Run extends CamelCommand { private static final Pattern CLASS_PATTERN = Pattern.compile( "^\\s*public class\\s+([a-zA-Z0-9]*)[\\s+|;].*$", Pattern.MULTILINE); - boolean silentRun; + public boolean silentRun; boolean scriptRun; boolean transformRun; + boolean transformMessageRun; boolean debugRun; private File logFile; - long spawnPid; + public long spawnPid; @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) @@ -129,7 +130,10 @@ public class Run extends CamelCommand { String sourceDir; @Option(names = { "--background" }, defaultValue = "false", description = "Run in the background") - boolean background; + public boolean background; + + @Option(names = { "--empty" }, defaultValue = "false", description = "Run an empty Camel without loading source files") + public boolean empty; @Option(names = { "--camel-version" }, description = "To run using a different Camel version than the default version.") String camelVersion; @@ -286,7 +290,7 @@ public class Run extends CamelCommand { return run(); } - protected Integer runSilent() throws Exception { + public Integer runSilent() throws Exception { return runSilent(false); } @@ -303,6 +307,16 @@ public class Run extends CamelCommand { return run(); } + public Integer runTransformMessage(String camelVersion) throws Exception { + // just boot silently an empty camel in the background and exit + this.transformMessageRun = true; + this.background = true; + this.camelVersion = camelVersion; + this.empty = true; + this.name = "transform"; + return run(); + } + protected Integer runScript(String file) throws Exception { this.files.add(file); this.scriptRun = true; @@ -358,7 +372,7 @@ public class Run extends CamelCommand { } private int run() throws Exception { - if (!files.isEmpty() && sourceDir != null) { + if (!empty && !files.isEmpty() && sourceDir != null) { // cannot have both files and source dir at the same time System.err.println("Cannot specify both file(s) and source-dir at the same time."); return 1; @@ -368,14 +382,14 @@ public class Run extends CamelCommand { removeDir(work); work.mkdirs(); - Properties profileProperties = loadProfileProperties(); + Properties profileProperties = !empty ? loadProfileProperties() : null; configureLogging(); if (openapi != null) { generateOpenApi(); } // route code as option - if (code != null) { + if (!empty && code != null) { // store code in temporary file String codeFile = loadFromCode(code); // use code as first file @@ -383,7 +397,7 @@ public class Run extends CamelCommand { } // if no specific file to run then try to auto-detect - if (files.isEmpty() && sourceDir == null) { + if (!empty && files.isEmpty() && sourceDir == null) { String routes = profileProperties != null ? profileProperties.getProperty("camel.main.routesIncludePattern") : null; if (routes == null) { if (!silentRun) { @@ -501,6 +515,9 @@ public class Run extends CamelCommand { // do not run for very long in silent run main.addInitialProperty("camel.main.autoStartup", "false"); main.addInitialProperty("camel.main.durationMaxSeconds", "1"); + } else if (transformMessageRun) { + // do not start any routes + main.addInitialProperty("camel.main.autoStartup", "false"); } else if (scriptRun) { // auto terminate if being idle main.addInitialProperty("camel.main.durationMaxIdleSeconds", "1"); @@ -800,6 +817,11 @@ public class Run extends CamelCommand { private Properties loadProfileProperties() throws Exception { Properties answer = null; + if (transformMessageRun) { + // do not load profile in transform message run as it should be vanilla empty + return answer; + } + File profilePropertiesFile; if (sourceDir != null) { profilePropertiesFile = new File(sourceDir, getProfile() + ".properties"); @@ -861,7 +883,16 @@ public class Run extends CamelCommand { } protected int runCamelVersion(KameletMain main) throws Exception { - List<String> cmds = new ArrayList<>(spec.commandLine().getParseResult().originalArgs()); + List<String> cmds; + if (spec != null) { + cmds = new ArrayList<>(spec.commandLine().getParseResult().originalArgs()); + } else { + cmds = new ArrayList<>(); + cmds.add("run"); + if (transformMessageRun) { + cmds.add("--empty"); + } + } if (background) { cmds.remove("--background=true"); @@ -893,11 +924,16 @@ public class Run extends CamelCommand { ProcessBuilder pb = new ProcessBuilder(); pb.command(jbangArgs); + + System.out.println(jbangArgs); + if (background) { Process p = pb.start(); this.spawnPid = p.pid(); - System.out.println("Running Camel integration: " + name + " (version: " + camelVersion - + ") in background with PID: " + p.pid()); + if (!silentRun) { + System.out.println("Running Camel integration: " + name + " (version: " + camelVersion + + ") in background with PID: " + p.pid()); + } return 0; } else { pb.inheritIO(); // run in foreground (with IO so logs are visible) @@ -909,7 +945,16 @@ public class Run extends CamelCommand { } protected int runBackground(KameletMain main) throws Exception { - List<String> cmds = new ArrayList<>(spec.commandLine().getParseResult().originalArgs()); + List<String> cmds; + if (spec != null) { + cmds = new ArrayList<>(spec.commandLine().getParseResult().originalArgs()); + } else { + cmds = new ArrayList<>(); + cmds.add("run"); + if (transformMessageRun) { + cmds.add("--empty"); + } + } cmds.remove("--background=true"); cmds.remove("--background"); @@ -920,7 +965,9 @@ public class Run extends CamelCommand { pb.command(cmds); Process p = pb.start(); this.spawnPid = p.pid(); - System.out.println("Running Camel integration: " + name + " in background with PID: " + p.pid()); + if (!silentRun) { + System.out.println("Running Camel integration: " + name + " in background with PID: " + p.pid()); + } return 0; } 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; + [email protected](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..b958329d31e --- /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,289 @@ +/* + * 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.dsl.jbang.core.commands.Run; +import org.apache.camel.dsl.jbang.core.common.VersionHelper; +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; + [email protected](name = "message", + description = "Transform message from one format to another via an existing running Camel integration", + sortOptions = false) +public class TransformMessageAction extends ActionWatchCommand { + + @CommandLine.Option(names = { "--camel-version" }, + description = "To run using a different Camel version than the default version.") + String camelVersion; + + @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 = { + "--template" }, + required = true, + description = "The template to use for message transformation (prefix with file: to refer to loading message body from file)") + private String template; + + @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 { + Integer exit; + try { + // start a new empty camel in the background + Run run = new Run(getMain()); + // requires camel 4.3 onwards + if (camelVersion != null && VersionHelper.isLE(camelVersion, "4.2.0")) { + System.err.println("This requires Camel version 4.3 or newer"); + return -1; + } + exit = run.runTransformMessage(camelVersion); + this.pid = run.spawnPid; + if (exit == 0) { + exit = super.doCall(); + } + } finally { + if (pid > 0) { + // cleanup output file + File outputFile = getOutputFile(Long.toString(pid)); + FileUtil.deleteFile(outputFile); + // stop running camel as we are done + File dir = new File(System.getProperty("user.home"), ".camel"); + File pidFile = new File(dir, Long.toString(pid)); + if (pidFile.exists()) { + FileUtil.deleteFile(pidFile); + } + } + } + + return exit; + } + + @Override + protected Integer doWatchCall() throws Exception { + // 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); + root.put("template", Jsoner.escape(template)); + root.put("body", Jsoner.escape(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) { + if (watch) { + clearScreen(); + } + 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; + } +}
