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 e1c5321df039 CAMEL-25330: the Groovy hint also names JSON text in a 
header (#27375)
e1c5321df039 is described below

commit e1c5321df03951701eff9b6b46042eba96675c7b
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Oct 5 14:56:03 2026 +0200

    CAMEL-25330: the Groovy hint also names JSON text in a header (#27375)
    
    CAMEL-25330: the Groovy hint also names JSON text in a header, and text 
iterated anywhere
    
    Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
 .../camel/language/groovy/GroovyExpression.java    | 60 +++++++++++++++++-----
 .../groovy/GroovyMissingPropertyHintTest.java      | 25 ++++++++-
 2 files changed, 72 insertions(+), 13 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 01855f9e584c..b8411e77a3d6 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
@@ -100,18 +100,31 @@ public class GroovyExpression extends ExpressionSupport {
             boolean bean = name != null && 
exchange.getContext().getRegistry().lookupByName(name) != null;
             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;
+            // unmarshalled (CAMEL-25330). The same with JSON text in a 
header: headers.items.find { ... }
+            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;
+            String hint;
+            if (bodyText) {
+                hint = "the body is still text (a " + (body instanceof byte[] 
? "byte[]" : "String")
+                       + "), not parsed data: " + unmarshalHint(exchange, 
body);
+            } 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"
+                       + " new groovy.json.JsonSlurper().parseText(" + ref + 
"), or set the header from the body after"
+                       + " unmarshal: json";
+            } else if (onText && !bean) {
+                hint = "the value read here is still text (a " + 
e.getType().getSimpleName()
+                       + " comes from iterating text), not parsed data: 
unmarshal it first (unmarshal: json for JSON),"
+                       + " or parse it in the script with new 
groovy.json.JsonSlurper().parseText(...)";
+            } else if (bean) {
+                hint = "'" + 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 + "}";
+            } else {
+                hint = "the script variables are " + SCRIPT_VARIABLES_HINT;
+            }
             throw new groovy.lang.MissingPropertyException(
                     e.getMessageWithoutLocationText() + " (" + hint + ")", 
name, e.getType());
         }
@@ -403,6 +416,29 @@ public class GroovyExpression extends ExpressionSupport {
         return null;
     }
 
+    /** The first header that holds JSON text (a String or byte[] starting 
with { or [), or null. */
+    private static String jsonTextHeader(Exchange exchange) {
+        for (Map.Entry<String, Object> header : 
exchange.getMessage().getHeaders().entrySet()) {
+            int first = firstNonBlank(header.getValue());
+            if (first == '{' || first == '[') {
+                return header.getKey();
+            }
+        }
+        return null;
+    }
+
+    private static boolean isIdentifier(String name) {
+        if (name.isEmpty() || 
!Character.isJavaIdentifierStart(name.charAt(0))) {
+            return false;
+        }
+        for (int i = 1; i < name.length(); i++) {
+            if (!Character.isJavaIdentifierPart(name.charAt(i))) {
+                return false;
+            }
+        }
+        return true;
+    }
+
     private static int firstNonBlank(Object body) {
         if (body instanceof byte[] bytes) {
             for (int i = 0; i < bytes.length && i < 256; i++) {
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 1e7d234180fb..72dd5a770858 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
@@ -31,7 +31,7 @@ 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). A field read on a body that is
- * still the payload text says to unmarshal it first (CAMEL-25330).
+ * still the payload text says to unmarshal it first, and JSON text in a 
header is named (CAMEL-25330).
  */
 public class GroovyMissingPropertyHintTest extends CamelTestSupport {
 
@@ -106,6 +106,28 @@ public class GroovyMissingPropertyHintTest extends 
CamelTestSupport {
                 cause.getMessage());
     }
 
+    @Test
+    public void fieldReadOnJsonTextInAHeaderNamesTheHeader() {
+        // the benchmark wrote headers.skuList.find { ... } with the stock 
list kept as text in a header
+        Exception e = assertThrows(Exception.class,
+                () -> template.sendBodyAndHeaders("direct:header", 
java.util.List.of("x"),
+                        java.util.Map.of("sku", "A1", "skuList", "[{\"sku\": 
\"A1\"}]")));
+        MissingPropertyException cause = 
assertInstanceOf(MissingPropertyException.class, e.getCause());
+        assertTrue(cause.getMessage().contains("header 'skuList' is still JSON 
text, not parsed data"), cause.getMessage());
+        assertTrue(cause.getMessage().contains("new 
groovy.json.JsonSlurper().parseText(headers.skuList)"),
+                cause.getMessage());
+    }
+
+    @Test
+    public void fieldReadOnTextElsewhereSaysItIsText() {
+        // a list of plain text values: an element is text, not data with 
fields
+        Exception e = assertThrows(Exception.class,
+                () -> template.sendBodyAndHeader("direct:field", 
java.util.List.of("A1", "B2"), "sku", "A1"));
+        MissingPropertyException cause = 
assertInstanceOf(MissingPropertyException.class, e.getCause());
+        assertTrue(cause.getMessage().contains("the value read here is still 
text (a String comes from iterating text)"),
+                cause.getMessage());
+    }
+
     @Test
     public void fieldReadOnParsedBodyWorks() {
         Object out = template.requestBodyAndHeader("direct:field", 
java.util.List.of(java.util.Map.of("sku", "A1")),
@@ -127,6 +149,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:header").transform().groovy("headers.skuList.find 
{ it.sku == headers.sku }");
                 
from("direct:message").transform().groovy("message.getHeader('name') + ' ' + 
message.body");
             }
         };

Reply via email to