You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by da...@apache.org on 2026/09/13 16:15:08 UTC
(camel) branch main updated: CAMEL-24698: runtime messages that say what to write: missing expression, xslt and file null body, groovy missing property, POJO aggregation strategy null body, timer header metadata (#26368)
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 1bf9ea3e9151 CAMEL-24698: runtime messages that say what to write: missing expression, xslt and file null body, groovy missing property, POJO aggregation strategy null body, timer header metadata (#26368)
1bf9ea3e9151 is described below
commit 1bf9ea3e91517eb81e0b993c16f16ffc593ea801
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Sun Sep 13 18:15:01 2026 +0200
CAMEL-24698: runtime messages that say what to write: missing expression, xslt and file null body, groovy missing property, POJO aggregation strategy null body, timer header metadata (#26368)
* CAMEL-24698: runtime messages that say what to write: missing expression, xslt/file null body, groovy missing property, POJO aggregation strategy null body, timer header metadata
Squash of the 6 commits on fix/CAMEL-24698-missing-expression (the run-by-run history is on bench/after-run).
camel-core-reifier - an expression node whose expression is missing says so instead of 'Unsupported definition: null'
camel-xslt - a null body says nothing was read or set before the xslt step
camel-file - 'Cannot write null body to file' says a to: file: writes the body and how a file is read
camel-groovy - a bean name used as a script variable says it is a bean in the registry and how to reach it
camel-timer - CamelTimerCounter, CamelTimerName, CamelTimerPeriod and CamelTimerTime are in the component's header metadata
camel-core-processor - a POJO aggregation strategy whose typed parameter meets a message without a body says so
Co-Authored-By: Claude Fable 5.1 <no...@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bp3538HRBPMQkb5ta9xRaj
* CAMEL-24698: camel-catalog, camel-endpointdsl - regenerated timer metadata and endpoint builder for the timer headers
The generated copies the CI build reported as uncommitted, applied from its diff.
Co-Authored-By: Claude Fable 5.1 <no...@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bp3538HRBPMQkb5ta9xRaj
* CAMEL-24698: camel-xslt, camel-file - the null-body hints keep the exception types and the message shape
XsltBuilderTest expects ExpectedBodyTypeException (it now takes a hint, appended to its message), and
FileProducerAllowNullBodyTest expects the message to end with the file name.
Co-Authored-By: Claude Fable 5.1 <no...@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bp3538HRBPMQkb5ta9xRaj
* CAMEL-24698: camel-timer - the timer's counter, name, period and time are exchange properties, not headers: header metadata reverted
TimerConsumer sets CamelTimerCounter, CamelTimerName, CamelTimerPeriod and CamelTimerTime with
exchange.setProperty; only CamelTimerFiredTime is also a header. Declaring them as headers would send
a route to ${header.CamelTimerCounter}, which is null; the write-time header check points to
${exchangeProperty.CamelTimerCounter} instead (CAMEL-24698, source checks).
Co-Authored-By: Claude Fable 5.1 <no...@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bp3538HRBPMQkb5ta9xRaj
* CAMEL-24698: camel-groovy - the missing-property hint keeps the MissingPropertyException type
GroovyLogEipTest expects the cause to be a MissingPropertyException; the hint is now its message.
Co-Authored-By: Claude Fable 5.1 <no...@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bp3538HRBPMQkb5ta9xRaj
---------
Co-authored-by: Claude Fable 5.1 <no...@anthropic.com>
---
.../camel/component/file/FileOperations.java | 5 +-
.../camel/language/groovy/GroovyExpression.java | 19 +++++-
.../groovy/GroovyMissingPropertyHintTest.java | 63 ++++++++++++++++++
.../xslt/XmlSourceHandlerFactoryImpl.java | 7 ++
.../apache/camel/ExpectedBodyTypeException.java | 13 ++++
.../aggregate/AggregationStrategyMethodInfo.java | 24 ++++++-
.../camel/reifier/language/ExpressionReifier.java | 6 ++
.../processor/SplitWithoutExpressionTest.java | 57 ++++++++++++++++
...AggregationStrategyBeanAdapterNullBodyTest.java | 76 ++++++++++++++++++++++
9 files changed, 266 insertions(+), 4 deletions(-)
diff --git a/components/camel-file/src/main/java/org/apache/camel/component/file/FileOperations.java b/components/camel-file/src/main/java/org/apache/camel/component/file/FileOperations.java
index 6bd74b8bdd61..ba6aabec6e9c 100644
--- a/components/camel-file/src/main/java/org/apache/camel/component/file/FileOperations.java
+++ b/components/camel-file/src/main/java/org/apache/camel/component/file/FileOperations.java
@@ -457,7 +457,10 @@ public class FileOperations implements GenericFileOperations<File> {
throw new GenericFileOperationFailedException("Cannot store file: " + file, e);
}
} else {
- throw new GenericFileOperationFailedException("Cannot write null body to file: " + file);
+ throw new GenericFileOperationFailedException(
+ "Cannot write null body to file (to: file: writes the message body to the file; to read a file in the"
+ + " middle of a route use poll: file:... or pollEnrich, and set"
+ + " the body with setBody before writing): " + file);
}
}
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 bdcf49f8e05b..8456e767cba7 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
@@ -59,7 +59,24 @@ public class GroovyExpression extends ExpressionSupport {
Script script = instantiateScript(exchange, globalVariables);
script.setBinding(createBinding(exchange, globalVariables));
- Object value = script.run();
+ Object value;
+ try {
+ value = script.run();
+ } catch (groovy.lang.MissingPropertyException e) {
+ // a bean name used as a variable (formatter.format(body)): say where beans are, and what the variables are.
+ // The same exception type with the hint in its message; no cause, as the message would then be built from
+ // 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 + "}"
+ : "the script variables are exchange, message, body, headers, variables, exchangeProperties,"
+ + " camelContext, request and log";
+ throw new groovy.lang.MissingPropertyException(
+ e.getMessageWithoutLocationText() + " (" + hint + ")", name, e.getType());
+ }
return exchange.getContext().getTypeConverter().convertTo(type, value);
}
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
new file mode 100644
index 000000000000..59b3dc6d6c4e
--- /dev/null
+++ b/components/camel-groovy/src/test/java/org/apache/camel/processor/groovy/GroovyMissingPropertyHintTest.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.processor.groovy;
+
+import groovy.lang.MissingPropertyException;
+import org.apache.camel.RoutesBuilder;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+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).
+ */
+public class GroovyMissingPropertyHintTest extends CamelTestSupport {
+
+ @Test
+ public void beanNameUsedAsVariableIsExplained() {
+ context.getRegistry().bind("formatter", new StringBuilder());
+ Exception e = assertThrows(Exception.class, () -> template.sendBody("direct:bean", "x"));
+ MissingPropertyException cause = assertInstanceOf(MissingPropertyException.class, e.getCause());
+ assertTrue(cause.getMessage().contains("'formatter' is a bean in the registry, not a script variable"),
+ cause.getMessage());
+ assertTrue(cause.getMessage().contains("bean: {ref: formatter}"), cause.getMessage());
+ }
+
+ @Test
+ public void unknownVariableListsTheScriptVariables() {
+ Exception e = assertThrows(Exception.class, () -> template.sendBody("direct:unknown", "x"));
+ MissingPropertyException cause = assertInstanceOf(MissingPropertyException.class, e.getCause());
+ assertTrue(cause.getMessage().contains("the script variables are exchange, message, body, headers"),
+ cause.getMessage());
+ }
+
+ @Override
+ protected RoutesBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:bean").transform().groovy("formatter.append(body)");
+ from("direct:unknown").transform().groovy("nosuch.toUpperCase()");
+ }
+ };
+ }
+}
diff --git a/components/camel-xslt/src/main/java/org/apache/camel/component/xslt/XmlSourceHandlerFactoryImpl.java b/components/camel-xslt/src/main/java/org/apache/camel/component/xslt/XmlSourceHandlerFactoryImpl.java
index 6e254d57f25a..b56a995a931d 100644
--- a/components/camel-xslt/src/main/java/org/apache/camel/component/xslt/XmlSourceHandlerFactoryImpl.java
+++ b/components/camel-xslt/src/main/java/org/apache/camel/component/xslt/XmlSourceHandlerFactoryImpl.java
@@ -145,6 +145,13 @@ public class XmlSourceHandlerFactoryImpl implements SourceHandlerFactory {
if (source == null) {
if (isFailOnNullBody()) {
+ if (body == null) {
+ // a timer alone, or a to: file: that wrote instead of read: nothing to transform
+ throw new ExpectedBodyTypeException(
+ exchange, Source.class,
+ "the xslt step got no message body to transform: read the XML before the step with"
+ + " poll: file:..., pollEnrich or a from: consumer, or set it with setBody");
+ }
throw new ExpectedBodyTypeException(exchange, Source.class);
} else {
try {
diff --git a/core/camel-api/src/main/java/org/apache/camel/ExpectedBodyTypeException.java b/core/camel-api/src/main/java/org/apache/camel/ExpectedBodyTypeException.java
index 521d9acb3652..225a91ddac48 100644
--- a/core/camel-api/src/main/java/org/apache/camel/ExpectedBodyTypeException.java
+++ b/core/camel-api/src/main/java/org/apache/camel/ExpectedBodyTypeException.java
@@ -39,6 +39,19 @@ public class ExpectedBodyTypeException extends RuntimeCamelException {
this.expectedBodyType = expectedBodyType;
}
+ /**
+ * @param exchange the exchange that caused the error
+ * @param expectedBodyType the expected body type that could not be extracted
+ * @param hint what to do about it, appended to the message
+ */
+ public ExpectedBodyTypeException(Exchange exchange, Class<?> expectedBodyType, String hint) {
+ super("Could not extract IN message body as type: " + Objects.requireNonNull(expectedBodyType, "expectedBodyType")
+ + " body is: " + Objects.requireNonNull(exchange, "exchange").getIn().getBody()
+ + (hint != null && !hint.isBlank() ? " (" + hint + ")" : ""));
+ this.exchange = exchange;
+ this.expectedBodyType = expectedBodyType;
+ }
+
public @Nullable Exchange getExchange() {
return exchange;
}
diff --git a/core/camel-core-processor/src/main/java/org/apache/camel/processor/aggregate/AggregationStrategyMethodInfo.java b/core/camel-core-processor/src/main/java/org/apache/camel/processor/aggregate/AggregationStrategyMethodInfo.java
index 130b7dc1ae0d..98c1d7eafd71 100644
--- a/core/camel-core-processor/src/main/java/org/apache/camel/processor/aggregate/AggregationStrategyMethodInfo.java
+++ b/core/camel-core-processor/src/main/java/org/apache/camel/processor/aggregate/AggregationStrategyMethodInfo.java
@@ -20,7 +20,10 @@ import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
+import org.apache.camel.CamelExecutionException;
import org.apache.camel.Exchange;
+import org.apache.camel.InvalidPayloadException;
+import org.apache.camel.RuntimeCamelException;
/**
* Method information about the POJO method to call when using the {@link AggregationStrategyBeanAdapter}.
@@ -44,7 +47,7 @@ public class AggregationStrategyMethodInfo {
List<Object> list = new ArrayList<>(oldParameters.size() + newParameters.size());
for (AggregationStrategyParameterInfo info : oldParameters) {
if (oldExchange != null) {
- Object value = info.getExpression().evaluate(oldExchange, info.getType());
+ Object value = evaluate(info, oldExchange);
list.add(value);
} else {
// use a null value as oldExchange is null
@@ -53,7 +56,7 @@ public class AggregationStrategyMethodInfo {
}
for (AggregationStrategyParameterInfo info : newParameters) {
if (newExchange != null) {
- Object value = info.getExpression().evaluate(newExchange, info.getType());
+ Object value = evaluate(info, newExchange);
list.add(value);
} else {
// use a null value as newExchange is null
@@ -65,4 +68,21 @@ public class AggregationStrategyMethodInfo {
return method.invoke(pojo, args);
}
+ private Object evaluate(AggregationStrategyParameterInfo info, Exchange exchange) {
+ try {
+ return info.getExpression().evaluate(exchange, info.getType());
+ } catch (CamelExecutionException e) {
+ if (e.getCause() instanceof InvalidPayloadException && exchange.getMessage().getBody() == null) {
+ // a typed parameter is bound to the message body, and there is none (a timer message is empty)
+ throw new RuntimeCamelException(
+ "The aggregation strategy method " + method.getName() + " has a parameter of type "
+ + info.getType().getSimpleName()
+ + " that is bound to the message body, but the message has no body (null):"
+ + " set a body before the aggregate (setBody), or declare the parameter as Exchange or Object",
+ e.getCause());
+ }
+ throw e;
+ }
+ }
+
}
diff --git a/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/language/ExpressionReifier.java b/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/language/ExpressionReifier.java
index c1dd71260ef7..a9c66b010427 100644
--- a/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/language/ExpressionReifier.java
+++ b/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/language/ExpressionReifier.java
@@ -79,6 +79,12 @@ public class ExpressionReifier<T extends ExpressionDefinition> extends AbstractR
public static ExpressionReifier<? extends ExpressionDefinition> reifier(
CamelContext camelContext, ExpressionDefinition definition) {
+ if (definition == null) {
+ // a split, filter, setBody... written without its expression (in YAML: only options such as delimiter)
+ throw new IllegalArgumentException(
+ "No expression: the EIP needs an expression (a language and its text), for example simple: \"${body}\","
+ + " constant: \"...\" or tokenize: \",\"");
+ }
ExpressionReifier<? extends ExpressionDefinition> answer = null;
if (!EXPRESSIONS.isEmpty()) {
// custom take precedence
diff --git a/core/camel-core/src/test/java/org/apache/camel/processor/SplitWithoutExpressionTest.java b/core/camel-core/src/test/java/org/apache/camel/processor/SplitWithoutExpressionTest.java
new file mode 100644
index 000000000000..354ff748def0
--- /dev/null
+++ b/core/camel-core/src/test/java/org/apache/camel/processor/SplitWithoutExpressionTest.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.processor;
+
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.FailedToCreateRouteException;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.model.RouteDefinition;
+import org.apache.camel.model.SplitDefinition;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * CAMEL-24698: a split (or any expression node) whose expression was never set, as a YAML file with only options
+ * produces, must say that it needs an expression instead of "Unsupported definition: null".
+ */
+public class SplitWithoutExpressionTest extends ContextTestSupport {
+
+ @Override
+ public boolean isUseRouteBuilder() {
+ return false;
+ }
+
+ @Test
+ public void testSplitWithoutExpression() throws Exception {
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+ RouteDefinition route = from("direct:start");
+ SplitDefinition split = new SplitDefinition();
+ split.setId("mySplit");
+ route.addOutput(split);
+ split.to("mock:result");
+ }
+ });
+
+ FailedToCreateRouteException e = assertThrows(FailedToCreateRouteException.class, () -> context.start());
+ String msg = e.getCause() != null ? e.getCause().getMessage() : e.getMessage();
+ assertTrue(msg.startsWith("No expression: the EIP needs an expression"), msg);
+ }
+}
diff --git a/core/camel-core/src/test/java/org/apache/camel/processor/aggregator/AggregationStrategyBeanAdapterNullBodyTest.java b/core/camel-core/src/test/java/org/apache/camel/processor/aggregator/AggregationStrategyBeanAdapterNullBodyTest.java
new file mode 100644
index 000000000000..4d667994dc8b
--- /dev/null
+++ b/core/camel-core/src/test/java/org/apache/camel/processor/aggregator/AggregationStrategyBeanAdapterNullBodyTest.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.processor.aggregator;
+
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.Exchange;
+import org.apache.camel.InvalidPayloadException;
+import org.apache.camel.RuntimeCamelException;
+import org.apache.camel.builder.AggregationStrategies;
+import org.apache.camel.builder.RouteBuilder;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * A POJO aggregation strategy with a String parameter on a message without a body (a timer message) says so.
+ */
+public class AggregationStrategyBeanAdapterNullBodyTest extends ContextTestSupport {
+
+ @Test
+ public void testNullBodyIsExplained() throws Exception {
+ // the first message is kept as is (no old exchange to aggregate with), the second one calls the POJO
+ getMockEndpoint("mock:dead").expectedMessageCount(1);
+ getMockEndpoint("mock:result").expectedMessageCount(0);
+
+ template.sendBody("direct:start", null);
+ template.sendBody("direct:start", null);
+
+ assertMockEndpointsSatisfied();
+ Throwable e = getMockEndpoint("mock:dead").getExchanges().get(0).getProperty(Exchange.EXCEPTION_CAUGHT,
+ Exception.class);
+ while (e != null && !(e instanceof RuntimeCamelException)) {
+ e = e.getCause();
+ }
+ assertNotNull(e);
+ assertTrue(e.getMessage().contains("append has a parameter of type String"), e.getMessage());
+ assertTrue(e.getMessage().contains("the message has no body (null)"), e.getMessage());
+ assertTrue(e.getMessage().contains("setBody"), e.getMessage());
+ assertInstanceOf(InvalidPayloadException.class, e.getCause());
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ errorHandler(deadLetterChannel("mock:dead"));
+
+ from("direct:start").aggregate(constant(true), AggregationStrategies.bean(new MyBodyAppender(), "append"))
+ .completionSize(2).to("mock:result");
+ }
+ };
+ }
+
+ public static final class MyBodyAppender {
+ public String append(String existing, String next) {
+ return existing + next;
+ }
+ }
+}