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