czwartek, 3 grudnia 2015

KIE Server: Extend existing server capability with extra REST endpoint

First and most likely the most frequently required extension to KIE Server is to extend REST api of already available extension - Drools or jBPM. There are few simple steps that needs to be done to provide extra endpoints in KIE Server.

Our use case

We are going to extend Drools extension with additional endpoint that will do very simple thing - expose single endpoint that will accept list of facts to be inserted and automatically call fire all rules and retrieve all objects from ksession.
Endpoint will be bound to following path:
server/containers/instances/{id}/ksession/{ksessionId}

where:
  • id is container identifier
  • ksessionId is name of the ksession within container to be used

Before you start create empty maven project (packaging jar) with following dependencies:

 
 <properties>
    <version.org.kie>6.4.0-SNAPSHOT</version.org.kie>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.kie</groupId>
      <artifactId>kie-api</artifactId>
      <version>${version.org.kie}</version>
    </dependency>
    <dependency>
      <groupId>org.kie</groupId>
      <artifactId>kie-internal</artifactId>
      <version>${version.org.kie}</version>
    </dependency>

    <dependency>
      <groupId>org.kie.server</groupId>
      <artifactId>kie-server-api</artifactId>
      <version>${version.org.kie}</version>
    </dependency>
    <dependency>
      <groupId>org.kie.server</groupId>
      <artifactId>kie-server-services-common</artifactId>
      <version>${version.org.kie}</version>
    </dependency>
    <dependency>
      <groupId>org.kie.server</groupId>
      <artifactId>kie-server-services-drools</artifactId>
      <version>${version.org.kie}</version>
    </dependency>
    
    <dependency>
      <groupId>org.kie.server</groupId>
      <artifactId>kie-server-rest-common</artifactId>
      <version>${version.org.kie}</version>
    </dependency>

    <dependency>
      <groupId>org.drools</groupId>
      <artifactId>drools-core</artifactId>
      <version>${version.org.kie}</version>
    </dependency>
    <dependency>
      <groupId>org.drools</groupId>
      <artifactId>drools-compiler</artifactId>
      <version>${version.org.kie}</version>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-api</artifactId>
      <version>1.7.2</version>
    </dependency>

  </dependencies>

Implement KieServerApplicationComponentsService

First step is to implement org.kie.server.services.api.KieServerApplicationComponentsService that is responsible for delivering REST endpoints (aka resources) to the KIE Server infrastructure that will be then deployed on application start. This interface is very simple and has only one method:

Collection<Object> getAppComponents(String extension, 
                                    SupportedTransports type, Object... services)

this method is then invoked by KIE Server when booting up and should return all resources that REST container should deploy.

This method implementation should take into consideration following:

  • it is called by all extensions and thus it provides extension name so custom implementations can decide if this extension is for it or not
  • supported type - either REST or JMS - in our case it will be REST only
  • services - dedicated services to given extensions that can be then used as part of custom extension - usually these are engine services
Here is a sample implementation that uses Drools extension as base (and by that its services)

 
public class CusomtDroolsKieServerApplicationComponentsService implements KieServerApplicationComponentsService {

    private static final String OWNER_EXTENSION = "Drools";
    
    public Collection<Object> getAppComponents(String extension, SupportedTransports type, Object... services) {
        // skip calls from other than owning extension
        if ( !OWNER_EXTENSION.equals(extension) ) {
            return Collections.emptyList();
        }
        
        RulesExecutionService rulesExecutionService = null;
        KieServerRegistry context = null;
       
        for( Object object : services ) { 
            if( RulesExecutionService.class.isAssignableFrom(object.getClass()) ) { 
                rulesExecutionService = (RulesExecutionService) object;
                continue;
            } else if( KieServerRegistry.class.isAssignableFrom(object.getClass()) ) {
                context = (KieServerRegistry) object;
                continue;
            }
        }
        
        List<Object> components = new ArrayList<Object>(1);
        if( SupportedTransports.REST.equals(type) ) {
            components.add(new CustomResource(rulesExecutionService, context));
        }
        
        return components;
    }

}


So what can be seen here is that it only reacts to Drools extension services and others are ignored. Next it will select RulesExecutionService and KieServerRegistry from available services. Last will create new CustomResource (implemented in next step) and returns it as part of the components list.

Implement REST resource

Next step is to implement custom REST resource that will be used by KIE Server to provide additional functionality. Here we do a simple, single method resource that:
  • uses POST http method
  • expects following data to be given:
    • container id as path argument
    • ksession id as path argument
    • list of facts as message payload 
  • supports all KIE Server data formats:
    • XML - JAXB
    • JSON
    • XML - Xstream
It will then unmarshal the payload into actual List<?> and create for each item in the list new InsertCommand. These inserts will be then followed by FireAllRules and GetObject commands. All will be then added as commands of BatchExecutionCommand and used to call rule engine. As simple as that. It is already available on KIE Server out of the box but requires complete setup of BatchExecutionCommand to be done on client side. Not that it's not possible but this extension is tailored one for simple pattern :
insert -> evaluate -> return

Here is how the simple implementation could look like:
 
@Path("server/containers/instances/{id}/ksession")
public class CustomResource {

    private static final Logger logger = LoggerFactory.getLogger(CustomResource.class);
    
    private KieCommands commandsFactory = KieServices.Factory.get().getCommands();

    private RulesExecutionService rulesExecutionService;
    private KieServerRegistry registry;

    public CustomResource() {

    }

    public CustomResource(RulesExecutionService rulesExecutionService, KieServerRegistry registry) {
        this.rulesExecutionService = rulesExecutionService;
        this.registry = registry;
    }
    
    @POST
    @Path("/{ksessionId}")
    @Consumes({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
    @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
    public Response insertFireReturn(@Context HttpHeaders headers, 
            @PathParam("id") String id, 
            @PathParam("ksessionId") String ksessionId, 
            String cmdPayload) {

        Variant v = getVariant(headers);
        String contentType = getContentType(headers);
        
        MarshallingFormat format = MarshallingFormat.fromType(contentType);
        if (format == null) {
            format = MarshallingFormat.valueOf(contentType);
        }
        try {    
            KieContainerInstance kci = registry.getContainer(id);
            
            Marshaller marshaller = kci.getMarshaller(format);
            
            List<?> listOfFacts = marshaller.unmarshall(cmdPayload, List.class);
            
            List<Command<?>> commands = new ArrayList<Command<?>>();
            BatchExecutionCommand executionCommand = commandsFactory.newBatchExecution(commands, ksessionId);
            
            for (Object fact : listOfFacts) {
                commands.add(commandsFactory.newInsert(fact, fact.toString()));
            }
            commands.add(commandsFactory.newFireAllRules());
            commands.add(commandsFactory.newGetObjects());
                
            ExecutionResults results = rulesExecutionService.call(kci, executionCommand);
                    
            String result = marshaller.marshall(results);
            
            
            logger.debug("Returning OK response with content '{}'", result);
            return createResponse(result, v, Response.Status.OK);
        } catch (Exception e) {
            // in case marshalling failed return the call container response to keep backward compatibility
            String response = "Execution failed with error : " + e.getMessage();
            logger.debug("Returning Failure response with content '{}'", response);
            return createResponse(response, v, Response.Status.INTERNAL_SERVER_ERROR);
        }

    }
}


Make it discoverable

Once we have all that needs to be implemented, it's time to make it discoverable so KIE Server can find and register this extension on runtime. Since KIE Server is based on Java SE ServiceLoader mechanism we need to add one file into our extension jar file:

META-INF/services/org.kie.server.services.api.KieServerApplicationComponentsService

And the content of this file is a single line that represents fully qualified class name of our custom implementation of  KieServerApplicationComponentsService.


Last step is to build this project (which will result in jar file) and copy the result into:
 kie-server.war/WEB-INF/lib

And that's all that is needed. Start KIE Server and then you can start interacting with your new REST endpoint that relies on Drools extension.

Usage example

Clone this repository and build the kie-server-demo project. Once you build it you will be able to deploy it to KIE Server (either directly using KIE Server management REST api) or via KIE workbench controller.

Once deployed you can use following to invoke new endpoint:
URL: 
http://localhost:8080/kie-server/services/rest/server/containers/instances/demo/ksession/defaultKieSession

HTTP Method: POST
Headers:
Content-Type: application/json
Accept: application/json

Message payload:
[
{
  "org.jbpm.test.Person":{
     "name":"john",
     "age":25}
   },
  {
    "org.jbpm.test.Person":{
       "name":"mary",
       "age":22}
   }
]

A simple list with two items representing people, execute it and you should see following in server log:
13:37:20,347 INFO  [stdout] (default task-24) Hello mary
13:37:20,348 INFO  [stdout] (default task-24) Hello john

And the response should contain objects retrieved after rule evaluation where each Person object has:
  • address set to 'JBoss Community'
  • registered flag set to true

With this sample use case we illustrated how easy it is to extend REST api of KIE Server. Complete code for this extension can be found here.

31 komentarzy:

  1. Finally got around experimenting with your code and this is just what i need to get started integrating Apache Thrift with KIE Server. Thank you for the advice.
    My method was building ie-server-parent 6.4.0-SNAPSHOT KIE :: Execution Server :: Wars :: Distribution Wars (Lost of WARNINGS) and adding the extension JAR to the kie-server-6.4.0-SNAPSHOT-ee7.war as described, before deploying per GUI to Wildfly 8.2.1..

    OdpowiedzUsuń
  2. Do you know how to hook in MessageBodyreaders and Writers in the internal RestEasy (2.3.10.FINAL). I created a modified war overlay that, if deployed, does not correctly register the reader/writer on the right Resteasy Instance. After a Wildfly restart everything works fine.

    OdpowiedzUsuń
  3. Solved the problem by adding a file named javax.ws.rs.ext.Providers to META-INF/services and declaring the fully qualified names of your provides in it. Take a look at RegisterBuiltin.registerProviders(ResteasyProviderFactory factory).

    OdpowiedzUsuń
  4. Maurice, great to hear that you have solved the issue. Missed these comments in my mail box so apologies for not replying. Anyway would be really good if you could write a blog about your work and cross link it here. Sounds like really nice piece of code!

    OdpowiedzUsuń
  5. Hello there,

    thanks a lot for this great post!
    I am using 6.3.0.Final and cannot find a way to deploy the builded project using the KIE workbench controller.
    Is there any other way of deploying than copying into the kie-server.war?

    Cheers!

    OdpowiedzUsuń
    Odpowiedzi
    1. you can either use controller (Deploy -> Rules deployments) perspective in workbench or use directly REST api of kie server to deploy projects. See my previous posts about KIE Server in general that describes that in details.

      Usuń
  6. hi,can you share a full demo in github?

    OdpowiedzUsuń
  7. Hi Maciej,

    We are able to expose REST extension with the help of this blog. But we are not able to use CDI with it e.g. we are having a service call in which we need to inject a stateless bean using CDI. This is not working. Can you please provide any pointer to achieve this.

    Regards
    Sandip Desale

    OdpowiedzUsuń
    Odpowiedzi
    1. you need to provide more details - what's not working, what is the service call, where do you want to inject the CDI bean etc.

      Usuń

  8. Hi Maciej,

    We have written a custom REST service as suggested in your blog. In this new custom REST service, we need to inject CDI Bean/pojo which will handle business logic related task.

    Sample code Service Code
    ------------------------
    @Inject
    CDIObject obj;

    @POST
    @Path("/{param}")
    public Response getMessage(@PathParam("param") String message) {

    String msg = getClass()+"getMessage | msg : "+ message;
    logger.debug(msg);
    System.out.println(msg);

    if(null != obj) {
    System.out.println(obj.toString());
    obj.setX("Modified");
    obj.setY(100);
    System.out.println(obj.toString());
    msg=obj.toString();
    }
    else{
    System.out.println("CDIObject is null.");
    msg="CDIObject is null.";
    }
    return Response.status(200).entity(msg).build();
    }


    Above service is registered with CusomtDroolsKieServerApplicationComponentsService. When I deploy this on KieServer, CDIObject is not getting injected in this service. CDIObject is just a new POJO we have created. Whenever we execute above service, response print "CDIObject is null." message.

    Can you please let me know how to make custom business entities available to this new REST service.

    Thanks & Regards
    Sandip Desale

    OdpowiedzUsuń
    Odpowiedzi
    1. what application server do you use? maybe is a matter of missing beans.xml file in kie-server.war/WEB-INF directory as by default kie server does not use CDI.

      Usuń
  9. We are using Jboss EAP v6.x. Shall we add beans.xml in WEB-INF?

    OdpowiedzUsuń
    Odpowiedzi
    1. We are using CDI as for dependency injection we do not really wants to use Spring.

      Usuń
    2. no-one said anything about spring :)

      Usuń
    3. Thanks. Let me add it and check out.

      Usuń
  10. Hi Maciej,

    Thanks for guiding us on this rest extension. We are able to develop and test this on kie server.

    Can we deploy JAR containing above custom REST extension as a jboss module or as a KJar? We have 2 jars containing custom REST extensions. As a part of deployment, we do not want to deploy it in lib folder as sub-sequent deployment will be an issue.
    Can you please let us know how to deploy do it.

    Regards
    Sandip

    OdpowiedzUsuń
    Odpowiedzi
    1. I don't think you could use it from another jboss module without changing the kie server.war itself (you still would have to declare dependency on that module). An alternative option I see is to package your jars with extension and then complete kie server war into an EAR where entire ear would share the same class loader

      Usuń
  11. Thanks Maciej for reply.

    We will also have kJars deployment on the same Kieserver instance. If we bundle Kieserver into an EAR then how to deploy kJar. I hope it will not change. Also a new kJar version deployment on KieServer remains same. But we may need to bring down the server for subsequent deployments.

    Can you please guide us how to resolve this.

    Also we need to return values from Process instead of just processinstance id. Can you put some light on it as well.

    Thanks & Regards
    Sandip

    OdpowiedzUsuń
    Odpowiedzi
    1. there is no difference whatsoever when it comes to deploying kjars, it will use exactly same mechanism.
      kjar deployments do not require kie server to be restarted, it is completely dynamic in nature.

      start process will only return process instance id as this is the most reliable way of not providing out od date information. For example if the start process reaches an async activity which will already complete by the time response of the call (start process) reaches the client - that means the information about the process instance and its state is already outdated. To avoid this kind of misleading responses only instance id is returned so client can decide himself if additional info is needed or not.

      Usuń
  12. Hi Maciej,

    Thank you very much for this article. I followed your article, prepared custom extension and tried to deploy it as Kie Server container to local kie server hosted on local WildFly 10.1 but it seems to be stuck at "Creating" stage. I also tried packaging "kie-server-demo" from Github repo and deploying it as kie server container. For that, deployment finished successfully and I saw container up and running but I couldn't reach (POST) to the service endpoint as mentioned in the article. I got 404. I tried both "http://localhost:8080/kie-server/services/rest/server/containers/instances/demo/ksession/defaultKieSession" and "http://localhost:8080/kie-server-6.5.0.Final-ee7/services/rest/server/containers/instances/demo/ksession/defaultKieSession". Appreciate any guidance. Warm Regards, Aung

    OdpowiedzUsuń
    Odpowiedzi
    1. 404 usually means that your extension was not found/registered in kie server - if it was found it should be printed in the logs. So double check if you have all the /services files created for automatic discovery to work.

      Usuń
    2. Appreciate your time and guidance, Maciej.

      My maven project has: extension class, resource class, META-INF/services/org.kie.server.services.api.KieServerApplicationComponentsService file for discovery, pom.xml with dependencies details from the article, and I even added kmodule.xml under the resources/META-INF folder. I even use the same maven artifact versions as those in the article. The only thing I modified in dependency area is exclusion of jboss-logging as Eclipse is complaining about it.

      After packing project with the command - "mvn clean install -DperformRelease=true -DcreateChecksum=true", I see new package created in local maven repository ("~/.m2/repository/xxxxxx"). Is there anything that I missed, Maciej?

      Warm regards, Aung

      Usuń
    3. have you copied that jar into kie-server.war/WEB-INF/lib?

      Usuń
    4. I did...Using jar command with parameter "uvf", I added jar file into kie server's war file and deployed into WildFly but, somehow, it doesn't work.

      Usuń
    5. difficult to trace it without seeing the code base actually so if you could share your jar file with extension I could try to figure it out.
      Note that you can deploy exploded archive on WildFly no need to repackage it. Just create folder kie-server.war in standalone/deployments and put the kie-server.war content into that folder. Then you can just copy your jar into WEB-INF/lib and that's it

      Usuń
    6. That's very kind of you, Maciej.

      I shared my extension project codes in Github (https://github.com/pyitphyoaung/simple-kie-server-extension).

      Just to repeat what I tried:
      - Got WildFly 10.1 server installed and run as standalone.
      - Expand Kie Server war file (ee7) and put it inside WildFly's "deployments" folder. It got deployed successfully.
      - Built test maven project and put jar file inside WEB-INF/lib folder.
      - Tried http POST call in the article but it failed.

      Appreciate your kind help.

      Usuń
    7. I just deployed your extension and it does work:

      09:24:28,846 INFO [org.jboss.resteasy.resteasy_jaxrs.i18n] (ServerService Thread Pool -- 67) RESTEASY002220: Adding singleton resource com.example.SimpleResource from Application class org.kie.server.remote.rest.common.KieServerApplication

      and when calling
      POST: http://localhost:8230/kie-server/services/rest/server/containers/instances/evaluation_1.0/ksession/defaultKieSession

      with simple payload:
      [
      "test"
      ]

      it returns:

      {
      "results" : [ {
      "value" : "test",
      "key" : "test"
      } ],
      "facts" : [ {
      "value" : {"org.drools.core.common.DefaultFactHandle":{
      "external-form" : "0:2:1721794551:3556498:2:DEFAULT:NON_TRAIT:java.lang.String"
      }},
      "key" : "test"
      } ]
      }

      Make sure that KIE Server does not have drools extension disabled as then it won't load your extensions for REST interface.

      Usuń
    8. Thank you so much, Maciej. I'll try it again.

      Have a nice week!

      Usuń
  13. What is the recommended way to add new classes to the marshaller context? My extension has a few classes which I wanted to be part of the marshaller, but I can't find an easy way to make it part of the context - I see some classes are hardcoded.

    OdpowiedzUsuń
    Odpowiedzi
    1. if you are on v7 then you can directly add it to the KieServerRegistry that most likely you already have in your extension https://github.com/kiegroup/droolsjbpm-integration/blob/7.0.x/kie-server-parent/kie-server-services/kie-server-services-common/src/main/java/org/kie/server/services/api/KieServerRegistry.java#L71

      If you are on 6.x and use jbpm extension then you can take advantage of deployment descriptor and create on on server level and declare remoteable-classes in it which will make sure to add them to all containers deployed to kie server.

      And last option is to add them same way as other extensions to it - directly to KieContainerInstance - https://github.com/kiegroup/droolsjbpm-integration/blob/6.5.x/kie-server-parent/kie-server-services/kie-server-services-jbpm/src/main/java/org/kie/server/services/jbpm/JbpmKieServerExtension.java#L350

      Usuń