Main.java revision bbda219dcbd366aaf1786384a8171e0409fd5a11
1package annotator;
2
3import java.io.*;
4import java.util.*;
5import java.util.regex.*;
6import plume.*;
7
8import annotator.find.Insertion;
9import annotator.find.TreeFinder;
10import annotator.Source;
11import annotator.Source.CompilerException;
12import annotator.specification.IndexFileSpecification;
13import annotator.specification.Specification;
14
15import com.sun.source.tree.*;
16import com.sun.source.util.TreePath;
17
18import com.google.common.collect.*;
19
20/**
21 * This is the main class for the annotator, which inserts annotations in
22 * Java source code.  It takes as input
23 * <ul>
24 *   <li>annotation (index) files, which indcate the annotations to insert</li>
25 *   <li>Java source files, into which the annotator inserts annotations</li>
26 * </ul>
27 * Use the --help option for full usage details.
28 * <p>
29 *
30 * Annotations that are not for the specified Java files are ignored.
31 */
32public class Main {
33
34  public static final String INDEX_UTILS_VERSION =
35    "Annotation file utilities: insert-annotations-to-source v3.0";
36
37  /** Directory in which output files are written. */
38  @Option("-d <directory> Directory in which output files are written")
39  public static String outdir = "annotated/";
40
41  /**
42   * If true, overwrite original source files (making a backup first).
43   * Furthermore, if the backup files already exist, they are used instead
44   * of the .java files.  This behavior permits a user to tweak the .jaif
45   * file and re-run the annotator.
46   * <p>
47   *
48   * Note that if the user runs the annotator with --in-place, makes edits,
49   * and then re-runs the annotator with this --in-place option, those
50   * edits are lost.  Similarly, if the user runs the annotator twice in a
51   * row with --in-place, only the last set of annotations will appear in
52   * the codebase at the end.
53   * <p>
54   *
55   * To preserve changes when using the --in-place option, first remove the
56   * backup files.  Or, use the <tt>-d .</tt> option, which makes (and
57   * reads) no backup, instead of --in-place.
58   */
59  @Option("-i Overwrite original source files")
60  public static boolean in_place = false;
61
62  @Option("-h Print usage information and exit")
63  public static boolean help = false;
64
65  @Option("-a Abbreviate annotation names")
66  public static boolean abbreviate = true;
67
68  @Option("-c Insert annotations in comments")
69  public static boolean comments = false;
70
71  @Option("-o Omit given annotation")
72  public static String omit_annotation;
73
74  @Option("-v Verbose (print progress information)")
75  public static boolean verbose;
76
77  @Option("Debug (print debug information)")
78  public static boolean debug = false;
79
80  // Implementation details:
81  //  1. The annotator partially compiles source
82  //     files using the compiler API (JSR-199), obtaining an AST.
83  //  2. The annotator reads the specification file, producing a set of
84  //     annotator.find.Insertions.  Insertions completely specify what to
85  //     write (as a String, which is ultimately translated according to the
86  //     keyword file) and how to write it (as annotator.find.Criteria).
87  //  3. It then traverses the tree, looking for nodes that satisfy the
88  //     Insertion Criteria, translating the Insertion text against the
89  //     keyword file, and inserting the annotations into the source file.
90
91  /**
92   * Runs the annotator, parsing the source and spec files and applying
93   * the annotations.
94   */
95  public static void main(String[] args) {
96
97    if (verbose) {
98      System.out.println(INDEX_UTILS_VERSION);
99    }
100
101    Options options = new Options("Main [options] ann-file... java-file...", Main.class);
102    String[] file_args = options.parse_and_usage (args);
103
104    if (debug) {
105      TreeFinder.debug = true;
106    }
107
108    if (help) {
109      options.print_usage();
110      System.exit(0);
111    }
112
113    if (in_place && outdir != "annotated/") { // interned
114      options.print_usage("The --outdir and --in-place options are mutually exclusive.");
115      System.exit(1);
116    }
117
118    if (file_args.length < 2) {
119      options.print_usage("Supplied %d arguments, at least 2 needed%n", file_args.length);
120      System.exit(1);
121    }
122
123    // The insertions specified by the annotation files.
124    List<Insertion> insertions = new ArrayList<Insertion>();
125    // The Java files into which to insert.
126    List<String> javafiles = new ArrayList<String>();
127
128    for (String arg : file_args) {
129      if (arg.endsWith(".java")) {
130        javafiles.add(arg);
131      } else if (arg.endsWith(".jaif")) {
132        try {
133          Specification spec = new IndexFileSpecification(arg);
134          List<Insertion> parsedSpec = spec.parse();
135          if (verbose || debug) {
136            System.out.printf("Read %d annotations from %s%n",
137                              parsedSpec.size(), arg);
138          }
139          if (omit_annotation != null) {
140            List<Insertion> filtered = new ArrayList<Insertion>(parsedSpec.size());
141            for (Insertion insertion : parsedSpec) {
142              if (! omit_annotation.equals(insertion.getText())) {
143                filtered.add(insertion);
144              }
145            }
146            parsedSpec = filtered;
147            if (verbose || debug) {
148              System.out.printf("After filtering: %d annotations from %s%n",
149                                parsedSpec.size(), arg);
150            }
151          }
152          insertions.addAll(parsedSpec);
153        } catch (RuntimeException e) {
154          if (e.getCause() != null
155              && e.getCause() instanceof FileNotFoundException) {
156            System.err.println("File not found: " + arg);
157            System.exit(1);
158          } else {
159            throw e;
160          }
161        } catch (FileIOException e) {
162          System.err.println("Error while parsing annotation file " + arg);
163          if (e.getMessage() != null) {
164            System.err.println(e.getMessage());
165          }
166          e.printStackTrace();
167          System.exit(1);
168        }
169      } else {
170        throw new Error("Unrecognized file extension: " + arg);
171      }
172    }
173
174    if (debug) {
175      System.out.printf("%d insertions, %d .java files%n", insertions.size(), javafiles.size());
176    }
177    if (debug) {
178      System.out.printf("Insertions:%n");
179      for (Insertion insertion : insertions) {
180        System.out.printf("  %s%n", insertion);
181      }
182    }
183
184    for (String javafilename : javafiles) {
185
186      if (verbose) {
187        System.out.println("Processing " + javafilename);
188      }
189
190      File javafile = new File(javafilename);
191
192      File outfile;
193      File unannotated = new File(javafilename + ".unannotated");
194      if (in_place) {
195        // It doesn't make sense to check timestamps;
196        // if the .java.unannotated file exists, then just use it.
197        // A user can rename that file back to just .java to cause the
198        // .java file to be read.
199        if (unannotated.exists()) {
200          if (verbose) {
201            System.out.printf("Renaming %s to %s%n", unannotated, javafile);
202          }
203          boolean success = unannotated.renameTo(javafile);
204          if (! success) {
205            throw new Error(String.format("Failed renaming %s to %s",
206                                          unannotated, javafile));
207          }
208        }
209        outfile = javafile;
210      } else {
211        String baseName;
212        if (javafile.isAbsolute()) {
213          baseName = javafile.getName();
214        } else {
215          baseName = javafile.getPath();
216        }
217        outfile = new File(outdir, baseName);
218      }
219
220      Set<String> imports = new LinkedHashSet<String>();
221
222      String fileLineSep = System.getProperty("line.separator");
223      Source src;
224      // Get the source file, and use it to obtain parse trees.
225      try {
226        // fileLineSep is set here so that exceptions can be caught
227        fileLineSep = UtilMDE.inferLineSeparator(javafilename);
228        src = new Source(javafilename);
229      } catch (CompilerException e) {
230        e.printStackTrace();
231        return;
232      } catch (IOException e) {
233        e.printStackTrace();
234        return;
235      }
236
237      for (CompilationUnitTree tree : src.parse()) {
238
239        // Create a finder, and use it to get positions.
240        TreeFinder finder = new TreeFinder(tree);
241        if (debug) {
242          finder.debug = true;
243        }
244        SetMultimap<Integer, Insertion> positions = finder.getPositions(tree, insertions);
245
246        // Apply the positions to the source file.
247        if (debug) {
248          System.err.printf("%d positions in tree for %s%n", positions.size(), javafilename);
249        }
250
251        Set<Integer> positionKeysUnsorted = positions.keySet();
252        Set<Integer> positionKeysSorted = new TreeSet<Integer>(new TreeFinder.ReverseIntegerComparator());
253        positionKeysSorted.addAll(positionKeysUnsorted);
254        for (Integer pos : positionKeysSorted) {
255          List<Insertion> toInsertList = new ArrayList<Insertion>(positions.get(pos));
256          Collections.reverse(toInsertList);
257          assert pos >= 0
258            : "pos is negative: " + pos + " " + toInsertList.get(0) + " " + javafilename;
259          for (Insertion iToInsert : toInsertList) {
260            String toInsert = iToInsert.getText();
261            if (! toInsert.startsWith("@")) {
262              throw new Error("Insertion doesn't start with '@': " + toInsert);
263            }
264            if (abbreviate) {
265              Pair<String,String> ps = removePackage(toInsert);
266              if (ps.a != null) {
267                imports.add(ps.a);
268              }
269              toInsert = ps.b;
270            }
271            if (comments) {
272              toInsert = "/*" + toInsert + "*/";
273            }
274
275            // Possibly add whitespace after the insertion
276            boolean gotSeparateLine = false;
277            if (iToInsert.getSeparateLine()) {
278              int indentation = 0;
279              while ((pos - indentation != 0)
280                     // horizontal whitespace
281                     && (src.charAt(pos-indentation-1) == ' '
282                         || src.charAt(pos-indentation-1) == '\t')) {
283                indentation++;
284              }
285              if ((pos - indentation == 0)
286                  // horizontal whitespace
287                  || (src.charAt(pos-indentation-1) == '\f'
288                      || src.charAt(pos-indentation-1) == '\n'
289                      || src.charAt(pos-indentation-1) == '\r')) {
290                toInsert = toInsert + fileLineSep + src.substring(pos-indentation, pos);
291                gotSeparateLine = true;
292              }
293            }
294
295            // Possibly add a leading space before the insertion
296            if ((! gotSeparateLine) && (pos != 0)) {
297              char precedingChar = src.charAt(pos-1);
298              if (! (Character.isWhitespace(precedingChar)
299                     // No space if it's the first formal or generic parameter
300                     || precedingChar == '('
301                     || precedingChar == '<')) {
302                toInsert = " " + toInsert;
303              }
304            }
305
306            // If it's already there, don't re-insert.  This is a hack!
307            // Also, I think this is already checked when constructing the
308            // innertions.
309            int precedingTextPos = pos-toInsert.length()-1;
310            if (precedingTextPos >= 0) {
311              String precedingTextPlusChar
312                = src.getString().substring(precedingTextPos, pos);
313              // System.out.println("Inserting " + toInsert + " at " + pos + " in code of length " + src.getString().length() + " with preceding text '" + precedingTextPlusChar + "'");
314              if (toInsert.equals(precedingTextPlusChar.substring(0, toInsert.length()))
315                  || toInsert.equals(precedingTextPlusChar.substring(1))) {
316                if (debug) {
317                  System.out.println("Already present, skipping");
318                }
319                continue;
320              }
321            }
322            // add trailing whitespace
323            if (! gotSeparateLine) {
324              toInsert = toInsert + " ";
325            }
326            src.insert(pos, toInsert);
327            if (debug) {
328              System.out.println("Post-insertion source: " + src.getString());
329            }
330          }
331        }
332      }
333
334      // insert import statements
335      {
336        if (debug) {
337          System.out.println(imports.size() + " imports to insert");
338          for (String classname : imports) {
339            System.out.println("  " + classname);
340          }
341        }
342        Pattern importPattern = Pattern.compile("(?m)^import\\b");
343        Pattern packagePattern = Pattern.compile("(?m)^package\\b.*;(\\n|\\r\\n?)");
344        int importIndex = 0;      // default: beginning of file
345        String srcString = src.getString();
346        Matcher m;
347        m = importPattern.matcher(srcString);
348        if (m.find()) {
349          importIndex = m.start();
350        } else {
351          // if (debug) {
352          //   System.out.println("Didn't find import in " + srcString);
353          // }
354          m = packagePattern.matcher(srcString);
355          if (m.find()) {
356            importIndex = m.end();
357          }
358        }
359        String lineSep = System.getProperty("line.separator");
360        for (String classname : imports) {
361          String toInsert = "import " + classname + ";" + fileLineSep;
362          src.insert(importIndex, toInsert);
363          importIndex += toInsert.length();
364        }
365      }
366
367      // Write the source file.
368      try {
369        if (in_place) {
370          if (verbose) {
371            System.out.printf("Renaming %s to %s%n", javafile, unannotated);
372          }
373          boolean success = javafile.renameTo(unannotated);
374          if (! success) {
375            throw new Error(String.format("Failed renaming %s to %s",
376                                          javafile, unannotated));
377          }
378        } else {
379          outfile.getParentFile().mkdirs();
380        }
381        OutputStream output = new FileOutputStream(outfile);
382        if (verbose) {
383          System.out.printf("Writing %s%n", outfile);
384        }
385        src.write(output);
386        output.close();
387      } catch (IOException e) {
388        System.err.println("Problem while writing file " + outfile);
389        e.printStackTrace();
390        System.exit(1);
391      }
392    }
393  }
394
395  ///
396  /// Utility methods
397  ///
398
399  public static String pathToString(TreePath path) {
400    if (path == null)
401      return "null";
402    return treeToString(path.getLeaf());
403  }
404
405  public static String treeToString(Tree node) {
406    String asString = node.toString();
407    String oneLine = firstLine(asString);
408    return "\"" + oneLine + "\"";
409  }
410
411  /**
412   * Return the first non-empty line of the string, adding an ellipsis
413   * (...) if the string was truncated.
414   */
415  public static String firstLine(String s) {
416    while (s.startsWith("\n")) {
417      s = s.substring(1);
418    }
419    int newlineIndex = s.indexOf('\n');
420    if (newlineIndex == -1) {
421      return s;
422    } else {
423      return s.substring(0, newlineIndex) + "...";
424    }
425  }
426
427  /**
428   * Removes the leading package.
429   *
430   * @return given <code>@com.foo.bar(baz)</code> it returns the pair
431   * <code>{ com.foo, @bar(baz) }</code>.
432   */
433  public static Pair<String,String> removePackage(String s) {
434    int nameEnd = s.indexOf("(");
435    if (nameEnd == -1) {
436      nameEnd = s.length();
437    }
438    int dotIndex = s.lastIndexOf(".", nameEnd);
439    if (dotIndex != -1) {
440      String packageName = s.substring(0, nameEnd);
441      if (packageName.startsWith("@")) {
442        return Pair.of(packageName.substring(1),
443                       "@" + s.substring(dotIndex + 1));
444      } else {
445        return Pair.of(packageName,
446                       s.substring(dotIndex + 1));
447      }
448    } else {
449      return Pair.of((String)null, s);
450    }
451  }
452
453  /**
454   * Separates the annotation class from its arguments.
455   *
456   * @return given <code>@foo(bar)</code> it returns the pair <code>{ @foo, (bar) }</code>.
457   */
458  public static Pair<String,String> removeArgs(String s) {
459    int pidx = s.indexOf("(");
460    return (pidx == -1) ?
461        Pair.of(s, (String)null) :
462        Pair.of(s.substring(0, pidx), s.substring(pidx));
463  }
464
465}
466