You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by da...@apache.org on 2026/06/09 19:54:37 UTC
(camel) branch feature/CAMEL-23724-simple-language-grammar created (now c2234b001155)
This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a change to branch feature/CAMEL-23724-simple-language-grammar
in repository https://gitbox.apache.org/repos/asf/camel.git
at c2234b001155 CAMEL-23724: Add operators section to Simple language catalog
This branch includes the following new commits:
new c2234b001155 CAMEL-23724: Add operators section to Simple language catalog
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(camel) 01/01: CAMEL-23724: Add operators section to Simple language catalog
Posted by da...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch feature/CAMEL-23724-simple-language-grammar
in repository https://gitbox.apache.org/repos/asf/camel.git
commit c2234b0011554ba5cbc24f937cd52da7797a4ff6
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Tue Jun 9 21:52:46 2026 +0200
CAMEL-23724: Add operators section to Simple language catalog
Extends the Simple language JSON catalog (simple.json) with a machine-readable
operators section documenting all 32 operators with their kind, syntax,
precedence, descriptions, and usage examples.
Co-Authored-By: Claude <no...@anthropic.com>
Signed-off-by: Claus Ibsen <cl...@gmail.com>
---
.../org/apache/camel/spi/annotations/Language.java | 13 ++
.../org/apache/camel/language/simple/simple.json | 34 +++
.../camel/language/simple/SimpleLanguage.java | 2 +-
.../language/simple/SimpleOperatorConstants.java | 232 +++++++++++++++++++++
.../org/apache/camel/tooling/model/JsonMapper.java | 65 ++++++
.../apache/camel/tooling/model/LanguageModel.java | 58 ++++++
.../camel/maven/packaging/PackageLanguageMojo.java | 54 +++++
.../org/apache/camel/spi/annotations/Language.java | 13 ++
8 files changed, 470 insertions(+), 1 deletion(-)
diff --git a/core/camel-api/src/generated/java/org/apache/camel/spi/annotations/Language.java b/core/camel-api/src/generated/java/org/apache/camel/spi/annotations/Language.java
index 5dc93f9f4b60..a45a9f2cef2d 100644
--- a/core/camel-api/src/generated/java/org/apache/camel/spi/annotations/Language.java
+++ b/core/camel-api/src/generated/java/org/apache/camel/spi/annotations/Language.java
@@ -43,4 +43,17 @@ public @interface Language {
*/
Class<?> functionsClass() default void.class;
+ /**
+ * The class that contains all the name of operators that are supported by the language. The name of the operators
+ * are defined as {@code String} constants in the operators class.
+ *
+ * The class to provide can be any class but by convention, we would expect a class whose name is of type
+ * <i>xxxOperatorConstants</i> where <i>xxx</i> is the name of the corresponding language like for example
+ * <i>SimpleOperatorConstants</i> for the language <i>camel-simple</i>.
+ *
+ * The metadata of a given operator are retrieved directly from the annotation {@code @Metadata} added to the
+ * {@code String} constant representing its name and defined in the operators class.
+ */
+ Class<?> operatorsClass() default void.class;
+
}
diff --git a/core/camel-core-languages/src/generated/resources/META-INF/org/apache/camel/language/simple/simple.json b/core/camel-core-languages/src/generated/resources/META-INF/org/apache/camel/language/simple/simple.json
index 1c6720fc16c3..d7224071f387 100644
--- a/core/camel-core-languages/src/generated/resources/META-INF/org/apache/camel/language/simple/simple.json
+++ b/core/camel-core-languages/src/generated/resources/META-INF/org/apache/camel/language/simple/simple.json
@@ -154,5 +154,39 @@
"variableAs(key,type)": { "index": 126, "kind": "function", "displayName": "Variable As", "group": "core", "label": "core", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Converts the variable to the given type (classname).", "ognl": false, "suffix": "}", "params": [ { "name": "key", "javaType": "String", "required": true, "description": "The variable name" }, { "name": "type", [...]
"variables": { "index": 127, "kind": "function", "displayName": "Variables", "group": "core", "label": "core", "required": false, "javaType": "java.util.Map", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Returns all the variables from the current Exchange in a Map", "ognl": false, "suffix": "}", "examples": [ "${variables} -> {myVar=value1, count=5}" ] },
"xpath(input,exp)": { "index": 128, "kind": "function", "displayName": "XPath", "group": "xml", "label": "xml", "required": false, "javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "When working with XML data, then this allows using the XPath language, for example, to extract data from the message body (in XML format). This requires having camel-xpath JAR on the classpath. For input (optional), you ca [...]
+ },
+ "operators": {
+ "==": { "index": 0, "kind": "operator", "displayName": "Eq", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests equality between left and right operand values. Camel will coerce the right operand type to match the left.", "operatorKind": "binary", "operatorSyntax": "LHS == RHS", "precedence": 10, "examples": [ "${header.foo} == 'bar'", "${header.count} == 5" ] },
+ "=~": { "index": 1, "kind": "operator", "displayName": "Eq ignore", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests equality between left and right operand values, ignoring case for string comparison.", "operatorKind": "binary", "operatorSyntax": "LHS =~ RHS", "precedence": 10, "examples": [ "${header.foo} =~ 'BAR'" ] },
+ ">": { "index": 2, "kind": "operator", "displayName": "Gt", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests whether the left operand is greater than the right operand.", "operatorKind": "binary", "operatorSyntax": "LHS > RHS", "precedence": 10, "examples": [ "${header.count} > 100" ] },
+ ">=": { "index": 3, "kind": "operator", "displayName": "Gte", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests whether the left operand is greater than or equal to the right operand.", "operatorKind": "binary", "operatorSyntax": "LHS >= RHS", "precedence": 10, "examples": [ "${header.count} >= 100" ] },
+ "<": { "index": 4, "kind": "operator", "displayName": "Lt", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests whether the left operand is less than the right operand.", "operatorKind": "binary", "operatorSyntax": "LHS < RHS", "precedence": 10, "examples": [ "${header.count} < 100" ] },
+ "<=": { "index": 5, "kind": "operator", "displayName": "Lte", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests whether the left operand is less than or equal to the right operand.", "operatorKind": "binary", "operatorSyntax": "LHS <= RHS", "precedence": 10, "examples": [ "${header.count} <= 100" ] },
+ "!=": { "index": 6, "kind": "operator", "displayName": "Not eq", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests inequality between left and right operand values.", "operatorKind": "binary", "operatorSyntax": "LHS != RHS", "precedence": 10, "examples": [ "${header.foo} != 'bar'" ] },
+ "!=~": { "index": 7, "kind": "operator", "displayName": "Not eq ignore", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests inequality between left and right operand values, ignoring case for string comparison.", "operatorKind": "binary", "operatorSyntax": "LHS !=~ RHS", "precedence": 10, "examples": [ "${header.foo} !=~ 'BAR'" ] },
+ "contains": { "index": 8, "kind": "operator", "displayName": "Contains", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests whether the left operand string contains the right operand string.", "operatorKind": "binary", "operatorSyntax": "LHS contains RHS", "precedence": 10, "examples": [ "${header.title} contains 'Camel'" ] },
+ "!contains": { "index": 9, "kind": "operator", "displayName": "Not contains", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests whether the left operand string does not contain the right operand string.", "operatorKind": "binary", "operatorSyntax": "LHS !contains RHS", "precedence": 10, "examples": [ "${header.title} !contains 'Camel'" ] },
+ "~~": { "index": 10, "kind": "operator", "displayName": "Contains ignorecase", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests whether the left operand string contains the right operand string, ignoring case.", "operatorKind": "binary", "operatorSyntax": "LHS ~~ RHS", "precedence": 10, "examples": [ "${header.title} ~~ 'camel'" ] },
+ "!~~": { "index": 11, "kind": "operator", "displayName": "Not contains ignorecase", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests whether the left operand string does not contain the right operand string, ignoring case.", "operatorKind": "binary", "operatorSyntax": "LHS !~~ RHS", "precedence": 10, "examples": [ "${header.title} !~~ 'camel'" ] },
+ "regex": { "index": 12, "kind": "operator", "displayName": "Regex", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests whether the left operand matches the right operand as a regular expression.", "operatorKind": "binary", "operatorSyntax": "LHS regex 'pattern'", "precedence": 10, "examples": [ "${header.number} regex '\\d{4}'" ] },
+ "!regex": { "index": 13, "kind": "operator", "displayName": "Not regex", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests whether the left operand does not match the right operand as a regular expression.", "operatorKind": "binary", "operatorSyntax": "LHS !regex 'pattern'", "precedence": 10, "examples": [ "${header.number} !regex '\\d{4}'" ] },
+ "in": { "index": 14, "kind": "operator", "displayName": "In", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests whether the left operand is in a set of comma-separated values.", "operatorKind": "binary", "operatorSyntax": "LHS in 'val1,val2,...'", "precedence": 10, "examples": [ "${header.type} in 'gold,silver'" ] },
+ "!in": { "index": 15, "kind": "operator", "displayName": "Not in", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests whether the left operand is not in a set of comma-separated values.", "operatorKind": "binary", "operatorSyntax": "LHS !in 'val1,val2,...'", "precedence": 10, "examples": [ "${header.type} !in 'gold,silver'" ] },
+ "is": { "index": 16, "kind": "operator", "displayName": "Is", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests whether the left operand is an instance of the right operand type (Java classname or short name).", "operatorKind": "binary", "operatorSyntax": "LHS is 'typeName'", "precedence": 10, "examples": [ "${header.type} is 'String'", "${body} is 'java.util.List'" ] },
+ "!is": { "index": 17, "kind": "operator", "displayName": "Not is", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests whether the left operand is not an instance of the right operand type.", "operatorKind": "binary", "operatorSyntax": "LHS !is 'typeName'", "precedence": 10, "examples": [ "${header.type} !is 'String'" ] },
+ "range": { "index": 18, "kind": "operator", "displayName": "Range", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests whether the left operand is within the numeric range specified by 'from..to'.", "operatorKind": "binary", "operatorSyntax": "LHS range 'from..to'", "precedence": 10, "examples": [ "${header.number} range '100..199'" ] },
+ "!range": { "index": 19, "kind": "operator", "displayName": "Not range", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests whether the left operand is not within the numeric range specified by 'from..to'.", "operatorKind": "binary", "operatorSyntax": "LHS !range 'from..to'", "precedence": 10, "examples": [ "${header.number} !range '100..199'" ] },
+ "startsWith": { "index": 20, "kind": "operator", "displayName": "Starts with", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests whether the left operand string starts with the right operand string.", "operatorKind": "binary", "operatorSyntax": "LHS startsWith RHS", "precedence": 10, "examples": [ "${header.name} startsWith 'Camel'" ] },
+ "!startsWith": { "index": 21, "kind": "operator", "displayName": "Not starts with", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests whether the left operand string does not start with the right operand string.", "operatorKind": "binary", "operatorSyntax": "LHS !startsWith RHS", "precedence": 10, "examples": [ "${header.name} !startsWith 'Camel'" ] },
+ "endsWith": { "index": 22, "kind": "operator", "displayName": "Ends with", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests whether the left operand string ends with the right operand string.", "operatorKind": "binary", "operatorSyntax": "LHS endsWith RHS", "precedence": 10, "examples": [ "${header.name} endsWith '.xml'" ] },
+ "!endsWith": { "index": 23, "kind": "operator", "displayName": "Not ends with", "label": "binary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Tests whether the left operand string does not end with the right operand string.", "operatorKind": "binary", "operatorSyntax": "LHS !endsWith RHS", "precedence": 10, "examples": [ "${header.name} !endsWith '.xml'" ] },
+ "++": { "index": 24, "kind": "operator", "displayName": "Inc", "label": "unary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Increments the numeric value by one. Must immediately follow a function closing brace.", "operatorKind": "unary", "operatorSyntax": "${fn}++", "precedence": 1, "examples": [ "${header.count}++" ] },
+ "--": { "index": 25, "kind": "operator", "displayName": "Dec", "label": "unary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Decrements the numeric value by one. Must immediately follow a function closing brace.", "operatorKind": "unary", "operatorSyntax": "${fn}--", "precedence": 1, "examples": [ "${header.count}--" ] },
+ "&&": { "index": 26, "kind": "operator", "displayName": "And", "label": "logical", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Logical AND. Both left and right predicates must evaluate to true.", "operatorKind": "logical", "operatorSyntax": "predicate && predicate", "precedence": 30, "examples": [ "${header.title} contains 'Camel' && ${header.type} == 'gold'" ] },
+ "||": { "index": 27, "kind": "operator", "displayName": "Or", "label": "logical", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Logical OR. At least one of the left or right predicates must evaluate to true.", "operatorKind": "logical", "operatorSyntax": "predicate || predicate", "precedence": 30, "examples": [ "${header.title} contains 'Camel' || ${header.type} == 'gold'" ] },
+ "? :": { "index": 28, "kind": "operator", "displayName": "Ternary", "label": "ternary", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Ternary conditional operator. Evaluates the predicate and returns trueValue if true, falseValue if false. Requires spaces around both ? and : tokens.", "operatorKind": "ternary", "operatorSyntax": "predicate ? trueValue : falseValue", "precedence": 25, "examples": [ "${header.foo} > [...]
+ "~>": { "index": 29, "kind": "operator", "displayName": "Chain", "label": "chain", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Pipes the result of the left expression as input body to the right expression. Use $param in the right expression to reference the piped value explicitly.", "operatorKind": "chain", "operatorSyntax": "expr ~> expr", "precedence": 5, "examples": [ "${trim()} ~> ${uppercase()}", "${substrin [...]
+ "?~>": { "index": 30, "kind": "operator", "displayName": "Chain null safe", "label": "chain", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Null-safe chain operator. Same as ~> but stops chaining and returns null if the left expression evaluates to null.", "operatorKind": "chain", "operatorSyntax": "expr ?~> expr", "precedence": 5, "examples": [ "${header.name} ?~> ${trim()} ?~> ${uppercase()}" ] },
+ "?:": { "index": 31, "kind": "operator", "displayName": "Elvis", "label": "other", "required": false, "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Elvis operator (null-coalescing). Returns the left operand if it is not null\/empty, otherwise returns the right operand as a fallback value.", "operatorKind": "other", "operatorSyntax": "expr ?: defaultValue", "precedence": 20, "examples": [ "${header.username} ?: 'Guest'", "${body} ?: $ [...]
}
}
diff --git a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleLanguage.java b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleLanguage.java
index 4be673a39504..c199f275b586 100644
--- a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleLanguage.java
+++ b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleLanguage.java
@@ -39,7 +39,7 @@ import org.slf4j.LoggerFactory;
/**
* The Camel simple language.
*/
-@Language(value = "simple", functionsClass = SimpleConstants.class)
+@Language(value = "simple", functionsClass = SimpleConstants.class, operatorsClass = SimpleOperatorConstants.class)
public class SimpleLanguage extends LanguageSupport implements StaticService {
private static final Logger LOG = LoggerFactory.getLogger(SimpleLanguage.class);
diff --git a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleOperatorConstants.java b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleOperatorConstants.java
new file mode 100644
index 000000000000..dfbca7987d91
--- /dev/null
+++ b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleOperatorConstants.java
@@ -0,0 +1,232 @@
+/*
+ * 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.language.simple;
+
+import org.apache.camel.spi.Metadata;
+
+@Metadata(label = "operator")
+public final class SimpleOperatorConstants {
+
+ // --- Binary comparison operators (precedence 10) ---
+
+ @Metadata(description = "Tests equality between left and right operand values. Camel will coerce the right operand type to match the left.",
+ label = "binary",
+ examples = { "${header.foo} == 'bar'", "${header.count} == 5" },
+ annotations = { "kind=binary", "syntax=LHS == RHS", "precedence=10" })
+ public static final String EQ = "==";
+
+ @Metadata(description = "Tests equality between left and right operand values, ignoring case for string comparison.",
+ label = "binary",
+ examples = { "${header.foo} =~ 'BAR'" },
+ annotations = { "kind=binary", "syntax=LHS =~ RHS", "precedence=10" })
+ public static final String EQ_IGNORE = "=~";
+
+ @Metadata(description = "Tests whether the left operand is greater than the right operand.",
+ label = "binary",
+ examples = { "${header.count} > 100" },
+ annotations = { "kind=binary", "syntax=LHS > RHS", "precedence=10" })
+ public static final String GT = ">";
+
+ @Metadata(description = "Tests whether the left operand is greater than or equal to the right operand.",
+ label = "binary",
+ examples = { "${header.count} >= 100" },
+ annotations = { "kind=binary", "syntax=LHS >= RHS", "precedence=10" })
+ public static final String GTE = ">=";
+
+ @Metadata(description = "Tests whether the left operand is less than the right operand.",
+ label = "binary",
+ examples = { "${header.count} < 100" },
+ annotations = { "kind=binary", "syntax=LHS < RHS", "precedence=10" })
+ public static final String LT = "<";
+
+ @Metadata(description = "Tests whether the left operand is less than or equal to the right operand.",
+ label = "binary",
+ examples = { "${header.count} <= 100" },
+ annotations = { "kind=binary", "syntax=LHS <= RHS", "precedence=10" })
+ public static final String LTE = "<=";
+
+ @Metadata(description = "Tests inequality between left and right operand values.",
+ label = "binary",
+ examples = { "${header.foo} != 'bar'" },
+ annotations = { "kind=binary", "syntax=LHS != RHS", "precedence=10" })
+ public static final String NOT_EQ = "!=";
+
+ @Metadata(description = "Tests inequality between left and right operand values, ignoring case for string comparison.",
+ label = "binary",
+ examples = { "${header.foo} !=~ 'BAR'" },
+ annotations = { "kind=binary", "syntax=LHS !=~ RHS", "precedence=10" })
+ public static final String NOT_EQ_IGNORE = "!=~";
+
+ @Metadata(description = "Tests whether the left operand string contains the right operand string.",
+ label = "binary",
+ examples = { "${header.title} contains 'Camel'" },
+ annotations = { "kind=binary", "syntax=LHS contains RHS", "precedence=10" })
+ public static final String CONTAINS = "contains";
+
+ @Metadata(description = "Tests whether the left operand string does not contain the right operand string.",
+ label = "binary",
+ examples = { "${header.title} !contains 'Camel'" },
+ annotations = { "kind=binary", "syntax=LHS !contains RHS", "precedence=10" })
+ public static final String NOT_CONTAINS = "!contains";
+
+ @Metadata(description = "Tests whether the left operand string contains the right operand string, ignoring case.",
+ label = "binary",
+ examples = { "${header.title} ~~ 'camel'" },
+ annotations = { "kind=binary", "syntax=LHS ~~ RHS", "precedence=10" })
+ public static final String CONTAINS_IGNORECASE = "~~";
+
+ @Metadata(description = "Tests whether the left operand string does not contain the right operand string, ignoring case.",
+ label = "binary",
+ examples = { "${header.title} !~~ 'camel'" },
+ annotations = { "kind=binary", "syntax=LHS !~~ RHS", "precedence=10" })
+ public static final String NOT_CONTAINS_IGNORECASE = "!~~";
+
+ @Metadata(description = "Tests whether the left operand matches the right operand as a regular expression.",
+ label = "binary",
+ examples = { "${header.number} regex '\\d{4}'" },
+ annotations = { "kind=binary", "syntax=LHS regex 'pattern'", "precedence=10" })
+ public static final String REGEX = "regex";
+
+ @Metadata(description = "Tests whether the left operand does not match the right operand as a regular expression.",
+ label = "binary",
+ examples = { "${header.number} !regex '\\d{4}'" },
+ annotations = { "kind=binary", "syntax=LHS !regex 'pattern'", "precedence=10" })
+ public static final String NOT_REGEX = "!regex";
+
+ @Metadata(description = "Tests whether the left operand is in a set of comma-separated values.",
+ label = "binary",
+ examples = { "${header.type} in 'gold,silver'" },
+ annotations = { "kind=binary", "syntax=LHS in 'val1,val2,...'", "precedence=10" })
+ public static final String IN = "in";
+
+ @Metadata(description = "Tests whether the left operand is not in a set of comma-separated values.",
+ label = "binary",
+ examples = { "${header.type} !in 'gold,silver'" },
+ annotations = { "kind=binary", "syntax=LHS !in 'val1,val2,...'", "precedence=10" })
+ public static final String NOT_IN = "!in";
+
+ @Metadata(description = "Tests whether the left operand is an instance of the right operand type (Java classname or short name).",
+ label = "binary",
+ examples = { "${header.type} is 'String'", "${body} is 'java.util.List'" },
+ annotations = { "kind=binary", "syntax=LHS is 'typeName'", "precedence=10" })
+ public static final String IS = "is";
+
+ @Metadata(description = "Tests whether the left operand is not an instance of the right operand type.",
+ label = "binary",
+ examples = { "${header.type} !is 'String'" },
+ annotations = { "kind=binary", "syntax=LHS !is 'typeName'", "precedence=10" })
+ public static final String NOT_IS = "!is";
+
+ @Metadata(description = "Tests whether the left operand is within the numeric range specified by 'from..to'.",
+ label = "binary",
+ examples = { "${header.number} range '100..199'" },
+ annotations = { "kind=binary", "syntax=LHS range 'from..to'", "precedence=10" })
+ public static final String RANGE = "range";
+
+ @Metadata(description = "Tests whether the left operand is not within the numeric range specified by 'from..to'.",
+ label = "binary",
+ examples = { "${header.number} !range '100..199'" },
+ annotations = { "kind=binary", "syntax=LHS !range 'from..to'", "precedence=10" })
+ public static final String NOT_RANGE = "!range";
+
+ @Metadata(description = "Tests whether the left operand string starts with the right operand string.",
+ label = "binary",
+ examples = { "${header.name} startsWith 'Camel'" },
+ annotations = { "kind=binary", "syntax=LHS startsWith RHS", "precedence=10" })
+ public static final String STARTS_WITH = "startsWith";
+
+ @Metadata(description = "Tests whether the left operand string does not start with the right operand string.",
+ label = "binary",
+ examples = { "${header.name} !startsWith 'Camel'" },
+ annotations = { "kind=binary", "syntax=LHS !startsWith RHS", "precedence=10" })
+ public static final String NOT_STARTS_WITH = "!startsWith";
+
+ @Metadata(description = "Tests whether the left operand string ends with the right operand string.",
+ label = "binary",
+ examples = { "${header.name} endsWith '.xml'" },
+ annotations = { "kind=binary", "syntax=LHS endsWith RHS", "precedence=10" })
+ public static final String ENDS_WITH = "endsWith";
+
+ @Metadata(description = "Tests whether the left operand string does not end with the right operand string.",
+ label = "binary",
+ examples = { "${header.name} !endsWith '.xml'" },
+ annotations = { "kind=binary", "syntax=LHS !endsWith RHS", "precedence=10" })
+ public static final String NOT_ENDS_WITH = "!endsWith";
+
+ // --- Unary operators (precedence 1) ---
+
+ @Metadata(description = "Increments the numeric value by one. Must immediately follow a function closing brace.",
+ label = "unary",
+ examples = { "${header.count}++" },
+ annotations = { "kind=unary", "syntax=${fn}++", "precedence=1" })
+ public static final String INC = "++";
+
+ @Metadata(description = "Decrements the numeric value by one. Must immediately follow a function closing brace.",
+ label = "unary",
+ examples = { "${header.count}--" },
+ annotations = { "kind=unary", "syntax=${fn}--", "precedence=1" })
+ public static final String DEC = "--";
+
+ // --- Logical operators (precedence 30) ---
+
+ @Metadata(description = "Logical AND. Both left and right predicates must evaluate to true.",
+ label = "logical",
+ examples = { "${header.title} contains 'Camel' && ${header.type} == 'gold'" },
+ annotations = { "kind=logical", "syntax=predicate && predicate", "precedence=30" })
+ public static final String AND = "&&";
+
+ @Metadata(description = "Logical OR. At least one of the left or right predicates must evaluate to true.",
+ label = "logical",
+ examples = { "${header.title} contains 'Camel' || ${header.type} == 'gold'" },
+ annotations = { "kind=logical", "syntax=predicate || predicate", "precedence=30" })
+ public static final String OR = "||";
+
+ // --- Ternary operator (precedence 25) ---
+
+ @Metadata(description = "Ternary conditional operator. Evaluates the predicate and returns trueValue if true, falseValue if false. Requires spaces around both ? and : tokens.",
+ label = "ternary",
+ examples = {
+ "${header.foo} > 0 ? 'positive' : 'negative'",
+ "${header.score} >= 90 ? 'A' : ${header.score} >= 80 ? 'B' : 'C'" },
+ annotations = { "kind=ternary", "syntax=predicate ? trueValue : falseValue", "precedence=25" })
+ public static final String TERNARY = "? :";
+
+ // --- Chain operators (precedence 5) ---
+
+ @Metadata(description = "Pipes the result of the left expression as input body to the right expression. Use $param in the right expression to reference the piped value explicitly.",
+ label = "chain",
+ examples = { "${trim()} ~> ${uppercase()}", "${substringAfter('Hello')} ~> ${trim()} ~> ${uppercase()}" },
+ annotations = { "kind=chain", "syntax=expr ~> expr", "precedence=5" })
+ public static final String CHAIN = "~>";
+
+ @Metadata(description = "Null-safe chain operator. Same as ~> but stops chaining and returns null if the left expression evaluates to null.",
+ label = "chain",
+ examples = { "${header.name} ?~> ${trim()} ?~> ${uppercase()}" },
+ annotations = { "kind=chain", "syntax=expr ?~> expr", "precedence=5" })
+ public static final String CHAIN_NULL_SAFE = "?~>";
+
+ // --- Other operators (precedence 20) ---
+
+ @Metadata(description = "Elvis operator (null-coalescing). Returns the left operand if it is not null/empty, otherwise returns the right operand as a fallback value.",
+ label = "other",
+ examples = { "${header.username} ?: 'Guest'", "${body} ?: ${header.default}" },
+ annotations = { "kind=other", "syntax=expr ?: defaultValue", "precedence=20" })
+ public static final String ELVIS = "?:";
+
+ private SimpleOperatorConstants() {
+ }
+}
diff --git a/tooling/camel-tooling-model/src/main/java/org/apache/camel/tooling/model/JsonMapper.java b/tooling/camel-tooling-model/src/main/java/org/apache/camel/tooling/model/JsonMapper.java
index b5759e911672..7d87db4c33c1 100644
--- a/tooling/camel-tooling-model/src/main/java/org/apache/camel/tooling/model/JsonMapper.java
+++ b/tooling/camel-tooling-model/src/main/java/org/apache/camel/tooling/model/JsonMapper.java
@@ -414,6 +414,15 @@ public final class JsonMapper {
model.addFunction(func);
}
}
+ JsonObject mpro = (JsonObject) obj.get("operators");
+ if (mpro != null) {
+ for (Map.Entry<String, Object> entry : mpro.entrySet()) {
+ JsonObject mp = (JsonObject) entry.getValue();
+ LanguageModel.LanguageOperatorModel op = new LanguageModel.LanguageOperatorModel();
+ parseOperator(mp, op, entry.getKey());
+ model.addOperator(op);
+ }
+ }
return model;
}
@@ -436,6 +445,10 @@ public final class JsonMapper {
if (!functions.isEmpty()) {
wrapper.put("functions", asJsonObjectFunctions(functions));
}
+ final List<LanguageModel.LanguageOperatorModel> operators = model.getOperators();
+ if (!operators.isEmpty()) {
+ wrapper.put("operators", asJsonObjectOperators(operators));
+ }
return wrapper;
}
@@ -667,6 +680,37 @@ public final class JsonMapper {
}
}
+ private static void parseOperator(JsonObject mp, LanguageModel.LanguageOperatorModel op, String name) {
+ op.setName(name);
+ op.setConstantName(name);
+ Integer idx = mp.getInteger("index");
+ if (idx != null) {
+ op.setIndex(idx);
+ }
+ op.setKind(mp.getString("kind"));
+ op.setDisplayName(mp.getString("displayName"));
+ op.setGroup(mp.getString("group"));
+ op.setLabel(mp.getString("label"));
+ op.setJavaType(mp.getString("javaType"));
+ op.setDeprecated(mp.getBooleanOrDefault("deprecated", false));
+ op.setDeprecationNote(mp.getString("deprecationNote"));
+ op.setDescription(mp.getString("description"));
+ op.setOperatorKind(mp.getString("operatorKind"));
+ op.setOperatorSyntax(mp.getString("operatorSyntax"));
+ Integer prec = mp.getInteger("precedence");
+ if (prec != null) {
+ op.setPrecedence(prec);
+ }
+ Object examplesObj = mp.get("examples");
+ if (examplesObj instanceof JsonArray examplesArr) {
+ for (Object e : examplesArr) {
+ if (e instanceof String s) {
+ op.addExample(s);
+ }
+ }
+ }
+ }
+
public static JsonObject asJsonObject(List<? extends BaseOptionModel> options) {
JsonObject json = new JsonObject();
for (int i = 0; i < options.size(); i++) {
@@ -717,6 +761,27 @@ public final class JsonMapper {
return json;
}
+ public static JsonObject asJsonObjectOperators(List<LanguageModel.LanguageOperatorModel> options) {
+ JsonObject json = new JsonObject();
+ for (int i = 0; i < options.size(); i++) {
+ var o = options.get(i);
+ o.setIndex(i);
+ JsonObject jo = asJsonObject(o);
+ if (o.getOperatorKind() != null) {
+ jo.put("operatorKind", o.getOperatorKind());
+ }
+ if (o.getOperatorSyntax() != null) {
+ jo.put("operatorSyntax", o.getOperatorSyntax());
+ }
+ jo.put("precedence", o.getPrecedence());
+ if (!o.getExamples().isEmpty()) {
+ jo.put("examples", new JsonArray(o.getExamples()));
+ }
+ json.put(o.getName(), jo);
+ }
+ return json;
+ }
+
public static JsonObject apiModelAsJsonObject(Collection<ApiModel> model, boolean options) {
JsonObject root = new JsonObject();
model.forEach(a -> {
diff --git a/tooling/camel-tooling-model/src/main/java/org/apache/camel/tooling/model/LanguageModel.java b/tooling/camel-tooling-model/src/main/java/org/apache/camel/tooling/model/LanguageModel.java
index e2264dbfafe8..ce1ba4a1e118 100644
--- a/tooling/camel-tooling-model/src/main/java/org/apache/camel/tooling/model/LanguageModel.java
+++ b/tooling/camel-tooling-model/src/main/java/org/apache/camel/tooling/model/LanguageModel.java
@@ -24,6 +24,7 @@ public class LanguageModel extends ArtifactModel<LanguageModel.LanguageOptionMod
protected String modelName;
protected String modelJavaType;
protected final List<LanguageFunctionModel> functions = new ArrayList<>();
+ protected final List<LanguageOperatorModel> operators = new ArrayList<>();
public static class LanguageOptionModel extends BaseOptionModel {
@@ -61,6 +62,14 @@ public class LanguageModel extends ArtifactModel<LanguageModel.LanguageOptionMod
functions.add(function);
}
+ public List<LanguageOperatorModel> getOperators() {
+ return operators;
+ }
+
+ public void addOperator(LanguageOperatorModel operator) {
+ operators.add(operator);
+ }
+
public static class LanguageFunctionModel extends BaseOptionModel {
/**
@@ -144,6 +153,55 @@ public class LanguageModel extends ArtifactModel<LanguageModel.LanguageOptionMod
}
}
+ public static class LanguageOperatorModel extends BaseOptionModel {
+
+ private String constantName;
+ private String operatorKind;
+ private String operatorSyntax;
+ private int precedence;
+ private final List<String> examples = new ArrayList<>();
+
+ public String getConstantName() {
+ return constantName;
+ }
+
+ public void setConstantName(String constantName) {
+ this.constantName = constantName;
+ }
+
+ public String getOperatorKind() {
+ return operatorKind;
+ }
+
+ public void setOperatorKind(String operatorKind) {
+ this.operatorKind = operatorKind;
+ }
+
+ public String getOperatorSyntax() {
+ return operatorSyntax;
+ }
+
+ public void setOperatorSyntax(String operatorSyntax) {
+ this.operatorSyntax = operatorSyntax;
+ }
+
+ public int getPrecedence() {
+ return precedence;
+ }
+
+ public void setPrecedence(int precedence) {
+ this.precedence = precedence;
+ }
+
+ public List<String> getExamples() {
+ return examples;
+ }
+
+ public void addExample(String example) {
+ examples.add(example);
+ }
+ }
+
public static class FunctionParamModel {
private String name;
private String javaType;
diff --git a/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/PackageLanguageMojo.java b/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/PackageLanguageMojo.java
index 00754c6d4c39..7ae51a8bdedc 100644
--- a/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/PackageLanguageMojo.java
+++ b/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/PackageLanguageMojo.java
@@ -286,6 +286,24 @@ public class PackageLanguageMojo extends AbstractGeneratorMojo {
}
}
+ // read class and find operatorsClass and add each field as an operator in the model
+ if (lan.operatorsClass() != void.class) {
+ classElement = loadClass(lan.operatorsClass().getName());
+ if (classElement != null) {
+ for (Field field : classElement.getFields()) {
+ final boolean isEnum = classElement.isEnum();
+ if ((isEnum || isStatic(field.getModifiers()) && field.getType() == String.class)
+ && field.isAnnotationPresent(Metadata.class)) {
+ try {
+ addOperator(model, field);
+ } catch (Exception e) {
+ getLog().warn(e);
+ }
+ }
+ }
+ }
+ }
+
return model;
}
@@ -370,6 +388,42 @@ public class PackageLanguageMojo extends AbstractGeneratorMojo {
model.addFunction(fun);
}
+ private void addOperator(LanguageModel model, Field field) throws Exception {
+ final Metadata metadata = field.getAnnotation(Metadata.class);
+ LanguageModel.LanguageOperatorModel op = new LanguageModel.LanguageOperatorModel();
+ op.setConstantName(String.format("%s#%s", field.getDeclaringClass().getName(), field.getName()));
+ op.setName((String) field.get(null));
+ op.setDescription(metadata.description().trim());
+ op.setKind("operator");
+ String displayName = metadata.displayName();
+ if (Strings.isNullOrEmpty(displayName)) {
+ displayName = Strings.asTitle(field.getName().replace('_', ' ').toLowerCase());
+ }
+ op.setDisplayName(displayName);
+ op.setDeprecated(field.isAnnotationPresent(Deprecated.class));
+ op.setDeprecationNote(metadata.deprecationNote());
+ op.setLabel(metadata.label());
+ if (metadata.examples() != null) {
+ for (String ex : metadata.examples()) {
+ if (!Strings.isNullOrEmpty(ex)) {
+ op.addExample(ex.trim());
+ }
+ }
+ }
+ if (metadata.annotations() != null) {
+ for (String s : metadata.annotations()) {
+ if (s.startsWith("kind=")) {
+ op.setOperatorKind(s.substring(5));
+ } else if (s.startsWith("syntax=")) {
+ op.setOperatorSyntax(s.substring(7));
+ } else if (s.startsWith("precedence=")) {
+ op.setPrecedence(Integer.parseInt(s.substring(11)));
+ }
+ }
+ }
+ model.addOperator(op);
+ }
+
private static LanguageModel.FunctionParamModel parseParam(String value) {
// format: name:javaType:required|optional:defaultValue:description
String[] parts = value.split(":", 5);
diff --git a/tooling/spi-annotations/src/main/java/org/apache/camel/spi/annotations/Language.java b/tooling/spi-annotations/src/main/java/org/apache/camel/spi/annotations/Language.java
index 5dc93f9f4b60..a45a9f2cef2d 100644
--- a/tooling/spi-annotations/src/main/java/org/apache/camel/spi/annotations/Language.java
+++ b/tooling/spi-annotations/src/main/java/org/apache/camel/spi/annotations/Language.java
@@ -43,4 +43,17 @@ public @interface Language {
*/
Class<?> functionsClass() default void.class;
+ /**
+ * The class that contains all the name of operators that are supported by the language. The name of the operators
+ * are defined as {@code String} constants in the operators class.
+ *
+ * The class to provide can be any class but by convention, we would expect a class whose name is of type
+ * <i>xxxOperatorConstants</i> where <i>xxx</i> is the name of the corresponding language like for example
+ * <i>SimpleOperatorConstants</i> for the language <i>camel-simple</i>.
+ *
+ * The metadata of a given operator are retrieved directly from the annotation {@code @Metadata} added to the
+ * {@code String} constant representing its name and defined in the operators class.
+ */
+ Class<?> operatorsClass() default void.class;
+
}