Sunday, September 21, 2014

JSR 199 - Compiler API

JSR 199 provides the compiler API to compile the Java code inside another Java program. The following are the important classes and interfaces provided for facilitating the compilation from a Java program.
  • JavaFileObject - Represents a compilation unit, typically a class source.
  • SimpleJavaFileObject - Implementation of the methods defined in JavaFileObject
  • DiagnosticCollector - Collects the compilation errors, warning into a list of Diagnostic type
  • Diagnostic - Reports the type of the problem and details like line number, character, error reason etc. 
  • JavaFileManager - To work on the Java source and class files.
  • JavaCompiler - The compiler instance for compiling the compilation unit. 
  • CompilationTask - A sub interface of JavaCompiler which helps to compile and return the status with diagnostic when used call method on it. 

Where to start

To compile a Java code, we need the Java source. The source can be a physical file on the disk or a string inside the program. Using the source, we need create an instance type of JavaFileObject.

Using String literal

Create a class which implements JavaFileObject, here i am using SimpleJavaFileObject. We need create the path URI of the class file
package com.test;

import java.io.IOException;
import java.net.URI;

import javax.tools.SimpleJavaFileObject;

public class SampleSource extends SimpleJavaFileObject
{ 
    private String source;

    protected SampleSource(String name, String code) {
        super(URI.create("string:///" +name.replaceAll("\\.", "/") + Kind.SOURCE.extension), Kind.SOURCE);
        this.source = code ;
    }
 
    @Override
    public CharSequence getCharContent(boolean ignoreEncodingErrors)
            throws IOException {
        return source ;
    }
}
Now, create the instance of JavaFileObject and from those, create the Compilation Unit (A collection of JavaFileObject)
String str = "package com.test;"
                + "\n" + "public class Test {"
                + "\npublic static void test() {"
                + "\nSystem.out.println(\"Comiler API Test\")-;" + ""
                        + "\n}" + "\n}";

        SimpleJavaFileObject fileObject = new SampleSource("com.test.Test", str);
        JavaFileObject javaFileObjects[] = new JavaFileObject[] { fileObject };
        Iterable<? extends JavaFileObject> compilationUnits = Arrays
                .asList(javaFileObjects);

From File System

If the source is from physical location. Then create like this.
File []files = new File[]{file1, file2, file3, file4} ;
Iterable<? extends JavaFileObject> units =
           fileManager.getJavaFileObjectsFromFiles(Arrays.asList(files));

Create a JavaFileManger

We will see, how to create a fileManger now.
JavaFileManager fileManager = compiler.getStandardFileManager(
                diagnostics, Locale.getDefault(), Charset.defaultCharset());
To get the FileManger, we need
  • diagnostic - A DiagnosticCollector of JavaFileObject
  • locale - The locale of the compilation
  • charset - The charset to be used.

Compiler

Get the compiler instance using ToolProvider. Finally, create the CompilationTask from the compiler instance using diagnostics, file manager and compilation units (Optionally writer and compilation options).
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
CompilationTask task = compiler.getTask(null, fileManager, diagnostics,
                compilationOptionss, null, compilationUnits);
The argument required to get the CompilationTask are
  • out - A writer which writes the output of the compiler. Defaults to System.err if null 
  • listener - A diagnostic listener, the errors or warning can be accessed using.
  • options - Compiler options (Ex : -d, like we give in command line using javac ) 
  • classes - Name of the classes to be processed 
  • compilationUnits - List of compilation units

Compile

Finally, call the method to compile. This method to be called only once otherwise it throws IllegalStateException on multiple calls. Once compiled, returns true for successful compilation otherwise false. We need to look the diagnosticCollector to get the error/warning details.
boolean status = task.call();

All together

Putting all together.

    public static void main(String[] args)
    {
        String str = "package com.test;"
                + "\n" + "public class Test {"
                + "\npublic static void test() {"
                + "\nSystem.out.println(\"Comiler API Test\")-;" + ""
                        + "\n}" + "\n}";

        SimpleJavaFileObject fileObject = new SampleSource("com.test.Test", str);
        JavaFileObject javaFileObjects[] = new JavaFileObject[] { fileObject };
        Iterable<? extends JavaFileObject> compilationUnits = Arrays
                .asList(javaFileObjects);

        Iterable<String> compilationOptionss = Arrays.asList(new String[] {
                "-d", "classes" });
        
        DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<JavaFileObject>();

        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();

        JavaFileManager fileManager = compiler.getStandardFileManager(
                diagnostics, Locale.getDefault(), Charset.defaultCharset());
        CompilationTask task = compiler.getTask(null, fileManager, diagnostics,
                compilationOptionss, null, compilationUnits);
        boolean status = task.call();
        
        if(!status)
        {
            System.out.println("Found errors in compilation");
            int errors = 1;
            for(Diagnostic diagnostic : diagnostics.getDiagnostics())
            {
                printError(errors, diagnostic);
                errors++;
            }
        }
        else
            System.out.println("Compilation sucessfull");
        
        try
        {
            fileManager.close();
        } catch (IOException e){}

    }
    
    public static void printError(int number,Diagnostic diagnostic)
    {
        System.out.println();
        System.out.print(diagnostic.getKind()+"  : "+number+" Type : "+diagnostic.getMessage(Locale.getDefault()));
        System.out.print(" at column : "+diagnostic.getColumnNumber());
        System.out.println(" Line number : "+diagnostic.getLineNumber());
        System.out.println("Source : "+diagnostic.getSource());
        
    }

Output

Output with an error will be (because of an hyphen in System.out.println in main method of Test)
Found errors in compilation

ERROR  : 1 Type : illegal start of expression at column : 40 Line number : 4
Source : com.test.SampleSource[string:///com/test/Test.java]

ERROR  : 2 Type : not a statement at column : 39 Line number : 4
Source : com.test.SampleSource[string:///com/test/Test.java]
To read more about JSR 199, follow the official link.

Happy Learning!!!!