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