001    /*
002    // $Id: Clapham.java 3 2009-05-11 08:11:57Z jhyde $
003    // Clapham generates railroad diagrams to represent computer language grammars.
004    // Copyright (C) 2008-2009 Julian Hyde
005    //
006    // This program is free software; you can redistribute it and/or modify it
007    // under the terms of the GNU General Public License as published by the Free
008    // Software Foundation; either version 2 of the License, or (at your option)
009    // any later version approved by The Eigenbase Project.
010    //
011    // This program is distributed in the hope that it will be useful,
012    // but WITHOUT ANY WARRANTY; without even the implied warranty of
013    // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
014    // GNU General Public License for more details.
015    //
016    // You should have received a copy of the GNU General Public License
017    // along with this program; if not, write to the Free Software
018    // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
019    */
020    package net.hydromatic.clapham;
021    
022    import net.hydromatic.clapham.parser.bnf.BnfParser;
023    import net.hydromatic.clapham.parser.bnf.ParseException;
024    import net.hydromatic.clapham.parser.*;
025    import net.hydromatic.clapham.parser.wirth.WirthParser;
026    import net.hydromatic.clapham.graph.*;
027    
028    import javax.xml.parsers.*;
029    import java.io.*;
030    import java.util.*;
031    
032    import org.apache.batik.svggen.SVGGraphics2D;
033    import org.apache.batik.transcoder.*;
034    import org.apache.batik.transcoder.image.PNGTranscoder;
035    import org.w3c.dom.Document;
036    
037    /**
038     * Command line utility Clapham, the railroad diagram generator.
039     *
040     * @author jhyde
041     * @version $Id: Clapham.java 3 2009-05-11 08:11:57Z jhyde $
042     * @since Sep 11, 2008
043     */
044    public class Clapham {
045        private File outputDir;
046        private boolean outputEscapeFilename;
047        private final HashSet<String> fileNameSet = new HashSet<String>();
048        private final List<String> nameList = new ArrayList<String>();
049        private Grammar grammar;
050        private EnumSet<ImageFormat> imageFormatSet;
051        private Map<Pair<Symbol, ImageFormat>, File> imageFileNames =
052            new HashMap<Pair<Symbol, ImageFormat>, File>();
053        private boolean outputDirCreated;
054    
055        public Clapham()
056        {
057            this.outputEscapeFilename = true;
058            this.imageFormatSet = EnumSet.of(ImageFormat.PNG, ImageFormat.SVG);
059        }
060    
061        public void setOutputDir(File file) {
062            this.outputDir = file;
063        }
064    
065        public void setOutputEscapeFilename(boolean b) {
066            this.outputEscapeFilename = b;
067        }
068    
069        public void generateIndex() {
070            final File htmlIndexFile = makeFile("index", ".html");
071            FileWriter w = null;
072            try {
073                // Open index.html for writing
074                w = new FileWriter(htmlIndexFile);
075                final PrintWriter pw = new PrintWriter(w);
076                pw.println("<html>");
077                pw.println("<body>");
078                pw.println("<table border='0'>");
079    
080                for (String name : nameList) {
081                    final Symbol symbol = grammar.symbolMap.get(name);
082                    assert symbol != null;
083    
084                    // add link to index
085                    File svgFile =
086                        imageFileNames.get(
087                            new Pair<Symbol, ImageFormat>(symbol, ImageFormat.SVG));
088                    File pngFile =
089                        imageFileNames.get(
090                            new Pair<Symbol, ImageFormat>(symbol, ImageFormat.PNG));
091                    if (svgFile == null) {
092                        svgFile = pngFile;
093                    }
094                    if (pngFile == null) {
095                        pngFile = svgFile;
096                    }
097                    pw.println(
098                        "<tr><td>" + name + "</td><td>"
099                        + "<a href='"
100                        + svgFile.getName()
101                        + "'><img src='"
102                        + pngFile.getName()
103                        + "'/>"
104                        + "</td></tr>");
105                }
106                // close index.html
107                pw.println("</table>");
108                pw.println("</body>");
109                pw.println("</html>");
110                pw.close();
111                System.out.println("Generated index: " + htmlIndexFile);
112            } catch (IOException e) {
113                throw new RuntimeException(
114                    "Error while generating index file " + htmlIndexFile);
115            } finally {
116                if (w != null) {
117                    try {
118                        w.close();
119                    } catch (IOException e) {
120                        // ignore
121                    }
122                }
123            }
124        }
125    
126        public void drawAll() {
127            for (String name : nameList) {
128                draw(name);
129            }
130        }
131    
132        /**
133         * Creates the output directory, if necessary, and prints a message. Only
134         * does this once.
135         */
136        private void checkOutputDir() {
137            if (outputDir != null) {
138                if (!outputDirCreated) {
139                    if (outputDir.mkdirs()) {
140                        System.out.println("Created output directory " + outputDir);
141                    } else {
142                        System.out.println("Output directory " + outputDir);
143                    }
144                    outputDirCreated = true;
145                }
146            }
147        }
148    
149        public void draw(String symbolName) {
150            checkGrammarLoaded();
151            checkOutputDir();
152            try {
153                final Symbol symbol = grammar.symbolMap.get(symbolName);
154                if (symbol.graph == null) {
155                    throw new RuntimeException(
156                        "Symbol '" + symbolName + "' not found");
157                }
158    
159                final DocumentBuilder documentBuilder =
160                    DocumentBuilderFactory.newInstance().newDocumentBuilder();
161                final Document document = documentBuilder.newDocument();
162                final SVGGraphics2D graphics = new SVGGraphics2D(document);
163                final Chart chart = new Chart(grammar, graphics);
164                chart.calcDrawing();
165                chart.drawComponent(symbol);
166    
167                // Write .svg file. If we want to generate .png we generate the
168                // .svg file and delete later.
169                final File svgFile;
170                final boolean generateSvg = imageFormatSet.contains(ImageFormat.SVG);
171                final boolean generatePng = imageFormatSet.contains(ImageFormat.PNG);
172                String gen = "";
173                if (generateSvg || generatePng) {
174                    svgFile = makeFile(symbolName, ".svg");
175                    if (generateSvg) {
176                        imageFileNames.put(
177                            new Pair<Symbol, ImageFormat>(symbol, ImageFormat.SVG),
178                            svgFile);
179                    }
180                    gen = svgFile.getPath();
181                    final String path = svgFile.getPath();
182                    graphics.stream(path, true);
183                } else {
184                    svgFile = null;
185                }
186    
187                // convert to .png file
188                if (generatePng) {
189                    final File pngFile = makeFile(symbolName, ".png");
190                    imageFileNames.put(
191                        new Pair<Symbol, ImageFormat>(symbol, ImageFormat.PNG),
192                        pngFile);
193                    if (!gen.equals("")) {
194                        gen += ", ";
195                    }
196                    gen += pngFile.getPath();
197                    toPng(svgFile, pngFile);
198                    if (!generateSvg) {
199                        svgFile.delete();
200                    }
201                }
202                System.out.println("Symbol " + symbolName + " (" + gen + ")");
203            } catch (ParserConfigurationException e) {
204                throw new RuntimeException(
205                    "Error while generating chart for symbol " + symbolName);
206            } catch (IOException e) {
207                throw new RuntimeException(
208                    "Error while generating chart for symbol " + symbolName);
209            } catch (TranscoderException e) {
210                throw new RuntimeException(
211                    "Error while generating chart for symbol " + symbolName);
212            }
213        }
214    
215        /**
216         * Checks that the grammar is loaded.
217         *
218         * @throws RuntimeException if grammar is not loaded
219         */
220        private void checkGrammarLoaded() {
221            if (grammar == null) {
222                throw new RuntimeException("No grammar loaded");
223            }
224        }
225    
226        /**
227         * Deduces the dialect of the grammar from the suffix of the file name.
228         *
229         * @param file Grammar file
230         * @return Dialect of grammar file
231         */
232        private Dialect deduceDialect(File file) {
233            if (file.getName().toLowerCase().endsWith(".bnf")) {
234                return Dialect.BNF;
235            } else {
236                return Dialect.WIRTH;
237            }
238        }
239    
240        /**
241         * Populates the grammar from the grammar file.
242         *
243         * @param inputFile Grammar file
244         * @param inputDialect Dialect of grammar
245         */
246        public void load(
247            File inputFile,
248            Dialect inputDialect)
249        {
250            if (inputDialect == null) {
251                inputDialect = deduceDialect(inputFile);
252            }
253            try {
254                // Parse input grammar.
255                final List<ProductionNode> productionNodes;
256    
257                final FileReader fileReader = new FileReader(inputFile);
258                switch (inputDialect) {
259                case BNF:
260                    final BnfParser bnfParser = new BnfParser(fileReader);
261                    productionNodes = bnfParser.Syntax();
262                    break;
263                case WIRTH:
264                    final WirthParser wirthParser = new WirthParser(fileReader);
265                    productionNodes = wirthParser.Syntax();
266                    break;
267                default:
268                    throw new IllegalArgumentException(
269                        "unknown dialect " + inputDialect);
270                }
271    
272                // Build grammar.
273                grammar = Clapham.buildGrammar(productionNodes);
274                nameList.clear();
275                nameList.addAll(grammar.symbolMap.keySet());
276                Collections.sort(nameList);
277            } catch (ParseException e) {
278                throw new RuntimeException(
279                    "Error while loading file '" + inputFile.getPath() + "'.",
280                    e);
281            } catch (net.hydromatic.clapham.parser.wirth.ParseException e) {
282                throw new RuntimeException(
283                    "Error while loading file '" + inputFile.getPath() + "'.",
284                    e);
285            } catch (FileNotFoundException e) {
286                throw new RuntimeException(
287                    "Error while loading file '" + inputFile + "'.",
288                    e);
289            }
290        }
291    
292        /**
293         * Generates a name for an output file.
294         *
295         * <p>The file is in the output directory
296         * (if {@link #setOutputDir(java.io.File)} specified);
297         * punctuation is replaced with underscores
298         * (if {@link #setOutputEscapeFilename(boolean) enabled});
299         * and is unique among output files generated this run.
300         *
301         * <p>If you want to know what file a symbol was generated to, record
302         * the generated file name in {@link #imageFileNames}.
303         *
304         * @param name Base name of file
305         * @param suffix Suffix of file
306         * @return Name of output file
307         */
308        private File makeFile(String name, String suffix) {
309            String s = name + suffix;
310            if (outputEscapeFilename) {
311                // Replace spaces etc. with underscores, then make sure that the
312                // name is distinct from other file name we have generated this
313                // run.
314                s = s.replaceAll("[^A-Za-z0-9_.]", "_");
315                s = uniquify(s, 128, fileNameSet);
316            }
317            return new File(outputDir, s);
318        }
319    
320        /**
321         * Makes a name distinct from other names which have already been used
322         * and shorter than a length limit, adds it to the list, and returns it.
323         *
324         * @param name Suggested name, may not be unique
325         * @param maxLength Maximum length of generated name
326         * @param nameList Collection of names already used
327         *
328         * @return Unique name
329         */
330        private static String uniquify(
331            String name,
332            int maxLength,
333            Collection<String> nameList)
334        {
335            assert name != null;
336            if (name.length() > maxLength) {
337                name = name.substring(0, maxLength);
338            }
339            if (nameList.contains(name)) {
340                String aliasBase = name;
341                int j = 0;
342                while (true) {
343                    name = aliasBase + j;
344                    if (name.length() > maxLength) {
345                        aliasBase = aliasBase.substring(0, aliasBase.length() - 1);
346                        continue;
347                    }
348                    if (!nameList.contains(name)) {
349                        break;
350                    }
351                    j++;
352                }
353            }
354            nameList.add(name);
355            return name;
356        }
357    
358        /**
359         * Main command-line entry point.
360         *
361         * @param args Command-line arguments
362         */
363        public static void main(String[] args) {
364            new Clapham().run(args);
365        }
366    
367        /**
368         * Parses command-line arguments an executes.
369         *
370         * @param args Command-line arguments
371         */
372        private void run(String[] args) {
373            final Iterator<String> argIter = Arrays.asList(args).iterator();
374            try {
375                String fileName = null;
376                String outputDirName = null;
377                while (argIter.hasNext()) {
378                    final String arg = argIter.next();
379                    if (arg.startsWith("-")) {
380                        if (arg.equals("-d")) {
381                            if (!argIter.hasNext()) {
382                                throw new RuntimeException(
383                                    "-d option requires argument");
384                            }
385                            outputDirName = argIter.next();
386                        } else if (arg.equals("--help")) {
387                            usage(System.out);
388                            return;
389                        } else {
390                            throw new RuntimeException(
391                                "Bad arg: " + arg);
392                        }
393                    } else {
394                        fileName = arg;
395                    }
396                }
397                if (fileName == null) {
398                    throw new RuntimeException(
399                        "File name must be specified");
400                }
401                load(new File(fileName), null);
402                final File outputDir =
403                    outputDirName == null
404                        ? new File("")
405                        : new File(outputDirName);
406                setOutputDir(outputDir);
407                setOutputFormats(
408                    EnumSet.of(ImageFormat.SVG, ImageFormat.PNG));
409                drawAll();
410                generateIndex();
411            } catch (Throwable e) {
412                e.printStackTrace();
413            }
414        }
415    
416        /**
417         * Prints command-line usage.
418         *
419         * @param out Output stream
420         */
421        private void usage(PrintStream out) {
422            out.println("Clapham - Railroad diagram generator");
423            out.println();
424            out.println("Usage:");
425            out.println("  clapham [ options ] filename");
426            out.println();
427            out.println("Options:");
428            out.println("  --help       Print this help");
429            out.println("  -d directory Specify output directory");
430            out.println("  filename     Name of file containing grammar");
431        }
432    
433        /**
434         * Sets the format(s) in which to generate images. The list must not be
435         * empty.
436         *
437         * @param imageFormatSet Set of output formats
438         */
439        public void setOutputFormats(EnumSet<ImageFormat> imageFormatSet) {
440            assert imageFormatSet != null;
441            assert imageFormatSet.size() > 0;
442            this.imageFormatSet = imageFormatSet;
443        }
444    
445        public static Grammar buildGrammar(
446            List<ProductionNode> productionNodes)
447        {
448            Grammar grammar = new Grammar();
449            for (ProductionNode productionNode : productionNodes) {
450                Symbol symbol = new Symbol(NodeType.NONTERM, productionNode.id.s);
451                grammar.nonterminals.add(symbol);
452                grammar.symbolMap.put(symbol.name, symbol);
453                Graph g = toGraph(grammar, productionNode.expression);
454                symbol.graph = g;
455                grammar.ruleMap.put(symbol, g);
456            }
457            return grammar;
458        }
459    
460        public static Graph toGraph(
461            Grammar grammar,
462            EbnfNode expression)
463        {
464            if (expression instanceof OptionNode) {
465                OptionNode optionNode = (OptionNode) expression;
466                final Graph g = toGraph(grammar, optionNode.n);
467                grammar.makeOption(g);
468                return g;
469            } else if (expression instanceof RepeatNode) {
470                RepeatNode repeatNode = (RepeatNode) expression;
471                final Graph g = toGraph(grammar, repeatNode.node);
472                grammar.makeIteration(g);
473                return g;
474            } else if (expression instanceof MandatoryRepeatNode) {
475                MandatoryRepeatNode repeatNode = (MandatoryRepeatNode) expression;
476                final Graph g = toGraph(grammar, repeatNode.node);
477                grammar.makeIteration(g); // TODO: make mandatory
478                return g;
479            } else if (expression instanceof AlternateNode) {
480                AlternateNode alternateNode = (AlternateNode) expression;
481                Graph g = null;
482                for (EbnfNode node : alternateNode.list) {
483                    if (g == null) {
484                        g = toGraph(grammar, node);
485                        grammar.makeFirstAlt(g);
486                    } else {
487                        Graph g2 = toGraph(grammar, node);
488                        grammar.makeAlternative(g, g2);
489                    }
490                }
491                return g;
492            } else if (expression instanceof SequenceNode) {
493                SequenceNode sequenceNode = (SequenceNode) expression;
494                Graph g = null;
495                for (EbnfNode node : sequenceNode.list) {
496                    if (g == null) {
497                        g = toGraph(grammar, node);
498                    } else {
499                        Graph g2 = toGraph(grammar, node);
500                        grammar.makeSequence(g, g2);
501                    }
502                }
503                return g;
504            } else if (expression instanceof EmptyNode) {
505                Graph g = new Graph();
506                grammar.makeEpsilon(g);
507                return g;
508            } else if (expression instanceof IdentifierNode) {
509                IdentifierNode identifierNode = (IdentifierNode) expression;
510                Symbol symbol = new Symbol(NodeType.NONTERM, identifierNode.s);
511    //            grammar.symbolMap.put(symbol.name, symbol);
512                return new Graph(new Node(grammar, symbol));
513            } else if (expression instanceof LiteralNode) {
514                LiteralNode literalNode = (LiteralNode) expression;
515                Symbol symbol = new Symbol(NodeType.TERM, literalNode.s);
516                grammar.terminals.add(symbol);
517    //            grammar.symbolMap.put(symbol.name, symbol);
518                return new Graph(new Node(grammar, symbol));
519            } else {
520                throw new UnsupportedOperationException(
521                    "unknown node type " + expression);
522            }
523        }
524    
525        public static void toPng(File inFile, File file)
526            throws IOException, TranscoderException
527        {
528            // Create a PNG transcoder
529            PNGTranscoder t = new PNGTranscoder();
530    
531            // Create the transcoder input.
532            TranscoderInput input = new TranscoderInput("file:" + inFile.getPath());
533    
534            // Create the transcoder output.
535            OutputStream ostream = new FileOutputStream(file);
536            TranscoderOutput output = new TranscoderOutput(ostream);
537    
538            // Save the image.
539            t.transcode(input, output);
540    
541            // Flush and close the stream.
542            ostream.flush();
543            ostream.close();
544        }
545    
546        private enum Dialect {
547            WIRTH,
548            BNF
549        }
550    
551        /**
552         * Output format for graphics.
553         */
554        public static enum ImageFormat {
555            SVG,
556            PNG,
557        }
558    
559        private static class Pair<L, R> {
560            L left;
561            R right;
562    
563            Pair(L left, R right) {
564                this.left = left;
565                this.right = right;
566            }
567    
568            public int hashCode() {
569                return (left == null ? 0 : left.hashCode()) << 4
570                    ^ (right == null ? 1 : right.hashCode());
571            }
572    
573            public boolean equals(Object obj) {
574                return obj instanceof Pair
575                    && eq(left, ((Pair) obj).left)
576                    && eq(right, ((Pair) obj).right);
577            }
578    
579            private static boolean eq(Object o, Object o2) {
580                return o == null ? o2 == null : o.equals(o2);
581            }
582        }
583    }
584    
585    // End Clapham.java