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 a535427a9664 CAMEL-25330: camel-groovy - a property read on a body 
that is still text says to unmarshal it first (#27359)
a535427a9664 is described below

commit a535427a9664bd6c0fdbed8381123f8d3079b6d8
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Oct 5 10:34:18 2026 +0200

    CAMEL-25330: camel-groovy - a property read on a body that is still text 
says to unmarshal it first (#27359)
    
    * camel-groovy - a property read on a body that is still text says to 
unmarshal it first
    
    * name the data format from the Content-Type, the file name or the first 
character, not always json
    
    * map the file name to a content type with MimeTypeHelper
    
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
---
 .../camel/language/groovy/GroovyExpression.java    | 87 +++++++++++++++++++++-
 .../groovy/GroovyMissingPropertyHintTest.java      | 65 +++++++++++++++-
 2 files changed, 147 insertions(+), 5 deletions(-)

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 11b7c89fddbe..01855f9e584c 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
@@ -21,6 +21,7 @@ import java.lang.invoke.MethodHandles;
 import java.lang.invoke.MethodType;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
@@ -37,6 +38,7 @@ import org.apache.camel.attachment.DefaultAttachmentMessage;
 import org.apache.camel.support.ExchangeHelper;
 import org.apache.camel.support.ExpressionSupport;
 import org.apache.camel.support.LanguageHelper;
+import org.apache.camel.util.MimeTypeHelper;
 import org.codehaus.groovy.control.CompilationFailedException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -96,10 +98,19 @@ public class GroovyExpression extends ExpressionSupport {
             // the cause and lose the hint
             String name = e.getProperty();
             boolean bean = name != null && 
exchange.getContext().getRegistry().lookupByName(name) != null;
-            String hint = bean
-                    ? "'" + name + "' is a bean in the registry, not a script 
variable; use"
-                      + " exchange.getContext().getRegistry().lookupByName('" 
+ name + "'), or call it from the route with"
-                      + " - bean: {ref: " + name + "}"
+            Object body = exchange.getMessage().getBody();
+            // body.find { it.sku == ... } on the payload text iterates its 
bytes or characters: the body was not
+            // unmarshalled (CAMEL-25330)
+            boolean text = (body instanceof byte[] || body instanceof String) 
&& e.getType() != null
+                    && (e.getType() == Byte.class || e.getType() == 
Character.class || e.getType() == String.class);
+            String hint = text
+                    ? "the body is still text (a " + (body instanceof byte[] ? 
"byte[]" : "String")
+                      + "), not parsed data: " + unmarshalHint(exchange, body)
+                    : bean
+                            ? "'" + name + "' is a bean in the registry, not a 
script variable; use"
+                              + " 
exchange.getContext().getRegistry().lookupByName('" + name
+                              + "'), or call it from the route with"
+                              + " - bean: {ref: " + name + "}"
                     : "the script variables are " + SCRIPT_VARIABLES_HINT;
             throw new groovy.lang.MissingPropertyException(
                     e.getMessageWithoutLocationText() + " (" + hint + ")", 
name, e.getType());
@@ -340,4 +351,72 @@ public class GroovyExpression extends ExpressionSupport {
             }
         }
     }
+
+    /**
+     * Which data format turns the payload text into data, from the 
Content-Type, the file name (by its extension, as
+     * {@link MimeTypeHelper} maps it to a content type), or the first 
character of the text; the generic advice when
+     * none of them tells.
+     */
+    static String unmarshalHint(Exchange exchange, Object body) {
+        String type = exchange.getMessage().getHeader(Exchange.CONTENT_TYPE, 
String.class);
+        String file = exchange.getMessage().getHeader(Exchange.FILE_NAME, 
String.class);
+        String format = formatOf(type);
+        if (format == null && file != null) {
+            format = formatOf(MimeTypeHelper.probeMimeType(file));
+        }
+        if (format == null) {
+            int first = firstNonBlank(body);
+            if (first == '{' || first == '[') {
+                format = "json";
+            } else if (first == '<') {
+                format = "xml";
+            }
+        }
+        if ("json".equals(format)) {
+            return "unmarshal it first (unmarshal: json) to read its fields";
+        } else if ("xml".equals(format)) {
+            return "unmarshal it first (unmarshal: jacksonXml) to read its 
fields, or read them with xpath";
+        } else if ("csv".equals(format)) {
+            return "unmarshal it first (unmarshal: csv) to read its rows: a 
List of rows, or a Map per row with"
+                   + " useMaps: true";
+        }
+        return "unmarshal it first with the data format of the payload (json, 
jacksonXml, csv, ...) to read its fields";
+    }
+
+    /** The kind of payload a content type is: json, xml or csv (tab-separated 
values included), or null. */
+    private static String formatOf(String contentType) {
+        if (contentType == null) {
+            return null;
+        }
+        String s = contentType.toLowerCase(Locale.ROOT);
+        int semi = s.indexOf(';');
+        if (semi > 0) {
+            s = s.substring(0, semi).trim();
+        }
+        if (s.endsWith("/json") || s.endsWith("+json")) {
+            return "json";
+        } else if (s.endsWith("/xml") || s.endsWith("+xml")) {
+            return "xml";
+        } else if (s.endsWith("/csv") || s.endsWith("/tab-separated-values")) {
+            return "csv";
+        }
+        return null;
+    }
+
+    private static int firstNonBlank(Object body) {
+        if (body instanceof byte[] bytes) {
+            for (int i = 0; i < bytes.length && i < 256; i++) {
+                if (!Character.isWhitespace(bytes[i])) {
+                    return bytes[i];
+                }
+            }
+        } else if (body instanceof String text) {
+            for (int i = 0; i < text.length() && i < 256; i++) {
+                if (!Character.isWhitespace(text.charAt(i))) {
+                    return text.charAt(i);
+                }
+            }
+        }
+        return -1;
+    }
 }
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 dd9f76b72f85..1e7d234180fb 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
@@ -17,6 +17,7 @@
 package org.apache.camel.processor.groovy;
 
 import groovy.lang.MissingPropertyException;
+import org.apache.camel.Exchange;
 import org.apache.camel.RoutesBuilder;
 import org.apache.camel.builder.RouteBuilder;
 import org.apache.camel.test.junit6.CamelTestSupport;
@@ -29,7 +30,8 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
 
 /**
  * A groovy script that uses a bean name as a variable, or an unknown 
variable, gets a MissingPropertyException whose
- * message says where the beans are and what the script variables are 
(CAMEL-24698).
+ * message says where the beans are and what the script variables are 
(CAMEL-24698). A field read on a body that is
+ * still the payload text says to unmarshal it first (CAMEL-25330).
  */
 public class GroovyMissingPropertyHintTest extends CamelTestSupport {
 
@@ -51,6 +53,66 @@ public class GroovyMissingPropertyHintTest extends 
CamelTestSupport {
                 cause.getMessage());
     }
 
+    @Test
+    public void fieldReadOnJsonBytesSaysToUnmarshal() {
+        byte[] json = "[{\"sku\": \"A1\"}]".getBytes();
+        Exception e = assertThrows(Exception.class,
+                () -> template.sendBodyAndHeader("direct:field", json, "sku", 
"A1"));
+        MissingPropertyException cause = 
assertInstanceOf(MissingPropertyException.class, e.getCause());
+        assertTrue(cause.getMessage().contains("the body is still text (a 
byte[])"), cause.getMessage());
+        assertTrue(cause.getMessage().contains("unmarshal: json"), 
cause.getMessage());
+    }
+
+    @Test
+    public void fieldReadOnJsonStringSaysToUnmarshal() {
+        Exception e = assertThrows(Exception.class,
+                () -> template.sendBodyAndHeader("direct:field", "[{\"sku\": 
\"A1\"}]", "sku", "A1"));
+        MissingPropertyException cause = 
assertInstanceOf(MissingPropertyException.class, e.getCause());
+        assertTrue(cause.getMessage().contains("the body is still text (a 
String)"), cause.getMessage());
+    }
+
+    @Test
+    public void fieldReadOnXmlTextSaysJacksonXml() {
+        Exception e = assertThrows(Exception.class,
+                () -> template.sendBodyAndHeader("direct:field", 
"<order><sku>A1</sku></order>", "sku", "A1"));
+        MissingPropertyException cause = 
assertInstanceOf(MissingPropertyException.class, e.getCause());
+        assertTrue(cause.getMessage().contains("unmarshal: jacksonXml"), 
cause.getMessage());
+    }
+
+    @Test
+    public void fieldReadOnCsvFileSaysCsv() {
+        Exception e = assertThrows(Exception.class,
+                () -> template.sendBodyAndHeaders("direct:field", 
"sku,qty\nA1,2".getBytes(),
+                        java.util.Map.of("sku", "A1", Exchange.FILE_NAME, 
"orders.csv")));
+        MissingPropertyException cause = 
assertInstanceOf(MissingPropertyException.class, e.getCause());
+        assertTrue(cause.getMessage().contains("unmarshal: csv"), 
cause.getMessage());
+    }
+
+    @Test
+    public void fieldReadOnJsonContentTypeSaysJson() {
+        Exception e = assertThrows(Exception.class,
+                () -> template.sendBodyAndHeaders("direct:field", "sku=A1",
+                        java.util.Map.of("sku", "A1", Exchange.CONTENT_TYPE, 
"application/json; charset=UTF-8")));
+        MissingPropertyException cause = 
assertInstanceOf(MissingPropertyException.class, e.getCause());
+        assertTrue(cause.getMessage().contains("unmarshal: json"), 
cause.getMessage());
+    }
+
+    @Test
+    public void fieldReadOnUnknownTextNamesTheChoices() {
+        Exception e = assertThrows(Exception.class,
+                () -> template.sendBodyAndHeader("direct:field", "sku=A1", 
"sku", "A1"));
+        MissingPropertyException cause = 
assertInstanceOf(MissingPropertyException.class, e.getCause());
+        assertTrue(cause.getMessage().contains("the data format of the payload 
(json, jacksonXml, csv, ...)"),
+                cause.getMessage());
+    }
+
+    @Test
+    public void fieldReadOnParsedBodyWorks() {
+        Object out = template.requestBodyAndHeader("direct:field", 
java.util.List.of(java.util.Map.of("sku", "A1")),
+                "sku", "A1");
+        assertEquals(java.util.Map.of("sku", "A1"), out);
+    }
+
     @Test
     public void messageIsAScriptVariableAsTheHintSays() {
         String out = template.requestBodyAndHeader("direct:message", "World", 
"name", "Hello", String.class);
@@ -64,6 +126,7 @@ public class GroovyMissingPropertyHintTest extends 
CamelTestSupport {
             public void configure() {
                 
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:message").transform().groovy("message.getHeader('name') + ' ' + 
message.body");
             }
         };

Reply via email to