This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch fix/CAMEL-24962 in repository https://gitbox.apache.org/repos/asf/camel.git
commit a80d45acd87e6b880598d9dc2736e84d068971de Author: Claus Ibsen <[email protected]> AuthorDate: Wed Sep 23 18:43:11 2026 +0200 CAMEL-24966: simple - fix numeric literals, elvis, ++/-- and null range/regex - a numeric literal evaluated as Long/int/Double is converted instead of cast (ClassCastException) - negative literals below the int range parse as long - elvis ?: treats any numeric zero as falsy and evaluates the left side once - ++ and -- keep decimals (1.5++ is 2.5) and evaluate the left side once - range and regex with a null right hand side do not match instead of a NullPointerException Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../language/simple/ast/BinaryExpression.java | 12 +++- .../language/simple/ast/NumericExpression.java | 8 +-- .../camel/language/simple/ast/OtherExpression.java | 10 ++- .../camel/language/simple/ast/UnaryExpression.java | 63 +++++++------------ .../camel/language/simple/SimpleOperatorTest.java | 72 ++++++++++++++++++++++ 5 files changed, 115 insertions(+), 50 deletions(-) diff --git a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/BinaryExpression.java b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/BinaryExpression.java index 46ce040e1c1f..39f4c8886e52 100644 --- a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/BinaryExpression.java +++ b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/BinaryExpression.java @@ -192,7 +192,10 @@ public class BinaryExpression extends BaseSimpleNode { @Override public <T> T evaluate(Exchange exchange, Class<T> type) { // reg ex should use String pattern, so we evaluate the right hand side as a String - Predicate predicate = PredicateBuilder.regex(leftExp, rightExp.evaluate(exchange, String.class)); + String pattern = rightExp.evaluate(exchange, String.class); + // no pattern (such as a missing header) matches nothing + Predicate predicate = pattern != null + ? PredicateBuilder.regex(leftExp, pattern) : PredicateBuilder.constant(false); if (operator == BinaryOperatorType.NOT_REGEX) { predicate = PredicateBuilder.not(predicate); } @@ -245,8 +248,11 @@ public class BinaryExpression extends BaseSimpleNode { Predicate predicate; String range = rightExp.evaluate(exchange, String.class); - Matcher matcher = RANGE_PATTERN.matcher(range); - if (matcher.matches()) { + Matcher matcher = range != null ? RANGE_PATTERN.matcher(range) : null; + if (range == null) { + // no range (such as a missing header) contains nothing + predicate = PredicateBuilder.constant(false); + } else if (matcher.matches()) { // wrap as constant expression for the from and to values Expression from = ExpressionBuilder.constantExpression(matcher.group(1)); Expression to = ExpressionBuilder.constantExpression(matcher.group(3)); 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 ac5cb062a88e..445abf97a0bb 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 @@ -40,8 +40,8 @@ public class NumericExpression extends BaseSimpleNode { } else { // its either a long or integer value (lets just avoid bytes) long lon = Long.parseLong(text); - if (lon < Integer.MAX_VALUE) { - number = Integer.valueOf(text); + if (lon >= Integer.MIN_VALUE && lon <= Integer.MAX_VALUE) { + number = (int) lon; } else { number = lon; } @@ -68,9 +68,7 @@ public class NumericExpression extends BaseSimpleNode { return new Expression() { @Override public <T> T evaluate(Exchange exchange, Class<T> type) { - if (type == Object.class || type == int.class || type == Integer.class - || type == long.class || type == Long.class - || type == double.class || type == Double.class) { + if (type == Object.class || type.isInstance(number)) { return type.cast(number); } return exchange.getContext().getTypeConverter().tryConvertTo(type, exchange, number); diff --git a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/OtherExpression.java b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/OtherExpression.java index ca3d97449937..f5dd6a376787 100644 --- a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/OtherExpression.java +++ b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/OtherExpression.java @@ -95,11 +95,12 @@ public class OtherExpression extends BaseSimpleNode { return new Expression() { @Override public <T> T evaluate(Exchange exchange, Class<T> type) { + // evaluate only once as the left hand side may have side effects Object value = leftExp.evaluate(exchange, Object.class); - if (value == null || Boolean.FALSE == value || ObjectHelper.isEmpty(value) || ObjectHelper.equal(0, value)) { + if (value == null || Boolean.FALSE == value || ObjectHelper.isEmpty(value) || isZero(value)) { return rightExp.evaluate(exchange, type); } else { - return leftExp.evaluate(exchange, type); + return camelContext.getTypeConverter().convertTo(type, exchange, value); } } @@ -109,4 +110,9 @@ public class OtherExpression extends BaseSimpleNode { } }; } + + private static boolean isZero(Object value) { + // any kind of number such as 0, 0L, 0.0 or BigDecimal.ZERO + return value instanceof Number n && n.doubleValue() == 0; + } } diff --git a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/UnaryExpression.java b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/UnaryExpression.java index 6b50e5b97b7d..07fcd0fb1c60 100644 --- a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/UnaryExpression.java +++ b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/ast/UnaryExpression.java @@ -16,6 +16,9 @@ */ package org.apache.camel.language.simple.ast; +import java.math.BigDecimal; +import java.math.BigInteger; + import org.apache.camel.CamelContext; import org.apache.camel.CamelExchangeException; import org.apache.camel.Exchange; @@ -73,65 +76,45 @@ public class UnaryExpression extends BaseSimpleNode { final Expression leftExp = left.createExpression(camelContext, expression); if (operator == UnaryOperatorType.INC) { - return createIncExpression(camelContext, leftExp); + return createIncDecExpression(camelContext, leftExp, 1); } else if (operator == UnaryOperatorType.DEC) { - return createDecExpression(camelContext, leftExp); + return createIncDecExpression(camelContext, leftExp, -1); } throw new SimpleParserException("Unknown unary operator " + operator, token.getIndex()); } - private Expression createIncExpression(CamelContext camelContext, final Expression leftExp) { + private Expression createIncDecExpression(CamelContext camelContext, final Expression leftExp, final int delta) { return new Expression() { @Override public <T> T evaluate(Exchange exchange, Class<T> type) { - Number num = leftExp.evaluate(exchange, Number.class); + // evaluate only once as the left hand side may have side effects + Object value = leftExp.evaluate(exchange, Object.class); + Number num = value instanceof Number n + ? n : camelContext.getTypeConverter().convertTo(Number.class, exchange, value); if (num != null) { - long val = num.longValue(); - val++; - - // convert value back to same type as input as we want to preserve type - Object left = leftExp.evaluate(exchange, Object.class); - try { - left = camelContext.getTypeConverter().mandatoryConvertTo(left.getClass(), exchange, val); - } catch (NoTypeConversionAvailableException e) { - throw RuntimeCamelException.wrapRuntimeCamelException(e); + // keep decimals such as 1.5++ is 2.5 + Number result; + if (num instanceof BigDecimal bd) { + result = bd.add(BigDecimal.valueOf(delta)); + } else if (num instanceof BigInteger bi) { + result = bi.add(BigInteger.valueOf(delta)); + } else if (num instanceof Double || num instanceof Float) { + result = num.doubleValue() + delta; + } else { + result = num.longValue() + delta; } - // and return the result - return camelContext.getTypeConverter().convertTo(type, left); - } - // cannot convert the expression as a number - Exception cause = new CamelExchangeException("Cannot evaluate " + leftExp + " as a number", exchange); - throw RuntimeCamelException.wrapRuntimeCamelException(cause); - } - - @Override - public String toString() { - return left + operator.toString(); - } - }; - } - - private Expression createDecExpression(CamelContext camelContext, final Expression leftExp) { - return new Expression() { - @Override - public <T> T evaluate(Exchange exchange, Class<T> type) { - Number num = leftExp.evaluate(exchange, Number.class); - if (num != null) { - long val = num.longValue(); - val--; - // convert value back to same type as input as we want to preserve type - Object left = leftExp.evaluate(exchange, Object.class); + Object answer; try { - left = camelContext.getTypeConverter().mandatoryConvertTo(left.getClass(), exchange, val); + answer = camelContext.getTypeConverter().mandatoryConvertTo(value.getClass(), exchange, result); } catch (NoTypeConversionAvailableException e) { throw RuntimeCamelException.wrapRuntimeCamelException(e); } // and return the result - return camelContext.getTypeConverter().convertTo(type, left); + return camelContext.getTypeConverter().convertTo(type, answer); } // cannot convert the expression as a number Exception cause = new CamelExchangeException("Cannot evaluate " + leftExp + " as a number", exchange); 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 a20eb732c9a2..4e1d666c33d8 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 @@ -16,7 +16,10 @@ */ package org.apache.camel.language.simple; +import java.math.BigDecimal; + import org.apache.camel.Exchange; +import org.apache.camel.Expression; import org.apache.camel.LanguageTestSupport; import org.apache.camel.Predicate; import org.apache.camel.language.simple.types.SimpleIllegalSyntaxException; @@ -1092,11 +1095,80 @@ public class SimpleOperatorTest extends LanguageTestSupport { assertPredicate("${header.Account1} > 7", true); } + @Test + public void testNumericLiteralAsOtherNumberType() { + // CAMEL-24966: a numeric literal evaluated as another number type is converted, not cast + exchange.getIn().setBody(5); + Expression exp = context.resolveLanguage("simple").createExpression("${body > 0 ? 1 : 0}"); + assertEquals(1L, exp.evaluate(exchange, Long.class)); + assertEquals(1, exp.evaluate(exchange, int.class)); + assertEquals(1.0d, exp.evaluate(exchange, Double.class)); + assertEquals("1", exp.evaluate(exchange, String.class)); + } + + @Test + public void testNegativeLongLiteral() { + exchange.getIn().setHeader("x", 5); + assertPredicate("${header.x} > -3000000000", true); + assertPredicate("${header.x} < -3000000000", false); + assertPredicate("${header.x} > -2147483648", true); + } + + @Test + public void testElvisAnyZero() { + exchange.getIn().setHeader("count", 0L); + assertExpression("${header.count} ?: 'none'", "none"); + exchange.getIn().setHeader("count", 0.0d); + assertExpression("${header.count} ?: 'none'", "none"); + exchange.getIn().setHeader("count", BigDecimal.ZERO); + assertExpression("${header.count} ?: 'none'", "none"); + exchange.getIn().setHeader("count", 3L); + assertExpression("${header.count} ?: 'none'", 3L); + } + + @Test + public void testElvisEvaluatesLeftOnce() { + MyCounter counter = new MyCounter(); + context.getRegistry().bind("counter", counter); + assertExpression("${bean:counter?method=next} ?: 'none'", 1); + assertEquals(1, counter.count); + } + + @Test + public void testUnaryIncDecKeepsDecimals() { + exchange.getIn().setHeader("price", 1.5d); + assertExpression("${header.price}++", 2.5d); + assertExpression("${header.price}--", 0.5d); + exchange.getIn().setHeader("price", new BigDecimal("1.25")); + assertExpression("${header.price}++", new BigDecimal("2.25")); + exchange.getIn().setHeader("price", "1.5"); + assertExpression("${header.price}++", "2.5"); + exchange.getIn().setHeader("price", 7); + assertExpression("${header.price}--", 6); + } + + @Test + public void testRegexAndRangeWithNullRightHandSide() { + exchange.getIn().setBody(5); + assertPredicate("${body} range ${header.none}", false); + assertPredicate("${body} !range ${header.none}", true); + assertPredicate("${body} regex ${header.none}", false); + assertPredicate("${body} !regex ${header.none}", true); + } + @Override protected String getLanguageName() { return "simple"; } + public static class MyCounter { + private int count; + + public int next() { + return ++count; + } + } + public static class MyFileNameGenerator { public String generateFilename(Exchange exchange) { return "abc";
