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

Reply via email to