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 */
List
.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