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 bdc6d8a8757e7..9158f62e694d9 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 @@ -16,6 +16,8 @@ */ package org.apache.camel.language.bean; +import java.io.InputStream; +import java.util.Collection; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -29,6 +31,7 @@ import org.apache.camel.NoSuchBeanException; import org.apache.camel.Predicate; import org.apache.camel.RuntimeCamelException; +import org.apache.camel.StreamCache; import org.apache.camel.TypeConverter; import org.apache.camel.component.bean.BeanComponent; import org.apache.camel.component.bean.BeanConstants; @@ -687,11 +690,33 @@ private static Object lookupResult( throw new IndexOutOfBoundsException( "Key: " + key + " not found in bean: " + bean + " of type: " + ObjectHelper.classCanonicalName(bean) + " using OGNL path [" + ognlPath - + "]"); + + "]" + noKeysHint(result != null ? result : bean, key)); } else { // null safe so we can return null return null; } } + /** + * What to do when a key is read from a value that has no keys (CAMEL-25746): the message body as a stream or bytes + * (a request body before it is unmarshalled, or a variable set from it), JSON text, or a List, array or other + * Collection, where [0] picks an element. + */ + static String noKeysHint(Object value, String key) { + if (value instanceof StreamCache || value instanceof InputStream || value instanceof byte[]) { + return " (the value is the message body as a stream, not a Map: unmarshal it first, for example with json," + + " then read the key; a variable keeps the value it had when it was set)"; + } + if (value instanceof CharSequence text) { + String trimmed = text.toString().strip(); + if (trimmed.startsWith("{") || trimmed.startsWith("[")) { + return " (the value is JSON text, not a Map: unmarshal it first, for example with json, then read the key)"; + } + } + if (value instanceof Collection || value instanceof Object[]) { + String type = value instanceof List ? "a List" : value instanceof Object[] ? "an array" : "a Collection"; + return " (" + type + " has no keys: pick an element first, such as [0][" + key + "], or split it)"; + } + return ""; + } } 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 3e29ee9a5e566..9ebb4de8394c4 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 @@ -16,6 +16,7 @@ */ package org.apache.camel.language.simple; +import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; @@ -521,6 +522,53 @@ public void testOGNLPropertyMapNotMap() { cause.getMessage()); } + @Test + public void testOGNLKeyOnAStreamSaysToUnmarshal() { + // CAMEL-25746: the request body as a stream, before it is unmarshalled + exchange.getIn().setBody(new ByteArrayInputStream("{\"qty\": 2}".getBytes(StandardCharsets.UTF_8))); + RuntimeBeanExpressionException e = assertThrows(RuntimeBeanExpressionException.class, + () -> assertExpression("${body[qty]}", null)); + + IndexOutOfBoundsException cause = assertIsInstanceOf(IndexOutOfBoundsException.class, e.getCause()); + assertTrue(cause.getMessage().startsWith("Key: qty not found in bean: "), cause.getMessage()); + assertTrue(cause.getMessage().contains("the value is the message body as a stream, not a Map: unmarshal it first"), + cause.getMessage()); + } + + @Test + public void testOGNLKeyOnJsonTextSaysToUnmarshal() { + exchange.getIn().setHeader("order", "{\"qty\": 2}"); + RuntimeBeanExpressionException e = assertThrows(RuntimeBeanExpressionException.class, + () -> assertExpression("${header.order[qty]}", null)); + + IndexOutOfBoundsException cause = assertIsInstanceOf(IndexOutOfBoundsException.class, e.getCause()); + assertTrue(cause.getMessage().contains("the value is JSON text, not a Map: unmarshal it first"), cause.getMessage()); + } + + @Test + public void testOGNLKeyOnAListSaysToPickAnElement() { + exchange.getIn().setBody(List.of(Map.of("sku", "CAMEL-MUG", "qty", 42))); + RuntimeBeanExpressionException e = assertThrows(RuntimeBeanExpressionException.class, + () -> assertExpression("${body[qty]}", null)); + + IndexOutOfBoundsException cause = assertIsInstanceOf(IndexOutOfBoundsException.class, e.getCause()); + assertTrue(cause.getMessage().contains("a List has no keys: pick an element first, such as [0][qty]"), + cause.getMessage()); + } + + @Test + public void testOGNLKeyOnAnArraySaysToPickAnElement() { + exchange.getIn().setBody(new Object[] { Map.of("sku", "CAMEL-MUG", "qty", 42) }); + RuntimeBeanExpressionException e = assertThrows(RuntimeBeanExpressionException.class, + () -> assertExpression("${body[qty]}", null)); + + IndexOutOfBoundsException cause = assertIsInstanceOf(IndexOutOfBoundsException.class, e.getCause()); + assertTrue(cause.getMessage().contains("an array has no keys: pick an element first, such as [0][qty]"), + cause.getMessage()); + // [0] picks the element the hint points to + assertExpression("${body[0][qty]}", 42); + } + @Test public void testOGNLPropertyMapIllegalSyntax() { ExpressionIllegalSyntaxException e = assertThrows(ExpressionIllegalSyntaxException.class,