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`,
