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