Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,32 @@ public void setOutputs(List<ProcessorDefinition<?>> outputs) {
public void addOutput(ProcessorDefinition<?> output) {
this.outputs.add(output);
}

@Override
public void setExpression(ExpressionDefinition expression) {
// Detect when an expression element (e.g. <method>) appears after processing steps
// inside a when/filter clause in XML/YAML DSL. This is almost certainly a user mistake
// where they intended to use <bean> (processor) instead of <method> (expression/predicate).
// We skip this check when the existing expression wraps an ExpressionClause (Java DSL),
// because preCreateProcessor() legitimately re-sets the expression after resolving it.
if (expression != null && getExpression() != null && !outputs.isEmpty()) {
ExpressionDefinition existing = getExpression();
boolean isExpressionClause
= existing.getExpressionValue() instanceof org.apache.camel.builder.ExpressionClause
|| existing.getPredicate() instanceof org.apache.camel.builder.ExpressionClause;
if (!isExpressionClause) {
String lang = expression.getLanguage() != null
? expression.getLanguage() : expression.getClass().getSimpleName();
throw new IllegalArgumentException(
"The " + getShortName() + " already has a predicate (" + existing
+ ") and " + outputs.size() + " output(s). "
+ "The expression '" + lang
+ "' is being parsed as an expression/predicate but appears after processing steps. "
+ "If you intended to call a bean method as a processing step, use <bean> instead of <method>. "
+ "An expression element must be the first child of <" + getShortName()
+ ">.");
}
}
super.setExpression(expression);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
Expand Down Expand Up @@ -449,6 +450,54 @@ public void testParseError() throws Exception {
assertTrue(e.getMessage().startsWith("Unexpected attribute '{}ref'"));
}

@Test
public void testMethodAfterStepsInWhenClauseShouldFail() throws Exception {
String routesXml = """
<routes xmlns="http://camel.apache.org/schema/xml-io">
<route>
<from uri="direct:start"/>
<choice>
<when>
<simple>${header.foo} == 'bar'</simple>
<log message="Condition met"/>
<method ref="someBean" method="processFooBar"/>
</when>
<otherwise>
<to uri="mock:other"/>
</otherwise>
</choice>
</route>
</routes>
""";
Exception e = assertThrows(Exception.class, () -> {
new ModelParser(new StringReader(routesXml)).parseRoutesDefinition();
});
assertThat(e).hasStackTraceContaining("already has a predicate");
}

@Test
public void testMethodAsPredicateInWhenClauseShouldWork() throws Exception {
String routesXml = """
<routes xmlns="http://camel.apache.org/schema/xml-io">
<route>
<from uri="direct:start"/>
<choice>
<when>
<method ref="someBean" method="isFoo"/>
<to uri="mock:foo"/>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be good to add a here as the exception message tells

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea — adding a <bean id="someBean" type="..."/> would make the test XML more realistic, even though this is purely a parser-level test (no runtime resolution). I'll update both test methods.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, on second thought — <bean> declarations live at the <camel> root level, not inside <routes>. This test uses parseRoutesDefinition() which expects <routes> as root. Restructuring to use a <camel> wrapper would require a different parse method and adds complexity for a purely parser-level test where bean refs aren't resolved. Happy to change it if you still think it's worth it, but I'd lean toward keeping it simple since the test is only validating XML structure parsing, not runtime behavior.

</when>
<otherwise>
<to uri="mock:other"/>
</otherwise>
</choice>
</route>
</routes>
""";
RoutesDefinition routes = new ModelParser(new StringReader(routesXml)).parseRoutesDefinition().orElse(null);
assertNotNull(routes);
assertEquals(1, routes.getRoutes().size());
}

private Path getResourceFolder() {
final URL resource = getClass().getClassLoader().getResource("barInterceptorRoute.xml");
assert resource != null : "Cannot find barInterceptorRoute.xml";
Expand Down
Loading