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 * /*if[JDK1.1]*/
49 * Properties props = System.getProperties();
50 * props.setProperty(key, value);
51 * System.setProperties(props);
52 * /*end[JDK1.1]*/
53 * <p>
54 * /*if[JDK1.2]
55 * // Use the new System method.
56 * System.setProperty(key, value);
57 * end[JDK1.2]*/
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 * /*if[JDK1.2]
68 * // Use the new System method.
69 * System.setProperty(key, value);
70 * else[JDK1.2]*/
71 * <p>
72 * Properties props = System.getProperties();
73 * props.setProperty(key, value);
74 * System.setProperties(props);
75 * /*end[JDK1.2]*/
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<symbol> ...] [-s <old>=<new> ...] [<in file>] [<out file>]
90 * java Munge [-D<symbol> ...] [-s <old>=<new> ...] <file> ... <directory>
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>@<cmdfile></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 }