drools

Drools – Calling the KIE Server

In the previous post we learned to write rules and setup KIE server. Now it’s time to integrate with it and insert our fact data. Make sure your instances of KIE server and Drools Workbench are up and running.

➜  ~ docker ps
CONTAINER ID   IMAGE                                    COMMAND                  CREATED        STATUS              PORTS                                            NAMES
aa3b6f28e060   my-kie:latest                            "./start_kie-server.…"   3 days ago     Up About a minute   0.0.0.0:8180->8080/tcp                           kie-server3
5d3e05267be6   jboss/drools-workbench-showcase:latest   "./start_drools-wb.sh"   4 months ago   Up 48 seconds       0.0.0.0:8001->8001/tcp, 0.0.0.0:8080->8080/tcp   drools-workbench

We will start with the rest api. With the following cUrl command we insert facts into the working memory. Remember that you need the credentials in order to access KIE server. By default these are kieserver/kieserver1!

curl --location --request POST 'http://localhost:8180/kie-server/services/rest/server/containers/instances/ImportProducts_1.0.0-SNAPSHOT' \
--header 'Authorization: Basic a2llc2VydmVyOmtpZXNlcnZlcjEh' \
--header 'Content-Type: application/json' \
--data-raw '{
    "commands": [
        {
            "insert": {
                "object": {
                    "ImportProduct": {
                        "id": 1,
                        "category": "Games",
                        "price": 2.0,
                        "status": "OK"
                    }
                }
            }
        },
        {
            "insert": {
                "object": {
                    "ImportProduct": {
                        "id": 2,
                        "category": "Games",
                        "price": 4.0,
                        "status": "NOT_OK"
                    }
                }
            }
        },
        {
            "insert": {
                "object": {
                    "ImportProduct": {
                        "id": 3,
                        "category": "Games",
                        "price": 5.0,
                        "status": "OK"
                    }
                }
            }
        },
        {
            "fire-all-rules": {
                "max": 10,
                "out-identifier": "firedActivations"
            }
        }
    ]
}'

This will insert 3 facts and it will trigger the rules that we have built in the previous post. The response will be

{
    "type": "SUCCESS",
    "msg": "Container ImportProducts_1.0.0-SNAPSHOT successfully called.",
    "result": {
        "execution-results": {
            "results": [
                {
                    "value": 2,
                    "key": "firedActivations"
                }
            ],
            "facts": []
        }
    }
}

In order to check the result of the engine run we need to do another request.

curl --location --request POST 'http://localhost:8180/kie-server/services/rest/server/containers/instances/ImportProducts_1.0.0-SNAPSHOT' \
--header 'Authorization: Basic a2llc2VydmVyOmtpZXNlcnZlcjEh' \
--header 'Content-Type: application/json' \
--data-raw '{
    "commands": [
        {
            "get-objects": {
                "out-identifier": "import"
            }
        }
    ]
}'

This will respond with

{
    "type": "SUCCESS",
    "msg": "Container ImportProducts_1.0.0-SNAPSHOT successfully called.",
    "result": {
        "execution-results": {
            "results": [
                {
                    "value": [
                        {
                            "com.test.importproducts.ImportProduct": {
                                "id": "1",
                                "category": "Games",
                                "status": "OK",
                                "price": 2.0
                            }
                        },
                        {
                            "com.test.importproducts.ImportProduct": {
                                "id": "3",
                                "category": "Games",
                                "status": "OK",
                                "price": 5.0
                            }
                        }
                    ],
                    "key": "import"
                }
            ],
            "facts": []
        }
    }
}

Bear in mind I use a stateful session in this case which means that facts are not discarded after the requests. You will get duplicates if you will not dispose the session. This can be achieved programatically be calling dispose() method or you can restart(stop/start from workbench) the container.

Start/Stop a container

Now obviously if your application is using java you might want to go with the java client instead. In order to do that you need the kjar from the workbench repo. The idea here is that in order to insert the facts into drools engine you need to know the structure of the fact. So either you import the kjar or you mirror the structure on the application side. In this case I chose to import the kjar from the repo. This means I need to clone the workbench repo locally, build the kjar and add it as dependency.

Firstly, we need the location of the repo. That can be easily found in the workbench interface.

Project settings in drools workbench

We can clone it with the following command. Default user and pass are admin as setup in the previous post.

git clone ssh://admin@0.0.0.0:8001/Test/ImportProducts

Cloning into 'ImportProducts'...
Password authentication
Password:
remote: Total 108 (delta 0), reused 108 (delta 0)
Receiving objects: 100% (108/108), 13.99 KiB | 4.66 MiB/s, done.
Resolving deltas: 100% (39/39), done.

Next is to build the maven project.

➜  ImportProducts git:(master) mvn clean install
[INFO] Scanning for projects...
[INFO]
[INFO] ----------------------< com.test:ImportProducts >-----------------------
[INFO] Building ImportProducts 1.0.0-SNAPSHOT
[INFO] --------------------------------[ kjar ]--------------------------------
[INFO]
[INFO] --- maven-clean-plugin:2.5:clean (default-clean) @ ImportProducts ---
[INFO]
[INFO] --- maven-resources-plugin:3.2.0:resources (default-resources) @ ImportProducts ---
[WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] Using 'null' encoding to copy filtered properties files.
[INFO] Copying 7 resources
[INFO]
[INFO] --- maven-compiler-plugin:3.8.1:compile (default-compile) @ ImportProducts ---
[INFO] Changes detected - recompiling the module!
[WARNING] File encoding has not been set, using platform encoding UTF-8, i.e. build is platform dependent!
[INFO] Compiling 1 source file to /Users/sergiuoltean/fun/ImportProducts/target/classes
[INFO] -------------------------------------------------------------
[ERROR] COMPILATION ERROR :
[INFO] -------------------------------------------------------------
[ERROR] Source option 6 is no longer supported. Use 7 or later.
[ERROR] Target option 6 is no longer supported. Use 7 or later.
[INFO] 2 errors
[INFO] -------------------------------------------------------------
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  3.840 s
[INFO] Finished at: 2021-03-05T13:06:22+02:00
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.1:compile (default-compile) on project ImportProducts: Compilation failure: Compilation failure:
[ERROR] Source option 6 is no longer supported. Use 7 or later.
[ERROR] Target option 6 is no longer supported. Use 7 or later.
[ERROR] -> [Help 1]
[ERROR]
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR]
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoFailureException

As you see it will give you an error. This is due to the way the maven project is configured. Add the following properties to the project’s pom file(depending on your java version).

<properties>
    <maven.compiler.source>11</maven.compiler.source>
    <maven.compiler.target>11</maven.compiler.target>
  </properties>

and the run mvn clean install again. This will fix it. Next step is to add it as a maven dependency to your project.

  <dependency>
      <groupId>com.test</groupId>
      <artifactId>ImportProducts</artifactId>
      <version>1.0.0-SNAPSHOT</version>
    </dependency>

Onto the code now. For this example I’ve chosen Apache Camel. I’ve defined the route

  from("file:src/main/resources/poll?noop=true")
            .unmarshal().csv()
            .process(ruleProcessor)
            .process(resultToFile)
            .end();

which will poll the resources/poll directory for import CVSs. These have the format

Category, Id, Status, Price

The KIE configuration was done

@Configuration
public class AppConfiguration {

  @Bean
  public KieServicesClient kieServicesClient(
          @Value("${KIE_SERVER_REST_ENDPOINT}") String kieRestEndpoint,
          @Value("${KIE_SERVER_USER}") String kieUser,
          @Value("${KIE_SERVER_PASSWORD}") String kiePassword,
          @Value("${EXECUTION_TIMEOUT:600000}") Integer executionTimeout) {
    KieServicesConfiguration conf = KieServicesFactory.newRestConfiguration(kieRestEndpoint, kieUser, kiePassword);
    conf.setMarshallingFormat(MarshallingFormat.JSON);
    conf.setTimeout(executionTimeout);
    return KieServicesFactory.newKieServicesClient(conf);
  }
}

After I read the CSV and map them to facts I need to call the KIE service.

 public void process(Exchange exchange) throws Exception {
    List<List<String>> data = (List<List<String>>) exchange.getIn().getBody();
    List<ImportProduct> products = data.stream().map(this::mapImportProducts).collect(Collectors.toList());
    if (!CollectionUtils.isEmpty(products)) {
      List<? super Object> facts = new ArrayList<>(products);
      final var containerId = "ImportProducts_1.0.0-SNAPSHOT";
      final var mySession = "mySession";
      exchange.getIn().setBody(kieService.executeCommands(containerId, facts, mySession));
    } else {
      log.info("Nothing to process");
    }
  }

The containerId is the identifier of the container deployed in the KIE server. Currently it’s the name of the maven module(by default), but you can give it an alias for easier reference. Now in the previous example with rest I chose to use a stateful session which holds the data across multiple invocations. In most of the cases you don’t need this type of behaviour. In this second example I’ve chosen a stateless session which needs a name. That’s what mySession represents. The state will not be kept across multiple invocations.

The next interesting bit it’s related on how to invoke the KIE server.

public List<T> executeCommands(String containerId, List<?> facts, String sessionName) {
    String outIdentifier = "out";
    RuleServicesClient rulesClient = kieServicesClient.getServicesClient(RuleServicesClient.class);

    Command<?> batchCommand = prepareCommands(facts, sessionName, outIdentifier);
    ServiceResponse<ExecutionResults> executeResponse = rulesClient
            .executeCommandsWithResults(containerId, batchCommand);

    if (executeResponse.getType() == ResponseType.SUCCESS) {
      log.debug("Commands executed with success!");
      ExecutionResults result = executeResponse.getResult();
      return (List<T>) result.getValue(outIdentifier);
    } else {
      throw new RuntimeException("Execution failed");
    }
  }

  private Command<?> prepareCommands(List<?> facts, String sessionName, String outIdentifier) {
    KieCommands commandsFactory = KieServices.Factory.get().getCommands();
    List<Command> commands = facts.stream().map(commandsFactory::newInsert).collect(Collectors.toList());
    commands.add(commandsFactory.newFireAllRules());
    ObjectFilter factsFilter = new ClassObjectFilter(resultClass);
    commands.add(commandsFactory.newGetObjects(factsFilter, outIdentifier));
    return commandsFactory.newBatchExecution(commands, sessionName);
  }

prepareCommands method prepares the facts as commands. Then we add a fire rule command and a getObjects command(which uses a filter to return only the objects we’re interested in). All these commands are bundled into a batch.

executeCommands method executes the batch against the server and uses the identifier to get the result. After this it will be written as CSV file.

Considering the following import file

Games,1,OK,2.0
Games,2,NOT_OK,1.0
Books,3,NOT_OK,2.5
Books,4,OK,4.0
Books,5,OK,5.0
Games,6,NOT_OK,5.5
Games,7,OK,1.1

By running the rule engine on these facts we get

category,id,price,status
Books,4,4.0,OK
Games,1,2.0,OK
Games,7,1.1,OK
Books,5,5.0,OK

Also the second rule(category price average) is triggered but since it only sys outs the result to the console we need to check the workbench logs for the results.

13:07:44,087 INFO  [stdout] (default task-1) -------------------
13:26:59,175 INFO  [stdout] (default task-1) Category Books
13:26:59,177 INFO  [stdout] (default task-1) Price 4.5
13:26:59,179 INFO  [stdout] (default task-1) -------------------
13:26:59,180 INFO  [stdout] (default task-1) Category Games
13:26:59,181 INFO  [stdout] (default task-1) Price 1.55
13:26:59,182 INFO  [stdout] (default task-1) -------------------

I’ve mentioned that I use a stateless session. This can be setup in the workbench. A session can be created from a KIE base.

Kie base in workbench

A KIE base can contain multiple sessions.

Stateless session in workbench

Full example is available here.

10 thoughts on “Drools – Calling the KIE Server”

  1. It’s passed as environment variable.

    KIE_SERVER_REST_ENDPOINT=http://localhost:8180/kie-server/services/rest/server
    KIE_SERVER_USER=kieserver
    KIE_SERVER_PASSWORD=kieserver1!

    Like

  2. I imported your project in drools workbench and deployed it to the KIE server. I can see the container on the deploy page. I have a simple spring boot application which also has ImportProduct class. I have used rest controller to call the service on tomcat server. I am passing json data in the request body while calling the service. I am getting SUCCESS response but the rules don’t seem have any effect on the data. For example, if the status != ‘OK’, it should delete that object but it’s not getting deleted. The curl POST request gives back all data that I passed.

    Liked by 1 person

  3. Did you use the out_identifier

    “get-objects”: {
    “out-identifier”: “import”
    }

    Also can you confirm that the rule was fired.

    “execution-results”: {
    “results”: [
    {
    “value”: 2,
    “key”: “firedActivations”
    }
    ],
    “facts”: []

    Also I recommend to add some logs to the rule.

    Like

    1. I didn’t use out identifier. I think the rule capability is not enabled as shown in the log below:
      17:55:35.116 [main] DEBUG org.kie.server.client.impl.KieServicesClientImpl – Supported capabilities by the server: [KieServer, BRM, BPM, CaseMgmt, BPM-UI, BRP, DMN, Swagger]

      ‘{
      “type” : “SUCCESS”,
      “msg” : “Container ImportProducts_1.0.1-LATEST successfully called.”,
      “result” : {
      “execution-results” : {
      “results” : [ {
      “value” : [{“com.test.importproducts.ImportProduct”:{
      “id” : “1”,
      “category” : “Grocery – Milk”,
      “status” : “OK”,
      “price” : 25.0
      }},{“com.test.importproducts.ImportProduct”:{
      “id” : “2”,
      “category” : “Fashion – Trouser”,
      “status” : “NOT_OK”,
      “price” : 1300.0
      }},{“com.test.importproducts.ImportProduct”:{
      “id” : “3”,
      “category” : “Grocery – Wheat”,
      “status” : “OK”,
      “price” : 425.0
      }},{“com.test.importproducts.ImportProduct”:{
      “id” : “4”,
      “category” : “Grocery – Dairy Milk Chocolate”,
      “status” : “OK”,
      “price” : 100.0
      }}],
      “key” : “ImportProduct”
      } ],
      “facts” : [ {
      “value” : {“org.drools.core.common.DefaultFactHandle”:{
      “external-form” : “0:1:1934721322:-270083718:1:DEFAULT:NON_TRAIT:java.util.ArrayList”
      }},
      “key” : “ImportProduct”
      } ]
      }
      }
      }’

      Like

      1. I think the rule is not getting fired, probably because it is not enabled.

        22:11:41.515 [main] DEBUG org.kie.server.client.impl.AbstractKieServicesClientImpl – About to deserialize content:
        ‘{
        “type” : “SUCCESS”,
        “msg” : “Container ImportProducts_1.0.1-LATEST successfully called.”,
        “result” : {
        “execution-results” : {
        “results” : [ {
        “value” : [{“com.test.importproducts.ImportProduct”:{
        “id” : “1”,
        “category” : “Grocery – Milk”,
        “status” : “OK”,
        “price” : 25.0
        }},{“com.test.importproducts.ImportProduct”:{
        “id” : “2”,
        “category” : “Fashion – Trouser”,
        “status” : “NOT_OK”,
        “price” : 1300.0
        }},{“com.test.importproducts.ImportProduct”:{
        “id” : “3”,
        “category” : “Grocery – Wheat”,
        “status” : “OK”,
        “price” : 425.0
        }},{“com.test.importproducts.ImportProduct”:{
        “id” : “4”,
        “category” : “Grocery – Dairy Milk Chocolate”,
        “status” : “OK”,
        “price” : 100.0
        }}],
        “key” : “ImportProduct”
        } ],
        “facts” : [ {
        “value” : {“org.drools.core.common.DefaultFactHandle”:{
        “external-form” : “0:3:459769708:-992461270:3:DEFAULT:NON_TRAIT:java.util.ArrayList”
        }},
        “key” : “ImportProduct”
        } ]
        }
        }
        }’

        Like

  4. Hi Sergui,

    I fixed the issue. I am trying to sync the github repository so that drools workbench can fetch the updated objects from the repository synced up. I think we can do that by creating a settings.xml file in which we can specify the github repository. I searched a lot on Google but couldn’t find a page where there is a clear way to do that. Please help me achieve that 🙂

    Thank you !

    Like

  5. ObjectFilter factsFilter = new ClassObjectFilter(resultClass);
    cmds.add(commands.newGetObjects(factsFilter, “Vendor”));

    getting factsFilter as null at runtime

    Like

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.