From 7da3c4217d9b1f6e3bca79b4d3bdb6515a046211 Mon Sep 17 00:00:00 2001 From: Karol Date: Mon, 31 Aug 2026 10:05:32 +0200 Subject: [PATCH] CAMEL-24407: simple predicate fails for long digital strings Numbers with more digits than a long can hold, such as bank account numbers, made comparisons throw NumberFormatException. isNumber only checks that the text is all digits, and the callers then parsed it with Long.parseLong or Integer.valueOf. Compare those as BigInteger instead. A number that does not fit in a long can never equal an int or long, so equality with one is false. A numeric literal in a simple predicate that does not fit in a long is now kept as literal text, so it takes the same comparison path. Co-authored-by: Claude --- .../simple/SimplePredicateParser.java | 5 +- .../simple/ast/NumericExpression.java | 12 +++ .../converter/TypeCoerceCompareTest.java | 21 ++++ .../language/simple/SimpleOperatorTest.java | 17 ++++ .../apache/camel/util/ObjectHelperTest.java | 22 +++++ .../apache/camel/support/ObjectHelper.java | 98 +++++++++++++------ .../camel/support/ObjectHelperTest.java | 29 ++++++ 7 files changed, 168 insertions(+), 36 deletions(-) diff --git a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimplePredicateParser.java b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimplePredicateParser.java index 016e819faa3d4..94d3cd2aac865 100644 --- a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimplePredicateParser.java +++ b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimplePredicateParser.java @@ -58,9 +58,6 @@ import org.apache.camel.support.builder.PredicateBuilder; import org.apache.camel.util.StringHelper; -import static org.apache.camel.support.ObjectHelper.isFloatingNumber; -import static org.apache.camel.support.ObjectHelper.isNumber; - /** * A parser to parse simple language as a Camel {@link Predicate} */ @@ -321,7 +318,7 @@ private void addImageToken(LiteralNode imageToken) { if (!quoted) { // if the text is not in a quoted block (literal text), then lets see if // its numeric then we can optimize this - numeric = isNumber(text) || isFloatingNumber(text); + numeric = NumericExpression.isNumericValue(text); } if (numeric) { nodes.add(new NumericExpression(imageToken.getToken(), text)); diff --git a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/NumericExpression.java b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/NumericExpression.java index 9f39a63e4f4f4..8f38ab8155ed3 100644 --- a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/NumericExpression.java +++ b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/NumericExpression.java @@ -21,6 +21,7 @@ import org.apache.camel.Expression; import org.apache.camel.language.simple.types.SimpleParserException; import org.apache.camel.language.simple.types.SimpleToken; +import org.apache.camel.support.ObjectHelper; /** * Represents a numeric value. @@ -47,6 +48,17 @@ public NumericExpression(SimpleToken token, String text) { } } + /** + * Whether the text can be represented as a numeric value. Numbers with more digits than a long can hold, such as + * bank account numbers, are kept as literal text instead, so they can be compared as big integers. + */ + public static boolean isNumericValue(String text) { + if (text.indexOf('.') != -1) { + return ObjectHelper.isFloatingNumber(text); + } + return ObjectHelper.isLongNumber(text); + } + public Object getNumber() { return number; } 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 67d35dd474813..68ef8d464f970 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 @@ -41,6 +41,27 @@ public void testCompareStringString() { assertEquals(0, ObjectHelper.typeCoerceCompare(tc, "7.0", "7")); } + @Test + public void testCompareStringStringTooBigForLong() { + TypeConverter tc = context.getTypeConverter(); + // numbers such as bank account numbers have more digits than a long can hold + assertEquals(0, ObjectHelper.typeCoerceCompare(tc, "12345678901234567890", "12345678901234567890")); + assertTrue(ObjectHelper.typeCoerceCompare(tc, "12345678901234567891", "12345678901234567890") > 0); + assertTrue(ObjectHelper.typeCoerceCompare(tc, "12345678901234567890", "12345678901234567891") < 0); + assertTrue(ObjectHelper.typeCoerceCompare(tc, "12345678901234567890", "7") > 0); + assertTrue(ObjectHelper.typeCoerceCompare(tc, "7", "12345678901234567890") < 0); + } + + @Test + public void testCompareStringNumberTooBigForLong() { + TypeConverter tc = context.getTypeConverter(); + assertTrue(ObjectHelper.typeCoerceCompare(tc, "12345678901234567890", 7L) > 0); + assertTrue(ObjectHelper.typeCoerceCompare(tc, 7L, "12345678901234567890") < 0); + // does not fit in an int, but still fits in a long + assertTrue(ObjectHelper.typeCoerceCompare(tc, "99999999999", 7) > 0); + assertTrue(ObjectHelper.typeCoerceCompare(tc, 7, "99999999999") < 0); + } + @Test public void testCompareStringInteger() { 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 6d123c342f721..dd1539b0beb32 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 @@ -973,6 +973,23 @@ public void testChainParam() { assertExpression("${trim()} ~> ${replace('Hello','Hi',$param)} ~> ${split($param,' ')} ~> ${size($param)}", 5); } + @Test + public void testDigitalStringTooBigForLong() { + // CAMEL-24407: numbers such as bank account numbers have more digits than a long can hold + exchange.getIn().setHeader("Account1", "12345678901234567890"); + exchange.getIn().setHeader("Account2", "12345678901234567890"); + exchange.getIn().setHeader("Account3", "12345678901234567891"); + + assertPredicate("${header.Account1} == ${header.Account2}", true); + assertPredicate("${header.Account1} == ${header.Account3}", false); + assertPredicate("${header.Account1} != ${header.Account3}", true); + assertPredicate("${header.Account1} < ${header.Account3}", true); + assertPredicate("${header.Account3} > ${header.Account1}", true); + assertPredicate("${header.Account1} == 12345678901234567890", true); + assertPredicate("${header.Account1} == '12345678901234567890'", true); + assertPredicate("${header.Account1} > 7", true); + } + @Override protected String getLanguageName() { return "simple"; diff --git a/core/camel-core/src/test/java/org/apache/camel/util/ObjectHelperTest.java b/core/camel-core/src/test/java/org/apache/camel/util/ObjectHelperTest.java index 981627e9d617f..3b4e1e09b865b 100644 --- a/core/camel-core/src/test/java/org/apache/camel/util/ObjectHelperTest.java +++ b/core/camel-core/src/test/java/org/apache/camel/util/ObjectHelperTest.java @@ -175,6 +175,28 @@ void testEqualsStreamCaching() throws Exception { } } + @Test + void testEqualsNumberTooBigForLong() throws Exception { + try (CamelContext context = new DefaultCamelContext()) { + context.start(); + TypeConverter tc = context.getTypeConverter(); + + // numbers such as bank account numbers have more digits than a long can hold + assertTrue(ObjectHelper.typeCoerceEquals(tc, "12345678901234567890", "12345678901234567890")); + assertFalse(ObjectHelper.typeCoerceEquals(tc, "12345678901234567890", "12345678901234567891")); + assertFalse(ObjectHelper.typeCoerceEquals(tc, "12345678901234567890", "7")); + + // such a number cannot be equal to an int or long + assertFalse(ObjectHelper.typeCoerceEquals(tc, "12345678901234567890", 7L)); + assertFalse(ObjectHelper.typeCoerceEquals(tc, 7L, "12345678901234567890")); + + // does not fit in an int, but still fits in a long + assertFalse(ObjectHelper.typeCoerceEquals(tc, "99999999999", 7)); + assertFalse(ObjectHelper.typeCoerceEquals(tc, 7, "99999999999")); + assertTrue(ObjectHelper.typeCoerceEquals(tc, "99999999999", 99999999999L)); + } + } + @Test void testContainsStringBuilder() throws Exception { try (CamelContext context = new DefaultCamelContext()) { 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 df24b780a78b8..f91a7e6e4e884 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 @@ -22,6 +22,7 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.math.BigInteger; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -217,19 +218,29 @@ private static boolean stringDoubleComparison(String leftValue, Double rightValu } private static boolean typeCoerceIntLong(Object leftValue, String rightValue) { + Long rightNum = toLong(rightValue); + if (rightNum == null) { + // too big for a long so it cannot be equal to an int or long + return false; + } if (leftValue instanceof Integer intValue) { - return integerPairComparison(intValue, Integer.valueOf(rightValue)); + return longPairComparison(intValue.longValue(), rightNum); } else if (leftValue instanceof Long longValue) { - return longPairComparison(longValue, Long.valueOf(rightValue)); + return longPairComparison(longValue, rightNum); } return false; } private static boolean typeCoerceILString(String leftValue, Object rightValue) { + Long leftNum = toLong(leftValue); + if (leftNum == null) { + // too big for a long so it cannot be equal to an int or long + return false; + } if (rightValue instanceof Integer intValue) { - return integerPairComparison(Integer.valueOf(leftValue), intValue); + return longPairComparison(leftNum, intValue.longValue()); } else if (rightValue instanceof Long longValue) { - return longPairComparison(Long.valueOf(leftValue), longValue); + return longPairComparison(leftNum, longValue); } return false; } @@ -237,7 +248,13 @@ private static boolean typeCoerceILString(String leftValue, Object rightValue) { private static boolean typeCoerceStringPair(String leftNum, String rightNum, boolean ignoreCase) { if (isNumber(leftNum) && isNumber(rightNum)) { // favour to use numeric comparison - return longPairComparison(Long.parseLong(leftNum), Long.parseLong(rightNum)); + Long left = toLong(leftNum); + Long right = toLong(rightNum); + if (left != null && right != null) { + return longPairComparison(left, right); + } + // too big for a long so compare as big integers + return new BigInteger(leftNum).equals(new BigInteger(rightNum)); } if (ignoreCase) { return leftNum.compareToIgnoreCase(rightNum) == 0; @@ -276,23 +293,22 @@ public static int typeCoerceCompare(TypeConverter converter, Object leftValue, O return leftNum.compareTo(rightNum); } else if ((rightValue instanceof Integer || rightValue instanceof Long) && leftValue instanceof String leftStr && isNumber(leftStr)) { - if (rightValue instanceof Integer rightNum) { - Integer leftNum = Integer.valueOf(leftStr); - return leftNum.compareTo(rightNum); - } else { - Long leftNum = Long.valueOf(leftStr); - Long rightNum = (Long) rightValue; - return leftNum.compareTo(rightNum); + long rightNum = ((Number) rightValue).longValue(); + Long leftNum = toLong(leftStr); + if (leftNum == null) { + // too big for a long so compare as big integers + return new BigInteger(leftStr).compareTo(BigInteger.valueOf(rightNum)); } + return Long.compare(leftNum, rightNum); } else if (rightValue instanceof String rightStr && (leftValue instanceof Integer || leftValue instanceof Long) && isNumber(rightStr)) { - if (leftValue instanceof Integer leftNum) { - Integer rightNum = Integer.valueOf(rightStr); - return leftNum.compareTo(rightNum); - } else if (leftValue instanceof Long leftNum) { - Long rightNum = Long.valueOf(rightStr); - return leftNum.compareTo(rightNum); + long leftNum = ((Number) leftValue).longValue(); + Long rightNum = toLong(rightStr); + if (rightNum == null) { + // too big for a long so compare as big integers + return BigInteger.valueOf(leftNum).compareTo(new BigInteger(rightStr)); } + return Long.compare(leftNum, rightNum); } else if (rightValue instanceof Double rightNum && leftValue instanceof String leftStr && isFloatingNumber(leftStr)) { Double leftNum = Double.valueOf(leftStr); @@ -355,27 +371,45 @@ && isFloatingNumber(leftStr)) { private static int typeCoerceCompareStringString(String leftNum, String rightNum) { // prioritize non-floating numbers first - Long num1 = isNumber(leftNum) ? Long.parseLong(leftNum) : null; - Long num2 = isNumber(rightNum) ? Long.parseLong(rightNum) : null; - Double dec1 = num1 == null && isFloatingNumber(leftNum) ? Double.parseDouble(leftNum) : null; - Double dec2 = num2 == null && isFloatingNumber(rightNum) ? Double.parseDouble(rightNum) : null; - if (num1 != null && num2 != null) { - return num1.compareTo(num2); - } else if (dec1 != null && dec2 != null) { - return dec1.compareTo(dec2); + if (isNumber(leftNum) && isNumber(rightNum)) { + Long num1 = toLong(leftNum); + Long num2 = toLong(rightNum); + if (num1 != null && num2 != null) { + return num1.compareTo(num2); + } + // too big for a long so compare as big integers + return new BigInteger(leftNum).compareTo(new BigInteger(rightNum)); } - // okay mixed but we need to convert to floating - if (num1 != null && dec2 != null) { - dec1 = Double.parseDouble(leftNum); - return dec1.compareTo(dec2); - } else if (num2 != null && dec1 != null) { - dec2 = Double.parseDouble(rightNum); + // mixed or floating numbers are compared as floating + Double dec1 = isFloatingNumber(leftNum) ? Double.parseDouble(leftNum) : null; + Double dec2 = isFloatingNumber(rightNum) ? Double.parseDouble(rightNum) : null; + if (dec1 != null && dec2 != null) { return dec1.compareTo(dec2); } // fallback to string comparison return leftNum.compareTo(rightNum); } + /** + * Checks whether the text is an integer number that fits in a {@link Long}. Numbers such as bank account numbers + * can have more digits than a long can hold, and must be compared as {@link BigInteger} instead. + */ + public static boolean isLongNumber(String text) { + return isNumber(text) && toLong(text) != null; + } + + /** + * Parses the text as a long, or null if the number has too many digits to fit in a {@link Long}. The text + * is expected to be checked with {@link #isNumber(String)} first, so overflow is the only way this fails. + */ + private static Long toLong(String text) { + try { + return Long.parseLong(text); + } catch (NumberFormatException e) { + return null; + } + } + /** * Checks whether the text is an integer number */ diff --git a/core/camel-support/src/test/java/org/apache/camel/support/ObjectHelperTest.java b/core/camel-support/src/test/java/org/apache/camel/support/ObjectHelperTest.java index 23f42f12191dc..1e7a24ace8810 100644 --- a/core/camel-support/src/test/java/org/apache/camel/support/ObjectHelperTest.java +++ b/core/camel-support/src/test/java/org/apache/camel/support/ObjectHelperTest.java @@ -62,6 +62,35 @@ void isNumberFloats() { assertFalse(ObjectHelper.isNumber("0.0")); } + @Test + @DisplayName("Tests that isLongNumber returns true for integers that fit in a long") + void isLongNumberIntegers() { + assertTrue(ObjectHelper.isLongNumber("1234")); + assertTrue(ObjectHelper.isLongNumber("-1234")); + assertTrue(ObjectHelper.isLongNumber("0")); + assertTrue(ObjectHelper.isLongNumber("9223372036854775807")); + assertTrue(ObjectHelper.isLongNumber("-9223372036854775808")); + assertTrue(ObjectHelper.isLongNumber("00000000000000000000001")); + } + + @Test + @DisplayName("Tests that isLongNumber returns false for integers with too many digits") + void isLongNumberTooBig() { + assertFalse(ObjectHelper.isLongNumber("9223372036854775808")); + assertFalse(ObjectHelper.isLongNumber("-9223372036854775809")); + assertFalse(ObjectHelper.isLongNumber("12345678901234567890")); + } + + @Test + @DisplayName("Tests that isLongNumber returns false for non-integers") + void isLongNumberNonIntegers() { + assertFalse(ObjectHelper.isLongNumber("")); + assertFalse(ObjectHelper.isLongNumber(" ")); + assertFalse(ObjectHelper.isLongNumber(null)); + assertFalse(ObjectHelper.isLongNumber("ABC")); + assertFalse(ObjectHelper.isLongNumber("12.34")); + } + @Test @DisplayName("Tests that isFloatingNumber returns true for empty, space or null") void isFloatingNumberEmpty() {