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