001/* 002 * Copyright 2012 Apache Software Foundation. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.apache.oozie.util; 017 018import edu.uci.ics.jung.algorithms.layout.StaticLayout; 019import edu.uci.ics.jung.graph.DirectedSparseGraph; 020import edu.uci.ics.jung.graph.Graph; 021import edu.uci.ics.jung.graph.util.Context; 022import edu.uci.ics.jung.visualization.VisualizationImageServer; 023import edu.uci.ics.jung.visualization.renderers.Renderer; 024import edu.uci.ics.jung.visualization.util.ArrowFactory; 025 026import java.awt.*; 027import java.awt.geom.Ellipse2D; 028import java.awt.geom.Point2D; 029import java.awt.image.BufferedImage; 030import java.io.IOException; 031import java.io.OutputStream; 032import java.io.StringReader; 033import java.nio.charset.Charset; 034import java.util.HashMap; 035import java.util.Iterator; 036import java.util.LinkedHashMap; 037import java.util.Map; 038 039import javax.imageio.ImageIO; 040import javax.swing.plaf.basic.BasicTextUI; 041import javax.xml.parsers.SAXParser; 042import javax.xml.parsers.SAXParserFactory; 043 044import org.apache.commons.collections15.Transformer; 045import org.apache.oozie.WorkflowJobBean; 046import org.apache.oozie.client.WorkflowAction; 047import org.apache.oozie.client.WorkflowAction.Status; 048import org.apache.oozie.client.WorkflowJob; 049import org.xml.sax.Attributes; 050import org.xml.sax.InputSource; 051import org.xml.sax.SAXException; 052import org.xml.sax.XMLReader; 053import org.xml.sax.helpers.DefaultHandler; 054 055/** 056 * Class to generate and plot runtime workflow DAG 057 */ 058public class GraphGenerator { 059 060 private String xml; 061 private WorkflowJobBean job; 062 private boolean showKill = false; 063 private final int actionsLimit = 25; 064 065 /** 066 * C'tor 067 * @param xml The workflow definition XML 068 * @param job Current status of the job 069 * @param showKill Flag to whether show 'kill' node 070 */ 071 public GraphGenerator(String xml, WorkflowJobBean job, boolean showKill) { 072 if(job == null) { 073 throw new IllegalArgumentException("JsonWorkflowJob can't be null"); 074 } 075 this.xml = xml; 076 this.job = job; 077 this.showKill = showKill; 078 } 079 080 /** 081 * C'tor 082 * @param xml 083 * @param job 084 */ 085 public GraphGenerator(String xml, WorkflowJobBean job) { 086 this(xml, job, false); 087 } 088 089 /** 090 * Overridden to thwart finalizer attack 091 */ 092 @Override 093 public final void finalize() { 094 // No-op; just to avoid finalizer attack 095 // as the constructor is throwing an exception 096 } 097 098 /** 099 * Stream the PNG file to client 100 * @param out 101 * @throws Exception 102 */ 103 public void write(OutputStream out) throws Exception { 104 SAXParserFactory spf = SAXParserFactory.newInstance(); 105 spf.setNamespaceAware(true); 106 SAXParser saxParser = spf.newSAXParser(); 107 XMLReader xmlReader = saxParser.getXMLReader(); 108 xmlReader.setContentHandler(new XMLParser(out)); 109 xmlReader.parse(new InputSource(new StringReader(xml))); 110 } 111 112 private class XMLParser extends DefaultHandler { 113 114 private OutputStream out; 115 private LinkedHashMap<String, OozieWFNode> tags; 116 117 private String action = null; 118 private String actionOK = null; 119 private String actionErr = null; 120 private String actionType = null; 121 private String fork; 122 private String decision; 123 124 public XMLParser(OutputStream out) { 125 this.out = out; 126 } 127 128 @Override 129 public void startDocument() throws SAXException { 130 tags = new LinkedHashMap(); 131 } 132 133 @Override 134 public void endDocument() throws SAXException { 135 136 if(tags.isEmpty()) { 137 // Nothing to do here! 138 return; 139 } 140 141 int maxX = Integer.MIN_VALUE; 142 int maxY = Integer.MIN_VALUE; 143 int minX = Integer.MAX_VALUE; 144 int currX = 45; 145 int currY = 45; 146 final int xMargin = 205; 147 final int yMargin = 50; 148 final int xIncr = 215; // The widest element is 200 pixels (Rectangle) 149 final int yIncr = 255; // The tallest element is 150 pixels; (Diamond) 150 HashMap<String, WorkflowAction> actionMap = new HashMap<String, WorkflowAction>(); 151 152 // Create a hashmap for faster lookups 153 // Also override showKill if there's any failed action 154 boolean found = false; 155 for(WorkflowAction wfAction : job.getActions()) { 156 actionMap.put(wfAction.getName(), wfAction); 157 if(!found) { 158 switch(wfAction.getStatus()) { 159 case KILLED: 160 case ERROR: 161 case FAILED: 162 showKill = true; // Assuming on error the workflow eventually ends with kill node 163 found = true; 164 } 165 } 166 } 167 168 // Start building the graph 169 DirectedSparseGraph<OozieWFNode, String> dg = new DirectedSparseGraph<OozieWFNode, String>(); 170 for(Map.Entry<String, OozieWFNode> entry : tags.entrySet()) { 171 String name = entry.getKey(); 172 OozieWFNode node = entry.getValue(); 173 if(actionMap.containsKey(name)) { 174 node.setStatus(actionMap.get(name).getStatus()); 175 } 176 177 // Set (x,y) coords of the vertices if not already set 178 if(node.getLocation().equals(new Point(0, 0))) { 179 node.setLocation(currX, currY); 180 } 181 182 float childStep = showKill ? -(((float)node.getArcs().size() - 1 ) / 2) 183 : -((float)node.getArcs().size() / 2 - 1); 184 int nodeX = node.getLocation().x; 185 int nodeY = node.getLocation().y; 186 for(Map.Entry<String, Boolean> arc : node.getArcs().entrySet()) { 187 if(!showKill && arc.getValue() && tags.get(arc.getKey()).getType().equals("kill")) { 188 // Don't show kill node (assumption: only error goes to kill node; 189 // No ok goes to kill node) 190 continue; 191 } 192 OozieWFNode child = tags.get(arc.getKey()); 193 if(child == null) { 194 continue; // or throw error? 195 } 196 dg.addEdge(name + "-->" + arc.getKey(), node, child); 197 // TODO: Experimental -- should we set coords even if they're already set? 198 //if(child.getLocation().equals(new Point(0, 0))) { 199 int childX = (int)(nodeX + childStep * xIncr); 200 int childY = nodeY + yIncr; 201 child.setLocation(childX, childY); 202 203 if(minX > childX) { 204 minX = childX; 205 } 206 if(maxX < childX) { 207 maxX = childX; 208 } 209 if(maxY < childY) { 210 maxY = childY; 211 } 212 //} 213 childStep += 1; 214 } 215 216 currY += yIncr; 217 currX = nodeX; 218 if(minX > nodeX) { 219 minX = nodeX; 220 } 221 if(maxX < nodeX) { 222 maxX = nodeX; 223 } 224 if(maxY < nodeY) { 225 maxY = nodeY; 226 } 227 } // Done building graph 228 229 final int padX = minX < 0 ? -minX: 0; 230 231 Transformer<OozieWFNode, Point2D> locationInit = new Transformer<OozieWFNode, Point2D>() { 232 233 @Override 234 public Point2D transform(OozieWFNode node) { 235 if(padX == 0) { 236 return node.getLocation(); 237 } else { 238 return new Point(node.getLocation().x + padX + xMargin, node.getLocation().y); 239 } 240 } 241 242 }; 243 244 StaticLayout<OozieWFNode, String> layout = new StaticLayout<OozieWFNode, String>(dg, locationInit, new Dimension(maxX + padX + xMargin, maxY)); 245 layout.lock(true); 246 VisualizationImageServer<OozieWFNode, String> vis = new VisualizationImageServer<OozieWFNode, String>(layout, new Dimension(maxX + padX + 2 * xMargin, maxY + yMargin)); 247 248 vis.getRenderContext().setEdgeArrowTransformer(new ArrowShapeTransformer()); 249 vis.getRenderContext().setArrowDrawPaintTransformer(new ArcPaintTransformer()); 250 vis.getRenderContext().setEdgeDrawPaintTransformer(new ArcPaintTransformer()); 251 vis.getRenderContext().setEdgeStrokeTransformer(new ArcStrokeTransformer()); 252 vis.getRenderContext().setVertexShapeTransformer(new NodeShapeTransformer()); 253 vis.getRenderContext().setVertexFillPaintTransformer(new NodePaintTransformer()); 254 vis.getRenderContext().setVertexStrokeTransformer(new NodeStrokeTransformer()); 255 vis.getRenderContext().setVertexLabelTransformer(new NodeLabelTransformer()); 256 vis.getRenderContext().setVertexFontTransformer(new NodeFontTransformer()); 257 vis.getRenderer().getVertexLabelRenderer().setPosition(Renderer.VertexLabel.Position.CNTR); 258 vis.setBackground(Color.WHITE); 259 260 Dimension d = vis.getSize(); 261 BufferedImage img = new BufferedImage(d.width, d.height, BufferedImage.TYPE_INT_RGB); 262 Graphics2D g = img.createGraphics(); 263 vis.paintAll(g); 264 265 try { 266 ImageIO.write(img, "png", out); 267 } 268 catch (IOException ioe) { 269 throw new SAXException(ioe); 270 } 271 finally { 272 try { 273 out.close(); //closing connection is imperative 274 //regardless of ImageIO.write throwing exception or not 275 //hence in finally block 276 } 277 catch (IOException e) { 278 XLog.getLog(getClass()).trace("Exception while closing OutputStream"); 279 } 280 out = null; 281 img.flush(); 282 g.dispose(); 283 vis.removeAll(); 284 } 285 } 286 287 @Override 288 public void startElement(String namespaceURI, 289 String localName, 290 String qName, 291 Attributes atts) 292 throws SAXException { 293 if(localName.equalsIgnoreCase("start")) { 294 String start = localName.toLowerCase(); 295 if(!tags.containsKey(start)) { 296 OozieWFNode v = new OozieWFNode(start, start); 297 v.addArc(atts.getValue("to")); 298 tags.put(start, v); 299 } 300 } else if(localName.equalsIgnoreCase("action")) { 301 action = atts.getValue("name"); 302 } else if(action != null && actionType == null) { 303 actionType = localName.toLowerCase(); 304 } else if(localName.equalsIgnoreCase("ok") && action != null && actionOK == null) { 305 actionOK = atts.getValue("to"); 306 } else if(localName.equalsIgnoreCase("error") && action != null && actionErr == null) { 307 actionErr = atts.getValue("to"); 308 } else if(localName.equalsIgnoreCase("fork")) { 309 fork = atts.getValue("name"); 310 if(!tags.containsKey(fork)) { 311 tags.put(fork, new OozieWFNode(fork, localName.toLowerCase())); 312 } 313 } else if(localName.equalsIgnoreCase("path")) { 314 tags.get(fork).addArc(atts.getValue("start")); 315 } else if(localName.equalsIgnoreCase("join")) { 316 String join = atts.getValue("name"); 317 if(!tags.containsKey(join)) { 318 OozieWFNode v = new OozieWFNode(join, localName.toLowerCase()); 319 v.addArc(atts.getValue("to")); 320 tags.put(join, v); 321 } 322 } else if(localName.equalsIgnoreCase("decision")) { 323 decision = atts.getValue("name"); 324 if(!tags.containsKey(decision)) { 325 tags.put(decision, new OozieWFNode(decision, localName.toLowerCase())); 326 } 327 } else if(localName.equalsIgnoreCase("case") 328 || localName.equalsIgnoreCase("default")) { 329 tags.get(decision).addArc(atts.getValue("to")); 330 } else if(localName.equalsIgnoreCase("kill") 331 || localName.equalsIgnoreCase("end")) { 332 String name = atts.getValue("name"); 333 if(!tags.containsKey(name)) { 334 tags.put(name, new OozieWFNode(name, localName.toLowerCase())); 335 } 336 } 337 if (tags.size() > actionsLimit) { 338 tags.clear(); 339 throw new SAXException("Can't display the graph. Number of actions are more than display limit " + actionsLimit); 340 } 341 } 342 343 @Override 344 public void endElement(String namespaceURI, 345 String localName, 346 String qName) 347 throws SAXException { 348 if(localName.equalsIgnoreCase("action")) { 349 tags.put(action, new OozieWFNode(action, actionType)); 350 tags.get(action).addArc(this.actionOK); 351 tags.get(action).addArc(this.actionErr, true); 352 action = null; 353 actionOK = null; 354 actionErr = null; 355 actionType = null; 356 } 357 } 358 359 private class OozieWFNode { 360 private String name; 361 private String type; 362 private Point loc; 363 private HashMap<String, Boolean> arcs; 364 private Status status = null; 365 366 public OozieWFNode(String name, 367 String type, 368 HashMap<String, Boolean> arcs, 369 Point loc, 370 Status status) { 371 this.name = name; 372 this.type = type; 373 this.arcs = arcs; 374 this.loc = loc; 375 this.status = status; 376 } 377 378 public OozieWFNode(String name, String type, HashMap<String, Boolean> arcs) { 379 this(name, type, arcs, new Point(0, 0), null); 380 } 381 382 public OozieWFNode(String name, String type) { 383 this(name, type, new HashMap<String, Boolean>(), new Point(0, 0), null); 384 } 385 386 public OozieWFNode(String name, String type, WorkflowAction.Status status) { 387 this(name, type, new HashMap<String, Boolean>(), new Point(0, 0), status); 388 } 389 390 public void addArc(String arc, boolean isError) { 391 arcs.put(arc, isError); 392 } 393 394 public void addArc(String arc) { 395 addArc(arc, false); 396 } 397 398 public void setName(String name) { 399 this.name = name; 400 } 401 402 public void setType(String type) { 403 this.type = type; 404 } 405 406 public void setLocation(Point loc) { 407 this.loc = loc; 408 } 409 410 public void setLocation(double x, double y) { 411 loc.setLocation(x, y); 412 } 413 414 public void setStatus(WorkflowAction.Status status) { 415 this.status = status; 416 } 417 418 public String getName() { 419 return name; 420 } 421 422 public String getType() { 423 return type; 424 } 425 426 public HashMap<String, Boolean> getArcs() { 427 return arcs; 428 } 429 430 public Point getLocation() { 431 return loc; 432 } 433 434 public WorkflowAction.Status getStatus() { 435 return status; 436 } 437 438 @Override 439 public String toString() { 440 StringBuilder s = new StringBuilder(); 441 442 s.append("Node: ").append(name).append("\t"); 443 s.append("Type: ").append(type).append("\t"); 444 s.append("Location: (").append(loc.getX()).append(", ").append(loc.getY()).append(")\t"); 445 s.append("Status: ").append(status).append("\n"); 446 Iterator<Map.Entry<String, Boolean>> it = arcs.entrySet().iterator(); 447 while(it.hasNext()) { 448 Map.Entry<String, Boolean> entry = it.next(); 449 450 s.append("\t").append(entry.getKey()); 451 if(entry.getValue().booleanValue()) { 452 s.append(" on error\n"); 453 } else { 454 s.append("\n"); 455 } 456 } 457 458 return s.toString(); 459 } 460 } 461 462 private class NodeFontTransformer implements Transformer<OozieWFNode, Font> { 463 private final Font font = new Font("Default", Font.BOLD, 15); 464 465 @Override 466 public Font transform(OozieWFNode node) { 467 return font; 468 } 469 } 470 471 private class ArrowShapeTransformer implements Transformer<Context<Graph<OozieWFNode, String>, String>, Shape> { 472 private final Shape arrow = ArrowFactory.getWedgeArrow(10.0f, 20.0f); 473 474 @Override 475 public Shape transform(Context<Graph<OozieWFNode, String>, String> i) { 476 return arrow; 477 } 478 } 479 480 private class ArcPaintTransformer implements Transformer<String, Paint> { 481 // Paint based on transition 482 @Override 483 public Paint transform(String arc) { 484 int sep = arc.indexOf("-->"); 485 String source = arc.substring(0, sep); 486 String target = arc.substring(sep + 3); 487 OozieWFNode src = tags.get(source); 488 OozieWFNode tgt = tags.get(target); 489 490 if(src.getType().equals("start")) { 491 if(tgt.getStatus() == null) { 492 return Color.LIGHT_GRAY; 493 } else { 494 return Color.GREEN; 495 } 496 } 497 498 if(src.getArcs().get(target)) { 499 // Dealing with error transition (i.e. target is error) 500 if(src.getStatus() == null) { 501 return Color.LIGHT_GRAY; 502 } 503 switch(src.getStatus()) { 504 case KILLED: 505 case ERROR: 506 case FAILED: 507 return Color.RED; 508 default: 509 return Color.LIGHT_GRAY; 510 } 511 } else { 512 // Non-error 513 if(src.getType().equals("decision")) { 514 // Check for target too 515 if(tgt.getStatus() != null) { 516 return Color.GREEN; 517 } else { 518 return Color.LIGHT_GRAY; 519 } 520 } else { 521 if(src.getStatus() == null) { 522 return Color.LIGHT_GRAY; 523 } 524 switch(src.getStatus()) { 525 case OK: 526 case DONE: 527 case END_RETRY: 528 case END_MANUAL: 529 return Color.GREEN; 530 default: 531 return Color.LIGHT_GRAY; 532 } 533 } 534 } 535 } 536 } 537 538 private class NodeStrokeTransformer implements Transformer<OozieWFNode, Stroke> { 539 private final Stroke stroke1 = new BasicStroke(2.0f); 540 private final Stroke stroke2 = new BasicStroke(4.0f); 541 542 @Override 543 public Stroke transform(OozieWFNode node) { 544 if(node.getType().equals("start") 545 || node.getType().equals("end") 546 || node.getType().equals("kill")) { 547 return stroke2; 548 } 549 return stroke1; 550 } 551 } 552 553 private class NodeLabelTransformer implements Transformer<OozieWFNode, String> { 554 /* 555 * 20 chars in rectangle in 2 rows max 556 * 14 chars in diamond in 2 rows max 557 * 9 in triangle in 2 rows max 558 * 8 in invtriangle in 2 rows max 559 * 8 in circle in 2 rows max 560 */ 561 @Override 562 public String transform(OozieWFNode node) { 563 //return node.getType(); 564 String name = node.getName(); 565 String type = node.getType(); 566 StringBuilder s = new StringBuilder(); 567 if(type.equals("decision")) { 568 if(name.length() <= 14) { 569 return name; 570 } else { 571 s.append("<html>").append(name.substring(0, 12)).append("-<br />"); 572 if(name.substring(13).length() > 14) { 573 s.append(name.substring(12, 25)).append("..."); 574 } else { 575 s.append(name.substring(12)); 576 } 577 s.append("</html>"); 578 return s.toString(); 579 } 580 } else if(type.equals("fork")) { 581 if(name.length() <= 9) { 582 return "<html><br />" + name + "</html>"; 583 } else { 584 s.append("<html><br />").append(name.substring(0, 7)).append("-<br />"); 585 if(name.substring(8).length() > 9) { 586 s.append(name.substring(7, 15)).append("..."); 587 } else { 588 s.append(name.substring(7)); 589 } 590 s.append("</html>"); 591 return s.toString(); 592 } 593 } else if(type.equals("join")) { 594 if(name.length() <= 8) { 595 return "<html>" + name + "</html>"; 596 } else { 597 s.append("<html>").append(name.substring(0, 6)).append("-<br />"); 598 if(name.substring(7).length() > 8) { 599 s.append(name.substring(6, 13)).append("..."); 600 } else { 601 s.append(name.substring(6)); 602 } 603 s.append("</html>"); 604 return s.toString(); 605 } 606 } else if(type.equals("start") 607 || type.equals("end") 608 || type.equals("kill")) { 609 if(name.length() <= 8) { 610 return "<html>" + name + "</html>"; 611 } else { 612 s.append("<html>").append(name.substring(0, 6)).append("-<br />"); 613 if(name.substring(7).length() > 8) { 614 s.append(name.substring(6, 13)).append("..."); 615 } else { 616 s.append(name.substring(6)); 617 } 618 s.append("</html>"); 619 return s.toString(); 620 } 621 }else { 622 if(name.length() <= 20) { 623 return name; 624 } else { 625 s.append("<html>").append(name.substring(0, 18)).append("-<br />"); 626 if(name.substring(19).length() > 20) { 627 s.append(name.substring(18, 37)).append("..."); 628 } else { 629 s.append(name.substring(18)); 630 } 631 s.append("</html>"); 632 return s.toString(); 633 } 634 } 635 } 636 } 637 638 private class NodePaintTransformer implements Transformer<OozieWFNode, Paint> { 639 @Override 640 public Paint transform(OozieWFNode node) { 641 WorkflowJob.Status jobStatus = job.getStatus(); 642 if(node.getType().equals("start")) { 643 return Color.WHITE; 644 } else if(node.getType().equals("end")) { 645 if(jobStatus == WorkflowJob.Status.SUCCEEDED) { 646 return Color.GREEN; 647 } 648 return Color.BLACK; 649 } else if(node.getType().equals("kill")) { 650 if(jobStatus == WorkflowJob.Status.FAILED 651 || jobStatus == WorkflowJob.Status.KILLED) { 652 return Color.RED; 653 } 654 return Color.WHITE; 655 } 656 657 // Paint based on status for rest 658 WorkflowAction.Status status = node.getStatus(); 659 if(status == null) { 660 return Color.LIGHT_GRAY; 661 } 662 switch(status) { 663 case OK: 664 case DONE: 665 case END_RETRY: 666 case END_MANUAL: 667 return Color.GREEN; 668 case PREP: 669 case RUNNING: 670 case USER_RETRY: 671 case START_RETRY: 672 case START_MANUAL: 673 return Color.YELLOW; 674 case KILLED: 675 case ERROR: 676 case FAILED: 677 return Color.RED; 678 default: 679 return Color.LIGHT_GRAY; 680 } 681 } 682 } 683 684 private class NodeShapeTransformer implements Transformer<OozieWFNode, Shape> { 685 private final Ellipse2D.Double circle = new Ellipse2D.Double(-40, -40, 80, 80); 686 private final Rectangle rect = new Rectangle(-100, -30, 200, 60); 687 private final Polygon diamond = new Polygon(new int[]{-75, 0, 75, 0}, new int[]{0, 75, 0, -75}, 4); 688 private final Polygon triangle = new Polygon(new int[]{-85, 85, 0}, new int[]{0, 0, -148}, 3); 689 private final Polygon invtriangle = new Polygon(new int[]{-85, 85, 0}, new int[]{0, 0, 148}, 3); 690 691 @Override 692 public Shape transform(OozieWFNode node) { 693 if("start".equals(node.getType()) 694 || "end".equals(node.getType()) 695 || "kill".equals(node.getType())) { 696 return circle; 697 } 698 if("fork".equals(node.getType())) { 699 return triangle; 700 } 701 if("join".equals(node.getType())) { 702 return invtriangle; 703 } 704 if("decision".equals(node.getType())) { 705 return diamond; 706 } 707 return rect; // All action nodes 708 } 709 } 710 711 private class ArcStrokeTransformer implements Transformer<String, Stroke> { 712 private final Stroke stroke1 = new BasicStroke(2.0f); 713 private final Stroke dashed = new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, new float[] {10.0f}, 0.0f); 714 715 // Draw based on transition 716 @Override 717 public Stroke transform(String arc) { 718 int sep = arc.indexOf("-->"); 719 String source = arc.substring(0, sep); 720 String target = arc.substring(sep + 3); 721 OozieWFNode src = tags.get(source); 722 if(src.getArcs().get(target)) { 723 if(src.getStatus() == null) { 724 return dashed; 725 } 726 switch(src.getStatus()) { 727 case KILLED: 728 case ERROR: 729 case FAILED: 730 return stroke1; 731 default: 732 return dashed; 733 } 734 } else { 735 return stroke1; 736 } 737 } 738 } 739 } 740}