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");
}
};