This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch CAMEL-25928-r22-body-type-gaps in repository https://gitbox.apache.org/repos/asf/camel.git
commit 762c8c46d8b8a304e46fa7af4cea2ab85ebc22b0 Author: Claus Ibsen <[email protected]> AuthorDate: Mon Oct 26 09:37:43 2026 +0100 CAMEL-25928: camel-support, camel-bean, camel-groovy - runtime hints for an unmarshal of data, a field read after a marshal, and a file in a variable - An unmarshal of a Map or a List says the body (or the variable) is data already, instead of the bare type converter error. - A field read on text that a marshal wrote from parsed data says to read the field before the marshal, as the message history tells, instead of "unmarshal it first". - A key read on the payload of the body gets one hint, from what the body is, not a second one that disagrees. - ${variable.customer.name} on a variable that holds the file as read says to unmarshal it in place, as the [name] form already did. - The Groovy hint "still text ... unmarshal it first" is given only when the body is text: on parsed data it says the value read is a text value of the data. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../apache/camel/language/bean/BeanExpression.java | 23 +++++++++-- .../camel/language/groovy/GroovyExpression.java | 10 +++++ .../groovy/GroovyMissingPropertyHintTest.java | 26 ++++++++++++- .../language/simple/SimpleSyntaxHintsTest.java | 18 +++++++++ .../apache/camel/language/simple/SimpleTest.java | 4 +- .../camel/processor/UnmarshalProcessorTest.java | 37 ++++++++++++++++++ .../org/apache/camel/support/BodyFactsTest.java | 36 +++++++++++++++++ .../java/org/apache/camel/support/BodyFacts.java | 45 +++++++++++++++++++++- .../support/processor/UnmarshalProcessor.java | 31 ++++++++++++++- 9 files changed, 221 insertions(+), 9 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 1e51db17bef9..8a83f96c09f8 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 @@ -571,11 +571,15 @@ public class BeanExpression implements Expression, Predicate { Object bean = beanOf(holder, exchange); String body = bodyHint(exchange, bean); String list = body == null ? listHint(bean, methodName) : null; + // ${variable.customer.name} on a variable that holds the file as read + String file = body == null && (bean instanceof WrappedFile<?> || bean instanceof File) + ? noKeysHint(bean, methodName) : null; throw new RuntimeBeanExpressionException( exchange, describeBean(holder, beanName, exchange), body != null ? method + " (" + body + ")" - : list != null ? method + list : keyHint(holder, exchange, methodName, method), + : list != null ? method + list + : file != null ? method + file : keyHint(holder, exchange, methodName, method), resultExchange.getException()); } return newResult; @@ -692,8 +696,12 @@ public class BeanExpression implements Expression, Predicate { } if (key != null) { Object bean = holder.getBean(exchange); + // the payload of the message body is told by bodyHint below, from what the message history knows + Object value = result != null ? result : bean; + boolean payloadBody = bean != null && bean == exchange.getMessage().getBody() && value == bean + && isPayload(value); try { - return lookupResult(resultExchange, key, result, nullSafe, ognlPath, bean); + return lookupResult(resultExchange, key, result, nullSafe, ognlPath, bean, !payloadBody); } catch (IndexOutOfBoundsException e) { // ${body[status]} on the file as read, or on a stream: the same exception, saying what the body is // and the step that parses it (CAMEL-25749) @@ -730,8 +738,15 @@ public class BeanExpression implements Expression, Predicate { } } + /** Text, bytes, a stream or the file as read: what an unmarshal turns into data. */ + private static boolean isPayload(Object value) { + return value instanceof WrappedFile<?> || value instanceof File || value instanceof StreamCache + || value instanceof InputStream || value instanceof byte[] || value instanceof String; + } + private static Object lookupResult( - Exchange exchange, String key, Object result, boolean nullSafe, String ognlPath, Object bean) { + Exchange exchange, String key, Object result, boolean nullSafe, String ognlPath, Object bean, + boolean hint) { StringHelper.notEmpty(key, "key", "in Simple language ognl path: " + ognlPath); final TypeConverter typeConverter = exchange.getContext().getTypeConverter(); @@ -786,7 +801,7 @@ public class BeanExpression implements Expression, Predicate { throw new IndexOutOfBoundsException( "Key: " + key + " not found in bean: " + bean + " of type: " + ObjectHelper.classCanonicalName(bean) + " using OGNL path [" + ognlPath - + "]" + noKeysHint(result != null ? result : bean, key)); + + "]" + (hint ? noKeysHint(result != null ? result : bean, key) : "")); } else { // null safe so we can return null return null; 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 1631050895c6..7cee45ee440c 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 @@ -34,6 +34,7 @@ import org.apache.camel.Message; import org.apache.camel.RuntimeCamelException; import org.apache.camel.attachment.AttachmentMessage; import org.apache.camel.attachment.DefaultAttachmentMessage; +import org.apache.camel.spi.BodyTypeRegistry; import org.apache.camel.support.BodyFacts; import org.apache.camel.support.ExchangeHelper; import org.apache.camel.support.ExpressionSupport; @@ -107,6 +108,10 @@ public class GroovyExpression extends ExpressionSupport { // (CAMEL-25749); null for an object, where the name read is wrong String payload = !onText && body != null && e.getType() == body.getClass() && !(body instanceof Map) ? BodyFacts.hint(exchange, BodyFacts.Need.DATA) : null; + // customer.customerNo on parsed data, where customer holds the text C-482: the text is a value of the + // data, and the body needs no unmarshal + BodyTypeRegistry.Kind kind = BodyFacts.kindOf(body); + boolean bodyData = kind == BodyTypeRegistry.Kind.DATA || kind == BodyTypeRegistry.Kind.COLLECTION; String hint; if (bodyText) { // what the body is, the step that set it, and the unmarshal that parses it (CAMEL-25749) @@ -119,6 +124,11 @@ public class GroovyExpression extends ExpressionSupport { 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 && bodyData && !bean) { + String keys = BodyFacts.hint(exchange, BodyFacts.Need.DATA); + hint = "the value read here is text (a " + e.getType().getSimpleName() + ") in the body, not an object" + + " with fields: " + BodyFacts.describe(exchange) + ", which is data already, so no unmarshal is" + + " needed; check the name read, and the value it is read on" + (keys != null ? " (" + keys + ")" : ""); } 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)," 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 b24bbd5b4d71..346246d740c3 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 @@ -18,6 +18,8 @@ package org.apache.camel.processor.groovy; import java.io.ByteArrayInputStream; import java.nio.charset.StandardCharsets; +import java.util.LinkedHashMap; +import java.util.Map; import groovy.lang.MissingPropertyException; import org.apache.camel.Exchange; @@ -28,6 +30,7 @@ import org.apache.camel.test.junit6.CamelTestSupport; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -139,12 +142,30 @@ public class GroovyMissingPropertyHintTest extends CamelTestSupport { @Test public void fieldReadOnTextElsewhereSaysItIsText() { - // a list of plain text values: an element is text, not data with fields + // a list of plain text values: an element is text, not data with fields, and the body needs no unmarshal 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)"), + assertTrue(cause.getMessage().contains("the value read here is text (a String) in the body, not an object with" + + " fields: the body is a List"), cause.getMessage()); + assertTrue(cause.getMessage().contains("which is data already, so no unmarshal is needed"), cause.getMessage()); + assertFalse(cause.getMessage().contains("unmarshal it first"), cause.getMessage()); + } + + @Test + public void fieldReadOnATextFieldOfParsedDataSaysTheBodyIsParsed() { + // what the benchmark wrote (r22): customer holds the customer number, and customer.customerNo reads it + Exception e = assertThrows(Exception.class, + () -> template.sendBody("direct:customer", new LinkedHashMap<>(Map.of("customer", "C-482")))); + MissingPropertyException cause = assertInstanceOf(MissingPropertyException.class, e.getCause()); + assertTrue(cause.getMessage().contains("No such property: customerNo for class: java.lang.String"), + cause.getMessage()); + assertTrue(cause.getMessage().contains("the value read here is text (a String) in the body, not an object with" + + " fields: the body is a Map (LinkedHashMap), which is data already"), + cause.getMessage()); + assertTrue(cause.getMessage().contains("(its keys are: customer)"), cause.getMessage()); + assertFalse(cause.getMessage().contains("unmarshal it first"), cause.getMessage()); } @Test @@ -169,6 +190,7 @@ public class GroovyMissingPropertyHintTest extends CamelTestSupport { from("direct:unknown").transform().groovy("nosuch.toUpperCase()"); from("direct:field").transform().groovy("body.find { it.sku == headers.sku }"); from("direct:read").transform().groovy("body.sku"); + from("direct:customer").transform().groovy("body.customer.customerNo"); from("direct:header").transform().groovy("headers.skuList.find { it.sku == headers.sku }"); from("direct:message").transform().groovy("message.getHeader('name') + ' ' + message.body"); } 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 e8c6096db880..22239384ac3c 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 @@ -324,6 +324,24 @@ public class SimpleSyntaxHintsTest extends ExchangeTestSupport { .contains("unmarshal: {json: {}, variableSend: customer, variableReceive: customer}"); } + @Test + public void testMethodOfAVariableThatHoldsAFileSaysToUnmarshalIt() { + // what the benchmark wrote (r22): ${variable.customer.name} reads the polled file, not the customer + 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("(the value is the file as read (a GenericFile), not its data: unmarshal it" + + " first") + .contains("unmarshal: {json: {}, variableSend: customer, variableReceive: customer}"); + // what the file has is read as it is + assertEquals("C-482.json", context.resolveLanguage("simple").createExpression("${variable.customer.fileName}") + .evaluate(exchange, String.class)); + } + @Test public void testOgnlOnAnObjectSaysNothingAboutTheBody() { // a name an object does not have is a wrong name, not a body of the wrong type diff --git a/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleTest.java b/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleTest.java index 1ec074ef84cc..d4d38598eaf4 100644 --- a/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleTest.java +++ b/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleTest.java @@ -531,8 +531,10 @@ public class SimpleTest extends LanguageTestSupport { IndexOutOfBoundsException cause = assertIsInstanceOf(IndexOutOfBoundsException.class, e.getCause()); assertTrue(cause.getMessage().startsWith("Key: qty not found in bean: "), cause.getMessage()); - assertTrue(cause.getMessage().contains("the value is the message body as a stream, not a Map: unmarshal it first"), + // what the body is says it once: no second hint about the value + assertTrue(cause.getMessage().contains("(the body is a stream (ByteArrayInputStream): unmarshal it first"), cause.getMessage()); + assertFalse(cause.getMessage().contains("the value is the message body as a stream"), cause.getMessage()); } @Test diff --git a/core/camel-core/src/test/java/org/apache/camel/processor/UnmarshalProcessorTest.java b/core/camel-core/src/test/java/org/apache/camel/processor/UnmarshalProcessorTest.java index 57c4c9d906c1..d6ac1b06f24c 100644 --- a/core/camel-core/src/test/java/org/apache/camel/processor/UnmarshalProcessorTest.java +++ b/core/camel-core/src/test/java/org/apache/camel/processor/UnmarshalProcessorTest.java @@ -18,6 +18,10 @@ package org.apache.camel.processor; import java.io.InputStream; import java.io.OutputStream; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; import org.apache.camel.CamelContext; import org.apache.camel.Exchange; @@ -135,6 +139,39 @@ public class UnmarshalProcessorTest extends TestSupport { exchange.getException().getMessage()); } + @Test + public void testParsedDataSaysItIsDataAlready() throws Exception { + // what the benchmark wrote (r22): the order was unmarshalled, and is unmarshalled again + Exchange exchange = createExchangeWithBody(new DefaultCamelContext(), new LinkedHashMap<>(Map.of("orderId", "1"))); + Processor processor = new UnmarshalProcessor(new MyDataFormat(new Object())); + + processor.process(exchange); + + NoTypeConversionAvailableException e + = assertInstanceOf(NoTypeConversionAvailableException.class, exchange.getException()); + assertEquals("Cannot unmarshal using MyDataFormat: the body is a Map (LinkedHashMap), which is data already, and" + + " the data format reads text: read its fields as they are, and leave this unmarshal out; to parse the" + + " text in a variable, unmarshal it with variableSend and variableReceive set to its name", + e.getMessage()); + assertEquals(InputStream.class, e.getToType()); + assertInstanceOf(NoTypeConversionAvailableException.class, e.getCause()); + } + + @Test + public void testParsedDataInAVariableSaysItIsDataAlready() throws Exception { + Exchange exchange = createExchangeWithBody(new DefaultCamelContext(), "body"); + exchange.setVariable("order", new ArrayList<>(List.of("A1"))); + UnmarshalProcessor processor = new UnmarshalProcessor(new MyDataFormat(new Object())); + processor.setVariableSend("order"); + + processor.process(exchange); + + assertEquals("Cannot unmarshal using MyDataFormat: the variable order holds data (ArrayList) already, and the data" + + " format reads text: read its fields as they are, such as ${variable.order[name]}, and leave this" + + " unmarshal out", + exchange.getException().getMessage()); + } + @Test public void testBodyThatCannotBeConvertedKeepsTheConverterMessage() throws Exception { Exchange exchange = createExchangeWithBody(new DefaultCamelContext(), new Object()); diff --git a/core/camel-core/src/test/java/org/apache/camel/support/BodyFactsTest.java b/core/camel-core/src/test/java/org/apache/camel/support/BodyFactsTest.java index 35b821925785..04490b33e180 100644 --- a/core/camel-core/src/test/java/org/apache/camel/support/BodyFactsTest.java +++ b/core/camel-core/src/test/java/org/apache/camel/support/BodyFactsTest.java @@ -17,6 +17,8 @@ package org.apache.camel.support; import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Iterator; @@ -30,7 +32,9 @@ import org.apache.camel.ContextTestSupport; import org.apache.camel.Exchange; import org.apache.camel.builder.RouteBuilder; import org.apache.camel.spi.BodyTypeRegistry.Kind; +import org.apache.camel.spi.DataFormat; import org.apache.camel.support.BodyFacts.Need; +import org.apache.camel.support.service.ServiceSupport; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -59,6 +63,8 @@ public class BodyFactsTest extends ContextTestSupport { from("direct:unchanged").process(e -> seen.add(BodyFacts.describe(e))); from("direct:setBy").setBody(constant(new LinkedHashMap<>(Map.of("sku", "A1")))).log("next") .process(e -> seen.add(BodyFacts.describe(e))); + from("direct:marshalled").marshal(new TextDataFormat()) + .process(e -> seen.add(BodyFacts.describe(e) + ": " + BodyFacts.hint(e, Need.DATA))); } }; } @@ -76,6 +82,22 @@ public class BodyFactsTest extends ContextTestSupport { assertTrue(seen.get(0).startsWith("the body is a Map (LinkedHashMap), set by setBody"), seen.get(0)); } + @Test + public void testAFieldReadAfterAMarshalOfData() { + // what the benchmark wrote (r22): the ticket is marshalled for a file, and a field is read after it + template.sendBody("direct:marshalled", new LinkedHashMap<>(Map.of("ticketId", "T-1"))); + assertEquals(1, seen.size()); + assertTrue(seen.get(0).contains(", set by marshal"), seen.get(0)); + assertTrue(seen.get(0).contains("wrote it from data (LinkedHashMap): read the field before the marshal, for" + + " example into a header with setHeader, or keep the data in the body and" + + " marshal into a variable"), + seen.get(0)); + // a marshal of text: unmarshal was meant + seen.clear(); + template.sendBody("direct:marshalled", "{\"ticketId\": \"T-1\"}"); + assertTrue(seen.get(0).contains("unmarshal it first"), seen.get(0)); + } + @Test public void testTheBodyInPlainWords() { assertEquals("null", BodyFacts.words(null)); @@ -191,6 +213,20 @@ public class BodyFactsTest extends ContextTestSupport { assertNull(BodyFacts.dataFormatOf(null)); } + /** Writes the body as its text. */ + private static final class TextDataFormat extends ServiceSupport implements DataFormat { + + @Override + public void marshal(Exchange exchange, Object graph, OutputStream stream) throws Exception { + stream.write(String.valueOf(graph).getBytes(StandardCharsets.UTF_8)); + } + + @Override + public Object unmarshal(Exchange exchange, InputStream stream) { + throw new UnsupportedOperationException(); + } + } + private Exchange exchange(Object body) { Exchange answer = new DefaultExchange(context); answer.getMessage().setBody(body); diff --git a/core/camel-support/src/main/java/org/apache/camel/support/BodyFacts.java b/core/camel-support/src/main/java/org/apache/camel/support/BodyFacts.java index ef8584393b57..44153b6ed59c 100644 --- a/core/camel-support/src/main/java/org/apache/camel/support/BodyFacts.java +++ b/core/camel-support/src/main/java/org/apache/camel/support/BodyFacts.java @@ -72,6 +72,12 @@ import org.apache.camel.util.URISupport; * </tr> * <tr> * <td>{@link Need#DATA}</td> + * <td>text, bytes or a stream a marshal wrote from a Map or a List, as the message history says</td> + * <td>read the field before the marshal, or marshal into a variable: an unmarshal would undo the marshal a later step + * needs</td> + * </tr> + * <tr> + * <td>{@link Need#DATA}</td> * <td>null, or empty text or bytes</td> * <td>as for {@link Need#A_BODY}</td> * </tr> @@ -214,7 +220,8 @@ public final class BodyFacts { return readTheDataFirst(); } if (isPayload(body)) { - return unmarshalFirst(exchange, body); + String marshal = marshalledData(exchange, body); + return marshal != null ? marshal : unmarshalFirst(exchange, body); } if (body instanceof Map<?, ?> map) { if (map.isEmpty()) { @@ -277,6 +284,42 @@ public final class BodyFacts { }; } + /** + * When a marshal wrote the body from a Map or a List, as the message history says: the marshal is wanted, for a + * later step that needs text (a file write), and the field is read after it, too late; an unmarshal would undo it. + * Null when the history does not say so, as for a marshal of text, where an unmarshal was meant. + */ + private static String marshalledData(Exchange exchange, Object body) { + List<MessageHistory> history = exchange.getProperty(ExchangePropertyKey.MESSAGE_HISTORY, List.class); + if (history == null || history.isEmpty()) { + return null; + } + String now = ObjectHelper.classCanonicalName(body); + for (int i = history.size() - 1; i >= 0; i--) { + MessageHistory step = history.get(i); + String type = step.getBodyType(); + if (type == null || type.equals(now)) { + continue; + } + // the step that set the body, and the body it was reached with + if (step.getNode() == null || !"marshal".equals(step.getNode().getShortName()) + || !isDataType(exchange.getContext(), type)) { + return null; + } + return "the " + step(step.getNode()) + " wrote it from data (" + StringHelper.afterLast(type, ".", type) + + "): read the field before the marshal, for example into a header with setHeader, or keep the data" + + " in the body and marshal into a variable (marshal with variableReceive, and send that variable with" + + " variableSend)"; + } + return null; + } + + /** Whether a class, by its name, is a Map or a collection. */ + private static boolean isDataType(CamelContext context, String type) { + Class<?> clazz = context != null ? context.getClassResolver().resolveClass(type) : null; + return clazz != null && (Map.class.isAssignableFrom(clazz) || Collection.class.isAssignableFrom(clazz)); + } + private static String readTheDataFirst() { return "read the data first - setBody with constant: resource:file:data.json for a known file, or poll:," + " pollEnrich or a from: consumer for one that is not (a route reached with direct: has the body of its" diff --git a/core/camel-support/src/main/java/org/apache/camel/support/processor/UnmarshalProcessor.java b/core/camel-support/src/main/java/org/apache/camel/support/processor/UnmarshalProcessor.java index 056c8fb13bf0..f34374e4099c 100644 --- a/core/camel-support/src/main/java/org/apache/camel/support/processor/UnmarshalProcessor.java +++ b/core/camel-support/src/main/java/org/apache/camel/support/processor/UnmarshalProcessor.java @@ -17,7 +17,9 @@ package org.apache.camel.support.processor; import java.io.InputStream; +import java.util.Collection; import java.util.Iterator; +import java.util.Map; import org.apache.camel.AsyncCallback; import org.apache.camel.CamelContext; @@ -124,7 +126,7 @@ public class UnmarshalProcessor extends AsyncProcessorSupport } catch (Exception e) { // remove OUT message, as an exception occurred exchange.setOut(null); - exchange.setException(body == null ? nullBodyException(exchange, e) : e); + exchange.setException(body == null ? nullBodyException(exchange, e) : parsedDataException(exchange, body, e)); } finally { // The Iterator will close the stream itself if (!(result instanceof Iterator)) { @@ -158,6 +160,33 @@ public class UnmarshalProcessor extends AsyncProcessorSupport return answer; } + /** + * A Map or a List fails in the type converter too, as the data format reads text: "No type converter available to + * convert from type: java.util.LinkedHashMap to the required type: java.io.InputStream". Say what it means here: + * the body, or the variable, is data already, and a variable is unmarshalled with variableSend and variableReceive. + * The exception type stays the same, so an onException for it still matches. + */ + private Exception parsedDataException(Exchange exchange, Object body, Exception e) { + if (!(e instanceof NoTypeConversionAvailableException ntc) || !(body instanceof Map) && !(body instanceof Collection)) { + return e; + } + String name + = dataFormat instanceof DataFormatName dfn ? dfn.getDataFormatName() : dataFormat.getClass().getSimpleName(); + String message = variableSend != null + ? "Cannot unmarshal using " + name + ": the variable " + variableSend + " holds data (" + + body.getClass().getSimpleName() + + ") already, and the data format reads text: read its fields as they are, such as" + + " ${variable." + variableSend + "[name]}, and leave this unmarshal out" + : "Cannot unmarshal using " + name + ": " + BodyFacts.describe(exchange) + + ", which is data already, and the data format reads text: read its fields as they are, and leave" + + " this unmarshal out; to parse the text in a variable, unmarshal it with variableSend and" + + " variableReceive set to its name"; + NoTypeConversionAvailableException answer + = new NoTypeConversionAvailableException(message, body, ntc.getToType()); + answer.initCause(e); + return answer; + } + @Override public String toString() { return "Unmarshal[" + dataFormat + "]";
