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

11 comentários:

Anônimo disse...

Hello,

Many thanks for the beautiful piece of code and information shared by you.

Picture speaks a thousand words.

Anônimo disse...

JpdlModel jpdlModel = new JpdlModel(JbpmAberturaEmpresa.class.getResourceAsStream("aberturaEmpresa.jpdl.xml"), hai);


What is JpdlModel????

Anônimo disse...

Can you put in a Zip File of the Entire Code as a Sample?

Ricardo A. Harari disse...

Hi,

JPDLModel is one of the 5 classes.

The source is inside the jar (zipped) - the link was broken this morning but is Ok now

http://www.planetacontabil.com.br/jbpm_image_sample.jar

kind regards

R.Harari

Anônimo disse...

What licenses are you releasing this code under? please say Apache2, BSD or MIT.

Ricardo A. Harari disse...

This code was obtained from Mr Yeong, a guy from China and had no information about licenses. I just adapted by adding some new features. You can freely use this piece of code just let the author's comments in the source code.

Marco disse...

Really really thx for this..
it's still useful in 2013...
Marco

Marco disse...

Really really thx for this..
it's still useful in 2013!

Marco

Anônimo disse...

Dear Ricardo,

Have you still got this code?

The question is that the code that is published in this page is missing a chunk in the area where the class JpdlModel should be defined:

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;

And the problem is that the .zip file mentioned in the post with the code is not available anymore...

Many thanks for your help and your post!

Ricardo A. Harari disse...

Yes, I do.
Follow the source code link

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

Anônimo disse...

Thank you very much Ricardo!

I've just realized your post and downloaded the .zip file!

I am going to check your code now.

You're great!

Best regards,


Oscar Manso