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() {