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