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 341048155d0f CAMEL-25083: camel-support - ObjectHelper type coercion 
and iterators: fix bugs found in a deep review (#26983)
341048155d0f is described below

commit 341048155d0f2cf6e6722838cfc82161a63a45af
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Sep 28 22:41:44 2026 +0200

    CAMEL-25083: camel-support - ObjectHelper type coercion and iterators: fix 
bugs found in a deep review (#26983)
    
    - contains on a collection now type coerces each element (as for an array)
    - contains with ignoreCase on a collection compares whole elements (not 
substrings), null safe
    - a String only equals a boolean when it is true or false
    - iterating a String that is only the delimiter no longer throws 
StringIndexOutOfBoundsException
    - iterating with a pattern delimiter that matches an empty string no longer 
loops forever
    - compare of Ordered values (and the hashCode fallback) no longer overflows
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../camel/util/ObjectHelperEdgeCasesTest.java      | 150 +++++++++++++++++++++
 .../org/apache/camel/support/ObjectHelper.java     |  55 +++++---
 .../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc    |  13 ++
 3 files changed, 200 insertions(+), 18 deletions(-)

diff --git 
a/core/camel-core/src/test/java/org/apache/camel/util/ObjectHelperEdgeCasesTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/util/ObjectHelperEdgeCasesTest.java
new file mode 100644
index 000000000000..47b6ac6eb080
--- /dev/null
+++ 
b/core/camel-core/src/test/java/org/apache/camel/util/ObjectHelperEdgeCasesTest.java
@@ -0,0 +1,150 @@
+/*
+ * 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.util;
+
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
+import org.apache.camel.Ordered;
+import org.apache.camel.TypeConverter;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.support.DefaultExchange;
+import org.apache.camel.support.ObjectHelper;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+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.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class ObjectHelperEdgeCasesTest {
+
+    private CamelContext context;
+    private TypeConverter tc;
+
+    @BeforeEach
+    public void setUp() {
+        context = new DefaultCamelContext();
+        context.start();
+        tc = context.getTypeConverter();
+    }
+
+    @AfterEach
+    public void tearDown() {
+        context.stop();
+    }
+
+    @Test
+    public void testContainsTypeCoercesCollectionElements() {
+        assertTrue(ObjectHelper.typeCoerceContains(tc, List.of(1, 2, 3), "2", 
false));
+        assertTrue(ObjectHelper.typeCoerceContains(tc, List.of(1, 2, 3), "2", 
true));
+        assertFalse(ObjectHelper.typeCoerceContains(tc, List.of(1, 2, 3), "4", 
false));
+    }
+
+    @Test
+    public void testContainsIgnoreCaseMatchesWholeElements() {
+        assertTrue(ObjectHelper.typeCoerceContains(tc, List.of("FooBar"), 
"foobar", true));
+        assertFalse(ObjectHelper.typeCoerceContains(tc, List.of("foobar"), 
"foo", true));
+        assertFalse(ObjectHelper.typeCoerceContains(tc, List.of("foobar"), 
"foo", false));
+
+        List<String> withNull = new ArrayList<>();
+        withNull.add(null);
+        withNull.add("a");
+        assertTrue(ObjectHelper.typeCoerceContains(tc, withNull, "A", true));
+        assertTrue(ObjectHelper.typeCoerceContains(tc, withNull, null, true));
+    }
+
+    @Test
+    public void testStringIsOnlyEqualToBooleanWhenTrueOrFalse() {
+        assertFalse(ObjectHelper.typeCoerceEquals(tc, "hello", Boolean.FALSE));
+        assertFalse(ObjectHelper.typeCoerceEquals(tc, Boolean.FALSE, "hello"));
+        assertNotEquals(0, ObjectHelper.typeCoerceCompare(tc, "hello", 
Boolean.FALSE));
+        assertNotEquals(0, ObjectHelper.typeCoerceCompare(tc, Boolean.FALSE, 
"hello"));
+
+        assertTrue(ObjectHelper.typeCoerceEquals(tc, "false", Boolean.FALSE));
+        assertTrue(ObjectHelper.typeCoerceEquals(tc, Boolean.TRUE, "TRUE"));
+        assertEquals(0, ObjectHelper.typeCoerceCompare(tc, "False", 
Boolean.FALSE));
+    }
+
+    @Test
+    public void testIteratorOfOnlyTheDelimiter() {
+        assertFalse(ObjectHelper.createIterator(";", ";").hasNext());
+        assertFalse(ObjectHelper.createIterator(":::", "::").hasNext());
+        assertFalse(ObjectHelper.createIterable(";", ";", false, 
false).iterator().hasNext());
+        // an empty value between two delimiters is kept
+        assertEquals(List.of(""), toList(ObjectHelper.createIterable(";;", 
";", true, false).iterator()));
+    }
+
+    @Test
+    public void testIteratorWithPatternMatchingEmpty() {
+        assertTimeoutPreemptively(Duration.ofSeconds(5), () -> {
+            assertEquals(List.of("a", "b"), 
toList(ObjectHelper.createIterator("a b", "\\s*", false, true)));
+            assertEquals(List.of("a", "b"), 
toList(ObjectHelper.createIterator("a;b", ";*", false, true)));
+        });
+    }
+
+    @Test
+    public void testCompareOrderedDoesNotOverflow() {
+        Ordered highest = () -> Ordered.HIGHEST;
+        Ordered one = () -> 1;
+        Ordered lowest = () -> Ordered.LOWEST;
+        assertTrue(ObjectHelper.compare(highest, one) < 0);
+        assertTrue(ObjectHelper.compare(one, highest) > 0);
+        assertTrue(ObjectHelper.compare(highest, lowest) < 0);
+
+        List<Ordered> list = new ArrayList<>(Arrays.asList(lowest, one, 
highest));
+        list.sort(ObjectHelper::compare);
+        assertEquals(List.of(highest, one, lowest), list);
+    }
+
+    @Test
+    public void testSimpleContainsOnListOfNumbers() {
+        Exchange exchange = new DefaultExchange(context);
+        exchange.getMessage().setHeader("ids", List.of(1, 2, 3));
+        assertTrue(simple(exchange, "${header.ids} contains '2'"));
+        assertFalse(simple(exchange, "${header.ids} contains '4'"));
+    }
+
+    @Test
+    public void testSimpleEqualsFalse() {
+        Exchange exchange = new DefaultExchange(context);
+        exchange.getMessage().setBody("hello");
+        assertFalse(simple(exchange, "${body} == false"));
+        assertTrue(simple(exchange, "${body} != false"));
+
+        exchange.getMessage().setBody("false");
+        assertTrue(simple(exchange, "${body} == false"));
+    }
+
+    private boolean simple(Exchange exchange, String predicate) {
+        return 
context.resolveLanguage("simple").createPredicate(predicate).matches(exchange);
+    }
+
+    private static List<Object> toList(Iterator<?> it) {
+        List<Object> answer = new ArrayList<>();
+        it.forEachRemaining(answer::add);
+        return answer;
+    }
+}
diff --git 
a/core/camel-support/src/main/java/org/apache/camel/support/ObjectHelper.java 
b/core/camel-support/src/main/java/org/apache/camel/support/ObjectHelper.java
index 129aab4d0a3d..a07fb7568cdb 100644
--- 
a/core/camel-support/src/main/java/org/apache/camel/support/ObjectHelper.java
+++ 
b/core/camel-support/src/main/java/org/apache/camel/support/ObjectHelper.java
@@ -254,8 +254,20 @@ public final class ObjectHelper {
     }
 
     private static boolean booleanStringComparison(Boolean leftBool, String 
rightValue) {
-        Boolean rightBool = Boolean.valueOf(rightValue);
-        return leftBool.compareTo(rightBool) == 0;
+        Boolean rightBool = toBoolean(rightValue);
+        return rightBool != null && leftBool.compareTo(rightBool) == 0;
+    }
+
+    /**
+     * Converts the string to a boolean only if it is true or false (such as 
hello is not false)
+     */
+    private static Boolean toBoolean(String value) {
+        if ("true".equalsIgnoreCase(value)) {
+            return Boolean.TRUE;
+        } else if ("false".equalsIgnoreCase(value)) {
+            return Boolean.FALSE;
+        }
+        return null;
     }
 
     private static boolean doublePairComparison(Object leftValue, Object 
rightValue) {
@@ -287,8 +299,8 @@ public final class ObjectHelper {
     }
 
     private static boolean stringBooleanComparison(String leftValue, Boolean 
rightValue) {
-        Boolean leftBool = Boolean.valueOf(leftValue);
-        return leftBool.compareTo(rightValue) == 0;
+        Boolean leftBool = toBoolean(leftValue);
+        return leftBool != null && leftBool.compareTo(rightValue) == 0;
     }
 
     private static boolean stringDoubleComparison(String leftValue, Double 
rightValue) {
@@ -395,12 +407,12 @@ public final class ObjectHelper {
                 && isFloatingNumber(leftStr)) {
             Float leftNum = Float.valueOf(leftStr);
             return leftNum.compareTo(rightNum);
-        } else if (rightValue instanceof Boolean rightBool && leftValue 
instanceof String leftStr) {
-            Boolean leftBool = Boolean.valueOf(leftStr);
-            return leftBool.compareTo(rightBool);
-        } else if (rightValue instanceof String rightStr && leftValue 
instanceof Boolean leftBool) {
-            Boolean rightBool = Boolean.valueOf(rightStr);
-            return leftBool.compareTo(rightBool);
+        } else if (rightValue instanceof Boolean rightBool && leftValue 
instanceof String leftStr
+                && toBoolean(leftStr) != null) {
+            return toBoolean(leftStr).compareTo(rightBool);
+        } else if (rightValue instanceof String rightStr && leftValue 
instanceof Boolean leftBool
+                && toBoolean(rightStr) != null) {
+            return leftBool.compareTo(toBoolean(rightStr));
         }
 
         // numbers of different types (such as Double and Integer), or a 
number and a numeric String, are compared
@@ -700,7 +712,7 @@ public final class ObjectHelper {
             return 1;
         }
         if (a instanceof Ordered orderedA && b instanceof Ordered orderedB) {
-            return orderedA.getOrder() - orderedB.getOrder();
+            return Integer.compare(orderedA.getOrder(), orderedB.getOrder());
         }
         if (ignoreCase && a instanceof String strA && b instanceof String 
strB) {
             return strA.compareToIgnoreCase(strB);
@@ -710,7 +722,7 @@ public final class ObjectHelper {
         }
         int answer = a.getClass().getName().compareTo(b.getClass().getName());
         if (answer == 0) {
-            answer = a.hashCode() - b.hashCode();
+            answer = Integer.compare(a.hashCode(), b.hashCode());
         }
         return answer;
     }
@@ -1149,12 +1161,10 @@ public final class ObjectHelper {
             collectionOrArray = new String(arr);
         }
         if (collectionOrArray instanceof Collection<?> collection) {
-            if (ignoreCase) {
-                String lower = value.toString().toLowerCase(Locale.ENGLISH);
-                return collection.stream().anyMatch(c -> 
c.toString().toLowerCase(Locale.ENGLISH).contains(lower));
-            } else {
-                return collection.contains(value);
+            if (!ignoreCase && collection.contains(value)) {
+                return true;
             }
+            // otherwise type coerce each element (below) the same way as for 
an array
         } else if (collectionOrArray instanceof String str) {
             String subStr;
             if (value instanceof String strValue) {
@@ -1239,6 +1249,10 @@ public final class ObjectHelper {
             } else {
                 this.contentLength = content.length();
             }
+            if (from > contentLength) {
+                // the content is only the separator (or the start and end 
separators overlap) so there are no values
+                this.to = -1;
+            }
         }
 
         @Override
@@ -1328,7 +1342,12 @@ public final class ObjectHelper {
                 } else if (to == -1) {
                     return false;
                 }
-                if (matcher.find(from)) {
+                boolean found = matcher.find(from);
+                if (found && matcher.end() == from) {
+                    // an empty match (such as with \s*) is not a separator, 
so find the next separator
+                    found = from < content.length() && matcher.find(from + 1);
+                }
+                if (found) {
                     to = matcher.start();
                     if (from == to) {
                         from = matcher.end();
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 d68b32838222..971676c7e91b 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
@@ -1618,6 +1618,7 @@ 
from("spring-redis://localhost:6379?command=SUBSCRIBE&channels=myChannel"
 
 Setting the `serializer` option to a custom `RedisSerializer` bypasses the 
filter entirely, since
 Camel then no longer controls how the payload is read.
+
 === camel-langchain4j
 
 The legacy `sse` `transportType` has been removed. It follows the support 
removal in 
@@ -1662,6 +1663,7 @@ Ordinary listings are unaffected, as a listed name is 
normally a single path seg
 resolves back inside the polled directory remains accepted. Two configurations 
can newly see files skipped: a
 server that reports names navigating above the polled directory, and a 
`fileName` expression (used when
 `useList=false`) that navigates above it. Set `jailStartingDirectory=false` if 
such a path is intended.
+
 === camel-salesforce
 
 The `camel-salesforce-maven-plugin` now supports JWT and Client Credentials 
authentication in addition to
@@ -3358,6 +3360,17 @@ logged endpoint URIs and route dumps; `passPhrase` and 
`keyPassword` were alread
 declare options with the same names. The YAML DSL JSON schema now also marks 
`passPhrase` and `keyPassword` with
 `"format": "password"`, so editors that consume the schema render them as 
password fields.
 
+=== camel-core - simple contains on a collection and comparing a String with a 
boolean
+
+The `contains` operator of the simple language now type coerces each element 
of a collection, the same way
+it already did for an array. For example `${header.ids} contains '2'` is now 
`true` when the header is a
+`List` of the numbers 1, 2 and 3. With `ignoreCase` (the `~~` operator) an 
element of a collection must now be
+equal to the value (ignoring case), where previously it was enough for the 
element to contain the value as a
+substring, which was also not the case without `ignoreCase`.
+
+A String is now only equal to a boolean when it is `true` or `false` (ignoring 
case). Previously any other
+String was equal to `false`, so `${body} == false` was `true` when the body 
was `hello`.
+
 === camel-coap
 
 The CoAP consumer now answers a failed exchange with `5.00 Internal Server 
Error`, and no longer returns

Reply via email to