Main.java revision 00623041550b198d6a389410ea3a34687cddced5
1package annotator;
2
3import java.io.*;
4import java.util.*;
5import java.util.regex.*;
6import utilMDE.*;
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
18/**
19 * This is the main class for the annotator, which inserts annotations in
20 * Java source code.  It takes as input
21 * <ul>
22 *   <li>annotation (index) files, which indcate the annotations to insert</li>
23 *   <li>Java source files, into which the annotator inserts annotations</li>
24 * </ul>
25 * Use the --help option for full usage details.
26 * <p>
27 *
28 * Annotations that are not for the specified Java files are ignored.
29 */
30public class Main {
31
32  public static final String INDEX_UTILS_VERSION =
33    "Annotation file utilities: insert-annotations-to-source v2.3";
34
35  /** Directory in which output files are written. */
36  @Option("-d <directory> Directory in which output files are written")
37  public static String outdir = "annotated/";
38
39  // It's already possible to emulate this (without the backing up) via
40  //   -d .
41  // but the --in-place argument is more convenient and explicit.
42  /** Directory in which output files are written. */
43  @Option("-i Overwrite original source files")
44  public static boolean in_place = false;
45
46  @Option("-h Print usage information and exit")
47  public static boolean help = false;
48
49  @Option("-a Abbreviate annotation names")
50  public static boolean abbreviate = true;
51
52  @Option("-c Insert annotations in comments")
53  public static boolean comments = false;
54
55  @Option("-v Verbose (print progress information)")
56  public static boolean verbose;
57
58  @Option("Debug (print debug information)")
59  public static boolean debug = false;
60
61  // Implementation details:
62  //  1. The annotator partially compiles source
63  //     files using the compiler API (JSR-199), obtaining an AST.
64  //  2. The annotator reads the specification file, producing a set of
65  //     annotator.find.Insertions.  Insertions completely specify what to
66  //     write (as a String, which is ultimately translated according to the
67  //     keyword file) and how to write it (as annotator.find.Criteria).
68  //  3. It then traverses the tree, looking for nodes that satisfy the
69  //     Insertion Criteria, translating the Insertion text against the
70  //     keyword file, and inserting the annotations into the source file.
71
72  /**
73   * Runs the annotator, parsing the source and spec files and applying
74   * the annotations.
75   */
76  public static void main(String[] args) {
77
78    if (verbose) {
79      System.out.println(INDEX_UTILS_VERSION);
80    }
81
82    Options options = new Options("Main [options] ann-file... java-file...", Main.class);
83    String[] file_args = options.parse_and_usage (args);
84
85    if (help) {
86      options.print_usage();
87      System.exit(0);
88    }
89
90    if (in_place && outdir != "annotated/") { // interned
91      options.print_usage("The --outdir and --in-place options are mutually exclusive.");
92      System.exit(1);
93    }
94
95    if (file_args.length < 2) {
96      options.print_usage("Supplied %d arguments, at least 2 needed%n", file_args.length);
97      System.exit(1);
98    }
99
100    // The insertions specified by the annotation files.
101    List<Insertion> insertions = new ArrayList<Insertion>();
102    // The Java files into which to insert.
103    List<String> javafiles = new ArrayList<String>();
104
105    for (String arg : file_args) {
106      if (arg.endsWith(".java")) {
107        javafiles.add(arg);
108      } else if (arg.endsWith(".jaif")) {
109        try {
110          Specification spec = new IndexFileSpecification(arg);
111          List<Insertion> parsedSpec = spec.parse();
112          insertions.addAll(parsedSpec);
113          if (verbose) {
114            System.out.printf("Read %d annotations from %s%n",
115                              parsedSpec.size(), arg);
116          }
117        } catch (FileIOException e) {
118          System.err.println("Error while parsing annotation file " + arg);
119          if (e.getMessage() != null) {
120            System.err.println(e.getMessage());
121          }
122          e.printStackTrace();
123          System.exit(1);
124        }
125      } else {
126        throw new Error("Unrecognized file extension: " + arg);
127      }
128    }
129
130    if (debug) {
131      System.err.printf("%d insertions, %d .java files%n", insertions.size(), javafiles.size());
132    }
133    if (debug) {
134      System.err.printf("Insertions:%n");
135      for (Insertion insertion : insertions) {
136        System.err.printf("  %s%n", insertion);
137      }
138    }
139
140    for (String javafilename : javafiles) {
141
142      if (verbose) {
143        System.out.println("Processing " + javafilename);
144      }
145
146      File javafile = new File(javafilename);
147
148      File outfile;
149      File unannotated = new File(javafilename + ".unannotated");
150      if (in_place) {
151        // It doesn't make sense to check timestamps;
152        // if the .java.unannotated file exists, then just use it.
153        // A user can rename that file back to just .java to cause the
154        // .java file to be read.
155        if (unannotated.exists()) {
156          if (verbose) {
157            System.out.printf("Renaming %s to %s%n", unannotated, javafile);
158          }
159          boolean success = unannotated.renameTo(javafile);
160          if (! success) {
161            throw new Error(String.format("Failed renaming %s to %s",
162                                          unannotated, javafile));
163          }
164        }
165        outfile = javafile;
166      } else {
167        String baseName;
168        if (javafile.isAbsolute()) {
169          baseName = javafile.getName();
170        } else {
171          baseName = javafile.getPath();
172        }
173        outfile = new File(outdir, baseName);
174      }
175
176      Set<String> imports = new LinkedHashSet<String>();
177
178      String fileLineSep = System.getProperty("line.separator");
179      Source src;
180      // Get the source file, and use it to obtain parse trees.
181      try {
182        // fileLineSep is set here so that exceptions can be caught
183        fileLineSep = UtilMDE.inferLineSeparator(javafilename);
184        src = new Source(javafilename);
185      } catch (CompilerException e) {
186        e.printStackTrace();
187        return;
188      } catch (IOException e) {
189        e.printStackTrace();
190        return;
191      }
192
193      for (CompilationUnitTree tree : src.parse()) {
194
195        // Create a finder, and use it to get positions.
196        TreeFinder finder = new TreeFinder(tree);
197        Map<Integer, String> positions = finder.getPositions(tree, insertions);
198
199        // Apply the positions to the source file.
200        if (debug) {
201          System.err.printf("%d positions in tree for %s%n", positions.size(), javafilename);
202        }
203
204        for (Integer pos : positions.keySet()) {
205          String toInsert = positions.get(pos).trim();
206          if (! toInsert.startsWith("@")) {
207            throw new Error("Insertion doesn't start with '@': " + toInsert);
208          }
209          if (abbreviate) {
210            int nameEnd = toInsert.indexOf("(");
211            if (nameEnd == -1) {
212              nameEnd = toInsert.length();
213            }
214            int dotIndex = toInsert.lastIndexOf(".", nameEnd);
215            if (dotIndex != -1) {
216              imports.add(toInsert.substring(1, nameEnd));
217              toInsert = "@" + toInsert.substring(dotIndex + 1);
218            }
219          }
220          if (comments) {
221            toInsert = "/*" + toInsert + "*/";
222          }
223
224          char precedingChar = src.charAt(pos-1);
225          if (! (Character.isWhitespace(precedingChar)
226                 // No space if it's the first formal or generic parameter
227                 || precedingChar == '('
228                 || precedingChar == '<')) {
229            toInsert = " " + toInsert;
230          }
231          // If it's already there, don't re-insert.  This is a hack!
232          String precedingTextPlusChar
233            = src.getString().substring(pos-toInsert.length()-1, pos);
234          // System.out.println("Inserting " + toInsert + " at " + pos + " in code of length " + src.getString().length() + " with preceding text '" + precedingTextPlusChar + "'");
235          if (toInsert.equals(precedingTextPlusChar.substring(0, toInsert.length()))
236              || toInsert.equals(precedingTextPlusChar.substring(1))) {
237            if (debug) {
238              System.out.println("Already present, skipping");
239            }
240            continue;
241          }
242          src.insert(pos, toInsert + " ");
243          if (debug) {
244            System.out.println("Post-insertion source: " + src.getString());
245          }
246        }
247      }
248
249      // insert import statements
250      {
251        if (debug) {
252          System.out.println(imports.size() + " imports to insert");
253        }
254        Pattern importPattern = Pattern.compile("(?m)^import\\b");
255        Pattern packagePattern = Pattern.compile("(?m)^package\\b.*;(\\n|\\r\\n?)");
256        int importIndex = 0;      // default: beginning of file
257        String srcString = src.getString();
258        Matcher m;
259        m = importPattern.matcher(srcString);
260        if (m.find()) {
261          importIndex = m.start();
262        } else {
263          // if (debug) {
264          //   System.out.println("Didn't find import in " + srcString);
265          // }
266          m = packagePattern.matcher(srcString);
267          if (m.find()) {
268            importIndex = m.end();
269          }
270        }
271        String lineSep = System.getProperty("line.separator");
272        for (String classname : imports) {
273          String toInsert = "import " + classname + ";" + fileLineSep;
274          src.insert(importIndex, toInsert);
275          importIndex += toInsert.length();
276        }
277      }
278
279      // Write the source file.
280      try {
281        if (in_place) {
282          if (verbose) {
283            System.out.printf("Renaming %s to %s%n", javafile, unannotated);
284          }
285          boolean success = javafile.renameTo(unannotated);
286          if (! success) {
287            throw new Error(String.format("Failed renaming %s to %s",
288                                          javafile, unannotated));
289          }
290        } else {
291          outfile.getParentFile().mkdirs();
292        }
293        OutputStream output = new FileOutputStream(outfile);
294        if (verbose) {
295          System.out.printf("Writing %s%n", outfile);
296        }
297        src.write(output);
298        output.close();
299      } catch (IOException e) {
300        System.err.println("Problem while writing file " + outfile);
301        e.printStackTrace();
302        System.exit(1);
303      }
304    }
305  }
306
307  ///
308  /// Utility methods
309  ///
310
311  public static String pathToString(TreePath path) {
312    if (path == null)
313      return "null";
314    return treeToString(path.getLeaf());
315  }
316
317  public static String treeToString(Tree node) {
318    String asString = node.toString();
319    String oneLine = firstLine(asString);
320    return "\"" + oneLine + "\"";
321  }
322
323  /**
324   * Return the first non-empty line of the string, adding an ellipsis
325   * (...) if the string was truncated.
326   */
327  public static String firstLine(String s) {
328    while (s.startsWith("\n")) {
329      s = s.substring(1);
330    }
331    int newlineIndex = s.indexOf('\n');
332    if (newlineIndex == -1) {
333      return s;
334    } else {
335      return s.substring(0, newlineIndex) + "...";
336    }
337  }
338
339}
340