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/09/13 13:26:36 UTC
(camel) branch main updated: CAMEL-24692: camel-catalog - validate simple/groovy with property placeholders as operands (#26349)
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 fb576804a5ae CAMEL-24692: camel-catalog - validate simple/groovy with property placeholders as operands (#26349)
fb576804a5ae is described below
commit fb576804a5ae468393121edfd81dbe4622e98d29
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Sun Sep 13 15:26:29 2026 +0200
CAMEL-24692: camel-catalog - validate simple/groovy with property placeholders as operands (#26349)
AbstractCamelCatalog replaced {{key}} with ~^key^~ before handing the text
to the Simple and Groovy parsers, so placeholders could be parsed without a
running CamelContext. That works when the placeholder sits inside quotes or
free text, but ~^key^~ is an unquoted literal, which the Simple grammar does
not accept as an operand of a binary operator. So valid predicates such as
${body} >= {{hot.threshold}}
${header.level} == {{level}}
{{hot.threshold}}
were rejected, even though the runtime resolves the placeholders before the
predicate is parsed and therefore accepts them. This produced false positives
in the camel:validate Maven goal and in the Camel TUI save-time validation.
Replace a placeholder that is not already inside a quoted literal with
'~key~' instead, which parses as a quoted literal in every position. The
dummy has the same length as the placeholder it replaces, so parser error
positions still map back to the original text.
Also fix the greedy regex, which collapsed several placeholders in one
expression into a single match, both when substituting and when restoring
the placeholders in the error message.
Signed-off-by: Claus Ibsen <cl...@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <no...@anthropic.com>
---
.../org/apache/camel/catalog/CamelCatalogTest.java | 73 +++++++++++++++++
.../camel/catalog/impl/AbstractCamelCatalog.java | 95 ++++++++++++++++++----
2 files changed, 152 insertions(+), 16 deletions(-)
diff --git a/catalog/camel-catalog/src/test/java/org/apache/camel/catalog/CamelCatalogTest.java b/catalog/camel-catalog/src/test/java/org/apache/camel/catalog/CamelCatalogTest.java
index b4ea441b8927..06f7114c03f4 100644
--- a/catalog/camel-catalog/src/test/java/org/apache/camel/catalog/CamelCatalogTest.java
+++ b/catalog/camel-catalog/src/test/java/org/apache/camel/catalog/CamelCatalogTest.java
@@ -1249,6 +1249,64 @@ public class CamelCatalogTest {
assertEquals(0, result.getIndex());
}
+ @Test
+ public void testPredicatePlaceholderAsOperand() {
+ // CAMEL-24692: a placeholder used as a bare operand of a binary operator is valid at runtime,
+ // as the placeholder is resolved before the predicate is parsed
+ LanguageValidationResult result = catalog.validateLanguagePredicate(null, "simple", "${body} >= {{hot.threshold}}");
+ assertTrue(result.isSuccess(), result.getError());
+ assertEquals("${body} >= {{hot.threshold}}", result.getText());
+
+ result = catalog.validateLanguagePredicate(null, "simple", "${header.level} == {{level}}");
+ assertTrue(result.isSuccess(), result.getError());
+
+ result = catalog.validateLanguagePredicate(null, "simple", "${body} in {{list}}");
+ assertTrue(result.isSuccess(), result.getError());
+
+ result = catalog.validateLanguagePredicate(null, "simple", "${body} range {{range}}");
+ assertTrue(result.isSuccess(), result.getError());
+
+ result = catalog.validateLanguagePredicate(null, "simple", "${body} regex {{pattern}}");
+ assertTrue(result.isSuccess(), result.getError());
+
+ // the placeholder can also be the entire predicate
+ result = catalog.validateLanguagePredicate(null, "simple", "{{hot.threshold}}");
+ assertTrue(result.isSuccess(), result.getError());
+ assertEquals("{{hot.threshold}}", result.getText());
+ }
+
+ @Test
+ public void testPredicateMultiplePlaceholders() {
+ // CAMEL-24692: each placeholder must be replaced on its own and not as one greedy match
+ LanguageValidationResult result
+ = catalog.validateLanguagePredicate(null, "simple", "${body} == {{a}} && ${header.x} == {{b}}");
+ assertTrue(result.isSuccess(), result.getError());
+ assertEquals("${body} == {{a}} && ${header.x} == {{b}}", result.getText());
+
+ // mixing a quoted and a bare placeholder
+ result = catalog.validateLanguagePredicate(null, "simple", "${body} == '{{a}}' && ${body} != {{b}}");
+ assertTrue(result.isSuccess(), result.getError());
+
+ // and the placeholders must be restored in the error message
+ result = catalog.validateLanguagePredicate(null, "simple", "${bdy} == {{a}} && ${header.x} == {{b}}");
+ assertFalse(result.isSuccess());
+ assertTrue(result.getError().contains("{{a}}"), result.getError());
+ assertTrue(result.getError().contains("{{b}}"), result.getError());
+ }
+
+ @Test
+ public void testExpressionPlaceholder() {
+ LanguageValidationResult result = catalog.validateLanguageExpression(null, "simple", "{{greeting}}");
+ assertTrue(result.isSuccess(), result.getError());
+ assertEquals("{{greeting}}", result.getText());
+
+ result = catalog.validateLanguageExpression(null, "simple", "Hello {{name}} how are you");
+ assertTrue(result.isSuccess(), result.getError());
+
+ result = catalog.validateLanguageExpression(null, "simple", "${body} and {{suffix}}");
+ assertTrue(result.isSuccess(), result.getError());
+ }
+
@Test
public void testValidateLanguage() {
LanguageValidationResult result = catalog.validateLanguageExpression(null, "simple", "${body}");
@@ -1343,6 +1401,21 @@ public class CamelCatalogTest {
assertEquals("Unexpected input: '*' @ line 2, column 11.", result.getShortError());
}
+ @Test
+ public void testValidateGroovyLanguagePlaceholder() {
+ // CAMEL-24692: the groovy validation uses the same placeholder replacement as simple
+ LanguageValidationResult result
+ = catalog.validateLanguageExpression(null, "groovy", "request.body >= {{hot.threshold}}");
+ assertTrue(result.isSuccess(), result.getError());
+ assertEquals("request.body >= {{hot.threshold}}", result.getText());
+
+ result = catalog.validateLanguageExpression(null, "groovy", "{{hot.threshold}}");
+ assertTrue(result.isSuccess(), result.getError());
+
+ result = catalog.validateLanguageExpression(null, "groovy", "request.body == '{{name}}'");
+ assertTrue(result.isSuccess(), result.getError());
+ }
+
@Test
public void testSpringCamelContext() {
String xml = catalog.springSchemaAsXml();
diff --git a/core/camel-core-catalog/src/main/java/org/apache/camel/catalog/impl/AbstractCamelCatalog.java b/core/camel-core-catalog/src/main/java/org/apache/camel/catalog/impl/AbstractCamelCatalog.java
index 1a6503583fb5..a03a189e4553 100644
--- a/core/camel-core-catalog/src/main/java/org/apache/camel/catalog/impl/AbstractCamelCatalog.java
+++ b/core/camel-core-catalog/src/main/java/org/apache/camel/catalog/impl/AbstractCamelCatalog.java
@@ -1380,16 +1380,85 @@ public abstract class AbstractCamelCatalog {
|| key.startsWith("camel.rest.");
}
+ /**
+ * Replaces property placeholders with a dummy value so the text can be parsed by the language parsers.
+ *
+ * The placeholders cannot be resolved during validation as we do not run the actual Camel application with the
+ * property placeholders setup, and the languages are parsed after the placeholders have been resolved at runtime.
+ *
+ * A placeholder that is already inside a quoted literal is replaced by <tt>{{XXX}}</tt> to <tt>~^XXX^~</tt>, and
+ * otherwise by <tt>{{XXX}}</tt> to <tt>'~XXX~'</tt>. The quotes are needed because the languages do not accept a
+ * bare unquoted literal as operand, such as in <tt>${body} >= {{threshold}}</tt>. Both dummies have the same length
+ * as the placeholder they replace, so the position reported in a parser error still points at the same location in
+ * the original text.
+ *
+ * @param text the text
+ * @return the text with the property placeholders replaced by a dummy value
+ * @see #restorePropertyPlaceholders(String)
+ */
+ private static String dummyPropertyPlaceholders(String text) {
+ if (text == null || !text.contains("{{")) {
+ return text;
+ }
+
+ StringBuilder sb = new StringBuilder(text.length());
+ // the quote character we are currently inside, or 0 when not inside a quoted literal
+ char quote = 0;
+ int i = 0;
+ while (i < text.length()) {
+ char ch = text.charAt(i);
+ if (ch == '\\' && i < text.length() - 1) {
+ // escaped character, so copy as-is
+ sb.append(ch).append(text.charAt(i + 1));
+ i += 2;
+ continue;
+ }
+ if (quote == 0 && (ch == '\'' || ch == '"')) {
+ quote = ch;
+ } else if (quote == ch) {
+ quote = 0;
+ } else if (ch == '{' && i < text.length() - 1 && text.charAt(i + 1) == '{') {
+ int end = text.indexOf("}}", i + 2);
+ if (end != -1) {
+ String key = text.substring(i + 2, end);
+ if (quote != 0 || key.indexOf('\'') != -1) {
+ // already inside a quoted literal, or the key has a single quote that would break the literal
+ sb.append("~^").append(key).append("^~");
+ } else {
+ sb.append("'~").append(key).append("~'");
+ }
+ i = end + 2;
+ continue;
+ }
+ }
+ sb.append(ch);
+ i++;
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Reverses {@link #dummyPropertyPlaceholders(String)} so an error message refers to the property placeholders the
+ * user actually wrote.
+ *
+ * @param text the text
+ * @return the text with the dummy values replaced by the property placeholders
+ */
+ private static String restorePropertyPlaceholders(String text) {
+ if (text == null) {
+ return null;
+ }
+ // reverse ~^XXX^~ first as it may be surrounded by the quotes the other dummy also uses
+ String answer = text.replaceAll("~\\^(.+?)\\^~", "{{$1}}");
+ return answer.replaceAll("'~(.+?)~'", "{{$1}}");
+ }
+
private LanguageValidationResult doValidateSimple(ClassLoader classLoader, String simple, boolean predicate) {
if (classLoader == null) {
classLoader = getClass().getClassLoader();
}
- // if there are {{ }}} property placeholders then we need to resolve them to something else
- // as the simple parse cannot resolve them before parsing as we dont run the actual Camel application
- // with property placeholders setup so we need to dummy this by replace the {{ }} to something else
- // therefore we use a more unlikely character: {{XXX}} to ~^XXX^~
- String resolved = simple.replaceAll("\\{\\{(.+)\\}\\}", "~^$1^~");
+ String resolved = dummyPropertyPlaceholders(simple);
LanguageValidationResult answer = new LanguageValidationResult(simple);
@@ -1426,9 +1495,8 @@ public abstract class AbstractCamelCatalog {
if (cause != null) {
- // reverse ~^XXX^~ back to {{XXX}}
- String errMsg = cause.getMessage();
- errMsg = errMsg.replaceAll("\\~\\^(.+)\\^\\~", "{{$1}}");
+ // reverse the dummy placeholders back to {{XXX}}
+ String errMsg = restorePropertyPlaceholders(cause.getMessage());
answer.setError(errMsg);
@@ -1485,11 +1553,7 @@ public abstract class AbstractCamelCatalog {
classLoader = getClass().getClassLoader();
}
- // if there are {{ }}} property placeholders then we need to resolve them to something else
- // as the simple parse cannot resolve them before parsing as we dont run the actual Camel application
- // with property placeholders setup so we need to dummy this by replace the {{ }} to something else
- // therefore we use a more unlikely character: {{XXX}} to ~^XXX^~
- String resolved = groovy.replaceAll("\\{\\{(.+)\\}\\}", "~^$1^~");
+ String resolved = dummyPropertyPlaceholders(groovy);
LanguageValidationResult answer = new LanguageValidationResult(groovy);
@@ -1526,9 +1590,8 @@ public abstract class AbstractCamelCatalog {
if (cause != null) {
- // reverse ~^XXX^~ back to {{XXX}}
- String errMsg = cause.getMessage();
- errMsg = errMsg.replaceAll("\\~\\^(.+)\\^\\~", "{{$1}}");
+ // reverse the dummy placeholders back to {{XXX}}
+ String errMsg = restorePropertyPlaceholders(cause.getMessage());
answer.setError(errMsg);