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
The following commit(s) were added to refs/heads/main by this push:
new 6862384f84e5 CAMEL-22935: camel-jbang - Add eval expression command
(#21265)
6862384f84e5 is described below
commit 6862384f84e56cd463bf8b64221783869e13f86d
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Feb 5 16:45:15 2026 +0100
CAMEL-22935: camel-jbang - Add eval expression command (#21265)
* CAMEL-22935: camel-jbang - Add eval expression command
---
.../apache/camel/catalog/dev-consoles.properties | 1 +
.../camel/catalog/dev-consoles/eval-language.json | 15 ++
.../impl/engine/DefaultSimpleFunctionRegistry.java | 3 +-
.../apache/camel/dev-console/eval-language.json | 15 ++
.../org/apache/camel/dev-console/eval-language | 2 +
.../org/apache/camel/dev-consoles.properties | 2 +-
.../camel/impl/console/EvalLanguageDevConsole.java | 133 ++++++++++++
.../impl/console/SimpleLanguageDevConsole.java | 12 +-
.../java/org/apache/camel/util/SimpleUtils.java | 4 +-
.../pages/jbang-commands/camel-jbang-commands.adoc | 1 +
.../camel-jbang-eval-expression.adoc | 35 ++++
.../pages/jbang-commands/camel-jbang-eval.adoc | 34 +++
.../camel-jbang-transform-message.adoc | 2 +-
.../camel/cli/connector/LocalCliConnector.java | 27 +++
.../META-INF/camel-jbang-commands-metadata.json | 3 +-
.../dsl/jbang/core/commands/CamelJBangMain.java | 2 +
.../camel/dsl/jbang/core/commands/EvalCommand.java | 36 ++++
.../commands/action/EvalExpressionCommand.java | 231 +++++++++++++++++++++
.../commands/action/TransformMessageAction.java | 2 +-
.../dsl/jbang/core/commands/EvalSimpleTest.java | 49 +++++
.../ParameterExceptionHandlerTest.java | 2 +-
21 files changed, 599 insertions(+), 12 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles.properties
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles.properties
index eccfcfb03b00..6954ed2b5d4e 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles.properties
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles.properties
@@ -10,6 +10,7 @@ consumer
context
debug
endpoint
+eval-language
event
fault-tolerance
gc
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/eval-language.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/eval-language.json
new file mode 100644
index 000000000000..6082795c18bb
--- /dev/null
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/eval-language.json
@@ -0,0 +1,15 @@
+{
+ "console": {
+ "kind": "console",
+ "group": "camel",
+ "name": "eval-language",
+ "title": "Evaluate Language",
+ "description": "Evaluate Language and display result",
+ "deprecated": false,
+ "javaType": "org.apache.camel.impl.console.EvalLanguageDevConsole",
+ "groupId": "org.apache.camel",
+ "artifactId": "camel-console",
+ "version": "4.18.0-SNAPSHOT"
+ }
+}
+
diff --git
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultSimpleFunctionRegistry.java
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultSimpleFunctionRegistry.java
index 143f8a110d6d..706d14c14521 100644
---
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultSimpleFunctionRegistry.java
+++
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultSimpleFunctionRegistry.java
@@ -19,6 +19,7 @@ package org.apache.camel.impl.engine;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
+import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.camel.CamelContext;
@@ -127,7 +128,7 @@ public class DefaultSimpleFunctionRegistry extends
ServiceSupport implements Sim
@Override
public Set<String> getCustomFunctionNames() {
- return functions.keySet();
+ return new TreeSet<>(functions.keySet());
}
@Override
diff --git
a/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/eval-language.json
b/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/eval-language.json
new file mode 100644
index 000000000000..6082795c18bb
--- /dev/null
+++
b/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/eval-language.json
@@ -0,0 +1,15 @@
+{
+ "console": {
+ "kind": "console",
+ "group": "camel",
+ "name": "eval-language",
+ "title": "Evaluate Language",
+ "description": "Evaluate Language and display result",
+ "deprecated": false,
+ "javaType": "org.apache.camel.impl.console.EvalLanguageDevConsole",
+ "groupId": "org.apache.camel",
+ "artifactId": "camel-console",
+ "version": "4.18.0-SNAPSHOT"
+ }
+}
+
diff --git
a/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-console/eval-language
b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-console/eval-language
new file mode 100644
index 000000000000..19ba1cc8d860
--- /dev/null
+++
b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-console/eval-language
@@ -0,0 +1,2 @@
+# Generated by camel build tools - do NOT edit this file!
+class=org.apache.camel.impl.console.EvalLanguageDevConsole
diff --git
a/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-consoles.properties
b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-consoles.properties
index d1f4d2b43665..c5c4e618f264 100644
---
a/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-consoles.properties
+++
b/core/camel-console/src/generated/resources/META-INF/services/org/apache/camel/dev-consoles.properties
@@ -1,5 +1,5 @@
# Generated by camel build tools - do NOT edit this file!
-dev-consoles=bean blocked browse circuit-breaker consumer context debug
endpoint event gc health inflight internal-tasks java-security jvm log memory
message-history processor producer properties receive reload rest route
route-controller route-dump route-group route-structure send service
simple-language source startup-recorder system-properties thread top trace
transformers type-converters variables
+dev-consoles=bean blocked browse circuit-breaker consumer context debug
endpoint eval-language event gc health inflight internal-tasks java-security
jvm log memory message-history processor producer properties receive reload
rest route route-controller route-dump route-group route-structure send service
simple-language source startup-recorder system-properties thread top trace
transformers type-converters variables
groupId=org.apache.camel
artifactId=camel-console
version=4.18.0-SNAPSHOT
diff --git
a/core/camel-console/src/main/java/org/apache/camel/impl/console/EvalLanguageDevConsole.java
b/core/camel-console/src/main/java/org/apache/camel/impl/console/EvalLanguageDevConsole.java
new file mode 100644
index 000000000000..0040eace7604
--- /dev/null
+++
b/core/camel-console/src/main/java/org/apache/camel/impl/console/EvalLanguageDevConsole.java
@@ -0,0 +1,133 @@
+/*
+ * 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.impl.console;
+
+import java.util.Map;
+
+import org.apache.camel.Exchange;
+import org.apache.camel.Expression;
+import org.apache.camel.Predicate;
+import org.apache.camel.spi.annotations.DevConsole;
+import org.apache.camel.support.DefaultExchange;
+import org.apache.camel.support.MessageHelper;
+import org.apache.camel.support.console.AbstractDevConsole;
+import org.apache.camel.util.json.JsonObject;
+
+@DevConsole(name = "eval-language", displayName = "Evaluate Language",
description = "Evaluate Language and display result")
+public class EvalLanguageDevConsole extends AbstractDevConsole {
+
+ /**
+ * The language to use
+ */
+ public static final String LANGUAGE = "language";
+
+ /**
+ * Template to use for executing simple language function
+ */
+ public static final String TEMPLATE = "template";
+
+ /**
+ * Whether to execute as predicate (use expression by default)
+ */
+ public static final String PREDICATE = "predicate";
+
+ /**
+ * Optional message body
+ */
+ public static final String BODY = "body";
+
+ /**
+ * Optional message headers
+ */
+ public static final String HEADERS = "headers";
+
+ public EvalLanguageDevConsole() {
+ super("camel", "eval-language", "Evaluate Language", "Evaluate
Language and display result");
+ }
+
+ @Override
+ protected String doCallText(Map<String, Object> options) {
+ StringBuilder sb = new StringBuilder();
+
+ String language = (String) options.getOrDefault(LANGUAGE, "simple");
+ String template = (String) options.get(TEMPLATE);
+ if (template != null) {
+ Exchange dummy = new DefaultExchange(getCamelContext());
+ dummy.getMessage().setBody(options.get(BODY));
+ var headers = options.get(HEADERS);
+ if (headers instanceof Map map) {
+ dummy.getMessage().setHeaders(map);
+ }
+
+ String out;
+ boolean predicate = options.getOrDefault(PREDICATE,
"false").equals("true");
+ if (predicate) {
+ Predicate pre =
getCamelContext().resolveLanguage(language).createPredicate(template);
+ out = pre.matches(dummy) ? "true" : "false";
+ } else {
+ Expression exp =
getCamelContext().resolveLanguage(language).createExpression(template);
+ out = exp.evaluate(dummy, String.class);
+ }
+ sb.append(String.format("%nEvaluating (%s): %s", language,
template));
+ sb.append("\n");
+ sb.append(out);
+ }
+ return sb.toString();
+ }
+
+ @Override
+ protected JsonObject doCallJson(Map<String, Object> options) {
+ JsonObject root = new JsonObject();
+
+ String language = (String) options.getOrDefault(LANGUAGE, "simple");
+ String template = (String) options.get(TEMPLATE);
+ if (template != null) {
+ Exchange dummy = new DefaultExchange(getCamelContext());
+ dummy.getMessage().setBody(options.get(BODY));
+ var headers = options.get(HEADERS);
+ if (headers instanceof Map map) {
+ dummy.getMessage().setHeaders(map);
+ }
+
+ Exception cause = null;
+ String out = null;
+ try {
+ boolean predicate = options.getOrDefault(PREDICATE,
"false").equals("true");
+ if (predicate) {
+ Predicate pre =
getCamelContext().resolveLanguage(language).createPredicate(template);
+ out = pre.matches(dummy) ? "true" : "false";
+ } else {
+ Expression exp =
getCamelContext().resolveLanguage(language).createExpression(template);
+ out = exp.evaluate(dummy, String.class);
+ }
+ } catch (Exception e) {
+ cause = e;
+ }
+
+ if (cause != null) {
+ root.put("status", "failed");
+ root.put("exception",
+
MessageHelper.dumpExceptionAsJSonObject(cause).getMap("exception"));
+ } else {
+ root.put("status", "success");
+ root.put("result", out);
+ }
+ }
+
+ return root;
+ }
+}
diff --git
a/core/camel-console/src/main/java/org/apache/camel/impl/console/SimpleLanguageDevConsole.java
b/core/camel-console/src/main/java/org/apache/camel/impl/console/SimpleLanguageDevConsole.java
index 517c1eaea2fb..4577a5e54d98 100644
---
a/core/camel-console/src/main/java/org/apache/camel/impl/console/SimpleLanguageDevConsole.java
+++
b/core/camel-console/src/main/java/org/apache/camel/impl/console/SimpleLanguageDevConsole.java
@@ -39,20 +39,23 @@ public class SimpleLanguageDevConsole extends
AbstractDevConsole {
SimpleFunctionRegistry reg =
PluginHelper.getSimpleFunctionRegistry(getCamelContext());
sb.append(String.format("%n Core Functions: %d", reg.coreSize()));
for (String name : reg.getCoreFunctionNames()) {
- sb.append(String.format("%n %s", name));
+ sb.append(String.format("%n %s", name));
}
+ sb.append("\n");
sb.append(String.format("%n Custom Functions: %d",
reg.customSize()));
for (String name : reg.getCustomFunctionNames()) {
- sb.append(String.format("%n %s", name));
+ sb.append(String.format("%n %s", name));
}
+ sb.append("\n");
+
return sb.toString();
}
@Override
protected JsonObject doCallJson(Map<String, Object> options) {
- SimpleFunctionRegistry reg =
PluginHelper.getSimpleFunctionRegistry(getCamelContext());
-
JsonObject root = new JsonObject();
+
+ SimpleFunctionRegistry reg =
PluginHelper.getSimpleFunctionRegistry(getCamelContext());
root.put("coreSize", reg.coreSize());
root.put("customSize", reg.customSize());
JsonArray arr = new JsonArray();
@@ -65,6 +68,7 @@ public class SimpleLanguageDevConsole extends
AbstractDevConsole {
if (!arr.isEmpty()) {
root.put("customFunctions", arr);
}
+
return root;
}
}
diff --git
a/core/camel-util/src/main/java/org/apache/camel/util/SimpleUtils.java
b/core/camel-util/src/main/java/org/apache/camel/util/SimpleUtils.java
index 7319c067c4ed..b55cba73b9d4 100644
--- a/core/camel-util/src/main/java/org/apache/camel/util/SimpleUtils.java
+++ b/core/camel-util/src/main/java/org/apache/camel/util/SimpleUtils.java
@@ -18,12 +18,12 @@ package org.apache.camel.util;
import java.util.Arrays;
import java.util.Collections;
-import java.util.HashSet;
+import java.util.LinkedHashSet;
import java.util.Set;
public class SimpleUtils {
- private static final Set<String> SIMPLE_FUNCTIONS =
Collections.unmodifiableSet(new HashSet<>(
+ private static final Set<String> SIMPLE_FUNCTIONS =
Collections.unmodifiableSet(new LinkedHashSet<>(
Arrays.asList(
// Generated by camel build tools - do NOT edit this list!
// SIMPLE-FUNCTIONS: START
diff --git
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-commands.adoc
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-commands.adoc
index 179bbddbfaa8..edfd48586a31 100644
---
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-commands.adoc
+++
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-commands.adoc
@@ -20,6 +20,7 @@ TIP: You can also use `camel --help` or `camel <command>
--help` to see availabl
| xref:jbang-commands/camel-jbang-dependency.adoc[camel dependency] | Displays
all Camel dependencies required to run
| xref:jbang-commands/camel-jbang-dirty.adoc[camel dirty] | Check if there are
dirty files from previous Camel runs that did not terminate gracefully
| xref:jbang-commands/camel-jbang-doc.adoc[camel doc] | Shows documentation
for kamelet, component, and other Camel resources
+| xref:jbang-commands/camel-jbang-eval.adoc[camel eval] | Evaluate Camel
expressions and scripts
| xref:jbang-commands/camel-jbang-explain.adoc[camel explain] | Explain what a
Camel route does using AI/LLM
| xref:jbang-commands/camel-jbang-export.adoc[camel export] | Export to other
runtimes (Camel Main, Spring Boot, or Quarkus)
| xref:jbang-commands/camel-jbang-get.adoc[camel get] | Get status of Camel
integrations
diff --git
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-eval-expression.adoc
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-eval-expression.adoc
new file mode 100644
index 000000000000..07626fc66fdb
--- /dev/null
+++
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-eval-expression.adoc
@@ -0,0 +1,35 @@
+
+// AUTO-GENERATED by camel-package-maven-plugin - DO NOT EDIT THIS FILE
+= camel eval expression
+
+Evaluates Camel expression
+
+
+== Usage
+
+[source,bash]
+----
+camel eval expression [options]
+----
+
+
+
+== Options
+
+[cols="2,5,1,2",options="header"]
+|===
+| Option | Description | Default | Type
+| `--body` | Message body (prefix with file: to refer to loading message body
from file) | | String
+| `--camel-version` | To run using a different Camel version than the default
version. | | String
+| `--header` | Message header (key=value) | | List
+| `--isolated` | Whether to run evaluation isolated in local process | false |
boolean
+| `--language` | Language to use | simple | String
+| `--predicate` | Whether to force evaluating as predicate | false | boolean
+| `--repo,--repos` | Additional maven repositories (Use commas to separate
multiple repositories) | | String
+| `--template` | The template to use for evaluating (prefix with file: to
refer to loading template from file) | | String
+| `--timeout` | Timeout in millis waiting for evaluation to be done | 10000 |
long
+| `--watch` | Execute periodically and showing output fullscreen | | boolean
+| `-h,--help` | Display the help and sub-commands | | boolean
+|===
+
+
diff --git
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-eval.adoc
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-eval.adoc
new file mode 100644
index 000000000000..20ca09b59751
--- /dev/null
+++ b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-eval.adoc
@@ -0,0 +1,34 @@
+
+// AUTO-GENERATED by camel-package-maven-plugin - DO NOT EDIT THIS FILE
+= camel eval
+
+Evaluate Camel expressions and scripts
+
+
+== Usage
+
+[source,bash]
+----
+camel eval [options]
+----
+
+
+== Subcommands
+
+[cols="2,5",options="header"]
+|===
+| Subcommand | Description
+| xref:jbang-commands/camel-jbang-eval-expression.adoc[expression] | Evaluates
Camel expression
+|===
+
+
+
+== Options
+
+[cols="2,5,1,2",options="header"]
+|===
+| Option | Description | Default | Type
+| `-h,--help` | Display the help and sub-commands | | boolean
+|===
+
+
diff --git
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-transform-message.adoc
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-transform-message.adoc
index 2e50c308475e..68d27963cb00 100644
---
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-transform-message.adoc
+++
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-transform-message.adoc
@@ -35,7 +35,7 @@ camel transform message [options]
| `--show-exchange-properties` | Show exchange properties from the output
message | false | boolean
| `--show-headers` | Show message headers from the output message | true |
boolean
| `--source` | Instead of using external template file then refer to an
existing Camel route source with inlined Camel language expression in a route.
(use :line-number or :id to refer to the exact location of the EIP to use) | |
String
-| `--template` | The template to use for message transformation (prefix with
file: to refer to loading message body from file) | | String
+| `--template` | The template to use for message transformation (prefix with
file: to refer to loading template from file) | | String
| `--timeout` | Timeout in millis waiting for message to be transformed |
20000 | long
| `--watch` | Execute periodically and showing output fullscreen | | boolean
| `-h,--help` | Display the help and sub-commands | | boolean
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 b12e69a6f7c7..eaf6e37df925 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
@@ -292,6 +292,8 @@ public class LocalCliConnector extends ServiceSupport
implements CliConnector, C
doActionLoggerTask(root);
} else if ("gc".equals(action)) {
System.gc();
+ } else if ("eval".equals(action)) {
+ doActionEvalTask(root);
} else if ("load".equals(action)) {
doActionLoadTask(root);
} else if ("reload".equals(action)) {
@@ -856,6 +858,31 @@ public class LocalCliConnector extends ServiceSupport
implements CliConnector, C
}
}
+ private void doActionEvalTask(JsonObject root) throws Exception {
+ DevConsole dc =
camelContext.getCamelContextExtension().getContextPlugin(DevConsoleRegistry.class)
+ .resolveById("eval-language");
+ if (dc != null) {
+ String lan = root.getStringOrDefault("language", "simple");
+ boolean predicate = root.getBooleanOrDefault("predicate", false);
+ String template = root.getStringOrDefault("template", "");
+ String body = root.getStringOrDefault("body", "");
+ Map<String, String> map = new LinkedHashMap<>();
+ Collection<JsonObject> headers = root.getCollection("headers");
+ if (headers != null) {
+ map = new LinkedHashMap<>();
+ for (JsonObject jo : headers) {
+ map.put(jo.getString("key"), jo.getString("value"));
+ }
+ }
+ JsonObject json = (JsonObject) dc.call(DevConsole.MediaType.JSON,
+ Map.of("language", lan, "predicate", predicate,
"template", template, "body", body, "headers", map));
+ LOG.trace("Updating output file: {}", outputFile);
+ IOHelper.writeText(json.toJson(), outputFile);
+ } else {
+ IOHelper.writeText("{}", outputFile);
+ }
+ }
+
private void doActionLoadTask(JsonObject root) throws Exception {
List<String> files = root.getCollection("source");
boolean restart = root.getBooleanOrDefault("restart", false);
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
index 49a490218e55..6fedc3030bea 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
+++
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
@@ -9,6 +9,7 @@
{ "name": "dependency", "fullName": "dependency", "description": "Displays
all Camel dependencies required to run", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.DependencyCommand", "options": [ {
"names": "-h,--help", "description": "Display the help and sub-commands",
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name":
"copy", "fullName": "dependency copy", "description": "Copies all Camel
dependencies required to run to a specific directory", "sourc [...]
{ "name": "dirty", "fullName": "dirty", "description": "Check if there are
dirty files from previous Camel runs that did not terminate gracefully",
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.process.Dirty",
"options": [ { "names": "--clean", "description": "Clean dirty files which are
no longer in use", "defaultValue": "false", "javaType": "boolean", "type":
"boolean" }, { "names": "-h,--help", "description": "Display the help and
sub-commands", "javaType": "boolean", " [...]
{ "name": "doc", "fullName": "doc", "description": "Shows documentation
for kamelet, component, and other Camel resources", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.catalog.CatalogDoc", "options": [ {
"names": "--camel-version", "description": "To use a different Camel version
than the default version", "javaType": "java.lang.String", "type": "string" },
{ "names": "--download", "description": "Whether to allow automatic downloading
JAR dependencies (over the internet [...]
+ { "name": "eval", "fullName": "eval", "description": "Evaluate Camel
expressions and scripts", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.EvalCommand", "options": [ { "names":
"-h,--help", "description": "Display the help and sub-commands", "javaType":
"boolean", "type": "boolean" } ], "subcommands": [ { "name": "expression",
"fullName": "eval expression", "description": "Evaluates Camel expression",
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.action.EvalEx [...]
{ "name": "explain", "fullName": "explain", "description": "Explain what a
Camel route does using AI\/LLM", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.Explain", "options": [ { "names":
"--api-key", "description": "API key for authentication. Also reads
OPENAI_API_KEY or LLM_API_KEY env vars", "javaType": "java.lang.String",
"type": "string" }, { "names": "--api-type", "description": "API type: 'ollama'
or 'openai' (OpenAI-compatible)", "defaultValue": "ollama", "javaTyp [...]
{ "name": "export", "fullName": "export", "description": "Export to other
runtimes (Camel Main, Spring Boot, or Quarkus)", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.Export", "options": [ { "names":
"--build-property", "description": "Maven\/Gradle build properties, ex.
--build-property=prop1=foo", "javaType": "java.util.List", "type": "array" }, {
"names": "--build-tool", "description": "DEPRECATED: Build tool to use (maven
or gradle) (gradle is deprecated)", "defaultV [...]
{ "name": "get", "fullName": "get", "description": "Get status of Camel
integrations", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.process.CamelStatus", "options": [ {
"names": "--watch", "description": "Execute periodically and showing output
fullscreen", "javaType": "boolean", "type": "boolean" }, { "names":
"-h,--help", "description": "Display the help and sub-commands", "javaType":
"boolean", "type": "boolean" } ], "subcommands": [ { "name": "bean",
"fullName": "get [...]
@@ -28,7 +29,7 @@
{ "name": "stop", "fullName": "stop", "description": "Shuts down running
Camel integrations", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.process.StopProcess", "options": [ {
"names": "--kill", "description": "To force killing the process (SIGKILL)",
"javaType": "boolean", "type": "boolean" }, { "names": "-h,--help",
"description": "Display the help and sub-commands", "javaType": "boolean",
"type": "boolean" } ] },
{ "name": "top", "fullName": "top", "description": "Top status of Camel
integrations", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.process.CamelTop", "options": [ {
"names": "--watch", "description": "Execute periodically and showing output
fullscreen", "javaType": "boolean", "type": "boolean" }, { "names":
"-h,--help", "description": "Display the help and sub-commands", "javaType":
"boolean", "type": "boolean" } ], "subcommands": [ { "name": "context",
"fullName": "top [...]
{ "name": "trace", "fullName": "trace", "description": "Tail message
traces from running Camel integrations", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.action.CamelTraceAction", "options":
[ { "names": "--action", "description": "Action to start, stop, clear, list
status, or dump traces", "defaultValue": "status", "javaType":
"java.lang.String", "type": "string" }, { "names": "--ago", "description": "Use
ago instead of yyyy-MM-dd HH:mm:ss in timestamp.", "javaType": "b [...]
- { "name": "transform", "fullName": "transform", "description": "Transform
message or Camel routes", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.TransformCommand", "options": [ {
"names": "-h,--help", "description": "Display the help and sub-commands",
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name":
"message", "fullName": "transform message", "description": "Transform message
from one format to another via an existing running Camel integration", " [...]
+ { "name": "transform", "fullName": "transform", "description": "Transform
message or Camel routes", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.TransformCommand", "options": [ {
"names": "-h,--help", "description": "Display the help and sub-commands",
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name":
"message", "fullName": "transform message", "description": "Transform message
from one format to another via an existing running Camel integration", " [...]
{ "name": "update", "fullName": "update", "description": "Update Camel
project", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.update.UpdateCommand", "options": [ {
"names": "-h,--help", "description": "Display the help and sub-commands",
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name":
"list", "fullName": "update list", "description": "List available update
versions for Camel and its runtime variants", "sourceClass":
"org.apache.camel.dsl.jbang.cor [...]
{ "name": "version", "fullName": "version", "description": "Manage Camel
versions", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.version.VersionCommand", "options": [
{ "names": "-h,--help", "description": "Display the help and sub-commands",
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "get",
"fullName": "version get", "description": "Displays current Camel version",
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.version.VersionGet",
[...]
]
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 c62c219295e8..8a55de137ba1 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
@@ -125,6 +125,8 @@ public class CamelJBangMain implements Callable<Integer> {
.addSubcommand("runtime", new CommandLine(new
DependencyRuntime(main)))
.addSubcommand("update", new CommandLine(new
DependencyUpdate(main))))
.addSubcommand("dirty", new CommandLine(new Dirty(main)))
+ .addSubcommand("eval", new CommandLine(new EvalCommand(main))
+ .addSubcommand("expression", new CommandLine(new
EvalExpressionCommand(main))))
.addSubcommand("export", new CommandLine(new Export(main)))
.addSubcommand("explain", new CommandLine(new Explain(main)))
.addSubcommand("harden", new CommandLine(new Harden(main)))
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/EvalCommand.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/EvalCommand.java
new file mode 100644
index 000000000000..3af4549c48d4
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/EvalCommand.java
@@ -0,0 +1,36 @@
+/*
+ * 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 = "eval",
+ description = "Evaluate Camel expressions and scripts
(use transform --help to see sub commands)",
+ sortOptions = false, showDefaultValues = true)
+public class EvalCommand extends CamelCommand {
+
+ public EvalCommand(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/action/EvalExpressionCommand.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/EvalExpressionCommand.java
new file mode 100644
index 000000000000..e45d0104f820
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/EvalExpressionCommand.java
@@ -0,0 +1,231 @@
+/*
+ * 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.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collections;
+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.CommandLineHelper;
+import org.apache.camel.dsl.jbang.core.common.PathUtils;
+import org.apache.camel.dsl.jbang.core.common.VersionHelper;
+import org.apache.camel.util.StopWatch;
+import org.apache.camel.util.StringHelper;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+import org.fusesource.jansi.Ansi;
+import picocli.CommandLine;
+
[email protected](name = "expression",
+ description = "Evaluates Camel expression", sortOptions =
false,
+ showDefaultValues = true)
+public class EvalExpressionCommand extends ActionWatchCommand {
+
+ @CommandLine.Parameters(description = "Name or pid of running Camel
integration", arity = "0..1")
+ String name = "*";
+
+ @CommandLine.Option(names = { "--isolated" },
+ description = "Whether to run evaluation isolated in
local process", defaultValue = "false")
+ boolean isolated;
+
+ @CommandLine.Option(names = { "--camel-version" },
+ description = "To run using a different Camel version
than the default version.")
+ String camelVersion;
+
+ @CommandLine.Option(names = { "--language" },
+ description = "Language to use", defaultValue =
"simple")
+ String language;
+
+ @CommandLine.Option(names = { "--predicate" },
+ description = "Whether to force evaluating as
predicate", defaultValue = "false")
+ boolean predicate;
+
+ @CommandLine.Option(names = {
+ "--template" },
+ description = "The template to use for evaluating
(prefix with file: to refer to loading template from file)",
+ required = true)
+ String template;
+
+ @CommandLine.Option(names = { "--body" },
+ description = "Message body (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 = "10000",
+ description = "Timeout in millis waiting for
evaluation to be done")
+ long timeout = 10000;
+
+ @CommandLine.Option(names = { "--repo", "--repos" },
+ description = "Additional maven repositories (Use
commas to separate multiple repositories)")
+ String repositories;
+
+ long pid;
+
+ public EvalExpressionCommand(CamelJBangMain main) {
+ super(main);
+ }
+
+ @Override
+ public Integer doCall() throws Exception {
+ if (template == null || template.isBlank()) {
+ printer().printErr("Template is required");
+ return -1;
+ }
+ if (template.startsWith("file:")) {
+ Path f = Path.of(template.substring(5));
+ if (!Files.exists(f)) {
+ printer().printErr("Template file does not exist: " + f);
+ return -1;
+ }
+ }
+
+ Integer exit;
+ List<Long> pids = isolated ? Collections.EMPTY_LIST : findPids(name);
+ if (pids.size() == 1) {
+ this.pid = pids.get(0);
+ printer().println("Using existing running Camel integration to
evaluate (pid: " + this.pid + ")");
+ exit = super.doCall();
+ } else {
+ 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")) {
+ printer().printErr("This requires Camel version 4.3 or
newer");
+ return -1;
+ }
+ exit = run.runTransformMessage(camelVersion, repositories);
+ this.pid = run.spawnPid;
+ if (exit == 0) {
+ exit = super.doCall();
+ }
+ } finally {
+ if (pid > 0) {
+ // cleanup output file
+ Path outputFile = getOutputFile(Long.toString(pid));
+ PathUtils.deleteFile(outputFile);
+ // stop running camel as we are done
+ Path parent = CommandLineHelper.getCamelDir();
+ Path pidFile = parent.resolve(Long.toString(pid));
+ if (Files.exists(pidFile)) {
+ PathUtils.deleteFile(pidFile);
+ }
+ }
+ }
+ }
+
+ return exit;
+ }
+
+ @Override
+ protected Integer doWatchCall() throws Exception {
+ JsonObject root = new JsonObject();
+ root.put("action", "eval");
+ root.put("language", language);
+ root.put("predicate", predicate);
+ root.put("template", Jsoner.escape(template));
+ if (body != null) {
+ root.put("body", Jsoner.escape(body));
+ }
+ if (headers != null) {
+ JsonArray arr = new JsonArray();
+ for (String h : headers) {
+ JsonObject jo = new JsonObject();
+ if (!h.contains("=")) {
+ printer().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);
+ }
+
+ Path outputFile = getOutputFile(Long.toString(pid));
+ PathUtils.deleteFile(outputFile);
+
+ Path f = getActionFile(Long.toString(pid));
+ try {
+ PathUtils.writeTextSafely(root.toJson(), f);
+ } catch (Exception e) {
+ // ignore
+ }
+
+ JsonObject jo = waitForOutputFile(outputFile);
+ if (jo != null) {
+ String status = jo.getString("status");
+ if ("success".equals(status)) {
+ String result = jo.getString("result");
+ printer().println(result);
+ } else {
+ JsonObject cause = jo.getMap("exception");
+ if (cause != null) {
+ String msg = cause.getString("message");
+ if (msg != null) {
+ msg = Jsoner.unescape(msg);
+ }
+ String st = cause.getString("stackTrace");
+ if (st != null) {
+ st = Jsoner.unescape(st);
+ }
+ if (msg != null) {
+ String text =
Ansi.ansi().fgRed().a(msg).reset().toString();
+ printer().printErr(text);
+ printer().println();
+ }
+ if (st != null) {
+ String text =
Ansi.ansi().fgRed().a(st).reset().toString();
+ printer().printErr(text);
+ printer().println();
+ }
+ return 1;
+ }
+ }
+ }
+
+ return 0;
+ }
+
+ protected JsonObject waitForOutputFile(Path outputFile) {
+ StopWatch watch = new StopWatch();
+ while (watch.taken() < timeout) {
+ try {
+ // give time for response to be ready
+ Thread.sleep(20);
+
+ if (Files.exists(outputFile)) {
+ String text = Files.readString(outputFile);
+ return (JsonObject) Jsoner.deserialize(text);
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ } 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/TransformMessageAction.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/TransformMessageAction.java
index 274af9d325c2..081fcbf4dec3 100644
---
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
@@ -76,7 +76,7 @@ public class TransformMessageAction extends
ActionWatchCommand {
@CommandLine.Option(names = {
"--template" },
- description = "The template to use for message
transformation (prefix with file: to refer to loading message body from file)")
+ description = "The template to use for message
transformation (prefix with file: to refer to loading template from file)")
private String template;
@CommandLine.Option(names = { "--option" },
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/EvalSimpleTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/EvalSimpleTest.java
new file mode 100644
index 000000000000..9d6fc22d3451
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/EvalSimpleTest.java
@@ -0,0 +1,49 @@
+/*
+ * 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 org.apache.camel.dsl.jbang.core.commands.action.EvalExpressionCommand;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledIfSystemProperty;
+import picocli.CommandLine;
+
+@DisabledIfSystemProperty(named = "ci.env.name", matches = ".*",
disabledReason = "Runs only local")
+class EvalSimpleTest extends CamelCommandBaseTestSupport {
+
+ @Test
+ public void shouldEvalSimple() throws Exception {
+ String[] args = new String[] { "--isolated=true",
"--template=${length()}", "--body=hello_world" };
+ EvalExpressionCommand command = createCommand(args);
+ int exit = command.doCall();
+ Assertions.assertEquals(0, exit);
+
+ var lines = printer.getLines();
+ Assertions.assertNotNull(lines);
+ Assertions.assertEquals(2, lines.size());
+ Assertions.assertEquals("11", lines.get(1));
+ }
+
+ private EvalExpressionCommand createCommand(String... args) {
+ EvalExpressionCommand command = new EvalExpressionCommand(new
CamelJBangMain().withPrinter(printer));
+ if (args != null) {
+ CommandLine.populateCommand(command, args);
+ }
+ return command;
+ }
+
+}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/exceptionhandler/ParameterExceptionHandlerTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/exceptionhandler/ParameterExceptionHandlerTest.java
index 83490a515a7b..0155c7ef70f2 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/exceptionhandler/ParameterExceptionHandlerTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/exceptionhandler/ParameterExceptionHandlerTest.java
@@ -34,7 +34,7 @@ class ParameterExceptionHandlerTest {
Assertions.assertEquals(5, lines.length, "5 lines for the error is
expected but received " + lines.length);
Assertions.assertEquals("Unmatched argument at index 0:
'firstInvalid'", lines[0],
"First line mentioning unmatched argument");
- Assertions.assertEquals("Did you mean: camel stop or camel infra or
camel plugin?", lines[1],
+ Assertions.assertEquals("Did you mean: camel eval or camel stop or
camel infra?", lines[1],
"Second line with suggestion in case it is a typo");
Assertions.assertEquals(
"Maybe a specific Camel JBang plugin must be installed? (Try
camel plugin --help' for more information)",