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