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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>net.ironoc.rules.engine</groupId>
<artifactId>simple-rules-engine</artifactId>
<version>2.3-SNAPSHOT</version>
<version>2.4-SNAPSHOT</version>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@

import module java.base;

import com.fasterxml.jackson.databind.ObjectMapper;
import net.ironoc.rules.engine.domain.ApiResponse;
import net.ironoc.rules.engine.domain.TestApiResponse;
import net.ironoc.rules.engine.dto.Feature;
import net.ironoc.rules.engine.dto.Rule;
import net.ironoc.rules.engine.enums.Country;
import net.ironoc.rules.engine.service.RulesService;
import org.camunda.bpm.engine.ProcessEngine;
import org.camunda.bpm.engine.ProcessEngines;
import org.camunda.bpm.engine.runtime.ProcessInstantiationBuilder;
import net.ironoc.rules.engine.enums.RuleGroup;
import net.ironoc.rules.engine.service.DetailCacheI;
import net.ironoc.rules.engine.service.RuleServiceI;
import org.camunda.bpm.engine.RuntimeService;
import org.camunda.bpm.engine.runtime.ProcessInstanceWithVariables;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
Expand All @@ -26,15 +26,19 @@ public class FlagController {

private static final Logger LOGGER = LoggerFactory.getLogger(FlagController.class);

private final RulesService rulesService;
private final RuleServiceI rulesService;

private final ObjectMapper objectMapper;
private final RuntimeService runtimeService;

private final DetailCacheI featureDetailsService;

@Autowired
public FlagController(RulesService rulesService,
ObjectMapper objectMapper) {
public FlagController(RuleServiceI rulesService,
RuntimeService runtimeService,
DetailCacheI featureDetailsService) {
this.rulesService = rulesService;
this.objectMapper = objectMapper;
this.runtimeService = runtimeService;
this.featureDetailsService = featureDetailsService;
}

@GetMapping(value = "api/test")
Expand All @@ -46,62 +50,33 @@ ResponseEntity<TestApiResponse> testApiCall() {
ResponseEntity<String> executeTaskToLoadInitialRuleSet() {
// TODO move to scheduled call or job,
// Note: This GET method/trigger is for testing purposes only to force load of initial rule set
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
ProcessInstantiationBuilder instance = engine.getRuntimeService().createProcessInstanceByKey("rules-init");
instance.executeWithVariablesInReturn();
return ResponseEntity.ok().body("Executed Camunda BPMN");
ProcessInstanceWithVariables result = runtimeService
.createProcessInstanceByKey("Rules_matcher")
.executeWithVariablesInReturn();

String pid = result.getProcessInstanceId();
LOGGER.info("Started Rules_matcher process instance id={}", pid);

return ResponseEntity.ok().body("{\"processInstanceId\":\"" + pid + "\"}");
}

@GetMapping(value = "api/eval", produces = MediaType.APPLICATION_JSON_VALUE)
ResponseEntity<ApiResponse> evaluateFlags(@RequestParam(value = "feature") String feature,
@RequestParam(value = "country") String country,
@RequestParam(value = "appVersion") String appVersion,
@RequestParam(value = "tier") String tier) {
Feature ft = rulesService.getFeaturesById().getOrDefault(feature, null);
Feature ft = featureDetailsService.getFeaturesById().getOrDefault(feature, null);
LOGGER.info("Evaluating feature: {} for country: {} appVersion: {} tier: {}", feature, country, appVersion, tier);
if (ft != null && ft.enabled()) {
if (ft.ruleGroups() != null) {
List<Rule> allRuleMatch = getAllRuleMatch(country, appVersion, tier, ft);
List<Rule> anyRuleMatch = getAnyRuleMatch(country, appVersion, tier, ft);
return createResponseFromMatches(feature, allRuleMatch, anyRuleMatch);
List<Rule> allRuleMatch = rulesService.getRuleMatchByRuleGroup(country, appVersion,
tier, ft, RuleGroup.ALL);
List<Rule> anyRuleMatch = rulesService.getRuleMatchByRuleGroup(country, appVersion,
tier, ft, RuleGroup.ANY);
return rulesService.createResponseFromMatches(feature, allRuleMatch, anyRuleMatch);
}
}
LOGGER.warn("Feature {} is not enabled.", feature);
return ResponseEntity.badRequest().body(new ApiResponse(Collections.emptyList()));
}

private ResponseEntity<ApiResponse> createResponseFromMatches(String feature,
List<Rule> allRuleMatch,
List<Rule> anyRuleMatch) {
if (allRuleMatch.isEmpty() && anyRuleMatch.isEmpty()) {
// no match
LOGGER.warn("Rules did not match for feature {}", feature);
return ResponseEntity.badRequest().body(new ApiResponse(Collections.emptyList()));
} else {
// direct match(es)
List<Rule> ruleMatch = new ArrayList<>();
ruleMatch.addAll(allRuleMatch);
ruleMatch.addAll(anyRuleMatch);
LOGGER.info("Rules set for feature {} is {}", feature, ruleMatch);
return ResponseEntity.ok().body(new ApiResponse(ruleMatch));
}
}

private List<Rule> getAnyRuleMatch(String country, String appVersion, String tier, Feature ft) {
List<Rule> anyRuleMatch;
Map<String, Map<String, Object>> featureAnyRules = objectMapper
.convertValue(ft.ruleGroups().any(), Map.class);
anyRuleMatch = rulesService.rulesMatcher(country,
appVersion, tier, featureAnyRules);
return anyRuleMatch;
}

private List<Rule> getAllRuleMatch(String country, String appVersion, String tier, Feature ft) {
List<Rule> allRuleMatch;
Map<String, Map<String, Object>> featureAllRules = objectMapper
.convertValue(ft.ruleGroups().all(), Map.class);
allRuleMatch = rulesService.rulesMatcher(country,
appVersion, tier, featureAllRules);
return allRuleMatch;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package net.ironoc.rules.engine.delegate;

import org.camunda.bpm.engine.delegate.DelegateExecution;
import org.camunda.bpm.engine.delegate.JavaDelegate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.util.Objects;

@Component
public class AppVersionDelegate implements JavaDelegate {

private static final Logger LOGGER = LoggerFactory.getLogger(AppVersionDelegate.class);

public static final String VAR_APP_VERSION = "appVersion";

@Override
public void execute(DelegateExecution execution) {
// TODO : validate appVersion format
String appVersion = Objects.toString(execution.getVariable(VAR_APP_VERSION), "").trim();
LOGGER.info("AppVersion captured='{}' (matching is performed in RulesAggregatorDelegate via RulesService).", appVersion);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package net.ironoc.rules.engine.delegate;

import org.camunda.bpm.engine.delegate.DelegateExecution;
import org.camunda.bpm.engine.delegate.JavaDelegate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.util.Objects;

@Component
public class FeatureDelegate implements JavaDelegate {

private static final Logger LOGGER = LoggerFactory.getLogger(FeatureDelegate.class);

public static final String VAR_COUNTRY = "country";

@Override
public void execute(DelegateExecution execution) {
// TODO
String country = Objects.toString(execution.getVariable(VAR_COUNTRY), "").trim();
LOGGER.info("Country captured='{}' (matching is performed in RulesAggregatorDelegate via RulesService).", country);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package net.ironoc.rules.engine.delegate;

import org.camunda.bpm.engine.delegate.DelegateExecution;
import org.camunda.bpm.engine.delegate.JavaDelegate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Component
public class FeatureDisabledDelegate implements JavaDelegate {

private static final Logger LOGGER = LoggerFactory.getLogger(FeatureDisabledDelegate.class);

@Override
public void execute(DelegateExecution execution) {
// TODO
execution.setVariable(FeatureEnabledDelegate.VAR_FEATURE_ENABLED, false);

execution.removeVariable(FeatureEnabledDelegate.VAR_FEATURE_DTO);
execution.removeVariable(FeatureEnabledDelegate.VAR_RULE_GROUPS_ALL);
execution.removeVariable(FeatureEnabledDelegate.VAR_RULE_GROUPS_ANY);

LOGGER.info("Feature marked disabled; rule context cleared.");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package net.ironoc.rules.engine.delegate;

import net.ironoc.rules.engine.dto.Feature;
import net.ironoc.rules.engine.service.DetailCacheI;
import org.camunda.bpm.engine.delegate.DelegateExecution;
import org.camunda.bpm.engine.delegate.JavaDelegate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.util.Objects;

@Component
public class FeatureEnabledDelegate implements JavaDelegate {

private static final Logger LOGGER = LoggerFactory.getLogger(FeatureEnabledDelegate.class);

public static final String VAR_FEATURE = "feature";
public static final String VAR_FEATURE_ENABLED = "featureEnabled";
public static final String VAR_FEATURE_DTO = "featureDto";
public static final String VAR_RULE_GROUPS_ALL = "ruleGroupsAll";
public static final String VAR_RULE_GROUPS_ANY = "ruleGroupsAny";

private final DetailCacheI featureDetailsService;

public FeatureEnabledDelegate(DetailCacheI featureDetailsService) {
this.featureDetailsService = featureDetailsService;
}

@Override
public void execute(DelegateExecution execution) {
// TODO
String featureId = Objects.toString(execution.getVariable(VAR_FEATURE), "").trim();

Feature feature = featureId.isEmpty() ? null : featureDetailsService.getFeaturesById().get(featureId);
boolean enabled = feature != null && feature.enabled();

execution.setVariable(VAR_FEATURE_ENABLED, enabled);

if (enabled) {
execution.setVariable(VAR_FEATURE_DTO, feature);
execution.setVariable(VAR_RULE_GROUPS_ALL, feature.ruleGroups() != null ? feature.ruleGroups().all() : null);
execution.setVariable(VAR_RULE_GROUPS_ANY, feature.ruleGroups() != null ? feature.ruleGroups().any() : null);
LOGGER.info("Feature '{}' is enabled; loaded rule groups into process context.", featureId);
} else {
execution.removeVariable(VAR_FEATURE_DTO);
execution.removeVariable(VAR_RULE_GROUPS_ALL);
execution.removeVariable(VAR_RULE_GROUPS_ANY);
LOGGER.info("Feature '{}' is disabled or missing; cleared rule context.", featureId);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package net.ironoc.rules.engine.delegate;

import net.ironoc.rules.engine.dto.Rule;
import org.camunda.bpm.engine.delegate.DelegateExecution;
import org.camunda.bpm.engine.delegate.JavaDelegate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.util.ArrayList;

@Component
public class PassEngineDelegate implements JavaDelegate {

private static final Logger LOGGER = LoggerFactory.getLogger(PassEngineDelegate.class);

public static final String VAR_MATCHED_RULES = "matchedRules";
public static final String VAR_SKIP_RULES_ENGINE = "skipRulesEngine";

@Override
public void execute(DelegateExecution execution) {
// TODO
execution.setVariable(VAR_SKIP_RULES_ENGINE, true);
execution.setVariable(VAR_MATCHED_RULES, new ArrayList<Rule>());

LOGGER.info("Bypassing rules engine: matchedRules set to empty and skipRulesEngine=true");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package net.ironoc.rules.engine.delegate;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import net.ironoc.rules.engine.dto.Feature;
import net.ironoc.rules.engine.dto.Rule;
import net.ironoc.rules.engine.enums.RuleGroup;
import net.ironoc.rules.engine.service.DetailCacheI;
import net.ironoc.rules.engine.service.RuleServiceI;
import org.camunda.bpm.engine.delegate.DelegateExecution;
import org.camunda.bpm.engine.delegate.JavaDelegate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

import static net.ironoc.rules.engine.delegate.FeatureEnabledDelegate.VAR_FEATURE;

@Component
public class RulesAggregatorDelegate implements JavaDelegate {

private static final Logger LOGGER = LoggerFactory.getLogger(RulesAggregatorDelegate.class);

public static final String VAR_RULES_JSON = "rulesJson";

private final ObjectMapper objectMapper;

private final RuleServiceI rulesService;

private final DetailCacheI featureDetailsService;

public RulesAggregatorDelegate(ObjectMapper objectMapper, RuleServiceI rulesService,
DetailCacheI featureDetailsService) {
this.objectMapper = objectMapper;
this.rulesService = rulesService;
this.featureDetailsService = featureDetailsService;
}

@Override
public void execute(DelegateExecution execution) {
boolean skip = Boolean.TRUE.equals(execution.getVariable(PassEngineDelegate.VAR_SKIP_RULES_ENGINE));
boolean featureEnabled = Boolean.TRUE.equals(execution.getVariable(FeatureEnabledDelegate.VAR_FEATURE_ENABLED));

List<Rule> matchedRules;

if (skip || !featureEnabled) {
matchedRules = new ArrayList<>();
} else {
String country = Objects.toString(execution.getVariable(FeatureDelegate.VAR_COUNTRY), "").trim();
String appVersion = Objects.toString(execution.getVariable(AppVersionDelegate.VAR_APP_VERSION), "").trim();
String tier = Objects.toString(execution.getVariable(TierDelegate.VAR_TIER), "").trim();

String featureId = Objects.toString(execution.getVariable(VAR_FEATURE), "").trim();
Feature ft = featureId.isEmpty() ? null : featureDetailsService.getFeaturesById().get(featureId);
List<Rule> allRuleMatch = rulesService.getRuleMatchByRuleGroup(country, appVersion, tier,
ft, RuleGroup.ALL);
List<Rule> anyRuleMatch = rulesService.getRuleMatchByRuleGroup(country, appVersion, tier,
ft, RuleGroup.ANY);
matchedRules = Objects.requireNonNull(rulesService.createResponseFromMatches(featureId, allRuleMatch,
anyRuleMatch).getBody()).rules();
}

execution.setVariable(PassEngineDelegate.VAR_MATCHED_RULES, matchedRules);

try {
String json = objectMapper.writeValueAsString(matchedRules);
execution.setVariable(VAR_RULES_JSON, json);
LOGGER.info("Aggregated {} matched rules into rulesJson.", matchedRules.size());
} catch (JsonProcessingException e) {
throw new IllegalStateException("Failed to serialize matched rules to JSON", e);
}
}
}
24 changes: 24 additions & 0 deletions src/main/java/net/ironoc/rules/engine/delegate/TierDelegate.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package net.ironoc.rules.engine.delegate;

import org.camunda.bpm.engine.delegate.DelegateExecution;
import org.camunda.bpm.engine.delegate.JavaDelegate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.util.Objects;

@Component
public class TierDelegate implements JavaDelegate {

private static final Logger LOGGER = LoggerFactory.getLogger(TierDelegate.class);

public static final String VAR_TIER = "tier";

@Override
public void execute(DelegateExecution execution) {
// TODO
String tier = Objects.toString(execution.getVariable(VAR_TIER), "").trim();
LOGGER.info("Tier captured='{}' (matching is performed in RulesAggregatorDelegate via RulesService).", tier);
}
}
Loading
Loading