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     *         &#47;*if[JDK1.1]*&#47;
038     *         Properties props = System.getProperties();
039     *         props.setProperty(key, value);
040     *         System.setProperties(props);
041     *         &#47;*end[JDK1.1]*&#47;
042     * <p>
043     *         &#47;*if[JDK1.2]
044     *         // Use the new System method.
045     *         System.setProperty(key, value);
046     *           end[JDK1.2]*&#47;
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     *         &#47;*if[JDK1.2]
057     *         // Use the new System method.
058     *         System.setProperty(key, value);
059     *           else[JDK1.2]*&#47;
060     * <p>
061     *         Properties props = System.getProperties();
062     *         props.setProperty(key, value);
063     *         System.setProperties(props);
064     *         &#47;*end[JDK1.2]*&#47;
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&lt;symbol&gt; ...] [-s &lt;old&gt;=&lt;new&gt; ...] [&lt;in file&gt;] [&lt;out file&gt;]
079     *    java Munge [-D&lt;symbol&gt; ...] [-s &lt;old&gt;=&lt;new&gt; ...] &lt;file&gt; ... &lt;directory&gt;
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>@&lt;cmdfile&gt;</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    }