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 f1653272d UNOMI-883 Validate and standardize condition handling:
f1653272d is described below
commit f1653272df6aef9d276f4bb02a43a2128a504110
Author: Serge Huber <[email protected]>
AuthorDate: Thu Jan 1 16:21:27 2026 +0100
UNOMI-883 Validate and standardize condition handling:
- **Validation**: Added proper condition validation in multiple services
using `ConditionValidationService`.
- **Enhancements**: Introduced type extensions for property conditions,
including double and multivalued doubles.
- **Refactoring**: Replaced redundant implementations with helper methods
to ensure consistent rule creation.
- **Blueprint Update**: Optimized service bindings with listener references
for reduced context restarts.
- **Miscellaneous**: Adjusted error handling, refined validation logging,
and improved recursive validation in test cases.
---
.../api/services/ConditionValidationService.java | 6 +-
.../apache/unomi/services/UserListServiceImpl.java | 4 +-
.../org/apache/unomi/itests/RuleServiceIT.java | 98 ++++++++----
.../resources/OSGI-INF/blueprint/blueprint.xml | 31 ++--
.../resources/OSGI-INF/blueprint/blueprint.xml | 10 ++
.../resources/OSGI-INF/blueprint/blueprint.xml | 31 ++--
.../actions/SetEventOccurenceCountAction.java | 15 +-
.../cxs/conditions/eventPropertyCondition.json | 10 ++
.../cxs/conditions/pastEventCondition.json | 2 +-
.../cxs/conditions/profilePropertyCondition.json | 38 ++---
.../cxs/conditions/sessionPropertyCondition.json | 10 ++
.../unomi/rest/endpoints/ContextJsonEndpoint.java | 8 +-
.../unomi/services/impl/AbstractServiceImpl.java | 4 +-
.../validation/ConditionValidationServiceImpl.java | 86 +++++++++--
.../ComparisonOperatorValueTypeValidator.java | 21 ++-
.../validators/ConditionValueTypeValidator.java | 3 +
.../rules/TestSetEventOccurrenceCountAction.java | 4 +-
.../ConditionValidationServiceImplTest.java | 164 +++++++++++++++++++--
18 files changed, 431 insertions(+), 114 deletions(-)
diff --git
a/api/src/main/java/org/apache/unomi/api/services/ConditionValidationService.java
b/api/src/main/java/org/apache/unomi/api/services/ConditionValidationService.java
index b9102565b..a0c4fbb16 100644
---
a/api/src/main/java/org/apache/unomi/api/services/ConditionValidationService.java
+++
b/api/src/main/java/org/apache/unomi/api/services/ConditionValidationService.java
@@ -28,9 +28,11 @@ import java.util.Map;
public interface ConditionValidationService {
/**
- * Validates a condition against its type definition
+ * Validates a condition against its type definition.
+ * Skips validation for parameters that contain references (`parameter::`)
or script expressions (`script::`).
+ * Only validates parameters that are NOT parameter references or script
expressions.
* @param condition the condition to validate
- * @return a list of validation errors, empty if the condition is valid
+ * @return a list of validation errors, empty if the condition is valid
(for non-reference/script values)
*/
List<ValidationError> validate(Condition condition);
diff --git
a/extensions/lists-extension/services/src/main/java/org/apache/unomi/services/UserListServiceImpl.java
b/extensions/lists-extension/services/src/main/java/org/apache/unomi/services/UserListServiceImpl.java
index 1c656d1de..122488c03 100644
---
a/extensions/lists-extension/services/src/main/java/org/apache/unomi/services/UserListServiceImpl.java
+++
b/extensions/lists-extension/services/src/main/java/org/apache/unomi/services/UserListServiceImpl.java
@@ -59,7 +59,9 @@ public class UserListServiceImpl implements UserListService {
if (query.isForceRefresh()) {
persistenceService.refreshIndex(UserList.class);
}
- definitionsService.resolveConditionType(query.getCondition());
+ if (query.getCondition() != null) {
+
definitionsService.getConditionValidationService().validate(query.getCondition());
+ }
PartialList<UserList> userLists =
persistenceService.query(query.getCondition(), query.getSortby(),
UserList.class, query.getOffset(), query.getLimit());
List<Metadata> metadata = new LinkedList<>();
for (UserList definition : userLists.getList()) {
diff --git a/itests/src/test/java/org/apache/unomi/itests/RuleServiceIT.java
b/itests/src/test/java/org/apache/unomi/itests/RuleServiceIT.java
index 1155d27c4..926f9b932 100644
--- a/itests/src/test/java/org/apache/unomi/itests/RuleServiceIT.java
+++ b/itests/src/test/java/org/apache/unomi/itests/RuleServiceIT.java
@@ -58,6 +58,51 @@ public class RuleServiceIT extends BaseIT {
TestUtils.removeAllProfiles(definitionsService, persistenceService);
}
+ /**
+ * Creates a default action for test rules. Uses setPropertyAction as a
simple, always-available action.
+ *
+ * @return a default action for test rules
+ */
+ private Action createDefaultAction() {
+ Action action = new
Action(definitionsService.getActionType("setPropertyAction"));
+ action.setParameter("propertyName", "testProperty");
+ action.setParameter("propertyValue", "testValue");
+ return action;
+ }
+
+ /**
+ * Creates a rule with a default action. This ensures all rules have
actions, which is required in newer versions.
+ *
+ * @param metadata the rule metadata
+ * @param condition the rule condition (may be null)
+ * @return a rule with default action
+ */
+ private Rule createRuleWithDefaultAction(Metadata metadata, Condition
condition) {
+ return createRuleWithActions(metadata, condition,
Collections.singletonList(createDefaultAction()));
+ }
+
+ /**
+ * Creates a rule with specified actions. If actions is null or empty, a
default action is added.
+ *
+ * @param metadata the rule metadata
+ * @param condition the rule condition (may be null)
+ * @param actions the list of actions (if null or empty, a default action
is added)
+ * @return a rule with actions
+ */
+ private Rule createRuleWithActions(Metadata metadata, Condition condition,
List<Action> actions) {
+ Rule rule = new Rule(metadata);
+ rule.setCondition(condition);
+
+ // Ensure rule always has at least one action (required in newer
versions)
+ if (actions == null || actions.isEmpty()) {
+ rule.setActions(Collections.singletonList(createDefaultAction()));
+ } else {
+ rule.setActions(actions);
+ }
+
+ return rule;
+ }
+
@Test
public void testRuleWithNullActions() throws InterruptedException {
Metadata metadata = new Metadata(TEST_RULE_ID);
@@ -87,12 +132,6 @@ public class RuleServiceIT extends BaseIT {
// Create a simple condition instead of null
Condition defaultCondition = new
Condition(definitionsService.getConditionType("matchAllCondition"));
- // Create a default action
- Action defaultAction = new
Action(definitionsService.getActionType("setPropertyAction"));
- defaultAction.setParameter("propertyName", "testProperty");
- defaultAction.setParameter("propertyValue", "testValue");
- List<Action> actions = Collections.singletonList(defaultAction);
-
int successfullyCreatedRules = 0;
for (int i = 0; i < 60; i++) {
String ruleID = ruleIDBase + "_" + i;
@@ -100,9 +139,8 @@ public class RuleServiceIT extends BaseIT {
metadata.setName(ruleID);
metadata.setDescription(ruleID);
metadata.setScope(TEST_SCOPE);
- Rule rule = new Rule(metadata);
- rule.setCondition(defaultCondition); // Use default condition
instead of null
- rule.setActions(actions); // Empty list instead of null
+ // Use helper method to ensure rule always has actions
+ Rule rule = createRuleWithDefaultAction(metadata,
defaultCondition);
try {
createAndWaitForRule(rule);
@@ -137,25 +175,29 @@ public class RuleServiceIT extends BaseIT {
@Test
public void testRuleEventTypeOptimization() throws InterruptedException {
ConditionBuilder builder = definitionsService.getConditionBuilder();
- Rule simpleEventTypeRule = new Rule(new Metadata(TEST_SCOPE,
"simple-event-type-rule", "Simple event type rule", "A rule with a simple
condition to match an event type"));
-
simpleEventTypeRule.setCondition(builder.condition("eventTypeCondition").parameter("eventTypeId",
"view").build());
+ Rule simpleEventTypeRule = createRuleWithDefaultAction(
+ new Metadata(TEST_SCOPE, "simple-event-type-rule", "Simple event
type rule", "A rule with a simple condition to match an event type"),
+ builder.condition("eventTypeCondition").parameter("eventTypeId",
"view").build()
+ );
createAndWaitForRule(simpleEventTypeRule);
- Rule complexEventTypeRule = new Rule(new Metadata(TEST_SCOPE,
"complex-event-type-rule", "Complex event type rule", "A rule with a complex
condition to match multiple event types with negations"));
- complexEventTypeRule.setCondition(
- builder.not(
- builder.or(
-
builder.condition("eventTypeCondition").parameter( "eventTypeId", "view"),
-
builder.condition("eventTypeCondition").parameter("eventTypeId", "form")
- )
- ).build()
+ Rule complexEventTypeRule = createRuleWithDefaultAction(
+ new Metadata(TEST_SCOPE, "complex-event-type-rule", "Complex event
type rule", "A rule with a complex condition to match multiple event types with
negations"),
+ builder.not(
+ builder.or(
+ builder.condition("eventTypeCondition").parameter(
"eventTypeId", "view"),
+
builder.condition("eventTypeCondition").parameter("eventTypeId", "form")
+ )
+ ).build()
);
createAndWaitForRule(complexEventTypeRule);
- Rule noEventTypeRule = new Rule(new Metadata(TEST_SCOPE,
"no-event-type-rule", "No event type rule", "A rule with a simple condition but
no event type matching"));
-
noEventTypeRule.setCondition(builder.condition("eventPropertyCondition")
+ Rule noEventTypeRule = createRuleWithDefaultAction(
+ new Metadata(TEST_SCOPE, "no-event-type-rule", "No event type
rule", "A rule with a simple condition but no event type matching"),
+ builder.condition("eventPropertyCondition")
.parameter("propertyName",
"target.properties.pageInfo.language")
.parameter("comparisonOperator", "equals")
.parameter("propertyValue", "en")
- .build());
+ .build()
+ );
createAndWaitForRule(noEventTypeRule);
Profile profile = new Profile(UUID.randomUUID().toString());
@@ -262,20 +304,24 @@ public class RuleServiceIT extends BaseIT {
// Test tracked parameter
// Add rule that has a trackParameter condition that matches
ConditionBuilder builder = new
ConditionBuilder(definitionsService);
- Rule trackParameterRule = new Rule(new Metadata(TEST_SCOPE,
"tracked-parameter-rule", "Tracked parameter rule", "A rule with tracked
parameter"));
Condition trackedCondition =
builder.condition("clickEventCondition").build();
trackedCondition.setParameter("path", "/test-page.html");
trackedCondition.setParameter("referrer",
"https://unomi.apache.org");
trackedCondition.getConditionType().getMetadata().getSystemTags().add("trackedCondition");
- trackParameterRule.setCondition(trackedCondition);
+ Rule trackParameterRule = createRuleWithDefaultAction(
+ new Metadata(TEST_SCOPE, "tracked-parameter-rule", "Tracked
parameter rule", "A rule with tracked parameter"),
+ trackedCondition
+ );
createAndWaitForRule(trackParameterRule);
// Add rule that has a trackParameter condition that does not match
- Rule unTrackParameterRule = new Rule(new Metadata(TEST_SCOPE,
"not-tracked-parameter-rule", "Not Tracked parameter rule", "A rule that has a
parameter not tracked"));
Condition unTrackedCondition =
builder.condition("clickEventCondition").build();
unTrackedCondition.setParameter("path", "/test-page.html");
unTrackedCondition.setParameter("referrer", "https://localhost");
unTrackedCondition.getConditionType().getMetadata().getSystemTags().add("trackedCondition");
- unTrackParameterRule.setCondition(unTrackedCondition);
+ Rule unTrackParameterRule = createRuleWithDefaultAction(
+ new Metadata(TEST_SCOPE, "not-tracked-parameter-rule", "Not
Tracked parameter rule", "A rule that has a parameter not tracked"),
+ unTrackedCondition
+ );
createAndWaitForRule(unTrackParameterRule);
// Check that the given event return the tracked condition
Profile profile = new Profile(UUID.randomUUID().toString());
diff --git
a/persistence-elasticsearch/conditions/src/main/resources/OSGI-INF/blueprint/blueprint.xml
b/persistence-elasticsearch/conditions/src/main/resources/OSGI-INF/blueprint/blueprint.xml
index 7597590de..16f7d7a71 100644
---
a/persistence-elasticsearch/conditions/src/main/resources/OSGI-INF/blueprint/blueprint.xml
+++
b/persistence-elasticsearch/conditions/src/main/resources/OSGI-INF/blueprint/blueprint.xml
@@ -30,7 +30,6 @@
</cm:default-properties>
</cm:property-placeholder>
- <reference id="definitionsService"
interface="org.apache.unomi.api.services.DefinitionsService"/>
<reference id="persistenceService"
interface="org.apache.unomi.persistence.spi.PersistenceService"/>
<reference id="segmentService"
interface="org.apache.unomi.api.services.SegmentService"/>
<reference id="scriptExecutor"
interface="org.apache.unomi.scripting.ScriptExecutor"/>
@@ -55,7 +54,16 @@
<bean
class="org.apache.unomi.persistence.elasticsearch.querybuilders.advanced.GeoLocationByPointSessionConditionESQueryBuilder"/>
</service>
- <service>
+ <bean id="pastEventConditionESQueryBuilder"
class="org.apache.unomi.persistence.elasticsearch.querybuilders.advanced.PastEventConditionESQueryBuilder">
+ <property name="persistenceService" ref="persistenceService"/>
+ <property name="segmentService" ref="segmentService"/>
+ <property name="scriptExecutor" ref="scriptExecutor"/>
+ <property name="maximumIdsQueryCount"
value="${es.maximumIdsQueryCount}"/>
+ <property name="pastEventsDisablePartitions"
value="${es.pastEventsDisablePartitions}"/>
+ <property name="aggregateQueryBucketSize"
value="${es.aggregateQueryBucketSize}"/>
+ </bean>
+
+ <service ref="pastEventConditionESQueryBuilder">
<interfaces>
<value>org.apache.unomi.persistence.elasticsearch.ConditionESQueryBuilder</value>
<value>org.apache.unomi.persistence.spi.conditions.PastEventConditionPersistenceQueryBuilder</value>
@@ -63,15 +71,16 @@
<service-properties>
<entry key="queryBuilderId"
value="pastEventConditionQueryBuilder"/>
</service-properties>
- <bean
class="org.apache.unomi.persistence.elasticsearch.querybuilders.advanced.PastEventConditionESQueryBuilder">
- <property name="definitionsService" ref="definitionsService"/>
- <property name="persistenceService" ref="persistenceService"/>
- <property name="segmentService" ref="segmentService"/>
- <property name="scriptExecutor" ref="scriptExecutor"/>
- <property name="maximumIdsQueryCount"
value="${es.maximumIdsQueryCount}"/>
- <property name="pastEventsDisablePartitions"
value="${es.pastEventsDisablePartitions}"/>
- <property name="aggregateQueryBucketSize"
value="${es.aggregateQueryBucketSize}"/>
- </bean>
</service>
+ <!-- DefinitionsService Reference with listener to avoid blueprint context
restart -->
+ <reference id="definitionsService"
+ interface="org.apache.unomi.api.services.DefinitionsService"
+ availability="optional">
+ <reference-listener bind-method="bindDefinitionsService"
+ unbind-method="unbindDefinitionsService">
+ <ref component-id="pastEventConditionESQueryBuilder"/>
+ </reference-listener>
+ </reference>
+
</blueprint>
diff --git
a/persistence-elasticsearch/core/src/main/resources/OSGI-INF/blueprint/blueprint.xml
b/persistence-elasticsearch/core/src/main/resources/OSGI-INF/blueprint/blueprint.xml
index 2b50f7c33..c645b0774 100644
---
a/persistence-elasticsearch/core/src/main/resources/OSGI-INF/blueprint/blueprint.xml
+++
b/persistence-elasticsearch/core/src/main/resources/OSGI-INF/blueprint/blueprint.xml
@@ -92,6 +92,16 @@
<property name="scriptExecutor" ref="scriptExecutor" />
</bean>
+ <!-- DefinitionsService Reference with listener to avoid blueprint context
restart -->
+ <reference id="definitionsService"
+ interface="org.apache.unomi.api.services.DefinitionsService"
+ availability="optional">
+ <reference-listener bind-method="bindDefinitionsService"
+ unbind-method="unbindDefinitionsService">
+ <ref component-id="conditionESQueryBuilderDispatcher"/>
+ </reference-listener>
+ </reference>
+
<reference id="conditionEvaluatorDispatcher"
interface="org.apache.unomi.persistence.spi.conditions.evaluator.ConditionEvaluatorDispatcher"
/>
<bean id="elasticSearchPersistenceServiceImpl"
diff --git
a/persistence-opensearch/conditions/src/main/resources/OSGI-INF/blueprint/blueprint.xml
b/persistence-opensearch/conditions/src/main/resources/OSGI-INF/blueprint/blueprint.xml
index db028d27f..606a43e4b 100644
---
a/persistence-opensearch/conditions/src/main/resources/OSGI-INF/blueprint/blueprint.xml
+++
b/persistence-opensearch/conditions/src/main/resources/OSGI-INF/blueprint/blueprint.xml
@@ -39,7 +39,6 @@
</cm:default-properties>
</cm:property-placeholder>
- <reference id="definitionsService"
interface="org.apache.unomi.api.services.DefinitionsService"/>
<reference id="persistenceService"
interface="org.apache.unomi.persistence.spi.PersistenceService"/>
<reference id="segmentService"
interface="org.apache.unomi.api.services.SegmentService"/>
<reference id="scriptExecutor"
interface="org.apache.unomi.scripting.ScriptExecutor"/>
@@ -64,7 +63,16 @@
<bean
class="org.apache.unomi.persistence.opensearch.querybuilders.advanced.GeoLocationByPointSessionConditionOSQueryBuilder"/>
</service>
- <service>
+ <bean id="pastEventConditionOSQueryBuilder"
class="org.apache.unomi.persistence.opensearch.querybuilders.advanced.PastEventConditionOSQueryBuilder">
+ <property name="persistenceService" ref="persistenceService"/>
+ <property name="segmentService" ref="segmentService"/>
+ <property name="scriptExecutor" ref="scriptExecutor"/>
+ <property name="maximumIdsQueryCount"
value="${os.maximumIdsQueryCount}"/>
+ <property name="pastEventsDisablePartitions"
value="${os.pastEventsDisablePartitions}"/>
+ <property name="aggregateQueryBucketSize"
value="${os.aggregateQueryBucketSize}"/>
+ </bean>
+
+ <service ref="pastEventConditionOSQueryBuilder">
<interfaces>
<value>org.apache.unomi.persistence.opensearch.ConditionOSQueryBuilder</value>
<value>org.apache.unomi.persistence.spi.conditions.PastEventConditionPersistenceQueryBuilder</value>
@@ -72,15 +80,16 @@
<service-properties>
<entry key="queryBuilderId"
value="pastEventConditionQueryBuilder"/>
</service-properties>
- <bean
class="org.apache.unomi.persistence.opensearch.querybuilders.advanced.PastEventConditionOSQueryBuilder">
- <property name="definitionsService" ref="definitionsService"/>
- <property name="persistenceService" ref="persistenceService"/>
- <property name="segmentService" ref="segmentService"/>
- <property name="scriptExecutor" ref="scriptExecutor"/>
- <property name="maximumIdsQueryCount"
value="${os.maximumIdsQueryCount}"/>
- <property name="pastEventsDisablePartitions"
value="${os.pastEventsDisablePartitions}"/>
- <property name="aggregateQueryBucketSize"
value="${os.aggregateQueryBucketSize}"/>
- </bean>
</service>
+ <!-- DefinitionsService Reference with listener to avoid blueprint context
restart -->
+ <reference id="definitionsService"
+ interface="org.apache.unomi.api.services.DefinitionsService"
+ availability="optional">
+ <reference-listener bind-method="bindDefinitionsService"
+ unbind-method="unbindDefinitionsService">
+ <ref component-id="pastEventConditionOSQueryBuilder"/>
+ </reference-listener>
+ </reference>
+
</blueprint>
diff --git
a/plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/actions/SetEventOccurenceCountAction.java
b/plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/actions/SetEventOccurenceCountAction.java
index ddff80d3b..6716ce74d 100644
---
a/plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/actions/SetEventOccurenceCountAction.java
+++
b/plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/actions/SetEventOccurenceCountAction.java
@@ -24,17 +24,14 @@ import org.apache.unomi.api.conditions.Condition;
import org.apache.unomi.api.services.DefinitionsService;
import org.apache.unomi.api.services.EventService;
import org.apache.unomi.persistence.spi.PersistenceService;
-import org.apache.unomi.persistence.spi.PropertyHelper;
-import org.apache.unomi.tracing.api.TracerService;
import org.apache.unomi.tracing.api.RequestTracer;
+import org.apache.unomi.tracing.api.TracerService;
+import javax.xml.bind.DatatypeConverter;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.*;
-import java.util.stream.Collectors;
-
-import javax.xml.bind.DatatypeConverter;
public class SetEventOccurenceCountAction implements ActionExecutor {
private DefinitionsService definitionsService;
@@ -58,7 +55,7 @@ public class SetEventOccurenceCountAction implements
ActionExecutor {
RequestTracer tracer = null;
if (tracerService != null && tracerService.isTracingEnabled()) {
tracer = tracerService.getCurrentTracer();
- tracer.startOperation("set-event-count",
+ tracer.startOperation("set-event-count",
"Setting event occurrence count", action);
}
@@ -78,7 +75,9 @@ public class SetEventOccurenceCountAction implements
ActionExecutor {
ArrayList<Condition> conditions = new ArrayList<Condition>();
Condition eventCondition = (Condition)
pastEventCondition.getParameter("eventCondition");
- definitionsService.resolveConditionType(eventCondition);
+ if (eventCondition != null) {
+
definitionsService.getConditionValidationService().validate(eventCondition);
+ }
conditions.add(eventCondition);
Condition c = new
Condition(definitionsService.getConditionType("eventPropertyCondition"));
@@ -149,7 +148,7 @@ public class SetEventOccurenceCountAction implements
ActionExecutor {
"count", count,
"isUpdated", updated
));
- tracer.endOperation(updated,
+ tracer.endOperation(updated,
updated ? "Event count updated successfully" : "No changes
needed");
}
return updated ? EventService.PROFILE_UPDATED :
EventService.NO_CHANGE;
diff --git
a/plugins/baseplugin/src/main/resources/META-INF/cxs/conditions/eventPropertyCondition.json
b/plugins/baseplugin/src/main/resources/META-INF/cxs/conditions/eventPropertyCondition.json
index a2e0a7927..6ac81225f 100644
---
a/plugins/baseplugin/src/main/resources/META-INF/cxs/conditions/eventPropertyCondition.json
+++
b/plugins/baseplugin/src/main/resources/META-INF/cxs/conditions/eventPropertyCondition.json
@@ -35,6 +35,11 @@
"type": "integer",
"multivalued": false
},
+ {
+ "id": "propertyValueDouble",
+ "type": "double",
+ "multivalued": false
+ },
{
"id": "propertyValueDate",
"type": "date",
@@ -55,6 +60,11 @@
"type": "integer",
"multivalued": true
},
+ {
+ "id": "propertyValuesDouble",
+ "type": "double",
+ "multivalued": true
+ },
{
"id": "propertyValuesDate",
"type": "date",
diff --git
a/plugins/baseplugin/src/main/resources/META-INF/cxs/conditions/pastEventCondition.json
b/plugins/baseplugin/src/main/resources/META-INF/cxs/conditions/pastEventCondition.json
index 3837438ab..b2550eef0 100644
---
a/plugins/baseplugin/src/main/resources/META-INF/cxs/conditions/pastEventCondition.json
+++
b/plugins/baseplugin/src/main/resources/META-INF/cxs/conditions/pastEventCondition.json
@@ -30,7 +30,7 @@
"id": "operator",
"type": "string",
"multivalued": false,
- "defaultValue": "true",
+ "defaultValue": "eventsOccurred",
"validation": {
"recommended": true,
"allowedValues": ["eventsOccurred", "eventsNotOccurred"]
diff --git
a/plugins/baseplugin/src/main/resources/META-INF/cxs/conditions/profilePropertyCondition.json
b/plugins/baseplugin/src/main/resources/META-INF/cxs/conditions/profilePropertyCondition.json
index 37817afb0..249d8c7d5 100644
---
a/plugins/baseplugin/src/main/resources/META-INF/cxs/conditions/profilePropertyCondition.json
+++
b/plugins/baseplugin/src/main/resources/META-INF/cxs/conditions/profilePropertyCondition.json
@@ -25,26 +25,10 @@
},
{
"id": "comparisonOperator",
- "type": "string",
+ "type": "comparisonOperator",
"multivalued": false,
"validation": {
- "required": true,
- "allowedValues": [
- "equals",
- "notEquals",
- "lessThan",
- "greaterThan",
- "lessThanOrEqualTo",
- "greaterThanOrEqualTo",
- "between",
- "exists",
- "missing",
- "contains",
- "notContains",
- "startsWith",
- "endsWith",
- "matchesRegex"
- ]
+ "required": true
}
},
{
@@ -65,6 +49,15 @@
"exclusiveGroup": "propertyValue"
}
},
+ {
+ "id": "propertyValueDouble",
+ "type": "double",
+ "multivalued": false,
+ "validation": {
+ "exclusive": true,
+ "exclusiveGroup": "propertyValue"
+ }
+ },
{
"id": "propertyValueDate",
"type": "date",
@@ -101,6 +94,15 @@
"exclusiveGroup": "propertyValues"
}
},
+ {
+ "id": "propertyValuesDouble",
+ "type": "double",
+ "multivalued": true,
+ "validation": {
+ "exclusive": true,
+ "exclusiveGroup": "propertyValues"
+ }
+ },
{
"id": "propertyValuesDate",
"type": "date",
diff --git
a/plugins/baseplugin/src/main/resources/META-INF/cxs/conditions/sessionPropertyCondition.json
b/plugins/baseplugin/src/main/resources/META-INF/cxs/conditions/sessionPropertyCondition.json
index 8dad7607f..32f7271cf 100644
---
a/plugins/baseplugin/src/main/resources/META-INF/cxs/conditions/sessionPropertyCondition.json
+++
b/plugins/baseplugin/src/main/resources/META-INF/cxs/conditions/sessionPropertyCondition.json
@@ -36,6 +36,11 @@
"type": "integer",
"multivalued": false
},
+ {
+ "id": "propertyValueDouble",
+ "type": "double",
+ "multivalued": false
+ },
{
"id": "propertyValueDate",
"type": "date",
@@ -56,6 +61,11 @@
"type": "integer",
"multivalued": true
},
+ {
+ "id": "propertyValuesDouble",
+ "type": "double",
+ "multivalued": true
+ },
{
"id": "propertyValuesDate",
"type": "date",
diff --git
a/rest/src/main/java/org/apache/unomi/rest/endpoints/ContextJsonEndpoint.java
b/rest/src/main/java/org/apache/unomi/rest/endpoints/ContextJsonEndpoint.java
index d3f476336..f5b70de9c 100644
---
a/rest/src/main/java/org/apache/unomi/rest/endpoints/ContextJsonEndpoint.java
+++
b/rest/src/main/java/org/apache/unomi/rest/endpoints/ContextJsonEndpoint.java
@@ -24,8 +24,12 @@ import
org.apache.cxf.rs.security.cors.CrossOriginResourceSharing;
import org.apache.unomi.api.*;
import org.apache.unomi.api.conditions.Condition;
import org.apache.unomi.api.security.UnomiRoles;
-import org.apache.unomi.api.services.*;
+import org.apache.unomi.api.services.PersonalizationService;
+import org.apache.unomi.api.services.PrivacyService;
+import org.apache.unomi.api.services.ProfileService;
+import org.apache.unomi.api.services.RulesService;
import org.apache.unomi.persistence.spi.CustomObjectMapper;
+import org.apache.unomi.persistence.spi.conditions.ConditionContextHelper;
import org.apache.unomi.rest.exception.InvalidRequestException;
import org.apache.unomi.rest.service.RestServiceUtils;
import org.apache.unomi.schema.api.SchemaService;
@@ -382,7 +386,7 @@ public class ContextJsonEndpoint {
private Object sanitizeValue(Object value) {
if (value instanceof String) {
String stringValue = (String) value;
- if (stringValue.startsWith("script::") ||
stringValue.startsWith("parameter::")) {
+ if (ConditionContextHelper.isParameterReference(value)) {
LOGGER.warn("Scripting detected in context request, filtering
out. See debug level for more information");
LOGGER.debug("Scripting detected in context request with value
{}, filtering out...", value);
return null;
diff --git
a/services/src/main/java/org/apache/unomi/services/impl/AbstractServiceImpl.java
b/services/src/main/java/org/apache/unomi/services/impl/AbstractServiceImpl.java
index afd4f801d..1f9247976 100644
---
a/services/src/main/java/org/apache/unomi/services/impl/AbstractServiceImpl.java
+++
b/services/src/main/java/org/apache/unomi/services/impl/AbstractServiceImpl.java
@@ -57,7 +57,9 @@ public abstract class AbstractServiceImpl {
if (query.isForceRefresh()) {
persistenceService.refreshIndex(clazz);
}
- definitionsService.resolveConditionType(query.getCondition());
+ if (query.getCondition() != null) {
+
definitionsService.getConditionValidationService().validate(query.getCondition());
+ }
PartialList<T> items = persistenceService.query(query.getCondition(),
query.getSortby(), clazz, query.getOffset(), query.getLimit());
List<Metadata> details = new LinkedList<>();
for (T definition : items.getList()) {
diff --git
a/services/src/main/java/org/apache/unomi/services/impl/validation/ConditionValidationServiceImpl.java
b/services/src/main/java/org/apache/unomi/services/impl/validation/ConditionValidationServiceImpl.java
index 50bbefd2b..526c60275 100644
---
a/services/src/main/java/org/apache/unomi/services/impl/validation/ConditionValidationServiceImpl.java
+++
b/services/src/main/java/org/apache/unomi/services/impl/validation/ConditionValidationServiceImpl.java
@@ -21,7 +21,9 @@ import org.apache.unomi.api.conditions.Condition;
import org.apache.unomi.api.conditions.ConditionType;
import org.apache.unomi.api.conditions.ConditionValidation;
import org.apache.unomi.api.services.ConditionValidationService;
+import org.apache.unomi.api.services.TypeResolutionService;
import org.apache.unomi.api.services.ValueTypeValidator;
+import org.apache.unomi.persistence.spi.conditions.ConditionContextHelper;
import
org.apache.unomi.services.impl.validation.validators.ConditionValueTypeValidator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -35,6 +37,7 @@ public class ConditionValidationServiceImpl implements
ConditionValidationServic
private final Map<String, ValueTypeValidator> validators = new
ConcurrentHashMap<>();
private List<ValueTypeValidator> builtInValidators;
+ private TypeResolutionService typeResolutionService;
public void setBuiltInValidators(List<ValueTypeValidator>
builtInValidators) {
this.builtInValidators = builtInValidators;
@@ -61,6 +64,17 @@ public class ConditionValidationServiceImpl implements
ConditionValidationServic
}
}
+ /**
+ * Sets the TypeResolutionService for automatic type resolution during
validation.
+ * This allows the validation service to automatically resolve condition
types
+ * if they haven't been resolved yet, including nested conditions.
+ *
+ * @param typeResolutionService the type resolution service to use
+ */
+ public void setTypeResolutionService(TypeResolutionService
typeResolutionService) {
+ this.typeResolutionService = typeResolutionService;
+ }
+
private Map<String, Object> buildValidationContext(String paramName,
Object value, Parameter param,
String location, Map<String, Object> additionalContext) {
Map<String, Object> context = new HashMap<>();
@@ -101,12 +115,24 @@ public class ConditionValidationServiceImpl implements
ConditionValidationServic
return errors;
}
+ // Auto-resolve condition type if needed (including nested conditions)
+ // This ensures types are resolved before validation, preventing
validation failures
+ if (condition.getConditionType() == null && typeResolutionService !=
null) {
+ typeResolutionService.resolveConditionType(condition,
"validation");
+ }
+
ConditionType type = condition.getConditionType();
if (type == null) {
- Map<String, Object> context = buildValidationContext(null, null,
null,
- "condition type", Collections.singletonMap("type",
condition.getConditionTypeId()));
- errors.add(new ValidationError(null, "Condition type cannot be
null",
- ValidationErrorType.INVALID_CONDITION_TYPE,
condition.getConditionTypeId(), null, context, null));
+ // Condition without type is invalid (could not be resolved)
+ String location = "condition[" + condition.getConditionTypeId() +
"]";
+ Map<String, Object> context = buildValidationContext(null, null,
null, location, null);
+ errors.add(new ValidationError(null,
+ "Condition type is missing or could not be resolved",
+ ValidationErrorType.INVALID_CONDITION_TYPE,
+ condition.getConditionTypeId(),
+ null,
+ context,
+ null));
return errors;
}
@@ -121,13 +147,18 @@ public class ConditionValidationServiceImpl implements
ConditionValidationServic
}
}
- // Check each parameter
+ // Check each parameter, skipping those with references/scripts
(partial validation)
for (Parameter param : type.getParameters()) {
String paramName = param.getId();
Object value = condition.getParameter(paramName);
String location = "condition[" + condition.getConditionTypeId() +
"]." + paramName;
- // Always validate basic type and multivalued constraints
+ // Skip validation entirely for parameters with references/scripts
+ if (ConditionContextHelper.hasContextualParameter(value)) {
+ continue;
+ }
+
+ // Validate basic type and multivalued constraints for
non-reference values
if (value != null) {
errors.addAll(validateParameterType(paramName, value, param,
condition, type, location));
}
@@ -138,11 +169,15 @@ public class ConditionValidationServiceImpl implements
ConditionValidationServic
}
}
- // Check exclusive parameter groups
+ // Check exclusive parameter groups (only for non-reference/script
values)
for (Map.Entry<String, List<Parameter>> entry :
exclusiveGroups.entrySet()) {
List<Parameter> group = entry.getValue();
long valuesCount = group.stream()
- .map(p -> condition.getParameter(p.getId()))
+ .map(p -> {
+ Object val = condition.getParameter(p.getId());
+ // Only count non-reference/script values
+ return val != null &&
!ConditionContextHelper.hasContextualParameter(val) ? val : null;
+ })
.filter(Objects::nonNull)
.count();
@@ -164,9 +199,23 @@ public class ConditionValidationServiceImpl implements
ConditionValidationServic
}
}
+ // Recursively validate nested conditions (with same partial logic)
+ for (Object value : condition.getParameterValues().values()) {
+ if (value instanceof Condition) {
+ errors.addAll(validate((Condition) value));
+ } else if (value instanceof Collection) {
+ for (Object item : (Collection<?>) value) {
+ if (item instanceof Condition) {
+ errors.addAll(validate((Condition) item));
+ }
+ }
+ }
+ }
+
return errors;
}
+
private List<ValidationError> validateAdditionalRules(String paramName,
Object value, Parameter param,
Condition condition, ConditionType type, String parentLocation) {
List<ValidationError> errors = new ArrayList<>();
@@ -174,7 +223,7 @@ public class ConditionValidationServiceImpl implements
ConditionValidationServic
Map<String, Object> context = buildValidationContext(paramName, value,
param, parentLocation, null);
- // Check required parameters
+ // Check required parameters (skip if value is a parameter
reference/script)
if (validation.isRequired() && value == null) {
errors.add(new ValidationError(paramName,
"Required parameter is missing",
@@ -186,7 +235,7 @@ public class ConditionValidationServiceImpl implements
ConditionValidationServic
return errors; // Skip other validations if required parameter is
missing
}
- // Check recommended parameters
+ // Check recommended parameters (skip if value is a parameter
reference/script)
if (validation.isRecommended() && value == null) {
errors.add(new ValidationError(paramName,
"Parameter is recommended for optimal functionality",
@@ -197,7 +246,7 @@ public class ConditionValidationServiceImpl implements
ConditionValidationServic
null));
}
- if (value != null) {
+ if (value != null &&
!ConditionContextHelper.hasContextualParameter(value)) {
// Check allowed values
if (validation.getAllowedValues() != null &&
!validation.getAllowedValues().isEmpty()) {
if (!validation.getAllowedValues().contains(value.toString()))
{
@@ -338,6 +387,21 @@ public class ConditionValidationServiceImpl implements
ConditionValidationServic
return errors;
}
+ // Skip type validation for parameter references and script expressions
+ // These will be resolved later (via
ConditionContextHelper.getContextualCondition)
+ // and validated at that point. Type validation here would fail
incorrectly
+ // since parameter references appear as Strings but will resolve to
the correct type.
+ if (ConditionContextHelper.isParameterReference(value)) {
+ // Parameter reference or script expression - skip type validation
+ // Other constraints (required, allowedValues, etc.) are still
validated
+ // Type will be validated when the reference is resolved
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debug("Skipping type validation for parameter
reference: {}={}",
+ paramName, value);
+ }
+ return errors;
+ }
+
// Special handling for object type with custom validation
if ("object".equals(paramType)) {
if (param.getValidation() != null &&
param.getValidation().getCustomType() != null) {
diff --git
a/services/src/main/java/org/apache/unomi/services/impl/validation/validators/ComparisonOperatorValueTypeValidator.java
b/services/src/main/java/org/apache/unomi/services/impl/validation/validators/ComparisonOperatorValueTypeValidator.java
index 85ac30c3f..4d16ebf71 100644
---
a/services/src/main/java/org/apache/unomi/services/impl/validation/validators/ComparisonOperatorValueTypeValidator.java
+++
b/services/src/main/java/org/apache/unomi/services/impl/validation/validators/ComparisonOperatorValueTypeValidator.java
@@ -24,11 +24,22 @@ import java.util.Set;
public class ComparisonOperatorValueTypeValidator implements
ValueTypeValidator {
private static final Set<String> VALID_OPERATORS = new
HashSet<>(Arrays.asList(
- "equals", "notEquals", "lessThan", "greaterThan",
- "lessThanOrEqualTo", "greaterThanOrEqualTo",
- "between", "contains", "startsWith", "endsWith",
- "matchesRegex", "in", "notIn", "all", "exists",
- "missing"
+ // Equality operators
+ "equals", "notEquals",
+ // Comparison operators
+ "lessThan", "greaterThan", "lessThanOrEqualTo", "greaterThanOrEqualTo",
+ // Range operator
+ "between",
+ // Existence operators
+ "exists", "missing",
+ // Content operators
+ "contains", "notContains", "startsWith", "endsWith", "matchesRegex",
+ // Collection operators
+ "in", "notIn", "all", "inContains", "hasSomeOf", "hasNoneOf",
+ // Date operators
+ "isDay", "isNotDay",
+ // Geographic operator
+ "distance"
));
@Override
diff --git
a/services/src/main/java/org/apache/unomi/services/impl/validation/validators/ConditionValueTypeValidator.java
b/services/src/main/java/org/apache/unomi/services/impl/validation/validators/ConditionValueTypeValidator.java
index efab47d57..f77c008cc 100644
---
a/services/src/main/java/org/apache/unomi/services/impl/validation/validators/ConditionValueTypeValidator.java
+++
b/services/src/main/java/org/apache/unomi/services/impl/validation/validators/ConditionValueTypeValidator.java
@@ -57,6 +57,9 @@ public class ConditionValueTypeValidator implements
ValueTypeValidator {
}
Condition condition = (Condition) value;
+ // Note: This validator performs basic structure validation.
+ // Condition type resolution should happen before validation in
ConditionValidationServiceImpl.
+ // If the type is not resolved here, it will be caught by the main
validation.
// Basic validation: must have type and metadata
ConditionType type = condition.getConditionType();
if (type == null || type.getMetadata() == null) {
diff --git
a/services/src/test/java/org/apache/unomi/services/impl/rules/TestSetEventOccurrenceCountAction.java
b/services/src/test/java/org/apache/unomi/services/impl/rules/TestSetEventOccurrenceCountAction.java
index 1c6961edd..e41e0a18a 100644
---
a/services/src/test/java/org/apache/unomi/services/impl/rules/TestSetEventOccurrenceCountAction.java
+++
b/services/src/test/java/org/apache/unomi/services/impl/rules/TestSetEventOccurrenceCountAction.java
@@ -53,7 +53,9 @@ public class TestSetEventOccurrenceCountAction implements
ActionExecutor {
ArrayList<Condition> conditions = new ArrayList<Condition>();
Condition eventCondition = (Condition)
pastEventCondition.getParameter("eventCondition");
- definitionsService.resolveConditionType(eventCondition);
+ if (eventCondition != null) {
+
definitionsService.getConditionValidationService().validate(eventCondition);
+ }
conditions.add(eventCondition);
Condition c = new
Condition(definitionsService.getConditionType("eventPropertyCondition"));
diff --git
a/services/src/test/java/org/apache/unomi/services/impl/validation/ConditionValidationServiceImplTest.java
b/services/src/test/java/org/apache/unomi/services/impl/validation/ConditionValidationServiceImplTest.java
index b06daca0d..0397040d4 100644
---
a/services/src/test/java/org/apache/unomi/services/impl/validation/ConditionValidationServiceImplTest.java
+++
b/services/src/test/java/org/apache/unomi/services/impl/validation/ConditionValidationServiceImplTest.java
@@ -31,11 +31,13 @@ import
org.apache.unomi.persistence.spi.conditions.evaluator.ConditionEvaluatorD
import org.apache.unomi.services.TestHelper;
import org.apache.unomi.services.common.security.ExecutionContextManagerImpl;
import org.apache.unomi.services.common.security.KarafSecurityService;
-import org.apache.unomi.services.impl.*;
+import org.apache.unomi.services.impl.InMemoryPersistenceServiceImpl;
+import org.apache.unomi.services.impl.TestConditionEvaluators;
+import org.apache.unomi.services.impl.TestTenantService;
import org.apache.unomi.services.impl.cache.MultiTypeCacheServiceImpl;
import org.apache.unomi.tracing.api.TracerService;
-import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
@@ -149,7 +151,7 @@ public class ConditionValidationServiceImplTest {
@BeforeEach
public void setUp() {
-
+
tracerService = TestHelper.createTracerService();
tenantService = new TestTenantService();
@@ -531,8 +533,11 @@ public class ConditionValidationServiceImplTest {
// Test missing required parameter in child
childCondition.setParameter("propertyName", null);
errors = conditionValidationService.validate(parentCondition);
- assertEquals(1, errors.size());
- assertEquals(ValidationErrorType.MISSING_REQUIRED_PARAMETER,
errors.get(0).getType());
+ // Should get error from child condition (nested conditions are
validated recursively)
+ assertTrue(errors.size() >= 1, "Should have at least one error from
child condition");
+ assertTrue(errors.stream().anyMatch(e -> e.getType() ==
ValidationErrorType.MISSING_REQUIRED_PARAMETER &&
+ e.getParameterName() != null
&& e.getParameterName().equals("propertyName")),
+ "Should have error for missing propertyName in child
condition");
// Test invalid condition tag
ConditionType profileType = createProfilePropertyConditionType();
@@ -869,9 +874,12 @@ public class ConditionValidationServiceImplTest {
// Validate and check results
List<ValidationError> errors =
conditionValidationService.validate(parentCondition);
- assertEquals(1, errors.size(), "Should have one validation error");
- ValidationError error = errors.get(0);
- assertEquals(ValidationErrorType.INVALID_VALUE, error.getType());
+ // Should have at least one error for invalid value in child condition
+ assertTrue(errors.size() >= 1, "Should have at least one validation
error");
+ assertTrue(errors.stream().anyMatch(e -> e.getType() ==
ValidationErrorType.INVALID_VALUE),
+ "Should have error for invalid value in child condition");
+ ValidationError error = errors.stream().filter(e -> e.getType() ==
ValidationErrorType.INVALID_VALUE).findFirst().orElse(null);
+ assertNotNull(error, "Should have invalid value error");
assertNotNull(error.getContext(), "Error should have context");
assertTrue(error.getContext().containsKey("location"), "Context should
contain location info");
}
@@ -903,10 +911,11 @@ public class ConditionValidationServiceImplTest {
Condition invalidChildCondition = new Condition(profileType);
parentCondition.setParameter("subConditions",
Arrays.asList(childCondition1, invalidChildCondition));
errors = conditionValidationService.validate(parentCondition);
- assertEquals(3, errors.size());
- assertTrue(errors.stream().anyMatch(e -> e.getType() ==
ValidationErrorType.MISSING_REQUIRED_PARAMETER &&
e.getParameterName().equals("propertyName")));
- assertTrue(errors.stream().anyMatch(e -> e.getType() ==
ValidationErrorType.MISSING_REQUIRED_PARAMETER &&
e.getParameterName().equals("comparisonOperator")));
- assertTrue(errors.stream().anyMatch(e -> e.getType() ==
ValidationErrorType.MISSING_REQUIRED_PARAMETER &&
e.getParameterName().equals("propertyValue")));
+ // Should have at least 3 errors for missing required parameters in
invalid child condition
+ assertTrue(errors.size() >= 3, "Should have at least 3 errors for
missing required parameters in invalid child condition");
+ assertTrue(errors.stream().anyMatch(e -> e.getType() ==
ValidationErrorType.MISSING_REQUIRED_PARAMETER && e.getParameterName() != null
&& e.getParameterName().equals("propertyName")));
+ assertTrue(errors.stream().anyMatch(e -> e.getType() ==
ValidationErrorType.MISSING_REQUIRED_PARAMETER && e.getParameterName() != null
&& e.getParameterName().equals("comparisonOperator")));
+ assertTrue(errors.stream().anyMatch(e -> e.getType() ==
ValidationErrorType.MISSING_REQUIRED_PARAMETER && e.getParameterName() != null
&& e.getParameterName().equals("propertyValue")));
// Test with non-condition object in the list
parentCondition.setParameter("subConditions",
Arrays.asList(childCondition1, "not a condition"));
@@ -1151,13 +1160,136 @@ public class ConditionValidationServiceImplTest {
containerCondition.setParameter("filter", null);
booleanCondition.setParameter("operator", "invalid");
errors = conditionValidationService.validate(containerCondition);
- assertEquals(1, errors.size());
- assertEquals(ValidationErrorType.INVALID_VALUE,
errors.get(0).getType());
+ // Should have at least one error for invalid operator
+ assertTrue(errors.size() >= 1, "Should have at least one error for
invalid operator");
+ assertTrue(errors.stream().anyMatch(e -> e.getType() ==
ValidationErrorType.INVALID_VALUE),
+ "Should have error for invalid operator value");
+ // Reset operator for next test
+ booleanCondition.setParameter("operator", "and");
// Test missing required parameter in deepest level
eventCondition.setParameter("propertyName", null);
errors = conditionValidationService.validate(containerCondition);
- assertEquals(2, errors.size()); // One for invalid operator, one for
missing propertyName
- assertTrue(errors.stream().anyMatch(e -> e.getType() ==
ValidationErrorType.MISSING_REQUIRED_PARAMETER));
+ // Should have at least 2 errors: one for invalid operator (if still
set), one for missing propertyName
+ assertTrue(errors.size() >= 1, "Should have at least one error for
missing propertyName");
+ assertTrue(errors.stream().anyMatch(e -> e.getType() ==
ValidationErrorType.MISSING_REQUIRED_PARAMETER),
+ "Should have error for missing required parameter");
+ }
+
+ public class PartialValidationTests {
+ @Test
+ public void testValidate_SkipsParameterReferences() {
+ ConditionType conditionType = createConditionType("testCondition");
+ Parameter param = new Parameter();
+ param.setId("testParam");
+ param.setType("string");
+ ConditionValidation validation = new ConditionValidation();
+ validation.setRequired(true);
+ param.setValidation(validation);
+ conditionType.setParameters(Collections.singletonList(param));
+
+ Condition condition = new Condition();
+ condition.setConditionType(conditionType);
+ condition.setParameter("testParam", "parameter::someReference");
+
+ List<ValidationError> errors =
conditionValidationService.validate(condition);
+
+ // Should skip validation for parameter references
+ assertNoErrors(errors);
+ }
+
+ @Test
+ public void testValidate_SkipsScriptExpressions() {
+ ConditionType conditionType = createConditionType("testCondition");
+ Parameter param = new Parameter();
+ param.setId("testParam");
+ param.setType("string");
+ ConditionValidation validation = new ConditionValidation();
+ validation.setRequired(true);
+ param.setValidation(validation);
+ conditionType.setParameters(Collections.singletonList(param));
+
+ Condition condition = new Condition();
+ condition.setConditionType(conditionType);
+ condition.setParameter("testParam", "script::someScript");
+
+ List<ValidationError> errors =
conditionValidationService.validate(condition);
+
+ // Should skip validation for script expressions
+ assertNoErrors(errors);
+ }
+
+ @Test
+ public void testValidate_ValidatesNonReferenceValues() {
+ ConditionType conditionType = createConditionType("testCondition");
+ Parameter param = new Parameter();
+ param.setId("testParam");
+ param.setType("string");
+ ConditionValidation validation = new ConditionValidation();
+ validation.setRequired(true);
+ param.setValidation(validation);
+ conditionType.setParameters(Collections.singletonList(param));
+
+ Condition condition = new Condition();
+ condition.setConditionType(conditionType);
+ condition.setParameter("testParam", "normalValue");
+
+ List<ValidationError> errors =
conditionValidationService.validate(condition);
+
+ // Should validate normal values
+ assertNoErrors(errors);
+ }
+
+ @Test
+ public void testValidate_ValidatesMissingRequiredForNonReferences() {
+ ConditionType conditionType = createConditionType("testCondition");
+ Parameter param = new Parameter();
+ param.setId("testParam");
+ param.setType("string");
+ ConditionValidation validation = new ConditionValidation();
+ validation.setRequired(true);
+ param.setValidation(validation);
+ conditionType.setParameters(Collections.singletonList(param));
+
+ Condition condition = new Condition();
+ condition.setConditionType(conditionType);
+ // testParam not set
+
+ List<ValidationError> errors =
conditionValidationService.validate(condition);
+
+ // Should validate missing required parameter
+ assertSingleError(errors, "testParam");
+ }
+
+ @Test
+ public void testValidate_RecursivelyValidatesNestedConditions() {
+ ConditionType parentType = createConditionType("parentCondition");
+ ConditionType childType = createConditionType("childCondition");
+ Parameter childParam = new Parameter();
+ childParam.setId("childParam");
+ childParam.setType("string");
+ ConditionValidation childValidation = new ConditionValidation();
+ childValidation.setRequired(true);
+ childParam.setValidation(childValidation);
+ childType.setParameters(Collections.singletonList(childParam));
+
+ Parameter parentParam = new Parameter();
+ parentParam.setId("childCondition");
+ parentParam.setType("condition");
+ parentType.setParameters(Collections.singletonList(parentParam));
+
+ Condition childCondition = new Condition();
+ childCondition.setConditionType(childType);
+ childCondition.setParameter("childParam", "parameter::reference");
+
+ Condition parentCondition = new Condition();
+ parentCondition.setConditionType(parentType);
+ parentCondition.setParameter("childCondition", childCondition);
+
+ List<ValidationError> errors =
conditionValidationService.validate(parentCondition);
+
+ // Should skip validation for nested condition with parameter
reference
+ assertNoErrors(errors);
+ }
}
}