This project has retired. For details please refer to its
Attic page.
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 final 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 }
262 catch (IOException ioe) {
263 throw new SAXException(ioe);
264 }
265 finally {
266 try {
267 out.close(); //closing connection is imperative
268 //regardless of ImageIO.write throwing exception or not
269 //hence in finally block
270 }
271 catch (IOException e) {
272 XLog.getLog(getClass()).trace("Exception while closing OutputStream");
273 }
274 out = null;
275 img.flush();
276 g.dispose();
277 vis.removeAll();
278 }
279 }
280
281 @Override
282 public void startElement(String namespaceURI,
283 String localName,
284 String qName,
285 Attributes atts)
286 throws SAXException {
287
288 if(localName.equalsIgnoreCase("start")) {
289 String start = localName.toLowerCase();
290 if(!tags.containsKey(start)) {
291 OozieWFNode v = new OozieWFNode(start, start);
292 v.addArc(atts.getValue("to"));
293 tags.put(start, v);
294 }
295 } else if(localName.equalsIgnoreCase("action")) {
296 action = atts.getValue("name");
297 } else if(action != null && actionType == null) {
298 actionType = localName.toLowerCase();
299 } else if(localName.equalsIgnoreCase("ok") && action != null && actionOK == null) {
300 actionOK = atts.getValue("to");
301 } else if(localName.equalsIgnoreCase("error") && action != null && actionErr == null) {
302 actionErr = atts.getValue("to");
303 } else if(localName.equalsIgnoreCase("fork")) {
304 fork = atts.getValue("name");
305 if(!tags.containsKey(fork)) {
306 tags.put(fork, new OozieWFNode(fork, localName.toLowerCase()));
307 }
308 } else if(localName.equalsIgnoreCase("path")) {
309 tags.get(fork).addArc(atts.getValue("start"));
310 } else if(localName.equalsIgnoreCase("join")) {
311 String join = atts.getValue("name");
312 if(!tags.containsKey(join)) {
313 OozieWFNode v = new OozieWFNode(join, localName.toLowerCase());
314 v.addArc(atts.getValue("to"));
315 tags.put(join, v);
316 }
317 } else if(localName.equalsIgnoreCase("decision")) {
318 decision = atts.getValue("name");
319 if(!tags.containsKey(decision)) {
320 tags.put(decision, new OozieWFNode(decision, localName.toLowerCase()));
321 }
322 } else if(localName.equalsIgnoreCase("case")
323 || localName.equalsIgnoreCase("default")) {
324 tags.get(decision).addArc(atts.getValue("to"));
325 } else if(localName.equalsIgnoreCase("kill")
326 || localName.equalsIgnoreCase("end")) {
327 String name = atts.getValue("name");
328 if(!tags.containsKey(name)) {
329 tags.put(name, new OozieWFNode(name, localName.toLowerCase()));
330 }
331 }
332 }
333
334 @Override
335 public void endElement(String namespaceURI,
336 String localName,
337 String qName)
338 throws SAXException {
339 if(localName.equalsIgnoreCase("action")) {
340 tags.put(action, new OozieWFNode(action, actionType));
341 tags.get(action).addArc(this.actionOK);
342 tags.get(action).addArc(this.actionErr, true);
343 action = null;
344 actionOK = null;
345 actionErr = null;
346 actionType = null;
347 }
348 }
349
350 private class OozieWFNode {
351 private String name;
352 private String type;
353 private Point loc;
354 private HashMap<String, Boolean> arcs;
355 private Status status = null;
356
357 public OozieWFNode(String name,
358 String type,
359 HashMap<String, Boolean> arcs,
360 Point loc,
361 Status status) {
362 this.name = name;
363 this.type = type;
364 this.arcs = arcs;
365 this.loc = loc;
366 this.status = status;
367 }
368
369 public OozieWFNode(String name, String type, HashMap<String, Boolean> arcs) {
370 this(name, type, arcs, new Point(0, 0), null);
371 }
372
373 public OozieWFNode(String name, String type) {
374 this(name, type, new HashMap<String, Boolean>(), new Point(0, 0), null);
375 }
376
377 public OozieWFNode(String name, String type, WorkflowAction.Status status) {
378 this(name, type, new HashMap<String, Boolean>(), new Point(0, 0), status);
379 }
380
381 public void addArc(String arc, boolean isError) {
382 arcs.put(arc, isError);
383 }
384
385 public void addArc(String arc) {
386 addArc(arc, false);
387 }
388
389 public void setName(String name) {
390 this.name = name;
391 }
392
393 public void setType(String type) {
394 this.type = type;
395 }
396
397 public void setLocation(Point loc) {
398 this.loc = loc;
399 }
400
401 public void setLocation(double x, double y) {
402 loc.setLocation(x, y);
403 }
404
405 public void setStatus(WorkflowAction.Status status) {
406 this.status = status;
407 }
408
409 public String getName() {
410 return name;
411 }
412
413 public String getType() {
414 return type;
415 }
416
417 public HashMap<String, Boolean> getArcs() {
418 return arcs;
419 }
420
421 public Point getLocation() {
422 return loc;
423 }
424
425 public WorkflowAction.Status getStatus() {
426 return status;
427 }
428
429 @Override
430 public String toString() {
431 StringBuilder s = new StringBuilder();
432
433 s.append("Node: ").append(name).append("\t");
434 s.append("Type: ").append(type).append("\t");
435 s.append("Location: (").append(loc.getX()).append(", ").append(loc.getY()).append(")\t");
436 s.append("Status: ").append(status).append("\n");
437 Iterator<Map.Entry<String, Boolean>> it = arcs.entrySet().iterator();
438 while(it.hasNext()) {
439 Map.Entry<String, Boolean> entry = it.next();
440
441 s.append("\t").append(entry.getKey());
442 if(entry.getValue().booleanValue()) {
443 s.append(" on error\n");
444 } else {
445 s.append("\n");
446 }
447 }
448
449 return s.toString();
450 }
451 }
452
453 private class NodeFontTransformer implements Transformer<OozieWFNode, Font> {
454 private final Font font = new Font("Default", Font.BOLD, 15);
455
456 @Override
457 public Font transform(OozieWFNode node) {
458 return font;
459 }
460 }
461
462 private class ArrowShapeTransformer implements Transformer<Context<Graph<OozieWFNode, String>, String>, Shape> {
463 private final Shape arrow = ArrowFactory.getWedgeArrow(10.0f, 20.0f);
464
465 @Override
466 public Shape transform(Context<Graph<OozieWFNode, String>, String> i) {
467 return arrow;
468 }
469 }
470
471 private class ArcPaintTransformer implements Transformer<String, Paint> {
472 // Paint based on transition
473 @Override
474 public Paint transform(String arc) {
475 int sep = arc.indexOf("-->");
476 String source = arc.substring(0, sep);
477 String target = arc.substring(sep + 3);
478 OozieWFNode src = tags.get(source);
479 OozieWFNode tgt = tags.get(target);
480
481 if(src.getType().equals("start")) {
482 if(tgt.getStatus() == null) {
483 return Color.LIGHT_GRAY;
484 } else {
485 return Color.GREEN;
486 }
487 }
488
489 if(src.getArcs().get(target)) {
490 // Dealing with error transition (i.e. target is error)
491 if(src.getStatus() == null) {
492 return Color.LIGHT_GRAY;
493 }
494 switch(src.getStatus()) {
495 case KILLED:
496 case ERROR:
497 case FAILED:
498 return Color.RED;
499 default:
500 return Color.LIGHT_GRAY;
501 }
502 } else {
503 // Non-error
504 if(src.getType().equals("decision")) {
505 // Check for target too
506 if(tgt.getStatus() != null) {
507 return Color.GREEN;
508 } else {
509 return Color.LIGHT_GRAY;
510 }
511 } else {
512 if(src.getStatus() == null) {
513 return Color.LIGHT_GRAY;
514 }
515 switch(src.getStatus()) {
516 case OK:
517 case DONE:
518 case END_RETRY:
519 case END_MANUAL:
520 return Color.GREEN;
521 default:
522 return Color.LIGHT_GRAY;
523 }
524 }
525 }
526 }
527 }
528
529 private class NodeStrokeTransformer implements Transformer<OozieWFNode, Stroke> {
530 private final Stroke stroke1 = new BasicStroke(2.0f);
531 private final Stroke stroke2 = new BasicStroke(4.0f);
532
533 @Override
534 public Stroke transform(OozieWFNode node) {
535 if(node.getType().equals("start")
536 || node.getType().equals("end")
537 || node.getType().equals("kill")) {
538 return stroke2;
539 }
540 return stroke1;
541 }
542 }
543
544 private class NodeLabelTransformer implements Transformer<OozieWFNode, String> {
545 /*
546 * 20 chars in rectangle in 2 rows max
547 * 14 chars in diamond in 2 rows max
548 * 9 in triangle in 2 rows max
549 * 8 in invtriangle in 2 rows max
550 * 8 in circle in 2 rows max
551 */
552 @Override
553 public String transform(OozieWFNode node) {
554 //return node.getType();
555 String name = node.getName();
556 String type = node.getType();
557 StringBuilder s = new StringBuilder();
558 if(type.equals("decision")) {
559 if(name.length() <= 14) {
560 return name;
561 } else {
562 s.append("<html>").append(name.substring(0, 12)).append("-<br />");
563 if(name.substring(13).length() > 14) {
564 s.append(name.substring(12, 25)).append("...");
565 } else {
566 s.append(name.substring(12));
567 }
568 s.append("</html>");
569 return s.toString();
570 }
571 } else if(type.equals("fork")) {
572 if(name.length() <= 9) {
573 return "<html><br />" + name + "</html>";
574 } else {
575 s.append("<html><br />").append(name.substring(0, 7)).append("-<br />");
576 if(name.substring(8).length() > 9) {
577 s.append(name.substring(7, 15)).append("...");
578 } else {
579 s.append(name.substring(7));
580 }
581 s.append("</html>");
582 return s.toString();
583 }
584 } else if(type.equals("join")) {
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 if(type.equals("start")
598 || type.equals("end")
599 || type.equals("kill")) {
600 if(name.length() <= 8) {
601 return "<html>" + name + "</html>";
602 } else {
603 s.append("<html>").append(name.substring(0, 6)).append("-<br />");
604 if(name.substring(7).length() > 8) {
605 s.append(name.substring(6, 13)).append("...");
606 } else {
607 s.append(name.substring(6));
608 }
609 s.append("</html>");
610 return s.toString();
611 }
612 }else {
613 if(name.length() <= 20) {
614 return name;
615 } else {
616 s.append("<html>").append(name.substring(0, 18)).append("-<br />");
617 if(name.substring(19).length() > 20) {
618 s.append(name.substring(18, 37)).append("...");
619 } else {
620 s.append(name.substring(18));
621 }
622 s.append("</html>");
623 return s.toString();
624 }
625 }
626 }
627 }
628
629 private class NodePaintTransformer implements Transformer<OozieWFNode, Paint> {
630 @Override
631 public Paint transform(OozieWFNode node) {
632 WorkflowJob.Status jobStatus = job.getStatus();
633 if(node.getType().equals("start")) {
634 return Color.WHITE;
635 } else if(node.getType().equals("end")) {
636 if(jobStatus == WorkflowJob.Status.SUCCEEDED) {
637 return Color.GREEN;
638 }
639 return Color.BLACK;
640 } else if(node.getType().equals("kill")) {
641 if(jobStatus == WorkflowJob.Status.FAILED
642 || jobStatus == WorkflowJob.Status.KILLED) {
643 return Color.RED;
644 }
645 return Color.WHITE;
646 }
647
648 // Paint based on status for rest
649 WorkflowAction.Status status = node.getStatus();
650 if(status == null) {
651 return Color.LIGHT_GRAY;
652 }
653 switch(status) {
654 case OK:
655 case DONE:
656 case END_RETRY:
657 case END_MANUAL:
658 return Color.GREEN;
659 case PREP:
660 case RUNNING:
661 case USER_RETRY:
662 case START_RETRY:
663 case START_MANUAL:
664 return Color.YELLOW;
665 case KILLED:
666 case ERROR:
667 case FAILED:
668 return Color.RED;
669 default:
670 return Color.LIGHT_GRAY;
671 }
672 }
673 }
674
675 private class NodeShapeTransformer implements Transformer<OozieWFNode, Shape> {
676 private final Ellipse2D.Double circle = new Ellipse2D.Double(-40, -40, 80, 80);
677 private final Rectangle rect = new Rectangle(-100, -30, 200, 60);
678 private final Polygon diamond = new Polygon(new int[]{-75, 0, 75, 0}, new int[]{0, 75, 0, -75}, 4);
679 private final Polygon triangle = new Polygon(new int[]{-85, 85, 0}, new int[]{0, 0, -148}, 3);
680 private final Polygon invtriangle = new Polygon(new int[]{-85, 85, 0}, new int[]{0, 0, 148}, 3);
681
682 @Override
683 public Shape transform(OozieWFNode node) {
684 if("start".equals(node.getType())
685 || "end".equals(node.getType())
686 || "kill".equals(node.getType())) {
687 return circle;
688 }
689 if("fork".equals(node.getType())) {
690 return triangle;
691 }
692 if("join".equals(node.getType())) {
693 return invtriangle;
694 }
695 if("decision".equals(node.getType())) {
696 return diamond;
697 }
698 return rect; // All action nodes
699 }
700 }
701
702 private class ArcStrokeTransformer implements Transformer<String, Stroke> {
703 private final Stroke stroke1 = new BasicStroke(2.0f);
704 private final Stroke dashed = new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, new float[] {10.0f}, 0.0f);
705
706 // Draw based on transition
707 @Override
708 public Stroke transform(String arc) {
709 int sep = arc.indexOf("-->");
710 String source = arc.substring(0, sep);
711 String target = arc.substring(sep + 3);
712 OozieWFNode src = tags.get(source);
713 if(src.getArcs().get(target)) {
714 if(src.getStatus() == null) {
715 return dashed;
716 }
717 switch(src.getStatus()) {
718 case KILLED:
719 case ERROR:
720 case FAILED:
721 return stroke1;
722 default:
723 return dashed;
724 }
725 } else {
726 return stroke1;
727 }
728 }
729 }
730 }
731 }