1package annotator; 2 3import java.io.*; 4import java.util.*; 5 6import javax.tools.*; 7import javax.tools.JavaCompiler.CompilationTask; 8 9import com.sun.source.tree.CompilationUnitTree; 10import com.sun.source.util.JavacTask; 11import com.sun.tools.javac.api.JavacTaskImpl; 12import com.sun.tools.javac.code.Types; 13 14/** 15 * Represents a Java source file. This class provides three major operations: 16 * parsing the source file to obtain a syntax tree (via JSR-199), inserting text 17 * into the source file at specified offsets, and writing the rewritten source 18 * file. 19 */ 20public final class Source { 21 22 private JavaCompiler compiler; 23 private StandardJavaFileManager fileManager; 24 private JavacTask task; 25 private StringBuilder source; 26 private DiagnosticCollector<JavaFileObject> diagnostics; 27 private String path; 28 private Types types; 29 30 /** 31 * Signifies that a problem has occurred with the compiler that produces 32 * the syntax tree for this source file. 33 */ 34 public static class CompilerException extends Exception { 35 36 private static final long serialVersionUID = -4751611137146719789L; 37 38 public CompilerException(String message) { 39 super(message); 40 } 41 } 42 43 /** 44 * Sets up a compiler for parsing the given Java source file. 45 * 46 * @throws CompilerException if the input file couldn't be read 47 */ 48 public Source(String src) throws CompilerException, IOException { 49 50 // Get the JSR-199 compiler. 51 this.compiler = javax.tools.ToolProvider.getSystemJavaCompiler(); 52 if (compiler == null) { 53 throw new CompilerException("could not get compiler instance"); 54 } 55 56 diagnostics = new DiagnosticCollector<JavaFileObject>(); 57 58 // Get the file manager for locating input files. 59 this.fileManager = compiler.getStandardFileManager(diagnostics, null, null); 60 if (fileManager == null) { 61 throw new CompilerException("could not get file manager"); 62 } 63 64 Iterable<? extends JavaFileObject> fileObjs = fileManager 65 .getJavaFileObjectsFromStrings(Collections.singletonList(src)); 66 67 // Compiler options. 68 // -Xlint:-options is a hack to get around Jenkins build problem: 69 // "target value 1.8 is obsolete and will be removed in a future release" 70 final String[] stringOpts = new String[] { "-g", "-Xlint:-options" }; 71 // "-XDTA:noannotationsincomments" 72 // TODO: figure out if these options are necessary? "-source", "1.6x" 73 List<String> optsList = Arrays.asList(stringOpts); 74 75 // Create a task. 76 // This seems to require that the file names end in .java 77 CompilationTask cTask = 78 compiler.getTask(null, fileManager, diagnostics, optsList, null, fileObjs); 79 if (!(cTask instanceof JavacTask)) { 80 throw new CompilerException("could not get a valid JavacTask: " + cTask.getClass()); 81 } 82 this.task = (JavacTask)cTask; 83 this.types = Types.instance(((JavacTaskImpl)cTask).getContext()); 84 85 // Read the source file into a buffer. 86 path = src; 87 source = new StringBuilder(); 88 FileInputStream in = new FileInputStream(src); 89 ByteArrayOutputStream bytes = new ByteArrayOutputStream(); 90 int c; 91 while ((c = in.read()) != -1) { 92 bytes.write(c); 93 } 94 in.close(); 95 source.append(bytes.toString()); 96 bytes.close(); 97 fileManager.close(); 98 } 99 100 /** 101 * @return an object that provides utility methods for types 102 */ 103 public Types getTypes() { return types; } 104 105 /** 106 * Parse the input file, returning a set of Tree API roots (as 107 * <code>CompilationUnitTree</code>s). 108 * 109 * @return the Tree API roots for the input file 110 */ 111 public Set<CompilationUnitTree> parse() { 112 113 try { 114 Set<CompilationUnitTree> compUnits = new HashSet<CompilationUnitTree>(); 115 116 for (CompilationUnitTree tree : task.parse()) { 117 compUnits.add(tree); 118 } 119 120 List<Diagnostic<? extends JavaFileObject>> errors = diagnostics.getDiagnostics(); 121 if (!diagnostics.getDiagnostics().isEmpty()) { 122 int numErrors = 0; 123 for (Diagnostic<? extends JavaFileObject> d : errors) { 124 System.err.println(d); 125 if (d.getKind() == Diagnostic.Kind.ERROR) { ++numErrors; } 126 } 127 if (numErrors > 0) { 128 System.err.println(numErrors + " error" + (numErrors != 1 ? "s" : "")); 129 System.err.println("WARNING: Error processing input source files. Please fix and try again."); 130 System.exit(1); 131 } 132 } 133 134 // Add type information to the AST. 135 try { 136 task.analyze(); 137 } catch (Throwable e) { 138 System.err.println("WARNING: " + path 139 + ": type analysis failed; skipping"); 140 System.err.println("(incomplete CLASSPATH?)"); 141 return Collections.<CompilationUnitTree>emptySet(); 142 } 143 144 return compUnits; 145 146 } catch (IOException e) { 147 e.printStackTrace(); 148 throw new Error(e); 149 } 150 151 // return Collections.<CompilationUnitTree>emptySet(); 152 } 153 154 // TODO: Can be a problem if offsets get thrown off by previous insertions? 155 /** 156 * Inserts the given string into the source file at the given offset. 157 * <p> 158 * 159 * Note that calling this can throw off indices in later parts of the 160 * file. Therefore, when doing multiple insertions, you should perform 161 * them from the end of the file forward. 162 * 163 * @param offset the offset to place the start of the insertion text 164 * @param str the text to insert 165 */ 166 public void insert(int offset, String str) { 167 source.insert(offset, str); 168 } 169 170 public char charAt(int index) { 171 return source.charAt(index); 172 } 173 174 public String substring(int start, int end) { 175 return source.substring(start, end); 176 } 177 178 public String getString() { 179 return source.toString(); 180 } 181 182 /** 183 * Writes the modified source file to the given stream. 184 * 185 * @param out the stream for writing the file 186 * @throws IOException if the source file couldn't be written 187 */ 188 public void write(OutputStream out) throws IOException { 189 out.write(source.toString().getBytes()); 190 out.flush(); 191 out.close(); 192 } 193 194} 195