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