This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch fix/CAMEL-24916
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 273996a4d6fd681b3550a0b7bf4ca534db18d418
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed Sep 23 08:01:13 2026 +0200

    CAMEL-24916: simple - a dot reads a Map key when there is no such method
    
    ${body.sku} on a map body threw "Method with name: sku not found on bean",
    which is the loudest runtime failure in the AI route benchmark (3582 times 
in
    one series) and a frequent question, because jq, JavaScript and Groovy all 
read
    a map with a dot. It now answers the key when the map has no method of that
    name.
    
    A method still wins, so ${body.size} calls size() as before, and a name 
that is
    neither a method nor a key still fails with the hint - a misspelled field is
    reported rather than answered with null. Only expressions that used to throw
    can now return a value.
    
    Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
    Claude-Session: https://claude.ai/code/session_01Bp3538HRBPMQkb5ta9xRaj
---
 .../apache/camel/language/bean/BeanExpression.java | 41 ++++++++++++++++++++++
 .../docs/modules/languages/pages/simple-ognl.adoc  | 13 +++++++
 .../language/simple/SimpleSyntaxHintsTest.java     | 36 ++++++++++++++++---
 .../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc    |  7 ++++
 4 files changed, 92 insertions(+), 5 deletions(-)

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 7b517069b805..1c896190395a 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
@@ -527,6 +527,13 @@ public class BeanExpression implements Expression, 
Predicate {
         }
         Object newResult = invokeBean(holder, beanName, methodName, 
resultExchange);
         if (resultExchange.getException() != null) {
+            // ${body.sku} on a Map with no method of that name: read the key, 
the only thing it can mean
+            // (CAMEL-24916)
+            Object value = mapValue(holder, exchange, methodName, 
resultExchange.getException());
+            if (value != NO_SUCH_KEY) {
+                resultExchange.setException(null);
+                return value;
+            }
             throw new RuntimeBeanExpressionException(
                     exchange, describeBean(holder, beanName, exchange),
                     keyHint(holder, exchange, methodName, 
methodHint(methodName, resultExchange.getException())),
@@ -535,6 +542,40 @@ public class BeanExpression implements Expression, 
Predicate {
         return newResult;
     }
 
+    /** Says that the bean is not a Map with that key, as null is a value a 
key can hold. */
+    private static final Object NO_SUCH_KEY = new Object();
+
+    /**
+     * The value of the key on a Map bean when the method of that name does 
not exist, so that ${body.sku} reads the sku
+     * of a map the way ${body[sku]} does - what a map means in jq, JavaScript 
and Groovy too (CAMEL-24916).
+     * <p/>
+     * A method still wins: ${body.size} on a Map calls size() as before. A 
name that is not a key still fails, so a
+     * misspelled field is still reported.
+     *
+     * @return the value of the key, or {@link #NO_SUCH_KEY} when this is not 
that case
+     */
+    private static Object mapValue(BeanHolder holder, Exchange exchange, 
String methodName, Exception cause) {
+        if (methodName == null || methodName.contains("(") || 
methodName.contains("[")) {
+            return NO_SUCH_KEY;
+        }
+        boolean noSuchMethod = false;
+        for (Throwable t = cause; t != null && !noSuchMethod; t = 
t.getCause()) {
+            noSuchMethod = t instanceof MethodNotFoundException;
+        }
+        if (!noSuchMethod) {
+            return NO_SUCH_KEY; // the method is there and it failed: that is 
a real error
+        }
+        try {
+            Object bean = holder != null ? holder.getBean(exchange) : null;
+            if (bean instanceof Map<?, ?> map && map.containsKey(methodName)) {
+                return map.get(methodName);
+            }
+        } catch (Exception e) {
+            // ignore and let the original failure stand
+        }
+        return NO_SUCH_KEY;
+    }
+
     /**
      * ${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.
diff --git 
a/core/camel-core-languages/src/main/docs/modules/languages/pages/simple-ognl.adoc
 
b/core/camel-core-languages/src/main/docs/modules/languages/pages/simple-ognl.adoc
index a79ab80ca0fa..5880f4a35b87 100644
--- 
a/core/camel-core-languages/src/main/docs/modules/languages/pages/simple-ognl.adoc
+++ 
b/core/camel-core-languages/src/main/docs/modules/languages/pages/simple-ognl.adoc
@@ -128,6 +128,19 @@ simple("${body[foo]}")
 simple("${body[this.is.foo]}")
 ----
 
+A `Map` can also be read with a dot when it has no method of that name, so a
+body unmarshalled from JSON reads the way it looks:
+
+[source,java]
+----
+simple("${body.sku}")     // the same as ${body[sku]} when the map has no 
sku() method
+----
+
+A method still wins: `${body.size}` on a `Map` calls `size()`, not the key
+`size`, so use `${body[size]}` for the key. A name that is neither a method nor
+a key still fails, so a misspelled field is reported rather than answered with
+null.
+
 Suppose there was no value with the key `foo` then you can use the null
 safe operator to avoid the NPE as shown:
 
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
index fe6b638bd944..4726419e3bee 100644
--- 
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
@@ -144,16 +144,42 @@ public class SimpleSyntaxHintsTest extends 
ExchangeTestSupport {
     }
 
     @Test
-    public void testOgnlDotOnAMapSaysToUseAKey() {
+    public void testOgnlDotOnAMapReadsTheKey() {
+        // CAMEL-24916: a map has no method type, so the key is what the dot 
can mean
         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));
         assertEquals("order", 
context.resolveLanguage("simple").createExpression("${body[type]}").evaluate(exchange,
                 String.class));
     }
 
+    @Test
+    public void testOgnlDotOnAMapWithoutThatKeySaysToUseAKey() {
+        exchange.getIn().setBody(new 
java.util.LinkedHashMap<>(java.util.Map.of("type", "order")));
+        Exception e = assertThrows(Exception.class,
+                () -> 
context.resolveLanguage("simple").createExpression("${body.typo}").evaluate(exchange,
+                        String.class));
+        assertThat(e.getMessage()).contains("the value is a Map: a key is read 
with [typo], as in ${body[typo]}");
+    }
+
+    @Test
+    public void testAMethodOfAMapStillWins() {
+        exchange.getIn().setBody(new 
java.util.LinkedHashMap<>(java.util.Map.of("size", "not the size")));
+        assertEquals("1", 
context.resolveLanguage("simple").createExpression("${body.size}").evaluate(exchange,
+                String.class), "size() is a method of Map, so it still answers 
before the key");
+    }
+
+    @Test
+    public void testOgnlDotOnANestedMapReadsTheKey() {
+        java.util.Map<String, Object> item = new java.util.LinkedHashMap<>();
+        item.put("sku", "CAMEL-MUG");
+        java.util.Map<String, Object> body = new java.util.LinkedHashMap<>();
+        body.put("item", item);
+        exchange.getIn().setBody(body);
+        assertEquals("CAMEL-MUG", 
context.resolveLanguage("simple").createExpression("${body.item.sku}")
+                .evaluate(exchange, String.class));
+    }
+
     @Test
     public void testArithmeticInAFunctionSaysThereIsNone() {
         Exception e = assertThrows(Exception.class,
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index be1c20b5d9c4..3b7891281a25 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -17,6 +17,13 @@ See the xref:camel-upgrade-recipes-tool.adoc[documentation] 
page for details.
 
 OAuth client credentials token caching now distinguishes profiles by client 
secret and requested scope, in addition to token endpoint and client ID. 
Profiles with different credentials or scopes request separate tokens instead 
of reusing the same cached token. Applications using such profiles may make 
additional token requests after upgrading.
 
+=== Simple language
+
+The simple language reads a `Map` with a dot as well as with a key: 
`${body.sku}` answers the `sku` entry of a map
+body when the map has no `sku()` method, the same value `${body[sku]}` gives. 
A method of the map still wins, so
+`${body.size}` calls `size()` as before, and a name that is neither a method 
nor a key still fails. Only expressions
+that used to throw can now return a value.
+
 === Circuit Breaker EIP
 
 The exchange property `CamelCircuitBreakerResponseRejected` is now also set 
inside the `onFallback`,

Reply via email to