From a04524399569f0886f380c3d544498bf3311ca73 Mon Sep 17 00:00:00 2001 From: Claus Ibsen Date: Thu, 22 Oct 2026 22:49:26 +0200 Subject: [PATCH] CAMEL-26041: camel-bean - a call of equals on the body says how to compare, not to unmarshal the body Camel does not call equals, hashCode and the other methods of java.lang.Object on a bean, so ${body.equals('x')} fails for any body. Since CAMEL-25749 the error added the body hint for a body that is still the payload, and so told to unmarshal a String body to read its fields. The error now says that Camel does not call the method, and for equals to compare with an operator, such as ${body} == 'value' in simple. Co-Authored-By: Claude Opus 5.5 (1M context) Signed-off-by: Claus Ibsen --- .../apache/camel/component/bean/BeanInfo.java | 22 +++++++++++++--- .../camel/language/bean/BeanExpression.java | 25 +++++++++++++++++++ .../simple/SimpleSyntaxHintsTest.java | 23 +++++++++++++++++ 3 files changed, 66 insertions(+), 4 deletions(-) diff --git a/components/camel-bean/src/main/java/org/apache/camel/component/bean/BeanInfo.java b/components/camel-bean/src/main/java/org/apache/camel/component/bean/BeanInfo.java index 13a0ae2a22e7a..3bb635ab6d690 100644 --- a/components/camel-bean/src/main/java/org/apache/camel/component/bean/BeanInfo.java +++ b/components/camel-bean/src/main/java/org/apache/camel/component/bean/BeanInfo.java @@ -983,6 +983,22 @@ private MethodInfo chooseBestPossibleMethodInfo( return null; } + /** + * Whether a method of this name is never a candidate for Camel Bean Binding, such as equals and the other methods + * of java.lang.Object. getClass is one too, but an OGNL call of it is invoked on its own. + * + * @param name the name of the method + * @return true if the name is excluded from Camel Bean Binding + */ + public static boolean isExcludedMethodName(String name) { + for (String s : EXCLUDED_METHOD_NAMES) { + if (s.equals(name)) { + return true; + } + } + return false; + } + /** * Validates whether the given method is a valid candidate for Camel Bean Binding. * @@ -993,10 +1009,8 @@ private MethodInfo chooseBestPossibleMethodInfo( protected boolean isValidMethod(Class clazz, Method method) { // method name must not be in the excluded list String name = method.getName(); - for (String s : EXCLUDED_METHOD_NAMES) { - if (name.equals(s)) { - return false; - } + if (isExcludedMethodName(name)) { + return false; } // special for Object where clone is not allowed to be called directly 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 1a5e368d6f26a..c81504b037c9f 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 @@ -415,6 +415,24 @@ private static String methodHint(String methodName, Throwable e) { return methodName; } + /** + * Why ${body.equals('x')}, ${body.hashCode()} and the other methods Camel does not call on a bean fail: the body is + * not the reason, so the hint says what to write instead of what the body is (CAMEL-26041). Null for another + * method. + */ + private static String excludedMethodHint(String methodName) { + String name = methodName != null ? StringHelper.before(methodName, "(", methodName).trim() : null; + // getClass is excluded from the bean binding, but BeanInfo invokes it on its own + if (name == null || "getClass".equals(name) || !BeanInfo.isExcludedMethodName(name)) { + return null; + } + if ("equals".equals(name)) { + return " (Camel does not call equals on a bean: compare with an operator instead, such as ${body} == 'value'" + + " in simple)"; + } + return " (Camel does not call " + name + " on a bean)"; + } + /** * The bean name, or for an OGNL on the message body (${body.foo()}) what the body is, so the message does not say * "on null". @@ -543,6 +561,13 @@ private Object invokeMethodIfPresent( return value; } String method = methodHint(methodName, resultExchange.getException()); + String excluded = excludedMethodHint(methodName); + if (excluded != null) { + // ${body.equals('x')}: no bean binding calls it, whatever the body is (CAMEL-26041) + throw new RuntimeBeanExpressionException( + exchange, describeBean(holder, beanName, exchange), method + excluded, + resultExchange.getException()); + } Object bean = beanOf(holder, exchange); String body = bodyHint(exchange, bean); String list = body == null ? listHint(bean, methodName) : null; 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 fcc8ac4567aa5..9e9946b61c4ef 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 @@ -220,6 +220,29 @@ public void testOgnlSizeOnAnIteratorSaysItHasNone() { .contains("it has no size and can be read once: split it, or have the producer give a List"); } + @Test + public void testEqualsOnTheBodySaysHowToCompare() { + // CAMEL-26041: Camel does not call equals on a bean, so the text of the body is not why it fails + exchange.getIn().setBody("test-value-2"); + Exception e = assertThrows(Exception.class, + () -> context.resolveLanguage("simple").createExpression("${body.equals('test-value-2')}") + .evaluate(exchange, String.class)); + assertThat(e.getMessage()).contains("equals('test-value-2') (Camel does not call equals on a bean: compare with an" + + " operator instead, such as ${body} == 'value' in simple)") + .doesNotContain("unmarshal"); + } + + @Test + public void testHashCodeOnAListIsNotAMissingElement() { + // CAMEL-26041: nor is the body a List without such a method or key + exchange.getIn().setBody(List.of("A1", "A2")); + Exception e = assertThrows(Exception.class, + () -> context.resolveLanguage("simple").createExpression("${body.hashCode()}").evaluate(exchange, + String.class)); + assertThat(e.getMessage()).contains("hashCode() (Camel does not call hashCode on a bean)") + .doesNotContain("a List has no method"); + } + @Test public void testKeyOnTheFileAsReadSaysToUnmarshal() { // CAMEL-25749: ${body[status]} on the file consumer's body, before any unmarshal