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 f1bfdeb32651 CAMEL-25013: camel-support - Do not drop the decimals of 
the values of the in operator (#26930)
f1bfdeb32651 is described below

commit f1bfdeb326510b56d019621f1e8b14d27cb6a1dd
Author: allthingssecurity <[email protected]>
AuthorDate: Mon Sep 28 14:17:32 2026 +0530

    CAMEL-25013: camel-support - Do not drop the decimals of the values of the 
in operator (#26930)
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../camel/catalog/docs/simple-operators.adoc       |  4 +-
 .../camel/component/mock/MockValueBuilder.java     |  2 +-
 .../modules/languages/pages/simple-operators.adoc  |  4 +-
 .../apache/camel/builder/PredicateBuilderTest.java | 34 ++++++++++
 .../component/mock/MockValueBuilderInTest.java     | 72 ++++++++++++++++++++++
 .../camel/language/simple/SimpleOperatorTest.java  | 21 +++++++
 .../camel/support/builder/ExpressionBuilder.java   | 66 ++++++++++++++++++++
 .../apache/camel/support/builder/ValueBuilder.java |  2 +-
 .../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc    |  5 ++
 9 files changed, 206 insertions(+), 4 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/simple-operators.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/simple-operators.adoc
index e797fc2ebadb..1cd2d38b4c94 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/simple-operators.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/simple-operators.adoc
@@ -387,7 +387,9 @@ simple("${header.number} regex '\\d{4}'")
 And finally an example if the header equals any of the values in the
 list. Each element must be separated by comma, and no space around.
 This also works for numbers etc., as Camel will convert each element
-into the type of the left-hand side.
+into the type of the left-hand side. When the left-hand side is a number,
+each element is compared by its numeric value, so an `Integer` header
+with value `2` is not in `'2.5,3.5'`, and it is in `'2.0,3.0'`.
 
 [source,java]
 ----
diff --git 
a/components/camel-mock/src/main/java/org/apache/camel/component/mock/MockValueBuilder.java
 
b/components/camel-mock/src/main/java/org/apache/camel/component/mock/MockValueBuilder.java
index a42918a795cd..c50f94de86be 100644
--- 
a/components/camel-mock/src/main/java/org/apache/camel/component/mock/MockValueBuilder.java
+++ 
b/components/camel-mock/src/main/java/org/apache/camel/component/mock/MockValueBuilder.java
@@ -184,7 +184,7 @@ public class MockValueBuilder implements Expression, 
Predicate {
         List<Predicate> predicates = new ArrayList<>();
         for (Object value : values) {
             Expression right = asExpression(value);
-            right = ExpressionBuilder.convertToExpression(right, expression);
+            right = ExpressionBuilder.inValueExpression(right, expression);
             Predicate predicate = PredicateBuilder.isEqualTo(expression, 
right);
             predicates.add(predicate);
         }
diff --git 
a/core/camel-core-languages/src/main/docs/modules/languages/pages/simple-operators.adoc
 
b/core/camel-core-languages/src/main/docs/modules/languages/pages/simple-operators.adoc
index e797fc2ebadb..1cd2d38b4c94 100644
--- 
a/core/camel-core-languages/src/main/docs/modules/languages/pages/simple-operators.adoc
+++ 
b/core/camel-core-languages/src/main/docs/modules/languages/pages/simple-operators.adoc
@@ -387,7 +387,9 @@ simple("${header.number} regex '\\d{4}'")
 And finally an example if the header equals any of the values in the
 list. Each element must be separated by comma, and no space around.
 This also works for numbers etc., as Camel will convert each element
-into the type of the left-hand side.
+into the type of the left-hand side. When the left-hand side is a number,
+each element is compared by its numeric value, so an `Integer` header
+with value `2` is not in `'2.5,3.5'`, and it is in `'2.0,3.0'`.
 
 [source,java]
 ----
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/builder/PredicateBuilderTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/builder/PredicateBuilderTest.java
index 28410e65c8f9..64195b01027a 100644
--- 
a/core/camel-core/src/test/java/org/apache/camel/builder/PredicateBuilderTest.java
+++ 
b/core/camel-core/src/test/java/org/apache/camel/builder/PredicateBuilderTest.java
@@ -16,6 +16,7 @@
  */
 package org.apache.camel.builder;
 
+import java.math.BigDecimal;
 import java.util.Arrays;
 
 import org.apache.camel.Exchange;
@@ -126,6 +127,39 @@ public class PredicateBuilderTest extends TestSupport {
         assertMatches(header("name").in("Hiram", "Jonathan", "James", 
"Claus"));
     }
 
+    @Test
+    public void testNumberValueIn() {
+        exchange.getIn().setHeader("n", 2);
+        // the values are compared by their numeric values, and are not 
converted to an Integer first
+        assertDoesNotMatch(header("n").in(2.5));
+        assertDoesNotMatch(header("n").in(2.9, 3.5));
+        assertDoesNotMatch(header("n").in(new BigDecimal("2.5")));
+        assertDoesNotMatch(header("n").in(4294967298L));
+        assertDoesNotMatch(header("n").in("2.5"));
+        assertMatches(header("n").in(1, 2, 3));
+        assertMatches(header("n").in(2.0));
+        assertMatches(header("n").in(2L));
+        assertMatches(header("n").in("2"));
+
+        exchange.getIn().setHeader("n", 0);
+        assertDoesNotMatch(header("n").in(-0.5));
+
+        exchange.getIn().setHeader("n", -1294967296);
+        assertDoesNotMatch(header("n").in(3000000000L));
+
+        exchange.getIn().setHeader("n", 99L);
+        assertDoesNotMatch(header("n").in(99.99, 100.01));
+        assertMatches(header("n").in(99));
+
+        exchange.getIn().setHeader("n", Long.MAX_VALUE);
+        assertDoesNotMatch(header("n").in(1e20));
+
+        exchange.getIn().setHeader("n", 2.5d);
+        assertMatches(header("n").in(2.5));
+        assertMatches(header("n").in("2.5"));
+        assertDoesNotMatch(header("n").in(2));
+    }
+
     @Test
     public void testEmptyHeaderValueIn() {
         // there is no header with xxx
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/component/mock/MockValueBuilderInTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/component/mock/MockValueBuilderInTest.java
new file mode 100644
index 000000000000..fac02af7f404
--- /dev/null
+++ 
b/core/camel-core/src/test/java/org/apache/camel/component/mock/MockValueBuilderInTest.java
@@ -0,0 +1,72 @@
+/*
+ * 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.component.mock;
+
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.builder.RouteBuilder;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class MockValueBuilderInTest extends ContextTestSupport {
+
+    @Test
+    public void testInWithNumbers() throws Exception {
+        MockEndpoint mock = getMockEndpoint("mock:result");
+        mock.expectedMessageCount(1);
+        mock.message(0).header("n").in(1, 2, 3);
+        mock.message(0).header("n").in(2.0);
+
+        template.sendBodyAndHeader("direct:start", "Hello", "n", 2);
+
+        assertMockEndpointsSatisfied();
+    }
+
+    @Test
+    public void testInWithDecimalsDoesNotMatch() {
+        MockEndpoint mock = getMockEndpoint("mock:result");
+        mock.expectedMessageCount(1);
+        // 2.5 must not be truncated to 2
+        mock.message(0).header("n").in(2.5, 3.5);
+
+        template.sendBodyAndHeader("direct:start", "Hello", "n", 2);
+
+        assertThrows(AssertionError.class, this::assertMockEndpointsSatisfied);
+    }
+
+    @Test
+    public void testInWithLongOutOfIntRangeDoesNotMatch() {
+        MockEndpoint mock = getMockEndpoint("mock:result");
+        mock.expectedMessageCount(1);
+        // 4294967298 must not wrap to 2
+        mock.message(0).header("n").in(4294967298L);
+
+        template.sendBodyAndHeader("direct:start", "Hello", "n", 2);
+
+        assertThrows(AssertionError.class, this::assertMockEndpointsSatisfied);
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:start").to("mock:result");
+            }
+        };
+    }
+}
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleOperatorTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleOperatorTest.java
index 0c25f3ec3e37..3e144f57640f 100644
--- 
a/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleOperatorTest.java
+++ 
b/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleOperatorTest.java
@@ -672,6 +672,27 @@ public class SimpleOperatorTest extends 
LanguageTestSupport {
         assertPredicate("${header.price} range '2..3'", true);
     }
 
+    @Test
+    void testInWithDecimals() {
+        exchange.getIn().setHeader("n", 2);
+        exchange.getIn().setHeader("big", 99L);
+        exchange.getIn().setHeader("amount", new BigDecimal("2.50"));
+        // the elements are compared with an integer header by their numeric 
values, without dropping the decimals
+        assertPredicate("${header.n} in '2.5,3.5'", false);
+        assertPredicate("${header.n} in '2.9'", false);
+        assertPredicate("${header.n} !in '2.5'", true);
+        assertPredicate("${header.n} in '2,3'", true);
+        assertPredicate("${header.n} in '1, 2'", true);
+        assertPredicate("${header.n} in '4294967298'", false);
+        // 2.0 is the same number as 2, as with ${header.n} == 2.0
+        assertPredicate("${header.n} in '2.0'", true);
+        assertPredicate("${header.n} == 2.0", true);
+        assertPredicate("${header.big} in '99.99,100.01'", false);
+        assertPredicate("${header.big} in '98,99'", true);
+        assertPredicate("${header.amount} in '2.5'", true);
+        assertPredicate("${header.amount} in '2'", false);
+    }
+
     @Test
     public void testRange() {
         assertPredicate("${in.header.bar} range '100..200'", true);
diff --git 
a/core/camel-support/src/main/java/org/apache/camel/support/builder/ExpressionBuilder.java
 
b/core/camel-support/src/main/java/org/apache/camel/support/builder/ExpressionBuilder.java
index 63f94bc9435e..bddd202ada29 100644
--- 
a/core/camel-support/src/main/java/org/apache/camel/support/builder/ExpressionBuilder.java
+++ 
b/core/camel-support/src/main/java/org/apache/camel/support/builder/ExpressionBuilder.java
@@ -18,6 +18,7 @@ package org.apache.camel.support.builder;
 
 import java.io.IOException;
 import java.io.StringWriter;
+import java.math.BigDecimal;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Comparator;
@@ -1783,6 +1784,71 @@ public class ExpressionBuilder {
         };
     }
 
+    /**
+     * Returns an expression for one value of the <tt>in</tt> operator, which 
is compared for equality with the value of
+     * the left expression.
+     * <p/>
+     * When the left value is a number, a number is returned as is, and a 
String with a decimal number is returned as a
+     * {@link BigDecimal}, so the values are compared by their numeric values 
(such as <tt>2</tt> and <tt>2.5</tt>,
+     * which are not equal). Converting them to the type of the left value 
first could drop the decimals or wrap a long
+     * that does not fit in an int. Otherwise the value is converted to the 
type of the left value, as by
+     * {@link #convertToExpression(Expression, Expression)}.
+     *
+     * @param  value the expression for the value in the list of the in 
operator
+     * @param  left  the left expression
+     * @return       the expression for the value to compare with the left 
value
+     */
+    public static Expression inValueExpression(final Expression value, final 
Expression left) {
+        final Expression converted = convertToExpression(value, left);
+        return new ExpressionAdapter() {
+            @Override
+            public Object evaluate(Exchange exchange) {
+                Object leftValue = left.evaluate(exchange, Object.class);
+                if (leftValue instanceof Number) {
+                    Object rightValue = value.evaluate(exchange, Object.class);
+                    if (rightValue instanceof Number) {
+                        return rightValue;
+                    }
+                    if (rightValue instanceof String text) {
+                        BigDecimal number = decimalOrNull(text);
+                        if (number != null) {
+                            return number;
+                        }
+                    }
+                    if (rightValue == null) {
+                        return null;
+                    }
+                    // not a number, so convert to the type of the left value 
as before
+                    return 
exchange.getContext().getTypeConverter().convertTo(leftValue.getClass(), 
exchange, rightValue);
+                }
+                return converted.evaluate(exchange, Object.class);
+            }
+
+            @Override
+            public void init(CamelContext context) {
+                super.init(context);
+                converted.init(context);
+            }
+
+            @Override
+            public String toString() {
+                return String.valueOf(value);
+            }
+        };
+    }
+
+    private static BigDecimal decimalOrNull(String text) {
+        String number = text.trim();
+        if (number.isEmpty()) {
+            return null;
+        }
+        try {
+            return new BigDecimal(number);
+        } catch (NumberFormatException e) {
+            return null;
+        }
+    }
+
     /**
      * Returns a tokenize expression which will tokenize the string with the 
given token
      */
diff --git 
a/core/camel-support/src/main/java/org/apache/camel/support/builder/ValueBuilder.java
 
b/core/camel-support/src/main/java/org/apache/camel/support/builder/ValueBuilder.java
index 87e8983781b1..a2e9d4bd3d73 100644
--- 
a/core/camel-support/src/main/java/org/apache/camel/support/builder/ValueBuilder.java
+++ 
b/core/camel-support/src/main/java/org/apache/camel/support/builder/ValueBuilder.java
@@ -130,7 +130,7 @@ public class ValueBuilder implements Expression, Predicate {
         List<Predicate> predicates = new ArrayList<>();
         for (Object value : values) {
             Expression right = asExpression(value);
-            right = ExpressionBuilder.convertToExpression(right, expression);
+            right = ExpressionBuilder.inValueExpression(right, expression);
             Predicate predicate = 
onNewPredicate(PredicateBuilder.isEqualTo(expression, right));
             predicates.add(predicate);
         }
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index 08f6cd9b845c..77a23e316e09 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -87,6 +87,11 @@ A review of the simple language fixed a number of edge 
cases. These change what
   The same applies to two `BigDecimal` or two `BigInteger` values, to a number 
compared with a numeric `String`, and to
   the Java DSL predicates such as `header("amount").isGreaterThan(100)`. Two 
`BigDecimal` values with a different scale,
   such as `2.50` and `2.5`, are now equal.
+  The `in` and `!in` operators (and the Java DSL `in`) compare the same way 
when the left value is a number: each
+  element of the list is no longer converted to the type of the left value 
first, which dropped its decimals or failed.
+  `${header.n} in '2.5,3.5'` is `false` for an `Integer` of `2` (it used to 
fail with a `TypeConversionException`),
+  `${header.n} in '2.0'` is `true`, as `${header.n} == 2.0` is, and 
`header("n").in(2.5)` and
+  `header("n").in(4294967298L)` are `false` (they used to be `true`).
 * Inside `${ }` the ternary `?` and `:` need whitespace around them, so 
`${bean:svc?method=at(10:30)}` is not a
   ternary.
 

Reply via email to