Skip to content

Commit ae158fc

Browse files
authored
Implement hook from Camunda UI to run rules matcher workflow / Business Process by Camunda form (#22)
* Upload valid BPMN template process for Rules Matcher flow with rules-init code triggered by Camunda UI * Implement flexible runtime and audit of rules engine via Camunda Platform UI (tasklist & cockpit) * Remove object mapper from controller - not needed * Add todos to delegates as they are placeholders without tests used in visual chain of events but only for passing flags right now in Java source * Refactor service layer, extract constants and interfaces
1 parent ac7f8d2 commit ae158fc

18 files changed

Lines changed: 718 additions & 71 deletions

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
<groupId>net.ironoc.rules.engine</groupId>
88
<artifactId>simple-rules-engine</artifactId>
9-
<version>2.3-SNAPSHOT</version>
9+
<version>2.4-SNAPSHOT</version>
1010

1111
<properties>
1212
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

src/main/java/net/ironoc/rules/engine/controller/FlagController.java

Lines changed: 28 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,16 @@
22

33
import module java.base;
44

5-
import com.fasterxml.jackson.databind.ObjectMapper;
65
import net.ironoc.rules.engine.domain.ApiResponse;
76
import net.ironoc.rules.engine.domain.TestApiResponse;
87
import net.ironoc.rules.engine.dto.Feature;
98
import net.ironoc.rules.engine.dto.Rule;
109
import net.ironoc.rules.engine.enums.Country;
11-
import net.ironoc.rules.engine.service.RulesService;
12-
import org.camunda.bpm.engine.ProcessEngine;
13-
import org.camunda.bpm.engine.ProcessEngines;
14-
import org.camunda.bpm.engine.runtime.ProcessInstantiationBuilder;
10+
import net.ironoc.rules.engine.enums.RuleGroup;
11+
import net.ironoc.rules.engine.service.DetailCacheI;
12+
import net.ironoc.rules.engine.service.RuleServiceI;
13+
import org.camunda.bpm.engine.RuntimeService;
14+
import org.camunda.bpm.engine.runtime.ProcessInstanceWithVariables;
1515
import org.slf4j.Logger;
1616
import org.slf4j.LoggerFactory;
1717
import org.springframework.beans.factory.annotation.Autowired;
@@ -26,15 +26,19 @@ public class FlagController {
2626

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

29-
private final RulesService rulesService;
29+
private final RuleServiceI rulesService;
3030

31-
private final ObjectMapper objectMapper;
31+
private final RuntimeService runtimeService;
32+
33+
private final DetailCacheI featureDetailsService;
3234

3335
@Autowired
34-
public FlagController(RulesService rulesService,
35-
ObjectMapper objectMapper) {
36+
public FlagController(RuleServiceI rulesService,
37+
RuntimeService runtimeService,
38+
DetailCacheI featureDetailsService) {
3639
this.rulesService = rulesService;
37-
this.objectMapper = objectMapper;
40+
this.runtimeService = runtimeService;
41+
this.featureDetailsService = featureDetailsService;
3842
}
3943

4044
@GetMapping(value = "api/test")
@@ -46,62 +50,33 @@ ResponseEntity<TestApiResponse> testApiCall() {
4650
ResponseEntity<String> executeTaskToLoadInitialRuleSet() {
4751
// TODO move to scheduled call or job,
4852
// Note: This GET method/trigger is for testing purposes only to force load of initial rule set
49-
ProcessEngine engine = ProcessEngines.getDefaultProcessEngine();
50-
ProcessInstantiationBuilder instance = engine.getRuntimeService().createProcessInstanceByKey("rules-init");
51-
instance.executeWithVariablesInReturn();
52-
return ResponseEntity.ok().body("Executed Camunda BPMN");
53+
ProcessInstanceWithVariables result = runtimeService
54+
.createProcessInstanceByKey("Rules_matcher")
55+
.executeWithVariablesInReturn();
56+
57+
String pid = result.getProcessInstanceId();
58+
LOGGER.info("Started Rules_matcher process instance id={}", pid);
59+
60+
return ResponseEntity.ok().body("{\"processInstanceId\":\"" + pid + "\"}");
5361
}
5462

5563
@GetMapping(value = "api/eval", produces = MediaType.APPLICATION_JSON_VALUE)
5664
ResponseEntity<ApiResponse> evaluateFlags(@RequestParam(value = "feature") String feature,
5765
@RequestParam(value = "country") String country,
5866
@RequestParam(value = "appVersion") String appVersion,
5967
@RequestParam(value = "tier") String tier) {
60-
Feature ft = rulesService.getFeaturesById().getOrDefault(feature, null);
68+
Feature ft = featureDetailsService.getFeaturesById().getOrDefault(feature, null);
6169
LOGGER.info("Evaluating feature: {} for country: {} appVersion: {} tier: {}", feature, country, appVersion, tier);
6270
if (ft != null && ft.enabled()) {
6371
if (ft.ruleGroups() != null) {
64-
List<Rule> allRuleMatch = getAllRuleMatch(country, appVersion, tier, ft);
65-
List<Rule> anyRuleMatch = getAnyRuleMatch(country, appVersion, tier, ft);
66-
return createResponseFromMatches(feature, allRuleMatch, anyRuleMatch);
72+
List<Rule> allRuleMatch = rulesService.getRuleMatchByRuleGroup(country, appVersion,
73+
tier, ft, RuleGroup.ALL);
74+
List<Rule> anyRuleMatch = rulesService.getRuleMatchByRuleGroup(country, appVersion,
75+
tier, ft, RuleGroup.ANY);
76+
return rulesService.createResponseFromMatches(feature, allRuleMatch, anyRuleMatch);
6777
}
6878
}
6979
LOGGER.warn("Feature {} is not enabled.", feature);
7080
return ResponseEntity.badRequest().body(new ApiResponse(Collections.emptyList()));
7181
}
72-
73-
private ResponseEntity<ApiResponse> createResponseFromMatches(String feature,
74-
List<Rule> allRuleMatch,
75-
List<Rule> anyRuleMatch) {
76-
if (allRuleMatch.isEmpty() && anyRuleMatch.isEmpty()) {
77-
// no match
78-
LOGGER.warn("Rules did not match for feature {}", feature);
79-
return ResponseEntity.badRequest().body(new ApiResponse(Collections.emptyList()));
80-
} else {
81-
// direct match(es)
82-
List<Rule> ruleMatch = new ArrayList<>();
83-
ruleMatch.addAll(allRuleMatch);
84-
ruleMatch.addAll(anyRuleMatch);
85-
LOGGER.info("Rules set for feature {} is {}", feature, ruleMatch);
86-
return ResponseEntity.ok().body(new ApiResponse(ruleMatch));
87-
}
88-
}
89-
90-
private List<Rule> getAnyRuleMatch(String country, String appVersion, String tier, Feature ft) {
91-
List<Rule> anyRuleMatch;
92-
Map<String, Map<String, Object>> featureAnyRules = objectMapper
93-
.convertValue(ft.ruleGroups().any(), Map.class);
94-
anyRuleMatch = rulesService.rulesMatcher(country,
95-
appVersion, tier, featureAnyRules);
96-
return anyRuleMatch;
97-
}
98-
99-
private List<Rule> getAllRuleMatch(String country, String appVersion, String tier, Feature ft) {
100-
List<Rule> allRuleMatch;
101-
Map<String, Map<String, Object>> featureAllRules = objectMapper
102-
.convertValue(ft.ruleGroups().all(), Map.class);
103-
allRuleMatch = rulesService.rulesMatcher(country,
104-
appVersion, tier, featureAllRules);
105-
return allRuleMatch;
106-
}
10782
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package net.ironoc.rules.engine.delegate;
2+
3+
import org.camunda.bpm.engine.delegate.DelegateExecution;
4+
import org.camunda.bpm.engine.delegate.JavaDelegate;
5+
import org.slf4j.Logger;
6+
import org.slf4j.LoggerFactory;
7+
import org.springframework.stereotype.Component;
8+
9+
import java.util.Objects;
10+
11+
@Component
12+
public class AppVersionDelegate implements JavaDelegate {
13+
14+
private static final Logger LOGGER = LoggerFactory.getLogger(AppVersionDelegate.class);
15+
16+
public static final String VAR_APP_VERSION = "appVersion";
17+
18+
@Override
19+
public void execute(DelegateExecution execution) {
20+
// TODO : validate appVersion format
21+
String appVersion = Objects.toString(execution.getVariable(VAR_APP_VERSION), "").trim();
22+
LOGGER.info("AppVersion captured='{}' (matching is performed in RulesAggregatorDelegate via RulesService).", appVersion);
23+
}
24+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package net.ironoc.rules.engine.delegate;
2+
3+
import org.camunda.bpm.engine.delegate.DelegateExecution;
4+
import org.camunda.bpm.engine.delegate.JavaDelegate;
5+
import org.slf4j.Logger;
6+
import org.slf4j.LoggerFactory;
7+
import org.springframework.stereotype.Component;
8+
9+
import java.util.Objects;
10+
11+
@Component
12+
public class FeatureDelegate implements JavaDelegate {
13+
14+
private static final Logger LOGGER = LoggerFactory.getLogger(FeatureDelegate.class);
15+
16+
public static final String VAR_COUNTRY = "country";
17+
18+
@Override
19+
public void execute(DelegateExecution execution) {
20+
// TODO
21+
String country = Objects.toString(execution.getVariable(VAR_COUNTRY), "").trim();
22+
LOGGER.info("Country captured='{}' (matching is performed in RulesAggregatorDelegate via RulesService).", country);
23+
}
24+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package net.ironoc.rules.engine.delegate;
2+
3+
import org.camunda.bpm.engine.delegate.DelegateExecution;
4+
import org.camunda.bpm.engine.delegate.JavaDelegate;
5+
import org.slf4j.Logger;
6+
import org.slf4j.LoggerFactory;
7+
import org.springframework.stereotype.Component;
8+
9+
@Component
10+
public class FeatureDisabledDelegate implements JavaDelegate {
11+
12+
private static final Logger LOGGER = LoggerFactory.getLogger(FeatureDisabledDelegate.class);
13+
14+
@Override
15+
public void execute(DelegateExecution execution) {
16+
// TODO
17+
execution.setVariable(FeatureEnabledDelegate.VAR_FEATURE_ENABLED, false);
18+
19+
execution.removeVariable(FeatureEnabledDelegate.VAR_FEATURE_DTO);
20+
execution.removeVariable(FeatureEnabledDelegate.VAR_RULE_GROUPS_ALL);
21+
execution.removeVariable(FeatureEnabledDelegate.VAR_RULE_GROUPS_ANY);
22+
23+
LOGGER.info("Feature marked disabled; rule context cleared.");
24+
}
25+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package net.ironoc.rules.engine.delegate;
2+
3+
import net.ironoc.rules.engine.dto.Feature;
4+
import net.ironoc.rules.engine.service.DetailCacheI;
5+
import org.camunda.bpm.engine.delegate.DelegateExecution;
6+
import org.camunda.bpm.engine.delegate.JavaDelegate;
7+
import org.slf4j.Logger;
8+
import org.slf4j.LoggerFactory;
9+
import org.springframework.stereotype.Component;
10+
11+
import java.util.Objects;
12+
13+
@Component
14+
public class FeatureEnabledDelegate implements JavaDelegate {
15+
16+
private static final Logger LOGGER = LoggerFactory.getLogger(FeatureEnabledDelegate.class);
17+
18+
public static final String VAR_FEATURE = "feature";
19+
public static final String VAR_FEATURE_ENABLED = "featureEnabled";
20+
public static final String VAR_FEATURE_DTO = "featureDto";
21+
public static final String VAR_RULE_GROUPS_ALL = "ruleGroupsAll";
22+
public static final String VAR_RULE_GROUPS_ANY = "ruleGroupsAny";
23+
24+
private final DetailCacheI featureDetailsService;
25+
26+
public FeatureEnabledDelegate(DetailCacheI featureDetailsService) {
27+
this.featureDetailsService = featureDetailsService;
28+
}
29+
30+
@Override
31+
public void execute(DelegateExecution execution) {
32+
// TODO
33+
String featureId = Objects.toString(execution.getVariable(VAR_FEATURE), "").trim();
34+
35+
Feature feature = featureId.isEmpty() ? null : featureDetailsService.getFeaturesById().get(featureId);
36+
boolean enabled = feature != null && feature.enabled();
37+
38+
execution.setVariable(VAR_FEATURE_ENABLED, enabled);
39+
40+
if (enabled) {
41+
execution.setVariable(VAR_FEATURE_DTO, feature);
42+
execution.setVariable(VAR_RULE_GROUPS_ALL, feature.ruleGroups() != null ? feature.ruleGroups().all() : null);
43+
execution.setVariable(VAR_RULE_GROUPS_ANY, feature.ruleGroups() != null ? feature.ruleGroups().any() : null);
44+
LOGGER.info("Feature '{}' is enabled; loaded rule groups into process context.", featureId);
45+
} else {
46+
execution.removeVariable(VAR_FEATURE_DTO);
47+
execution.removeVariable(VAR_RULE_GROUPS_ALL);
48+
execution.removeVariable(VAR_RULE_GROUPS_ANY);
49+
LOGGER.info("Feature '{}' is disabled or missing; cleared rule context.", featureId);
50+
}
51+
}
52+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package net.ironoc.rules.engine.delegate;
2+
3+
import net.ironoc.rules.engine.dto.Rule;
4+
import org.camunda.bpm.engine.delegate.DelegateExecution;
5+
import org.camunda.bpm.engine.delegate.JavaDelegate;
6+
import org.slf4j.Logger;
7+
import org.slf4j.LoggerFactory;
8+
import org.springframework.stereotype.Component;
9+
10+
import java.util.ArrayList;
11+
12+
@Component
13+
public class PassEngineDelegate implements JavaDelegate {
14+
15+
private static final Logger LOGGER = LoggerFactory.getLogger(PassEngineDelegate.class);
16+
17+
public static final String VAR_MATCHED_RULES = "matchedRules";
18+
public static final String VAR_SKIP_RULES_ENGINE = "skipRulesEngine";
19+
20+
@Override
21+
public void execute(DelegateExecution execution) {
22+
// TODO
23+
execution.setVariable(VAR_SKIP_RULES_ENGINE, true);
24+
execution.setVariable(VAR_MATCHED_RULES, new ArrayList<Rule>());
25+
26+
LOGGER.info("Bypassing rules engine: matchedRules set to empty and skipRulesEngine=true");
27+
}
28+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package net.ironoc.rules.engine.delegate;
2+
3+
import com.fasterxml.jackson.core.JsonProcessingException;
4+
import com.fasterxml.jackson.databind.ObjectMapper;
5+
import net.ironoc.rules.engine.dto.Feature;
6+
import net.ironoc.rules.engine.dto.Rule;
7+
import net.ironoc.rules.engine.enums.RuleGroup;
8+
import net.ironoc.rules.engine.service.DetailCacheI;
9+
import net.ironoc.rules.engine.service.RuleServiceI;
10+
import org.camunda.bpm.engine.delegate.DelegateExecution;
11+
import org.camunda.bpm.engine.delegate.JavaDelegate;
12+
import org.slf4j.Logger;
13+
import org.slf4j.LoggerFactory;
14+
import org.springframework.stereotype.Component;
15+
16+
import java.util.ArrayList;
17+
import java.util.List;
18+
import java.util.Objects;
19+
20+
import static net.ironoc.rules.engine.delegate.FeatureEnabledDelegate.VAR_FEATURE;
21+
22+
@Component
23+
public class RulesAggregatorDelegate implements JavaDelegate {
24+
25+
private static final Logger LOGGER = LoggerFactory.getLogger(RulesAggregatorDelegate.class);
26+
27+
public static final String VAR_RULES_JSON = "rulesJson";
28+
29+
private final ObjectMapper objectMapper;
30+
31+
private final RuleServiceI rulesService;
32+
33+
private final DetailCacheI featureDetailsService;
34+
35+
public RulesAggregatorDelegate(ObjectMapper objectMapper, RuleServiceI rulesService,
36+
DetailCacheI featureDetailsService) {
37+
this.objectMapper = objectMapper;
38+
this.rulesService = rulesService;
39+
this.featureDetailsService = featureDetailsService;
40+
}
41+
42+
@Override
43+
public void execute(DelegateExecution execution) {
44+
boolean skip = Boolean.TRUE.equals(execution.getVariable(PassEngineDelegate.VAR_SKIP_RULES_ENGINE));
45+
boolean featureEnabled = Boolean.TRUE.equals(execution.getVariable(FeatureEnabledDelegate.VAR_FEATURE_ENABLED));
46+
47+
List<Rule> matchedRules;
48+
49+
if (skip || !featureEnabled) {
50+
matchedRules = new ArrayList<>();
51+
} else {
52+
String country = Objects.toString(execution.getVariable(FeatureDelegate.VAR_COUNTRY), "").trim();
53+
String appVersion = Objects.toString(execution.getVariable(AppVersionDelegate.VAR_APP_VERSION), "").trim();
54+
String tier = Objects.toString(execution.getVariable(TierDelegate.VAR_TIER), "").trim();
55+
56+
String featureId = Objects.toString(execution.getVariable(VAR_FEATURE), "").trim();
57+
Feature ft = featureId.isEmpty() ? null : featureDetailsService.getFeaturesById().get(featureId);
58+
List<Rule> allRuleMatch = rulesService.getRuleMatchByRuleGroup(country, appVersion, tier,
59+
ft, RuleGroup.ALL);
60+
List<Rule> anyRuleMatch = rulesService.getRuleMatchByRuleGroup(country, appVersion, tier,
61+
ft, RuleGroup.ANY);
62+
matchedRules = Objects.requireNonNull(rulesService.createResponseFromMatches(featureId, allRuleMatch,
63+
anyRuleMatch).getBody()).rules();
64+
}
65+
66+
execution.setVariable(PassEngineDelegate.VAR_MATCHED_RULES, matchedRules);
67+
68+
try {
69+
String json = objectMapper.writeValueAsString(matchedRules);
70+
execution.setVariable(VAR_RULES_JSON, json);
71+
LOGGER.info("Aggregated {} matched rules into rulesJson.", matchedRules.size());
72+
} catch (JsonProcessingException e) {
73+
throw new IllegalStateException("Failed to serialize matched rules to JSON", e);
74+
}
75+
}
76+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package net.ironoc.rules.engine.delegate;
2+
3+
import org.camunda.bpm.engine.delegate.DelegateExecution;
4+
import org.camunda.bpm.engine.delegate.JavaDelegate;
5+
import org.slf4j.Logger;
6+
import org.slf4j.LoggerFactory;
7+
import org.springframework.stereotype.Component;
8+
9+
import java.util.Objects;
10+
11+
@Component
12+
public class TierDelegate implements JavaDelegate {
13+
14+
private static final Logger LOGGER = LoggerFactory.getLogger(TierDelegate.class);
15+
16+
public static final String VAR_TIER = "tier";
17+
18+
@Override
19+
public void execute(DelegateExecution execution) {
20+
// TODO
21+
String tier = Objects.toString(execution.getVariable(VAR_TIER), "").trim();
22+
LOGGER.info("Tier captured='{}' (matching is performed in RulesAggregatorDelegate via RulesService).", tier);
23+
}
24+
}

0 commit comments

Comments
 (0)