This is an automated email from the ASF dual-hosted git repository.
shuber pushed a commit to branch unomi-3-dev
in repository https://gitbox.apache.org/repos/asf/unomi.git
The following commit(s) were added to refs/heads/unomi-3-dev by this push:
new 6f455efea Add null-safe handling for comparisons in
`PropertyConditionEvaluator` and extend test coverage
6f455efea is described below
commit 6f455efeaa2462277c255349d436b3d65a7b52c3
Author: Serge Huber <[email protected]>
AuthorDate: Thu Jan 1 16:15:58 2026 +0100
Add null-safe handling for comparisons in `PropertyConditionEvaluator` and
extend test coverage
- Updated `PropertyConditionEvaluator` to handle null values gracefully
during numeric (Integer, Double) and date (Date, DateExpr) comparisons,
avoiding NullPointerExceptions.
- Introduced new unit tests in `PropertyConditionEvaluatorTest` to validate
null-safe comparison logic for non-numeric and non-date values.
- Enhanced "between" and other comparison operators to handle edge cases
involving invalid or null inputs.
---
.../conditions/PropertyConditionEvaluator.java | 153 +++++++++++++++++--
.../conditions/PropertyConditionEvaluatorTest.java | 166 ++++++++++++++++++++-
2 files changed, 301 insertions(+), 18 deletions(-)
diff --git
a/plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/conditions/PropertyConditionEvaluator.java
b/plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/conditions/PropertyConditionEvaluator.java
index 31ecdef7d..8229fe6c1 100644
---
a/plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/conditions/PropertyConditionEvaluator.java
+++
b/plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/conditions/PropertyConditionEvaluator.java
@@ -25,9 +25,9 @@ import org.apache.unomi.api.Item;
import org.apache.unomi.api.conditions.Condition;
import org.apache.unomi.persistence.spi.PropertyHelper;
import org.apache.unomi.persistence.spi.conditions.ConditionContextHelper;
+import org.apache.unomi.persistence.spi.conditions.DateUtils;
import
org.apache.unomi.persistence.spi.conditions.evaluator.ConditionEvaluator;
import
org.apache.unomi.persistence.spi.conditions.evaluator.ConditionEvaluatorDispatcher;
-import org.apache.unomi.persistence.spi.conditions.DateUtils;
import org.apache.unomi.persistence.spi.conditions.geo.DistanceUnit;
import
org.apache.unomi.plugins.baseplugin.conditions.accessors.HardcodedPropertyAccessor;
import org.apache.unomi.scripting.ExpressionFilterFactory;
@@ -83,13 +83,37 @@ public class PropertyConditionEvaluator implements
ConditionEvaluator {
}
if (expectedValueInteger != null) {
- return
PropertyHelper.getInteger(actualValue).compareTo(PropertyHelper.getInteger(expectedValueInteger));
+ Integer actualInt = PropertyHelper.getInteger(actualValue);
+ Integer expectedInt =
PropertyHelper.getInteger(expectedValueInteger);
+ if (actualInt == null || expectedInt == null) {
+ // If either value cannot be converted to integer, they are
not equal
+ return actualInt == null && expectedInt == null ? 0 :
(actualInt == null ? -1 : 1);
+ }
+ return actualInt.compareTo(expectedInt);
} else if (expectedValueDouble != null) {
- return
PropertyHelper.getDouble(actualValue).compareTo(PropertyHelper.getDouble(expectedValueDouble));
+ Double actualDouble = PropertyHelper.getDouble(actualValue);
+ Double expectedDouble =
PropertyHelper.getDouble(expectedValueDouble);
+ if (actualDouble == null || expectedDouble == null) {
+ // If either value cannot be converted to double, they are not
equal
+ return actualDouble == null && expectedDouble == null ? 0 :
(actualDouble == null ? -1 : 1);
+ }
+ return actualDouble.compareTo(expectedDouble);
} else if (expectedValueDate != null) {
- return getDate(actualValue).compareTo(getDate(expectedValueDate));
+ Date actualDate = getDate(actualValue);
+ Date expectedDate = getDate(expectedValueDate);
+ if (actualDate == null || expectedDate == null) {
+ // If either value cannot be converted to date, they are not
equal
+ return actualDate == null && expectedDate == null ? 0 :
(actualDate == null ? -1 : 1);
+ }
+ return actualDate.compareTo(expectedDate);
} else if (expectedValueDateExpr != null) {
- return
getDate(actualValue).compareTo(getDate(expectedValueDateExpr));
+ Date actualDate = getDate(actualValue);
+ Date expectedDate = getDate(expectedValueDateExpr);
+ if (actualDate == null || expectedDate == null) {
+ // If either value cannot be converted to date, they are not
equal
+ return actualDate == null && expectedDate == null ? 0 :
(actualDate == null ? -1 : 1);
+ }
+ return actualDate.compareTo(expectedDate);
} else {
// We use foldToASCII here to match the behavior of the analyzer
configuration in the persistence configuration
return
ConditionContextHelper.foldToASCII(actualValue.toString()).compareTo(expectedValue);
@@ -267,6 +291,64 @@ public class PropertyConditionEvaluator implements
ConditionEvaluator {
return false;
} else if (actualValue == null) {
return op.equals("missing") || op.equals("notIn") ||
op.equals("notEquals") || op.equals("hasNoneOf");
+ } else if (expectedValueInteger != null &&
+ ("greaterThan".equals(op) || "greaterThanOrEqualTo".equals(op)
|| "lessThan".equals(op) || "lessThanOrEqualTo".equals(op))) {
+ Integer actualInt = PropertyHelper.getInteger(actualValue);
+ Integer expectedInt =
PropertyHelper.getInteger(expectedValueInteger);
+ if (actualInt == null || expectedInt == null) {
+ return false;
+ }
+ switch (op) {
+ case "greaterThan":
+ return actualInt.compareTo(expectedInt) > 0;
+ case "greaterThanOrEqualTo":
+ return actualInt.compareTo(expectedInt) >= 0;
+ case "lessThan":
+ return actualInt.compareTo(expectedInt) < 0;
+ case "lessThanOrEqualTo":
+ return actualInt.compareTo(expectedInt) <= 0;
+ default:
+ return false;
+ }
+ } else if (expectedValueDouble != null &&
+ ("greaterThan".equals(op) || "greaterThanOrEqualTo".equals(op)
|| "lessThan".equals(op) || "lessThanOrEqualTo".equals(op))) {
+ Double actualDouble = PropertyHelper.getDouble(actualValue);
+ Double expectedDouble =
PropertyHelper.getDouble(expectedValueDouble);
+ if (actualDouble == null || expectedDouble == null) {
+ return false;
+ }
+ switch (op) {
+ case "greaterThan":
+ return actualDouble.compareTo(expectedDouble) > 0;
+ case "greaterThanOrEqualTo":
+ return actualDouble.compareTo(expectedDouble) >= 0;
+ case "lessThan":
+ return actualDouble.compareTo(expectedDouble) < 0;
+ case "lessThanOrEqualTo":
+ return actualDouble.compareTo(expectedDouble) <= 0;
+ default:
+ return false;
+ }
+ } else if ((expectedValueDate != null || expectedValueDateExpr !=
null) &&
+ ("greaterThan".equals(op) || "greaterThanOrEqualTo".equals(op)
|| "lessThan".equals(op) || "lessThanOrEqualTo".equals(op))) {
+ Date actualDate = getDate(actualValue);
+ Object expectedDateObj = expectedValueDate != null ?
expectedValueDate : expectedValueDateExpr;
+ Date expectedDate = getDate(expectedDateObj);
+ if (actualDate == null || expectedDate == null) {
+ return false;
+ }
+ switch (op) {
+ case "greaterThan":
+ return actualDate.compareTo(expectedDate) > 0;
+ case "greaterThanOrEqualTo":
+ return actualDate.compareTo(expectedDate) >= 0;
+ case "lessThan":
+ return actualDate.compareTo(expectedDate) < 0;
+ case "lessThanOrEqualTo":
+ return actualDate.compareTo(expectedDate) <= 0;
+ default:
+ return false;
+ }
} else if (op.equals("exists")) {
if (actualValue instanceof List) {
return ((List) actualValue).size() > 0;
@@ -300,17 +382,56 @@ public class PropertyConditionEvaluator implements
ConditionEvaluator {
Collection<?> expectedValuesDouble = (Collection<?>)
condition.getParameter("propertyValuesDouble");
Collection<?> expectedValuesDate = (Collection<?>)
condition.getParameter("propertyValuesDate");
Collection<?> expectedValuesDateExpr = (Collection<?>)
condition.getParameter("propertyValuesDateExpr");
- return compare(actualValue, null,
- getDate(getFirst(expectedValuesDate)),
- getFirst(expectedValuesInteger),
- getFirst(expectedValuesDateExpr),
- getFirst(expectedValuesDouble)) >= 0
- &&
- compare(actualValue, null,
- getDate(getSecond(expectedValuesDate)),
- getSecond(expectedValuesInteger),
- getSecond(expectedValuesDateExpr),
- getSecond(expectedValuesDouble)) <= 0;
+ Object firstInteger = getFirst(expectedValuesInteger);
+ Object secondInteger = getSecond(expectedValuesInteger);
+ Object firstDouble = getFirst(expectedValuesDouble);
+ Object secondDouble = getSecond(expectedValuesDouble);
+ Date firstDate = getDate(getFirst(expectedValuesDate));
+ Date secondDate = getDate(getSecond(expectedValuesDate));
+ Date firstDateExpr = getDate(getFirst(expectedValuesDateExpr));
+ Date secondDateExpr = getDate(getSecond(expectedValuesDateExpr));
+
+ // Numeric between (integer)
+ if (firstInteger != null || secondInteger != null) {
+ Integer actualInt = PropertyHelper.getInteger(actualValue);
+ Integer firstInt = PropertyHelper.getInteger(firstInteger);
+ Integer secondInt = PropertyHelper.getInteger(secondInteger);
+ if (actualInt == null || firstInt == null || secondInt ==
null) {
+ return false;
+ }
+ return actualInt.compareTo(firstInt) >= 0 &&
actualInt.compareTo(secondInt) <= 0;
+ }
+
+ // Numeric between (double)
+ if (firstDouble != null || secondDouble != null) {
+ Double actualDouble = PropertyHelper.getDouble(actualValue);
+ Double firstD = PropertyHelper.getDouble(firstDouble);
+ Double secondD = PropertyHelper.getDouble(secondDouble);
+ if (actualDouble == null || firstD == null || secondD == null)
{
+ return false;
+ }
+ return actualDouble.compareTo(firstD) >= 0 &&
actualDouble.compareTo(secondD) <= 0;
+ }
+
+ // Date between (explicit date)
+ if (firstDate != null || secondDate != null) {
+ Date actualDate = getDate(actualValue);
+ if (actualDate == null || firstDate == null || secondDate ==
null) {
+ return false;
+ }
+ return actualDate.compareTo(firstDate) >= 0 &&
actualDate.compareTo(secondDate) <= 0;
+ }
+
+ // Date between (date expression)
+ if (firstDateExpr != null || secondDateExpr != null) {
+ Date actualDate = getDate(actualValue);
+ if (actualDate == null || firstDateExpr == null ||
secondDateExpr == null) {
+ return false;
+ }
+ return actualDate.compareTo(firstDateExpr) >= 0 &&
actualDate.compareTo(secondDateExpr) <= 0;
+ }
+
+ return false;
} else if (op.equals("contains")) {
return actualValue.toString().contains(expectedValue);
} else if (op.equals("notContains")) {
diff --git
a/plugins/baseplugin/src/test/java/org/apache/unomi/plugins/baseplugin/conditions/PropertyConditionEvaluatorTest.java
b/plugins/baseplugin/src/test/java/org/apache/unomi/plugins/baseplugin/conditions/PropertyConditionEvaluatorTest.java
index 7ebf40e8a..a95520332 100644
---
a/plugins/baseplugin/src/test/java/org/apache/unomi/plugins/baseplugin/conditions/PropertyConditionEvaluatorTest.java
+++
b/plugins/baseplugin/src/test/java/org/apache/unomi/plugins/baseplugin/conditions/PropertyConditionEvaluatorTest.java
@@ -17,6 +17,8 @@
package org.apache.unomi.plugins.baseplugin.conditions;
import org.apache.unomi.api.*;
+import org.apache.unomi.api.conditions.Condition;
+import org.apache.unomi.api.conditions.ConditionType;
import org.apache.unomi.api.rules.Rule;
import
org.apache.unomi.plugins.baseplugin.conditions.accessors.HardcodedPropertyAccessor;
import org.apache.unomi.scripting.ExpressionFilter;
@@ -24,11 +26,9 @@ import org.apache.unomi.scripting.ExpressionFilterFactory;
import org.junit.Before;
import org.junit.Test;
-import java.io.File;
import java.util.*;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.regex.Pattern;
@@ -193,6 +193,168 @@ public class PropertyConditionEvaluatorTest {
return mockSession;
}
+ @Test
+ public void testNullSafeIntegerComparison_NonNumericActualValue() throws
Exception {
+ // Test that non-numeric actualValue with expectedValueInteger does
not throw NPE
+ Profile testProfile = new Profile();
+ testProfile.setItemId("testProfile");
+ testProfile.getProperties().put("nonNumericProperty", "notANumber");
+
+ Condition condition = new Condition();
+ ConditionType conditionType = new ConditionType();
+ conditionType.setItemId("propertyCondition");
+ condition.setConditionType(conditionType);
+ condition.setParameter("propertyName",
"properties.nonNumericProperty");
+ condition.setParameter("comparisonOperator", "greaterThan");
+ condition.setParameter("propertyValueInteger", 10);
+
+ // Should not throw NPE, should return false since non-numeric cannot
be compared
+ boolean result = propertyConditionEvaluator.eval(condition,
testProfile, new HashMap<>(), null);
+ assertFalse("Non-numeric value should not match greaterThan comparison
with integer", result);
+ }
+
+ @Test
+ public void testNullSafeIntegerComparison_NonNumericExpectedValue() throws
Exception {
+ // Test that non-numeric expectedValueInteger does not throw NPE
+ Profile testProfile = new Profile();
+ testProfile.setItemId("testProfile");
+ testProfile.getProperties().put("numericProperty", 42);
+
+ Condition condition = new Condition();
+ ConditionType conditionType = new ConditionType();
+ conditionType.setItemId("propertyCondition");
+ condition.setConditionType(conditionType);
+ condition.setParameter("propertyName", "properties.numericProperty");
+ condition.setParameter("comparisonOperator", "greaterThan");
+ condition.setParameter("propertyValueInteger", "notANumber");
+
+ // Should not throw NPE, should return false since non-numeric
expected cannot be compared
+ boolean result = propertyConditionEvaluator.eval(condition,
testProfile, new HashMap<>(), null);
+ assertFalse("Non-numeric expected value should not match greaterThan
comparison", result);
+ }
+
+ @Test
+ public void testNullSafeDoubleComparison_NonNumericActualValue() throws
Exception {
+ // Test that non-numeric actualValue with expectedValueDouble does not
throw NPE
+ Profile testProfile = new Profile();
+ testProfile.setItemId("testProfile");
+ testProfile.getProperties().put("nonNumericProperty", "notANumber");
+
+ Condition condition = new Condition();
+ ConditionType conditionType = new ConditionType();
+ conditionType.setItemId("propertyCondition");
+ condition.setConditionType(conditionType);
+ condition.setParameter("propertyName",
"properties.nonNumericProperty");
+ condition.setParameter("comparisonOperator", "lessThan");
+ condition.setParameter("propertyValueDouble", 10.5);
+
+ // Should not throw NPE, should return false since non-numeric cannot
be compared
+ boolean result = propertyConditionEvaluator.eval(condition,
testProfile, new HashMap<>(), null);
+ assertFalse("Non-numeric value should not match lessThan comparison
with double", result);
+ }
+
+ @Test
+ public void testNullSafeDoubleComparison_NonNumericExpectedValue() throws
Exception {
+ // Test that non-numeric expectedValueDouble does not throw NPE
+ Profile testProfile = new Profile();
+ testProfile.setItemId("testProfile");
+ testProfile.getProperties().put("numericProperty", 42.5);
+
+ Condition condition = new Condition();
+ ConditionType conditionType = new ConditionType();
+ conditionType.setItemId("propertyCondition");
+ condition.setConditionType(conditionType);
+ condition.setParameter("propertyName", "properties.numericProperty");
+ condition.setParameter("comparisonOperator", "lessThan");
+ condition.setParameter("propertyValueDouble", "notANumber");
+
+ // Should not throw NPE, should return false since non-numeric
expected cannot be compared
+ boolean result = propertyConditionEvaluator.eval(condition,
testProfile, new HashMap<>(), null);
+ assertFalse("Non-numeric expected value should not match lessThan
comparison", result);
+ }
+
+ @Test
+ public void testNullSafeDateComparison_NonDateActualValue() throws
Exception {
+ // Test that non-date actualValue with expectedValueDate does not
throw NPE
+ Profile testProfile = new Profile();
+ testProfile.setItemId("testProfile");
+ testProfile.getProperties().put("nonDateProperty", "notADate");
+
+ Condition condition = new Condition();
+ ConditionType conditionType = new ConditionType();
+ conditionType.setItemId("propertyCondition");
+ condition.setConditionType(conditionType);
+ condition.setParameter("propertyName", "properties.nonDateProperty");
+ condition.setParameter("comparisonOperator", "greaterThan");
+ condition.setParameter("propertyValueDate", new Date());
+
+ // Should not throw NPE, should return false since non-date cannot be
compared
+ boolean result = propertyConditionEvaluator.eval(condition,
testProfile, new HashMap<>(), null);
+ assertFalse("Non-date value should not match greaterThan comparison
with date", result);
+ }
+
+ @Test
+ public void testNullSafeDateComparison_NonDateExpectedValue() throws
Exception {
+ // Test that non-date expectedValueDate does not throw NPE
+ Profile testProfile = new Profile();
+ testProfile.setItemId("testProfile");
+ Date testDate = new Date();
+ testProfile.getProperties().put("dateProperty", testDate);
+
+ Condition condition = new Condition();
+ ConditionType conditionType = new ConditionType();
+ conditionType.setItemId("propertyCondition");
+ condition.setConditionType(conditionType);
+ condition.setParameter("propertyName", "properties.dateProperty");
+ condition.setParameter("comparisonOperator", "lessThan");
+ condition.setParameter("propertyValueDate", "notADate");
+
+ // Should not throw NPE, should return false since non-date expected
cannot be compared
+ boolean result = propertyConditionEvaluator.eval(condition,
testProfile, new HashMap<>(), null);
+ assertFalse("Non-date expected value should not match lessThan
comparison", result);
+ }
+
+ @Test
+ public void testNullSafeDateExprComparison_NonDateActualValue() throws
Exception {
+ // Test that non-date actualValue with expectedValueDateExpr does not
throw NPE
+ Profile testProfile = new Profile();
+ testProfile.setItemId("testProfile");
+ testProfile.getProperties().put("nonDateProperty", "notADate");
+
+ Condition condition = new Condition();
+ ConditionType conditionType = new ConditionType();
+ conditionType.setItemId("propertyCondition");
+ condition.setConditionType(conditionType);
+ condition.setParameter("propertyName", "properties.nonDateProperty");
+ condition.setParameter("comparisonOperator", "greaterThanOrEqualTo");
+ condition.setParameter("propertyValueDateExpr", new Date());
+
+ // Should not throw NPE, should return false since non-date cannot be
compared
+ boolean result = propertyConditionEvaluator.eval(condition,
testProfile, new HashMap<>(), null);
+ assertFalse("Non-date value should not match greaterThanOrEqualTo
comparison with dateExpr", result);
+ }
+
+ @Test
+ public void testNullSafeDateExprComparison_NonDateExpectedValue() throws
Exception {
+ // Test that non-date expectedValueDateExpr does not throw NPE
+ Profile testProfile = new Profile();
+ testProfile.setItemId("testProfile");
+ Date testDate = new Date();
+ testProfile.getProperties().put("dateProperty", testDate);
+
+ Condition condition = new Condition();
+ ConditionType conditionType = new ConditionType();
+ conditionType.setItemId("propertyCondition");
+ condition.setConditionType(conditionType);
+ condition.setParameter("propertyName", "properties.dateProperty");
+ condition.setParameter("comparisonOperator", "lessThanOrEqualTo");
+ condition.setParameter("propertyValueDateExpr", "notADate");
+
+ // Should not throw NPE, should return false since non-date expected
cannot be compared
+ boolean result = propertyConditionEvaluator.eval(condition,
testProfile, new HashMap<>(), null);
+ assertFalse("Non-date expected value should not match
lessThanOrEqualTo comparison", result);
+ }
+
class HardcodedWorker implements Callable<Object> {
@Override