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    }