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