Skip to content
Open
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 @@ -27,5 +27,6 @@ public enum FlowType {
PRE_UPDATE_PASSWORD,
PRE_UPDATE_PROFILE,
PRE_ISSUE_ID_TOKEN,
APPROVAL_WORKFLOW
APPROVAL_WORKFLOW,
DEVICE_POLICY
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@

package org.wso2.carbon.identity.rule.evaluation.api.model;

import java.util.Collections;
import java.util.List;

/**
* Rule evaluation result.
* This class is used to represent the result of a rule evaluation.
Expand All @@ -26,11 +29,18 @@ public class RuleEvaluationResult {

private final String ruleId;
private final boolean ruleSatisfied;
private final List<String> failedFields;

public RuleEvaluationResult(String ruleId, boolean ruleSatisfied) {

this(ruleId, ruleSatisfied, Collections.emptyList());
}

public RuleEvaluationResult(String ruleId, boolean ruleSatisfied, List<String> failedFields) {

this.ruleId = ruleId;
this.ruleSatisfied = ruleSatisfied;
this.failedFields = failedFields != null ? failedFields : Collections.emptyList();
}

public String getRuleId() {
Expand All @@ -42,4 +52,13 @@ public boolean isRuleSatisfied() {

return ruleSatisfied;
}

/**
* Returns the list of fields that failed evaluation.
* Empty when the rule is satisfied.
*/
public List<String> getFailedFields() {

return Collections.unmodifiableList(failedFields);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright (c) 2026, WSO2 LLC. (http://www.wso2.com).
*
* WSO2 LLC. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package org.wso2.carbon.identity.rule.evaluation.api.resolver;

import org.wso2.carbon.identity.rule.management.api.model.Value;

/**
* Resolves a symbolic field value to a concrete {@link Value} at rule evaluation time.
* Implementations are registered per field name via {@link SymbolicValueResolverRegistry}.
*/
public interface SymbolicValueResolver {

/**
* Resolves the given symbolic string to a concrete Value.
* The returned Value must have type NUMBER or LIST.
*
* @param symbolicValue Symbolic token or comma-separated list of tokens to resolve.
* @return Resolved Value with type NUMBER or LIST.
*/
Value resolve(String symbolicValue);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright (c) 2026, WSO2 LLC. (http://www.wso2.com).
*
* WSO2 LLC. licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except
* in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package org.wso2.carbon.identity.rule.evaluation.api.resolver;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
* Singleton registry for {@link SymbolicValueResolver} instances, keyed by field name.
* Bundles register resolvers on OSGi bundle activation and deregister on deactivation.
*/
public class SymbolicValueResolverRegistry {

private static final SymbolicValueResolverRegistry INSTANCE = new SymbolicValueResolverRegistry();
private final Map<String, SymbolicValueResolver> resolvers = new ConcurrentHashMap<>();

private SymbolicValueResolverRegistry() {

}

/**
* Returns the singleton instance of this registry.
*
* @return Singleton SymbolicValueResolverRegistry.
*/
public static SymbolicValueResolverRegistry getInstance() {

return INSTANCE;
}

/**
* Registers a resolver for the given field name.
*
* @param fieldName Field name to associate the resolver with.
* @param resolver Resolver implementation.
*/
public void register(String fieldName, SymbolicValueResolver resolver) {

resolvers.put(fieldName, resolver);
}

/**
* Removes the resolver registered for the given field name.
*
* @param fieldName Field name whose resolver should be removed.
*/
public void deregister(String fieldName) {

resolvers.remove(fieldName);
}

/**
* Returns the resolver for the given field name, or null if none is registered.
*
* @param fieldName Field name.
* @return Registered resolver, or null if not found.
*/
public SymbolicValueResolver getResolver(String fieldName) {

return resolvers.get(fieldName);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ private ValueType resolveValueType(Value fieldDefinitionValue) {
case REFERENCE:
OptionsReferenceValue referenceValue = (OptionsReferenceValue) fieldDefinitionValue;
return ValueType.createReferenceType(referenceValue.getValueReferenceAttribute());
case SYMBOLIC:
return ValueType.NUMBER;
default:
throw new IllegalArgumentException("Unsupported value type: " + fieldDefinitionValueType);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ public class OperatorRegistry {
supportedOperators.put("endsWith", stringPredicate(String::endsWith));
supportedOperators.put("greaterThan", comparablePredicate(result -> result > 0));
supportedOperators.put("lessThan", comparablePredicate(result -> result < 0));
supportedOperators.put("greaterThanOrEquals", comparablePredicate(result -> result >= 0));
supportedOperators.put("in", stringPredicate((a, b) -> java.util.Arrays.stream(b.split(","))
.map(String::trim).anyMatch(token -> token.equals(a))));
}

private OperatorRegistry() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ public RuleEvaluationResult evaluate(String ruleId, FlowContext flowContext, Str
boolean evaluationStatus = ruleEvaluator.evaluate(rule, evaluationData);
LOG.debug("Evaluated rule: " + rule.getId() + " to: " + evaluationStatus + ".");

return new RuleEvaluationResult(ruleId, evaluationStatus);
return new RuleEvaluationResult(ruleId, evaluationStatus, ruleEvaluator.getFailedFields());
}

private Map<String, FieldValue> getEvaluationData(String ruleId, FlowContext flowContext,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,17 @@
import org.wso2.carbon.identity.rule.evaluation.api.exception.RuleEvaluationException;
import org.wso2.carbon.identity.rule.evaluation.api.model.FieldValue;
import org.wso2.carbon.identity.rule.evaluation.api.model.Operator;
import org.wso2.carbon.identity.rule.evaluation.api.resolver.SymbolicValueResolver;
import org.wso2.carbon.identity.rule.evaluation.api.resolver.SymbolicValueResolverRegistry;
import org.wso2.carbon.identity.rule.management.api.model.ANDCombinedRule;
import org.wso2.carbon.identity.rule.management.api.model.Expression;
import org.wso2.carbon.identity.rule.management.api.model.ORCombinedRule;
import org.wso2.carbon.identity.rule.management.api.model.Rule;
import org.wso2.carbon.identity.rule.management.api.model.Value;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;

Expand All @@ -46,11 +52,13 @@ public class RuleEvaluator {
private static final Log LOG = LogFactory.getLog(RuleEvaluator.class);

private final OperatorRegistry operatorRegistry;
private final List<String> failedFields = new ArrayList<>();

// Operators
private static final String EQUALS = "equals";
private static final String NOT_EQUALS = "notEquals";
private static final String CONTAINS = "contains";
private static final String IN = "in";

public RuleEvaluator(OperatorRegistry operatorRegistry) {

Expand All @@ -71,22 +79,32 @@ public boolean evaluate(Rule rule, Map<String, FieldValue> evaluationData) throw
return evaluateORCombinedRule(orRule, evaluationData);
}

public List<String> getFailedFields() {

return Collections.unmodifiableList(failedFields);
}

private boolean evaluateORCombinedRule(ORCombinedRule orRule, Map<String, FieldValue> evaluationData)
throws RuleEvaluationException {

for (ANDCombinedRule andRule : orRule.getRules()) {
if (evaluateANDCombinedRule(andRule, evaluationData)) {
List<String> branchFailedFields = new ArrayList<>();
if (evaluateANDCombinedRule(andRule, evaluationData, branchFailedFields)) {
failedFields.clear();
return true; // If any ANDCombinedRule evaluates to true, the ORCombinedRule passes
}
failedFields.addAll(branchFailedFields);
}
return false; // If none of the ANDCombinedRules pass, the ORCombinedRule fails
}

private boolean evaluateANDCombinedRule(ANDCombinedRule andRule, Map<String, FieldValue> evaluationData)
private boolean evaluateANDCombinedRule(ANDCombinedRule andRule, Map<String, FieldValue> evaluationData,
List<String> branchFailedFields)
throws RuleEvaluationException {

for (Expression expression : andRule.getExpressions()) {
if (!evaluateExpression(expression, evaluationData)) {
branchFailedFields.add(expression.getField());
return false; // If any expression fails, the ANDCombinedRule fails
}
}
Expand All @@ -103,13 +121,21 @@ private boolean evaluateExpression(Expression expression, Map<String, FieldValue

Operator operator = operatorRegistry.getOperator(expression.getOperator());

if (expression.getValue().getType() == Value.Type.SYMBOLIC) {
return evaluateSymbolicExpression(expression, fieldValue, operator);
}

// Evaluate based on the value type of the field
if (fieldValue.getValueType().equals(STRING)) {
return operator.apply(fieldValue.getValue(), expression.getValue().getFieldValue());
} else if (fieldValue.getValueType().equals(BOOLEAN)) {
return operator.apply(fieldValue.getValue(),
Boolean.parseBoolean(expression.getValue().getFieldValue()));
} else if (fieldValue.getValueType().equals(NUMBER)) {
if (IN.equals(expression.getOperator())) {
return evaluateInForNumber((Double) fieldValue.getValue(),
expression.getValue().getFieldValue());
}
return operator.apply(fieldValue.getValue(), Double.parseDouble(expression.getValue().getFieldValue()));
} else if (fieldValue.getValueType().equals(REFERENCE)) {
return operator.apply(fieldValue.getValue(), expression.getValue().getFieldValue());
Expand All @@ -120,6 +146,42 @@ private boolean evaluateExpression(Expression expression, Map<String, FieldValue
throw new IllegalStateException("Unsupported value type: " + fieldValue.getValueType());
}

private boolean evaluateInForNumber(Double numericValue, String commaSeparated)
throws RuleEvaluationException {

try {
return Arrays.stream(commaSeparated.split(","))
.map(String::trim)
.map(Double::parseDouble)
.anyMatch(v -> v.equals(numericValue));
} catch (NumberFormatException e) {
throw new RuleEvaluationException(
"Failed to parse numeric value in 'in' expression: " + commaSeparated, e);
}
}

private boolean evaluateSymbolicExpression(Expression expression, FieldValue fieldValue, Operator operator)
throws RuleEvaluationException {

SymbolicValueResolver resolver = SymbolicValueResolverRegistry.getInstance()
.getResolver(expression.getField());
if (resolver == null) {
throw new RuleEvaluationException(
"No symbolic resolver registered for field: " + expression.getField());
}
Value resolved = resolver.resolve(expression.getValue().getFieldValue());
if (resolved.getType() != Value.Type.NUMBER && resolved.getType() != Value.Type.LIST) {
throw new RuleEvaluationException(
"Symbolic resolver returned unsupported type: " + resolved.getType());
}

// Route on the operator (consistent with evaluateExpression), not on the resolved value shape.
if (IN.equals(expression.getOperator())) {
return evaluateInForNumber((Double) fieldValue.getValue(), resolved.getFieldValue());
}
return operator.apply(fieldValue.getValue(), Double.parseDouble(resolved.getFieldValue()));
}
Comment thread
kaviska marked this conversation as resolved.

private boolean applyOperatorForList(Operator operator, Object fieldValue, Object expressionValue) {

List<?> list = (List<?>) fieldValue;
Expand Down
Loading
Loading