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 58966e93ab23 CAMEL-25749: a read on a body that is still the payload,
or an iterator, says what the body is and what to write (#28066)
58966e93ab23 is described below
commit 58966e93ab23949f5c72f0aedaa7c0dce4c3a57e
Author: Claus Ibsen <[email protected]>
AuthorDate: Sun Oct 18 21:48:15 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 (#28066)
* CAMEL-25749: camel-support - BodyFacts says what the body is, the step
that set it, and what to write; the existing body hints move onto it
BodyFacts.describe(exchange) says what the body is in plain words and,
from the message history, the step that set it ("the body is a Map
(LinkedHashMap), set by unmarshal at orders.yaml:9", or "unchanged since
from: file://orders"). BodyFacts.hint(exchange, need[, target]) says what
to write for what the failing step needed: DATA (it reads fields), BYTES
(it writes or sends the payload) or A_BODY (it needs a body at all), in the
data format the Content-Type, the file name or the text names.
The sentences that existed move onto it, so the wording is one: the http
producer (CAMEL-25309), the platform-http reply and the file write
(CAMEL-25751), Groovy on text (CAMEL-25330), unmarshal on a null body
(CAMEL-25285), jsonpath and jq on a null body (CAMEL-24838), and a missing
key of the body Map (CAMEL-25322). The three copies of the Content-Type to
data format mapping are gone.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
* 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]>
---------
Signed-off-by: Claus Ibsen <[email protected]>
Co-authored-by: Claude Opus 5.5 (1M context) <[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 9158f62e694d..53a3c458d5b5 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
@@ -539,9 +539,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;
@@ -629,11 +631,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<>());