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