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<>());
