package hudson.drools.renderer; import hudson.drools.GraphicsUtil; import static hudson.drools.renderer.RendererConstants.*; import hudson.drools.NodeInstanceLog; import hudson.drools.WorkItemAction; import hudson.model.Hudson; import hudson.model.Job; import hudson.model.Run; import java.awt.BasicStroke; import java.awt.Color; import java.awt.GradientPaint; import java.awt.Graphics2D; import java.awt.Paint; import java.awt.Polygon; import java.awt.RadialGradientPaint; import java.awt.RenderingHints; import java.awt.geom.AffineTransform; import java.awt.geom.Line2D; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.awt.geom.Point2D.Double; import java.awt.image.BufferedImage; import java.io.IOException; import java.io.OutputStream; import java.io.StringReader; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import javax.imageio.ImageIO; import javax.servlet.ServletOutputStream; import org.dom4j.Document; import org.dom4j.DocumentException; import org.dom4j.Element; import org.dom4j.io.SAXReader; public class RuleFlowRenderer { private Map<String, RendererNode> nodes = new HashMap<String, RendererNode>(); private List<Connection> connections = new ArrayList<Connection>(); private List<Connection> compositeConnections = new ArrayList<Connection>(); private int width, height; // private List<NodeInstanceLog> logs; public RuleFlowRenderer(String xml) { try { readResource(new SAXReader().read(new StringReader(xml))); } catch (DocumentException e) { throw new IllegalArgumentException("Cannot parse workflow xml"); } } public RuleFlowRenderer(String xml, List<NodeInstanceLog> logs) { this(xml); // this.logs = logs; for (NodeInstanceLog log : logs) { RendererNode node = nodes.get(log.getNodeId()); if (node == null) { System.out.println("unknown node for " + log); continue; } if (log.getType() == NodeInstanceLog.TYPE_ENTER) { node.state = NodeState.IN_PROGRESS; } else if (log.getType() == NodeInstanceLog.TYPE_EXIT) { node.state = NodeState.COMPLETED; } if (node instanceof Build) { String projectName = ((Build) node).project; Job project = getJobUrl(projectName); if (project != null) { Run run = WorkItemAction.findRun(project, log .getProcessInstanceId()); if (run != null) { ((Build) node).run = run; } } } } } static Job getJobUrl(String projectName) { return (Hudson.getInstance() != null) ? (Job) Hudson.getInstance() .getItem(projectName) : null; } private void readResource(Document document) throws DocumentException { int maxX = 0; int maxY = 0; int minX = Integer.MAX_VALUE; int minY = Integer.MAX_VALUE; Element root = document.getRootElement(); Iterator it = root.element("nodes").elementIterator(); while (it.hasNext()) { Element el = (Element) it.next(); int x = Integer.parseInt(el.attributeValue("x")); int y = Integer.parseInt(el.attributeValue("y")); int width = Integer.parseInt(el.attributeValue("width")); int height = Integer.parseInt(el.attributeValue("height")); maxX = Math.max(maxX, x + width); maxY = Math.max(maxY, y + height); minX = Math.min(minX, x); minY = Math.min(minY, y); } int offsetX = minX - 5; int offsetY = minY - 5; it = root.element("nodes").elementIterator(); while (it.hasNext()) { Element el = (Element) it.next(); RendererNode node = createNode(el, offsetX, offsetY); nodes.put(node.id, node); } width = maxX - minX + 10; height = maxY - minY + 10; it = root.element("connections").elementIterator(); while (it.hasNext()) { Element el = (Element) it.next(); String from = el.attributeValue("from"); String to = el.attributeValue("to"); connections.add(new Connection(nodes.get(from), nodes.get(to))); } } private RendererNode createNode(Element el, int offsetX, int offsetY) { String type = el.getName(); String name = el.attributeValue("name"); String id = el.attributeValue("id"); int x = Integer.parseInt(el.attributeValue("x")) - offsetX; int y = Integer.parseInt(el.attributeValue("y")) - offsetY; int width = el.attributeValue("width") != null ? Integer.parseInt(el .attributeValue("width")) : 80; int height = el.attributeValue("height") != null ? Integer.parseInt(el .attributeValue("height")) : 40; RendererNode node; if ("workItem".equals(type)) { String workName = el.element("work").attributeValue("name"); if ("Script".equals(workName)) { node = new Script(type, name, id, x, y, width, height); } else if ("Build".equals(workName)) { Iterator<Element> eit = el.element("work").elementIterator(); String project = null; while (eit.hasNext()) { Element param = eit.next(); if ("Project".equals(param.attributeValue("name"))) { project = param.elementText("value"); } } node = new Build(type, name, id, project, x, y, width, height); } else { node = new WorkItem(type, name, id, x, y, width, height); } } else if ("humanTask".equals(type)) { node = new HumanTask(type, name, id, x, y, width, height); } else if ("start".equals(type)) { node = new Start(type, name, id, x, y, width, height); } else if ("end".equals(type)) { node = new End(type, name, id, x, y, width, height); } else if ("split".equals(type)) { node = new Split(type, name, id, x, y, width, height); } else if ("join".equals(type)) { node = new Split(type, name, id, x, y, width, height); } else if ("eventNode".equals(type)) { node = new Event(type, name, id, x, y, width, height); } else if ("forEach".equals(type)) { node = new ForEach(type, name, id, x, y, width, height); Iterator it = el.element("nodes").elementIterator(); while (it.hasNext()) { Element e = (Element) it.next(); RendererNode child = createNode(e, -x, -y); nodes.put(node.id + ":2:" + child.id, child); } it = el.element("connections").elementIterator(); while (it.hasNext()) { Element conn = (Element) it.next(); String from = node.id + ":2:" + conn.attributeValue("from"); String to = node.id + ":2:" + conn.attributeValue("to"); compositeConnections.add(new Connection(nodes.get(from), nodes .get(to))); } } else { node = new RendererNode(type, name, id, x, y, width, height); } return node; } public Collection<RendererNode> getNodes() { return nodes.values(); } public List<Connection> getConnections() { return connections; } public void paint(Graphics2D g2) { g2.setColor(Color.WHITE); g2.fillRect(0, 0, getWidth(), getHeight()); for (Connection connection : connections) { Rectangle2D.Double fromRect = connection.from.getRectangle(); Rectangle2D.Double toRect = connection.to.getRectangle(); paintLine(g2, fromRect, toRect); } for (RendererNode node : nodes.values()) { if (node instanceof ForEach) node.paint(g2); } for (Connection connection : compositeConnections) { Rectangle2D.Double fromRect = connection.from.getRectangle(); Rectangle2D.Double toRect = connection.to.getRectangle(); paintLine(g2, fromRect, toRect); } for (RendererNode node : nodes.values()) { if (!(node instanceof ForEach)) node.paint(g2); } } public static void paintLine(Graphics2D g2, Rectangle2D.Double from, Rectangle2D.Double to) { Point2D.Double fromRectCenter = new Point2D.Double(from.getCenterX(), from.getCenterY()); Point2D.Double toRectCenter = new Point2D.Double(to.getCenterX(), to .getCenterY()); Line2D.Double line = new Line2D.Double(fromRectCenter, toRectCenter); Double p1 = new Point2D.Double(); GraphicsUtil.getLineRectangleIntersection(from, line, p1); Double p2 = new Point2D.Double(); GraphicsUtil.getLineRectangleIntersection(to, line, p2); // drawArrow(g2, new Line2D.Double(p1,p2), 1, true); drawArrow(g2, line, 1, true); } public static void drawArrow(Graphics2D g2d, Line2D.Double line, float stroke, boolean arrow) { int xCenter = (int) line.getX1(); int yCenter = (int) line.getY1(); double x = line.getX2(); double y = line.getY2(); double aDir = Math.atan2(xCenter - x, yCenter - y); int i1 = 12 + (int) (stroke * 2); int i2 = 6 + (int) stroke; // make the arrow head the same size Line2D.Double base = new Line2D.Double(x + xCor(i1, aDir + .5), y + yCor(i1, aDir + .5), x + xCor(i1, aDir - .5), y + yCor(i1, aDir - .5)); Point2D.Double intersect = new Point2D.Double(); GraphicsUtil.getLineLineIntersection(line, base, intersect); g2d.setPaint(LINE_COLOR); if (arrow) { g2d.draw(new Line2D.Double(xCenter, yCenter, intersect.x, intersect.y)); g2d.setStroke(new BasicStroke(1f)); // make the arrow head solid // even if // dash pattern has been specified Polygon tmpPoly = new Polygon(); // regardless of the length tmpPoly.addPoint((int) x, (int) y); // arrow tip tmpPoly.addPoint((int) x + xCor(i1, aDir + .5), (int) y + yCor(i1, aDir + .5)); // tmpPoly.addPoint(x + xCor(i2, aDir), y + yCor(i2, aDir)); tmpPoly.addPoint((int) x + xCor(i1, aDir - .5), (int) y + yCor(i1, aDir - .5)); tmpPoly.addPoint((int) x, (int) y); // arrow tip g2d.drawPolygon(tmpPoly); } else { g2d.draw(new Line2D.Double(xCenter, yCenter, x, y)); } // g2d.setPaint(Color.WHITE); } private static int yCor(int len, double dir) { return (int) (len * Math.cos(dir)); } private static int xCor(int len, double dir) { return (int) (len * Math.sin(dir)); } public String getNodeName(String id) { RendererNode node = nodes.get(id); return node != null ? node.name : null; } public void write(OutputStream output) throws IOException { BufferedImage aimg = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_RGB); Graphics2D g = aimg.createGraphics(); paint(g); g.dispose(); ImageIO.write(aimg, "png", output); } public int getWidth() { return width; } public int getHeight() { return height; } public static void paintBall(Graphics2D g2, Color c) { g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); int diameter = 16; // Retains the previous state Paint oldPaint = g2.getPaint(); // Fills the circle with solid blue color g2.setColor(c); g2.fillOval(0, 0, diameter - 1, diameter - 1); // Adds shadows at the top Paint p; p = new GradientPaint(0, 0, new Color(0.0f, 0.0f, 0.0f, 0.4f), 0, diameter, new Color(0.0f, 0.0f, 0.0f, 0.0f)); g2.setPaint(p); g2.fillOval(0, 0, diameter - 1, diameter - 1); // Adds highlights at the bottom p = new GradientPaint(0, 0, new Color(1.0f, 1.0f, 1.0f, 0.0f), 0, diameter, new Color(1.0f, 1.0f, 1.0f, 0.0f)); g2.setPaint(p); g2.fillOval(0, 0, diameter - 1, diameter - 1); // Creates dark edges for 3D effect p = new RadialGradientPaint(new Point2D.Double(diameter * .4, diameter * .45), diameter / 2.0f, new float[] { 0.0f, 0.95f }, new Color[] { new Color(c.getRed(), c.getGreen(), c.getBlue(), 127), new Color(0.0f, 0.0f, 0.0f, 0.0f) }); g2.setPaint(p); g2.fillOval(0, 0, diameter - 1, diameter - 1); // Adds oval inner highlight at the bottom p = new RadialGradientPaint(new Point2D.Double(diameter / 2.0, diameter * 1.5), diameter / 2.3f, new Point2D.Double( diameter / 2.0, diameter * 1.75 + 6), new float[] { 0.0f, 0.8f }, new Color[] { new Color(c.getRed(), c.getGreen(), c.getBlue(), 255), new Color(c.getRed(), c.getGreen(), c.getBlue(), 0) }, RadialGradientPaint.CycleMethod.NO_CYCLE, RadialGradientPaint.ColorSpaceType.SRGB, AffineTransform .getScaleInstance(1.0, 0.5)); g2.setPaint(p); g2.fillOval(0, 0, diameter - 1, diameter - 1); // Restores the previous state g2.setPaint(oldPaint); } public void writeSVG(ServletOutputStream output) throws IOException { output .print("<svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' width='" + getWidth() + "' height='" + getHeight() + "'>"); output.println("<g>"); for (Connection connection : connections) { Rectangle2D.Double fromRect = connection.from.getRectangle(); Rectangle2D.Double toRect = connection.to.getRectangle(); output.println("<line x1='" + fromRect.getCenterX() + "' y1='" + fromRect.getCenterY() + "' x2='" + toRect.getCenterX() + "' y2='" + toRect.getCenterY() + "' style='stroke:rgb(0,0,0);stoke-width:1'/>"); } for (RendererNode node : nodes.values()) { // if (node instanceof ForEach) node.paint(g2); } for (Connection connection : compositeConnections) { Rectangle2D.Double fromRect = connection.from.getRectangle(); Rectangle2D.Double toRect = connection.to.getRectangle(); output.println("<line x1='" + fromRect.getCenterX() + "' y1='" + fromRect.getCenterY() + "' x2='" + toRect.getCenterX() + "' y2='" + toRect.getCenterY() + "'"); } for (RendererNode node : nodes.values()) { // if (!(node instanceof ForEach)) node.paint(g2); output .println("<rect x='" + node.x + "' y='" + node.y + "' width='" + node.width + "' height='" + node.height + "' style='fill:rgb(255,255,255);stroke-width:1;stroke:rgb(0,0,0)'/>"); output.println("<text x='" + (node.x + 16) + "' y='" + (node.y + (node.height / 2) + 4) + "' font-size='11'>" + node.name + "</text>"); output .println("<image x='" + node.x + "' y='" + (node.y + (node.height / 2) - 8) + "' width='16' height='16' xlink:href='/plugin/drools/icons/event.gif'/>"); } output.println("</g>"); output.println("</svg>"); } }