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*/
020package net.hydromatic.clapham;
021
022import net.hydromatic.clapham.parser.bnf.BnfParser;
023import net.hydromatic.clapham.parser.bnf.ParseException;
024import net.hydromatic.clapham.parser.*;
025import net.hydromatic.clapham.parser.wirth.WirthParser;
026import net.hydromatic.clapham.graph.*;
027
028import javax.xml.parsers.*;
029import java.io.*;
030import java.util.*;
031
032import org.apache.batik.svggen.SVGGraphics2D;
033import org.apache.batik.transcoder.*;
034import org.apache.batik.transcoder.image.PNGTranscoder;
035import 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 */
044public 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