166 CHAPTER 8 THE JAVA COMPILER API Source: null Message: Note: Recompile with -Xlint:unchecked for details. Success: true Compiling from Memory My favorite use of JavaCompilerTool isn t just compiling source files found on disk. The class also allows you to generate files in memory, compile them, and then, using reflection, run them. The javadoc for the JavaCompilerTool interface defines a JavaSourceFromString class that makes this so much easier. Basically, the JavaSourceFromString class is a JavaFileObject that defines an in-memory source file. Once created, you can then pass it on to the compiler to define a CompilationTask. You can then compile that source directly using the same run() call as before, to get a generated .class file. Compiling source from memory sounds like a lot of work, but as Listing 8-7 shows, it isn t really that hard at all. The in-memory class definition is shown in bold. While the compiler doesn t care about whitespace, it is best to format the source in a logical way for readability s sake. The definition of the JavaSourceFromString class follows in Listing 8-8. The source file is literally in-memory only, without anything stored to disk. Listing 8-7. Compiling from Memory import java.lang.reflect.*; import java.io.*; import javax.tools.*; import javax.tools.JavaCompilerTool.CompilationTask; import java.util.*; public class CompileSource { public static void main(String args[]) throws IOException { JavaCompilerTool compiler = ToolProvider.getSystemJavaCompilerTool(); DiagnosticCollector diagnostics = new DiagnosticCollector(); // Define class StringWriter writer = new StringWriter(); PrintWriter out = new PrintWriter(writer); out.println(”public class HelloWorld {”); out.println(” public static void main(String args[]) {”); out.println(” System.out.println(”Hello, World”);”);
Note: If you are looking for good and high quality web space to host and run your application check Lunarwebhost Java Web Hosting services
164 CHAPTER 8 THE JAVA COMPILER API In the first case, the usage of the List object does not properly identify that it is a list of String objects via generics, so the compiler issues a warning. In the latter case, the suspend() method of Thread has been deprecated, so it shouldn t be used. Note At this point, you should delete the generated .class files for both the Bar and Baz classes. Listing 8-6 puts all these pieces of JavaCompilerTool together with its DiagnosticCollector and changes to the default source and destination directories. Be sure to change the file to be compiled from Foo.java to Bar.java. Along with the new output from the DiagnosticCollection, the three bold source lines are the ones that changed from the earlier example. Listing 8-6. Compiling with a DiagnosticListener import java.io.*; import java.util.*; import javax.tools.*; public class SecondCompile { public static void main(String args[]) throws IOException { JavaCompilerTool compiler = ToolProvider.getSystemJavaCompilerTool(); DiagnosticCollector diagnostics = new DiagnosticCollector(); StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics); Iterable extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjectsFromStrings(Arrays.asList(”Bar.java”)); Iterable options = Arrays.asList( “-d”, “classes”, “-sourcepath”, “src”); JavaCompilerTool.CompilationTask task = compiler.getTask( null, fileManager, diagnostics, options, null, compilationUnits); task.run(); boolean success = task.getResult(); for (Diagnostic diagnostic : diagnostics.getDiagnostics()) System.console().printf( “Code: %s%n” + “Kind: %s%n” + “Position: %s%n” + “Start Position: %s%n” + “End Position: %s%n” +
Note: If you are looking for good and high quality web space to host and run your application check Lunarwebhost JSP Web Hosting services
CHAPTER 8 THE JAVA COMPILER API 163 To demonstrate these new options, Listings 8-4 and 8-5 provide two classes to use. Place the Bar class of Listing 8-4 in the current directory, and the Baz class of Listing 8-5 in a subdirectory named src. Listing 8-4. Simple Class to Compile with Dependency import java.util.*; public class Bar { public static void main(String args[]) { System.out.println(”Move that Bus”); new Baz(); List list = new ArrayList(); list.add(”Hello”); new Thread().suspend(); } } Listing 8-5. Dependent Class to Compile public class Baz { } Before creating a new program to compile this with JSR 199 and the Java Compiler API, it is important to see what happens when you compile with javac and the extended lint option enabled. > javac -d classes -sourcepath src -Xlint:all Bar.java Bar.java:8: warning: [unchecked] unchecked call to add(E) as a member of the raw type java.util.List list.add(”Hello”); ^ Bar.java:9: warning: [deprecation] suspend() in java.lang.Thread has been deprecated new Thread().suspend(); ^ 2 warnings
Note: If you are looking for good and high quality web space to host and run your application check Lunarwebhost JSP Web Hosting services
162 CHAPTER 8 THE JAVA COMPILER API Changing the Output Directory One of the typical things developers do is maintain separate source and destination directories, into which the source and the compiled .class files are respectively placed. The JavaCompilerTool class supports this by setting the output directory via the options argument passed into its getTask() method. Configuring the compilation task appropriately will tell the tool to place the compiled class files into a different location than the source files. Iterable options = Arrays.asList(”-d”, “classes”); JavaCompilerTool.CompilationTask task = compiler.getTask( null, fileManager, diagnostics, options, null, compilationUnits); As it probably appears, you re just configuring the command-line options, just as if you used the -d command-line switch with the javac compiler. Changing the Input Directory The JavaFileObject class is used to identify each source file to compile. You provide the file name as a string to the getJavaFileObjectsFromStrings() method, or as a File to the getJavaFileObjectsFromFiles() method of your StandardJavaFileManager. For instance, “Foo.java” would be used to compile the Foo class located in the default package. As soon as the source code belongs to a package, you then maintain that package structure within the argument of the getJavaFileObjectsFromStrings() method call. For instance, had the Foo class been in the com.example package, the argument to getJavaFileObjectsFromStrings() would have been “com/example/Foo.java” instead. Tip Even on Windows platforms, the path elements should be separated by Unix-style file separators. If compiling one class reveals that a second class needs to be compiled, where does the system look for it? By default, it looks in the current directory, or at least relative to the top-level package directory when in a package. From the command-line compiler, you can provide an additional set of locations for the compiler to look, via the -sourcepath option. With the JavaCompilerTool class, you just need to add more options for the getTask() method to identify those locations. Iterable options = Arrays.asList(”-d”, “classes”, “-sourcepath”, “src”); This doesn t help in locating the actual JavaFileObject being compiled, only in finding the source files for its dependent classes.
Note: If you are looking for good and high quality web space to host and run your application check Lunarwebhost JSP Web Hosting services
160 CHAPTER 8 THE JAVA COMPILER API You now have all the bits to do the actual compilation of your source files. The JavaCompilerTool.CompilationTask returned from getTask() implements Runnable. You can either pass it off to a Thread to execute separately or call the run() method directly for synchronous execution. JavaCompilerTool.CompilationTask task = compiler.getTask(null, fileManager, null, null, null, compilationUnits); task.run(); Assuming there are no compilation warnings or errors, you ll get all the files identified by the compilationUnits variable compiled (and their dependencies). To find out if everything succeeded, call the getResult() method of CompilationTask. This returns true if all the compilation units compiled without errors, and false if one or more fail. As a last task, remember to release the resources of the file manager with its close() method. You can call getTask() multiple times to reuse the compiler, in order to reduce any overhead during multiple compilation requests. Just close things up when you re done. fileManager.close(); Putting all the pieces together and doing nothing special with the DiagnosticListener/ DiagnosticCollector produces Listing 8-3. If you re following the source examples in the book, don t forget to rename the pritnln() method to be println() again. Listing 8-3. More Advanced Compilation Options import java.io.*; import java.util.*; import javax.tools.*; public class SecondCompile { public static void main(String args[]) throws IOException { JavaCompilerTool compiler = ToolProvider.getSystemJavaCompilerTool(); DiagnosticCollector diagnostics = new DiagnosticCollector(); StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics); Iterable extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjectsFromStrings(Arrays.asList(”Foo.java”)); JavaCompilerTool.CompilationTask task = compiler.getTask( null, fileManager, diagnostic, null, null, compilationUnits); task.run(); boolean success = task.getResult();
Note: If you are looking for good and high quality web space to host and run your application check Lunarwebhost JSP Web Hosting services
CHAPTER 8 THE JAVA COMPILER API 161 fileManager.close(); System.out.println(”Success: ” + success); } } Assuming there are no compilation errors, compiling and running the program in Listing 8-3 produces the following output and a compiled Foo class: java SecondCompile Success: true With a compilation error, you would see false there, instead of true, but no diagnostics about the problems which takes us to DiagnosticListener and its DiagnosticCollector implementation. Working with DiagnosticListener Compilation errors are reported to the registered DiagnosticListener. The DiagnosticListener interface has a single method, public void report(Diagnostic extends S> diagnostic), which you must implement. Actually, you don t have to implement it yourself. The standard libraries offer one such implementation in the DiagnosticCollector class. As the name implies, the DiagnosticCollector class collects the diagnostic problems it encounters. You can then loop through the information with a simple enhanced for loop. for (Diagnostic diagnostic : diagnostics.getDiagnostics()) System.console().printf( “Code: %s%n” + “Kind: %s%n” + “Position: %s%n” + “Start Position: %s%n” + “End Position: %s%n” + “Source: %s%n” + “Message: %s%n”, diagnostic.getCode(), diagnostic.getKind(), diagnostic.getPosition(), diagnostic.getStartPosition(), diagnostic.getEndPosition(), diagnostic.getSource(), diagnostic.getMessage(null)); Of course, if you want to create your own DiagnosticListener, you can do that, too. As previously mentioned, it gets a Diagnostic passed into its report() method, too.
Note: If you are looking for good and high quality web space to host and run your application check Lunarwebhost JSP Web Hosting services
CHAPTER 8 THE JAVA COMPILER API 159 Providing a null listener works, but that puts you back to where you were before, without a way to monitor diagnostic messages. I ll discuss more on DiagnosticCollector and DiagnosticListener later, though. Before getting into the depths of StandardJavaFileManager, I want to discuss the getTask() method of the JavaCompilerTool class, an important part of the compilation process. It takes six arguments and passes back an instance of an inner class of itself, called CompilationTask. JavaCompilerTool.CompilationTask getTask( Writer out, JavaFileManager fileManager, DiagnosticListener super JavaFileObject> diagnosticListener, Iterable options, Iterable classes, Iterable extends JavaFileObject> compilationUnits) Most of these arguments can be null, with logical defaults. out: System.err fileManager: The compiler s standard file manager diagnosticListener: The compiler s default behavior options: No command-line options given to the compiler classes: No class names provided for annotation processing The last argument, compilationUnits, really shouldn t be null, as that is what you want to compile. And that brings us back to StandardJavaFileManager. Notice the argument type: Iterable extends JavaFileObject>. There are two methods of StandardJavaFileManager that give you these results. You can either start with a List of File objects, or a List of String objects, representing the file names. Iterable extends JavaFileObject> getJavaFileObjectsFromFiles( Iterable extends File> files) Iterable extends JavaFileObject> getJavaFileObjectsFromStrings( Iterable names) Actually, anything that implements Iterable can be used to identify the collection of items to compile here not just a List it just happens to be the easiest to create. String[] filenames = …; Iterable extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjectsFromFiles(Arrays.asList(filenames));
Note: If you are looking for good and high quality web space to host and run your application check Lunarwebhost JSP Web Hosting services
158 CHAPTER 8 THE JAVA COMPILER API javac Foo.java Foo.java:3: cannot find symbol symbol : method pritnln(java.lang.String) location: class java.io.PrintStream System.out.pritnln(”Hello, World”); ^ 1 error Compiling Source, Take 2 The FirstCompile program in Listing 8-1 shows one way of using the JavaCompilerTool class. It compiles your source files to generate .class files. For that example, all default options of the JavaCompilerTool were used for compilation just as in doing javac Foo.java from the command line. While it certainly works, you can do a little more work up front to generate better results, or at least a potentially better user experience. Introducing StandardJavaFileManager The Compiling Source, Take 1 section took the easy way out to compile source code. Yes, it worked, but it didn t really offer a way to see or do much with the results from within the program, short of reading standard error/output, that is. The better approach at compiling source from source is to take advantage of the StandardJavaFileManager class. The file manager provides a way to work with regular files for both input and output operations, and to get diagnostic messages reported through the help of a DiagnosticListener. The DiagnosticCollector class is just one such implementation of that listener that you ll be using. Before identifying what needs to be compiled, the basic two-step process to get the file manager is to create a DiagnosticCollector and then ask the JavaCompilerTool for the file manager with getStandardFileManager(), passing in DiagnosticListener. This listener reports non-fatal problems and can be shared with the compiler by passing it into the getTask() method. DiagnosticCollector diagnostics = new DiagnosticCollector(); StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics);
Note: If you are looking for good and high quality web space to host and run your application check Lunarwebhost JSP Web Hosting services
CHAPTER 8 THE JAVA COMPILER API 157 Compiling and running the program without a Foo.java file in the current directory will produce the following results: >java FirstCompile error: cannot read: Foo.java 1 error Success: false If instead you have the Foo class source defined in the current directory (as in Listing 8-2), running the program will generate a Foo.class file. By default, the compiled class file will be placed in the same directory as the source. On successful completion, the program displays a Success: true message. Listing 8-2. Simple Class to Compile public class Foo { public static void main(String args[]) { System.out.println(”Hello, World”); } } To see what happens when the program to compile has an error, add a problem to the Foo source file, such as renaming the println() method to be pritnln(). You don t need to recompile the FirstCompile program; just save the updated Foo.java file. Then, rerunning the program gives the following output: >java FirstCompile Foo.java:3: cannot find symbol symbol : method pritnln(java.lang.String) location: class java.io.PrintStream System.out.pritnln(”Hello, World”); ^ 1 error Success: false You re seeing here exactly what javac spits out, since all Listing 8-1 does is use the default stdout and stderr when running the compiler.
Note: If you are looking for good and high quality web space to host and run your application check Lunarwebhost JSP Web Hosting services
156 CHAPTER 8 THE JAVA COMPILER API Note The JavaCompilerTool interface is not related to the java.lang.Compiler class, which serves as a placeholder for a just-in-time (JIT) compiler implementation. The simple way to compile with the JavaCompilerTool relies on only the Tool interface it implements more specifically, its run() method. int run(InputStream in, OutputStream out, OutputStream err, String… arguments) The stream arguments can all be passed in null to use the defaults of System.in, System.out, and System.err, respectively, for the first three arguments. The variable set of String arguments represents the file names to pass into the compiler. Technically speaking, you can pass any command-line arguments into javac here. So, if your source code for the Foo class is located in the current subdirectory, the way to compile its source would be as follows: int results = tool.run(null, null, null, “Foo.java”); There are two ways to see the results of a compilation. The first way is the obvious one: look for the necessary .class file in the destination directory. The third null argument passed into the run() method of JavaCompilerTool says to send output to standard error (stderr). This is for the compilation error messages, not just messages like those you get from not passing any files to compile to the run() method. The second way to check is via the returned integer. This method returns an int indicating success or failure of the operation. On error, you would get a nonzero value. On success, you would get a zero. The javadoc for the class gives no significance to what the non-zero error value is. If multiple source files are passed into the run() method, 0 will be returned only if all files compile successfully. Listing 8-1 puts all these pieces together for a first look at initiating the Java compiler from source. Listing 8-1. Using the Java Compiling Tool import java.io.*; import javax.tools.*; public class FirstCompile { public static void main(String args[]) throws IOException { JavaCompilerTool compiler = ToolProvider.getSystemJavaCompilerTool(); int results = compiler.run(null, null, null, “Foo.java”); System.out.println(”Success: ” + (results == 0)); } }
Note: If you are looking for good and high quality web space to host and run your application check Lunarwebhost JSP Web Hosting services