001 /*
002 * @(#)Munge.java 1.13 00/02/02
003 *
004 * Copyright 1998-2000 Sun Microsystems, Inc. All Rights Reserved.
005 *
006 * This software is the proprietary information of Sun Microsystems, Inc.
007 * Use is subject to license terms.
008 *
009 */
010
011 import java.io.*;
012 import java.util.*;
013
014 /**
015 * Munge: a purposely-simple Java preprocessor. It only
016 * supports conditional inclusion of source based on defined strings of
017 * the form "if[tag]",
018 * "if_not[tag]", "else[tag], and "end[tag]". Unlike traditional
019 * preprocessors, comments and formatting are all preserved for the
020 * included lines. This is on purpose, as the output of Munge
021 * will be distributed as human-readable source code.
022 * <p>
023 * To avoid creating a separate Java dialect, the conditional tags are
024 * contained in Java comments. This allows one build to compile the
025 * source files without pre-processing, to facilitate faster incremental
026 * development. Other builds from the same source have their code contained
027 * within that comment. The format of the tags is a little verbose, so
028 * that the tags won't accidentally be used by other comment readers
029 * such as javadoc. Munge tags <b>must</b> be in C-style comments;
030 * C++-style comments may be used to comment code within a comment.
031 *
032 * <p>
033 * To demonstrate this, our sample source has 1.1 and 1.2-specific code,
034 * with 1.1 as the default build:
035 * <pre><code>
036 * public void setSystemProperty(String key, String value) {
037 * /*if[JDK1.1]*/
038 * Properties props = System.getProperties();
039 * props.setProperty(key, value);
040 * System.setProperties(props);
041 * /*end[JDK1.1]*/
042 * <p>
043 * /*if[JDK1.2]
044 * // Use the new System method.
045 * System.setProperty(key, value);
046 * end[JDK1.2]*/
047 * }
048 * </code></pre>
049 * <p>
050 * When the above code is directly compiled, the code bracketed by
051 * the JDK1.1 tags will be used. If the file is run through
052 * Munge with the JDK1.2 tag defined, the second code block
053 * will used instead. This code can also be written as:
054 * <pre><code>
055 * public void setSystemProperty(String key, String value) {
056 * /*if[JDK1.2]
057 * // Use the new System method.
058 * System.setProperty(key, value);
059 * else[JDK1.2]*/
060 * <p>
061 * Properties props = System.getProperties();
062 * props.setProperty(key, value);
063 * System.setProperties(props);
064 * /*end[JDK1.2]*/
065 * }
066 * </code></pre>
067 *
068 * Munge also performs text substitution; the Swing build uses this to
069 * convert its package references from <code>javax.swing</code>
070 * to <code>java.awt.swing</code>, for example. This substitution is
071 * has no knowledge of Java syntax, so only use it to convert strings
072 * which are unambiguous. Substitutions are made in the same order as
073 * the arguments are specified, so the first substitution is made over
074 * the whole file before the second one, and so on.
075 * <p>
076 * Munge's command line takes one of the following forms:
077 * <pre><code>
078 * java Munge [-D<symbol> ...] [-s <old>=<new> ...] [<in file>] [<out file>]
079 * java Munge [-D<symbol> ...] [-s <old>=<new> ...] <file> ... <directory>
080 * </code></pre>
081 * <p>
082 * In the first form, if no output file is given, System.out is used. If
083 * neither input nor output file are given, System.in and System.out are used.
084 * Munge can also take an <code>@<cmdfile></code> argument. If one is
085 * specified then the given file is read for additional command line arguments.
086 * <p>
087 * Like any preprocessor, developers must be careful not to abuse its
088 * capabilities so that their code becomes unreadable. Please use it
089 * as little as possible.
090 *
091 * @author: Thomas Ball
092 * @version: 1.7 98/10/13
093 */
094 public class Munge {
095
096 static Hashtable symbols = new Hashtable(2);
097
098 static Vector oldTextStrings = new Vector();
099 static Vector newTextStrings = new Vector();
100
101 int errors = 0;
102 int line = 1;
103 String inName;
104 BufferedReader in;
105 PrintWriter out;
106 Stack stack = new Stack();
107 boolean printing = true;
108 String source = null;
109 String block = null;
110
111 final String[] commands = { "if", "if_not", "else", "end" };
112 final int IF = 0;
113 final int IF_NOT = 1;
114 final int ELSE = 2;
115 final int END = 3;
116 final int numCommands = 4;
117
118 final int EOF = 0;
119 final int COMMENT = 1; // text surrounded by /* */ delimiters
120 final int CODE = 2; // can just be whitespace
121
122 int getCommand(String s) {
123 for (int i = 0; i < numCommands; i++) {
124 if (s.equals(commands[i])) {
125 return i;
126 }
127 }
128 return -1;
129 }
130
131 public void error(String text) {
132 System.err.println("File " + inName + " line " + line + ": " + text);
133 errors++;
134 }
135
136 public void printErrorCount() {
137 if (errors > 0) {
138 System.err.println(Integer.toString(errors) +
139 (errors > 1 ? " errors" : " error"));
140 }
141 }
142
143 public boolean hasErrors() {
144 return (errors > 0);
145 }
146
147 public Munge(String inName, String outName) {
148 this.inName = inName;
149 if( inName == null ) {
150 in = new BufferedReader( new InputStreamReader(System.in) );
151 } else {
152 try {
153 in = new BufferedReader( new FileReader(inName) );
154 } catch (FileNotFoundException fnf) {
155 System.err.println("Cannot find input file " + inName);
156 errors++;
157 return;
158 }
159 }
160
161 if( outName == null ) {
162 out = new PrintWriter(System.out);
163 } else {
164 try {
165 out = new PrintWriter( new FileWriter(outName) );
166 } catch (IOException ioe) {
167 System.err.println("Cannot write to file " + outName);
168 errors++;
169 }
170 }
171 }
172
173 public void close() throws IOException {
174 in.close();
175 out.flush();
176 out.close();
177 }
178
179 void cmd_if(String version) {
180 Boolean b = new Boolean(printing);
181 stack.push(b);
182 printing = (symbols.get(version) != null);
183 }
184
185 void cmd_if_not(String version) {
186 Boolean b = new Boolean(printing);
187 stack.push(b);
188 printing = (symbols.get(version) == null);
189 }
190
191 void cmd_else() {
192 printing = !printing;
193 }
194
195 void cmd_end() throws EmptyStackException {
196 Boolean b = (Boolean)stack.pop();
197 printing = b.booleanValue();
198 }
199
200 void print(String s) throws IOException {
201 if (printing) {
202 out.write(s);
203 } else {
204 // Output empty lines to preserve line numbering.
205 int n = countLines(s);
206 for (int i = 0; i < n; i++) {
207 out.write('\n');
208 }
209 }
210 }
211
212 // Return the number of line endings in a string.
213 int countLines(String s) {
214 int i = 0;
215 int n = 0;
216 while ((i = block.indexOf('\n', i) + 1) > 0) {
217 n++;
218 }
219 return n;
220 }
221
222 /*
223 * If there's a preprocessor tag in this comment, act on it and return
224 * any text within it. If not, just return the whole comment unchanged.
225 */
226 void processComment(String comment) throws IOException {
227 String commentText = comment.substring(2, comment.length() - 2);
228 StringTokenizer st = new StringTokenizer(
229 commentText, "[] \t\r\n", true);
230 boolean foundTag = false;
231 StringBuffer buffer = new StringBuffer();
232
233 try {
234 while (st.hasMoreTokens()) {
235 String token = st.nextToken();
236 int cmd = getCommand(token);
237 if (cmd == -1) {
238 buffer.append(token);
239 if (token.equals("\n")) {
240 line++;
241 }
242 } else {
243 token = st.nextToken();
244 if (!token.equals("[")) {
245 // Not a real tag: save it and continue...
246 buffer.append(commands[cmd]);
247 buffer.append(token);
248 } else {
249 String symbol = st.nextToken();
250 if (!st.nextToken().equals("]")) {
251 error("invalid preprocessor statement");
252 }
253 foundTag = true;
254
255 // flush text, as command may change printing state
256 print(buffer.toString());
257 buffer.setLength(0); // reset buffer
258
259 switch (cmd) {
260 case IF:
261 cmd_if(symbol);
262 break;
263 case IF_NOT:
264 cmd_if_not(symbol);
265 break;
266 case ELSE:
267 cmd_else();
268 break;
269 case END:
270 cmd_end();
271 break;
272 default:
273 throw new InternalError("bad command");
274 }
275 }
276 }
277 }
278 } catch (NoSuchElementException nse) {
279 error("invalid preprocessor statement");
280 } catch (EmptyStackException ese) {
281 error("unmatched end or else statement");
282 }
283
284 if (foundTag) {
285 print(buffer.toString());
286 } else {
287 print(comment);
288 }
289 }
290
291 // Munge views a Java source file as consisting of
292 // blocks, alternating between comments and the text between them.
293 int nextBlock() throws IOException {
294 if (source == null || source.length() == 0) {
295 block = null;
296 return EOF;
297 }
298 if (source.startsWith("/*")) {
299 // Return comment as next block.
300 int i = source.indexOf("*/");
301 if (i == -1) {
302 // malformed comment, skip
303 block = source;
304 return CODE;
305 }
306 i += 2; // include comment close
307 block = source.substring(0, i);
308 source = source.substring(i);
309 return COMMENT;
310 }
311
312 // Return text up to next comment, or rest of file if no more comments.
313 int i = source.indexOf("/*");
314 if (i != -1) {
315 block = source.substring(0, i);
316 source = source.substring(i);
317 } else {
318 block = source;
319 source = null;
320 }
321
322 // Update line count -- this isn't done for comments because
323 // line counting has to be done during parsing.
324 line += countLines(block);
325
326 return CODE;
327 }
328
329 void substitute() {
330 for (int i = 0; i < oldTextStrings.size(); i++) {
331 String oldText = (String)oldTextStrings.elementAt(i);
332 String newText = (String)newTextStrings.elementAt(i);
333 int n;
334 while ((n = source.indexOf(oldText)) >= 0) {
335 source = source.substring(0, n) + newText +
336 source.substring(n + oldText.length());
337 }
338 }
339 }
340
341 public void process() throws IOException {
342 // Read all of file into a single stream for easier scanning.
343 StringWriter sw = new StringWriter();
344 char[] buffer = new char[8192];
345 int n;
346 while ((n = in.read(buffer, 0, 8192)) > 0) {
347 sw.write(buffer, 0, n);
348 }
349 source = sw.toString();
350
351 // Perform any text substitutions.
352 substitute();
353
354 // Do preprocessing.
355 int blockType;
356 do {
357 blockType = nextBlock();
358 if (blockType == COMMENT) {
359 processComment(block);
360 } else if (blockType == CODE) {
361 print(block);
362 }
363 } while (blockType != EOF);
364
365 // Make sure any conditional statements were closed.
366 if (!stack.empty()) {
367 error("missing end statement(s)");
368 }
369 }
370
371 /**
372 * Report how this utility is used and exit.
373 */
374 public static void usage() {
375 System.err.println("usage:" +
376 "\n java Munge [-D<symbol> ...] " +
377 "[-s <old>=<new> ...] " +
378 "[<in file>] [<out file>]" +
379 "\n java Munge [-D<symbol> ...] " +
380 "[-s <old>=<new> ...] " +
381 "<file> ... <directory>"
382 );
383 System.exit(1);
384 }
385 public static void usage(String msg) {
386 System.err.println(msg);
387 usage();
388 }
389
390 /**
391 * Munge's main entry point.
392 */
393 public static void main(String[] args) {
394
395 // Use a dummy object as the hash entry value.
396 Object obj = new Object();
397
398 // Replace and @file arguments with the contents of the specified file.
399 try {
400 args = CommandLine.parse( args );
401 } catch( IOException e ) {
402 usage("Unable to read @file argument.");
403 }
404
405 // Load symbol definitions
406 int iArg = 0;
407 while (iArg < args.length && args[iArg].startsWith("-")) {
408 if (args[iArg].startsWith("-D")) {
409 String symbol = args[iArg].substring(2);
410 symbols.put(symbol, obj);
411 }
412
413 else if (args[iArg].equals("-s")) {
414 if (iArg == args.length) {
415 usage("no substitution string specified for -s parameter");
416 }
417
418 // Parse and store <old_text>=<new_text> parameter.
419 String subst = args[++iArg];
420 int equals = subst.indexOf('=');
421 if (equals < 1 || equals >= subst.length()) {
422 usage("invalid substitution string \"" + subst + "\"");
423 }
424 String oldText = subst.substring(0, equals);
425 oldTextStrings.addElement(oldText);
426 String newText = subst.substring(equals + 1);
427 newTextStrings.addElement(newText);
428 }
429
430 else {
431 usage("invalid flag \"" + args[iArg] + "\"");
432 }
433
434 ++iArg;
435 }
436
437 // Parse file name arguments into an array of input file names and
438 // output file names.
439 String[] inFiles = new String[Math.max(args.length-iArg-1, 1)];
440 String[] outFiles = new String[inFiles.length];
441
442 if( iArg < args.length ) {
443 File targetDir = new File( args[args.length-1] );
444 if( targetDir.isDirectory() ) {
445 int i = 0;
446 for( ; iArg<args.length-1; i++, iArg++ ) {
447 inFiles[i] = args[iArg];
448 File inFile = new File( args[iArg] );
449 File outFile = new File( targetDir, inFile.getName() );
450 outFiles[i] = outFile.getAbsolutePath();
451 }
452 if( i == 0 ) {
453 usage("No source files specified.");
454 }
455 } else {
456 inFiles[0] = args[iArg++];
457 if( iArg < args.length ) {
458 outFiles[0] = args[iArg++];
459 }
460 if( iArg < args.length ) {
461 usage(args[args.length-1] + " is not a directory.");
462 }
463 }
464 }
465
466 // Now do the munging.
467 for( int i=0; i<inFiles.length; i++ ) {
468
469 Munge munge = new Munge(inFiles[i], outFiles[i]);
470 if (munge.hasErrors()) {
471 munge.printErrorCount();
472 System.exit(munge.errors);
473 }
474
475 try {
476 munge.process();
477 munge.close();
478 } catch (IOException e) {
479 munge.error(e.toString());
480 }
481
482 if (munge.hasErrors()) {
483 munge.printErrorCount();
484 System.exit(munge.errors);
485 }
486 }
487
488 System.exit(0);
489 }
490
491
492 /**
493 * This class was cut and pasted from the JDK1.2 sun.tools.util package.
494 * Since Munge needs to be used when only a JRE is present, we could not
495 * use it from that place. Likewise, Munge needs to be able to run under 1.1
496 * so the 1.2 collections classes had to be replaced in this version.
497 */
498 static class CommandLine {
499 /**
500 * Process Win32-style command files for the specified command line
501 * arguments and return the resulting arguments. A command file argument
502 * is of the form '@file' where 'file' is the name of the file whose
503 * contents are to be parsed for additional arguments. The contents of
504 * the command file are parsed using StreamTokenizer and the original
505 * '@file' argument replaced with the resulting tokens. Recursive command
506 * files are not supported. The '@' character itself can be quoted with
507 * the sequence '@@'.
508 */
509 static String[] parse(String[] args)
510 throws IOException
511 {
512 Vector newArgs = new Vector(args.length);
513 for (int i = 0; i < args.length; i++) {
514 String arg = args[i];
515 if (arg.length() > 1 && arg.charAt(0) == '@') {
516 arg = arg.substring(1);
517 if (arg.charAt(0) == '@') {
518 newArgs.addElement(arg);
519 } else {
520 loadCmdFile(arg, newArgs);
521 }
522 } else {
523 newArgs.addElement(arg);
524 }
525 }
526 String[] newArgsArray = new String[newArgs.size()];
527 newArgs.copyInto(newArgsArray);
528 return newArgsArray;
529 }
530
531 private static void loadCmdFile(String name, Vector args)
532 throws IOException
533 {
534 Reader r = new BufferedReader(new FileReader(name));
535 StreamTokenizer st = new StreamTokenizer(r);
536 st.resetSyntax();
537 st.wordChars(' ', 255);
538 st.whitespaceChars(0, ' ');
539 st.commentChar('#');
540 st.quoteChar('"');
541 st.quoteChar('\'');
542 while (st.nextToken() != st.TT_EOF) {
543 args.addElement(st.sval);
544 }
545 r.close();
546 }
547 }
548 }