2.3. Advanced Migration

2.3.1. REST API Migration

As you know, there are two ways you can use Red Hat JBoss BRMS/JBoss BPM Suite: in an embedded mode or in a remote mode. In the embedded mode, you package the runtime libraries with your application and execute the BPMN processes in the same JVM. In the remote mode, the business assets are created, deployed and executed in the same server, but they are accessed via a remote client application using the REST or the JMS API.
Both of these modes presents different challenges when it comes to the migration. In this section we will focus on the migration for the remote usage of JBoss BPM Suite with the help of a REST example.

REST API Example

In this example, let's assume that you want to:
- start a process which is already deployed in the JBoss BPM Suite server.
- pass some parameters to this process during its creation.
The client side for JBoss BRMS 5 can be created using the following code:
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.NameValuePair;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity;
import org.apache.commons.httpclient.methods.multipart.Part;
import org.apache.commons.httpclient.methods.multipart.StringPart;
import org.jboss.bpm.console.client.model.ProcessDefinitionRef;
import org.jboss.bpm.console.client.model.ProcessDefinitionRefWrapper;
import org.jboss.bpm.console.client.model.ProcessInstanceRef;
 
 
 public class RestClientStartWithParam {
    private static final String BASE_URL = "http://localhost:8080/business-central-server/rs/";
    private static final String AUTH_URL = BASE_URL + "identity/secure/j_security_check";
    private final String username;
    private final String password;

    private static final String PROCESS_ID = "defaultPackage.hello";

    public RestClientStartWithParam(final String u, final String p) {
        this.username = u;
        this.password = p;
    }

    public static void main(String[] args) throws Exception {

        RestClientStartWithParam client = new RestClientStartWithParam("admin", "admin");

        // get process definitions
        ProcessDefinitionRefWrapper processDefinitionWrapper = client.getProcessDefinitions(client);

        // pick up "com.sample.bpmn.hello"
        ProcessDefinitionRef definitionRef = null;
        for (ProcessDefinitionRef processDefinitionRef : processDefinitionWrapper.getDefinitions()) {
            if (processDefinitionRef.getId().equals(PROCESS_ID)) {
                definitionRef = processDefinitionRef;
                break;
            }
        }
        if (definitionRef == null) {
            System.out.println(PROCESS_ID + " doesn't exist");
            return;
        }

        // start a process instance with parameters
        Map<String, String> params = new HashMap<String, String>();
        params.put("employee", "thomas");
        params.put("reason", "theReason");
        client.startProcessWithParameters(client, definitionRef, params);
    }
    
        private void startProcessWithParameters(RestClientStartWithParam client, ProcessDefinitionRef def,
            Map<String, String> params) throws Exception {
        String newInstanceUrl = BASE_URL + "form/process/" + def.getId() + "/complete";
        String dataFromService = client.getDataFromService(newInstanceUrl, "POST", params, true);

        System.out.println(dataFromService); 
    }    
    
    // get DataFromService method can be implemented like this
    
    private String getDataFromService(String urlpath, String method, Map<String, String> params, boolean multipart)
            throws Exception {
        HttpClient httpclient = new HttpClient();

        HttpMethod theMethod = null;
        StringBuffer sb = new StringBuffer();

        if ("GET".equalsIgnoreCase(method)) {
            theMethod = new GetMethod(urlpath);
        } else if ("POST".equalsIgnoreCase(method)) {
            theMethod = new PostMethod(urlpath);

            if (params != null) {

                if (multipart) {
                    List<Part> parts = new ArrayList<Part>();
                    for (String key : params.keySet()) {
                        StringPart stringPart = new StringPart(key, params.get(key));
                        stringPart.setCharSet("UTF-8");
                        parts.add(stringPart);
                    }
                    ((PostMethod) theMethod).setRequestEntity(new MultipartRequestEntity(parts.toArray(new Part[0]),
                            theMethod.getParams()));
                } else {

                    List<NameValuePair> NameValuePairList = new ArrayList<NameValuePair>();
                    for (String key : params.keySet()) {
                        NameValuePairList.add(new NameValuePair(key, params.get(key)));
                    }
                    ((PostMethod) theMethod).setRequestBody(NameValuePairList.toArray(new NameValuePair[0]));

                }
            }

        }

        if (username != null && password != null) {

            try {
                int result = httpclient.executeMethod(theMethod);
                System.out.println("result = " + result);
                // System.out.println(theMethod.getResponseBodyAsString());
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                theMethod.releaseConnection();
            }
            PostMethod authMethod = new PostMethod(AUTH_URL);
            NameValuePair[] data = { new NameValuePair("j_username", username),
                    new NameValuePair("j_password", password) };
            authMethod.setRequestBody(data);
            try {
                int result = httpclient.executeMethod(authMethod);
                System.out.println("result = " + result);
                // System.out.println(theMethod.getResponseBodyAsString());
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                authMethod.releaseConnection();
            }
        }

        try {
            int result = httpclient.executeMethod(theMethod);
            System.out.println("result = " + result);
            sb.append(theMethod.getResponseBodyAsString());
            String rawResult = sb.toString();
            return rawResult;

        } catch (Exception e) {
            throw e;
        } finally {
            theMethod.releaseConnection();
        }
    }
    

The JBoss BRMS 5 endpoints are documented here and here.
As you can see, even this very simple example looks rather complex when implemented. The reason for this is partially that there is no native client for JBoss BRMS 5 server. You can however choose the optional web client - Apache HttpClient, RestEasy or even use just plain java.net libraries. This applies in JBoss BPM Suite/BRMS 6 as well - you can still choose the web client - however, there is also a native java client provided for remote communication with version 6 which is much simpler to use.

Migrate to JBoss BRMS/JBoss BPM Suite 6

So let's migrate the same use case to JBoss BPM Suite 6:
- process is already deployed in the JBoss BPM Suite server
- we want to start it with some parameters
- this time, there are some human tasks in this process, so we want to complete those.
All of the available REST endpoints for JBoss BRMS/JBoss BPM Suite are documented here. The code for the requirements above in the standalone application will look like this:
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.kie.api.runtime.KieSession;
import org.kie.api.task.TaskService;
import org.kie.api.task.model.TaskSummary;
import org.kie.services.client.api.RemoteRestRuntimeEngineFactory;
import org.kie.services.client.api.command.RemoteRuntimeEngine;
 
 public class Main {

	public static void main(String[] args) throws MalformedURLException {

		/*
		 * Set the parameters according to your installation
		 */

		String APP_URL = "http://localhost:8080/business-central/";

		URL url = new URL(APP_URL);
		String USER = "anton";
		String PASSWORD = "password1!";

		RemoteRestRuntimeEngineFactory factory = RemoteRestRuntimeEngineFactory.newBuilder()
				.addDeploymentId("org.redhat.gss:remote-test-project:3.0").addUrl(url).addUserName(USER)
				.addPassword(PASSWORD).build(); 

		RemoteRuntimeEngine engine = factory.newRuntimeEngine();
		KieSession kSession = engine.getKieSession(); 
		TaskService taskService = engine.getTaskService();
		
		 // start a process instance with parameters
     Map<String, Object> params = new HashMap<String, Object>();
     params.put("employee", "thomas");
     params.put("reason", "theReason");
     kSession.startProcess("com.sample", params);

     taskService.claim(taskSummaryList.get(0).getId(), "anton");
     taskService.start(taskSummaryList.get(0).getId(), "anton");
     taskService.complete(taskSummaryList.get(0).getId(), "anton", null); // not passing any data = null
  }
}
As you can see, this example is much more simple and readable than the one for JBoss BRMS 5. The RemoteRuntimeEngine gives us direct access to the TaskService / KieSession and AuditLogService API.
However, it is still possible to use your arbitrary Java web client and achieve the same scenario by sending GET/POST requests to the appropriate endpoints.

Note

While the basic functionality is provided by both APIs - JBoss BRMS 5 and JBoss BRMS/JBoss BPM Suite 6, (starting the process, completing the tasks and so on) not all endpoints from BRMS 5 have their replacement in JBoss BRMS/JBoss BPM Suite 6. If in doubt, consult the corresponding documentation of the REST API.

2.3.2. KnowledgeAgent to KieScanner Migration

KnowledgeAgent is a component of JBoss BRMS 5 which allows you to obtain Knowledge Bases dynamically as it gets updated. If you correctly configure an instance of KnowledgeAgent and then you try to obtain the KnowledgeBase from the agent, you will be able to receive the latest version of the KnowledgeBase including updated resources - whether it is a freshly built package (*.PKG) in Business Central or BPMN process definition updated via the Eclipse designer tool.
See a working example of how this works in version 5 here: KnowlegeAgent Example.
In JBoss BRMS and JBoss BPM Suite 6 it is also possible to obtain KieBase (instead of KnowledgeBase) dynamically as it gets updated. However, the migration is not so straightforward, because of a few things:
  • In JBoss BRMS 5, the native storage for packages was Guvnor - which used JackRabbit repository underneath. You could also point to a single resource (drl, bpmn..) with any valid URL (i.e. file://, http://, ...).
  • The API is completely different as there is no direct mapping between KnowledgeAgent API in JBoss BRMS/JBoss BPM Suite 6.
The component which replaces the KnowledgeAgent in BRMS 6 is called KieScanner and therefore you need to include kie-ci library on classspath if you want to use it.
To see an example of how this works in version 6: KieScanner Example.
In version 6, you no longer refer to *.PKG files or specific business resources such as drl, bpmn. Instead you configure your KieScanner with a specific KJAR, which is a Maven artifact including your resources, identified by GAV. KieScanner uses the Maven Repository to figure out where to look for these built KJARs. If not specified otherwise, it will look into your local Maven repository (by default stored under ~/.m2/ directory on your filesystem).
A typical scenario will be where you set GAV so it identifies projects created in Business Central. KieScanner is now bound to this project, and once you make changes to this project in Business Central and build the project, it's latest build will be stored into the local Maven repository (this is the default). KieScanner scans the local Maven repository and picks up the changes. If you want to configure KieScanner in a way that it scans other repositories besides your local one you can do so by setting a system property: kie.maven.settings.custom which can point to the custom settings.xml (a standard Maven configuration file where you include all repositories which should be taken into consideration).
KieScanner invokes Maven under the hood for artifact lookup by following known Maven conventions and rules. For example:
  • If the remote repository requires authentication, you need to configure this authentication in a Maven way by updating settings.xml.
  • If you point your KieScanner to a KJar with GAV org.my:project:1.0 your KieBase will never get updated even if you build the project with the same GAV again. This is because Maven will resolve the artifact to a fixed version.
  • If you point your KieScanner to a KJar with GAV org.my:project:1.0-SNAPSHOT your KieBase will get updated for any new build of the project with that GAV - it will be resolved to the LATEST build of that GAV, identified by the timestamp.
A KCS article which discuss various scenarios and configurations is available here: https://access.redhat.com/solutions/710763

2.3.3. Database Migration

The default underlying database in JBoss BPM Suite is an instance of H2. This is fine for most test systems, but production systems are generally based around MySQL, PostgreSQL, Oracle or others databases. This section lists some of the tips and tricks related to databases when migrating from BRMS 5 to JBoss BPM Suite 6.x.
  • Include hbm.xml for PostgreSQL

    If the underlying database on which the migration is being performed is PostgreSQL, you will need to include an additional configuration file, called hbm.xml inside the META-INF directory, next to persistence.xml, with the following contents:
    <?xml version="1.0"?>
    <!DOCTYPE hibernate-mapping PUBLIC
        "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
        "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
    <hibernate-mapping>
      <typedef name="materialized_clob" class="org.hibernate.type.TextType" />
    </hibernate-mapping>
    
    This file instructs Hibernate to use TextType for materialized CLOBS and solves an issue where Hibernate incorrectly tries to interpret the type of a parameter as Long when it should be String based.
  • Avoid ID constraint violations in PostgresSQL and Oracle

    NodeInstanceLog in BRMS 5.2.x doesn't have a sequence generator associated with it and was added to have more consistent behavior with multiple databases. Since not all databases initialize the id sequence correctly on migration it is necessary to update the NODE_INST_LOG_ID_SEQ id manually. The two databases that are affected are: PostgreSQL and Oracle.
    • PostgreSQL: In PostgreSQL two actions are required:
      • Find the id with the biggest value in the NodeInstanceLog table:
        SELECT MAX(id) FROM nodeinstancelog;
      • Restart sequence NODE_INST_LOG_ID_SEQ using the result from the previous step, increased by 1. So if the command in the previous step returned the number 10 you will use 11 in the following command.
        ALTER SEQUENCE node_inst_log_id_seq RESTART WITH 11;
        The reason to increase the result from the first step by 1 is that restarting the sequence sets the is_called flag to false, which tells the system that the sequence was not yet used.
    • Oracle: In Oracle, the following steps are required:
      • Find the id with the biggest value in the NodeInstanceLog table:
        SELECT MAX(id) FROM nodeinstancelog;
      • Execute the following commands in SQL:
        -- Re-create the sequence by first dropping it and then creating a new one.
        DROP SEQUENCE NODE_INST_LOG_ID_SEQ;
        CREATE SEQUENCE NODE_INST_LOG_ID_SEQ START WITH 11 INCREMENT BY 1 NOCYCLE;
        
        -- Increase the sequence (the result must be greater then the result obtained in step 1)
        ALTER SEQUENCE NODE_INST_LOG_ID_SEQ INCREMENT BY 100;