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 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 }