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 37e5bf71cc59 CAMEL-25871: camel-bean - a hint for an OGNL read on a
List and on a variable that holds a file (#28152)
37e5bf71cc59 is described below
commit 37e5bf71cc59862b341f28a07e69b45660060858
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Oct 20 12:32:55 2026 +0200
CAMEL-25871: camel-bean - a hint for an OGNL read on a List and on a
variable that holds a file (#28152)
- ${body.orderId} (or a made-up method such as ${body.find(...)}) on a List
says how to get to an element: by its
index, by a field with jsonpath (resultType java.util.Map), or with split.
- ${variable.customer[name]} on a variable that holds the file as read
(pollEnrich with variableReceive) says to
unmarshal it first, in place with variableSend and variableReceive.
Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
.../apache/camel/language/bean/BeanExpression.java | 45 +++++++++++++++++++---
.../language/simple/SimpleSyntaxHintsTest.java | 37 ++++++++++++++++++
2 files changed, 77 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 53a3c458d5b5..d91e7ef5c00c 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,10 +16,12 @@
*/
package org.apache.camel.language.bean;
+import java.io.File;
import java.io.InputStream;
import java.util.Collection;
import java.util.List;
import java.util.Map;
+import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.apache.camel.BeanScope;
@@ -33,6 +35,7 @@ import org.apache.camel.Predicate;
import org.apache.camel.RuntimeCamelException;
import org.apache.camel.StreamCache;
import org.apache.camel.TypeConverter;
+import org.apache.camel.WrappedFile;
import org.apache.camel.component.bean.BeanComponent;
import org.apache.camel.component.bean.BeanConstants;
import org.apache.camel.component.bean.BeanExpressionProcessor;
@@ -540,15 +543,39 @@ public class BeanExpression implements Expression,
Predicate {
return value;
}
String method = methodHint(methodName,
resultExchange.getException());
- String body = bodyHint(exchange, beanOf(holder, exchange));
+ Object bean = beanOf(holder, exchange);
+ String body = bodyHint(exchange, bean);
+ String list = body == null ? listHint(bean, methodName) : null;
throw new RuntimeBeanExpressionException(
exchange, describeBean(holder, beanName, exchange),
- body != null ? method + " (" + body + ")" :
keyHint(holder, exchange, methodName, method),
+ body != null
+ ? method + " (" + body + ")"
+ : list != null ? method + list : keyHint(holder,
exchange, methodName, method),
resultExchange.getException());
}
return newResult;
}
+ /**
+ * ${body.orderId}, or a method a model makes up such as
${body.find(...)}, on a List: neither is there, so say how
+ * to get to an element: by its index, by a field with jsonpath, or one at
a time with split. The way
+ * {@link #noKeysHint} does for ${body[orderId]}; null for any other bean.
+ */
+ static String listHint(Object bean, String methodName) {
+ if (!(bean instanceof Collection<?>) && !(bean instanceof Object[])) {
+ return null;
+ }
+ String type = bean instanceof List ? "a List" : bean instanceof
Object[] ? "an array" : "a Collection";
+ boolean field = methodName != null &&
FIELD_NAME.matcher(methodName).matches();
+ String name = field ? methodName : "field";
+ return " (" + type + " has no " + (field ? "method or key " +
methodName : "such method")
+ + ": pick an element first, such as [0]." + name + ", or the
element whose " + name
+ + " has a given value with jsonpath, such as jsonpath:
\"$[?(@." + name + " == '${header." + name
+ + "}')]\" with resultType java.util.Map, or split it)";
+ }
+
+ private static final Pattern FIELD_NAME =
Pattern.compile("[A-Za-z_][A-Za-z0-9_]*");
+
/** 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();
@@ -734,11 +761,19 @@ public class BeanExpression implements Expression,
Predicate {
}
/**
- * 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.
+ * What to do when a key is read from a value that has no keys
(CAMEL-25746): the file as read (a variable set from
+ * the body of a file consumer or of pollEnrich), 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 WrappedFile<?> || value instanceof File) {
+ // a variable set from the body of a file consumer or of
pollEnrich holds the file, not its data
+ String type = value instanceof WrappedFile<?> ?
value.getClass().getSimpleName() : "File";
+ return " (the value is the file as read (a " + type + "), not its
data: unmarshal it first, for example with"
+ + " json, then read the key; a variable is unmarshalled in
place with variableSend and variableReceive"
+ + " set to its name, such as unmarshal: {json: {},
variableSend: customer, variableReceive: customer})";
+ }
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)";
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 c7c7c85c2018..3bf213142d2b 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
@@ -235,6 +235,43 @@ public class SimpleSyntaxHintsTest extends
ExchangeTestSupport {
+ " name order-1003.json) to read its fields)");
}
+ @Test
+ public void testOgnlDotOnAListSaysHowToGetToAnElement() {
+ // ${body.orderId} on the List of an unmarshalled JSON array: pick an
element, by index or by a field
+ exchange.getIn().setBody(List.of(java.util.Map.of("orderId",
"ORD-1001"), java.util.Map.of("orderId", "ORD-1002")));
+ Exception e = assertThrows(Exception.class,
+ () ->
context.resolveLanguage("simple").createExpression("${body.orderId}").evaluate(exchange,
+ String.class));
+ assertThat(e.getMessage()).contains("(a List has no method or key
orderId: pick an element first, such as"
+ + " [0].orderId, or the element
whose orderId has a given value with jsonpath,"
+ + " such as jsonpath:
\"$[?(@.orderId == '${header.orderId}')]\" with"
+ + " resultType java.util.Map, or
split it)");
+ }
+
+ @Test
+ public void testAMadeUpMethodOnAListSaysHowToGetToAnElement() {
+ exchange.getIn().setBody(List.of(java.util.Map.of("orderId",
"ORD-1001")));
+ Exception e = assertThrows(Exception.class,
+ () ->
context.resolveLanguage("simple").createExpression("${body.find(o -> o.orderId
== 1)}")
+ .evaluate(exchange, String.class));
+ assertThat(e.getMessage()).contains("(a List has no such method: pick
an element first, such as [0].field");
+ }
+
+ @Test
+ public void testKeyOfAVariableThatHoldsAFileSaysToUnmarshalIt() {
+ // pollEnrich with variableReceive: the variable holds the polled
file, not its data
+ GenericFile<File> file = new GenericFile<>();
+ file.setFile(new File("customers/C-482.json"));
+ file.setFileName("C-482.json");
+ exchange.setVariable("customer", file);
+ Exception e = assertThrows(Exception.class,
+ () ->
context.resolveLanguage("simple").createExpression("${variable.customer[name]}").evaluate(exchange,
+ String.class));
+ assertThat(e.getMessage()).contains("Key: name not found in bean")
+ .contains("(the value is the file as read (a GenericFile), not
its data: unmarshal it first")
+ .contains("unmarshal: {json: {}, variableSend: customer,
variableReceive: customer}");
+ }
+
@Test
public void testOgnlOnAnObjectSaysNothingAboutTheBody() {
// a name an object does not have is a wrong name, not a body of the
wrong type