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 d27c674ff297 CAMEL-24703: camel-core-languages - Simple parser errors 
say what to write (#26367)
d27c674ff297 is described below

commit d27c674ff2975435c56c4fad06152cc21fd28397
Author: Claus Ibsen <[email protected]>
AuthorDate: Sun Sep 13 16:45:36 2026 +0200

    CAMEL-24703: camel-core-languages - Simple parser errors say what to write 
(#26367)
    
    CAMEL-24703: camel-core-languages - Simple parser errors say what to write 
(functions, operators, OGNL, arithmetic, language-as-function)
    
    Squash of the 10 commits on fix/CAMEL-24703 (the run-by-run history is on 
bench/after-run).
    
    camel-core-languages - simple parser messages say what to write
    camel-bean - OGNL errors name the message body and suggest the closest 
method
    camel-api, camel-bean - a missing language names its dependency, an OGNL 
method without parentheses says so
    camel-core-languages - the file: functions say they describe the consumed 
file and do not read one
    restore generated files rewritten by a partial catalog build in the worktree
    camel-bean - ${body.key} on a Map body says a key is read with ${body[key]}
    camel-bean - 'no matching method could be found' names the bean's methods 
and how to choose one
    camel-core-languages - ${exchangeCounter % 3} is an unknown function that 
says simple has no arithmetic
    camel-bean - a method with several parameters invoked with only the body 
says to pass the arguments in the method name
    camel-core-languages - a language name used as a simple function 
(${simple}, ${groovy(...)}) says what to write
    
    
    Co-authored-by: Claude Fable 5.1 <[email protected]>
---
 .../org/apache/camel/catalog/CamelCatalogTest.java |  20 +-
 .../component/bean/AbstractBeanProcessor.java      |  28 +-
 .../org/apache/camel/component/bean/BeanInfo.java  |  44 +++
 .../apache/camel/component/bean/MethodInfo.java    |  32 ++-
 .../apache/camel/language/bean/BeanExpression.java |  63 ++++-
 .../org/apache/camel/NoSuchLanguageException.java  |   4 +-
 .../camel/language/simple/BaseSimpleParser.java    |  14 +-
 .../language/simple/SimpleExpressionParser.java    |   6 +-
 .../language/simple/SimplePredicateParser.java     |  18 +-
 .../camel/language/simple/SimpleSyntaxHints.java   | 301 +++++++++++++++++++++
 .../simple/ast/SimpleFunctionExpression.java       |  20 +-
 .../simple/functions/BodyFunctionFactory.java      |   7 +-
 .../simple/functions/ExchangeFunctionFactory.java  |   3 +-
 .../bean/BeanNoMatchingMethodHintTest.java         |  77 ++++++
 .../bean/BeanParameterInvalidValueTest.java        |   2 +-
 .../apache/camel/language/FileLanguageTest.java    |   9 +-
 .../apache/camel/language/NoSuchLanguageTest.java  |   3 +-
 .../language/simple/SimpleSyntaxHintsTest.java     | 171 ++++++++++++
 .../apache/camel/language/simple/SimpleTest.java   |   4 +-
 .../camel/management/BacklogDebuggerTest.java      |   2 +-
 20 files changed, 790 insertions(+), 38 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 06f7114c03f4..6dc1999f96f8 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
@@ -1206,15 +1206,16 @@ public class CamelCatalogTest {
         assertFalse(result.isSuccess());
         assertEquals("${body", result.getText());
         LOG.info(result.getError());
-        assertTrue(result.getError().startsWith("expected symbol functionEnd 
but was eol at location 5"));
-        assertEquals("expected symbol functionEnd but was eol", 
result.getShortError());
+        assertTrue(result.getError().startsWith("expected symbol functionEnd 
but was eol"));
+        assertTrue(result.getError().contains("missing } to close the function 
at location 5"));
+        assertTrue(result.getShortError().startsWith("expected symbol 
functionEnd but was eol"));
         assertEquals(5, result.getIndex());
 
         result = catalog.validateLanguageExpression(null, "simple", 
"${bodyxxx}");
         assertFalse(result.isSuccess());
         assertEquals("${bodyxxx}", result.getText());
         LOG.info(result.getError());
-        assertEquals("Valid syntax: ${body.OGNL} was: bodyxxx", 
result.getShortError());
+        assertEquals("Unknown function: bodyxxx (did you mean ${body}?)", 
result.getShortError());
         assertEquals(0, result.getIndex());
     }
 
@@ -1228,8 +1229,9 @@ public class CamelCatalogTest {
         assertFalse(result.isSuccess());
         assertEquals("${body} > ${header.size", result.getText());
         LOG.info(result.getError());
-        assertTrue(result.getError().startsWith("expected symbol functionEnd 
but was eol at location 22"));
-        assertEquals("expected symbol functionEnd but was eol", 
result.getShortError());
+        assertTrue(result.getError().startsWith("expected symbol functionEnd 
but was eol"));
+        assertTrue(result.getError().contains("missing } to close the function 
at location 22"));
+        assertTrue(result.getShortError().startsWith("expected symbol 
functionEnd but was eol"));
         assertEquals(22, result.getIndex());
     }
 
@@ -1243,9 +1245,9 @@ public class CamelCatalogTest {
         assertFalse(result.isSuccess());
         assertEquals("${bdy} contains '{{danger}}'", result.getText());
         LOG.info(result.getError());
-        assertTrue(result.getError().startsWith("Unknown function: bdy at 
location 0"));
+        assertTrue(result.getError().startsWith("Unknown function: bdy (did 
you mean ${body}?) at location 0"));
         assertTrue(result.getError().contains("'{{danger}}'"));
-        assertEquals("Unknown function: bdy", result.getShortError());
+        assertEquals("Unknown function: bdy (did you mean ${body}?)", 
result.getShortError());
         assertEquals(0, result.getIndex());
     }
 
@@ -1331,7 +1333,7 @@ public class CamelCatalogTest {
 
         result = catalog.validateLanguagePredicate(null, "simple", 
"${body.length} =!= 12");
         assertFalse(result.isSuccess());
-        assertEquals("Unexpected token =", result.getShortError());
+        assertTrue(result.getShortError().startsWith("Unknown operator =!=: 
did you mean !=?"), result.getShortError());
 
         result = catalog.validateLanguageExpression(null, "simple", 
"${int:body}");
         assertTrue(result.isSuccess());
@@ -1340,7 +1342,7 @@ public class CamelCatalogTest {
         result = catalog.validateLanguageExpression(null, "simple", 
"${unknown:body}");
         assertFalse(result.isSuccess());
         assertEquals("${unknown:body}", result.getText());
-        assertEquals("Unknown function: unknown:body", result.getShortError());
+        assertTrue(result.getShortError().startsWith("Unknown function: 
unknown:body"), result.getShortError());
     }
 
     @Test
diff --git 
a/components/camel-bean/src/main/java/org/apache/camel/component/bean/AbstractBeanProcessor.java
 
b/components/camel-bean/src/main/java/org/apache/camel/component/bean/AbstractBeanProcessor.java
index 92412328f379..5b909e7a495f 100644
--- 
a/components/camel-bean/src/main/java/org/apache/camel/component/bean/AbstractBeanProcessor.java
+++ 
b/components/camel-bean/src/main/java/org/apache/camel/component/bean/AbstractBeanProcessor.java
@@ -111,7 +111,8 @@ public abstract class AbstractBeanProcessor extends 
AsyncProcessorSupport {
 
         if (invocation == null) {
             exchange.setException(new IllegalStateException(
-                    "No method invocation could be created, no matching method 
could be found on: " + beanInstance));
+                    "No method invocation could be created, no matching method 
could be found on: " + beanInstance
+                                                            + 
methodsHint(beanInfo, explicitMethodName)));
             callback.done(true);
             return true;
         }
@@ -284,4 +285,29 @@ public abstract class AbstractBeanProcessor extends 
AsyncProcessorSupport {
         // fallback and allow using the processor
         return true;
     }
+
+    /**
+     * The public methods of the bean and how to choose one: a bean with 
several methods and no method name cannot be
+     * called, and the message alone did not say what to write.
+     */
+    private static String methodsHint(BeanInfo beanInfo, String 
explicitMethodName) {
+        try {
+            java.util.List<String> names = beanInfo.getMethods().stream()
+                    .map(m -> m.getMethod().getName())
+                    .distinct()
+                    .sorted()
+                    .toList();
+            if (names.isEmpty()) {
+                return " (the bean has no public method to call)";
+            }
+            if (explicitMethodName != null) {
+                return " (method " + explicitMethodName + " was asked for; the 
bean has: " + String.join(", ", names)
+                       + ")";
+            }
+            return " (the bean has the methods " + String.join(", ", names)
+                   + ": name the one to call with method: <name> on the bean 
step, or annotate it with @Handler)";
+        } catch (Exception e) {
+            return "";
+        }
+    }
 }
diff --git 
a/components/camel-bean/src/main/java/org/apache/camel/component/bean/BeanInfo.java
 
b/components/camel-bean/src/main/java/org/apache/camel/component/bean/BeanInfo.java
index 9619d58b9ba1..ee174f94493d 100644
--- 
a/components/camel-bean/src/main/java/org/apache/camel/component/bean/BeanInfo.java
+++ 
b/components/camel-bean/src/main/java/org/apache/camel/component/bean/BeanInfo.java
@@ -220,6 +220,10 @@ public class BeanInfo {
                     methodInfo = evalMethods(pojo, exchange, methodName, 
emptyParameters, name, methods);
                 } else {
                     // a specific method was given to invoke but not found
+                    String closest = closestMethodName(name);
+                    if (closest != null) {
+                        throw new MethodNotFoundException(exchange, pojo, 
methodName, "(did you mean " + closest + "()?)");
+                    }
                     throw new MethodNotFoundException(exchange, pojo, 
methodName);
                 }
             }
@@ -264,6 +268,46 @@ public class BeanInfo {
         return methodInfo;
     }
 
+    /**
+     * The public method name of the bean closest to the given one (a typo 
such as lenght for length), or null.
+     */
+    private String closestMethodName(String name) {
+        if (name == null || name.length() < 3) {
+            return null;
+        }
+        String best = null;
+        int bestDistance = Integer.MAX_VALUE;
+        int threshold = Math.max(2, name.length() / 3);
+        String lower = name.toLowerCase(Locale.ROOT);
+        for (String candidate : operations.keySet()) {
+            int d = editDistance(lower, candidate.toLowerCase(Locale.ROOT));
+            if (d > 0 && d <= threshold && (d < bestDistance || d == 
bestDistance && candidate.length() < best.length())) {
+                best = candidate;
+                bestDistance = d;
+            }
+        }
+        return best;
+    }
+
+    private static int editDistance(String a, String b) {
+        int[] prev = new int[b.length() + 1];
+        int[] cur = new int[b.length() + 1];
+        for (int j = 0; j <= b.length(); j++) {
+            prev[j] = j;
+        }
+        for (int i = 1; i <= a.length(); i++) {
+            cur[0] = i;
+            for (int j = 1; j <= b.length(); j++) {
+                int cost = a.charAt(i - 1) == b.charAt(j - 1) ? 0 : 1;
+                cur[j] = Math.min(Math.min(cur[j - 1] + 1, prev[j] + 1), 
prev[j - 1] + cost);
+            }
+            int[] t = prev;
+            prev = cur;
+            cur = t;
+        }
+        return prev[b.length()];
+    }
+
     private static MethodInfo createSingleMethodInvocation(
             Object pojo, Exchange exchange, List<MethodInfo> methods, boolean 
emptyParameters, String methodName) {
         MethodInfo methodInfo;
diff --git 
a/components/camel-bean/src/main/java/org/apache/camel/component/bean/MethodInfo.java
 
b/components/camel-bean/src/main/java/org/apache/camel/component/bean/MethodInfo.java
index 0a5d009284cb..a3aac1320046 100644
--- 
a/components/camel-bean/src/main/java/org/apache/camel/component/bean/MethodInfo.java
+++ 
b/components/camel-bean/src/main/java/org/apache/camel/component/bean/MethodInfo.java
@@ -500,11 +500,41 @@ public class MethodInfo {
                     exchange, e);
         } catch (IllegalArgumentException e) {
             throw new RuntimeExchangeException(
-                    "IllegalArgumentException occurred invoking method: " + 
mth + " using arguments: " + asList(arguments),
+                    "IllegalArgumentException occurred invoking method: " + 
mth + " using arguments: " + asList(arguments)
+                                               + argumentsHint(mth, arguments),
                     exchange, e);
         }
     }
 
+    /**
+     * A method with several parameters gets the body as the first and nothing 
for the others: say that the arguments
+     * are written in the method name.
+     */
+    private static String argumentsHint(Method mth, Object[] arguments) {
+        Class<?>[] types = mth.getParameterTypes();
+        if (types.length < 2 || arguments == null) {
+            return "";
+        }
+        boolean nullForPrimitive = false;
+        for (int i = 0; i < types.length && i < arguments.length; i++) {
+            if (arguments[i] == null && types[i].isPrimitive()) {
+                nullForPrimitive = true;
+            }
+        }
+        if (!nullForPrimitive && arguments.length == types.length) {
+            return "";
+        }
+        StringBuilder sb = new StringBuilder(" (the method takes 
").append(types.length)
+                .append(" parameters and only the message body is bound by 
default: pass them in the method name, for"
+                        + " example method: \"")
+                .append(mth.getName()).append("(${body}");
+        for (int i = 1; i < types.length; i++) {
+            sb.append(", ").append(types[i].isPrimitive() || 
Number.class.isAssignableFrom(types[i]) ? "1" : "'value'");
+        }
+        sb.append(")\"; ${header.name} and ${exchangeProperty.name} work 
too)");
+        return sb.toString();
+    }
+
     protected Expression[] createParameterExpressions() {
         final int size = parameters.size();
         LOG.trace("Creating parameters expression for {} parameters", size);
diff --git 
a/components/camel-bean/src/main/java/org/apache/camel/language/bean/BeanExpression.java
 
b/components/camel-bean/src/main/java/org/apache/camel/language/bean/BeanExpression.java
index c30cb51cc795..7b517069b805 100644
--- 
a/components/camel-bean/src/main/java/org/apache/camel/language/bean/BeanExpression.java
+++ 
b/components/camel-bean/src/main/java/org/apache/camel/language/bean/BeanExpression.java
@@ -384,12 +384,48 @@ public class BeanExpression implements Expression, 
Predicate {
                 exchange.setException(resultExchange.getException());
             }
         } catch (Exception e) {
-            throw new RuntimeBeanExpressionException(exchange, beanName, 
methodName, e);
+            throw new RuntimeBeanExpressionException(
+                    exchange, describeBean(beanHolder, beanName, exchange), 
methodHint(methodName, e), e);
         }
 
         return result;
     }
 
+    /**
+     * ${body.toUpperCase} without parentheses binds the body as the parameter 
of the one argument overload, which fails
+     * on the conversion; say that a method call needs parentheses.
+     */
+    private static String methodHint(String methodName, Throwable e) {
+        if (methodName != null && !methodName.contains("(") && 
!methodName.contains("[")) {
+            for (Throwable t = e; t != null; t = t.getCause()) {
+                String n = t.getClass().getSimpleName();
+                if (n.equals("NoTypeConversionAvailableException") || 
n.equals("InvalidPayloadException")) {
+                    return methodName + " (a method call needs parentheses: " 
+ methodName + "())";
+                }
+            }
+        }
+        return methodName;
+    }
+
+    /**
+     * The bean name, or for an OGNL on the message body (${body.foo()}) what 
the body is, so the message does not say
+     * "on null".
+     */
+    private static String describeBean(BeanHolder beanHolder, String beanName, 
Exchange exchange) {
+        if (beanName != null) {
+            return beanName;
+        }
+        try {
+            Object bean = beanHolder != null ? beanHolder.getBean(exchange) : 
null;
+            if (bean != null) {
+                return "the message body of type " + 
ObjectHelper.className(bean);
+            }
+        } catch (Exception e) {
+            // ignore
+        }
+        return "the message body";
+    }
+
     /**
      * To invoke a bean using a OGNL notation which denotes the chain of 
methods to invoke.
      * <p/>
@@ -491,11 +527,34 @@ public class BeanExpression implements Expression, 
Predicate {
         }
         Object newResult = invokeBean(holder, beanName, methodName, 
resultExchange);
         if (resultExchange.getException() != null) {
-            throw new RuntimeBeanExpressionException(exchange, beanName, 
methodName, resultExchange.getException());
+            throw new RuntimeBeanExpressionException(
+                    exchange, describeBean(holder, beanName, exchange),
+                    keyHint(holder, exchange, methodName, 
methodHint(methodName, resultExchange.getException())),
+                    resultExchange.getException());
         }
         return newResult;
     }
 
+    /**
+     * ${body.type} on a Map body looks for a method named type; a key is read 
with ${body[type]}. Say so when the bean
+     * is a Map and the name is not a method call.
+     */
+    private static String keyHint(BeanHolder holder, Exchange exchange, String 
methodName, String hint) {
+        if (methodName == null || methodName.contains("(") || 
methodName.contains("[")) {
+            return hint;
+        }
+        try {
+            Object bean = holder != null ? holder.getBean(exchange) : null;
+            if (bean instanceof Map) {
+                return hint + " (the value is a Map: a key is read with [" + 
methodName + "], as in ${body[" + methodName
+                       + "]}, not with ." + methodName + ")";
+            }
+        } catch (Exception e) {
+            // ignore
+        }
+        return hint;
+    }
+
     private Object lookupByKeyIfPresent(
             String key, Object result, Exchange resultExchange, Exchange 
exchange,
             boolean nullSafe, String ognlPath, BeanHolder holder) {
diff --git 
a/core/camel-api/src/main/java/org/apache/camel/NoSuchLanguageException.java 
b/core/camel-api/src/main/java/org/apache/camel/NoSuchLanguageException.java
index fcdfcfe6b2e8..846593469527 100644
--- a/core/camel-api/src/main/java/org/apache/camel/NoSuchLanguageException.java
+++ b/core/camel-api/src/main/java/org/apache/camel/NoSuchLanguageException.java
@@ -31,7 +31,9 @@ public class NoSuchLanguageException extends 
RuntimeCamelException {
      * @param language the language name that could not be resolved
      */
     public NoSuchLanguageException(String language) {
-        super("No language could be found for: " + 
Objects.requireNonNull(language, "language"));
+        super("No language could be found for: " + 
Objects.requireNonNull(language, "language")
+              + (language.matches("[a-z0-9-]+")
+                      ? " (a Camel language needs its dependency on the 
classpath, e.g. camel-" + language + ")" : ""));
         this.language = language;
     }
 
diff --git 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/BaseSimpleParser.java
 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/BaseSimpleParser.java
index cd16855e5ae8..2b4bc0c726fc 100644
--- 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/BaseSimpleParser.java
+++ 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/BaseSimpleParser.java
@@ -469,11 +469,23 @@ public abstract class BaseSimpleParser {
             return;
         } else if (token == null) {
             // use the previous index as that is where the problem is
+            if (expect == TokenType.functionEnd) {
+                throw new SimpleParserException(
+                        "expected symbol " + expect + " but reached eol: 
missing } to close the function", previousIndex);
+            }
             throw new SimpleParserException("expected symbol " + expect + " 
but reached eol", previousIndex);
         } else {
             // use the previous index as that is where the problem is
+            String hint = "";
+            if (expect == TokenType.functionEnd && token.getType().isEol()) {
+                hint = ": missing } to close the function";
+            } else if (expect == TokenType.whiteSpace && 
token.getType().getType() == TokenType.singleQuote) {
+                hint = ": a literal that contains a single quote must be 
written with double quotes, e.g. \"it's\"";
+            } else if (expect == TokenType.whiteSpace) {
+                hint = ": operators and values must be separated by a space";
+            }
             throw new SimpleParserException(
-                    "expected symbol " + expect + " but was " + 
token.getType().getType(), previousIndex);
+                    "expected symbol " + expect + " but was " + 
token.getType().getType() + hint, previousIndex);
         }
     }
 
diff --git 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleExpressionParser.java
 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleExpressionParser.java
index 848e89f73465..eacaf07a7e8e 100644
--- 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleExpressionParser.java
+++ 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleExpressionParser.java
@@ -400,7 +400,8 @@ public class SimpleExpressionParser extends 
BaseSimpleParser {
                 }
             } else {
                 throw new SimpleParserException(
-                        "Other operator " + operatorType + " does not support 
token " + token, token.getIndex());
+                        SimpleSyntaxHints.unsupportedOperand("Other", 
operatorType, expression, token.getIndex()),
+                        token.getIndex());
             }
             return true;
         }
@@ -430,7 +431,8 @@ public class SimpleExpressionParser extends 
BaseSimpleParser {
                 }
             } else {
                 throw new SimpleParserException(
-                        "Chain operator " + operatorType + " does not support 
token " + token, token.getIndex());
+                        SimpleSyntaxHints.unsupportedOperand("Chain", 
operatorType, expression, token.getIndex()),
+                        token.getIndex());
             }
             return true;
         }
diff --git 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimplePredicateParser.java
 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimplePredicateParser.java
index 430874fc02d1..6b6ed4acdc08 100644
--- 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimplePredicateParser.java
+++ 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimplePredicateParser.java
@@ -147,7 +147,7 @@ public class SimplePredicateParser extends BaseSimpleParser 
{
                     && !token.getType().isEol()) {
                 // okay the symbol was not one of the above, so its not 
supported
                 // use the previous index as that is where the problem is
-                throw new SimpleParserException("Unexpected token " + token, 
previousIndex);
+                throw new 
SimpleParserException(SimpleSyntaxHints.unexpectedToken(expression, 
previousIndex), previousIndex);
             }
             // take the next token
             nextToken();
@@ -270,7 +270,7 @@ public class SimplePredicateParser extends BaseSimpleParser 
{
         if (startFunction.get()) {
             // we have a start function, but no ending function
             int index = evalIndex(lastFunction);
-            throw new SimpleParserException("function has no ending token", 
index);
+            throw new SimpleParserException("function has no ending token: 
missing } to close ${...}", index);
         }
     }
 
@@ -739,7 +739,8 @@ public class SimplePredicateParser extends BaseSimpleParser 
{
                 }
             } else {
                 throw new SimpleParserException(
-                        "Binary operator " + operatorType + " does not support 
token " + token, token.getIndex());
+                        SimpleSyntaxHints.unsupportedOperand("Binary", 
operatorType, expression, token.getIndex()),
+                        token.getIndex());
             }
             return true;
         }
@@ -766,7 +767,7 @@ public class SimplePredicateParser extends BaseSimpleParser 
{
                 }
             } else {
                 throw new SimpleParserException(
-                        "Ternary operator does not support token " + token, 
token.getIndex());
+                        SimpleSyntaxHints.unsupportedOperand("Ternary", "?:", 
expression, token.getIndex()), token.getIndex());
             }
             return true;
         }
@@ -796,7 +797,8 @@ public class SimplePredicateParser extends BaseSimpleParser 
{
                 }
             } else {
                 throw new SimpleParserException(
-                        "Other operator " + operatorType + " does not support 
token " + token, token.getIndex());
+                        SimpleSyntaxHints.unsupportedOperand("Other", 
operatorType, expression, token.getIndex()),
+                        token.getIndex());
             }
             return true;
         }
@@ -826,7 +828,8 @@ public class SimplePredicateParser extends BaseSimpleParser 
{
                 }
             } else {
                 throw new SimpleParserException(
-                        "Chain operator " + operatorType + " does not support 
token " + token, token.getIndex());
+                        SimpleSyntaxHints.unsupportedOperand("Chain", 
operatorType, expression, token.getIndex()),
+                        token.getIndex());
             }
             return true;
         }
@@ -856,7 +859,8 @@ public class SimplePredicateParser extends BaseSimpleParser 
{
                 }
             } else {
                 throw new SimpleParserException(
-                        "Logical operator " + operatorType + " does not 
support token " + token, token.getIndex());
+                        SimpleSyntaxHints.unsupportedOperand("Logical", 
operatorType, expression, token.getIndex()),
+                        token.getIndex());
             }
             return true;
         }
diff --git 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleSyntaxHints.java
 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleSyntaxHints.java
new file mode 100644
index 000000000000..e771f59f6bab
--- /dev/null
+++ 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleSyntaxHints.java
@@ -0,0 +1,301 @@
+/*
+ * 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 java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * Hints that turn a Simple parser error into a message that says what to 
write (CAMEL-24703). Every consumer of the
+ * parser (routes at startup, the catalog validator, camel validate, the TUI 
and the MCP tools) shows the message, so a
+ * hint here reaches humans and AI agents alike.
+ */
+public final class SimpleSyntaxHints {
+
+    public static final String OPERATORS = "==, =~, !=, <, <=, >, >=, 
contains, !contains, ~~, !~~, regex, !regex, in, !in, "
+                                           + "is, !is, range, !range, 
startsWith, endsWith, && and ||";
+
+    /** The value forms an operator accepts on its right hand side. */
+    public static final String VALUE_FORMS = "a quoted literal 'x', a number, 
true, false, null, or a function ${...}";
+
+    /** Function names a model is likely to write, for the did-you-mean 
suggestion. */
+    static final List<String> FUNCTIONS = List.of("body", "bodyAs", 
"mandatoryBodyAs", "bodyOneLine", "prettyBody",
+            "originalBody", "header", "headerAs", "headers", 
"exchangeProperty", "exchangePropertyAs",
+            "exchangeProperties", "variable", "variableAs", "variables", 
"exception", "exchange", "camelContext",
+            "camelId", "routeId", "routeGroup", "stepId", "id", 
"messageTimestamp", "threadName", "threadId",
+            "hostname", "date", "date-with-timezone", "random", "skip", 
"collate", "join", "sum", "avg", "min", "max",
+            "replace", "substring", "substringBefore", "substringAfter", 
"substringBetween", "contains", "pad",
+            "concat", "val", "length", "empty", "newEmpty", "iif", "hash", 
"convertTo", "throwException", "assert",
+            "load", "uuid", "env", "sys", "ref", "bean", "properties", 
"propertiesExist", "type", "messageAs",
+            "messageHistory", "pretty", "toJson", "toPrettyJson", "jq", 
"jsonpath", "xpath", "simpleJsonpath",
+            "function", "list", "map", "range", "split", "sort", "forEach", 
"filter", "listAdd", "listRemove",
+            "mapAdd", "mapRemove", "file", "null");
+
+    /** Names from older Camel versions or other languages that a model still 
writes. */
+    static final Map<String, String> ALIASES = Map.ofEntries(
+            Map.entry("property", "exchangeProperty"),
+            Map.entry("in.body", "body"),
+            Map.entry("out.body", "body"),
+            Map.entry("in.header", "header"),
+            Map.entry("out.header", "header"),
+            Map.entry("in.headers", "headers"),
+            Map.entry("var", "variable"),
+            Map.entry("prop", "exchangeProperty"),
+            Map.entry("json", "jsonpath"),
+            Map.entry("upper", "bodyAs(String).toUpperCase()"),
+            Map.entry("lower", "bodyAs(String).toLowerCase()"),
+            Map.entry("trim", "bodyAs(String).trim()"),
+            Map.entry("padding", "pad"),
+            Map.entry("now", "date:now:yyyy-MM-dd'T'HH:mm:ss"),
+            Map.entry("timestamp", "messageTimestamp"),
+            Map.entry("size", "length"),
+            Map.entry("count", "length"));
+
+    private static final String[] OPERATOR_WORDS = {
+            "==", "!=", ">=", "<=", ">", "<", "=~", "!=~", "~~", "!~~", 
"contains",
+            "!contains", "regex", "!regex", "in", "!in", "is", "!is", "range", 
"!range", "startsWith", "endsWith", "&&", "||" };
+
+    private SimpleSyntaxHints() {
+    }
+
+    /** The whole word (up to whitespace on both sides) around the index in 
the expression. */
+    public static String wordAt(String expression, int index) {
+        if (expression == null || expression.isEmpty()) {
+            return "";
+        }
+        int start = Math.min(Math.max(index, 0), expression.length());
+        int end = start;
+        while (start > 0 && !Character.isWhitespace(expression.charAt(start - 
1))) {
+            start--;
+        }
+        while (end < expression.length() && 
!Character.isWhitespace(expression.charAt(end))) {
+            end++;
+        }
+        return expression.substring(start, end);
+    }
+
+    /** The message for a token the grammar does not know at the given index. 
*/
+    public static String unexpectedToken(String expression, int index) {
+        String word = wordAt(expression, index);
+        if (word.isEmpty()) {
+            return "Unexpected token at location " + index;
+        }
+        String lower = word.toLowerCase(Locale.ROOT);
+        switch (lower) {
+            case "=":
+            case "===":
+            case "=<":
+            case "=>":
+                return "Unknown operator " + word + ": did you mean ==? 
Operators are " + OPERATORS;
+            case "<>":
+            case "=!=":
+            case "!==":
+            case "~=":
+                return "Unknown operator " + word + ": did you mean !=? 
Operators are " + OPERATORS;
+            case "and":
+            case "&":
+                return "Unknown operator " + word + ": use && for and, and || 
for or";
+            case "or":
+            case "|":
+                return "Unknown operator " + word + ": use || for or, and && 
for and";
+            case "not":
+            case "!":
+                return "Unknown operator " + word + ": negate the operator 
instead, e.g. != or !contains";
+            default:
+        }
+        String name = functionName(word);
+        if (isKnownFunction(name)) {
+            String rewrite = expression.replace(word, "${" + word + "}");
+            return "Unexpected token " + word + ": text outside ${...} is a 
literal, functions are written as ${body}, "
+                   + "${header.name}; did you mean " + rewrite + "?";
+        }
+        return "Unexpected token " + word + ": text outside ${...} must be " + 
VALUE_FORMS + ", or an operator ("
+               + OPERATORS + ")";
+    }
+
+    /** The message when an operator has no usable value next to it. */
+    public static String unsupportedOperand(String kind, Object operator, 
String expression, int index) {
+        String word = wordAt(expression, index);
+        if ("Logical".equals(kind)) {
+            return kind + " operator " + operator + " needs a predicate on the 
right hand side, e.g. ${header.foo} == 'bar'"
+                   + (word.isEmpty() ? "" : "; was: " + word);
+        }
+        if (word.isEmpty() || word.equals(String.valueOf(operator))) {
+            return kind + " operator " + operator + " needs a value on the 
right hand side: " + VALUE_FORMS;
+        }
+        return kind + " operator " + operator + " does not accept " + word + " 
on the right hand side: write it as "
+               + VALUE_FORMS + (isKnownFunction(functionName(word)) ? ", e.g. 
${" + word + "}" : ", e.g. '" + word + "'");
+    }
+
+    /**
+     * When an operator is written inside the function (${body == 'x'}), the 
rewrite with the operator outside, else
+     * null.
+     */
+    public static String operatorsOutside(String function) {
+        int best = -1;
+        String bestOp = null;
+        for (String op : OPERATOR_WORDS) {
+            int i = function.indexOf(" " + op + " ");
+            if (i > 0 && (best < 0 || i < best)) {
+                best = i;
+                bestOp = op;
+            }
+        }
+        if (bestOp == null) {
+            return null;
+        }
+        String head = function.substring(0, best);
+        if (head.contains("'") || head.contains("\"")) {
+            return null;
+        }
+        int open = 0;
+        for (int i = 0; i < head.length(); i++) {
+            if (head.charAt(i) == '(') {
+                open++;
+            } else if (head.charAt(i) == ')') {
+                open--;
+            }
+        }
+        if (open > 0) {
+            // the operator is inside an argument list that may hold a 
predicate (iif, filter, forEach)
+            return null;
+        }
+        return "${" + head + "}" + function.substring(best);
+    }
+
+    /** A hint for an unknown function, or null. */
+    public static String unknownFunction(String function) {
+        if (function == null) {
+            return null;
+        }
+        String trimmed = function.trim();
+        if (!trimmed.equals(function)) {
+            return "remove the spaces: ${" + trimmed + "}";
+        }
+        String rewrite = operatorsOutside(function);
+        if (rewrite != null) {
+            return "operators go outside the function: " + rewrite;
+        }
+        String bare = function.contains("(") ? function.substring(0, 
function.indexOf('(')) : function;
+        bare = bare.contains(".") ? bare.substring(0, bare.indexOf('.')) : 
bare;
+        if (bare.equals("simple")) {
+            // ${simple} in a log message or a simple: expression: the text is 
already simple
+            return "simple is the language, not a function: the text is 
already a simple expression, write the values"
+                   + " with ${body}, ${header.name}, ${date:now:HH:mm:ss} and 
leave the rest as plain text";
+        }
+        if 
(bare.matches("groovy|jsonpath|xpath|xquery|jq|mvel|ognl|spel|js|python|java|constant|tokenize|method"))
 {
+            return bare + " is a language, not a simple function: another 
language cannot be nested inside ${...}; write"
+                   + " the expression with its own key, for example " + bare + 
": \"...\"";
+        }
+        if (function.matches(".*\\S\\s+[-+*/%]\\s+\\S.*")) {
+            // ${exchangeCounter % 3}, ${header.total * 2}: there is no 
arithmetic in simple
+            return "simple has no arithmetic operators (+ - * / %): compute 
the value in a groovy expression"
+                   + " (groovy: \"...\") or in a bean, and use the result 
here";
+        }
+        String name = functionName(function);
+        String rest = function.substring(name.length());
+        String alias = ALIASES.get(name);
+        if (alias == null && name.contains(".")) {
+            alias = ALIASES.get(name.substring(0, name.indexOf('.')));
+            if (alias != null) {
+                alias = alias + name.substring(name.indexOf('.'));
+            }
+        }
+        if (alias != null) {
+            return "did you mean ${" + alias + rest + "}?";
+        }
+        for (String f : FUNCTIONS) {
+            if (f.equalsIgnoreCase(name) && !f.equals(name)) {
+                return "function names are case sensitive: ${" + f + rest + 
"}";
+            }
+        }
+        String closest = closest(name);
+        if (closest != null) {
+            return "did you mean ${" + closest + rest + "}?";
+        }
+        return "the functions are documented on the simple language page 
(functions)";
+    }
+
+    static String functionName(String function) {
+        int end = 0;
+        while (end < function.length()) {
+            char ch = function.charAt(end);
+            if (ch == '(' || ch == ':' || ch == '[' || ch == '?' || 
Character.isWhitespace(ch)) {
+                break;
+            }
+            if (ch == '.' && !function.startsWith("in.", 0) && 
!function.startsWith("out.", 0)
+                    && !function.startsWith("date-with", 0)) {
+                // header.foo and exchangeProperty.foo: the name is the part 
before the dot; in.body keeps its dot
+                break;
+            }
+            end++;
+        }
+        return function.substring(0, end);
+    }
+
+    static boolean isKnownFunction(String name) {
+        return !name.isEmpty() && (FUNCTIONS.contains(name) || 
ALIASES.containsKey(name));
+    }
+
+    static String closest(String name) {
+        if (name.length() < 3) {
+            return null;
+        }
+        // bodyxxx, headerz: a known function name followed by something that 
is not OGNL
+        String prefix = null;
+        for (String f : FUNCTIONS) {
+            if (f.length() >= 3 && name.startsWith(f) && (prefix == null || 
f.length() > prefix.length())) {
+                prefix = f;
+            }
+        }
+        if (prefix != null) {
+            return prefix;
+        }
+        String best = null;
+        int bestDistance = Integer.MAX_VALUE;
+        int threshold = Math.max(2, name.length() / 3);
+        String lower = name.toLowerCase(Locale.ROOT);
+        for (String f : FUNCTIONS) {
+            int d = distance(lower, f.toLowerCase(Locale.ROOT));
+            if (d <= threshold && (d < bestDistance || d == bestDistance && 
f.length() < best.length())) {
+                best = f;
+                bestDistance = d;
+            }
+        }
+        return best;
+    }
+
+    static int distance(String a, String b) {
+        int[] prev = new int[b.length() + 1];
+        int[] cur = new int[b.length() + 1];
+        for (int j = 0; j <= b.length(); j++) {
+            prev[j] = j;
+        }
+        for (int i = 1; i <= a.length(); i++) {
+            cur[0] = i;
+            for (int j = 1; j <= b.length(); j++) {
+                int cost = a.charAt(i - 1) == b.charAt(j - 1) ? 0 : 1;
+                cur[j] = Math.min(Math.min(cur[j - 1] + 1, prev[j] + 1), 
prev[j - 1] + cost);
+            }
+            int[] t = prev;
+            prev = cur;
+            cur = t;
+        }
+        return prev[b.length()];
+    }
+}
diff --git 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/SimpleFunctionExpression.java
 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/SimpleFunctionExpression.java
index 9c36cfcd4093..c120eabf3082 100644
--- 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/SimpleFunctionExpression.java
+++ 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/SimpleFunctionExpression.java
@@ -23,6 +23,7 @@ import org.apache.camel.Expression;
 import org.apache.camel.language.simple.FileExpressionBuilder;
 import org.apache.camel.language.simple.SimpleFunctionDispatcher;
 import org.apache.camel.language.simple.SimpleFunctionHelper;
+import org.apache.camel.language.simple.SimpleSyntaxHints;
 import org.apache.camel.language.simple.functions.DirectFunctionFactory;
 import org.apache.camel.language.simple.types.SimpleParserException;
 import org.apache.camel.language.simple.types.SimpleToken;
@@ -98,6 +99,13 @@ public class SimpleFunctionExpression extends 
LiteralExpression {
     private static final DirectFunctionFactory DIRECT_FACTORY = new 
DirectFunctionFactory();
 
     private Expression doCreateSimpleExpression(CamelContext camelContext, 
String function, boolean strict) {
+        if (strict) {
+            // ${body == 'x'}: the operator belongs outside the function 
(CAMEL-24703)
+            String rewrite = SimpleSyntaxHints.operatorsOutside(function);
+            if (rewrite != null) {
+                throw new SimpleParserException("Operators go outside the 
function: " + rewrite, token.getIndex());
+            }
+        }
         // return the function directly if we can create function without 
analyzing the prefix
         Expression answer = DIRECT_FACTORY.createFunction(camelContext, 
function, token.getIndex());
         if (answer != null) {
@@ -147,7 +155,10 @@ public class SimpleFunctionExpression extends 
LiteralExpression {
         }
 
         if (strict) {
-            throw new SimpleParserException("Unknown function: " + function, 
token.getIndex());
+            String hint = SimpleSyntaxHints.unknownFunction(function);
+            throw new SimpleParserException(
+                    "Unknown function: " + function + (hint != null ? " (" + 
hint + ")" : ""),
+                    token.getIndex());
         } else {
             return null;
         }
@@ -184,7 +195,12 @@ public class SimpleFunctionExpression extends 
LiteralExpression {
             return FileExpressionBuilder.fileLastModifiedExpression();
         }
         if (strict) {
-            throw new SimpleParserException("Unknown file language syntax: " + 
remainder, token.getIndex());
+            throw new SimpleParserException(
+                    "Unknown file language syntax: " + remainder + " (the 
file: functions describe the file being consumed:"
+                                            + " ${file:name}, ${file:size}, 
${file:parent}, ${file:absolute.path};"
+                                            + " they do not read a file. To 
read a file into the body use the poll"
+                                            + " EIP with a file: endpoint)",
+                    token.getIndex());
         }
         return null;
     }
diff --git 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/BodyFunctionFactory.java
 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/BodyFunctionFactory.java
index b826f2ef4ff8..ad267e78d056 100644
--- 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/BodyFunctionFactory.java
+++ 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/BodyFunctionFactory.java
@@ -100,8 +100,11 @@ public final class BodyFunctionFactory implements 
SimpleLanguageFunctionFactory
         }
         if (remainder != null) {
             boolean ognlStart = remainder.startsWith(".") || 
remainder.startsWith("?") || remainder.startsWith("[");
-            boolean invalid = !ognlStart || 
OgnlHelper.isInvalidValidOgnlExpression(remainder);
-            if (invalid) {
+            if (!ognlStart) {
+                // bodyy or bodyX: not a body OGNL at all, let the caller 
report an unknown function with a hint
+                return null;
+            }
+            if (OgnlHelper.isInvalidValidOgnlExpression(remainder)) {
                 throw new SimpleParserException("Valid syntax: ${body.OGNL} 
was: " + function, index);
             }
             return OgnlExpressionBuilder.bodyOgnlExpression(remainder);
diff --git 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/ExchangeFunctionFactory.java
 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/ExchangeFunctionFactory.java
index a3a78028b0a1..6b2cdd98bdcd 100644
--- 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/ExchangeFunctionFactory.java
+++ 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/functions/ExchangeFunctionFactory.java
@@ -82,7 +82,8 @@ public final class ExchangeFunctionFactory implements 
SimpleLanguageFunctionFact
 
         // exchange OGNL
         remainder = ifStartsWithReturnRemainder("exchange", function);
-        if (remainder != null) {
+        if (remainder != null && (remainder.startsWith(".") || 
remainder.startsWith("?") || remainder.startsWith("["))) {
+            // only ${exchange.OGNL}: ${exchangeCounter} is not an exchange 
OGNL but an unknown function
             boolean invalid = 
OgnlHelper.isInvalidValidOgnlExpression(remainder);
             if (invalid) {
                 throw new SimpleParserException("Valid syntax: 
${exchange.OGNL} was: " + function, index);
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/component/bean/BeanNoMatchingMethodHintTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/component/bean/BeanNoMatchingMethodHintTest.java
new file mode 100644
index 000000000000..093c91d9fa77
--- /dev/null
+++ 
b/core/camel-core/src/test/java/org/apache/camel/component/bean/BeanNoMatchingMethodHintTest.java
@@ -0,0 +1,77 @@
+/*
+ * 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.component.bean;
+
+import org.apache.camel.CamelExecutionException;
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.builder.RouteBuilder;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * CAMEL-24703: a bean with several methods and no method name must say which 
methods it has and how to choose one.
+ */
+public class BeanNoMatchingMethodHintTest extends ContextTestSupport {
+
+    @Test
+    public void testHintNamesTheMethods() {
+        CamelExecutionException e = assertThrows(CamelExecutionException.class,
+                () -> template.sendBody("direct:start", "hi"));
+        String msg = e.getCause().getMessage();
+        assertTrue(msg.contains("No method invocation could be created"), msg);
+        assertTrue(msg.contains("the bean has the methods addObjects, 
getLeakedObjectCount"), msg);
+        assertTrue(msg.contains("method: <name>"), msg);
+    }
+
+    @Test
+    public void testSeveralParametersHint() {
+        CamelExecutionException e = assertThrows(CamelExecutionException.class,
+                () -> template.sendBody("direct:two", "hi"));
+        String msg = e.getCause().getMessage();
+        assertTrue(msg.contains("the method takes 2 parameters and only the 
message body is bound by default"), msg);
+        assertTrue(msg.contains("method: \"format(${body}, 1)\""), msg);
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:start").bean(new TwoMethods());
+                from("direct:two").bean(new TwoParams(), "format");
+            }
+        };
+    }
+
+    public static class TwoParams {
+        public String format(String body, int times) {
+            return body.repeat(times);
+        }
+    }
+
+    public static class TwoMethods {
+        public int addObjects() {
+            return 1;
+        }
+
+        public int getLeakedObjectCount() {
+            return 2;
+        }
+    }
+}
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/component/bean/BeanParameterInvalidValueTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/component/bean/BeanParameterInvalidValueTest.java
index 38f9c037002d..7c417e5caef2 100644
--- 
a/core/camel-core/src/test/java/org/apache/camel/component/bean/BeanParameterInvalidValueTest.java
+++ 
b/core/camel-core/src/test/java/org/apache/camel/component/bean/BeanParameterInvalidValueTest.java
@@ -86,7 +86,7 @@ public class BeanParameterInvalidValueTest extends 
ContextTestSupport {
                 "Should have thrown exception");
 
         ExpressionEvaluationException cause = 
assertIsInstanceOf(ExpressionEvaluationException.class, e.getCause());
-        assertTrue(cause.getCause().getMessage().startsWith("Unknown function: 
xxx at location 0"));
+        assertTrue(cause.getCause().getMessage().startsWith("Unknown function: 
xxx"));
 
         assertMockEndpointsSatisfied();
     }
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/language/FileLanguageTest.java 
b/core/camel-core/src/test/java/org/apache/camel/language/FileLanguageTest.java
index f8765cbee59c..7a5382926057 100644
--- 
a/core/camel-core/src/test/java/org/apache/camel/language/FileLanguageTest.java
+++ 
b/core/camel-core/src/test/java/org/apache/camel/language/FileLanguageTest.java
@@ -77,7 +77,8 @@ public class FileLanguageTest extends LanguageTestSupport {
                 () -> assertExpression("${file:onlyName}", file.getName()),
                 "Should have thrown exception");
 
-        assertTrue(e.getMessage().startsWith("Unknown file language syntax: 
onlyName at location 0"));
+        assertTrue(e.getMessage().startsWith("Unknown file language syntax: 
onlyName"), e.getMessage());
+        assertTrue(e.getMessage().contains("at location 0"), e.getMessage());
     }
 
     @Test
@@ -230,19 +231,19 @@ public class FileLanguageTest extends LanguageTestSupport 
{
                 () -> assertExpression("${file.name}", ""),
                 "Should have thrown an exception");
 
-        assertTrue(e1.getMessage().startsWith("Unknown function: file.name at 
location 0"));
+        assertTrue(e1.getMessage().startsWith("Unknown function: file.name"));
 
         ExpressionIllegalSyntaxException e2 = 
assertThrows(ExpressionIllegalSyntaxException.class,
                 () -> assertExpression("hey ${xxx} how are you?", ""),
                 "Should have thrown an exception");
 
-        assertTrue(e2.getMessage().startsWith("Unknown function: xxx at 
location 4"));
+        assertTrue(e2.getMessage().startsWith("Unknown function: xxx"));
 
         ExpressionIllegalSyntaxException e3 = 
assertThrows(ExpressionIllegalSyntaxException.class,
                 () -> assertExpression("${xxx}", ""),
                 "Should have thrown an exception");
 
-        assertTrue(e3.getMessage().startsWith("Unknown function: xxx at 
location 0"));
+        assertTrue(e3.getMessage().startsWith("Unknown function: xxx"));
     }
 
     @Test
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/language/NoSuchLanguageTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/language/NoSuchLanguageTest.java
index 5a72795c62cd..f1fe9af8e574 100644
--- 
a/core/camel-core/src/test/java/org/apache/camel/language/NoSuchLanguageTest.java
+++ 
b/core/camel-core/src/test/java/org/apache/camel/language/NoSuchLanguageTest.java
@@ -22,6 +22,7 @@ import org.junit.jupiter.api.Test;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 public class NoSuchLanguageTest extends LanguageTestSupport {
 
@@ -31,7 +32,7 @@ public class NoSuchLanguageTest extends LanguageTestSupport {
                 () -> assertPredicate("foo"),
                 "Should have thrown an exception");
 
-        assertEquals("No language could be found for: unknown", 
e.getMessage());
+        assertTrue(e.getMessage().startsWith("No language could be found for: 
unknown"), e.getMessage());
         assertEquals("unknown", e.getLanguage());
     }
 
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleSyntaxHintsTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleSyntaxHintsTest.java
new file mode 100644
index 000000000000..73ce4fd93499
--- /dev/null
+++ 
b/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleSyntaxHintsTest.java
@@ -0,0 +1,171 @@
+/*
+ * 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.ExchangeTestSupport;
+import org.apache.camel.language.simple.types.SimpleIllegalSyntaxException;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * CAMEL-24703: the parser messages say what to write, for the mistakes people 
and AI agents make most.
+ */
+public class SimpleSyntaxHintsTest extends ExchangeTestSupport {
+
+    private String predicateError(String text) {
+        SimplePredicateParser parser = new SimplePredicateParser(context, 
text, true, null);
+        return assertThrows(SimpleIllegalSyntaxException.class, 
parser::parsePredicate).getMessage();
+    }
+
+    private String expressionError(String text) {
+        SimpleExpressionParser parser = new SimpleExpressionParser(context, 
text, true, null);
+        return assertThrows(SimpleIllegalSyntaxException.class, 
parser::parseExpression).getMessage();
+    }
+
+    @Test
+    public void testFunctionWithoutDollarBraces() {
+        assertThat(predicateError("body == 'Hello World'"))
+                .contains("text outside ${...} is a literal")
+                .contains("did you mean ${body} == 'Hello World'?");
+        assertThat(predicateError("header.foo == 'bar'")).contains("did you 
mean ${header.foo} == 'bar'?");
+    }
+
+    @Test
+    public void testOperatorInsideFunction() {
+        assertThat(predicateError("${body == 'x'}")).contains("Operators go 
outside the function: ${body} == 'x'");
+    }
+
+    @Test
+    public void testUnquotedValue() {
+        assertThat(predicateError("${body} == x"))
+                .contains("does not accept x on the right hand side")
+                .contains("a quoted literal 'x'")
+                .doesNotContain("token null");
+        assertThat(predicateError("${body} contains x")).contains("does not 
accept x on the right hand side");
+    }
+
+    @Test
+    public void testUnknownOperators() {
+        assertThat(predicateError("${body} = 'x'")).contains("Unknown operator 
=: did you mean ==?");
+        assertThat(predicateError("${body} == 'x' and ${header.y} == 
1")).contains("use && for and");
+        assertThat(predicateError("${body} == 'x' || ")).contains("needs a 
predicate on the right hand side");
+    }
+
+    @Test
+    public void testQuotes() {
+        assertThat(predicateError("${body} == 'it''s'")).contains("double 
quotes");
+    }
+
+    @Test
+    public void testMissingClosingBrace() {
+        assertThat(expressionError("${header.foo")).contains("missing } to 
close");
+        assertThat(expressionError("${bodyAs(String)")).contains("missing } to 
close");
+    }
+
+    @Test
+    public void testUnknownFunctionSuggestions() {
+        assertThat(expressionError("${padding(3)}")).contains("Unknown 
function: padding(3)")
+                .contains("did you mean ${pad(3)}?");
+        assertThat(expressionError("${property.foo}")).contains("did you mean 
${exchangeProperty.foo}?");
+        assertThat(expressionError("${Body}")).contains("case sensitive: 
${body}");
+        assertThat(expressionError("${ body }")).contains("remove the spaces: 
${body}");
+        assertThat(expressionError("${bodyy}")).contains("did you mean 
${body}?");
+    }
+
+    @Test
+    public void testOperatorAfterOgnlMethod() {
+        assertThat(predicateError("${body.length() > 3}")).contains("Operators 
go outside the function: ${body.length()} > 3");
+    }
+
+    @Test
+    public void testOgnlRuntimeMessages() {
+        exchange.getIn().setBody("hello");
+        Exception e = assertThrows(Exception.class,
+                () -> 
context.resolveLanguage("simple").createExpression("${body.lenght()}").evaluate(exchange,
 String.class));
+        assertThat(e.getMessage())
+                .contains("on the message body of type java.lang.String")
+                .contains("did you mean length()?")
+                .doesNotContain("on null");
+    }
+
+    @Test
+    public void testOgnlDotOnAMapSaysToUseAKey() {
+        exchange.getIn().setBody(new 
java.util.LinkedHashMap<>(java.util.Map.of("type", "order")));
+        Exception e = assertThrows(Exception.class,
+                () -> 
context.resolveLanguage("simple").createExpression("${body.type}").evaluate(exchange,
+                        String.class));
+        assertThat(e.getMessage()).contains("the value is a Map: a key is read 
with [type], as in ${body[type]}");
+        assertEquals("order", 
context.resolveLanguage("simple").createExpression("${body[type]}").evaluate(exchange,
+                String.class));
+    }
+
+    @Test
+    public void testArithmeticInAFunctionSaysThereIsNone() {
+        Exception e = assertThrows(Exception.class,
+                () -> 
context.resolveLanguage("simple").createExpression("${exchangeCounter % 3}"));
+        assertThat(e.getMessage()).contains("Unknown function: exchangeCounter 
% 3")
+                .contains("simple has no arithmetic operators");
+    }
+
+    @Test
+    public void testLanguageNameAsAFunctionIsNamed() {
+        Exception e = assertThrows(Exception.class,
+                () -> 
context.resolveLanguage("simple").createExpression("${simple}[${date:now:HH:mm}]
 size ${body.length}"));
+        assertThat(e.getMessage()).contains("Unknown function: simple")
+                .contains("simple is the language, not a function");
+        Exception g = assertThrows(Exception.class,
+                () -> 
context.resolveLanguage("simple").createExpression("${groovy(body.size())}"));
+        assertThat(g.getMessage()).contains("groovy is a language, not a 
simple function").contains("groovy: \"...\"");
+    }
+
+    @Test
+    public void testOgnlMethodWithoutParentheses() {
+        exchange.getIn().setBody("hello");
+        Exception e = assertThrows(Exception.class,
+                () -> 
context.resolveLanguage("simple").createExpression("${body.toUpperCase}").evaluate(exchange,
+                        String.class));
+        assertThat(e.getMessage()).contains("a method call needs parentheses: 
toUpperCase()");
+    }
+
+    @Test
+    public void testMissingLanguageNamesTheDependency() {
+        Exception e = assertThrows(Exception.class, () -> 
context.resolveLanguage("cheese"));
+        assertThat(e.getMessage()).contains("No language could be found for: 
cheese").contains("camel-cheese");
+    }
+
+    @Test
+    public void testFileFunctionDoesNotReadAFile() {
+        assertThat(expressionError("${file:src/main/resources/input.xml}"))
+                .contains("Unknown file language syntax")
+                .contains("they do not read a file")
+                .contains("poll EIP");
+    }
+
+    @Test
+    public void testHelpers() {
+        assertThat(SimpleSyntaxHints.wordAt("${body} == x", 
12)).isEqualTo("x");
+        assertThat(SimpleSyntaxHints.wordAt("${body} == 'x' and ${header.y} == 
1", 15)).isEqualTo("and");
+        
assertThat(SimpleSyntaxHints.functionName("header.foo")).isEqualTo("header");
+        
assertThat(SimpleSyntaxHints.functionName("bodyAs(String)")).isEqualTo("bodyAs");
+        
assertThat(SimpleSyntaxHints.functionName("in.body")).isEqualTo("in.body");
+        assertThat(SimpleSyntaxHints.operatorsOutside("iif(${body} == 'x', 
'a', 'b')")).isNull();
+        assertThat(SimpleSyntaxHints.operatorsOutside("body == 
'x'")).isEqualTo("${body} == 'x'");
+    }
+}
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleTest.java
index 9e14b4bf6da3..97ce538fc685 100644
--- 
a/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleTest.java
+++ 
b/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleTest.java
@@ -938,13 +938,13 @@ public class SimpleTest extends LanguageTestSupport {
                 () -> assertExpression("hey ${xxx} how are you?", ""),
                 "Should have thrown an exception");
 
-        assertTrue(e1.getMessage().startsWith("Unknown function: xxx at 
location 4"));
+        assertTrue(e1.getMessage().startsWith("Unknown function: xxx"));
 
         ExpressionIllegalSyntaxException e2 = 
assertThrows(ExpressionIllegalSyntaxException.class,
                 () -> assertExpression("${xxx}", ""),
                 "Should have thrown an exception");
 
-        assertTrue(e2.getMessage().startsWith("Unknown function: xxx at 
location 0"));
+        assertTrue(e2.getMessage().startsWith("Unknown function: xxx"));
 
         ExpressionIllegalSyntaxException e3 = 
assertThrows(ExpressionIllegalSyntaxException.class,
                 () -> assertExpression("${bodyAs(xxx}", ""),
diff --git 
a/core/camel-management/src/test/java/org/apache/camel/management/BacklogDebuggerTest.java
 
b/core/camel-management/src/test/java/org/apache/camel/management/BacklogDebuggerTest.java
index 458ff9ab4da5..c101b3706580 100644
--- 
a/core/camel-management/src/test/java/org/apache/camel/management/BacklogDebuggerTest.java
+++ 
b/core/camel-management/src/test/java/org/apache/camel/management/BacklogDebuggerTest.java
@@ -448,7 +448,7 @@ public class BacklogDebuggerTest extends 
ManagementTestSupport {
         // validate conditional breakpoint (mistake on purpose)
         Object out = mbeanServer.invoke(on, "validateConditionalBreakpoint",
                 new Object[] { "unknown", "${body contains 'Camel'" }, new 
String[] { "java.lang.String", "java.lang.String" });
-        assertEquals("No language could be found for: unknown", out);
+        assertTrue(String.valueOf(out).startsWith("No language could be found 
for: unknown"), String.valueOf(out));
 
         // validate conditional breakpoint (mistake on purpose)
         out = mbeanServer.invoke(on, "validateConditionalBreakpoint", new 
Object[] { "simple", "${body contains 'Camel'" },

Reply via email to