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 8ee2d3485f7c CAMEL-26329: camel-support - Compare a number and a
numeric String by their values in typeCoerceEquals (#28752)
8ee2d3485f7c is described below
commit 8ee2d3485f7c375b95164faa1278bee160c7fe5c
Author: allthingssecurity <[email protected]>
AuthorDate: Sat Oct 31 14:58:41 2026 +0530
CAMEL-26329: camel-support - Compare a number and a numeric String by their
values in typeCoerceEquals (#28752)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../camel/converter/TypeCoerceCompareTest.java | 69 ++++++++++++++
.../camel/language/simple/SimpleOperatorTest.java | 21 +++++
.../org/apache/camel/support/ObjectHelper.java | 100 +++++++++++++++++++--
.../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 13 +++
4 files changed, 197 insertions(+), 6 deletions(-)
diff --git
a/core/camel-core/src/test/java/org/apache/camel/converter/TypeCoerceCompareTest.java
b/core/camel-core/src/test/java/org/apache/camel/converter/TypeCoerceCompareTest.java
index 5b197229cadd..0d14c42ff801 100644
---
a/core/camel-core/src/test/java/org/apache/camel/converter/TypeCoerceCompareTest.java
+++
b/core/camel-core/src/test/java/org/apache/camel/converter/TypeCoerceCompareTest.java
@@ -84,6 +84,75 @@ public class TypeCoerceCompareTest extends
ContextTestSupport {
assertFalse(ObjectHelper.typeCoerceEquals(tc, new BigDecimal("2.51"),
new BigDecimal("2.5")));
}
+ @Test
+ void testEqualsNumberAndNumericStringWrittenDifferently() {
+ TypeConverter tc = context.getTypeConverter();
+ // a number and a numeric String with the same value are equal, as <=
and >= say, also when they are not
+ // written the same way
+ assertEquals(0, ObjectHelper.typeCoerceCompare(tc, new
BigDecimal("10.00"), "10"));
+ assertTrue(ObjectHelper.typeCoerceEquals(tc, new BigDecimal("10.00"),
"10"));
+ assertTrue(ObjectHelper.typeCoerceEquals(tc, "10", new
BigDecimal("10.00")));
+ assertTrue(ObjectHelper.typeCoerceEquals(tc, new BigDecimal("10"),
"10.0"));
+ assertTrue(ObjectHelper.typeCoerceEquals(tc, "10.00", 10));
+ assertTrue(ObjectHelper.typeCoerceEquals(tc, 10, "10.0"));
+ assertTrue(ObjectHelper.typeCoerceEquals(tc, 10L, "1e1"));
+ assertTrue(ObjectHelper.typeCoerceEquals(tc, "1e1", 10L));
+ assertTrue(ObjectHelper.typeCoerceEquals(tc, 10, " 10"));
+ assertTrue(ObjectHelper.typeCoerceEquals(tc, (short) 3, "3.0"));
+ assertTrue(ObjectHelper.typeCoerceEquals(tc, BigInteger.TEN, "10.0"));
+ // a double was already compared with a numeric String by its value
+ assertTrue(ObjectHelper.typeCoerceEquals(tc, 10d, "10.0"));
+ assertTrue(ObjectHelper.typeCoerceEquals(tc, 10d, "1e1"));
+ assertTrue(ObjectHelper.typeCoerceEquals(tc, 10d, " 10"));
+ // other values, text that is not a number, and null are not equal
+ assertFalse(ObjectHelper.typeCoerceEquals(tc, new BigDecimal("10.00"),
"10.01"));
+ assertFalse(ObjectHelper.typeCoerceEquals(tc, 10, "10.5"));
+ assertFalse(ObjectHelper.typeCoerceEquals(tc, "10.5", 10));
+ assertFalse(ObjectHelper.typeCoerceEquals(tc, 10, "ten"));
+ assertFalse(ObjectHelper.typeCoerceEquals(tc, new BigDecimal("10"),
""));
+ assertFalse(ObjectHelper.typeCoerceEquals(tc, 10, null));
+ assertFalse(ObjectHelper.typeCoerceEquals(tc, null, "10"));
+ }
+
+ @Test
+ void testCompareNumberAndNumericStringWithSpaceOrExponent() {
+ TypeConverter tc = context.getTypeConverter();
+ // compared by their exact values, as == compares them, and not as
doubles
+ assertTrue(ObjectHelper.typeCoerceCompare(tc, 9007199254740993L, "
9007199254740992") > 0);
+ assertTrue(ObjectHelper.typeCoerceCompare(tc, " 9007199254740992",
9007199254740993L) < 0);
+ assertTrue(ObjectHelper.typeCoerceCompare(tc, new
BigDecimal("1.00000000000000001"), "1e0") > 0);
+ assertFalse(ObjectHelper.typeCoerceEquals(tc, new
BigDecimal("1.00000000000000001"), "1e0"));
+ assertEquals(0, ObjectHelper.typeCoerceCompare(tc, 10, " 1e1 "));
+ }
+
+ @Test
+ void testEqualsNumberAndTextThatIsNotANumber() {
+ TypeConverter tc = context.getTypeConverter();
+ // text made of the characters of a number is not always a number
+ for (String text : new String[] {
+ "-", "+", ".", "e", "1e", "1e+", "e1", "--1", "+-1", "1.2.3",
"1e1.5", "2026-10-26" }) {
+ assertFalse(ObjectHelper.typeCoerceEquals(tc, 1, text), text);
+ assertFalse(ObjectHelper.typeCoerceEquals(tc, text,
BigDecimal.ONE), text);
+ }
+ // a sign, a decimal point at either end, and an exponent with a sign
are numbers, as BigDecimal parses them
+ assertTrue(ObjectHelper.typeCoerceEquals(tc, 10, "+10"));
+ assertTrue(ObjectHelper.typeCoerceEquals(tc, 10, "10."));
+ assertTrue(ObjectHelper.typeCoerceEquals(tc, new BigDecimal("0.5"),
".5"));
+ assertTrue(ObjectHelper.typeCoerceEquals(tc, -10, "-1e+1"));
+ assertTrue(ObjectHelper.typeCoerceEquals(tc, new BigDecimal("0.01"),
"1E-2"));
+ }
+
+ @Test
+ void testEqualsNumberAndTooLongNumericString() {
+ TypeConverter tc = context.getTypeConverter();
+ // text of up to 1000 characters is compared as a number; longer text
is not parsed, as parsing a BigDecimal
+ // takes time that grows with the square of the number of digits
+ String longest = "0".repeat(996) + "10.0";
+ assertTrue(ObjectHelper.typeCoerceEquals(tc, 10, longest));
+ assertTrue(ObjectHelper.typeCoerceEquals(tc, longest, 10L));
+ assertFalse(ObjectHelper.typeCoerceEquals(tc, 10, "0" + longest));
+ }
+
@Test
public void testCompareStringStringTooBigForLong() {
TypeConverter tc = context.getTypeConverter();
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 3e144f57640f..1b71b92ab02b 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 testEqualsNumberWithNumericTextWrittenDifferently() {
+ exchange.getIn().setHeader("amount", new BigDecimal("10.00"));
+ exchange.getIn().setHeader("ten", "10");
+ exchange.getIn().setHeader("count", 3);
+ exchange.getIn().setHeader("text", "10.00");
+ // == agrees with <= and >=, which compare the values
+ assertPredicate("${header.amount} <= '10'", true);
+ assertPredicate("${header.amount} >= '10'", true);
+ assertPredicate("${header.amount} == '10'", true);
+ assertPredicate("${header.amount} != '10'", false);
+ assertPredicate("${header.amount} == ${header.ten}", true);
+ assertPredicate("${header.amount} == '10.01'", false);
+ assertPredicate("${header.count} == '3.0'", true);
+ assertPredicate("${header.count} in '3.0'", true);
+ assertPredicate("${header.count} == '3.5'", false);
+ assertPredicate("${header.text} == 10", true);
+ assertPredicate("${header.text} != 10", false);
+ assertPredicate("${header.text} == 11", false);
+ }
+
@Test
void testInWithDecimals() {
exchange.getIn().setHeader("n", 2);
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 a07fb7568cdb..85125f9ba57d 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
@@ -71,6 +71,12 @@ public final class ObjectHelper {
private static final Pattern PARENTHESIS_PATTERN;
private static final String DEFAULT_DELIMITER = ",";
private static final char DEFAULT_DELIMITER_CHAR = ',';
+ /**
+ * The longest text that is parsed as a number to compare it with a
number. Parsing a {@link BigDecimal} takes time
+ * that grows with the square of the number of digits (a million digits
take seconds), and no number in a message is
+ * this long.
+ */
+ private static final int MAX_NUMBER_LENGTH = 1000;
/**
* Utility classes should not have a public constructor.
@@ -118,6 +124,13 @@ public final class ObjectHelper {
// converting one to the type of the other may drop the decimals,
and BigDecimal.equals also compares the
// scale (2.50 is not equal to 2.5)
return compareNumbers(leftNum, rightNum) == 0;
+ } else if (rightValue instanceof String text &&
isExactNumber(leftValue)) {
+ // a number and a numeric String (such as 10.00 and "10", or 10
and "1e1") are equal if their values are
+ // equal, as the <=, >= and in operators compare them, even when
the String is written differently
+ BigDecimal rightNum = decimalOrNull(text);
+ if (rightNum != null) {
+ return compareNumbers((Number) leftValue, rightNum) == 0;
+ }
}
// try without type coerce
@@ -134,6 +147,12 @@ public final class ObjectHelper {
return stringDoubleComparison((String) leftValue, doubleValue);
} else if (rightValue instanceof Boolean) {
return stringBooleanComparison(leftValue, rightValue);
+ } else if (isExactNumber(rightValue)) {
+ // a numeric String and a number are equal if their values are
equal (see typeCoerceEquals)
+ BigDecimal leftNum = decimalOrNull((String) leftValue);
+ if (leftNum != null) {
+ return compareNumbers(leftNum, (Number) rightValue) == 0;
+ }
}
return tryConverters(converter, leftValue, rightValue, ignoreCase);
@@ -146,16 +165,85 @@ public final class ObjectHelper {
private static Number asNumber(Object value) {
if (value instanceof Number number) {
return number;
- } else if (value instanceof String text && (isNumber(text) ||
isFloatingNumber(text))) {
- try {
- return new BigDecimal(text);
- } catch (NumberFormatException e) {
- return null;
- }
+ } else if (value instanceof String text) {
+ return decimalOrNull(text);
}
return null;
}
+ /**
+ * Parses the text as a decimal number, such as {@code 10}, {@code 10.00},
{@code -2.5} or {@code 1e1}, ignoring
+ * leading and trailing whitespace, or returns {@code null} if it is not a
number. Text longer than
+ * {@link #MAX_NUMBER_LENGTH} is not parsed.
+ */
+ private static BigDecimal decimalOrNull(String text) {
+ String number = text.trim();
+ if (number.isEmpty() || number.length() > MAX_NUMBER_LENGTH ||
!isDecimalNumber(number)) {
+ return null;
+ }
+ try {
+ return new BigDecimal(number);
+ } catch (NumberFormatException e) {
+ // such as an exponent that is too large
+ return null;
+ }
+ }
+
+ /**
+ * Whether the text is a number as {@link BigDecimal} parses it: an
optional sign, digits with an optional decimal
+ * point, and an optional exponent, such as {@code +10}, {@code 10.},
{@code .5} or {@code 1e-3}. This is checked
+ * before parsing, so text that is not a number (most text) does not throw
an exception.
+ */
+ private static boolean isDecimalNumber(String text) {
+ int len = text.length();
+ int i = 0;
+ char ch = text.charAt(0);
+ if (ch == '-' || ch == '+') {
+ i++;
+ }
+ boolean digits = false;
+ boolean dot = false;
+ for (; i < len; i++) {
+ ch = text.charAt(i);
+ if (Character.isDigit(ch)) {
+ digits = true;
+ } else if (ch == '.' && !dot) {
+ dot = true;
+ } else {
+ break;
+ }
+ }
+ if (!digits) {
+ return false;
+ } else if (i == len) {
+ return true;
+ } else if (ch != 'e' && ch != 'E') {
+ return false;
+ }
+ // the exponent
+ i++;
+ if (i < len && (text.charAt(i) == '-' || text.charAt(i) == '+')) {
+ i++;
+ }
+ if (i == len) {
+ return false;
+ }
+ for (; i < len; i++) {
+ if (!Character.isDigit(text.charAt(i))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Whether the value is a number other than a {@link Double} or {@link
Float}. A double or float is compared with a
+ * numeric String by converting the String to a double or float, which
compares their values already.
+ */
+ private static boolean isExactNumber(Object value) {
+ return value instanceof Number && !(value instanceof Double) &&
!(value instanceof Float);
+ }
+
/**
* Compares two numbers of any type by their numeric values.
*/
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index 936a5773a1be..bd8443700b2d 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -2747,6 +2747,19 @@ route of its route builder or file. The route structure
now leaves these clauses
`route-structure` dev console, `camel cmd route-structure`, and the route
diagrams drawn from it. Before, they were
listed among the steps of the route.
+=== camel-core - simple `==` compares a number and a numeric String by their
values
+
+The `==` and `!=` operators of the simple language compare a number with a
`String` that holds a number by their
+values, as `<`, `<=`, `>` and `>=` (and `in` when the left value is a number)
do since Camel 4.23. Before, when the two
+were written differently, the `String` was converted to the type of the number
and compared with `equals` (which also
+compares the scale of a `BigDecimal`), or the two were compared as text. So
`${header.amount} == '10'` is now `true`
+for a `BigDecimal` of `10.00` (it was `false`, while `${header.amount} <=
'10'` and `>= '10'` were `true`), and so are
+`${header.count} == '3.0'` for an `Integer` of `3` and `${header.text} == 10`
for a `String` of `10.00`. Spaces around
+the number and an exponent (such as `' 10'` and `'1e1'`) are allowed, as in
the `in` operator, and `<` and `>` compare
+such a `String` by its exact value too (it was compared as a double). The same
applies to `=~`, to `contains` on a
+collection, and to the Java DSL and mock predicates such as
`header("amount").isEqualTo("10")`. Two `String` values,
+and a `Double` or `Float` compared with a `String`, compare as before. Use
`equals` to compare the two values as text.
+
=== camel-diagram - a route diagram draws the steps of a route
A route diagram drew the `onException` clauses of a route among its steps,
though they are not steps: a global