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 0ce7b78e91f4 CAMEL-24916: simple - a dot reads a Map key when there is 
no such method (#26757)
0ce7b78e91f4 is described below

commit 0ce7b78e91f49e3ea0891ba342f313d2d19a3620
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed Sep 23 10:28:57 2026 +0200

    CAMEL-24916: simple - a dot reads a Map key when there is no such method 
(#26757)
    
    ${body.sku} on a map body threw Method with name: sku not found. A map now 
answers the key when it has no method of that name, the way jq, JavaScript and 
Groovy read a map. A method still wins, and a name that is neither a method nor 
a key still fails with the hint.
---
 .../org/apache/camel/catalog/docs/simple-ognl.adoc | 13 ++++++
 .../apache/camel/language/bean/BeanExpression.java | 45 +++++++++++++++++++++
 .../docs/modules/languages/pages/simple-ognl.adoc  | 13 ++++++
 .../language/simple/SimpleSyntaxHintsTest.java     | 47 +++++++++++++++++++---
 .../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc    |  7 ++++
 5 files changed, 120 insertions(+), 5 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/simple-ognl.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/simple-ognl.adoc
index a79ab80ca0fa..5880f4a35b87 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/simple-ognl.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/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/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..4be7cc3187cc 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,44 @@ 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.
+     *
+     * @param  holder     the bean the OGNL step is called on; null when there 
is none
+     * @param  exchange   the exchange the bean is resolved against
+     * @param  methodName the name that failed as a method call, and is tried 
as a key
+     * @param  cause      the failure of that call, so that only a missing 
method is read as a key
+     * @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..38dbdbc25faa 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
@@ -22,6 +22,7 @@ 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.assertNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 
 /**
@@ -144,16 +145,52 @@ 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 testOgnlDotOnAMapWithANullValueAnswersNull() {
+        // a key that is there and holds null is a value, not a missing key
+        java.util.Map<String, Object> body = new java.util.HashMap<>();
+        body.put("sku", null);
+        exchange.getIn().setBody(body);
+        
assertNull(context.resolveLanguage("simple").createExpression("${body.sku}").evaluate(exchange,
 Object.class),
+                "a null value is a map entry: the expression answers null 
rather than throwing");
+    }
+
+    @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 586085c0b156..4eb4fa17c4d4 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