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 <[email protected]> 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 <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../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; + }
