Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,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}
*/
Expand Down Expand Up @@ -296,7 +293,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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -217,27 +218,43 @@ 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;
}

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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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 <tt>null</tt> 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
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down