I was able to use Drools to categorize transactions when I had a rules stored as a file in resources folder. But I need CRUD API, so I started to load rules from a database. I spent an afternoon, but no transaction is categorized and I think it's because of misconfiguration.
gradle:
droolsVersion = '8.44.0.Final'
implementation ("org.drools:drools-ruleunits-engine:${droolsVersion}")
implementation ("org.kie:kie-ci:${droolsVersion}")
implementation ("org.drools:drools-xml-support:${droolsVersion}")
Drools Configuration bean
@Component
public class DroolsConfig {
private final DroolsService droolsService;
private final KieServices kieServices;
private final KieFileSystem kieFileSystem;
private KieContainer kieContainer;
private KieBase kieBase;
public DroolsConfig(DroolsService droolsService) {
this.droolsService = droolsService;
kieServices = KieServices.Factory.get();
kieFileSystem = kieServices.newKieFileSystem();
buildInitialKieBase();
}
private void buildInitialKieBase() {
kieContainer = droolsService.loadRulesFromDatabase(kieServices, kieFileSystem);
kieBase = kieContainer.getKieBase();
}
}
Drools init service
@Service
public class DroolsService {
@Autowired private final DroolsRuleRepository ruleRepository;
public KieContainer loadRulesFromDatabase(KieServices kieServices, KieFileSystem kieFileSystem) {
List<DroolsRule> rules = ruleRepository.findByActive(true);
log.info("Found {} active rules", rules.size());
for (DroolsRule rule : rules) {
kieFileSystem.write("/rules/global/" + rule.getId() + ".drl", rule.getContent());
}
KieBuilder kieBuilder = kieServices.newKieBuilder(kieFileSystem);
kieBuilder.buildAll();
log.info("Rules compiled with following messages: {}", kieBuilder.getResults().getMessages());
for (Message message : kieBuilder.getResults().getMessages(Message.Level.ERROR)) {
System.out.println("Error in rule file: " + message.getText());
}
if (kieBuilder.getResults().hasMessages(Message.Level.ERROR)) {
throw new IllegalStateException("Errors while building rules: " + kieBuilder.getResults());
}
return kieServices.newKieContainer(kieServices.getRepository().getDefaultReleaseId());
}
}
Rules executor
@Service
public class RuleExecutorService {
private final DroolsConfig droolsConfig;
private final TransactionCategorizationService categorization;
private StatelessKieSession kieSession;
@PostConstruct
private void initializeSession() {
refreshSession();
}
public void refreshSession() {
log.info("Creating a new Kie session");
var kieContainer = droolsConfig.getKieContainer();
kieSession = kieContainer.newStatelessKieSession();
kieSession.setGlobal("TOOLS", categorization);
KieBase kieBase = kieSession.getKieBase();
for (KiePackage kiePackage : kieBase.getKiePackages()) {
for (Rule rule : kiePackage.getRules()) {
System.out.println("Rule: " + rule.getName());
}
}
}
public void executeRules(Transaction transaction) {
log.info("Executing rules for transaction {}", transaction.getBankReference());
kieSession.execute(transaction);
log.info("Rules executed");
}
One rule
rule "Tesco transactions"
salience 10
activation-group "shopIdentification"
when
$t : Transaction(transactionType == null && TOOLS.containsText($t, "Tesco"))
then
$t.setMerchant("Tesco");
$t.setExpenseCategory("FOOD");
$t.setTransactionType(TransactionType.EXPENSE);
end
Maven is set up (M2_HOME). I verified that the rules are loaded from the database. There is no compilation error. I have even added a listener to KieStatelessSession
, but it was never invoked. When I try to inspect KieBase
, it seems empty, but KieFileSystem
contains the files from database.
Logs:
INFO c.l.a.s.DroolsService [restartedMain] Loading rules from database
INFO c.l.a.s.DroolsService [restartedMain] Found 14 active rules
WARN o.d.c.k.b.i.AbstractKieProject [restartedMain] No files found for KieBase defaultKieBase
INFO c.l.a.s.DroolsService [restartedMain] Rules compiled with following messages: []
INFO o.d.c.k.b.i.KieContainerImpl [restartedMain] Start creation of KieBase: defaultKieBase
INFO o.d.c.k.b.i.KieContainerImpl [restartedMain] End creation of KieBase: defaultKieBase
INFO c.l.a.s.RuleExecutorService [restartedMain] Creating a new Kie session
I guess that the third lines indicates the trouble.
Here is the working code. The problem was that the file path in kieFileSystem.write()
did not start with "src/main/resources"
, which is enforced in Drools.
Service
import com.lelifin.alfa.model.entity.DroolsRule;
import com.lelifin.alfa.model.entity.Transaction;
import com.lelifin.alfa.repository.DroolsRuleRepository;
import jakarta.annotation.PostConstruct;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.drools.compiler.compiler.io.memory.MemoryFileSystem;
import org.drools.compiler.kie.builder.impl.KieBuilderImpl;
import org.drools.compiler.kie.builder.impl.KieFileSystemImpl;
import org.kie.api.KieServices;
import org.kie.api.builder.Message;
import org.kie.api.definition.KiePackage;
import org.kie.api.definition.rule.Rule;
import org.kie.api.runtime.KieContainer;
import org.kie.api.runtime.StatelessKieSession;
import org.springframework.stereotype.Service;
import java.io.File;
import java.io.FileOutputStream;
import java.util.List;
@Service
@Slf4j
public class RuleExecutorService {
private final TransactionClassifier categorization;
private final DroolsRuleRepository ruleRepository;
private final KieServices kieServices;
private KieContainer kieContainer;
private StatelessKieSession kieSession;
public RuleExecutorService(DroolsRuleRepository ruleRepository, TransactionClassifier categorization) {
this.ruleRepository = ruleRepository;
this.categorization = categorization;
kieServices = KieServices.Factory.get();
buildInitialKieBase();
}
public void buildInitialKieBase() {
log.info("Loading rules from database");
var rules = ruleRepository.findByActive(true);
log.info("Found {} active rules", rules.size());
var startTime = System.currentTimeMillis();
var kieFileSystem = kieServices.newKieFileSystem();
for (DroolsRule rule : rules) {
kieFileSystem.write("src/main/resources/rules/global/" + rule.getId() + ".drl", rule.getContent());
}
var endTime = System.currentTimeMillis();
log.info("Rules persisted in KieFileSystem in {} ms", endTime - startTime);
var kieBuilder = kieServices.newKieBuilder(kieFileSystem);
startTime = System.currentTimeMillis();
kieBuilder.buildAll();
endTime = System.currentTimeMillis();
log.info("Rules compiled in {} ms with the following messages: {}", endTime - startTime,
kieBuilder.getResults().getMessages());
if (kieBuilder.getResults().hasMessages(Message.Level.ERROR)) {
throw new IllegalStateException("Building rules failed!");
}
// Dump the internal MFS
var internalKieBuilder = (KieBuilderImpl) kieBuilder;
dumpKieBuilderMFS(((KieFileSystemImpl) kieFileSystem).getMfs(), "src-mfs");
dumpKieBuilderMFS(internalKieBuilder.getTrgMfs(), "target-mfs");
var kr = kieServices.getRepository();
startTime = System.currentTimeMillis();
kieContainer = kieServices.newKieContainer(kr.getDefaultReleaseId());
endTime = System.currentTimeMillis();
log.info("Time to load container: {} ms", endTime - startTime);
var kieBase = kieContainer.getKieBase();
for (KiePackage kiePackage : kieBase.getKiePackages()) {
for (Rule rule : kiePackage.getRules()) {
log.info("Rule: {}", rule.getName());
}
}
}
@SneakyThrows
public static void dumpKieBuilderMFS(MemoryFileSystem mfs, String outputDir) {
// Create the output directory if it doesn't exist
File outputDirectory = new File(outputDir);
if (!outputDirectory.exists()) {
outputDirectory.mkdirs();
}
// Iterate through all files in the MFS
for (var filePaths : mfs.getFilePaths()) {
byte[] fileContent = mfs.getBytes(filePaths);
// Write each file to the output directory
var fileName = filePaths.getFileName();
File outputFile = new File(outputDirectory, fileName.replace("/", "_")); // Flatten path for simpler debugging
try (FileOutputStream fos = new FileOutputStream(outputFile)) {
fos.write(fileContent);
}
}
}
@PostConstruct
private void initializeSession() {
refreshSession();
}
public void refreshSession() {
log.info("Creating a new Kie session");
kieSession = kieContainer.newStatelessKieSession();
kieSession.setGlobal("TOOLS", categorization);
}
public void executeRules(Transaction transaction) {
log.info("Executing rules for transaction {}", transaction.getBankReference());
kieSession.execute(transaction);
log.info("Rules executed");
}
public void executeRules(List<Transaction> transactions) {
log.info("Executing rules for {} transactions", transactions.size());
kieSession.execute(transactions);
log.info("Rules executed");
}
}
Rule:
package rules.global;
global com.lelifin.alfa.service.TransactionClassifier TOOLS;
rule "Lidl transactions"
salience 10
activation-group "shopIdentification"
when
$t : Transaction(transactionType == null && TOOLS.containsText($t, "Lidl"))
then
$t.setMerchant("Lidl");
$t.setExpenseCategory("FOOD");
$t.setTransactionType(TransactionType.EXPENSE);
end
gradle
droolsVersion = '8.44.0.Final'
implementation ("org.drools:drools-ruleunits-engine:${droolsVersion}")
implementation ("org.kie:kie-ci:${droolsVersion}")
implementation ("org.drools:drools-xml-support:${droolsVersion}")
Target FS dump: