domingo, 8 de agosto de 2010

JBoss JBPM - Generating an image at runtime (JPDL -> PNG)

This article is an example of how to dynamically generate an image of a process JBPM.
JBoss JBPM and other BPM tools make use of XML to describe the business processes.
The concept is then to do a parsing of the XML and generate a process image at runtime.
The original idea was obtained from a Chinese discussion group.
In this article, Mr. Yeyong presents a simple solution to generate at run-time an image of a process modeled in JBPM, parsing the JPDL.xml and using the resources of elementary geometry with AWT lib.
Using the same idea and reusing the source code of Mr. Yeyong I extend this concept to provide a visual representation of the steps already completed of the process.
The original article can be found at the following URL: http://jbpm.group.javaeye.com/group/blog/470760?page=2

The following is the source code which is basically composed of five classes

/**
* Technique TI Ltda - Project: PlanetaContabilWeb - www.planetacontabil.com.br
* @author yeyong - http://jbpm.group.javaeye.com/group/blog/470760?page=2
* @author Ricardo A. Harari - ricardo.harari@gmail.com
* @date 25/01/2010 12:18:45
* br.com.technique.process.render.graph
*
* TODO
*/

package br.com.technique.process.render.graph;

import java.awt.Point;
import java.awt.Rectangle;

/**
* @author yeyong
*
*/
public class GeometryUtils {
/**
 * ????(x1,y1)-(x2,y2)???
 *
 * @param x1
 * @param y1
 * @param x2
 * @param y2
 * @return
 */
public static double getSlope(int x1, int y1, int x2, int y2) {
  return ((double) y2 - y1) / (x2 - x1);
}

/**
 * ????(x1,y1)-(x2,y2)?y???
 *
 * @param x1
 * @param y1
 * @param x2
 * @param y2
 * @return
 */
public static double getYIntercep(int x1, int y1, int x2, int y2) {
  return y1 - x1 * getSlope(x1, y1, x2, y2);
}
/**
 * ???????
 *
 * @param rect
 * @return
 */
public static Point getRectangleCenter(Rectangle rect) {
  return new Point((int) rect.getCenterX(), (int) rect.getCenterY());
}

/**
 * ??????p0?p1?????????
 *
 * @param rectangle
 * @param p1
 * @return
 */
public static Point getRectangleLineCrossPoint(Rectangle rectangle, Point p1, int grow) {
  Rectangle rect = rectangle.getBounds();
  rect.grow(grow, grow);
  Point p0 = GeometryUtils.getRectangleCenter(rect);

  if (p1.x == p0.x) {
    if (p1.y < y ="="" slope =" GeometryUtils.getSlope(p0.x," slopeline =" GeometryUtils.getSlope(p0.x," yintercep =" GeometryUtils.getYIntercep(p0.x,"> slope - 1e-2) {
    if (p1.y < page="2"> nodes = new LinkedHashMap();
public static final int RECT_OFFSET_X = -7;
public static final int RECT_OFFSET_Y = -8;
public static final int DEFAULT_PIC_SIZE = 48;

/** R.Harari - activities list */
public Hashtable listActivities;


private final static Map nodeInfos = new HashMap();
static {
  nodeInfos.put("start", "start_event_empty.png");
  nodeInfos.put("end", "end_event_terminate.png");
  nodeInfos.put("end-cancel", "end_event_cancel.png");
  nodeInfos.put("end-error", "end_event_error.png");
  nodeInfos.put("decision", "gateway_exclusive.png");
  nodeInfos.put("fork", "gateway_parallel.png");
  nodeInfos.put("join", "gateway_parallel.png");
  nodeInfos.put("state", null);
  nodeInfos.put("hql", null);
  nodeInfos.put("sql", null);
  nodeInfos.put("java", null);
  nodeInfos.put("script", null);
  nodeInfos.put("task", null);
  nodeInfos.put("sub-process", null);
  nodeInfos.put("custom", null);
}

public JpdlModel(InputStream is) throws Exception {
  this(new SAXReader().read(is).getRootElement());
}

public JpdlModel(InputStream is, List listHistoryActivities) throws Exception {
    this(new SAXReader().read(is).getRootElement());
    if (listHistoryActivities != null) {
     listActivities = new Hashtable();
     for (HistoryActivityInstance hai : listHistoryActivities) {
      listActivities.put(hai.getActivityName(), hai);
     }
    }
}

@SuppressWarnings("unchecked")
private JpdlModel(Element rootEl) throws Exception {
  for (Element el : (List) rootEl.elements()) {
    String type = el.getQName().getName();
    if (!nodeInfos.containsKey(type)) { // ????????
      continue;
    }
    String name = null;
    if (el.attribute("name") != null) {
      name = el.attributeValue("name");
    }
    String[] location = el.attributeValue("g").split(",");
    int x = Integer.parseInt(location[0]);
    int y = Integer.parseInt(location[1]);
    int w = Integer.parseInt(location[2]);
    int h = Integer.parseInt(location[3]);

    if (nodeInfos.get(type) != null) {
      w = DEFAULT_PIC_SIZE;
      h = DEFAULT_PIC_SIZE;
    } else {
      x -= RECT_OFFSET_X;
      y -= RECT_OFFSET_Y;
      w += (RECT_OFFSET_X + RECT_OFFSET_X);
      h += (RECT_OFFSET_Y + RECT_OFFSET_Y);
    }
    Node node = new Node(name, type, x, y, w, h);
    parserTransition(node, el);
    nodes.put(name, node);
  }
}

@SuppressWarnings("unchecked")
private void parserTransition(Node node, Element nodeEl) {
  for (Element el : (List) nodeEl.elements("transition")) {
    String label = el.attributeValue("name");
    String to = el.attributeValue("to");
    Transition transition = new Transition(label, to);
    String g = el.attributeValue("g");
    if (g != null && g.length() > 0) {
      if (g.indexOf(":") < p =" g.split(" lines =" p[0].split(" exp ="="" p =" exp.split("> getNodes() {
  return nodes;
}
  public static Map getNodeInfos() {
  return nodeInfos;
}

}


/**
* Technique TI Ltda - Project: PlanetaContabilWeb - www.planetacontabil.com.br
* @author yeyong - http://jbpm.group.javaeye.com/group/blog/470760?page=2
* @author Ricardo A. Harari - ricardo.harari@gmail.com - improved to represent completed steps of a running process
* @date 25/01/2010 12:17:04
* br.com.technique.process.render.graph
*
* TODO
*/

package br.com.technique.process.render.graph;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.awt.font.FontRenderContext;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import javax.imageio.ImageIO;

import org.jbpm.api.history.HistoryActivityInstance;

/**
* @author
*
*/
public class JpdlModelDrawer {
public static final int RECT_OFFSET_X = JpdlModel.RECT_OFFSET_X;
public static final int RECT_OFFSET_Y = JpdlModel.RECT_OFFSET_Y;
public static final int RECT_ROUND = 15;

public static final int DEFAULT_FONT_SIZE = 12;

public static final Color DEFAULT_STROKE_COLOR = Color.decode("#03689A");
public static final Stroke DEFAULT_STROKE = new BasicStroke(2);

public static final Color DEFAULT_LINE_STROKE_COLOR = Color.decode("#808080");
public static final Stroke DEFAULT_LINE_STROKE = new BasicStroke(1);

public static final Color DEFAULT_FILL_COLOR = Color.decode("#F6F7FF");

/** R.Harari - nova cores para representar o estado das etapas */
public static final Color DEFAULT_FILL_COLOR_FINISHED = Color.decode("#C4FFC1");
public static final Color DEFAULT_FILL_COLOR_CURRENT = Color.decode("#FFFF97");
/** */


private final static Map nodeInfos = JpdlModel.getNodeInfos();

public BufferedImage draw(JpdlModel jpdlModel) throws IOException {
  Rectangle dimension = getCanvasDimension(jpdlModel);
  BufferedImage bi = new BufferedImage(dimension.width, dimension.height, BufferedImage.TYPE_INT_ARGB);
  Graphics2D g2 = bi.createGraphics();
  g2.setColor(Color.WHITE);
  g2.fillRect(0, 0, dimension.width, dimension.height);
  g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
  Font font = new Font("Arial", Font.PLAIN, DEFAULT_FONT_SIZE);
  g2.setFont(font);
  Map nodes = jpdlModel.getNodes();
  drawNode(nodes, g2, font, jpdlModel.listActivities);
  drawTransition(nodes, g2);
  return bi;
}

/**
 * ?????????
 *
 * @return
 */
private Rectangle getCanvasDimension(JpdlModel jpdlModel) {
  Rectangle rectangle = new Rectangle();
  Rectangle rect;
  for (Node node : jpdlModel.getNodes().values()) {
    rect = node.getRectangle();
    if (rect.getMaxX() > rectangle.getMaxX()) {
      rectangle.width = (int) rect.getMaxX();
    }
    if (rect.getMaxY() > rectangle.getMaxY()) {
      rectangle.height = (int) rect.getMaxY();
    }
    for (Transition transition : node.getTransitions()) {
      List trace = transition.getLineTrace();
      for (Point point : trace) {
        if (rectangle.getMaxX() < width =" point.x;" height =" point.y;"> nodes, Graphics2D g2) throws IOException {
  g2.setStroke(DEFAULT_LINE_STROKE);
  g2.setColor(DEFAULT_LINE_STROKE_COLOR);
  for (Node node : nodes.values()) {
    for (Transition transition : node.getTransitions()) {
      String to = transition.getTo();
      Node toNode = nodes.get(to);
      List trace = new LinkedList(transition.getLineTrace());
      int len = trace.size() + 2;
      trace.add(0, new Point(node.getCenterX(), node.getCenterY()));
      trace.add(new Point(toNode.getCenterX(), toNode.getCenterY()));
      int[] xPoints = new int[len];
      int[] yPoints = new int[len];
      for (int i = 0; i < taskgrow =" 4;" smallgrow =" -2;" grow =" 0;" grow =" smallGrow;" grow =" taskGrow;" p =" GeometryUtils.getRectangleLineCrossPoint(node.getRectangle()," grow =" smallGrow;" grow =" taskGrow;" p =" GeometryUtils.getRectangleLineCrossPoint(toNode.getRectangle()," label =" transition.getLabel();"> 0) {
        int cx, cy;
        if (len % 2 == 0) {
          cx = (xPoints[len / 2 - 1] + xPoints[len / 2]) / 2;
          cy = (yPoints[len / 2 - 1] + yPoints[len / 2]) / 2;
        } else {
          cx = xPoints[len / 2];
          cy = yPoints[len / 2];
        }
        Point labelPoint = transition.getLabelPosition();
        if (labelPoint != null) {
          cx += labelPoint.x;
          cy += labelPoint.y;
        }
        cy -= RECT_OFFSET_Y + RECT_OFFSET_Y / 2;
        g2.drawString(label, cx, cy);
      }
    }
  }
}

private void drawArrow(Graphics2D g2, int x1, int y1, int x2, int y2) {
  final double len = 8.0;
  double slopy = Math.atan2(y2 - y1, x2 - x1);
  double cosy = Math.cos(slopy);
  double siny = Math.sin(slopy);
  int[] xPoints = { 0, x2, 0 };
  int[] yPoints = { 0, y2, 0 };
  double a = len * siny, b = len * cosy;
  double c = len / 2.0 * siny, d = len / 2.0 * cosy;
  xPoints[0] = x2 - (int) (b + c);
  yPoints[0] = y2 - (int) (a - d);
  xPoints[2] = x2 - (int) (b - c);
  yPoints[2] = y2 - (int) (d + a);
  
  g2.fillPolygon(xPoints, yPoints, 3);
}

/**
 * @param g2
 * @throws IOException
 */
private void drawNode(Map nodes, Graphics2D g2, Font font, Hashtable listActivities) throws IOException {
  for (Node node : nodes.values()) {
    String name = node.getName();

    if (nodeInfos.get(node.getType()) != null) {
      BufferedImage bi2 = ImageIO.read(getClass().getResourceAsStream(
          "icons/48/" + nodeInfos.get(node.getType())));
      g2.drawImage(bi2, node.getX(), node.getY(), null);
    } else {
      int x = node.getX();
      int y = node.getY();
      int w = node.getWitdth();
      int h = node.getHeight();
    
      HistoryActivityInstance hai = null;
      Color fillColor = DEFAULT_FILL_COLOR;

      if (listActivities != null) {
       hai = listActivities.get(name);
       if (hai != null) {
         if (hai.getEndTime() != null) {
          fillColor = DEFAULT_FILL_COLOR_FINISHED;
         } else {
          fillColor = DEFAULT_FILL_COLOR_CURRENT;
         }
       }
      }

      g2.setColor(fillColor);
      g2.fillRoundRect(x, y, w, h, RECT_ROUND, RECT_ROUND);
      g2.setColor(DEFAULT_STROKE_COLOR);
      g2.setStroke(DEFAULT_STROKE);
      g2.drawRoundRect(x, y, w, h, RECT_ROUND, RECT_ROUND);

      FontRenderContext frc = g2.getFontRenderContext();
      Rectangle2D r2 = font.getStringBounds(name, frc);
      int xLabel = (int) (node.getX() + ((node.getWitdth() - r2.getWidth()) / 2));
      int yLabel = (int) ((node.getY() + ((node.getHeight() - r2.getHeight()) / 2)) - r2.getY());
      g2.setStroke(DEFAULT_LINE_STROKE);
      g2.setColor(Color.black);
      g2.drawString(name, xLabel, yLabel);
    }
  }
}
}


/**
* Technique TI Ltda - Project: PlanetaContabilWeb - www.planetacontabil.com.br
* @author yeyong - http://jbpm.group.javaeye.com/group/blog/470760?page=2
* @author Ricardo A. Harari - ricardo.harari@gmail.com
* @date 25/01/2010 12:13:40
* br.com.technique.process.render.graph
*
* TODO
*/

package br.com.technique.process.render.graph;

import java.awt.Rectangle;
import java.util.ArrayList;
import java.util.List;

public class Node {
private String name;
private String type;
private Rectangle rectangle;
private List transitions = new ArrayList();

public Node(String name, String type) {
  this.name = name;
  this.type = type;
}

public Node(String name, String type, int x, int y, int w, int h) {
  this.name = name;
  this.type = type;
  this.rectangle = new Rectangle(x, y, w, h);
}

public Rectangle getRectangle() {
  return rectangle;
}

public void setRectangle(Rectangle rectangle) {
  this.rectangle = rectangle;
}

public String getType() {
  return type;
}

public void setType(String type) {
  this.type = type;
}

public String getName() {
  return name;
}

public void setName(String name) {
  this.name = name;
}

public void addTransition(Transition transition) {
  transitions.add(transition);
}

public List getTransitions() {
  return transitions;
}

public void setTransitions(List transitions) {
  this.transitions = transitions;
}

public int getX() {
  return rectangle.x;
}

public int getY() {
  return rectangle.y;
}

public int getCenterX() {
  return (int) rectangle.getCenterX();
}

public int getCenterY() {
  return (int) rectangle.getCenterY();
}

public int getWitdth() {
  return rectangle.width;
}

public int getHeight() {
  return rectangle.height;
}
}


/**
* Technique TI Ltda - Project: PlanetaContabilWeb - www.planetacontabil.com.br
* @author yeyong - http://jbpm.group.javaeye.com/group/blog/470760?page=2
* @author Ricardo Alberto Harari - ricardo.harari@gmail.com
* @date 25/01/2010 12:14:48
* br.com.technique.process.render.graph
*
* TODO
*/

package br.com.technique.process.render.graph;

import java.awt.Point;
import java.util.ArrayList;
import java.util.List;

public class Transition {
private Point labelPosition;
private List lineTrace = new ArrayList();
private String label;
private String to;

public Transition(String label, String to) {
  this.label = label;
  this.to = to;
}

public Point getLabelPosition() {
  return labelPosition;
}

public void setLabelPosition(Point labelPosition) {
  this.labelPosition = labelPosition;
}

public List getLineTrace() {
  return lineTrace;
}

public void setLineTrace(List lineTrace) {
  this.lineTrace = lineTrace;
}

public void addLineTrace(Point lineTrace) {
  if (lineTrace != null) {
    this.lineTrace.add(lineTrace);
  }
}

public String getLabel() {
  return label;
}
public void setLabel(String label) {
  this.label = label;
}

public String getTo() {
  return to;
}

public void setTo(String to) {
  this.to = to;
}

}  



Example of use:

JpdlModel jpdlModel = new JpdlModel(JbpmAberturaEmpresa.class.getResourceAsStream("aberturaEmpresa.jpdl.xml"), hai);
ImageIO.write(new JpdlModelDrawer().draw(jpdlModel), "png", new File("/tmp/myprocess.png"));

aberturaEmpresa.jpdl.xml -> is my business process located at the same package of JbpmAberturaEmpresa class. You should customize to retrieve your business process.
hai -> History activities - see bellow a method to retrieve the activities
/tmp/myprocess.png -> output path+file of the PNG image

Retrieving the history activities:

String execID = "";
/** if you are running as a java application - outside a j2ee container */
ProcessEngine processEngine = new Configuration().setResource("jbpm.cfg.xml").buildProcessEngine();

List hai = processEngine.getHistoryService().createHistoryActivityInstanceQuery()
.processInstanceId(execID)
.list();


Below is an example of generated image:
Green -> completed steps
Yellow -> current step
White -> uncompleted step




Was tested with the latest version of JBPM 4.4 and works normally.
The following is a direct access to source code containing also the icons. The icons are extracted from a JAR used by the modeler of the eclipse ide.

https://drive.google.com/open?id=14OKHx0EkrgsB2zrP62t1OGjKK9lK5fNX