Wednesday, September 05, 2012

Displaying jBPM diagram of the current process

In large enough processes, user might need to see where in the diagram process stopped. Now jBPM console comes with this, but to actually integrate it with front end application, I had to change it slightly. Code base used was from bpm-console. First part is setting up properties with the location of your Guvnor (jbpm.console.properties):

guvnor.protocol=http
guvnor.host=localhost:8080
guvnor.usr=admin
guvnor.pwd=admin
guvnor.subdomain=drools-guvnor
guvnor.connect.timeout=4000
guvnor.read.timeout=4000
bpm.package=/defaultPackage/package
bpm.type=local

Following is the code (you can remove static if you want):

public class DrawGraph {

        /**
  * Gets the console properties.
  *
  * @return the console properties
  */
 public static Properties getConsoleProperties() {
  Properties properties = new Properties();
  try {  
                  properties.load(DrawGraph.class.getResourceAsStream("/jbpm.console.properties"));
  } catch (Throwable t) {
   //do nothing, defaults will be loaded
  }
  return properties;
 }

    /**
     * Gets the bPM active nodes diagram.
     *
     * @param processInstance the process instance
     * @return the bPM active nodes diagram
     */
    public static String getBPMActiveNodesDiagram(ProcessInstance processInstance) {
        try {
            List<ActiveNodeInfo> activeNodeInfoList = getActiveNodeInfo(processInstance);
            String s = "<div style='width:1024px; height:768px; background-color:#ffffff; overflow:auto;'>"
                + "<div id=\"imageContainer\" style=\"position:relative;top:-1;left:-1;\">" + "<img src=\""
                + getImageData(processInstance) + "\" style=\"position:absolute;top:0;left:0\" />";

            for (ActiveNodeInfo activeNodeInfo : activeNodeInfoList) {
                s += "<div class=\"bpm-graphView-activityImage\" style=\"position:absolute;top:"
                    + Math.round((activeNodeInfo.getActiveNode().getY()))
                    + "px;left:"
                    + Math.round((activeNodeInfo.getActiveNode().getX()))
                    + "px;width:50px;height:50px; z-index:1000;background-image: url(/Project/images/play_red_big.png);background-repeat:no-repeat;\"></div>";
            }
            s += "</div>" + "</div>";
            return s;
        } catch (Exception e) {
            logger.error(e);
            return "<div>Could not obtaing process image. Please consult logs to find more details</div>";
        }
    }

    /**
     * Gets the active node info.
     *
     * @param processInstance the process instance
     * @return the active node info
     * @throws Exception the exception
     */
    @SuppressWarnings("unchecked")
        private static List<ActiveNodeInfo> getActiveNodeInfo(ProcessInstance processInstance) throws Exception {
            Properties properties = getProperties();
            String persistenceEnabled = properties.getProperty("persistence.enabled");

            if ("true".equals(persistenceEnabled)) {
                EntityManagerFactory emf = null;
                if (BPMConstants.IS_JUNIT_TEST) {
                    emf = Persistence.createEntityManagerFactory(properties.getProperty("persistence.persistenceunit.name"));
                } else {
                    try {
                        emf = (EntityManagerFactory) new javax.naming.InitialContext().lookup("java:/myFactory");
                    } catch (NamingException e) {
                        e.printStackTrace();
                    }
                }

                EntityManager em = emf.createEntityManager();

                ProcessInstanceLog processInstanceLog = (ProcessInstanceLog) em
                    .createQuery("from ProcessInstanceLog as log where log.processInstanceId = :pid")
                    .setParameter("pid", processInstance.getId()).getSingleResult();

                if (processInstanceLog == null) {
                    throw new IllegalArgumentException("Could not find process instance " + processInstance.getId());
                }
                Map<String, NodeInstanceLog> nodeInstances = new HashMap<String, NodeInstanceLog>();

                for (NodeInstanceLog nodeInstance : (List<NodeInstanceLog>) em
                        .createQuery("from NodeInstanceLog as log where log.processInstanceId = :pid")
                        .setParameter("pid", processInstance.getId()).getResultList()) {
                    if (nodeInstance.getType() == NodeInstanceLog.TYPE_ENTER) {
                        nodeInstances.put(nodeInstance.getNodeInstanceId(), nodeInstance);
                    } else {
                        nodeInstances.remove(nodeInstance.getNodeInstanceId());
                    }
                        }
                if (!nodeInstances.isEmpty()) {
                    List<ActiveNodeInfo> result = new ArrayList<ActiveNodeInfo>();
                    for (NodeInstanceLog nodeInstance : nodeInstances.values()) {
                        boolean found = false;
                        DiagramInfo diagramInfo = getDiagramInfo(processInstance);
                        for (DiagramNodeInfo nodeInfo : diagramInfo.getNodeList()) {
                            if (nodeInfo.getName().equals("id=" + nodeInstance.getNodeId())) {
                                result.add(new ActiveNodeInfo(diagramInfo.getWidth(), diagramInfo.getHeight(), nodeInfo));
                                found = true;
                                break;
                            }
                        }
                        if (!found) {
                            throw new IllegalArgumentException("Could not find info for node " + nodeInstance.getNodeId() + " of process "
                                    + processInstanceLog.getProcessId());
                        }
                    }
                    return result;
                }
            } else {
                throw new IllegalArgumentException("Persistence has to be enabled with logging to be able to print process");
            }
            return null;
        }

    /**
     * Gets the diagram info.
     *
     * @param processInstance the process instance
     * @return the diagram info
     * @throws Exception the exception
     */
    private static DiagramInfo getDiagramInfo(ProcessInstance processInstance) throws Exception {
        DiagramInfo result = new DiagramInfo();
        // TODO: diagram width and height?
        result.setWidth(1024);
        result.setHeight(768);
        List<DiagramNodeInfo> nodeList = new ArrayList<DiagramNodeInfo>();
        //be careful here. If BTM transaction manager is set, kruntime is null?!
        if (processInstance.getProcess() instanceof WorkflowProcess) {
            TDefinitions um = null;
            IBindingFactory bfact = BindingDirectory.getFactory(TDefinitions.class);
            IUnmarshallingContext uctx = bfact.createUnmarshallingContext();
            String source = getProcessSourceContent(processInstance);
            if (source != null) {
                um = (TDefinitions) uctx.unmarshalDocument(new ByteArrayInputStream(source.getBytes()), null);
            }

            addNodesInfo(nodeList, ((WorkflowProcess) processInstance.getProcess()).getNodes(), "id=", um);
        }
        result.setNodeList(nodeList);
        return result;

    }

    /**
     * Gets the process source content.
     *
     * @param packageName the package name
     * @param assetName the asset name
     * @return the process source content
     * @throws IOException 
     */
    private static String getProcessSourceContent(ProcessInstance processInstance) throws IOException {

        if ("local".equalsIgnoreCase(getConsoleProperties().getProperty("bpm.type", "remote"))) {
            return IOUtils.toString(
                    DrawGraph.class.getResourceAsStream("/resources/" + processInstance.getProcessId() + ".bpmn2"), "UTF-8");
        } else {

            String assetSourceURL = getConsoleProperties().getProperty("guvnor.protocol", "http") + "://"
                + getConsoleProperties().getProperty("guvnor.host", "localhost:8080") + "/"
                + getConsoleProperties().getProperty("guvnor.subdomain", "drools-guvnor") + "/rest/packages"
                + getConsoleProperties().getProperty("bpm.package") + "/assets/" + processInstance.getProcessId() + "/source/";

            try {
                InputStream in = getInputStreamForURL(assetSourceURL, "GET");
                StringWriter writer = new StringWriter();
                IOUtils.copy(in, writer);
                return writer.toString();
            } catch (Exception e) {
                logger.error("Error retrieving asset content: " + e.getMessage());
                return "";
            }
        }

    }

    /**
     * Gets the input stream for url.
     *
     * @param urlLocation the url location
     * @param requestMethod the request method
     * @param guvnorUtils the guvnor utils
     * @return the input stream for url
     * @throws Exception the exception
     */
    private static InputStream getInputStreamForURL(String urlLocation, String requestMethod) throws Exception {
        URL url = new URL(urlLocation);
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();

        connection.setRequestMethod(requestMethod);
        connection.setRequestProperty("User-Agent",
                "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; en-US; rv:1.9.2.16) Gecko/20110319 Firefox/3.6.16");
        connection.setRequestProperty("Accept", "text/plain,text/html,application/xhtml+xml,application/xml");
        connection.setRequestProperty("charset", "UTF-8");
        connection.setConnectTimeout(Integer.parseInt(getConsoleProperties().getProperty("guvnor.connect.timeout", "4000")));
        connection.setReadTimeout(Integer.parseInt(getConsoleProperties().getProperty("guvnor.read.timeout", "4000")));
        applyAuth(connection);
        connection.connect();

        BufferedReader sreader = new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8"));
        StringBuilder stringBuilder = new StringBuilder();

        String line = null;
        while ((line = sreader.readLine()) != null) {
            stringBuilder.append(line + "\n");
        }

        return new ByteArrayInputStream(stringBuilder.toString().getBytes("UTF-8"));
    }

    /**
     * Apply auth.
     *
     * @param connection the connection
     * @param guvnorUtils the guvnor utils
     */
    private static void applyAuth(HttpURLConnection connection) {
        String auth = getConsoleProperties().getProperty("guvnor.usr", "admin") + ":"
            + getConsoleProperties().getProperty("guvnor.pwd", "admin");
        connection.setRequestProperty("Authorization", "Basic " + Base64.encodeBase64String(auth.getBytes()));
    }

    /**
     * Adds the nodes info.
     *
     * @param nodeInfos the node infos
     * @param nodes the nodes
     * @param prefix the prefix
     * @param um the um
     */
    private static void addNodesInfo(List<DiagramNodeInfo> nodeInfos, Node[] nodes, String prefix, TDefinitions um) {
        for (Node node : nodes) {
            Point p = getOffset((String) node.getMetaData().get("UniqueId"), um);
            Integer x = (Integer) node.getMetaData().get("x") + ((Integer) node.getMetaData().get("width"))/2;
            x += p.getX().intValue();
            Integer y = (Integer) node.getMetaData().get("y") + ((Integer) node.getMetaData().get("height"))/2;
            y += p.getY().intValue();
            nodeInfos.add(new DiagramNodeInfo(prefix + node.getId(), x, y, (Integer) node.getMetaData().get("width"), (Integer) node
                        .getMetaData().get("height")));
            if (node instanceof NodeContainer) {
                addNodesInfo(nodeInfos, ((NodeContainer) node).getNodes(), prefix + node.getId() + ":", um);
            }
        }
    }

    /**
     * Gets the offset.
     *
     * @param uniqueId the unique id
     * @param um the um
     * @return the offset
     */
    private static Point getOffset(String uniqueId, TDefinitions um) {
        //get lanes and see if this id belongs
        String laneId = null;
        Point point = new Point();

        if (um != null) {
            start: for (Object rootElement : um.getRootElementList()) {
                    if (rootElement instanceof TProcess) {
                        if (((TProcess) rootElement).getLaneSetList() != null) {
                            for (TLaneSet lanes : ((TProcess) rootElement).getLaneSetList()) {
                                for (TLane lane : lanes.getLaneList()) {
                                    for (String reference : lane.getFlowNodeRefList()) {
                                        if (reference.equalsIgnoreCase(uniqueId)) {
                                            laneId = lane.getId();
                                            break start;
                                        }
                                    }
                                }
                            }
                        }
                    }
            }

            if (laneId != null) {
                for (BPMNDiagram diagram : um.getBPMNDiagramList()) {
                    if (diagram.getBPMNPlane() != null) {
                        for (DiagramElement element : diagram.getBPMNPlane().getDiagramElementList()) {
                            if (element instanceof BPMNShape) {
                                BPMNShape bshape = (BPMNShape) element;
                                if (laneId.equalsIgnoreCase(bshape.getBpmnElement().getName())) {
                                    point.setX(bshape.getBounds().getX());
                                    point.setY(bshape.getBounds().getY());
                                    return point;
                                }
                            }
                        }
                    }
                }
            }
        }

        point.setX(0d);
        point.setY(0d);
        return point;
    }

    /**
    * Gets the diagram url.
    *
    * @param processInstance the process instance
    * @return the diagram url
    */
    private static URL getDiagramURL(ProcessInstance processInstance) {

        try {
            return new URL(getConsoleProperties().getProperty("guvnor.protocol", "http") + "://"
                    + getConsoleProperties().getProperty("guvnor.host", "localhost:8080") + "/"
                    + getConsoleProperties().getProperty("guvnor.subdomain", "drools-guvnor") + "/org.drools.guvnor.Guvnor/package"
                    + getConsoleProperties().getProperty("bpm.package") + "/" + processInstance.getProcessId() + "-image.png");

        } catch (Throwable t) {
            logger.error("Could not get diagram url from Guvnor: " + t.getMessage());
        }

        return null;
    }

    /**
    * Gets the local image data.
    *
    * @return the local image data
    * @throws IOException 
    */
    private static String getImageData(ProcessInstance processInstance) throws IOException {
        String result = "data:image/png;base64,";
        String imageBase64 = null;
        if ("local".equalsIgnoreCase(getConsoleProperties().getProperty("bpm.type", "remote"))) {
            imageBase64 = Base64.encodeBase64String(IOUtils.toByteArray(DrawGraph.class.getResourceAsStream("/resources/"
                            + processInstance.getProcessId() + ".png")));
        } else {
            GuvnorConnectionUtils gcu = new GuvnorConnectionUtils();
            if (gcu.guvnorExists()) {
                imageBase64 = Base64.encodeBase64String(gcu.getProcessImageFromGuvnor(processInstance.getProcessId()));
            }
        }
        return result + imageBase64;
    }
}


To run everything I use JIBX to compile XSD schemas for BPMN2 (you can find them in jbpm-bpmn2-5.2.0.Final.jar). You will need to slightly modify JIBX output with extending classes to compile it, but after that it should work OK.

Before running this, you will need BPMN2 file and PNG in you resources directory (named same as process ID). In case that in the future versions of Designer positioning of the elements inside BPMN2 becomes absolute, then JIBX will not be needed as PNG you can get from Guvnor directly and source will not be parsed as positions are already in the metadata of the node. Currently, as positions are relative we need to calculate the offset to overlay the arrow correctly over PNG. Please also note that I only offset positions for the Lanes and not, for example, from the  Sub-process. You can extend this code to fit your needs.

Usage in xhtml:

<ice:panelGroup>
    <ice:outputText escape="false" value="#{bean.diagramCode}"/>
    <br />
    <div align="right">
        <ice:commandButton value="Close" immediate="true" action="#{bean.cancelAction}" />
    </div>
</ice:panelGroup>

And not to forget, you need to initialize JPAWorkingMemoryDbLogger and add corresponding Entities to persistence.xml (ProcessInstanceLog, NodeInstanceLog, VariableInstanceLog).

As always, if you have any suggestions, please drop me a note.

Thanks to tsurdilo for help!

5 comments:

Naai Kutty said...

Is it possible to create the process instance active nodes diagram without guvnor.

Branislav Cavlin said...

Yes,

You can use bpmn2 and png file as local resources. Check parameter "bpm.type" local/remote.

Regards.

Emilio Francesco Giuseppe Fuoco said...

Hi Branislav,

thanks for your post, it's very interesting.
I would like to know if your code is compatible with jBPM 5.4

Thanks a lot!

Emilio

Branislav Cavlin said...

It should be possible with 5.X version with small code adjustments where needed as far as I can remember. You can copy it locally and try to compile and if there are errors you can always download source for jBPM and take a look what is different there (packages and structure).

raj4uuec said...

/**
* Gets the bPM active nodes diagram.
*
* @param processInstance the process instance

Can you tell me what function is described by the above declaration.. Is it a function from the bpm-console api