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

davsclaus pushed a commit to branch feature/CAMEL-25749-body-facts-reads
in repository https://gitbox.apache.org/repos/asf/camel.git

commit da3700fcd80c7438f235ec7c958b0d9559cafd45
Author: Claus Ibsen <[email protected]>
AuthorDate: Sun Oct 18 17:51:35 2026 +0200

    CAMEL-25749: a read on a body that is still the payload, or an iterator, 
says what the body is and what to write
    
    The failures that had no sentence get the BodyFacts one: simple and bean
    OGNL on the message body - a method or property ${body.qty}, ${body.size()},
    or a key ${body[status]} - and Groovy body.sku on the body itself, when the
    body is the payload as text, bytes, a stream or the file as read, or an
    iterator. So ${body.qty} on the cached stream of an HTTP request says to
    unmarshal it in the data format of its Content-Type, ${body[status]} on the
    file consumer's body says the same for the file name, and ${body.size()} on
    sql's StreamList says an iterator has no size. A name an object does not
    have still reports only the wrong name.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../apache/camel/language/bean/BeanExpression.java | 40 +++++++++++++++-
 .../camel/language/groovy/GroovyExpression.java    |  6 +++
 .../groovy/GroovyMissingPropertyHintTest.java      | 18 ++++++++
 .../language/simple/SimpleSyntaxHintsTest.java     | 54 ++++++++++++++++++++++
 4 files changed, 116 insertions(+), 2 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 bdc6d8a8757e..cf86e3107c22 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
@@ -536,9 +536,11 @@ public class BeanExpression implements Expression, 
Predicate {
                 resultExchange.setException(null);
                 return value;
             }
+            String method = methodHint(methodName, 
resultExchange.getException());
+            String body = bodyHint(exchange, beanOf(holder, exchange));
             throw new RuntimeBeanExpressionException(
                     exchange, describeBean(holder, beanName, exchange),
-                    keyHint(holder, exchange, methodName, 
methodHint(methodName, resultExchange.getException())),
+                    body != null ? method + " (" + body + ")" : 
keyHint(holder, exchange, methodName, method),
                     resultExchange.getException());
         }
         return newResult;
@@ -626,11 +628,45 @@ public class BeanExpression implements Expression, 
Predicate {
             key = exp.evaluate(exchange, String.class);
         }
         if (key != null) {
-            return lookupResult(resultExchange, key, result, nullSafe, 
ognlPath, holder.getBean(exchange));
+            Object bean = holder.getBean(exchange);
+            try {
+                return lookupResult(resultExchange, key, result, nullSafe, 
ognlPath, bean);
+            } catch (IndexOutOfBoundsException e) {
+                // ${body[status]} on the file as read, or on a stream: the 
same exception, saying what the body is
+                // and the step that parses it (CAMEL-25749)
+                String body = bodyHint(exchange, bean);
+                if (body == null) {
+                    throw e;
+                }
+                throw new IndexOutOfBoundsException(e.getMessage() + " (" + 
body + ")");
+            }
         }
         return result;
     }
 
+    /**
+     * What the body is and what to write, when an OGNL read on the message 
body failed because of its type: the payload
+     * as text, bytes, a stream or the file as read, which an unmarshal turns 
into data, or an iterator, which has no
+     * size (CAMEL-25749). Null for a Map, whose keys {@link #keyHint} lists, 
and for a List or an object, where the
+     * name read is wrong rather than the body.
+     */
+    private static String bodyHint(Exchange exchange, Object bean) {
+        if (bean == null || bean instanceof Map || bean != 
exchange.getMessage().getBody()) {
+            return null;
+        }
+        String hint = BodyFacts.hint(exchange, BodyFacts.Need.DATA);
+        return hint != null ? BodyFacts.describe(exchange) + ": " + hint : 
null;
+    }
+
+    /** The bean an OGNL step is called on, or null when it cannot be 
resolved. */
+    private static Object beanOf(BeanHolder holder, Exchange exchange) {
+        try {
+            return holder != null ? holder.getBean(exchange) : null;
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
     private static Object lookupResult(
             Exchange exchange, String key, Object result, boolean nullSafe, 
String ognlPath, Object bean) {
         StringHelper.notEmpty(key, "key", "in Simple language ognl path: " + 
ognlPath);
diff --git 
a/components/camel-groovy/src/main/java/org/apache/camel/language/groovy/GroovyExpression.java
 
b/components/camel-groovy/src/main/java/org/apache/camel/language/groovy/GroovyExpression.java
index a7ded29fbe60..1631050895c6 100644
--- 
a/components/camel-groovy/src/main/java/org/apache/camel/language/groovy/GroovyExpression.java
+++ 
b/components/camel-groovy/src/main/java/org/apache/camel/language/groovy/GroovyExpression.java
@@ -103,11 +103,17 @@ public class GroovyExpression extends ExpressionSupport {
             boolean onText = e.getType() == Byte.class || e.getType() == 
Character.class || e.getType() == String.class;
             boolean bodyText = onText && (body instanceof byte[] || body 
instanceof String);
             String textHeader = onText && !bodyText ? jsonTextHeader(exchange) 
: null;
+            // body.email on the body itself: the file as read or a stream, 
which is not data yet, or an iterator
+            // (CAMEL-25749); null for an object, where the name read is wrong
+            String payload = !onText && body != null && e.getType() == 
body.getClass() && !(body instanceof Map)
+                    ? BodyFacts.hint(exchange, BodyFacts.Need.DATA) : null;
             String hint;
             if (bodyText) {
                 // what the body is, the step that set it, and the unmarshal 
that parses it (CAMEL-25749)
                 hint = BodyFacts.describe(exchange) + ", not parsed data: "
                        + BodyFacts.hint(exchange, BodyFacts.Need.DATA);
+            } else if (payload != null) {
+                hint = BodyFacts.describe(exchange) + ": " + payload;
             } else if (textHeader != null) {
                 String ref = isIdentifier(textHeader) ? "headers." + 
textHeader : "headers['" + textHeader + "']";
                 hint = "header '" + textHeader + "' is still JSON text, not 
parsed data: parse it in the script with"
diff --git 
a/components/camel-groovy/src/test/java/org/apache/camel/processor/groovy/GroovyMissingPropertyHintTest.java
 
b/components/camel-groovy/src/test/java/org/apache/camel/processor/groovy/GroovyMissingPropertyHintTest.java
index 39fdf00d7e6a..b24bbd5b4d71 100644
--- 
a/components/camel-groovy/src/test/java/org/apache/camel/processor/groovy/GroovyMissingPropertyHintTest.java
+++ 
b/components/camel-groovy/src/test/java/org/apache/camel/processor/groovy/GroovyMissingPropertyHintTest.java
@@ -16,10 +16,14 @@
  */
 package org.apache.camel.processor.groovy;
 
+import java.io.ByteArrayInputStream;
+import java.nio.charset.StandardCharsets;
+
 import groovy.lang.MissingPropertyException;
 import org.apache.camel.Exchange;
 import org.apache.camel.RoutesBuilder;
 import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.converter.stream.ByteArrayInputStreamCache;
 import org.apache.camel.test.junit6.CamelTestSupport;
 import org.junit.jupiter.api.Test;
 
@@ -53,6 +57,19 @@ public class GroovyMissingPropertyHintTest extends 
CamelTestSupport {
                 cause.getMessage());
     }
 
+    @Test
+    public void fieldReadOnAStreamSaysToUnmarshal() {
+        // CAMEL-25749: body.sku on the body itself, the text of a request as 
a cached stream
+        ByteArrayInputStreamCache body = new ByteArrayInputStreamCache(
+                new ByteArrayInputStream("{\"sku\": 
\"A1\"}".getBytes(StandardCharsets.UTF_8)));
+        Exception e = assertThrows(Exception.class,
+                () -> template.sendBodyAndHeader("direct:read", body, 
Exchange.CONTENT_TYPE, "application/json"));
+        MissingPropertyException cause = 
assertInstanceOf(MissingPropertyException.class, e.getCause());
+        assertTrue(cause.getMessage().contains("the body is a cached stream 
(ByteArrayInputStreamCache)"), cause.getMessage());
+        assertTrue(cause.getMessage().contains("unmarshal it first (unmarshal: 
json, for the Content-Type application/json)"),
+                cause.getMessage());
+    }
+
     @Test
     public void fieldReadOnJsonBytesSaysToUnmarshal() {
         byte[] json = "[{\"sku\": \"A1\"}]".getBytes();
@@ -151,6 +168,7 @@ public class GroovyMissingPropertyHintTest extends 
CamelTestSupport {
                 
from("direct:bean").transform().groovy("formatter.append(body)");
                 
from("direct:unknown").transform().groovy("nosuch.toUpperCase()");
                 from("direct:field").transform().groovy("body.find { it.sku == 
headers.sku }");
+                from("direct:read").transform().groovy("body.sku");
                 from("direct:header").transform().groovy("headers.skuList.find 
{ it.sku == headers.sku }");
                 
from("direct:message").transform().groovy("message.getHeader('name') + ' ' + 
message.body");
             }
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 a80ad2bbb730..c7c7c85c2018 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
@@ -16,7 +16,15 @@
  */
 package org.apache.camel.language.simple;
 
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
+import org.apache.camel.Exchange;
 import org.apache.camel.ExchangeTestSupport;
+import org.apache.camel.component.file.GenericFile;
+import org.apache.camel.converter.stream.ByteArrayInputStreamCache;
 import org.apache.camel.language.simple.types.SimpleIllegalSyntaxException;
 import org.junit.jupiter.api.Test;
 
@@ -190,6 +198,52 @@ public class SimpleSyntaxHintsTest extends 
ExchangeTestSupport {
         assertThat(e.getMessage()).contains("(the Map has no key typo; the 
body is a Map (LinkedHashMap): its keys are: type)");
     }
 
+    @Test
+    public void testOgnlOnAStreamSaysToUnmarshal() {
+        // CAMEL-25749: ${body.qty} on the body of an HTTP request, still the 
text as a cached stream
+        exchange.getIn().setBody(new ByteArrayInputStreamCache(
+                new ByteArrayInputStream("{\"qty\": 
2}".getBytes(StandardCharsets.UTF_8))));
+        exchange.getIn().setHeader(Exchange.CONTENT_TYPE, "application/json");
+        Exception e = assertThrows(Exception.class,
+                () -> 
context.resolveLanguage("simple").createExpression("${body.qty}").evaluate(exchange,
 String.class));
+        assertThat(e.getMessage()).contains("the body is a cached stream 
(ByteArrayInputStreamCache): unmarshal it first"
+                                            + " (unmarshal: json, for the 
Content-Type application/json) to read its fields");
+    }
+
+    @Test
+    public void testOgnlSizeOnAnIteratorSaysItHasNone() {
+        // CAMEL-25749: ${body.size()} on what sql gives with 
outputType=StreamList
+        exchange.getIn().setBody(List.of("A1", "A2").iterator());
+        Exception e = assertThrows(Exception.class,
+                () -> 
context.resolveLanguage("simple").createExpression("${body.size()}").evaluate(exchange,
 String.class));
+        assertThat(e.getMessage()).contains("the body is an iterator (")
+                .contains("it has no size and can be read once: split it, or 
have the producer give a List");
+    }
+
+    @Test
+    public void testKeyOnTheFileAsReadSaysToUnmarshal() {
+        // CAMEL-25749: ${body[status]} on the file consumer's body, before 
any unmarshal
+        GenericFile<File> file = new GenericFile<>();
+        file.setFile(new File("orders/order-1003.json"));
+        file.setFileName("order-1003.json");
+        exchange.getIn().setBody(file);
+        exchange.getIn().setHeader(Exchange.FILE_NAME, "order-1003.json");
+        Exception e = assertThrows(Exception.class,
+                () -> 
context.resolveLanguage("simple").createExpression("${body[status]}").evaluate(exchange,
 String.class));
+        assertThat(e.getMessage()).contains("Key: status not found in bean")
+                .contains("(the body is the file as read (a GenericFile): 
unmarshal it first (unmarshal: json, for the file"
+                          + " name order-1003.json) to read its fields)");
+    }
+
+    @Test
+    public void testOgnlOnAnObjectSaysNothingAboutTheBody() {
+        // a name an object does not have is a wrong name, not a body of the 
wrong type
+        exchange.getIn().setBody(new StringBuilder("x"));
+        Exception e = assertThrows(Exception.class,
+                () -> 
context.resolveLanguage("simple").createExpression("${body.qty}").evaluate(exchange,
 String.class));
+        assertThat(e.getMessage()).doesNotContain("the body is");
+    }
+
     @Test
     public void testOgnlDotOnAnEmptyMapSaysItIsEmpty() {
         exchange.getIn().setBody(new java.util.LinkedHashMap<>());

Reply via email to