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