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