View Javadoc

1   package org.sonatype.plugins.munge;
2   
3   /*
4    * The contents of this file are subject to the terms of the Common Development
5    * and Distribution License (the License). You may not use this file except in
6    * compliance with the License.
7    *
8    * You can obtain a copy of the License at http://www.netbeans.org/cddl.html
9    * or http://www.netbeans.org/cddl.txt.
10   *
11   * When distributing Covered Code, include this CDDL Header Notice in each file
12   * and include the License file at http://www.netbeans.org/cddl.txt.
13   * If applicable, add the following below the CDDL Header, with the fields
14   * enclosed by brackets [] replaced by your own identifying information:
15   * "Portions Copyrighted [year] [name of copyright owner]"
16   *
17   * The Original Software is NetBeans. The Initial Developer of the Original
18   * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
19   * Microsystems, Inc. All Rights Reserved.
20   */
21  
22  import java.io.*;
23  import java.util.*;
24  
25  /**
26   * Munge: a purposely-simple Java preprocessor.  It only 
27   * supports conditional inclusion of source based on defined strings of 
28   * the form "if[tag]",
29   * "if_not[tag]", "else[tag], and "end[tag]".  Unlike traditional 
30   * preprocessors, comments and formatting are all preserved for the 
31   * included lines.  This is on purpose, as the output of Munge
32   * will be distributed as human-readable source code.
33   * <p>
34   * To avoid creating a separate Java dialect, the conditional tags are
35   * contained in Java comments.  This allows one build to compile the
36   * source files without pre-processing, to facilitate faster incremental
37   * development.  Other builds from the same source have their code contained 
38   * within that comment.  The format of the tags is a little verbose, so 
39   * that the tags won't accidentally be used by other comment readers
40   * such as javadoc.  Munge tags <b>must</b> be in C-style comments; 
41   * C++-style comments may be used to comment code within a comment.
42   *
43   * <p>
44   * To demonstrate this, our sample source has 1.1 and 1.2-specific code,
45   * with 1.1 as the default build:
46   * <pre><code>
47   *     public void setSystemProperty(String key, String value) {
48   *         &#47;*if[JDK1.1]*&#47;
49   *         Properties props = System.getProperties();
50   *         props.setProperty(key, value);
51   *         System.setProperties(props);
52   *         &#47;*end[JDK1.1]*&#47;
53   * <p>
54   *         &#47;*if[JDK1.2]
55   *         // Use the new System method.
56   *         System.setProperty(key, value);
57   *           end[JDK1.2]*&#47;
58   *     }
59   * </code></pre>
60   * <p>
61   * When the above code is directly compiled, the code bracketed by
62   * the JDK1.1 tags will be used.  If the file is run through 
63   * Munge with the JDK1.2 tag defined, the second code block 
64   * will used instead. This code can also be written as:
65   * <pre><code>
66   *     public void setSystemProperty(String key, String value) {
67   *         &#47;*if[JDK1.2]
68   *         // Use the new System method.
69   *         System.setProperty(key, value);
70   *           else[JDK1.2]*&#47;
71   * <p>
72   *         Properties props = System.getProperties();
73   *         props.setProperty(key, value);
74   *         System.setProperties(props);
75   *         &#47;*end[JDK1.2]*&#47;
76   *     }
77   * </code></pre>
78   *
79   * Munge also performs text substitution; the Swing build uses this to
80   * convert its package references from <code>javax.swing</code>
81   * to <code>java.awt.swing</code>, for example.  This substitution is
82   * has no knowledge of Java syntax, so only use it to convert strings
83   * which are unambiguous.  Substitutions are made in the same order as
84   * the arguments are specified, so the first substitution is made over
85   * the whole file before the second one, and so on.
86   * <p>
87   * Munge's command line takes one of the following forms:
88   * <pre><code>
89   *    java Munge [-D&lt;symbol&gt; ...] [-s &lt;old&gt;=&lt;new&gt; ...] [&lt;in file&gt;] [&lt;out file&gt;]
90   *    java Munge [-D&lt;symbol&gt; ...] [-s &lt;old&gt;=&lt;new&gt; ...] &lt;file&gt; ... &lt;directory&gt;
91   * </code></pre>
92   * <p>
93   * In the first form, if no output file is given, System.out is used.  If
94   * neither input nor output file are given, System.in and System.out are used.
95   * Munge can also take an <code>@&lt;cmdfile&gt;</code> argument.  If one is
96   * specified then the given file is read for additional command line arguments.
97   * <p>
98   * Like any preprocessor, developers must be careful not to abuse its
99   * capabilities so that their code becomes unreadable.  Please use it
100  * as little as possible.
101  *
102  * @author: Thomas Ball
103  * @author jessewilson@google.com (Jesse Wilson)
104  */
105 public class Munge {
106 
107     static Hashtable symbols = new Hashtable(2);
108 
109     static Vector oldTextStrings = new Vector();
110     static Vector newTextStrings = new Vector();
111 
112     int errors = 0;
113     int line = 1;
114     String inName;
115     BufferedReader in;
116     PrintWriter out;
117     Stack stack = new Stack();
118     boolean printing = true;
119     String source = null;
120     String block = null;
121 
122     final String[] commands = { "if", "if_not", "else", "end" };
123     final int IF = 0;
124     final int IF_NOT = 1;
125     final int ELSE = 2;
126     final int END = 3;
127     final int numCommands = 4;
128 
129     final int EOF = 0;
130     final int COMMENT = 1;     // text surrounded by /* */ delimiters
131     final int CODE = 2;        // can just be whitespace
132 
133     int getCommand(String s) {
134         for (int i = 0; i < numCommands; i++) {
135             if (s.equals(commands[i])) {
136                 return i;
137             }
138         }
139         return -1;
140     }
141 
142     public void error(String text) {
143 	System.err.println("File " + inName + " line " + line + ": " + text);
144 	errors++;
145     }
146 
147     public void printErrorCount() {
148 	if (errors > 0) {
149 	    System.err.println(Integer.toString(errors) + 
150 		(errors > 1 ? " errors" : " error"));
151 	}
152     }
153 
154     public boolean hasErrors() {
155 	return (errors > 0);
156     }
157 
158     public Munge(String inName, String outName) {
159         this.inName = inName;
160         if( inName == null ) {
161             in = new BufferedReader( new InputStreamReader(System.in) );
162         } else {
163             try {
164                 in = new BufferedReader( new FileReader(inName) );
165             } catch (FileNotFoundException fnf) {
166                 System.err.println("Cannot find input file " + inName);
167                 errors++;
168                 return;
169             }
170         }
171 
172         if( outName == null ) {
173             out = new PrintWriter(System.out);
174         } else {
175             try {
176                 out = new PrintWriter( new FileWriter(outName) );
177             } catch (IOException ioe) {
178                 System.err.println("Cannot write to file " + outName);
179                 errors++;
180             }
181         }
182     }
183 
184     public void close() throws IOException {
185 	in.close();
186 	out.flush();
187 	out.close();
188     }
189 
190     void cmd_if(String version) {
191 	Boolean b = new Boolean(printing);
192 	stack.push(b);
193 	printing = (symbols.get(version) != null);
194     }
195 
196     void cmd_if_not(String version) {
197 	Boolean b = new Boolean(printing);
198 	stack.push(b);
199 	printing = (symbols.get(version) == null);
200     }
201 
202     void cmd_else() {
203         printing = !printing;
204     }
205 
206     void cmd_end() throws EmptyStackException {
207 	Boolean b = (Boolean)stack.pop();
208 	printing = b.booleanValue();
209     }
210 
211     void print(String s) throws IOException {
212         if (printing) {
213             out.write(s);
214         } else {
215             // Output empty lines to preserve line numbering.
216             int n = countLines(s);
217             for (int i = 0; i < n; i++) {
218                 out.write('\n');
219             }
220         }
221     }
222 
223     // Return the number of line endings in a string.
224     int countLines(String s) {
225         int i = 0;
226         int n = 0;
227         while ((i = block.indexOf('\n', i) + 1) > 0) {
228             n++;
229         }
230         return n;
231     }
232 
233     /*
234      * If there's a preprocessor tag in this comment, act on it and return
235      * any text within it.  If not, just return the whole comment unchanged.
236      */
237     void processComment(String comment) throws IOException {
238         String commentText = comment.substring(2, comment.length() - 2);
239         StringTokenizer st = new StringTokenizer(
240             commentText, "[] \t\r\n", true);
241         boolean foundTag = false;
242         StringBuffer buffer = new StringBuffer();
243 
244         try {
245             while (st.hasMoreTokens()) {
246                 String token = st.nextToken();
247                 int cmd = getCommand(token);
248                 if (cmd == -1) {
249                     buffer.append(token);
250                     if (token.equals("\n")) {
251                         line++;
252                     }
253                 } else {
254                     token = st.nextToken();
255                     if (!token.equals("[")) {
256                         // Not a real tag: save it and continue...
257                         buffer.append(commands[cmd]);
258                         buffer.append(token);
259                     } else {
260                         String symbol = st.nextToken();
261                         if (!st.nextToken().equals("]")) {
262                             error("invalid preprocessor statement");
263                         }
264                         foundTag = true;
265 
266                         // flush text, as command may change printing state
267                         print(buffer.toString());
268                         buffer.setLength(0);  // reset buffer
269 
270                         switch (cmd) {
271                           case IF:
272                               cmd_if(symbol);
273                               break;
274                           case IF_NOT:
275                               cmd_if_not(symbol);
276                               break;
277                           case ELSE:
278                               cmd_else();
279                               break;
280                           case END:
281                               cmd_end();
282                               break;
283                           default:
284                               throw new InternalError("bad command");
285                         }
286                     }
287                 }
288             }
289         } catch (NoSuchElementException nse) {
290             error("invalid preprocessor statement");
291         } catch (EmptyStackException ese) {
292             error("unmatched end or else statement");
293         }
294 
295         if (foundTag) {
296             print(buffer.toString());
297         } else {
298             print(comment);
299         }
300     }
301 
302     // Munge views a Java source file as consisting of 
303     // blocks, alternating between comments and the text between them.
304     int nextBlock() throws IOException {
305         if (source == null || source.length() == 0) {
306             block = null;
307             return EOF;
308         }
309         if (source.startsWith("/*")) {
310             // Return comment as next block.
311             int i = source.indexOf("*/");
312             if (i == -1) {
313                 // malformed comment, skip
314                 block = source;
315                 return CODE;
316             }
317             i += 2;  // include comment close
318             block = source.substring(0, i);
319             source = source.substring(i);
320             return COMMENT;
321         }
322 
323         // Return text up to next comment, or rest of file if no more comments.
324       int i = findCommentStart(source, 0);
325         if (i != -1) {
326             block = source.substring(0, i);
327             source = source.substring(i);
328         } else {
329             block = source;
330             source = null;
331         }
332 
333         // Update line count -- this isn't done for comments because
334         // line counting has to be done during parsing.
335         line += countLines(block);
336 
337         return CODE;
338     }
339 
340    /*
341     * Naively try to find the start of the next comment in a block of source
342     * code. This method handles end-of-line comments and Strings, but nothing
343     * more complicated (like strings containing end-of-line comments).
344     */
345     static int findCommentStart(String source, int fromIndex) {
346         if (fromIndex >= source.length()) {
347             return -1;
348         }
349       
350         int commentStart = source.indexOf("/*", fromIndex);
351         if (commentStart == -1) {
352             return -1;
353         }
354 
355         int commentLineStart = source.lastIndexOf("\n", commentStart);
356         if (commentLineStart == -1) {
357             commentLineStart = 0;
358         }
359         String line = source.substring(commentLineStart, commentStart + 2);
360 
361         if (line.contains("//")) {
362             return findCommentStart(source, commentStart + 1);
363         } else if (countQuotes(line) % 2 == 1) {
364             return findCommentStart(source, commentStart + 1);
365         } else {
366             return commentStart;
367         }
368     }
369 
370     static int countQuotes(String input) {
371       int result = 0;
372       for (int i = 0; i < input.length(); i++) {
373         if (input.charAt(i) == '\"') {
374           result++;
375         }
376       }
377       return result;
378     }
379 
380     void substitute() {
381         for (int i = 0; i < oldTextStrings.size(); i++) {
382             String oldText = (String)oldTextStrings.elementAt(i);
383             String newText = (String)newTextStrings.elementAt(i);
384             int n;
385             while ((n = source.indexOf(oldText)) >= 0) {
386                 source = source.substring(0, n) + newText + 
387                     source.substring(n + oldText.length());
388             }
389         }
390     }
391 
392     public void process() throws IOException {
393         // Read all of file into a single stream for easier scanning.
394         StringWriter sw = new StringWriter();
395         char[] buffer = new char[8192];
396         int n;
397         while ((n = in.read(buffer, 0, 8192)) > 0) {
398             sw.write(buffer, 0, n);
399         }
400         source = sw.toString();
401 
402         // Perform any text substitutions.
403         substitute();
404 
405         // Do preprocessing.
406         int blockType;
407         do {
408             blockType = nextBlock();
409             if (blockType == COMMENT) {
410                 processComment(block);
411             } else if (blockType == CODE) {
412                 print(block);
413             }
414 	} while (blockType != EOF);
415 
416 	// Make sure any conditional statements were closed.
417 	if (!stack.empty()) {
418 	    error("missing end statement(s)");
419 	}
420     }
421 
422     /**
423      * Report how this utility is used and exit.
424      */
425     public static void usage() {
426 	System.err.println("usage:" +
427                            "\n    java Munge [-D<symbol> ...] " +
428                            "[-s <old>=<new> ...] " + 
429 	                   "[<in file>] [<out file>]" +
430                            "\n    java Munge [-D<symbol> ...] " +
431                            "[-s <old>=<new> ...] " + 
432 	                   "<file> ... <directory>"
433                            );
434 	System.exit(1);
435     }
436     public static void usage(String msg) {
437         System.err.println(msg);
438         usage();
439     }
440 
441     /**
442      * Munge's main entry point.
443      */
444     public static void main(String[] args) {
445 
446 	// Use a dummy object as the hash entry value.
447 	Object obj = new Object();
448 
449         // Replace and @file arguments with the contents of the specified file.
450         try {
451             args = CommandLine.parse( args );
452         } catch( IOException e ) {
453             usage("Unable to read @file argument.");
454         }
455         
456 	// Load symbol definitions
457 	int iArg = 0;
458 	while (iArg < args.length && args[iArg].startsWith("-")) {
459             if (args[iArg].startsWith("-D")) {
460                 String symbol = args[iArg].substring(2);
461                 symbols.put(symbol, obj);
462             }
463 
464             else if (args[iArg].equals("-s")) {
465                 if (iArg == args.length) {
466                     usage("no substitution string specified for -s parameter");
467                 }
468 
469                 // Parse and store <old_text>=<new_text> parameter.
470                 String subst = args[++iArg];
471                 int equals = subst.indexOf('=');
472                 if (equals < 1 || equals >= subst.length()) {
473                     usage("invalid substitution string \"" + subst + "\"");
474                 }
475                 String oldText = subst.substring(0, equals);
476                 oldTextStrings.addElement(oldText);
477                 String newText = subst.substring(equals + 1);
478                 newTextStrings.addElement(newText);
479             }
480 
481             else {
482                 usage("invalid flag \"" + args[iArg] + "\"");
483             }
484 
485             ++iArg;
486 	}
487 
488 	// Parse file name arguments into an array of input file names and
489         // output file names.
490         String[] inFiles = new String[Math.max(args.length-iArg-1, 1)];
491         String[] outFiles = new String[inFiles.length];
492 
493         if( iArg < args.length ) {
494             File targetDir = new File( args[args.length-1] );
495             if( targetDir.isDirectory() ) {
496                 int i = 0;
497                 for( ; iArg<args.length-1; i++, iArg++ ) {
498                     inFiles[i] = args[iArg];
499                     File inFile = new File( args[iArg] );
500                     File outFile = new File( targetDir, inFile.getName() );
501                     outFiles[i] = outFile.getAbsolutePath();
502                 }
503                 if( i == 0 ) {
504                     usage("No source files specified.");
505                 }
506             } else {            
507                 inFiles[0] = args[iArg++];
508                 if( iArg < args.length ) {
509                     outFiles[0] = args[iArg++];
510                 } 
511                 if( iArg < args.length ) {
512                     usage(args[args.length-1] + " is not a directory.");
513                 }                
514             }
515         }
516 
517         // Now do the munging.
518         for( int i=0; i<inFiles.length; i++ ) {
519 
520             Munge munge = new Munge(inFiles[i], outFiles[i]);
521             if (munge.hasErrors()) {
522                 munge.printErrorCount();
523                 System.exit(munge.errors);
524             }
525            
526             try {
527                 munge.process();
528                 munge.close();
529             } catch (IOException e) {
530                 munge.error(e.toString());
531             }
532 
533             if (munge.hasErrors()) {
534                 munge.printErrorCount();
535                 System.exit(munge.errors);
536             }
537         }
538     }
539 
540 
541     /**
542      * This class was cut and pasted from the JDK1.2 sun.tools.util package.
543      * Since Munge needs to be used when only a JRE is present, we could not
544      * use it from that place. Likewise, Munge needs to be able to run under 1.1
545      * so the 1.2 collections classes had to be replaced in this version.
546      */
547     static class CommandLine {
548         /**
549          * Process Win32-style command files for the specified command line
550          * arguments and return the resulting arguments. A command file argument
551          * is of the form '@file' where 'file' is the name of the file whose
552          * contents are to be parsed for additional arguments. The contents of
553          * the command file are parsed using StreamTokenizer and the original
554          * '@file' argument replaced with the resulting tokens. Recursive command
555          * files are not supported. The '@' character itself can be quoted with
556          * the sequence '@@'.
557          */
558          static String[] parse(String[] args)
559             throws IOException
560         {
561             Vector newArgs = new Vector(args.length);
562             for (int i = 0; i < args.length; i++) {
563                 String arg = args[i];
564                 if (arg.length() > 1 && arg.charAt(0) == '@') {
565                     arg = arg.substring(1);
566                     if (arg.charAt(0) == '@') {
567                         newArgs.addElement(arg);
568                     } else {
569                         loadCmdFile(arg, newArgs);
570                     }
571                 } else {
572                     newArgs.addElement(arg);
573                 }
574             }
575             String[] newArgsArray = new String[newArgs.size()];
576             newArgs.copyInto(newArgsArray);
577             return newArgsArray;
578         }
579 
580         private static void loadCmdFile(String name, Vector args)
581             throws IOException
582         {
583             Reader r = new BufferedReader(new FileReader(name));
584             StreamTokenizer st = new StreamTokenizer(r);
585             st.resetSyntax();
586             st.wordChars(' ', 255);
587             st.whitespaceChars(0, ' ');
588             st.commentChar('#');
589             st.quoteChar('"');
590             st.quoteChar('\'');
591             while (st.nextToken() != st.TT_EOF) {
592                 args.addElement(st.sval);
593             }
594             r.close();
595         }
596     }
597 }