org.apache.oozie.util.GraphGenerator.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.oozie.util.GraphGenerator.java

Source

/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.oozie.util;

import edu.uci.ics.jung.algorithms.layout.StaticLayout;
import edu.uci.ics.jung.graph.DirectedSparseGraph;
import edu.uci.ics.jung.graph.Graph;
import edu.uci.ics.jung.graph.util.Context;
import edu.uci.ics.jung.visualization.VisualizationImageServer;
import edu.uci.ics.jung.visualization.renderers.Renderer;
import edu.uci.ics.jung.visualization.util.ArrowFactory;
import org.apache.commons.collections15.Transformer;
import org.apache.oozie.WorkflowJobBean;
import org.apache.oozie.client.WorkflowAction;
import org.apache.oozie.client.WorkflowAction.Status;
import org.apache.oozie.client.WorkflowJob;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;

import javax.imageio.ImageIO;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import java.awt.*;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.OutputStream;
import java.io.StringReader;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * Class to generate and plot runtime workflow DAG
 */
public class GraphGenerator {

    private String xml;
    private WorkflowJobBean job;
    private boolean showKill = false;
    private final int actionsLimit = 25;

    /**
     * C'tor
     * @param xml The workflow definition XML
     * @param job Current status of the job
     * @param showKill Flag to whether show 'kill' node
     */
    public GraphGenerator(String xml, WorkflowJobBean job, boolean showKill) {
        if (job == null) {
            throw new IllegalArgumentException("JsonWorkflowJob can't be null");
        }
        this.xml = xml;
        this.job = job;
        this.showKill = showKill;
    }

    /**
     * C'tor
     * @param xml
     * @param job
     */
    public GraphGenerator(String xml, WorkflowJobBean job) {
        this(xml, job, false);
    }

    /**
     * Overridden to thwart finalizer attack
     */
    @Override
    public final void finalize() {
        // No-op; just to avoid finalizer attack
        // as the constructor is throwing an exception
    }

    /**
     * Stream the PNG file to client
     * @param out
     * @throws Exception
     */
    public void write(OutputStream out) throws Exception {
        SAXParserFactory spf = SAXParserFactory.newInstance();
        spf.setFeature("http://xml.org/sax/features/external-general-entities", false);
        spf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
        spf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
        spf.setNamespaceAware(true);
        SAXParser saxParser = spf.newSAXParser();
        XMLReader xmlReader = saxParser.getXMLReader();
        xmlReader.setContentHandler(new XMLParser(out));
        xmlReader.parse(new InputSource(new StringReader(xml)));
    }

    private class XMLParser extends DefaultHandler {

        private OutputStream out;
        private LinkedHashMap<String, OozieWFNode> tags;

        private String action = null;
        private String actionOK = null;
        private String actionErr = null;
        private String actionType = null;
        private String fork;
        private String decision;

        public XMLParser(OutputStream out) {
            this.out = out;
        }

        @Override
        public void startDocument() throws SAXException {
            tags = new LinkedHashMap();
        }

        @Override
        public void endDocument() throws SAXException {

            if (tags.isEmpty()) {
                // Nothing to do here!
                return;
            }

            int maxX = Integer.MIN_VALUE;
            int maxY = Integer.MIN_VALUE;
            int minX = Integer.MAX_VALUE;
            int currX = 45;
            int currY = 45;
            final int xMargin = 205;
            final int yMargin = 50;
            final int xIncr = 215; // The widest element is 200 pixels (Rectangle)
            final int yIncr = 255; // The tallest element is 150 pixels; (Diamond)
            HashMap<String, WorkflowAction> actionMap = new HashMap<String, WorkflowAction>();

            // Create a hashmap for faster lookups
            // Also override showKill if there's any failed action
            boolean found = false;
            for (WorkflowAction wfAction : job.getActions()) {
                actionMap.put(wfAction.getName(), wfAction);
                if (!found) {
                    switch (wfAction.getStatus()) {
                    case KILLED:
                    case ERROR:
                    case FAILED:
                        showKill = true; // Assuming on error the workflow eventually ends with kill node
                        found = true;
                    }
                }
            }

            // Start building the graph
            DirectedSparseGraph<OozieWFNode, String> dg = new DirectedSparseGraph<OozieWFNode, String>();
            for (Map.Entry<String, OozieWFNode> entry : tags.entrySet()) {
                String name = entry.getKey();
                OozieWFNode node = entry.getValue();
                if (actionMap.containsKey(name)) {
                    node.setStatus(actionMap.get(name).getStatus());
                }

                // Set (x,y) coords of the vertices if not already set
                if (node.getLocation().equals(new Point(0, 0))) {
                    node.setLocation(currX, currY);
                }

                float childStep = showKill ? -(((float) node.getArcs().size() - 1) / 2)
                        : -((float) node.getArcs().size() / 2 - 1);
                int nodeX = node.getLocation().x;
                int nodeY = node.getLocation().y;
                for (Map.Entry<String, Boolean> arc : node.getArcs().entrySet()) {
                    if (!showKill && arc.getValue() && tags.get(arc.getKey()).getType().equals("kill")) {
                        // Don't show kill node (assumption: only error goes to kill node;
                        // No ok goes to kill node)
                        continue;
                    }
                    OozieWFNode child = tags.get(arc.getKey());
                    if (child == null) {
                        continue; // or throw error?
                    }
                    dg.addEdge(name + "-->" + arc.getKey(), node, child);
                    // TODO: Experimental -- should we set coords even if they're already set?
                    //if(child.getLocation().equals(new Point(0, 0))) {
                    int childX = (int) (nodeX + childStep * xIncr);
                    int childY = nodeY + yIncr;
                    child.setLocation(childX, childY);

                    if (minX > childX) {
                        minX = childX;
                    }
                    if (maxX < childX) {
                        maxX = childX;
                    }
                    if (maxY < childY) {
                        maxY = childY;
                    }
                    //}
                    childStep += 1;
                }

                currY += yIncr;
                currX = nodeX;
                if (minX > nodeX) {
                    minX = nodeX;
                }
                if (maxX < nodeX) {
                    maxX = nodeX;
                }
                if (maxY < nodeY) {
                    maxY = nodeY;
                }
            } // Done building graph

            final int padX = minX < 0 ? -minX : 0;

            Transformer<OozieWFNode, Point2D> locationInit = new Transformer<OozieWFNode, Point2D>() {

                @Override
                public Point2D transform(OozieWFNode node) {
                    if (padX == 0) {
                        return node.getLocation();
                    } else {
                        return new Point(node.getLocation().x + padX + xMargin, node.getLocation().y);
                    }
                }

            };

            StaticLayout<OozieWFNode, String> layout = new StaticLayout<OozieWFNode, String>(dg, locationInit,
                    new Dimension(maxX + padX + xMargin, maxY));
            layout.lock(true);
            VisualizationImageServer<OozieWFNode, String> vis = new VisualizationImageServer<OozieWFNode, String>(
                    layout, new Dimension(maxX + padX + 2 * xMargin, maxY + yMargin));

            vis.getRenderContext().setEdgeArrowTransformer(new ArrowShapeTransformer());
            vis.getRenderContext().setArrowDrawPaintTransformer(new ArcPaintTransformer());
            vis.getRenderContext().setEdgeDrawPaintTransformer(new ArcPaintTransformer());
            vis.getRenderContext().setEdgeStrokeTransformer(new ArcStrokeTransformer());
            vis.getRenderContext().setVertexShapeTransformer(new NodeShapeTransformer());
            vis.getRenderContext().setVertexFillPaintTransformer(new NodePaintTransformer());
            vis.getRenderContext().setVertexStrokeTransformer(new NodeStrokeTransformer());
            vis.getRenderContext().setVertexLabelTransformer(new NodeLabelTransformer());
            vis.getRenderContext().setVertexFontTransformer(new NodeFontTransformer());
            vis.getRenderer().getVertexLabelRenderer().setPosition(Renderer.VertexLabel.Position.CNTR);
            vis.setBackground(Color.WHITE);

            Dimension d = vis.getSize();
            BufferedImage img = new BufferedImage(d.width, d.height, BufferedImage.TYPE_INT_RGB);
            Graphics2D g = img.createGraphics();
            vis.paintAll(g);

            try {
                ImageIO.write(img, "png", out);
            } catch (IOException ioe) {
                throw new SAXException(ioe);
            } finally {
                try {
                    out.close(); //closing connection is imperative
                                 //regardless of ImageIO.write throwing exception or not
                                 //hence in finally block
                } catch (IOException e) {
                    XLog.getLog(getClass()).trace("Exception while closing OutputStream");
                }
                out = null;
                img.flush();
                g.dispose();
                vis.removeAll();
            }
        }

        @Override
        public void startElement(String namespaceURI, String localName, String qName, Attributes atts)
                throws SAXException {
            if (localName.equalsIgnoreCase("start")) {
                String start = localName.toLowerCase();
                if (!tags.containsKey(start)) {
                    OozieWFNode v = new OozieWFNode(start, start);
                    v.addArc(atts.getValue("to"));
                    tags.put(start, v);
                }
            } else if (localName.equalsIgnoreCase("action")) {
                action = atts.getValue("name");
            } else if (action != null && actionType == null) {
                actionType = localName.toLowerCase();
            } else if (localName.equalsIgnoreCase("ok") && action != null && actionOK == null) {
                actionOK = atts.getValue("to");
            } else if (localName.equalsIgnoreCase("error") && action != null && actionErr == null) {
                actionErr = atts.getValue("to");
            } else if (localName.equalsIgnoreCase("fork")) {
                fork = atts.getValue("name");
                if (!tags.containsKey(fork)) {
                    tags.put(fork, new OozieWFNode(fork, localName.toLowerCase()));
                }
            } else if (localName.equalsIgnoreCase("path")) {
                tags.get(fork).addArc(atts.getValue("start"));
            } else if (localName.equalsIgnoreCase("join")) {
                String join = atts.getValue("name");
                if (!tags.containsKey(join)) {
                    OozieWFNode v = new OozieWFNode(join, localName.toLowerCase());
                    v.addArc(atts.getValue("to"));
                    tags.put(join, v);
                }
            } else if (localName.equalsIgnoreCase("decision")) {
                decision = atts.getValue("name");
                if (!tags.containsKey(decision)) {
                    tags.put(decision, new OozieWFNode(decision, localName.toLowerCase()));
                }
            } else if (localName.equalsIgnoreCase("case") || localName.equalsIgnoreCase("default")) {
                tags.get(decision).addArc(atts.getValue("to"));
            } else if (localName.equalsIgnoreCase("kill") || localName.equalsIgnoreCase("end")) {
                String name = atts.getValue("name");
                if (!tags.containsKey(name)) {
                    tags.put(name, new OozieWFNode(name, localName.toLowerCase()));
                }
            }
            if (tags.size() > actionsLimit) {
                tags.clear();
                throw new SAXException(
                        "Can't display the graph. Number of actions are more than display limit " + actionsLimit);
            }
        }

        @Override
        public void endElement(String namespaceURI, String localName, String qName) throws SAXException {
            if (localName.equalsIgnoreCase("action")) {
                tags.put(action, new OozieWFNode(action, actionType));
                tags.get(action).addArc(this.actionOK);
                tags.get(action).addArc(this.actionErr, true);
                action = null;
                actionOK = null;
                actionErr = null;
                actionType = null;
            }
        }

        private class OozieWFNode {
            private String name;
            private String type;
            private Point loc;
            private HashMap<String, Boolean> arcs;
            private Status status = null;

            public OozieWFNode(String name, String type, HashMap<String, Boolean> arcs, Point loc, Status status) {
                this.name = name;
                this.type = type;
                this.arcs = arcs;
                this.loc = loc;
                this.status = status;
            }

            public OozieWFNode(String name, String type, HashMap<String, Boolean> arcs) {
                this(name, type, arcs, new Point(0, 0), null);
            }

            public OozieWFNode(String name, String type) {
                this(name, type, new HashMap<String, Boolean>(), new Point(0, 0), null);
            }

            public OozieWFNode(String name, String type, WorkflowAction.Status status) {
                this(name, type, new HashMap<String, Boolean>(), new Point(0, 0), status);
            }

            public void addArc(String arc, boolean isError) {
                arcs.put(arc, isError);
            }

            public void addArc(String arc) {
                addArc(arc, false);
            }

            public void setName(String name) {
                this.name = name;
            }

            public void setType(String type) {
                this.type = type;
            }

            public void setLocation(Point loc) {
                this.loc = loc;
            }

            public void setLocation(double x, double y) {
                loc.setLocation(x, y);
            }

            public void setStatus(WorkflowAction.Status status) {
                this.status = status;
            }

            public String getName() {
                return name;
            }

            public String getType() {
                return type;
            }

            public HashMap<String, Boolean> getArcs() {
                return arcs;
            }

            public Point getLocation() {
                return loc;
            }

            public WorkflowAction.Status getStatus() {
                return status;
            }

            @Override
            public String toString() {
                StringBuilder s = new StringBuilder();

                s.append("Node: ").append(name).append("\t");
                s.append("Type: ").append(type).append("\t");
                s.append("Location: (").append(loc.getX()).append(", ").append(loc.getY()).append(")\t");
                s.append("Status: ").append(status).append("\n");
                Iterator<Map.Entry<String, Boolean>> it = arcs.entrySet().iterator();
                while (it.hasNext()) {
                    Map.Entry<String, Boolean> entry = it.next();

                    s.append("\t").append(entry.getKey());
                    if (entry.getValue().booleanValue()) {
                        s.append(" on error\n");
                    } else {
                        s.append("\n");
                    }
                }

                return s.toString();
            }
        }

        private class NodeFontTransformer implements Transformer<OozieWFNode, Font> {
            private final Font font = new Font("Default", Font.BOLD, 15);

            @Override
            public Font transform(OozieWFNode node) {
                return font;
            }
        }

        private class ArrowShapeTransformer
                implements Transformer<Context<Graph<OozieWFNode, String>, String>, Shape> {
            private final Shape arrow = ArrowFactory.getWedgeArrow(10.0f, 20.0f);

            @Override
            public Shape transform(Context<Graph<OozieWFNode, String>, String> i) {
                return arrow;
            }
        }

        private class ArcPaintTransformer implements Transformer<String, Paint> {
            // Paint based on transition
            @Override
            public Paint transform(String arc) {
                int sep = arc.indexOf("-->");
                String source = arc.substring(0, sep);
                String target = arc.substring(sep + 3);
                OozieWFNode src = tags.get(source);
                OozieWFNode tgt = tags.get(target);

                if (src.getType().equals("start")) {
                    if (tgt.getStatus() == null) {
                        return Color.LIGHT_GRAY;
                    } else {
                        return Color.GREEN;
                    }
                }

                if (src.getArcs().get(target)) {
                    // Dealing with error transition (i.e. target is error)
                    if (src.getStatus() == null) {
                        return Color.LIGHT_GRAY;
                    }
                    switch (src.getStatus()) {
                    case KILLED:
                    case ERROR:
                    case FAILED:
                        return Color.RED;
                    default:
                        return Color.LIGHT_GRAY;
                    }
                } else {
                    // Non-error
                    if (src.getType().equals("decision")) {
                        // Check for target too
                        if (tgt.getStatus() != null) {
                            return Color.GREEN;
                        } else {
                            return Color.LIGHT_GRAY;
                        }
                    } else {
                        if (src.getStatus() == null) {
                            return Color.LIGHT_GRAY;
                        }
                        switch (src.getStatus()) {
                        case OK:
                        case DONE:
                        case END_RETRY:
                        case END_MANUAL:
                            return Color.GREEN;
                        default:
                            return Color.LIGHT_GRAY;
                        }
                    }
                }
            }
        }

        private class NodeStrokeTransformer implements Transformer<OozieWFNode, Stroke> {
            private final Stroke stroke1 = new BasicStroke(2.0f);
            private final Stroke stroke2 = new BasicStroke(4.0f);

            @Override
            public Stroke transform(OozieWFNode node) {
                if (node.getType().equals("start") || node.getType().equals("end")
                        || node.getType().equals("kill")) {
                    return stroke2;
                }
                return stroke1;
            }
        }

        private class NodeLabelTransformer implements Transformer<OozieWFNode, String> {
            /*
            * 20 chars in rectangle in 2 rows max
            * 14 chars in diamond in 2 rows max
            * 9 in triangle in 2 rows max
            * 8 in invtriangle in 2 rows max
            * 8 in circle in 2 rows max
            */
            @Override
            public String transform(OozieWFNode node) {
                //return node.getType();
                String name = node.getName();
                String type = node.getType();
                StringBuilder s = new StringBuilder();
                if (type.equals("decision")) {
                    if (name.length() <= 14) {
                        return name;
                    } else {
                        s.append("<html>").append(name.substring(0, 12)).append("-<br />");
                        if (name.substring(13).length() > 14) {
                            s.append(name.substring(12, 25)).append("...");
                        } else {
                            s.append(name.substring(12));
                        }
                        s.append("</html>");
                        return s.toString();
                    }
                } else if (type.equals("fork")) {
                    if (name.length() <= 9) {
                        return "<html><br />" + name + "</html>";
                    } else {
                        s.append("<html><br />").append(name.substring(0, 7)).append("-<br />");
                        if (name.substring(8).length() > 9) {
                            s.append(name.substring(7, 15)).append("...");
                        } else {
                            s.append(name.substring(7));
                        }
                        s.append("</html>");
                        return s.toString();
                    }
                } else if (type.equals("join")) {
                    if (name.length() <= 8) {
                        return "<html>" + name + "</html>";
                    } else {
                        s.append("<html>").append(name.substring(0, 6)).append("-<br />");
                        if (name.substring(7).length() > 8) {
                            s.append(name.substring(6, 13)).append("...");
                        } else {
                            s.append(name.substring(6));
                        }
                        s.append("</html>");
                        return s.toString();
                    }
                } else if (type.equals("start") || type.equals("end") || type.equals("kill")) {
                    if (name.length() <= 8) {
                        return "<html>" + name + "</html>";
                    } else {
                        s.append("<html>").append(name.substring(0, 6)).append("-<br />");
                        if (name.substring(7).length() > 8) {
                            s.append(name.substring(6, 13)).append("...");
                        } else {
                            s.append(name.substring(6));
                        }
                        s.append("</html>");
                        return s.toString();
                    }
                } else {
                    if (name.length() <= 20) {
                        return name;
                    } else {
                        s.append("<html>").append(name.substring(0, 18)).append("-<br />");
                        if (name.substring(19).length() > 20) {
                            s.append(name.substring(18, 37)).append("...");
                        } else {
                            s.append(name.substring(18));
                        }
                        s.append("</html>");
                        return s.toString();
                    }
                }
            }
        }

        private class NodePaintTransformer implements Transformer<OozieWFNode, Paint> {
            @Override
            public Paint transform(OozieWFNode node) {
                WorkflowJob.Status jobStatus = job.getStatus();
                if (node.getType().equals("start")) {
                    return Color.WHITE;
                } else if (node.getType().equals("end")) {
                    if (jobStatus == WorkflowJob.Status.SUCCEEDED) {
                        return Color.GREEN;
                    }
                    return Color.BLACK;
                } else if (node.getType().equals("kill")) {
                    if (jobStatus == WorkflowJob.Status.FAILED || jobStatus == WorkflowJob.Status.KILLED) {
                        return Color.RED;
                    }
                    return Color.WHITE;
                }

                // Paint based on status for rest
                WorkflowAction.Status status = node.getStatus();
                if (status == null) {
                    return Color.LIGHT_GRAY;
                }
                switch (status) {
                case OK:
                case DONE:
                case END_RETRY:
                case END_MANUAL:
                    return Color.GREEN;
                case PREP:
                case RUNNING:
                case USER_RETRY:
                case START_RETRY:
                case START_MANUAL:
                    return Color.YELLOW;
                case KILLED:
                case ERROR:
                case FAILED:
                    return Color.RED;
                default:
                    return Color.LIGHT_GRAY;
                }
            }
        }

        private class NodeShapeTransformer implements Transformer<OozieWFNode, Shape> {
            private final Ellipse2D.Double circle = new Ellipse2D.Double(-40, -40, 80, 80);
            private final Rectangle rect = new Rectangle(-100, -30, 200, 60);
            private final Polygon diamond = new Polygon(new int[] { -75, 0, 75, 0 }, new int[] { 0, 75, 0, -75 },
                    4);
            private final Polygon triangle = new Polygon(new int[] { -85, 85, 0 }, new int[] { 0, 0, -148 }, 3);
            private final Polygon invtriangle = new Polygon(new int[] { -85, 85, 0 }, new int[] { 0, 0, 148 }, 3);

            @Override
            public Shape transform(OozieWFNode node) {
                if ("start".equals(node.getType()) || "end".equals(node.getType())
                        || "kill".equals(node.getType())) {
                    return circle;
                }
                if ("fork".equals(node.getType())) {
                    return triangle;
                }
                if ("join".equals(node.getType())) {
                    return invtriangle;
                }
                if ("decision".equals(node.getType())) {
                    return diamond;
                }
                return rect; // All action nodes
            }
        }

        private class ArcStrokeTransformer implements Transformer<String, Stroke> {
            private final Stroke stroke1 = new BasicStroke(2.0f);
            private final Stroke dashed = new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f,
                    new float[] { 10.0f }, 0.0f);

            // Draw based on transition
            @Override
            public Stroke transform(String arc) {
                int sep = arc.indexOf("-->");
                String source = arc.substring(0, sep);
                String target = arc.substring(sep + 3);
                OozieWFNode src = tags.get(source);
                if (src.getArcs().get(target)) {
                    if (src.getStatus() == null) {
                        return dashed;
                    }
                    switch (src.getStatus()) {
                    case KILLED:
                    case ERROR:
                    case FAILED:
                        return stroke1;
                    default:
                        return dashed;
                    }
                } else {
                    return stroke1;
                }
            }
        }
    }
}